From f6e551ef5f3773cf5637c2ae0e11fdeb5f40645b Mon Sep 17 00:00:00 2001 From: frankiebee Date: Mon, 13 Mar 2017 21:14:19 -0700 Subject: Swithch the default network to mainnet --- app/scripts/first-time-state.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/scripts/first-time-state.js b/app/scripts/first-time-state.js index 3196981ba..46fcde998 100644 --- a/app/scripts/first-time-state.js +++ b/app/scripts/first-time-state.js @@ -1,11 +1,14 @@ +// test and development environment variables +const env = process.env.METAMASK_ENV +const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' + // // The default state of MetaMask // - module.exports = { config: { provider: { - type: 'testnet', + type: (METAMASK_DEBUG || env === 'test') ? 'testnet' : 'mainnet', }, }, -} \ No newline at end of file +} -- cgit v1.2.3 From 33a70a695bf054cc156262c45ad875d8eda02dc6 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Mon, 13 Mar 2017 21:15:21 -0700 Subject: Add to CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8e73c286..01551ee9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Current Master +- The default network on installation is now MainNet - Allow sending to ENS names in send form on Ropsten. - Can now change network to custom RPC URL from lock screen. -- cgit v1.2.3 From 94984437dbfb19d8a39750a0331e09a380456c53 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Mon, 17 Apr 2017 13:05:40 -0700 Subject: Remove kovan notice. --- notices/archive/notice_1.md | 1 - notices/notices.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 notices/archive/notice_1.md diff --git a/notices/archive/notice_1.md b/notices/archive/notice_1.md deleted file mode 100644 index 488b60cc9..000000000 --- a/notices/archive/notice_1.md +++ /dev/null @@ -1 +0,0 @@ -MetaMask now lists a new network on our dropdown list: Kovan, a [Proof of Authority](https://github.com/paritytech/parity/wiki/Proof-of-Authority-Chains) testchain managed by several blockchain organizations such as Digix, Etherscan, and Parity. It is designed to be a more stable and reliable testnet alternative to Ropsten and was created in response to recent attacks that slowed down the Ropsten network. You can read more about Kovan [here](https://medium.com/@Digix/announcing-kovan-a-stable-ethereum-public-testnet-10ac7cb6c85f#.6o8sz8cct) and [here](https://medium.com/@Digix/letter-from-the-ceo-some-context-regarding-kovan-7b5121adb901#.kfv7zhw83). As with Ropsten, the default remote node to connect to Kovan is managed by Infura. diff --git a/notices/notices.json b/notices/notices.json index 5503c7855..9f28b32a6 100644 --- a/notices/notices.json +++ b/notices/notices.json @@ -1 +1 @@ -[{"read":false,"date":"Thu Feb 09 2017","title":"Terms of Use","body":"# Terms of Use #\n\n**THIS AGREEMENT IS SUBJECT TO BINDING ARBITRATION AND A WAIVER OF CLASS ACTION RIGHTS AS DETAILED IN SECTION 13. PLEASE READ THE AGREEMENT CAREFULLY.**\n\n_Our Terms of Use have been updated as of September 5, 2016_\n\n## 1. Acceptance of Terms ##\n\nMetaMask provides a platform for managing Ethereum (or \"ETH\") accounts, and allowing ordinary websites to interact with the Ethereum blockchain, while keeping the user in control over what transactions they approve, through our website located at[ ](http://metamask.io)[https://metamask.io/](https://metamask.io/) and browser plugin (the \"Site\") — which includes text, images, audio, code and other materials (collectively, the “Content”) and all of the features, and services provided. The Site, and any other features, tools, materials, or other services offered from time to time by MetaMask are referred to here as the “Service.” Please read these Terms of Use (the “Terms” or “Terms of Use”) carefully before using the Service. By using or otherwise accessing the Services, or clicking to accept or agree to these Terms where that option is made available, you (1) accept and agree to these Terms (2) consent to the collection, use, disclosure and other handling of information as described in our Privacy Policy and (3) any additional terms, rules and conditions of participation issued by MetaMask from time to time. If you do not agree to the Terms, then you may not access or use the Content or Services.\n\n## 2. Modification of Terms of Use ##\n\nExcept for Section 13, providing for binding arbitration and waiver of class action rights, MetaMask reserves the right, at its sole discretion, to modify or replace the Terms of Use at any time. The most current version of these Terms will be posted on our Site. You shall be responsible for reviewing and becoming familiar with any such modifications. Use of the Services by you after any modification to the Terms constitutes your acceptance of the Terms of Use as modified.\n\n\n\n## 3. Eligibility ##\n\nYou hereby represent and warrant that you are fully able and competent to enter into the terms, conditions, obligations, affirmations, representations and warranties set forth in these Terms and to abide by and comply with these Terms.\n\nMetaMask is a global platform and by accessing the Content or Services, you are representing and warranting that, you are of the legal age of majority in your jurisdiction as is required to access such Services and Content and enter into arrangements as provided by the Service. You further represent that you are otherwise legally permitted to use the service in your jurisdiction including owning cryptographic tokens of value, and interacting with the Services or Content in any way. You further represent you are responsible for ensuring compliance with the laws of your jurisdiction and acknowledge that MetaMask is not liable for your compliance with such laws.\n\n## 4 Account Password and Security ##\n\nWhen setting up an account within MetaMask, you will be responsible for keeping your own account secrets, which may be a twelve-word seed phrase, an account file, or other locally stored secret information. MetaMask encrypts this information locally with a password you provide, that we never send to our servers. You agree to (a) never use the same password for MetaMask that you have ever used outside of this service; (b) keep your secret information and password confidential and do not share them with anyone else; (c) immediately notify MetaMask of any unauthorized use of your account or breach of security. MetaMask cannot and will not be liable for any loss or damage arising from your failure to comply with this section.\n\n## 5. Representations, Warranties, and Risks ##\n\n### 5.1. Warranty Disclaimer ###\n\nYou expressly understand and agree that your use of the Service is at your sole risk. The Service (including the Service and the Content) are provided on an \"AS IS\" and \"as available\" basis, without warranties of any kind, either express or implied, including, without limitation, implied warranties of merchantability, fitness for a particular purpose or non-infringement. You acknowledge that MetaMask has no control over, and no duty to take any action regarding: which users gain access to or use the Service; what effects the Content may have on you; how you may interpret or use the Content; or what actions you may take as a result of having been exposed to the Content. You release MetaMask from all liability for you having acquired or not acquired Content through the Service. MetaMask makes no representations concerning any Content contained in or accessed through the Service, and MetaMask will not be responsible or liable for the accuracy, copyright compliance, legality or decency of material contained in or accessed through the Service.\n\n### 5.2 Sophistication and Risk of Cryptographic Systems ###\n\nBy utilizing the Service or interacting with the Content or platform in any way, you represent that you understand the inherent risks associated with cryptographic systems; and warrant that you have an understanding of the usage and intricacies of native cryptographic tokens, like Ether (ETH) and Bitcoin (BTC), smart contract based tokens such as those that follow the Ethereum Token Standard (https://github.com/ethereum/EIPs/issues/20), and blockchain-based software systems.\n\n### 5.3 Risk of Regulatory Actions in One or More Jurisdictions ###\n\nMetaMask and ETH could be impacted by one or more regulatory inquiries or regulatory action, which could impede or limit the ability of MetaMask to continue to develop, or which could impede or limit your ability to access or use the Service or Ethereum blockchain.\n\n### 5.4 Risk of Weaknesses or Exploits in the Field of Cryptography ###\n\nYou acknowledge and understand that Cryptography is a progressing field. Advances in code cracking or technical advances such as the development of quantum computers may present risks to cryptocurrencies and Services of Content, which could result in the theft or loss of your cryptographic tokens or property. To the extent possible, MetaMask intends to update the protocol underlying Services to account for any advances in cryptography and to incorporate additional security measures, but does not guarantee or otherwise represent full security of the system. By using the Service or accessing Content, you acknowledge these inherent risks.\n\n### 5.5 Volatility of Crypto Currencies ###\n\nYou understand that Ethereum and other blockchain technologies and associated currencies or tokens are highly volatile due to many factors including but not limited to adoption, speculation, technology and security risks. You also acknowledge that the cost of transacting on such technologies is variable and may increase at any time causing impact to any activities taking place on the Ethereum blockchain. You acknowledge these risks and represent that MetaMask cannot be held liable for such fluctuations or increased costs.\n\n### 5.6 Application Security ###\n\nYou acknowledge that Ethereum applications are code subject to flaws and acknowledge that you are solely responsible for evaluating any code provided by the Services or Content and the trustworthiness of any third-party websites, products, smart-contracts, or Content you access or use through the Service. You further expressly acknowledge and represent that Ethereum applications can be written maliciously or negligently, that MetaMask cannot be held liable for your interaction with such applications and that such applications may cause the loss of property or even identity. This warning and others later provided by MetaMask in no way evidence or represent an on-going duty to alert you to all of the potential risks of utilizing the Service or Content.\n\n## 6. Indemnity ##\n\nYou agree to release and to indemnify, defend and hold harmless MetaMask and its parents, subsidiaries, affiliates and agencies, as well as the officers, directors, employees, shareholders and representatives of any of the foregoing entities, from and against any and all losses, liabilities, expenses, damages, costs (including attorneys’ fees and court costs) claims or actions of any kind whatsoever arising or resulting from your use of the Service, your violation of these Terms of Use, and any of your acts or omissions that implicate publicity rights, defamation or invasion of privacy. MetaMask reserves the right, at its own expense, to assume exclusive defense and control of any matter otherwise subject to indemnification by you and, in such case, you agree to cooperate with MetaMask in the defense of such matter.\n\n## 7. Limitation on liability ##\n\nYOU ACKNOWLEDGE AND AGREE THAT YOU ASSUME FULL RESPONSIBILITY FOR YOUR USE OF THE SITE AND SERVICE. YOU ACKNOWLEDGE AND AGREE THAT ANY INFORMATION YOU SEND OR RECEIVE DURING YOUR USE OF THE SITE AND SERVICE MAY NOT BE SECURE AND MAY BE INTERCEPTED OR LATER ACQUIRED BY UNAUTHORIZED PARTIES. YOU ACKNOWLEDGE AND AGREE THAT YOUR USE OF THE SITE AND SERVICE IS AT YOUR OWN RISK. RECOGNIZING SUCH, YOU UNDERSTAND AND AGREE THAT, TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, NEITHER METAMASK NOR ITS SUPPLIERS OR LICENSORS WILL BE LIABLE TO YOU FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY OR OTHER DAMAGES OF ANY KIND, INCLUDING WITHOUT LIMITATION DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER TANGIBLE OR INTANGIBLE LOSSES OR ANY OTHER DAMAGES BASED ON CONTRACT, TORT, STRICT LIABILITY OR ANY OTHER THEORY (EVEN IF METAMASK HAD BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES), RESULTING FROM THE SITE OR SERVICE; THE USE OR THE INABILITY TO USE THE SITE OR SERVICE; UNAUTHORIZED ACCESS TO OR ALTERATION OF YOUR TRANSMISSIONS OR DATA; STATEMENTS OR CONDUCT OF ANY THIRD PARTY ON THE SITE OR SERVICE; ANY ACTIONS WE TAKE OR FAIL TO TAKE AS A RESULT OF COMMUNICATIONS YOU SEND TO US; HUMAN ERRORS; TECHNICAL MALFUNCTIONS; FAILURES, INCLUDING PUBLIC UTILITY OR TELEPHONE OUTAGES; OMISSIONS, INTERRUPTIONS, LATENCY, DELETIONS OR DEFECTS OF ANY DEVICE OR NETWORK, PROVIDERS, OR SOFTWARE (INCLUDING, BUT NOT LIMITED TO, THOSE THAT DO NOT PERMIT PARTICIPATION IN THE SERVICE); ANY INJURY OR DAMAGE TO COMPUTER EQUIPMENT; INABILITY TO FULLY ACCESS THE SITE OR SERVICE OR ANY OTHER WEBSITE; THEFT, TAMPERING, DESTRUCTION, OR UNAUTHORIZED ACCESS TO, IMAGES OR OTHER CONTENT OF ANY KIND; DATA THAT IS PROCESSED LATE OR INCORRECTLY OR IS INCOMPLETE OR LOST; TYPOGRAPHICAL, PRINTING OR OTHER ERRORS, OR ANY COMBINATION THEREOF; OR ANY OTHER MATTER RELATING TO THE SITE OR SERVICE.\n\nSOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR THE LIMITATION OR EXCLUSION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES. ACCORDINGLY, SOME OF THE ABOVE LIMITATIONS MAY NOT APPLY TO YOU.\n\n## 8. Our Proprietary Rights ##\n\nAll title, ownership and intellectual property rights in and to the Service are owned by MetaMask or its licensors. You acknowledge and agree that the Service contains proprietary and confidential information that is protected by applicable intellectual property and other laws. Except as expressly authorized by MetaMask, you agree not to copy, modify, rent, lease, loan, sell, distribute, perform, display or create derivative works based on the Service, in whole or in part. MetaMask issues a license for MetaMask, found [here](https://github.com/MetaMask/metamask-plugin/blob/master/LICENSE). For information on other licenses utilized in the development of MetaMask, please see our attribution page at: [https://metamask.io/attributions.html](https://metamask.io/attributions.html)\n\n## 9. Links ##\n\nThe Service provides, or third parties may provide, links to other World Wide Web or accessible sites, applications or resources. Because MetaMask has no control over such sites, applications and resources, you acknowledge and agree that MetaMask is not responsible for the availability of such external sites, applications or resources, and does not endorse and is not responsible or liable for any content, advertising, products or other materials on or available from such sites or resources. You further acknowledge and agree that MetaMask shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such site or resource.\n\n## 10. Termination and Suspension ##\n\nMetaMask may terminate or suspend all or part of the Service and your MetaMask access immediately, without prior notice or liability, if you breach any of the terms or conditions of the Terms. Upon termination of your access, your right to use the Service will immediately cease.\n\nThe following provisions of the Terms survive any termination of these Terms: INDEMNITY; WARRANTY DISCLAIMERS; LIMITATION ON LIABILITY; OUR PROPRIETARY RIGHTS; LINKS; TERMINATION; NO THIRD PARTY BENEFICIARIES; BINDING ARBITRATION AND CLASS ACTION WAIVER; GENERAL INFORMATION.\n\n## 11. No Third Party Beneficiaries ##\n\nYou agree that, except as otherwise expressly provided in these Terms, there shall be no third party beneficiaries to the Terms.\n\n## 12. Notice and Procedure For Making Claims of Copyright Infringement ##\n\nIf you believe that your copyright or the copyright of a person on whose behalf you are authorized to act has been infringed, please provide MetaMask’s Copyright Agent a written Notice containing the following information:\n\n· an electronic or physical signature of the person authorized to act on behalf of the owner of the copyright or other intellectual property interest;\n\n· a description of the copyrighted work or other intellectual property that you claim has been infringed;\n\n· a description of where the material that you claim is infringing is located on the Service;\n\n· your address, telephone number, and email address;\n\n· a statement by you that you have a good faith belief that the disputed use is not authorized by the copyright owner, its agent, or the law;\n\n· a statement by you, made under penalty of perjury, that the above information in your Notice is accurate and that you are the copyright or intellectual property owner or authorized to act on the copyright or intellectual property owner's behalf.\n\nMetaMask’s Copyright Agent can be reached at:\n\nEmail: copyright [at] metamask [dot] io\n\nMail:\n\nAttention:\n\nMetaMask Copyright ℅ ConsenSys\n\n49 Bogart Street\n\nBrooklyn, NY 11206\n\n## 13. Binding Arbitration and Class Action Waiver ##\n\nPLEASE READ THIS SECTION CAREFULLY – IT MAY SIGNIFICANTLY AFFECT YOUR LEGAL RIGHTS, INCLUDING YOUR RIGHT TO FILE A LAWSUIT IN COURT\n\n### 13.1 Initial Dispute Resolution ###\n\nThe parties shall use their best efforts to engage directly to settle any dispute, claim, question, or disagreement and engage in good faith negotiations which shall be a condition to either party initiating a lawsuit or arbitration.\n\n### 13.2 Binding Arbitration ###\n\nIf the parties do not reach an agreed upon solution within a period of 30 days from the time informal dispute resolution under the Initial Dispute Resolution provision begins, then either party may initiate binding arbitration as the sole means to resolve claims, subject to the terms set forth below. Specifically, all claims arising out of or relating to these Terms (including their formation, performance and breach), the parties’ relationship with each other and/or your use of the Service shall be finally settled by binding arbitration administered by the American Arbitration Association in accordance with the provisions of its Commercial Arbitration Rules and the supplementary procedures for consumer related disputes of the American Arbitration Association (the \"AAA\"), excluding any rules or procedures governing or permitting class actions.\n\nThe arbitrator, and not any federal, state or local court or agency, shall have exclusive authority to resolve all disputes arising out of or relating to the interpretation, applicability, enforceability or formation of these Terms, including, but not limited to any claim that all or any part of these Terms are void or voidable, or whether a claim is subject to arbitration. The arbitrator shall be empowered to grant whatever relief would be available in a court under law or in equity. The arbitrator’s award shall be written, and binding on the parties and may be entered as a judgment in any court of competent jurisdiction.\n\nThe parties understand that, absent this mandatory provision, they would have the right to sue in court and have a jury trial. They further understand that, in some instances, the costs of arbitration could exceed the costs of litigation and the right to discovery may be more limited in arbitration than in court.\n\n### 13.3 Location ###\n\nBinding arbitration shall take place in New York. You agree to submit to the personal jurisdiction of any federal or state court in New York County, New York, in order to compel arbitration, to stay proceedings pending arbitration, or to confirm, modify, vacate or enter judgment on the award entered by the arbitrator.\n\n### 13.4 Class Action Waiver ###\n\nThe parties further agree that any arbitration shall be conducted in their individual capacities only and not as a class action or other representative action, and the parties expressly waive their right to file a class action or seek relief on a class basis. YOU AND METAMASK AGREE THAT EACH MAY BRING CLAIMS AGAINST THE OTHER ONLY IN YOUR OR ITS INDIVIDUAL CAPACITY, AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR REPRESENTATIVE PROCEEDING. If any court or arbitrator determines that the class action waiver set forth in this paragraph is void or unenforceable for any reason or that an arbitration can proceed on a class basis, then the arbitration provision set forth above shall be deemed null and void in its entirety and the parties shall be deemed to have not agreed to arbitrate disputes.\n\n### 13.5 Exception - Litigation of Intellectual Property and Small Claims Court Claims ###\n\nNotwithstanding the parties' decision to resolve all disputes through arbitration, either party may bring an action in state or federal court to protect its intellectual property rights (\"intellectual property rights\" means patents, copyrights, moral rights, trademarks, and trade secrets, but not privacy or publicity rights). Either party may also seek relief in a small claims court for disputes or claims within the scope of that court’s jurisdiction.\n\n### 13.6 30-Day Right to Opt Out ###\n\nYou have the right to opt-out and not be bound by the arbitration and class action waiver provisions set forth above by sending written notice of your decision to opt-out to the following address: MetaMask ℅ ConsenSys, 49 Bogart Street, Brooklyn NY 11206 and via email at legal-opt@metamask.io. The notice must be sent within 30 days of September 6, 2016 or your first use of the Service, whichever is later, otherwise you shall be bound to arbitrate disputes in accordance with the terms of those paragraphs. If you opt-out of these arbitration provisions, MetaMask also will not be bound by them.\n\n### 13.7 Changes to This Section ###\n\nMetaMask will provide 60-days’ notice of any changes to this section. Changes will become effective on the 60th day, and will apply prospectively only to any claims arising after the 60th day.\n\nFor any dispute not subject to arbitration you and MetaMask agree to submit to the personal and exclusive jurisdiction of and venue in the federal and state courts located in New York, New York. You further agree to accept service of process by mail, and hereby waive any and all jurisdictional and venue defenses otherwise available.\n\nThe Terms and the relationship between you and MetaMask shall be governed by the laws of the State of New York without regard to conflict of law provisions.\n\n## 14. General Information ##\n\n### 14.1 Entire Agreement ###\n\nThese Terms (and any additional terms, rules and conditions of participation that MetaMask may post on the Service) constitute the entire agreement between you and MetaMask with respect to the Service and supersedes any prior agreements, oral or written, between you and MetaMask. In the event of a conflict between these Terms and the additional terms, rules and conditions of participation, the latter will prevail over the Terms to the extent of the conflict.\n\n### 14.2 Waiver and Severability of Terms ###\n\nThe failure of MetaMask to exercise or enforce any right or provision of the Terms shall not constitute a waiver of such right or provision. If any provision of the Terms is found by an arbitrator or court of competent jurisdiction to be invalid, the parties nevertheless agree that the arbitrator or court should endeavor to give effect to the parties' intentions as reflected in the provision, and the other provisions of the Terms remain in full force and effect.\n\n### 14.3 Statute of Limitations ###\n\nYou agree that regardless of any statute or law to the contrary, any claim or cause of action arising out of or related to the use of the Service or the Terms must be filed within one (1) year after such claim or cause of action arose or be forever barred.\n\n### 14.4 Section Titles ###\n\nThe section titles in the Terms are for convenience only and have no legal or contractual effect.\n\n### 14.5 Communications ###\n\nUsers with questions, complaints or claims with respect to the Service may contact us using the relevant contact information set forth above and at communications@metamask.io.\n\n## 15 Related Links ##\n\n**[Terms of Use](https://metamask.io/terms.html)**\n\n**[Privacy](https://metamask.io/privacy.html)**\n\n**[Attributions](https://metamask.io/attributions.html)**\n\n","id":0},{"read":false,"date":"Wed Mar 22 2017","title":"Announcing Kovan Support","body":"MetaMask now lists a new network on our dropdown list: Kovan, a [Proof of Authority](https://github.com/paritytech/parity/wiki/Proof-of-Authority-Chains) testchain managed by several blockchain organizations such as Digix, Etherscan, and Parity. It is designed to be a more stable and reliable testnet alternative to Ropsten and was created in response to recent attacks that slowed down the Ropsten network. You can read more about Kovan [here](https://medium.com/@Digix/announcing-kovan-a-stable-ethereum-public-testnet-10ac7cb6c85f#.6o8sz8cct) and [here](https://medium.com/@Digix/letter-from-the-ceo-some-context-regarding-kovan-7b5121adb901#.kfv7zhw83). As with Ropsten, the default remote node to connect to Kovan is managed by Infura.\n","id":1}] \ No newline at end of file +[{"read":false,"date":"Thu Feb 09 2017","title":"Terms of Use","body":"# Terms of Use #\n\n**THIS AGREEMENT IS SUBJECT TO BINDING ARBITRATION AND A WAIVER OF CLASS ACTION RIGHTS AS DETAILED IN SECTION 13. PLEASE READ THE AGREEMENT CAREFULLY.**\n\n_Our Terms of Use have been updated as of September 5, 2016_\n\n## 1. Acceptance of Terms ##\n\nMetaMask provides a platform for managing Ethereum (or \"ETH\") accounts, and allowing ordinary websites to interact with the Ethereum blockchain, while keeping the user in control over what transactions they approve, through our website located at[ ](http://metamask.io)[https://metamask.io/](https://metamask.io/) and browser plugin (the \"Site\") — which includes text, images, audio, code and other materials (collectively, the “Content”) and all of the features, and services provided. The Site, and any other features, tools, materials, or other services offered from time to time by MetaMask are referred to here as the “Service.” Please read these Terms of Use (the “Terms” or “Terms of Use”) carefully before using the Service. By using or otherwise accessing the Services, or clicking to accept or agree to these Terms where that option is made available, you (1) accept and agree to these Terms (2) consent to the collection, use, disclosure and other handling of information as described in our Privacy Policy and (3) any additional terms, rules and conditions of participation issued by MetaMask from time to time. If you do not agree to the Terms, then you may not access or use the Content or Services.\n\n## 2. Modification of Terms of Use ##\n\nExcept for Section 13, providing for binding arbitration and waiver of class action rights, MetaMask reserves the right, at its sole discretion, to modify or replace the Terms of Use at any time. The most current version of these Terms will be posted on our Site. You shall be responsible for reviewing and becoming familiar with any such modifications. Use of the Services by you after any modification to the Terms constitutes your acceptance of the Terms of Use as modified.\n\n\n\n## 3. Eligibility ##\n\nYou hereby represent and warrant that you are fully able and competent to enter into the terms, conditions, obligations, affirmations, representations and warranties set forth in these Terms and to abide by and comply with these Terms.\n\nMetaMask is a global platform and by accessing the Content or Services, you are representing and warranting that, you are of the legal age of majority in your jurisdiction as is required to access such Services and Content and enter into arrangements as provided by the Service. You further represent that you are otherwise legally permitted to use the service in your jurisdiction including owning cryptographic tokens of value, and interacting with the Services or Content in any way. You further represent you are responsible for ensuring compliance with the laws of your jurisdiction and acknowledge that MetaMask is not liable for your compliance with such laws.\n\n## 4 Account Password and Security ##\n\nWhen setting up an account within MetaMask, you will be responsible for keeping your own account secrets, which may be a twelve-word seed phrase, an account file, or other locally stored secret information. MetaMask encrypts this information locally with a password you provide, that we never send to our servers. You agree to (a) never use the same password for MetaMask that you have ever used outside of this service; (b) keep your secret information and password confidential and do not share them with anyone else; (c) immediately notify MetaMask of any unauthorized use of your account or breach of security. MetaMask cannot and will not be liable for any loss or damage arising from your failure to comply with this section.\n\n## 5. Representations, Warranties, and Risks ##\n\n### 5.1. Warranty Disclaimer ###\n\nYou expressly understand and agree that your use of the Service is at your sole risk. The Service (including the Service and the Content) are provided on an \"AS IS\" and \"as available\" basis, without warranties of any kind, either express or implied, including, without limitation, implied warranties of merchantability, fitness for a particular purpose or non-infringement. You acknowledge that MetaMask has no control over, and no duty to take any action regarding: which users gain access to or use the Service; what effects the Content may have on you; how you may interpret or use the Content; or what actions you may take as a result of having been exposed to the Content. You release MetaMask from all liability for you having acquired or not acquired Content through the Service. MetaMask makes no representations concerning any Content contained in or accessed through the Service, and MetaMask will not be responsible or liable for the accuracy, copyright compliance, legality or decency of material contained in or accessed through the Service.\n\n### 5.2 Sophistication and Risk of Cryptographic Systems ###\n\nBy utilizing the Service or interacting with the Content or platform in any way, you represent that you understand the inherent risks associated with cryptographic systems; and warrant that you have an understanding of the usage and intricacies of native cryptographic tokens, like Ether (ETH) and Bitcoin (BTC), smart contract based tokens such as those that follow the Ethereum Token Standard (https://github.com/ethereum/EIPs/issues/20), and blockchain-based software systems.\n\n### 5.3 Risk of Regulatory Actions in One or More Jurisdictions ###\n\nMetaMask and ETH could be impacted by one or more regulatory inquiries or regulatory action, which could impede or limit the ability of MetaMask to continue to develop, or which could impede or limit your ability to access or use the Service or Ethereum blockchain.\n\n### 5.4 Risk of Weaknesses or Exploits in the Field of Cryptography ###\n\nYou acknowledge and understand that Cryptography is a progressing field. Advances in code cracking or technical advances such as the development of quantum computers may present risks to cryptocurrencies and Services of Content, which could result in the theft or loss of your cryptographic tokens or property. To the extent possible, MetaMask intends to update the protocol underlying Services to account for any advances in cryptography and to incorporate additional security measures, but does not guarantee or otherwise represent full security of the system. By using the Service or accessing Content, you acknowledge these inherent risks.\n\n### 5.5 Volatility of Crypto Currencies ###\n\nYou understand that Ethereum and other blockchain technologies and associated currencies or tokens are highly volatile due to many factors including but not limited to adoption, speculation, technology and security risks. You also acknowledge that the cost of transacting on such technologies is variable and may increase at any time causing impact to any activities taking place on the Ethereum blockchain. You acknowledge these risks and represent that MetaMask cannot be held liable for such fluctuations or increased costs.\n\n### 5.6 Application Security ###\n\nYou acknowledge that Ethereum applications are code subject to flaws and acknowledge that you are solely responsible for evaluating any code provided by the Services or Content and the trustworthiness of any third-party websites, products, smart-contracts, or Content you access or use through the Service. You further expressly acknowledge and represent that Ethereum applications can be written maliciously or negligently, that MetaMask cannot be held liable for your interaction with such applications and that such applications may cause the loss of property or even identity. This warning and others later provided by MetaMask in no way evidence or represent an on-going duty to alert you to all of the potential risks of utilizing the Service or Content.\n\n## 6. Indemnity ##\n\nYou agree to release and to indemnify, defend and hold harmless MetaMask and its parents, subsidiaries, affiliates and agencies, as well as the officers, directors, employees, shareholders and representatives of any of the foregoing entities, from and against any and all losses, liabilities, expenses, damages, costs (including attorneys’ fees and court costs) claims or actions of any kind whatsoever arising or resulting from your use of the Service, your violation of these Terms of Use, and any of your acts or omissions that implicate publicity rights, defamation or invasion of privacy. MetaMask reserves the right, at its own expense, to assume exclusive defense and control of any matter otherwise subject to indemnification by you and, in such case, you agree to cooperate with MetaMask in the defense of such matter.\n\n## 7. Limitation on liability ##\n\nYOU ACKNOWLEDGE AND AGREE THAT YOU ASSUME FULL RESPONSIBILITY FOR YOUR USE OF THE SITE AND SERVICE. YOU ACKNOWLEDGE AND AGREE THAT ANY INFORMATION YOU SEND OR RECEIVE DURING YOUR USE OF THE SITE AND SERVICE MAY NOT BE SECURE AND MAY BE INTERCEPTED OR LATER ACQUIRED BY UNAUTHORIZED PARTIES. YOU ACKNOWLEDGE AND AGREE THAT YOUR USE OF THE SITE AND SERVICE IS AT YOUR OWN RISK. RECOGNIZING SUCH, YOU UNDERSTAND AND AGREE THAT, TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, NEITHER METAMASK NOR ITS SUPPLIERS OR LICENSORS WILL BE LIABLE TO YOU FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY OR OTHER DAMAGES OF ANY KIND, INCLUDING WITHOUT LIMITATION DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER TANGIBLE OR INTANGIBLE LOSSES OR ANY OTHER DAMAGES BASED ON CONTRACT, TORT, STRICT LIABILITY OR ANY OTHER THEORY (EVEN IF METAMASK HAD BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES), RESULTING FROM THE SITE OR SERVICE; THE USE OR THE INABILITY TO USE THE SITE OR SERVICE; UNAUTHORIZED ACCESS TO OR ALTERATION OF YOUR TRANSMISSIONS OR DATA; STATEMENTS OR CONDUCT OF ANY THIRD PARTY ON THE SITE OR SERVICE; ANY ACTIONS WE TAKE OR FAIL TO TAKE AS A RESULT OF COMMUNICATIONS YOU SEND TO US; HUMAN ERRORS; TECHNICAL MALFUNCTIONS; FAILURES, INCLUDING PUBLIC UTILITY OR TELEPHONE OUTAGES; OMISSIONS, INTERRUPTIONS, LATENCY, DELETIONS OR DEFECTS OF ANY DEVICE OR NETWORK, PROVIDERS, OR SOFTWARE (INCLUDING, BUT NOT LIMITED TO, THOSE THAT DO NOT PERMIT PARTICIPATION IN THE SERVICE); ANY INJURY OR DAMAGE TO COMPUTER EQUIPMENT; INABILITY TO FULLY ACCESS THE SITE OR SERVICE OR ANY OTHER WEBSITE; THEFT, TAMPERING, DESTRUCTION, OR UNAUTHORIZED ACCESS TO, IMAGES OR OTHER CONTENT OF ANY KIND; DATA THAT IS PROCESSED LATE OR INCORRECTLY OR IS INCOMPLETE OR LOST; TYPOGRAPHICAL, PRINTING OR OTHER ERRORS, OR ANY COMBINATION THEREOF; OR ANY OTHER MATTER RELATING TO THE SITE OR SERVICE.\n\nSOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR THE LIMITATION OR EXCLUSION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES. ACCORDINGLY, SOME OF THE ABOVE LIMITATIONS MAY NOT APPLY TO YOU.\n\n## 8. Our Proprietary Rights ##\n\nAll title, ownership and intellectual property rights in and to the Service are owned by MetaMask or its licensors. You acknowledge and agree that the Service contains proprietary and confidential information that is protected by applicable intellectual property and other laws. Except as expressly authorized by MetaMask, you agree not to copy, modify, rent, lease, loan, sell, distribute, perform, display or create derivative works based on the Service, in whole or in part. MetaMask issues a license for MetaMask, found [here](https://github.com/MetaMask/metamask-plugin/blob/master/LICENSE). For information on other licenses utilized in the development of MetaMask, please see our attribution page at: [https://metamask.io/attributions.html](https://metamask.io/attributions.html)\n\n## 9. Links ##\n\nThe Service provides, or third parties may provide, links to other World Wide Web or accessible sites, applications or resources. Because MetaMask has no control over such sites, applications and resources, you acknowledge and agree that MetaMask is not responsible for the availability of such external sites, applications or resources, and does not endorse and is not responsible or liable for any content, advertising, products or other materials on or available from such sites or resources. You further acknowledge and agree that MetaMask shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such site or resource.\n\n## 10. Termination and Suspension ##\n\nMetaMask may terminate or suspend all or part of the Service and your MetaMask access immediately, without prior notice or liability, if you breach any of the terms or conditions of the Terms. Upon termination of your access, your right to use the Service will immediately cease.\n\nThe following provisions of the Terms survive any termination of these Terms: INDEMNITY; WARRANTY DISCLAIMERS; LIMITATION ON LIABILITY; OUR PROPRIETARY RIGHTS; LINKS; TERMINATION; NO THIRD PARTY BENEFICIARIES; BINDING ARBITRATION AND CLASS ACTION WAIVER; GENERAL INFORMATION.\n\n## 11. No Third Party Beneficiaries ##\n\nYou agree that, except as otherwise expressly provided in these Terms, there shall be no third party beneficiaries to the Terms.\n\n## 12. Notice and Procedure For Making Claims of Copyright Infringement ##\n\nIf you believe that your copyright or the copyright of a person on whose behalf you are authorized to act has been infringed, please provide MetaMask’s Copyright Agent a written Notice containing the following information:\n\n· an electronic or physical signature of the person authorized to act on behalf of the owner of the copyright or other intellectual property interest;\n\n· a description of the copyrighted work or other intellectual property that you claim has been infringed;\n\n· a description of where the material that you claim is infringing is located on the Service;\n\n· your address, telephone number, and email address;\n\n· a statement by you that you have a good faith belief that the disputed use is not authorized by the copyright owner, its agent, or the law;\n\n· a statement by you, made under penalty of perjury, that the above information in your Notice is accurate and that you are the copyright or intellectual property owner or authorized to act on the copyright or intellectual property owner's behalf.\n\nMetaMask’s Copyright Agent can be reached at:\n\nEmail: copyright [at] metamask [dot] io\n\nMail:\n\nAttention:\n\nMetaMask Copyright ℅ ConsenSys\n\n49 Bogart Street\n\nBrooklyn, NY 11206\n\n## 13. Binding Arbitration and Class Action Waiver ##\n\nPLEASE READ THIS SECTION CAREFULLY – IT MAY SIGNIFICANTLY AFFECT YOUR LEGAL RIGHTS, INCLUDING YOUR RIGHT TO FILE A LAWSUIT IN COURT\n\n### 13.1 Initial Dispute Resolution ###\n\nThe parties shall use their best efforts to engage directly to settle any dispute, claim, question, or disagreement and engage in good faith negotiations which shall be a condition to either party initiating a lawsuit or arbitration.\n\n### 13.2 Binding Arbitration ###\n\nIf the parties do not reach an agreed upon solution within a period of 30 days from the time informal dispute resolution under the Initial Dispute Resolution provision begins, then either party may initiate binding arbitration as the sole means to resolve claims, subject to the terms set forth below. Specifically, all claims arising out of or relating to these Terms (including their formation, performance and breach), the parties’ relationship with each other and/or your use of the Service shall be finally settled by binding arbitration administered by the American Arbitration Association in accordance with the provisions of its Commercial Arbitration Rules and the supplementary procedures for consumer related disputes of the American Arbitration Association (the \"AAA\"), excluding any rules or procedures governing or permitting class actions.\n\nThe arbitrator, and not any federal, state or local court or agency, shall have exclusive authority to resolve all disputes arising out of or relating to the interpretation, applicability, enforceability or formation of these Terms, including, but not limited to any claim that all or any part of these Terms are void or voidable, or whether a claim is subject to arbitration. The arbitrator shall be empowered to grant whatever relief would be available in a court under law or in equity. The arbitrator’s award shall be written, and binding on the parties and may be entered as a judgment in any court of competent jurisdiction.\n\nThe parties understand that, absent this mandatory provision, they would have the right to sue in court and have a jury trial. They further understand that, in some instances, the costs of arbitration could exceed the costs of litigation and the right to discovery may be more limited in arbitration than in court.\n\n### 13.3 Location ###\n\nBinding arbitration shall take place in New York. You agree to submit to the personal jurisdiction of any federal or state court in New York County, New York, in order to compel arbitration, to stay proceedings pending arbitration, or to confirm, modify, vacate or enter judgment on the award entered by the arbitrator.\n\n### 13.4 Class Action Waiver ###\n\nThe parties further agree that any arbitration shall be conducted in their individual capacities only and not as a class action or other representative action, and the parties expressly waive their right to file a class action or seek relief on a class basis. YOU AND METAMASK AGREE THAT EACH MAY BRING CLAIMS AGAINST THE OTHER ONLY IN YOUR OR ITS INDIVIDUAL CAPACITY, AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR REPRESENTATIVE PROCEEDING. If any court or arbitrator determines that the class action waiver set forth in this paragraph is void or unenforceable for any reason or that an arbitration can proceed on a class basis, then the arbitration provision set forth above shall be deemed null and void in its entirety and the parties shall be deemed to have not agreed to arbitrate disputes.\n\n### 13.5 Exception - Litigation of Intellectual Property and Small Claims Court Claims ###\n\nNotwithstanding the parties' decision to resolve all disputes through arbitration, either party may bring an action in state or federal court to protect its intellectual property rights (\"intellectual property rights\" means patents, copyrights, moral rights, trademarks, and trade secrets, but not privacy or publicity rights). Either party may also seek relief in a small claims court for disputes or claims within the scope of that court’s jurisdiction.\n\n### 13.6 30-Day Right to Opt Out ###\n\nYou have the right to opt-out and not be bound by the arbitration and class action waiver provisions set forth above by sending written notice of your decision to opt-out to the following address: MetaMask ℅ ConsenSys, 49 Bogart Street, Brooklyn NY 11206 and via email at legal-opt@metamask.io. The notice must be sent within 30 days of September 6, 2016 or your first use of the Service, whichever is later, otherwise you shall be bound to arbitrate disputes in accordance with the terms of those paragraphs. If you opt-out of these arbitration provisions, MetaMask also will not be bound by them.\n\n### 13.7 Changes to This Section ###\n\nMetaMask will provide 60-days’ notice of any changes to this section. Changes will become effective on the 60th day, and will apply prospectively only to any claims arising after the 60th day.\n\nFor any dispute not subject to arbitration you and MetaMask agree to submit to the personal and exclusive jurisdiction of and venue in the federal and state courts located in New York, New York. You further agree to accept service of process by mail, and hereby waive any and all jurisdictional and venue defenses otherwise available.\n\nThe Terms and the relationship between you and MetaMask shall be governed by the laws of the State of New York without regard to conflict of law provisions.\n\n## 14. General Information ##\n\n### 14.1 Entire Agreement ###\n\nThese Terms (and any additional terms, rules and conditions of participation that MetaMask may post on the Service) constitute the entire agreement between you and MetaMask with respect to the Service and supersedes any prior agreements, oral or written, between you and MetaMask. In the event of a conflict between these Terms and the additional terms, rules and conditions of participation, the latter will prevail over the Terms to the extent of the conflict.\n\n### 14.2 Waiver and Severability of Terms ###\n\nThe failure of MetaMask to exercise or enforce any right or provision of the Terms shall not constitute a waiver of such right or provision. If any provision of the Terms is found by an arbitrator or court of competent jurisdiction to be invalid, the parties nevertheless agree that the arbitrator or court should endeavor to give effect to the parties' intentions as reflected in the provision, and the other provisions of the Terms remain in full force and effect.\n\n### 14.3 Statute of Limitations ###\n\nYou agree that regardless of any statute or law to the contrary, any claim or cause of action arising out of or related to the use of the Service or the Terms must be filed within one (1) year after such claim or cause of action arose or be forever barred.\n\n### 14.4 Section Titles ###\n\nThe section titles in the Terms are for convenience only and have no legal or contractual effect.\n\n### 14.5 Communications ###\n\nUsers with questions, complaints or claims with respect to the Service may contact us using the relevant contact information set forth above and at communications@metamask.io.\n\n## 15 Related Links ##\n\n**[Terms of Use](https://metamask.io/terms.html)**\n\n**[Privacy](https://metamask.io/privacy.html)**\n\n**[Attributions](https://metamask.io/attributions.html)**\n\n","id":0}] \ No newline at end of file -- cgit v1.2.3 From 2920faf63cea9951d4c26aa37bf589c225ee6af0 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Mon, 17 Apr 2017 13:45:49 -0700 Subject: Add more detailed instructions on generating and deleting notices. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index aa79f4564..496b5423f 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,11 @@ To add a notice: ``` npm run generateNotice ``` +Enter the body of your notice into the text editor that pops up, without including the body. Be sure to save the file before closing the window! +Afterwards, enter the title of the notice in the command line and press enter. Afterwards, add and commit the new changes made. + To delete a notice: ``` npm run deleteNotice ``` +A list of active notices will pop up. Enter the corresponding id in the command line prompt and add and commit the new changes afterwards. -- cgit v1.2.3 From 83811910c84342094be4ac94dca829e8f5ff630f Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 18 Apr 2017 18:20:31 +0200 Subject: Create a custom radio list component --- ui/app/components/custom-radio-list.js | 61 ++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 ui/app/components/custom-radio-list.js diff --git a/ui/app/components/custom-radio-list.js b/ui/app/components/custom-radio-list.js new file mode 100644 index 000000000..201ec11dc --- /dev/null +++ b/ui/app/components/custom-radio-list.js @@ -0,0 +1,61 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = RadioList + +inherits(RadioList, Component) +function RadioList () { + Component.call(this) +} + +RadioList.prototype.render = function () { + const props = this.props + let activeClass = '.custom-radio-selected' + let inactiveClass = '.custom-radio-inactive' + let { + lables, + defaultFocus, + onClick, + } = props + + + return ( + h('.flex-row', { + style: { + fontSize: '12px', + } + }, [ + h('.flex-column.custom-radios', { + style: { + marginRight: '5px', + } + }, + lables.map((lable, i) => { + let isSelcted = (this.state !== null ) + isSelcted = isSelcted ? (this.state.selected === lable) : (this.props.defaultFocus === lable) + return h(isSelcted ? activeClass : inactiveClass, { + title: lable, + onClick: (event) => { + this.setState({selected: event.target.title}) + props.onClick(event) + } + }) + }), + ), + h('.text', {}, + lables.map((lable) => { + if (props.subtext) { + return h('.flex-row', {}, [ + h('.radio-titles', lable), + h('.radio-titles-subtext', `- ${props.subtext[lable]}`) + ]) + } else { + return h('.radio-titles', lable) + } + }) + ) + ]) + ) +} + -- cgit v1.2.3 From ce03b7db51570295c7868382cf997dbb1bc5725a Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 18 Apr 2017 18:21:24 +0200 Subject: Initial redo attempt of the buy view to look like vladt's desighn --- ui/app/components/buy-button-subview.js | 139 ++++++++++++++++++++------------ ui/app/components/coinbase-form.js | 101 ++--------------------- ui/app/components/custom-radio-list.js | 49 ++++++----- ui/app/components/shapeshift-form.js | 24 ++---- ui/app/css/index.css | 41 ++++++++++ ui/app/reducers/app.js | 14 ++-- 6 files changed, 173 insertions(+), 195 deletions(-) diff --git a/ui/app/components/buy-button-subview.js b/ui/app/components/buy-button-subview.js index 6810303e1..8d3e9aa21 100644 --- a/ui/app/components/buy-button-subview.js +++ b/ui/app/components/buy-button-subview.js @@ -6,12 +6,15 @@ const actions = require('../actions') const CoinbaseForm = require('./coinbase-form') const ShapeshiftForm = require('./shapeshift-form') const Loading = require('./loading') -const TabBar = require('./tab-bar') +const AccountPanel = require('./account-panel') +const RadioList = require('./custom-radio-list') module.exports = connect(mapStateToProps)(BuyButtonSubview) function mapStateToProps (state) { return { + identity: state.appState.identity, + account: state.metamask.accounts[state.appState.buyView.buyAddress], warning: state.appState.warning, buyView: state.appState.buyView, network: state.metamask.network, @@ -31,7 +34,11 @@ BuyButtonSubview.prototype.render = function () { const isLoading = props.isSubLoading return ( - h('.buy-eth-section', [ + h('.buy-eth-section.flex-column', { + style: { + alignItems: 'center', + }, + }, [ // back button h('.flex-row', { style: { @@ -46,58 +53,79 @@ BuyButtonSubview.prototype.render = function () { left: '10px', }, }), - h('h2.page-subtitle', 'Buy Eth'), - ]), - - h(Loading, { isLoading }), - - h(TabBar, { - tabs: [ - { - content: [ - 'Coinbase', - h('a', { - onClick: (event) => this.navigateTo('https://github.com/MetaMask/faq/blob/master/COINBASE.md'), - }, [ - h('i.fa.fa-question-circle', { - style: { - margin: '0px 5px', - }, - }), - ]), - ], - key: 'coinbase', + h('h2.text-transform-uppercase.flex-center', { + style: { + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', }, - { - content: [ - 'Shapeshift', - h('a', { - href: 'https://github.com/MetaMask/faq/blob/master/COINBASE.md', - onClick: (event) => this.navigateTo('https://info.shapeshift.io/about'), - }, [ - h('i.fa.fa-question-circle', { - style: { - margin: '0px 5px', - }, - }), - ]), - ], - key: 'shapeshift', + }, 'Buy Eth'), + ]), + h('div', { + style: { + position: 'absolute', + top: '57vh', + left: '49vw', + }, + }, [ + h(Loading, {isLoading}), + ]), + h('div', { + style: { + width: '80%', + }, + }, [ + h(AccountPanel, { + showFullAddress: true, + identity: props.identity, + account: props.account, + }), + ]), + h('h3.text-transform-uppercase', { + style: { + paddingLeft: '15px', + fontFamily: 'Montserrat Light', + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', + }, + }, 'Select Service'), + h('.flex-row.selected-exchange', { + style: { + position: 'relative', + right: '35px', + marginTop: '20px', + marginBottom: '20px', + }, + }, [ + h(RadioList, { + defaultFocus: props.buyView.subview, + lables: [ + 'Coinbase', + 'ShapeShift', + ], + subtext: { + 'Coinbase': 'Crypto/FIAT (USA only)', + 'ShapeShift': 'Crypto', }, - ], - defaultTab: 'coinbase', - tabSelected: (key) => { - switch (key) { - case 'coinbase': - props.dispatch(actions.coinBaseSubview()) - break - case 'shapeshift': - props.dispatch(actions.shapeShiftSubview(props.provider.type)) - break - } + onClick: this.radioHandler.bind(this), + }), + ]), + h('h3.text-transform-uppercase', { + style: { + paddingLeft: '15px', + fontFamily: 'Montserrat Light', + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', }, - }), - + }, props.buyView.subview), this.formVersionSubview(), ]) ) @@ -152,3 +180,12 @@ BuyButtonSubview.prototype.backButtonContext = function () { this.props.dispatch(actions.goHome()) } } + +BuyButtonSubview.prototype.radioHandler = function (event) { + switch (event.target.title) { + case 'Coinbase': + return this.props.dispatch(actions.coinBaseSubview()) + case 'ShapeShift': + return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type)) + } +} diff --git a/ui/app/components/coinbase-form.js b/ui/app/components/coinbase-form.js index fd5816a21..b92799375 100644 --- a/ui/app/components/coinbase-form.js +++ b/ui/app/components/coinbase-form.js @@ -4,7 +4,6 @@ const inherits = require('util').inherits const connect = require('react-redux').connect const actions = require('../actions') -const isValidAddress = require('../util').isValidAddress module.exports = connect(mapStateToProps)(CoinbaseForm) function mapStateToProps (state) { @@ -21,72 +20,19 @@ function CoinbaseForm () { CoinbaseForm.prototype.render = function () { var props = this.props - var amount = props.buyView.amount - var address = props.buyView.buyAddress return h('.flex-column', { style: { // margin: '10px', padding: '25px', + width: '100%', }, }, [ - h('.flex-column', { - style: { - alignItems: 'flex-start', - }, - }, [ - h('.flex-row', [ - h('div', 'Address:'), - h('.ellip-address', address), - ]), - h('.flex-row', [ - h('div', 'Amount: $'), - h('.input-container', [ - h('input.buy-inputs', { - style: { - width: '3em', - boxSizing: 'border-box', - }, - defaultValue: amount, - onChange: this.handleAmount.bind(this), - }), - h('i.fa.fa-pencil-square-o.edit-text', { - style: { - fontSize: '12px', - color: '#F7861C', - position: 'relative', - bottom: '5px', - right: '11px', - }, - }), - ]), - ]), - ]), - - h('.info-gray', { - style: { - fontSize: '10px', - fontFamily: 'Montserrat Light', - margin: '15px', - lineHeight: '13px', - }, - }, - `there is a USD$ 15 a day max and a USD$ 50 - dollar limit per the life time of an account without a - coinbase account. A fee of 3.75% will be aplied to debit/credit cards.`), - - !props.warning ? h('div', { - style: { - width: '340px', - height: '22px', - }, - }) : props.warning && h('span.error.flex-center', props.warning), - - h('.flex-row', { style: { justifyContent: 'space-around', margin: '33px', + marginTop: '0px', }, }, [ h('button', { @@ -106,20 +52,9 @@ CoinbaseForm.prototype.handleAddress = function (event) { this.props.dispatch(actions.updateBuyAddress(event.target.value)) } CoinbaseForm.prototype.toCoinbase = function () { - var props = this.props - var amount = props.buyView.amount - var address = props.buyView.buyAddress - var message - - if (isValidAddress(address) && isValidAmountforCoinBase(amount).valid) { - props.dispatch(actions.buyEth({ network: '1', address, amount: props.buyView.amount })) - } else if (!isValidAmountforCoinBase(amount).valid) { - message = isValidAmountforCoinBase(amount).message - return props.dispatch(actions.displayWarning(message)) - } else { - message = 'Receiving address is invalid.' - return props.dispatch(actions.displayWarning(message)) - } + const props = this.props + const address = props.buyView.buyAddress + props.dispatch(actions.buyEth({ network: '1', address, amount: 0 })) } CoinbaseForm.prototype.renderLoading = function () { @@ -131,29 +66,3 @@ CoinbaseForm.prototype.renderLoading = function () { src: 'images/loading.svg', }) } - -function isValidAmountforCoinBase (amount) { - amount = parseFloat(amount) - if (amount) { - if (amount <= 15 && amount > 0) { - return { - valid: true, - } - } else if (amount > 15) { - return { - valid: false, - message: 'The amount can not be greater then $15', - } - } else { - return { - valid: false, - message: 'Can not buy amounts less then $0', - } - } - } else { - return { - valid: false, - message: 'The amount entered is not a number', - } - } -} diff --git a/ui/app/components/custom-radio-list.js b/ui/app/components/custom-radio-list.js index 201ec11dc..a19287630 100644 --- a/ui/app/components/custom-radio-list.js +++ b/ui/app/components/custom-radio-list.js @@ -11,12 +11,11 @@ function RadioList () { RadioList.prototype.render = function () { const props = this.props - let activeClass = '.custom-radio-selected' - let inactiveClass = '.custom-radio-inactive' - let { + const activeClass = '.custom-radio-selected' + const inactiveClass = '.custom-radio-inactive' + const { lables, defaultFocus, - onClick, } = props @@ -24,37 +23,37 @@ RadioList.prototype.render = function () { h('.flex-row', { style: { fontSize: '12px', - } + }, }, [ - h('.flex-column.custom-radios', { - style: { - marginRight: '5px', - } + h('.flex-column.custom-radios', { + style: { + marginRight: '5px', }, - lables.map((lable, i) => { - let isSelcted = (this.state !== null ) - isSelcted = isSelcted ? (this.state.selected === lable) : (this.props.defaultFocus === lable) - return h(isSelcted ? activeClass : inactiveClass, { - title: lable, - onClick: (event) => { - this.setState({selected: event.target.title}) - props.onClick(event) - } - }) - }), - ), - h('.text', {}, - lables.map((lable) => { + }, + lables.map((lable, i) => { + let isSelcted = (this.state !== null) + isSelcted = isSelcted ? (this.state.selected === lable) : (defaultFocus === lable) + return h(isSelcted ? activeClass : inactiveClass, { + title: lable, + onClick: (event) => { + this.setState({selected: event.target.title}) + props.onClick(event) + }, + }) + }) + ), + h('.text', {}, + lables.map((lable) => { if (props.subtext) { return h('.flex-row', {}, [ h('.radio-titles', lable), - h('.radio-titles-subtext', `- ${props.subtext[lable]}`) + h('.radio-titles-subtext', `- ${props.subtext[lable]}`), ]) } else { return h('.radio-titles', lable) } }) - ) + ), ]) ) } diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js index 8c9686035..2745b1b11 100644 --- a/ui/app/components/shapeshift-form.js +++ b/ui/app/components/shapeshift-form.js @@ -43,14 +43,18 @@ ShapeshiftForm.prototype.renderMain = function () { style: { // marginTop: '10px', padding: '25px', + paddingTop: '5px', width: '100%', + minHeight: '215px', alignItems: 'center', + overflowY: 'auto', }, }, [ h('.flex-row', { style: { justifyContent: 'center', alignItems: 'baseline', + height: '42px', }, }, [ h('img', { @@ -82,7 +86,6 @@ ShapeshiftForm.prototype.renderMain = function () { style: { fontSize: '12px', color: '#F7861C', - position: 'relative', bottom: '48px', left: '106px', }, @@ -92,7 +95,6 @@ ShapeshiftForm.prototype.renderMain = function () { h('.icon-control', [ h('i.fa.fa-refresh.fa-4.orange', { style: { - position: 'relative', bottom: '5px', left: '5px', color: '#F7861C', @@ -121,8 +123,6 @@ ShapeshiftForm.prototype.renderMain = function () { }, }), ]), - - this.props.isSubLoading ? this.renderLoading() : null, h('.flex-column', { style: { alignItems: 'flex-start', @@ -138,17 +138,6 @@ ShapeshiftForm.prototype.renderMain = function () { this.props.warning) : this.renderInfo(), ]), - h('.flex-row', { - style: { - padding: '10px', - paddingBottom: '2px', - width: '100%', - }, - }, [ - h('div', 'Receiving address:'), - h('.ellip-address', this.props.buyView.buyAddress), - ]), - h(this.activeToggle('.input-container'), { style: { padding: '10px', @@ -156,6 +145,7 @@ ShapeshiftForm.prototype.renderMain = function () { width: '100%', }, }, [ + h('div', `${coin} Address:`), h('input#fromCoinAddress.buy-inputs', { @@ -190,6 +180,8 @@ ShapeshiftForm.prototype.renderMain = function () { onClick: this.shift.bind(this), style: { marginTop: '10px', + position: 'relative', + bottom: '33px', }, }, 'Submit'), @@ -266,8 +258,6 @@ ShapeshiftForm.prototype.renderInfo = function () { return h('span', { style: { - marginTop: '10px', - marginBottom: '15px', }, }, [ h('h3.flex-row.text-transform-uppercase', { diff --git a/ui/app/css/index.css b/ui/app/css/index.css index 033502f5a..808aafb4c 100644 --- a/ui/app/css/index.css +++ b/ui/app/css/index.css @@ -489,6 +489,47 @@ input.large-input { } /* buy eth warning screen */ +.custom-radios { + justify-content: space-around; + align-items: center; +} + + +.custom-radio-selected { + width: 17px; + height: 17px; + border: solid; + border-style: double; + border-radius: 15px; + border-width: 5px; + background: rgba(247, 134, 28, 1); + border-color: #F7F7F7; +} + +.custom-radio-inactive { + width: 14px; + height: 14px; + border: solid; + border-width: 1px; + border-radius: 24px; + border-color: #AEAEAE; +} + +.radio-titles { + color: rgba(247, 134, 28, 1); +} + +.radio-titles-subtext { + +} + +.selected-exchange { + +} + +.buy-radio { + +} .eth-warning{ transition: opacity 400ms ease-in, transform 400ms ease-in; diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 7ad1229e5..6b040e988 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -469,8 +469,10 @@ function reduceApp (state, action) { name: 'buyEth', context: appState.currentView.name, }, + identity: state.metamask.identities[action.value], + buyAddress: action.value, buyView: { - subview: 'buyForm', + subview: 'Coinbase', amount: '15.00', buyAddress: action.value, formView: { @@ -483,7 +485,7 @@ function reduceApp (state, action) { case actions.UPDATE_BUY_ADDRESS: return extend(appState, { buyView: { - subview: 'buyForm', + subview: appState.subview, formView: { coinbase: appState.buyView.formView.coinbase, shapeshift: appState.buyView.formView.shapeshift, @@ -496,7 +498,7 @@ function reduceApp (state, action) { case actions.UPDATE_COINBASE_AMOUNT: return extend(appState, { buyView: { - subview: 'buyForm', + subview: 'Coinbase', formView: { coinbase: true, shapeshift: false, @@ -509,7 +511,7 @@ function reduceApp (state, action) { case actions.COINBASE_SUBVIEW: return extend(appState, { buyView: { - subview: 'buyForm', + subview: 'Coinbase', formView: { coinbase: true, shapeshift: false, @@ -522,7 +524,7 @@ function reduceApp (state, action) { case actions.SHAPESHIFT_SUBVIEW: return extend(appState, { buyView: { - subview: 'buyForm', + subview: 'ShapeShift', formView: { coinbase: false, shapeshift: true, @@ -537,7 +539,7 @@ function reduceApp (state, action) { case actions.PAIR_UPDATE: return extend(appState, { buyView: { - subview: 'buyForm', + subview: 'ShapeShift', formView: { coinbase: false, shapeshift: true, -- cgit v1.2.3 From 9bae32e78b230ede45ab159e0022da5728f0f267 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 20 Apr 2017 19:07:09 -0700 Subject: Add functional but ugly and hard-coded token list --- .babelrc | 5 +++- package.json | 6 +++++ ui/app/account-detail.js | 37 ++++++++++++++++++++++++- ui/app/components/account-export.js | 2 -- ui/app/components/token-cell.js | 22 +++++++++++++++ ui/app/components/token-list.js | 51 +++++++++++++++++++++++++++++++++++ ui/app/components/transaction-list.js | 11 -------- 7 files changed, 119 insertions(+), 15 deletions(-) create mode 100644 ui/app/components/token-cell.js create mode 100644 ui/app/components/token-list.js diff --git a/.babelrc b/.babelrc index 9d8d51656..bca3364fc 100644 --- a/.babelrc +++ b/.babelrc @@ -1 +1,4 @@ -{ "presets": ["es2015"] } +{ + "presets": ["es2015", "stage-0"], + "plugins": ["transform-runtime", "transform-async-to-generator"] +} diff --git a/package.json b/package.json index b892653fa..2eaaf7154 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "eth-query": "^1.0.3", "eth-sig-util": "^1.1.1", "eth-simple-keyring": "^1.1.1", + "eth-token-tracker": "^1.0.4", "ethereumjs-tx": "^1.2.5", "ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", @@ -118,7 +119,12 @@ "xtend": "^4.0.1" }, "devDependencies": { + "babel-core": "^6.24.1", "babel-eslint": "^6.0.5", + "babel-plugin-transform-async-to-generator": "^6.24.1", + "babel-plugin-transform-runtime": "^6.23.0", + "babel-polyfill": "^6.23.0", + "babel-preset-stage-0": "^6.24.1", "babel-register": "^6.7.2", "babelify": "^7.2.0", "beefy": "^2.1.5", diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index 018e74893..411f38e5e 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -17,6 +17,8 @@ const ethUtil = require('ethereumjs-util') const EditableLabel = require('./components/editable-label') const Tooltip = require('./components/tooltip') const BuyButtonSubview = require('./components/buy-button-subview') +const TabBar = require('./components/tab-bar') +const TokenList = require('./components/token-list') module.exports = connect(mapStateToProps)(AccountDetailScreen) function mapStateToProps (state) { @@ -35,6 +37,7 @@ function mapStateToProps (state) { inherits(AccountDetailScreen, Component) function AccountDetailScreen () { + this.state = {} Component.call(this) } @@ -234,18 +237,50 @@ AccountDetailScreen.prototype.subview = function () { switch (subview) { case 'transactions': - return this.transactionList() + return this.tabSections() case 'export': var state = extend({key: 'export'}, this.props) return h(ExportAccountView, state) case 'buyForm': return h(BuyButtonSubview, extend({key: 'buyForm'}, this.props)) + default: + return this.tabSections() + } +} + +AccountDetailScreen.prototype.tabSections = function () { + + return h('section.tabSection', [ + + h(TabBar, { + tabs: [ + { content: 'History', key: 'history' }, + { content: 'Tokens', key: 'tokens' }, + ], + defaultTab: 'history', + tabSelected: (key) => { + this.setState({ tabSelection: key }) + }, + }), + + this.tabSwitchView(), + ]) +} + +AccountDetailScreen.prototype.tabSwitchView = function () { + const tabSelection = this.state.tabSelection || 'history' + const userAddress = this.props.address + + switch (tabSelection) { + case 'tokens': + return h(TokenList, { userAddress }) default: return this.transactionList() } } AccountDetailScreen.prototype.transactionList = function () { + const {transactions, unapprovedMsgs, address, network, shapeShiftTxList } = this.props return h(TransactionList, { transactions: transactions.sort((a, b) => b.time - a.time), diff --git a/ui/app/components/account-export.js b/ui/app/components/account-export.js index 888196c5d..394d878f7 100644 --- a/ui/app/components/account-export.js +++ b/ui/app/components/account-export.js @@ -20,8 +20,6 @@ function mapStateToProps (state) { } ExportAccountView.prototype.render = function () { - console.log('EXPORT VIEW') - console.dir(this.props) var state = this.props var accountDetail = state.accountDetail diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js new file mode 100644 index 000000000..34a12733f --- /dev/null +++ b/ui/app/components/token-cell.js @@ -0,0 +1,22 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = TokenCell + +inherits(TokenCell, Component) +function TokenCell () { + Component.call(this) +} + +TokenCell.prototype.render = function () { + const props = this.props + const { address, symbol, string } = props + log.info({ address, symbol, string }) + + return ( + h('li', [ + h('span', `${symbol}: ${string}`), + ]) + ) +} diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js new file mode 100644 index 000000000..35e79401b --- /dev/null +++ b/ui/app/components/token-list.js @@ -0,0 +1,51 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const TokenTracker = require('eth-token-tracker') +const TokenCell = require('./token-cell.js') + +module.exports = TokenList + +inherits(TokenList, Component) +function TokenList () { + + // Hard coded for development for now: + const tokens = [ + { address: '0x48c80F1f4D53D5951e5D5438B54Cba84f29F32a5', symbol: 'REP', balance: 'aa'}, + { address: '0xc66ea802717bfb9833400264dd12c2bceaa34a6d', symbol: 'MKR', balance: '1000', decimals: 18}, + { address: '0xa74476443119A942dE498590Fe1f2454d7D4aC0d', symbol: 'GOL', balance: 'ff'}, + { address: '0xaec2e87e0a235266d9c5adc9deb4b2e29b54d009', symbol: 'SNGLS', balance: '0' }, + ] + + this.state = { tokens } + Component.call(this) +} + +TokenList.prototype.render = function () { + const tokens = this.state.tokens + + return ( + h('ol', tokens.map((tokenData) => { + console.log('rendering token with', tokenData) + return h(TokenCell, tokenData) + })) + ) +} + +TokenList.prototype.componentDidMount = function () { + const { userAddress } = this.props + + this.tracker = new TokenTracker({ + userAddress, + provider: web3.currentProvider, + tokens: this.state.tokens, + }) + + this.setState({ tokens: this.tracker.serialize() }) + this.tracker.on('update', (tokenData) => this.setState({ tokens: tokenData })) + this.tracker.updateBalances() +} + +TokenList.prototype.componentWillUnmount = function () { + this.tracker.stop() +} diff --git a/ui/app/components/transaction-list.js b/ui/app/components/transaction-list.js index 3ae953637..4c25f3dd9 100644 --- a/ui/app/components/transaction-list.js +++ b/ui/app/components/transaction-list.js @@ -36,17 +36,6 @@ TransactionList.prototype.render = function () { } `), - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - paddingTop: '4px', - paddingBottom: '4px', - }, - }, [ - 'History', - ]), - h('.tx-list', { style: { overflowY: 'auto', -- cgit v1.2.3 From 4a4a7373607518066eb334dfae8b428832a31369 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Fri, 21 Apr 2017 17:56:14 +0200 Subject: Fix for firefox --- mascara/src/lib/index-db-controller.js | 94 ++++++++++++++-------------------- 1 file changed, 39 insertions(+), 55 deletions(-) diff --git a/mascara/src/lib/index-db-controller.js b/mascara/src/lib/index-db-controller.js index 8db1d5d21..1e4148b16 100644 --- a/mascara/src/lib/index-db-controller.js +++ b/mascara/src/lib/index-db-controller.js @@ -1,39 +1,20 @@ +// module.exports = const EventEmitter = require('events') module.exports = class IndexDbController extends EventEmitter { constructor (opts) { super() + global.IDBTransaction = global.IDBTransaction || global.webkitIDBTransaction || global.msIDBTransaction || {READ_WRITE: "readwrite"}; // This line should only be needed if it is needed to support the object's constants for older browsers + global.IDBKeyRange = global.IDBKeyRange || global.webkitIDBKeyRange || global.msIDBKeyRange this.migrations = opts.migrations this.key = opts.key - this.dbObject = global.indexedDB - this.IDBTransaction = global.IDBTransaction || global.webkitIDBTransaction || global.msIDBTransaction || {READ_WRITE: "readwrite"}; // This line should only be needed if it is needed to support the object's constants for older browsers - this.IDBKeyRange = global.IDBKeyRange || global.webkitIDBKeyRange || global.msIDBKeyRange; this.version = opts.version - this.logging = opts.logging this.initialState = opts.initialState - if (this.logging) this.on('log', logger) } // Opens the database connection and returns a promise open (version = this.version) { - return new Promise((resolve, reject) => { - const dbOpenRequest = this.dbObject.open(this.key, version) - dbOpenRequest.onerror = (event) => { - return reject(event) - } - dbOpenRequest.onsuccess = (event) => { - this.db = dbOpenRequest.result - this.emit('success') - resolve(this.db) - } - dbOpenRequest.onupgradeneeded = (event) => { - this.db = event.target.result - this.db.createObjectStore('dataStore') - } - }) - .then((openRequest) => { - return this.get('dataStore') - }) + return this.get('dataStore') .then((data) => { if (!data) { return this._add('dataStore', this.initialState) @@ -44,45 +25,48 @@ module.exports = class IndexDbController extends EventEmitter { }) } - requestObjectStore (key, type = 'readonly') { - return new Promise((resolve, reject) => { - const dbReadWrite = this.db.transaction(key, type) - const dataStore = dbReadWrite.objectStore(key) - resolve(dataStore) - }) - } get (key = 'dataStore') { - return this.requestObjectStore(key) - .then((dataObject)=> { - return new Promise((resolve, reject) => { - const getRequest = dataObject.get(key) - getRequest.onsuccess = (event) => resolve(event.currentTarget.result) - getRequest.onerror = (event) => reject(event) - }) - }) + return this._request('get', key) } - put (state) { - return this.requestObjectStore('dataStore', 'readwrite') - .then((dataObject)=> { - const putRequest = dataObject.put(state, 'dataStore') - putRequest.onsuccess = (event) => Promise.resolve(event.currentTarget.result) - putRequest.onerror = (event) => Promise.reject(event) - }) + return this._request('put', state, 'dataStore') } - _add (key, objStore, cb = logger) { - return this.requestObjectStore(key, 'readwrite') - .then((dataObject)=> { - const addRequest = dataObject.add(objStore, key) - addRequest.onsuccess = (event) => Promise.resolve(event.currentTarget.result) - addRequest.onerror = (event) => Promise.reject(event) - }) + _add (key = 'dataStore', objStore) { + return this._request('add', objStore, key) } -} + _request (call, ...args) { + return new Promise((resolve, reject) => { + const self = this + const dbOpenRequest = global.indexedDB.open(this.key, this.version) + + dbOpenRequest.onupgradeneeded = (event) => { + this.db = event.target.result + this.db.createObjectStore('dataStore') + } -function logger (err, ress) { - err ? console.error(`Logger says: ${err}`) : console.dir(`Logger says: ${ress}`) + dbOpenRequest.onsuccess = (event) => { + this.db = dbOpenRequest.result + this.emit('success') + const dbTransaction = this.db.transaction('dataStore', 'readwrite') + const request = dbTransaction.objectStore('dataStore') + const objRequest = request[call](...args) + objRequest.onsuccess = (event) => { + return resolve(objRequest.result) + } + objRequest.onerror = (err) => { + return reject(err.message) + } + dbTransaction.oncomplete = (event) => { + this.emit('complete') + } + } + + dbOpenRequest.onerror = (event) => { + return reject(event) + } + }) + } } -- cgit v1.2.3 From 437c4acc9f6738ff5b07682860a72c270f2bfad6 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Fri, 21 Apr 2017 17:58:18 +0200 Subject: Reduce wakeup time for firefox --- mascara/src/ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mascara/src/ui.js b/mascara/src/ui.js index 2873017cb..cdf985ebd 100644 --- a/mascara/src/ui.js +++ b/mascara/src/ui.js @@ -24,7 +24,7 @@ const background = new SWcontroller({ fileName: '/background.js', letBeIdle: false, intervalDelay, - wakeUpInterval: 30000 + wakeUpInterval: 20000 }) // Setup listener for when the service worker is read background.on('ready', (readSw) => { -- cgit v1.2.3 From 7202639b370279f90886f2e94ab77aba5b205bc3 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 21 Apr 2017 09:01:38 -0700 Subject: Add token-list dev state --- development/states/token-list.json | 93 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 development/states/token-list.json diff --git a/development/states/token-list.json b/development/states/token-list.json new file mode 100644 index 000000000..404f1aedd --- /dev/null +++ b/development/states/token-list.json @@ -0,0 +1,93 @@ +{ + "metamask": { + "isInitialized": true, + "isUnlocked": true, + "rpcTarget": "https://rawtestrpc.metamask.io/", + "identities": { + "0x55e2780588aa5000f464f700d2676fd0a22ee160": { + "address": "0x55e2780588aa5000f464f700d2676fd0a22ee160", + "name": "Account 1" + }, + "0x1c3d6d41dcb245c11a449ec46c9cf9eb7dada4af": { + "address": "0x1c3d6d41dcb245c11a449ec46c9cf9eb7dada4af", + "name": "Account 2" + }, + "0xe34b1ac3074121418152c7a68b4ae6cb7803d725": { + "address": "0xe34b1ac3074121418152c7a68b4ae6cb7803d725", + "name": "Account 3" + } + }, + "unapprovedTxs": {}, + "noActiveNotices": true, + "frequentRpcList": [], + "addressBook": [], + "network": "1", + "accounts": { + "0x55e2780588aa5000f464f700d2676fd0a22ee160": { + "balance": "0x4622f471c28b8a53", + "nonce": "0x17", + "code": "0x", + "address": "0x55e2780588aa5000f464f700d2676fd0a22ee160" + }, + "0x1c3d6d41dcb245c11a449ec46c9cf9eb7dada4af": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "address": "0x1c3d6d41dcb245c11a449ec46c9cf9eb7dada4af" + }, + "0xe34b1ac3074121418152c7a68b4ae6cb7803d725": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "address": "0xe34b1ac3074121418152c7a68b4ae6cb7803d725" + } + }, + "transactions": {}, + "currentBlockNumber": 3575443, + "currentBlockHash": "0xf03bdb8ad844336723473865a5368fa618de837d8290ad380fadbc9fa2bf87f6", + "selectedAddressTxList": [], + "unapprovedMsgs": {}, + "unapprovedMsgCount": 0, + "unapprovedPersonalMsgs": {}, + "unapprovedPersonalMsgCount": 0, + "keyringTypes": [ + "Simple Key Pair", + "HD Key Tree" + ], + "keyrings": [ + { + "type": "HD Key Tree", + "accounts": [ + "55e2780588aa5000f464f700d2676fd0a22ee160", + "1c3d6d41dcb245c11a449ec46c9cf9eb7dada4af", + "e34b1ac3074121418152c7a68b4ae6cb7803d725" + ] + } + ], + "selectedAddress": "0x55e2780588aa5000f464f700d2676fd0a22ee160", + "currentCurrency": "USD", + "conversionRate": 51.12009214, + "conversionDate": 1492788481, + "provider": { + "type": "mainnet" + }, + "shapeShiftTxList": [], + "lostAccounts": [] + }, + "appState": { + "shouldClose": false, + "menuOpen": false, + "currentView": { + "name": "accountDetail", + "detailView": "tokens", + "context": "0x55e2780588aa5000f464f700d2676fd0a22ee160" + }, + "accountDetail": { + "subview": "transactions" + }, + "transForward": true, + "isLoading": false, + "warning": null + }, + "identities": {} +} -- cgit v1.2.3 From 40e2450022488daa5e36d4c1b866e061bba7c5d2 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 21 Apr 2017 09:01:51 -0700 Subject: Get token list looking ok --- ui/app/account-detail.js | 18 ++++++++- ui/app/components/balance.js | 88 +++++++++++++++++++++++++++++++++++++++++ ui/app/components/identicon.js | 8 ++-- ui/app/components/token-cell.js | 11 +++++- ui/app/components/token-list.js | 23 +++++++++-- 5 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 ui/app/components/balance.js diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index 411f38e5e..5ceee4fe0 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -249,6 +249,12 @@ AccountDetailScreen.prototype.subview = function () { } AccountDetailScreen.prototype.tabSections = function () { + var subview + try { + subview = this.props.accountDetail.subview + } catch (e) { + subview = null + } return h('section.tabSection', [ @@ -257,7 +263,7 @@ AccountDetailScreen.prototype.tabSections = function () { { content: 'History', key: 'history' }, { content: 'Tokens', key: 'tokens' }, ], - defaultTab: 'history', + defaultTab: subview || 'history', tabSelected: (key) => { this.setState({ tabSelection: key }) }, @@ -268,8 +274,16 @@ AccountDetailScreen.prototype.tabSections = function () { } AccountDetailScreen.prototype.tabSwitchView = function () { - const tabSelection = this.state.tabSelection || 'history' const userAddress = this.props.address + var subview + try { + subview = this.props.accountDetail.subview + return h(TokenList, { userAddress }) + } catch (e) { + subview = null + } + + const tabSelection = this.state.tabSelection || 'history' switch (tabSelection) { case 'tokens': diff --git a/ui/app/components/balance.js b/ui/app/components/balance.js new file mode 100644 index 000000000..3c5e24b65 --- /dev/null +++ b/ui/app/components/balance.js @@ -0,0 +1,88 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const formatBalance = require('../util').formatBalance const generateBalanceObject = require('../util').generateBalanceObject +const Tooltip = require('./tooltip.js') +const FiatValue = require('./fiat-value.js') + +module.exports = EthBalanceComponent + +inherits(EthBalanceComponent, Component) +function EthBalanceComponent () { + Component.call(this) +} + +EthBalanceComponent.prototype.render = function () { + var props = this.props + let { value } = props + var style = props.style + var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true + value = value ? formatBalance(value, 6, needsParse) : '...' + var width = props.width + + return ( + + h('.ether-balance.ether-balance-amount', { + style: style, + }, [ + h('div', { + style: { + display: 'inline', + width: width, + }, + }, this.renderBalance(value)), + ]) + + ) +} +EthBalanceComponent.prototype.renderBalance = function (value) { + var props = this.props + if (value === 'None') return value + if (value === '...') return value + var balanceObj = generateBalanceObject(value, props.shorten ? 1 : 3) + var balance + var splitBalance = value.split(' ') + var ethNumber = splitBalance[0] + var ethSuffix = splitBalance[1] + const showFiat = 'showFiat' in props ? props.showFiat : true + + if (props.shorten) { + balance = balanceObj.shortBalance + } else { + balance = balanceObj.balance + } + + var label = balanceObj.label + + return ( + h(Tooltip, { + position: 'bottom', + title: `${ethNumber} ${ethSuffix}`, + }, h('div.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('div', { + style: { + width: '100%', + textAlign: 'right', + }, + }, this.props.incoming ? `+${balance}` : balance), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + }, + }, label), + ]), + + showFiat ? h(FiatValue, { value: props.value }) : null, + ])) + ) +} diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js index 6d4871d02..1bb92301e 100644 --- a/ui/app/components/identicon.js +++ b/ui/app/components/identicon.js @@ -34,19 +34,19 @@ IdenticonComponent.prototype.render = function () { IdenticonComponent.prototype.componentDidMount = function () { var props = this.props - var address = props.address + const { address, network } = props if (!address) return var container = findDOMNode(this) var diameter = props.diameter || this.defaultDiameter - var img = iconFactory.iconForAddress(address, diameter, false) + var img = iconFactory.iconForAddress(address, diameter, false, network) container.appendChild(img) } IdenticonComponent.prototype.componentDidUpdate = function () { var props = this.props - var address = props.address + const { address, network } = props if (!address) return @@ -58,6 +58,6 @@ IdenticonComponent.prototype.componentDidUpdate = function () { } var diameter = props.diameter || this.defaultDiameter - var img = iconFactory.iconForAddress(address, diameter, false) + var img = iconFactory.iconForAddress(address, diameter, false, network) container.appendChild(img) } diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js index 34a12733f..81e92b301 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/token-cell.js @@ -1,6 +1,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits +const Identicon = require('./identicon') module.exports = TokenCell @@ -15,8 +16,14 @@ TokenCell.prototype.render = function () { log.info({ address, symbol, string }) return ( - h('li', [ - h('span', `${symbol}: ${string}`), + h('li.token-cell', [ + + h(Identicon, { + diameter: 50, + address, + }), + + h('h3', `${string || 0} ${symbol}`), ]) ) } diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index 35e79401b..c6a7d3552 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -24,11 +24,26 @@ function TokenList () { TokenList.prototype.render = function () { const tokens = this.state.tokens + const tokenViews = tokens.map((tokenData) => { + console.log('rendering token with', tokenData) + return h(TokenCell, tokenData) + }) + return ( - h('ol', tokens.map((tokenData) => { - console.log('rendering token with', tokenData) - return h(TokenCell, tokenData) - })) + h('ol', [h('style', ` + + li.token-cell { + display: flex; + flex-direction: row; + align-items: center; + padding: 10px; + } + + li.token-cell > h3 { + margin-left: 12px; + } + + `)].concat(tokenViews)) ) } -- cgit v1.2.3 From 1b19b51e0823726a01eab49ef9416f852f365500 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Fri, 21 Apr 2017 23:00:32 +0200 Subject: Clean up code --- mascara/src/lib/index-db-controller.js | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/mascara/src/lib/index-db-controller.js b/mascara/src/lib/index-db-controller.js index 1e4148b16..5aded1cbe 100644 --- a/mascara/src/lib/index-db-controller.js +++ b/mascara/src/lib/index-db-controller.js @@ -1,4 +1,3 @@ -// module.exports = const EventEmitter = require('events') module.exports = class IndexDbController extends EventEmitter { @@ -13,7 +12,7 @@ module.exports = class IndexDbController extends EventEmitter { } // Opens the database connection and returns a promise - open (version = this.version) { + open () { return this.get('dataStore') .then((data) => { if (!data) { @@ -42,10 +41,10 @@ module.exports = class IndexDbController extends EventEmitter { const self = this const dbOpenRequest = global.indexedDB.open(this.key, this.version) - dbOpenRequest.onupgradeneeded = (event) => { + dbOpenRequest.addEventListener('upgradeneeded', (event) => { this.db = event.target.result this.db.createObjectStore('dataStore') - } + }) dbOpenRequest.onsuccess = (event) => { this.db = dbOpenRequest.result @@ -53,20 +52,20 @@ module.exports = class IndexDbController extends EventEmitter { const dbTransaction = this.db.transaction('dataStore', 'readwrite') const request = dbTransaction.objectStore('dataStore') const objRequest = request[call](...args) - objRequest.onsuccess = (event) => { + objRequest.addEventListener('success', (event) => { return resolve(objRequest.result) - } - objRequest.onerror = (err) => { - return reject(err.message) - } - dbTransaction.oncomplete = (event) => { + }) + objRequest.addEventListener('error', (err) => { + return reject(`IndexDBController - ${call} failed to excute on indexedDB`) + }) + dbTransaction.addEventListener('complete', (event) => { this.emit('complete') - } + }) } - dbOpenRequest.onerror = (event) => { - return reject(event) - } + dbOpenRequest.addEventListener('error', (event) => { + return reject({message: `IndexDBController - open:@${call} failed to excute on indexedDB`, errorEvent: event}) + }) }) } } -- cgit v1.2.3 From e543050868b58ea1a1b0cad363d763eab2ade25d Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Sun, 23 Apr 2017 15:27:17 +0400 Subject: remove extra buyAddress in state --- ui/app/reducers/app.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 6b040e988..036286a8b 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -470,7 +470,6 @@ function reduceApp (state, action) { context: appState.currentView.name, }, identity: state.metamask.identities[action.value], - buyAddress: action.value, buyView: { subview: 'Coinbase', amount: '15.00', -- cgit v1.2.3 From 7a8496f9da894bf8821e91746b33f53fe23cf150 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Sun, 23 Apr 2017 15:28:45 +0400 Subject: remove buyButtonDeligator function --- ui/app/account-detail.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index 018e74893..d4b371947 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -262,15 +262,3 @@ AccountDetailScreen.prototype.transactionList = function () { AccountDetailScreen.prototype.requestAccountExport = function () { this.props.dispatch(actions.requestExportAccount()) } - - -AccountDetailScreen.prototype.buyButtonDeligator = function () { - var props = this.props - var selected = props.address || Object.keys(props.accounts)[0] - - if (this.props.accountDetail.subview === 'buyForm') { - props.dispatch(actions.backToAccountDetail(props.address)) - } else { - props.dispatch(actions.buyEthView(selected)) - } -} -- cgit v1.2.3 From c1df7dedd9065c8fe600642638e0e0a5d71e6f6e Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Sun, 23 Apr 2017 15:36:48 +0400 Subject: remove case buyForm --- ui/app/account-detail.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index d4b371947..d592a5ad6 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -16,7 +16,6 @@ const ExportAccountView = require('./components/account-export') const ethUtil = require('ethereumjs-util') const EditableLabel = require('./components/editable-label') const Tooltip = require('./components/tooltip') -const BuyButtonSubview = require('./components/buy-button-subview') module.exports = connect(mapStateToProps)(AccountDetailScreen) function mapStateToProps (state) { @@ -238,8 +237,6 @@ AccountDetailScreen.prototype.subview = function () { case 'export': var state = extend({key: 'export'}, this.props) return h(ExportAccountView, state) - case 'buyForm': - return h(BuyButtonSubview, extend({key: 'buyForm'}, this.props)) default: return this.transactionList() } -- cgit v1.2.3 From 5cabd3e02d0eeef8bd7c65db193b0bdb8cc9cc04 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Sun, 23 Apr 2017 21:18:14 +0400 Subject: remove updateBuyAddress action --- ui/app/actions.js | 9 --------- ui/app/components/coinbase-form.js | 4 +--- ui/app/components/shapeshift-form.js | 4 ---- ui/app/reducers/app.js | 13 ------------- 4 files changed, 1 insertion(+), 29 deletions(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index 8934299e7..f08a93ff4 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -136,8 +136,6 @@ var actions = { BUY_ETH_VIEW: 'BUY_ETH_VIEW', UPDATE_COINBASE_AMOUNT: 'UPDATE_COIBASE_AMOUNT', updateCoinBaseAmount: updateCoinBaseAmount, - UPDATE_BUY_ADDRESS: 'UPDATE_BUY_ADDRESS', - updateBuyAddress: updateBuyAddress, COINBASE_SUBVIEW: 'COINBASE_SUBVIEW', coinBaseSubview: coinBaseSubview, SHAPESHIFT_SUBVIEW: 'SHAPESHIFT_SUBVIEW', @@ -859,13 +857,6 @@ function updateCoinBaseAmount (value) { } } -function updateBuyAddress (value) { - return { - type: actions.UPDATE_BUY_ADDRESS, - value, - } -} - function coinBaseSubview () { return { type: actions.COINBASE_SUBVIEW, diff --git a/ui/app/components/coinbase-form.js b/ui/app/components/coinbase-form.js index b92799375..82d1458bf 100644 --- a/ui/app/components/coinbase-form.js +++ b/ui/app/components/coinbase-form.js @@ -48,9 +48,7 @@ CoinbaseForm.prototype.render = function () { CoinbaseForm.prototype.handleAmount = function (event) { this.props.dispatch(actions.updateCoinBaseAmount(event.target.value)) } -CoinbaseForm.prototype.handleAddress = function (event) { - this.props.dispatch(actions.updateBuyAddress(event.target.value)) -} + CoinbaseForm.prototype.toCoinbase = function () { const props = this.props const address = props.buyView.buyAddress diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js index 2745b1b11..f0a067c05 100644 --- a/ui/app/components/shapeshift-form.js +++ b/ui/app/components/shapeshift-form.js @@ -276,10 +276,6 @@ ShapeshiftForm.prototype.renderInfo = function () { ]) } -ShapeshiftForm.prototype.handleAddress = function (event) { - this.props.dispatch(actions.updateBuyAddress(event.target.value)) -} - ShapeshiftForm.prototype.activeToggle = function (elementType) { if (!this.props.buyView.formView.response || this.props.warning) return elementType return `${elementType}.inactive` diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 036286a8b..016ddb569 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -481,19 +481,6 @@ function reduceApp (state, action) { }, }) - case actions.UPDATE_BUY_ADDRESS: - return extend(appState, { - buyView: { - subview: appState.subview, - formView: { - coinbase: appState.buyView.formView.coinbase, - shapeshift: appState.buyView.formView.shapeshift, - }, - buyAddress: action.value, - amount: appState.buyView.amount, - }, - }) - case actions.UPDATE_COINBASE_AMOUNT: return extend(appState, { buyView: { -- cgit v1.2.3 From 7f12be6a014286d727766174bff9391b2cc55ae9 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 24 Apr 2017 12:18:54 +0400 Subject: remove updateCoinBaseAmount action --- ui/app/actions.js | 9 --------- ui/app/components/coinbase-form.js | 3 --- ui/app/reducers/app.js | 13 ------------- 3 files changed, 25 deletions(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index f08a93ff4..18f341411 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -134,8 +134,6 @@ var actions = { buyEth: buyEth, buyEthView: buyEthView, BUY_ETH_VIEW: 'BUY_ETH_VIEW', - UPDATE_COINBASE_AMOUNT: 'UPDATE_COIBASE_AMOUNT', - updateCoinBaseAmount: updateCoinBaseAmount, COINBASE_SUBVIEW: 'COINBASE_SUBVIEW', coinBaseSubview: coinBaseSubview, SHAPESHIFT_SUBVIEW: 'SHAPESHIFT_SUBVIEW', @@ -850,13 +848,6 @@ function buyEthView (address) { } } -function updateCoinBaseAmount (value) { - return { - type: actions.UPDATE_COINBASE_AMOUNT, - value, - } -} - function coinBaseSubview () { return { type: actions.COINBASE_SUBVIEW, diff --git a/ui/app/components/coinbase-form.js b/ui/app/components/coinbase-form.js index 82d1458bf..7ba8ca79e 100644 --- a/ui/app/components/coinbase-form.js +++ b/ui/app/components/coinbase-form.js @@ -45,9 +45,6 @@ CoinbaseForm.prototype.render = function () { ]), ]) } -CoinbaseForm.prototype.handleAmount = function (event) { - this.props.dispatch(actions.updateCoinBaseAmount(event.target.value)) -} CoinbaseForm.prototype.toCoinbase = function () { const props = this.props diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 016ddb569..324a4df35 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -481,19 +481,6 @@ function reduceApp (state, action) { }, }) - case actions.UPDATE_COINBASE_AMOUNT: - return extend(appState, { - buyView: { - subview: 'Coinbase', - formView: { - coinbase: true, - shapeshift: false, - }, - buyAddress: appState.buyView.buyAddress, - amount: action.value, - }, - }) - case actions.COINBASE_SUBVIEW: return extend(appState, { buyView: { -- cgit v1.2.3 From 1eda55c85a958eef7814fadd29b257c5fbf884e0 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Mon, 24 Apr 2017 12:35:45 +0200 Subject: Fix issue where stopPropagation didnt stop submitting the tx when clicking buy button --- ui/app/conf-tx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 3b8618992..770f79b19 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -141,7 +141,7 @@ function currentTxView (opts) { } ConfirmTxScreen.prototype.buyEth = function (address, event) { - this.stopPropagation(event) + event.preventDefault() this.props.dispatch(actions.buyEthView(address)) } -- cgit v1.2.3 From 9ebc5ed33cd3450262c00ccdde9f617544dfa784 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Mon, 24 Apr 2017 12:36:17 +0200 Subject: make buy button green --- ui/app/components/pending-tx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 1b83f5043..4b28ae099 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -289,7 +289,7 @@ PendingTx.prototype.render = function () { insufficientBalance ? - h('button', { + h('button.btn-green', { onClick: props.buyEth, }, 'Buy Ether') : null, -- cgit v1.2.3 From df9e40be636738336d6fa3c775bbcc99a9a0e210 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Mon, 24 Apr 2017 12:58:01 +0200 Subject: Css fixes --- ui/app/components/coinbase-form.js | 6 +++--- ui/app/components/shapeshift-form.js | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/ui/app/components/coinbase-form.js b/ui/app/components/coinbase-form.js index 7ba8ca79e..f44d86045 100644 --- a/ui/app/components/coinbase-form.js +++ b/ui/app/components/coinbase-form.js @@ -23,7 +23,7 @@ CoinbaseForm.prototype.render = function () { return h('.flex-column', { style: { - // margin: '10px', + marginTop: '35px', padding: '25px', width: '100%', }, @@ -35,11 +35,11 @@ CoinbaseForm.prototype.render = function () { marginTop: '0px', }, }, [ - h('button', { + h('button.btn-green', { onClick: this.toCoinbase.bind(this), }, 'Continue to Coinbase'), - h('button', { + h('button.btn-red', { onClick: () => props.dispatch(actions.backTobuyView(props.accounts.address)), }, 'Cancel'), ]), diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js index f0a067c05..e0a720426 100644 --- a/ui/app/components/shapeshift-form.js +++ b/ui/app/components/shapeshift-form.js @@ -70,6 +70,7 @@ ShapeshiftForm.prototype.renderMain = function () { h('input#fromCoin.buy-inputs.ex-coins', { type: 'text', list: 'coinList', + autoFocus: true, dataset: { persistentFormId: 'input-coin', }, @@ -86,6 +87,7 @@ ShapeshiftForm.prototype.renderMain = function () { style: { fontSize: '12px', color: '#F7861C', + position: 'relative', bottom: '48px', left: '106px', }, @@ -156,8 +158,8 @@ ShapeshiftForm.prototype.renderMain = function () { }, style: { boxSizing: 'border-box', - width: '278px', - height: '20px', + width: '227px', + height: '30px', padding: ' 5px ', }, }), @@ -167,7 +169,7 @@ ShapeshiftForm.prototype.renderMain = function () { fontSize: '12px', color: '#F7861C', position: 'relative', - bottom: '5px', + bottom: '10px', right: '11px', }, }), @@ -181,7 +183,7 @@ ShapeshiftForm.prototype.renderMain = function () { style: { marginTop: '10px', position: 'relative', - bottom: '33px', + bottom: '40px', }, }, 'Submit'), -- cgit v1.2.3 From 79f88398acd116980fe91d4c56a1ec6a15672745 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Mon, 24 Apr 2017 20:56:31 +0200 Subject: fix spelling --- ui/app/components/buy-button-subview.js | 2 +- ui/app/components/custom-radio-list.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/app/components/buy-button-subview.js b/ui/app/components/buy-button-subview.js index 8d3e9aa21..191f46319 100644 --- a/ui/app/components/buy-button-subview.js +++ b/ui/app/components/buy-button-subview.js @@ -104,7 +104,7 @@ BuyButtonSubview.prototype.render = function () { }, [ h(RadioList, { defaultFocus: props.buyView.subview, - lables: [ + labels: [ 'Coinbase', 'ShapeShift', ], diff --git a/ui/app/components/custom-radio-list.js b/ui/app/components/custom-radio-list.js index a19287630..a4c525396 100644 --- a/ui/app/components/custom-radio-list.js +++ b/ui/app/components/custom-radio-list.js @@ -14,7 +14,7 @@ RadioList.prototype.render = function () { const activeClass = '.custom-radio-selected' const inactiveClass = '.custom-radio-inactive' const { - lables, + labels, defaultFocus, } = props @@ -30,7 +30,7 @@ RadioList.prototype.render = function () { marginRight: '5px', }, }, - lables.map((lable, i) => { + labels.map((lable, i) => { let isSelcted = (this.state !== null) isSelcted = isSelcted ? (this.state.selected === lable) : (defaultFocus === lable) return h(isSelcted ? activeClass : inactiveClass, { @@ -43,7 +43,7 @@ RadioList.prototype.render = function () { }) ), h('.text', {}, - lables.map((lable) => { + labels.map((lable) => { if (props.subtext) { return h('.flex-row', {}, [ h('.radio-titles', lable), -- cgit v1.2.3 From bce4af2dcaeeab3bd931afbbcc6f17da675ce2b6 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 24 Apr 2017 13:55:19 -0700 Subject: Add placeholder etherscan token icons --- ui/app/components/token-cell.js | 4 +++- ui/app/components/token-list.js | 8 +++++++- ui/lib/icon-factory.js | 26 +++++++++++++++++++++++++- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js index 81e92b301..879dc01d1 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/token-cell.js @@ -12,7 +12,7 @@ function TokenCell () { TokenCell.prototype.render = function () { const props = this.props - const { address, symbol, string } = props + const { address, symbol, string, network } = props log.info({ address, symbol, string }) return ( @@ -21,9 +21,11 @@ TokenCell.prototype.render = function () { h(Identicon, { diameter: 50, address, + network, }), h('h3', `${string || 0} ${symbol}`), ]) ) } + diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index c6a7d3552..6589dea62 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -23,9 +23,10 @@ function TokenList () { TokenList.prototype.render = function () { const tokens = this.state.tokens + const network = this.props.network const tokenViews = tokens.map((tokenData) => { - console.log('rendering token with', tokenData) + tokenData.network = network return h(TokenCell, tokenData) }) @@ -43,6 +44,11 @@ TokenList.prototype.render = function () { margin-left: 12px; } + li.token-cell:hover { + background: white; + cursor: pointer; + } + `)].concat(tokenViews)) ) } diff --git a/ui/lib/icon-factory.js b/ui/lib/icon-factory.js index 82cc839d6..ac703ae40 100644 --- a/ui/lib/icon-factory.js +++ b/ui/lib/icon-factory.js @@ -10,9 +10,33 @@ module.exports = function (jazzicon) { function IconFactory (jazzicon) { this.jazzicon = jazzicon this.cache = {} + + this.presets = { + '1':{ // Main network: + '0x48c80f1f4d53d5951e5d5438b54cba84f29f32a5': 'https://etherscan.io/token/images/augur.png', + '0xc66ea802717bfb9833400264dd12c2bceaa34a6d': 'https://etherscan.io/token/images/mkr-etherscan-35.png', + '0xa74476443119a942de498590fe1f2454d7d4ac0d': 'https://etherscan.io/token/images/golem.png', + '0xaec2e87e0a235266d9c5adc9deb4b2e29b54d009': 'https://etherscan.io/token/images/sngls.png', + + } + } } -IconFactory.prototype.iconForAddress = function (address, diameter, imageify) { +IconFactory.prototype.iconForAddress = function (address, diameter, imageify, network) { + + try { + const presetUri = this.presets[network][address.toLowerCase()] + if (presetUri) { + var img = document.createElement('img') + img.src = presetUri + img.style.width = `${diameter}px` + img.style.height = `${diameter}px` + img.style.borderRadius = `${diameter/2}px` + return img + } + } catch (e) {} + + if (imageify) { return this.generateIdenticonImg(address, diameter) } else { -- cgit v1.2.3 From d05d9a5f57b9311d6f29539233f9065330e8bda4 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 24 Apr 2017 13:55:33 -0700 Subject: Add missing tx manager state --- app/scripts/transaction-manager.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/scripts/transaction-manager.js b/app/scripts/transaction-manager.js index d7051b2cb..22d807748 100644 --- a/app/scripts/transaction-manager.js +++ b/app/scripts/transaction-manager.js @@ -373,6 +373,7 @@ module.exports = class TransactionManager extends EventEmitter { // - `'signed'` the tx is signed // - `'submitted'` the tx is sent to a server // - `'confirmed'` the tx has been included in a block. + // - `'failed'` the tx failed for some reason, included on tx data. _setTxStatus (txId, status) { var txMeta = this.getTx(txId) txMeta.status = status -- cgit v1.2.3 From c3746e62ec50de87a85bd2af0bfaf66fb19fbc1b Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 24 Apr 2017 13:58:46 -0700 Subject: Version 3.5.3 --- CHANGELOG.md | 4 ++++ app/manifest.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd754cadd..1a32b8138 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,12 @@ ## Current Master +## 3.5.3 2017-4-24 + - Popup new transactions in Firefox. - Fix transition issue from account detail screen. +- Revise buy screen for more modularity. +- Fixed some other small bugs. ## 3.5.2 2017-3-28 diff --git a/app/manifest.json b/app/manifest.json index a3242149b..aeb47dfe3 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.5.2", + "version": "3.5.3", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From 3ae2a829956941d7703be714650d85a989c1b488 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 24 Apr 2017 18:49:23 -0700 Subject: Bump provider engine Should now pass test suite, and include several sweet recent fixes! --- CHANGELOG.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a32b8138..dbe63083c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Current Master +- Fix occasional nonce tracking issue. +- Fix bug where some events would not be emitted by web3. + ## 3.5.3 2017-4-24 - Popup new transactions in Firefox. diff --git a/package.json b/package.json index b892653fa..e3c222734 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,7 @@ "valid-url": "^1.0.9", "vreme": "^3.0.2", "web3": "0.18.2", - "web3-provider-engine": "^11.0.2", + "web3-provider-engine": "^12.0.1", "web3-stream-provider": "^2.0.6", "xtend": "^4.0.1" }, -- cgit v1.2.3 From 292e2dca83ff7b300aeb680d6470ab827668b1db Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 24 Apr 2017 20:59:59 -0700 Subject: Bump provider-engine --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e3c222734..8aa449c8d 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,7 @@ "valid-url": "^1.0.9", "vreme": "^3.0.2", "web3": "0.18.2", - "web3-provider-engine": "^12.0.1", + "web3-provider-engine": "^12.0.2", "web3-stream-provider": "^2.0.6", "xtend": "^4.0.1" }, -- cgit v1.2.3 From 04e489f4df06b13f2bdd76d815f742b98bee7a37 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 25 Apr 2017 10:19:26 -0700 Subject: Allow signature V values over 1 byte By bumping ethereumjs-tx. --- CHANGELOG.md | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbe63083c..fb28dc9c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Fix occasional nonce tracking issue. - Fix bug where some events would not be emitted by web3. +- Fix bug where an error would be thrown when composing signatures for networks with large ID values. ## 3.5.3 2017-4-24 diff --git a/package.json b/package.json index 8aa449c8d..157dbda65 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "eth-query": "^1.0.3", "eth-sig-util": "^1.1.1", "eth-simple-keyring": "^1.1.1", - "ethereumjs-tx": "^1.2.5", + "ethereumjs-tx": "^1.3.0", "ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", "ethjs-ens": "^1.0.2", -- cgit v1.2.3 From f1beb0720a0964e45a71b473f173f62c6abdac6e Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 25 Apr 2017 10:55:00 -0700 Subject: Version 3.5.4 --- CHANGELOG.md | 2 ++ app/manifest.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb28dc9c0..b855fefe0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +## 3.5.4 2017-4-25 + - Fix occasional nonce tracking issue. - Fix bug where some events would not be emitted by web3. - Fix bug where an error would be thrown when composing signatures for networks with large ID values. diff --git a/app/manifest.json b/app/manifest.json index aeb47dfe3..6ef428d2c 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.5.3", + "version": "3.5.4", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From e45d5c858e10db774d40a09704d38b65128ef821 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 25 Apr 2017 12:49:24 -0700 Subject: mascara - docker - bump to node7 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d06f5377b..be0a328fe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:6 +FROM node:7 MAINTAINER kumavis # setup app dir -- cgit v1.2.3 From e9aa37b699a105019384cbde88a114965ff1e2cd Mon Sep 17 00:00:00 2001 From: Nickyg Date: Wed, 26 Apr 2017 01:40:33 +0530 Subject: add rinkeby network --- app/scripts/config.js | 2 ++ app/scripts/lib/config-manager.js | 5 +++++ ui/app/app.js | 9 +++++++++ ui/app/components/drop-menu-item.js | 3 +++ ui/app/components/network.js | 5 ++++- ui/app/config.js | 5 +++++ 6 files changed, 28 insertions(+), 1 deletion(-) diff --git a/app/scripts/config.js b/app/scripts/config.js index ec421744d..4f62268e1 100644 --- a/app/scripts/config.js +++ b/app/scripts/config.js @@ -1,6 +1,7 @@ const MAINET_RPC_URL = 'https://mainnet.infura.io/metamask' const TESTNET_RPC_URL = 'https://ropsten.infura.io/metamask' const KOVAN_RPC_URL = 'https://kovan.infura.io/metamask' +const RINKEBY_RPC_URL = 'https://rinkeby.infura.io' const DEFAULT_RPC_URL = TESTNET_RPC_URL global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' @@ -12,5 +13,6 @@ module.exports = { testnet: TESTNET_RPC_URL, morden: TESTNET_RPC_URL, kovan: KOVAN_RPC_URL, + rinkeby: RINKEBY_RPC_URL, }, } diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index e31cb45ed..340ad4292 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -6,6 +6,8 @@ const TESTNET_RPC = MetamaskConfig.network.testnet const MAINNET_RPC = MetamaskConfig.network.mainnet const MORDEN_RPC = MetamaskConfig.network.morden const KOVAN_RPC = MetamaskConfig.network.kovan +const RINKEBY_RPC = MetamaskConfig.network.rinkeby + /* The config-manager is a convenience object * wrapping a pojo-migrator. @@ -153,6 +155,9 @@ ConfigManager.prototype.getCurrentRpcAddress = function () { case 'kovan': return KOVAN_RPC + + case 'rinkeby': + return RINKEBY_RPC default: return provider && provider.rpcTarget ? provider.rpcTarget : TESTNET_RPC diff --git a/ui/app/app.js b/ui/app/app.js index 5a7596aca..4ae40b659 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -264,6 +264,15 @@ App.prototype.renderNetworkDropdown = function () { provider: props.provider, }), + h(DropMenuItem, { + label: 'Rinkeby Test Network', + closeMenu: () => this.setState({ isNetworkMenuOpen: false}), + action: () => props.dispatch(actions.setProviderType('rinkeby')), + icon: h('.menu-icon.hollow-diamond'), + activeNetworkRender: props.network, + provider: props.provider, + }), + h(DropMenuItem, { label: 'Localhost 8545', closeMenu: () => this.setState({ isNetworkMenuOpen: false }), diff --git a/ui/app/components/drop-menu-item.js b/ui/app/components/drop-menu-item.js index 3eb6ec876..bd9d8f597 100644 --- a/ui/app/components/drop-menu-item.js +++ b/ui/app/components/drop-menu-item.js @@ -47,6 +47,9 @@ DropMenuItem.prototype.activeNetworkRender = function () { case 'Kovan Test Network': if (providerType === 'kovan') return h('.check', '✓') break + case 'Rinkeby Test Network': + if (providerType === 'rinkeby') return h('.check', '✓') + break case 'Localhost 8545': if (activeNetwork === 'http://localhost:8545') return h('.check', '✓') break diff --git a/ui/app/components/network.js b/ui/app/components/network.js index d9045167f..f0fc14454 100644 --- a/ui/app/components/network.js +++ b/ui/app/components/network.js @@ -43,7 +43,10 @@ Network.prototype.render = function () { } else if (providerName === 'kovan') { hoverText = 'Kovan Test Network' iconName = 'kovan-test-network' - } else { + } else if (providerName === 'rinkeby') { + hoverText = 'Rinkeby Test Network' + iconName = 'unknown-private-network' + }else { hoverText = 'Unknown Private Network' iconName = 'unknown-private-network' } diff --git a/ui/app/config.js b/ui/app/config.js index 444365de2..26cfe663f 100644 --- a/ui/app/config.js +++ b/ui/app/config.js @@ -166,6 +166,11 @@ function currentProviderDisplay (metamaskState) { value = 'Kovan Test Network' break + case 'rinkeby': + title = 'Current Network' + value = 'Rinkeby Test Network' + break + default: title = 'Current RPC' value = metamaskState.provider.rpcTarget -- cgit v1.2.3 From d764e46a500d0dc156a1da86498b751d23c94747 Mon Sep 17 00:00:00 2001 From: Nickyg Date: Wed, 26 Apr 2017 02:15:15 +0530 Subject: change network name to rinkeby when selected --- ui/app/components/network.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ui/app/components/network.js b/ui/app/components/network.js index f0fc14454..94704b1bc 100644 --- a/ui/app/components/network.js +++ b/ui/app/components/network.js @@ -45,7 +45,7 @@ Network.prototype.render = function () { iconName = 'kovan-test-network' } else if (providerName === 'rinkeby') { hoverText = 'Rinkeby Test Network' - iconName = 'unknown-private-network' + iconName = 'rinkeby-test-network' }else { hoverText = 'Unknown Private Network' iconName = 'unknown-private-network' @@ -85,6 +85,15 @@ Network.prototype.render = function () { }}, 'Kovan Test Net'), ]) + case 'rinkeby-test-network': + return h('.network-indicator', [ + h('.menu-icon.hollow-diamond'), + h('.network-name', { + style: { + color: '#550077', + }}, + 'Rinkeby Test Net'), + ]) default: return h('.network-indicator', [ h('i.fa.fa-question-circle.fa-lg', { -- cgit v1.2.3 From 4d42cfaa25366ec4f0f30018a8d5213d70205fb8 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 25 Apr 2017 14:04:51 -0700 Subject: build - make gulp return non-zero error code on bundle fail --- gulpfile.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index fe223adf1..21b925780 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -296,8 +296,6 @@ function bundleTask(opts) { return ( bundler.bundle() - // log errors if they happen - .on('error', gutil.log.bind(gutil, 'Browserify Error')) // convert bundle stream to gulp vinyl stream .pipe(source(opts.filename)) // inject variables into bundle -- cgit v1.2.3 From 242dc1e99f1dd53e2bec9deefb5da0c8329b5f00 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Tue, 25 Apr 2017 14:39:01 -0700 Subject: Add missing changes. Create unique style for rinkeby icon. --- app/scripts/config.js | 2 +- app/scripts/lib/buy-eth-url.js | 6 +++++- ui/app/app.js | 2 +- ui/app/components/buy-button-subview.js | 8 +++++++- ui/app/components/network.js | 2 +- ui/app/components/transaction-list-item.js | 2 +- ui/app/css/lib.css | 4 ++++ ui/lib/account-link.js | 3 +++ ui/lib/explorer-link.js | 3 +++ 9 files changed, 26 insertions(+), 6 deletions(-) diff --git a/app/scripts/config.js b/app/scripts/config.js index 4f62268e1..391c67230 100644 --- a/app/scripts/config.js +++ b/app/scripts/config.js @@ -1,7 +1,7 @@ const MAINET_RPC_URL = 'https://mainnet.infura.io/metamask' const TESTNET_RPC_URL = 'https://ropsten.infura.io/metamask' const KOVAN_RPC_URL = 'https://kovan.infura.io/metamask' -const RINKEBY_RPC_URL = 'https://rinkeby.infura.io' +const RINKEBY_RPC_URL = 'https://rinkeby.infura.io/metamask' const DEFAULT_RPC_URL = TESTNET_RPC_URL global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' diff --git a/app/scripts/lib/buy-eth-url.js b/app/scripts/lib/buy-eth-url.js index 91a1ec322..957a00211 100644 --- a/app/scripts/lib/buy-eth-url.js +++ b/app/scripts/lib/buy-eth-url.js @@ -11,9 +11,13 @@ function getBuyEthUrl({ network, amount, address }){ url = 'https://faucet.metamask.io/' break + case '4': + url = 'https://www.rinkeby.io/' + break + case '42': url = 'https://github.com/kovan-testnet/faucet' break } return url -} \ No newline at end of file +} diff --git a/ui/app/app.js b/ui/app/app.js index 4ae40b659..156490914 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -268,7 +268,7 @@ App.prototype.renderNetworkDropdown = function () { label: 'Rinkeby Test Network', closeMenu: () => this.setState({ isNetworkMenuOpen: false}), action: () => props.dispatch(actions.setProviderType('rinkeby')), - icon: h('.menu-icon.hollow-diamond'), + icon: h('.menu-icon.golden-square'), activeNetworkRender: props.network, provider: props.provider, }), diff --git a/ui/app/components/buy-button-subview.js b/ui/app/components/buy-button-subview.js index 191f46319..87084f92d 100644 --- a/ui/app/components/buy-button-subview.js +++ b/ui/app/components/buy-button-subview.js @@ -152,13 +152,19 @@ BuyButtonSubview.prototype.formVersionSubview = function () { marginBottom: '15px', }, }, 'In order to access this feature, please switch to the Main Network'), - ((network === '3') || (network === '42')) ? h('h3.text-transform-uppercase', 'or go to the') : null, + ((network === '3') || (network === '4') || (network === '42')) ? h('h3.text-transform-uppercase', 'or go to the') : null, (network === '3') ? h('button.text-transform-uppercase', { onClick: () => this.props.dispatch(actions.buyEth({ network })), style: { marginTop: '15px', }, }, 'Ropsten Test Faucet') : null, + (network === '4') ? h('button.text-transform-uppercase', { + onClick: () => this.props.dispatch(actions.buyEth({ network })), + style: { + marginTop: '15px', + }, + }, 'Rinkeby Test Faucet') : null, (network === '42') ? h('button.text-transform-uppercase', { onClick: () => this.props.dispatch(actions.buyEth({ network })), style: { diff --git a/ui/app/components/network.js b/ui/app/components/network.js index 94704b1bc..e8065cf00 100644 --- a/ui/app/components/network.js +++ b/ui/app/components/network.js @@ -87,7 +87,7 @@ Network.prototype.render = function () { ]) case 'rinkeby-test-network': return h('.network-indicator', [ - h('.menu-icon.hollow-diamond'), + h('.menu-icon.golden-square'), h('.network-name', { style: { color: '#550077', diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js index 9fef52355..6f4dfae2d 100644 --- a/ui/app/components/transaction-list-item.js +++ b/ui/app/components/transaction-list-item.js @@ -27,7 +27,7 @@ TransactionListItem.prototype.render = function () { let isLinkable = false const numericNet = parseInt(network) - isLinkable = numericNet === 1 || numericNet === 3 || numericNet === 42 + isLinkable = numericNet === 1 || numericNet === 3 || numericNet === 4 || numericNet === 42 var isMsg = ('msgParams' in transaction) var isTx = ('txParams' in transaction) diff --git a/ui/app/css/lib.css b/ui/app/css/lib.css index 670dc9fd0..910a24ee2 100644 --- a/ui/app/css/lib.css +++ b/ui/app/css/lib.css @@ -191,6 +191,10 @@ hr.horizontal-line { border: 3px solid #690496; } +.golden-square { + background: #EBB33F; +} + .pending-dot { background: red; left: 14px; diff --git a/ui/lib/account-link.js b/ui/lib/account-link.js index 4f27b35c0..d061d0ad1 100644 --- a/ui/lib/account-link.js +++ b/ui/lib/account-link.js @@ -11,6 +11,9 @@ module.exports = function (address, network) { case 3: // ropsten test net link = `http://ropsten.etherscan.io/address/${address}` break + case 4: // rinkeby test net + link = `http://rinkeby.etherscan.io/address/${address}` + break case 42: // kovan test net link = `http://kovan.etherscan.io/address/${address}` break diff --git a/ui/lib/explorer-link.js b/ui/lib/explorer-link.js index ca89f8b25..e11249551 100644 --- a/ui/lib/explorer-link.js +++ b/ui/lib/explorer-link.js @@ -8,6 +8,9 @@ module.exports = function (hash, network) { case 3: // ropsten test net prefix = 'ropsten.' break + case 4: // rinkeby test net + prefix = 'rinkeby.' + break case 42: // kovan test net prefix = 'kovan.' break -- cgit v1.2.3 From f2bf7326ccc87fa944307f0b1ffd7ca51621e53c Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Tue, 25 Apr 2017 14:44:25 -0700 Subject: Linting. --- ui/app/app.js | 2 +- ui/app/components/network.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index 156490914..f3661568f 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -264,7 +264,7 @@ App.prototype.renderNetworkDropdown = function () { provider: props.provider, }), - h(DropMenuItem, { + h(DropMenuItem, { label: 'Rinkeby Test Network', closeMenu: () => this.setState({ isNetworkMenuOpen: false}), action: () => props.dispatch(actions.setProviderType('rinkeby')), diff --git a/ui/app/components/network.js b/ui/app/components/network.js index e8065cf00..f7ea8c49e 100644 --- a/ui/app/components/network.js +++ b/ui/app/components/network.js @@ -46,7 +46,7 @@ Network.prototype.render = function () { } else if (providerName === 'rinkeby') { hoverText = 'Rinkeby Test Network' iconName = 'rinkeby-test-network' - }else { + } else { hoverText = 'Unknown Private Network' iconName = 'unknown-private-network' } -- cgit v1.2.3 From 09034772d0536b6dd0a2c591986059b3eaba221d Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Tue, 25 Apr 2017 14:57:07 -0700 Subject: Add (vague) instructions to adding a new network to the dropdown. --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 496b5423f..821e1cdfd 100644 --- a/README.md +++ b/README.md @@ -168,3 +168,28 @@ To delete a notice: npm run deleteNotice ``` A list of active notices will pop up. Enter the corresponding id in the command line prompt and add and commit the new changes afterwards. + +## Adding Custom Networks + +To add another network to our dropdown menu, make sure the following files are adjusted properly: + +``` +app/scripts/config.js +app/scripts/lib/buy-eth-url.js +app/scripts/lib/config-manager.js +ui/app/app.js +ui/app/components/buy-button-subview.js +ui/app/components/drop-menu-item.js +ui/app/components/network.js +ui/app/components/transaction-list-item.js +ui/app/config.js +ui/app/css/lib.css +ui/lib/account-link.js +ui/lib/explorer-link.js +``` + +You will need: ++ The network ID ++ An RPC Endpoint url ++ An explorer link ++ CSS for the display icon -- cgit v1.2.3 From b0919ba7296553200161913ab413f214aa58e741 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Tue, 25 Apr 2017 14:57:35 -0700 Subject: Add to changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb28dc9c0..8a093a9e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Fix occasional nonce tracking issue. - Fix bug where some events would not be emitted by web3. - Fix bug where an error would be thrown when composing signatures for networks with large ID values. +- Add Rinkeby Test Network to our network list. ## 3.5.3 2017-4-24 -- cgit v1.2.3 From 6bdb4c87288a522d9ea2e984bc1f6436d6c7369a Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Wed, 26 Apr 2017 21:05:45 -0700 Subject: Fix linting warnings --- app/scripts/account-import-strategies/index.js | 2 +- app/scripts/background.js | 12 ++-- app/scripts/contentscript.js | 2 +- app/scripts/controllers/address-book.js | 8 +-- app/scripts/controllers/currency.js | 8 ++- app/scripts/controllers/preferences.js | 8 +-- app/scripts/first-time-state.js | 2 +- app/scripts/keyring-controller.js | 4 +- app/scripts/lib/buy-eth-url.js | 4 +- app/scripts/lib/eth-store.js | 2 +- app/scripts/lib/inpage-provider.js | 4 +- app/scripts/lib/message-manager.js | 4 +- app/scripts/lib/migrator/index.js | 12 ++-- app/scripts/lib/notification-manager.js | 2 +- app/scripts/lib/personal-message-manager.js | 4 +- app/scripts/lib/tx-utils.js | 12 ++-- app/scripts/metamask-controller.js | 77 ++++++++++++------------- app/scripts/migrations/002.js | 2 +- app/scripts/migrations/003.js | 2 +- app/scripts/migrations/004.js | 2 +- app/scripts/migrations/005.js | 2 +- app/scripts/migrations/006.js | 2 +- app/scripts/migrations/007.js | 2 +- app/scripts/migrations/008.js | 2 +- app/scripts/migrations/009.js | 2 +- app/scripts/migrations/010.js | 2 +- app/scripts/migrations/011.js | 2 +- app/scripts/migrations/012.js | 2 +- app/scripts/migrations/_multi-keyring.js | 11 ++-- app/scripts/popup-core.js | 1 - app/scripts/popup.js | 2 +- app/scripts/transaction-manager.js | 28 ++++----- ui/app/accounts/import/index.js | 2 +- ui/app/actions.js | 6 +- ui/app/app.js | 1 - ui/app/components/ens-input.js | 6 +- ui/app/components/notice.js | 5 +- ui/app/components/transaction-list-item-icon.js | 2 +- ui/app/components/transaction-list-item.js | 2 - ui/app/conf-tx.js | 2 - ui/app/reducers/app.js | 2 +- 41 files changed, 124 insertions(+), 135 deletions(-) diff --git a/app/scripts/account-import-strategies/index.js b/app/scripts/account-import-strategies/index.js index d5124eb7f..96e2b5912 100644 --- a/app/scripts/account-import-strategies/index.js +++ b/app/scripts/account-import-strategies/index.js @@ -4,7 +4,7 @@ const ethUtil = require('ethereumjs-util') const accountImporter = { - importAccount(strategy, args) { + importAccount (strategy, args) { try { const importer = this.strategies[strategy] const privateKeyHex = importer.apply(null, args) diff --git a/app/scripts/background.js b/app/scripts/background.js index 7211f1e0c..58f8e7556 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -41,10 +41,10 @@ asyncQ.waterfall([ // State and Persistence // -function loadStateFromPersistence() { +function loadStateFromPersistence () { // migrations - let migrator = new Migrator({ migrations }) - let initialState = migrator.generateInitialState(firstTimeState) + const migrator = new Migrator({ migrations }) + const initialState = migrator.generateInitialState(firstTimeState) return asyncQ.waterfall([ // read from disk () => Promise.resolve(diskStore.getState() || initialState), @@ -61,7 +61,6 @@ function loadStateFromPersistence() { } function setupController (initState) { - // // MetaMask Controller // @@ -85,8 +84,8 @@ function setupController (initState) { diskStore ) - function versionifyData(state) { - let versionedData = diskStore.getState() + function versionifyData (state) { + const versionedData = diskStore.getState() versionedData.data = state return versionedData } @@ -138,7 +137,6 @@ function setupController (initState) { } return Promise.resolve() - } // diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 4d7e682d3..f7237b32e 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -77,7 +77,7 @@ function doctypeCheck () { } } -function suffixCheck() { +function suffixCheck () { var prohibitedTypes = ['xml', 'pdf'] var currentUrl = window.location.href var currentRegex diff --git a/app/scripts/controllers/address-book.js b/app/scripts/controllers/address-book.js index c66eb2bd4..6fb4ee114 100644 --- a/app/scripts/controllers/address-book.js +++ b/app/scripts/controllers/address-book.js @@ -39,11 +39,11 @@ class AddressBookController { // pushed object is an object of two fields. Current behavior does not set an // upper limit to the number of addresses. _addToAddressBook (address, name) { - let addressBook = this._getAddressBook() - let identities = this._getIdentities() + const addressBook = this._getAddressBook() + const identities = this._getIdentities() - let addressBookIndex = addressBook.findIndex((element) => { return element.address.toLowerCase() === address.toLowerCase() || element.name === name }) - let identitiesIndex = Object.keys(identities).findIndex((element) => { return element.toLowerCase() === address.toLowerCase() }) + const addressBookIndex = addressBook.findIndex((element) => { return element.address.toLowerCase() === address.toLowerCase() || element.name === name }) + const identitiesIndex = Object.keys(identities).findIndex((element) => { return element.toLowerCase() === address.toLowerCase() }) // trigger this condition if we own this address--no need to overwrite. if (identitiesIndex !== -1) { return Promise.resolve(addressBook) diff --git a/app/scripts/controllers/currency.js b/app/scripts/controllers/currency.js index c4904f8ac..fb130ed76 100644 --- a/app/scripts/controllers/currency.js +++ b/app/scripts/controllers/currency.js @@ -51,9 +51,11 @@ class CurrencyController { this.setConversionRate(Number(parsedResponse.ticker.price)) this.setConversionDate(Number(parsedResponse.timestamp)) }).catch((err) => { - console.warn('MetaMask - Failed to query currency conversion.') - this.setConversionRate(0) - this.setConversionDate('N/A') + if (err) { + console.warn('MetaMask - Failed to query currency conversion.') + this.setConversionRate(0) + this.setConversionDate('N/A') + } }) } diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index c7f675a41..7212c7c43 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -36,8 +36,8 @@ class PreferencesController { } addToFrequentRpcList (_url) { - let rpcList = this.getFrequentRpcList() - let index = rpcList.findIndex((element) => { return element === _url }) + const rpcList = this.getFrequentRpcList() + const index = rpcList.findIndex((element) => { return element === _url }) if (index !== -1) { rpcList.splice(index, 1) } @@ -53,13 +53,9 @@ class PreferencesController { getFrequentRpcList () { return this.store.getState().frequentRpcList } - // // PRIVATE METHODS // - - - } module.exports = PreferencesController diff --git a/app/scripts/first-time-state.js b/app/scripts/first-time-state.js index 3196981ba..87a7bb7b5 100644 --- a/app/scripts/first-time-state.js +++ b/app/scripts/first-time-state.js @@ -8,4 +8,4 @@ module.exports = { type: 'testnet', }, }, -} \ No newline at end of file +} diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js index 16df6efa6..5b3c80e40 100644 --- a/app/scripts/keyring-controller.js +++ b/app/scripts/keyring-controller.js @@ -187,7 +187,7 @@ class KeyringController extends EventEmitter { .then((accounts) => { switch (type) { case 'Simple Key Pair': - let isNotIncluded = !accounts.find((key) => key === newAccount[0] || key === ethUtil.stripHexPrefix(newAccount[0])) + const isNotIncluded = !accounts.find((key) => key === newAccount[0] || key === ethUtil.stripHexPrefix(newAccount[0])) return (isNotIncluded) ? Promise.resolve(newAccount) : Promise.reject(new Error('The account you\'re are trying to import is a duplicate')) default: return Promise.resolve(newAccount) @@ -582,7 +582,7 @@ class KeyringController extends EventEmitter { }) } - _updateMemStoreKeyrings() { + _updateMemStoreKeyrings () { Promise.all(this.keyrings.map(this.displayForKeyring)) .then((keyrings) => { this.memStore.updateState({ keyrings }) diff --git a/app/scripts/lib/buy-eth-url.js b/app/scripts/lib/buy-eth-url.js index 91a1ec322..30db78a6c 100644 --- a/app/scripts/lib/buy-eth-url.js +++ b/app/scripts/lib/buy-eth-url.js @@ -1,6 +1,6 @@ module.exports = getBuyEthUrl -function getBuyEthUrl({ network, amount, address }){ +function getBuyEthUrl ({ network, amount, address }) { let url switch (network) { case '1': @@ -16,4 +16,4 @@ function getBuyEthUrl({ network, amount, address }){ break } return url -} \ No newline at end of file +} diff --git a/app/scripts/lib/eth-store.js b/app/scripts/lib/eth-store.js index 243253df2..6f04a9dd6 100644 --- a/app/scripts/lib/eth-store.js +++ b/app/scripts/lib/eth-store.js @@ -10,7 +10,7 @@ const async = require('async') const EthQuery = require('eth-query') const ObservableStore = require('obs-store') -function noop() {} +function noop () {} class EthereumStore extends ObservableStore { diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js index 92936de2f..e5e398e24 100644 --- a/app/scripts/lib/inpage-provider.js +++ b/app/scripts/lib/inpage-provider.js @@ -85,7 +85,7 @@ MetamaskInpageProvider.prototype.send = function (payload) { break case 'net_version': - let networkVersion = self.publicConfigStore.getState().networkVersion + const networkVersion = self.publicConfigStore.getState().networkVersion result = networkVersion break @@ -125,7 +125,7 @@ function eachJsonMessage (payload, transformFn) { } } -function logStreamDisconnectWarning(remoteLabel, err){ +function logStreamDisconnectWarning (remoteLabel, err) { let warningMsg = `MetamaskInpageProvider - lost connection to ${remoteLabel}` if (err) warningMsg += '\n' + err.stack console.warn(warningMsg) diff --git a/app/scripts/lib/message-manager.js b/app/scripts/lib/message-manager.js index 711d5f159..f52e048e0 100644 --- a/app/scripts/lib/message-manager.js +++ b/app/scripts/lib/message-manager.js @@ -4,7 +4,7 @@ const ethUtil = require('ethereumjs-util') const createId = require('./random-id') -module.exports = class MessageManager extends EventEmitter{ +module.exports = class MessageManager extends EventEmitter { constructor (opts) { super() this.memStore = new ObservableStore({ @@ -108,7 +108,7 @@ module.exports = class MessageManager extends EventEmitter{ } -function normalizeMsgData(data) { +function normalizeMsgData (data) { if (data.slice(0, 2) === '0x') { // data is already hex return data diff --git a/app/scripts/lib/migrator/index.js b/app/scripts/lib/migrator/index.js index 312345263..c40c347b5 100644 --- a/app/scripts/lib/migrator/index.js +++ b/app/scripts/lib/migrator/index.js @@ -3,17 +3,17 @@ const asyncQ = require('async-q') class Migrator { constructor (opts = {}) { - let migrations = opts.migrations || [] + const migrations = opts.migrations || [] this.migrations = migrations.sort((a, b) => a.version - b.version) - let lastMigration = this.migrations.slice(-1)[0] + const lastMigration = this.migrations.slice(-1)[0] // use specified defaultVersion or highest migration version this.defaultVersion = opts.defaultVersion || (lastMigration && lastMigration.version) || 0 } // run all pending migrations on meta in place migrateData (versionedData = this.generateInitialState()) { - let remaining = this.migrations.filter(migrationIsPending) - + const remaining = this.migrations.filter(migrationIsPending) + return ( asyncQ.eachSeries(remaining, (migration) => this.runMigration(versionedData, migration)) .then(() => versionedData) @@ -21,12 +21,12 @@ class Migrator { // migration is "pending" if hit has a higher // version number than currentVersion - function migrationIsPending(migration) { + function migrationIsPending (migration) { return migration.version > versionedData.meta.version } } - runMigration(versionedData, migration) { + runMigration (versionedData, migration) { return ( migration.migrate(versionedData) .then((versionedData) => { diff --git a/app/scripts/lib/notification-manager.js b/app/scripts/lib/notification-manager.js index 55e5b8dd2..799282f6d 100644 --- a/app/scripts/lib/notification-manager.js +++ b/app/scripts/lib/notification-manager.js @@ -71,4 +71,4 @@ class NotificationManager { } -module.exports = NotificationManager \ No newline at end of file +module.exports = NotificationManager diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js index bbc978446..6602f5aa8 100644 --- a/app/scripts/lib/personal-message-manager.js +++ b/app/scripts/lib/personal-message-manager.js @@ -5,7 +5,7 @@ const createId = require('./random-id') const hexRe = /^[0-9A-Fa-f]+$/g -module.exports = class PersonalMessageManager extends EventEmitter{ +module.exports = class PersonalMessageManager extends EventEmitter { constructor (opts) { super() this.memStore = new ObservableStore({ @@ -108,7 +108,7 @@ module.exports = class PersonalMessageManager extends EventEmitter{ this.emit('updateBadge') } - normalizeMsgData(data) { + normalizeMsgData (data) { try { const stripped = ethUtil.stripHexPrefix(data) if (stripped.match(hexRe)) { diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js index e8e23f8b5..084ca3721 100644 --- a/app/scripts/lib/tx-utils.js +++ b/app/scripts/lib/tx-utils.js @@ -75,14 +75,14 @@ module.exports = class txProviderUtils { } fillInTxParams (txParams, cb) { - let fromAddress = txParams.from - let reqs = {} + const fromAddress = txParams.from + const reqs = {} if (isUndef(txParams.gas)) reqs.gas = (cb) => this.query.estimateGas(txParams, cb) if (isUndef(txParams.gasPrice)) reqs.gasPrice = (cb) => this.query.gasPrice(cb) if (isUndef(txParams.nonce)) reqs.nonce = (cb) => this.query.getTransactionCount(fromAddress, 'pending', cb) - async.parallel(reqs, function(err, result) { + async.parallel(reqs, function (err, result) { if (err) return cb(err) // write results to txParams obj Object.assign(txParams, result) @@ -123,14 +123,14 @@ module.exports = class txProviderUtils { // util -function isUndef(value) { +function isUndef (value) { return value === undefined } -function bnToHex(inputBn) { +function bnToHex (inputBn) { return ethUtil.addHexPrefix(inputBn.toString(16)) } -function hexToBn(inputHex) { +function hexToBn (inputHex) { return new BN(ethUtil.stripHexPrefix(inputHex), 16) } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 2b8fc9cb8..b91b5efe8 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -32,7 +32,7 @@ module.exports = class MetamaskController extends EventEmitter { constructor (opts) { super() this.opts = opts - let initState = opts.initState || {} + const initState = opts.initState || {} // platform-specific api this.platform = opts.platform @@ -161,8 +161,7 @@ module.exports = class MetamaskController extends EventEmitter { // initializeProvider () { - - let provider = MetaMaskProvider({ + const provider = MetaMaskProvider({ static: { eth_syncing: false, web3_clientVersion: `MetaMask/v${version}`, @@ -170,8 +169,8 @@ module.exports = class MetamaskController extends EventEmitter { rpcUrl: this.configManager.getCurrentRpcAddress(), // account mgmt getAccounts: (cb) => { - let selectedAddress = this.preferencesController.getSelectedAddress() - let result = selectedAddress ? [selectedAddress] : [] + const selectedAddress = this.preferencesController.getSelectedAddress() + const result = selectedAddress ? [selectedAddress] : [] cb(null, result) }, // tx signing @@ -196,7 +195,7 @@ module.exports = class MetamaskController extends EventEmitter { publicConfigStore ) - function selectPublicState(state) { + function selectPublicState (state) { const result = { selectedAddress: undefined } try { result.selectedAddress = state.PreferencesController.selectedAddress @@ -253,56 +252,56 @@ module.exports = class MetamaskController extends EventEmitter { return { // etc - getState: (cb) => cb(null, this.getState()), - setProviderType: this.setProviderType.bind(this), - useEtherscanProvider: this.useEtherscanProvider.bind(this), - setCurrentCurrency: this.setCurrentCurrency.bind(this), - markAccountsFound: this.markAccountsFound.bind(this), + getState: (cb) => cb(null, this.getState()), + setProviderType: this.setProviderType.bind(this), + useEtherscanProvider: this.useEtherscanProvider.bind(this), + setCurrentCurrency: this.setCurrentCurrency.bind(this), + markAccountsFound: this.markAccountsFound.bind(this), // coinbase buyEth: this.buyEth.bind(this), // shapeshift createShapeShiftTx: this.createShapeShiftTx.bind(this), // primary HD keyring management - addNewAccount: this.addNewAccount.bind(this), - placeSeedWords: this.placeSeedWords.bind(this), - clearSeedWordCache: this.clearSeedWordCache.bind(this), - importAccountWithStrategy: this.importAccountWithStrategy.bind(this), + addNewAccount: this.addNewAccount.bind(this), + placeSeedWords: this.placeSeedWords.bind(this), + clearSeedWordCache: this.clearSeedWordCache.bind(this), + importAccountWithStrategy: this.importAccountWithStrategy.bind(this), // vault management submitPassword: this.submitPassword.bind(this), // PreferencesController - setSelectedAddress: nodeify(preferencesController.setSelectedAddress).bind(preferencesController), - setDefaultRpc: nodeify(this.setDefaultRpc).bind(this), - setCustomRpc: nodeify(this.setCustomRpc).bind(this), + setSelectedAddress: nodeify(preferencesController.setSelectedAddress).bind(preferencesController), + setDefaultRpc: nodeify(this.setDefaultRpc).bind(this), + setCustomRpc: nodeify(this.setCustomRpc).bind(this), // AddressController - setAddressBook: nodeify(addressBookController.setAddressBook).bind(addressBookController), + setAddressBook: nodeify(addressBookController.setAddressBook).bind(addressBookController), // KeyringController - setLocked: nodeify(keyringController.setLocked).bind(keyringController), + setLocked: nodeify(keyringController.setLocked).bind(keyringController), createNewVaultAndKeychain: nodeify(keyringController.createNewVaultAndKeychain).bind(keyringController), - createNewVaultAndRestore: nodeify(keyringController.createNewVaultAndRestore).bind(keyringController), - addNewKeyring: nodeify(keyringController.addNewKeyring).bind(keyringController), - saveAccountLabel: nodeify(keyringController.saveAccountLabel).bind(keyringController), - exportAccount: nodeify(keyringController.exportAccount).bind(keyringController), + createNewVaultAndRestore: nodeify(keyringController.createNewVaultAndRestore).bind(keyringController), + addNewKeyring: nodeify(keyringController.addNewKeyring).bind(keyringController), + saveAccountLabel: nodeify(keyringController.saveAccountLabel).bind(keyringController), + exportAccount: nodeify(keyringController.exportAccount).bind(keyringController), // txManager - approveTransaction: txManager.approveTransaction.bind(txManager), - cancelTransaction: txManager.cancelTransaction.bind(txManager), + approveTransaction: txManager.approveTransaction.bind(txManager), + cancelTransaction: txManager.cancelTransaction.bind(txManager), updateAndApproveTransaction: this.updateAndApproveTx.bind(this), // messageManager - signMessage: nodeify(this.signMessage).bind(this), - cancelMessage: this.cancelMessage.bind(this), + signMessage: nodeify(this.signMessage).bind(this), + cancelMessage: this.cancelMessage.bind(this), // personalMessageManager - signPersonalMessage: nodeify(this.signPersonalMessage).bind(this), - cancelPersonalMessage: this.cancelPersonalMessage.bind(this), + signPersonalMessage: nodeify(this.signPersonalMessage).bind(this), + cancelPersonalMessage: this.cancelPersonalMessage.bind(this), // notices - checkNotices: noticeController.updateNoticesList.bind(noticeController), + checkNotices: noticeController.updateNoticesList.bind(noticeController), markNoticeRead: noticeController.markNoticeRead.bind(noticeController), } } @@ -441,7 +440,7 @@ module.exports = class MetamaskController extends EventEmitter { } newUnsignedMessage (msgParams, cb) { - let msgId = this.messageManager.addUnapprovedMessage(msgParams) + const msgId = this.messageManager.addUnapprovedMessage(msgParams) this.sendUpdate() this.opts.showUnconfirmedMessage() this.messageManager.once(`${msgId}:finished`, (data) => { @@ -461,7 +460,7 @@ module.exports = class MetamaskController extends EventEmitter { return cb(new Error('MetaMask Message Signature: from field is required.')) } - let msgId = this.personalMessageManager.addUnapprovedMessage(msgParams) + const msgId = this.personalMessageManager.addUnapprovedMessage(msgParams) this.sendUpdate() this.opts.showUnconfirmedMessage() this.personalMessageManager.once(`${msgId}:finished`, (data) => { @@ -476,7 +475,7 @@ module.exports = class MetamaskController extends EventEmitter { }) } - updateAndApproveTx(txMeta, cb) { + updateAndApproveTx (txMeta, cb) { log.debug(`MetaMaskController - updateAndApproveTx: ${JSON.stringify(txMeta)}`) const txManager = this.txManager txManager.updateTx(txMeta) @@ -502,7 +501,7 @@ module.exports = class MetamaskController extends EventEmitter { }) } - cancelMessage(msgId, cb) { + cancelMessage (msgId, cb) { const messageManager = this.messageManager messageManager.rejectMsg(msgId) if (cb && typeof cb === 'function') { @@ -512,7 +511,7 @@ module.exports = class MetamaskController extends EventEmitter { // Prefixed Style Message Signing Methods: approvePersonalMessage (msgParams, cb) { - let msgId = this.personalMessageManager.addUnapprovedMessage(msgParams) + const msgId = this.personalMessageManager.addUnapprovedMessage(msgParams) this.sendUpdate() this.opts.showUnconfirmedMessage() this.personalMessageManager.once(`${msgId}:finished`, (data) => { @@ -545,7 +544,7 @@ module.exports = class MetamaskController extends EventEmitter { }) } - cancelPersonalMessage(msgId, cb) { + cancelPersonalMessage (msgId, cb) { const messageManager = this.personalMessageManager messageManager.rejectMsg(msgId) if (cb && typeof cb === 'function') { @@ -559,13 +558,13 @@ module.exports = class MetamaskController extends EventEmitter { cb(null, this.getState()) } - restoreOldVaultAccounts(migratorOutput) { + restoreOldVaultAccounts (migratorOutput) { const { serialized } = migratorOutput return this.keyringController.restoreKeyring(serialized) .then(() => migratorOutput) } - restoreOldLostAccounts(migratorOutput) { + restoreOldLostAccounts (migratorOutput) { const { lostAccounts } = migratorOutput if (lostAccounts) { this.configManager.setLostAccounts(lostAccounts.map(acct => acct.address)) diff --git a/app/scripts/migrations/002.js b/app/scripts/migrations/002.js index 36a870342..b1d88f2ef 100644 --- a/app/scripts/migrations/002.js +++ b/app/scripts/migrations/002.js @@ -7,7 +7,7 @@ module.exports = { version, migrate: function (originalVersionedData) { - let versionedData = clone(originalVersionedData) + const versionedData = clone(originalVersionedData) versionedData.meta.version = version try { if (versionedData.data.config.provider.type === 'etherscan') { diff --git a/app/scripts/migrations/003.js b/app/scripts/migrations/003.js index 1893576ad..140f81d40 100644 --- a/app/scripts/migrations/003.js +++ b/app/scripts/migrations/003.js @@ -8,7 +8,7 @@ module.exports = { version, migrate: function (originalVersionedData) { - let versionedData = clone(originalVersionedData) + const versionedData = clone(originalVersionedData) versionedData.meta.version = version try { if (versionedData.data.config.provider.rpcTarget === oldTestRpc) { diff --git a/app/scripts/migrations/004.js b/app/scripts/migrations/004.js index 405d932f8..cd558300c 100644 --- a/app/scripts/migrations/004.js +++ b/app/scripts/migrations/004.js @@ -6,7 +6,7 @@ module.exports = { version, migrate: function (versionedData) { - let safeVersionedData = clone(versionedData) + const safeVersionedData = clone(versionedData) safeVersionedData.meta.version = version try { if (safeVersionedData.data.config.provider.type !== 'rpc') return Promise.resolve(safeVersionedData) diff --git a/app/scripts/migrations/005.js b/app/scripts/migrations/005.js index e4b84f460..f7b68dfe4 100644 --- a/app/scripts/migrations/005.js +++ b/app/scripts/migrations/005.js @@ -14,7 +14,7 @@ module.exports = { version, migrate: function (originalVersionedData) { - let versionedData = clone(originalVersionedData) + const versionedData = clone(originalVersionedData) versionedData.meta.version = version try { const state = versionedData.data diff --git a/app/scripts/migrations/006.js b/app/scripts/migrations/006.js index 94d1b6ecd..51ea6e3e7 100644 --- a/app/scripts/migrations/006.js +++ b/app/scripts/migrations/006.js @@ -13,7 +13,7 @@ module.exports = { version, migrate: function (originalVersionedData) { - let versionedData = clone(originalVersionedData) + const versionedData = clone(originalVersionedData) versionedData.meta.version = version try { const state = versionedData.data diff --git a/app/scripts/migrations/007.js b/app/scripts/migrations/007.js index 236e35224..d9887b9c8 100644 --- a/app/scripts/migrations/007.js +++ b/app/scripts/migrations/007.js @@ -13,7 +13,7 @@ module.exports = { version, migrate: function (originalVersionedData) { - let versionedData = clone(originalVersionedData) + const versionedData = clone(originalVersionedData) versionedData.meta.version = version try { const state = versionedData.data diff --git a/app/scripts/migrations/008.js b/app/scripts/migrations/008.js index cd5e95d22..da7cb2e60 100644 --- a/app/scripts/migrations/008.js +++ b/app/scripts/migrations/008.js @@ -13,7 +13,7 @@ module.exports = { version, migrate: function (originalVersionedData) { - let versionedData = clone(originalVersionedData) + const versionedData = clone(originalVersionedData) versionedData.meta.version = version try { const state = versionedData.data diff --git a/app/scripts/migrations/009.js b/app/scripts/migrations/009.js index 4612fefdc..f47db55ac 100644 --- a/app/scripts/migrations/009.js +++ b/app/scripts/migrations/009.js @@ -13,7 +13,7 @@ module.exports = { version, migrate: function (originalVersionedData) { - let versionedData = clone(originalVersionedData) + const versionedData = clone(originalVersionedData) versionedData.meta.version = version try { const state = versionedData.data diff --git a/app/scripts/migrations/010.js b/app/scripts/migrations/010.js index c0cc56ae4..e4b9ac07e 100644 --- a/app/scripts/migrations/010.js +++ b/app/scripts/migrations/010.js @@ -13,7 +13,7 @@ module.exports = { version, migrate: function (originalVersionedData) { - let versionedData = clone(originalVersionedData) + const versionedData = clone(originalVersionedData) versionedData.meta.version = version try { const state = versionedData.data diff --git a/app/scripts/migrations/011.js b/app/scripts/migrations/011.js index 0d5d6d307..782ec809d 100644 --- a/app/scripts/migrations/011.js +++ b/app/scripts/migrations/011.js @@ -12,7 +12,7 @@ module.exports = { version, migrate: function (originalVersionedData) { - let versionedData = clone(originalVersionedData) + const versionedData = clone(originalVersionedData) versionedData.meta.version = version try { const state = versionedData.data diff --git a/app/scripts/migrations/012.js b/app/scripts/migrations/012.js index 8361b3793..f69ccbb02 100644 --- a/app/scripts/migrations/012.js +++ b/app/scripts/migrations/012.js @@ -12,7 +12,7 @@ module.exports = { version, migrate: function (originalVersionedData) { - let versionedData = clone(originalVersionedData) + const versionedData = clone(originalVersionedData) versionedData.meta.version = version try { const state = versionedData.data diff --git a/app/scripts/migrations/_multi-keyring.js b/app/scripts/migrations/_multi-keyring.js index 04c966d4d..253aa3d9d 100644 --- a/app/scripts/migrations/_multi-keyring.js +++ b/app/scripts/migrations/_multi-keyring.js @@ -15,15 +15,15 @@ const KeyringController = require('../../app/scripts/lib/keyring-controller') const password = 'obviously not correct' module.exports = { - version, + version, migrate: function (versionedData) { versionedData.meta.version = version - let store = new ObservableStore(versionedData.data) - let configManager = new ConfigManager({ store }) - let idStoreMigrator = new IdentityStoreMigrator({ configManager }) - let keyringController = new KeyringController({ + const store = new ObservableStore(versionedData.data) + const configManager = new ConfigManager({ store }) + const idStoreMigrator = new IdentityStoreMigrator({ configManager }) + const keyringController = new KeyringController({ configManager: configManager, }) @@ -46,6 +46,5 @@ module.exports = { return Promise.resolve(versionedData) }) }) - }, } diff --git a/app/scripts/popup-core.js b/app/scripts/popup-core.js index 1e5d70e8b..f9ac4d052 100644 --- a/app/scripts/popup-core.js +++ b/app/scripts/popup-core.js @@ -16,7 +16,6 @@ function initializePopup ({ container, connectionStream }, cb) { (cb) => connectToAccountManager(connectionStream, cb), (accountManager, cb) => launchMetamaskUi({ container, accountManager }, cb), ], cb) - } function connectToAccountManager (connectionStream, cb) { diff --git a/app/scripts/popup.js b/app/scripts/popup.js index 0fbde54b3..5f17f0651 100644 --- a/app/scripts/popup.js +++ b/app/scripts/popup.js @@ -41,7 +41,7 @@ function closePopupIfOpen (windowType) { } } -function displayCriticalError(err) { +function displayCriticalError (err) { container.innerHTML = '
The MetaMask app failed to load: please open and close MetaMask again to restart.
' container.style.height = '80px' log.error(err.stack) diff --git a/app/scripts/transaction-manager.js b/app/scripts/transaction-manager.js index d7051b2cb..9f267160f 100644 --- a/app/scripts/transaction-manager.js +++ b/app/scripts/transaction-manager.js @@ -28,9 +28,9 @@ module.exports = class TransactionManager extends EventEmitter { // memstore is computed from a few different stores this._updateMemstore() - this.store.subscribe(() => this._updateMemstore() ) - this.networkStore.subscribe(() => this._updateMemstore() ) - this.preferencesStore.subscribe(() => this._updateMemstore() ) + this.store.subscribe(() => this._updateMemstore()) + this.networkStore.subscribe(() => this._updateMemstore()) + this.preferencesStore.subscribe(() => this._updateMemstore()) } getState () { @@ -47,8 +47,8 @@ module.exports = class TransactionManager extends EventEmitter { // Returns the tx list getTxList () { - let network = this.getNetwork() - let fullTxList = this.getFullTxList() + const network = this.getNetwork() + const fullTxList = this.getFullTxList() return fullTxList.filter(txMeta => txMeta.metamaskNetworkId === network) } @@ -64,10 +64,10 @@ module.exports = class TransactionManager extends EventEmitter { // Adds a tx to the txlist addTx (txMeta) { - let txCount = this.getTxCount() - let network = this.getNetwork() - let fullTxList = this.getFullTxList() - let txHistoryLimit = this.txHistoryLimit + const txCount = this.getTxCount() + const network = this.getNetwork() + const fullTxList = this.getFullTxList() + const txHistoryLimit = this.txHistoryLimit // checks if the length of the tx history is // longer then desired persistence limit @@ -197,7 +197,7 @@ module.exports = class TransactionManager extends EventEmitter { } fillInTxParams (txId, cb) { - let txMeta = this.getTx(txId) + const txMeta = this.getTx(txId) this.txProviderUtils.fillInTxParams(txMeta.txParams, (err) => { if (err) return cb(err) this.updateTx(txMeta) @@ -205,7 +205,7 @@ module.exports = class TransactionManager extends EventEmitter { }) } - getChainId() { + getChainId () { const networkState = this.networkStore.getState() const getChainId = parseInt(networkState.network) if (Number.isNaN(getChainId)) { @@ -242,7 +242,7 @@ module.exports = class TransactionManager extends EventEmitter { // receives a txHash records the tx as signed setTxHash (txId, txHash) { // Add the tx hash to the persisted meta-tx object - let txMeta = this.getTx(txId) + const txMeta = this.getTx(txId) txMeta.hash = txHash this.updateTx(txMeta) } @@ -315,7 +315,7 @@ module.exports = class TransactionManager extends EventEmitter { } setTxStatusFailed (txId, reason) { - let txMeta = this.getTx(txId) + const txMeta = this.getTx(txId) txMeta.err = reason this.updateTx(txMeta) this._setTxStatus(txId, 'failed') @@ -338,7 +338,7 @@ module.exports = class TransactionManager extends EventEmitter { var txHash = txMeta.hash var txId = txMeta.id if (!txHash) { - let errReason = { + const errReason = { errCode: 'No hash was provided', message: 'We had an error while submitting this transaction, please try again.', } diff --git a/ui/app/accounts/import/index.js b/ui/app/accounts/import/index.js index 96350852a..a0f0f9bdb 100644 --- a/ui/app/accounts/import/index.js +++ b/ui/app/accounts/import/index.js @@ -73,7 +73,7 @@ AccountImportSubview.prototype.render = function () { ) } -AccountImportSubview.prototype.renderImportView = function() { +AccountImportSubview.prototype.renderImportView = function () { const props = this.props const state = this.state || {} const { type } = state diff --git a/ui/app/actions.js b/ui/app/actions.js index 18f341411..c15c9be7e 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -314,7 +314,7 @@ function importNewAccount (strategy, args) { } } -function navigateToNewAccountScreen() { +function navigateToNewAccountScreen () { return { type: this.NEW_ACCOUNT_SCREEN, } @@ -665,7 +665,7 @@ function clearNotices () { } } -function markAccountsFound() { +function markAccountsFound () { log.debug(`background.markAccountsFound`) return callBackgroundThenUpdate(background.markAccountsFound) } @@ -978,7 +978,7 @@ function callBackgroundThenUpdate (method, ...args) { } } -function forceUpdateMetamaskState(dispatch){ +function forceUpdateMetamaskState (dispatch) { log.debug(`background.getState`) background.getState((err, newState) => { if (err) { diff --git a/ui/app/app.js b/ui/app/app.js index 5a7596aca..521f453cc 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -552,5 +552,4 @@ App.prototype.renderCommonRpc = function (rpcList, provider) { }) } }) - } diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js index facf29d97..f1cf49998 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/ens-input.js @@ -24,7 +24,7 @@ EnsInput.prototype.render = function () { list: 'addresses', onChange: () => { const network = this.props.network - let resolverAddress = networkResolvers[network] + const resolverAddress = networkResolvers[network] if (!resolverAddress) return const recipient = document.querySelector('input[name="address"]').value @@ -52,7 +52,7 @@ EnsInput.prototype.render = function () { [ // Corresponds to the addresses owned. Object.keys(props.identities).map((key) => { - let identity = props.identities[key] + const identity = props.identities[key] return h('option', { value: identity.address, label: identity.name, @@ -72,7 +72,7 @@ EnsInput.prototype.render = function () { EnsInput.prototype.componentDidMount = function () { const network = this.props.network - let resolverAddress = networkResolvers[network] + const resolverAddress = networkResolvers[network] if (resolverAddress) { const provider = web3.currentProvider diff --git a/ui/app/components/notice.js b/ui/app/components/notice.js index b85787033..3c8523daf 100644 --- a/ui/app/components/notice.js +++ b/ui/app/components/notice.js @@ -115,8 +115,9 @@ Notice.prototype.render = function () { Notice.prototype.componentDidMount = function () { var node = findDOMNode(this) linker.setupListener(node) - if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) { this.setState({disclaimerDisabled: false}) } - + if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) { + this.setState({disclaimerDisabled: false}) + } } Notice.prototype.componentWillUnmount = function () { diff --git a/ui/app/components/transaction-list-item-icon.js b/ui/app/components/transaction-list-item-icon.js index ca2781451..d63cae259 100644 --- a/ui/app/components/transaction-list-item-icon.js +++ b/ui/app/components/transaction-list-item-icon.js @@ -15,7 +15,7 @@ TransactionIcon.prototype.render = function () { const { transaction, txParams, isMsg } = this.props switch (transaction.status) { case 'unapproved': - return h( !isMsg ? '.unapproved-tx-icon' : 'i.fa.fa-certificate.fa-lg') + return h(!isMsg ? '.unapproved-tx-icon' : 'i.fa.fa-certificate.fa-lg') case 'rejected': return h('i.fa.fa-exclamation-triangle.fa-lg.warning', { diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js index 9fef52355..ec1b0d66c 100644 --- a/ui/app/components/transaction-list-item.js +++ b/ui/app/components/transaction-list-item.js @@ -134,7 +134,6 @@ function failIfFailed (transaction) { return h('span.error', ' (Rejected)') } if (transaction.err) { - return h(Tooltip, { title: transaction.err.message, position: 'bottom', @@ -142,5 +141,4 @@ function failIfFailed (transaction) { h('span.error', ' (Failed)'), ]) } - } diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 770f79b19..83ac5a4fd 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -125,14 +125,12 @@ function currentTxView (opts) { if (txParams) { log.debug('txParams detected, rendering pending tx') return h(PendingTx, opts) - } else if (msgParams) { log.debug('msgParams detected, rendering pending msg') if (type === 'eth_sign') { log.debug('rendering eth_sign message') return h(PendingMsg, opts) - } else if (type === 'personal_sign') { log.debug('rendering personal_sign message') return h(PendingPersonalMsg, opts) diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 324a4df35..deacad0a7 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -315,7 +315,7 @@ function reduceApp (state, action) { case actions.COMPLETED_TX: log.debug('reducing COMPLETED_TX for tx ' + action.value) const otherUnconfActions = getUnconfActionList(state) - .filter(tx => tx.id !== action.value ) + .filter(tx => tx.id !== action.value) const hasOtherUnconfActions = otherUnconfActions.length > 0 if (hasOtherUnconfActions) { -- cgit v1.2.3 From e665dd7e1caa19d3df7c7854ff12d520398e3b34 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 27 Apr 2017 13:31:54 +0200 Subject: bump client-sw-ready-event --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 157dbda65..73c285d06 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "bluebird": "^3.5.0", "browser-passworder": "^2.0.3", "browserify-derequire": "^0.9.4", - "client-sw-ready-event": "^3.0.1", + "client-sw-ready-event": "^3.0.3", "clone": "^1.0.2", "copy-to-clipboard": "^2.0.0", "debounce": "^1.0.0", -- cgit v1.2.3 From e7c7c85791377bdd55042e6a4b026f4424230408 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 27 Apr 2017 14:26:29 +0200 Subject: Update README for mascara --- mascara/README.md | 14 +++----------- package.json | 3 ++- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/mascara/README.md b/mascara/README.md index cdeb4795c..75d3cabad 100644 --- a/mascara/README.md +++ b/mascara/README.md @@ -1,20 +1,12 @@ start the dual servers (dapp + mascara) ``` -node server.js +npm run mascara ``` ## First time use: -- navigate to: http://localhost:9001/popup/popup.html +- navigate to: http://localhost:9001 - Create an Account -- go back to http://localhost:9002/ +- go back to http://localhost:9002 - open devTools - click Sync Tx - -### Todos - - - [ ] Figure out user flows and UI redesign - - [ ] Figure out FireFox - Standing problems: - - [ ] IndexDb - diff --git a/package.json b/package.json index 73c285d06..2d6d52765 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "testem": "npm run buildMock && testem", "announce": "node development/announcer.js", "generateNotice": "node notices/notice-generator.js", - "deleteNotice": "node notices/notice-delete.js" + "deleteNotice": "node notices/notice-delete.js", + "mascara": "node ./mascara/example/server" }, "browserify": { "transform": [ -- cgit v1.2.3 From 6d1fe7845c3e74858bbc9c7181348153dfcd55e9 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Apr 2017 15:11:01 -0700 Subject: Version 3.6.0 t # Explicit paths specified without -i or -o; assuming --only paths... --- CHANGELOG.md | 5 ++++- app/manifest.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63346fa5b..e629e0bc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,15 @@ ## Current Master +## 3.6.0 2017-4-25 + +- Add Rinkeby Test Network to our network list. + ## 3.5.4 2017-4-25 - Fix occasional nonce tracking issue. - Fix bug where some events would not be emitted by web3. - Fix bug where an error would be thrown when composing signatures for networks with large ID values. -- Add Rinkeby Test Network to our network list. ## 3.5.3 2017-4-24 diff --git a/app/manifest.json b/app/manifest.json index 6ef428d2c..d5f66173c 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.5.4", + "version": "3.6.0", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From e7e0919d7c76c818590df4435db0152298298bd9 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 27 Apr 2017 15:25:00 +0200 Subject: Setup test enviroment for mascara --- mascara/src/background.js | 1 + mascara/test/helpers.js | 7 ++ mascara/test/index.html | 20 ++++++ mascara/test/index.js | 20 ++++++ mascara/test/jquery-3.1.0.min.js | 4 ++ mascara/test/lib/first-time.js | 119 +++++++++++++++++++++++++++++++ mascara/test/testem.yml | 13 ++++ mascara/test/util/mascara-test-helper.js | 40 +++++++++++ mascara/test/window-load.js | 7 ++ package.json | 6 +- 10 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 mascara/test/helpers.js create mode 100644 mascara/test/index.html create mode 100644 mascara/test/index.js create mode 100644 mascara/test/jquery-3.1.0.min.js create mode 100644 mascara/test/lib/first-time.js create mode 100644 mascara/test/testem.yml create mode 100644 mascara/test/util/mascara-test-helper.js create mode 100644 mascara/test/window-load.js diff --git a/mascara/src/background.js b/mascara/src/background.js index 957570050..746c479f9 100644 --- a/mascara/src/background.js +++ b/mascara/src/background.js @@ -1,4 +1,5 @@ global.window = global +const self = global const pipe = require('pump') const SwGlobalListener = require('sw-stream/lib/sw-global-listener.js') diff --git a/mascara/test/helpers.js b/mascara/test/helpers.js new file mode 100644 index 000000000..eede103b4 --- /dev/null +++ b/mascara/test/helpers.js @@ -0,0 +1,7 @@ +function wait(time) { + return new Promise(function(resolve, reject) { + setTimeout(function() { + resolve() + }, time * 3 || 1500) + }) +} diff --git a/mascara/test/index.html b/mascara/test/index.html new file mode 100644 index 000000000..abd985121 --- /dev/null +++ b/mascara/test/index.html @@ -0,0 +1,20 @@ + + + + + + QUnit Example + + + +
+
+ + + + + + +
+ + diff --git a/mascara/test/index.js b/mascara/test/index.js new file mode 100644 index 000000000..b94ce16dd --- /dev/null +++ b/mascara/test/index.js @@ -0,0 +1,20 @@ +var fs = require('fs') +var path = require('path') +var browserify = require('browserify'); +var tests = fs.readdirSync(path.join(__dirname, 'lib')) +var bundlePath = path.join(__dirname, 'test-bundle.js') +var b = browserify(); + +// Remove old bundle +try { + fs.unlinkSync(bundlePath) +} catch (e) {} + +var writeStream = fs.createWriteStream(bundlePath) + +tests.forEach(function(fileName) { + b.add(path.join(__dirname, 'lib', fileName)) +}) + +b.bundle().pipe(writeStream); + diff --git a/mascara/test/jquery-3.1.0.min.js b/mascara/test/jquery-3.1.0.min.js new file mode 100644 index 000000000..f6a6a99e6 --- /dev/null +++ b/mascara/test/jquery-3.1.0.min.js @@ -0,0 +1,4 @@ +/*! jQuery v3.1.0 | (c) jQuery Foundation | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.1.0",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null!=a?a<0?this[a+this.length]:this[a]:f.call(this)},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"label"in b&&b.disabled===a||"form"in b&&b.disabled===a||"form"in b&&b.disabled===!1&&(b.isDisabled===a||b.isDisabled!==!a&&("label"in b||!ea(b))!==a)}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(_,aa),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=V.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(_,aa),$.test(j[0].type)&&qa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&sa(j),!a)return G.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||$.test(a)&&qa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){if(r.isFunction(b))return r.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return r.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(C.test(b))return r.filter(b,a,c);b=r.filter(b,a)}return r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType})}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/\S+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R),a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0, +r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,ja=/^$|\/(?:java|ecma)script/i,ka={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ka.optgroup=ka.option,ka.tbody=ka.tfoot=ka.colgroup=ka.caption=ka.thead,ka.th=ka.td;function la(a,b){var c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function ma(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=la(l.appendChild(f),"script"),j&&ma(g),c){k=0;while(f=g[k++])ja.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var pa=d.documentElement,qa=/^key/,ra=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,sa=/^([^.]*)(?:\.(.+)|)/;function ta(){return!0}function ua(){return!1}function va(){try{return d.activeElement}catch(a){}}function wa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)wa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=ua;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(pa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=sa.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=sa.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c-1:r.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h\x20\t\r\n\f]*)[^>]*)\/>/gi,ya=/\s*$/g;function Ca(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Da(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ea(a){var b=Aa.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Fa(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&za.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ha(f,b,c,d)});if(m&&(e=oa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(la(e,"script"),Da),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=la(h),f=la(a),d=0,e=f.length;d0&&ma(g,!i&&la(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ia(this,a,!0)},remove:function(a){return Ia(this,a)},text:function(a){return S(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.appendChild(a)}})},prepend:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(la(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!ya.test(a)&&!ka[(ia.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function Xa(a,b,c,d,e){return new Xa.prototype.init(a,b,c,d,e)}r.Tween=Xa,Xa.prototype={constructor:Xa,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Xa.propHooks[this.prop];return a&&a.get?a.get(this):Xa.propHooks._default.get(this)},run:function(a){var b,c=Xa.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Xa.propHooks._default.set(this),this}},Xa.prototype.init.prototype=Xa.prototype,Xa.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Xa.propHooks.scrollTop=Xa.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Xa.prototype.init,r.fx.step={};var Ya,Za,$a=/^(?:toggle|show|hide)$/,_a=/queueHooks$/;function ab(){Za&&(a.requestAnimationFrame(ab),r.fx.tick())}function bb(){return a.setTimeout(function(){Ya=void 0}),Ya=r.now()}function cb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=aa[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function db(a,b,c){for(var d,e=(gb.tweeners[b]||[]).concat(gb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?hb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K); +if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),hb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=ib[b]||r.find.attr;ib[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=ib[g],ib[g]=e,e=null!=c(a,b,d)?g:null,ib[g]=f),e}});var jb=/^(?:input|select|textarea|button)$/i,kb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):jb.test(a.nodeName)||kb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});var lb=/[\t\r\n\f]/g;function mb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,mb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=mb(c),d=1===c.nodeType&&(" "+e+" ").replace(lb," ")){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=r.trim(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,mb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=mb(c),d=1===c.nodeType&&(" "+e+" ").replace(lb," ")){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=r.trim(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,mb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=mb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(c)+" ").replace(lb," ").indexOf(b)>-1)return!0;return!1}});var nb=/\r/g,ob=/[\x20\t\r\n\f]+/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(nb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:r.trim(r.text(a)).replace(ob," ")}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type,g=f?null:[],h=f?e+1:d.length,i=e<0?h:f?e:0;i-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(r.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ha.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Nb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,""),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r(" - +
+ diff --git a/mascara/test/lib/first-time.js b/mascara/test/lib/first-time.js index a3b2a41a5..8e33c8a06 100644 --- a/mascara/test/lib/first-time.js +++ b/mascara/test/lib/first-time.js @@ -6,8 +6,8 @@ QUnit.test('render init screen', function (assert) { var done = assert.async() let app - wait().then(function() { - app = $('#app-content') + wait(1000).then(function() { + app = $('#app-content').contents() const recurseNotices = function () { let button = app.find('button') if (button.html() === 'Continue') { diff --git a/mascara/test/util/mascara-test-helper.js b/mascara/test/util/mascara-test-helper.js index 1ed576005..9cf4fa900 100644 --- a/mascara/test/util/mascara-test-helper.js +++ b/mascara/test/util/mascara-test-helper.js @@ -1,5 +1,5 @@ const EventEmitter = require('events') -const IDB = require('../../../mascara/src/lib/index-db-controller') +const IDB = require('idb-global') const KEY = 'metamask-test-config' module.exports = class Helper extends EventEmitter { constructor () { diff --git a/mascara/test/window-load.js b/mascara/test/window-load.js index de79faaa1..41166c466 100644 --- a/mascara/test/window-load.js +++ b/mascara/test/window-load.js @@ -1,7 +1,8 @@ const Helper = require('./util/mascara-test-helper.js') -debugger + window.addEventListener('load', () => { - const helper = new Helper() - helper.on('complete', () => require('../src/ui.js')) - helper.tryToCleanContext() + // const helper = new Helper() + // helper.on('complete', () => require('../src/ui.js')) + // helper.tryToCleanContext() + require('../src/ui.js') }) diff --git a/package.json b/package.json index 4dc42aa01..2e7887e0c 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "extensionizer": "^1.0.0", "gulp-eslint": "^2.0.0", "hat": "0.0.3", + "idb-global": "^1.0.0", "identicon.js": "^1.2.1", "iframe": "^1.0.0", "iframe-stream": "^1.0.2", -- cgit v1.2.3 From 36bafbaebf826933262c4ad381a8953e82f8ebc3 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Fri, 28 Apr 2017 14:05:10 +0200 Subject: General cleanup and window reload if an update is found --- mascara/README.md | 12 ++++++++++++ mascara/src/background.js | 3 +-- mascara/src/proxy.js | 1 + mascara/src/ui.js | 13 +++---------- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/mascara/README.md b/mascara/README.md index 75d3cabad..db5b4f404 100644 --- a/mascara/README.md +++ b/mascara/README.md @@ -10,3 +10,15 @@ npm run mascara - go back to http://localhost:9002 - open devTools - click Sync Tx + +## Tests: + +``` +npm run testMascara +``` + +Test will run in browser, you will have to have these browsers installed: + +- Chrome +- Firefox +- Opera diff --git a/mascara/src/background.js b/mascara/src/background.js index a6703b291..dff5e6a7c 100644 --- a/mascara/src/background.js +++ b/mascara/src/background.js @@ -7,7 +7,7 @@ const connectionListener = new SwGlobalListener(self) const setupMultiplex = require('../../app/scripts/lib/stream-utils.js').setupMultiplex const PortStream = require('../../app/scripts/lib/port-stream.js') -const DbController = require('./lib/index-db-controller') +const DbController = require('idb-global') const SwPlatform = require('../../app/scripts/platforms/sw') const MetamaskController = require('../../app/scripts/metamask-controller') @@ -47,7 +47,6 @@ console.log('inside:open') let diskStore const dbController = new DbController({ key: STORAGE_KEY, - version: 2, }) loadStateFromPersistence() .then((initState) => setupController(initState)) diff --git a/mascara/src/proxy.js b/mascara/src/proxy.js index ec5665240..eabc547b4 100644 --- a/mascara/src/proxy.js +++ b/mascara/src/proxy.js @@ -20,6 +20,7 @@ background.on('ready', (_) => { pageStream.pipe(swStream).pipe(pageStream) }) +background.on('updatefound', () => window.location.reload()) background.on('error', console.error) background.startWorker() diff --git a/mascara/src/ui.js b/mascara/src/ui.js index 07763cc5e..65a55ccc3 100644 --- a/mascara/src/ui.js +++ b/mascara/src/ui.js @@ -46,15 +46,8 @@ background.on('ready', (sw) => { background.removeListener('updatefound', connectApp) connectApp(sw) }) -background.on('updatefound', () => background.serviceWorkerApi.ready - .then((sw) =>{ - background.removeListener('ready', connectApp) - connectApp(sw.active) - }) -) -background.on('message', (messageEvent) => { - console.log(messageEvent) -}) -window.addEventListener('load', () => background.startWorker()) +background.on('updatefound', () => window.location.reload()) + +background.startWorker() // background.startWorker() console.log('hello from MetaMascara ui!') -- cgit v1.2.3 From d4cad22fa7b2e9e05ac98a490cb446b37d6978f9 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Fri, 28 Apr 2017 21:19:14 +0200 Subject: clean up code --- mascara/src/ui.js | 1 - mascara/test/index.js | 4 +++- mascara/test/window-load.js | 3 --- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/mascara/src/ui.js b/mascara/src/ui.js index 65a55ccc3..e798847a7 100644 --- a/mascara/src/ui.js +++ b/mascara/src/ui.js @@ -37,7 +37,6 @@ const connectApp = function (readSw) { store.subscribe(() => { const state = store.getState() if (state.appState.shouldClose) window.close() - console.log('IN the things?') }) }) } diff --git a/mascara/test/index.js b/mascara/test/index.js index b94ce16dd..0134cdf00 100644 --- a/mascara/test/index.js +++ b/mascara/test/index.js @@ -8,7 +8,9 @@ var b = browserify(); // Remove old bundle try { fs.unlinkSync(bundlePath) -} catch (e) {} +} catch (e) { + console.error(e) +} var writeStream = fs.createWriteStream(bundlePath) diff --git a/mascara/test/window-load.js b/mascara/test/window-load.js index 41166c466..d3f44f05f 100644 --- a/mascara/test/window-load.js +++ b/mascara/test/window-load.js @@ -1,8 +1,5 @@ const Helper = require('./util/mascara-test-helper.js') window.addEventListener('load', () => { - // const helper = new Helper() - // helper.on('complete', () => require('../src/ui.js')) - // helper.tryToCleanContext() require('../src/ui.js') }) -- cgit v1.2.3 From 6ace0c9afbf7d71415bcf0608977d40210651d39 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 28 Apr 2017 16:04:00 -0700 Subject: notification-manager - remove promise listener seems chrome changed their API? MDN suggests that a Promise should be returned but getting `undefined` https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/windows/create Chrome docs suggest its a callback API lolwut https://developer.chrome.com/extensions/windows#method-create --- app/scripts/lib/notification-manager.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/scripts/lib/notification-manager.js b/app/scripts/lib/notification-manager.js index 799282f6d..7846ef7f0 100644 --- a/app/scripts/lib/notification-manager.js +++ b/app/scripts/lib/notification-manager.js @@ -24,9 +24,6 @@ class NotificationManager { width, height, }) - .catch((reason) => { - log.error('failed to create poupup', reason) - }) } }) } -- cgit v1.2.3 From a21339dd8519d2f99e1762fa57524e6c674cee5e Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 28 Apr 2017 16:10:26 -0700 Subject: Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e629e0bc1..cd71a0339 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Fix bug where error was reported in debugger console when Chrome opened a new window. + ## 3.6.0 2017-4-25 - Add Rinkeby Test Network to our network list. -- cgit v1.2.3 From 527068b84ef17bb88a29b699c93b3f1ed7645b24 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Sun, 30 Apr 2017 12:38:22 -0700 Subject: Bump provider engine --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2e7887e0c..6e83b3560 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,7 @@ "valid-url": "^1.0.9", "vreme": "^3.0.2", "web3": "0.18.2", - "web3-provider-engine": "^12.0.2", + "web3-provider-engine": "^12.0.3", "web3-stream-provider": "^2.0.6", "xtend": "^4.0.1" }, -- cgit v1.2.3 From a3149c17526c320f0cfdbe07a54ca1c2f01a6fad Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Sun, 30 Apr 2017 12:38:38 -0700 Subject: Use loglevel for more logs --- app/scripts/metamask-controller.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b91b5efe8..2e4bf07e1 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -341,9 +341,7 @@ module.exports = class MetamaskController extends EventEmitter { console.error('Error in RPC response:\n', response.error) } if (request.isMetamaskInternal) return - if (global.METAMASK_DEBUG) { - console.log(`RPC (${originDomain}):`, request, '->', response) - } + log.info(`RPC (${originDomain}):`, request, '->', response) } } @@ -591,9 +589,7 @@ module.exports = class MetamaskController extends EventEmitter { // Log blocks logBlock (block) { - if (global.METAMASK_DEBUG) { - console.log(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`) - } + log.info(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`) this.verifyNetwork() } @@ -682,9 +678,7 @@ module.exports = class MetamaskController extends EventEmitter { this.setNetworkState('loading') return } - if (global.METAMASK_DEBUG) { - console.log('web3.getNetwork returned ' + network) - } + log.info('web3.getNetwork returned ' + network) this.setNetworkState(network) }) } -- cgit v1.2.3 From 7ddbd1a193ed4e6d087480cf17a6b486bcd10826 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Sun, 30 Apr 2017 12:39:17 -0700 Subject: Version 3.6.1 --- CHANGELOG.md | 3 +++ app/manifest.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd71a0339..517a1830b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,10 @@ ## Current Master +## 3.6.1 2017-4-25 + - Fix bug where error was reported in debugger console when Chrome opened a new window. +- Fix bug where block-tracker could stop polling for new blocks. ## 3.6.0 2017-4-25 diff --git a/app/manifest.json b/app/manifest.json index d5f66173c..3a9b0b29f 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.6.0", + "version": "3.6.1", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From c3481f0eb1e78de69a02155d7bba68d4a33a3efb Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Sun, 30 Apr 2017 13:33:05 -0700 Subject: Correct change dates --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 517a1830b..2990be3d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,12 @@ ## Current Master -## 3.6.1 2017-4-25 +## 3.6.1 2017-4-30 - Fix bug where error was reported in debugger console when Chrome opened a new window. - Fix bug where block-tracker could stop polling for new blocks. -## 3.6.0 2017-4-25 +## 3.6.0 2017-4-26 - Add Rinkeby Test Network to our network list. -- cgit v1.2.3 From 89b0d3d40318cc119195f295e39a595cac5ff540 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Sun, 30 Apr 2017 18:46:27 -0700 Subject: Make fox look away while typing password Inspired by this tweet: https://twitter.com/Aashay/status/858791285976481792 --- ui/app/unlock.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/app/unlock.js b/ui/app/unlock.js index 1aee3c5d0..de372817d 100644 --- a/ui/app/unlock.js +++ b/ui/app/unlock.js @@ -109,10 +109,10 @@ UnlockScreen.prototype.submitPassword = function (event) { UnlockScreen.prototype.inputChanged = function (event) { // tell mascot to look at page action var element = event.target - var boundingRect = element.getBoundingClientRect() - var coordinates = getCaretCoordinates(element, element.selectionEnd) - this.animationEventEmitter.emit('point', { - x: boundingRect.left + coordinates.left - element.scrollLeft, - y: boundingRect.top + coordinates.top - element.scrollTop, - }) + var viewRect = element.getBoundingClientRect() + var carat = getCaretCoordinates(element, element.selectionEnd) + var x = viewRect.right - carat.left + element.scrollLeft + var y = 300 + var pointAt = { x, y } + this.animationEventEmitter.emit('point', pointAt) } -- cgit v1.2.3 From 53cecf77a2ae5a764e31797ca41d1a19cdb86063 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Sun, 30 Apr 2017 18:54:57 -0700 Subject: Adjust fox look height --- ui/app/unlock.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/unlock.js b/ui/app/unlock.js index de372817d..5014744f5 100644 --- a/ui/app/unlock.js +++ b/ui/app/unlock.js @@ -112,7 +112,7 @@ UnlockScreen.prototype.inputChanged = function (event) { var viewRect = element.getBoundingClientRect() var carat = getCaretCoordinates(element, element.selectionEnd) var x = viewRect.right - carat.left + element.scrollLeft - var y = 300 + var y = 100 var pointAt = { x, y } this.animationEventEmitter.emit('point', pointAt) } -- cgit v1.2.3 From 791c8fccf8e4049b90f096968dc57d9dab7b3c7a Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Sun, 30 Apr 2017 18:56:11 -0700 Subject: Bump changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 517a1830b..ddbf0808c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Made fox less nosy. + ## 3.6.1 2017-4-25 - Fix bug where error was reported in debugger console when Chrome opened a new window. -- cgit v1.2.3 From 061f56b2070c4f4720a147a901e8879ca193f2ae Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 3 May 2017 07:21:42 -0700 Subject: Fox watches over us again. --- ui/app/unlock.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/app/unlock.js b/ui/app/unlock.js index 5014744f5..1aee3c5d0 100644 --- a/ui/app/unlock.js +++ b/ui/app/unlock.js @@ -109,10 +109,10 @@ UnlockScreen.prototype.submitPassword = function (event) { UnlockScreen.prototype.inputChanged = function (event) { // tell mascot to look at page action var element = event.target - var viewRect = element.getBoundingClientRect() - var carat = getCaretCoordinates(element, element.selectionEnd) - var x = viewRect.right - carat.left + element.scrollLeft - var y = 100 - var pointAt = { x, y } - this.animationEventEmitter.emit('point', pointAt) + var boundingRect = element.getBoundingClientRect() + var coordinates = getCaretCoordinates(element, element.selectionEnd) + this.animationEventEmitter.emit('point', { + x: boundingRect.left + coordinates.left - element.scrollLeft, + y: boundingRect.top + coordinates.top - element.scrollTop, + }) } -- cgit v1.2.3 From 833b9f183fca17599d4d225eeec077ddbc7bc7b0 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 3 May 2017 07:22:36 -0700 Subject: Minor lint --- app/scripts/lib/config-manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index 340ad4292..ab9410842 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -155,7 +155,7 @@ ConfigManager.prototype.getCurrentRpcAddress = function () { case 'kovan': return KOVAN_RPC - + case 'rinkeby': return RINKEBY_RPC -- cgit v1.2.3 From 1895fa1489f758714d7ff5dee870eac6507492e7 Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Thu, 4 May 2017 14:32:42 -0700 Subject: Add mocha and chai plugins eslint --- .eslintrc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index 8bbfe13c7..91c95874e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -17,10 +17,13 @@ "env": { "es6": true, "node": true, - "browser": true + "browser": true, + "mocha" : true }, "plugins": [ + "mocha", + "chai" ], "globals": { -- cgit v1.2.3 From 73ee4bdec35aa2af744c2cbb61146a1cca49ebe9 Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Thu, 4 May 2017 14:33:29 -0700 Subject: Ignore tests bundle, jquery, abd helpers --- .eslintignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.eslintignore b/.eslintignore index df49525be..b96f79011 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,5 @@ app/scripts/lib/extension-instance.js +test/integration/bundle.js +test/integration/jquery-3.1.0.min.js +test/integration/helpers.js +test/integration/lib/first-time.js \ No newline at end of file -- cgit v1.2.3 From 8f5334e4aca8b95963f57b0fbf862256b692dbd9 Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Thu, 4 May 2017 14:34:25 -0700 Subject: Add Mocha/Chai eslint plugins --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index 6e83b3560..ee7ba7aab 100644 --- a/package.json +++ b/package.json @@ -135,6 +135,8 @@ "deep-freeze-strict": "^1.1.1", "del": "^2.2.0", "envify": "^4.0.0", + "eslint-plugin-chai": "0.0.1", + "eslint-plugin-mocha": "^4.9.0", "fs-promise": "^1.0.0", "gulp": "github:gulpjs/gulp#4.0", "gulp-if": "^2.0.1", -- cgit v1.2.3 From 0b13429daf00ddd5bdf2705c7a95d7a9d5792f54 Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Thu, 4 May 2017 14:35:10 -0700 Subject: Lint tests --- app/scripts/lib/config-manager.js | 2 +- test/helper.js | 8 +- test/integration/helpers.js | 4 +- test/integration/index.js | 8 +- test/lib/mock-config-manager.js | 8 +- test/lib/mock-encryptor.js | 12 +-- test/lib/mock-simple-keychain.js | 14 +-- test/unit/account-link-test.js | 8 +- test/unit/actions/config_test.js | 23 +++-- test/unit/actions/save_account_label_test.js | 21 +++-- test/unit/actions/set_selected_account_test.js | 23 +++-- test/unit/actions/tx_test.js | 92 +++++++++---------- test/unit/actions/view_info_test.js | 15 ++-- test/unit/actions/warning_test.js | 13 ++- test/unit/address-book-controller.js | 17 ++-- test/unit/components/binary-renderer-test.js | 10 +-- test/unit/config-manager-test.js | 45 +++++----- test/unit/currency-controller-test.js | 45 +++++----- test/unit/explorer-link-test.js | 7 +- test/unit/keyring-controller-test.js | 63 +++++++------ test/unit/linting_test.js | 6 +- test/unit/message-manager-test.js | 42 ++++----- test/unit/metamask-controller-test.js | 16 ++-- test/unit/migrations-test.js | 4 +- test/unit/nameForAccount_test.js | 17 ++-- test/unit/nodeify-test.js | 8 +- test/unit/notice-controller-test.js | 75 ++++++++-------- test/unit/personal-message-manager-test.js | 49 +++++----- test/unit/reducers/unlock_vault_test.js | 27 +++--- test/unit/tx-manager-test.js | 94 ++++++++++--------- test/unit/tx-utils-test.js | 26 +++--- test/unit/util_test.js | 119 ++++++++++++------------- 32 files changed, 439 insertions(+), 482 deletions(-) diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index 340ad4292..ab9410842 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -155,7 +155,7 @@ ConfigManager.prototype.getCurrentRpcAddress = function () { case 'kovan': return KOVAN_RPC - + case 'rinkeby': return RINKEBY_RPC diff --git a/test/helper.js b/test/helper.js index aaac7580e..1c5934a89 100644 --- a/test/helper.js +++ b/test/helper.js @@ -20,14 +20,12 @@ window.localStorage = {} if (!window.crypto) window.crypto = {} if (!window.crypto.getRandomValues) window.crypto.getRandomValues = require('polyfill-crypto.getrandomvalues') - - -function enableFailureOnUnhandledPromiseRejection() { +function enableFailureOnUnhandledPromiseRejection () { // overwrite node's promise with the stricter Bluebird promise global.Promise = require('bluebird') // modified from https://github.com/mochajs/mocha/issues/1926#issuecomment-180842722 - + // rethrow unhandledRejections if (typeof process !== 'undefined') { process.on('unhandledRejection', function (reason) { @@ -51,4 +49,4 @@ function enableFailureOnUnhandledPromiseRejection() { typeof (console.error || console.log) === 'function') { (console.error || console.log)('Unhandled rejections will be ignored!') } -} \ No newline at end of file +} diff --git a/test/integration/helpers.js b/test/integration/helpers.js index eede103b4..10cd74e64 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -1,6 +1,6 @@ function wait(time) { - return new Promise(function(resolve, reject) { - setTimeout(function() { + return new Promise(function (resolve, reject) { + setTimeout(function () { resolve() }, time * 3 || 1500) }) diff --git a/test/integration/index.js b/test/integration/index.js index ff6d1baf8..f2d656b0b 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -1,10 +1,10 @@ var fs = require('fs') var path = require('path') -var browserify = require('browserify'); +var browserify = require('browserify') var tests = fs.readdirSync(path.join(__dirname, 'lib')) var bundlePath = path.join(__dirname, 'bundle.js') -var b = browserify(); +var b = browserify() // Remove old bundle try { @@ -13,9 +13,9 @@ try { var writeStream = fs.createWriteStream(bundlePath) -tests.forEach(function(fileName) { +tests.forEach(function (fileName) { b.add(path.join(__dirname, 'lib', fileName)) }) -b.bundle().pipe(writeStream); +b.bundle().pipe(writeStream) diff --git a/test/lib/mock-config-manager.js b/test/lib/mock-config-manager.js index 72be86ed1..3238d3501 100644 --- a/test/lib/mock-config-manager.js +++ b/test/lib/mock-config-manager.js @@ -2,9 +2,9 @@ const ObservableStore = require('obs-store') const clone = require('clone') const ConfigManager = require('../../app/scripts/lib/config-manager') const firstTimeState = require('../../app/scripts/first-time-state') -const STORAGE_KEY = 'metamask-config' +// const STORAGE_KEY = 'metamask-config' -module.exports = function() { - let store = new ObservableStore(clone(firstTimeState)) +module.exports = function () { + const store = new ObservableStore(clone(firstTimeState)) return new ConfigManager({ store }) -} \ No newline at end of file +} diff --git a/test/lib/mock-encryptor.js b/test/lib/mock-encryptor.js index 09bbf7ad5..cdf13c507 100644 --- a/test/lib/mock-encryptor.js +++ b/test/lib/mock-encryptor.js @@ -4,28 +4,28 @@ let cacheVal module.exports = { - encrypt(password, dataObj) { + encrypt (password, dataObj) { cacheVal = dataObj return Promise.resolve(mockHex) }, - decrypt(password, text) { + decrypt (password, text) { return Promise.resolve(cacheVal || {}) }, - encryptWithKey(key, dataObj) { + encryptWithKey (key, dataObj) { return this.encrypt(key, dataObj) }, - decryptWithKey(key, text) { + decryptWithKey (key, text) { return this.decrypt(key, text) }, - keyFromPassword(password) { + keyFromPassword (password) { return Promise.resolve(mockKey) }, - generateSalt() { + generateSalt () { return 'WHADDASALT!' }, diff --git a/test/lib/mock-simple-keychain.js b/test/lib/mock-simple-keychain.js index 615b3e537..d3addc3e8 100644 --- a/test/lib/mock-simple-keychain.js +++ b/test/lib/mock-simple-keychain.js @@ -6,32 +6,32 @@ const type = 'Simple Key Pair' module.exports = class MockSimpleKeychain { - static type() { return type } + static type () { return type } - constructor(opts) { + constructor (opts) { this.type = type this.opts = opts || {} this.wallets = [] } - serialize() { + serialize () { return [ fakeWallet.privKey ] } - deserialize(data) { + deserialize (data) { if (!Array.isArray(data)) { throw new Error('Simple keychain deserialize requires a privKey array.') } this.wallets = [ fakeWallet ] } - addAccounts(n = 1) { - for(var i = 0; i < n; i++) { + addAccounts (n = 1) { + for (var i = 0; i < n; i++) { this.wallets.push(fakeWallet) } } - getAccounts() { + getAccounts () { return this.wallets.map(w => w.address) } diff --git a/test/unit/account-link-test.js b/test/unit/account-link-test.js index 803a70f37..47a961d1f 100644 --- a/test/unit/account-link-test.js +++ b/test/unit/account-link-test.js @@ -1,18 +1,16 @@ var assert = require('assert') var linkGen = require('../../ui/lib/account-link') -describe('account-link', function() { - - it('adds ropsten prefix to ropsten test network', function() { +describe('account-link', function () { + it('adds ropsten prefix to ropsten test network', function () { var result = linkGen('account', '3') assert.notEqual(result.indexOf('ropsten'), -1, 'ropsten included') assert.notEqual(result.indexOf('account'), -1, 'account included') }) - it('adds kovan prefix to kovan test network', function() { + it('adds kovan prefix to kovan test network', function () { var result = linkGen('account', '42') assert.notEqual(result.indexOf('kovan'), -1, 'kovan included') assert.notEqual(result.indexOf('account'), -1, 'account included') }) - }) diff --git a/test/unit/actions/config_test.js b/test/unit/actions/config_test.js index 14198fa8a..648f456fb 100644 --- a/test/unit/actions/config_test.js +++ b/test/unit/actions/config_test.js @@ -1,36 +1,34 @@ -var jsdom = require('mocha-jsdom') +// var jsdom = require('mocha-jsdom') var assert = require('assert') var freeze = require('deep-freeze-strict') var path = require('path') var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) -var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) - -describe ('config view actions', function() { +var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) +describe('config view actions', function () { var initialState = { metamask: { rpcTarget: 'foo', - frequentRpcList: [] + frequentRpcList: [], }, appState: { currentView: { name: 'accounts', - } - } + }, + }, } freeze(initialState) - describe('SHOW_CONFIG_PAGE', function() { - it('should set appState.currentView.name to config', function() { + describe('SHOW_CONFIG_PAGE', function () { + it('should set appState.currentView.name to config', function () { var result = reducers(initialState, actions.showConfigPage()) assert.equal(result.appState.currentView.name, 'config') }) }) - describe('SET_RPC_TARGET', function() { - - it('sets the state.metamask.rpcTarget property of the state to the action.value', function() { + describe('SET_RPC_TARGET', function () { + it('sets the state.metamask.rpcTarget property of the state to the action.value', function () { const action = { type: actions.SET_RPC_TARGET, value: 'foo', @@ -41,5 +39,4 @@ describe ('config view actions', function() { assert.equal(result.metamask.provider.rpcTarget, 'foo') }) }) - }) diff --git a/test/unit/actions/save_account_label_test.js b/test/unit/actions/save_account_label_test.js index 1df428b1d..c5ffd6cbf 100644 --- a/test/unit/actions/save_account_label_test.js +++ b/test/unit/actions/save_account_label_test.js @@ -1,22 +1,21 @@ -var jsdom = require('mocha-jsdom') +// var jsdom = require('mocha-jsdom') var assert = require('assert') var freeze = require('deep-freeze-strict') var path = require('path') var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) -var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) +var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) -describe('SAVE_ACCOUNT_LABEL', function() { - - it('updates the state.metamask.identities[:i].name property of the state to the action.value.label', function() { +describe('SAVE_ACCOUNT_LABEL', function () { + it('updates the state.metamask.identities[:i].name property of the state to the action.value.label', function () { var initialState = { metamask: { identities: { foo: { - name: 'bar' - } + name: 'bar', + }, }, - } + }, } freeze(initialState) @@ -24,13 +23,13 @@ describe('SAVE_ACCOUNT_LABEL', function() { type: actions.SAVE_ACCOUNT_LABEL, value: { account: 'foo', - label: 'baz' + label: 'baz', }, } freeze(action) var resultingState = reducers(initialState, action) assert.equal(resultingState.metamask.identities.foo.name, action.value.label) - }); -}); + }) +}) diff --git a/test/unit/actions/set_selected_account_test.js b/test/unit/actions/set_selected_account_test.js index 2dc42d2ec..28b47d09d 100644 --- a/test/unit/actions/set_selected_account_test.js +++ b/test/unit/actions/set_selected_account_test.js @@ -1,18 +1,17 @@ -var jsdom = require('mocha-jsdom') +// var jsdom = require('mocha-jsdom') var assert = require('assert') var freeze = require('deep-freeze-strict') var path = require('path') var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) -var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) +var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) -describe('SET_SELECTED_ACCOUNT', function() { - - it('sets the state.appState.activeAddress property of the state to the action.value', function() { +describe('SET_SELECTED_ACCOUNT', function () { + it('sets the state.appState.activeAddress property of the state to the action.value', function () { var initialState = { appState: { activeAddress: 'foo', - } + }, } freeze(initialState) @@ -24,15 +23,15 @@ describe('SET_SELECTED_ACCOUNT', function() { var resultingState = reducers(initialState, action) assert.equal(resultingState.appState.activeAddress, action.value) - }); -}); + }) +}) -describe('SHOW_ACCOUNT_DETAIL', function() { - it('updates metamask state', function() { +describe('SHOW_ACCOUNT_DETAIL', function () { + it('updates metamask state', function () { var initialState = { metamask: { - selectedAddress: 'foo' - } + selectedAddress: 'foo', + }, } freeze(initialState) diff --git a/test/unit/actions/tx_test.js b/test/unit/actions/tx_test.js index bd72a666e..0ea1bfdc7 100644 --- a/test/unit/actions/tx_test.js +++ b/test/unit/actions/tx_test.js @@ -1,29 +1,27 @@ -var jsdom = require('mocha-jsdom') +// var jsdom = require('mocha-jsdom') var assert = require('assert') var freeze = require('deep-freeze-strict') var path = require('path') var sinon = require('sinon') var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) -var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) +var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) -describe('tx confirmation screen', function() { - - beforeEach(function() { - this.sinon = sinon.sandbox.create(); - }); +describe('tx confirmation screen', function () { + beforeEach(function () { + this.sinon = sinon.sandbox.create() + }) - afterEach(function(){ - this.sinon.restore(); - }); + afterEach(function () { + this.sinon.restore() + }) var initialState, result - describe('when there is only one tx', function() { + describe('when there is only one tx', function () { var firstTxId = 1457634084250832 - beforeEach(function() { - + beforeEach(function () { initialState = { appState: { currentView: { @@ -34,70 +32,66 @@ describe('tx confirmation screen', function() { unapprovedTxs: { '1457634084250832': { id: 1457634084250832, - status: "unconfirmed", + status: 'unconfirmed', time: 1457634084250, - } + }, }, - } + }, } freeze(initialState) }) - describe('cancelTx', function() { - - before(function(done) { + describe('cancelTx', function () { + before(function (done) { actions._setBackgroundConnection({ - approveTransaction(txId, cb) { cb('An error!') }, - cancelTransaction(txId) { /* noop */ }, - clearSeedWordCache(cb) { cb() }, + approveTransaction (txId, cb) { cb('An error!') }, + cancelTransaction (txId) { /* noop */ }, + clearSeedWordCache (cb) { cb() }, }) - let action = actions.cancelTx({value: firstTxId}) + const action = actions.cancelTx({value: firstTxId}) result = reducers(initialState, action) done() }) - it('should transition to the account detail view', function() { + it('should transition to the account detail view', function () { assert.equal(result.appState.currentView.name, 'accountDetail') }) - it('should have no unconfirmed txs remaining', function() { + it('should have no unconfirmed txs remaining', function () { var count = getUnconfirmedTxCount(result) assert.equal(count, 0) }) }) - describe('sendTx', function() { + describe('sendTx', function () { var result - describe('when there is an error', function() { - - before(function(done) { - alert = () => {/* noop */} - + describe('when there is an error', function () { + before(function (done) { actions._setBackgroundConnection({ - approveTransaction(txId, cb) { cb({message: 'An error!'}) }, + approveTransaction (txId, cb) { cb({message: 'An error!'}) }, }) - actions.sendTx({id: firstTxId})(function(action) { + actions.sendTx({id: firstTxId})(function (action) { result = reducers(initialState, action) done() }) }) - it('should stay on the page', function() { + it('should stay on the page', function () { assert.equal(result.appState.currentView.name, 'confTx') }) - it('should set errorMessage on the currentView', function() { + it('should set errorMessage on the currentView', function () { assert(result.appState.currentView.errorMessage) }) }) - describe('when there is success', function() { - it('should complete tx and go home', function() { + describe('when there is success', function () { + it('should complete tx and go home', function () { actions._setBackgroundConnection({ - approveTransaction(txId, cb) { cb() }, + approveTransaction (txId, cb) { cb() }, }) var dispatchExpect = sinon.mock() @@ -108,10 +102,10 @@ describe('tx confirmation screen', function() { }) }) - describe('when there are two pending txs', function() { + describe('when there are two pending txs', function () { var firstTxId = 1457634084250832 var result, initialState - before(function(done) { + before(function (done) { initialState = { appState: { currentView: { @@ -122,42 +116,42 @@ describe('tx confirmation screen', function() { unapprovedTxs: { '1457634084250832': { id: firstTxId, - status: "unconfirmed", + status: 'unconfirmed', time: 1457634084250, }, '1457634084250833': { id: 1457634084250833, - status: "unconfirmed", + status: 'unconfirmed', time: 1457634084255, }, }, - } + }, } freeze(initialState) // Mocking a background connection: actions._setBackgroundConnection({ - approveTransaction(firstTxId, cb) { cb() }, + approveTransaction (firstTxId, cb) { cb() }, }) - let action = actions.sendTx({id: firstTxId})(function(action) { + actions.sendTx({id: firstTxId})(function (action) { result = reducers(initialState, action) }) done() }) - it('should stay on the confTx view', function() { + it('should stay on the confTx view', function () { assert.equal(result.appState.currentView.name, 'confTx') }) - it('should transition to the first tx', function() { + it('should transition to the first tx', function () { assert.equal(result.appState.currentView.context, 0) }) }) }) -}); +}) -function getUnconfirmedTxCount(state) { +function getUnconfirmedTxCount (state) { var txs = state.metamask.unapprovedTxs var count = Object.keys(txs).length return count diff --git a/test/unit/actions/view_info_test.js b/test/unit/actions/view_info_test.js index 0558c6e42..69895d801 100644 --- a/test/unit/actions/view_info_test.js +++ b/test/unit/actions/view_info_test.js @@ -1,23 +1,22 @@ -var jsdom = require('mocha-jsdom') +// var jsdom = require('mocha-jsdom') var assert = require('assert') var freeze = require('deep-freeze-strict') var path = require('path') var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) -var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) +var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) -describe('SHOW_INFO_PAGE', function() { - - it('sets the state.appState.currentView.name property to info', function() { +describe('SHOW_INFO_PAGE', function () { + it('sets the state.appState.currentView.name property to info', function () { var initialState = { appState: { activeAddress: 'foo', - } + }, } freeze(initialState) const action = actions.showInfoPage() var resultingState = reducers(initialState, action) assert.equal(resultingState.appState.currentView.name, 'info') - }); -}); + }) +}) diff --git a/test/unit/actions/warning_test.js b/test/unit/actions/warning_test.js index 37be9ee85..28b565499 100644 --- a/test/unit/actions/warning_test.js +++ b/test/unit/actions/warning_test.js @@ -1,14 +1,13 @@ -var jsdom = require('mocha-jsdom') +// var jsdom = require('mocha-jsdom') var assert = require('assert') var freeze = require('deep-freeze-strict') var path = require('path') var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) -var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) +var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) -describe('action DISPLAY_WARNING', function() { - - it('sets appState.warning to provided value', function() { +describe('action DISPLAY_WARNING', function () { + it('sets appState.warning to provided value', function () { var initialState = { appState: {}, } @@ -20,5 +19,5 @@ describe('action DISPLAY_WARNING', function() { const resultingState = reducers(initialState, action) assert.equal(resultingState.appState.warning, warningText, 'warning text set') - }); -}); + }) +}) diff --git a/test/unit/address-book-controller.js b/test/unit/address-book-controller.js index f345b0328..85deb8905 100644 --- a/test/unit/address-book-controller.js +++ b/test/unit/address-book-controller.js @@ -1,5 +1,5 @@ const assert = require('assert') -const extend = require('xtend') +// const extend = require('xtend') const AddressBookController = require('../../app/scripts/controllers/address-book') const mockKeyringController = { @@ -7,21 +7,20 @@ const mockKeyringController = { getState: function () { return { identities: { - '0x0aaa' : { + '0x0aaa': { address: '0x0aaa', name: 'owned', - } - } + }, + }, } - } - } + }, + }, } - -describe('address-book-controller', function() { +describe('address-book-controller', function () { var addressBookController - beforeEach(function() { + beforeEach(function () { addressBookController = new AddressBookController({}, mockKeyringController) }) diff --git a/test/unit/components/binary-renderer-test.js b/test/unit/components/binary-renderer-test.js index 3264faddc..ee2fa8b60 100644 --- a/test/unit/components/binary-renderer-test.js +++ b/test/unit/components/binary-renderer-test.js @@ -1,24 +1,22 @@ var assert = require('assert') var BinaryRenderer = require('../../../ui/app/components/binary-renderer') -describe('BinaryRenderer', function() { - +describe('BinaryRenderer', function () { let binaryRenderer const message = 'Hello, world!' const buffer = new Buffer(message, 'utf8') const hex = buffer.toString('hex') - beforeEach(function() { + beforeEach(function () { binaryRenderer = new BinaryRenderer() }) - it('recovers message', function() { + it('recovers message', function () { const result = binaryRenderer.hexToText(hex) assert.equal(result, message) }) - - it('recovers message with hex prefix', function() { + it('recovers message with hex prefix', function () { const result = binaryRenderer.hexToText('0x' + hex) assert.equal(result, message) }) diff --git a/test/unit/config-manager-test.js b/test/unit/config-manager-test.js index 05324e741..eeb193e91 100644 --- a/test/unit/config-manager-test.js +++ b/test/unit/config-manager-test.js @@ -2,26 +2,25 @@ global.fetch = global.fetch || require('isomorphic-fetch') const assert = require('assert') -const extend = require('xtend') -const rp = require('request-promise') -const nock = require('nock') +// const extend = require('xtend') +// const rp = require('request-promise') +// const nock = require('nock') const configManagerGen = require('../lib/mock-config-manager') -describe('config-manager', function() { +describe('config-manager', function () { var configManager - beforeEach(function() { + beforeEach(function () { configManager = configManagerGen() }) - describe('#setConfig', function() { - + describe('#setConfig', function () { it('should set the config key', function () { var testConfig = { provider: { type: 'rpc', - rpcTarget: 'foobar' - } + rpcTarget: 'foobar', + }, } configManager.setConfig(testConfig) var result = configManager.getData() @@ -30,17 +29,17 @@ describe('config-manager', function() { assert.equal(result.config.provider.rpcTarget, testConfig.provider.rpcTarget) }) - it('setting wallet should not overwrite config', function() { + it('setting wallet should not overwrite config', function () { var testConfig = { provider: { type: 'rpc', - rpcTarget: 'foobar' + rpcTarget: 'foobar', }, } configManager.setConfig(testConfig) var testWallet = { - name: 'this is my fake wallet' + name: 'this is my fake wallet', } configManager.setWallet(testWallet) @@ -58,13 +57,13 @@ describe('config-manager', function() { }) }) - describe('wallet nicknames', function() { - it('should return null when no nicknames are saved', function() { + describe('wallet nicknames', function () { + it('should return null when no nicknames are saved', function () { var nick = configManager.nicknameForWallet('0x0') assert.equal(nick, null, 'no nickname returned') }) - it('should persist nicknames', function() { + it('should persist nicknames', function () { var account = '0x0' var nick1 = 'foo' var nick2 = 'bar' @@ -79,8 +78,8 @@ describe('config-manager', function() { }) }) - describe('rpc manipulations', function() { - it('changing rpc should return a different rpc', function() { + describe('rpc manipulations', function () { + it('changing rpc should return a different rpc', function () { var firstRpc = 'first' var secondRpc = 'second' @@ -94,21 +93,21 @@ describe('config-manager', function() { }) }) - describe('transactions', function() { - beforeEach(function() { + describe('transactions', function () { + beforeEach(function () { configManager.setTxList([]) }) - describe('#getTxList', function() { - it('when new should return empty array', function() { + describe('#getTxList', function () { + it('when new should return empty array', function () { var result = configManager.getTxList() assert.ok(Array.isArray(result)) assert.equal(result.length, 0) }) }) - describe('#setTxList', function() { - it('saves the submitted data to the tx list', function() { + describe('#setTxList', function () { + it('saves the submitted data to the tx list', function () { var target = [{ foo: 'bar' }] configManager.setTxList(target) var result = configManager.getTxList() diff --git a/test/unit/currency-controller-test.js b/test/unit/currency-controller-test.js index 079f8b488..40868912c 100644 --- a/test/unit/currency-controller-test.js +++ b/test/unit/currency-controller-test.js @@ -2,26 +2,25 @@ global.fetch = global.fetch || require('isomorphic-fetch') const assert = require('assert') -const extend = require('xtend') -const rp = require('request-promise') +// const extend = require('xtend') +// const rp = require('request-promise') const nock = require('nock') const CurrencyController = require('../../app/scripts/controllers/currency') -describe('currency-controller', function() { +describe('currency-controller', function () { var currencyController - beforeEach(function() { + beforeEach(function () { currencyController = new CurrencyController() }) - describe('currency conversions', function() { - - describe('#setCurrentCurrency', function() { - it('should return USD as default', function() { + describe('currency conversions', function () { + describe('#setCurrentCurrency', function () { + it('should return USD as default', function () { assert.equal(currencyController.getCurrentCurrency(), 'USD') }) - it('should be able to set to other currency', function() { + it('should be able to set to other currency', function () { assert.equal(currencyController.getCurrentCurrency(), 'USD') currencyController.setCurrentCurrency('JPY') var result = currencyController.getCurrentCurrency() @@ -29,39 +28,38 @@ describe('currency-controller', function() { }) }) - describe('#getConversionRate', function() { - it('should return undefined if non-existent', function() { + describe('#getConversionRate', function () { + it('should return undefined if non-existent', function () { var result = currencyController.getConversionRate() assert.ok(!result) }) }) - describe('#updateConversionRate', function() { - it('should retrieve an update for ETH to USD and set it in memory', function(done) { + describe('#updateConversionRate', function () { + it('should retrieve an update for ETH to USD and set it in memory', function (done) { this.timeout(15000) - var usdMock = nock('https://www.cryptonator.com') + nock('https://www.cryptonator.com') .get('/api/ticker/eth-USD') .reply(200, '{"ticker":{"base":"ETH","target":"USD","price":"11.02456145","volume":"44948.91745289","change":"-0.01472534"},"timestamp":1472072136,"success":true,"error":""}') assert.equal(currencyController.getConversionRate(), 0) currencyController.setCurrentCurrency('USD') currencyController.updateConversionRate() - .then(function() { + .then(function () { var result = currencyController.getConversionRate() console.log('currencyController.getConversionRate:', result) assert.equal(typeof result, 'number') done() - }).catch(function(err) { + }).catch(function (err) { done(err) }) - }) - it('should work for JPY as well.', function() { + it('should work for JPY as well.', function () { this.timeout(15000) assert.equal(currencyController.getConversionRate(), 0) - var jpyMock = nock('https://www.cryptonator.com') + nock('https://www.cryptonator.com') .get('/api/ticker/eth-JPY') .reply(200, '{"ticker":{"base":"ETH","target":"JPY","price":"11.02456145","volume":"44948.91745289","change":"-0.01472534"},"timestamp":1472072136,"success":true,"error":""}') @@ -69,19 +67,18 @@ describe('currency-controller', function() { var promise = new Promise( function (resolve, reject) { currencyController.setCurrentCurrency('JPY') - currencyController.updateConversionRate().then(function() { + currencyController.updateConversionRate().then(function () { resolve() }) - }) + }) - promise.then(function() { + promise.then(function () { var result = currencyController.getConversionRate() assert.equal(typeof result, 'number') - }).catch(function(err) { + }).catch(function (done, err) { done(err) }) }) }) }) - }) diff --git a/test/unit/explorer-link-test.js b/test/unit/explorer-link-test.js index 4f0230c2c..e672b36ed 100644 --- a/test/unit/explorer-link-test.js +++ b/test/unit/explorer-link-test.js @@ -1,14 +1,13 @@ var assert = require('assert') var linkGen = require('../../ui/lib/explorer-link') -describe('explorer-link', function() { - - it('adds ropsten prefix to ropsten test network', function() { +describe('explorer-link', function () { + it('adds ropsten prefix to ropsten test network', function () { var result = linkGen('hash', '3') assert.notEqual(result.indexOf('ropsten'), -1, 'ropsten injected') }) - it('adds kovan prefix to kovan test network', function() { + it('adds kovan prefix to kovan test network', function () { var result = linkGen('hash', '42') assert.notEqual(result.indexOf('kovan'), -1, 'kovan injected') }) diff --git a/test/unit/keyring-controller-test.js b/test/unit/keyring-controller-test.js index efd0a3546..f7e2ec89d 100644 --- a/test/unit/keyring-controller-test.js +++ b/test/unit/keyring-controller-test.js @@ -3,21 +3,20 @@ const KeyringController = require('../../app/scripts/keyring-controller') const configManagerGen = require('../lib/mock-config-manager') const ethUtil = require('ethereumjs-util') const BN = ethUtil.BN -const async = require('async') +// const async = require('async') const mockEncryptor = require('../lib/mock-encryptor') -const MockSimpleKeychain = require('../lib/mock-simple-keychain') +// const MockSimpleKeychain = require('../lib/mock-simple-keychain') const sinon = require('sinon') -describe('KeyringController', function() { +describe('KeyringController', function () { + let keyringController + const password = 'password123' + const seedWords = 'puzzle seed penalty soldier say clay field arctic metal hen cage runway' + const addresses = ['eF35cA8EbB9669A35c31b5F6f249A9941a812AC1'.toLowerCase()] + const accounts = [] + // let originalKeystore - let keyringController, state - let password = 'password123' - let seedWords = 'puzzle seed penalty soldier say clay field arctic metal hen cage runway' - let addresses = ['eF35cA8EbB9669A35c31b5F6f249A9941a812AC1'.toLowerCase()] - let accounts = [] - let originalKeystore - - beforeEach(function(done) { + beforeEach(function (done) { this.sinon = sinon.sandbox.create() window.localStorage = {} // Hacking localStorage support into JSDom @@ -25,10 +24,10 @@ describe('KeyringController', function() { configManager: configManagerGen(), txManager: { getTxList: () => [], - getUnapprovedTxList: () => [] + getUnapprovedTxList: () => [], }, ethStore: { - addAccount(acct) { accounts.push(ethUtil.addHexPrefix(acct)) }, + addAccount (acct) { accounts.push(ethUtil.addHexPrefix(acct)) }, }, }) @@ -38,7 +37,7 @@ describe('KeyringController', function() { keyringController.createNewVaultAndKeychain(password) .then(function (newState) { - state = newState + newState done() }) .catch((err) => { @@ -46,7 +45,7 @@ describe('KeyringController', function() { }) }) - afterEach(function() { + afterEach(function () { // Cleanup mocks this.sinon.restore() }) @@ -54,7 +53,7 @@ describe('KeyringController', function() { describe('#createNewVaultAndKeychain', function () { this.timeout(10000) - it('should set a vault on the configManager', function(done) { + it('should set a vault on the configManager', function (done) { keyringController.store.updateState({ vault: null }) assert(!keyringController.store.getState().vault, 'no previous vault') keyringController.createNewVaultAndKeychain(password) @@ -69,15 +68,14 @@ describe('KeyringController', function() { }) }) - describe('#restoreKeyring', function() { - - it(`should pass a keyring's serialized data back to the correct type.`, function(done) { + describe('#restoreKeyring', function () { + it(`should pass a keyring's serialized data back to the correct type.`, function (done) { const mockSerialized = { type: 'HD Key Tree', data: { mnemonic: seedWords, numberOfAccounts: 1, - } + }, } const mock = this.sinon.mock(keyringController) @@ -100,8 +98,8 @@ describe('KeyringController', function() { }) }) - describe('#createNickname', function() { - it('should add the address to the identities hash', function() { + describe('#createNickname', function () { + it('should add the address to the identities hash', function () { const fakeAddress = '0x12345678' keyringController.createNickname(fakeAddress) const identities = keyringController.memStore.getState().identities @@ -110,8 +108,8 @@ describe('KeyringController', function() { }) }) - describe('#saveAccountLabel', function() { - it ('sets the nickname', function(done) { + describe('#saveAccountLabel', function () { + it('sets the nickname', function (done) { const account = addresses[0] var nick = 'Test nickname' const identities = keyringController.memStore.getState().identities @@ -134,31 +132,30 @@ describe('KeyringController', function() { }) }) - describe('#getAccounts', function() { - it('returns the result of getAccounts for each keyring', function(done) { + describe('#getAccounts', function () { + it('returns the result of getAccounts for each keyring', function (done) { keyringController.keyrings = [ - { getAccounts() { return Promise.resolve([1,2,3]) } }, - { getAccounts() { return Promise.resolve([4,5,6]) } }, + { getAccounts () { return Promise.resolve([1, 2, 3]) } }, + { getAccounts () { return Promise.resolve([4, 5, 6]) } }, ] keyringController.getAccounts() .then((result) => { - assert.deepEqual(result, [1,2,3,4,5,6]) + assert.deepEqual(result, [1, 2, 3, 4, 5, 6]) done() }) }) }) - describe('#addGasBuffer', function() { - it('adds 100k gas buffer to estimates', function() { - + describe('#addGasBuffer', function () { + it('adds 100k gas buffer to estimates', function () { const gas = '0x04ee59' // Actual estimated gas example const tooBigOutput = '0x80674f9' // Actual bad output const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16) const correctBuffer = new BN('100000', 10) const correct = bnGas.add(correctBuffer) - const tooBig = new BN(tooBigOutput, 16) + // const tooBig = new BN(tooBigOutput, 16) const result = keyringController.addGasBuffer(gas) const bnResult = new BN(ethUtil.stripHexPrefix(result), 16) diff --git a/test/unit/linting_test.js b/test/unit/linting_test.js index 75d90652d..45578fc36 100644 --- a/test/unit/linting_test.js +++ b/test/unit/linting_test.js @@ -1,9 +1,9 @@ // LINTING: -const lint = require('mocha-eslint'); -const lintPaths = ['app/**/*.js', 'ui/**/*.js', '!node_modules/**', '!dist/**', '!docs/**', '!app/scripts/chromereload.js'] +const lint = require('mocha-eslint') +const lintPaths = ['app/**/*.js', 'ui/**/*.js', 'test/**/*.js', '!node_modules/**', '!dist/**', '!docs/**', '!app/scripts/chromereload.js'] const lintOptions = { strict: false, } -lint(lintPaths, lintOptions) \ No newline at end of file +lint(lintPaths, lintOptions) diff --git a/test/unit/message-manager-test.js b/test/unit/message-manager-test.js index faf7429d4..44190c8d0 100644 --- a/test/unit/message-manager-test.js +++ b/test/unit/message-manager-test.js @@ -1,29 +1,29 @@ const assert = require('assert') -const extend = require('xtend') -const EventEmitter = require('events') +// const extend = require('xtend') +// const EventEmitter = require('events') const MessageManger = require('../../app/scripts/lib/message-manager') -describe('Transaction Manager', function() { +describe('Transaction Manager', function () { let messageManager - beforeEach(function() { - messageManager = new MessageManger () + beforeEach(function () { + messageManager = new MessageManger() }) - describe('#getMsgList', function() { - it('when new should return empty array', function() { + describe('#getMsgList', function () { + it('when new should return empty array', function () { var result = messageManager.messages assert.ok(Array.isArray(result)) assert.equal(result.length, 0) }) - it('should also return transactions from local storage if any', function() { + it('should also return transactions from local storage if any', function () { }) }) - describe('#addMsg', function() { - it('adds a Msg returned in getMsgList', function() { + describe('#addMsg', function () { + it('adds a Msg returned in getMsgList', function () { var Msg = { id: 1, status: 'approved', metamaskNetworkId: 'unit test' } messageManager.addMsg(Msg) var result = messageManager.messages @@ -33,8 +33,8 @@ describe('Transaction Manager', function() { }) }) - describe('#setMsgStatusApproved', function() { - it('sets the Msg status to approved', function() { + describe('#setMsgStatusApproved', function () { + it('sets the Msg status to approved', function () { var Msg = { id: 1, status: 'unapproved', metamaskNetworkId: 'unit test' } messageManager.addMsg(Msg) messageManager.setMsgStatusApproved(1) @@ -45,8 +45,8 @@ describe('Transaction Manager', function() { }) }) - describe('#rejectMsg', function() { - it('sets the Msg status to rejected', function() { + describe('#rejectMsg', function () { + it('sets the Msg status to rejected', function () { var Msg = { id: 1, status: 'unapproved', metamaskNetworkId: 'unit test' } messageManager.addMsg(Msg) messageManager.rejectMsg(1) @@ -57,8 +57,8 @@ describe('Transaction Manager', function() { }) }) - describe('#_updateMsg', function() { - it('replaces the Msg with the same id', function() { + describe('#_updateMsg', function () { + it('replaces the Msg with the same id', function () { messageManager.addMsg({ id: '1', status: 'unapproved', metamaskNetworkId: 'unit test' }) messageManager.addMsg({ id: '2', status: 'approved', metamaskNetworkId: 'unit test' }) messageManager._updateMsg({ id: '1', status: 'blah', hash: 'foo', metamaskNetworkId: 'unit test' }) @@ -67,19 +67,19 @@ describe('Transaction Manager', function() { }) }) - describe('#getUnapprovedMsgs', function() { - it('returns unapproved Msgs in a hash', function() { + describe('#getUnapprovedMsgs', function () { + it('returns unapproved Msgs in a hash', function () { messageManager.addMsg({ id: '1', status: 'unapproved', metamaskNetworkId: 'unit test' }) messageManager.addMsg({ id: '2', status: 'approved', metamaskNetworkId: 'unit test' }) - let result = messageManager.getUnapprovedMsgs() + const result = messageManager.getUnapprovedMsgs() assert.equal(typeof result, 'object') assert.equal(result['1'].status, 'unapproved') assert.equal(result['2'], undefined) }) }) - describe('#getMsg', function() { - it('returns a Msg with the requested id', function() { + describe('#getMsg', function () { + it('returns a Msg with the requested id', function () { messageManager.addMsg({ id: '1', status: 'unapproved', metamaskNetworkId: 'unit test' }) messageManager.addMsg({ id: '2', status: 'approved', metamaskNetworkId: 'unit test' }) assert.equal(messageManager.getMsg('1').status, 'unapproved') diff --git a/test/unit/metamask-controller-test.js b/test/unit/metamask-controller-test.js index 78b9e9df7..ac588b313 100644 --- a/test/unit/metamask-controller-test.js +++ b/test/unit/metamask-controller-test.js @@ -4,11 +4,11 @@ const clone = require('clone') const MetaMaskController = require('../../app/scripts/metamask-controller') const firstTimeState = require('../../app/scripts/first-time-state') -const STORAGE_KEY = 'metamask-config' +// const STORAGE_KEY = 'metamask-config' -describe('MetaMaskController', function() { +describe('MetaMaskController', function () { const noop = () => {} - let controller = new MetaMaskController({ + const metamaskController = new MetaMaskController({ showUnconfirmedMessage: noop, unlockAccountMessage: noop, showUnapprovedTx: noop, @@ -16,14 +16,18 @@ describe('MetaMaskController', function() { initState: clone(firstTimeState), }) - beforeEach(function() { + beforeEach(function () { // sinon allows stubbing methods that are easily verified this.sinon = sinon.sandbox.create() }) - afterEach(function() { + afterEach(function () { // sinon requires cleanup otherwise it will overwrite context this.sinon.restore() }) -}) \ No newline at end of file + describe('Metamask Controller', function () { + assert(metamaskController) + }) +}) + diff --git a/test/unit/migrations-test.js b/test/unit/migrations-test.js index ccd1477b0..324e4d056 100644 --- a/test/unit/migrations-test.js +++ b/test/unit/migrations-test.js @@ -3,7 +3,7 @@ const path = require('path') const wallet1 = require(path.join('..', 'lib', 'migrations', '001.json')) const vault4 = require(path.join('..', 'lib', 'migrations', '004.json')) -let vault5, vault6, vault7, vault8, vault9, vault10, vault11 +let vault5, vault6, vault7, vault8, vault9 // vault10, vault11 const migration2 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '002')) const migration3 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '003')) @@ -23,7 +23,6 @@ const newTestRpc = 'https://testrpc.metamask.io/' describe('wallet1 is migrated successfully', () => { it('should convert providers', () => { - wallet1.data.config.provider = { type: 'etherscan', rpcTarget: null } return migration2.migrate(wallet1) @@ -99,6 +98,5 @@ describe('wallet1 is migrated successfully', () => { assert.equal(twelfthResult.data.NoticeController.noticesList[0].body, '', 'notices that have been read should have an empty body.') assert.equal(twelfthResult.data.NoticeController.noticesList[1].body, 'nonempty', 'notices that have not been read should not have an empty body.') }) - }) }) diff --git a/test/unit/nameForAccount_test.js b/test/unit/nameForAccount_test.js index 6839d40f8..e7c0b18b4 100644 --- a/test/unit/nameForAccount_test.js +++ b/test/unit/nameForAccount_test.js @@ -4,25 +4,23 @@ var sinon = require('sinon') var path = require('path') var contractNamer = require(path.join(__dirname, '..', '..', 'ui', 'lib', 'contract-namer.js')) -describe('contractNamer', function() { - - beforeEach(function() { +describe('contractNamer', function () { + beforeEach(function () { this.sinon = sinon.sandbox.create() }) - afterEach(function() { + afterEach(function () { this.sinon.restore() }) - describe('naming a contract', function() { - - it('should return nothing for an unknown random account', function() { + describe('naming a contract', function () { + it('should return nothing for an unknown random account', function () { const input = '0x2386F26FC10000' const output = contractNamer(input) assert.deepEqual(output, null) }) - it('should accept identities as an optional second parameter', function() { + it('should accept identities as an optional second parameter', function () { const input = '0x2386F26FC10000'.toLowerCase() const expected = 'bar' const identities = {} @@ -31,7 +29,7 @@ describe('contractNamer', function() { assert.deepEqual(output, expected) }) - it('should check for identities case insensitively', function() { + it('should check for identities case insensitively', function () { const input = '0x2386F26FC10000'.toLowerCase() const expected = 'bar' const identities = {} @@ -39,6 +37,5 @@ describe('contractNamer', function() { const output = contractNamer(input.toUpperCase(), identities) assert.deepEqual(output, expected) }) - }) }) diff --git a/test/unit/nodeify-test.js b/test/unit/nodeify-test.js index a14d34338..5aed758fa 100644 --- a/test/unit/nodeify-test.js +++ b/test/unit/nodeify-test.js @@ -1,22 +1,20 @@ const assert = require('assert') const nodeify = require('../../app/scripts/lib/nodeify') -describe('nodeify', function() { - +describe('nodeify', function () { var obj = { foo: 'bar', promiseFunc: function (a) { var solution = this.foo + a return Promise.resolve(solution) - } + }, } - it('should retain original context', function(done) { + it('should retain original context', function (done) { var nodified = nodeify(obj.promiseFunc).bind(obj) nodified('baz', function (err, res) { assert.equal(res, 'barbaz') done() }) }) - }) diff --git a/test/unit/notice-controller-test.js b/test/unit/notice-controller-test.js index ea37108bb..7eef615d5 100644 --- a/test/unit/notice-controller-test.js +++ b/test/unit/notice-controller-test.js @@ -1,42 +1,42 @@ const assert = require('assert') -const extend = require('xtend') -const rp = require('request-promise') -const nock = require('nock') +// const extend = require('xtend') +// const rp = require('request-promise') +// const nock = require('nock') const configManagerGen = require('../lib/mock-config-manager') const NoticeController = require('../../app/scripts/notice-controller') -const STORAGE_KEY = 'metamask-persistence-key' +// const STORAGE_KEY = 'metamask-persistence-key' -describe('notice-controller', function() { +describe('notice-controller', function () { var noticeController - beforeEach(function() { + beforeEach(function () { // simple localStorage polyfill - let configManager = configManagerGen() + const configManager = configManagerGen() noticeController = new NoticeController({ configManager: configManager, }) }) - describe('notices', function() { - describe('#getNoticesList', function() { - it('should return an empty array when new', function(done) { - var testList = [{ - id:0, - read:false, - title:"Futuristic Notice" - }] + describe('notices', function () { + describe('#getNoticesList', function () { + it('should return an empty array when new', function (done) { + // const testList = [{ + // id: 0, + // read: false, + // title: 'Futuristic Notice', + // }] var result = noticeController.getNoticesList() assert.equal(result.length, 0) done() }) }) - describe('#setNoticesList', function() { + describe('#setNoticesList', function () { it('should set data appropriately', function (done) { var testList = [{ - id:0, - read:false, - title:"Futuristic Notice" + id: 0, + read: false, + title: 'Futuristic Notice', }] noticeController.setNoticesList(testList) var testListId = noticeController.getNoticesList()[0].id @@ -45,12 +45,12 @@ describe('notice-controller', function() { }) }) - describe('#updateNoticeslist', function() { - it('should integrate the latest changes from the source', function(done) { + describe('#updateNoticeslist', function () { + it('should integrate the latest changes from the source', function (done) { var testList = [{ - id:55, - read:false, - title:"Futuristic Notice" + id: 55, + read: false, + title: 'Futuristic Notice', }] noticeController.setNoticesList(testList) noticeController.updateNoticesList().then(() => { @@ -62,14 +62,14 @@ describe('notice-controller', function() { }) it('should not overwrite any existing fields', function (done) { var testList = [{ - id:0, - read:false, - title:"Futuristic Notice" + id: 0, + read: false, + title: 'Futuristic Notice', }] noticeController.setNoticesList(testList) var newList = noticeController.getNoticesList() assert.equal(newList[0].id, 0) - assert.equal(newList[0].title, "Futuristic Notice") + assert.equal(newList[0].title, 'Futuristic Notice') assert.equal(newList.length, 1) done() }) @@ -78,9 +78,9 @@ describe('notice-controller', function() { describe('#markNoticeRead', function () { it('should mark a notice as read', function (done) { var testList = [{ - id:0, - read:false, - title:"Futuristic Notice" + id: 0, + read: false, + title: 'Futuristic Notice', }] noticeController.setNoticesList(testList) noticeController.markNoticeRead(testList[0]) @@ -93,9 +93,9 @@ describe('notice-controller', function() { describe('#getLatestUnreadNotice', function () { it('should retrieve the latest unread notice', function (done) { var testList = [ - {id:0,read:true,title:"Past Notice"}, - {id:1,read:false,title:"Current Notice"}, - {id:2,read:false,title:"Future Notice"}, + {id: 0, read: true, title: 'Past Notice'}, + {id: 1, read: false, title: 'Current Notice'}, + {id: 2, read: false, title: 'Future Notice'}, ] noticeController.setNoticesList(testList) var latestUnread = noticeController.getLatestUnreadNotice() @@ -104,9 +104,9 @@ describe('notice-controller', function() { }) it('should return undefined if no unread notices exist.', function (done) { var testList = [ - {id:0,read:true,title:"Past Notice"}, - {id:1,read:true,title:"Current Notice"}, - {id:2,read:true,title:"Future Notice"}, + {id: 0, read: true, title: 'Past Notice'}, + {id: 1, read: true, title: 'Current Notice'}, + {id: 2, read: true, title: 'Future Notice'}, ] noticeController.setNoticesList(testList) var latestUnread = noticeController.getLatestUnreadNotice() @@ -115,5 +115,4 @@ describe('notice-controller', function() { }) }) }) - }) diff --git a/test/unit/personal-message-manager-test.js b/test/unit/personal-message-manager-test.js index f2c01392c..b7a29b7b0 100644 --- a/test/unit/personal-message-manager-test.js +++ b/test/unit/personal-message-manager-test.js @@ -1,29 +1,29 @@ const assert = require('assert') -const extend = require('xtend') -const EventEmitter = require('events') +// const extend = require('xtend') +// const EventEmitter = require('events') const PersonalMessageManager = require('../../app/scripts/lib/personal-message-manager') -describe('Personal Message Manager', function() { +describe('Personal Message Manager', function () { let messageManager - beforeEach(function() { + beforeEach(function () { messageManager = new PersonalMessageManager() }) - describe('#getMsgList', function() { - it('when new should return empty array', function() { + describe('#getMsgList', function () { + it('when new should return empty array', function () { var result = messageManager.messages assert.ok(Array.isArray(result)) assert.equal(result.length, 0) }) - it('should also return transactions from local storage if any', function() { + it('should also return transactions from local storage if any', function () { }) }) - describe('#addMsg', function() { - it('adds a Msg returned in getMsgList', function() { + describe('#addMsg', function () { + it('adds a Msg returned in getMsgList', function () { var Msg = { id: 1, status: 'approved', metamaskNetworkId: 'unit test' } messageManager.addMsg(Msg) var result = messageManager.messages @@ -33,8 +33,8 @@ describe('Personal Message Manager', function() { }) }) - describe('#setMsgStatusApproved', function() { - it('sets the Msg status to approved', function() { + describe('#setMsgStatusApproved', function () { + it('sets the Msg status to approved', function () { var Msg = { id: 1, status: 'unapproved', metamaskNetworkId: 'unit test' } messageManager.addMsg(Msg) messageManager.setMsgStatusApproved(1) @@ -45,8 +45,8 @@ describe('Personal Message Manager', function() { }) }) - describe('#rejectMsg', function() { - it('sets the Msg status to rejected', function() { + describe('#rejectMsg', function () { + it('sets the Msg status to rejected', function () { var Msg = { id: 1, status: 'unapproved', metamaskNetworkId: 'unit test' } messageManager.addMsg(Msg) messageManager.rejectMsg(1) @@ -57,8 +57,8 @@ describe('Personal Message Manager', function() { }) }) - describe('#_updateMsg', function() { - it('replaces the Msg with the same id', function() { + describe('#_updateMsg', function () { + it('replaces the Msg with the same id', function () { messageManager.addMsg({ id: '1', status: 'unapproved', metamaskNetworkId: 'unit test' }) messageManager.addMsg({ id: '2', status: 'approved', metamaskNetworkId: 'unit test' }) messageManager._updateMsg({ id: '1', status: 'blah', hash: 'foo', metamaskNetworkId: 'unit test' }) @@ -67,19 +67,19 @@ describe('Personal Message Manager', function() { }) }) - describe('#getUnapprovedMsgs', function() { - it('returns unapproved Msgs in a hash', function() { + describe('#getUnapprovedMsgs', function () { + it('returns unapproved Msgs in a hash', function () { messageManager.addMsg({ id: '1', status: 'unapproved', metamaskNetworkId: 'unit test' }) messageManager.addMsg({ id: '2', status: 'approved', metamaskNetworkId: 'unit test' }) - let result = messageManager.getUnapprovedMsgs() + const result = messageManager.getUnapprovedMsgs() assert.equal(typeof result, 'object') assert.equal(result['1'].status, 'unapproved') assert.equal(result['2'], undefined) }) }) - describe('#getMsg', function() { - it('returns a Msg with the requested id', function() { + describe('#getMsg', function () { + it('returns a Msg with the requested id', function () { messageManager.addMsg({ id: '1', status: 'unapproved', metamaskNetworkId: 'unit test' }) messageManager.addMsg({ id: '2', status: 'approved', metamaskNetworkId: 'unit test' }) assert.equal(messageManager.getMsg('1').status, 'unapproved') @@ -87,24 +87,23 @@ describe('Personal Message Manager', function() { }) }) - describe('#normalizeMsgData', function() { - it('converts text to a utf8 hex string', function() { + describe('#normalizeMsgData', function () { + it('converts text to a utf8 hex string', function () { var input = 'hello' var output = messageManager.normalizeMsgData(input) assert.equal(output, '0x68656c6c6f', 'predictably hex encoded') }) - it('tolerates a hex prefix', function() { + it('tolerates a hex prefix', function () { var input = '0x12' var output = messageManager.normalizeMsgData(input) assert.equal(output, '0x12', 'un modified') }) - it('tolerates normal hex', function() { + it('tolerates normal hex', function () { var input = '12' var output = messageManager.normalizeMsgData(input) assert.equal(output, '0x12', 'adds prefix') }) }) - }) diff --git a/test/unit/reducers/unlock_vault_test.js b/test/unit/reducers/unlock_vault_test.js index b7540af08..2b7d70b2c 100644 --- a/test/unit/reducers/unlock_vault_test.js +++ b/test/unit/reducers/unlock_vault_test.js @@ -1,32 +1,31 @@ -var jsdom = require('mocha-jsdom') +// var jsdom = require('mocha-jsdom') var assert = require('assert') -var freeze = require('deep-freeze-strict') +// var freeze = require('deep-freeze-strict') var path = require('path') var sinon = require('sinon') var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) -var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) +var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) -describe('#unlockMetamask(selectedAccount)', function() { - - beforeEach(function() { +describe('#unlockMetamask(selectedAccount)', function () { + beforeEach(function () { // sinon allows stubbing methods that are easily verified this.sinon = sinon.sandbox.create() }) - afterEach(function() { + afterEach(function () { // sinon requires cleanup otherwise it will overwrite context this.sinon.restore() }) - describe('after an error', function() { - it('clears warning', function() { + describe('after an error', function () { + it('clears warning', function () { const warning = 'this is the wrong warning' const account = 'foo_account' const initialState = { appState: { warning: warning, - } + }, } const resultState = reducers(initialState, actions.unlockMetamask(account)) @@ -34,14 +33,14 @@ describe('#unlockMetamask(selectedAccount)', function() { }) }) - describe('going home after an error', function() { - it('clears warning', function() { + describe('going home after an error', function () { + it('clears warning', function () { const warning = 'this is the wrong warning' - const account = 'foo_account' + // const account = 'foo_account' const initialState = { appState: { warning: warning, - } + }, } const resultState = reducers(initialState, actions.goHome()) diff --git a/test/unit/tx-manager-test.js b/test/unit/tx-manager-test.js index 21e94357b..ce4a2acf7 100644 --- a/test/unit/tx-manager-test.js +++ b/test/unit/tx-manager-test.js @@ -1,20 +1,20 @@ const assert = require('assert') -const extend = require('xtend') +// const extend = require('xtend') const EventEmitter = require('events') const ethUtil = require('ethereumjs-util') const EthTx = require('ethereumjs-tx') const ObservableStore = require('obs-store') -const STORAGE_KEY = 'metamask-persistance-key' +// const STORAGE_KEY = 'metamask-persistance-key' const TransactionManager = require('../../app/scripts/transaction-manager') const noop = () => true const currentNetworkId = 42 const otherNetworkId = 36 const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex') -describe('Transaction Manager', function() { +describe('Transaction Manager', function () { let txManager - beforeEach(function() { + beforeEach(function () { txManager = new TransactionManager({ networkStore: new ObservableStore({ network: currentNetworkId }), txHistoryLimit: 10, @@ -22,43 +22,43 @@ describe('Transaction Manager', function() { signTransaction: (ethTx) => new Promise((resolve) => { ethTx.sign(privKey) resolve() - }) + }), }) }) describe('#validateTxParams', function () { - it('returns null for positive values', function() { + it('returns null for positive values', function () { var sample = { - value: '0x01' + value: '0x01', } - var res = txManager.txProviderUtils.validateTxParams(sample, (err) => { + txManager.txProviderUtils.validateTxParams(sample, (err) => { assert.equal(err, null, 'no error') }) }) - it('returns error for negative values', function() { + it('returns error for negative values', function () { var sample = { - value: '-0x01' + value: '-0x01', } - var res = txManager.txProviderUtils.validateTxParams(sample, (err) => { + txManager.txProviderUtils.validateTxParams(sample, (err) => { assert.ok(err, 'error') }) }) }) - describe('#getTxList', function() { - it('when new should return empty array', function() { + describe('#getTxList', function () { + it('when new should return empty array', function () { var result = txManager.getTxList() assert.ok(Array.isArray(result)) assert.equal(result.length, 0) }) - it('should also return transactions from local storage if any', function() { + it('should also return transactions from local storage if any', function () { }) }) - describe('#addTx', function() { - it('adds a tx returned in getTxList', function() { + describe('#addTx', function () { + it('adds a tx returned in getTxList', function () { var tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } txManager.addTx(tx, noop) var result = txManager.getTxList() @@ -67,7 +67,7 @@ describe('Transaction Manager', function() { assert.equal(result[0].id, 1) }) - it('does not override txs from other networks', function() { + it('does not override txs from other networks', function () { var tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } var tx2 = { id: 2, status: 'confirmed', metamaskNetworkId: otherNetworkId, txParams: {} } txManager.addTx(tx, noop) @@ -78,10 +78,10 @@ describe('Transaction Manager', function() { assert.equal(result2.length, 1, 'incorrect number of txs on network.') }) - it('cuts off early txs beyond a limit', function() { + it('cuts off early txs beyond a limit', function () { const limit = txManager.txHistoryLimit for (let i = 0; i < limit + 1; i++) { - let tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } + const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } txManager.addTx(tx, noop) } var result = txManager.getTxList() @@ -89,10 +89,10 @@ describe('Transaction Manager', function() { assert.equal(result[0].id, 1, 'early txs truncted') }) - it('cuts off early txs beyond a limit whether or not it is confirmed or rejected', function() { + it('cuts off early txs beyond a limit whether or not it is confirmed or rejected', function () { const limit = txManager.txHistoryLimit for (let i = 0; i < limit + 1; i++) { - let tx = { id: i, time: new Date(), status: 'rejected', metamaskNetworkId: currentNetworkId, txParams: {} } + const tx = { id: i, time: new Date(), status: 'rejected', metamaskNetworkId: currentNetworkId, txParams: {} } txManager.addTx(tx, noop) } var result = txManager.getTxList() @@ -100,12 +100,12 @@ describe('Transaction Manager', function() { assert.equal(result[0].id, 1, 'early txs truncted') }) - it('cuts off early txs beyond a limit but does not cut unapproved txs', function() { + it('cuts off early txs beyond a limit but does not cut unapproved txs', function () { var unconfirmedTx = { id: 0, time: new Date(), status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } txManager.addTx(unconfirmedTx, noop) const limit = txManager.txHistoryLimit for (let i = 1; i < limit + 1; i++) { - let tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } + const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } txManager.addTx(tx, noop) } var result = txManager.getTxList() @@ -116,8 +116,8 @@ describe('Transaction Manager', function() { }) }) - describe('#setTxStatusSigned', function() { - it('sets the tx status to signed', function() { + describe('#setTxStatusSigned', function () { + it('sets the tx status to signed', function () { var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } txManager.addTx(tx, noop) txManager.setTxStatusSigned(1) @@ -130,7 +130,7 @@ describe('Transaction Manager', function() { it('should emit a signed event to signal the exciton of callback', (done) => { this.timeout(10000) var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } - let noop = function () { + const noop = function () { assert(true, 'event listener has been triggered and noop executed') done() } @@ -140,8 +140,8 @@ describe('Transaction Manager', function() { }) }) - describe('#setTxStatusRejected', function() { - it('sets the tx status to rejected', function() { + describe('#setTxStatusRejected', function () { + it('sets the tx status to rejected', function () { var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } txManager.addTx(tx) txManager.setTxStatusRejected(1) @@ -155,18 +155,17 @@ describe('Transaction Manager', function() { this.timeout(10000) var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } txManager.addTx(tx) - let noop = function (err, txId) { + const noop = function (err, txId) { assert(true, 'event listener has been triggered and noop executed') done() } txManager.on('1:rejected', noop) txManager.setTxStatusRejected(1) }) - }) - describe('#updateTx', function() { - it('replaces the tx with the same id', function() { + describe('#updateTx', function () { + it('replaces the tx with the same id', function () { txManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) txManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) txManager.updateTx({ id: '1', status: 'blah', hash: 'foo', metamaskNetworkId: currentNetworkId, txParams: {} }) @@ -175,19 +174,19 @@ describe('Transaction Manager', function() { }) }) - describe('#getUnapprovedTxList', function() { - it('returns unapproved txs in a hash', function() { + describe('#getUnapprovedTxList', function () { + it('returns unapproved txs in a hash', function () { txManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) txManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) - let result = txManager.getUnapprovedTxList() + const result = txManager.getUnapprovedTxList() assert.equal(typeof result, 'object') assert.equal(result['1'].status, 'unapproved') assert.equal(result['2'], undefined) }) }) - describe('#getTx', function() { - it('returns a tx with the requested id', function() { + describe('#getTx', function () { + it('returns a tx with the requested id', function () { txManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) txManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) assert.equal(txManager.getTx('1').status, 'unapproved') @@ -195,19 +194,19 @@ describe('Transaction Manager', function() { }) }) - describe('#getFilteredTxList', function() { - it('returns a tx with the requested data', function() { - let txMetas = [ + describe('#getFilteredTxList', function () { + it('returns a tx with the requested data', function () { + const txMetas = [ { id: 0, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, { id: 1, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, { id: 2, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, { id: 3, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, { id: 4, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, - { id: 5, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, - { id: 6, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, - { id: 7, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, - { id: 8, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, - { id: 9, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, + { id: 5, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, + { id: 6, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, + { id: 7, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, + { id: 8, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, + { id: 9, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, ] txMetas.forEach((txMeta) => txManager.addTx(txMeta, noop)) let filterParams @@ -227,8 +226,8 @@ describe('Transaction Manager', function() { }) }) - describe('#sign replay-protected tx', function() { - it('prepares a tx with the chainId set', function() { + describe('#sign replay-protected tx', function () { + it('prepares a tx with the chainId set', function () { txManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) txManager.signTransaction('1', (err, rawTx) => { if (err) return assert.fail('it should not fail') @@ -237,5 +236,4 @@ describe('Transaction Manager', function() { }) }) }) - }) diff --git a/test/unit/tx-utils-test.js b/test/unit/tx-utils-test.js index 93e9e4134..57d4638a0 100644 --- a/test/unit/tx-utils-test.js +++ b/test/unit/tx-utils-test.js @@ -5,15 +5,15 @@ const BN = ethUtil.BN const TxUtils = require('../../app/scripts/lib/tx-utils') -describe('txUtils', function() { +describe('txUtils', function () { let txUtils - before(function() { + before(function () { txUtils = new TxUtils() }) - describe('chain Id', function() { - it('prepares a transaction with the provided chainId', function() { + describe('chain Id', function () { + it('prepares a transaction with the provided chainId', function () { const txParams = { to: '0x70ad465e0bab6504002ad58c744ed89c7da38524', from: '0x69ad465e0bab6504002ad58c744ed89c7da38525', @@ -29,8 +29,8 @@ describe('txUtils', function() { }) }) - describe('addGasBuffer', function() { - it('multiplies by 1.5, when within block gas limit', function() { + describe('addGasBuffer', function () { + it('multiplies by 1.5, when within block gas limit', function () { // naive estimatedGas: 0x16e360 (1.5 mil) const inputHex = '0x16e360' // dummy gas limit: 0x3d4c52 (4 mil) @@ -41,20 +41,20 @@ describe('txUtils', function() { const expectedBn = inputBn.muln(1.5) assert(outputBn.eq(expectedBn), 'returns 1.5 the input value') }) - - it('uses original estimatedGas, when above block gas limit', function() { + + it('uses original estimatedGas, when above block gas limit', function () { // naive estimatedGas: 0x16e360 (1.5 mil) const inputHex = '0x16e360' // dummy gas limit: 0x0f4240 (1 mil) const blockGasLimitHex = '0x0f4240' const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex) - const inputBn = hexToBn(inputHex) + // const inputBn = hexToBn(inputHex) const outputBn = hexToBn(output) const expectedBn = hexToBn(inputHex) assert(outputBn.eq(expectedBn), 'returns the original estimatedGas value') }) - it('buffers up to reccomend gas limit reccomended ceiling', function() { + it('buffers up to reccomend gas limit reccomended ceiling', function () { // naive estimatedGas: 0x16e360 (1.5 mil) const inputHex = '0x16e360' // dummy gas limit: 0x1e8480 (2 mil) @@ -72,10 +72,10 @@ describe('txUtils', function() { // util -function hexToBn(inputHex) { +function hexToBn (inputHex) { return new BN(ethUtil.stripHexPrefix(inputHex), 16) } -function bnToHex(inputBn) { +function bnToHex (inputBn) { return ethUtil.addHexPrefix(inputBn.toString(16)) -} \ No newline at end of file +} diff --git a/test/unit/util_test.js b/test/unit/util_test.js index 00528b905..3a8b6bdfd 100644 --- a/test/unit/util_test.js +++ b/test/unit/util_test.js @@ -5,96 +5,96 @@ const ethUtil = require('ethereumjs-util') var path = require('path') var util = require(path.join(__dirname, '..', '..', 'ui', 'app', 'util.js')) -describe('util', function() { +describe('util', function () { var ethInWei = '1' - for (var i = 0; i < 18; i++ ) { ethInWei += '0' } + for (var i = 0; i < 18; i++) { ethInWei += '0' } - beforeEach(function() { + beforeEach(function () { this.sinon = sinon.sandbox.create() }) - afterEach(function() { + afterEach(function () { this.sinon.restore() }) - describe('#parseBalance', function() { - it('should render 0.01 eth correctly', function() { + describe('#parseBalance', function () { + it('should render 0.01 eth correctly', function () { const input = '0x2386F26FC10000' const output = util.parseBalance(input) assert.deepEqual(output, ['0', '01']) }) - it('should render 12.023 eth correctly', function() { + it('should render 12.023 eth correctly', function () { const input = 'A6DA46CCA6858000' const output = util.parseBalance(input) assert.deepEqual(output, ['12', '023']) }) - it('should render 0.0000000342422 eth correctly', function() { + it('should render 0.0000000342422 eth correctly', function () { const input = '0x7F8FE81C0' const output = util.parseBalance(input) assert.deepEqual(output, ['0', '0000000342422']) }) - it('should render 0 eth correctly', function() { + it('should render 0 eth correctly', function () { const input = '0x0' const output = util.parseBalance(input) assert.deepEqual(output, ['0', '0']) }) }) - describe('#addressSummary', function() { - it('should add case-sensitive checksum', function() { + describe('#addressSummary', function () { + it('should add case-sensitive checksum', function () { var address = '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825' var result = util.addressSummary(address) assert.equal(result, '0xFDEa65C8...b825') }) - it('should accept arguments for firstseg, lastseg, and keepPrefix', function() { + it('should accept arguments for firstseg, lastseg, and keepPrefix', function () { var address = '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825' var result = util.addressSummary(address, 4, 4, false) assert.equal(result, 'FDEa...b825') }) }) - describe('#isValidAddress', function() { - it('should allow 40-char non-prefixed hex', function() { + describe('#isValidAddress', function () { + it('should allow 40-char non-prefixed hex', function () { var address = 'fdea65c8e26263f6d9a1b5de9555d2931a33b825' var result = util.isValidAddress(address) assert.ok(result) }) - it('should allow 42-char non-prefixed hex', function() { + it('should allow 42-char non-prefixed hex', function () { var address = '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825' var result = util.isValidAddress(address) assert.ok(result) }) - it('should not allow less non hex-prefixed', function() { + it('should not allow less non hex-prefixed', function () { var address = 'fdea65c8e26263f6d9a1b5de9555d2931a33b85' var result = util.isValidAddress(address) assert.ok(!result) }) - it('should not allow less hex-prefixed', function() { + it('should not allow less hex-prefixed', function () { var address = '0xfdea65ce26263f6d9a1b5de9555d2931a33b85' var result = util.isValidAddress(address) assert.ok(!result) }) - it('should recognize correct capitalized checksum', function() { + it('should recognize correct capitalized checksum', function () { var address = '0xFDEa65C8e26263F6d9A1B5de9555D2931A33b825' var result = util.isValidAddress(address) assert.ok(result) }) - it('should recognize incorrect capitalized checksum', function() { + it('should recognize incorrect capitalized checksum', function () { var address = '0xFDea65C8e26263F6d9A1B5de9555D2931A33b825' var result = util.isValidAddress(address) assert.ok(!result) }) - it('should recognize this sample hashed address', function() { + it('should recognize this sample hashed address', function () { const address = '0x5Fda30Bb72B8Dfe20e48A00dFc108d0915BE9Bb0' const result = util.isValidAddress(address) const hashed = ethUtil.toChecksumAddress(address.toLowerCase()) @@ -103,60 +103,57 @@ describe('util', function() { }) }) - describe('#numericBalance', function() { - - it('should return a BN 0 if given nothing', function() { + describe('#numericBalance', function () { + it('should return a BN 0 if given nothing', function () { var result = util.numericBalance() assert.equal(result.toString(10), 0) }) - it('should work with hex prefix', function() { + it('should work with hex prefix', function () { var result = util.numericBalance('0x012') assert.equal(result.toString(10), '18') }) - it('should work with no hex prefix', function() { + it('should work with no hex prefix', function () { var result = util.numericBalance('012') assert.equal(result.toString(10), '18') }) - }) - describe('#formatBalance', function() { - - it('when given nothing', function() { + describe('#formatBalance', function () { + it('when given nothing', function () { var result = util.formatBalance() assert.equal(result, 'None', 'should return "None"') }) - it('should return eth as string followed by ETH', function() { + it('should return eth as string followed by ETH', function () { var input = new ethUtil.BN(ethInWei, 10).toJSON() var result = util.formatBalance(input, 4) assert.equal(result, '1.0000 ETH') }) - it('should return eth as string followed by ETH', function() { + it('should return eth as string followed by ETH', function () { var input = new ethUtil.BN(ethInWei, 10).div(new ethUtil.BN('2', 10)).toJSON() var result = util.formatBalance(input, 3) assert.equal(result, '0.500 ETH') }) - it('should display specified decimal points', function() { - var input = "0x128dfa6a90b28000" + it('should display specified decimal points', function () { + var input = '0x128dfa6a90b28000' var result = util.formatBalance(input, 2) assert.equal(result, '1.33 ETH') }) - it('should default to 3 decimal points', function() { - var input = "0x128dfa6a90b28000" + it('should default to 3 decimal points', function () { + var input = '0x128dfa6a90b28000' var result = util.formatBalance(input) assert.equal(result, '1.337 ETH') }) - it('should show 2 significant digits for tiny balances', function() { - var input = "0x1230fa6a90b28" + it('should show 2 significant digits for tiny balances', function () { + var input = '0x1230fa6a90b28' var result = util.formatBalance(input) assert.equal(result, '0.00032 ETH') }) - it('should not parse the balance and return value with 2 decimal points with ETH at the end', function() { + it('should not parse the balance and return value with 2 decimal points with ETH at the end', function () { var value = '1.2456789' var needsParse = false var result = util.formatBalance(value, 2, needsParse) @@ -164,17 +161,16 @@ describe('util', function() { }) }) - describe('normalizing values', function() { - - describe('#normalizeToWei', function() { - it('should convert an eth to the appropriate equivalent values', function() { + describe('normalizing values', function () { + describe('#normalizeToWei', function () { + it('should convert an eth to the appropriate equivalent values', function () { var valueTable = { - wei: '1000000000000000000', - kwei: '1000000000000000', - mwei: '1000000000000', - gwei: '1000000000', + wei: '1000000000000000000', + kwei: '1000000000000000', + mwei: '1000000000000', + gwei: '1000000000', szabo: '1000000', - finney:'1000', + finney: '1000', ether: '1', // kether:'0.001', // mether:'0.000001', @@ -185,8 +181,7 @@ describe('util', function() { } var oneEthBn = new ethUtil.BN(ethInWei, 10) - for(var currency in valueTable) { - + for (var currency in valueTable) { var value = new ethUtil.BN(valueTable[currency], 10) var output = util.normalizeToWei(value, currency) assert.equal(output.toString(10), valueTable.wei, `value of ${output.toString(10)} ${currency} should convert to ${oneEthBn}`) @@ -194,60 +189,58 @@ describe('util', function() { }) }) - describe('#normalizeEthStringToWei', function() { - it('should convert decimal eth to pure wei BN', function() { + describe('#normalizeEthStringToWei', function () { + it('should convert decimal eth to pure wei BN', function () { var input = '1.23456789' var output = util.normalizeEthStringToWei(input) assert.equal(output.toString(10), '1234567890000000000') }) - it('should convert 1 to expected wei', function() { + it('should convert 1 to expected wei', function () { var input = '1' var output = util.normalizeEthStringToWei(input) assert.equal(output.toString(10), ethInWei) }) }) - describe('#normalizeNumberToWei', function() { - - it('should handle a simple use case', function() { + describe('#normalizeNumberToWei', function () { + it('should handle a simple use case', function () { var input = 0.0002 var output = util.normalizeNumberToWei(input, 'ether') var str = output.toString(10) assert.equal(str, '200000000000000') }) - it('should convert a kwei number to the appropriate equivalent wei', function() { + it('should convert a kwei number to the appropriate equivalent wei', function () { var result = util.normalizeNumberToWei(1.111, 'kwei') assert.equal(result.toString(10), '1111', 'accepts decimals') }) - it('should convert a ether number to the appropriate equivalent wei', function() { + it('should convert a ether number to the appropriate equivalent wei', function () { var result = util.normalizeNumberToWei(1.111, 'ether') assert.equal(result.toString(10), '1111000000000000000', 'accepts decimals') }) }) - describe('#isHex', function(){ - it('should return true when given a hex string', function() { + describe('#isHex', function () { + it('should return true when given a hex string', function () { var result = util.isHex('c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2') assert(result) }) - it('should return false when given a non-hex string', function() { + it('should return false when given a non-hex string', function () { var result = util.isHex('c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714imnotreal') assert(!result) }) - it('should return false when given a string containing a non letter/number character', function() { + it('should return false when given a string containing a non letter/number character', function () { var result = util.isHex('c3ab8ff13720!8ad9047dd39466b3c%8974e592c2fa383d4a396071imnotreal') assert(!result) }) - it('should return true when given a hex string with hex-prefix', function() { + it('should return true when given a hex string with hex-prefix', function () { var result = util.isHex('0xc3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2') assert(result) }) - }) }) }) -- cgit v1.2.3 From 9bd7d06c4f3aab94308335f2e13c01bcca88eb4b Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Thu, 4 May 2017 15:06:27 -0700 Subject: Remove unused modules and STORAGE_KEY --- test/lib/mock-config-manager.js | 1 - test/unit/currency-controller-test.js | 2 -- test/unit/keyring-controller-test.js | 2 -- test/unit/message-manager-test.js | 3 --- test/unit/metamask-controller-test.js | 2 -- test/unit/notice-controller-test.js | 4 ---- 6 files changed, 14 deletions(-) diff --git a/test/lib/mock-config-manager.js b/test/lib/mock-config-manager.js index 3238d3501..0cc6953bb 100644 --- a/test/lib/mock-config-manager.js +++ b/test/lib/mock-config-manager.js @@ -2,7 +2,6 @@ const ObservableStore = require('obs-store') const clone = require('clone') const ConfigManager = require('../../app/scripts/lib/config-manager') const firstTimeState = require('../../app/scripts/first-time-state') -// const STORAGE_KEY = 'metamask-config' module.exports = function () { const store = new ObservableStore(clone(firstTimeState)) diff --git a/test/unit/currency-controller-test.js b/test/unit/currency-controller-test.js index 40868912c..cfbce7fb3 100644 --- a/test/unit/currency-controller-test.js +++ b/test/unit/currency-controller-test.js @@ -2,8 +2,6 @@ global.fetch = global.fetch || require('isomorphic-fetch') const assert = require('assert') -// const extend = require('xtend') -// const rp = require('request-promise') const nock = require('nock') const CurrencyController = require('../../app/scripts/controllers/currency') diff --git a/test/unit/keyring-controller-test.js b/test/unit/keyring-controller-test.js index f7e2ec89d..2d9a53723 100644 --- a/test/unit/keyring-controller-test.js +++ b/test/unit/keyring-controller-test.js @@ -3,9 +3,7 @@ const KeyringController = require('../../app/scripts/keyring-controller') const configManagerGen = require('../lib/mock-config-manager') const ethUtil = require('ethereumjs-util') const BN = ethUtil.BN -// const async = require('async') const mockEncryptor = require('../lib/mock-encryptor') -// const MockSimpleKeychain = require('../lib/mock-simple-keychain') const sinon = require('sinon') describe('KeyringController', function () { diff --git a/test/unit/message-manager-test.js b/test/unit/message-manager-test.js index 44190c8d0..30cb4f067 100644 --- a/test/unit/message-manager-test.js +++ b/test/unit/message-manager-test.js @@ -1,7 +1,4 @@ const assert = require('assert') -// const extend = require('xtend') -// const EventEmitter = require('events') - const MessageManger = require('../../app/scripts/lib/message-manager') describe('Transaction Manager', function () { diff --git a/test/unit/metamask-controller-test.js b/test/unit/metamask-controller-test.js index ac588b313..5ee0a6c84 100644 --- a/test/unit/metamask-controller-test.js +++ b/test/unit/metamask-controller-test.js @@ -4,8 +4,6 @@ const clone = require('clone') const MetaMaskController = require('../../app/scripts/metamask-controller') const firstTimeState = require('../../app/scripts/first-time-state') -// const STORAGE_KEY = 'metamask-config' - describe('MetaMaskController', function () { const noop = () => {} const metamaskController = new MetaMaskController({ diff --git a/test/unit/notice-controller-test.js b/test/unit/notice-controller-test.js index 7eef615d5..09eeda15c 100644 --- a/test/unit/notice-controller-test.js +++ b/test/unit/notice-controller-test.js @@ -1,10 +1,6 @@ const assert = require('assert') -// const extend = require('xtend') -// const rp = require('request-promise') -// const nock = require('nock') const configManagerGen = require('../lib/mock-config-manager') const NoticeController = require('../../app/scripts/notice-controller') -// const STORAGE_KEY = 'metamask-persistence-key' describe('notice-controller', function () { var noticeController -- cgit v1.2.3 From 1e4855fc0e2e89ebe6b50ff8d34762f742433110 Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Thu, 4 May 2017 15:21:51 -0700 Subject: Whoops missed some modules --- test/unit/address-book-controller.js | 1 - test/unit/config-manager-test.js | 3 --- test/unit/personal-message-manager-test.js | 2 -- test/unit/tx-manager-test.js | 2 -- 4 files changed, 8 deletions(-) diff --git a/test/unit/address-book-controller.js b/test/unit/address-book-controller.js index 85deb8905..655c9022c 100644 --- a/test/unit/address-book-controller.js +++ b/test/unit/address-book-controller.js @@ -1,5 +1,4 @@ const assert = require('assert') -// const extend = require('xtend') const AddressBookController = require('../../app/scripts/controllers/address-book') const mockKeyringController = { diff --git a/test/unit/config-manager-test.js b/test/unit/config-manager-test.js index eeb193e91..b710e2dfb 100644 --- a/test/unit/config-manager-test.js +++ b/test/unit/config-manager-test.js @@ -2,9 +2,6 @@ global.fetch = global.fetch || require('isomorphic-fetch') const assert = require('assert') -// const extend = require('xtend') -// const rp = require('request-promise') -// const nock = require('nock') const configManagerGen = require('../lib/mock-config-manager') describe('config-manager', function () { diff --git a/test/unit/personal-message-manager-test.js b/test/unit/personal-message-manager-test.js index b7a29b7b0..ec2f9a4d1 100644 --- a/test/unit/personal-message-manager-test.js +++ b/test/unit/personal-message-manager-test.js @@ -1,6 +1,4 @@ const assert = require('assert') -// const extend = require('xtend') -// const EventEmitter = require('events') const PersonalMessageManager = require('../../app/scripts/lib/personal-message-manager') diff --git a/test/unit/tx-manager-test.js b/test/unit/tx-manager-test.js index ce4a2acf7..b5d148723 100644 --- a/test/unit/tx-manager-test.js +++ b/test/unit/tx-manager-test.js @@ -1,10 +1,8 @@ const assert = require('assert') -// const extend = require('xtend') const EventEmitter = require('events') const ethUtil = require('ethereumjs-util') const EthTx = require('ethereumjs-tx') const ObservableStore = require('obs-store') -// const STORAGE_KEY = 'metamask-persistance-key' const TransactionManager = require('../../app/scripts/transaction-manager') const noop = () => true const currentNetworkId = 42 -- cgit v1.2.3 From 10ba760ed32be6e23186bd9f9a025e28bd757042 Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 4 May 2017 17:50:59 -0700 Subject: metamask - selected accounts - dont reveal when locked --- app/scripts/metamask-controller.js | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 2e4bf07e1..497b661d4 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -4,7 +4,6 @@ const promiseToCallback = require('promise-to-callback') const pipe = require('pump') const Dnode = require('dnode') const ObservableStore = require('obs-store') -const storeTransform = require('obs-store/lib/transform') const EthStore = require('./lib/eth-store') const EthQuery = require('eth-query') const streamIntoProvider = require('web3-stream-provider/handler') @@ -169,8 +168,13 @@ module.exports = class MetamaskController extends EventEmitter { rpcUrl: this.configManager.getCurrentRpcAddress(), // account mgmt getAccounts: (cb) => { + const isUnlocked = this.keyringController.memStore.getState().isUnlocked + const result = [] const selectedAddress = this.preferencesController.getSelectedAddress() - const result = selectedAddress ? [selectedAddress] : [] + // only show address if account is unlocked + if (isUnlocked && selectedAddress) { + result.push(selectedAddress) + } cb(null, result) }, // tx signing @@ -186,21 +190,19 @@ module.exports = class MetamaskController extends EventEmitter { initPublicConfigStore () { // get init state - const publicConfigStore = new ObservableStore() + const publicConfigStore = new ObservableStore(this.store.getState()) - // sync publicConfigStore with transform - pipe( - this.store, - storeTransform(selectPublicState.bind(this)), - publicConfigStore - ) + // memStore -> transform -> publicConfigStore + this.on('update', (memState) => { + const publicState = selectPublicState(memState) + publicConfigStore.putState(publicState) + }) - function selectPublicState (state) { - const result = { selectedAddress: undefined } - try { - result.selectedAddress = state.PreferencesController.selectedAddress - result.networkVersion = this.getNetworkState() - } catch (_) {} + function selectPublicState (memState) { + const result = { + selectedAddress: memState.isUnlocked ? memState.selectedAddress : undefined, + networkVersion: memState.network, + } return result } -- cgit v1.2.3 From fb08c4a1316248485710a277d397fb5d4f395231 Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 4 May 2017 17:56:30 -0700 Subject: metamask - publicConfig - fix init state --- app/scripts/metamask-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 497b661d4..175602ec1 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -190,7 +190,7 @@ module.exports = class MetamaskController extends EventEmitter { initPublicConfigStore () { // get init state - const publicConfigStore = new ObservableStore(this.store.getState()) + const publicConfigStore = new ObservableStore() // memStore -> transform -> publicConfigStore this.on('update', (memState) => { -- cgit v1.2.3 From 998128d4dfd246ecf7bc206536f6143da4c97bbd Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Sun, 7 May 2017 15:20:56 -0700 Subject: allow copy(logState()) to copy to clipboard --- ui/app/reducers.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/app/reducers.js b/ui/app/reducers.js index c656af849..11efca529 100644 --- a/ui/app/reducers.js +++ b/ui/app/reducers.js @@ -44,6 +44,7 @@ function rootReducer (state, action) { window.logState = function () { var stateString = JSON.stringify(window.METAMASK_CACHED_LOG_STATE, removeSeedWords, 2) console.log(stateString) + return stateString } function removeSeedWords (key, value) { -- cgit v1.2.3 From a09dca68a456ba0406b9d88556e44f9a1e0f8c57 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Sun, 7 May 2017 16:44:23 -0700 Subject: Fix changelog formatting --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d552e6ab8..627f13aea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,6 @@ ## 3.6.1 2017-4-30 - Made fox less nosy. - - - Fix bug where error was reported in debugger console when Chrome opened a new window. - Fix bug where block-tracker could stop polling for new blocks. -- cgit v1.2.3 From f17c6b4eef8defe55e316ee782b499072e1e795a Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Sun, 7 May 2017 16:44:43 -0700 Subject: Fix ens iterated element without key error --- ui/app/components/ens-input.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js index f1cf49998..001c227c4 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/ens-input.js @@ -63,6 +63,7 @@ EnsInput.prototype.render = function () { return h('option', { value: identity.address, label: identity.name, + key: identity.address, }) }), ]), -- cgit v1.2.3 From 80d8a4e73ef2d55dd9024f6e4f8cf94f263703cc Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Sun, 7 May 2017 16:51:57 -0700 Subject: Input gas in gwei Also enforces "safe low gas" minimum recommended by this article by eth-gas-station: https://medium.com/@ethgasstation/the-safe-low-gas-price-fb44fdc85b91 Fixes #1381 --- ui/app/components/pending-tx.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 4b28ae099..c381066a9 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -15,7 +15,9 @@ const addressSummary = util.addressSummary const nameForAddress = require('../../lib/contract-namer') const HexInput = require('./hex-as-decimal-input') -const MIN_GAS_PRICE_BN = new BN(20000000) +const MIN_GAS_PRICE_GWEI_BN = new BN(2) +const GWEI_FACTOR = new BN(Math.pow(10, 9)) +const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) const MIN_GAS_LIMIT_BN = new BN(21000) module.exports = connect(mapStateToProps)(PendingTx) @@ -39,16 +41,20 @@ PendingTx.prototype.render = function () { const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} + // Account Details const address = txParams.from || props.selectedAddress const identity = props.identities[address] || { address: address } const account = props.accounts[address] const balance = account ? account.balance : '0x0' + // Gas const gas = txParams.gas - const gasPrice = txParams.gasPrice - const gasBn = hexToBn(gas) + + // Gas Price + const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) const gasPriceBn = hexToBn(gasPrice) + const gasPriceGweiBn = gasPriceBn.div(new BN(Math.pow(10, 9))) const txFeeBn = gasBn.mul(gasPriceBn) const valueBn = hexToBn(txParams.value) @@ -187,17 +193,18 @@ PendingTx.prototype.render = function () { }, [ h(HexInput, { name: 'Gas Price', - value: gasPrice, - suffix: 'WEI', - min: MIN_GAS_PRICE_BN.toString(10), + value: gasPriceGweiBn.toString(16), + suffix: 'GWEI', + min: MIN_GAS_PRICE_GWEI_BN.toString(10), style: { position: 'relative', top: '5px', }, onChange: (newHex) => { log.info(`Gas price changed to: ${newHex}`) + const inWei = hexToBn(newHex).mul(GWEI_FACTOR) const txMeta = this.gatherTxMeta() - txMeta.txParams.gasPrice = newHex + txMeta.txParams.gasPrice = inWei.toString(16) this.setState({ txData: txMeta }) }, ref: (hexInput) => { this.inputs.push(hexInput) }, -- cgit v1.2.3 From 17accd3a200109b286fda0920b1a73edeeedfaa2 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Sun, 7 May 2017 16:53:14 -0700 Subject: Bump changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 627f13aea..8a57cc217 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Current Master +- Input gas price in Gwei. +- Enforce Safe Gas Minimum recommended by EthGasStation. + ## 3.6.1 2017-4-30 - Made fox less nosy. -- cgit v1.2.3 From 0d39de6d66aab2b87ea7415cb5451ac61c055fd8 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 8 May 2017 09:53:30 -0700 Subject: Run install before dist --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6e83b3560..8e63c2467 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "start": "npm run dev", "dev": "gulp dev --debug", "disc": "gulp disc --debug", - "dist": "gulp dist --disableLiveReload", + "dist": "npm install && gulp dist --disableLiveReload", "test": "npm run lint && npm run test-unit && npm run test-integration", "test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"", "test-integration": "npm run buildMock && npm run buildCiUnits && testem ci -P 2", -- cgit v1.2.3 From 469648239fc94a6599a0b2483364a8a74d66e5a1 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 8 May 2017 09:55:14 -0700 Subject: Linted --- app/scripts/lib/config-manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index 340ad4292..ab9410842 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -155,7 +155,7 @@ ConfigManager.prototype.getCurrentRpcAddress = function () { case 'kovan': return KOVAN_RPC - + case 'rinkeby': return RINKEBY_RPC -- cgit v1.2.3 From d7a2d29e6c6fadae5fa7f94d1bd2762560534463 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 8 May 2017 12:04:57 -0700 Subject: fix block polling changelog note --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a57cc217..c94799a8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,12 @@ - Input gas price in Gwei. - Enforce Safe Gas Minimum recommended by EthGasStation. +- Fix bug where block-tracker could stop polling for new blocks. ## 3.6.1 2017-4-30 - Made fox less nosy. - Fix bug where error was reported in debugger console when Chrome opened a new window. -- Fix bug where block-tracker could stop polling for new blocks. ## 3.6.0 2017-4-26 -- cgit v1.2.3 From 68be86abe959afd26ea50e0833c552c442bfce51 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 8 May 2017 12:29:08 -0700 Subject: ui - remove web3, use eth-query --- app/scripts/popup-core.js | 5 +++-- ui/app/actions.js | 2 +- ui/app/components/ens-input.js | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/scripts/popup-core.js b/app/scripts/popup-core.js index f9ac4d052..f1eb394d7 100644 --- a/app/scripts/popup-core.js +++ b/app/scripts/popup-core.js @@ -1,7 +1,7 @@ const EventEmitter = require('events').EventEmitter const async = require('async') const Dnode = require('dnode') -const Web3 = require('web3') +const EthQuery = require('eth-query') const launchMetamaskUi = require('../../ui') const StreamProvider = require('web3-stream-provider') const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex @@ -32,7 +32,8 @@ function setupWeb3Connection (connectionStream) { providerStream.pipe(connectionStream).pipe(providerStream) connectionStream.on('error', console.error.bind(console)) providerStream.on('error', console.error.bind(console)) - global.web3 = new Web3(providerStream) + global.ethereumProvider = providerStream + global.ethQuery = new EthQuery(providerStream) } function setupControllerConnection (connectionStream, cb) { diff --git a/ui/app/actions.js b/ui/app/actions.js index c15c9be7e..1a3557cb4 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -393,7 +393,7 @@ function signPersonalMsg (msgData) { function signTx (txData) { return (dispatch) => { - web3.eth.sendTransaction(txData, (err, data) => { + global.ethQuery.sendTransaction(txData, (err, data) => { dispatch(actions.hideLoadingIndication()) if (err) return dispatch(actions.displayWarning(err.message)) dispatch(actions.hideWarning()) diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js index f1cf49998..0457bde2e 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/ens-input.js @@ -75,7 +75,7 @@ EnsInput.prototype.componentDidMount = function () { const resolverAddress = networkResolvers[network] if (resolverAddress) { - const provider = web3.currentProvider + const provider = global.ethereumProvider this.ens = new ENS({ provider, network }) this.checkName = debounce(this.lookupEnsName.bind(this), 200) } -- cgit v1.2.3 From 21b6a1b478032fa51505c1be517587515fe8bd6c Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 8 May 2017 12:29:38 -0700 Subject: deps - bump eth-query for smaller bundle size --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ee7ba7aab..1ec784a69 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "ensnare": "^1.0.0", "eth-bin-to-ops": "^1.0.1", "eth-hd-keyring": "^1.1.1", - "eth-query": "^1.0.3", + "eth-query": "^2.1.1", "eth-sig-util": "^1.1.1", "eth-simple-keyring": "^1.1.1", "ethereumjs-tx": "^1.3.0", -- cgit v1.2.3 From 05000683277cd3eb2dec9ce656fbfddb1e48f0b2 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 8 May 2017 12:30:47 -0700 Subject: build - fix disc task --- gulpfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index 21b925780..9f4a606be 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -182,7 +182,7 @@ gulp.task('build:js', gulp.parallel(...jsBuildStrings)) // disc bundle analyzer tasks jsFiles.forEach((jsFile) => { - gulp.task(`disc:${jsFile}`, bundleTask({ label: jsFile, filename: `${jsFile}.js` })) + gulp.task(`disc:${jsFile}`, discTask({ label: jsFile, filename: `${jsFile}.js` })) }) gulp.task('disc', gulp.parallel(jsFiles.map(jsFile => `disc:${jsFile}`))) -- cgit v1.2.3 From e921f7f13a81bbf2e10fb996a001f16daf944940 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 8 May 2017 13:32:45 -0700 Subject: Add changelog entry for 1390 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c94799a8a..157c4186a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Input gas price in Gwei. - Enforce Safe Gas Minimum recommended by EthGasStation. - Fix bug where block-tracker could stop polling for new blocks. +- Reduce UI size by removing internal web3. ## 3.6.1 2017-4-30 -- cgit v1.2.3 From c7b2f2f2e986981496168dbf57cee787882ffd59 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 8 May 2017 13:34:01 -0700 Subject: Cleanup --- ui/app/components/pending-tx.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index c381066a9..71d4d767a 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -16,7 +16,7 @@ const nameForAddress = require('../../lib/contract-namer') const HexInput = require('./hex-as-decimal-input') const MIN_GAS_PRICE_GWEI_BN = new BN(2) -const GWEI_FACTOR = new BN(Math.pow(10, 9)) +const GWEI_FACTOR = new BN(1e9) const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) const MIN_GAS_LIMIT_BN = new BN(21000) @@ -54,7 +54,7 @@ PendingTx.prototype.render = function () { // Gas Price const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) const gasPriceBn = hexToBn(gasPrice) - const gasPriceGweiBn = gasPriceBn.div(new BN(Math.pow(10, 9))) + const gasPriceGweiBn = gasPriceBn.div(GWEI_FACTOR) const txFeeBn = gasBn.mul(gasPriceBn) const valueBn = hexToBn(txParams.value) -- cgit v1.2.3 From aa1139762c500428406b1ef725eb72280cf74995 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Mon, 8 May 2017 13:54:25 -0700 Subject: Add new beta notice. --- notices/archive/notice_2.md | 8 ++++++++ notices/notice-nonce.json | 2 +- notices/notices.json | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 notices/archive/notice_2.md diff --git a/notices/archive/notice_2.md b/notices/archive/notice_2.md new file mode 100644 index 000000000..76e9bd8fb --- /dev/null +++ b/notices/archive/notice_2.md @@ -0,0 +1,8 @@ +MetaMask is beta software. + +When you log in to MetaMask, your current account is visible to every new site you visit. + +For your privacy, for now, please sign out of MetaMask when you're done using a site. + +Also, by default, you will be signed in to a test network. To use real Ether, you must connect to the main network manually in the top left network menu. + diff --git a/notices/notice-nonce.json b/notices/notice-nonce.json index d8263ee98..e440e5c84 100644 --- a/notices/notice-nonce.json +++ b/notices/notice-nonce.json @@ -1 +1 @@ -2 \ No newline at end of file +3 \ No newline at end of file diff --git a/notices/notices.json b/notices/notices.json index 9f28b32a6..e7f74c925 100644 --- a/notices/notices.json +++ b/notices/notices.json @@ -1 +1 @@ -[{"read":false,"date":"Thu Feb 09 2017","title":"Terms of Use","body":"# Terms of Use #\n\n**THIS AGREEMENT IS SUBJECT TO BINDING ARBITRATION AND A WAIVER OF CLASS ACTION RIGHTS AS DETAILED IN SECTION 13. PLEASE READ THE AGREEMENT CAREFULLY.**\n\n_Our Terms of Use have been updated as of September 5, 2016_\n\n## 1. Acceptance of Terms ##\n\nMetaMask provides a platform for managing Ethereum (or \"ETH\") accounts, and allowing ordinary websites to interact with the Ethereum blockchain, while keeping the user in control over what transactions they approve, through our website located at[ ](http://metamask.io)[https://metamask.io/](https://metamask.io/) and browser plugin (the \"Site\") — which includes text, images, audio, code and other materials (collectively, the “Content”) and all of the features, and services provided. The Site, and any other features, tools, materials, or other services offered from time to time by MetaMask are referred to here as the “Service.” Please read these Terms of Use (the “Terms” or “Terms of Use”) carefully before using the Service. By using or otherwise accessing the Services, or clicking to accept or agree to these Terms where that option is made available, you (1) accept and agree to these Terms (2) consent to the collection, use, disclosure and other handling of information as described in our Privacy Policy and (3) any additional terms, rules and conditions of participation issued by MetaMask from time to time. If you do not agree to the Terms, then you may not access or use the Content or Services.\n\n## 2. Modification of Terms of Use ##\n\nExcept for Section 13, providing for binding arbitration and waiver of class action rights, MetaMask reserves the right, at its sole discretion, to modify or replace the Terms of Use at any time. The most current version of these Terms will be posted on our Site. You shall be responsible for reviewing and becoming familiar with any such modifications. Use of the Services by you after any modification to the Terms constitutes your acceptance of the Terms of Use as modified.\n\n\n\n## 3. Eligibility ##\n\nYou hereby represent and warrant that you are fully able and competent to enter into the terms, conditions, obligations, affirmations, representations and warranties set forth in these Terms and to abide by and comply with these Terms.\n\nMetaMask is a global platform and by accessing the Content or Services, you are representing and warranting that, you are of the legal age of majority in your jurisdiction as is required to access such Services and Content and enter into arrangements as provided by the Service. You further represent that you are otherwise legally permitted to use the service in your jurisdiction including owning cryptographic tokens of value, and interacting with the Services or Content in any way. You further represent you are responsible for ensuring compliance with the laws of your jurisdiction and acknowledge that MetaMask is not liable for your compliance with such laws.\n\n## 4 Account Password and Security ##\n\nWhen setting up an account within MetaMask, you will be responsible for keeping your own account secrets, which may be a twelve-word seed phrase, an account file, or other locally stored secret information. MetaMask encrypts this information locally with a password you provide, that we never send to our servers. You agree to (a) never use the same password for MetaMask that you have ever used outside of this service; (b) keep your secret information and password confidential and do not share them with anyone else; (c) immediately notify MetaMask of any unauthorized use of your account or breach of security. MetaMask cannot and will not be liable for any loss or damage arising from your failure to comply with this section.\n\n## 5. Representations, Warranties, and Risks ##\n\n### 5.1. Warranty Disclaimer ###\n\nYou expressly understand and agree that your use of the Service is at your sole risk. The Service (including the Service and the Content) are provided on an \"AS IS\" and \"as available\" basis, without warranties of any kind, either express or implied, including, without limitation, implied warranties of merchantability, fitness for a particular purpose or non-infringement. You acknowledge that MetaMask has no control over, and no duty to take any action regarding: which users gain access to or use the Service; what effects the Content may have on you; how you may interpret or use the Content; or what actions you may take as a result of having been exposed to the Content. You release MetaMask from all liability for you having acquired or not acquired Content through the Service. MetaMask makes no representations concerning any Content contained in or accessed through the Service, and MetaMask will not be responsible or liable for the accuracy, copyright compliance, legality or decency of material contained in or accessed through the Service.\n\n### 5.2 Sophistication and Risk of Cryptographic Systems ###\n\nBy utilizing the Service or interacting with the Content or platform in any way, you represent that you understand the inherent risks associated with cryptographic systems; and warrant that you have an understanding of the usage and intricacies of native cryptographic tokens, like Ether (ETH) and Bitcoin (BTC), smart contract based tokens such as those that follow the Ethereum Token Standard (https://github.com/ethereum/EIPs/issues/20), and blockchain-based software systems.\n\n### 5.3 Risk of Regulatory Actions in One or More Jurisdictions ###\n\nMetaMask and ETH could be impacted by one or more regulatory inquiries or regulatory action, which could impede or limit the ability of MetaMask to continue to develop, or which could impede or limit your ability to access or use the Service or Ethereum blockchain.\n\n### 5.4 Risk of Weaknesses or Exploits in the Field of Cryptography ###\n\nYou acknowledge and understand that Cryptography is a progressing field. Advances in code cracking or technical advances such as the development of quantum computers may present risks to cryptocurrencies and Services of Content, which could result in the theft or loss of your cryptographic tokens or property. To the extent possible, MetaMask intends to update the protocol underlying Services to account for any advances in cryptography and to incorporate additional security measures, but does not guarantee or otherwise represent full security of the system. By using the Service or accessing Content, you acknowledge these inherent risks.\n\n### 5.5 Volatility of Crypto Currencies ###\n\nYou understand that Ethereum and other blockchain technologies and associated currencies or tokens are highly volatile due to many factors including but not limited to adoption, speculation, technology and security risks. You also acknowledge that the cost of transacting on such technologies is variable and may increase at any time causing impact to any activities taking place on the Ethereum blockchain. You acknowledge these risks and represent that MetaMask cannot be held liable for such fluctuations or increased costs.\n\n### 5.6 Application Security ###\n\nYou acknowledge that Ethereum applications are code subject to flaws and acknowledge that you are solely responsible for evaluating any code provided by the Services or Content and the trustworthiness of any third-party websites, products, smart-contracts, or Content you access or use through the Service. You further expressly acknowledge and represent that Ethereum applications can be written maliciously or negligently, that MetaMask cannot be held liable for your interaction with such applications and that such applications may cause the loss of property or even identity. This warning and others later provided by MetaMask in no way evidence or represent an on-going duty to alert you to all of the potential risks of utilizing the Service or Content.\n\n## 6. Indemnity ##\n\nYou agree to release and to indemnify, defend and hold harmless MetaMask and its parents, subsidiaries, affiliates and agencies, as well as the officers, directors, employees, shareholders and representatives of any of the foregoing entities, from and against any and all losses, liabilities, expenses, damages, costs (including attorneys’ fees and court costs) claims or actions of any kind whatsoever arising or resulting from your use of the Service, your violation of these Terms of Use, and any of your acts or omissions that implicate publicity rights, defamation or invasion of privacy. MetaMask reserves the right, at its own expense, to assume exclusive defense and control of any matter otherwise subject to indemnification by you and, in such case, you agree to cooperate with MetaMask in the defense of such matter.\n\n## 7. Limitation on liability ##\n\nYOU ACKNOWLEDGE AND AGREE THAT YOU ASSUME FULL RESPONSIBILITY FOR YOUR USE OF THE SITE AND SERVICE. YOU ACKNOWLEDGE AND AGREE THAT ANY INFORMATION YOU SEND OR RECEIVE DURING YOUR USE OF THE SITE AND SERVICE MAY NOT BE SECURE AND MAY BE INTERCEPTED OR LATER ACQUIRED BY UNAUTHORIZED PARTIES. YOU ACKNOWLEDGE AND AGREE THAT YOUR USE OF THE SITE AND SERVICE IS AT YOUR OWN RISK. RECOGNIZING SUCH, YOU UNDERSTAND AND AGREE THAT, TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, NEITHER METAMASK NOR ITS SUPPLIERS OR LICENSORS WILL BE LIABLE TO YOU FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY OR OTHER DAMAGES OF ANY KIND, INCLUDING WITHOUT LIMITATION DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER TANGIBLE OR INTANGIBLE LOSSES OR ANY OTHER DAMAGES BASED ON CONTRACT, TORT, STRICT LIABILITY OR ANY OTHER THEORY (EVEN IF METAMASK HAD BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES), RESULTING FROM THE SITE OR SERVICE; THE USE OR THE INABILITY TO USE THE SITE OR SERVICE; UNAUTHORIZED ACCESS TO OR ALTERATION OF YOUR TRANSMISSIONS OR DATA; STATEMENTS OR CONDUCT OF ANY THIRD PARTY ON THE SITE OR SERVICE; ANY ACTIONS WE TAKE OR FAIL TO TAKE AS A RESULT OF COMMUNICATIONS YOU SEND TO US; HUMAN ERRORS; TECHNICAL MALFUNCTIONS; FAILURES, INCLUDING PUBLIC UTILITY OR TELEPHONE OUTAGES; OMISSIONS, INTERRUPTIONS, LATENCY, DELETIONS OR DEFECTS OF ANY DEVICE OR NETWORK, PROVIDERS, OR SOFTWARE (INCLUDING, BUT NOT LIMITED TO, THOSE THAT DO NOT PERMIT PARTICIPATION IN THE SERVICE); ANY INJURY OR DAMAGE TO COMPUTER EQUIPMENT; INABILITY TO FULLY ACCESS THE SITE OR SERVICE OR ANY OTHER WEBSITE; THEFT, TAMPERING, DESTRUCTION, OR UNAUTHORIZED ACCESS TO, IMAGES OR OTHER CONTENT OF ANY KIND; DATA THAT IS PROCESSED LATE OR INCORRECTLY OR IS INCOMPLETE OR LOST; TYPOGRAPHICAL, PRINTING OR OTHER ERRORS, OR ANY COMBINATION THEREOF; OR ANY OTHER MATTER RELATING TO THE SITE OR SERVICE.\n\nSOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR THE LIMITATION OR EXCLUSION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES. ACCORDINGLY, SOME OF THE ABOVE LIMITATIONS MAY NOT APPLY TO YOU.\n\n## 8. Our Proprietary Rights ##\n\nAll title, ownership and intellectual property rights in and to the Service are owned by MetaMask or its licensors. You acknowledge and agree that the Service contains proprietary and confidential information that is protected by applicable intellectual property and other laws. Except as expressly authorized by MetaMask, you agree not to copy, modify, rent, lease, loan, sell, distribute, perform, display or create derivative works based on the Service, in whole or in part. MetaMask issues a license for MetaMask, found [here](https://github.com/MetaMask/metamask-plugin/blob/master/LICENSE). For information on other licenses utilized in the development of MetaMask, please see our attribution page at: [https://metamask.io/attributions.html](https://metamask.io/attributions.html)\n\n## 9. Links ##\n\nThe Service provides, or third parties may provide, links to other World Wide Web or accessible sites, applications or resources. Because MetaMask has no control over such sites, applications and resources, you acknowledge and agree that MetaMask is not responsible for the availability of such external sites, applications or resources, and does not endorse and is not responsible or liable for any content, advertising, products or other materials on or available from such sites or resources. You further acknowledge and agree that MetaMask shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such site or resource.\n\n## 10. Termination and Suspension ##\n\nMetaMask may terminate or suspend all or part of the Service and your MetaMask access immediately, without prior notice or liability, if you breach any of the terms or conditions of the Terms. Upon termination of your access, your right to use the Service will immediately cease.\n\nThe following provisions of the Terms survive any termination of these Terms: INDEMNITY; WARRANTY DISCLAIMERS; LIMITATION ON LIABILITY; OUR PROPRIETARY RIGHTS; LINKS; TERMINATION; NO THIRD PARTY BENEFICIARIES; BINDING ARBITRATION AND CLASS ACTION WAIVER; GENERAL INFORMATION.\n\n## 11. No Third Party Beneficiaries ##\n\nYou agree that, except as otherwise expressly provided in these Terms, there shall be no third party beneficiaries to the Terms.\n\n## 12. Notice and Procedure For Making Claims of Copyright Infringement ##\n\nIf you believe that your copyright or the copyright of a person on whose behalf you are authorized to act has been infringed, please provide MetaMask’s Copyright Agent a written Notice containing the following information:\n\n· an electronic or physical signature of the person authorized to act on behalf of the owner of the copyright or other intellectual property interest;\n\n· a description of the copyrighted work or other intellectual property that you claim has been infringed;\n\n· a description of where the material that you claim is infringing is located on the Service;\n\n· your address, telephone number, and email address;\n\n· a statement by you that you have a good faith belief that the disputed use is not authorized by the copyright owner, its agent, or the law;\n\n· a statement by you, made under penalty of perjury, that the above information in your Notice is accurate and that you are the copyright or intellectual property owner or authorized to act on the copyright or intellectual property owner's behalf.\n\nMetaMask’s Copyright Agent can be reached at:\n\nEmail: copyright [at] metamask [dot] io\n\nMail:\n\nAttention:\n\nMetaMask Copyright ℅ ConsenSys\n\n49 Bogart Street\n\nBrooklyn, NY 11206\n\n## 13. Binding Arbitration and Class Action Waiver ##\n\nPLEASE READ THIS SECTION CAREFULLY – IT MAY SIGNIFICANTLY AFFECT YOUR LEGAL RIGHTS, INCLUDING YOUR RIGHT TO FILE A LAWSUIT IN COURT\n\n### 13.1 Initial Dispute Resolution ###\n\nThe parties shall use their best efforts to engage directly to settle any dispute, claim, question, or disagreement and engage in good faith negotiations which shall be a condition to either party initiating a lawsuit or arbitration.\n\n### 13.2 Binding Arbitration ###\n\nIf the parties do not reach an agreed upon solution within a period of 30 days from the time informal dispute resolution under the Initial Dispute Resolution provision begins, then either party may initiate binding arbitration as the sole means to resolve claims, subject to the terms set forth below. Specifically, all claims arising out of or relating to these Terms (including their formation, performance and breach), the parties’ relationship with each other and/or your use of the Service shall be finally settled by binding arbitration administered by the American Arbitration Association in accordance with the provisions of its Commercial Arbitration Rules and the supplementary procedures for consumer related disputes of the American Arbitration Association (the \"AAA\"), excluding any rules or procedures governing or permitting class actions.\n\nThe arbitrator, and not any federal, state or local court or agency, shall have exclusive authority to resolve all disputes arising out of or relating to the interpretation, applicability, enforceability or formation of these Terms, including, but not limited to any claim that all or any part of these Terms are void or voidable, or whether a claim is subject to arbitration. The arbitrator shall be empowered to grant whatever relief would be available in a court under law or in equity. The arbitrator’s award shall be written, and binding on the parties and may be entered as a judgment in any court of competent jurisdiction.\n\nThe parties understand that, absent this mandatory provision, they would have the right to sue in court and have a jury trial. They further understand that, in some instances, the costs of arbitration could exceed the costs of litigation and the right to discovery may be more limited in arbitration than in court.\n\n### 13.3 Location ###\n\nBinding arbitration shall take place in New York. You agree to submit to the personal jurisdiction of any federal or state court in New York County, New York, in order to compel arbitration, to stay proceedings pending arbitration, or to confirm, modify, vacate or enter judgment on the award entered by the arbitrator.\n\n### 13.4 Class Action Waiver ###\n\nThe parties further agree that any arbitration shall be conducted in their individual capacities only and not as a class action or other representative action, and the parties expressly waive their right to file a class action or seek relief on a class basis. YOU AND METAMASK AGREE THAT EACH MAY BRING CLAIMS AGAINST THE OTHER ONLY IN YOUR OR ITS INDIVIDUAL CAPACITY, AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR REPRESENTATIVE PROCEEDING. If any court or arbitrator determines that the class action waiver set forth in this paragraph is void or unenforceable for any reason or that an arbitration can proceed on a class basis, then the arbitration provision set forth above shall be deemed null and void in its entirety and the parties shall be deemed to have not agreed to arbitrate disputes.\n\n### 13.5 Exception - Litigation of Intellectual Property and Small Claims Court Claims ###\n\nNotwithstanding the parties' decision to resolve all disputes through arbitration, either party may bring an action in state or federal court to protect its intellectual property rights (\"intellectual property rights\" means patents, copyrights, moral rights, trademarks, and trade secrets, but not privacy or publicity rights). Either party may also seek relief in a small claims court for disputes or claims within the scope of that court’s jurisdiction.\n\n### 13.6 30-Day Right to Opt Out ###\n\nYou have the right to opt-out and not be bound by the arbitration and class action waiver provisions set forth above by sending written notice of your decision to opt-out to the following address: MetaMask ℅ ConsenSys, 49 Bogart Street, Brooklyn NY 11206 and via email at legal-opt@metamask.io. The notice must be sent within 30 days of September 6, 2016 or your first use of the Service, whichever is later, otherwise you shall be bound to arbitrate disputes in accordance with the terms of those paragraphs. If you opt-out of these arbitration provisions, MetaMask also will not be bound by them.\n\n### 13.7 Changes to This Section ###\n\nMetaMask will provide 60-days’ notice of any changes to this section. Changes will become effective on the 60th day, and will apply prospectively only to any claims arising after the 60th day.\n\nFor any dispute not subject to arbitration you and MetaMask agree to submit to the personal and exclusive jurisdiction of and venue in the federal and state courts located in New York, New York. You further agree to accept service of process by mail, and hereby waive any and all jurisdictional and venue defenses otherwise available.\n\nThe Terms and the relationship between you and MetaMask shall be governed by the laws of the State of New York without regard to conflict of law provisions.\n\n## 14. General Information ##\n\n### 14.1 Entire Agreement ###\n\nThese Terms (and any additional terms, rules and conditions of participation that MetaMask may post on the Service) constitute the entire agreement between you and MetaMask with respect to the Service and supersedes any prior agreements, oral or written, between you and MetaMask. In the event of a conflict between these Terms and the additional terms, rules and conditions of participation, the latter will prevail over the Terms to the extent of the conflict.\n\n### 14.2 Waiver and Severability of Terms ###\n\nThe failure of MetaMask to exercise or enforce any right or provision of the Terms shall not constitute a waiver of such right or provision. If any provision of the Terms is found by an arbitrator or court of competent jurisdiction to be invalid, the parties nevertheless agree that the arbitrator or court should endeavor to give effect to the parties' intentions as reflected in the provision, and the other provisions of the Terms remain in full force and effect.\n\n### 14.3 Statute of Limitations ###\n\nYou agree that regardless of any statute or law to the contrary, any claim or cause of action arising out of or related to the use of the Service or the Terms must be filed within one (1) year after such claim or cause of action arose or be forever barred.\n\n### 14.4 Section Titles ###\n\nThe section titles in the Terms are for convenience only and have no legal or contractual effect.\n\n### 14.5 Communications ###\n\nUsers with questions, complaints or claims with respect to the Service may contact us using the relevant contact information set forth above and at communications@metamask.io.\n\n## 15 Related Links ##\n\n**[Terms of Use](https://metamask.io/terms.html)**\n\n**[Privacy](https://metamask.io/privacy.html)**\n\n**[Attributions](https://metamask.io/attributions.html)**\n\n","id":0}] \ No newline at end of file +[{"read":false,"date":"Thu Feb 09 2017","title":"Terms of Use","body":"# Terms of Use #\n\n**THIS AGREEMENT IS SUBJECT TO BINDING ARBITRATION AND A WAIVER OF CLASS ACTION RIGHTS AS DETAILED IN SECTION 13. PLEASE READ THE AGREEMENT CAREFULLY.**\n\n_Our Terms of Use have been updated as of September 5, 2016_\n\n## 1. Acceptance of Terms ##\n\nMetaMask provides a platform for managing Ethereum (or \"ETH\") accounts, and allowing ordinary websites to interact with the Ethereum blockchain, while keeping the user in control over what transactions they approve, through our website located at[ ](http://metamask.io)[https://metamask.io/](https://metamask.io/) and browser plugin (the \"Site\") — which includes text, images, audio, code and other materials (collectively, the “Content”) and all of the features, and services provided. The Site, and any other features, tools, materials, or other services offered from time to time by MetaMask are referred to here as the “Service.” Please read these Terms of Use (the “Terms” or “Terms of Use”) carefully before using the Service. By using or otherwise accessing the Services, or clicking to accept or agree to these Terms where that option is made available, you (1) accept and agree to these Terms (2) consent to the collection, use, disclosure and other handling of information as described in our Privacy Policy and (3) any additional terms, rules and conditions of participation issued by MetaMask from time to time. If you do not agree to the Terms, then you may not access or use the Content or Services.\n\n## 2. Modification of Terms of Use ##\n\nExcept for Section 13, providing for binding arbitration and waiver of class action rights, MetaMask reserves the right, at its sole discretion, to modify or replace the Terms of Use at any time. The most current version of these Terms will be posted on our Site. You shall be responsible for reviewing and becoming familiar with any such modifications. Use of the Services by you after any modification to the Terms constitutes your acceptance of the Terms of Use as modified.\n\n\n\n## 3. Eligibility ##\n\nYou hereby represent and warrant that you are fully able and competent to enter into the terms, conditions, obligations, affirmations, representations and warranties set forth in these Terms and to abide by and comply with these Terms.\n\nMetaMask is a global platform and by accessing the Content or Services, you are representing and warranting that, you are of the legal age of majority in your jurisdiction as is required to access such Services and Content and enter into arrangements as provided by the Service. You further represent that you are otherwise legally permitted to use the service in your jurisdiction including owning cryptographic tokens of value, and interacting with the Services or Content in any way. You further represent you are responsible for ensuring compliance with the laws of your jurisdiction and acknowledge that MetaMask is not liable for your compliance with such laws.\n\n## 4 Account Password and Security ##\n\nWhen setting up an account within MetaMask, you will be responsible for keeping your own account secrets, which may be a twelve-word seed phrase, an account file, or other locally stored secret information. MetaMask encrypts this information locally with a password you provide, that we never send to our servers. You agree to (a) never use the same password for MetaMask that you have ever used outside of this service; (b) keep your secret information and password confidential and do not share them with anyone else; (c) immediately notify MetaMask of any unauthorized use of your account or breach of security. MetaMask cannot and will not be liable for any loss or damage arising from your failure to comply with this section.\n\n## 5. Representations, Warranties, and Risks ##\n\n### 5.1. Warranty Disclaimer ###\n\nYou expressly understand and agree that your use of the Service is at your sole risk. The Service (including the Service and the Content) are provided on an \"AS IS\" and \"as available\" basis, without warranties of any kind, either express or implied, including, without limitation, implied warranties of merchantability, fitness for a particular purpose or non-infringement. You acknowledge that MetaMask has no control over, and no duty to take any action regarding: which users gain access to or use the Service; what effects the Content may have on you; how you may interpret or use the Content; or what actions you may take as a result of having been exposed to the Content. You release MetaMask from all liability for you having acquired or not acquired Content through the Service. MetaMask makes no representations concerning any Content contained in or accessed through the Service, and MetaMask will not be responsible or liable for the accuracy, copyright compliance, legality or decency of material contained in or accessed through the Service.\n\n### 5.2 Sophistication and Risk of Cryptographic Systems ###\n\nBy utilizing the Service or interacting with the Content or platform in any way, you represent that you understand the inherent risks associated with cryptographic systems; and warrant that you have an understanding of the usage and intricacies of native cryptographic tokens, like Ether (ETH) and Bitcoin (BTC), smart contract based tokens such as those that follow the Ethereum Token Standard (https://github.com/ethereum/EIPs/issues/20), and blockchain-based software systems.\n\n### 5.3 Risk of Regulatory Actions in One or More Jurisdictions ###\n\nMetaMask and ETH could be impacted by one or more regulatory inquiries or regulatory action, which could impede or limit the ability of MetaMask to continue to develop, or which could impede or limit your ability to access or use the Service or Ethereum blockchain.\n\n### 5.4 Risk of Weaknesses or Exploits in the Field of Cryptography ###\n\nYou acknowledge and understand that Cryptography is a progressing field. Advances in code cracking or technical advances such as the development of quantum computers may present risks to cryptocurrencies and Services of Content, which could result in the theft or loss of your cryptographic tokens or property. To the extent possible, MetaMask intends to update the protocol underlying Services to account for any advances in cryptography and to incorporate additional security measures, but does not guarantee or otherwise represent full security of the system. By using the Service or accessing Content, you acknowledge these inherent risks.\n\n### 5.5 Volatility of Crypto Currencies ###\n\nYou understand that Ethereum and other blockchain technologies and associated currencies or tokens are highly volatile due to many factors including but not limited to adoption, speculation, technology and security risks. You also acknowledge that the cost of transacting on such technologies is variable and may increase at any time causing impact to any activities taking place on the Ethereum blockchain. You acknowledge these risks and represent that MetaMask cannot be held liable for such fluctuations or increased costs.\n\n### 5.6 Application Security ###\n\nYou acknowledge that Ethereum applications are code subject to flaws and acknowledge that you are solely responsible for evaluating any code provided by the Services or Content and the trustworthiness of any third-party websites, products, smart-contracts, or Content you access or use through the Service. You further expressly acknowledge and represent that Ethereum applications can be written maliciously or negligently, that MetaMask cannot be held liable for your interaction with such applications and that such applications may cause the loss of property or even identity. This warning and others later provided by MetaMask in no way evidence or represent an on-going duty to alert you to all of the potential risks of utilizing the Service or Content.\n\n## 6. Indemnity ##\n\nYou agree to release and to indemnify, defend and hold harmless MetaMask and its parents, subsidiaries, affiliates and agencies, as well as the officers, directors, employees, shareholders and representatives of any of the foregoing entities, from and against any and all losses, liabilities, expenses, damages, costs (including attorneys’ fees and court costs) claims or actions of any kind whatsoever arising or resulting from your use of the Service, your violation of these Terms of Use, and any of your acts or omissions that implicate publicity rights, defamation or invasion of privacy. MetaMask reserves the right, at its own expense, to assume exclusive defense and control of any matter otherwise subject to indemnification by you and, in such case, you agree to cooperate with MetaMask in the defense of such matter.\n\n## 7. Limitation on liability ##\n\nYOU ACKNOWLEDGE AND AGREE THAT YOU ASSUME FULL RESPONSIBILITY FOR YOUR USE OF THE SITE AND SERVICE. YOU ACKNOWLEDGE AND AGREE THAT ANY INFORMATION YOU SEND OR RECEIVE DURING YOUR USE OF THE SITE AND SERVICE MAY NOT BE SECURE AND MAY BE INTERCEPTED OR LATER ACQUIRED BY UNAUTHORIZED PARTIES. YOU ACKNOWLEDGE AND AGREE THAT YOUR USE OF THE SITE AND SERVICE IS AT YOUR OWN RISK. RECOGNIZING SUCH, YOU UNDERSTAND AND AGREE THAT, TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, NEITHER METAMASK NOR ITS SUPPLIERS OR LICENSORS WILL BE LIABLE TO YOU FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY OR OTHER DAMAGES OF ANY KIND, INCLUDING WITHOUT LIMITATION DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER TANGIBLE OR INTANGIBLE LOSSES OR ANY OTHER DAMAGES BASED ON CONTRACT, TORT, STRICT LIABILITY OR ANY OTHER THEORY (EVEN IF METAMASK HAD BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES), RESULTING FROM THE SITE OR SERVICE; THE USE OR THE INABILITY TO USE THE SITE OR SERVICE; UNAUTHORIZED ACCESS TO OR ALTERATION OF YOUR TRANSMISSIONS OR DATA; STATEMENTS OR CONDUCT OF ANY THIRD PARTY ON THE SITE OR SERVICE; ANY ACTIONS WE TAKE OR FAIL TO TAKE AS A RESULT OF COMMUNICATIONS YOU SEND TO US; HUMAN ERRORS; TECHNICAL MALFUNCTIONS; FAILURES, INCLUDING PUBLIC UTILITY OR TELEPHONE OUTAGES; OMISSIONS, INTERRUPTIONS, LATENCY, DELETIONS OR DEFECTS OF ANY DEVICE OR NETWORK, PROVIDERS, OR SOFTWARE (INCLUDING, BUT NOT LIMITED TO, THOSE THAT DO NOT PERMIT PARTICIPATION IN THE SERVICE); ANY INJURY OR DAMAGE TO COMPUTER EQUIPMENT; INABILITY TO FULLY ACCESS THE SITE OR SERVICE OR ANY OTHER WEBSITE; THEFT, TAMPERING, DESTRUCTION, OR UNAUTHORIZED ACCESS TO, IMAGES OR OTHER CONTENT OF ANY KIND; DATA THAT IS PROCESSED LATE OR INCORRECTLY OR IS INCOMPLETE OR LOST; TYPOGRAPHICAL, PRINTING OR OTHER ERRORS, OR ANY COMBINATION THEREOF; OR ANY OTHER MATTER RELATING TO THE SITE OR SERVICE.\n\nSOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR THE LIMITATION OR EXCLUSION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES. ACCORDINGLY, SOME OF THE ABOVE LIMITATIONS MAY NOT APPLY TO YOU.\n\n## 8. Our Proprietary Rights ##\n\nAll title, ownership and intellectual property rights in and to the Service are owned by MetaMask or its licensors. You acknowledge and agree that the Service contains proprietary and confidential information that is protected by applicable intellectual property and other laws. Except as expressly authorized by MetaMask, you agree not to copy, modify, rent, lease, loan, sell, distribute, perform, display or create derivative works based on the Service, in whole or in part. MetaMask issues a license for MetaMask, found [here](https://github.com/MetaMask/metamask-plugin/blob/master/LICENSE). For information on other licenses utilized in the development of MetaMask, please see our attribution page at: [https://metamask.io/attributions.html](https://metamask.io/attributions.html)\n\n## 9. Links ##\n\nThe Service provides, or third parties may provide, links to other World Wide Web or accessible sites, applications or resources. Because MetaMask has no control over such sites, applications and resources, you acknowledge and agree that MetaMask is not responsible for the availability of such external sites, applications or resources, and does not endorse and is not responsible or liable for any content, advertising, products or other materials on or available from such sites or resources. You further acknowledge and agree that MetaMask shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such site or resource.\n\n## 10. Termination and Suspension ##\n\nMetaMask may terminate or suspend all or part of the Service and your MetaMask access immediately, without prior notice or liability, if you breach any of the terms or conditions of the Terms. Upon termination of your access, your right to use the Service will immediately cease.\n\nThe following provisions of the Terms survive any termination of these Terms: INDEMNITY; WARRANTY DISCLAIMERS; LIMITATION ON LIABILITY; OUR PROPRIETARY RIGHTS; LINKS; TERMINATION; NO THIRD PARTY BENEFICIARIES; BINDING ARBITRATION AND CLASS ACTION WAIVER; GENERAL INFORMATION.\n\n## 11. No Third Party Beneficiaries ##\n\nYou agree that, except as otherwise expressly provided in these Terms, there shall be no third party beneficiaries to the Terms.\n\n## 12. Notice and Procedure For Making Claims of Copyright Infringement ##\n\nIf you believe that your copyright or the copyright of a person on whose behalf you are authorized to act has been infringed, please provide MetaMask’s Copyright Agent a written Notice containing the following information:\n\n· an electronic or physical signature of the person authorized to act on behalf of the owner of the copyright or other intellectual property interest;\n\n· a description of the copyrighted work or other intellectual property that you claim has been infringed;\n\n· a description of where the material that you claim is infringing is located on the Service;\n\n· your address, telephone number, and email address;\n\n· a statement by you that you have a good faith belief that the disputed use is not authorized by the copyright owner, its agent, or the law;\n\n· a statement by you, made under penalty of perjury, that the above information in your Notice is accurate and that you are the copyright or intellectual property owner or authorized to act on the copyright or intellectual property owner's behalf.\n\nMetaMask’s Copyright Agent can be reached at:\n\nEmail: copyright [at] metamask [dot] io\n\nMail:\n\nAttention:\n\nMetaMask Copyright ℅ ConsenSys\n\n49 Bogart Street\n\nBrooklyn, NY 11206\n\n## 13. Binding Arbitration and Class Action Waiver ##\n\nPLEASE READ THIS SECTION CAREFULLY – IT MAY SIGNIFICANTLY AFFECT YOUR LEGAL RIGHTS, INCLUDING YOUR RIGHT TO FILE A LAWSUIT IN COURT\n\n### 13.1 Initial Dispute Resolution ###\n\nThe parties shall use their best efforts to engage directly to settle any dispute, claim, question, or disagreement and engage in good faith negotiations which shall be a condition to either party initiating a lawsuit or arbitration.\n\n### 13.2 Binding Arbitration ###\n\nIf the parties do not reach an agreed upon solution within a period of 30 days from the time informal dispute resolution under the Initial Dispute Resolution provision begins, then either party may initiate binding arbitration as the sole means to resolve claims, subject to the terms set forth below. Specifically, all claims arising out of or relating to these Terms (including their formation, performance and breach), the parties’ relationship with each other and/or your use of the Service shall be finally settled by binding arbitration administered by the American Arbitration Association in accordance with the provisions of its Commercial Arbitration Rules and the supplementary procedures for consumer related disputes of the American Arbitration Association (the \"AAA\"), excluding any rules or procedures governing or permitting class actions.\n\nThe arbitrator, and not any federal, state or local court or agency, shall have exclusive authority to resolve all disputes arising out of or relating to the interpretation, applicability, enforceability or formation of these Terms, including, but not limited to any claim that all or any part of these Terms are void or voidable, or whether a claim is subject to arbitration. The arbitrator shall be empowered to grant whatever relief would be available in a court under law or in equity. The arbitrator’s award shall be written, and binding on the parties and may be entered as a judgment in any court of competent jurisdiction.\n\nThe parties understand that, absent this mandatory provision, they would have the right to sue in court and have a jury trial. They further understand that, in some instances, the costs of arbitration could exceed the costs of litigation and the right to discovery may be more limited in arbitration than in court.\n\n### 13.3 Location ###\n\nBinding arbitration shall take place in New York. You agree to submit to the personal jurisdiction of any federal or state court in New York County, New York, in order to compel arbitration, to stay proceedings pending arbitration, or to confirm, modify, vacate or enter judgment on the award entered by the arbitrator.\n\n### 13.4 Class Action Waiver ###\n\nThe parties further agree that any arbitration shall be conducted in their individual capacities only and not as a class action or other representative action, and the parties expressly waive their right to file a class action or seek relief on a class basis. YOU AND METAMASK AGREE THAT EACH MAY BRING CLAIMS AGAINST THE OTHER ONLY IN YOUR OR ITS INDIVIDUAL CAPACITY, AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR REPRESENTATIVE PROCEEDING. If any court or arbitrator determines that the class action waiver set forth in this paragraph is void or unenforceable for any reason or that an arbitration can proceed on a class basis, then the arbitration provision set forth above shall be deemed null and void in its entirety and the parties shall be deemed to have not agreed to arbitrate disputes.\n\n### 13.5 Exception - Litigation of Intellectual Property and Small Claims Court Claims ###\n\nNotwithstanding the parties' decision to resolve all disputes through arbitration, either party may bring an action in state or federal court to protect its intellectual property rights (\"intellectual property rights\" means patents, copyrights, moral rights, trademarks, and trade secrets, but not privacy or publicity rights). Either party may also seek relief in a small claims court for disputes or claims within the scope of that court’s jurisdiction.\n\n### 13.6 30-Day Right to Opt Out ###\n\nYou have the right to opt-out and not be bound by the arbitration and class action waiver provisions set forth above by sending written notice of your decision to opt-out to the following address: MetaMask ℅ ConsenSys, 49 Bogart Street, Brooklyn NY 11206 and via email at legal-opt@metamask.io. The notice must be sent within 30 days of September 6, 2016 or your first use of the Service, whichever is later, otherwise you shall be bound to arbitrate disputes in accordance with the terms of those paragraphs. If you opt-out of these arbitration provisions, MetaMask also will not be bound by them.\n\n### 13.7 Changes to This Section ###\n\nMetaMask will provide 60-days’ notice of any changes to this section. Changes will become effective on the 60th day, and will apply prospectively only to any claims arising after the 60th day.\n\nFor any dispute not subject to arbitration you and MetaMask agree to submit to the personal and exclusive jurisdiction of and venue in the federal and state courts located in New York, New York. You further agree to accept service of process by mail, and hereby waive any and all jurisdictional and venue defenses otherwise available.\n\nThe Terms and the relationship between you and MetaMask shall be governed by the laws of the State of New York without regard to conflict of law provisions.\n\n## 14. General Information ##\n\n### 14.1 Entire Agreement ###\n\nThese Terms (and any additional terms, rules and conditions of participation that MetaMask may post on the Service) constitute the entire agreement between you and MetaMask with respect to the Service and supersedes any prior agreements, oral or written, between you and MetaMask. In the event of a conflict between these Terms and the additional terms, rules and conditions of participation, the latter will prevail over the Terms to the extent of the conflict.\n\n### 14.2 Waiver and Severability of Terms ###\n\nThe failure of MetaMask to exercise or enforce any right or provision of the Terms shall not constitute a waiver of such right or provision. If any provision of the Terms is found by an arbitrator or court of competent jurisdiction to be invalid, the parties nevertheless agree that the arbitrator or court should endeavor to give effect to the parties' intentions as reflected in the provision, and the other provisions of the Terms remain in full force and effect.\n\n### 14.3 Statute of Limitations ###\n\nYou agree that regardless of any statute or law to the contrary, any claim or cause of action arising out of or related to the use of the Service or the Terms must be filed within one (1) year after such claim or cause of action arose or be forever barred.\n\n### 14.4 Section Titles ###\n\nThe section titles in the Terms are for convenience only and have no legal or contractual effect.\n\n### 14.5 Communications ###\n\nUsers with questions, complaints or claims with respect to the Service may contact us using the relevant contact information set forth above and at communications@metamask.io.\n\n## 15 Related Links ##\n\n**[Terms of Use](https://metamask.io/terms.html)**\n\n**[Privacy](https://metamask.io/privacy.html)**\n\n**[Attributions](https://metamask.io/attributions.html)**\n\n","id":0},{"read":false,"date":"Mon May 08 2017","title":"Privacy Notice","body":"MetaMask is beta software. \n\nWhen you log in to MetaMask, your current account is visible to every new site you visit.\n\nFor your privacy, for now, please sign out of MetaMask when you're done using a site.\n\nAlso, by default, you will be signed in to a test network. To use real Ether, you must connect to the main network manually in the top left network menu.\n\n","id":2}] \ No newline at end of file -- cgit v1.2.3 From d61b587f3014823fcd23b1e5becce7949e88d952 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Mon, 8 May 2017 16:02:41 -0700 Subject: Redefine txmeta when submitting. --- ui/app/components/pending-tx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 71d4d767a..5ea885195 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -76,6 +76,7 @@ PendingTx.prototype.render = function () { h('form#pending-tx-form', { onSubmit: (event) => { + const txMeta = this.gatherTxMeta() event.preventDefault() const form = document.querySelector('form#pending-tx-form') const valid = form.checkValidity() @@ -418,4 +419,3 @@ function forwardCarrat () { ) } - -- cgit v1.2.3 From f131f59c82ed7446d98c4d2301ce83aa25877d49 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Mon, 8 May 2017 16:03:28 -0700 Subject: Changelog bump --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 157c4186a..2d3c1513a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Enforce Safe Gas Minimum recommended by EthGasStation. - Fix bug where block-tracker could stop polling for new blocks. - Reduce UI size by removing internal web3. +- Fix bug where gas parameters would not properly update on adjustment. ## 3.6.1 2017-4-30 -- cgit v1.2.3 From 662a646fa92479e30b492520bd0edd753179bbdd Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 8 May 2017 16:20:37 -0700 Subject: Version 3.6.2 --- CHANGELOG.md | 2 ++ app/manifest.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d3c1513a..1abab9c7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +## 3.6.2 2017-5-8 + - Input gas price in Gwei. - Enforce Safe Gas Minimum recommended by EthGasStation. - Fix bug where block-tracker could stop polling for new blocks. diff --git a/app/manifest.json b/app/manifest.json index 3a9b0b29f..faceea60b 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.6.1", + "version": "3.6.2", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From ff1a1284cc768a0e7d179083d360a5579373e256 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 8 May 2017 22:05:38 -0700 Subject: Version 3.6.3 --- CHANGELOG.md | 4 ++++ app/manifest.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1abab9c7a..40ba4d34d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Current Master +## 3.6.3 2017-5-8 + +- Fix bug that could stop newer versions of Geth from working with MetaMask. + ## 3.6.2 2017-5-8 - Input gas price in Gwei. diff --git a/app/manifest.json b/app/manifest.json index faceea60b..cef44446e 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.6.2", + "version": "3.6.3", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From ac54c7d96b503e8d79fae4a67289ae95d09c3c75 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 9 May 2017 11:28:39 -0700 Subject: ens - add mainnet ens support --- ui/app/components/ens-input.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js index ec3cd60ed..04c6222c2 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/ens-input.js @@ -5,11 +5,9 @@ const extend = require('xtend') const debounce = require('debounce') const copyToClipboard = require('copy-to-clipboard') const ENS = require('ethjs-ens') +const networkMap = require('ethjs-ens/lib/network-map.json') const ensRE = /.+\.eth$/ -const networkResolvers = { - '3': '112234455c3a32fd11230c42e7bccd4a84e02010', -} module.exports = EnsInput @@ -24,8 +22,8 @@ EnsInput.prototype.render = function () { list: 'addresses', onChange: () => { const network = this.props.network - const resolverAddress = networkResolvers[network] - if (!resolverAddress) return + const networkHasEnsSupport = getNetworkEnsSupport(network) + if (!networkHasEnsSupport) return const recipient = document.querySelector('input[name="address"]').value if (recipient.match(ensRE) === null) { @@ -73,9 +71,9 @@ EnsInput.prototype.render = function () { EnsInput.prototype.componentDidMount = function () { const network = this.props.network - const resolverAddress = networkResolvers[network] + const networkHasEnsSupport = getNetworkEnsSupport(network) - if (resolverAddress) { + if (networkHasEnsSupport) { const provider = global.ethereumProvider this.ens = new ENS({ provider, network }) this.checkName = debounce(this.lookupEnsName.bind(this), 200) @@ -169,3 +167,7 @@ EnsInput.prototype.ensIconContents = function (recipient) { }) } } + +function getNetworkEnsSupport(network) { + return Boolean(networkMap[network]) +} \ No newline at end of file -- cgit v1.2.3 From 3ed7205b7505133a1dd6a278665070eb83bd4a32 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 9 May 2017 17:08:33 -0700 Subject: Version 3.6.4 --- CHANGELOG.md | 4 ++++ app/manifest.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40ba4d34d..532abcca9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Current Master +## 3.6.4 2017-5-8 + +- Fix main-net ENS resolution. + ## 3.6.3 2017-5-8 - Fix bug that could stop newer versions of Geth from working with MetaMask. diff --git a/app/manifest.json b/app/manifest.json index cef44446e..a1f6d7855 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.6.3", + "version": "3.6.4", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From d737bd1633977174ddf3d3248ee6873cc3adca8e Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 10 May 2017 17:26:09 -0700 Subject: Break up pending-tx component for better unit testability --- ui/app/components/pending-tx.js | 77 +++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 5ea885195..6b8f16dae 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -1,5 +1,4 @@ const Component = require('react').Component -const connect = require('react-redux').connect const h = require('react-hyperscript') const inherits = require('util').inherits const actions = require('../actions') @@ -20,12 +19,7 @@ const GWEI_FACTOR = new BN(1e9) const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) const MIN_GAS_LIMIT_BN = new BN(21000) -module.exports = connect(mapStateToProps)(PendingTx) - -function mapStateToProps (state) { - return {} -} - +module.exports = PendingTx inherits(PendingTx, Component) function PendingTx () { Component.call(this) @@ -37,7 +31,6 @@ function PendingTx () { PendingTx.prototype.render = function () { const props = this.props - const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} @@ -61,7 +54,6 @@ PendingTx.prototype.render = function () { const maxCost = txFeeBn.add(valueBn) const dataLength = txParams.data ? (txParams.data.length - 2) / 2 : 0 - const imageify = props.imageifyIdenticons === undefined ? true : props.imageifyIdenticons const balanceBn = hexToBn(balance) const insufficientBalance = balanceBn.lt(maxCost) @@ -75,18 +67,8 @@ PendingTx.prototype.render = function () { }, [ h('form#pending-tx-form', { - onSubmit: (event) => { - const txMeta = this.gatherTxMeta() - event.preventDefault() - const form = document.querySelector('form#pending-tx-form') - const valid = form.checkValidity() - this.setState({ valid }) - if (valid && this.verifyGasParams()) { - props.sendTransaction(txMeta, event) - } else { - this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) - } - }, + onSubmit: this.onSubmit.bind(this), + }, [ // tx info @@ -100,7 +82,6 @@ PendingTx.prototype.render = function () { h(MiniAccountPanel, { imageSeed: address, - imageifyIdenticons: imageify, picOrder: 'right', }, [ h('span.font-small', { @@ -176,12 +157,8 @@ PendingTx.prototype.render = function () { position: 'relative', top: '5px', }, - onChange: (newHex) => { - log.info(`Gas limit changed to ${newHex}`) - const txMeta = this.gatherTxMeta() - txMeta.txParams.gas = newHex - this.setState({ txData: txMeta }) - }, + onChange: this.gasLimitChanged.bind(this), + ref: (hexInput) => { this.inputs.push(hexInput) }, }), ]), @@ -201,13 +178,7 @@ PendingTx.prototype.render = function () { position: 'relative', top: '5px', }, - onChange: (newHex) => { - log.info(`Gas price changed to: ${newHex}`) - const inWei = hexToBn(newHex).mul(GWEI_FACTOR) - const txMeta = this.gatherTxMeta() - txMeta.txParams.gasPrice = inWei.toString(16) - this.setState({ txData: txMeta }) - }, + onChange: this.gasPriceChanged.bind(this), ref: (hexInput) => { this.inputs.push(hexInput) }, }), ]), @@ -331,13 +302,11 @@ PendingTx.prototype.miniAccountPanelForRecipient = function () { const txData = props.txData const txParams = txData.txParams || {} const isContractDeploy = !('to' in txParams) - const imageify = props.imageifyIdenticons === undefined ? true : props.imageifyIdenticons // If it's not a contract deploy, send to the account if (!isContractDeploy) { return h(MiniAccountPanel, { imageSeed: txParams.to, - imageifyIdenticons: imageify, picOrder: 'left', }, [ h('span.font-small', { @@ -353,7 +322,6 @@ PendingTx.prototype.miniAccountPanelForRecipient = function () { ]) } else { return h(MiniAccountPanel, { - imageifyIdenticons: imageify, picOrder: 'left', }, [ @@ -367,6 +335,21 @@ PendingTx.prototype.miniAccountPanelForRecipient = function () { } } +PendingTx.prototype.gasPriceChanged = function (newHex) { + log.info(`Gas price changed to: ${newHex}`) + const inWei = hexToBn(newHex).mul(GWEI_FACTOR) + const txMeta = this.gatherTxMeta() + txMeta.txParams.gasPrice = inWei.toString(16) + this.setState({ txData: txMeta }) +} + +PendingTx.prototype.gasLimitChanged = function (newHex) { + log.info(`Gas limit changed to ${newHex}`) + const txMeta = this.gatherTxMeta() + txMeta.txParams.gas = newHex + this.setState({ txData: txMeta }) +} + PendingTx.prototype.resetGasFields = function () { log.debug(`pending-tx resetGasFields`) @@ -382,6 +365,24 @@ PendingTx.prototype.resetGasFields = function () { }) } +PendingTx.prototype.onSubmit = function (event) { + event.preventDefault() + const txMeta = this.gatherTxMeta() + const valid = this.checkValidity() + this.setState({ valid }) + if (valid && this.verifyGasParams()) { + this.props.sendTransaction(txMeta, event) + } else { + this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) + } +} + +PendingTx.prototype.checkValidity = function() { + const form = document.querySelector('form#pending-tx-form') + const valid = form.checkValidity() + return valid +} + // After a customizable state value has been updated, PendingTx.prototype.gatherTxMeta = function () { log.debug(`pending-tx gatherTxMeta`) -- cgit v1.2.3 From e9b11a430b8f447e9c6f21c1b639d150976f98cf Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 10 May 2017 17:26:51 -0700 Subject: Add an attempt at a unit test for reproducing #1407 --- test/unit/components/pending-tx-test.js | 61 +++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 test/unit/components/pending-tx-test.js diff --git a/test/unit/components/pending-tx-test.js b/test/unit/components/pending-tx-test.js new file mode 100644 index 000000000..e0f02a5bb --- /dev/null +++ b/test/unit/components/pending-tx-test.js @@ -0,0 +1,61 @@ +var assert = require('assert') +var PendingTx = require('../../../ui/app/components/pending-tx') + +describe('PendingTx', function () { + let pendingTxComponent + + const identities = { + '0xfdea65c8e26263f6d9a1b5de9555d2931a33b826': { + name: 'Main Account 1', + balance: '0x00000000000000056bc75e2d63100000', + }, + } + + const gasPrice = '0x4A817C800' // 20 Gwei + const txData = { + 'id':5021615666270214, + 'time':1494458763011, + 'status':'unapproved', + 'metamaskNetworkId':'1494442339676', + 'txParams':{ + 'from':'0xfdea65c8e26263f6d9a1b5de9555d2931a33b826', + 'to':'0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', + 'value':'0xde0b6b3a7640000', + gasPrice, + 'gas':'0x7b0c'}, + 'gasLimitSpecified':false, + 'estimatedGas':'0x5208', + } + + + it('should use updated values when edited.', function (done) { + + const props = { + identities, + accounts: identities, + txData, + sendTransaction: (txMeta, event) => { + assert.notEqual(txMeta.txParams.gasPrice, gasPrice, 'gas price should change') + done() + }, + } + + pendingTxComponent = new PendingTx(props) + + const noop = () => {} + + pendingTxComponent.componentDidMount = () => { + + const newGasPrice = '0x451456' + pendingTxComponent.gasPriceChanged(newGasPrice) + + setTimeout(() => { + pendingTxComponent.onSubmit({ preventDefault: noop }) + }, 20) + } + + pendingTxComponent.props = props + pendingTxComponent.render() + }) + +}) -- cgit v1.2.3 From 1772d34e947ec5e940cc99f53ff0a102e048d69c Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 11 May 2017 10:10:50 +0200 Subject: fix migrator --- app/scripts/lib/migrator/index.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/app/scripts/lib/migrator/index.js b/app/scripts/lib/migrator/index.js index c40c347b5..caa0ef318 100644 --- a/app/scripts/lib/migrator/index.js +++ b/app/scripts/lib/migrator/index.js @@ -13,10 +13,10 @@ class Migrator { // run all pending migrations on meta in place migrateData (versionedData = this.generateInitialState()) { const remaining = this.migrations.filter(migrationIsPending) - + if (remaining.length === 0) return versionedData return ( asyncQ.eachSeries(remaining, (migration) => this.runMigration(versionedData, migration)) - .then(() => versionedData) + .then((migratedData) => migratedData.pop()) ) // migration is "pending" if hit has a higher @@ -27,14 +27,13 @@ class Migrator { } runMigration (versionedData, migration) { - return ( - migration.migrate(versionedData) - .then((versionedData) => { - if (!versionedData.data) return Promise.reject(new Error('Migrator - Migration returned empty data')) - if (migration.version !== undefined && versionedData.meta.version !== migration.version) return Promise.reject(new Error('Migrator - Migration did not update version number correctly')) - return Promise.resolve(versionedData) + return migration.migrate(versionedData) + .then((migratedData) => { + if (!migratedData.data) return Promise.reject(new Error('Migrator - Migration returned empty data')) + if (migration.version !== undefined && migratedData.meta.version !== migration.version) return Promise.reject(new Error('Migrator - Migration did not update version number correctly')) + + return Promise.resolve(migratedData) }) - ) } generateInitialState (initState) { -- cgit v1.2.3 From 73e1cd2317db4366a6e29aa9f8119cc871747a1b Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Thu, 11 May 2017 12:30:39 -0700 Subject: Trim currency list. --- app/currencies.json | 1 - ui/app/conversion.json | 5731 +----------------------------------------------- 2 files changed, 1 insertion(+), 5731 deletions(-) delete mode 100644 app/currencies.json diff --git a/app/currencies.json b/app/currencies.json deleted file mode 100644 index 07889798b..000000000 --- a/app/currencies.json +++ /dev/null @@ -1 +0,0 @@ -{"rows":[{"code":"007","name":"007","statuses":["primary"]},{"code":"1337","name":"1337","statuses":["primary"]},{"code":"1CR","name":"1CR","statuses":["primary"]},{"code":"256","name":"256","statuses":["primary"]},{"code":"2FLAV","name":"2FLAV","statuses":["primary"]},{"code":"2GIVE","name":"2GIVE","statuses":["primary"]},{"code":"404","name":"404","statuses":["primary"]},{"code":"611","name":"611","statuses":["primary"]},{"code":"888","name":"888","statuses":["primary"]},{"code":"8BIT","name":"8Bit","statuses":["primary"]},{"code":"ACLR","name":"ACLR","statuses":["primary"]},{"code":"ACOIN","name":"ACOIN","statuses":["primary"]},{"code":"ACP","name":"ACP","statuses":["primary"]},{"code":"ADC","name":"ADC","statuses":["primary"]},{"code":"ADZ","name":"Adzcoin","statuses":["primary"]},{"code":"AEC","name":"AEC","statuses":["primary"]},{"code":"AEON","name":"Aeon","statuses":["primary"]},{"code":"AGRS","name":"Agoras Tokens","statuses":["primary"]},{"code":"AIB","name":"AIB","statuses":["primary"]},{"code":"ADN","name":"Aiden","statuses":["primary"]},{"code":"AIR","name":"AIR","statuses":["primary"]},{"code":"ALC","name":"ALC","statuses":["primary"]},{"code":"ALTC","name":"ALTC","statuses":["primary"]},{"code":"AM","name":"AM","statuses":["primary"]},{"code":"AMBER","name":"AMBER","statuses":["primary"]},{"code":"AMS","name":"AMS","statuses":["primary"]},{"code":"ANAL","name":"ANAL","statuses":["primary"]},{"code":"AND","name":"AND","statuses":["primary"]},{"code":"ANI","name":"ANI","statuses":["primary"]},{"code":"ANC","name":"Anoncoin","statuses":["primary"]},{"code":"ANTI","name":"AntiBitcoin","statuses":["primary"]},{"code":"APEX","name":"APEX","statuses":["primary"]},{"code":"APC","name":"Applecoin","statuses":["primary"]},{"code":"APT","name":"APT","statuses":["primary"]},{"code":"AR2","name":"AR2","statuses":["primary"]},{"code":"ARB","name":"ARB","statuses":["primary"]},{"code":"ARC","name":"ARC","statuses":["primary"]},{"code":"ARCH","name":"ARCH","statuses":["primary"]},{"code":"ABY","name":"ArtByte","statuses":["primary"]},{"code":"ARTC","name":"ARTC","statuses":["primary"]},{"code":"ADCN","name":"Asiadigicoin","statuses":["primary"]},{"code":"ATEN","name":"ATEN","statuses":["primary"]},{"code":"REP","name":"Augur","statuses":["primary"]},{"code":"AUR","name":"Auroracoin","statuses":["primary"]},{"code":"AUD","name":"Australian Dollar","statuses":["secondary"]},{"code":"AV","name":"AV","statuses":["primary"]},{"code":"BA","name":"BA","statuses":["primary"]},{"code":"BAC","name":"BAC","statuses":["primary"]},{"code":"BTA","name":"Bata","statuses":["primary"]},{"code":"BAY","name":"BAY","statuses":["primary"]},{"code":"BBCC","name":"BBCC","statuses":["primary"]},{"code":"BQC","name":"BBQCoin","statuses":["primary"]},{"code":"BDC","name":"BDC","statuses":["primary"]},{"code":"BEC","name":"BEC","statuses":["primary"]},{"code":"BEEZ","name":"BEEZ","statuses":["primary"]},{"code":"BELA","name":"BellaCoin","statuses":["primary"]},{"code":"BERN","name":"BERNcash","statuses":["primary"]},{"code":"BILL","name":"BILL","statuses":["primary"]},{"code":"BILS","name":"BILS","statuses":["primary"]},{"code":"BIOS","name":"BiosCrypto","statuses":["primary"]},{"code":"BIT","name":"BIT","statuses":["primary"]},{"code":"BIT16","name":"BIT16","statuses":["primary"]},{"code":"BITB","name":"BitBean","statuses":["primary"]},{"code":"BTC","name":"Bitcoin","statuses":["primary","secondary"]},{"code":"XBC","name":"Bitcoin Plus","statuses":["primary"]},{"code":"BTCD","name":"BitcoinDark","statuses":["primary"]},{"code":"BCY","name":"Bitcrystals","statuses":["primary"]},{"code":"BTM","name":"Bitmark","statuses":["primary"]},{"code":"BTQ","name":"BitQuark","statuses":["primary"]},{"code":"BITS","name":"BITS","statuses":["primary"]},{"code":"BSD","name":"BitSend","statuses":["primary"]},{"code":"BTS","name":"BitShares","statuses":["primary"]},{"code":"PTS","name":"BitShares PTS","statuses":["primary"]},{"code":"SWIFT","name":"BitSwift","statuses":["primary"]},{"code":"BITZ","name":"Bitz","statuses":["primary"]},{"code":"BLK","name":"Blackcoin","statuses":["primary"]},{"code":"JACK","name":"BlackJack","statuses":["primary"]},{"code":"BLC","name":"Blakecoin","statuses":["primary"]},{"code":"BLEU","name":"BLEU","statuses":["primary"]},{"code":"BLITZ","name":"Blitzcoin","statuses":["primary"]},{"code":"BLOCK","name":"Blocknet","statuses":["primary"]},{"code":"BLRY","name":"BLRY","statuses":["primary"]},{"code":"BLU","name":"BLU","statuses":["primary"]},{"code":"BM","name":"BM","statuses":["primary"]},{"code":"BNT","name":"BNT","statuses":["primary"]},{"code":"BOB","name":"BOB","statuses":["primary"]},{"code":"BON","name":"BON","statuses":["primary"]},{"code":"BBR","name":"Boolberry","statuses":["primary"]},{"code":"BOST","name":"BoostCoin","statuses":["primary"]},{"code":"BOSS","name":"BOSS","statuses":["primary"]},{"code":"BPOK","name":"BPOK","statuses":["primary"]},{"code":"BRAIN","name":"BRAIN","statuses":["primary"]},{"code":"BRC","name":"BRC","statuses":["primary"]},{"code":"BRDD","name":"BRDD","statuses":["primary"]},{"code":"BRIT","name":"BRIT","statuses":["primary"]},{"code":"GBP","name":"British Pound Sterling","statuses":["secondary"]},{"code":"BRK","name":"BRK","statuses":["primary"]},{"code":"BRX","name":"BRX","statuses":["primary"]},{"code":"BSC","name":"BSC","statuses":["primary"]},{"code":"BST","name":"BST","statuses":["primary"]},{"code":"BTCHC","name":"BTCHC","statuses":["primary"]},{"code":"BTCR","name":"BTCR","statuses":["primary"]},{"code":"BTCS","name":"BTCS","statuses":["primary"]},{"code":"BTCU","name":"BTCU","statuses":["primary"]},{"code":"BTTF","name":"BTTF","statuses":["primary"]},{"code":"BTX","name":"BTX","statuses":["primary"]},{"code":"BUCKS","name":"BUCKS","statuses":["primary"]},{"code":"BUN","name":"BUN","statuses":["primary"]},{"code":"BURST","name":"Burst","statuses":["primary"]},{"code":"BUZZ","name":"BUZZ","statuses":["primary"]},{"code":"BVC","name":"BVC","statuses":["primary"]},{"code":"BYC","name":"Bytecent","statuses":["primary"]},{"code":"BCN","name":"Bytecoin","statuses":["primary"]},{"code":"XCT","name":"C-Bit","statuses":["primary"]},{"code":"C0C0","name":"C0C0","statuses":["primary"]},{"code":"CAB","name":"Cabbage Unit","statuses":["primary"]},{"code":"CAD","name":"CAD","statuses":["primary","secondary"]},{"code":"CAGE","name":"CAGE","statuses":["primary"]},{"code":"CANN","name":"CannabisCoin","statuses":["primary"]},{"code":"CCN","name":"Cannacoin","statuses":["primary"]},{"code":"CPC","name":"Capricoin","statuses":["primary"]},{"code":"DIEM","name":"CarpeDiemCoin","statuses":["primary"]},{"code":"CASH","name":"CASH","statuses":["primary"]},{"code":"CBIT","name":"CBIT","statuses":["primary"]},{"code":"CC","name":"CC","statuses":["primary"]},{"code":"CCB","name":"CCB","statuses":["primary"]},{"code":"CD","name":"CD","statuses":["primary"]},{"code":"CDN","name":"CDN","statuses":["primary"]},{"code":"CF","name":"CF","statuses":["primary"]},{"code":"CFC","name":"CFC","statuses":["primary"]},{"code":"CGA","name":"CGA","statuses":["primary"]},{"code":"CHC","name":"CHC","statuses":["primary"]},{"code":"CKC","name":"Checkcoin","statuses":["primary"]},{"code":"CHEMX","name":"CHEMX","statuses":["primary"]},{"code":"CHESS","name":"CHESS","statuses":["primary"]},{"code":"CHF","name":"CHF","statuses":["primary","secondary"]},{"code":"CNY","name":"Chinese Yuan","statuses":["secondary"]},{"code":"CHRG","name":"CHRG","statuses":["primary"]},{"code":"CJ","name":"CJ","statuses":["primary"]},{"code":"CLAM","name":"Clams","statuses":["primary"]},{"code":"CLICK","name":"CLICK","statuses":["primary"]},{"code":"CLINT","name":"CLINT","statuses":["primary"]},{"code":"CLOAK","name":"Cloakcoin","statuses":["primary"]},{"code":"CLR","name":"CLR","statuses":["primary"]},{"code":"CLUB","name":"CLUB","statuses":["primary"]},{"code":"CLUD","name":"CLUD","statuses":["primary"]},{"code":"CMT","name":"CMT","statuses":["primary"]},{"code":"CNC","name":"CNC","statuses":["primary"]},{"code":"COXST","name":"CoExistCoin","statuses":["primary"]},{"code":"COIN","name":"COIN","statuses":["primary"]},{"code":"C2","name":"Coin2.1","statuses":["primary"]},{"code":"CNMT","name":"Coinomat","statuses":["primary"]},{"code":"CV2","name":"Colossuscoin2.0","statuses":["primary"]},{"code":"CON","name":"CON","statuses":["primary"]},{"code":"XCP","name":"Counterparty","statuses":["primary"]},{"code":"COV","name":"COV","statuses":["primary"]},{"code":"CRAFT","name":"CRAFT","statuses":["primary"]},{"code":"CRAVE","name":"CRAVE","statuses":["primary"]},{"code":"CRC","name":"CRC","statuses":["primary"]},{"code":"CRE","name":"CRE","statuses":["primary"]},{"code":"CRBIT","name":"Creditbit","statuses":["primary"]},{"code":"CREVA","name":"CrevaCoin","statuses":["primary"]},{"code":"CRIME","name":"CRIME","statuses":["primary"]},{"code":"CRT","name":"CRT","statuses":["primary"]},{"code":"CRW","name":"CRW","statuses":["primary"]},{"code":"CRY","name":"CRY","statuses":["primary"]},{"code":"XCR","name":"Crypti","statuses":["primary"]},{"code":"CBX","name":"Crypto Bullion","statuses":["primary"]},{"code":"CESC","name":"CryptoEscudo","statuses":["primary"]},{"code":"XCN","name":"Cryptonite","statuses":["primary"]},{"code":"CSMIC","name":"CSMIC","statuses":["primary"]},{"code":"CST","name":"CST","statuses":["primary"]},{"code":"CTC","name":"CTC","statuses":["primary"]},{"code":"CTO","name":"CTO","statuses":["primary"]},{"code":"CURE","name":"Curecoin","statuses":["primary"]},{"code":"CYP","name":"Cypher","statuses":["primary"]},{"code":"CZC","name":"CZC","statuses":["primary"]},{"code":"CZECO","name":"CZECO","statuses":["primary"]},{"code":"CZR","name":"CZR","statuses":["primary"]},{"code":"DAO","name":"DAO","statuses":["primary"]},{"code":"DGD","name":"DarkGoldCoin","statuses":["primary"]},{"code":"DNET","name":"Darknet","statuses":["primary"]},{"code":"DASH","name":"Dash","statuses":["primary"]},{"code":"DTC","name":"Datacoin","statuses":["primary"]},{"code":"DBG","name":"DBG","statuses":["primary"]},{"code":"DBLK","name":"DBLK","statuses":["primary"]},{"code":"DBTC","name":"DBTC","statuses":["primary"]},{"code":"DCK","name":"DCK","statuses":["primary"]},{"code":"DCR","name":"Decred","statuses":["primary"]},{"code":"DES","name":"Destiny","statuses":["primary"]},{"code":"DETH","name":"DETH","statuses":["primary"]},{"code":"DEUR","name":"DEUR","statuses":["primary"]},{"code":"DEM","name":"Deutsche eMark","statuses":["primary"]},{"code":"DVC","name":"Devcoin","statuses":["primary"]},{"code":"DGCS","name":"DGCS","statuses":["primary"]},{"code":"DGMS","name":"DGMS","statuses":["primary"]},{"code":"DGORE","name":"DGORE","statuses":["primary"]},{"code":"DMD","name":"Diamond","statuses":["primary"]},{"code":"DGB","name":"Digibyte","statuses":["primary"]},{"code":"CUBE","name":"DigiCube","statuses":["primary"]},{"code":"DGC","name":"Digitalcoin","statuses":["primary"]},{"code":"XDN","name":"DigitalNote","statuses":["primary"]},{"code":"DP","name":"DigitalPrice","statuses":["primary"]},{"code":"DIGS","name":"DIGS","statuses":["primary"]},{"code":"DIME","name":"Dimecoin","statuses":["primary"]},{"code":"DISK","name":"DISK","statuses":["primary"]},{"code":"DLISK","name":"DLISK","statuses":["primary"]},{"code":"NOTE","name":"DNotes","statuses":["primary"]},{"code":"DOGE","name":"DOGE","statuses":["primary","secondary"]},{"code":"DOGE","name":"Dogecoin","statuses":["primary","secondary"]},{"code":"DON","name":"DON","statuses":["primary"]},{"code":"DOPE","name":"DopeCoin","statuses":["primary"]},{"code":"DOX","name":"DOX","statuses":["primary"]},{"code":"DRACO","name":"DRACO","statuses":["primary"]},{"code":"DRM","name":"DRM","statuses":["primary"]},{"code":"DROP","name":"DROP","statuses":["primary"]},{"code":"DRZ","name":"DRZ","statuses":["primary"]},{"code":"DSH","name":"DSH","statuses":["primary"]},{"code":"DBIC","name":"DubaiCoin","statuses":["primary"]},{"code":"DUO","name":"DUO","statuses":["primary"]},{"code":"DUST","name":"DUST","statuses":["primary"]},{"code":"EAC","name":"Earthcoin","statuses":["primary"]},{"code":"ECCHI","name":"ECCHI","statuses":["primary"]},{"code":"ECC","name":"ECCoin","statuses":["primary"]},{"code":"ECOS","name":"ECOS","statuses":["primary"]},{"code":"EDC","name":"EDC","statuses":["primary"]},{"code":"EDRC","name":"EDRC","statuses":["primary"]},{"code":"EGG","name":"EGG","statuses":["primary"]},{"code":"EMC2","name":"Einsteinium","statuses":["primary"]},{"code":"EKO","name":"EKO","statuses":["primary"]},{"code":"EL","name":"EL","statuses":["primary"]},{"code":"ELCO","name":"ELcoin","statuses":["primary"]},{"code":"ELE","name":"ELE","statuses":["primary"]},{"code":"EFL","name":"Electronic Gulden","statuses":["primary"]},{"code":"EMC","name":"Emercoin","statuses":["primary"]},{"code":"EMIRG","name":"EMIRG","statuses":["primary"]},{"code":"ENE","name":"ENE","statuses":["primary"]},{"code":"ENRG","name":"Energycoin","statuses":["primary"]},{"code":"EPC","name":"EPC","statuses":["primary"]},{"code":"EPY","name":"EPY","statuses":["primary"]},{"code":"ERC","name":"ERC","statuses":["primary"]},{"code":"ERC3","name":"ERC3","statuses":["primary"]},{"code":"ESC","name":"ESC","statuses":["primary"]},{"code":"ETH","name":"Ethereum","statuses":["primary","secondary"]},{"code":"ETC","name":"Ethereum Classic","statuses":["primary"]},{"code":"ETHS","name":"ETHS","statuses":["primary"]},{"code":"EURC","name":"EURC","statuses":["primary"]},{"code":"EUR","name":"Euro","statuses":["primary","secondary"]},{"code":"EGC","name":"EvergreenCoin","statuses":["primary"]},{"code":"EVIL","name":"EVIL","statuses":["primary"]},{"code":"EVO","name":"EVO","statuses":["primary"]},{"code":"EXCL","name":"EXCL","statuses":["primary"]},{"code":"EXIT","name":"EXIT","statuses":["primary"]},{"code":"EXP","name":"Expanse","statuses":["primary"]},{"code":"FCT","name":"Factom","statuses":["primary"]},{"code":"FAIR","name":"Faircoin","statuses":["primary"]},{"code":"FC2","name":"FC2","statuses":["primary"]},{"code":"FCN","name":"FCN","statuses":["primary"]},{"code":"FTC","name":"Feathercoin","statuses":["primary"]},{"code":"TIPS","name":"Fedoracoin","statuses":["primary"]},{"code":"FFC","name":"FFC","statuses":["primary"]},{"code":"FIBRE","name":"Fibre","statuses":["primary"]},{"code":"FIT","name":"FIT","statuses":["primary"]},{"code":"FJC","name":"FJC","statuses":["primary"]},{"code":"FLO","name":"Florincoin","statuses":["primary"]},{"code":"FLOZ","name":"FLOZ","statuses":["primary"]},{"code":"FLT","name":"FlutterCoin","statuses":["primary"]},{"code":"FLX","name":"FLX","statuses":["primary"]},{"code":"FLY","name":"Flycoin","statuses":["primary"]},{"code":"FLDC","name":"FoldingCoin","statuses":["primary"]},{"code":"FONZ","name":"FONZ","statuses":["primary"]},{"code":"FRK","name":"Franko","statuses":["primary"]},{"code":"FRC","name":"Freicoin","statuses":["primary"]},{"code":"FRN","name":"FRN","statuses":["primary"]},{"code":"FRWC","name":"FRWC","statuses":["primary"]},{"code":"FSC2","name":"FSC2","statuses":["primary"]},{"code":"FST","name":"FST","statuses":["primary"]},{"code":"FTP","name":"FTP","statuses":["primary"]},{"code":"FUN","name":"FUN","statuses":["primary"]},{"code":"FUTC","name":"FUTC","statuses":["primary"]},{"code":"FUZZ","name":"FUZZ","statuses":["primary"]},{"code":"GAIA","name":"GAIA","statuses":["primary"]},{"code":"GAIN","name":"GAIN","statuses":["primary"]},{"code":"GAKH","name":"GAKH","statuses":["primary"]},{"code":"GAM","name":"GAM","statuses":["primary"]},{"code":"GBT","name":"GameBet Coin","statuses":["primary"]},{"code":"GAME","name":"GameCredits","statuses":["primary"]},{"code":"GAP","name":"Gapcoin","statuses":["primary"]},{"code":"GARY","name":"GARY","statuses":["primary"]},{"code":"GB","name":"GB","statuses":["primary"]},{"code":"GBC","name":"GBC","statuses":["primary"]},{"code":"GBIT","name":"GBIT","statuses":["primary"]},{"code":"GCC","name":"GCC","statuses":["primary"]},{"code":"GCN","name":"GCN","statuses":["primary"]},{"code":"GEO","name":"GeoCoin","statuses":["primary"]},{"code":"GEMZ","name":"GetGems","statuses":["primary"]},{"code":"GHOST","name":"GHOST","statuses":["primary"]},{"code":"GHS","name":"GHS","statuses":["primary"]},{"code":"GIFT","name":"GIFT","statuses":["primary"]},{"code":"GIG","name":"GIG","statuses":["primary"]},{"code":"GLC","name":"GLC","statuses":["primary"]},{"code":"BSTY","name":"GlobalBoost-Y","statuses":["primary"]},{"code":"GML","name":"GML","statuses":["primary"]},{"code":"GMX","name":"GMX","statuses":["primary"]},{"code":"GCR","name":"GoCoineR","statuses":["primary"]},{"code":"GLD","name":"GoldCoin","statuses":["primary"]},{"code":"GOON","name":"GOON","statuses":["primary"]},{"code":"GP","name":"GP","statuses":["primary"]},{"code":"GPU","name":"GPU","statuses":["primary"]},{"code":"GRAM","name":"GRAM","statuses":["primary"]},{"code":"GRT","name":"Grantcoin","statuses":["primary"]},{"code":"GRE","name":"GRE","statuses":["primary"]},{"code":"GRC","name":"Gridcoin","statuses":["primary"]},{"code":"GRN","name":"GRN","statuses":["primary"]},{"code":"GRS","name":"Groestlcoin","statuses":["primary"]},{"code":"GRW","name":"GRW","statuses":["primary"]},{"code":"GSM","name":"GSM","statuses":["primary"]},{"code":"GSX","name":"GSX","statuses":["primary"]},{"code":"GUA","name":"GUA","statuses":["primary"]},{"code":"NLG","name":"Gulden","statuses":["primary"]},{"code":"GUN","name":"GUN","statuses":["primary"]},{"code":"HAM","name":"HAM","statuses":["primary"]},{"code":"HAWK","name":"HAWK","statuses":["primary"]},{"code":"HCC","name":"HCC","statuses":["primary"]},{"code":"HEAT","name":"HEAT","statuses":["primary"]},{"code":"HMP","name":"HempCoin","statuses":["primary"]},{"code":"XHI","name":"HiCoin","statuses":["primary"]},{"code":"HIFUN","name":"HIFUN","statuses":["primary"]},{"code":"HILL","name":"HILL","statuses":["primary"]},{"code":"HIRE","name":"HIRE","statuses":["primary"]},{"code":"HNC","name":"HNC","statuses":["primary"]},{"code":"HODL","name":"HOdlcoin","statuses":["primary"]},{"code":"HKD","name":"Hong Kong Dollar","statuses":["secondary"]},{"code":"HZ","name":"Horizon","statuses":["primary"]},{"code":"HTC","name":"HTC","statuses":["primary"]},{"code":"HTML5","name":"HTMLCOIN","statuses":["primary"]},{"code":"HUC","name":"HUC","statuses":["primary"]},{"code":"HVCO","name":"HVCO","statuses":["primary"]},{"code":"HYPER","name":"Hyper","statuses":["primary"]},{"code":"HYP","name":"HyperStake","statuses":["primary"]},{"code":"I0C","name":"I0C","statuses":["primary"]},{"code":"IBANK","name":"IBANK","statuses":["primary"]},{"code":"ICASH","name":"iCash","statuses":["primary"]},{"code":"ICN","name":"ICN","statuses":["primary"]},{"code":"IEC","name":"IEC","statuses":["primary"]},{"code":"IFC","name":"Infinitecoin","statuses":["primary"]},{"code":"INFX","name":"Influxcoin","statuses":["primary"]},{"code":"INV","name":"INV","statuses":["primary"]},{"code":"IOC","name":"IO Coin","statuses":["primary"]},{"code":"ION","name":"ION","statuses":["primary"]},{"code":"IRL","name":"IRL","statuses":["primary"]},{"code":"ISL","name":"IslaCoin","statuses":["primary"]},{"code":"IVZ","name":"IVZ","statuses":["primary"]},{"code":"IXC","name":"IXC","statuses":["primary"]},{"code":"JIF","name":"JIF","statuses":["primary"]},{"code":"JPC","name":"JPC","statuses":["primary"]},{"code":"JPY","name":"JPY","statuses":["primary","secondary"]},{"code":"JBS","name":"Jumbucks","statuses":["primary"]},{"code":"KAT","name":"KAT","statuses":["primary"]},{"code":"KGC","name":"KGC","statuses":["primary"]},{"code":"KNC","name":"KhanCoin","statuses":["primary"]},{"code":"KLC","name":"KLC","statuses":["primary"]},{"code":"KOBO","name":"KOBO","statuses":["primary"]},{"code":"KORE","name":"KoreCoin","statuses":["primary"]},{"code":"KRAK","name":"KRAK","statuses":["primary"]},{"code":"KRYP","name":"KRYP","statuses":["primary"]},{"code":"KR","name":"Krypton","statuses":["primary"]},{"code":"KTK","name":"KTK","statuses":["primary"]},{"code":"KUBO","name":"KUBO","statuses":["primary"]},{"code":"LANA","name":"LANA","statuses":["primary"]},{"code":"LBC","name":"LBC","statuses":["primary"]},{"code":"LC","name":"LC","statuses":["primary"]},{"code":"LEA","name":"LeaCoin","statuses":["primary"]},{"code":"LEMON","name":"LEMON","statuses":["primary"]},{"code":"LEO","name":"LEO","statuses":["primary"]},{"code":"LFC","name":"LFC","statuses":["primary"]},{"code":"LFO","name":"LFO","statuses":["primary"]},{"code":"LFTC","name":"LFTC","statuses":["primary"]},{"code":"LQD","name":"LIQUID","statuses":["primary"]},{"code":"LIR","name":"LIR","statuses":["primary"]},{"code":"LSK","name":"Lisk","statuses":["primary"]},{"code":"LTC","name":"Litecoin","statuses":["primary","secondary"]},{"code":"LTCR","name":"Litecred","statuses":["primary"]},{"code":"LDOGE","name":"LiteDoge","statuses":["primary"]},{"code":"LKC","name":"LKC","statuses":["primary"]},{"code":"LOC","name":"LOC","statuses":["primary"]},{"code":"LOOT","name":"LOOT","statuses":["primary"]},{"code":"LTBC","name":"LTBcoin","statuses":["primary"]},{"code":"LTC","name":"LTC","statuses":["primary","secondary"]},{"code":"LTH","name":"LTH","statuses":["primary"]},{"code":"LTS","name":"LTS","statuses":["primary"]},{"code":"LUN","name":"LUN","statuses":["primary"]},{"code":"LXC","name":"LXC","statuses":["primary"]},{"code":"LYB","name":"LYB","statuses":["primary"]},{"code":"M1","name":"M1","statuses":["primary"]},{"code":"MAD","name":"MAD","statuses":["primary"]},{"code":"XMG","name":"Magi","statuses":["primary"]},{"code":"MAID","name":"MaidSafeCoin","statuses":["primary"]},{"code":"MXT","name":"MarteXcoin","statuses":["primary"]},{"code":"MARV","name":"MARV","statuses":["primary"]},{"code":"MARYJ","name":"MARYJ","statuses":["primary"]},{"code":"OMNI","name":"Mastercoin (Omni)","statuses":["primary"]},{"code":"MTR","name":"MasterTraderCoin","statuses":["primary"]},{"code":"MAX","name":"Maxcoin","statuses":["primary"]},{"code":"MZC","name":"Mazacoin","statuses":["primary"]},{"code":"MBL","name":"MBL","statuses":["primary"]},{"code":"MCAR","name":"MCAR","statuses":["primary"]},{"code":"MCN","name":"MCN","statuses":["primary"]},{"code":"MCZ","name":"MCZ","statuses":["primary"]},{"code":"MED","name":"MediterraneanCoin","statuses":["primary"]},{"code":"MEC","name":"Megacoin","statuses":["primary"]},{"code":"MEME","name":"Memetic","statuses":["primary"]},{"code":"METAL","name":"METAL","statuses":["primary"]},{"code":"MND","name":"MindCoin","statuses":["primary"]},{"code":"MINT","name":"Mintcoin","statuses":["primary"]},{"code":"MIS","name":"MIS","statuses":["primary"]},{"code":"MM","name":"MM","statuses":["primary"]},{"code":"MMC","name":"MMC","statuses":["primary"]},{"code":"MMNXT","name":"MMNXT","statuses":["primary"]},{"code":"MMXVI","name":"MMXVI","statuses":["primary"]},{"code":"MNM","name":"MNM","statuses":["primary"]},{"code":"MOIN","name":"MOIN","statuses":["primary"]},{"code":"MOJO","name":"MojoCoin","statuses":["primary"]},{"code":"MONA","name":"MonaCoin","statuses":["primary"]},{"code":"XMR","name":"Monero","statuses":["primary","secondary"]},{"code":"MNTA","name":"Moneta","statuses":["primary"]},{"code":"MUE","name":"MonetaryUnit","statuses":["primary"]},{"code":"MOON","name":"Mooncoin","statuses":["primary"]},{"code":"MOOND","name":"MOOND","statuses":["primary"]},{"code":"MOTO","name":"MOTO","statuses":["primary"]},{"code":"MPRO","name":"MPRO","statuses":["primary"]},{"code":"MRB","name":"MRB","statuses":["primary"]},{"code":"MRP","name":"MRP","statuses":["primary"]},{"code":"MSC","name":"MSC","statuses":["primary"]},{"code":"MYR","name":"Myriadcoin","statuses":["primary"]},{"code":"NMC","name":"Namecoin","statuses":["primary"]},{"code":"NAUT","name":"Nautiluscoin","statuses":["primary"]},{"code":"NAV","name":"NAV Coin","statuses":["primary"]},{"code":"NCS","name":"NCS","statuses":["primary"]},{"code":"XEM","name":"NEM","statuses":["primary"]},{"code":"NEOS","name":"NeosCoin","statuses":["primary"]},{"code":"NETC","name":"NETC","statuses":["primary"]},{"code":"NET","name":"NetCoin","statuses":["primary"]},{"code":"NEU","name":"NeuCoin","statuses":["primary"]},{"code":"NTRN","name":"Neutron","statuses":["primary"]},{"code":"NEVA","name":"NevaCoin","statuses":["primary"]},{"code":"NEWB","name":"NEWB","statuses":["primary"]},{"code":"NIRO","name":"Nexus","statuses":["primary"]},{"code":"NIC","name":"NIC","statuses":["primary"]},{"code":"NKA","name":"NKA","statuses":["primary"]},{"code":"NKC","name":"NKC","statuses":["primary"]},{"code":"NOBL","name":"NobleCoin","statuses":["primary"]},{"code":"NODE","name":"NODE","statuses":["primary"]},{"code":"NODES","name":"NODES","statuses":["primary"]},{"code":"NOO","name":"NOO","statuses":["primary"]},{"code":"NVC","name":"Novacoin","statuses":["primary"]},{"code":"NRC","name":"NRC","statuses":["primary"]},{"code":"NRS","name":"NRS","statuses":["primary"]},{"code":"NUBIS","name":"NUBIS","statuses":["primary"]},{"code":"NBT","name":"NuBits","statuses":["primary"]},{"code":"NUM","name":"NUM","statuses":["primary"]},{"code":"NSR","name":"NuShares","statuses":["primary"]},{"code":"NXE","name":"NXE","statuses":["primary"]},{"code":"NXT","name":"NXT","statuses":["primary"]},{"code":"NXTTY","name":"Nxttycoin","statuses":["primary"]},{"code":"NYC","name":"NYC","statuses":["primary"]},{"code":"NZC","name":"NZC","statuses":["primary"]},{"code":"NZD","name":"NZD","statuses":["primary","secondary"]},{"code":"OC","name":"OC","statuses":["primary"]},{"code":"OCOW","name":"OCOW","statuses":["primary"]},{"code":"OK","name":"OKCash","statuses":["primary"]},{"code":"OMA","name":"OMA","statuses":["primary"]},{"code":"ONE","name":"ONE","statuses":["primary"]},{"code":"ONEC","name":"ONEC","statuses":["primary"]},{"code":"OP","name":"OP","statuses":["primary"]},{"code":"OPAL","name":"OPAL","statuses":["primary"]},{"code":"OPES","name":"OPES","statuses":["primary"]},{"code":"ORB","name":"Orbitcoin","statuses":["primary"]},{"code":"ORLY","name":"Orlycoin","statuses":["primary"]},{"code":"OS76","name":"OS76","statuses":["primary"]},{"code":"OZC","name":"OZC","statuses":["primary"]},{"code":"PAC","name":"PAC","statuses":["primary"]},{"code":"PAK","name":"PAK","statuses":["primary"]},{"code":"PND","name":"Pandacoin","statuses":["primary"]},{"code":"PAPAF","name":"PAPAF","statuses":["primary"]},{"code":"XPY","name":"Paycoin","statuses":["primary"]},{"code":"PBC","name":"PBC","statuses":["primary"]},{"code":"PDC","name":"PDC","statuses":["primary"]},{"code":"XPB","name":"Pebblecoin","statuses":["primary"]},{"code":"PPC","name":"Peercoin","statuses":["primary"]},{"code":"PEN","name":"PEN","statuses":["primary"]},{"code":"PHR","name":"PHR","statuses":["primary"]},{"code":"PIGGY","name":"Piggycoin","statuses":["primary"]},{"code":"PC","name":"Pinkcoin","statuses":["primary"]},{"code":"PKB","name":"PKB","statuses":["primary"]},{"code":"PLN","name":"PLN","statuses":["primary","secondary"]},{"code":"PLNC","name":"PLNC","statuses":["primary"]},{"code":"PNC","name":"PNC","statuses":["primary"]},{"code":"PNK","name":"PNK","statuses":["primary"]},{"code":"POKE","name":"POKE","statuses":["primary"]},{"code":"PONZ2","name":"PONZ2","statuses":["primary"]},{"code":"PONZI","name":"PONZI","statuses":["primary"]},{"code":"PEX","name":"PosEx","statuses":["primary"]},{"code":"POST","name":"POST","statuses":["primary"]},{"code":"POT","name":"Potcoin","statuses":["primary"]},{"code":"PRES","name":"PRES","statuses":["primary"]},{"code":"PXI","name":"Prime-XI","statuses":["primary"]},{"code":"PRIME","name":"PrimeChain","statuses":["primary"]},{"code":"XPM","name":"Primecoin","statuses":["primary"]},{"code":"PRM","name":"PRM","statuses":["primary"]},{"code":"PRT","name":"PRT","statuses":["primary"]},{"code":"PSP","name":"PSP","statuses":["primary"]},{"code":"PTC","name":"PTC","statuses":["primary"]},{"code":"PULSE","name":"PULSE","statuses":["primary"]},{"code":"PURE","name":"PURE","statuses":["primary"]},{"code":"PUTIN","name":"PUTIN","statuses":["primary"]},{"code":"PWR","name":"PWR","statuses":["primary"]},{"code":"PXL","name":"PXL","statuses":["primary"]},{"code":"QBC","name":"QBC","statuses":["primary"]},{"code":"QBK","name":"QBK","statuses":["primary"]},{"code":"QCN","name":"QCN","statuses":["primary"]},{"code":"QORA","name":"Qora","statuses":["primary"]},{"code":"QTZ","name":"QTZ","statuses":["primary"]},{"code":"QRK","name":"Quark","statuses":["primary"]},{"code":"QTL","name":"Quatloo","statuses":["primary"]},{"code":"RADI","name":"RADI","statuses":["primary"]},{"code":"RADS","name":"Radium","statuses":["primary"]},{"code":"RED","name":"RED","statuses":["primary"]},{"code":"RDD","name":"Reddcoin","statuses":["primary"]},{"code":"REE","name":"REE","statuses":["primary"]},{"code":"REV","name":"Revenu","statuses":["primary"]},{"code":"RBR","name":"RibbitRewards","statuses":["primary"]},{"code":"RICHX","name":"RICHX","statuses":["primary"]},{"code":"RIC","name":"Riecoin","statuses":["primary"]},{"code":"RBT","name":"Rimbit","statuses":["primary"]},{"code":"RIO","name":"RIO","statuses":["primary"]},{"code":"XRP","name":"Ripple","statuses":["primary"]},{"code":"RISE","name":"RISE","statuses":["primary"]},{"code":"RMS","name":"RMS","statuses":["primary"]},{"code":"RONIN","name":"RONIN","statuses":["primary"]},{"code":"ROOT","name":"ROOT","statuses":["primary"]},{"code":"ROS","name":"RosCoin","statuses":["primary"]},{"code":"RPC","name":"RPC","statuses":["primary"]},{"code":"RBIES","name":"Rubies","statuses":["primary"]},{"code":"RUBIT","name":"RUBIT","statuses":["primary"]},{"code":"RUR","name":"Ruble","statuses":["secondary"]},{"code":"RBY","name":"Rubycoin","statuses":["primary"]},{"code":"RUST","name":"RUST","statuses":["primary"]},{"code":"SEC","name":"Safe Exchange Coin","statuses":["primary"]},{"code":"SAK","name":"SAK","statuses":["primary"]},{"code":"SAR","name":"SAR","statuses":["primary"]},{"code":"SBD","name":"SBD","statuses":["primary"]},{"code":"SBIT","name":"SBIT","statuses":["primary"]},{"code":"SCAN","name":"SCAN","statuses":["primary"]},{"code":"SCOT","name":"Scotcoin","statuses":["primary"]},{"code":"SCRPT","name":"SCRPT","statuses":["primary"]},{"code":"SCRT","name":"SCRT","statuses":["primary"]},{"code":"SRC","name":"SecureCoin","statuses":["primary"]},{"code":"SXC","name":"Sexcoin","statuses":["primary"]},{"code":"SFE","name":"SFE","statuses":["primary"]},{"code":"SFR","name":"SFR","statuses":["primary"]},{"code":"SGD","name":"SGD","statuses":["primary","secondary"]},{"code":"SDC","name":"ShadowCash","statuses":["primary"]},{"code":"SHELL","name":"SHELL","statuses":["primary"]},{"code":"SHF","name":"SHF","statuses":["primary"]},{"code":"SHI","name":"SHI","statuses":["primary"]},{"code":"SHIFT","name":"Shift","statuses":["primary"]},{"code":"SHREK","name":"SHREK","statuses":["primary"]},{"code":"SC","name":"Siacoin","statuses":["primary"]},{"code":"SIB","name":"Siberian chervonets","statuses":["primary"]},{"code":"SIC","name":"SIC","statuses":["primary"]},{"code":"SIGU","name":"SIGU","statuses":["primary"]},{"code":"SILK","name":"Silkcoin","statuses":["primary"]},{"code":"SIX","name":"SIX","statuses":["primary"]},{"code":"SLING","name":"Sling","statuses":["primary"]},{"code":"SLS","name":"SLS","statuses":["primary"]},{"code":"SMBR","name":"SMBR","statuses":["primary"]},{"code":"SMC","name":"SMC","statuses":["primary"]},{"code":"SMLY","name":"SmileyCoin","statuses":["primary"]},{"code":"SNRG","name":"SNRG","statuses":["primary"]},{"code":"SOIL","name":"SOILcoin","statuses":["primary"]},{"code":"SLR","name":"Solarcoin","statuses":["primary"]},{"code":"SOLO","name":"SOLO","statuses":["primary"]},{"code":"SONG","name":"SongCoin","statuses":["primary"]},{"code":"SOON","name":"SOON","statuses":["primary"]},{"code":"SPC","name":"SPC","statuses":["primary"]},{"code":"SPEX","name":"SPEX","statuses":["primary"]},{"code":"SPHR","name":"Sphere","statuses":["primary"]},{"code":"SPM","name":"SPM","statuses":["primary"]},{"code":"SPN","name":"SPN","statuses":["primary"]},{"code":"SPOTS","name":"SPOTS","statuses":["primary"]},{"code":"SPR","name":"SpreadCoin","statuses":["primary"]},{"code":"SPRTS","name":"Sprouts","statuses":["primary"]},{"code":"SQC","name":"SQC","statuses":["primary"]},{"code":"SSC","name":"SSC","statuses":["primary"]},{"code":"SSTC","name":"SSTC","statuses":["primary"]},{"code":"STA","name":"STA","statuses":["primary"]},{"code":"START","name":"Startcoin","statuses":["primary"]},{"code":"XST","name":"Stealthcoin","statuses":["primary"]},{"code":"STEEM","name":"Steem","statuses":["primary"]},{"code":"XLM","name":"Stellar","statuses":["primary"]},{"code":"STR","name":"Stellar","statuses":["primary"]},{"code":"STEPS","name":"Steps","statuses":["primary"]},{"code":"SLG","name":"Sterlingcoin","statuses":["primary"]},{"code":"STL","name":"STL","statuses":["primary"]},{"code":"SJCX","name":"Storjcoin X","statuses":["primary"]},{"code":"STP","name":"STP","statuses":["primary"]},{"code":"STRB","name":"STRB","statuses":["primary"]},{"code":"STS","name":"Stress","statuses":["primary"]},{"code":"STRP","name":"STRP","statuses":["primary"]},{"code":"STV","name":"STV","statuses":["primary"]},{"code":"SUB","name":"Subcriptio","statuses":["primary"]},{"code":"SUPER","name":"SUPER","statuses":["primary"]},{"code":"UNITY","name":"SuperNET","statuses":["primary"]},{"code":"SWARM","name":"Swarm","statuses":["primary"]},{"code":"SWING","name":"SWING","statuses":["primary"]},{"code":"SDP","name":"SydPak Coin","statuses":["primary"]},{"code":"SYNC","name":"SYNC","statuses":["primary"]},{"code":"AMP","name":"Synereo","statuses":["primary"]},{"code":"SYS","name":"Syscoin","statuses":["primary"]},{"code":"TAG","name":"TagCoin","statuses":["primary"]},{"code":"TAJ","name":"TAJ","statuses":["primary"]},{"code":"TAK","name":"TAK","statuses":["primary"]},{"code":"TAM","name":"TAM","statuses":["primary"]},{"code":"TAO","name":"TAO","statuses":["primary"]},{"code":"TBC","name":"TBC","statuses":["primary"]},{"code":"TBCX","name":"TBCX","statuses":["primary"]},{"code":"TCR","name":"TCR","statuses":["primary"]},{"code":"TDFB","name":"TDFB","statuses":["primary"]},{"code":"TDY","name":"TDY","statuses":["primary"]},{"code":"TEK","name":"TEKcoin","statuses":["primary"]},{"code":"TRC","name":"Terracoin","statuses":["primary"]},{"code":"TESLA","name":"TESLA","statuses":["primary"]},{"code":"TES","name":"TeslaCoin","statuses":["primary"]},{"code":"TET","name":"TET","statuses":["primary"]},{"code":"USDT","name":"Tether","statuses":["primary","secondary"]},{"code":"THC","name":"THC","statuses":["primary"]},{"code":"THS","name":"THS","statuses":["primary"]},{"code":"TIX","name":"Tickets","statuses":["primary"]},{"code":"XTC","name":"TileCoin","statuses":["primary"]},{"code":"TIT","name":"Titcoin","statuses":["primary"]},{"code":"TTC","name":"TittieCoin","statuses":["primary"]},{"code":"TMC","name":"TMC","statuses":["primary"]},{"code":"TODAY","name":"TODAY","statuses":["primary"]},{"code":"TOKEN","name":"TOKEN","statuses":["primary"]},{"code":"TP1","name":"TP1","statuses":["primary"]},{"code":"TPC","name":"TPC","statuses":["primary"]},{"code":"TPG","name":"TPG","statuses":["primary"]},{"code":"TX","name":"Transfercoin","statuses":["primary"]},{"code":"TRAP","name":"TRAP","statuses":["primary"]},{"code":"TRICK","name":"TRICK","statuses":["primary"]},{"code":"TROLL","name":"TROLL","statuses":["primary"]},{"code":"TRK","name":"Truckcoin","statuses":["primary"]},{"code":"TRUMP","name":"TrumpCoin","statuses":["primary"]},{"code":"TRUST","name":"TRUST","statuses":["primary"]},{"code":"UAE","name":"UAE","statuses":["primary"]},{"code":"UFO","name":"UFO Coin","statuses":["primary"]},{"code":"UIS","name":"UIS","statuses":["primary"]},{"code":"UTC","name":"UltraCoin","statuses":["primary"]},{"code":"UNC","name":"UNC","statuses":["primary"]},{"code":"UNIQ","name":"UNIQ","statuses":["primary"]},{"code":"UNIT","name":"Universal Currency","statuses":["primary"]},{"code":"UNO","name":"Unobtanium","statuses":["primary"]},{"code":"URO","name":"Uro","statuses":["primary"]},{"code":"USD","name":"US Dollar","statuses":["primary","secondary"]},{"code":"USDE","name":"USDE","statuses":["primary"]},{"code":"UTH","name":"UTH","statuses":["primary"]},{"code":"VAL","name":"VAL","statuses":["primary"]},{"code":"XVC","name":"Vcash","statuses":["primary"]},{"code":"VCN","name":"VCN","statuses":["primary"]},{"code":"VEG","name":"VEG","statuses":["primary"]},{"code":"VENE","name":"VENE","statuses":["primary"]},{"code":"XVG","name":"Verge","statuses":["primary"]},{"code":"VRC","name":"VeriCoin","statuses":["primary"]},{"code":"VTC","name":"Vertcoin","statuses":["primary"]},{"code":"VIA","name":"Viacoin","statuses":["primary"]},{"code":"VIOR","name":"Viorcoin","statuses":["primary"]},{"code":"VIP","name":"VIP Tokens","statuses":["primary"]},{"code":"VIRAL","name":"Viral","statuses":["primary"]},{"code":"VOOT","name":"VootCoin","statuses":["primary"]},{"code":"VOX","name":"Voxels","statuses":["primary"]},{"code":"VOYA","name":"VOYA","statuses":["primary"]},{"code":"VPN","name":"VPNCoin","statuses":["primary"]},{"code":"VPRC","name":"VPRC","statuses":["primary"]},{"code":"VTA","name":"VTA","statuses":["primary"]},{"code":"VTN","name":"VTN","statuses":["primary"]},{"code":"VTR","name":"VTR","statuses":["primary"]},{"code":"WAC","name":"WAC","statuses":["primary"]},{"code":"WARP","name":"WARP","statuses":["primary"]},{"code":"WAVES","name":"WAVES","statuses":["primary"]},{"code":"WGC","name":"WGC","statuses":["primary"]},{"code":"XWC","name":"Whitecoin","statuses":["primary"]},{"code":"WBB","name":"Wild Beast Block","statuses":["primary"]},{"code":"WLC","name":"WLC","statuses":["primary"]},{"code":"WMC","name":"WMC","statuses":["primary"]},{"code":"LOG","name":"Woodcoin","statuses":["primary"]},{"code":"WOP","name":"WOP","statuses":["primary"]},{"code":"WDC","name":"Worldcoin","statuses":["primary"]},{"code":"XAB","name":"XAB","statuses":["primary"]},{"code":"XAI","name":"XAI","statuses":["primary"]},{"code":"XAU","name":"Xaurum","statuses":["primary"]},{"code":"XBS","name":"XBS","statuses":["primary"]},{"code":"XBU","name":"XBU","statuses":["primary"]},{"code":"XCO","name":"XCO","statuses":["primary"]},{"code":"XC","name":"XCurrency","statuses":["primary"]},{"code":"XDB","name":"XDB","statuses":["primary"]},{"code":"XEMP","name":"XEMP","statuses":["primary"]},{"code":"XFC","name":"XFC","statuses":["primary"]},{"code":"MI","name":"Xiaomicoin","statuses":["primary"]},{"code":"XID","name":"XID","statuses":["primary"]},{"code":"XJO","name":"XJO","statuses":["primary"]},{"code":"XLTCG","name":"XLTCG","statuses":["primary"]},{"code":"XMS","name":"XMS","statuses":["primary"]},{"code":"XNX","name":"XNX","statuses":["primary"]},{"code":"XPD","name":"XPD","statuses":["primary"]},{"code":"XPOKE","name":"XPOKE","statuses":["primary"]},{"code":"XPRO","name":"XPRO","statuses":["primary"]},{"code":"XQN","name":"XQN","statuses":["primary"]},{"code":"XSEED","name":"XSEED","statuses":["primary"]},{"code":"XSP","name":"XSP","statuses":["primary"]},{"code":"XT","name":"XT","statuses":["primary"]},{"code":"XTP","name":"XTP","statuses":["primary"]},{"code":"XUSD","name":"XUSD","statuses":["primary"]},{"code":"YACC","name":"YACC","statuses":["primary"]},{"code":"YAC","name":"Yacoin","statuses":["primary"]},{"code":"YAY","name":"YAY","statuses":["primary"]},{"code":"YBC","name":"Ybcoin","statuses":["primary"]},{"code":"YOC","name":"YOC","statuses":["primary"]},{"code":"YOVI","name":"YOVI","statuses":["primary"]},{"code":"YUM","name":"YUM","statuses":["primary"]},{"code":"ZCC","name":"ZCC","statuses":["primary"]},{"code":"ZEIT","name":"Zeitcoin","statuses":["primary"]},{"code":"ZET","name":"Zetacoin","statuses":["primary"]},{"code":"ZRC","name":"ZiftrCOIN","statuses":["primary"]},{"code":"ZMC","name":"ZMC","statuses":["primary"]},{"code":"ZNY","name":"ZNY","statuses":["primary"]},{"code":"ZS","name":"ZS","statuses":["primary"]}]} \ No newline at end of file diff --git a/ui/app/conversion.json b/ui/app/conversion.json index eeca164ce..e0c033a76 100644 --- a/ui/app/conversion.json +++ b/ui/app/conversion.json @@ -1,5730 +1 @@ -{ - "rows":[ - { - "code":"007", - "name":"007", - "statuses":[ - "primary" - ] - }, - { - "code":"1337", - "name":"1337", - "statuses":[ - "primary" - ] - }, - { - "code":"1CR", - "name":"1CR", - "statuses":[ - "primary" - ] - }, - { - "code":"256", - "name":"256", - "statuses":[ - "primary" - ] - }, - { - "code":"2FLAV", - "name":"2FLAV", - "statuses":[ - "primary" - ] - }, - { - "code":"2GIVE", - "name":"2GIVE", - "statuses":[ - "primary" - ] - }, - { - "code":"32BIT", - "name":"32BIT", - "statuses":[ - "primary" - ] - }, - { - "code":"404", - "name":"404", - "statuses":[ - "primary" - ] - }, - { - "code":"611", - "name":"611", - "statuses":[ - "primary" - ] - }, - { - "code":"888", - "name":"888", - "statuses":[ - "primary" - ] - }, - { - "code":"8BIT", - "name":"8Bit", - "statuses":[ - "primary" - ] - }, - { - "code":"ACES", - "name":"ACES", - "statuses":[ - "primary" - ] - }, - { - "code":"ACID", - "name":"ACID", - "statuses":[ - "primary" - ] - }, - { - "code":"ACLR", - "name":"ACLR", - "statuses":[ - "primary" - ] - }, - { - "code":"ACP", - "name":"ACP", - "statuses":[ - "primary" - ] - }, - { - "code":"ADC", - "name":"ADC", - "statuses":[ - "primary" - ] - }, - { - "code":"ADZ", - "name":"Adzcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"AEON", - "name":"Aeon", - "statuses":[ - "primary" - ] - }, - { - "code":"AGRS", - "name":"Agoras Tokens", - "statuses":[ - "primary" - ] - }, - { - "code":"AIB", - "name":"AIB", - "statuses":[ - "primary" - ] - }, - { - "code":"ALC", - "name":"ALC", - "statuses":[ - "primary" - ] - }, - { - "code":"ALTC", - "name":"ALTC", - "statuses":[ - "primary" - ] - }, - { - "code":"AM", - "name":"AM", - "statuses":[ - "primary" - ] - }, - { - "code":"AMBER", - "name":"AMBER", - "statuses":[ - "primary" - ] - }, - { - "code":"AMS", - "name":"AMS", - "statuses":[ - "primary" - ] - }, - { - "code":"ANAL", - "name":"ANAL", - "statuses":[ - "primary" - ] - }, - { - "code":"ANI", - "name":"ANI", - "statuses":[ - "primary" - ] - }, - { - "code":"ANC", - "name":"Anoncoin", - "statuses":[ - "primary" - ] - }, - { - "code":"ANS", - "name":"ANS", - "statuses":[ - "primary" - ] - }, - { - "code":"ANTI", - "name":"AntiBitcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"APEX", - "name":"APEX", - "statuses":[ - "primary" - ] - }, - { - "code":"APC", - "name":"Applecoin", - "statuses":[ - "primary" - ] - }, - { - "code":"APT", - "name":"APT", - "statuses":[ - "primary" - ] - }, - { - "code":"AR2", - "name":"AR2", - "statuses":[ - "primary" - ] - }, - { - "code":"ARB", - "name":"ARB", - "statuses":[ - "primary" - ] - }, - { - "code":"ARC", - "name":"ARC", - "statuses":[ - "primary" - ] - }, - { - "code":"ARCH", - "name":"ARCH", - "statuses":[ - "primary" - ] - }, - { - "code":"ARD", - "name":"ARD", - "statuses":[ - "primary" - ] - }, - { - "code":"ARDR", - "name":"ARDR", - "statuses":[ - "primary" - ] - }, - { - "code":"ABY", - "name":"ArtByte", - "statuses":[ - "primary" - ] - }, - { - "code":"ARTC", - "name":"ARTC", - "statuses":[ - "primary" - ] - }, - { - "code":"ASAFE", - "name":"ASAFE", - "statuses":[ - "primary" - ] - }, - { - "code":"ADCN", - "name":"Asiadigicoin", - "statuses":[ - "primary" - ] - }, - { - "code":"ASN", - "name":"ASN", - "statuses":[ - "primary" - ] - }, - { - "code":"ATEN", - "name":"ATEN", - "statuses":[ - "primary" - ] - }, - { - "code":"ATOM", - "name":"ATOM", - "statuses":[ - "primary" - ] - }, - { - "code":"ATX", - "name":"ATX", - "statuses":[ - "primary" - ] - }, - { - "code":"REP", - "name":"Augur", - "statuses":[ - "primary" - ] - }, - { - "code":"AUR", - "name":"Auroracoin", - "statuses":[ - "primary" - ] - }, - { - "code":"AUD", - "name":"Australian Dollar", - "statuses":[ - "secondary" - ] - }, - { - "code":"AV", - "name":"AV", - "statuses":[ - "primary" - ] - }, - { - "code":"B2", - "name":"B2", - "statuses":[ - "primary" - ] - }, - { - "code":"B3", - "name":"B3", - "statuses":[ - "primary" - ] - }, - { - "code":"BA", - "name":"BA", - "statuses":[ - "primary" - ] - }, - { - "code":"BAC", - "name":"BAC", - "statuses":[ - "primary" - ] - }, - { - "code":"BASH", - "name":"BASH", - "statuses":[ - "primary" - ] - }, - { - "code":"BTA", - "name":"Bata", - "statuses":[ - "primary" - ] - }, - { - "code":"BAY", - "name":"BAY", - "statuses":[ - "primary" - ] - }, - { - "code":"BBCC", - "name":"BBCC", - "statuses":[ - "primary" - ] - }, - { - "code":"BQC", - "name":"BBQCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"BEC", - "name":"BEC", - "statuses":[ - "primary" - ] - }, - { - "code":"BEEP", - "name":"BEEP", - "statuses":[ - "primary" - ] - }, - { - "code":"BELA", - "name":"BellaCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"BERN", - "name":"BERNcash", - "statuses":[ - "primary" - ] - }, - { - "code":"BHC", - "name":"BHC", - "statuses":[ - "primary" - ] - }, - { - "code":"BILL", - "name":"BILL", - "statuses":[ - "primary" - ] - }, - { - "code":"BILS", - "name":"BILS", - "statuses":[ - "primary" - ] - }, - { - "code":"BIOS", - "name":"BiosCrypto", - "statuses":[ - "primary" - ] - }, - { - "code":"BIT", - "name":"BIT", - "statuses":[ - "primary" - ] - }, - { - "code":"BIT16", - "name":"BIT16", - "statuses":[ - "primary" - ] - }, - { - "code":"BITB", - "name":"BitBean", - "statuses":[ - "primary" - ] - }, - { - "code":"BTC", - "name":"Bitcoin", - "statuses":[ - "primary", - "secondary" - ] - }, - { - "code":"XBC", - "name":"Bitcoin Plus", - "statuses":[ - "primary" - ] - }, - { - "code":"BTCD", - "name":"BitcoinDark", - "statuses":[ - "primary" - ] - }, - { - "code":"BCY", - "name":"Bitcrystals", - "statuses":[ - "primary" - ] - }, - { - "code":"BFX", - "name":"Bitfinex Debt token", - "statuses":[ - "primary" - ] - }, - { - "code":"BTM", - "name":"Bitmark", - "statuses":[ - "primary" - ] - }, - { - "code":"BITON", - "name":"BITON", - "statuses":[ - "primary" - ] - }, - { - "code":"BTQ", - "name":"BitQuark", - "statuses":[ - "primary" - ] - }, - { - "code":"BITS", - "name":"BITS", - "statuses":[ - "primary" - ] - }, - { - "code":"BSD", - "name":"BitSend", - "statuses":[ - "primary" - ] - }, - { - "code":"BTS", - "name":"BitShares", - "statuses":[ - "primary" - ] - }, - { - "code":"SWIFT", - "name":"BitSwift", - "statuses":[ - "primary" - ] - }, - { - "code":"BITZ", - "name":"Bitz", - "statuses":[ - "primary" - ] - }, - { - "code":"BLK", - "name":"Blackcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"BLC", - "name":"Blakecoin", - "statuses":[ - "primary" - ] - }, - { - "code":"BLEU", - "name":"BLEU", - "statuses":[ - "primary" - ] - }, - { - "code":"BLITZ", - "name":"Blitzcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"BLOCK", - "name":"Blocknet", - "statuses":[ - "primary" - ] - }, - { - "code":"BLRY", - "name":"BLRY", - "statuses":[ - "primary" - ] - }, - { - "code":"BLU", - "name":"BLU", - "statuses":[ - "primary" - ] - }, - { - "code":"BLUS", - "name":"BLUS", - "statuses":[ - "primary" - ] - }, - { - "code":"BNT", - "name":"BNT", - "statuses":[ - "primary" - ] - }, - { - "code":"BOLI", - "name":"Bolivarcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"BBR", - "name":"Boolberry", - "statuses":[ - "primary" - ] - }, - { - "code":"BOOM", - "name":"BOOM", - "statuses":[ - "primary" - ] - }, - { - "code":"BOST", - "name":"BoostCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"BOSS", - "name":"BOSS", - "statuses":[ - "primary" - ] - }, - { - "code":"BPOK", - "name":"BPOK", - "statuses":[ - "primary" - ] - }, - { - "code":"BRAIN", - "name":"BRAIN", - "statuses":[ - "primary" - ] - }, - { - "code":"BRC", - "name":"BRC", - "statuses":[ - "primary" - ] - }, - { - "code":"BRDD", - "name":"BRDD", - "statuses":[ - "primary" - ] - }, - { - "code":"BRIT", - "name":"BRIT", - "statuses":[ - "primary" - ] - }, - { - "code":"GBP", - "name":"British Pound Sterling", - "statuses":[ - "secondary" - ] - }, - { - "code":"BRK", - "name":"BRK", - "statuses":[ - "primary" - ] - }, - { - "code":"BRX", - "name":"BRX", - "statuses":[ - "primary" - ] - }, - { - "code":"BS", - "name":"BS", - "statuses":[ - "primary" - ] - }, - { - "code":"BSC", - "name":"BSC", - "statuses":[ - "primary" - ] - }, - { - "code":"BST", - "name":"BST", - "statuses":[ - "primary" - ] - }, - { - "code":"BTCHC", - "name":"BTCHC", - "statuses":[ - "primary" - ] - }, - { - "code":"BTCR", - "name":"BTCR", - "statuses":[ - "primary" - ] - }, - { - "code":"BTCS", - "name":"BTCS", - "statuses":[ - "primary" - ] - }, - { - "code":"BTD", - "name":"BTD", - "statuses":[ - "primary" - ] - }, - { - "code":"BTLC", - "name":"BTLC", - "statuses":[ - "primary" - ] - }, - { - "code":"BTTF", - "name":"BTTF", - "statuses":[ - "primary" - ] - }, - { - "code":"BTZ", - "name":"BTZ", - "statuses":[ - "primary" - ] - }, - { - "code":"BUCKS", - "name":"BUCKS", - "statuses":[ - "primary" - ] - }, - { - "code":"BUN", - "name":"BUN", - "statuses":[ - "primary" - ] - }, - { - "code":"BURST", - "name":"Burst", - "statuses":[ - "primary" - ] - }, - { - "code":"BUZZ", - "name":"BUZZ", - "statuses":[ - "primary" - ] - }, - { - "code":"BVC", - "name":"BVC", - "statuses":[ - "primary" - ] - }, - { - "code":"BXT", - "name":"BXT", - "statuses":[ - "primary" - ] - }, - { - "code":"BYC", - "name":"Bytecent", - "statuses":[ - "primary" - ] - }, - { - "code":"BCN", - "name":"Bytecoin", - "statuses":[ - "primary" - ] - }, - { - "code":"CAB", - "name":"Cabbage Unit", - "statuses":[ - "primary" - ] - }, - { - "code":"CAGE", - "name":"CAGE", - "statuses":[ - "primary" - ] - }, - { - "code":"CAID", - "name":"CAID", - "statuses":[ - "primary" - ] - }, - { - "code":"CAD", - "name":"Canadian Dollar", - "statuses":[ - "secondary" - ] - }, - { - "code":"CANN", - "name":"CannabisCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"CCN", - "name":"Cannacoin", - "statuses":[ - "primary" - ] - }, - { - "code":"CPC", - "name":"Capricoin", - "statuses":[ - "primary" - ] - }, - { - "code":"CAPT", - "name":"CAPT", - "statuses":[ - "primary" - ] - }, - { - "code":"DIEM", - "name":"CarpeDiemCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"CASH", - "name":"CASH", - "statuses":[ - "primary" - ] - }, - { - "code":"CBD", - "name":"CBD", - "statuses":[ - "primary" - ] - }, - { - "code":"CBIT", - "name":"CBIT", - "statuses":[ - "primary" - ] - }, - { - "code":"CCX", - "name":"CCX", - "statuses":[ - "primary" - ] - }, - { - "code":"CD", - "name":"CD", - "statuses":[ - "primary" - ] - }, - { - "code":"CDN", - "name":"CDN", - "statuses":[ - "primary" - ] - }, - { - "code":"CF", - "name":"CF", - "statuses":[ - "primary" - ] - }, - { - "code":"CGA", - "name":"CGA", - "statuses":[ - "primary" - ] - }, - { - "code":"CKC", - "name":"Checkcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"CHEMX", - "name":"CHEMX", - "statuses":[ - "primary" - ] - }, - { - "code":"CHESS", - "name":"CHESS", - "statuses":[ - "primary" - ] - }, - { - "code":"CHF", - "name":"CHF", - "statuses":[ - "primary", - "secondary" - ] - }, - { - "code":"CNY", - "name":"Chinese Yuan", - "statuses":[ - "secondary" - ] - }, - { - "code":"CHOOF", - "name":"CHOOF", - "statuses":[ - "primary" - ] - }, - { - "code":"CJ", - "name":"CJ", - "statuses":[ - "primary" - ] - }, - { - "code":"CLAM", - "name":"Clams", - "statuses":[ - "primary" - ] - }, - { - "code":"CLICK", - "name":"CLICK", - "statuses":[ - "primary" - ] - }, - { - "code":"CLINT", - "name":"CLINT", - "statuses":[ - "primary" - ] - }, - { - "code":"CLOAK", - "name":"Cloakcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"CLR", - "name":"CLR", - "statuses":[ - "primary" - ] - }, - { - "code":"CLUB", - "name":"CLUB", - "statuses":[ - "primary" - ] - }, - { - "code":"CLUD", - "name":"CLUD", - "statuses":[ - "primary" - ] - }, - { - "code":"CLV", - "name":"CLV", - "statuses":[ - "primary" - ] - }, - { - "code":"CME", - "name":"CME", - "statuses":[ - "primary" - ] - }, - { - "code":"CMT", - "name":"CMT", - "statuses":[ - "primary" - ] - }, - { - "code":"CNC", - "name":"CNC", - "statuses":[ - "primary" - ] - }, - { - "code":"COC", - "name":"COC", - "statuses":[ - "primary" - ] - }, - { - "code":"COXST", - "name":"CoExistCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"COIN", - "name":"COIN", - "statuses":[ - "primary" - ] - }, - { - "code":"C2", - "name":"Coin2.1", - "statuses":[ - "primary" - ] - }, - { - "code":"CV2", - "name":"Colossuscoin2.0", - "statuses":[ - "primary" - ] - }, - { - "code":"CON", - "name":"CON", - "statuses":[ - "primary" - ] - }, - { - "code":"XCP", - "name":"Counterparty", - "statuses":[ - "primary" - ] - }, - { - "code":"COVAL", - "name":"COVAL", - "statuses":[ - "primary" - ] - }, - { - "code":"COX", - "name":"COX", - "statuses":[ - "primary" - ] - }, - { - "code":"CRAB", - "name":"CRAB", - "statuses":[ - "primary" - ] - }, - { - "code":"CRC", - "name":"CRC", - "statuses":[ - "primary" - ] - }, - { - "code":"CRE", - "name":"CRE", - "statuses":[ - "primary" - ] - }, - { - "code":"CRBIT", - "name":"Creditbit", - "statuses":[ - "primary" - ] - }, - { - "code":"CREVA", - "name":"CrevaCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"CRNK", - "name":"CRNK", - "statuses":[ - "primary" - ] - }, - { - "code":"CRPC", - "name":"CRPC", - "statuses":[ - "primary" - ] - }, - { - "code":"CRPS", - "name":"CRPS", - "statuses":[ - "primary" - ] - }, - { - "code":"CRT", - "name":"CRT", - "statuses":[ - "primary" - ] - }, - { - "code":"CRW", - "name":"CRW", - "statuses":[ - "primary" - ] - }, - { - "code":"CRX", - "name":"CRX", - "statuses":[ - "primary" - ] - }, - { - "code":"CRY", - "name":"CRY", - "statuses":[ - "primary" - ] - }, - { - "code":"CBX", - "name":"Crypto Bullion", - "statuses":[ - "primary" - ] - }, - { - "code":"CESC", - "name":"CryptoEscudo", - "statuses":[ - "primary" - ] - }, - { - "code":"XCN", - "name":"Cryptonite", - "statuses":[ - "primary" - ] - }, - { - "code":"CSH", - "name":"CSH", - "statuses":[ - "primary" - ] - }, - { - "code":"CST", - "name":"CST", - "statuses":[ - "primary" - ] - }, - { - "code":"CTK", - "name":"CTK", - "statuses":[ - "primary" - ] - }, - { - "code":"CTL", - "name":"CTL", - "statuses":[ - "primary" - ] - }, - { - "code":"CTO", - "name":"CTO", - "statuses":[ - "primary" - ] - }, - { - "code":"CURE", - "name":"Curecoin", - "statuses":[ - "primary" - ] - }, - { - "code":"CYC", - "name":"CYC", - "statuses":[ - "primary" - ] - }, - { - "code":"CYP", - "name":"Cypher", - "statuses":[ - "primary" - ] - }, - { - "code":"CZC", - "name":"CZC", - "statuses":[ - "primary" - ] - }, - { - "code":"CZR", - "name":"CZR", - "statuses":[ - "primary" - ] - }, - { - "code":"DGD", - "name":"DarkGoldCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"DNET", - "name":"Darknet", - "statuses":[ - "primary" - ] - }, - { - "code":"DAS", - "name":"DAS", - "statuses":[ - "primary" - ] - }, - { - "code":"DASH", - "name":"Dash", - "statuses":[ - "primary" - ] - }, - { - "code":"DTC", - "name":"Datacoin", - "statuses":[ - "primary" - ] - }, - { - "code":"DB", - "name":"DB", - "statuses":[ - "primary" - ] - }, - { - "code":"DBG", - "name":"DBG", - "statuses":[ - "primary" - ] - }, - { - "code":"DBLK", - "name":"DBLK", - "statuses":[ - "primary" - ] - }, - { - "code":"DBTC", - "name":"DBTC", - "statuses":[ - "primary" - ] - }, - { - "code":"DC", - "name":"DC", - "statuses":[ - "primary" - ] - }, - { - "code":"DCK", - "name":"DCK", - "statuses":[ - "primary" - ] - }, - { - "code":"DCRE", - "name":"DCRE", - "statuses":[ - "primary" - ] - }, - { - "code":"DCT", - "name":"DCT", - "statuses":[ - "primary" - ] - }, - { - "code":"DCYP", - "name":"DCYP", - "statuses":[ - "primary" - ] - }, - { - "code":"DCR", - "name":"Decred", - "statuses":[ - "primary" - ] - }, - { - "code":"DES", - "name":"Destiny", - "statuses":[ - "primary" - ] - }, - { - "code":"DEUR", - "name":"DEUR", - "statuses":[ - "primary" - ] - }, - { - "code":"DEM", - "name":"Deutsche eMark", - "statuses":[ - "primary" - ] - }, - { - "code":"DVC", - "name":"Devcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"DGMS", - "name":"DGMS", - "statuses":[ - "primary" - ] - }, - { - "code":"DGORE", - "name":"DGORE", - "statuses":[ - "primary" - ] - }, - { - "code":"DMD", - "name":"Diamond", - "statuses":[ - "primary" - ] - }, - { - "code":"DGB", - "name":"Digibyte", - "statuses":[ - "primary" - ] - }, - { - "code":"CUBE", - "name":"DigiCube", - "statuses":[ - "primary" - ] - }, - { - "code":"DGC", - "name":"Digitalcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"XDN", - "name":"DigitalNote", - "statuses":[ - "primary" - ] - }, - { - "code":"DP", - "name":"DigitalPrice", - "statuses":[ - "primary" - ] - }, - { - "code":"DIME", - "name":"Dimecoin", - "statuses":[ - "primary" - ] - }, - { - "code":"DISK", - "name":"DISK", - "statuses":[ - "primary" - ] - }, - { - "code":"DKC", - "name":"DKC", - "statuses":[ - "primary" - ] - }, - { - "code":"DLC", - "name":"DLC", - "statuses":[ - "primary" - ] - }, - { - "code":"DLISK", - "name":"DLISK", - "statuses":[ - "primary" - ] - }, - { - "code":"DMC", - "name":"DMC", - "statuses":[ - "primary" - ] - }, - { - "code":"NOTE", - "name":"DNotes", - "statuses":[ - "primary" - ] - }, - { - "code":"DOGE", - "name":"Dogecoin", - "statuses":[ - "primary", - "secondary" - ] - }, - { - "code":"DOPE", - "name":"DopeCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"DOV", - "name":"DOV", - "statuses":[ - "primary" - ] - }, - { - "code":"DOX", - "name":"DOX", - "statuses":[ - "primary" - ] - }, - { - "code":"DPAY", - "name":"DPAY", - "statuses":[ - "primary" - ] - }, - { - "code":"DRACO", - "name":"DRACO", - "statuses":[ - "primary" - ] - }, - { - "code":"DRM8", - "name":"DRM8", - "statuses":[ - "primary" - ] - }, - { - "code":"DROP", - "name":"DROP", - "statuses":[ - "primary" - ] - }, - { - "code":"DRZ", - "name":"DRZ", - "statuses":[ - "primary" - ] - }, - { - "code":"DSH", - "name":"DSH", - "statuses":[ - "primary" - ] - }, - { - "code":"DTT", - "name":"DTT", - "statuses":[ - "primary" - ] - }, - { - "code":"DBIC", - "name":"DubaiCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"DUO", - "name":"DUO", - "statuses":[ - "primary" - ] - }, - { - "code":"DUST", - "name":"DUST", - "statuses":[ - "primary" - ] - }, - { - "code":"EAGS", - "name":"EAGS", - "statuses":[ - "primary" - ] - }, - { - "code":"EAC", - "name":"Earthcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"EBST", - "name":"EBST", - "statuses":[ - "primary" - ] - }, - { - "code":"EC", - "name":"EC", - "statuses":[ - "primary" - ] - }, - { - "code":"ECC", - "name":"ECCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"ECLI", - "name":"ECLI", - "statuses":[ - "primary" - ] - }, - { - "code":"EDC", - "name":"EDC", - "statuses":[ - "primary" - ] - }, - { - "code":"EDRC", - "name":"EDRC", - "statuses":[ - "primary" - ] - }, - { - "code":"EDR", - "name":"EDRCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"EGG", - "name":"EGG", - "statuses":[ - "primary" - ] - }, - { - "code":"EGO", - "name":"EGO", - "statuses":[ - "primary" - ] - }, - { - "code":"EMC2", - "name":"Einsteinium", - "statuses":[ - "primary" - ] - }, - { - "code":"EL", - "name":"EL", - "statuses":[ - "primary" - ] - }, - { - "code":"ELE", - "name":"ELE", - "statuses":[ - "primary" - ] - }, - { - "code":"EFL", - "name":"Electronic Gulden", - "statuses":[ - "primary" - ] - }, - { - "code":"EMB", - "name":"EMB", - "statuses":[ - "secondary" - ] - }, - { - "code":"EME", - "name":"EME", - "statuses":[ - "secondary" - ] - }, - { - "code":"EMC", - "name":"Emercoin", - "statuses":[ - "primary" - ] - }, - { - "code":"EMIRG", - "name":"EMIRG", - "statuses":[ - "primary" - ] - }, - { - "code":"EMP", - "name":"EMP", - "statuses":[ - "primary" - ] - }, - { - "code":"EMPC", - "name":"EMPC", - "statuses":[ - "primary" - ] - }, - { - "code":"ENRG", - "name":"Energycoin", - "statuses":[ - "primary" - ] - }, - { - "code":"ENT", - "name":"ENT", - "statuses":[ - "primary" - ] - }, - { - "code":"EPC", - "name":"EPC", - "statuses":[ - "primary" - ] - }, - { - "code":"EQM", - "name":"EQM", - "statuses":[ - "primary" - ] - }, - { - "code":"EQUAL", - "name":"EQUAL", - "statuses":[ - "primary" - ] - }, - { - "code":"ERC", - "name":"ERC", - "statuses":[ - "primary" - ] - }, - { - "code":"ERC3", - "name":"ERC3", - "statuses":[ - "primary" - ] - }, - { - "code":"ESB", - "name":"ESB", - "statuses":[ - "secondary" - ] - }, - { - "code":"ESC", - "name":"ESC", - "statuses":[ - "primary" - ] - }, - { - "code":"ESP", - "name":"ESP", - "statuses":[ - "primary" - ] - }, - { - "code":"ETCO", - "name":"ETCO", - "statuses":[ - "primary" - ] - }, - { - "code":"ETH", - "name":"Ethereum", - "statuses":[ - "primary", - "secondary" - ] - }, - { - "code":"ETC", - "name":"Ethereum Classic", - "statuses":[ - "primary" - ] - }, - { - "code":"ETHS", - "name":"ETHS", - "statuses":[ - "primary" - ] - }, - { - "code":"EUC", - "name":"EUC", - "statuses":[ - "primary" - ] - }, - { - "code":"EUR", - "name":"Euro", - "statuses":[ - "primary", - "secondary" - ] - }, - { - "code":"EGC", - "name":"EvergreenCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"EVIL", - "name":"EVIL", - "statuses":[ - "primary" - ] - }, - { - "code":"EXCL", - "name":"EXCL", - "statuses":[ - "primary" - ] - }, - { - "code":"EXP", - "name":"Expanse", - "statuses":[ - "primary" - ] - }, - { - "code":"FCT", - "name":"Factom", - "statuses":[ - "primary" - ] - }, - { - "code":"FAIR", - "name":"Faircoin", - "statuses":[ - "primary" - ] - }, - { - "code":"FC2", - "name":"FC2", - "statuses":[ - "primary" - ] - }, - { - "code":"FCH", - "name":"FCH", - "statuses":[ - "primary" - ] - }, - { - "code":"FCN", - "name":"FCN", - "statuses":[ - "primary" - ] - }, - { - "code":"FCP", - "name":"FCP", - "statuses":[ - "primary" - ] - }, - { - "code":"FTC", - "name":"Feathercoin", - "statuses":[ - "primary" - ] - }, - { - "code":"TIPS", - "name":"Fedoracoin", - "statuses":[ - "primary" - ] - }, - { - "code":"FIND", - "name":"FIND", - "statuses":[ - "primary" - ] - }, - { - "code":"FIT", - "name":"FIT", - "statuses":[ - "primary" - ] - }, - { - "code":"FJC", - "name":"FJC", - "statuses":[ - "primary" - ] - }, - { - "code":"FLO", - "name":"Florincoin", - "statuses":[ - "primary" - ] - }, - { - "code":"FLOZ", - "name":"FLOZ", - "statuses":[ - "primary" - ] - }, - { - "code":"FLT", - "name":"FlutterCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"FLY", - "name":"Flycoin", - "statuses":[ - "primary" - ] - }, - { - "code":"FLDC", - "name":"FoldingCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"FOREX", - "name":"FOREX", - "statuses":[ - "primary" - ] - }, - { - "code":"FRK", - "name":"Franko", - "statuses":[ - "primary" - ] - }, - { - "code":"FRDC", - "name":"FRDC", - "statuses":[ - "primary" - ] - }, - { - "code":"FRC", - "name":"Freicoin", - "statuses":[ - "primary" - ] - }, - { - "code":"FRN", - "name":"FRN", - "statuses":[ - "primary" - ] - }, - { - "code":"FRWC", - "name":"FRWC", - "statuses":[ - "primary" - ] - }, - { - "code":"FSN", - "name":"FSN", - "statuses":[ - "primary" - ] - }, - { - "code":"FST", - "name":"FST", - "statuses":[ - "primary" - ] - }, - { - "code":"FTP", - "name":"FTP", - "statuses":[ - "primary" - ] - }, - { - "code":"FUEL", - "name":"FUEL", - "statuses":[ - "primary" - ] - }, - { - "code":"FUN", - "name":"FUN", - "statuses":[ - "primary" - ] - }, - { - "code":"FUTC", - "name":"FUTC", - "statuses":[ - "primary" - ] - }, - { - "code":"FUZZ", - "name":"FUZZ", - "statuses":[ - "primary" - ] - }, - { - "code":"FX", - "name":"FX", - "statuses":[ - "primary" - ] - }, - { - "code":"GAIA", - "name":"GAIA", - "statuses":[ - "primary" - ] - }, - { - "code":"GAIN", - "name":"GAIN", - "statuses":[ - "primary" - ] - }, - { - "code":"GAKH", - "name":"GAKH", - "statuses":[ - "primary" - ] - }, - { - "code":"GAM", - "name":"GAM", - "statuses":[ - "primary" - ] - }, - { - "code":"GBT", - "name":"GameBet Coin", - "statuses":[ - "primary" - ] - }, - { - "code":"GAME", - "name":"GameCredits", - "statuses":[ - "primary" - ] - }, - { - "code":"GAP", - "name":"Gapcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"GARY", - "name":"GARY", - "statuses":[ - "primary" - ] - }, - { - "code":"GB", - "name":"GB", - "statuses":[ - "primary" - ] - }, - { - "code":"GBC", - "name":"GBC", - "statuses":[ - "primary" - ] - }, - { - "code":"GBIT", - "name":"GBIT", - "statuses":[ - "primary" - ] - }, - { - "code":"GBRC", - "name":"GBRC", - "statuses":[ - "primary" - ] - }, - { - "code":"GCN", - "name":"GCN", - "statuses":[ - "primary" - ] - }, - { - "code":"GENE", - "name":"GENE", - "statuses":[ - "primary" - ] - }, - { - "code":"GEO", - "name":"GeoCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"GEMZ", - "name":"GetGems", - "statuses":[ - "primary" - ] - }, - { - "code":"GHOST", - "name":"GHOST", - "statuses":[ - "primary" - ] - }, - { - "code":"GHS", - "name":"GHS", - "statuses":[ - "primary" - ] - }, - { - "code":"GLC", - "name":"GLC", - "statuses":[ - "primary" - ] - }, - { - "code":"BSTY", - "name":"GlobalBoost-Y", - "statuses":[ - "primary" - ] - }, - { - "code":"GMCX", - "name":"GMCX", - "statuses":[ - "primary" - ] - }, - { - "code":"GML", - "name":"GML", - "statuses":[ - "primary" - ] - }, - { - "code":"GMX", - "name":"GMX", - "statuses":[ - "primary" - ] - }, - { - "code":"GOAT", - "name":"GOAT", - "statuses":[ - "primary" - ] - }, - { - "code":"GCR", - "name":"GoCoineR", - "statuses":[ - "primary" - ] - }, - { - "code":"GLD", - "name":"GoldCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"GOON", - "name":"GOON", - "statuses":[ - "primary" - ] - }, - { - "code":"GOTX", - "name":"GOTX", - "statuses":[ - "primary" - ] - }, - { - "code":"GP", - "name":"GP", - "statuses":[ - "primary" - ] - }, - { - "code":"GPU", - "name":"GPU", - "statuses":[ - "primary" - ] - }, - { - "code":"GRF", - "name":"Graffiti", - "statuses":[ - "primary" - ] - }, - { - "code":"GRAM", - "name":"GRAM", - "statuses":[ - "primary" - ] - }, - { - "code":"GRT", - "name":"Grantcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"GREED", - "name":"GREED", - "statuses":[ - "primary" - ] - }, - { - "code":"GRC", - "name":"Gridcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"GRN", - "name":"GRN", - "statuses":[ - "primary" - ] - }, - { - "code":"GRS", - "name":"Groestlcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"GROW", - "name":"GrowCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"GRW", - "name":"GRW", - "statuses":[ - "primary" - ] - }, - { - "code":"GSY", - "name":"GSY", - "statuses":[ - "primary" - ] - }, - { - "code":"GUA", - "name":"GUA", - "statuses":[ - "primary" - ] - }, - { - "code":"NLG", - "name":"Gulden", - "statuses":[ - "primary" - ] - }, - { - "code":"GUM", - "name":"GUM", - "statuses":[ - "primary" - ] - }, - { - "code":"GUN", - "name":"GUN", - "statuses":[ - "primary" - ] - }, - { - "code":"GYC", - "name":"GYC", - "statuses":[ - "primary" - ] - }, - { - "code":"HALLO", - "name":"HALLO", - "statuses":[ - "primary" - ] - }, - { - "code":"HAM", - "name":"HAM", - "statuses":[ - "primary" - ] - }, - { - "code":"HBT", - "name":"HBT", - "statuses":[ - "secondary" - ] - }, - { - "code":"HCC", - "name":"HCC", - "statuses":[ - "primary" - ] - }, - { - "code":"HEAT", - "name":"HEAT", - "statuses":[ - "primary" - ] - }, - { - "code":"HMP", - "name":"HempCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"XHI", - "name":"HiCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"HILL", - "name":"HILL", - "statuses":[ - "primary" - ] - }, - { - "code":"HODL", - "name":"HOdlcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"HKD", - "name":"Hong Kong Dollar", - "statuses":[ - "secondary" - ] - }, - { - "code":"HZ", - "name":"Horizon", - "statuses":[ - "primary" - ] - }, - { - "code":"HSP", - "name":"HSP", - "statuses":[ - "primary" - ] - }, - { - "code":"HTC", - "name":"HTC", - "statuses":[ - "primary" - ] - }, - { - "code":"HTML5", - "name":"HTMLCOIN", - "statuses":[ - "primary" - ] - }, - { - "code":"HUC", - "name":"HUC", - "statuses":[ - "primary" - ] - }, - { - "code":"HVCO", - "name":"HVCO", - "statuses":[ - "primary" - ] - }, - { - "code":"HXX", - "name":"HXX", - "statuses":[ - "primary" - ] - }, - { - "code":"HYPER", - "name":"Hyper", - "statuses":[ - "primary" - ] - }, - { - "code":"HYP", - "name":"HyperStake", - "statuses":[ - "primary" - ] - }, - { - "code":"IBANK", - "name":"IBANK", - "statuses":[ - "primary" - ] - }, - { - "code":"ICASH", - "name":"iCash", - "statuses":[ - "primary" - ] - }, - { - "code":"ICN", - "name":"iCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"IFLT", - "name":"IFLT", - "statuses":[ - "primary" - ] - }, - { - "code":"IMPS", - "name":"IMPS", - "statuses":[ - "primary" - ] - }, - { - "code":"INCP", - "name":"INCP", - "statuses":[ - "primary" - ] - }, - { - "code":"IFC", - "name":"Infinitecoin", - "statuses":[ - "primary" - ] - }, - { - "code":"INFX", - "name":"Influxcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"IOC", - "name":"IO Coin", - "statuses":[ - "primary" - ] - }, - { - "code":"ION", - "name":"ION", - "statuses":[ - "primary" - ] - }, - { - "code":"ISL", - "name":"IslaCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"IVZ", - "name":"IVZ", - "statuses":[ - "primary" - ] - }, - { - "code":"IXC", - "name":"IXC", - "statuses":[ - "primary" - ] - }, - { - "code":"JPY", - "name":"Japanese Yen", - "statuses":[ - "secondary" - ] - }, - { - "code":"JOBS", - "name":"JOBS", - "statuses":[ - "primary" - ] - }, - { - "code":"JPC", - "name":"JPC", - "statuses":[ - "primary" - ] - }, - { - "code":"JBS", - "name":"Jumbucks", - "statuses":[ - "primary" - ] - }, - { - "code":"JW", - "name":"JW", - "statuses":[ - "primary" - ] - }, - { - "code":"JWL", - "name":"JWL", - "statuses":[ - "primary" - ] - }, - { - "code":"KAT", - "name":"KAT", - "statuses":[ - "primary" - ] - }, - { - "code":"KC", - "name":"KC", - "statuses":[ - "primary" - ] - }, - { - "code":"KNC", - "name":"KhanCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"KLC", - "name":"KLC", - "statuses":[ - "primary" - ] - }, - { - "code":"KOBO", - "name":"KOBO", - "statuses":[ - "primary" - ] - }, - { - "code":"KORE", - "name":"KoreCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"KRAK", - "name":"KRAK", - "statuses":[ - "primary" - ] - }, - { - "code":"KRB", - "name":"KRB", - "statuses":[ - "primary" - ] - }, - { - "code":"KRC", - "name":"KRC", - "statuses":[ - "primary" - ] - }, - { - "code":"KRYP", - "name":"KRYP", - "statuses":[ - "primary" - ] - }, - { - "code":"KR", - "name":"Krypton", - "statuses":[ - "primary" - ] - }, - { - "code":"KTK", - "name":"KTK", - "statuses":[ - "primary" - ] - }, - { - "code":"LANA", - "name":"LANA", - "statuses":[ - "primary" - ] - }, - { - "code":"LAZ", - "name":"LAZ", - "statuses":[ - "primary" - ] - }, - { - "code":"LBC", - "name":"LBC", - "statuses":[ - "primary" - ] - }, - { - "code":"LC", - "name":"LC", - "statuses":[ - "primary" - ] - }, - { - "code":"LEA", - "name":"LeaCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"LEAF", - "name":"LEAF", - "statuses":[ - "primary" - ] - }, - { - "code":"LEO", - "name":"LEO", - "statuses":[ - "primary", - "secondary" - ] - }, - { - "code":"LFC", - "name":"LFC", - "statuses":[ - "primary" - ] - }, - { - "code":"LFO", - "name":"LFO", - "statuses":[ - "primary" - ] - }, - { - "code":"LFTC", - "name":"LFTC", - "statuses":[ - "primary" - ] - }, - { - "code":"LGBTQ", - "name":"LGBTQ", - "statuses":[ - "primary" - ] - }, - { - "code":"LIR", - "name":"LIR", - "statuses":[ - "primary" - ] - }, - { - "code":"LSK", - "name":"Lisk", - "statuses":[ - "primary" - ] - }, - { - "code":"LTC", - "name":"Litecoin", - "statuses":[ - "primary", - "secondary" - ] - }, - { - "code":"LTCR", - "name":"Litecred", - "statuses":[ - "primary" - ] - }, - { - "code":"LIV", - "name":"LIV", - "statuses":[ - "primary" - ] - }, - { - "code":"LKC", - "name":"LKC", - "statuses":[ - "primary" - ] - }, - { - "code":"LOC", - "name":"LOC", - "statuses":[ - "primary" - ] - }, - { - "code":"LOOT", - "name":"LOOT", - "statuses":[ - "primary" - ] - }, - { - "code":"LTBC", - "name":"LTBcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"LTH", - "name":"LTH", - "statuses":[ - "primary" - ] - }, - { - "code":"LTS", - "name":"LTS", - "statuses":[ - "primary" - ] - }, - { - "code":"LUCKY", - "name":"LUCKY", - "statuses":[ - "primary" - ] - }, - { - "code":"LUN", - "name":"LUN", - "statuses":[ - "primary" - ] - }, - { - "code":"LXC", - "name":"LXC", - "statuses":[ - "primary" - ] - }, - { - "code":"MAD", - "name":"MAD", - "statuses":[ - "primary" - ] - }, - { - "code":"XMG", - "name":"Magi", - "statuses":[ - "primary" - ] - }, - { - "code":"MAID", - "name":"MaidSafeCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"MXT", - "name":"MarteXcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"OMNI", - "name":"Mastercoin (Omni)", - "statuses":[ - "primary" - ] - }, - { - "code":"MTR", - "name":"MasterTraderCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"MAX", - "name":"Maxcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"MZC", - "name":"Mazacoin", - "statuses":[ - "primary" - ] - }, - { - "code":"MBL", - "name":"MBL", - "statuses":[ - "primary" - ] - }, - { - "code":"MCZ", - "name":"MCZ", - "statuses":[ - "primary" - ] - }, - { - "code":"MED", - "name":"MediterraneanCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"MEGA", - "name":"MEGA", - "statuses":[ - "primary" - ] - }, - { - "code":"MEC", - "name":"Megacoin", - "statuses":[ - "primary" - ] - }, - { - "code":"MEME", - "name":"Memetic", - "statuses":[ - "primary" - ] - }, - { - "code":"METAL", - "name":"METAL", - "statuses":[ - "primary" - ] - }, - { - "code":"MG", - "name":"MG", - "statuses":[ - "primary" - ] - }, - { - "code":"MND", - "name":"MindCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"MINT", - "name":"Mintcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"MIS", - "name":"MIS", - "statuses":[ - "primary" - ] - }, - { - "code":"MMNXT", - "name":"MMNXT", - "statuses":[ - "primary" - ] - }, - { - "code":"MMXVI", - "name":"MMXVI", - "statuses":[ - "primary" - ] - }, - { - "code":"MNM", - "name":"MNM", - "statuses":[ - "primary" - ] - }, - { - "code":"MOIN", - "name":"MOIN", - "statuses":[ - "primary" - ] - }, - { - "code":"MOJO", - "name":"MojoCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"MONA", - "name":"MonaCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"XMR", - "name":"Monero", - "statuses":[ - "primary", - "secondary" - ] - }, - { - "code":"MUE", - "name":"MonetaryUnit", - "statuses":[ - "primary" - ] - }, - { - "code":"MOON", - "name":"Mooncoin", - "statuses":[ - "primary" - ] - }, - { - "code":"MOOND", - "name":"MOOND", - "statuses":[ - "primary" - ] - }, - { - "code":"MPRO", - "name":"MPRO", - "statuses":[ - "primary" - ] - }, - { - "code":"MRB", - "name":"MRB", - "statuses":[ - "primary" - ] - }, - { - "code":"MUDRA", - "name":"MUDRA", - "statuses":[ - "primary" - ] - }, - { - "code":"MYR", - "name":"Myriadcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"N2O", - "name":"N2O", - "statuses":[ - "primary" - ] - }, - { - "code":"N7", - "name":"N7", - "statuses":[ - "primary" - ] - }, - { - "code":"NMC", - "name":"Namecoin", - "statuses":[ - "primary" - ] - }, - { - "code":"NAT", - "name":"NAT", - "statuses":[ - "primary" - ] - }, - { - "code":"NAUT", - "name":"Nautiluscoin", - "statuses":[ - "primary" - ] - }, - { - "code":"NAV", - "name":"NAV Coin", - "statuses":[ - "primary" - ] - }, - { - "code":"NBIT", - "name":"NBIT", - "statuses":[ - "primary" - ] - }, - { - "code":"NCS", - "name":"NCS", - "statuses":[ - "primary" - ] - }, - { - "code":"NDOGE", - "name":"NDOGE", - "statuses":[ - "primary" - ] - }, - { - "code":"XEM", - "name":"NEM", - "statuses":[ - "primary" - ] - }, - { - "code":"NEOS", - "name":"NeosCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"NET", - "name":"NetCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"NEU", - "name":"NeuCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"NTRN", - "name":"Neutron", - "statuses":[ - "primary" - ] - }, - { - "code":"NEVA", - "name":"NevaCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"NEWB", - "name":"NEWB", - "statuses":[ - "primary" - ] - }, - { - "code":"NXS", - "name":"Nexus", - "statuses":[ - "primary" - ] - }, - { - "code":"NIC", - "name":"NIC", - "statuses":[ - "primary" - ] - }, - { - "code":"NICE", - "name":"NICE", - "statuses":[ - "primary" - ] - }, - { - "code":"NKC", - "name":"NKC", - "statuses":[ - "primary" - ] - }, - { - "code":"NLC", - "name":"NLC", - "statuses":[ - "primary" - ] - }, - { - "code":"NOBL", - "name":"NobleCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"NODES", - "name":"NODES", - "statuses":[ - "primary" - ] - }, - { - "code":"NVC", - "name":"Novacoin", - "statuses":[ - "primary" - ] - }, - { - "code":"NRS", - "name":"NRS", - "statuses":[ - "primary" - ] - }, - { - "code":"NTC", - "name":"NTC", - "statuses":[ - "primary" - ] - }, - { - "code":"NBT", - "name":"NuBits", - "statuses":[ - "primary" - ] - }, - { - "code":"NUKE", - "name":"NUKE", - "statuses":[ - "primary" - ] - }, - { - "code":"NUM", - "name":"NUM", - "statuses":[ - "primary" - ] - }, - { - "code":"NSR", - "name":"NuShares", - "statuses":[ - "primary" - ] - }, - { - "code":"NXE", - "name":"NXE", - "statuses":[ - "primary" - ] - }, - { - "code":"NXT", - "name":"NXT", - "statuses":[ - "primary" - ] - }, - { - "code":"NXTTY", - "name":"Nxttycoin", - "statuses":[ - "primary" - ] - }, - { - "code":"NYC", - "name":"NYC", - "statuses":[ - "primary" - ] - }, - { - "code":"NZC", - "name":"NZC", - "statuses":[ - "primary" - ] - }, - { - "code":"NZD", - "name":"NZD", - "statuses":[ - "primary", - "secondary" - ] - }, - { - "code":"OBS", - "name":"OBS", - "statuses":[ - "primary" - ] - }, - { - "code":"OCOW", - "name":"OCOW", - "statuses":[ - "primary" - ] - }, - { - "code":"OK", - "name":"OKCash", - "statuses":[ - "primary" - ] - }, - { - "code":"OLYMP", - "name":"OLYMP", - "statuses":[ - "primary" - ] - }, - { - "code":"OMC", - "name":"OMC", - "statuses":[ - "primary" - ] - }, - { - "code":"ONE", - "name":"ONE", - "statuses":[ - "primary" - ] - }, - { - "code":"OP", - "name":"OP", - "statuses":[ - "primary" - ] - }, - { - "code":"OPAL", - "name":"OPAL", - "statuses":[ - "primary" - ] - }, - { - "code":"ORB", - "name":"Orbitcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"OZC", - "name":"OZC", - "statuses":[ - "primary" - ] - }, - { - "code":"PAC", - "name":"PAC", - "statuses":[ - "primary" - ] - }, - { - "code":"PAL", - "name":"PAL", - "statuses":[ - "primary" - ] - }, - { - "code":"PND", - "name":"Pandacoin", - "statuses":[ - "primary" - ] - }, - { - "code":"PARA", - "name":"PARA", - "statuses":[ - "primary" - ] - }, - { - "code":"PAY", - "name":"PAY", - "statuses":[ - "primary" - ] - }, - { - "code":"XPY", - "name":"Paycoin", - "statuses":[ - "primary" - ] - }, - { - "code":"PBC", - "name":"PBC", - "statuses":[ - "primary" - ] - }, - { - "code":"PCM", - "name":"PCM", - "statuses":[ - "primary" - ] - }, - { - "code":"PCS", - "name":"PCS", - "statuses":[ - "primary" - ] - }, - { - "code":"PDC", - "name":"PDC", - "statuses":[ - "primary" - ] - }, - { - "code":"PEC", - "name":"PEC", - "statuses":[ - "primary" - ] - }, - { - "code":"PPC", - "name":"Peercoin", - "statuses":[ - "primary" - ] - }, - { - "code":"PEN", - "name":"PEN", - "statuses":[ - "primary" - ] - }, - { - "code":"PHR", - "name":"PHR", - "statuses":[ - "primary" - ] - }, - { - "code":"PIN", - "name":"PIN", - "statuses":[ - "primary" - ] - }, - { - "code":"PC", - "name":"Pinkcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"PIO", - "name":"PIO", - "statuses":[ - "primary" - ] - }, - { - "code":"PIZZA", - "name":"PIZZA", - "statuses":[ - "primary" - ] - }, - { - "code":"PKB", - "name":"PKB", - "statuses":[ - "primary" - ] - }, - { - "code":"PLN", - "name":"PLN", - "statuses":[ - "primary", - "secondary" - ] - }, - { - "code":"PLNC", - "name":"PLNC", - "statuses":[ - "primary" - ] - }, - { - "code":"PNK", - "name":"PNK", - "statuses":[ - "primary" - ] - }, - { - "code":"POKE", - "name":"POKE", - "statuses":[ - "primary" - ] - }, - { - "code":"PONZ2", - "name":"PONZ2", - "statuses":[ - "primary" - ] - }, - { - "code":"PONZI", - "name":"PONZI", - "statuses":[ - "primary" - ] - }, - { - "code":"PEX", - "name":"PosEx", - "statuses":[ - "primary" - ] - }, - { - "code":"POST", - "name":"POST", - "statuses":[ - "primary" - ] - }, - { - "code":"POT", - "name":"Potcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"PRE", - "name":"PRE", - "statuses":[ - "primary" - ] - }, - { - "code":"PRES", - "name":"PRES", - "statuses":[ - "primary" - ] - }, - { - "code":"PXI", - "name":"Prime-XI", - "statuses":[ - "primary" - ] - }, - { - "code":"PRIME", - "name":"PrimeChain", - "statuses":[ - "primary" - ] - }, - { - "code":"XPM", - "name":"Primecoin", - "statuses":[ - "primary" - ] - }, - { - "code":"PRM", - "name":"PRM", - "statuses":[ - "primary" - ] - }, - { - "code":"PRT", - "name":"PRT", - "statuses":[ - "primary" - ] - }, - { - "code":"PSB", - "name":"PSB", - "statuses":[ - "primary" - ] - }, - { - "code":"PSP", - "name":"PSP", - "statuses":[ - "primary" - ] - }, - { - "code":"PSY", - "name":"PSY", - "statuses":[ - "primary" - ] - }, - { - "code":"PTC", - "name":"PTC", - "statuses":[ - "primary" - ] - }, - { - "code":"PURE", - "name":"PURE", - "statuses":[ - "primary" - ] - }, - { - "code":"PUTIN", - "name":"PUTIN", - "statuses":[ - "primary" - ] - }, - { - "code":"PWR", - "name":"PWR", - "statuses":[ - "primary" - ] - }, - { - "code":"PX", - "name":"PX", - "statuses":[ - "primary" - ] - }, - { - "code":"PXL", - "name":"PXL", - "statuses":[ - "primary" - ] - }, - { - "code":"QBC", - "name":"QBC", - "statuses":[ - "primary" - ] - }, - { - "code":"QBK", - "name":"QBK", - "statuses":[ - "primary" - ] - }, - { - "code":"QCN", - "name":"QCN", - "statuses":[ - "primary" - ] - }, - { - "code":"QORA", - "name":"Qora", - "statuses":[ - "primary" - ] - }, - { - "code":"QTZ", - "name":"QTZ", - "statuses":[ - "primary" - ] - }, - { - "code":"QRK", - "name":"Quark", - "statuses":[ - "primary" - ] - }, - { - "code":"QTL", - "name":"Quatloo", - "statuses":[ - "primary" - ] - }, - { - "code":"RADI", - "name":"RADI", - "statuses":[ - "primary" - ] - }, - { - "code":"RADS", - "name":"Radium", - "statuses":[ - "primary" - ] - }, - { - "code":"XRA", - "name":"RateCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"RBIT", - "name":"RBIT", - "statuses":[ - "primary" - ] - }, - { - "code":"RCN", - "name":"RCN", - "statuses":[ - "primary" - ] - }, - { - "code":"RED", - "name":"RED", - "statuses":[ - "primary" - ] - }, - { - "code":"RDD", - "name":"Reddcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"REE", - "name":"REE", - "statuses":[ - "primary" - ] - }, - { - "code":"REV", - "name":"Revenu", - "statuses":[ - "primary" - ] - }, - { - "code":"RICHX", - "name":"RICHX", - "statuses":[ - "primary" - ] - }, - { - "code":"RIC", - "name":"Riecoin", - "statuses":[ - "primary" - ] - }, - { - "code":"RBT", - "name":"Rimbit", - "statuses":[ - "primary", - "secondary" - ] - }, - { - "code":"RIO", - "name":"RIO", - "statuses":[ - "primary" - ] - }, - { - "code":"XRP", - "name":"Ripple", - "statuses":[ - "primary" - ] - }, - { - "code":"RISE", - "name":"RISE", - "statuses":[ - "primary" - ] - }, - { - "code":"RMS", - "name":"RMS", - "statuses":[ - "primary" - ] - }, - { - "code":"RONIN", - "name":"RONIN", - "statuses":[ - "primary" - ] - }, - { - "code":"ROYAL", - "name":"ROYAL", - "statuses":[ - "primary" - ] - }, - { - "code":"RPC", - "name":"RPC", - "statuses":[ - "primary" - ] - }, - { - "code":"RRT", - "name":"RRT", - "statuses":[ - "primary" - ] - }, - { - "code":"RBIES", - "name":"Rubies", - "statuses":[ - "primary" - ] - }, - { - "code":"RUBIT", - "name":"RUBIT", - "statuses":[ - "primary" - ] - }, - { - "code":"RUR", - "name":"Ruble", - "statuses":[ - "secondary" - ] - }, - { - "code":"RBY", - "name":"Rubycoin", - "statuses":[ - "primary" - ] - }, - { - "code":"RUST", - "name":"RUST", - "statuses":[ - "primary" - ] - }, - { - "code":"RYCN", - "name":"RYCN", - "statuses":[ - "primary" - ] - }, - { - "code":"SEC", - "name":"Safe Exchange Coin", - "statuses":[ - "primary" - ] - }, - { - "code":"SAK", - "name":"SAK", - "statuses":[ - "primary" - ] - }, - { - "code":"SAR", - "name":"SAR", - "statuses":[ - "primary" - ] - }, - { - "code":"SBD", - "name":"SBD", - "statuses":[ - "primary" - ] - }, - { - "code":"SCAN", - "name":"SCAN", - "statuses":[ - "primary" - ] - }, - { - "code":"SCB", - "name":"SCB", - "statuses":[ - "primary" - ] - }, - { - "code":"SCN", - "name":"SCN", - "statuses":[ - "primary" - ] - }, - { - "code":"SCOT", - "name":"Scotcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"SCRPT", - "name":"SCRPT", - "statuses":[ - "primary" - ] - }, - { - "code":"SCRT", - "name":"SCRT", - "statuses":[ - "primary" - ] - }, - { - "code":"SCT", - "name":"SCT", - "statuses":[ - "primary" - ] - }, - { - "code":"SRC", - "name":"SecureCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"SED", - "name":"SED", - "statuses":[ - "primary" - ] - }, - { - "code":"SXC", - "name":"Sexcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"SGD", - "name":"SGD", - "statuses":[ - "primary", - "secondary" - ] - }, - { - "code":"SH", - "name":"SH", - "statuses":[ - "primary" - ] - }, - { - "code":"SDC", - "name":"ShadowCash", - "statuses":[ - "primary" - ] - }, - { - "code":"SHELL", - "name":"SHELL", - "statuses":[ - "primary" - ] - }, - { - "code":"SHI", - "name":"SHI", - "statuses":[ - "primary" - ] - }, - { - "code":"SHIFT", - "name":"Shift", - "statuses":[ - "primary" - ] - }, - { - "code":"SC", - "name":"Siacoin", - "statuses":[ - "primary" - ] - }, - { - "code":"SIB", - "name":"Siberian chervonets", - "statuses":[ - "primary" - ] - }, - { - "code":"SIGU", - "name":"SIGU", - "statuses":[ - "primary" - ] - }, - { - "code":"SLFI", - "name":"SLFI", - "statuses":[ - "primary" - ] - }, - { - "code":"SLING", - "name":"Sling", - "statuses":[ - "primary" - ] - }, - { - "code":"SLK", - "name":"SLK", - "statuses":[ - "primary" - ] - }, - { - "code":"SLS", - "name":"SLS", - "statuses":[ - "primary" - ] - }, - { - "code":"SMC", - "name":"SMC", - "statuses":[ - "primary" - ] - }, - { - "code":"SMLY", - "name":"SmileyCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"SNGLS", - "name":"SNGLS", - "statuses":[ - "primary" - ] - }, - { - "code":"SNRG", - "name":"SNRG", - "statuses":[ - "primary" - ] - }, - { - "code":"SOIL", - "name":"SOILcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"SLR", - "name":"Solarcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"SONG", - "name":"SongCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"SOON", - "name":"SOON", - "statuses":[ - "primary" - ] - }, - { - "code":"SP", - "name":"SP", - "statuses":[ - "primary" - ] - }, - { - "code":"SPACE", - "name":"SPACE", - "statuses":[ - "primary" - ] - }, - { - "code":"SPEX", - "name":"SPEX", - "statuses":[ - "primary" - ] - }, - { - "code":"SPHR", - "name":"Sphere", - "statuses":[ - "primary" - ] - }, - { - "code":"SPKTR", - "name":"SPKTR", - "statuses":[ - "primary" - ] - }, - { - "code":"SPN", - "name":"SPN", - "statuses":[ - "primary" - ] - }, - { - "code":"SPORT", - "name":"SPORT", - "statuses":[ - "primary" - ] - }, - { - "code":"SPR", - "name":"SpreadCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"SPT", - "name":"SPT", - "statuses":[ - "primary" - ] - }, - { - "code":"SPX", - "name":"SPX", - "statuses":[ - "primary" - ] - }, - { - "code":"SSC", - "name":"SSC", - "statuses":[ - "primary" - ] - }, - { - "code":"STA", - "name":"STA", - "statuses":[ - "primary" - ] - }, - { - "code":"STAR", - "name":"STAR", - "statuses":[ - "primary" - ] - }, - { - "code":"START", - "name":"Startcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"STE", - "name":"STE", - "statuses":[ - "primary" - ] - }, - { - "code":"XST", - "name":"Stealthcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"STEEM", - "name":"Steem", - "statuses":[ - "primary" - ] - }, - { - "code":"XLM", - "name":"Stellar", - "statuses":[ - "primary" - ] - }, - { - "code":"STR", - "name":"Stellar", - "statuses":[ - "primary" - ] - }, - { - "code":"STEPS", - "name":"Steps", - "statuses":[ - "primary" - ] - }, - { - "code":"SLG", - "name":"Sterlingcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"STHR", - "name":"STHR", - "statuses":[ - "primary" - ] - }, - { - "code":"STL", - "name":"STL", - "statuses":[ - "primary" - ] - }, - { - "code":"STO", - "name":"STO", - "statuses":[ - "primary" - ] - }, - { - "code":"SJCX", - "name":"Storjcoin X", - "statuses":[ - "primary" - ] - }, - { - "code":"STP", - "name":"STP", - "statuses":[ - "primary" - ] - }, - { - "code":"STRAT", - "name":"STRAT", - "statuses":[ - "primary" - ] - }, - { - "code":"STS", - "name":"Stress", - "statuses":[ - "primary" - ] - }, - { - "code":"STV", - "name":"STV", - "statuses":[ - "primary" - ] - }, - { - "code":"SUB", - "name":"Subcriptio", - "statuses":[ - "primary" - ] - }, - { - "code":"UNITY", - "name":"SuperNET", - "statuses":[ - "primary" - ] - }, - { - "code":"SWEET", - "name":"SWEET", - "statuses":[ - "primary" - ] - }, - { - "code":"SWING", - "name":"SWING", - "statuses":[ - "primary" - ] - }, - { - "code":"SYNC", - "name":"SYNC", - "statuses":[ - "primary" - ] - }, - { - "code":"AMP", - "name":"Synereo", - "statuses":[ - "primary" - ] - }, - { - "code":"SYNX", - "name":"SYNX", - "statuses":[ - "primary" - ] - }, - { - "code":"SYS", - "name":"Syscoin", - "statuses":[ - "primary" - ] - }, - { - "code":"TAB", - "name":"TAB", - "statuses":[ - "primary" - ] - }, - { - "code":"TAG", - "name":"TagCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"TAJ", - "name":"TAJ", - "statuses":[ - "primary" - ] - }, - { - "code":"TAK", - "name":"TAK", - "statuses":[ - "primary" - ] - }, - { - "code":"TAO", - "name":"TAO", - "statuses":[ - "primary" - ] - }, - { - "code":"TBC", - "name":"TBC", - "statuses":[ - "primary" - ] - }, - { - "code":"TC", - "name":"TC", - "statuses":[ - "secondary" - ] - }, - { - "code":"TCOIN", - "name":"TCOIN", - "statuses":[ - "primary" - ] - }, - { - "code":"TCR", - "name":"TCR", - "statuses":[ - "primary" - ] - }, - { - "code":"TDFB", - "name":"TDFB", - "statuses":[ - "primary" - ] - }, - { - "code":"TDY", - "name":"TDY", - "statuses":[ - "primary" - ] - }, - { - "code":"TEAM", - "name":"TEAM", - "statuses":[ - "primary" - ] - }, - { - "code":"TEC", - "name":"TEC", - "statuses":[ - "primary" - ] - }, - { - "code":"TECH", - "name":"TECH", - "statuses":[ - "primary" - ] - }, - { - "code":"TEK", - "name":"TEKcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"TRC", - "name":"Terracoin", - "statuses":[ - "primary" - ] - }, - { - "code":"TESLA", - "name":"TESLA", - "statuses":[ - "primary" - ] - }, - { - "code":"TES", - "name":"TeslaCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"TET", - "name":"TET", - "statuses":[ - "primary" - ] - }, - { - "code":"THC", - "name":"THC", - "statuses":[ - "primary" - ] - }, - { - "code":"DAO", - "name":"The DAO", - "statuses":[ - "primary" - ] - }, - { - "code":"TIA", - "name":"TIA", - "statuses":[ - "primary" - ] - }, - { - "code":"TIX", - "name":"Tickets", - "statuses":[ - "primary" - ] - }, - { - "code":"XTC", - "name":"TileCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"TIT", - "name":"Titcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"TMC", - "name":"TMC", - "statuses":[ - "primary" - ] - }, - { - "code":"TNG", - "name":"TNG", - "statuses":[ - "primary" - ] - }, - { - "code":"TODAY", - "name":"TODAY", - "statuses":[ - "primary" - ] - }, - { - "code":"TOKEN", - "name":"TOKEN", - "statuses":[ - "primary" - ] - }, - { - "code":"TP1", - "name":"TP1", - "statuses":[ - "primary" - ] - }, - { - "code":"TPC", - "name":"TPC", - "statuses":[ - "primary" - ] - }, - { - "code":"TPG", - "name":"TPG", - "statuses":[ - "primary" - ] - }, - { - "code":"TX", - "name":"Transfercoin", - "statuses":[ - "primary" - ] - }, - { - "code":"TRAP", - "name":"TRAP", - "statuses":[ - "primary" - ] - }, - { - "code":"TRICK", - "name":"TRICK", - "statuses":[ - "primary" - ] - }, - { - "code":"TRIG", - "name":"TRIG", - "statuses":[ - "primary" - ] - }, - { - "code":"TROLL", - "name":"TROLL", - "statuses":[ - "primary" - ] - }, - { - "code":"TRK", - "name":"Truckcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"TRUMP", - "name":"TrumpCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"TRUST", - "name":"TRUST", - "statuses":[ - "primary" - ] - }, - { - "code":"TSC", - "name":"TSC", - "statuses":[ - "primary" - ] - }, - { - "code":"TWERK", - "name":"TWERK", - "statuses":[ - "primary" - ] - }, - { - "code":"TWIST", - "name":"TWIST", - "statuses":[ - "primary" - ] - }, - { - "code":"TWO", - "name":"TWO", - "statuses":[ - "primary" - ] - }, - { - "code":"UAE", - "name":"UAE", - "statuses":[ - "primary" - ] - }, - { - "code":"UB", - "name":"UB", - "statuses":[ - "primary" - ] - }, - { - "code":"UFO", - "name":"UFO Coin", - "statuses":[ - "primary" - ] - }, - { - "code":"UIS", - "name":"UIS", - "statuses":[ - "primary" - ] - }, - { - "code":"UAH", - "name":"Ukrainian Hryvnia", - "statuses":[ - "secondary" - ] - }, - { - "code":"UTC", - "name":"UltraCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"UNB", - "name":"UNB", - "statuses":[ - "primary" - ] - }, - { - "code":"UNC", - "name":"UNC", - "statuses":[ - "primary" - ] - }, - { - "code":"UNF", - "name":"Unfed", - "statuses":[ - "primary" - ] - }, - { - "code":"UNIQ", - "name":"UNIQ", - "statuses":[ - "primary" - ] - }, - { - "code":"UNIT", - "name":"Universal Currency", - "statuses":[ - "primary" - ] - }, - { - "code":"UNO", - "name":"Unobtanium", - "statuses":[ - "primary" - ] - }, - { - "code":"URC", - "name":"URC", - "statuses":[ - "primary" - ] - }, - { - "code":"URO", - "name":"Uro", - "statuses":[ - "primary" - ] - }, - { - "code":"USD", - "name":"US Dollar", - "statuses":[ - "primary", - "secondary" - ] - }, - { - "code":"USDE", - "name":"USDE", - "statuses":[ - "primary" - ] - }, - { - "code":"XVC", - "name":"Vcash", - "statuses":[ - "primary" - ] - }, - { - "code":"VCN", - "name":"VCN", - "statuses":[ - "primary" - ] - }, - { - "code":"VCOIN", - "name":"VCOIN", - "statuses":[ - "primary" - ] - }, - { - "code":"VEC", - "name":"VEC", - "statuses":[ - "primary" - ] - }, - { - "code":"VEG", - "name":"VEG", - "statuses":[ - "primary" - ] - }, - { - "code":"XVG", - "name":"Verge", - "statuses":[ - "primary" - ] - }, - { - "code":"VRC", - "name":"VeriCoin", - "statuses":[ - "primary", - "secondary" - ] - }, - { - "code":"VTC", - "name":"Vertcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"VIA", - "name":"Viacoin", - "statuses":[ - "primary" - ] - }, - { - "code":"VIP", - "name":"VIP Tokens", - "statuses":[ - "primary" - ] - }, - { - "code":"VIRAL", - "name":"Viral", - "statuses":[ - "primary" - ] - }, - { - "code":"VLT", - "name":"VLT", - "statuses":[ - "primary" - ] - }, - { - "code":"VOOT", - "name":"VootCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"VOX", - "name":"Voxels", - "statuses":[ - "primary" - ] - }, - { - "code":"VOYA", - "name":"VOYA", - "statuses":[ - "primary" - ] - }, - { - "code":"VPN", - "name":"VPNCoin", - "statuses":[ - "primary" - ] - }, - { - "code":"VRM", - "name":"VRM", - "statuses":[ - "primary" - ] - }, - { - "code":"VRS", - "name":"VRS", - "statuses":[ - "primary" - ] - }, - { - "code":"VTA", - "name":"VTA", - "statuses":[ - "primary" - ] - }, - { - "code":"VTN", - "name":"VTN", - "statuses":[ - "primary" - ] - }, - { - "code":"VTR", - "name":"VTR", - "statuses":[ - "primary" - ] - }, - { - "code":"VTY", - "name":"VTY", - "statuses":[ - "primary" - ] - }, - { - "code":"WA", - "name":"WA", - "statuses":[ - "primary" - ] - }, - { - "code":"WAC", - "name":"WAC", - "statuses":[ - "primary" - ] - }, - { - "code":"WARP", - "name":"WARP", - "statuses":[ - "primary" - ] - }, - { - "code":"WASH", - "name":"WASH", - "statuses":[ - "primary" - ] - }, - { - "code":"WAV", - "name":"WAV", - "statuses":[ - "primary" - ] - }, - { - "code":"WAVES", - "name":"WAVES", - "statuses":[ - "primary" - ] - }, - { - "code":"WAY", - "name":"WAY", - "statuses":[ - "primary" - ] - }, - { - "code":"WCN", - "name":"WCN", - "statuses":[ - "primary" - ] - }, - { - "code":"WEX", - "name":"WEX", - "statuses":[ - "primary" - ] - }, - { - "code":"WGC", - "name":"WGC", - "statuses":[ - "primary" - ] - }, - { - "code":"XWC", - "name":"Whitecoin", - "statuses":[ - "primary" - ] - }, - { - "code":"WBB", - "name":"Wild Beast Block", - "statuses":[ - "primary" - ] - }, - { - "code":"WINE", - "name":"WINE", - "statuses":[ - "primary" - ] - }, - { - "code":"WLC", - "name":"WLC", - "statuses":[ - "primary" - ] - }, - { - "code":"WMC", - "name":"WMC", - "statuses":[ - "primary" - ] - }, - { - "code":"LOG", - "name":"Woodcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"WDC", - "name":"Worldcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"WRP", - "name":"WRP", - "statuses":[ - "primary" - ] - }, - { - "code":"X2", - "name":"X2", - "statuses":[ - "primary" - ] - }, - { - "code":"X2C", - "name":"X2C", - "statuses":[ - "primary" - ] - }, - { - "code":"XAB", - "name":"XAB", - "statuses":[ - "primary" - ] - }, - { - "code":"XAUR", - "name":"XAUR", - "statuses":[ - "primary" - ] - }, - { - "code":"XAU", - "name":"Xaurum", - "statuses":[ - "primary" - ] - }, - { - "code":"XBS", - "name":"XBS", - "statuses":[ - "primary" - ] - }, - { - "code":"XBTS", - "name":"XBTS", - "statuses":[ - "primary" - ] - }, - { - "code":"XBU", - "name":"XBU", - "statuses":[ - "primary" - ] - }, - { - "code":"XCO", - "name":"XCO", - "statuses":[ - "primary" - ] - }, - { - "code":"XC", - "name":"XCurrency", - "statuses":[ - "primary" - ] - }, - { - "code":"XDB", - "name":"XDB", - "statuses":[ - "primary" - ] - }, - { - "code":"XDE2", - "name":"XDE2", - "statuses":[ - "primary" - ] - }, - { - "code":"MI", - "name":"Xiaomicoin", - "statuses":[ - "primary" - ] - }, - { - "code":"XID", - "name":"XID", - "statuses":[ - "primary" - ] - }, - { - "code":"XJO", - "name":"XJO", - "statuses":[ - "primary" - ] - }, - { - "code":"XLTCG", - "name":"XLTCG", - "statuses":[ - "primary" - ] - }, - { - "code":"XMINE", - "name":"XMINE", - "statuses":[ - "primary" - ] - }, - { - "code":"XMS", - "name":"XMS", - "statuses":[ - "primary" - ] - }, - { - "code":"XNG", - "name":"XNG", - "statuses":[ - "primary" - ] - }, - { - "code":"XODUS", - "name":"XODUS", - "statuses":[ - "primary" - ] - }, - { - "code":"XPC", - "name":"XPC", - "statuses":[ - "primary" - ] - }, - { - "code":"XPO", - "name":"XPO", - "statuses":[ - "primary" - ] - }, - { - "code":"XPOKE", - "name":"XPOKE", - "statuses":[ - "primary" - ] - }, - { - "code":"XPRO", - "name":"XPRO", - "statuses":[ - "primary" - ] - }, - { - "code":"XPTX", - "name":"XPTX", - "statuses":[ - "primary" - ] - }, - { - "code":"XQN", - "name":"XQN", - "statuses":[ - "primary" - ] - }, - { - "code":"XRC", - "name":"XRC", - "statuses":[ - "primary" - ] - }, - { - "code":"XSEED", - "name":"XSEED", - "statuses":[ - "primary" - ] - }, - { - "code":"XSY", - "name":"XSY", - "statuses":[ - "primary" - ] - }, - { - "code":"XTP", - "name":"XTP", - "statuses":[ - "primary" - ] - }, - { - "code":"XUP", - "name":"XUP", - "statuses":[ - "primary" - ] - }, - { - "code":"YAC", - "name":"Yacoin", - "statuses":[ - "primary" - ] - }, - { - "code":"YAY", - "name":"YAY", - "statuses":[ - "primary" - ] - }, - { - "code":"YBC", - "name":"Ybcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"YMC", - "name":"YMC", - "statuses":[ - "primary" - ] - }, - { - "code":"YOC", - "name":"YOC", - "statuses":[ - "primary" - ] - }, - { - "code":"YOVI", - "name":"YOVI", - "statuses":[ - "primary" - ] - }, - { - "code":"YUM", - "name":"YUM", - "statuses":[ - "primary" - ] - }, - { - "code":"ZEC", - "name":"Zcash", - "statuses":[ - "primary" - ] - }, - { - "code":"ZCL", - "name":"Zcash classic", - "statuses":[ - "primary" - ] - }, - { - "code":"ZCC", - "name":"ZCC", - "statuses":[ - "primary" - ] - }, - { - "code":"ZCOIN", - "name":"ZCOIN", - "statuses":[ - "primary" - ] - }, - { - "code":"XZC", - "name":"Zcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"ZECD", - "name":"ZECD", - "statuses":[ - "primary" - ] - }, - { - "code":"ZEIT", - "name":"Zeitcoin", - "statuses":[ - "primary" - ] - }, - { - "code":"ZET2", - "name":"ZET2", - "statuses":[ - "primary" - ] - }, - { - "code":"ZET", - "name":"Zetacoin", - "statuses":[ - "primary" - ] - }, - { - "code":"ZRC", - "name":"ZiftrCOIN", - "statuses":[ - "primary" - ] - }, - { - "code":"ZLQ", - "name":"ZLQ", - "statuses":[ - "primary" - ] - }, - { - "code":"ZMC", - "name":"ZMC", - "statuses":[ - "primary" - ] - }, - { - "code":"ZNE", - "name":"ZNE", - "statuses":[ - "primary" - ] - }, - { - "code":"ZNY", - "name":"ZNY", - "statuses":[ - "primary" - ] - }, - { - "code":"ZS", - "name":"ZS", - "statuses":[ - "primary" - ] - }, - { - "code":"ZUR", - "name":"ZUR", - "statuses":[ - "primary" - ] - }, - { - "code":"ZXT", - "name":"ZXT", - "statuses":[ - "primary" - ] - }, - { - "code":"ZYD", - "name":"ZYD", - "statuses":[ - "primary" - ] - } - ] -} \ No newline at end of file +{"rows":[{"code":"REP","name":"Augur","statuses":["primary"]},{"code":"BCN","name":"Bytecoin","statuses":["primary"]},{"code":"BTC","name":"Bitcoin","statuses":["primary","secondary"]},{"code":"BTS","name":"BitShares","statuses":["primary","secondary"]},{"code":"BLK","name":"Blackcoin","statuses":["primary"]},{"code":"GBP","name":"British Pound Sterling","statuses":["secondary"]},{"code":"CAD","name":"Canadian Dollar","statuses":["secondary"]},{"code":"CNY","name":"Chinese Yuan","statuses":["secondary"]},{"code":"DSH","name":"Dashcoin","statuses":["primary"]},{"code":"DOGE","name":"Dogecoin","statuses":["primary","secondary"]},{"code":"ETC","name":"Ethereum Classic","statuses":["primary"]},{"code":"EUR","name":"Euro","statuses":["primary","secondary"]},{"code":"GNO","name":"GNO","statuses":["primary"]},{"code":"GNT","name":"GNT","statuses":["primary"]},{"code":"JPY","name":"Japanese Yen","statuses":["secondary"]},{"code":"LTC","name":"Litecoin","statuses":["primary","secondary"]},{"code":"MAID","name":"MaidSafeCoin","statuses":["primary"]},{"code":"XEM","name":"NEM","statuses":["primary"]},{"code":"XLM","name":"Stellar","statuses":["primary"]},{"code":"XMR","name":"Monero","statuses":["primary","secondary"]},{"code":"XRP","name":"Ripple","statuses":["primary"]},{"code":"RUR","name":"Ruble","statuses":["secondary"]},{"code":"STEEM","name":"Steem","statuses":["primary"]},{"code":"STRAT","name":"STRAT","statuses":["primary"]},{"code":"UAH","name":"Ukrainian Hryvnia","statuses":["secondary"]},{"code":"USD","name":"US Dollar","statuses":["primary","secondary"]},{"code":"WAVES","name":"WAVES","statuses":["primary"]},{"code":"ZEC","name":"Zcash","statuses":["primary"]}]} -- cgit v1.2.3 From d389d0efd645966c251c49673009bbc113645068 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Thu, 11 May 2017 12:31:08 -0700 Subject: Bump changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 532abcca9..422639f70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Trim currency list. + ## 3.6.4 2017-5-8 - Fix main-net ENS resolution. -- cgit v1.2.3 From 6c01b26845cfb8e113d3016ebd595a9098623f62 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 11 May 2017 10:46:17 +0200 Subject: use asyncQ.waterfall instead of asyncQ.eachSeries --- app/scripts/lib/migrator/index.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/scripts/lib/migrator/index.js b/app/scripts/lib/migrator/index.js index caa0ef318..0bf88dbec 100644 --- a/app/scripts/lib/migrator/index.js +++ b/app/scripts/lib/migrator/index.js @@ -14,9 +14,15 @@ class Migrator { migrateData (versionedData = this.generateInitialState()) { const remaining = this.migrations.filter(migrationIsPending) if (remaining.length === 0) return versionedData + + const migrations = remaining.map((migration, i) => { + if (i === 0) return this.runMigration.bind(this, migration, versionedData) + return this.runMigration.bind(this, migration) + }) + return ( - asyncQ.eachSeries(remaining, (migration) => this.runMigration(versionedData, migration)) - .then((migratedData) => migratedData.pop()) + asyncQ.waterfall(migrations) + .then((migratedData) => Promise.resolve(migratedData)) ) // migration is "pending" if hit has a higher @@ -26,10 +32,10 @@ class Migrator { } } - runMigration (versionedData, migration) { + runMigration (migration, versionedData) { return migration.migrate(versionedData) .then((migratedData) => { - if (!migratedData.data) return Promise.reject(new Error('Migrator - Migration returned empty data')) + if (!migratedData.data) return Promise.reject(new Error('Migrator - migration returned empty data')) if (migration.version !== undefined && migratedData.meta.version !== migration.version) return Promise.reject(new Error('Migrator - Migration did not update version number correctly')) return Promise.resolve(migratedData) -- cgit v1.2.3 From 8421cf9ccea80e4958a1a70d068bbffa57577390 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 11 May 2017 19:47:58 +0200 Subject: Create test for Migrator --- test/unit/migrator-test.js | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 test/unit/migrator-test.js diff --git a/test/unit/migrator-test.js b/test/unit/migrator-test.js new file mode 100644 index 000000000..ece95b9f6 --- /dev/null +++ b/test/unit/migrator-test.js @@ -0,0 +1,41 @@ +const assert = require('assert') +const clone = require('clone') +const Migrator = require('../../app/scripts/lib/migrator/') +const migrations = [ + { + version: 1, + migrate: (data) => { + // clone the data just like we do in migrations + const clonedData = clone(data) + clonedData.meta.version = 1 + return Promise.resolve(clonedData) + }, + }, + { + version: 2, + migrate: (data) => { + const clonedData = clone(data) + clonedData.meta.version = 2 + return Promise.resolve(clonedData) + }, + }, + { + version: 3, + migrate: (data) => { + const clonedData = clone(data) + clonedData.meta.version = 3 + return Promise.resolve(clonedData) + }, + }, +] +const versionedData = {meta: {version: 0}, data:{hello:'world'}} +describe('Migrator', () => { + const migrator = new Migrator({ migrations }) + it('migratedData version should be version 3', (done) => { + migrator.migrateData(versionedData) + .then((migratedData) => { + assert.equal(migratedData.meta.version, migrations[2].version) + done() + }).catch(done) + }) +}) -- cgit v1.2.3 From 113f7d67f1973d1468b95517895701af8ca16f95 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 11 May 2017 14:29:44 -0700 Subject: Fix tests add logs --- test/unit/components/pending-tx-test.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/unit/components/pending-tx-test.js b/test/unit/components/pending-tx-test.js index e0f02a5bb..3e26fc6f5 100644 --- a/test/unit/components/pending-tx-test.js +++ b/test/unit/components/pending-tx-test.js @@ -1,7 +1,7 @@ var assert = require('assert') var PendingTx = require('../../../ui/app/components/pending-tx') -describe('PendingTx', function () { +describe.only('PendingTx', function () { let pendingTxComponent const identities = { @@ -44,17 +44,21 @@ describe('PendingTx', function () { const noop = () => {} - pendingTxComponent.componentDidMount = () => { + setTimeout(() => { + console.log('component mounted') const newGasPrice = '0x451456' pendingTxComponent.gasPriceChanged(newGasPrice) setTimeout(() => { + console.log('hitting submit') pendingTxComponent.onSubmit({ preventDefault: noop }) }, 20) - } + }, 200) + console.log('calling render') pendingTxComponent.props = props + pendingTxComponent.checkValidity = () => { return true } pendingTxComponent.render() }) -- cgit v1.2.3 From 16005ebd3a7db5c48f9d81d5a1c77ace7ff92958 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 11 May 2017 15:28:33 -0700 Subject: Got test failing --- test/unit/components/pending-tx-test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/unit/components/pending-tx-test.js b/test/unit/components/pending-tx-test.js index 3e26fc6f5..b798865cc 100644 --- a/test/unit/components/pending-tx-test.js +++ b/test/unit/components/pending-tx-test.js @@ -30,12 +30,15 @@ describe.only('PendingTx', function () { it('should use updated values when edited.', function (done) { + const newGasPrice = '0x451456' + const props = { identities, accounts: identities, txData, sendTransaction: (txMeta, event) => { assert.notEqual(txMeta.txParams.gasPrice, gasPrice, 'gas price should change') + assert.equal(txMeta.txParams.gasPrice, newGasPrice, 'gas price assigned.') done() }, } @@ -47,7 +50,6 @@ describe.only('PendingTx', function () { setTimeout(() => { console.log('component mounted') - const newGasPrice = '0x451456' pendingTxComponent.gasPriceChanged(newGasPrice) setTimeout(() => { -- cgit v1.2.3 From 974368fe0849e58dc24398ee814d24fd799dff3b Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Thu, 11 May 2017 16:28:11 -0700 Subject: Prettify JSON --- ui/app/conversion.json | 208 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 207 insertions(+), 1 deletion(-) diff --git a/ui/app/conversion.json b/ui/app/conversion.json index e0c033a76..155ffc4fc 100644 --- a/ui/app/conversion.json +++ b/ui/app/conversion.json @@ -1 +1,207 @@ -{"rows":[{"code":"REP","name":"Augur","statuses":["primary"]},{"code":"BCN","name":"Bytecoin","statuses":["primary"]},{"code":"BTC","name":"Bitcoin","statuses":["primary","secondary"]},{"code":"BTS","name":"BitShares","statuses":["primary","secondary"]},{"code":"BLK","name":"Blackcoin","statuses":["primary"]},{"code":"GBP","name":"British Pound Sterling","statuses":["secondary"]},{"code":"CAD","name":"Canadian Dollar","statuses":["secondary"]},{"code":"CNY","name":"Chinese Yuan","statuses":["secondary"]},{"code":"DSH","name":"Dashcoin","statuses":["primary"]},{"code":"DOGE","name":"Dogecoin","statuses":["primary","secondary"]},{"code":"ETC","name":"Ethereum Classic","statuses":["primary"]},{"code":"EUR","name":"Euro","statuses":["primary","secondary"]},{"code":"GNO","name":"GNO","statuses":["primary"]},{"code":"GNT","name":"GNT","statuses":["primary"]},{"code":"JPY","name":"Japanese Yen","statuses":["secondary"]},{"code":"LTC","name":"Litecoin","statuses":["primary","secondary"]},{"code":"MAID","name":"MaidSafeCoin","statuses":["primary"]},{"code":"XEM","name":"NEM","statuses":["primary"]},{"code":"XLM","name":"Stellar","statuses":["primary"]},{"code":"XMR","name":"Monero","statuses":["primary","secondary"]},{"code":"XRP","name":"Ripple","statuses":["primary"]},{"code":"RUR","name":"Ruble","statuses":["secondary"]},{"code":"STEEM","name":"Steem","statuses":["primary"]},{"code":"STRAT","name":"STRAT","statuses":["primary"]},{"code":"UAH","name":"Ukrainian Hryvnia","statuses":["secondary"]},{"code":"USD","name":"US Dollar","statuses":["primary","secondary"]},{"code":"WAVES","name":"WAVES","statuses":["primary"]},{"code":"ZEC","name":"Zcash","statuses":["primary"]}]} +{ + "rows": [ + { + "code": "REP", + "name": "Augur", + "statuses": [ + "primary" + ] + }, + { + "code": "BCN", + "name": "Bytecoin", + "statuses": [ + "primary" + ] + }, + { + "code": "BTC", + "name": "Bitcoin", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "BTS", + "name": "BitShares", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "BLK", + "name": "Blackcoin", + "statuses": [ + "primary" + ] + }, + { + "code": "GBP", + "name": "British Pound Sterling", + "statuses": [ + "secondary" + ] + }, + { + "code": "CAD", + "name": "Canadian Dollar", + "statuses": [ + "secondary" + ] + }, + { + "code": "CNY", + "name": "Chinese Yuan", + "statuses": [ + "secondary" + ] + }, + { + "code": "DSH", + "name": "Dashcoin", + "statuses": [ + "primary" + ] + }, + { + "code": "DOGE", + "name": "Dogecoin", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "ETC", + "name": "Ethereum Classic", + "statuses": [ + "primary" + ] + }, + { + "code": "EUR", + "name": "Euro", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "GNO", + "name": "GNO", + "statuses": [ + "primary" + ] + }, + { + "code": "GNT", + "name": "GNT", + "statuses": [ + "primary" + ] + }, + { + "code": "JPY", + "name": "Japanese Yen", + "statuses": [ + "secondary" + ] + }, + { + "code": "LTC", + "name": "Litecoin", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "MAID", + "name": "MaidSafeCoin", + "statuses": [ + "primary" + ] + }, + { + "code": "XEM", + "name": "NEM", + "statuses": [ + "primary" + ] + }, + { + "code": "XLM", + "name": "Stellar", + "statuses": [ + "primary" + ] + }, + { + "code": "XMR", + "name": "Monero", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "XRP", + "name": "Ripple", + "statuses": [ + "primary" + ] + }, + { + "code": "RUR", + "name": "Ruble", + "statuses": [ + "secondary" + ] + }, + { + "code": "STEEM", + "name": "Steem", + "statuses": [ + "primary" + ] + }, + { + "code": "STRAT", + "name": "STRAT", + "statuses": [ + "primary" + ] + }, + { + "code": "UAH", + "name": "Ukrainian Hryvnia", + "statuses": [ + "secondary" + ] + }, + { + "code": "USD", + "name": "US Dollar", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "WAVES", + "name": "WAVES", + "statuses": [ + "primary" + ] + }, + { + "code": "ZEC", + "name": "Zcash", + "statuses": [ + "primary" + ] + } + ] +} -- cgit v1.2.3 From 60746a985997693612af0c8b43aac95b2a6e56e6 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 11 May 2017 17:09:23 -0700 Subject: Use react test utils to start composing test --- package.json | 4 ++++ test/unit/components/pending-tx-test.js | 25 +++++++++++++++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index e2268d20a..5f2436112 100644 --- a/package.json +++ b/package.json @@ -132,9 +132,11 @@ "browserify": "^13.0.0", "chai": "^3.5.0", "clone": "^1.0.2", + "create-react-factory": "^0.2.1", "deep-freeze-strict": "^1.1.1", "del": "^2.2.0", "envify": "^4.0.0", + "enzyme": "^2.8.2", "eslint-plugin-chai": "0.0.1", "eslint-plugin-mocha": "^4.9.0", "fs-promise": "^1.0.0", @@ -161,6 +163,8 @@ "prompt": "^1.0.0", "qs": "^6.2.0", "qunit": "^0.9.1", + "react-addons-test-utils": "^15.5.1", + "react-dom": "^15.5.4", "sinon": "^1.17.3", "tape": "^4.5.1", "testem": "^1.10.3", diff --git a/test/unit/components/pending-tx-test.js b/test/unit/components/pending-tx-test.js index b798865cc..caaf66b49 100644 --- a/test/unit/components/pending-tx-test.js +++ b/test/unit/components/pending-tx-test.js @@ -1,5 +1,13 @@ var assert = require('assert') +const h = require('react-hyperscript') var PendingTx = require('../../../ui/app/components/pending-tx') +const createReactFactory = require('create-react-factory').createReactFactory +const React = require('react') +console.dir(createReactFactory) +const shallow = require('enzyme').shallow +const Factory = createReactFactory(PendingTx) +const ReactTestUtils = require('react-addons-test-utils') +const renderer = ReactTestUtils.createRenderer(); describe.only('PendingTx', function () { let pendingTxComponent @@ -43,14 +51,21 @@ describe.only('PendingTx', function () { }, } - pendingTxComponent = new PendingTx(props) + const pendingTxComponent = h(PendingTx, props) + renderer.render(pendingTxComponent) + console.dir(pendingTxComponent) const noop = () => {} setTimeout(() => { - console.log('component mounted') + console.log('timeout finished') - pendingTxComponent.gasPriceChanged(newGasPrice) + // Get the gas price input + // Set it to the newGasPrice value + // Wait for the value to change + // Get the submit button + // Click the submit button + // Get the output of the submit event. setTimeout(() => { console.log('hitting submit') @@ -59,9 +74,7 @@ describe.only('PendingTx', function () { }, 200) console.log('calling render') - pendingTxComponent.props = props - pendingTxComponent.checkValidity = () => { return true } - pendingTxComponent.render() }) }) + -- cgit v1.2.3 From de5cf2526ca3b3791545afc32d90a3bb88a8b8e3 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 11 May 2017 17:15:45 -0700 Subject: Fix test up a bit --- test/unit/components/pending-tx-test.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/unit/components/pending-tx-test.js b/test/unit/components/pending-tx-test.js index caaf66b49..5ddea7b23 100644 --- a/test/unit/components/pending-tx-test.js +++ b/test/unit/components/pending-tx-test.js @@ -7,7 +7,6 @@ console.dir(createReactFactory) const shallow = require('enzyme').shallow const Factory = createReactFactory(PendingTx) const ReactTestUtils = require('react-addons-test-utils') -const renderer = ReactTestUtils.createRenderer(); describe.only('PendingTx', function () { let pendingTxComponent @@ -38,6 +37,7 @@ describe.only('PendingTx', function () { it('should use updated values when edited.', function (done) { + const renderer = ReactTestUtils.createRenderer(); const newGasPrice = '0x451456' const props = { @@ -53,7 +53,9 @@ describe.only('PendingTx', function () { const pendingTxComponent = h(PendingTx, props) renderer.render(pendingTxComponent) - console.dir(pendingTxComponent) + const result = renderer.getRenderOutput() + assert.equal(result.type, 'div', 'should create a div') + console.dir(result) const noop = () => {} @@ -67,10 +69,6 @@ describe.only('PendingTx', function () { // Click the submit button // Get the output of the submit event. - setTimeout(() => { - console.log('hitting submit') - pendingTxComponent.onSubmit({ preventDefault: noop }) - }, 20) }, 200) console.log('calling render') -- cgit v1.2.3 From f0eeb1e1620b9c607390505c2e443979c9b44b1a Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 11 May 2017 17:43:40 -0700 Subject: Got a useful error message for next step --- package.json | 1 + test/unit/components/pending-tx-test.js | 24 ++++++++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5f2436112..11931b32f 100644 --- a/package.json +++ b/package.json @@ -165,6 +165,7 @@ "qunit": "^0.9.1", "react-addons-test-utils": "^15.5.1", "react-dom": "^15.5.4", + "react-testutils-additions": "^15.2.0", "sinon": "^1.17.3", "tape": "^4.5.1", "testem": "^1.10.3", diff --git a/test/unit/components/pending-tx-test.js b/test/unit/components/pending-tx-test.js index 5ddea7b23..2594a1a26 100644 --- a/test/unit/components/pending-tx-test.js +++ b/test/unit/components/pending-tx-test.js @@ -1,4 +1,5 @@ -var assert = require('assert') +const assert = require('assert') +const additions = require('react-testutils-additions') const h = require('react-hyperscript') var PendingTx = require('../../../ui/app/components/pending-tx') const createReactFactory = require('create-react-factory').createReactFactory @@ -52,10 +53,28 @@ describe.only('PendingTx', function () { } const pendingTxComponent = h(PendingTx, props) + var component = additions.renderIntoDocument(pendingTxComponent); renderer.render(pendingTxComponent) const result = renderer.getRenderOutput() + const form = result.props.children + console.log('FORM children') + console.dir(form.props.children) + const children = form.props.children[form.props.children.length - 1] assert.equal(result.type, 'div', 'should create a div') - console.dir(result) + console.dir(children) + + console.log('finding input') + + try{ + + const input = additions.find(component, '.cell.row input[type="number"]') + console.log('input') + console.dir(input) + + } catch (e) { + console.log("WHAAAA") + console.error(e) + } const noop = () => {} @@ -68,6 +87,7 @@ describe.only('PendingTx', function () { // Get the submit button // Click the submit button // Get the output of the submit event. + // Assert that the value was updated. }, 200) -- cgit v1.2.3 From 70a328e028230f8bffbd88a28d961fde6a4b819f Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 11 May 2017 18:15:59 -0700 Subject: migrator - cleaner migration runner with es7 --- app/scripts/lib/migrator/index.js | 38 +++++++++++++------------------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/app/scripts/lib/migrator/index.js b/app/scripts/lib/migrator/index.js index 0bf88dbec..ed07a0c60 100644 --- a/app/scripts/lib/migrator/index.js +++ b/app/scripts/lib/migrator/index.js @@ -1,47 +1,35 @@ -const asyncQ = require('async-q') - class Migrator { constructor (opts = {}) { const migrations = opts.migrations || [] + // sort migrations by version this.migrations = migrations.sort((a, b) => a.version - b.version) + // grab migration with highest version const lastMigration = this.migrations.slice(-1)[0] // use specified defaultVersion or highest migration version this.defaultVersion = opts.defaultVersion || (lastMigration && lastMigration.version) || 0 } // run all pending migrations on meta in place - migrateData (versionedData = this.generateInitialState()) { - const remaining = this.migrations.filter(migrationIsPending) - if (remaining.length === 0) return versionedData - - const migrations = remaining.map((migration, i) => { - if (i === 0) return this.runMigration.bind(this, migration, versionedData) - return this.runMigration.bind(this, migration) - }) + async migrateData (versionedData = this.generateInitialState()) { + const pendingMigrations = this.migrations.filter(migrationIsPending) + + for (let index in pendingMigrations) { + let migration = pendingMigrations[index] + versionedData = await migration.migrate(versionedData) + if (!versionedData.data) throw new Error('Migrator - migration returned empty data') + if (versionedData.version !== undefined && migratedData.meta.version !== migration.version) throw new Error('Migrator - Migration did not update version number correctly') + } - return ( - asyncQ.waterfall(migrations) - .then((migratedData) => Promise.resolve(migratedData)) - ) + return versionedData - // migration is "pending" if hit has a higher + // migration is "pending" if it has a higher // version number than currentVersion function migrationIsPending (migration) { return migration.version > versionedData.meta.version } } - runMigration (migration, versionedData) { - return migration.migrate(versionedData) - .then((migratedData) => { - if (!migratedData.data) return Promise.reject(new Error('Migrator - migration returned empty data')) - if (migration.version !== undefined && migratedData.meta.version !== migration.version) return Promise.reject(new Error('Migrator - Migration did not update version number correctly')) - - return Promise.resolve(migratedData) - }) - } - generateInitialState (initState) { return { meta: { -- cgit v1.2.3 From daec667c16c9a55e6357871a82ff2b863501a393 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 12 May 2017 11:31:40 -0700 Subject: Add support for async/await --- .babelrc | 5 ++++- .eslintrc | 2 +- package.json | 6 +++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.babelrc b/.babelrc index 9d8d51656..3ca197980 100644 --- a/.babelrc +++ b/.babelrc @@ -1 +1,4 @@ -{ "presets": ["es2015"] } +{ + "presets": ["es2015"], + "plugins": ["transform-runtime"] +} diff --git a/.eslintrc b/.eslintrc index 91c95874e..a57a93cdc 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,7 +1,7 @@ { "parserOptions": { "sourceType": "module", - "ecmaVersion": 6, + "ecmaVersion": 2017, "ecmaFeatures": { "experimentalObjectRestSpread": true, "impliedStrict": true, diff --git a/package.json b/package.json index 11931b32f..2f5f8434b 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "babelify", { "presets": [ - "es2015" + "es2015", + "stage-3" ] } ], @@ -45,6 +46,7 @@ "dependencies": { "async": "^1.5.2", "async-q": "^0.3.1", + "babel-runtime": "^6.23.0", "bip39": "^2.2.0", "bluebird": "^3.5.0", "browser-passworder": "^2.0.3", @@ -125,6 +127,8 @@ }, "devDependencies": { "babel-eslint": "^6.0.5", + "babel-plugin-transform-runtime": "^6.23.0", + "babel-preset-stage-3": "^6.24.1", "babel-register": "^6.7.2", "babelify": "^7.2.0", "beefy": "^2.1.5", -- cgit v1.2.3 From 61f5c42a4508aa13f41f5ac85bd13bc53dfa7b2e Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 12 May 2017 11:31:40 -0700 Subject: Add support for async/await --- .babelrc | 5 ++++- .eslintrc | 2 +- package.json | 6 +++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.babelrc b/.babelrc index 9d8d51656..3ca197980 100644 --- a/.babelrc +++ b/.babelrc @@ -1 +1,4 @@ -{ "presets": ["es2015"] } +{ + "presets": ["es2015"], + "plugins": ["transform-runtime"] +} diff --git a/.eslintrc b/.eslintrc index 91c95874e..a57a93cdc 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,7 +1,7 @@ { "parserOptions": { "sourceType": "module", - "ecmaVersion": 6, + "ecmaVersion": 2017, "ecmaFeatures": { "experimentalObjectRestSpread": true, "impliedStrict": true, diff --git a/package.json b/package.json index e2268d20a..f4bdbd998 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "babelify", { "presets": [ - "es2015" + "es2015", + "stage-3" ] } ], @@ -45,6 +46,7 @@ "dependencies": { "async": "^1.5.2", "async-q": "^0.3.1", + "babel-runtime": "^6.23.0", "bip39": "^2.2.0", "bluebird": "^3.5.0", "browser-passworder": "^2.0.3", @@ -125,6 +127,8 @@ }, "devDependencies": { "babel-eslint": "^6.0.5", + "babel-plugin-transform-runtime": "^6.23.0", + "babel-preset-stage-3": "^6.24.1", "babel-register": "^6.7.2", "babelify": "^7.2.0", "beefy": "^2.1.5", -- cgit v1.2.3 From 2c8bbe3b25c726b8a0cebb572c3c9d962136a693 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 12 May 2017 12:27:40 -0700 Subject: migrator - fix typo --- app/scripts/lib/migrator/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/lib/migrator/index.js b/app/scripts/lib/migrator/index.js index ed07a0c60..de6f5d5cd 100644 --- a/app/scripts/lib/migrator/index.js +++ b/app/scripts/lib/migrator/index.js @@ -18,7 +18,7 @@ class Migrator { let migration = pendingMigrations[index] versionedData = await migration.migrate(versionedData) if (!versionedData.data) throw new Error('Migrator - migration returned empty data') - if (versionedData.version !== undefined && migratedData.meta.version !== migration.version) throw new Error('Migrator - Migration did not update version number correctly') + if (versionedData.version !== undefined && versionedData.meta.version !== migration.version) throw new Error('Migrator - Migration did not update version number correctly') } return versionedData -- cgit v1.2.3 From dde3beb0446a64ab9348c75386c2e7ab0641d828 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 12 May 2017 12:37:31 -0700 Subject: ci - use node 7.6.0 --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index c9ea787ff..4305ca3b4 100644 --- a/circle.yml +++ b/circle.yml @@ -1,6 +1,6 @@ machine: node: - version: 6.0.0 + version: 7.6.0 dependencies: pre: - "npm i -g testem" -- cgit v1.2.3 From 19db11856bef65c38d96eb1d6084d88ab6e7eebc Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 12 May 2017 12:41:31 -0700 Subject: Remove redux dependency from eth-balance and its dependent tree For better unit testability of the conf-tx view. --- ui/app/account-detail.js | 8 ++++++-- ui/app/accounts/account-list-item.js | 3 ++- ui/app/accounts/index.js | 4 +++- ui/app/components/eth-balance.js | 16 ++++++++-------- ui/app/components/fiat-value.js | 16 +++++----------- ui/app/components/pending-tx.js | 2 ++ ui/app/components/shift-list-item.js | 6 +++++- ui/app/components/transaction-list-item.js | 3 ++- ui/app/components/transaction-list.js | 3 ++- ui/app/conf-tx.js | 4 +++- ui/app/send.js | 19 +++++++++++-------- 11 files changed, 49 insertions(+), 35 deletions(-) diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index d592a5ad6..7cadb9d47 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -29,6 +29,7 @@ function mapStateToProps (state) { unapprovedMsgs: valuesFor(state.metamask.unapprovedMsgs), shapeShiftTxList: state.metamask.shapeShiftTxList, transactions: state.metamask.selectedAddressTxList || [], + conversionRate: state.metamask.conversionRate, } } @@ -43,7 +44,7 @@ AccountDetailScreen.prototype.render = function () { var checksumAddress = selected && ethUtil.toChecksumAddress(selected) var identity = props.identities[selected] var account = props.accounts[selected] - const { network } = props + const { network, conversionRate } = props return ( @@ -182,6 +183,7 @@ AccountDetailScreen.prototype.render = function () { h(EthBalance, { value: account && account.balance, + conversionRate, style: { lineHeight: '7px', marginTop: '10px', @@ -243,11 +245,13 @@ AccountDetailScreen.prototype.subview = function () { } AccountDetailScreen.prototype.transactionList = function () { - const {transactions, unapprovedMsgs, address, network, shapeShiftTxList } = this.props + const {transactions, unapprovedMsgs, address, + network, shapeShiftTxList, conversionRate } = this.props return h(TransactionList, { transactions: transactions.sort((a, b) => b.time - a.time), network, unapprovedMsgs, + conversionRate, address, shapeShiftTxList, viewPendingTx: (txId) => { diff --git a/ui/app/accounts/account-list-item.js b/ui/app/accounts/account-list-item.js index 2a3c13d05..0e87af612 100644 --- a/ui/app/accounts/account-list-item.js +++ b/ui/app/accounts/account-list-item.js @@ -15,7 +15,7 @@ function AccountListItem () { } AccountListItem.prototype.render = function () { - const { identity, selectedAddress, accounts, onShowDetail } = this.props + const { identity, selectedAddress, accounts, onShowDetail, conversionRate } = this.props const checksumAddress = identity && identity.address && ethUtil.toChecksumAddress(identity.address) const isSelected = selectedAddress === identity.address @@ -52,6 +52,7 @@ AccountListItem.prototype.render = function () { }, checksumAddress), h(EthBalance, { value: account && account.balance, + conversionRate, style: { lineHeight: '7px', marginTop: '10px', diff --git a/ui/app/accounts/index.js b/ui/app/accounts/index.js index 9584ebad9..5105c214b 100644 --- a/ui/app/accounts/index.js +++ b/ui/app/accounts/index.js @@ -23,6 +23,7 @@ function mapStateToProps (state) { scrollToBottom: state.appState.scrollToBottom, pending, keyrings: state.metamask.keyrings, + conversionRate: state.metamask.conversionRate, } } @@ -33,7 +34,7 @@ function AccountsScreen () { AccountsScreen.prototype.render = function () { const props = this.props - const { keyrings } = props + const { keyrings, conversionRate } = props const identityList = valuesFor(props.identities) const unapprovedTxList = valuesFor(props.unapprovedTxs) @@ -81,6 +82,7 @@ AccountsScreen.prototype.render = function () { key: `acct-panel-${identity.address}`, identity, selectedAddress: this.props.selectedAddress, + conversionRate, accounts: this.props.accounts, onShowDetail: this.onShowDetail.bind(this), pending, diff --git a/ui/app/components/eth-balance.js b/ui/app/components/eth-balance.js index 57ca84564..21906aa09 100644 --- a/ui/app/components/eth-balance.js +++ b/ui/app/components/eth-balance.js @@ -16,20 +16,19 @@ function EthBalanceComponent () { EthBalanceComponent.prototype.render = function () { var props = this.props let { value } = props - var style = props.style + const { style, width } = props var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true value = value ? formatBalance(value, 6, needsParse) : '...' - var width = props.width return ( h('.ether-balance.ether-balance-amount', { - style: style, + style, }, [ h('div', { style: { display: 'inline', - width: width, + width, }, }, this.renderBalance(value)), ]) @@ -38,16 +37,17 @@ EthBalanceComponent.prototype.render = function () { } EthBalanceComponent.prototype.renderBalance = function (value) { var props = this.props + const { conversionRate, shorten, incoming } = props if (value === 'None') return value if (value === '...') return value - var balanceObj = generateBalanceObject(value, props.shorten ? 1 : 3) + var balanceObj = generateBalanceObject(value, shorten ? 1 : 3) var balance var splitBalance = value.split(' ') var ethNumber = splitBalance[0] var ethSuffix = splitBalance[1] const showFiat = 'showFiat' in props ? props.showFiat : true - if (props.shorten) { + if (shorten) { balance = balanceObj.shortBalance } else { balance = balanceObj.balance @@ -73,7 +73,7 @@ EthBalanceComponent.prototype.renderBalance = function (value) { width: '100%', textAlign: 'right', }, - }, this.props.incoming ? `+${balance}` : balance), + }, incoming ? `+${balance}` : balance), h('div', { style: { color: ' #AEAEAE', @@ -83,7 +83,7 @@ EthBalanceComponent.prototype.renderBalance = function (value) { }, label), ]), - showFiat ? h(FiatValue, { value: props.value }) : null, + showFiat ? h(FiatValue, { value, conversionRate }) : null, ])) ) } diff --git a/ui/app/components/fiat-value.js b/ui/app/components/fiat-value.js index 298809b30..6e306c9e6 100644 --- a/ui/app/components/fiat-value.js +++ b/ui/app/components/fiat-value.js @@ -1,17 +1,9 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits -const connect = require('react-redux').connect const formatBalance = require('../util').formatBalance -module.exports = connect(mapStateToProps)(FiatValue) - -function mapStateToProps (state) { - return { - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - } -} +module.exports = FiatValue inherits(FiatValue, Component) function FiatValue () { @@ -20,14 +12,16 @@ function FiatValue () { FiatValue.prototype.render = function () { const props = this.props + const { conversionRate } = props + const value = formatBalance(props.value, 6) if (value === 'None') return value var fiatDisplayNumber, fiatTooltipNumber var splitBalance = value.split(' ') - if (props.conversionRate !== 0) { - fiatTooltipNumber = Number(splitBalance[0]) * props.conversionRate + if (conversionRate !== 0) { + fiatTooltipNumber = Number(splitBalance[0]) * conversionRate fiatDisplayNumber = fiatTooltipNumber.toFixed(2) } else { fiatDisplayNumber = 'N/A' diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 6b8f16dae..fb555b821 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -31,6 +31,7 @@ function PendingTx () { PendingTx.prototype.render = function () { const props = this.props + const conversionRate = props.conversionRate const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} @@ -102,6 +103,7 @@ PendingTx.prototype.render = function () { }, [ h(EthBalance, { value: balance, + conversionRate, inline: true, labelColor: '#F7861C', }), diff --git a/ui/app/components/shift-list-item.js b/ui/app/components/shift-list-item.js index 96a7cba6e..db5fda5cb 100644 --- a/ui/app/components/shift-list-item.js +++ b/ui/app/components/shift-list-item.js @@ -15,7 +15,9 @@ const Tooltip = require('./tooltip') module.exports = connect(mapStateToProps)(ShiftListItem) function mapStateToProps (state) { - return {} + return { + conversionRate: state.metamask.conversionRate, + } } inherits(ShiftListItem, Component) @@ -64,6 +66,7 @@ function formatDate (date) { ShiftListItem.prototype.renderUtilComponents = function () { var props = this.props + const { conversionRate } = props switch (props.response.status) { case 'no_deposits': @@ -96,6 +99,7 @@ ShiftListItem.prototype.renderUtilComponents = function () { }), h(EtherBalance, { value: `${props.response.outgoingCoin}`, + conversionRate, width: '55px', shorten: true, needsParse: false, diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js index 7fb2e88d9..3db4c016e 100644 --- a/ui/app/components/transaction-list-item.js +++ b/ui/app/components/transaction-list-item.js @@ -19,7 +19,7 @@ function TransactionListItem () { } TransactionListItem.prototype.render = function () { - const { transaction, network } = this.props + const { transaction, network, conversionRate } = this.props if (transaction.key === 'shapeshift') { if (network === '1') return h(ShiftListItem, transaction) } @@ -80,6 +80,7 @@ TransactionListItem.prototype.render = function () { isTx ? h(EtherBalance, { value: txParams.value, + conversionRate, width: '55px', shorten: true, showFiat: false, diff --git a/ui/app/components/transaction-list.js b/ui/app/components/transaction-list.js index 3ae953637..37a757309 100644 --- a/ui/app/components/transaction-list.js +++ b/ui/app/components/transaction-list.js @@ -13,7 +13,7 @@ function TransactionList () { } TransactionList.prototype.render = function () { - const { transactions, network, unapprovedMsgs } = this.props + const { transactions, network, unapprovedMsgs, conversionRate } = this.props var shapeShiftTxList if (network === '1') { @@ -69,6 +69,7 @@ TransactionList.prototype.render = function () { } return h(TransactionListItem, { transaction, i, network, key, + conversionRate, showTx: (txId) => { this.props.viewPendingTx(txId) }, diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 83ac5a4fd..c4df66931 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -27,6 +27,7 @@ function mapStateToProps (state) { warning: state.appState.warning, network: state.metamask.network, provider: state.metamask.provider, + conversionRate: state.metamask.conversionRate, } } @@ -38,7 +39,7 @@ function ConfirmTxScreen () { ConfirmTxScreen.prototype.render = function () { const props = this.props const { network, provider, unapprovedTxs, - unapprovedMsgs, unapprovedPersonalMsgs } = props + unapprovedMsgs, unapprovedPersonalMsgs, conversionRate } = props var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) @@ -102,6 +103,7 @@ ConfirmTxScreen.prototype.render = function () { selectedAddress: props.selectedAddress, accounts: props.accounts, identities: props.identities, + conversionRate, // Actions buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), sendTransaction: this.sendTransaction.bind(this, txData), diff --git a/ui/app/send.js b/ui/app/send.js index eb32d5e06..d73744f70 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -21,6 +21,7 @@ function mapStateToProps (state) { warning: state.appState.warning, network: state.metamask.network, addressBook: state.metamask.addressBook, + conversionRate: state.metamask.conversionRate, } result.error = result.warning && result.warning.split('.')[0] @@ -40,13 +41,14 @@ function SendTransactionScreen () { SendTransactionScreen.prototype.render = function () { this.persistentFormParentId = 'send-tx-form' - var state = this.props - var address = state.address - var account = state.account - var identity = state.identity - var network = state.network - var identities = state.identities - var addressBook = state.addressBook + var props = this.props + var address = props.address + var account = props.account + var identity = props.identity + var network = props.network + var identities = props.identities + var addressBook = props.addressBook + var conversionRate = props.conversionRate return ( @@ -125,6 +127,7 @@ SendTransactionScreen.prototype.render = function () { h(EthBalance, { value: account && account.balance, + conversionRate, }), ]), @@ -147,7 +150,7 @@ SendTransactionScreen.prototype.render = function () { ]), // error message - state.error && h('span.error.flex-center', state.error), + props.error && h('span.error.flex-center', props.error), // 'to' field h('section.flex-row.flex-center', [ -- cgit v1.2.3 From 5c9449dec1a4662ebb3e1ce18ede018a9e874c39 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 12 May 2017 13:09:23 -0700 Subject: background - drop async-q in favor of async/await --- app/scripts/background.js | 38 ++++++++++++++++---------------------- package.json | 1 - 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index 58f8e7556..e738a9712 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -1,6 +1,5 @@ const urlUtil = require('url') const endOfStream = require('end-of-stream') -const asyncQ = require('async-q') const pipe = require('pump') const LocalStorageStore = require('obs-store/lib/localStorage') const storeTransform = require('obs-store/lib/transform') @@ -30,34 +29,29 @@ let popupIsOpen = false const diskStore = new LocalStorageStore({ storageKey: STORAGE_KEY }) // initialization flow -asyncQ.waterfall([ - () => loadStateFromPersistence(), - (initState) => setupController(initState), -]) -.then(() => console.log('MetaMask initialization complete.')) -.catch((err) => { console.error(err) }) +initialize().catch(console.error) + +async function initialize() { + const initState = await loadStateFromPersistence() + await setupController(initState) + console.log('MetaMask initialization complete.') +} // // State and Persistence // -function loadStateFromPersistence () { +async function loadStateFromPersistence () { // migrations const migrator = new Migrator({ migrations }) - const initialState = migrator.generateInitialState(firstTimeState) - return asyncQ.waterfall([ - // read from disk - () => Promise.resolve(diskStore.getState() || initialState), - // migrate data - (versionedData) => migrator.migrateData(versionedData), - // write to disk - (versionedData) => { - diskStore.putState(versionedData) - return Promise.resolve(versionedData) - }, - // resolve to just data - (versionedData) => Promise.resolve(versionedData.data), - ]) + // read from disk + let versionedData = diskStore.getState() || migrator.generateInitialState(firstTimeState) + // migrate data + versionedData = await migrator.migrateData(versionedData) + // write to disk + diskStore.putState(versionedData) + // return just the data + return versionedData.data } function setupController (initState) { diff --git a/package.json b/package.json index f4bdbd998..423821fbe 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ }, "dependencies": { "async": "^1.5.2", - "async-q": "^0.3.1", "babel-runtime": "^6.23.0", "bip39": "^2.2.0", "bluebird": "^3.5.0", -- cgit v1.2.3 From c4be4c7195c05936fba61783b9f8a3c96d161ce0 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 15 May 2017 14:35:24 -0700 Subject: Skip jazzicons in unit tests --- package.json | 2 ++ ui/app/components/identicon.js | 13 +++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 2f5f8434b..9c0a91e3b 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "debounce": "^1.0.0", "deep-extend": "^0.4.1", "denodeify": "^1.2.1", + "detect-node": "^2.0.3", "disc": "^1.3.2", "dnode": "^1.2.2", "end-of-stream": "^1.1.0", @@ -169,6 +170,7 @@ "qunit": "^0.9.1", "react-addons-test-utils": "^15.5.1", "react-dom": "^15.5.4", + "react-test-renderer": "^15.5.4", "react-testutils-additions": "^15.2.0", "sinon": "^1.17.3", "tape": "^4.5.1", diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js index 6d4871d02..9de854b54 100644 --- a/ui/app/components/identicon.js +++ b/ui/app/components/identicon.js @@ -1,6 +1,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits +const isNode = require('detect-node') const findDOMNode = require('react-dom').findDOMNode const jazzicon = require('jazzicon') const iconFactoryGen = require('../../lib/icon-factory') @@ -40,8 +41,10 @@ IdenticonComponent.prototype.componentDidMount = function () { var container = findDOMNode(this) var diameter = props.diameter || this.defaultDiameter - var img = iconFactory.iconForAddress(address, diameter, false) - container.appendChild(img) + if (!isNode) { + var img = iconFactory.iconForAddress(address, diameter, false) + container.appendChild(img) + } } IdenticonComponent.prototype.componentDidUpdate = function () { @@ -58,6 +61,8 @@ IdenticonComponent.prototype.componentDidUpdate = function () { } var diameter = props.diameter || this.defaultDiameter - var img = iconFactory.iconForAddress(address, diameter, false) - container.appendChild(img) + if (!isNode) { + var img = iconFactory.iconForAddress(address, diameter, false) + container.appendChild(img) + } } -- cgit v1.2.3 From 4b341e6a955d1fa71decfb021a86e7da09a933b0 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 15 May 2017 15:07:38 -0700 Subject: Got test failing nearly correctly --- test/lib/mock-store.js | 18 ++++++++++++++++++ test/unit/components/pending-tx-test.js | 29 +++++++++++++++-------------- ui/app/components/pending-tx.js | 13 ++++++++++++- 3 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 test/lib/mock-store.js diff --git a/test/lib/mock-store.js b/test/lib/mock-store.js new file mode 100644 index 000000000..4714c3485 --- /dev/null +++ b/test/lib/mock-store.js @@ -0,0 +1,18 @@ +const createStore = require('redux').createStore +const applyMiddleware = require('redux').applyMiddleware +const thunkMiddleware = require('redux-thunk') +const createLogger = require('redux-logger') +const rootReducer = function() {} + +module.exports = configureStore + +const loggerMiddleware = createLogger() + +const createStoreWithMiddleware = applyMiddleware( + thunkMiddleware, + loggerMiddleware +)(createStore) + +function configureStore (initialState) { + return createStoreWithMiddleware(rootReducer, initialState) +} diff --git a/test/unit/components/pending-tx-test.js b/test/unit/components/pending-tx-test.js index 2594a1a26..d7825d40e 100644 --- a/test/unit/components/pending-tx-test.js +++ b/test/unit/components/pending-tx-test.js @@ -1,11 +1,10 @@ const assert = require('assert') const additions = require('react-testutils-additions') const h = require('react-hyperscript') -var PendingTx = require('../../../ui/app/components/pending-tx') +const PendingTx = require('../../../ui/app/components/pending-tx') const createReactFactory = require('create-react-factory').createReactFactory const React = require('react') -console.dir(createReactFactory) -const shallow = require('enzyme').shallow +const shallow = require('react-test-renderer/shallow') const Factory = createReactFactory(PendingTx) const ReactTestUtils = require('react-addons-test-utils') @@ -53,23 +52,27 @@ describe.only('PendingTx', function () { } const pendingTxComponent = h(PendingTx, props) - var component = additions.renderIntoDocument(pendingTxComponent); + const component = additions.renderIntoDocument(pendingTxComponent); renderer.render(pendingTxComponent) const result = renderer.getRenderOutput() const form = result.props.children - console.log('FORM children') - console.dir(form.props.children) const children = form.props.children[form.props.children.length - 1] assert.equal(result.type, 'div', 'should create a div') - console.dir(children) - - console.log('finding input') try{ - const input = additions.find(component, '.cell.row input[type="number"]') - console.log('input') - console.dir(input) + const input = additions.find(component, '.cell.row input[type="number"]')[1] + ReactTestUtils.Simulate.change(input, { + target: { + value: 2, + checkValidity() { return true }, + } + }) + + let form = additions.find(component, 'form')[0] + form.checkValidity = () => true + form.getFormEl = () => { return { checkValidity() { return true } } } + ReactTestUtils.Simulate.submit(form, { preventDefault() {}, target: { checkValidity() {return true} } }) } catch (e) { console.log("WHAAAA") @@ -79,7 +82,6 @@ describe.only('PendingTx', function () { const noop = () => {} setTimeout(() => { - console.log('timeout finished') // Get the gas price input // Set it to the newGasPrice value @@ -91,7 +93,6 @@ describe.only('PendingTx', function () { }, 200) - console.log('calling render') }) }) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index fb555b821..d31ccf2e5 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -380,11 +380,22 @@ PendingTx.prototype.onSubmit = function (event) { } PendingTx.prototype.checkValidity = function() { - const form = document.querySelector('form#pending-tx-form') + const form = this.getFormEl() + console.log("check validity got form el:") + console.dir(form) const valid = form.checkValidity() return valid } +PendingTx.prototype.getFormEl = function() { + const form = document.querySelector('form#pending-tx-form') + // Stub out form for unit tests: + if (!form) { + return { checkValidity() { return true } } + } + return form +} + // After a customizable state value has been updated, PendingTx.prototype.gatherTxMeta = function () { log.debug(`pending-tx gatherTxMeta`) -- cgit v1.2.3 From 75d9b5619c1b7e0949136702e7301ed0bb648f09 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 15 May 2017 15:21:28 -0700 Subject: Verify updating gas value updates --- test/unit/components/pending-tx-test.js | 8 +++++--- ui/app/components/pending-tx.js | 2 -- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/unit/components/pending-tx-test.js b/test/unit/components/pending-tx-test.js index d7825d40e..57fccba71 100644 --- a/test/unit/components/pending-tx-test.js +++ b/test/unit/components/pending-tx-test.js @@ -7,6 +7,7 @@ const React = require('react') const shallow = require('react-test-renderer/shallow') const Factory = createReactFactory(PendingTx) const ReactTestUtils = require('react-addons-test-utils') +const ethUtil = require('ethereumjs-util') describe.only('PendingTx', function () { let pendingTxComponent @@ -38,15 +39,16 @@ describe.only('PendingTx', function () { it('should use updated values when edited.', function (done) { const renderer = ReactTestUtils.createRenderer(); - const newGasPrice = '0x451456' + const newGasPrice = '0x77359400' const props = { identities, accounts: identities, txData, sendTransaction: (txMeta, event) => { - assert.notEqual(txMeta.txParams.gasPrice, gasPrice, 'gas price should change') - assert.equal(txMeta.txParams.gasPrice, newGasPrice, 'gas price assigned.') + const result = ethUtil.addHexPrefix(txMeta.txParams.gasPrice) + assert.notEqual(result, gasPrice, 'gas price should change') + assert.equal(result, newGasPrice, 'gas price assigned.') done() }, } diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index d31ccf2e5..bbdac3bc4 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -381,8 +381,6 @@ PendingTx.prototype.onSubmit = function (event) { PendingTx.prototype.checkValidity = function() { const form = this.getFormEl() - console.log("check validity got form el:") - console.dir(form) const valid = form.checkValidity() return valid } -- cgit v1.2.3 From fc7b4cb4bc5141f920131f8d79c56ca9ff3b6d2c Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 15 May 2017 15:22:49 -0700 Subject: Linted --- ui/app/components/ens-input.js | 5 +++-- ui/app/components/pending-tx.js | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js index 04c6222c2..3e44d83af 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/ens-input.js @@ -168,6 +168,7 @@ EnsInput.prototype.ensIconContents = function (recipient) { } } -function getNetworkEnsSupport(network) { +function getNetworkEnsSupport (network) { return Boolean(networkMap[network]) -} \ No newline at end of file +} + diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index bbdac3bc4..b084a1d2a 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -379,17 +379,17 @@ PendingTx.prototype.onSubmit = function (event) { } } -PendingTx.prototype.checkValidity = function() { +PendingTx.prototype.checkValidity = function () { const form = this.getFormEl() const valid = form.checkValidity() return valid } -PendingTx.prototype.getFormEl = function() { +PendingTx.prototype.getFormEl = function () { const form = document.querySelector('form#pending-tx-form') // Stub out form for unit tests: if (!form) { - return { checkValidity() { return true } } + return { checkValidity () { return true } } } return form } -- cgit v1.2.3 From f9c0fc0e8cb04f371ce8e99c41c74989841c2c24 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 15 May 2017 15:23:38 -0700 Subject: Clean up test --- test/unit/components/pending-tx-test.js | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/test/unit/components/pending-tx-test.js b/test/unit/components/pending-tx-test.js index 57fccba71..fe8290003 100644 --- a/test/unit/components/pending-tx-test.js +++ b/test/unit/components/pending-tx-test.js @@ -46,6 +46,8 @@ describe.only('PendingTx', function () { accounts: identities, txData, sendTransaction: (txMeta, event) => { + + // Assert changes: const result = ethUtil.addHexPrefix(txMeta.txParams.gasPrice) assert.notEqual(result, gasPrice, 'gas price should change') assert.equal(result, newGasPrice, 'gas price assigned.') @@ -81,20 +83,6 @@ describe.only('PendingTx', function () { console.error(e) } - const noop = () => {} - - setTimeout(() => { - - // Get the gas price input - // Set it to the newGasPrice value - // Wait for the value to change - // Get the submit button - // Click the submit button - // Get the output of the submit event. - // Assert that the value was updated. - - }, 200) - }) }) -- cgit v1.2.3 From 81122170b5e1b5853a823a9290c58e514062cb3f Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 15 May 2017 15:31:19 -0700 Subject: Add stage 0 support to build system --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 423821fbe..3355b4442 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ { "presets": [ "es2015", - "stage-3" + "stage-0" ] } ], @@ -127,7 +127,7 @@ "devDependencies": { "babel-eslint": "^6.0.5", "babel-plugin-transform-runtime": "^6.23.0", - "babel-preset-stage-3": "^6.24.1", + "babel-preset-stage-0": "^6.24.1", "babel-register": "^6.7.2", "babelify": "^7.2.0", "beefy": "^2.1.5", -- cgit v1.2.3 From c1b0aaa4432acf3017c2653374fc5601b563163a Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 15 May 2017 17:11:07 -0700 Subject: deps - bump provider-engine 12.0.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f4bdbd998..6358ab850 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "valid-url": "^1.0.9", "vreme": "^3.0.2", "web3": "0.18.2", - "web3-provider-engine": "^12.0.3", + "web3-provider-engine": "^12.0.6", "web3-stream-provider": "^2.0.6", "xtend": "^4.0.1" }, -- cgit v1.2.3 From b904fa5d86067e4d23d383f15597b1d79b833088 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 15 May 2017 17:13:13 -0700 Subject: changelog - add note on filter fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 422639f70..3653e5018 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Current Master - Trim currency list. +- Fix event filter bug introduced by newer versions of Geth. ## 3.6.4 2017-5-8 -- cgit v1.2.3 From 4c10e2021aa0cdc4f34a22368a37c76e0e1fea22 Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Mon, 15 May 2017 18:05:11 -0700 Subject: Change default network to rinkeby --- app/scripts/config.js | 6 +++--- app/scripts/lib/config-manager.js | 10 +++++----- ui/app/app.js | 4 ++-- ui/app/components/drop-menu-item.js | 4 ++-- ui/app/components/network.js | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/scripts/config.js b/app/scripts/config.js index 391c67230..67067cfd7 100644 --- a/app/scripts/config.js +++ b/app/scripts/config.js @@ -1,7 +1,7 @@ const MAINET_RPC_URL = 'https://mainnet.infura.io/metamask' -const TESTNET_RPC_URL = 'https://ropsten.infura.io/metamask' +const TESTNET_RPC_URL = 'https://rinkeby.infura.io/metamask' const KOVAN_RPC_URL = 'https://kovan.infura.io/metamask' -const RINKEBY_RPC_URL = 'https://rinkeby.infura.io/metamask' +const ROPSTEN_RPC_URL = 'https://ropsten.infura.io/metamask' const DEFAULT_RPC_URL = TESTNET_RPC_URL global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' @@ -13,6 +13,6 @@ module.exports = { testnet: TESTNET_RPC_URL, morden: TESTNET_RPC_URL, kovan: KOVAN_RPC_URL, - rinkeby: RINKEBY_RPC_URL, + ropsten: ROPSTEN_RPC_URL, }, } diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index ab9410842..4ca02135a 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -6,7 +6,7 @@ const TESTNET_RPC = MetamaskConfig.network.testnet const MAINNET_RPC = MetamaskConfig.network.mainnet const MORDEN_RPC = MetamaskConfig.network.morden const KOVAN_RPC = MetamaskConfig.network.kovan -const RINKEBY_RPC = MetamaskConfig.network.rinkeby +const ROPSTEN_RPC = MetamaskConfig.network.ropsten /* The config-manager is a convenience object @@ -147,8 +147,8 @@ ConfigManager.prototype.getCurrentRpcAddress = function () { case 'mainnet': return MAINNET_RPC - case 'testnet': - return TESTNET_RPC + case 'ropsten': + return ROPSTEN_RPC case 'morden': return MORDEN_RPC @@ -156,8 +156,8 @@ ConfigManager.prototype.getCurrentRpcAddress = function () { case 'kovan': return KOVAN_RPC - case 'rinkeby': - return RINKEBY_RPC + case 'testnet': + return TESTNET_RPC default: return provider && provider.rpcTarget ? provider.rpcTarget : TESTNET_RPC diff --git a/ui/app/app.js b/ui/app/app.js index bbfd58588..d096ca531 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -249,7 +249,7 @@ App.prototype.renderNetworkDropdown = function () { h(DropMenuItem, { label: 'Ropsten Test Network', closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setProviderType('testnet')), + action: () => props.dispatch(actions.setProviderType('ropsten')), icon: h('.menu-icon.red-dot'), activeNetworkRender: props.network, provider: props.provider, @@ -267,7 +267,7 @@ App.prototype.renderNetworkDropdown = function () { h(DropMenuItem, { label: 'Rinkeby Test Network', closeMenu: () => this.setState({ isNetworkMenuOpen: false}), - action: () => props.dispatch(actions.setProviderType('rinkeby')), + action: () => props.dispatch(actions.setProviderType('testnet')), icon: h('.menu-icon.golden-square'), activeNetworkRender: props.network, provider: props.provider, diff --git a/ui/app/components/drop-menu-item.js b/ui/app/components/drop-menu-item.js index bd9d8f597..27c2afde3 100644 --- a/ui/app/components/drop-menu-item.js +++ b/ui/app/components/drop-menu-item.js @@ -42,13 +42,13 @@ DropMenuItem.prototype.activeNetworkRender = function () { if (providerType === 'mainnet') return h('.check', '✓') break case 'Ropsten Test Network': - if (providerType === 'testnet') return h('.check', '✓') + if (providerType === 'ropsten') return h('.check', '✓') break case 'Kovan Test Network': if (providerType === 'kovan') return h('.check', '✓') break case 'Rinkeby Test Network': - if (providerType === 'rinkeby') return h('.check', '✓') + if (providerType === 'testnet') return h('.check', '✓') break case 'Localhost 8545': if (activeNetwork === 'http://localhost:8545') return h('.check', '✓') diff --git a/ui/app/components/network.js b/ui/app/components/network.js index f7ea8c49e..1e7c082e1 100644 --- a/ui/app/components/network.js +++ b/ui/app/components/network.js @@ -34,7 +34,7 @@ Network.prototype.render = function () { } else if (providerName === 'mainnet') { hoverText = 'Main Ethereum Network' iconName = 'ethereum-network' - } else if (providerName === 'testnet') { + } else if (providerName === 'ropsten') { hoverText = 'Ropsten Test Network' iconName = 'ropsten-test-network' } else if (parseInt(networkNumber) === 3) { @@ -43,7 +43,7 @@ Network.prototype.render = function () { } else if (providerName === 'kovan') { hoverText = 'Kovan Test Network' iconName = 'kovan-test-network' - } else if (providerName === 'rinkeby') { + } else if (providerName === 'testnet') { hoverText = 'Rinkeby Test Network' iconName = 'rinkeby-test-network' } else { @@ -90,7 +90,7 @@ Network.prototype.render = function () { h('.menu-icon.golden-square'), h('.network-name', { style: { - color: '#550077', + color: '#e7a218', }}, 'Rinkeby Test Net'), ]) -- cgit v1.2.3 From 3367363b1234a076695758762d7f1220fe4a7f8c Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Mon, 15 May 2017 19:11:16 -0700 Subject: Remove all traces of testnet --- app/scripts/config.js | 11 ++++------- app/scripts/first-time-state.js | 2 +- app/scripts/lib/config-manager.js | 15 +++++---------- ui/app/app.js | 2 +- ui/app/components/drop-menu-item.js | 2 +- ui/app/components/network.js | 2 +- ui/app/config.js | 2 +- 7 files changed, 14 insertions(+), 22 deletions(-) diff --git a/app/scripts/config.js b/app/scripts/config.js index 67067cfd7..8e28db80e 100644 --- a/app/scripts/config.js +++ b/app/scripts/config.js @@ -1,18 +1,15 @@ const MAINET_RPC_URL = 'https://mainnet.infura.io/metamask' -const TESTNET_RPC_URL = 'https://rinkeby.infura.io/metamask' -const KOVAN_RPC_URL = 'https://kovan.infura.io/metamask' const ROPSTEN_RPC_URL = 'https://ropsten.infura.io/metamask' -const DEFAULT_RPC_URL = TESTNET_RPC_URL +const KOVAN_RPC_URL = 'https://kovan.infura.io/metamask' +const RINKEBY_RPC_URL = 'https://rinkeby.infura.io/metamask' global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' module.exports = { network: { - default: DEFAULT_RPC_URL, mainnet: MAINET_RPC_URL, - testnet: TESTNET_RPC_URL, - morden: TESTNET_RPC_URL, - kovan: KOVAN_RPC_URL, ropsten: ROPSTEN_RPC_URL, + kovan: KOVAN_RPC_URL, + rinkeby: RINKEBY_RPC_URL, }, } diff --git a/app/scripts/first-time-state.js b/app/scripts/first-time-state.js index 87a7bb7b5..29ec1d8d3 100644 --- a/app/scripts/first-time-state.js +++ b/app/scripts/first-time-state.js @@ -5,7 +5,7 @@ module.exports = { config: { provider: { - type: 'testnet', + type: 'rinkeby', }, }, } diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index 4ca02135a..d77cd2126 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -2,12 +2,10 @@ const MetamaskConfig = require('../config.js') const ethUtil = require('ethereumjs-util') const normalize = require('eth-sig-util').normalize -const TESTNET_RPC = MetamaskConfig.network.testnet const MAINNET_RPC = MetamaskConfig.network.mainnet -const MORDEN_RPC = MetamaskConfig.network.morden -const KOVAN_RPC = MetamaskConfig.network.kovan const ROPSTEN_RPC = MetamaskConfig.network.ropsten - +const KOVAN_RPC = MetamaskConfig.network.kovan +const RINKEBY_RPC = MetamaskConfig.network.rinkeby /* The config-manager is a convenience object * wrapping a pojo-migrator. @@ -150,17 +148,14 @@ ConfigManager.prototype.getCurrentRpcAddress = function () { case 'ropsten': return ROPSTEN_RPC - case 'morden': - return MORDEN_RPC - case 'kovan': return KOVAN_RPC - case 'testnet': - return TESTNET_RPC + case 'rinkeby': + return RINKEBY_RPC default: - return provider && provider.rpcTarget ? provider.rpcTarget : TESTNET_RPC + return provider && provider.rpcTarget ? provider.rpcTarget : RINKEBY_RPC } } diff --git a/ui/app/app.js b/ui/app/app.js index d096ca531..6e5aa57cd 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -267,7 +267,7 @@ App.prototype.renderNetworkDropdown = function () { h(DropMenuItem, { label: 'Rinkeby Test Network', closeMenu: () => this.setState({ isNetworkMenuOpen: false}), - action: () => props.dispatch(actions.setProviderType('testnet')), + action: () => props.dispatch(actions.setProviderType('rinkeby')), icon: h('.menu-icon.golden-square'), activeNetworkRender: props.network, provider: props.provider, diff --git a/ui/app/components/drop-menu-item.js b/ui/app/components/drop-menu-item.js index 27c2afde3..e42948209 100644 --- a/ui/app/components/drop-menu-item.js +++ b/ui/app/components/drop-menu-item.js @@ -48,7 +48,7 @@ DropMenuItem.prototype.activeNetworkRender = function () { if (providerType === 'kovan') return h('.check', '✓') break case 'Rinkeby Test Network': - if (providerType === 'testnet') return h('.check', '✓') + if (providerType === 'rinkeby') return h('.check', '✓') break case 'Localhost 8545': if (activeNetwork === 'http://localhost:8545') return h('.check', '✓') diff --git a/ui/app/components/network.js b/ui/app/components/network.js index 1e7c082e1..31a8fc17c 100644 --- a/ui/app/components/network.js +++ b/ui/app/components/network.js @@ -43,7 +43,7 @@ Network.prototype.render = function () { } else if (providerName === 'kovan') { hoverText = 'Kovan Test Network' iconName = 'kovan-test-network' - } else if (providerName === 'testnet') { + } else if (providerName === 'rinkeby') { hoverText = 'Rinkeby Test Network' iconName = 'rinkeby-test-network' } else { diff --git a/ui/app/config.js b/ui/app/config.js index 26cfe663f..d7be26757 100644 --- a/ui/app/config.js +++ b/ui/app/config.js @@ -156,7 +156,7 @@ function currentProviderDisplay (metamaskState) { value = 'Main Ethereum Network' break - case 'testnet': + case 'ropsten': title = 'Current Network' value = 'Ropsten Test Network' break -- cgit v1.2.3 From a8306f15790b3fe49017518bcc3aecd73ec51e9c Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 15 May 2017 21:59:24 -0700 Subject: mascara - add deploy instructions --- mascara/README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/mascara/README.md b/mascara/README.md index db5b4f404..14cf7f563 100644 --- a/mascara/README.md +++ b/mascara/README.md @@ -3,7 +3,7 @@ start the dual servers (dapp + mascara) npm run mascara ``` -## First time use: +### First time use: - navigate to: http://localhost:9001 - Create an Account @@ -11,7 +11,7 @@ npm run mascara - open devTools - click Sync Tx -## Tests: +### Tests: ``` npm run testMascara @@ -22,3 +22,12 @@ Test will run in browser, you will have to have these browsers installed: - Chrome - Firefox - Opera + + +### Deploy: + +Will build and deploy mascara via docker + +``` +docker-compose build && docker-compose stop && docker-compose up && docker-compose -t 200 -f +``` \ No newline at end of file -- cgit v1.2.3 From 05933a7acb4a3967fbef95508c4474331a98d715 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 15 May 2017 22:01:04 -0700 Subject: mascara - ui - fix scale on mobile --- mascara/ui/index.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mascara/ui/index.html b/mascara/ui/index.html index c5eeb05ef..eac8e4898 100644 --- a/mascara/ui/index.html +++ b/mascara/ui/index.html @@ -2,7 +2,8 @@ - MetaMask Plugin + MetaMascara Alpha +
-- cgit v1.2.3 From 9fbe3e53714db8d359fa122c8701355ca5e9247c Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 15 May 2017 22:12:06 -0700 Subject: mascara - fix deploy instructions --- mascara/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mascara/README.md b/mascara/README.md index 14cf7f563..6e3bfe96b 100644 --- a/mascara/README.md +++ b/mascara/README.md @@ -29,5 +29,5 @@ Test will run in browser, you will have to have these browsers installed: Will build and deploy mascara via docker ``` -docker-compose build && docker-compose stop && docker-compose up && docker-compose -t 200 -f +docker-compose build && docker-compose stop && docker-compose up -d && docker-compose logs --tail 200 -f ``` \ No newline at end of file -- cgit v1.2.3 From d5636080cf48fef8381a01006d4a3156a6b62aba Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 15 May 2017 23:12:47 -0700 Subject: ui - send - clean props assignment --- ui/app/send.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/ui/app/send.js b/ui/app/send.js index d73744f70..6cfe909e6 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -41,14 +41,16 @@ function SendTransactionScreen () { SendTransactionScreen.prototype.render = function () { this.persistentFormParentId = 'send-tx-form' - var props = this.props - var address = props.address - var account = props.account - var identity = props.identity - var network = props.network - var identities = props.identities - var addressBook = props.addressBook - var conversionRate = props.conversionRate + const props = this.props + const { + address, + account, + identity, + network, + identities, + addressBook, + conversionRate + } = props return ( -- cgit v1.2.3 From e28e0acaa8fb690a7fe5ac45597837f20d2079e1 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 15 May 2017 23:21:46 -0700 Subject: lint - mandatory dangle :stuck_out_tongue: --- ui/app/send.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/send.js b/ui/app/send.js index 6cfe909e6..a313c6bee 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -49,7 +49,7 @@ SendTransactionScreen.prototype.render = function () { network, identities, addressBook, - conversionRate + conversionRate, } = props return ( -- cgit v1.2.3 From 01b6d9c374476bd8c59fc0ba342639ddcea7ca8d Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 15 May 2017 23:54:05 -0700 Subject: test - format test data 001 --- test/lib/migrations/001.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test/lib/migrations/001.json b/test/lib/migrations/001.json index 2fe6dd836..7bd55a50e 100644 --- a/test/lib/migrations/001.json +++ b/test/lib/migrations/001.json @@ -1 +1,14 @@ -{"version":0,"data":{"wallet":"{\"encSeed\":{\"encStr\":\"rT1C1jjkFRfmrwefscFcwZohl4f+HfIFlBZ9AM4ZD8atJmfKDIQCVK11NYDKYv8ZMIY03f3t8MuoZvfzBL8IJsWnZUhpzVTNNiARQJD2WpGA19eNBzgZm4vd0GwkIUruUDeJXu0iv2j9wU8hOQUqPbOePPy2Am5ro97iuvMAroRTnEKD60qFVg==\",\"nonce\":\"YUY2mwNq2v3FV0Fi94QnSiKFOLYfDR95\"},\"ksData\":{\"m/44'/60'/0'/0\":{\"info\":{\"curve\":\"secp256k1\",\"purpose\":\"sign\"},\"encHdPathPriv\":{\"encStr\":\"Iyi7ft4JQ9UtwrSXRT6ZIHPtZqJhe99rh0uWhNc6QLan6GanY2ZQeU0tt76CBealEWJyrJReSxGQdqDmSDYjpjH3m4JO5l0DfPLPseCqzXV/W+dzM0ubJ8lztLwpwi0L+vULNMqCx4dQtoNbNBq1QZUnjtpm6O8mWpScspboww==\",\"nonce\":\"Z7RqtjNjC6FrLUj5wVW1+HkjOW6Hib6K\"},\"hdIndex\":3,\"encPrivKeys\":{\"edb81c10122f34040cc4bef719a272fbbb1cf897\":{\"key\":\"8ab81tKBd4+CLAbzvS7SBFRTd6VWXBs86uBE43lgcmBu2U7UB22xdH64Q2hUf9eB\",\"nonce\":\"aGUEqI033FY39zKjWmZSI6PQrCLvkiRP\"},\"8bd7d5c000cf05284e98356370dc5ccaa3dbfc38\":{\"key\":\"+i3wmf4b+B898QtlOBfL0Ixirjg59/LLPX61vQ2L0xRPjXzNog0O4Wn15RemM5mY\",\"nonce\":\"imKrlkuoC5uuFkzJBbuDBluGCPJXNTKm\"},\"2340695474656e3124b8eba1172fbfb00eeac8f8\":{\"key\":\"pi+H9D8LYKsdCQKrfaJtsGFjE+X9s74xN675tsoIKrbPXhtpxMLOIQVtSqYveF62\",\"nonce\":\"49g80wDTovHwbguVVYf2FsYbp7Db5OAR\"}},\"addresses\":[\"edb81c10122f34040cc4bef719a272fbbb1cf897\",\"8bd7d5c000cf05284e98356370dc5ccaa3dbfc38\",\"2340695474656e3124b8eba1172fbfb00eeac8f8\"]}},\"version\":2}","config":{"provider":{"type":"etherscan"}}},"meta":{"version":0}} \ No newline at end of file +{ + "version": 0, + "data": { + "wallet": "{\"encSeed\":{\"encStr\":\"rT1C1jjkFRfmrwefscFcwZohl4f+HfIFlBZ9AM4ZD8atJmfKDIQCVK11NYDKYv8ZMIY03f3t8MuoZvfzBL8IJsWnZUhpzVTNNiARQJD2WpGA19eNBzgZm4vd0GwkIUruUDeJXu0iv2j9wU8hOQUqPbOePPy2Am5ro97iuvMAroRTnEKD60qFVg==\",\"nonce\":\"YUY2mwNq2v3FV0Fi94QnSiKFOLYfDR95\"},\"ksData\":{\"m/44'/60'/0'/0\":{\"info\":{\"curve\":\"secp256k1\",\"purpose\":\"sign\"},\"encHdPathPriv\":{\"encStr\":\"Iyi7ft4JQ9UtwrSXRT6ZIHPtZqJhe99rh0uWhNc6QLan6GanY2ZQeU0tt76CBealEWJyrJReSxGQdqDmSDYjpjH3m4JO5l0DfPLPseCqzXV/W+dzM0ubJ8lztLwpwi0L+vULNMqCx4dQtoNbNBq1QZUnjtpm6O8mWpScspboww==\",\"nonce\":\"Z7RqtjNjC6FrLUj5wVW1+HkjOW6Hib6K\"},\"hdIndex\":3,\"encPrivKeys\":{\"edb81c10122f34040cc4bef719a272fbbb1cf897\":{\"key\":\"8ab81tKBd4+CLAbzvS7SBFRTd6VWXBs86uBE43lgcmBu2U7UB22xdH64Q2hUf9eB\",\"nonce\":\"aGUEqI033FY39zKjWmZSI6PQrCLvkiRP\"},\"8bd7d5c000cf05284e98356370dc5ccaa3dbfc38\":{\"key\":\"+i3wmf4b+B898QtlOBfL0Ixirjg59/LLPX61vQ2L0xRPjXzNog0O4Wn15RemM5mY\",\"nonce\":\"imKrlkuoC5uuFkzJBbuDBluGCPJXNTKm\"},\"2340695474656e3124b8eba1172fbfb00eeac8f8\":{\"key\":\"pi+H9D8LYKsdCQKrfaJtsGFjE+X9s74xN675tsoIKrbPXhtpxMLOIQVtSqYveF62\",\"nonce\":\"49g80wDTovHwbguVVYf2FsYbp7Db5OAR\"}},\"addresses\":[\"edb81c10122f34040cc4bef719a272fbbb1cf897\",\"8bd7d5c000cf05284e98356370dc5ccaa3dbfc38\",\"2340695474656e3124b8eba1172fbfb00eeac8f8\"]}},\"version\":2}", + "config": { + "provider": { + "type": "etherscan" + } + } + }, + "meta": { + "version": 0 + } +} \ No newline at end of file -- cgit v1.2.3 From 28aba6e9dea52b66534d6ecb9713a7d20947c57c Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 15 May 2017 23:56:13 -0700 Subject: migration 13 - change provider from testnet to ropsten --- app/scripts/migrations/013.js | 34 ++++++++++++++++++++++++++++++++++ app/scripts/migrations/index.js | 1 + test/unit/migrations-test.js | 6 ++++++ 3 files changed, 41 insertions(+) create mode 100644 app/scripts/migrations/013.js diff --git a/app/scripts/migrations/013.js b/app/scripts/migrations/013.js new file mode 100644 index 000000000..8f11e510e --- /dev/null +++ b/app/scripts/migrations/013.js @@ -0,0 +1,34 @@ +const version = 13 + +/* + +This migration modifies the network config from ambiguous 'testnet' to explicit 'ropsten' + +*/ + +const clone = require('clone') + +module.exports = { + version, + + migrate: function (originalVersionedData) { + const versionedData = clone(originalVersionedData) + versionedData.meta.version = version + try { + const state = versionedData.data + const newState = transformState(state) + versionedData.data = newState + } catch (err) { + console.warn(`MetaMask Migration #${version}` + err.stack) + } + return Promise.resolve(versionedData) + }, +} + +function transformState (state) { + const newState = state + if (newState.config.provider.type === 'testnet') { + newState.config.provider.type = 'ropsten' + } + return newState +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 019b4d13d..3a95cf88e 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -23,4 +23,5 @@ module.exports = [ require('./010'), require('./011'), require('./012'), + require('./013'), ] diff --git a/test/unit/migrations-test.js b/test/unit/migrations-test.js index 324e4d056..5bad25a45 100644 --- a/test/unit/migrations-test.js +++ b/test/unit/migrations-test.js @@ -16,6 +16,7 @@ const migration9 = require(path.join('..', '..', 'app', 'scripts', 'migrations', const migration10 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '010')) const migration11 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '011')) const migration12 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '012')) +const migration13 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '013')) const oldTestRpc = 'https://rawtestrpc.metamask.io/' @@ -97,6 +98,11 @@ describe('wallet1 is migrated successfully', () => { }).then((twelfthResult) => { assert.equal(twelfthResult.data.NoticeController.noticesList[0].body, '', 'notices that have been read should have an empty body.') assert.equal(twelfthResult.data.NoticeController.noticesList[1].body, 'nonempty', 'notices that have not been read should not have an empty body.') + + assert.equal(twelfthResult.data.config.provider.type, 'testnet', 'network is originally testnet.') + return migration13.migrate(twelfthResult) + }).then((thirteenthResult) => { + assert.equal(thirteenthResult.data.config.provider.type, 'ropsten', 'network has been changed to ropsten.') }) }) }) -- cgit v1.2.3 From 7a3b3e0f8a7a28d20980a9f839490138a562ee29 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 16 May 2017 10:27:41 -0700 Subject: Rename tx manager to tx controller --- app/scripts/controllers/transactions.js | 404 ++++++++++++++++++++++++++++++++ app/scripts/metamask-controller.js | 32 +-- app/scripts/transaction-manager.js | 404 -------------------------------- test/unit/tx-controller-test.js | 237 +++++++++++++++++++ test/unit/tx-manager-test.js | 237 ------------------- 5 files changed, 657 insertions(+), 657 deletions(-) create mode 100644 app/scripts/controllers/transactions.js delete mode 100644 app/scripts/transaction-manager.js create mode 100644 test/unit/tx-controller-test.js delete mode 100644 test/unit/tx-manager-test.js diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js new file mode 100644 index 000000000..9f267160f --- /dev/null +++ b/app/scripts/controllers/transactions.js @@ -0,0 +1,404 @@ +const EventEmitter = require('events') +const async = require('async') +const extend = require('xtend') +const Semaphore = require('semaphore') +const ObservableStore = require('obs-store') +const ethUtil = require('ethereumjs-util') +const EthQuery = require('eth-query') +const TxProviderUtil = require('./lib/tx-utils') +const createId = require('./lib/random-id') + +module.exports = class TransactionManager extends EventEmitter { + constructor (opts) { + super() + this.store = new ObservableStore(extend({ + transactions: [], + }, opts.initState)) + this.memStore = new ObservableStore({}) + this.networkStore = opts.networkStore || new ObservableStore({}) + this.preferencesStore = opts.preferencesStore || new ObservableStore({}) + this.txHistoryLimit = opts.txHistoryLimit + this.provider = opts.provider + this.blockTracker = opts.blockTracker + this.query = new EthQuery(this.provider) + this.txProviderUtils = new TxProviderUtil(this.provider) + this.blockTracker.on('block', this.checkForTxInBlock.bind(this)) + this.signEthTx = opts.signTransaction + this.nonceLock = Semaphore(1) + + // memstore is computed from a few different stores + this._updateMemstore() + this.store.subscribe(() => this._updateMemstore()) + this.networkStore.subscribe(() => this._updateMemstore()) + this.preferencesStore.subscribe(() => this._updateMemstore()) + } + + getState () { + return this.memStore.getState() + } + + getNetwork () { + return this.networkStore.getState().network + } + + getSelectedAddress () { + return this.preferencesStore.getState().selectedAddress + } + + // Returns the tx list + getTxList () { + const network = this.getNetwork() + const fullTxList = this.getFullTxList() + return fullTxList.filter(txMeta => txMeta.metamaskNetworkId === network) + } + + // Returns the number of txs for the current network. + getTxCount () { + return this.getTxList().length + } + + // Returns the full tx list across all networks + getFullTxList () { + return this.store.getState().transactions + } + + // Adds a tx to the txlist + addTx (txMeta) { + const txCount = this.getTxCount() + const network = this.getNetwork() + const fullTxList = this.getFullTxList() + const txHistoryLimit = this.txHistoryLimit + + // checks if the length of the tx history is + // longer then desired persistence limit + // and then if it is removes only confirmed + // or rejected tx's. + // not tx's that are pending or unapproved + if (txCount > txHistoryLimit - 1) { + var index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId)) + fullTxList.splice(index, 1) + } + fullTxList.push(txMeta) + this._saveTxList(fullTxList) + this.emit('update') + + this.once(`${txMeta.id}:signed`, function (txId) { + this.removeAllListeners(`${txMeta.id}:rejected`) + }) + this.once(`${txMeta.id}:rejected`, function (txId) { + this.removeAllListeners(`${txMeta.id}:signed`) + }) + + this.emit('updateBadge') + this.emit(`${txMeta.id}:unapproved`, txMeta) + } + + // gets tx by Id and returns it + getTx (txId, cb) { + var txList = this.getTxList() + var txMeta = txList.find(txData => txData.id === txId) + return cb ? cb(txMeta) : txMeta + } + + // + updateTx (txMeta) { + var txId = txMeta.id + var txList = this.getFullTxList() + var index = txList.findIndex(txData => txData.id === txId) + txList[index] = txMeta + this._saveTxList(txList) + this.emit('update') + } + + get unapprovedTxCount () { + return Object.keys(this.getUnapprovedTxList()).length + } + + get pendingTxCount () { + return this.getTxsByMetaData('status', 'signed').length + } + + addUnapprovedTransaction (txParams, done) { + let txMeta + async.waterfall([ + // validate + (cb) => this.txProviderUtils.validateTxParams(txParams, cb), + // construct txMeta + (cb) => { + txMeta = { + id: createId(), + time: (new Date()).getTime(), + status: 'unapproved', + metamaskNetworkId: this.getNetwork(), + txParams: txParams, + } + cb() + }, + // add default tx params + (cb) => this.addTxDefaults(txMeta, cb), + // save txMeta + (cb) => { + this.addTx(txMeta) + cb(null, txMeta) + }, + ], done) + } + + addTxDefaults (txMeta, cb) { + const txParams = txMeta.txParams + // ensure value + txParams.value = txParams.value || '0x0' + this.query.gasPrice((err, gasPrice) => { + if (err) return cb(err) + // set gasPrice + txParams.gasPrice = gasPrice + // set gasLimit + this.txProviderUtils.analyzeGasUsage(txMeta, cb) + }) + } + + getUnapprovedTxList () { + var txList = this.getTxList() + return txList.filter((txMeta) => txMeta.status === 'unapproved') + .reduce((result, tx) => { + result[tx.id] = tx + return result + }, {}) + } + + approveTransaction (txId, cb = warn) { + const self = this + // approve + self.setTxStatusApproved(txId) + // only allow one tx at a time for atomic nonce usage + self.nonceLock.take(() => { + // begin signature process + async.waterfall([ + (cb) => self.fillInTxParams(txId, cb), + (cb) => self.signTransaction(txId, cb), + (rawTx, cb) => self.publishTransaction(txId, rawTx, cb), + ], (err) => { + self.nonceLock.leave() + if (err) { + this.setTxStatusFailed(txId, { + errCode: err.errCode || err, + message: err.message || 'Transaction failed during approval', + }) + return cb(err) + } + cb() + }) + }) + } + + cancelTransaction (txId, cb = warn) { + this.setTxStatusRejected(txId) + cb() + } + + fillInTxParams (txId, cb) { + const txMeta = this.getTx(txId) + this.txProviderUtils.fillInTxParams(txMeta.txParams, (err) => { + if (err) return cb(err) + this.updateTx(txMeta) + cb() + }) + } + + getChainId () { + const networkState = this.networkStore.getState() + const getChainId = parseInt(networkState.network) + if (Number.isNaN(getChainId)) { + return 0 + } else { + return getChainId + } + } + + signTransaction (txId, cb) { + const txMeta = this.getTx(txId) + const txParams = txMeta.txParams + const fromAddress = txParams.from + // add network/chain id + txParams.chainId = this.getChainId() + const ethTx = this.txProviderUtils.buildEthTxFromParams(txParams) + this.signEthTx(ethTx, fromAddress).then(() => { + this.setTxStatusSigned(txMeta.id) + cb(null, ethUtil.bufferToHex(ethTx.serialize())) + }).catch((err) => { + cb(err) + }) + } + + publishTransaction (txId, rawTx, cb) { + this.txProviderUtils.publishTransaction(rawTx, (err, txHash) => { + if (err) return cb(err) + this.setTxHash(txId, txHash) + this.setTxStatusSubmitted(txId) + cb() + }) + } + + // receives a txHash records the tx as signed + setTxHash (txId, txHash) { + // Add the tx hash to the persisted meta-tx object + const txMeta = this.getTx(txId) + txMeta.hash = txHash + this.updateTx(txMeta) + } + + /* + Takes an object of fields to search for eg: + var thingsToLookFor = { + to: '0x0..', + from: '0x0..', + status: 'signed', + } + and returns a list of tx with all + options matching + + this is for things like filtering a the tx list + for only tx's from 1 account + or for filltering for all txs from one account + and that have been 'confirmed' + */ + getFilteredTxList (opts) { + var filteredTxList + Object.keys(opts).forEach((key) => { + filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList) + }) + return filteredTxList + } + + getTxsByMetaData (key, value, txList = this.getTxList()) { + return txList.filter((txMeta) => { + if (txMeta.txParams[key]) { + return txMeta.txParams[key] === value + } else { + return txMeta[key] === value + } + }) + } + + // STATUS METHODS + // get::set status + + // should return the status of the tx. + getTxStatus (txId) { + const txMeta = this.getTx(txId) + return txMeta.status + } + + // should update the status of the tx to 'rejected'. + setTxStatusRejected (txId) { + this._setTxStatus(txId, 'rejected') + } + + // should update the status of the tx to 'approved'. + setTxStatusApproved (txId) { + this._setTxStatus(txId, 'approved') + } + + // should update the status of the tx to 'signed'. + setTxStatusSigned (txId) { + this._setTxStatus(txId, 'signed') + } + + // should update the status of the tx to 'submitted'. + setTxStatusSubmitted (txId) { + this._setTxStatus(txId, 'submitted') + } + + // should update the status of the tx to 'confirmed'. + setTxStatusConfirmed (txId) { + this._setTxStatus(txId, 'confirmed') + } + + setTxStatusFailed (txId, reason) { + const txMeta = this.getTx(txId) + txMeta.err = reason + this.updateTx(txMeta) + this._setTxStatus(txId, 'failed') + } + + // merges txParams obj onto txData.txParams + // use extend to ensure that all fields are filled + updateTxParams (txId, txParams) { + var txMeta = this.getTx(txId) + txMeta.txParams = extend(txMeta.txParams, txParams) + this.updateTx(txMeta) + } + + // checks if a signed tx is in a block and + // if included sets the tx status as 'confirmed' + checkForTxInBlock () { + var signedTxList = this.getFilteredTxList({status: 'submitted'}) + if (!signedTxList.length) return + signedTxList.forEach((txMeta) => { + var txHash = txMeta.hash + var txId = txMeta.id + if (!txHash) { + const errReason = { + errCode: 'No hash was provided', + message: 'We had an error while submitting this transaction, please try again.', + } + return this.setTxStatusFailed(txId, errReason) + } + this.query.getTransactionByHash(txHash, (err, txParams) => { + if (err || !txParams) { + if (!txParams) return + txMeta.err = { + isWarning: true, + errorCode: err, + message: 'There was a problem loading this transaction.', + } + this.updateTx(txMeta) + return console.error(err) + } + if (txParams.blockNumber) { + this.setTxStatusConfirmed(txId) + } + }) + }) + } + + // PRIVATE METHODS + + // Should find the tx in the tx list and + // update it. + // should set the status in txData + // - `'unapproved'` the user has not responded + // - `'rejected'` the user has responded no! + // - `'approved'` the user has approved the tx + // - `'signed'` the tx is signed + // - `'submitted'` the tx is sent to a server + // - `'confirmed'` the tx has been included in a block. + _setTxStatus (txId, status) { + var txMeta = this.getTx(txId) + txMeta.status = status + this.emit(`${txMeta.id}:${status}`, txId) + if (status === 'submitted' || status === 'rejected') { + this.emit(`${txMeta.id}:finished`, txMeta) + } + this.updateTx(txMeta) + this.emit('updateBadge') + } + + // Saves the new/updated txList. + // Function is intended only for internal use + _saveTxList (transactions) { + this.store.updateState({ transactions }) + } + + _updateMemstore () { + const unapprovedTxs = this.getUnapprovedTxList() + const selectedAddressTxList = this.getFilteredTxList({ + from: this.getSelectedAddress(), + metamaskNetworkId: this.getNetwork(), + }) + this.memStore.updateState({ unapprovedTxs, selectedAddressTxList }) + } +} + + +const warn = () => console.warn('warn was used no cb provided') diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 175602ec1..2406bda0d 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -17,7 +17,7 @@ const ShapeShiftController = require('./controllers/shapeshift') const AddressBookController = require('./controllers/address-book') const MessageManager = require('./lib/message-manager') const PersonalMessageManager = require('./lib/personal-message-manager') -const TxManager = require('./transaction-manager') +const TransactionController = require('./controllers/transactions') const ConfigManager = require('./lib/config-manager') const autoFaucet = require('./lib/auto-faucet') const nodeify = require('./lib/nodeify') @@ -90,8 +90,8 @@ module.exports = class MetamaskController extends EventEmitter { }, this.keyringController) // tx mgmt - this.txManager = new TxManager({ - initState: initState.TransactionManager, + this.txController = new TransactionController({ + initState: initState.TransactionController, networkStore: this.networkStore, preferencesStore: this.preferencesController.store, txHistoryLimit: 40, @@ -119,8 +119,8 @@ module.exports = class MetamaskController extends EventEmitter { this.publicConfigStore = this.initPublicConfigStore() // manual disk state subscriptions - this.txManager.store.subscribe((state) => { - this.store.updateState({ TransactionManager: state }) + this.txController.store.subscribe((state) => { + this.store.updateState({ TransactionController: state }) }) this.keyringController.store.subscribe((state) => { this.store.updateState({ KeyringController: state }) @@ -144,7 +144,7 @@ module.exports = class MetamaskController extends EventEmitter { // manual mem state subscriptions this.networkStore.subscribe(this.sendUpdate.bind(this)) this.ethStore.subscribe(this.sendUpdate.bind(this)) - this.txManager.memStore.subscribe(this.sendUpdate.bind(this)) + this.txController.memStore.subscribe(this.sendUpdate.bind(this)) this.messageManager.memStore.subscribe(this.sendUpdate.bind(this)) this.personalMessageManager.memStore.subscribe(this.sendUpdate.bind(this)) this.keyringController.memStore.subscribe(this.sendUpdate.bind(this)) @@ -223,7 +223,7 @@ module.exports = class MetamaskController extends EventEmitter { }, this.networkStore.getState(), this.ethStore.getState(), - this.txManager.memStore.getState(), + this.txController.memStore.getState(), this.messageManager.memStore.getState(), this.personalMessageManager.memStore.getState(), this.keyringController.memStore.getState(), @@ -248,7 +248,7 @@ module.exports = class MetamaskController extends EventEmitter { getApi () { const keyringController = this.keyringController const preferencesController = this.preferencesController - const txManager = this.txManager + const txController = this.txController const noticeController = this.noticeController const addressBookController = this.addressBookController @@ -289,9 +289,9 @@ module.exports = class MetamaskController extends EventEmitter { saveAccountLabel: nodeify(keyringController.saveAccountLabel).bind(keyringController), exportAccount: nodeify(keyringController.exportAccount).bind(keyringController), - // txManager - approveTransaction: txManager.approveTransaction.bind(txManager), - cancelTransaction: txManager.cancelTransaction.bind(txManager), + // txController + approveTransaction: txController.approveTransaction.bind(txController), + cancelTransaction: txController.cancelTransaction.bind(txController), updateAndApproveTransaction: this.updateAndApproveTx.bind(this), // messageManager @@ -421,12 +421,12 @@ module.exports = class MetamaskController extends EventEmitter { newUnapprovedTransaction (txParams, cb) { log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) const self = this - self.txManager.addUnapprovedTransaction(txParams, (err, txMeta) => { + self.txController.addUnapprovedTransaction(txParams, (err, txMeta) => { if (err) return cb(err) self.sendUpdate() self.opts.showUnapprovedTx(txMeta) // listen for tx completion (success, fail) - self.txManager.once(`${txMeta.id}:finished`, (completedTx) => { + self.txController.once(`${txMeta.id}:finished`, (completedTx) => { switch (completedTx.status) { case 'submitted': return cb(null, completedTx.hash) @@ -477,9 +477,9 @@ module.exports = class MetamaskController extends EventEmitter { updateAndApproveTx (txMeta, cb) { log.debug(`MetaMaskController - updateAndApproveTx: ${JSON.stringify(txMeta)}`) - const txManager = this.txManager - txManager.updateTx(txMeta) - txManager.approveTransaction(txMeta.id, cb) + const txController = this.txController + txController.updateTx(txMeta) + txController.approveTransaction(txMeta.id, cb) } signMessage (msgParams, cb) { diff --git a/app/scripts/transaction-manager.js b/app/scripts/transaction-manager.js deleted file mode 100644 index 9f267160f..000000000 --- a/app/scripts/transaction-manager.js +++ /dev/null @@ -1,404 +0,0 @@ -const EventEmitter = require('events') -const async = require('async') -const extend = require('xtend') -const Semaphore = require('semaphore') -const ObservableStore = require('obs-store') -const ethUtil = require('ethereumjs-util') -const EthQuery = require('eth-query') -const TxProviderUtil = require('./lib/tx-utils') -const createId = require('./lib/random-id') - -module.exports = class TransactionManager extends EventEmitter { - constructor (opts) { - super() - this.store = new ObservableStore(extend({ - transactions: [], - }, opts.initState)) - this.memStore = new ObservableStore({}) - this.networkStore = opts.networkStore || new ObservableStore({}) - this.preferencesStore = opts.preferencesStore || new ObservableStore({}) - this.txHistoryLimit = opts.txHistoryLimit - this.provider = opts.provider - this.blockTracker = opts.blockTracker - this.query = new EthQuery(this.provider) - this.txProviderUtils = new TxProviderUtil(this.provider) - this.blockTracker.on('block', this.checkForTxInBlock.bind(this)) - this.signEthTx = opts.signTransaction - this.nonceLock = Semaphore(1) - - // memstore is computed from a few different stores - this._updateMemstore() - this.store.subscribe(() => this._updateMemstore()) - this.networkStore.subscribe(() => this._updateMemstore()) - this.preferencesStore.subscribe(() => this._updateMemstore()) - } - - getState () { - return this.memStore.getState() - } - - getNetwork () { - return this.networkStore.getState().network - } - - getSelectedAddress () { - return this.preferencesStore.getState().selectedAddress - } - - // Returns the tx list - getTxList () { - const network = this.getNetwork() - const fullTxList = this.getFullTxList() - return fullTxList.filter(txMeta => txMeta.metamaskNetworkId === network) - } - - // Returns the number of txs for the current network. - getTxCount () { - return this.getTxList().length - } - - // Returns the full tx list across all networks - getFullTxList () { - return this.store.getState().transactions - } - - // Adds a tx to the txlist - addTx (txMeta) { - const txCount = this.getTxCount() - const network = this.getNetwork() - const fullTxList = this.getFullTxList() - const txHistoryLimit = this.txHistoryLimit - - // checks if the length of the tx history is - // longer then desired persistence limit - // and then if it is removes only confirmed - // or rejected tx's. - // not tx's that are pending or unapproved - if (txCount > txHistoryLimit - 1) { - var index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId)) - fullTxList.splice(index, 1) - } - fullTxList.push(txMeta) - this._saveTxList(fullTxList) - this.emit('update') - - this.once(`${txMeta.id}:signed`, function (txId) { - this.removeAllListeners(`${txMeta.id}:rejected`) - }) - this.once(`${txMeta.id}:rejected`, function (txId) { - this.removeAllListeners(`${txMeta.id}:signed`) - }) - - this.emit('updateBadge') - this.emit(`${txMeta.id}:unapproved`, txMeta) - } - - // gets tx by Id and returns it - getTx (txId, cb) { - var txList = this.getTxList() - var txMeta = txList.find(txData => txData.id === txId) - return cb ? cb(txMeta) : txMeta - } - - // - updateTx (txMeta) { - var txId = txMeta.id - var txList = this.getFullTxList() - var index = txList.findIndex(txData => txData.id === txId) - txList[index] = txMeta - this._saveTxList(txList) - this.emit('update') - } - - get unapprovedTxCount () { - return Object.keys(this.getUnapprovedTxList()).length - } - - get pendingTxCount () { - return this.getTxsByMetaData('status', 'signed').length - } - - addUnapprovedTransaction (txParams, done) { - let txMeta - async.waterfall([ - // validate - (cb) => this.txProviderUtils.validateTxParams(txParams, cb), - // construct txMeta - (cb) => { - txMeta = { - id: createId(), - time: (new Date()).getTime(), - status: 'unapproved', - metamaskNetworkId: this.getNetwork(), - txParams: txParams, - } - cb() - }, - // add default tx params - (cb) => this.addTxDefaults(txMeta, cb), - // save txMeta - (cb) => { - this.addTx(txMeta) - cb(null, txMeta) - }, - ], done) - } - - addTxDefaults (txMeta, cb) { - const txParams = txMeta.txParams - // ensure value - txParams.value = txParams.value || '0x0' - this.query.gasPrice((err, gasPrice) => { - if (err) return cb(err) - // set gasPrice - txParams.gasPrice = gasPrice - // set gasLimit - this.txProviderUtils.analyzeGasUsage(txMeta, cb) - }) - } - - getUnapprovedTxList () { - var txList = this.getTxList() - return txList.filter((txMeta) => txMeta.status === 'unapproved') - .reduce((result, tx) => { - result[tx.id] = tx - return result - }, {}) - } - - approveTransaction (txId, cb = warn) { - const self = this - // approve - self.setTxStatusApproved(txId) - // only allow one tx at a time for atomic nonce usage - self.nonceLock.take(() => { - // begin signature process - async.waterfall([ - (cb) => self.fillInTxParams(txId, cb), - (cb) => self.signTransaction(txId, cb), - (rawTx, cb) => self.publishTransaction(txId, rawTx, cb), - ], (err) => { - self.nonceLock.leave() - if (err) { - this.setTxStatusFailed(txId, { - errCode: err.errCode || err, - message: err.message || 'Transaction failed during approval', - }) - return cb(err) - } - cb() - }) - }) - } - - cancelTransaction (txId, cb = warn) { - this.setTxStatusRejected(txId) - cb() - } - - fillInTxParams (txId, cb) { - const txMeta = this.getTx(txId) - this.txProviderUtils.fillInTxParams(txMeta.txParams, (err) => { - if (err) return cb(err) - this.updateTx(txMeta) - cb() - }) - } - - getChainId () { - const networkState = this.networkStore.getState() - const getChainId = parseInt(networkState.network) - if (Number.isNaN(getChainId)) { - return 0 - } else { - return getChainId - } - } - - signTransaction (txId, cb) { - const txMeta = this.getTx(txId) - const txParams = txMeta.txParams - const fromAddress = txParams.from - // add network/chain id - txParams.chainId = this.getChainId() - const ethTx = this.txProviderUtils.buildEthTxFromParams(txParams) - this.signEthTx(ethTx, fromAddress).then(() => { - this.setTxStatusSigned(txMeta.id) - cb(null, ethUtil.bufferToHex(ethTx.serialize())) - }).catch((err) => { - cb(err) - }) - } - - publishTransaction (txId, rawTx, cb) { - this.txProviderUtils.publishTransaction(rawTx, (err, txHash) => { - if (err) return cb(err) - this.setTxHash(txId, txHash) - this.setTxStatusSubmitted(txId) - cb() - }) - } - - // receives a txHash records the tx as signed - setTxHash (txId, txHash) { - // Add the tx hash to the persisted meta-tx object - const txMeta = this.getTx(txId) - txMeta.hash = txHash - this.updateTx(txMeta) - } - - /* - Takes an object of fields to search for eg: - var thingsToLookFor = { - to: '0x0..', - from: '0x0..', - status: 'signed', - } - and returns a list of tx with all - options matching - - this is for things like filtering a the tx list - for only tx's from 1 account - or for filltering for all txs from one account - and that have been 'confirmed' - */ - getFilteredTxList (opts) { - var filteredTxList - Object.keys(opts).forEach((key) => { - filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList) - }) - return filteredTxList - } - - getTxsByMetaData (key, value, txList = this.getTxList()) { - return txList.filter((txMeta) => { - if (txMeta.txParams[key]) { - return txMeta.txParams[key] === value - } else { - return txMeta[key] === value - } - }) - } - - // STATUS METHODS - // get::set status - - // should return the status of the tx. - getTxStatus (txId) { - const txMeta = this.getTx(txId) - return txMeta.status - } - - // should update the status of the tx to 'rejected'. - setTxStatusRejected (txId) { - this._setTxStatus(txId, 'rejected') - } - - // should update the status of the tx to 'approved'. - setTxStatusApproved (txId) { - this._setTxStatus(txId, 'approved') - } - - // should update the status of the tx to 'signed'. - setTxStatusSigned (txId) { - this._setTxStatus(txId, 'signed') - } - - // should update the status of the tx to 'submitted'. - setTxStatusSubmitted (txId) { - this._setTxStatus(txId, 'submitted') - } - - // should update the status of the tx to 'confirmed'. - setTxStatusConfirmed (txId) { - this._setTxStatus(txId, 'confirmed') - } - - setTxStatusFailed (txId, reason) { - const txMeta = this.getTx(txId) - txMeta.err = reason - this.updateTx(txMeta) - this._setTxStatus(txId, 'failed') - } - - // merges txParams obj onto txData.txParams - // use extend to ensure that all fields are filled - updateTxParams (txId, txParams) { - var txMeta = this.getTx(txId) - txMeta.txParams = extend(txMeta.txParams, txParams) - this.updateTx(txMeta) - } - - // checks if a signed tx is in a block and - // if included sets the tx status as 'confirmed' - checkForTxInBlock () { - var signedTxList = this.getFilteredTxList({status: 'submitted'}) - if (!signedTxList.length) return - signedTxList.forEach((txMeta) => { - var txHash = txMeta.hash - var txId = txMeta.id - if (!txHash) { - const errReason = { - errCode: 'No hash was provided', - message: 'We had an error while submitting this transaction, please try again.', - } - return this.setTxStatusFailed(txId, errReason) - } - this.query.getTransactionByHash(txHash, (err, txParams) => { - if (err || !txParams) { - if (!txParams) return - txMeta.err = { - isWarning: true, - errorCode: err, - message: 'There was a problem loading this transaction.', - } - this.updateTx(txMeta) - return console.error(err) - } - if (txParams.blockNumber) { - this.setTxStatusConfirmed(txId) - } - }) - }) - } - - // PRIVATE METHODS - - // Should find the tx in the tx list and - // update it. - // should set the status in txData - // - `'unapproved'` the user has not responded - // - `'rejected'` the user has responded no! - // - `'approved'` the user has approved the tx - // - `'signed'` the tx is signed - // - `'submitted'` the tx is sent to a server - // - `'confirmed'` the tx has been included in a block. - _setTxStatus (txId, status) { - var txMeta = this.getTx(txId) - txMeta.status = status - this.emit(`${txMeta.id}:${status}`, txId) - if (status === 'submitted' || status === 'rejected') { - this.emit(`${txMeta.id}:finished`, txMeta) - } - this.updateTx(txMeta) - this.emit('updateBadge') - } - - // Saves the new/updated txList. - // Function is intended only for internal use - _saveTxList (transactions) { - this.store.updateState({ transactions }) - } - - _updateMemstore () { - const unapprovedTxs = this.getUnapprovedTxList() - const selectedAddressTxList = this.getFilteredTxList({ - from: this.getSelectedAddress(), - metamaskNetworkId: this.getNetwork(), - }) - this.memStore.updateState({ unapprovedTxs, selectedAddressTxList }) - } -} - - -const warn = () => console.warn('warn was used no cb provided') diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js new file mode 100644 index 000000000..d0b32ff41 --- /dev/null +++ b/test/unit/tx-controller-test.js @@ -0,0 +1,237 @@ +const assert = require('assert') +const EventEmitter = require('events') +const ethUtil = require('ethereumjs-util') +const EthTx = require('ethereumjs-tx') +const ObservableStore = require('obs-store') +const TransactionController = require('../../app/scripts/controllers/transactions') +const noop = () => true +const currentNetworkId = 42 +const otherNetworkId = 36 +const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex') + +describe('Transaction Manager', function () { + let txController + + beforeEach(function () { + txController = new TransactionController({ + networkStore: new ObservableStore({ network: currentNetworkId }), + txHistoryLimit: 10, + blockTracker: new EventEmitter(), + signTransaction: (ethTx) => new Promise((resolve) => { + ethTx.sign(privKey) + resolve() + }), + }) + }) + + describe('#validateTxParams', function () { + it('returns null for positive values', function () { + var sample = { + value: '0x01', + } + txController.txProviderUtils.validateTxParams(sample, (err) => { + assert.equal(err, null, 'no error') + }) + }) + + it('returns error for negative values', function () { + var sample = { + value: '-0x01', + } + txController.txProviderUtils.validateTxParams(sample, (err) => { + assert.ok(err, 'error') + }) + }) + }) + + describe('#getTxList', function () { + it('when new should return empty array', function () { + var result = txController.getTxList() + assert.ok(Array.isArray(result)) + assert.equal(result.length, 0) + }) + it('should also return transactions from local storage if any', function () { + + }) + }) + + describe('#addTx', function () { + it('adds a tx returned in getTxList', function () { + var tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } + txController.addTx(tx, noop) + var result = txController.getTxList() + assert.ok(Array.isArray(result)) + assert.equal(result.length, 1) + assert.equal(result[0].id, 1) + }) + + it('does not override txs from other networks', function () { + var tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } + var tx2 = { id: 2, status: 'confirmed', metamaskNetworkId: otherNetworkId, txParams: {} } + txController.addTx(tx, noop) + txController.addTx(tx2, noop) + var result = txController.getFullTxList() + var result2 = txController.getTxList() + assert.equal(result.length, 2, 'txs were deleted') + assert.equal(result2.length, 1, 'incorrect number of txs on network.') + }) + + it('cuts off early txs beyond a limit', function () { + const limit = txController.txHistoryLimit + for (let i = 0; i < limit + 1; i++) { + const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } + txController.addTx(tx, noop) + } + var result = txController.getTxList() + assert.equal(result.length, limit, `limit of ${limit} txs enforced`) + assert.equal(result[0].id, 1, 'early txs truncted') + }) + + it('cuts off early txs beyond a limit whether or not it is confirmed or rejected', function () { + const limit = txController.txHistoryLimit + for (let i = 0; i < limit + 1; i++) { + const tx = { id: i, time: new Date(), status: 'rejected', metamaskNetworkId: currentNetworkId, txParams: {} } + txController.addTx(tx, noop) + } + var result = txController.getTxList() + assert.equal(result.length, limit, `limit of ${limit} txs enforced`) + assert.equal(result[0].id, 1, 'early txs truncted') + }) + + it('cuts off early txs beyond a limit but does not cut unapproved txs', function () { + var unconfirmedTx = { id: 0, time: new Date(), status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } + txController.addTx(unconfirmedTx, noop) + const limit = txController.txHistoryLimit + for (let i = 1; i < limit + 1; i++) { + const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } + txController.addTx(tx, noop) + } + var result = txController.getTxList() + assert.equal(result.length, limit, `limit of ${limit} txs enforced`) + assert.equal(result[0].id, 0, 'first tx should still be there') + assert.equal(result[0].status, 'unapproved', 'first tx should be unapproved') + assert.equal(result[1].id, 2, 'early txs truncted') + }) + }) + + describe('#setTxStatusSigned', function () { + it('sets the tx status to signed', function () { + var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } + txController.addTx(tx, noop) + txController.setTxStatusSigned(1) + var result = txController.getTxList() + assert.ok(Array.isArray(result)) + assert.equal(result.length, 1) + assert.equal(result[0].status, 'signed') + }) + + it('should emit a signed event to signal the exciton of callback', (done) => { + this.timeout(10000) + var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } + const noop = function () { + assert(true, 'event listener has been triggered and noop executed') + done() + } + txController.addTx(tx) + txController.on('1:signed', noop) + txController.setTxStatusSigned(1) + }) + }) + + describe('#setTxStatusRejected', function () { + it('sets the tx status to rejected', function () { + var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } + txController.addTx(tx) + txController.setTxStatusRejected(1) + var result = txController.getTxList() + assert.ok(Array.isArray(result)) + assert.equal(result.length, 1) + assert.equal(result[0].status, 'rejected') + }) + + it('should emit a rejected event to signal the exciton of callback', (done) => { + this.timeout(10000) + var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } + txController.addTx(tx) + const noop = function (err, txId) { + assert(true, 'event listener has been triggered and noop executed') + done() + } + txController.on('1:rejected', noop) + txController.setTxStatusRejected(1) + }) + }) + + describe('#updateTx', function () { + it('replaces the tx with the same id', function () { + txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) + txController.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) + txController.updateTx({ id: '1', status: 'blah', hash: 'foo', metamaskNetworkId: currentNetworkId, txParams: {} }) + var result = txController.getTx('1') + assert.equal(result.hash, 'foo') + }) + }) + + describe('#getUnapprovedTxList', function () { + it('returns unapproved txs in a hash', function () { + txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) + txController.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) + const result = txController.getUnapprovedTxList() + assert.equal(typeof result, 'object') + assert.equal(result['1'].status, 'unapproved') + assert.equal(result['2'], undefined) + }) + }) + + describe('#getTx', function () { + it('returns a tx with the requested id', function () { + txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) + txController.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) + assert.equal(txController.getTx('1').status, 'unapproved') + assert.equal(txController.getTx('2').status, 'confirmed') + }) + }) + + describe('#getFilteredTxList', function () { + it('returns a tx with the requested data', function () { + const txMetas = [ + { id: 0, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, + { id: 1, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, + { id: 2, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, + { id: 3, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, + { id: 4, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, + { id: 5, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, + { id: 6, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, + { id: 7, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, + { id: 8, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, + { id: 9, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, + ] + txMetas.forEach((txMeta) => txController.addTx(txMeta, noop)) + let filterParams + + filterParams = { status: 'unapproved', from: '0xaa' } + assert.equal(txController.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`) + filterParams = { status: 'unapproved', to: '0xaa' } + assert.equal(txController.getFilteredTxList(filterParams).length, 2, `getFilteredTxList - ${JSON.stringify(filterParams)}`) + filterParams = { status: 'confirmed', from: '0xbb' } + assert.equal(txController.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`) + filterParams = { status: 'confirmed' } + assert.equal(txController.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`) + filterParams = { from: '0xaa' } + assert.equal(txController.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`) + filterParams = { to: '0xaa' } + assert.equal(txController.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`) + }) + }) + + describe('#sign replay-protected tx', function () { + it('prepares a tx with the chainId set', function () { + txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) + txController.signTransaction('1', (err, rawTx) => { + if (err) return assert.fail('it should not fail') + const ethTx = new EthTx(ethUtil.toBuffer(rawTx)) + assert.equal(ethTx.getChainId(), currentNetworkId) + }) + }) + }) +}) diff --git a/test/unit/tx-manager-test.js b/test/unit/tx-manager-test.js deleted file mode 100644 index b5d148723..000000000 --- a/test/unit/tx-manager-test.js +++ /dev/null @@ -1,237 +0,0 @@ -const assert = require('assert') -const EventEmitter = require('events') -const ethUtil = require('ethereumjs-util') -const EthTx = require('ethereumjs-tx') -const ObservableStore = require('obs-store') -const TransactionManager = require('../../app/scripts/transaction-manager') -const noop = () => true -const currentNetworkId = 42 -const otherNetworkId = 36 -const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex') - -describe('Transaction Manager', function () { - let txManager - - beforeEach(function () { - txManager = new TransactionManager({ - networkStore: new ObservableStore({ network: currentNetworkId }), - txHistoryLimit: 10, - blockTracker: new EventEmitter(), - signTransaction: (ethTx) => new Promise((resolve) => { - ethTx.sign(privKey) - resolve() - }), - }) - }) - - describe('#validateTxParams', function () { - it('returns null for positive values', function () { - var sample = { - value: '0x01', - } - txManager.txProviderUtils.validateTxParams(sample, (err) => { - assert.equal(err, null, 'no error') - }) - }) - - it('returns error for negative values', function () { - var sample = { - value: '-0x01', - } - txManager.txProviderUtils.validateTxParams(sample, (err) => { - assert.ok(err, 'error') - }) - }) - }) - - describe('#getTxList', function () { - it('when new should return empty array', function () { - var result = txManager.getTxList() - assert.ok(Array.isArray(result)) - assert.equal(result.length, 0) - }) - it('should also return transactions from local storage if any', function () { - - }) - }) - - describe('#addTx', function () { - it('adds a tx returned in getTxList', function () { - var tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } - txManager.addTx(tx, noop) - var result = txManager.getTxList() - assert.ok(Array.isArray(result)) - assert.equal(result.length, 1) - assert.equal(result[0].id, 1) - }) - - it('does not override txs from other networks', function () { - var tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } - var tx2 = { id: 2, status: 'confirmed', metamaskNetworkId: otherNetworkId, txParams: {} } - txManager.addTx(tx, noop) - txManager.addTx(tx2, noop) - var result = txManager.getFullTxList() - var result2 = txManager.getTxList() - assert.equal(result.length, 2, 'txs were deleted') - assert.equal(result2.length, 1, 'incorrect number of txs on network.') - }) - - it('cuts off early txs beyond a limit', function () { - const limit = txManager.txHistoryLimit - for (let i = 0; i < limit + 1; i++) { - const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } - txManager.addTx(tx, noop) - } - var result = txManager.getTxList() - assert.equal(result.length, limit, `limit of ${limit} txs enforced`) - assert.equal(result[0].id, 1, 'early txs truncted') - }) - - it('cuts off early txs beyond a limit whether or not it is confirmed or rejected', function () { - const limit = txManager.txHistoryLimit - for (let i = 0; i < limit + 1; i++) { - const tx = { id: i, time: new Date(), status: 'rejected', metamaskNetworkId: currentNetworkId, txParams: {} } - txManager.addTx(tx, noop) - } - var result = txManager.getTxList() - assert.equal(result.length, limit, `limit of ${limit} txs enforced`) - assert.equal(result[0].id, 1, 'early txs truncted') - }) - - it('cuts off early txs beyond a limit but does not cut unapproved txs', function () { - var unconfirmedTx = { id: 0, time: new Date(), status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } - txManager.addTx(unconfirmedTx, noop) - const limit = txManager.txHistoryLimit - for (let i = 1; i < limit + 1; i++) { - const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } - txManager.addTx(tx, noop) - } - var result = txManager.getTxList() - assert.equal(result.length, limit, `limit of ${limit} txs enforced`) - assert.equal(result[0].id, 0, 'first tx should still be there') - assert.equal(result[0].status, 'unapproved', 'first tx should be unapproved') - assert.equal(result[1].id, 2, 'early txs truncted') - }) - }) - - describe('#setTxStatusSigned', function () { - it('sets the tx status to signed', function () { - var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } - txManager.addTx(tx, noop) - txManager.setTxStatusSigned(1) - var result = txManager.getTxList() - assert.ok(Array.isArray(result)) - assert.equal(result.length, 1) - assert.equal(result[0].status, 'signed') - }) - - it('should emit a signed event to signal the exciton of callback', (done) => { - this.timeout(10000) - var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } - const noop = function () { - assert(true, 'event listener has been triggered and noop executed') - done() - } - txManager.addTx(tx) - txManager.on('1:signed', noop) - txManager.setTxStatusSigned(1) - }) - }) - - describe('#setTxStatusRejected', function () { - it('sets the tx status to rejected', function () { - var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } - txManager.addTx(tx) - txManager.setTxStatusRejected(1) - var result = txManager.getTxList() - assert.ok(Array.isArray(result)) - assert.equal(result.length, 1) - assert.equal(result[0].status, 'rejected') - }) - - it('should emit a rejected event to signal the exciton of callback', (done) => { - this.timeout(10000) - var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } - txManager.addTx(tx) - const noop = function (err, txId) { - assert(true, 'event listener has been triggered and noop executed') - done() - } - txManager.on('1:rejected', noop) - txManager.setTxStatusRejected(1) - }) - }) - - describe('#updateTx', function () { - it('replaces the tx with the same id', function () { - txManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) - txManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) - txManager.updateTx({ id: '1', status: 'blah', hash: 'foo', metamaskNetworkId: currentNetworkId, txParams: {} }) - var result = txManager.getTx('1') - assert.equal(result.hash, 'foo') - }) - }) - - describe('#getUnapprovedTxList', function () { - it('returns unapproved txs in a hash', function () { - txManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) - txManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) - const result = txManager.getUnapprovedTxList() - assert.equal(typeof result, 'object') - assert.equal(result['1'].status, 'unapproved') - assert.equal(result['2'], undefined) - }) - }) - - describe('#getTx', function () { - it('returns a tx with the requested id', function () { - txManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) - txManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) - assert.equal(txManager.getTx('1').status, 'unapproved') - assert.equal(txManager.getTx('2').status, 'confirmed') - }) - }) - - describe('#getFilteredTxList', function () { - it('returns a tx with the requested data', function () { - const txMetas = [ - { id: 0, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, - { id: 1, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, - { id: 2, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, - { id: 3, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, - { id: 4, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, - { id: 5, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, - { id: 6, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, - { id: 7, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, - { id: 8, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, - { id: 9, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, - ] - txMetas.forEach((txMeta) => txManager.addTx(txMeta, noop)) - let filterParams - - filterParams = { status: 'unapproved', from: '0xaa' } - assert.equal(txManager.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`) - filterParams = { status: 'unapproved', to: '0xaa' } - assert.equal(txManager.getFilteredTxList(filterParams).length, 2, `getFilteredTxList - ${JSON.stringify(filterParams)}`) - filterParams = { status: 'confirmed', from: '0xbb' } - assert.equal(txManager.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`) - filterParams = { status: 'confirmed' } - assert.equal(txManager.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`) - filterParams = { from: '0xaa' } - assert.equal(txManager.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`) - filterParams = { to: '0xaa' } - assert.equal(txManager.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`) - }) - }) - - describe('#sign replay-protected tx', function () { - it('prepares a tx with the chainId set', function () { - txManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) - txManager.signTransaction('1', (err, rawTx) => { - if (err) return assert.fail('it should not fail') - const ethTx = new EthTx(ethUtil.toBuffer(rawTx)) - assert.equal(ethTx.getChainId(), currentNetworkId) - }) - }) - }) -}) -- cgit v1.2.3 From 2df9344be5bf1c65daae2ca7ea47982fe2a1c2fb Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 16 May 2017 10:27:41 -0700 Subject: Rename tx manager to tx controller --- app/scripts/background.js | 4 +- app/scripts/controllers/transactions.js | 404 ++++++++++++++++++++++++++++++++ app/scripts/metamask-controller.js | 32 +-- app/scripts/transaction-manager.js | 404 -------------------------------- test/unit/tx-controller-test.js | 237 +++++++++++++++++++ test/unit/tx-manager-test.js | 237 ------------------- 6 files changed, 659 insertions(+), 659 deletions(-) create mode 100644 app/scripts/controllers/transactions.js delete mode 100644 app/scripts/transaction-manager.js create mode 100644 test/unit/tx-controller-test.js delete mode 100644 test/unit/tx-manager-test.js diff --git a/app/scripts/background.js b/app/scripts/background.js index e738a9712..63c8a7252 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -114,13 +114,13 @@ function setupController (initState) { // updateBadge() - controller.txManager.on('updateBadge', updateBadge) + controller.txController.on('updateBadge', updateBadge) controller.messageManager.on('updateBadge', updateBadge) // plugin badge text function updateBadge () { var label = '' - var unapprovedTxCount = controller.txManager.unapprovedTxCount + var unapprovedTxCount = controller.txController.unapprovedTxCount var unapprovedMsgCount = controller.messageManager.unapprovedMsgCount var count = unapprovedTxCount + unapprovedMsgCount if (count) { diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js new file mode 100644 index 000000000..9f267160f --- /dev/null +++ b/app/scripts/controllers/transactions.js @@ -0,0 +1,404 @@ +const EventEmitter = require('events') +const async = require('async') +const extend = require('xtend') +const Semaphore = require('semaphore') +const ObservableStore = require('obs-store') +const ethUtil = require('ethereumjs-util') +const EthQuery = require('eth-query') +const TxProviderUtil = require('./lib/tx-utils') +const createId = require('./lib/random-id') + +module.exports = class TransactionManager extends EventEmitter { + constructor (opts) { + super() + this.store = new ObservableStore(extend({ + transactions: [], + }, opts.initState)) + this.memStore = new ObservableStore({}) + this.networkStore = opts.networkStore || new ObservableStore({}) + this.preferencesStore = opts.preferencesStore || new ObservableStore({}) + this.txHistoryLimit = opts.txHistoryLimit + this.provider = opts.provider + this.blockTracker = opts.blockTracker + this.query = new EthQuery(this.provider) + this.txProviderUtils = new TxProviderUtil(this.provider) + this.blockTracker.on('block', this.checkForTxInBlock.bind(this)) + this.signEthTx = opts.signTransaction + this.nonceLock = Semaphore(1) + + // memstore is computed from a few different stores + this._updateMemstore() + this.store.subscribe(() => this._updateMemstore()) + this.networkStore.subscribe(() => this._updateMemstore()) + this.preferencesStore.subscribe(() => this._updateMemstore()) + } + + getState () { + return this.memStore.getState() + } + + getNetwork () { + return this.networkStore.getState().network + } + + getSelectedAddress () { + return this.preferencesStore.getState().selectedAddress + } + + // Returns the tx list + getTxList () { + const network = this.getNetwork() + const fullTxList = this.getFullTxList() + return fullTxList.filter(txMeta => txMeta.metamaskNetworkId === network) + } + + // Returns the number of txs for the current network. + getTxCount () { + return this.getTxList().length + } + + // Returns the full tx list across all networks + getFullTxList () { + return this.store.getState().transactions + } + + // Adds a tx to the txlist + addTx (txMeta) { + const txCount = this.getTxCount() + const network = this.getNetwork() + const fullTxList = this.getFullTxList() + const txHistoryLimit = this.txHistoryLimit + + // checks if the length of the tx history is + // longer then desired persistence limit + // and then if it is removes only confirmed + // or rejected tx's. + // not tx's that are pending or unapproved + if (txCount > txHistoryLimit - 1) { + var index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId)) + fullTxList.splice(index, 1) + } + fullTxList.push(txMeta) + this._saveTxList(fullTxList) + this.emit('update') + + this.once(`${txMeta.id}:signed`, function (txId) { + this.removeAllListeners(`${txMeta.id}:rejected`) + }) + this.once(`${txMeta.id}:rejected`, function (txId) { + this.removeAllListeners(`${txMeta.id}:signed`) + }) + + this.emit('updateBadge') + this.emit(`${txMeta.id}:unapproved`, txMeta) + } + + // gets tx by Id and returns it + getTx (txId, cb) { + var txList = this.getTxList() + var txMeta = txList.find(txData => txData.id === txId) + return cb ? cb(txMeta) : txMeta + } + + // + updateTx (txMeta) { + var txId = txMeta.id + var txList = this.getFullTxList() + var index = txList.findIndex(txData => txData.id === txId) + txList[index] = txMeta + this._saveTxList(txList) + this.emit('update') + } + + get unapprovedTxCount () { + return Object.keys(this.getUnapprovedTxList()).length + } + + get pendingTxCount () { + return this.getTxsByMetaData('status', 'signed').length + } + + addUnapprovedTransaction (txParams, done) { + let txMeta + async.waterfall([ + // validate + (cb) => this.txProviderUtils.validateTxParams(txParams, cb), + // construct txMeta + (cb) => { + txMeta = { + id: createId(), + time: (new Date()).getTime(), + status: 'unapproved', + metamaskNetworkId: this.getNetwork(), + txParams: txParams, + } + cb() + }, + // add default tx params + (cb) => this.addTxDefaults(txMeta, cb), + // save txMeta + (cb) => { + this.addTx(txMeta) + cb(null, txMeta) + }, + ], done) + } + + addTxDefaults (txMeta, cb) { + const txParams = txMeta.txParams + // ensure value + txParams.value = txParams.value || '0x0' + this.query.gasPrice((err, gasPrice) => { + if (err) return cb(err) + // set gasPrice + txParams.gasPrice = gasPrice + // set gasLimit + this.txProviderUtils.analyzeGasUsage(txMeta, cb) + }) + } + + getUnapprovedTxList () { + var txList = this.getTxList() + return txList.filter((txMeta) => txMeta.status === 'unapproved') + .reduce((result, tx) => { + result[tx.id] = tx + return result + }, {}) + } + + approveTransaction (txId, cb = warn) { + const self = this + // approve + self.setTxStatusApproved(txId) + // only allow one tx at a time for atomic nonce usage + self.nonceLock.take(() => { + // begin signature process + async.waterfall([ + (cb) => self.fillInTxParams(txId, cb), + (cb) => self.signTransaction(txId, cb), + (rawTx, cb) => self.publishTransaction(txId, rawTx, cb), + ], (err) => { + self.nonceLock.leave() + if (err) { + this.setTxStatusFailed(txId, { + errCode: err.errCode || err, + message: err.message || 'Transaction failed during approval', + }) + return cb(err) + } + cb() + }) + }) + } + + cancelTransaction (txId, cb = warn) { + this.setTxStatusRejected(txId) + cb() + } + + fillInTxParams (txId, cb) { + const txMeta = this.getTx(txId) + this.txProviderUtils.fillInTxParams(txMeta.txParams, (err) => { + if (err) return cb(err) + this.updateTx(txMeta) + cb() + }) + } + + getChainId () { + const networkState = this.networkStore.getState() + const getChainId = parseInt(networkState.network) + if (Number.isNaN(getChainId)) { + return 0 + } else { + return getChainId + } + } + + signTransaction (txId, cb) { + const txMeta = this.getTx(txId) + const txParams = txMeta.txParams + const fromAddress = txParams.from + // add network/chain id + txParams.chainId = this.getChainId() + const ethTx = this.txProviderUtils.buildEthTxFromParams(txParams) + this.signEthTx(ethTx, fromAddress).then(() => { + this.setTxStatusSigned(txMeta.id) + cb(null, ethUtil.bufferToHex(ethTx.serialize())) + }).catch((err) => { + cb(err) + }) + } + + publishTransaction (txId, rawTx, cb) { + this.txProviderUtils.publishTransaction(rawTx, (err, txHash) => { + if (err) return cb(err) + this.setTxHash(txId, txHash) + this.setTxStatusSubmitted(txId) + cb() + }) + } + + // receives a txHash records the tx as signed + setTxHash (txId, txHash) { + // Add the tx hash to the persisted meta-tx object + const txMeta = this.getTx(txId) + txMeta.hash = txHash + this.updateTx(txMeta) + } + + /* + Takes an object of fields to search for eg: + var thingsToLookFor = { + to: '0x0..', + from: '0x0..', + status: 'signed', + } + and returns a list of tx with all + options matching + + this is for things like filtering a the tx list + for only tx's from 1 account + or for filltering for all txs from one account + and that have been 'confirmed' + */ + getFilteredTxList (opts) { + var filteredTxList + Object.keys(opts).forEach((key) => { + filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList) + }) + return filteredTxList + } + + getTxsByMetaData (key, value, txList = this.getTxList()) { + return txList.filter((txMeta) => { + if (txMeta.txParams[key]) { + return txMeta.txParams[key] === value + } else { + return txMeta[key] === value + } + }) + } + + // STATUS METHODS + // get::set status + + // should return the status of the tx. + getTxStatus (txId) { + const txMeta = this.getTx(txId) + return txMeta.status + } + + // should update the status of the tx to 'rejected'. + setTxStatusRejected (txId) { + this._setTxStatus(txId, 'rejected') + } + + // should update the status of the tx to 'approved'. + setTxStatusApproved (txId) { + this._setTxStatus(txId, 'approved') + } + + // should update the status of the tx to 'signed'. + setTxStatusSigned (txId) { + this._setTxStatus(txId, 'signed') + } + + // should update the status of the tx to 'submitted'. + setTxStatusSubmitted (txId) { + this._setTxStatus(txId, 'submitted') + } + + // should update the status of the tx to 'confirmed'. + setTxStatusConfirmed (txId) { + this._setTxStatus(txId, 'confirmed') + } + + setTxStatusFailed (txId, reason) { + const txMeta = this.getTx(txId) + txMeta.err = reason + this.updateTx(txMeta) + this._setTxStatus(txId, 'failed') + } + + // merges txParams obj onto txData.txParams + // use extend to ensure that all fields are filled + updateTxParams (txId, txParams) { + var txMeta = this.getTx(txId) + txMeta.txParams = extend(txMeta.txParams, txParams) + this.updateTx(txMeta) + } + + // checks if a signed tx is in a block and + // if included sets the tx status as 'confirmed' + checkForTxInBlock () { + var signedTxList = this.getFilteredTxList({status: 'submitted'}) + if (!signedTxList.length) return + signedTxList.forEach((txMeta) => { + var txHash = txMeta.hash + var txId = txMeta.id + if (!txHash) { + const errReason = { + errCode: 'No hash was provided', + message: 'We had an error while submitting this transaction, please try again.', + } + return this.setTxStatusFailed(txId, errReason) + } + this.query.getTransactionByHash(txHash, (err, txParams) => { + if (err || !txParams) { + if (!txParams) return + txMeta.err = { + isWarning: true, + errorCode: err, + message: 'There was a problem loading this transaction.', + } + this.updateTx(txMeta) + return console.error(err) + } + if (txParams.blockNumber) { + this.setTxStatusConfirmed(txId) + } + }) + }) + } + + // PRIVATE METHODS + + // Should find the tx in the tx list and + // update it. + // should set the status in txData + // - `'unapproved'` the user has not responded + // - `'rejected'` the user has responded no! + // - `'approved'` the user has approved the tx + // - `'signed'` the tx is signed + // - `'submitted'` the tx is sent to a server + // - `'confirmed'` the tx has been included in a block. + _setTxStatus (txId, status) { + var txMeta = this.getTx(txId) + txMeta.status = status + this.emit(`${txMeta.id}:${status}`, txId) + if (status === 'submitted' || status === 'rejected') { + this.emit(`${txMeta.id}:finished`, txMeta) + } + this.updateTx(txMeta) + this.emit('updateBadge') + } + + // Saves the new/updated txList. + // Function is intended only for internal use + _saveTxList (transactions) { + this.store.updateState({ transactions }) + } + + _updateMemstore () { + const unapprovedTxs = this.getUnapprovedTxList() + const selectedAddressTxList = this.getFilteredTxList({ + from: this.getSelectedAddress(), + metamaskNetworkId: this.getNetwork(), + }) + this.memStore.updateState({ unapprovedTxs, selectedAddressTxList }) + } +} + + +const warn = () => console.warn('warn was used no cb provided') diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 175602ec1..f18da9033 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -17,7 +17,7 @@ const ShapeShiftController = require('./controllers/shapeshift') const AddressBookController = require('./controllers/address-book') const MessageManager = require('./lib/message-manager') const PersonalMessageManager = require('./lib/personal-message-manager') -const TxManager = require('./transaction-manager') +const TransactionController = require('./controllers/transactions') const ConfigManager = require('./lib/config-manager') const autoFaucet = require('./lib/auto-faucet') const nodeify = require('./lib/nodeify') @@ -90,8 +90,8 @@ module.exports = class MetamaskController extends EventEmitter { }, this.keyringController) // tx mgmt - this.txManager = new TxManager({ - initState: initState.TransactionManager, + this.txController = new TransactionController({ + initState: initState.TransactionController || initState.TransactionManager, networkStore: this.networkStore, preferencesStore: this.preferencesController.store, txHistoryLimit: 40, @@ -119,8 +119,8 @@ module.exports = class MetamaskController extends EventEmitter { this.publicConfigStore = this.initPublicConfigStore() // manual disk state subscriptions - this.txManager.store.subscribe((state) => { - this.store.updateState({ TransactionManager: state }) + this.txController.store.subscribe((state) => { + this.store.updateState({ TransactionController: state }) }) this.keyringController.store.subscribe((state) => { this.store.updateState({ KeyringController: state }) @@ -144,7 +144,7 @@ module.exports = class MetamaskController extends EventEmitter { // manual mem state subscriptions this.networkStore.subscribe(this.sendUpdate.bind(this)) this.ethStore.subscribe(this.sendUpdate.bind(this)) - this.txManager.memStore.subscribe(this.sendUpdate.bind(this)) + this.txController.memStore.subscribe(this.sendUpdate.bind(this)) this.messageManager.memStore.subscribe(this.sendUpdate.bind(this)) this.personalMessageManager.memStore.subscribe(this.sendUpdate.bind(this)) this.keyringController.memStore.subscribe(this.sendUpdate.bind(this)) @@ -223,7 +223,7 @@ module.exports = class MetamaskController extends EventEmitter { }, this.networkStore.getState(), this.ethStore.getState(), - this.txManager.memStore.getState(), + this.txController.memStore.getState(), this.messageManager.memStore.getState(), this.personalMessageManager.memStore.getState(), this.keyringController.memStore.getState(), @@ -248,7 +248,7 @@ module.exports = class MetamaskController extends EventEmitter { getApi () { const keyringController = this.keyringController const preferencesController = this.preferencesController - const txManager = this.txManager + const txController = this.txController const noticeController = this.noticeController const addressBookController = this.addressBookController @@ -289,9 +289,9 @@ module.exports = class MetamaskController extends EventEmitter { saveAccountLabel: nodeify(keyringController.saveAccountLabel).bind(keyringController), exportAccount: nodeify(keyringController.exportAccount).bind(keyringController), - // txManager - approveTransaction: txManager.approveTransaction.bind(txManager), - cancelTransaction: txManager.cancelTransaction.bind(txManager), + // txController + approveTransaction: txController.approveTransaction.bind(txController), + cancelTransaction: txController.cancelTransaction.bind(txController), updateAndApproveTransaction: this.updateAndApproveTx.bind(this), // messageManager @@ -421,12 +421,12 @@ module.exports = class MetamaskController extends EventEmitter { newUnapprovedTransaction (txParams, cb) { log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) const self = this - self.txManager.addUnapprovedTransaction(txParams, (err, txMeta) => { + self.txController.addUnapprovedTransaction(txParams, (err, txMeta) => { if (err) return cb(err) self.sendUpdate() self.opts.showUnapprovedTx(txMeta) // listen for tx completion (success, fail) - self.txManager.once(`${txMeta.id}:finished`, (completedTx) => { + self.txController.once(`${txMeta.id}:finished`, (completedTx) => { switch (completedTx.status) { case 'submitted': return cb(null, completedTx.hash) @@ -477,9 +477,9 @@ module.exports = class MetamaskController extends EventEmitter { updateAndApproveTx (txMeta, cb) { log.debug(`MetaMaskController - updateAndApproveTx: ${JSON.stringify(txMeta)}`) - const txManager = this.txManager - txManager.updateTx(txMeta) - txManager.approveTransaction(txMeta.id, cb) + const txController = this.txController + txController.updateTx(txMeta) + txController.approveTransaction(txMeta.id, cb) } signMessage (msgParams, cb) { diff --git a/app/scripts/transaction-manager.js b/app/scripts/transaction-manager.js deleted file mode 100644 index 9f267160f..000000000 --- a/app/scripts/transaction-manager.js +++ /dev/null @@ -1,404 +0,0 @@ -const EventEmitter = require('events') -const async = require('async') -const extend = require('xtend') -const Semaphore = require('semaphore') -const ObservableStore = require('obs-store') -const ethUtil = require('ethereumjs-util') -const EthQuery = require('eth-query') -const TxProviderUtil = require('./lib/tx-utils') -const createId = require('./lib/random-id') - -module.exports = class TransactionManager extends EventEmitter { - constructor (opts) { - super() - this.store = new ObservableStore(extend({ - transactions: [], - }, opts.initState)) - this.memStore = new ObservableStore({}) - this.networkStore = opts.networkStore || new ObservableStore({}) - this.preferencesStore = opts.preferencesStore || new ObservableStore({}) - this.txHistoryLimit = opts.txHistoryLimit - this.provider = opts.provider - this.blockTracker = opts.blockTracker - this.query = new EthQuery(this.provider) - this.txProviderUtils = new TxProviderUtil(this.provider) - this.blockTracker.on('block', this.checkForTxInBlock.bind(this)) - this.signEthTx = opts.signTransaction - this.nonceLock = Semaphore(1) - - // memstore is computed from a few different stores - this._updateMemstore() - this.store.subscribe(() => this._updateMemstore()) - this.networkStore.subscribe(() => this._updateMemstore()) - this.preferencesStore.subscribe(() => this._updateMemstore()) - } - - getState () { - return this.memStore.getState() - } - - getNetwork () { - return this.networkStore.getState().network - } - - getSelectedAddress () { - return this.preferencesStore.getState().selectedAddress - } - - // Returns the tx list - getTxList () { - const network = this.getNetwork() - const fullTxList = this.getFullTxList() - return fullTxList.filter(txMeta => txMeta.metamaskNetworkId === network) - } - - // Returns the number of txs for the current network. - getTxCount () { - return this.getTxList().length - } - - // Returns the full tx list across all networks - getFullTxList () { - return this.store.getState().transactions - } - - // Adds a tx to the txlist - addTx (txMeta) { - const txCount = this.getTxCount() - const network = this.getNetwork() - const fullTxList = this.getFullTxList() - const txHistoryLimit = this.txHistoryLimit - - // checks if the length of the tx history is - // longer then desired persistence limit - // and then if it is removes only confirmed - // or rejected tx's. - // not tx's that are pending or unapproved - if (txCount > txHistoryLimit - 1) { - var index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId)) - fullTxList.splice(index, 1) - } - fullTxList.push(txMeta) - this._saveTxList(fullTxList) - this.emit('update') - - this.once(`${txMeta.id}:signed`, function (txId) { - this.removeAllListeners(`${txMeta.id}:rejected`) - }) - this.once(`${txMeta.id}:rejected`, function (txId) { - this.removeAllListeners(`${txMeta.id}:signed`) - }) - - this.emit('updateBadge') - this.emit(`${txMeta.id}:unapproved`, txMeta) - } - - // gets tx by Id and returns it - getTx (txId, cb) { - var txList = this.getTxList() - var txMeta = txList.find(txData => txData.id === txId) - return cb ? cb(txMeta) : txMeta - } - - // - updateTx (txMeta) { - var txId = txMeta.id - var txList = this.getFullTxList() - var index = txList.findIndex(txData => txData.id === txId) - txList[index] = txMeta - this._saveTxList(txList) - this.emit('update') - } - - get unapprovedTxCount () { - return Object.keys(this.getUnapprovedTxList()).length - } - - get pendingTxCount () { - return this.getTxsByMetaData('status', 'signed').length - } - - addUnapprovedTransaction (txParams, done) { - let txMeta - async.waterfall([ - // validate - (cb) => this.txProviderUtils.validateTxParams(txParams, cb), - // construct txMeta - (cb) => { - txMeta = { - id: createId(), - time: (new Date()).getTime(), - status: 'unapproved', - metamaskNetworkId: this.getNetwork(), - txParams: txParams, - } - cb() - }, - // add default tx params - (cb) => this.addTxDefaults(txMeta, cb), - // save txMeta - (cb) => { - this.addTx(txMeta) - cb(null, txMeta) - }, - ], done) - } - - addTxDefaults (txMeta, cb) { - const txParams = txMeta.txParams - // ensure value - txParams.value = txParams.value || '0x0' - this.query.gasPrice((err, gasPrice) => { - if (err) return cb(err) - // set gasPrice - txParams.gasPrice = gasPrice - // set gasLimit - this.txProviderUtils.analyzeGasUsage(txMeta, cb) - }) - } - - getUnapprovedTxList () { - var txList = this.getTxList() - return txList.filter((txMeta) => txMeta.status === 'unapproved') - .reduce((result, tx) => { - result[tx.id] = tx - return result - }, {}) - } - - approveTransaction (txId, cb = warn) { - const self = this - // approve - self.setTxStatusApproved(txId) - // only allow one tx at a time for atomic nonce usage - self.nonceLock.take(() => { - // begin signature process - async.waterfall([ - (cb) => self.fillInTxParams(txId, cb), - (cb) => self.signTransaction(txId, cb), - (rawTx, cb) => self.publishTransaction(txId, rawTx, cb), - ], (err) => { - self.nonceLock.leave() - if (err) { - this.setTxStatusFailed(txId, { - errCode: err.errCode || err, - message: err.message || 'Transaction failed during approval', - }) - return cb(err) - } - cb() - }) - }) - } - - cancelTransaction (txId, cb = warn) { - this.setTxStatusRejected(txId) - cb() - } - - fillInTxParams (txId, cb) { - const txMeta = this.getTx(txId) - this.txProviderUtils.fillInTxParams(txMeta.txParams, (err) => { - if (err) return cb(err) - this.updateTx(txMeta) - cb() - }) - } - - getChainId () { - const networkState = this.networkStore.getState() - const getChainId = parseInt(networkState.network) - if (Number.isNaN(getChainId)) { - return 0 - } else { - return getChainId - } - } - - signTransaction (txId, cb) { - const txMeta = this.getTx(txId) - const txParams = txMeta.txParams - const fromAddress = txParams.from - // add network/chain id - txParams.chainId = this.getChainId() - const ethTx = this.txProviderUtils.buildEthTxFromParams(txParams) - this.signEthTx(ethTx, fromAddress).then(() => { - this.setTxStatusSigned(txMeta.id) - cb(null, ethUtil.bufferToHex(ethTx.serialize())) - }).catch((err) => { - cb(err) - }) - } - - publishTransaction (txId, rawTx, cb) { - this.txProviderUtils.publishTransaction(rawTx, (err, txHash) => { - if (err) return cb(err) - this.setTxHash(txId, txHash) - this.setTxStatusSubmitted(txId) - cb() - }) - } - - // receives a txHash records the tx as signed - setTxHash (txId, txHash) { - // Add the tx hash to the persisted meta-tx object - const txMeta = this.getTx(txId) - txMeta.hash = txHash - this.updateTx(txMeta) - } - - /* - Takes an object of fields to search for eg: - var thingsToLookFor = { - to: '0x0..', - from: '0x0..', - status: 'signed', - } - and returns a list of tx with all - options matching - - this is for things like filtering a the tx list - for only tx's from 1 account - or for filltering for all txs from one account - and that have been 'confirmed' - */ - getFilteredTxList (opts) { - var filteredTxList - Object.keys(opts).forEach((key) => { - filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList) - }) - return filteredTxList - } - - getTxsByMetaData (key, value, txList = this.getTxList()) { - return txList.filter((txMeta) => { - if (txMeta.txParams[key]) { - return txMeta.txParams[key] === value - } else { - return txMeta[key] === value - } - }) - } - - // STATUS METHODS - // get::set status - - // should return the status of the tx. - getTxStatus (txId) { - const txMeta = this.getTx(txId) - return txMeta.status - } - - // should update the status of the tx to 'rejected'. - setTxStatusRejected (txId) { - this._setTxStatus(txId, 'rejected') - } - - // should update the status of the tx to 'approved'. - setTxStatusApproved (txId) { - this._setTxStatus(txId, 'approved') - } - - // should update the status of the tx to 'signed'. - setTxStatusSigned (txId) { - this._setTxStatus(txId, 'signed') - } - - // should update the status of the tx to 'submitted'. - setTxStatusSubmitted (txId) { - this._setTxStatus(txId, 'submitted') - } - - // should update the status of the tx to 'confirmed'. - setTxStatusConfirmed (txId) { - this._setTxStatus(txId, 'confirmed') - } - - setTxStatusFailed (txId, reason) { - const txMeta = this.getTx(txId) - txMeta.err = reason - this.updateTx(txMeta) - this._setTxStatus(txId, 'failed') - } - - // merges txParams obj onto txData.txParams - // use extend to ensure that all fields are filled - updateTxParams (txId, txParams) { - var txMeta = this.getTx(txId) - txMeta.txParams = extend(txMeta.txParams, txParams) - this.updateTx(txMeta) - } - - // checks if a signed tx is in a block and - // if included sets the tx status as 'confirmed' - checkForTxInBlock () { - var signedTxList = this.getFilteredTxList({status: 'submitted'}) - if (!signedTxList.length) return - signedTxList.forEach((txMeta) => { - var txHash = txMeta.hash - var txId = txMeta.id - if (!txHash) { - const errReason = { - errCode: 'No hash was provided', - message: 'We had an error while submitting this transaction, please try again.', - } - return this.setTxStatusFailed(txId, errReason) - } - this.query.getTransactionByHash(txHash, (err, txParams) => { - if (err || !txParams) { - if (!txParams) return - txMeta.err = { - isWarning: true, - errorCode: err, - message: 'There was a problem loading this transaction.', - } - this.updateTx(txMeta) - return console.error(err) - } - if (txParams.blockNumber) { - this.setTxStatusConfirmed(txId) - } - }) - }) - } - - // PRIVATE METHODS - - // Should find the tx in the tx list and - // update it. - // should set the status in txData - // - `'unapproved'` the user has not responded - // - `'rejected'` the user has responded no! - // - `'approved'` the user has approved the tx - // - `'signed'` the tx is signed - // - `'submitted'` the tx is sent to a server - // - `'confirmed'` the tx has been included in a block. - _setTxStatus (txId, status) { - var txMeta = this.getTx(txId) - txMeta.status = status - this.emit(`${txMeta.id}:${status}`, txId) - if (status === 'submitted' || status === 'rejected') { - this.emit(`${txMeta.id}:finished`, txMeta) - } - this.updateTx(txMeta) - this.emit('updateBadge') - } - - // Saves the new/updated txList. - // Function is intended only for internal use - _saveTxList (transactions) { - this.store.updateState({ transactions }) - } - - _updateMemstore () { - const unapprovedTxs = this.getUnapprovedTxList() - const selectedAddressTxList = this.getFilteredTxList({ - from: this.getSelectedAddress(), - metamaskNetworkId: this.getNetwork(), - }) - this.memStore.updateState({ unapprovedTxs, selectedAddressTxList }) - } -} - - -const warn = () => console.warn('warn was used no cb provided') diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js new file mode 100644 index 000000000..d0b32ff41 --- /dev/null +++ b/test/unit/tx-controller-test.js @@ -0,0 +1,237 @@ +const assert = require('assert') +const EventEmitter = require('events') +const ethUtil = require('ethereumjs-util') +const EthTx = require('ethereumjs-tx') +const ObservableStore = require('obs-store') +const TransactionController = require('../../app/scripts/controllers/transactions') +const noop = () => true +const currentNetworkId = 42 +const otherNetworkId = 36 +const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex') + +describe('Transaction Manager', function () { + let txController + + beforeEach(function () { + txController = new TransactionController({ + networkStore: new ObservableStore({ network: currentNetworkId }), + txHistoryLimit: 10, + blockTracker: new EventEmitter(), + signTransaction: (ethTx) => new Promise((resolve) => { + ethTx.sign(privKey) + resolve() + }), + }) + }) + + describe('#validateTxParams', function () { + it('returns null for positive values', function () { + var sample = { + value: '0x01', + } + txController.txProviderUtils.validateTxParams(sample, (err) => { + assert.equal(err, null, 'no error') + }) + }) + + it('returns error for negative values', function () { + var sample = { + value: '-0x01', + } + txController.txProviderUtils.validateTxParams(sample, (err) => { + assert.ok(err, 'error') + }) + }) + }) + + describe('#getTxList', function () { + it('when new should return empty array', function () { + var result = txController.getTxList() + assert.ok(Array.isArray(result)) + assert.equal(result.length, 0) + }) + it('should also return transactions from local storage if any', function () { + + }) + }) + + describe('#addTx', function () { + it('adds a tx returned in getTxList', function () { + var tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } + txController.addTx(tx, noop) + var result = txController.getTxList() + assert.ok(Array.isArray(result)) + assert.equal(result.length, 1) + assert.equal(result[0].id, 1) + }) + + it('does not override txs from other networks', function () { + var tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } + var tx2 = { id: 2, status: 'confirmed', metamaskNetworkId: otherNetworkId, txParams: {} } + txController.addTx(tx, noop) + txController.addTx(tx2, noop) + var result = txController.getFullTxList() + var result2 = txController.getTxList() + assert.equal(result.length, 2, 'txs were deleted') + assert.equal(result2.length, 1, 'incorrect number of txs on network.') + }) + + it('cuts off early txs beyond a limit', function () { + const limit = txController.txHistoryLimit + for (let i = 0; i < limit + 1; i++) { + const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } + txController.addTx(tx, noop) + } + var result = txController.getTxList() + assert.equal(result.length, limit, `limit of ${limit} txs enforced`) + assert.equal(result[0].id, 1, 'early txs truncted') + }) + + it('cuts off early txs beyond a limit whether or not it is confirmed or rejected', function () { + const limit = txController.txHistoryLimit + for (let i = 0; i < limit + 1; i++) { + const tx = { id: i, time: new Date(), status: 'rejected', metamaskNetworkId: currentNetworkId, txParams: {} } + txController.addTx(tx, noop) + } + var result = txController.getTxList() + assert.equal(result.length, limit, `limit of ${limit} txs enforced`) + assert.equal(result[0].id, 1, 'early txs truncted') + }) + + it('cuts off early txs beyond a limit but does not cut unapproved txs', function () { + var unconfirmedTx = { id: 0, time: new Date(), status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } + txController.addTx(unconfirmedTx, noop) + const limit = txController.txHistoryLimit + for (let i = 1; i < limit + 1; i++) { + const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } + txController.addTx(tx, noop) + } + var result = txController.getTxList() + assert.equal(result.length, limit, `limit of ${limit} txs enforced`) + assert.equal(result[0].id, 0, 'first tx should still be there') + assert.equal(result[0].status, 'unapproved', 'first tx should be unapproved') + assert.equal(result[1].id, 2, 'early txs truncted') + }) + }) + + describe('#setTxStatusSigned', function () { + it('sets the tx status to signed', function () { + var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } + txController.addTx(tx, noop) + txController.setTxStatusSigned(1) + var result = txController.getTxList() + assert.ok(Array.isArray(result)) + assert.equal(result.length, 1) + assert.equal(result[0].status, 'signed') + }) + + it('should emit a signed event to signal the exciton of callback', (done) => { + this.timeout(10000) + var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } + const noop = function () { + assert(true, 'event listener has been triggered and noop executed') + done() + } + txController.addTx(tx) + txController.on('1:signed', noop) + txController.setTxStatusSigned(1) + }) + }) + + describe('#setTxStatusRejected', function () { + it('sets the tx status to rejected', function () { + var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } + txController.addTx(tx) + txController.setTxStatusRejected(1) + var result = txController.getTxList() + assert.ok(Array.isArray(result)) + assert.equal(result.length, 1) + assert.equal(result[0].status, 'rejected') + }) + + it('should emit a rejected event to signal the exciton of callback', (done) => { + this.timeout(10000) + var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } + txController.addTx(tx) + const noop = function (err, txId) { + assert(true, 'event listener has been triggered and noop executed') + done() + } + txController.on('1:rejected', noop) + txController.setTxStatusRejected(1) + }) + }) + + describe('#updateTx', function () { + it('replaces the tx with the same id', function () { + txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) + txController.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) + txController.updateTx({ id: '1', status: 'blah', hash: 'foo', metamaskNetworkId: currentNetworkId, txParams: {} }) + var result = txController.getTx('1') + assert.equal(result.hash, 'foo') + }) + }) + + describe('#getUnapprovedTxList', function () { + it('returns unapproved txs in a hash', function () { + txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) + txController.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) + const result = txController.getUnapprovedTxList() + assert.equal(typeof result, 'object') + assert.equal(result['1'].status, 'unapproved') + assert.equal(result['2'], undefined) + }) + }) + + describe('#getTx', function () { + it('returns a tx with the requested id', function () { + txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) + txController.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) + assert.equal(txController.getTx('1').status, 'unapproved') + assert.equal(txController.getTx('2').status, 'confirmed') + }) + }) + + describe('#getFilteredTxList', function () { + it('returns a tx with the requested data', function () { + const txMetas = [ + { id: 0, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, + { id: 1, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, + { id: 2, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, + { id: 3, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, + { id: 4, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, + { id: 5, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, + { id: 6, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, + { id: 7, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, + { id: 8, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, + { id: 9, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, + ] + txMetas.forEach((txMeta) => txController.addTx(txMeta, noop)) + let filterParams + + filterParams = { status: 'unapproved', from: '0xaa' } + assert.equal(txController.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`) + filterParams = { status: 'unapproved', to: '0xaa' } + assert.equal(txController.getFilteredTxList(filterParams).length, 2, `getFilteredTxList - ${JSON.stringify(filterParams)}`) + filterParams = { status: 'confirmed', from: '0xbb' } + assert.equal(txController.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`) + filterParams = { status: 'confirmed' } + assert.equal(txController.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`) + filterParams = { from: '0xaa' } + assert.equal(txController.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`) + filterParams = { to: '0xaa' } + assert.equal(txController.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`) + }) + }) + + describe('#sign replay-protected tx', function () { + it('prepares a tx with the chainId set', function () { + txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) + txController.signTransaction('1', (err, rawTx) => { + if (err) return assert.fail('it should not fail') + const ethTx = new EthTx(ethUtil.toBuffer(rawTx)) + assert.equal(ethTx.getChainId(), currentNetworkId) + }) + }) + }) +}) diff --git a/test/unit/tx-manager-test.js b/test/unit/tx-manager-test.js deleted file mode 100644 index b5d148723..000000000 --- a/test/unit/tx-manager-test.js +++ /dev/null @@ -1,237 +0,0 @@ -const assert = require('assert') -const EventEmitter = require('events') -const ethUtil = require('ethereumjs-util') -const EthTx = require('ethereumjs-tx') -const ObservableStore = require('obs-store') -const TransactionManager = require('../../app/scripts/transaction-manager') -const noop = () => true -const currentNetworkId = 42 -const otherNetworkId = 36 -const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex') - -describe('Transaction Manager', function () { - let txManager - - beforeEach(function () { - txManager = new TransactionManager({ - networkStore: new ObservableStore({ network: currentNetworkId }), - txHistoryLimit: 10, - blockTracker: new EventEmitter(), - signTransaction: (ethTx) => new Promise((resolve) => { - ethTx.sign(privKey) - resolve() - }), - }) - }) - - describe('#validateTxParams', function () { - it('returns null for positive values', function () { - var sample = { - value: '0x01', - } - txManager.txProviderUtils.validateTxParams(sample, (err) => { - assert.equal(err, null, 'no error') - }) - }) - - it('returns error for negative values', function () { - var sample = { - value: '-0x01', - } - txManager.txProviderUtils.validateTxParams(sample, (err) => { - assert.ok(err, 'error') - }) - }) - }) - - describe('#getTxList', function () { - it('when new should return empty array', function () { - var result = txManager.getTxList() - assert.ok(Array.isArray(result)) - assert.equal(result.length, 0) - }) - it('should also return transactions from local storage if any', function () { - - }) - }) - - describe('#addTx', function () { - it('adds a tx returned in getTxList', function () { - var tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } - txManager.addTx(tx, noop) - var result = txManager.getTxList() - assert.ok(Array.isArray(result)) - assert.equal(result.length, 1) - assert.equal(result[0].id, 1) - }) - - it('does not override txs from other networks', function () { - var tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } - var tx2 = { id: 2, status: 'confirmed', metamaskNetworkId: otherNetworkId, txParams: {} } - txManager.addTx(tx, noop) - txManager.addTx(tx2, noop) - var result = txManager.getFullTxList() - var result2 = txManager.getTxList() - assert.equal(result.length, 2, 'txs were deleted') - assert.equal(result2.length, 1, 'incorrect number of txs on network.') - }) - - it('cuts off early txs beyond a limit', function () { - const limit = txManager.txHistoryLimit - for (let i = 0; i < limit + 1; i++) { - const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } - txManager.addTx(tx, noop) - } - var result = txManager.getTxList() - assert.equal(result.length, limit, `limit of ${limit} txs enforced`) - assert.equal(result[0].id, 1, 'early txs truncted') - }) - - it('cuts off early txs beyond a limit whether or not it is confirmed or rejected', function () { - const limit = txManager.txHistoryLimit - for (let i = 0; i < limit + 1; i++) { - const tx = { id: i, time: new Date(), status: 'rejected', metamaskNetworkId: currentNetworkId, txParams: {} } - txManager.addTx(tx, noop) - } - var result = txManager.getTxList() - assert.equal(result.length, limit, `limit of ${limit} txs enforced`) - assert.equal(result[0].id, 1, 'early txs truncted') - }) - - it('cuts off early txs beyond a limit but does not cut unapproved txs', function () { - var unconfirmedTx = { id: 0, time: new Date(), status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } - txManager.addTx(unconfirmedTx, noop) - const limit = txManager.txHistoryLimit - for (let i = 1; i < limit + 1; i++) { - const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } - txManager.addTx(tx, noop) - } - var result = txManager.getTxList() - assert.equal(result.length, limit, `limit of ${limit} txs enforced`) - assert.equal(result[0].id, 0, 'first tx should still be there') - assert.equal(result[0].status, 'unapproved', 'first tx should be unapproved') - assert.equal(result[1].id, 2, 'early txs truncted') - }) - }) - - describe('#setTxStatusSigned', function () { - it('sets the tx status to signed', function () { - var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } - txManager.addTx(tx, noop) - txManager.setTxStatusSigned(1) - var result = txManager.getTxList() - assert.ok(Array.isArray(result)) - assert.equal(result.length, 1) - assert.equal(result[0].status, 'signed') - }) - - it('should emit a signed event to signal the exciton of callback', (done) => { - this.timeout(10000) - var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } - const noop = function () { - assert(true, 'event listener has been triggered and noop executed') - done() - } - txManager.addTx(tx) - txManager.on('1:signed', noop) - txManager.setTxStatusSigned(1) - }) - }) - - describe('#setTxStatusRejected', function () { - it('sets the tx status to rejected', function () { - var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } - txManager.addTx(tx) - txManager.setTxStatusRejected(1) - var result = txManager.getTxList() - assert.ok(Array.isArray(result)) - assert.equal(result.length, 1) - assert.equal(result[0].status, 'rejected') - }) - - it('should emit a rejected event to signal the exciton of callback', (done) => { - this.timeout(10000) - var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } - txManager.addTx(tx) - const noop = function (err, txId) { - assert(true, 'event listener has been triggered and noop executed') - done() - } - txManager.on('1:rejected', noop) - txManager.setTxStatusRejected(1) - }) - }) - - describe('#updateTx', function () { - it('replaces the tx with the same id', function () { - txManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) - txManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) - txManager.updateTx({ id: '1', status: 'blah', hash: 'foo', metamaskNetworkId: currentNetworkId, txParams: {} }) - var result = txManager.getTx('1') - assert.equal(result.hash, 'foo') - }) - }) - - describe('#getUnapprovedTxList', function () { - it('returns unapproved txs in a hash', function () { - txManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) - txManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) - const result = txManager.getUnapprovedTxList() - assert.equal(typeof result, 'object') - assert.equal(result['1'].status, 'unapproved') - assert.equal(result['2'], undefined) - }) - }) - - describe('#getTx', function () { - it('returns a tx with the requested id', function () { - txManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) - txManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) - assert.equal(txManager.getTx('1').status, 'unapproved') - assert.equal(txManager.getTx('2').status, 'confirmed') - }) - }) - - describe('#getFilteredTxList', function () { - it('returns a tx with the requested data', function () { - const txMetas = [ - { id: 0, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, - { id: 1, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, - { id: 2, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, - { id: 3, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, - { id: 4, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, - { id: 5, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, - { id: 6, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, - { id: 7, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, - { id: 8, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, - { id: 9, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, - ] - txMetas.forEach((txMeta) => txManager.addTx(txMeta, noop)) - let filterParams - - filterParams = { status: 'unapproved', from: '0xaa' } - assert.equal(txManager.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`) - filterParams = { status: 'unapproved', to: '0xaa' } - assert.equal(txManager.getFilteredTxList(filterParams).length, 2, `getFilteredTxList - ${JSON.stringify(filterParams)}`) - filterParams = { status: 'confirmed', from: '0xbb' } - assert.equal(txManager.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`) - filterParams = { status: 'confirmed' } - assert.equal(txManager.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`) - filterParams = { from: '0xaa' } - assert.equal(txManager.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`) - filterParams = { to: '0xaa' } - assert.equal(txManager.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`) - }) - }) - - describe('#sign replay-protected tx', function () { - it('prepares a tx with the chainId set', function () { - txManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) - txManager.signTransaction('1', (err, rawTx) => { - if (err) return assert.fail('it should not fail') - const ethTx = new EthTx(ethUtil.toBuffer(rawTx)) - assert.equal(ethTx.getChainId(), currentNetworkId) - }) - }) - }) -}) -- cgit v1.2.3 From b4e6ea9db787cbf7839d9caad6e1ea0385cc588e Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 16 May 2017 11:34:53 -0700 Subject: Fix fiat rendering Fixes #1439. When reorganizing fiat-value component to not use global state, had missed its necessary `currentCurrency` parameter. This now passes it in wherever it's used. --- ui/app/account-detail.js | 4 +++- ui/app/accounts/account-list-item.js | 4 +++- ui/app/accounts/index.js | 4 +++- ui/app/components/eth-balance.js | 4 ++-- ui/app/components/fiat-value.js | 6 ++---- ui/app/components/pending-tx.js | 9 +++++++-- ui/app/components/shift-list-item.js | 8 +++++--- ui/app/components/transaction-list-item.js | 7 ++++--- ui/app/conf-tx.js | 4 +++- ui/app/send.js | 3 +++ 10 files changed, 35 insertions(+), 18 deletions(-) diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index 7cadb9d47..7a78a360c 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -30,6 +30,7 @@ function mapStateToProps (state) { shapeShiftTxList: state.metamask.shapeShiftTxList, transactions: state.metamask.selectedAddressTxList || [], conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, } } @@ -44,7 +45,7 @@ AccountDetailScreen.prototype.render = function () { var checksumAddress = selected && ethUtil.toChecksumAddress(selected) var identity = props.identities[selected] var account = props.accounts[selected] - const { network, conversionRate } = props + const { network, conversionRate, currentCurrency } = props return ( @@ -184,6 +185,7 @@ AccountDetailScreen.prototype.render = function () { h(EthBalance, { value: account && account.balance, conversionRate, + currentCurrency, style: { lineHeight: '7px', marginTop: '10px', diff --git a/ui/app/accounts/account-list-item.js b/ui/app/accounts/account-list-item.js index 0e87af612..10a0b6cc7 100644 --- a/ui/app/accounts/account-list-item.js +++ b/ui/app/accounts/account-list-item.js @@ -15,7 +15,8 @@ function AccountListItem () { } AccountListItem.prototype.render = function () { - const { identity, selectedAddress, accounts, onShowDetail, conversionRate } = this.props + const { identity, selectedAddress, accounts, onShowDetail, + conversionRate, currentCurrency } = this.props const checksumAddress = identity && identity.address && ethUtil.toChecksumAddress(identity.address) const isSelected = selectedAddress === identity.address @@ -52,6 +53,7 @@ AccountListItem.prototype.render = function () { }, checksumAddress), h(EthBalance, { value: account && account.balance, + currentCurrency, conversionRate, style: { lineHeight: '7px', diff --git a/ui/app/accounts/index.js b/ui/app/accounts/index.js index 5105c214b..ac2615cd7 100644 --- a/ui/app/accounts/index.js +++ b/ui/app/accounts/index.js @@ -24,6 +24,7 @@ function mapStateToProps (state) { pending, keyrings: state.metamask.keyrings, conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, } } @@ -34,7 +35,7 @@ function AccountsScreen () { AccountsScreen.prototype.render = function () { const props = this.props - const { keyrings, conversionRate } = props + const { keyrings, conversionRate, currentCurrency } = props const identityList = valuesFor(props.identities) const unapprovedTxList = valuesFor(props.unapprovedTxs) @@ -83,6 +84,7 @@ AccountsScreen.prototype.render = function () { identity, selectedAddress: this.props.selectedAddress, conversionRate, + currentCurrency, accounts: this.props.accounts, onShowDetail: this.onShowDetail.bind(this), pending, diff --git a/ui/app/components/eth-balance.js b/ui/app/components/eth-balance.js index 21906aa09..4f538fd31 100644 --- a/ui/app/components/eth-balance.js +++ b/ui/app/components/eth-balance.js @@ -37,7 +37,7 @@ EthBalanceComponent.prototype.render = function () { } EthBalanceComponent.prototype.renderBalance = function (value) { var props = this.props - const { conversionRate, shorten, incoming } = props + const { conversionRate, shorten, incoming, currentCurrency } = props if (value === 'None') return value if (value === '...') return value var balanceObj = generateBalanceObject(value, shorten ? 1 : 3) @@ -83,7 +83,7 @@ EthBalanceComponent.prototype.renderBalance = function (value) { }, label), ]), - showFiat ? h(FiatValue, { value, conversionRate }) : null, + showFiat ? h(FiatValue, { value: props.value, conversionRate, currentCurrency }) : null, ])) ) } diff --git a/ui/app/components/fiat-value.js b/ui/app/components/fiat-value.js index 6e306c9e6..8a64a1cfc 100644 --- a/ui/app/components/fiat-value.js +++ b/ui/app/components/fiat-value.js @@ -12,7 +12,7 @@ function FiatValue () { FiatValue.prototype.render = function () { const props = this.props - const { conversionRate } = props + const { conversionRate, currentCurrency } = props const value = formatBalance(props.value, 6) @@ -28,9 +28,7 @@ FiatValue.prototype.render = function () { fiatTooltipNumber = 'Unknown' } - var fiatSuffix = props.currentCurrency - - return fiatDisplay(fiatDisplayNumber, fiatSuffix) + return fiatDisplay(fiatDisplayNumber, currentCurrency) } function fiatDisplay (fiatDisplayNumber, fiatSuffix) { diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index b084a1d2a..0d1f06ba6 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -31,6 +31,8 @@ function PendingTx () { PendingTx.prototype.render = function () { const props = this.props + const { currentCurrency } = props + const conversionRate = props.conversionRate const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} @@ -104,6 +106,7 @@ PendingTx.prototype.render = function () { h(EthBalance, { value: balance, conversionRate, + currentCurrency, inline: true, labelColor: '#F7861C', }), @@ -141,7 +144,7 @@ PendingTx.prototype.render = function () { // in the way that gas and gasLimit currently are. h('.row', [ h('.cell.label', 'Amount'), - h(EthBalance, { value: txParams.value }), + h(EthBalance, { value: txParams.value, currentCurrency, conversionRate }), ]), // Gas Limit (customizable) @@ -189,7 +192,7 @@ PendingTx.prototype.render = function () { // Max Transaction Fee (calculated) h('.cell.row', [ h('.cell.label', 'Max Transaction Fee'), - h(EthBalance, { value: txFeeBn.toString(16) }), + h(EthBalance, { value: txFeeBn.toString(16), currentCurrency, conversionRate }), ]), h('.cell.row', { @@ -208,6 +211,8 @@ PendingTx.prototype.render = function () { }, [ h(EthBalance, { value: maxCost.toString(16), + currentCurrency, + conversionRate, inline: true, labelColor: 'black', fontSize: '16px', diff --git a/ui/app/components/shift-list-item.js b/ui/app/components/shift-list-item.js index db5fda5cb..32bfbeda4 100644 --- a/ui/app/components/shift-list-item.js +++ b/ui/app/components/shift-list-item.js @@ -8,7 +8,7 @@ const actions = require('../actions') const addressSummary = require('../util').addressSummary const CopyButton = require('./copyButton') -const EtherBalance = require('./eth-balance') +const EthBalance = require('./eth-balance') const Tooltip = require('./tooltip') @@ -17,6 +17,7 @@ module.exports = connect(mapStateToProps)(ShiftListItem) function mapStateToProps (state) { return { conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, } } @@ -66,7 +67,7 @@ function formatDate (date) { ShiftListItem.prototype.renderUtilComponents = function () { var props = this.props - const { conversionRate } = props + const { conversionRate, currentCurrency } = props switch (props.response.status) { case 'no_deposits': @@ -97,9 +98,10 @@ ShiftListItem.prototype.renderUtilComponents = function () { h(CopyButton, { value: this.props.response.transaction, }), - h(EtherBalance, { + h(EthBalance, { value: `${props.response.outgoingCoin}`, conversionRate, + currentCurrency, width: '55px', shorten: true, needsParse: false, diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js index 3db4c016e..c2a585003 100644 --- a/ui/app/components/transaction-list-item.js +++ b/ui/app/components/transaction-list-item.js @@ -2,7 +2,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits -const EtherBalance = require('./eth-balance') +const EthBalance = require('./eth-balance') const addressSummary = require('../util').addressSummary const explorerLink = require('../../lib/explorer-link') const CopyButton = require('./copyButton') @@ -19,7 +19,7 @@ function TransactionListItem () { } TransactionListItem.prototype.render = function () { - const { transaction, network, conversionRate } = this.props + const { transaction, network, conversionRate, currentCurrency } = this.props if (transaction.key === 'shapeshift') { if (network === '1') return h(ShiftListItem, transaction) } @@ -78,9 +78,10 @@ TransactionListItem.prototype.render = function () { // Places a copy button if tx is successful, else places a placeholder empty div. transaction.hash ? h(CopyButton, { value: transaction.hash }) : h('div', {style: { display: 'flex', alignItems: 'center', width: '26px' }}), - isTx ? h(EtherBalance, { + isTx ? h(EthBalance, { value: txParams.value, conversionRate, + currentCurrency, width: '55px', shorten: true, showFiat: false, diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index c4df66931..0d7c4c1bb 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -28,6 +28,7 @@ function mapStateToProps (state) { network: state.metamask.network, provider: state.metamask.provider, conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, } } @@ -38,7 +39,7 @@ function ConfirmTxScreen () { ConfirmTxScreen.prototype.render = function () { const props = this.props - const { network, provider, unapprovedTxs, + const { network, provider, unapprovedTxs, currentCurrency, unapprovedMsgs, unapprovedPersonalMsgs, conversionRate } = props var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) @@ -104,6 +105,7 @@ ConfirmTxScreen.prototype.render = function () { accounts: props.accounts, identities: props.identities, conversionRate, + currentCurrency, // Actions buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), sendTransaction: this.sendTransaction.bind(this, txData), diff --git a/ui/app/send.js b/ui/app/send.js index a313c6bee..fd6994145 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -22,6 +22,7 @@ function mapStateToProps (state) { network: state.metamask.network, addressBook: state.metamask.addressBook, conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, } result.error = result.warning && result.warning.split('.')[0] @@ -50,6 +51,7 @@ SendTransactionScreen.prototype.render = function () { identities, addressBook, conversionRate, + currentCurrency, } = props return ( @@ -130,6 +132,7 @@ SendTransactionScreen.prototype.render = function () { h(EthBalance, { value: account && account.balance, conversionRate, + currentCurrency, }), ]), -- cgit v1.2.3 From 68d6ea44a0c9f3d75415ccadefe182f9a0872db1 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 16 May 2017 11:39:00 -0700 Subject: Fix path references --- app/scripts/controllers/transactions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 9f267160f..21dd25b30 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -5,8 +5,8 @@ const Semaphore = require('semaphore') const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') const EthQuery = require('eth-query') -const TxProviderUtil = require('./lib/tx-utils') -const createId = require('./lib/random-id') +const TxProviderUtil = require('../lib/tx-utils') +const createId = require('../lib/random-id') module.exports = class TransactionManager extends EventEmitter { constructor (opts) { -- cgit v1.2.3 From a00941c8894258a7534f8373405a0f8f4d27a904 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 16 May 2017 13:21:31 -0700 Subject: Remove only line from test --- test/unit/components/pending-tx-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/components/pending-tx-test.js b/test/unit/components/pending-tx-test.js index fe8290003..166b471cb 100644 --- a/test/unit/components/pending-tx-test.js +++ b/test/unit/components/pending-tx-test.js @@ -9,7 +9,7 @@ const Factory = createReactFactory(PendingTx) const ReactTestUtils = require('react-addons-test-utils') const ethUtil = require('ethereumjs-util') -describe.only('PendingTx', function () { +describe('PendingTx', function () { let pendingTxComponent const identities = { -- cgit v1.2.3 From a15e753c800617879384634a7096497550588eaf Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 16 May 2017 13:22:03 -0700 Subject: Add gas updating test to tx controller tests --- test/unit/tx-controller-test.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index d0b32ff41..51e0b9a17 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -9,7 +9,7 @@ const currentNetworkId = 42 const otherNetworkId = 36 const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex') -describe('Transaction Manager', function () { +describe('Transaction Controller', function () { let txController beforeEach(function () { @@ -170,6 +170,25 @@ describe('Transaction Manager', function () { var result = txController.getTx('1') assert.equal(result.hash, 'foo') }) + + it('updates gas price', function () { + const originalGasPrice = '0x01' + const desiredGasPriced = '0x02' + + const txMeta = { + id: '1', + status: 'unapproved', + metamaskNetworkId: currentNetworkId, + txParams: { + gasPrice: originalGasPrice, + }, + } + + txController.addTx(txMeta) + txMeta.txParams.gasPrice = desiredGasPriced + var result = txController.getTx('1') + assert.equal(result.txParams.gasPrice, desiredGasPriced, 'gas price updated') + }) }) describe('#getUnapprovedTxList', function () { @@ -234,4 +253,5 @@ describe('Transaction Manager', function () { }) }) }) + }) -- cgit v1.2.3 From 5c71149a8f1503bd59038b3b31ddfff60e7e6482 Mon Sep 17 00:00:00 2001 From: Nihar Date: Tue, 16 May 2017 14:23:42 -0700 Subject: continue button changed to agree --- mascara/test/lib/first-time.js | 2 +- test/integration/lib/first-time.js | 2 +- ui/app/components/notice.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mascara/test/lib/first-time.js b/mascara/test/lib/first-time.js index 8e33c8a06..76a4545bf 100644 --- a/mascara/test/lib/first-time.js +++ b/mascara/test/lib/first-time.js @@ -10,7 +10,7 @@ QUnit.test('render init screen', function (assert) { app = $('#app-content').contents() const recurseNotices = function () { let button = app.find('button') - if (button.html() === 'Continue') { + if (button.html() === 'Agree') { let termsPage = app.find('.markdown')[0] termsPage.scrollTop = termsPage.scrollHeight return wait().then(() => { diff --git a/test/integration/lib/first-time.js b/test/integration/lib/first-time.js index dbb88a3da..f0fa4ee3f 100644 --- a/test/integration/lib/first-time.js +++ b/test/integration/lib/first-time.js @@ -11,7 +11,7 @@ QUnit.test('render init screen', function (assert) { const recurseNotices = function () { let button = app.find('button') - if (button.html() === 'Continue') { + if (button.html() === 'Agree') { let termsPage = app.find('.markdown')[0] termsPage.scrollTop = termsPage.scrollHeight return wait().then(() => { diff --git a/ui/app/components/notice.js b/ui/app/components/notice.js index 3c8523daf..7fe41fa88 100644 --- a/ui/app/components/notice.js +++ b/ui/app/components/notice.js @@ -107,7 +107,7 @@ Notice.prototype.render = function () { style: { marginTop: '18px', }, - }, 'Continue'), + }, 'Agree'), ]) ) } -- cgit v1.2.3 From 53b8d18a5f649c73a58a96e36a9458903d8af6aa Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Tue, 16 May 2017 15:30:22 -0700 Subject: Complete transition into BN. --- ui/app/components/bn-as-decimal-input.js | 143 +++++++++++++++++++++++++++++++ ui/app/components/pending-tx.js | 27 +++--- 2 files changed, 157 insertions(+), 13 deletions(-) create mode 100644 ui/app/components/bn-as-decimal-input.js diff --git a/ui/app/components/bn-as-decimal-input.js b/ui/app/components/bn-as-decimal-input.js new file mode 100644 index 000000000..6c2132ca1 --- /dev/null +++ b/ui/app/components/bn-as-decimal-input.js @@ -0,0 +1,143 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const extend = require('xtend') + +module.exports = BnAsDecimalInput + +inherits(BnAsDecimalInput, Component) +function BnAsDecimalInput () { + this.state = { invalid: null } + Component.call(this) +} + +/* Bn as Decimal Input + * + * A component for allowing easy, decimal editing + * of a passed in hex string value. + * + * On change, calls back its `onChange` function parameter + * and passes it an updated hex string. + */ + +BnAsDecimalInput.prototype.render = function () { + const props = this.props + const state = this.state + + const { value, precision, onChange, min, max } = props + + const suffix = props.suffix + const style = props.style + const newValue = value.toNumber(10) / scale + const scale = Math.pow(10, precision) + + return ( + h('.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('input.hex-input', { + type: 'number', + step: 'any', + required: true, + min: min, + max: max, + style: extend({ + display: 'block', + textAlign: 'right', + backgroundColor: 'transparent', + border: '1px solid #bdbdbd', + + }, style), + value: newValue, + onBlur: (event) => { + this.updateValidity(event) + }, + onChange: (event) => { + this.updateValidity(event) + const value = (event.target.value === '') ? '' : event.target.value + const scaledNumber = Math.floor(scale * value) + const precisionBN = new BN(scaledNumber, 10) + onChange(precisionBN) + }, + onInvalid: (event) => { + const msg = this.constructWarning() + if (msg === state.invalid) { + return + } + this.setState({ invalid: msg }) + event.preventDefault() + return false + }, + }), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + marginRight: '6px', + width: '20px', + }, + }, suffix), + ]), + + state.invalid ? h('span.error', { + style: { + position: 'absolute', + right: '0px', + textAlign: 'right', + transform: 'translateY(26px)', + padding: '3px', + background: 'rgba(255,255,255,0.85)', + zIndex: '1', + textTransform: 'capitalize', + border: '2px solid #E20202', + }, + }, state.invalid) : null, + ]) + ) +} + +BnAsDecimalInput.prototype.setValid = function (message) { + this.setState({ invalid: null }) +} + +BnAsDecimalInput.prototype.updateValidity = function (event) { + const target = event.target + const value = this.props.value + const newValue = target.value + + if (value === newValue) { + return + } + + const valid = target.checkValidity() + + if (valid) { + this.setState({ invalid: null }) + } +} + +BnAsDecimalInput.prototype.constructWarning = function () { + const { name, min, max } = this.props + let message = name ? name + ' ' : '' + + if (min && max) { + message += `must be greater than or equal to ${min} and less than or equal to ${max}.` + } else if (min) { + message += `must be greater than or equal to ${min}.` + } else if (max) { + message += `must be less than or equal to ${max}.` + } else { + message += 'Invalid input.' + } + + return message +} diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 5ea885195..95d345a3d 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -13,7 +13,7 @@ const EthBalance = require('./eth-balance') const util = require('../util') const addressSummary = util.addressSummary const nameForAddress = require('../../lib/contract-namer') -const HexInput = require('./hex-as-decimal-input') +const BNInput = require('./bn-as-decimal-input') const MIN_GAS_PRICE_GWEI_BN = new BN(2) const GWEI_FACTOR = new BN(1e9) @@ -54,7 +54,6 @@ PendingTx.prototype.render = function () { // Gas Price const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) const gasPriceBn = hexToBn(gasPrice) - const gasPriceGweiBn = gasPriceBn.div(GWEI_FACTOR) const txFeeBn = gasBn.mul(gasPriceBn) const valueBn = hexToBn(txParams.value) @@ -166,9 +165,10 @@ PendingTx.prototype.render = function () { h('.cell.label', 'Gas Limit'), h('.cell.value', { }, [ - h(HexInput, { + h(BNInput, { name: 'Gas Limit', - value: gas, + value: gasBn, + precision: 0, // The hard lower limit for gas. min: MIN_GAS_LIMIT_BN.toString(10), suffix: 'UNITS', @@ -176,10 +176,10 @@ PendingTx.prototype.render = function () { position: 'relative', top: '5px', }, - onChange: (newHex) => { - log.info(`Gas limit changed to ${newHex}`) + onChange: (newBN) => { + log.info(`Gas limit changed to ${newBN.toString(10)}`) const txMeta = this.gatherTxMeta() - txMeta.txParams.gas = newHex + txMeta.txParams.gas = '0x' + newBN.toString('hex') this.setState({ txData: txMeta }) }, ref: (hexInput) => { this.inputs.push(hexInput) }, @@ -192,20 +192,20 @@ PendingTx.prototype.render = function () { h('.cell.label', 'Gas Price'), h('.cell.value', { }, [ - h(HexInput, { + h(BNInput, { name: 'Gas Price', - value: gasPriceGweiBn.toString(16), + value: gasPriceBn, + precision: 9, suffix: 'GWEI', min: MIN_GAS_PRICE_GWEI_BN.toString(10), style: { position: 'relative', top: '5px', }, - onChange: (newHex) => { - log.info(`Gas price changed to: ${newHex}`) - const inWei = hexToBn(newHex).mul(GWEI_FACTOR) + onChange: (newBN) => { + log.info(`Gas price changed to: ${newBN.toString(10)}`) const txMeta = this.gatherTxMeta() - txMeta.txParams.gasPrice = inWei.toString(16) + txMeta.txParams.gasPrice = '0x' + newBN.toString('hex') this.setState({ txData: txMeta }) }, ref: (hexInput) => { this.inputs.push(hexInput) }, @@ -368,6 +368,7 @@ PendingTx.prototype.miniAccountPanelForRecipient = function () { } PendingTx.prototype.resetGasFields = function () { + log.debug(`pending-tx resetGasFields`) this.inputs.forEach((hexInput) => { -- cgit v1.2.3 From caeadc24072829deaabd0f6a33563bb84c10008a Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 16 May 2017 16:19:10 -0700 Subject: Linted and removed unused deps --- package.json | 1 - test/unit/components/pending-tx-test.js | 23 ++++++++--------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index eb5ed8a32..14ddd2886 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,6 @@ "browserify": "^13.0.0", "chai": "^3.5.0", "clone": "^1.0.2", - "create-react-factory": "^0.2.1", "deep-freeze-strict": "^1.1.1", "del": "^2.2.0", "envify": "^4.0.0", diff --git a/test/unit/components/pending-tx-test.js b/test/unit/components/pending-tx-test.js index 166b471cb..36339474c 100644 --- a/test/unit/components/pending-tx-test.js +++ b/test/unit/components/pending-tx-test.js @@ -2,15 +2,10 @@ const assert = require('assert') const additions = require('react-testutils-additions') const h = require('react-hyperscript') const PendingTx = require('../../../ui/app/components/pending-tx') -const createReactFactory = require('create-react-factory').createReactFactory -const React = require('react') -const shallow = require('react-test-renderer/shallow') -const Factory = createReactFactory(PendingTx) const ReactTestUtils = require('react-addons-test-utils') const ethUtil = require('ethereumjs-util') describe('PendingTx', function () { - let pendingTxComponent const identities = { '0xfdea65c8e26263f6d9a1b5de9555d2931a33b826': { @@ -38,7 +33,7 @@ describe('PendingTx', function () { it('should use updated values when edited.', function (done) { - const renderer = ReactTestUtils.createRenderer(); + const renderer = ReactTestUtils.createRenderer() const newGasPrice = '0x77359400' const props = { @@ -56,16 +51,14 @@ describe('PendingTx', function () { } const pendingTxComponent = h(PendingTx, props) - const component = additions.renderIntoDocument(pendingTxComponent); + const component = additions.renderIntoDocument(pendingTxComponent) renderer.render(pendingTxComponent) const result = renderer.getRenderOutput() const form = result.props.children - const children = form.props.children[form.props.children.length - 1] assert.equal(result.type, 'div', 'should create a div') - try{ - - const input = additions.find(component, '.cell.row input[type="number"]')[1] + try { + const input = additions.find(component, '.cell.row input[type='number']')[1] ReactTestUtils.Simulate.change(input, { target: { value: 2, @@ -76,14 +69,14 @@ describe('PendingTx', function () { let form = additions.find(component, 'form')[0] form.checkValidity = () => true form.getFormEl = () => { return { checkValidity() { return true } } } - ReactTestUtils.Simulate.submit(form, { preventDefault() {}, target: { checkValidity() {return true} } }) + ReactTestUtils.Simulate.submit(form, { preventDefault() {}, target: { checkValidity() { + return true + } } }) } catch (e) { - console.log("WHAAAA") + console.log('WHAAAA') console.error(e) } - }) - }) -- cgit v1.2.3 From d8130f1effc31a866476e10153bd854709ae23be Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Tue, 16 May 2017 16:20:58 -0700 Subject: Fix reset button. --- ui/app/components/bn-as-decimal-input.js | 6 +++--- ui/app/components/pending-tx.js | 12 +++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/ui/app/components/bn-as-decimal-input.js b/ui/app/components/bn-as-decimal-input.js index 6c2132ca1..d0eebe09e 100644 --- a/ui/app/components/bn-as-decimal-input.js +++ b/ui/app/components/bn-as-decimal-input.js @@ -16,10 +16,10 @@ function BnAsDecimalInput () { /* Bn as Decimal Input * * A component for allowing easy, decimal editing - * of a passed in hex string value. + * of a passed in bn string value. * * On change, calls back its `onChange` function parameter - * and passes it an updated hex string. + * and passes it an updated bn string. */ BnAsDecimalInput.prototype.render = function () { @@ -30,8 +30,8 @@ BnAsDecimalInput.prototype.render = function () { const suffix = props.suffix const style = props.style - const newValue = value.toNumber(10) / scale const scale = Math.pow(10, precision) + const newValue = value.toNumber(10) / scale return ( h('.flex-column', [ diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 95d345a3d..5c8d81d07 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -180,7 +180,7 @@ PendingTx.prototype.render = function () { log.info(`Gas limit changed to ${newBN.toString(10)}`) const txMeta = this.gatherTxMeta() txMeta.txParams.gas = '0x' + newBN.toString('hex') - this.setState({ txData: txMeta }) + this.setState({ txData: cloneObj(txMeta) }) }, ref: (hexInput) => { this.inputs.push(hexInput) }, }), @@ -206,7 +206,7 @@ PendingTx.prototype.render = function () { log.info(`Gas price changed to: ${newBN.toString(10)}`) const txMeta = this.gatherTxMeta() txMeta.txParams.gasPrice = '0x' + newBN.toString('hex') - this.setState({ txData: txMeta }) + this.setState({ txData: cloneObj(txMeta) }) }, ref: (hexInput) => { this.inputs.push(hexInput) }, }), @@ -388,7 +388,7 @@ PendingTx.prototype.gatherTxMeta = function () { log.debug(`pending-tx gatherTxMeta`) const props = this.props const state = this.state - const txData = state.txData || props.txData + const txData = cloneObj(state.txData) || cloneObj(props.txData) log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) return txData @@ -409,7 +409,6 @@ PendingTx.prototype._notZeroOrEmptyString = function (obj) { function forwardCarrat () { return ( - h('img', { src: 'images/forward-carrat.svg', style: { @@ -417,6 +416,9 @@ function forwardCarrat () { height: '37px', }, }) - ) } + +function cloneObj (obj) { + return JSON.parse(JSON.stringify(obj)) +} -- cgit v1.2.3 From 44f25cd93c18c57acf993ace3387a4d569d8dcca Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Tue, 16 May 2017 16:21:50 -0700 Subject: Changelog bump --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 422639f70..08dd41a3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## Current Master - Trim currency list. +- Enable decimals in our gas prices. +- Fix reset button. ## 3.6.4 2017-5-8 -- cgit v1.2.3 From cfb7bfed186a03e4e879d932501a51a9758dd3ad Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 16 May 2017 16:44:17 -0700 Subject: Fix quotation mark --- test/unit/components/pending-tx-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/components/pending-tx-test.js b/test/unit/components/pending-tx-test.js index 36339474c..89e6892a3 100644 --- a/test/unit/components/pending-tx-test.js +++ b/test/unit/components/pending-tx-test.js @@ -58,7 +58,7 @@ describe('PendingTx', function () { assert.equal(result.type, 'div', 'should create a div') try { - const input = additions.find(component, '.cell.row input[type='number']')[1] + const input = additions.find(component, '.cell.row input[type="number"]')[1] ReactTestUtils.Simulate.change(input, { target: { value: 2, -- cgit v1.2.3 From c1bef31d9d3b2cf091ac94c908700c3c0081318f Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 16 May 2017 16:49:59 -0700 Subject: Linted --- test/unit/components/pending-tx-test.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/unit/components/pending-tx-test.js b/test/unit/components/pending-tx-test.js index 89e6892a3..9ff948604 100644 --- a/test/unit/components/pending-tx-test.js +++ b/test/unit/components/pending-tx-test.js @@ -54,7 +54,6 @@ describe('PendingTx', function () { const component = additions.renderIntoDocument(pendingTxComponent) renderer.render(pendingTxComponent) const result = renderer.getRenderOutput() - const form = result.props.children assert.equal(result.type, 'div', 'should create a div') try { @@ -63,10 +62,10 @@ describe('PendingTx', function () { target: { value: 2, checkValidity() { return true }, - } + }, }) - let form = additions.find(component, 'form')[0] + const form = additions.find(component, 'form')[0] form.checkValidity = () => true form.getFormEl = () => { return { checkValidity() { return true } } } ReactTestUtils.Simulate.submit(form, { preventDefault() {}, target: { checkValidity() { -- cgit v1.2.3 From c6fd5090519af64bbe3d29346484bcf45572d3c2 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 16 May 2017 17:06:19 -0700 Subject: Improve test --- test/unit/tx-controller-test.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index 51e0b9a17..e6645090e 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -3,6 +3,7 @@ const EventEmitter = require('events') const ethUtil = require('ethereumjs-util') const EthTx = require('ethereumjs-tx') const ObservableStore = require('obs-store') +const clone = require('clone') const TransactionController = require('../../app/scripts/controllers/transactions') const noop = () => true const currentNetworkId = 42 @@ -184,8 +185,11 @@ describe('Transaction Controller', function () { }, } + const updatedMeta = clone(txMeta) + txController.addTx(txMeta) - txMeta.txParams.gasPrice = desiredGasPriced + updatedMeta.txParams.gasPrice = desiredGasPriced + txController.updateTx(updatedMeta) var result = txController.getTx('1') assert.equal(result.txParams.gasPrice, desiredGasPriced, 'gas price updated') }) -- cgit v1.2.3 From 6491b42266b2114af72e72a46a6453dc3dd59290 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 16 May 2017 18:16:18 -0700 Subject: Add test around txManager#approveTransaction --- test/unit/tx-controller-test.js | 67 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index e6645090e..d4e8d79f0 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -4,6 +4,7 @@ const ethUtil = require('ethereumjs-util') const EthTx = require('ethereumjs-tx') const ObservableStore = require('obs-store') const clone = require('clone') +const sinon = require('sinon') const TransactionController = require('../../app/scripts/controllers/transactions') const noop = () => true const currentNetworkId = 42 @@ -174,7 +175,7 @@ describe('Transaction Controller', function () { it('updates gas price', function () { const originalGasPrice = '0x01' - const desiredGasPriced = '0x02' + const desiredGasPrice = '0x02' const txMeta = { id: '1', @@ -188,10 +189,10 @@ describe('Transaction Controller', function () { const updatedMeta = clone(txMeta) txController.addTx(txMeta) - updatedMeta.txParams.gasPrice = desiredGasPriced + updatedMeta.txParams.gasPrice = desiredGasPrice txController.updateTx(updatedMeta) var result = txController.getTx('1') - assert.equal(result.txParams.gasPrice, desiredGasPriced, 'gas price updated') + assert.equal(result.txParams.gasPrice, desiredGasPrice, 'gas price updated') }) }) @@ -247,6 +248,66 @@ describe('Transaction Controller', function () { }) }) + describe('#approveTransaction', function () { + let txMeta, originalValue + + beforeEach(function () { + originalValue = '0x01' + txMeta = { + id: '1', + status: 'unapproved', + metamaskNetworkId: currentNetworkId, + txParams: { + nonce: originalValue, + gas: originalValue, + gasPrice: originalValue, + }, + } + }) + + + it('does not overwrite set values', function (done) { + const wrongValue = '0x05' + + txController.addTx(txMeta) + + const estimateStub = sinon.stub(txController.txProviderUtils.query, 'estimateGas') + .callsArgWith(1, null, wrongValue) + + const priceStub = sinon.stub(txController.txProviderUtils.query, 'gasPrice') + .callsArgWith(0, null, wrongValue) + + const nonceStub = sinon.stub(txController.txProviderUtils.query, 'getTransactionCount') + .callsArgWith(2, null, wrongValue) + + const signStub = sinon.stub(txController, 'signTransaction') + .callsArgWith(1, null, noop) + + const pubStub = sinon.stub(txController.txProviderUtils, 'publishTransaction') + .callsArgWith(1, null, originalValue) + + txController.approveTransaction(txMeta.id, (err) => { + assert.ifError(err, 'should not error') + + const result = txController.getTx(txMeta.id) + const params = result.txParams + + assert.equal(params.gas, originalValue, 'gas unmodified') + assert.equal(params.gasPrice, originalValue, 'gas price unmodified') + assert.equal(params.nonce, originalValue, 'nonce unmodified') + assert.equal(result.hash, originalValue, 'hash was set') + + estimateStub.restore() + priceStub.restore() + signStub.restore() + nonceStub.restore() + pubStub.restore() + + done() + }) + }) + }) + describe('#sign replay-protected tx', function () { it('prepares a tx with the chainId set', function () { txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) -- cgit v1.2.3 From 31c7daee73fa41e356d1bd5cd92186e60b252212 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 16 May 2017 23:33:40 -0700 Subject: Fix bug where edited gas parameters did not take effect Fixes #1407 --- CHANGELOG.md | 1 + ui/app/conf-tx.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3653e5018..998219254 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Current Master +- Fix bug where edited gas parameters would not take effect. - Trim currency list. - Fix event filter bug introduced by newer versions of Geth. diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 0d7c4c1bb..008627ce6 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -108,7 +108,7 @@ ConfirmTxScreen.prototype.render = function () { currentCurrency, // Actions buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), - sendTransaction: this.sendTransaction.bind(this, txData), + sendTransaction: this.sendTransaction.bind(this), cancelTransaction: this.cancelTransaction.bind(this, txData), signMessage: this.signMessage.bind(this, txData), signPersonalMessage: this.signPersonalMessage.bind(this, txData), -- cgit v1.2.3 From c0516ddf333336a7784787a02183c4fe212364b9 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 17 May 2017 00:09:59 -0700 Subject: Add test requiring high precision --- test/unit/components/bn-as-decimal-input-test.js | 59 ++++++++++++++++++++++++ test/unit/components/pending-tx-test.js | 2 +- 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 test/unit/components/bn-as-decimal-input-test.js diff --git a/test/unit/components/bn-as-decimal-input-test.js b/test/unit/components/bn-as-decimal-input-test.js new file mode 100644 index 000000000..4ea910fb0 --- /dev/null +++ b/test/unit/components/bn-as-decimal-input-test.js @@ -0,0 +1,59 @@ +var assert = require('assert') + +const additions = require('react-testutils-additions') +const h = require('react-hyperscript') +const ReactTestUtils = require('react-addons-test-utils') +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN + +var BnInput = require('../../../ui/app/components/bn-as-decimal-input') + +describe.only('BnInput', function () { + let bnInput + const message = 'Hello, world!' + const buffer = new Buffer(message, 'utf8') + const hex = buffer.toString('hex') + + it('can tolerate a large number at a high precision', function (done) { + + const renderer = ReactTestUtils.createRenderer(); + + let valueStr = '1' + while (valueStr.length < 18 + 7) { + valueStr += '0' + } + const value = new BN(valueStr, 10) + + let inputStr = '11' + while (inputStr.length < 7) { + inputStr += '0' + } + inputStr += '.01' + + let targetStr = inputStr.split('.').join() + while (targetStr.length < 18 + 7) { + targetStr += '0' + } + const target = new BN(targetStr, 10) + + const precision = 1e18 // ether precision + + const props = { + value, + precision, + onChange: (newBn) => { + assert.equal(newBn.toString(), targetValue.toString(), 'should tolerate increase') + done() + } + } + + const inputComponent = h(BnInput, props) + const component = additions.renderIntoDocument(inputComponent) + renderer.render(inputComponent) + const input = additions.find(component, 'input.hex-input')[0] + ReactTestUtils.Simulate.change(input, { preventDefault() {}, target: { + value: inputStr, + checkValidity() {return true} }, + }) + }) +}) diff --git a/test/unit/components/pending-tx-test.js b/test/unit/components/pending-tx-test.js index fe8290003..166b471cb 100644 --- a/test/unit/components/pending-tx-test.js +++ b/test/unit/components/pending-tx-test.js @@ -9,7 +9,7 @@ const Factory = createReactFactory(PendingTx) const ReactTestUtils = require('react-addons-test-utils') const ethUtil = require('ethereumjs-util') -describe.only('PendingTx', function () { +describe('PendingTx', function () { let pendingTxComponent const identities = { -- cgit v1.2.3 From e26501aa0192b26880a8fe63f41d76fdfa849d7b Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 17 May 2017 00:19:31 -0700 Subject: Simplify test to represent realistic use case --- test/unit/components/bn-as-decimal-input-test.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/test/unit/components/bn-as-decimal-input-test.js b/test/unit/components/bn-as-decimal-input-test.js index 4ea910fb0..1f589c210 100644 --- a/test/unit/components/bn-as-decimal-input-test.js +++ b/test/unit/components/bn-as-decimal-input-test.js @@ -14,24 +14,20 @@ describe.only('BnInput', function () { const buffer = new Buffer(message, 'utf8') const hex = buffer.toString('hex') - it('can tolerate a large number at a high precision', function (done) { + it('can tolerate a gas decimal number at a high precision', function (done) { const renderer = ReactTestUtils.createRenderer(); - let valueStr = '1' - while (valueStr.length < 18 + 7) { + let valueStr = '20' + while (valueStr.length < 20) { valueStr += '0' } const value = new BN(valueStr, 10) - let inputStr = '11' - while (inputStr.length < 7) { - inputStr += '0' - } - inputStr += '.01' + let inputStr = '2.3' - let targetStr = inputStr.split('.').join() - while (targetStr.length < 18 + 7) { + let targetStr = '23' + while (targetStr.length < 19) { targetStr += '0' } const target = new BN(targetStr, 10) -- cgit v1.2.3 From 6f02f5bc5d741810edb2c011fc3095ee50f84bf9 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 17 May 2017 00:33:19 -0700 Subject: Clean up test --- test/unit/components/bn-as-decimal-input-test.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/unit/components/bn-as-decimal-input-test.js b/test/unit/components/bn-as-decimal-input-test.js index 1f589c210..502c9a2de 100644 --- a/test/unit/components/bn-as-decimal-input-test.js +++ b/test/unit/components/bn-as-decimal-input-test.js @@ -8,11 +8,8 @@ const BN = ethUtil.BN var BnInput = require('../../../ui/app/components/bn-as-decimal-input') -describe.only('BnInput', function () { +describe('BnInput', function () { let bnInput - const message = 'Hello, world!' - const buffer = new Buffer(message, 'utf8') - const hex = buffer.toString('hex') it('can tolerate a gas decimal number at a high precision', function (done) { -- cgit v1.2.3 From bfb1c92ded2bef1a2f08e1c185721010278ca69b Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 17 May 2017 00:34:56 -0700 Subject: Linted test --- test/unit/components/bn-as-decimal-input-test.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/unit/components/bn-as-decimal-input-test.js b/test/unit/components/bn-as-decimal-input-test.js index 502c9a2de..034bc3e18 100644 --- a/test/unit/components/bn-as-decimal-input-test.js +++ b/test/unit/components/bn-as-decimal-input-test.js @@ -9,11 +9,9 @@ const BN = ethUtil.BN var BnInput = require('../../../ui/app/components/bn-as-decimal-input') describe('BnInput', function () { - let bnInput - it('can tolerate a gas decimal number at a high precision', function (done) { - const renderer = ReactTestUtils.createRenderer(); + const renderer = ReactTestUtils.createRenderer() let valueStr = '20' while (valueStr.length < 20) { @@ -35,9 +33,9 @@ describe('BnInput', function () { value, precision, onChange: (newBn) => { - assert.equal(newBn.toString(), targetValue.toString(), 'should tolerate increase') + assert.equal(newBn.toString(), target.toString(), 'should tolerate increase') done() - } + }, } const inputComponent = h(BnInput, props) @@ -46,7 +44,7 @@ describe('BnInput', function () { const input = additions.find(component, 'input.hex-input')[0] ReactTestUtils.Simulate.change(input, { preventDefault() {}, target: { value: inputStr, - checkValidity() {return true} }, + checkValidity() { return true } }, }) }) }) -- cgit v1.2.3 From 24737ded3466da61fd96015beb6931e59d8232a7 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 17 May 2017 14:13:05 -0700 Subject: Fix bug where decimals in gas inputs gave strange results --- CHANGELOG.md | 1 + ui/app/components/hex-as-decimal-input.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 998219254..24ae4e781 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Fix bug where edited gas parameters would not take effect. - Trim currency list. - Fix event filter bug introduced by newer versions of Geth. +- Fix bug where decimals in gas inputs could result in strange values. ## 3.6.4 2017-5-8 diff --git a/ui/app/components/hex-as-decimal-input.js b/ui/app/components/hex-as-decimal-input.js index e37aaa8c3..d24fdcb7b 100644 --- a/ui/app/components/hex-as-decimal-input.js +++ b/ui/app/components/hex-as-decimal-input.js @@ -139,7 +139,7 @@ HexAsDecimalInput.prototype.constructWarning = function () { } function hexify (decimalString) { - const hexBN = new BN(decimalString, 10) + const hexBN = new BN(decimalString.split('.')[0], 10) return '0x' + hexBN.toString('hex') } -- cgit v1.2.3 From 717db41d0b7bcd7b6f88a5c460aa4dcbc5828116 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 17 May 2017 14:18:01 -0700 Subject: Modify test, replace clone package. --- test/unit/components/bn-as-decimal-input-test.js | 4 ++-- ui/app/components/pending-tx.js | 11 ++++------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/test/unit/components/bn-as-decimal-input-test.js b/test/unit/components/bn-as-decimal-input-test.js index 034bc3e18..f515003bb 100644 --- a/test/unit/components/bn-as-decimal-input-test.js +++ b/test/unit/components/bn-as-decimal-input-test.js @@ -14,7 +14,7 @@ describe('BnInput', function () { const renderer = ReactTestUtils.createRenderer() let valueStr = '20' - while (valueStr.length < 20) { + while (valueStr.length < 15) { valueStr += '0' } const value = new BN(valueStr, 10) @@ -22,7 +22,7 @@ describe('BnInput', function () { let inputStr = '2.3' let targetStr = '23' - while (targetStr.length < 19) { + while (targetStr.length < 14) { targetStr += '0' } const target = new BN(targetStr, 10) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 37a3a3bf3..5b238187c 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -2,6 +2,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits const actions = require('../actions') +const clone = require('clone') const ethUtil = require('ethereumjs-util') const BN = ethUtil.BN @@ -347,14 +348,14 @@ PendingTx.prototype.gasPriceChanged = function (newBN) { log.info(`Gas price changed to: ${newBN.toString(10)}`) const txMeta = this.gatherTxMeta() txMeta.txParams.gasPrice = '0x' + newBN.toString('hex') - this.setState({ txData: cloneObj(txMeta) }) + this.setState({ txData: clone(txMeta) }) } PendingTx.prototype.gasLimitChanged = function (newBN) { log.info(`Gas limit changed to ${newBN.toString(10)}`) const txMeta = this.gatherTxMeta() txMeta.txParams.gas = '0x' + newBN.toString('hex') - this.setState({ txData: cloneObj(txMeta) }) + this.setState({ txData: clone(txMeta) }) } PendingTx.prototype.resetGasFields = function () { @@ -405,7 +406,7 @@ PendingTx.prototype.gatherTxMeta = function () { log.debug(`pending-tx gatherTxMeta`) const props = this.props const state = this.state - const txData = cloneObj(state.txData) || cloneObj(props.txData) + const txData = clone(state.txData) || clone(props.txData) log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) return txData @@ -435,7 +436,3 @@ function forwardCarrat () { }) ) } - -function cloneObj (obj) { - return JSON.parse(JSON.stringify(obj)) -} -- cgit v1.2.3 From 7e7ceab95edcf27a240da478a1f5da2d97cd5e85 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 17 May 2017 14:31:06 -0700 Subject: Fix decimal tolerance --- ui/app/components/hex-as-decimal-input.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/hex-as-decimal-input.js b/ui/app/components/hex-as-decimal-input.js index d24fdcb7b..4a71e9585 100644 --- a/ui/app/components/hex-as-decimal-input.js +++ b/ui/app/components/hex-as-decimal-input.js @@ -139,7 +139,7 @@ HexAsDecimalInput.prototype.constructWarning = function () { } function hexify (decimalString) { - const hexBN = new BN(decimalString.split('.')[0], 10) + const hexBN = new BN(parseInt(decimalString), 10) return '0x' + hexBN.toString('hex') } -- cgit v1.2.3 From 2a25f99461c01ac9e0aec3d90f73675c58303860 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 17 May 2017 14:36:50 -0700 Subject: Version 3.6.5 --- CHANGELOG.md | 2 ++ app/manifest.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24ae4e781..371b348ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +## 3.6.5 2017-5-17 + - Fix bug where edited gas parameters would not take effect. - Trim currency list. - Fix event filter bug introduced by newer versions of Geth. diff --git a/app/manifest.json b/app/manifest.json index a1f6d7855..31e4598c7 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.6.4", + "version": "3.6.5", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From f87ea49b5ac2d66d8f281f08f42e8cfd2d701ba7 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 18 May 2017 23:54:02 +0200 Subject: Create a network controller to manage switcing networks an updating the provider --- app/scripts/controllers/network.js | 152 +++++++++++++++++++++++++++++++++++++ app/scripts/keyring-controller.js | 6 ++ app/scripts/lib/config-manager.js | 67 ---------------- app/scripts/lib/tx-utils.js | 8 +- app/scripts/metamask-controller.js | 124 +++++++++++++----------------- app/scripts/transaction-manager.js | 25 ++++-- 6 files changed, 232 insertions(+), 150 deletions(-) create mode 100644 app/scripts/controllers/network.js diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js new file mode 100644 index 000000000..82eabb573 --- /dev/null +++ b/app/scripts/controllers/network.js @@ -0,0 +1,152 @@ +const EventEmitter = require('events') +const MetaMaskProvider = require('web3-provider-engine/zero.js') +const ObservableStore = require('obs-store') +const extend = require('xtend') +const EthQuery = require('eth-query') +const MetamaskConfig = require('../config.js') + +const TESTNET_RPC = MetamaskConfig.network.testnet +const MAINNET_RPC = MetamaskConfig.network.mainnet +const MORDEN_RPC = MetamaskConfig.network.morden +const KOVAN_RPC = MetamaskConfig.network.kovan +const RINKEBY_RPC = MetamaskConfig.network.rinkeby + +module.exports = class NetworkController extends EventEmitter { + constructor (providerOpts) { + super() + this.networkStore = new ObservableStore({ network: 'loading' }) + providerOpts.provider.rpcTarget = this.getRpcAddressForType(providerOpts.provider.type) + this.providerStore = new ObservableStore(providerOpts) + this._claimed = 0 + } + + getState () { + return extend({}, + this.networkStore.getState(), + this.providerStore.getState() + ) + } + + initializeProvider (opts) { + this.providerConfig = opts + this.provider = MetaMaskProvider(opts) + this.ethQuery = new EthQuery(this.provider) + this.lookupNetwork() + return Promise.resolve(this.provider) + } + switchNetwork (providerConfig) { + delete this.provider + delete this.ethQuery + const newConfig = extend(this.providerConfig, providerConfig) + this.providerConfig = newConfig + this.provider = MetaMaskProvider(newConfig) + this.ethQuery = new EthQuery(this.provider) + this.emit('networkSwitch', { + provider: this.provider, + ethQuery: this.ethQuery, + }, this.claim.bind(this)) + } + + subscribe (cb) { + this.networkStore.subscribe(cb) + this.providerStore.subscribe(cb) + } + + verifyNetwork () { + // Check network when restoring connectivity: + if (this.isNetworkLoading()) this.lookupNetwork() + } + + getNetworkState () { + return this.networkStore.getState().network + } + + setNetworkState (network) { + return this.networkStore.updateState({ network }) + } + + isNetworkLoading () { + return this.getNetworkState() === 'loading' + } + + lookupNetwork (err) { + if (err) this.setNetworkState('loading') + + this.ethQuery.sendAsync({ method: 'net_version' }, (err, network) => { + if (err) return this.setNetworkState('loading') + + log.info('web3.getNetwork returned ' + network) + this.setNetworkState(network) + }) + } + + setRpcTarget (rpcUrl) { + this.providerStore.updateState({ + provider: { + type: 'rpc', + rpcTarget: rpcUrl, + }, + }) + } + + getCurrentRpcAddress () { + var provider = this.getProvider() + if (!provider) return null + return this.getRpcAddressForType(provider.type) + } + + setProviderType (type) { + if (type === this.getProvider().type) return + const rpcTarget = this.getRpcAddressForType(type) + this.networkStore.updateState({network: 'loading'}) + this.switchNetwork({ + rpcUrl: rpcTarget, + }) + this.once('claimed', () => { + this.providerStore.updateState({provider: {type, rpcTarget}}) + console.log('CLAIMED') + this.lookupNetwork() + }) + + } + + useEtherscanProvider () { + this.setProviderType('etherscan') + } + + getProvider () { + return this.providerStore.getState().provider + } + + getRpcAddressForType (type) { + const provider = this.getProvider() + switch (type) { + + case 'mainnet': + return MAINNET_RPC + + case 'testnet': + return TESTNET_RPC + + case 'morden': + return MORDEN_RPC + + case 'kovan': + return KOVAN_RPC + + case 'rinkeby': + return RINKEBY_RPC + + default: + return provider && provider.rpcTarget ? provider.rpcTarget : TESTNET_RPC + } + } + + claim () { + this._claimed += 1 + if (this._claimed === this.listenerCount('networkSwitch')) { + this.emit('claimed') + this._claimed = 0 + } + } +} diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js index 5b3c80e40..bb699ca8b 100644 --- a/app/scripts/keyring-controller.js +++ b/app/scripts/keyring-controller.js @@ -41,6 +41,12 @@ class KeyringController extends EventEmitter { this.getNetwork = opts.getNetwork } + setEthStore (ethStore) { + delete this.ethStore + this.ethStore = ethStore + return this.setupAccounts() + } + // Full Update // returns Promise( @object state ) // diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index ab9410842..1098cc474 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -1,14 +1,6 @@ -const MetamaskConfig = require('../config.js') const ethUtil = require('ethereumjs-util') const normalize = require('eth-sig-util').normalize -const TESTNET_RPC = MetamaskConfig.network.testnet -const MAINNET_RPC = MetamaskConfig.network.mainnet -const MORDEN_RPC = MetamaskConfig.network.morden -const KOVAN_RPC = MetamaskConfig.network.kovan -const RINKEBY_RPC = MetamaskConfig.network.rinkeby - - /* The config-manager is a convenience object * wrapping a pojo-migrator. * @@ -35,36 +27,6 @@ ConfigManager.prototype.getConfig = function () { return data.config } -ConfigManager.prototype.setRpcTarget = function (rpcUrl) { - var config = this.getConfig() - config.provider = { - type: 'rpc', - rpcTarget: rpcUrl, - } - this.setConfig(config) -} - -ConfigManager.prototype.setProviderType = function (type) { - var config = this.getConfig() - config.provider = { - type: type, - } - this.setConfig(config) -} - -ConfigManager.prototype.useEtherscanProvider = function () { - var config = this.getConfig() - config.provider = { - type: 'etherscan', - } - this.setConfig(config) -} - -ConfigManager.prototype.getProvider = function () { - var config = this.getConfig() - return config.provider -} - ConfigManager.prototype.setData = function (data) { this.store.putState(data) } @@ -139,35 +101,6 @@ ConfigManager.prototype.getSeedWords = function () { return data.seedWords } -ConfigManager.prototype.getCurrentRpcAddress = function () { - var provider = this.getProvider() - if (!provider) return null - switch (provider.type) { - - case 'mainnet': - return MAINNET_RPC - - case 'testnet': - return TESTNET_RPC - - case 'morden': - return MORDEN_RPC - - case 'kovan': - return KOVAN_RPC - - case 'rinkeby': - return RINKEBY_RPC - - default: - return provider && provider.rpcTarget ? provider.rpcTarget : TESTNET_RPC - } -} - -// -// Tx -// - ConfigManager.prototype.getTxList = function () { var data = this.getData() if (data.transactions !== undefined) { diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js index 084ca3721..76b311653 100644 --- a/app/scripts/lib/tx-utils.js +++ b/app/scripts/lib/tx-utils.js @@ -1,5 +1,4 @@ const async = require('async') -const EthQuery = require('eth-query') const ethUtil = require('ethereumjs-util') const Transaction = require('ethereumjs-tx') const normalize = require('eth-sig-util').normalize @@ -7,15 +6,14 @@ const BN = ethUtil.BN /* tx-utils are utility methods for Transaction manager -its passed a provider and that is passed to ethquery +its passed ethquery and used to do things like calculate gas of a tx. */ module.exports = class txProviderUtils { - constructor (provider) { - this.provider = provider - this.query = new EthQuery(provider) + constructor (ethQuery) { + this.query = ethQuery } analyzeGasUsage (txMeta, cb) { diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 175602ec1..71293d05f 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -7,9 +7,9 @@ const ObservableStore = require('obs-store') const EthStore = require('./lib/eth-store') const EthQuery = require('eth-query') const streamIntoProvider = require('web3-stream-provider/handler') -const MetaMaskProvider = require('web3-provider-engine/zero.js') const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex const KeyringController = require('./keyring-controller') +const NetworkController = require('./controllers/network') const PreferencesController = require('./controllers/preferences') const CurrencyController = require('./controllers/currency') const NoticeController = require('./notice-controller') @@ -40,8 +40,7 @@ module.exports = class MetamaskController extends EventEmitter { this.store = new ObservableStore(initState) // network store - this.networkStore = new ObservableStore({ network: 'loading' }) - + this.networkController = new NetworkController(initState.NetworkController) // config manager this.configManager = new ConfigManager({ store: this.store, @@ -62,7 +61,7 @@ module.exports = class MetamaskController extends EventEmitter { // rpc provider this.provider = this.initializeProvider() this.provider.on('block', this.logBlock.bind(this)) - this.provider.on('error', this.verifyNetwork.bind(this)) + this.provider.on('error', this.networkController.verifyNetwork.bind(this.networkController)) // eth data query tools this.ethQuery = new EthQuery(this.provider) @@ -75,7 +74,7 @@ module.exports = class MetamaskController extends EventEmitter { this.keyringController = new KeyringController({ initState: initState.KeyringController, ethStore: this.ethStore, - getNetwork: this.getNetworkState.bind(this), + getNetwork: this.networkController.getNetworkState.bind(this.networkController), }) this.keyringController.on('newAccount', (address) => { this.preferencesController.setSelectedAddress(address) @@ -92,10 +91,10 @@ module.exports = class MetamaskController extends EventEmitter { // tx mgmt this.txManager = new TxManager({ initState: initState.TransactionManager, - networkStore: this.networkStore, + networkStore: this.networkController.networkStore, preferencesStore: this.preferencesController.store, txHistoryLimit: 40, - getNetwork: this.getNetworkState.bind(this), + getNetwork: this.networkController.getNetworkState.bind(this), signTransaction: this.keyringController.signTransaction.bind(this.keyringController), provider: this.provider, blockTracker: this.provider, @@ -112,8 +111,34 @@ module.exports = class MetamaskController extends EventEmitter { this.shapeshiftController = new ShapeShiftController({ initState: initState.ShapeShiftController, }) + this.networkController.on('networkSwitch', (providerUtil, claimed) => { + delete this.provider + delete this.ethQuery + delete this.ethStore + console.log('order:@? 1') + this.provider = providerUtil.provider + this.provider.on('block', this.logBlock.bind(this)) + this.provider.on('error', this.networkController.verifyNetwork.bind(this.networkController)) + + this.ethQuery = providerUtil.ethQuery + this.ethStore = new EthStore({ + provider: this.provider, + blockTracker: this.provider, + }) + this.provider.once('block', claimed) + }) + this.networkController.on('networkSwitch', (_, claimed) => { + console.log('order:@? 2') + this.txManager.setupProviderAndEthQuery({ + provider: this.provider, + blockTracker: this.provider, + ethQuery: this.ethQuery, + }) + this.keyringController.setEthStore(this.ethStore) + .then(claimed) + }) - this.lookupNetwork() + this.networkController.lookupNetwork() this.messageManager = new MessageManager() this.personalMessageManager = new PersonalMessageManager() this.publicConfigStore = this.initPublicConfigStore() @@ -140,9 +165,12 @@ module.exports = class MetamaskController extends EventEmitter { this.shapeshiftController.store.subscribe((state) => { this.store.updateState({ ShapeShiftController: state }) }) + this.networkController.providerStore.subscribe((state) => { + this.store.updateState({ NetworkController: state }) + }) // manual mem state subscriptions - this.networkStore.subscribe(this.sendUpdate.bind(this)) + this.networkController.subscribe(this.sendUpdate.bind(this)) this.ethStore.subscribe(this.sendUpdate.bind(this)) this.txManager.memStore.subscribe(this.sendUpdate.bind(this)) this.messageManager.memStore.subscribe(this.sendUpdate.bind(this)) @@ -160,12 +188,12 @@ module.exports = class MetamaskController extends EventEmitter { // initializeProvider () { - const provider = MetaMaskProvider({ + this.networkController.initializeProvider({ static: { eth_syncing: false, web3_clientVersion: `MetaMask/v${version}`, }, - rpcUrl: this.configManager.getCurrentRpcAddress(), + rpcUrl: this.networkController.getCurrentRpcAddress(), // account mgmt getAccounts: (cb) => { const isUnlocked = this.keyringController.memStore.getState().isUnlocked @@ -185,7 +213,7 @@ module.exports = class MetamaskController extends EventEmitter { // new style msg signing processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), }) - return provider + return this.networkController.provider } initPublicConfigStore () { @@ -221,7 +249,7 @@ module.exports = class MetamaskController extends EventEmitter { { isInitialized, }, - this.networkStore.getState(), + this.networkController.getState(), this.ethStore.getState(), this.txManager.memStore.getState(), this.messageManager.memStore.getState(), @@ -255,8 +283,8 @@ module.exports = class MetamaskController extends EventEmitter { return { // etc getState: (cb) => cb(null, this.getState()), - setProviderType: this.setProviderType.bind(this), - useEtherscanProvider: this.useEtherscanProvider.bind(this), + setProviderType: this.networkController.setProviderType.bind(this.networkController), + useEtherscanProvider: this.networkController.useEtherscanProvider.bind(this.networkController), setCurrentCurrency: this.setCurrentCurrency.bind(this), markAccountsFound: this.markAccountsFound.bind(this), // coinbase @@ -592,7 +620,7 @@ module.exports = class MetamaskController extends EventEmitter { // Log blocks logBlock (block) { log.info(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`) - this.verifyNetwork() + this.networkController.verifyNetwork() } setCurrentCurrency (currencyCode, cb) { @@ -612,7 +640,7 @@ module.exports = class MetamaskController extends EventEmitter { buyEth (address, amount) { if (!amount) amount = '5' - const network = this.getNetworkState() + const network = this.networkController.getNetworkState() const url = getBuyEthUrl({ network, address, amount }) if (url) this.platform.openWindow({ url }) } @@ -620,69 +648,21 @@ module.exports = class MetamaskController extends EventEmitter { createShapeShiftTx (depositAddress, depositType) { this.shapeshiftController.createShapeShiftTx(depositAddress, depositType) } - - // - // network - // - - verifyNetwork () { - // Check network when restoring connectivity: - if (this.isNetworkLoading()) this.lookupNetwork() - } +// network setDefaultRpc () { - this.configManager.setRpcTarget('http://localhost:8545') - this.platform.reload() - this.lookupNetwork() + this.networkController.setRpcTarget('http://localhost:8545') return Promise.resolve('http://localhost:8545') } setCustomRpc (rpcTarget, rpcList) { - this.configManager.setRpcTarget(rpcTarget) - return this.preferencesController.updateFrequentRpcList(rpcTarget) - .then(() => { - this.platform.reload() - this.lookupNetwork() - return Promise.resolve(rpcTarget) - }) - } - - setProviderType (type) { - this.configManager.setProviderType(type) - this.platform.reload() - this.lookupNetwork() - } - - useEtherscanProvider () { - this.configManager.useEtherscanProvider() - this.platform.reload() - } - - getNetworkState () { - return this.networkStore.getState().network - } - - setNetworkState (network) { - return this.networkStore.updateState({ network }) - } + this.networkController.setRpcTarget(rpcTarget) - isNetworkLoading () { - return this.getNetworkState() === 'loading' - } - - lookupNetwork (err) { - if (err) { - this.setNetworkState('loading') - } - - this.ethQuery.sendAsync({ method: 'net_version' }, (err, network) => { - if (err) { - this.setNetworkState('loading') - return - } - log.info('web3.getNetwork returned ' + network) - this.setNetworkState(network) + return this.preferencesController.updateFrequentRpcList(rpcTarget) + .then(() => { + return Promise.resolve(rpcTarget) }) } + } diff --git a/app/scripts/transaction-manager.js b/app/scripts/transaction-manager.js index 9f267160f..1e15128f9 100644 --- a/app/scripts/transaction-manager.js +++ b/app/scripts/transaction-manager.js @@ -4,7 +4,6 @@ const extend = require('xtend') const Semaphore = require('semaphore') const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') -const EthQuery = require('eth-query') const TxProviderUtil = require('./lib/tx-utils') const createId = require('./lib/random-id') @@ -18,11 +17,11 @@ module.exports = class TransactionManager extends EventEmitter { this.networkStore = opts.networkStore || new ObservableStore({}) this.preferencesStore = opts.preferencesStore || new ObservableStore({}) this.txHistoryLimit = opts.txHistoryLimit - this.provider = opts.provider - this.blockTracker = opts.blockTracker - this.query = new EthQuery(this.provider) - this.txProviderUtils = new TxProviderUtil(this.provider) - this.blockTracker.on('block', this.checkForTxInBlock.bind(this)) + this.setupProviderAndEthQuery({ + provider: opts.provider, + blockTracker: opts.blockTracker, + ethQuery: opts.ethQuery, + }) this.signEthTx = opts.signTransaction this.nonceLock = Semaphore(1) @@ -41,6 +40,20 @@ module.exports = class TransactionManager extends EventEmitter { return this.networkStore.getState().network } + setupProviderAndEthQuery ({provider, blockTracker, ethQuery}) { + if (this.provider) { + delete this.provider + delete this.blockTracker + delete this.query + delete this.txProviderUtils + } + this.provider = provider + this.query = ethQuery + this.txProviderUtils = new TxProviderUtil(ethQuery) + blockTracker ? this.blockTracker = blockTracker : this.blockTracker = provider + this.blockTracker.on('block', this.checkForTxInBlock.bind(this)) + } + getSelectedAddress () { return this.preferencesStore.getState().selectedAddress } -- cgit v1.2.3 From c5432da567b9953c1294e0bf598a0310127bf808 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Sat, 20 May 2017 20:37:47 -0700 Subject: Add new streaming subprovider but getting a loop Regarding #1458 Uses a new streaming subprovider architecture on an experimental branch of StreamProvider: https://github.com/flyswatter/web3-stream-provider/tree/StreamSubprovider --- app/scripts/lib/inpage-provider.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js index e5e398e24..88d81cca5 100644 --- a/app/scripts/lib/inpage-provider.js +++ b/app/scripts/lib/inpage-provider.js @@ -1,5 +1,6 @@ const pipe = require('pump') -const StreamProvider = require('web3-stream-provider') +const StreamSubprovider = require('web3-stream-provider/stream-subprovider') +const ProviderEngine = require('web3-provider-engine') const LocalStorageStore = require('obs-store') const ObjectMultiplex = require('./obj-multiplex') const createRandomId = require('./random-id') @@ -27,14 +28,21 @@ function MetamaskInpageProvider (connectionStream) { ) // connect to async provider - const asyncProvider = self.asyncProvider = new StreamProvider() + const engine = self.asyncProvider = new ProviderEngine() + + const stream = self.stream = new StreamSubprovider() + engine.addProvider(stream) + pipe( - asyncProvider, + stream, multiStream.createStream('provider'), - asyncProvider, + stream, (err) => logStreamDisconnectWarning('MetaMask RpcProvider', err) ) + // start polling + engine.start() + self.idMap = {} // handle sendAsync requests via asyncProvider self.sendAsync = function (payload, cb) { @@ -46,7 +54,9 @@ function MetamaskInpageProvider (connectionStream) { return message }) // forward to asyncProvider - asyncProvider.sendAsync(request, function (err, res) { + console.log('sending async to engine', request) + engine.sendAsync(request, function (err, res) { + console.log('send async returned !!', err, res) if (err) return cb(err) // transform messages to original ids eachJsonMessage(res, (message) => { -- cgit v1.2.3 From 6209224a6c1129817ebb4cd4a433cf456631c33a Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Sun, 21 May 2017 14:09:44 -0700 Subject: Add transaction number (nonce) to tx list --- CHANGELOG.md | 2 ++ ui/app/components/transaction-list-item.js | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 371b348ca..7f894f935 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Add Transaction Number (nonce) to transaction list. + ## 3.6.5 2017-5-17 - Fix bug where edited gas parameters would not take effect. diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js index c2a585003..18ee10578 100644 --- a/ui/app/components/transaction-list-item.js +++ b/ui/app/components/transaction-list-item.js @@ -8,6 +8,7 @@ const explorerLink = require('../../lib/explorer-link') const CopyButton = require('./copyButton') const vreme = new (require('vreme')) const Tooltip = require('./tooltip') +const BN = require('ethereumjs-util').BN const TransactionIcon = require('./transaction-list-item-icon') const ShiftListItem = require('./shift-list-item') @@ -39,6 +40,8 @@ TransactionListItem.prototype.render = function () { txParams = transaction.msgParams } + const nonce = (new BN(txParams.nonce.substr(2))).toString(10) + const isClickable = ('hash' in transaction && isLinkable) || isPending return ( h(`.transaction-list-item.flex-row.flex-space-between${isClickable ? '.pointer' : ''}`, { @@ -69,6 +72,24 @@ TransactionListItem.prototype.render = function () { ]), ]), + h(Tooltip, { + title: 'Transaction Number', + position: 'bottom', + }, + [ + h('span', { + style: { + display: 'flex', + cursor: 'normal', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + padding: '10px', + }, + }, nonce), + ]), + + h('.flex-column', {style: {width: '200px', overflow: 'hidden'}}, [ domainField(txParams), h('div', date), -- cgit v1.2.3 From 3c90024564bee78fe0a9178d3772efaabe147ac5 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Sun, 21 May 2017 14:15:34 -0700 Subject: Label the pending tx icon with a tooltip --- CHANGELOG.md | 1 + ui/app/components/transaction-list-item-icon.js | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f894f935..50fa0d858 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Current Master - Add Transaction Number (nonce) to transaction list. +- Label the pending tx icon with a tooltip. ## 3.6.5 2017-5-17 diff --git a/ui/app/components/transaction-list-item-icon.js b/ui/app/components/transaction-list-item-icon.js index d63cae259..03c6f6615 100644 --- a/ui/app/components/transaction-list-item-icon.js +++ b/ui/app/components/transaction-list-item-icon.js @@ -1,6 +1,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits +const Tooltip = require('./tooltip') const Identicon = require('./identicon') @@ -32,11 +33,17 @@ TransactionIcon.prototype.render = function () { }) case 'submitted': - return h('i.fa.fa-ellipsis-h', { - style: { - fontSize: '27px', - }, - }) + return h(Tooltip, { + title: 'Pending', + position: 'bottom', + }, + [ + h('i.fa.fa-ellipsis-h', { + style: { + fontSize: '27px', + }, + }) + ]) } if (isMsg) { -- cgit v1.2.3 From 0ef9e8b7094374b44d7a3bed6730d9d6815a17ec Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Sun, 21 May 2017 14:18:23 -0700 Subject: Lint --- ui/app/components/transaction-list-item-icon.js | 5 ++--- ui/app/components/transaction-list-item.js | 4 +--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/ui/app/components/transaction-list-item-icon.js b/ui/app/components/transaction-list-item-icon.js index 03c6f6615..431054340 100644 --- a/ui/app/components/transaction-list-item-icon.js +++ b/ui/app/components/transaction-list-item-icon.js @@ -36,13 +36,12 @@ TransactionIcon.prototype.render = function () { return h(Tooltip, { title: 'Pending', position: 'bottom', - }, - [ + }, [ h('i.fa.fa-ellipsis-h', { style: { fontSize: '27px', }, - }) + }), ]) } diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js index 18ee10578..e0612c7bf 100644 --- a/ui/app/components/transaction-list-item.js +++ b/ui/app/components/transaction-list-item.js @@ -75,8 +75,7 @@ TransactionListItem.prototype.render = function () { h(Tooltip, { title: 'Transaction Number', position: 'bottom', - }, - [ + }, [ h('span', { style: { display: 'flex', @@ -89,7 +88,6 @@ TransactionListItem.prototype.render = function () { }, nonce), ]), - h('.flex-column', {style: {width: '200px', overflow: 'hidden'}}, [ domainField(txParams), h('div', date), -- cgit v1.2.3 From 954d8bd111ea70b823267b89edb415e2d28caec3 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 22 May 2017 14:14:13 -0700 Subject: Render txs with no nonce --- ui/app/components/transaction-list-item.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js index e0612c7bf..4f0e6132a 100644 --- a/ui/app/components/transaction-list-item.js +++ b/ui/app/components/transaction-list-item.js @@ -40,7 +40,7 @@ TransactionListItem.prototype.render = function () { txParams = transaction.msgParams } - const nonce = (new BN(txParams.nonce.substr(2))).toString(10) + const nonce = txParams.nonce ? (new BN(txParams.nonce.substr(2))).toString(10) : '' const isClickable = ('hash' in transaction && isLinkable) || isPending return ( -- cgit v1.2.3 From 709c0eb307e2cda9aa16b67191a43e99e1b22fa0 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 22 May 2017 15:21:25 -0700 Subject: Use stream-provider v3 api --- app/scripts/popup-core.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/popup-core.js b/app/scripts/popup-core.js index f1eb394d7..7de1a6fda 100644 --- a/app/scripts/popup-core.js +++ b/app/scripts/popup-core.js @@ -29,9 +29,9 @@ function connectToAccountManager (connectionStream, cb) { function setupWeb3Connection (connectionStream) { var providerStream = new StreamProvider() - providerStream.pipe(connectionStream).pipe(providerStream) + providerStream.stream.pipe(connectionStream).pipe(providerStream.stream) connectionStream.on('error', console.error.bind(console)) - providerStream.on('error', console.error.bind(console)) + providerStream.stream.on('error', console.error.bind(console)) global.ethereumProvider = providerStream global.ethQuery = new EthQuery(providerStream) } -- cgit v1.2.3 From 48d9a2107130e3850077c6c1789b29a09634b168 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 22 May 2017 15:23:29 -0700 Subject: Use filter subprovider in-page to avoid filter leaks --- app/scripts/lib/inpage-provider.js | 8 ++++++-- package.json | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js index 88d81cca5..9dea05dbb 100644 --- a/app/scripts/lib/inpage-provider.js +++ b/app/scripts/lib/inpage-provider.js @@ -1,6 +1,7 @@ const pipe = require('pump') -const StreamSubprovider = require('web3-stream-provider/stream-subprovider') const ProviderEngine = require('web3-provider-engine') +const FilterSubprovider = require('web3-provider-engine/subproviders/filters') +const StreamSubprovider = require('web3-stream-provider/stream-subprovider') const LocalStorageStore = require('obs-store') const ObjectMultiplex = require('./obj-multiplex') const createRandomId = require('./random-id') @@ -28,7 +29,10 @@ function MetamaskInpageProvider (connectionStream) { ) // connect to async provider - const engine = self.asyncProvider = new ProviderEngine() + const engine = new ProviderEngine() + + const filterSubprovider = new FilterSubprovider() + engine.addProvider(filterSubprovider) const stream = self.stream = new StreamSubprovider() engine.addProvider(stream) diff --git a/package.json b/package.json index 14ddd2886..5512fa6a4 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,7 @@ "vreme": "^3.0.2", "web3": "0.18.2", "web3-provider-engine": "^12.0.6", - "web3-stream-provider": "^2.0.6", + "web3-stream-provider": "^3.0.0", "xtend": "^4.0.1" }, "devDependencies": { -- cgit v1.2.3 From 39f9ffa18ab76fe154f9d5d4b3b2e5631d95fdc4 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 22 May 2017 15:25:31 -0700 Subject: Bump changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 371b348ca..c31f26f04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Fix bug where website filters would pile up and not deallocate when leaving a site. + ## 3.6.5 2017-5-17 - Fix bug where edited gas parameters would not take effect. -- cgit v1.2.3 From cbfaa6f56f685d515e170de2bf305da76b8ec1e0 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 22 May 2017 15:41:13 -0700 Subject: Rename stream to streamSubprovider --- app/scripts/lib/inpage-provider.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js index 9dea05dbb..3b60756b9 100644 --- a/app/scripts/lib/inpage-provider.js +++ b/app/scripts/lib/inpage-provider.js @@ -34,13 +34,13 @@ function MetamaskInpageProvider (connectionStream) { const filterSubprovider = new FilterSubprovider() engine.addProvider(filterSubprovider) - const stream = self.stream = new StreamSubprovider() - engine.addProvider(stream) + const streamSubprovider = new StreamSubprovider() + engine.addProvider(streamSubprovider) pipe( - stream, + streamSubprovider, multiStream.createStream('provider'), - stream, + streamSubprovider, (err) => logStreamDisconnectWarning('MetaMask RpcProvider', err) ) -- cgit v1.2.3 From 058b73221393467549cca04937267635e471aae1 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 22 May 2017 15:43:20 -0700 Subject: Tolerate nonces of any format --- package.json | 1 + ui/app/components/transaction-list-item.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 14ddd2886..6fb9513d5 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "mississippi": "^1.2.0", "mkdirp": "^0.5.1", "multiplex": "^6.7.0", + "number-to-bn": "^1.7.0", "obs-store": "^2.3.1", "once": "^1.3.3", "ping-pong-stream": "^1.0.0", diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js index 4f0e6132a..dbda66a31 100644 --- a/ui/app/components/transaction-list-item.js +++ b/ui/app/components/transaction-list-item.js @@ -8,7 +8,7 @@ const explorerLink = require('../../lib/explorer-link') const CopyButton = require('./copyButton') const vreme = new (require('vreme')) const Tooltip = require('./tooltip') -const BN = require('ethereumjs-util').BN +const numberToBN = require('number-to-bn') const TransactionIcon = require('./transaction-list-item-icon') const ShiftListItem = require('./shift-list-item') @@ -40,7 +40,7 @@ TransactionListItem.prototype.render = function () { txParams = transaction.msgParams } - const nonce = txParams.nonce ? (new BN(txParams.nonce.substr(2))).toString(10) : '' + const nonce = txParams.nonce ? numberToBN(txParams.nonce).toString(10) : '' const isClickable = ('hash' in transaction && isLinkable) || isPending return ( -- cgit v1.2.3 From 1c1400b584a97e05e3f39748e5f44f076328d89b Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 22 May 2017 15:59:07 -0700 Subject: deps - use stream-subprovider from provider-engine --- app/scripts/lib/inpage-provider.js | 2 +- app/scripts/popup-core.js | 4 ++-- package.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js index 3b60756b9..d24121ade 100644 --- a/app/scripts/lib/inpage-provider.js +++ b/app/scripts/lib/inpage-provider.js @@ -1,7 +1,7 @@ const pipe = require('pump') const ProviderEngine = require('web3-provider-engine') const FilterSubprovider = require('web3-provider-engine/subproviders/filters') -const StreamSubprovider = require('web3-stream-provider/stream-subprovider') +const StreamSubprovider = require('web3-provider-engine/subproviders/stream') const LocalStorageStore = require('obs-store') const ObjectMultiplex = require('./obj-multiplex') const createRandomId = require('./random-id') diff --git a/app/scripts/popup-core.js b/app/scripts/popup-core.js index 7de1a6fda..f1eb394d7 100644 --- a/app/scripts/popup-core.js +++ b/app/scripts/popup-core.js @@ -29,9 +29,9 @@ function connectToAccountManager (connectionStream, cb) { function setupWeb3Connection (connectionStream) { var providerStream = new StreamProvider() - providerStream.stream.pipe(connectionStream).pipe(providerStream.stream) + providerStream.pipe(connectionStream).pipe(providerStream) connectionStream.on('error', console.error.bind(console)) - providerStream.stream.on('error', console.error.bind(console)) + providerStream.on('error', console.error.bind(console)) global.ethereumProvider = providerStream global.ethQuery = new EthQuery(providerStream) } diff --git a/package.json b/package.json index 5512fa6a4..dba82d17c 100644 --- a/package.json +++ b/package.json @@ -121,8 +121,8 @@ "valid-url": "^1.0.9", "vreme": "^3.0.2", "web3": "0.18.2", - "web3-provider-engine": "^12.0.6", - "web3-stream-provider": "^3.0.0", + "web3-provider-engine": "^12.1.0", + "web3-stream-provider": "^2.0.6", "xtend": "^4.0.1" }, "devDependencies": { -- cgit v1.2.3 From b217ad1ae8109e9648da948bde06cdc88317396a Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 22 May 2017 16:06:22 -0700 Subject: clean - remove console logs --- app/scripts/lib/inpage-provider.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js index d24121ade..e54f547bd 100644 --- a/app/scripts/lib/inpage-provider.js +++ b/app/scripts/lib/inpage-provider.js @@ -58,9 +58,7 @@ function MetamaskInpageProvider (connectionStream) { return message }) // forward to asyncProvider - console.log('sending async to engine', request) engine.sendAsync(request, function (err, res) { - console.log('send async returned !!', err, res) if (err) return cb(err) // transform messages to original ids eachJsonMessage(res, (message) => { -- cgit v1.2.3 From be5af7cb4bb44731504357e010bf78b3eda9d543 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 22 May 2017 17:45:29 -0700 Subject: Throw if ENS Resolver isn't set up Instead of resolving to name owners, which can encourage inconsistent usage of ENS. Fixes #1427. --- CHANGELOG.md | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 265c239b2..dab16c89a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Add Transaction Number (nonce) to transaction list. - Label the pending tx icon with a tooltip. - Fix bug where website filters would pile up and not deallocate when leaving a site. +- ENS names will no longer resolve to their owner if no resolver is set. Resolvers must be explicitly set and configured. ## 3.6.5 2017-5-17 diff --git a/package.json b/package.json index 271e2a397..6b6996d9d 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "ethereumjs-tx": "^1.3.0", "ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", - "ethjs-ens": "^1.0.2", + "ethjs-ens": "^2.0.0", "express": "^4.14.0", "extension-link-enabler": "^1.0.0", "extensionizer": "^1.0.0", -- cgit v1.2.3 From e08c1541e5a91d7958b15753982d22066c1a0a7d Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 23 May 2017 01:55:20 -0400 Subject: Add a migration for the network controller --- app/scripts/migrations/014.js | 34 ++++++++++++++++++++++++++++++++++ app/scripts/migrations/index.js | 1 + 2 files changed, 35 insertions(+) create mode 100644 app/scripts/migrations/014.js diff --git a/app/scripts/migrations/014.js b/app/scripts/migrations/014.js new file mode 100644 index 000000000..0fe92125b --- /dev/null +++ b/app/scripts/migrations/014.js @@ -0,0 +1,34 @@ +const version = 14 + +/* + +This migration removes provider from config and moves it too NetworkController. + +*/ + +const clone = require('clone') + +module.exports = { + version, + + migrate: function (originalVersionedData) { + const versionedData = clone(originalVersionedData) + versionedData.meta.version = version + try { + const state = versionedData.data + const newState = transformState(state) + versionedData.data = newState + } catch (err) { + console.warn(`MetaMask Migration #${version}` + err.stack) + } + return Promise.resolve(versionedData) + }, +} + +function transformState (state) { + const newState = state + newState.NetworkController = {} + newState.NetworkController.provider = newState.config.provider + delete newState.config.provider + return newState +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 3a95cf88e..fb1ad7863 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -24,4 +24,5 @@ module.exports = [ require('./011'), require('./012'), require('./013'), + require('./014'), ] -- cgit v1.2.3 From 529304c005318852b60bb93846a58d6eb3da2066 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 23 May 2017 01:56:10 -0400 Subject: Wrap the provider in a proxy --- app/scripts/controllers/network.js | 110 +++++++++++++++----------------- app/scripts/controllers/transactions.js | 27 ++------ app/scripts/keyring-controller.js | 6 -- app/scripts/lib/config-manager.js | 5 +- app/scripts/metamask-controller.js | 42 ++---------- 5 files changed, 64 insertions(+), 126 deletions(-) diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js index 82eabb573..97c2ccbc2 100644 --- a/app/scripts/controllers/network.js +++ b/app/scripts/controllers/network.js @@ -3,21 +3,29 @@ const MetaMaskProvider = require('web3-provider-engine/zero.js') const ObservableStore = require('obs-store') const extend = require('xtend') const EthQuery = require('eth-query') -const MetamaskConfig = require('../config.js') - -const TESTNET_RPC = MetamaskConfig.network.testnet -const MAINNET_RPC = MetamaskConfig.network.mainnet -const MORDEN_RPC = MetamaskConfig.network.morden -const KOVAN_RPC = MetamaskConfig.network.kovan -const RINKEBY_RPC = MetamaskConfig.network.rinkeby +const RPC_ADDRESS_LIST = require('../config.js').network module.exports = class NetworkController extends EventEmitter { constructor (providerOpts) { super() this.networkStore = new ObservableStore({ network: 'loading' }) - providerOpts.provider.rpcTarget = this.getRpcAddressForType(providerOpts.provider.type) + providerOpts.provider.rpcTarget = this.getRpcAddressForType(providerOpts.provider.type, providerOpts.provider) this.providerStore = new ObservableStore(providerOpts) - this._claimed = 0 + this.store = new ObservableStore(extend(this.networkStore.getState(), this.providerStore.getState())) + + this._providerListners = {} + + this.networkStore.subscribe((state) => this.store.updateState(state)) + this.providerStore.subscribe((state) => this.store.updateState(state)) + this.on('networkSwitch', this.lookupNetwork) + } + + get provider () { + return this._proxy + } + + set provider (provider) { + this._provider = provider } getState () { @@ -29,28 +37,35 @@ module.exports = class NetworkController extends EventEmitter { initializeProvider (opts) { this.providerConfig = opts - this.provider = MetaMaskProvider(opts) + this._provider = MetaMaskProvider(opts) + this._proxy = new Proxy(this._provider, { + get: (obj, name) => { + if (name === 'on') return this._on.bind(this) + return this._provider[name] + }, + set: (obj, name, value) => { + this._provider[name] = value + }, + }) + this.provider.on('block', this._logBlock.bind(this)) + this.provider.on('error', this.verifyNetwork.bind(this)) this.ethQuery = new EthQuery(this.provider) this.lookupNetwork() - return Promise.resolve(this.provider) + return this.provider } + switchNetwork (providerConfig) { - delete this.provider - delete this.ethQuery const newConfig = extend(this.providerConfig, providerConfig) this.providerConfig = newConfig + this.provider = MetaMaskProvider(newConfig) - this.ethQuery = new EthQuery(this.provider) - this.emit('networkSwitch', { - provider: this.provider, - ethQuery: this.ethQuery, - }, this.claim.bind(this)) + // apply the listners created by other controllers + Object.keys(this._providerListners).forEach((key) => { + this._providerListners[key].forEach((handler) => this._provider.addListener(key, handler)) + }) + this.emit('networkSwitch', this.provider) } - subscribe (cb) { - this.networkStore.subscribe(cb) - this.providerStore.subscribe(cb) - } verifyNetwork () { // Check network when restoring connectivity: @@ -74,7 +89,6 @@ module.exports = class NetworkController extends EventEmitter { this.ethQuery.sendAsync({ method: 'net_version' }, (err, network) => { if (err) return this.setNetworkState('loading') - log.info('web3.getNetwork returned ' + network) this.setNetworkState(network) }) @@ -102,51 +116,27 @@ module.exports = class NetworkController extends EventEmitter { this.switchNetwork({ rpcUrl: rpcTarget, }) - this.once('claimed', () => { - this.providerStore.updateState({provider: {type, rpcTarget}}) - console.log('CLAIMED') - this.lookupNetwork() - }) - - } - - useEtherscanProvider () { - this.setProviderType('etherscan') + this.providerStore.updateState({provider: {type, rpcTarget}}) } getProvider () { return this.providerStore.getState().provider } - getRpcAddressForType (type) { - const provider = this.getProvider() - switch (type) { - - case 'mainnet': - return MAINNET_RPC - - case 'testnet': - return TESTNET_RPC - - case 'morden': - return MORDEN_RPC - - case 'kovan': - return KOVAN_RPC - - case 'rinkeby': - return RINKEBY_RPC + getRpcAddressForType (type, provider = this.getProvider()) { + console.log(`#getRpcAddressForType: ${type}`) + if (type in RPC_ADDRESS_LIST) return RPC_ADDRESS_LIST[type] + return provider && provider.rpcTarget ? provider.rpcTarget : RPC_ADDRESS_LIST['rinkeby'] + } - default: - return provider && provider.rpcTarget ? provider.rpcTarget : TESTNET_RPC - } + _logBlock (block) { + log.info(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`) + this.verifyNetwork() } - claim () { - this._claimed += 1 - if (this._claimed === this.listenerCount('networkSwitch')) { - this.emit('claimed') - this._claimed = 0 - } + _on (event, handler) { + if (!this._providerListners[event]) this._providerListners[event] = [] + this._providerListners[event].push(handler) + this._provider.on(event, handler) } } diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index cfeeab6e6..b9bea2f1c 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -4,7 +4,6 @@ const extend = require('xtend') const Semaphore = require('semaphore') const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') -const EthQuery = require('eth-query') const TxProviderUtil = require('../lib/tx-utils') const createId = require('../lib/random-id') @@ -18,11 +17,13 @@ module.exports = class TransactionManager extends EventEmitter { this.networkStore = opts.networkStore || new ObservableStore({}) this.preferencesStore = opts.preferencesStore || new ObservableStore({}) this.txHistoryLimit = opts.txHistoryLimit - this.setupProviderAndEthQuery({ - provider: opts.provider, - blockTracker: opts.blockTracker, - ethQuery: opts.ethQuery, - }) + this.provider = opts.provider + this.blockTracker = opts.blockTracker + this.query = opts.ethQuery + this.txProviderUtils = new TxProviderUtil(this.query) + this.networkStore.subscribe((_) => this.blockTracker.on('block', this.checkForTxInBlock.bind(this))) + this.blockTracker.on('block', this.checkForTxInBlock.bind(this)) + this.signEthTx = opts.signTransaction this.nonceLock = Semaphore(1) @@ -41,20 +42,6 @@ module.exports = class TransactionManager extends EventEmitter { return this.networkStore.getState().network } - setupProviderAndEthQuery ({provider, blockTracker, ethQuery}) { - if (this.provider) { - delete this.provider - delete this.blockTracker - delete this.query - delete this.txProviderUtils - } - this.provider = provider - this.query = ethQuery - this.txProviderUtils = new TxProviderUtil(ethQuery) - blockTracker ? this.blockTracker = blockTracker : this.blockTracker = provider - this.blockTracker.on('block', this.checkForTxInBlock.bind(this)) - } - getSelectedAddress () { return this.preferencesStore.getState().selectedAddress } diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js index bb699ca8b..5b3c80e40 100644 --- a/app/scripts/keyring-controller.js +++ b/app/scripts/keyring-controller.js @@ -41,12 +41,6 @@ class KeyringController extends EventEmitter { this.getNetwork = opts.getNetwork } - setEthStore (ethStore) { - delete this.ethStore - this.ethStore = ethStore - return this.setupAccounts() - } - // Full Update // returns Promise( @object state ) // diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index 6ca9bd9ea..fee8423fa 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -1,14 +1,13 @@ const ethUtil = require('ethereumjs-util') const normalize = require('eth-sig-util').normalize +const MetamaskConfig = require('../config.js') + -<<<<<<< HEAD -======= const MAINNET_RPC = MetamaskConfig.network.mainnet const ROPSTEN_RPC = MetamaskConfig.network.ropsten const KOVAN_RPC = MetamaskConfig.network.kovan const RINKEBY_RPC = MetamaskConfig.network.rinkeby ->>>>>>> master /* The config-manager is a convenience object * wrapping a pojo-migrator. * diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b93f627bb..ef82da0d3 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -40,6 +40,7 @@ module.exports = class MetamaskController extends EventEmitter { this.store = new ObservableStore(initState) // network store + this.networkController = new NetworkController(initState.NetworkController) // config manager this.configManager = new ConfigManager({ @@ -60,12 +61,11 @@ module.exports = class MetamaskController extends EventEmitter { // rpc provider this.provider = this.initializeProvider() - this.provider.on('block', this.logBlock.bind(this)) - this.provider.on('error', this.networkController.verifyNetwork.bind(this.networkController)) // eth data query tools this.ethQuery = new EthQuery(this.provider) this.ethStore = new EthStore({ + network: this.networkController.networkStore, provider: this.provider, blockTracker: this.provider, }) @@ -111,32 +111,6 @@ module.exports = class MetamaskController extends EventEmitter { this.shapeshiftController = new ShapeShiftController({ initState: initState.ShapeShiftController, }) - this.networkController.on('networkSwitch', (providerUtil, claimed) => { - delete this.provider - delete this.ethQuery - delete this.ethStore - console.log('order:@? 1') - this.provider = providerUtil.provider - this.provider.on('block', this.logBlock.bind(this)) - this.provider.on('error', this.networkController.verifyNetwork.bind(this.networkController)) - - this.ethQuery = providerUtil.ethQuery - this.ethStore = new EthStore({ - provider: this.provider, - blockTracker: this.provider, - }) - this.provider.once('block', claimed) - }) - this.networkController.on('networkSwitch', (_, claimed) => { - console.log('order:@? 2') - this.txManager.setupProviderAndEthQuery({ - provider: this.provider, - blockTracker: this.provider, - ethQuery: this.ethQuery, - }) - this.keyringController.setEthStore(this.ethStore) - .then(claimed) - }) this.networkController.lookupNetwork() this.messageManager = new MessageManager() @@ -170,7 +144,7 @@ module.exports = class MetamaskController extends EventEmitter { }) // manual mem state subscriptions - this.networkController.subscribe(this.sendUpdate.bind(this)) + this.networkController.store.subscribe(this.sendUpdate.bind(this)) this.ethStore.subscribe(this.sendUpdate.bind(this)) this.txController.memStore.subscribe(this.sendUpdate.bind(this)) this.messageManager.memStore.subscribe(this.sendUpdate.bind(this)) @@ -188,7 +162,7 @@ module.exports = class MetamaskController extends EventEmitter { // initializeProvider () { - this.networkController.initializeProvider({ + return this.networkController.initializeProvider({ static: { eth_syncing: false, web3_clientVersion: `MetaMask/v${version}`, @@ -213,7 +187,6 @@ module.exports = class MetamaskController extends EventEmitter { // new style msg signing processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), }) - return this.networkController.provider } initPublicConfigStore () { @@ -249,7 +222,7 @@ module.exports = class MetamaskController extends EventEmitter { { isInitialized, }, - this.networkController.getState(), + this.networkController.store.getState(), this.ethStore.getState(), this.txController.memStore.getState(), this.messageManager.memStore.getState(), @@ -284,7 +257,6 @@ module.exports = class MetamaskController extends EventEmitter { // etc getState: (cb) => cb(null, this.getState()), setProviderType: this.networkController.setProviderType.bind(this.networkController), - useEtherscanProvider: this.networkController.useEtherscanProvider.bind(this.networkController), setCurrentCurrency: this.setCurrentCurrency.bind(this), markAccountsFound: this.markAccountsFound.bind(this), // coinbase @@ -618,10 +590,6 @@ module.exports = class MetamaskController extends EventEmitter { // // Log blocks - logBlock (block) { - log.info(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`) - this.networkController.verifyNetwork() - } setCurrentCurrency (currencyCode, cb) { try { -- cgit v1.2.3 From 959038132a6780f1dd7a4db3696d3fdbaad83b88 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Tue, 23 May 2017 10:43:37 -0700 Subject: Increase accuracy of our rounding schemes. --- ui/app/components/bn-as-decimal-input.js | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/ui/app/components/bn-as-decimal-input.js b/ui/app/components/bn-as-decimal-input.js index d0eebe09e..fbe36abfb 100644 --- a/ui/app/components/bn-as-decimal-input.js +++ b/ui/app/components/bn-as-decimal-input.js @@ -30,8 +30,8 @@ BnAsDecimalInput.prototype.render = function () { const suffix = props.suffix const style = props.style - const scale = Math.pow(10, precision) - const newValue = value.toNumber(10) / scale + const valueString = value.toString(10) + const newValue = downsize(valueString, precision, precision) return ( h('.flex-column', [ @@ -63,7 +63,9 @@ BnAsDecimalInput.prototype.render = function () { onChange: (event) => { this.updateValidity(event) const value = (event.target.value === '') ? '' : event.target.value - const scaledNumber = Math.floor(scale * value) + + + const scaledNumber = upsize(value, precision, precision) const precisionBN = new BN(scaledNumber, 10) onChange(precisionBN) }, @@ -141,3 +143,24 @@ BnAsDecimalInput.prototype.constructWarning = function () { return message } + + +function downsize (number, scale, precision) { + if (scale === 0) { + return Number(number) + } else { + var decimals = (scale === precision) ? -1 : scale - precision + return Number(number.slice(0, -scale) + '.' + number.slice(-scale, decimals)) + } +} + +function upsize (number, scale, precision) { + var string = number.toString() + var stringArray = string.split('.') + var decimalLength = stringArray[1] ? stringArray[1].length : 0 + var newString = ((scale === 0) || (decimalLength === 0)) ? stringArray[0] : stringArray[0] + stringArray[1].slice(0, precision) + for (var i = decimalLength; i < scale; i++) { + newString += '0' + } + return newString +} -- cgit v1.2.3 From cd2ad1733da16aa3d84158e5df9fbf33f51450aa Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 23 May 2017 11:49:25 -0700 Subject: Continually resubmit pending txs --- CHANGELOG.md | 1 + app/scripts/controllers/transactions.js | 54 +++++++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 265c239b2..e84413bd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Add Transaction Number (nonce) to transaction list. - Label the pending tx icon with a tooltip. - Fix bug where website filters would pile up and not deallocate when leaving a site. +- Continually resubmit pending txs for a period of time to ensure successful broadcast. ## 3.6.5 2017-5-17 diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 21dd25b30..3d86a171e 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -7,6 +7,10 @@ const ethUtil = require('ethereumjs-util') const EthQuery = require('eth-query') const TxProviderUtil = require('../lib/tx-utils') const createId = require('../lib/random-id') +const denodeify = require('denodeify') + +const RETRY_LIMIT = 200 +const RESUBMIT_INTERVAL = 10000 // Ten seconds module.exports = class TransactionManager extends EventEmitter { constructor (opts) { @@ -31,6 +35,8 @@ module.exports = class TransactionManager extends EventEmitter { this.store.subscribe(() => this._updateMemstore()) this.networkStore.subscribe(() => this._updateMemstore()) this.preferencesStore.subscribe(() => this._updateMemstore()) + + this.continuallyResubmitPendingTxs() } getState () { @@ -230,7 +236,11 @@ module.exports = class TransactionManager extends EventEmitter { }) } - publishTransaction (txId, rawTx, cb) { + publishTransaction (txId, rawTx, cb = warn) { + const txMeta = this.getTx(txId) + txMeta.rawTx = rawTx + this.updateTx(txMeta) + this.txProviderUtils.publishTransaction(rawTx, (err, txHash) => { if (err) return cb(err) this.setTxHash(txId, txHash) @@ -353,7 +363,7 @@ module.exports = class TransactionManager extends EventEmitter { message: 'There was a problem loading this transaction.', } this.updateTx(txMeta) - return console.error(err) + return log.error(err) } if (txParams.blockNumber) { this.setTxStatusConfirmed(txId) @@ -379,6 +389,7 @@ module.exports = class TransactionManager extends EventEmitter { this.emit(`${txMeta.id}:${status}`, txId) if (status === 'submitted' || status === 'rejected') { this.emit(`${txMeta.id}:finished`, txMeta) + } this.updateTx(txMeta) this.emit('updateBadge') @@ -398,6 +409,45 @@ module.exports = class TransactionManager extends EventEmitter { }) this.memStore.updateState({ unapprovedTxs, selectedAddressTxList }) } + + continuallyResubmitPendingTxs () { + const pending = this.getTxsByMetaData('status', 'submitted') + const resubmit = denodeify(this.resubmitTx.bind(this)) + Promise.all(pending.map(txMeta => resubmit(txMeta))) + .catch((reason) => { + log.info('Problem resubmitting tx', reason) + }) + .then(() => { + global.setTimeout(() => { + this.continuallyResubmitPendingTxs() + }, RESUBMIT_INTERVAL) + }) + } + + resubmitTx (txMeta, cb) { + // Increment a try counter. + if (!('retryCount' in txMeta)) { + txMeta.retryCount = 0 + } + + // Only auto-submit already-signed txs: + if (!('rawTx' in txMeta)) { + return cb() + } + + if (txMeta.retryCount > RETRY_LIMIT) { + txMeta.err = { + isWarning: true, + message: 'Gave up submitting tx.', + } + this.updateTx(txMeta) + return log.error(txMeta.err.message) + } + + txMeta.retryCount++ + const rawTx = txMeta.rawTx + this.txProviderUtils.publishTransaction(rawTx, cb) + } } -- cgit v1.2.3 From 31d17c9e25458cd47f8c18ec3b967aecff236ba6 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Tue, 23 May 2017 14:26:37 -0700 Subject: Fix test, create new value for precision/scale --- test/unit/components/bn-as-decimal-input-test.js | 5 +++-- ui/app/components/bn-as-decimal-input.js | 6 +++--- ui/app/components/pending-tx.js | 2 ++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/test/unit/components/bn-as-decimal-input-test.js b/test/unit/components/bn-as-decimal-input-test.js index f515003bb..6fe684dc5 100644 --- a/test/unit/components/bn-as-decimal-input-test.js +++ b/test/unit/components/bn-as-decimal-input-test.js @@ -10,7 +10,6 @@ var BnInput = require('../../../ui/app/components/bn-as-decimal-input') describe('BnInput', function () { it('can tolerate a gas decimal number at a high precision', function (done) { - const renderer = ReactTestUtils.createRenderer() let valueStr = '20' @@ -27,10 +26,12 @@ describe('BnInput', function () { } const target = new BN(targetStr, 10) - const precision = 1e18 // ether precision + const precision = 13 // ether precision + const scale = 13 const props = { value, + scale, precision, onChange: (newBn) => { assert.equal(newBn.toString(), target.toString(), 'should tolerate increase') diff --git a/ui/app/components/bn-as-decimal-input.js b/ui/app/components/bn-as-decimal-input.js index fbe36abfb..8f56f29ab 100644 --- a/ui/app/components/bn-as-decimal-input.js +++ b/ui/app/components/bn-as-decimal-input.js @@ -26,12 +26,12 @@ BnAsDecimalInput.prototype.render = function () { const props = this.props const state = this.state - const { value, precision, onChange, min, max } = props + const { value, scale, precision, onChange, min, max } = props const suffix = props.suffix const style = props.style const valueString = value.toString(10) - const newValue = downsize(valueString, precision, precision) + const newValue = downsize(valueString, scale, precision) return ( h('.flex-column', [ @@ -65,7 +65,7 @@ BnAsDecimalInput.prototype.render = function () { const value = (event.target.value === '') ? '' : event.target.value - const scaledNumber = upsize(value, precision, precision) + const scaledNumber = upsize(value, scale, precision) const precisionBN = new BN(scaledNumber, 10) onChange(precisionBN) }, diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 5b238187c..eed0fd9ae 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -156,6 +156,7 @@ PendingTx.prototype.render = function () { name: 'Gas Limit', value: gasBn, precision: 0, + scale: 0, // The hard lower limit for gas. min: MIN_GAS_LIMIT_BN.toString(10), suffix: 'UNITS', @@ -179,6 +180,7 @@ PendingTx.prototype.render = function () { name: 'Gas Price', value: gasPriceBn, precision: 9, + scale: 9, suffix: 'GWEI', min: MIN_GAS_PRICE_GWEI_BN.toString(10), style: { -- cgit v1.2.3 From e4d09aebf470f9c014f2b658ccf5042a3995d708 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 23 May 2017 14:49:10 -0700 Subject: Cleanup --- app/scripts/controllers/transactions.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 3d86a171e..4d3197cba 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -12,7 +12,7 @@ const denodeify = require('denodeify') const RETRY_LIMIT = 200 const RESUBMIT_INTERVAL = 10000 // Ten seconds -module.exports = class TransactionManager extends EventEmitter { +module.exports = class TransactionController extends EventEmitter { constructor (opts) { super() this.store = new ObservableStore(extend({ @@ -448,7 +448,8 @@ module.exports = class TransactionManager extends EventEmitter { const rawTx = txMeta.rawTx this.txProviderUtils.publishTransaction(rawTx, cb) } + } -const warn = () => console.warn('warn was used no cb provided') +const warn = () => log.warn('warn was used no cb provided') -- cgit v1.2.3 From 17604f1ef5c880a391026aa02d19493d05c1dd18 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 23 May 2017 14:49:45 -0700 Subject: Version 3.7.0 --- CHANGELOG.md | 2 ++ app/manifest.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88d7a81e7..c0473baee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +## 3.7.0 2017-5-23 + - Add Transaction Number (nonce) to transaction list. - Label the pending tx icon with a tooltip. - Fix bug where website filters would pile up and not deallocate when leaving a site. diff --git a/app/manifest.json b/app/manifest.json index 31e4598c7..5fd4420be 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.6.5", + "version": "3.7.0", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From bffd174ec71ab21326884bd9e356477657bd0867 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 23 May 2017 15:02:03 -0700 Subject: Update announcer wording --- development/announcer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/development/announcer.js b/development/announcer.js index 110d41fd4..43ae60acb 100644 --- a/development/announcer.js +++ b/development/announcer.js @@ -7,6 +7,6 @@ var changelog = fs.readFileSync(path.join(__dirname, '..', 'CHANGELOG.md')).toSt var log = changelog.split(version)[1].split('##')[0].trim() -let msg = `*MetaMask ${version}* now published to the Chrome Store! It should auto-update over the next hour!\n${log}` +let msg = `*MetaMask ${version}* now published to the Chrome Store! It should auto-update soon!\n${log}` console.log(msg) -- cgit v1.2.3 From 243eeff7cb0d4c5d613a9250d234f81fdccbbf15 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 23 May 2017 02:12:28 -0400 Subject: Fix for tests --- app/scripts/controllers/network.js | 84 ++++++++++++++------------------- app/scripts/controllers/transactions.js | 4 +- app/scripts/first-time-state.js | 3 +- app/scripts/lib/config-manager.js | 29 ++++++++++++ app/scripts/metamask-controller.js | 3 +- test/unit/network-contoller-test.js | 74 +++++++++++++++++++++++++++++ test/unit/tx-controller-test.js | 4 +- test/unit/tx-utils-test.js | 6 ++- 8 files changed, 150 insertions(+), 57 deletions(-) create mode 100644 test/unit/network-contoller-test.js diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js index 97c2ccbc2..4fdd92921 100644 --- a/app/scripts/controllers/network.js +++ b/app/scripts/controllers/network.js @@ -1,23 +1,23 @@ const EventEmitter = require('events') const MetaMaskProvider = require('web3-provider-engine/zero.js') const ObservableStore = require('obs-store') +const ComposedStore = require('obs-store/lib/composed') const extend = require('xtend') const EthQuery = require('eth-query') const RPC_ADDRESS_LIST = require('../config.js').network +const DEFAULT_RPC = RPC_ADDRESS_LIST['rinkeby'] module.exports = class NetworkController extends EventEmitter { - constructor (providerOpts) { + constructor (config) { super() - this.networkStore = new ObservableStore({ network: 'loading' }) - providerOpts.provider.rpcTarget = this.getRpcAddressForType(providerOpts.provider.type, providerOpts.provider) - this.providerStore = new ObservableStore(providerOpts) - this.store = new ObservableStore(extend(this.networkStore.getState(), this.providerStore.getState())) + this.networkStore = new ObservableStore('loading') + config.provider.rpcTarget = this.getRpcAddressForType(config.provider.type, config.provider) + this.providerStore = new ObservableStore(config.provider) + this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore }) + this._providerListeners = {} - this._providerListners = {} - - this.networkStore.subscribe((state) => this.store.updateState(state)) - this.providerStore.subscribe((state) => this.store.updateState(state)) - this.on('networkSwitch', this.lookupNetwork) + this.on('networkDidChange', this.lookupNetwork) + this.providerStore.subscribe((state) => this.switchNetwork({rpcUrl: state.rpcTarget})) } get provider () { @@ -28,15 +28,8 @@ module.exports = class NetworkController extends EventEmitter { this._provider = provider } - getState () { - return extend({}, - this.networkStore.getState(), - this.providerStore.getState() - ) - } - initializeProvider (opts) { - this.providerConfig = opts + this.providerInit = opts this._provider = MetaMaskProvider(opts) this._proxy = new Proxy(this._provider, { get: (obj, name) => { @@ -54,16 +47,18 @@ module.exports = class NetworkController extends EventEmitter { return this.provider } - switchNetwork (providerConfig) { - const newConfig = extend(this.providerConfig, providerConfig) - this.providerConfig = newConfig + switchNetwork (providerInit) { + this.setNetworkState('loading') + const newInit = extend(this.providerInit, providerInit) + this.providerInit = newInit - this.provider = MetaMaskProvider(newConfig) + this._provider.removeAllListeners() + this.provider = MetaMaskProvider(newInit) // apply the listners created by other controllers - Object.keys(this._providerListners).forEach((key) => { - this._providerListners[key].forEach((handler) => this._provider.addListener(key, handler)) + Object.keys(this._providerListeners).forEach((key) => { + this._providerListeners[key].forEach((handler) => this._provider.addListener(key, handler)) }) - this.emit('networkSwitch', this.provider) + this.emit('networkDidChange') } @@ -73,20 +68,18 @@ module.exports = class NetworkController extends EventEmitter { } getNetworkState () { - return this.networkStore.getState().network + return this.networkStore.getState() } setNetworkState (network) { - return this.networkStore.updateState({ network }) + return this.networkStore.putState(network) } isNetworkLoading () { return this.getNetworkState() === 'loading' } - lookupNetwork (err) { - if (err) this.setNetworkState('loading') - + lookupNetwork () { this.ethQuery.sendAsync({ method: 'net_version' }, (err, network) => { if (err) return this.setNetworkState('loading') log.info('web3.getNetwork returned ' + network) @@ -96,37 +89,30 @@ module.exports = class NetworkController extends EventEmitter { setRpcTarget (rpcUrl) { this.providerStore.updateState({ - provider: { - type: 'rpc', - rpcTarget: rpcUrl, - }, + type: 'rpc', + rpcTarget: rpcUrl, }) } getCurrentRpcAddress () { - var provider = this.getProvider() + const provider = this.getProviderConfig() if (!provider) return null return this.getRpcAddressForType(provider.type) } setProviderType (type) { - if (type === this.getProvider().type) return + if (type === this.getProviderConfig().type) return const rpcTarget = this.getRpcAddressForType(type) - this.networkStore.updateState({network: 'loading'}) - this.switchNetwork({ - rpcUrl: rpcTarget, - }) - this.providerStore.updateState({provider: {type, rpcTarget}}) + this.providerStore.updateState({type, rpcTarget}) } - getProvider () { - return this.providerStore.getState().provider + getProviderConfig () { + return this.providerStore.getState() } - getRpcAddressForType (type, provider = this.getProvider()) { - console.log(`#getRpcAddressForType: ${type}`) - if (type in RPC_ADDRESS_LIST) return RPC_ADDRESS_LIST[type] - return provider && provider.rpcTarget ? provider.rpcTarget : RPC_ADDRESS_LIST['rinkeby'] + getRpcAddressForType (type, provider = this.getProviderConfig()) { + if (RPC_ADDRESS_LIST[type]) return RPC_ADDRESS_LIST[type] + return provider && provider.rpcTarget ? provider.rpcTarget : DEFAULT_RPC } _logBlock (block) { @@ -135,8 +121,8 @@ module.exports = class NetworkController extends EventEmitter { } _on (event, handler) { - if (!this._providerListners[event]) this._providerListners[event] = [] - this._providerListners[event].push(handler) + if (!this._providerListeners[event]) this._providerListeners[event] = [] + this._providerListeners[event].push(handler) this._provider.on(event, handler) } } diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index b9bea2f1c..2ebeed3ab 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -21,9 +21,7 @@ module.exports = class TransactionManager extends EventEmitter { this.blockTracker = opts.blockTracker this.query = opts.ethQuery this.txProviderUtils = new TxProviderUtil(this.query) - this.networkStore.subscribe((_) => this.blockTracker.on('block', this.checkForTxInBlock.bind(this))) this.blockTracker.on('block', this.checkForTxInBlock.bind(this)) - this.signEthTx = opts.signTransaction this.nonceLock = Semaphore(1) @@ -39,7 +37,7 @@ module.exports = class TransactionManager extends EventEmitter { } getNetwork () { - return this.networkStore.getState().network + return this.networkStore.getState() } getSelectedAddress () { diff --git a/app/scripts/first-time-state.js b/app/scripts/first-time-state.js index 29ec1d8d3..dc7788311 100644 --- a/app/scripts/first-time-state.js +++ b/app/scripts/first-time-state.js @@ -3,7 +3,8 @@ // module.exports = { - config: { + config: {}, + NetworkController: { provider: { type: 'rinkeby', }, diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index fee8423fa..9c0dffe9c 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -107,6 +107,35 @@ ConfigManager.prototype.getSeedWords = function () { var data = this.getData() return data.seedWords } +ConfigManager.prototype.setRpcTarget = function (rpcUrl) { + var config = this.getConfig() + config.provider = { + type: 'rpc', + rpcTarget: rpcUrl, + } + this.setConfig(config) +} + +ConfigManager.prototype.setProviderType = function (type) { + var config = this.getConfig() + config.provider = { + type: type, + } + this.setConfig(config) +} + +ConfigManager.prototype.useEtherscanProvider = function () { + var config = this.getConfig() + config.provider = { + type: 'etherscan', + } + this.setConfig(config) +} + +ConfigManager.prototype.getProvider = function () { + var config = this.getConfig() + return config.provider +} ConfigManager.prototype.getCurrentRpcAddress = function () { var provider = this.getProvider() diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index ef82da0d3..c0ad1e93b 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -65,7 +65,6 @@ module.exports = class MetamaskController extends EventEmitter { // eth data query tools this.ethQuery = new EthQuery(this.provider) this.ethStore = new EthStore({ - network: this.networkController.networkStore, provider: this.provider, blockTracker: this.provider, }) @@ -139,7 +138,7 @@ module.exports = class MetamaskController extends EventEmitter { this.shapeshiftController.store.subscribe((state) => { this.store.updateState({ ShapeShiftController: state }) }) - this.networkController.providerStore.subscribe((state) => { + this.networkController.store.subscribe((state) => { this.store.updateState({ NetworkController: state }) }) diff --git a/test/unit/network-contoller-test.js b/test/unit/network-contoller-test.js new file mode 100644 index 000000000..183e69cab --- /dev/null +++ b/test/unit/network-contoller-test.js @@ -0,0 +1,74 @@ +const EventEmitter = require('events') +const assert = require('assert') +const NetworkController = require('../../app/scripts/controllers/network') + +describe('# Network Controller', function () { + let networkController + + beforeEach(function () { + networkController = new NetworkController({ + provider: { + type: 'rinkeby', + }, + }) + // stub out provider + networkController._provider = new EventEmitter() + networkController.providerInit = { + getAccounts: () => {}, + } + + networkController.ethQuery = new Proxy({}, { + get: (obj, name) => { + return () => {} + }, + }) + }) + describe('network', function () { + describe('#provider', function() { + it('provider should be updatable without reassignment', function () { + networkController.initializeProvider(networkController.providerInit) + const provider = networkController.provider + networkController._provider = {test: true} + assert.ok(provider.test) + }) + }) + describe('#getNetworkState', function () { + it('should return loading when new', function () { + let networkState = networkController.getNetworkState() + assert.equal(networkState, 'loading', 'network is loading') + }) + }) + + describe('#setNetworkState', function () { + it('should update the network', function () { + networkController.setNetworkState(1) + let networkState = networkController.getNetworkState() + assert.equal(networkState, 1, 'network is 1') + }) + }) + + describe('#getRpcAddressForType', function () { + it('should return the right rpc address', function () { + let rpcTarget = networkController.getRpcAddressForType('mainnet') + assert.equal(rpcTarget, 'https://mainnet.infura.io/metamask', 'returns the right rpcAddress') + }) + }) + describe('#setProviderType', function () { + it('should update provider.type', function () { + networkController.setProviderType('mainnet') + const type = networkController.getProviderConfig().type + assert.equal(type, 'mainnet', 'provider type is updated') + }) + it('should set the network to loading', function () { + networkController.setProviderType('mainnet') + const loading = networkController.isNetworkLoading() + assert.ok(loading, 'network is loading') + }) + it('should set the right rpcTarget', function () { + networkController.setProviderType('mainnet') + const rpcTarget = networkController.getProviderConfig().rpcTarget + assert.equal(rpcTarget, 'https://mainnet.infura.io/metamask', 'returns the right rpcAddress') + }) + }) + }) +}) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index d4e8d79f0..711e1ea79 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -2,6 +2,7 @@ const assert = require('assert') const EventEmitter = require('events') const ethUtil = require('ethereumjs-util') const EthTx = require('ethereumjs-tx') +const EthQuery = require('eth-query') const ObservableStore = require('obs-store') const clone = require('clone') const sinon = require('sinon') @@ -16,9 +17,10 @@ describe('Transaction Controller', function () { beforeEach(function () { txController = new TransactionController({ - networkStore: new ObservableStore({ network: currentNetworkId }), + networkStore: new ObservableStore(currentNetworkId), txHistoryLimit: 10, blockTracker: new EventEmitter(), + ethQuery: new EthQuery(new EventEmitter()), signTransaction: (ethTx) => new Promise((resolve) => { ethTx.sign(privKey) resolve() diff --git a/test/unit/tx-utils-test.js b/test/unit/tx-utils-test.js index 57d4638a0..7ace1f587 100644 --- a/test/unit/tx-utils-test.js +++ b/test/unit/tx-utils-test.js @@ -9,7 +9,11 @@ describe('txUtils', function () { let txUtils before(function () { - txUtils = new TxUtils() + txUtils = new TxUtils(new Proxy({}, { + get: (obj, name) => { + return () => {} + }, + })) }) describe('chain Id', function () { -- cgit v1.2.3 From c5d74e6421486140eba801375eaeeb3c9c0a3091 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 23 May 2017 20:06:19 -0400 Subject: include ethQuery in txController --- app/scripts/metamask-controller.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index c0ad1e93b..a7eb3d056 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -97,6 +97,7 @@ module.exports = class MetamaskController extends EventEmitter { signTransaction: this.keyringController.signTransaction.bind(this.keyringController), provider: this.provider, blockTracker: this.provider, + ethQuery: this.ethQuery, }) // notices -- cgit v1.2.3 From c1239608a62a4283d138b1a6586653ac791eb655 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 23 May 2017 20:27:51 -0400 Subject: add to CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 371b348ca..85dc9ce81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Now when switching networks the extension does not restart + ## 3.6.5 2017-5-17 - Fix bug where edited gas parameters would not take effect. -- cgit v1.2.3 From e55329d28b61b2d1066cf794a2099a18a94be7a4 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 24 May 2017 00:15:59 -0700 Subject: Version 3.7.1 --- app/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/manifest.json b/app/manifest.json index 31e4598c7..c3e2b7511 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.6.5", + "version": "3.7.1", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From db982cf795d30dd32b4722249daea9296430b5b9 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 24 May 2017 11:52:18 -0400 Subject: stop polling when switching networks --- app/scripts/controllers/network.js | 1 + test/unit/network-contoller-test.js | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js index 4fdd92921..c07f13b8d 100644 --- a/app/scripts/controllers/network.js +++ b/app/scripts/controllers/network.js @@ -53,6 +53,7 @@ module.exports = class NetworkController extends EventEmitter { this.providerInit = newInit this._provider.removeAllListeners() + this._provider.stop() this.provider = MetaMaskProvider(newInit) // apply the listners created by other controllers Object.keys(this._providerListeners).forEach((key) => { diff --git a/test/unit/network-contoller-test.js b/test/unit/network-contoller-test.js index 183e69cab..46f473af2 100644 --- a/test/unit/network-contoller-test.js +++ b/test/unit/network-contoller-test.js @@ -12,7 +12,11 @@ describe('# Network Controller', function () { }, }) // stub out provider - networkController._provider = new EventEmitter() + networkController._provider = new Proxy(new EventEmitter(), { + get: (obj, name) => { + return () => {} + }, + }) networkController.providerInit = { getAccounts: () => {}, } -- cgit v1.2.3 From e23c0f98faef3282d684bba48a53a1f48425b52d Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 24 May 2017 11:57:31 -0400 Subject: clean up test --- test/unit/network-contoller-test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/unit/network-contoller-test.js b/test/unit/network-contoller-test.js index 46f473af2..76452b303 100644 --- a/test/unit/network-contoller-test.js +++ b/test/unit/network-contoller-test.js @@ -1,4 +1,3 @@ -const EventEmitter = require('events') const assert = require('assert') const NetworkController = require('../../app/scripts/controllers/network') @@ -12,7 +11,7 @@ describe('# Network Controller', function () { }, }) // stub out provider - networkController._provider = new Proxy(new EventEmitter(), { + networkController._provider = new Proxy({}, { get: (obj, name) => { return () => {} }, -- cgit v1.2.3 From 60281f72506c6b1775e75e8426a09d91893ab6ac Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 24 May 2017 09:55:16 -0700 Subject: Cleanup code. --- ui/app/components/bn-as-decimal-input.js | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/ui/app/components/bn-as-decimal-input.js b/ui/app/components/bn-as-decimal-input.js index 8f56f29ab..de01f8b5f 100644 --- a/ui/app/components/bn-as-decimal-input.js +++ b/ui/app/components/bn-as-decimal-input.js @@ -31,7 +31,7 @@ BnAsDecimalInput.prototype.render = function () { const suffix = props.suffix const style = props.style const valueString = value.toString(10) - const newValue = downsize(valueString, scale, precision) + const newValue = this.downsize(valueString, scale, precision) return ( h('.flex-column', [ @@ -65,7 +65,7 @@ BnAsDecimalInput.prototype.render = function () { const value = (event.target.value === '') ? '' : event.target.value - const scaledNumber = upsize(value, scale, precision) + const scaledNumber = this.upsize(value, scale, precision) const precisionBN = new BN(scaledNumber, 10) onChange(precisionBN) }, @@ -145,20 +145,28 @@ BnAsDecimalInput.prototype.constructWarning = function () { } -function downsize (number, scale, precision) { +BnAsDecimalInput.prototype.downsize = function (number, scale, precision) { + // if there is no scaling, simply return the number if (scale === 0) { return Number(number) } else { + // if the scale is the same as the precision, account for this edge case. var decimals = (scale === precision) ? -1 : scale - precision return Number(number.slice(0, -scale) + '.' + number.slice(-scale, decimals)) } } -function upsize (number, scale, precision) { - var string = number.toString() - var stringArray = string.split('.') +BnAsDecimalInput.prototype.upsize = function (number, scale, precision) { + var stringArray = number.toString().split('.') var decimalLength = stringArray[1] ? stringArray[1].length : 0 - var newString = ((scale === 0) || (decimalLength === 0)) ? stringArray[0] : stringArray[0] + stringArray[1].slice(0, precision) + var newString = stringArray[0] + + // If there is scaling and decimal parts exist, integrate them in. + if ((scale !== 0) && (decimalLength !== 0)) { + newString += stringArray[1].slice(0, precision) + } + + // Add 0s to account for the upscaling. for (var i = decimalLength; i < scale; i++) { newString += '0' } -- cgit v1.2.3 From 2d739647b909732c26a7725cb78cf018c71d6621 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 24 May 2017 10:01:45 -0700 Subject: Bump changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 215aee936..2ad61254c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Current Master - Now when switching networks the extension does not restart +- Cleanup decimal bugs in our gas inputs. ## 3.7.0 2017-5-23 -- cgit v1.2.3 From 10ca3b6467af2bea723e661160ba5cf2a41ab3b0 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 24 May 2017 10:13:43 -0700 Subject: Fix bug where submit was enabled when invalid params were filled out. --- CHANGELOG.md | 1 + ui/app/components/bn-as-decimal-input.js | 2 +- ui/app/components/pending-tx.js | 14 ++++++++++---- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ad61254c..aea0df1fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Now when switching networks the extension does not restart - Cleanup decimal bugs in our gas inputs. +- Fix bug where submit button was enabled for invalid gas inputs. ## 3.7.0 2017-5-23 diff --git a/ui/app/components/bn-as-decimal-input.js b/ui/app/components/bn-as-decimal-input.js index de01f8b5f..1d292ca2a 100644 --- a/ui/app/components/bn-as-decimal-input.js +++ b/ui/app/components/bn-as-decimal-input.js @@ -67,7 +67,7 @@ BnAsDecimalInput.prototype.render = function () { const scaledNumber = this.upsize(value, scale, precision) const precisionBN = new BN(scaledNumber, 10) - onChange(precisionBN) + onChange(precisionBN, event.target.checkValidity()) }, onInvalid: (event) => { const msg = this.constructWarning() diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index eed0fd9ae..8e63f5c76 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -346,18 +346,24 @@ PendingTx.prototype.miniAccountPanelForRecipient = function () { } } -PendingTx.prototype.gasPriceChanged = function (newBN) { +PendingTx.prototype.gasPriceChanged = function (newBN, valid) { log.info(`Gas price changed to: ${newBN.toString(10)}`) const txMeta = this.gatherTxMeta() txMeta.txParams.gasPrice = '0x' + newBN.toString('hex') - this.setState({ txData: clone(txMeta) }) + this.setState({ + txData: clone(txMeta), + valid, + }) } -PendingTx.prototype.gasLimitChanged = function (newBN) { +PendingTx.prototype.gasLimitChanged = function (newBN, valid) { log.info(`Gas limit changed to ${newBN.toString(10)}`) const txMeta = this.gatherTxMeta() txMeta.txParams.gas = '0x' + newBN.toString('hex') - this.setState({ txData: clone(txMeta) }) + this.setState({ + txData: clone(txMeta), + valid, + }) } PendingTx.prototype.resetGasFields = function () { -- cgit v1.2.3 From 5e19a4a8332703aa3467470073411ad9cccca52a Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 24 May 2017 10:51:44 -0700 Subject: Modfiy test to ether standards. --- test/unit/components/bn-as-decimal-input-test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/unit/components/bn-as-decimal-input-test.js b/test/unit/components/bn-as-decimal-input-test.js index 6fe684dc5..b3365b6f9 100644 --- a/test/unit/components/bn-as-decimal-input-test.js +++ b/test/unit/components/bn-as-decimal-input-test.js @@ -13,7 +13,7 @@ describe('BnInput', function () { const renderer = ReactTestUtils.createRenderer() let valueStr = '20' - while (valueStr.length < 15) { + while (valueStr.length < 20) { valueStr += '0' } const value = new BN(valueStr, 10) @@ -21,13 +21,13 @@ describe('BnInput', function () { let inputStr = '2.3' let targetStr = '23' - while (targetStr.length < 14) { + while (targetStr.length < 19) { targetStr += '0' } const target = new BN(targetStr, 10) - const precision = 13 // ether precision - const scale = 13 + const precision = 18 // ether precision + const scale = 18 const props = { value, -- cgit v1.2.3 From 293d0b4a574e5b20662da244d54357138ac81d5b Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 24 May 2017 11:02:26 -0700 Subject: Minor cleanup --- ui/app/components/bn-as-decimal-input.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/app/components/bn-as-decimal-input.js b/ui/app/components/bn-as-decimal-input.js index 1d292ca2a..f3ace4720 100644 --- a/ui/app/components/bn-as-decimal-input.js +++ b/ui/app/components/bn-as-decimal-input.js @@ -47,8 +47,8 @@ BnAsDecimalInput.prototype.render = function () { type: 'number', step: 'any', required: true, - min: min, - max: max, + min, + max, style: extend({ display: 'block', textAlign: 'right', -- cgit v1.2.3 From 9554788c14eab7be51abe0496bab17f9fe40291b Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 24 May 2017 11:02:58 -0700 Subject: Minor cleanup of lint --- ui/app/components/pending-tx.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 8e63f5c76..d66d98dd5 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -367,7 +367,6 @@ PendingTx.prototype.gasLimitChanged = function (newBN, valid) { } PendingTx.prototype.resetGasFields = function () { - log.debug(`pending-tx resetGasFields`) this.inputs.forEach((hexInput) => { -- cgit v1.2.3 From e6b278569ea072aa88416e769d4803a73b77194c Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 24 May 2017 11:34:26 -0700 Subject: inpage-provider - disable polling after first block --- app/scripts/lib/inpage-provider.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js index e54f547bd..39196e240 100644 --- a/app/scripts/lib/inpage-provider.js +++ b/app/scripts/lib/inpage-provider.js @@ -44,8 +44,9 @@ function MetamaskInpageProvider (connectionStream) { (err) => logStreamDisconnectWarning('MetaMask RpcProvider', err) ) - // start polling + // start and stop polling to unblock first block lock engine.start() + engine.once('latest', () => engine.stop()) self.idMap = {} // handle sendAsync requests via asyncProvider -- cgit v1.2.3 From 26fd016b63c4f3a15436d08dff9e94f72d2b4041 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 24 May 2017 16:17:03 -0700 Subject: Add new blockGasLimit property to txMeta object. --- app/scripts/lib/tx-utils.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js index 76b311653..8cf304d0b 100644 --- a/app/scripts/lib/tx-utils.js +++ b/app/scripts/lib/tx-utils.js @@ -21,19 +21,30 @@ module.exports = class txProviderUtils { this.query.getBlockByNumber('latest', true, (err, block) => { if (err) return cb(err) async.waterfall([ + self.setBlockGasLimit.bind(self, txMeta, block.gasLimit), self.estimateTxGas.bind(self, txMeta, block.gasLimit), self.setTxGas.bind(self, txMeta, block.gasLimit), ], cb) }) } + setBlockGasLimit (txMeta, blockGasLimitHex, cb) { + const blockGasLimitBN = hexToBn(blockGasLimitHex) + const saferGasLimitBN = blockGasLimitBN.muln(0.95) + txMeta.blockGasLimit = bnToHex(saferGasLimitBN) + cb() + return + } + estimateTxGas (txMeta, blockGasLimitHex, cb) { const txParams = txMeta.txParams // check if gasLimit is already specified txMeta.gasLimitSpecified = Boolean(txParams.gas) // if not, fallback to block gasLimit if (!txMeta.gasLimitSpecified) { - txParams.gas = blockGasLimitHex + const blockGasLimitBN = hexToBn(blockGasLimitHex) + const saferGasLimitBN = blockGasLimitBN.muln(0.95) + txParams.gas = bnToHex(saferGasLimitBN) } // run tx, see if it will OOG this.query.estimateGas(txParams, cb) -- cgit v1.2.3 From 51b5e2f6e74a91198816afee8ddaf468ab7ab583 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 24 May 2017 16:18:37 -0700 Subject: Add max gas limit to UI --- ui/app/components/pending-tx.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index d66d98dd5..b46f715bc 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -47,6 +47,7 @@ PendingTx.prototype.render = function () { // Gas const gas = txParams.gas const gasBn = hexToBn(gas) + const safeGasLimit = parseInt(txMeta.blockGasLimit) // Gas Price const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) @@ -159,6 +160,7 @@ PendingTx.prototype.render = function () { scale: 0, // The hard lower limit for gas. min: MIN_GAS_LIMIT_BN.toString(10), + max: safeGasLimit, suffix: 'UNITS', style: { position: 'relative', -- cgit v1.2.3 From b95f2158bb3094f6a47236c2e0e4e0ae9b87bb91 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 24 May 2017 16:23:31 -0700 Subject: bump changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aea0df1fa..caa87b697 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Now when switching networks the extension does not restart - Cleanup decimal bugs in our gas inputs. - Fix bug where submit button was enabled for invalid gas inputs. +- Now enforce 95% of block's gasLimit to protect users. ## 3.7.0 2017-5-23 -- cgit v1.2.3 From 473b88f399478b47bfa53d44ef9981aeb6d9960b Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 24 May 2017 22:13:35 -0400 Subject: Reload the page when switching networks for sites that use web3 --- app/scripts/contentscript.js | 1 - app/scripts/inpage.js | 21 +++---------------- app/scripts/lib/auto-reload.js | 47 ++++++++++++++++++++++-------------------- 3 files changed, 28 insertions(+), 41 deletions(-) diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index f7237b32e..291b922e8 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -61,7 +61,6 @@ function setupStreams () { // ignore unused channels (handled by background) mx.ignoreStream('provider') mx.ignoreStream('publicConfig') - mx.ignoreStream('reload') } function shouldInjectWeb3 () { diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 419f78cd6..ec764535e 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -31,26 +31,11 @@ web3.setProvider = function () { console.log('MetaMask - overrode web3.setProvider') } console.log('MetaMask - injected web3') -// export global web3, with usage-detection reload fn -var triggerReload = setupDappAutoReload(web3) - -// listen for reset requests from metamask -var reloadStream = inpageProvider.multiStream.createStream('reload') -reloadStream.once('data', triggerReload) - -// setup ping timeout autoreload -// LocalMessageDuplexStream does not self-close, so reload if pingStream fails -// var pingChannel = inpageProvider.multiStream.createStream('pingpong') -// var pingStream = new PingStream({ objectMode: true }) -// wait for first successful reponse - -// disable pingStream until https://github.com/MetaMask/metamask-plugin/issues/746 is resolved more gracefully -// metamaskStream.once('data', function(){ -// pingStream.pipe(pingChannel).pipe(pingStream) -// }) -// endOfStream(pingStream, triggerReload) +// export global web3, with usage-detection +setupDappAutoReload(web3, inpageProvider.publicConfigStore) // set web3 defaultAccount + inpageProvider.publicConfigStore.subscribe(function (state) { web3.eth.defaultAccount = state.selectedAddress }) diff --git a/app/scripts/lib/auto-reload.js b/app/scripts/lib/auto-reload.js index 1302df35f..30ddd2395 100644 --- a/app/scripts/lib/auto-reload.js +++ b/app/scripts/lib/auto-reload.js @@ -1,30 +1,33 @@ -const once = require('once') -const ensnare = require('ensnare') - module.exports = setupDappAutoReload -function setupDappAutoReload (web3) { +function setupDappAutoReload (web3, observable) { // export web3 as a global, checking for usage - var pageIsUsingWeb3 = false - var resetWasRequested = false - global.web3 = ensnare(web3, once(function () { - // if web3 usage happened after a reset request, trigger reset late - if (resetWasRequested) return triggerReset() - // mark web3 as used - pageIsUsingWeb3 = true - // reset web3 reference - global.web3 = web3 - })) + global.web3 = new Proxy(web3, { + get: (_web3, name) => { + // get the time of use + if (name !== '_used') _web3._used = Date.now() + return _web3[name] + }, + set: (_web3, name, value) => { + _web3[name] = value + }, + }) + var networkVersion - return handleResetRequest + observable.subscribe(function (state) { + // get the initial network + const curentNetVersion = state.networkVersion + if (!networkVersion) networkVersion = curentNetVersion - function handleResetRequest () { - resetWasRequested = true - // ignore if web3 was not used - if (!pageIsUsingWeb3) return - // reload after short timeout - setTimeout(triggerReset, 500) - } + if (curentNetVersion !== networkVersion && web3._used) { + const timeSenseUse = Date.now() - web3._used + // if web3 was recently used then delay the reloading of the page + timeSenseUse > 500 ? triggerReset() : setTimeout(triggerReset, 500) + // prevent reentry into if statement if state updates again before + // reload + networkVersion = curentNetVersion + } + }) } // reload the page -- cgit v1.2.3 From a2064bd16e3f37c537ef6c818a5a10024ef24a2a Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 24 May 2017 23:18:09 -0400 Subject: add to CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index caa87b697..b755e3999 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Current Master +- Now when switching networks sites that use web3 will reload - Now when switching networks the extension does not restart - Cleanup decimal bugs in our gas inputs. - Fix bug where submit button was enabled for invalid gas inputs. -- cgit v1.2.3 From 717dceede84980050420fc3e3ff015caf2bcd553 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 24 May 2017 23:36:10 -0400 Subject: fix spelling --- app/scripts/lib/auto-reload.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/lib/auto-reload.js b/app/scripts/lib/auto-reload.js index 30ddd2395..534047330 100644 --- a/app/scripts/lib/auto-reload.js +++ b/app/scripts/lib/auto-reload.js @@ -20,9 +20,9 @@ function setupDappAutoReload (web3, observable) { if (!networkVersion) networkVersion = curentNetVersion if (curentNetVersion !== networkVersion && web3._used) { - const timeSenseUse = Date.now() - web3._used + const timeSinceUse = Date.now() - web3._used // if web3 was recently used then delay the reloading of the page - timeSenseUse > 500 ? triggerReset() : setTimeout(triggerReset, 500) + timeSinceUse > 500 ? triggerReset() : setTimeout(triggerReset, 500) // prevent reentry into if statement if state updates again before // reload networkVersion = curentNetVersion -- cgit v1.2.3 From ad40e4d2608e0b1e329a6f9af851fbe2cc54e747 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 25 May 2017 12:37:04 -0700 Subject: Remove stream subprovider Since the polling leak seems to be coming from elsewhere, and new bugs came from this, I'm rolling back this change so that we can push the other improvements sooner and fix the bug at its true root. --- app/scripts/lib/inpage-provider.js | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js index 39196e240..8b8623974 100644 --- a/app/scripts/lib/inpage-provider.js +++ b/app/scripts/lib/inpage-provider.js @@ -1,7 +1,5 @@ const pipe = require('pump') -const ProviderEngine = require('web3-provider-engine') -const FilterSubprovider = require('web3-provider-engine/subproviders/filters') -const StreamSubprovider = require('web3-provider-engine/subproviders/stream') +const StreamProvider = require('web3-stream-provider') const LocalStorageStore = require('obs-store') const ObjectMultiplex = require('./obj-multiplex') const createRandomId = require('./random-id') @@ -29,24 +27,14 @@ function MetamaskInpageProvider (connectionStream) { ) // connect to async provider - const engine = new ProviderEngine() - - const filterSubprovider = new FilterSubprovider() - engine.addProvider(filterSubprovider) - - const streamSubprovider = new StreamSubprovider() - engine.addProvider(streamSubprovider) - + const asyncProvider = self.asyncProvider = new StreamProvider() pipe( - streamSubprovider, + asyncProvider, multiStream.createStream('provider'), - streamSubprovider, + asyncProvider, (err) => logStreamDisconnectWarning('MetaMask RpcProvider', err) ) - // start and stop polling to unblock first block lock - engine.start() - engine.once('latest', () => engine.stop()) self.idMap = {} // handle sendAsync requests via asyncProvider @@ -59,7 +47,7 @@ function MetamaskInpageProvider (connectionStream) { return message }) // forward to asyncProvider - engine.sendAsync(request, function (err, res) { + asyncProvider.sendAsync(request, function (err, res) { if (err) return cb(err) // transform messages to original ids eachJsonMessage(res, (message) => { -- cgit v1.2.3 From 606416121508342bd6eb0c2f40f6c482bc7d3fa0 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 25 May 2017 13:43:53 -0700 Subject: Bump changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index caa87b697..9b0a47157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Cleanup decimal bugs in our gas inputs. - Fix bug where submit button was enabled for invalid gas inputs. - Now enforce 95% of block's gasLimit to protect users. +- Removing provider-engine from the inpage provider. This fixes some error handling inconsistencies introduced in 3.7.0. ## 3.7.0 2017-5-23 -- cgit v1.2.3 From d8c94fca75ca2aed11387c0b1d4c6064bead447e Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 26 May 2017 00:19:24 -0700 Subject: Add address image map to icon factory Deriving from the new address image map repository I've added here: https://github.com/MetaMask/ethereum-contract-icons With this PR, images for addresses added to that repository will be shown instead of jazzicons in MetaMask. --- app/scripts/inpage.js | 4 ++-- app/scripts/lib/inpage-provider.js | 2 ++ gulpfile.js | 10 ++++++++++ package.json | 1 + ui/lib/icon-factory.js | 39 ++++++++++++++++++++------------------ 5 files changed, 36 insertions(+), 20 deletions(-) diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index ec764535e..10e5ea39b 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -1,6 +1,6 @@ /*global Web3*/ cleanContextForImports() -require('web3/dist/web3.min.js') +require('web3') const LocalMessageDuplexStream = require('post-message-stream') // const PingStream = require('ping-pong-stream/ping') // const endOfStream = require('end-of-stream') @@ -30,7 +30,7 @@ var web3 = new Web3(inpageProvider) web3.setProvider = function () { console.log('MetaMask - overrode web3.setProvider') } -console.log('MetaMask - injected web3') +console.log('MetaMask - injected modified web3') // export global web3, with usage-detection setupDappAutoReload(web3, inpageProvider.publicConfigStore) diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js index 8b8623974..5206adc82 100644 --- a/app/scripts/lib/inpage-provider.js +++ b/app/scripts/lib/inpage-provider.js @@ -39,6 +39,8 @@ function MetamaskInpageProvider (connectionStream) { self.idMap = {} // handle sendAsync requests via asyncProvider self.sendAsync = function (payload, cb) { + console.trace('sending async ' + payload.method) + console.dir(payload) // rewrite request ids var request = eachJsonMessage(payload, (message) => { var newId = createRandomId() diff --git a/gulpfile.js b/gulpfile.js index 9f4a606be..5bba1b9b3 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -52,6 +52,15 @@ gulp.task('copy:images', copyTask({ './dist/opera/images', ], })) +gulp.task('copy:contractImages', copyTask({ + source: './node_modules/ethereum-contract-icons/images/', + destinations: [ + './dist/firefox/images/contract', + './dist/chrome/images/contract', + './dist/edge/images/contract', + './dist/opera/images/contract', + ], +})) gulp.task('copy:fonts', copyTask({ source: './app/fonts/', destinations: [ @@ -127,6 +136,7 @@ const staticFiles = [ ] var copyStrings = staticFiles.map(staticFile => `copy:${staticFile}`) +copyStrings.push('copy:contractImages') if (!disableLiveReload) { copyStrings.push('copy:reload') diff --git a/package.json b/package.json index 6b6996d9d..9f47d76cb 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "eth-query": "^2.1.1", "eth-sig-util": "^1.1.1", "eth-simple-keyring": "^1.1.1", + "ethereum-contract-icons": "^1.0.0", "ethereumjs-tx": "^1.3.0", "ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", diff --git a/ui/lib/icon-factory.js b/ui/lib/icon-factory.js index 82cc839d6..4aed9109b 100644 --- a/ui/lib/icon-factory.js +++ b/ui/lib/icon-factory.js @@ -1,4 +1,7 @@ var iconFactory +const isValidAddress = require('ethereumjs-util').isValidAddress +const toChecksumAddress = require('ethereumjs-util').toChecksumAddress +const iconMap = require('ethereum-contract-icons') module.exports = function (jazzicon) { if (!iconFactory) { @@ -12,22 +15,12 @@ function IconFactory (jazzicon) { this.cache = {} } -IconFactory.prototype.iconForAddress = function (address, diameter, imageify) { - if (imageify) { - return this.generateIdenticonImg(address, diameter) - } else { - return this.generateIdenticonSvg(address, diameter) +IconFactory.prototype.iconForAddress = function (address, diameter) { + const addr = toChecksumAddress(address) + if (iconExistsFor(addr)) { + return imageElFor(addr) } -} - -// returns img dom element -IconFactory.prototype.generateIdenticonImg = function (address, diameter) { - var identicon = this.generateIdenticonSvg(address, diameter) - var identiconSrc = identicon.innerHTML - var dataUri = toDataUri(identiconSrc) - var img = document.createElement('img') - img.src = dataUri - return img + return this.generateIdenticonSvg(address, diameter) } // returns svg dom element @@ -49,12 +42,22 @@ IconFactory.prototype.generateNewIdenticon = function (address, diameter) { // util +function iconExistsFor (address) { + return (address in iconMap) && isValidAddress(address) +} + +function imageElFor (address) { + const fileName = iconMap[address] + const path = `images/contract/${fileName}` + const img = document.createElement('img') + img.src = path + img.style.width = '100%' + return img +} + function jsNumberForAddress (address) { var addr = address.slice(2, 10) var seed = parseInt(addr, 16) return seed } -function toDataUri (identiconSrc) { - return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(identiconSrc) -} -- cgit v1.2.3 From 7cd42ae9ba07b04bae8e86be1536610ea4931f51 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 26 May 2017 00:45:58 -0700 Subject: Bump changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ad1b2f50..ca9cc6f5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Fix bug where submit button was enabled for invalid gas inputs. - Now enforce 95% of block's gasLimit to protect users. - Removing provider-engine from the inpage provider. This fixes some error handling inconsistencies introduced in 3.7.0. +- Some contracts will now display logos instead of jazzicons. ## 3.7.0 2017-5-23 -- cgit v1.2.3 From 1dfc0f74bf578524107b796672f96eb71a2fed62 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 26 May 2017 00:50:27 -0700 Subject: Correct inpage to be not modified --- app/scripts/inpage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 10e5ea39b..ec764535e 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -1,6 +1,6 @@ /*global Web3*/ cleanContextForImports() -require('web3') +require('web3/dist/web3.min.js') const LocalMessageDuplexStream = require('post-message-stream') // const PingStream = require('ping-pong-stream/ping') // const endOfStream = require('end-of-stream') @@ -30,7 +30,7 @@ var web3 = new Web3(inpageProvider) web3.setProvider = function () { console.log('MetaMask - overrode web3.setProvider') } -console.log('MetaMask - injected modified web3') +console.log('MetaMask - injected web3') // export global web3, with usage-detection setupDappAutoReload(web3, inpageProvider.publicConfigStore) -- cgit v1.2.3 From 7268fcb694b1cf43e1983b2f693ed731d3c2ec9b Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 26 May 2017 00:50:59 -0700 Subject: Revert inpage-provider --- app/scripts/lib/inpage-provider.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js index 5206adc82..8b8623974 100644 --- a/app/scripts/lib/inpage-provider.js +++ b/app/scripts/lib/inpage-provider.js @@ -39,8 +39,6 @@ function MetamaskInpageProvider (connectionStream) { self.idMap = {} // handle sendAsync requests via asyncProvider self.sendAsync = function (payload, cb) { - console.trace('sending async ' + payload.method) - console.dir(payload) // rewrite request ids var request = eachJsonMessage(payload, (message) => { var newId = createRandomId() -- cgit v1.2.3 From f06ad954b900aa94a36fbb3e4765d0a9222e0920 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 26 May 2017 09:58:33 -0700 Subject: Move to eth-contract-metadata --- package.json | 2 +- ui/lib/icon-factory.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 9f47d76cb..9efba3866 100644 --- a/package.json +++ b/package.json @@ -62,11 +62,11 @@ "end-of-stream": "^1.1.0", "ensnare": "^1.0.0", "eth-bin-to-ops": "^1.0.1", + "eth-contract-metadata": "^1.0.0", "eth-hd-keyring": "^1.1.1", "eth-query": "^2.1.1", "eth-sig-util": "^1.1.1", "eth-simple-keyring": "^1.1.1", - "ethereum-contract-icons": "^1.0.0", "ethereumjs-tx": "^1.3.0", "ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", diff --git a/ui/lib/icon-factory.js b/ui/lib/icon-factory.js index 4aed9109b..c16507527 100644 --- a/ui/lib/icon-factory.js +++ b/ui/lib/icon-factory.js @@ -1,7 +1,7 @@ var iconFactory const isValidAddress = require('ethereumjs-util').isValidAddress const toChecksumAddress = require('ethereumjs-util').toChecksumAddress -const iconMap = require('ethereum-contract-icons') +const contractMap = require('eth-contract-metadata') module.exports = function (jazzicon) { if (!iconFactory) { @@ -43,11 +43,12 @@ IconFactory.prototype.generateNewIdenticon = function (address, diameter) { // util function iconExistsFor (address) { - return (address in iconMap) && isValidAddress(address) + return (address in contractMap) && isValidAddress(address) && ('logo' in contractMap[address]) } function imageElFor (address) { - const fileName = iconMap[address] + const contract = contractMap[address] + const fileName = contract.logo const path = `images/contract/${fileName}` const img = document.createElement('img') img.src = path -- cgit v1.2.3 From 1203bd15c2861332b62e4a39a3d961f61daed6dc Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 26 May 2017 10:25:00 -0700 Subject: Add names to contract map & conf view --- CHANGELOG.md | 1 + ui/lib/contract-namer.js | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca9cc6f5a..e050a7509 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Now enforce 95% of block's gasLimit to protect users. - Removing provider-engine from the inpage provider. This fixes some error handling inconsistencies introduced in 3.7.0. - Some contracts will now display logos instead of jazzicons. +- Some contracts will now have names displayed in the confirmation view. ## 3.7.0 2017-5-23 diff --git a/ui/lib/contract-namer.js b/ui/lib/contract-namer.js index a94c62b62..0800ee7df 100644 --- a/ui/lib/contract-namer.js +++ b/ui/lib/contract-namer.js @@ -6,13 +6,18 @@ */ // Nickname keys must be stored in lower case. -const nicknames = {} +const contractMap = require('eth-contract-metadata') +const ethUtil = require('ethereumjs-util') module.exports = function (addr, identities = {}) { + const checksummed = ethUtil.toChecksumAddress(addr) + if (checksummed in contractMap && 'name' in contractMap[checksummed]) { + return contractMap[checksummed].name + } + const address = addr.toLowerCase() const ids = hashFromIdentities(identities) - - return addrFromHash(address, ids) || addrFromHash(address, nicknames) + return addrFromHash(address, ids) } function hashFromIdentities (identities) { -- cgit v1.2.3 From 5e6b23082147f530adbf52cacf61202d7edf1111 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 26 May 2017 10:54:16 -0700 Subject: Move off in operator --- ui/lib/contract-namer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/lib/contract-namer.js b/ui/lib/contract-namer.js index 0800ee7df..2400fa576 100644 --- a/ui/lib/contract-namer.js +++ b/ui/lib/contract-namer.js @@ -11,7 +11,7 @@ const ethUtil = require('ethereumjs-util') module.exports = function (addr, identities = {}) { const checksummed = ethUtil.toChecksumAddress(addr) - if (checksummed in contractMap && 'name' in contractMap[checksummed]) { + if (contractMap.checksummed && contractMap[checksummed].name) { return contractMap[checksummed].name } -- cgit v1.2.3 From fd42d7bfd5cede30f14e232b6d93ba4205dbcf1d Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 26 May 2017 11:05:51 -0700 Subject: Fix contract map reference --- ui/lib/contract-namer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/lib/contract-namer.js b/ui/lib/contract-namer.js index 2400fa576..86bba1edc 100644 --- a/ui/lib/contract-namer.js +++ b/ui/lib/contract-namer.js @@ -11,7 +11,7 @@ const ethUtil = require('ethereumjs-util') module.exports = function (addr, identities = {}) { const checksummed = ethUtil.toChecksumAddress(addr) - if (contractMap.checksummed && contractMap[checksummed].name) { + if (contractMap[checksummed] && contractMap[checksummed].name) { return contractMap[checksummed].name } -- cgit v1.2.3 From fb2565c9d1f6c3cc6601c0f317fe10651ad21097 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 26 May 2017 11:13:39 -0700 Subject: Remove comment --- ui/lib/contract-namer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/lib/contract-namer.js b/ui/lib/contract-namer.js index 86bba1edc..f05e770cc 100644 --- a/ui/lib/contract-namer.js +++ b/ui/lib/contract-namer.js @@ -5,7 +5,6 @@ * otherwise returns null. */ -// Nickname keys must be stored in lower case. const contractMap = require('eth-contract-metadata') const ethUtil = require('ethereumjs-util') -- cgit v1.2.3 From 9d2844c7128c79314529e163b473353d42200e9c Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 26 May 2017 11:16:58 -0700 Subject: remove more in operators --- ui/lib/icon-factory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/lib/icon-factory.js b/ui/lib/icon-factory.js index c16507527..45be47b7a 100644 --- a/ui/lib/icon-factory.js +++ b/ui/lib/icon-factory.js @@ -43,7 +43,7 @@ IconFactory.prototype.generateNewIdenticon = function (address, diameter) { // util function iconExistsFor (address) { - return (address in contractMap) && isValidAddress(address) && ('logo' in contractMap[address]) + return (contractMap.address) && isValidAddress(address) && (contractMap[address].logo) } function imageElFor (address) { -- cgit v1.2.3 From 76a78fdb3b4d5341682a37ef523651ca163bfe15 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 31 May 2017 14:06:13 -0700 Subject: Version 3.7.2 --- CHANGELOG.md | 4 ++++ app/manifest.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e050a7509..ea7723ac2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,16 @@ ## Current Master +## 3.7.2 2017-5-31 + - Now when switching networks sites that use web3 will reload - Now when switching networks the extension does not restart - Cleanup decimal bugs in our gas inputs. - Fix bug where submit button was enabled for invalid gas inputs. - Now enforce 95% of block's gasLimit to protect users. - Removing provider-engine from the inpage provider. This fixes some error handling inconsistencies introduced in 3.7.0. +- Added "inflight cache", which prevents identical requests from clogging up the network, dramatically improving ENS performance. +- Fixed bug where filter subscriptions would sometimes fail to unsubscribe. - Some contracts will now display logos instead of jazzicons. - Some contracts will now have names displayed in the confirmation view. diff --git a/app/manifest.json b/app/manifest.json index c3e2b7511..2b59002b0 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.7.1", + "version": "3.7.2", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From d59021f7548e5b6c4cedc39b753b41840340398a Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 1 Jun 2017 10:18:20 -0700 Subject: Version 3.7.3 --- CHANGELOG.md | 4 + app/manifest.json | 2 +- package-lock.json | 7348 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 7353 insertions(+), 1 deletion(-) create mode 100644 package-lock.json diff --git a/CHANGELOG.md b/CHANGELOG.md index ea7723ac2..b48889f5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Current Master +## 3.7.3 2017-6-1 + +- Rebuilt to fix cache clearing bug. + ## 3.7.2 2017-5-31 - Now when switching networks sites that use web3 will reload diff --git a/app/manifest.json b/app/manifest.json index 2b59002b0..4dcd6df31 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.7.2", + "version": "3.7.3", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..cd2a6ee8c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,7348 @@ +{ + "name": "metamask-crx", + "version": "0.0.0", + "lockfileVersion": 1, + "dependencies": { + "@gulp-sourcemaps/map-sources": { + "version": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", + "integrity": "sha1-iQrnxdjId/bThIYCFazp1+yUW9o=", + "dev": true + }, + "abab": { + "version": "https://registry.npmjs.org/abab/-/abab-1.0.3.tgz", + "integrity": "sha1-uB3l9ydOxOdW15fNg08wNkJyTl0=", + "dev": true + }, + "abbrev": { + "version": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz", + "integrity": "sha1-0FVMIlZjbi9W58LlrRg/hZQo2B8=", + "dev": true + }, + "abstract-leveldown": { + "version": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-2.4.1.tgz", + "integrity": "sha1-s7/tuITraToSd18MVenwpCDM7mQ=" + }, + "accepts": { + "version": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz", + "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=" + }, + "acorn": { + "version": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + }, + "acorn-globals": { + "version": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz", + "integrity": "sha1-VbtemGkVB7dFedBRNBMhfDgMVM8=", + "dev": true, + "dependencies": { + "acorn": { + "version": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", + "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=", + "dev": true + } + } + }, + "acorn-jsx": { + "version": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dependencies": { + "acorn": { + "version": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=" + } + } + }, + "aes-js": { + "version": "https://registry.npmjs.org/aes-js/-/aes-js-0.2.4.tgz", + "integrity": "sha1-lLiBq3FyhtAV+iGeCPtmcJ3aWj0=" + }, + "after": { + "version": "https://registry.npmjs.org/after/-/after-0.8.1.tgz", + "integrity": "sha1-q11PuIP1loFtNRX495HAr0ht1ic=", + "dev": true + }, + "ajv": { + "version": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=" + }, + "ajv-keywords": { + "version": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", + "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=" + }, + "amdefine": { + "version": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" + }, + "ansi-escapes": { + "version": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=" + }, + "ansi-regex": { + "version": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "ansicolors": { + "version": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=", + "dev": true + }, + "any-promise": { + "version": "https://registry.npmjs.org/any-promise/-/any-promise-0.1.0.tgz", + "integrity": "sha1-gwtoCqflbzNFHUsEnzvYBESY7ic=" + }, + "anymatch": { + "version": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.0.tgz", + "integrity": "sha1-o+Uvo5FoyCX/V7AkgSbOWo/5VQc=", + "dev": true + }, + "aproba": { + "version": "https://registry.npmjs.org/aproba/-/aproba-1.1.1.tgz", + "integrity": "sha1-ldNgDwdxCqDpKYxyatXs8urLq6s=" + }, + "archy": { + "version": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "are-we-there-yet": { + "version": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=" + }, + "argparse": { + "version": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=" + }, + "argsparser": { + "version": "https://registry.npmjs.org/argsparser/-/argsparser-0.0.6.tgz", + "integrity": "sha1-/0XrW5LABCJc8UalHTOdzLJlvmM=", + "dev": true + }, + "arr-diff": { + "version": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true + }, + "arr-filter": { + "version": "https://registry.npmjs.org/arr-filter/-/arr-filter-1.1.2.tgz", + "integrity": "sha1-Q/3d0JHo7xGqTEXZzcGOLf8XEe4=", + "dev": true + }, + "arr-flatten": { + "version": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.0.3.tgz", + "integrity": "sha1-onTthawIhJtr14R8RYB0XcUa37E=", + "dev": true + }, + "arr-map": { + "version": "https://registry.npmjs.org/arr-map/-/arr-map-2.0.2.tgz", + "integrity": "sha1-Onc0X/wc814qkYJWAfnljy4kysQ=", + "dev": true + }, + "array-differ": { + "version": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", + "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=" + }, + "array-each": { + "version": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", + "dev": true + }, + "array-equal": { + "version": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", + "dev": true + }, + "array-filter": { + "version": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", + "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", + "dev": true + }, + "array-flatten": { + "version": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "array-initial": { + "version": "https://registry.npmjs.org/array-initial/-/array-initial-1.0.0.tgz", + "integrity": "sha1-CbE8WNVqBQNC53erb/zllbEI2tk=", + "dev": true, + "dependencies": { + "array-slice": { + "version": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", + "dev": true + }, + "is-number": { + "version": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz", + "integrity": "sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY=", + "dev": true + } + } + }, + "array-last": { + "version": "https://registry.npmjs.org/array-last/-/array-last-1.1.1.tgz", + "integrity": "sha1-9GWPmI2SEya1itARPPdtM3x7IKo=", + "dev": true, + "dependencies": { + "is-number": { + "version": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz", + "integrity": "sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY=", + "dev": true + } + } + }, + "array-map": { + "version": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", + "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", + "dev": true + }, + "array-reduce": { + "version": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", + "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", + "dev": true + }, + "array-slice": { + "version": "https://registry.npmjs.org/array-slice/-/array-slice-1.0.0.tgz", + "integrity": "sha1-5zA08A3MH0CHYAj9IP6ud71LfC8=", + "dev": true + }, + "array-union": { + "version": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=" + }, + "array-uniq": { + "version": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" + }, + "array-unique": { + "version": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "arraybuffer.slice": { + "version": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz", + "integrity": "sha1-8zshWfBTKj8xB6JywMz70a0peco=", + "dev": true + }, + "arrify": { + "version": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" + }, + "asap": { + "version": "https://registry.npmjs.org/asap/-/asap-2.0.5.tgz", + "integrity": "sha1-UidltQw1EEkOUtfc/ghe+bqWlY8=" + }, + "asn1": { + "version": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "asn1.js": { + "version": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.9.1.tgz", + "integrity": "sha1-SLokC0WpKA6UdImQull9IWYX/UA=", + "dev": true + }, + "assert": { + "version": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "dev": true + }, + "assert-plus": { + "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" + }, + "assertion-error": { + "version": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", + "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", + "dev": true + }, + "astw": { + "version": "https://registry.npmjs.org/astw/-/astw-2.2.0.tgz", + "integrity": "sha1-e9QXhNMkk5h66yOba04cV6hzuRc=", + "dev": true + }, + "async": { + "version": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, + "async-done": { + "version": "https://registry.npmjs.org/async-done/-/async-done-1.2.2.tgz", + "integrity": "sha1-ukKA2lWhbhX0u4vzqESpGHh0DjE=", + "dev": true + }, + "async-each": { + "version": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", + "dev": true + }, + "async-eventemitter": { + "version": "https://registry.npmjs.org/async-eventemitter/-/async-eventemitter-0.2.3.tgz", + "integrity": "sha1-959IDf2mZFqXvWFCwBcVDWO05w4=", + "dependencies": { + "async": { + "version": "https://registry.npmjs.org/async/-/async-2.4.1.tgz", + "integrity": "sha1-YqVrJ5yYoR0JhwlqAcw+6463u9c=" + } + } + }, + "async-reduce": { + "version": "https://registry.npmjs.org/async-reduce/-/async-reduce-0.0.1.tgz", + "integrity": "sha1-sja183bW+uOBze2QBqp/LHOxfzE=" + }, + "async-settle": { + "version": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", + "integrity": "sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs=", + "dev": true + }, + "asynckit": { + "version": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "atob": { + "version": "https://registry.npmjs.org/atob/-/atob-1.1.3.tgz", + "integrity": "sha1-lfE2KbEsOlGl0hWr3OKqnzL4B3M=", + "dev": true + }, + "aws-sign2": { + "version": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" + }, + "aws4": { + "version": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + }, + "babel-code-frame": { + "version": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.22.0.tgz", + "integrity": "sha1-AnYgvuVnqIwyVhV05/0IAdMxGOQ=" + }, + "babel-core": { + "version": "https://registry.npmjs.org/babel-core/-/babel-core-6.24.1.tgz", + "integrity": "sha1-jEKFZNzh4fQfszfsNPTDsCK1rYM=" + }, + "babel-eslint": { + "version": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-6.1.2.tgz", + "integrity": "sha1-UpNBn+NnLWZZjTJ9qWlFZ7pqXy8=", + "dev": true + }, + "babel-generator": { + "version": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.24.1.tgz", + "integrity": "sha1-5xX0hsWN7SVknYiJRNUqoHxdlJc=", + "dependencies": { + "jsesc": { + "version": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=" + } + } + }, + "babel-helper-bindify-decorators": { + "version": "https://registry.npmjs.org/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz", + "integrity": "sha1-FMGeXxQte0fxmlJDHlKxzLxAozA=", + "dev": true + }, + "babel-helper-builder-binary-assignment-operator-visitor": { + "version": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", + "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", + "dev": true + }, + "babel-helper-call-delegate": { + "version": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", + "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=" + }, + "babel-helper-define-map": { + "version": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.24.1.tgz", + "integrity": "sha1-epdH8ljYlH0y1RX2qhx70CIEoIA=" + }, + "babel-helper-explode-assignable-expression": { + "version": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", + "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", + "dev": true + }, + "babel-helper-explode-class": { + "version": "https://registry.npmjs.org/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz", + "integrity": "sha1-fcKjkQ3uAHBW4eMdZAztPVTqqes=", + "dev": true + }, + "babel-helper-function-name": { + "version": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=" + }, + "babel-helper-get-function-arity": { + "version": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=" + }, + "babel-helper-hoist-variables": { + "version": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", + "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=" + }, + "babel-helper-optimise-call-expression": { + "version": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", + "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=" + }, + "babel-helper-regex": { + "version": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.24.1.tgz", + "integrity": "sha1-024i+rEAjXnYhkjjIRaGgShFbOg=" + }, + "babel-helper-remap-async-to-generator": { + "version": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", + "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", + "dev": true + }, + "babel-helper-replace-supers": { + "version": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", + "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=" + }, + "babel-helpers": { + "version": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=" + }, + "babel-messages": { + "version": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=" + }, + "babel-plugin-check-es2015-constants": { + "version": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", + "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=" + }, + "babel-plugin-syntax-async-functions": { + "version": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", + "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", + "dev": true + }, + "babel-plugin-syntax-async-generators": { + "version": "https://registry.npmjs.org/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz", + "integrity": "sha1-a8lj67FuzLrmuStZbrfzXDQqi5o=", + "dev": true + }, + "babel-plugin-syntax-class-constructor-call": { + "version": "https://registry.npmjs.org/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz", + "integrity": "sha1-nLnTn+Q8hgC+yBRkVt3L1OGnZBY=", + "dev": true + }, + "babel-plugin-syntax-class-properties": { + "version": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", + "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=", + "dev": true + }, + "babel-plugin-syntax-decorators": { + "version": "https://registry.npmjs.org/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz", + "integrity": "sha1-MSVjtNvePMgGzuPkFszurd0RrAs=", + "dev": true + }, + "babel-plugin-syntax-do-expressions": { + "version": "https://registry.npmjs.org/babel-plugin-syntax-do-expressions/-/babel-plugin-syntax-do-expressions-6.13.0.tgz", + "integrity": "sha1-V0d1YTmqJtOQ0JQQsDdEugfkeW0=", + "dev": true + }, + "babel-plugin-syntax-dynamic-import": { + "version": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", + "integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo=", + "dev": true + }, + "babel-plugin-syntax-exponentiation-operator": { + "version": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", + "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=", + "dev": true + }, + "babel-plugin-syntax-export-extensions": { + "version": "https://registry.npmjs.org/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz", + "integrity": "sha1-cKFITw+QiaToStRLrDU8lbmxJyE=", + "dev": true + }, + "babel-plugin-syntax-function-bind": { + "version": "https://registry.npmjs.org/babel-plugin-syntax-function-bind/-/babel-plugin-syntax-function-bind-6.13.0.tgz", + "integrity": "sha1-SMSV8Xe98xqYHnMvVa3AvdJgH0Y=", + "dev": true + }, + "babel-plugin-syntax-object-rest-spread": { + "version": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", + "dev": true + }, + "babel-plugin-syntax-trailing-function-commas": { + "version": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", + "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=", + "dev": true + }, + "babel-plugin-transform-async-generator-functions": { + "version": "https://registry.npmjs.org/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz", + "integrity": "sha1-8FiQAUX9PpkHpt3yjaWfIVJYpds=", + "dev": true + }, + "babel-plugin-transform-async-to-generator": { + "version": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", + "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", + "dev": true + }, + "babel-plugin-transform-class-constructor-call": { + "version": "https://registry.npmjs.org/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz", + "integrity": "sha1-gNwoVQWsBn3LjWxl4vbxGrd2Xvk=", + "dev": true + }, + "babel-plugin-transform-class-properties": { + "version": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", + "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", + "dev": true + }, + "babel-plugin-transform-decorators": { + "version": "https://registry.npmjs.org/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz", + "integrity": "sha1-eIAT2PjGtSIr33s0Q5Df13Vp4k0=", + "dev": true + }, + "babel-plugin-transform-do-expressions": { + "version": "https://registry.npmjs.org/babel-plugin-transform-do-expressions/-/babel-plugin-transform-do-expressions-6.22.0.tgz", + "integrity": "sha1-KMyvkoEtlJws0SgfaQyP3EaK6bs=", + "dev": true + }, + "babel-plugin-transform-es2015-arrow-functions": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", + "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=" + }, + "babel-plugin-transform-es2015-block-scoped-functions": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", + "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=" + }, + "babel-plugin-transform-es2015-block-scoping": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.24.1.tgz", + "integrity": "sha1-dsKV3DpHQbFmWt/TFnIV3P8ypXY=" + }, + "babel-plugin-transform-es2015-classes": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", + "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=" + }, + "babel-plugin-transform-es2015-computed-properties": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", + "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=" + }, + "babel-plugin-transform-es2015-destructuring": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", + "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=" + }, + "babel-plugin-transform-es2015-duplicate-keys": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", + "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=" + }, + "babel-plugin-transform-es2015-for-of": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", + "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=" + }, + "babel-plugin-transform-es2015-function-name": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", + "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=" + }, + "babel-plugin-transform-es2015-literals": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", + "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=" + }, + "babel-plugin-transform-es2015-modules-amd": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", + "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=" + }, + "babel-plugin-transform-es2015-modules-commonjs": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.1.tgz", + "integrity": "sha1-0+MQtA72ZKNmIiAAl8bUQCmPK/4=" + }, + "babel-plugin-transform-es2015-modules-systemjs": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", + "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=" + }, + "babel-plugin-transform-es2015-modules-umd": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", + "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=" + }, + "babel-plugin-transform-es2015-object-super": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", + "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=" + }, + "babel-plugin-transform-es2015-parameters": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", + "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=" + }, + "babel-plugin-transform-es2015-shorthand-properties": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", + "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=" + }, + "babel-plugin-transform-es2015-spread": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", + "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=" + }, + "babel-plugin-transform-es2015-sticky-regex": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", + "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=" + }, + "babel-plugin-transform-es2015-template-literals": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", + "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=" + }, + "babel-plugin-transform-es2015-typeof-symbol": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", + "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=" + }, + "babel-plugin-transform-es2015-unicode-regex": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", + "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=" + }, + "babel-plugin-transform-exponentiation-operator": { + "version": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", + "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", + "dev": true + }, + "babel-plugin-transform-export-extensions": { + "version": "https://registry.npmjs.org/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz", + "integrity": "sha1-U3OLR+deghhYnuqUbLvTkQm75lM=", + "dev": true + }, + "babel-plugin-transform-function-bind": { + "version": "https://registry.npmjs.org/babel-plugin-transform-function-bind/-/babel-plugin-transform-function-bind-6.22.0.tgz", + "integrity": "sha1-xvuOlqwpajELjPjqQBRiQH3fapc=", + "dev": true + }, + "babel-plugin-transform-object-rest-spread": { + "version": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.23.0.tgz", + "integrity": "sha1-h11ryb52HFiirj/u5dxIldjH+SE=", + "dev": true + }, + "babel-plugin-transform-regenerator": { + "version": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.24.1.tgz", + "integrity": "sha1-uNowWtQ8PJm0hI5P5AN7dw0jxBg=" + }, + "babel-plugin-transform-runtime": { + "version": "https://registry.npmjs.org/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz", + "integrity": "sha1-iEkNRGUC6puOfvsP4J7E2ZR5se4=", + "dev": true + }, + "babel-plugin-transform-strict-mode": { + "version": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=" + }, + "babel-preset-es2015": { + "version": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz", + "integrity": "sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk=" + }, + "babel-preset-stage-0": { + "version": "https://registry.npmjs.org/babel-preset-stage-0/-/babel-preset-stage-0-6.24.1.tgz", + "integrity": "sha1-VkLRUEL5E4TX5a+LyIsduVsDnmo=", + "dev": true + }, + "babel-preset-stage-1": { + "version": "https://registry.npmjs.org/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz", + "integrity": "sha1-dpLNfc1oSZB+auSgqFWJz7niv7A=", + "dev": true + }, + "babel-preset-stage-2": { + "version": "https://registry.npmjs.org/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz", + "integrity": "sha1-2eKWD7PXEYfw5k7sYrwHdnIZvcE=", + "dev": true + }, + "babel-preset-stage-3": { + "version": "https://registry.npmjs.org/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz", + "integrity": "sha1-g2raCp56f6N8sTj7kyb4eTSkg5U=", + "dev": true + }, + "babel-register": { + "version": "https://registry.npmjs.org/babel-register/-/babel-register-6.24.1.tgz", + "integrity": "sha1-fhDhOi9xBlvfrVoXh7pFvKbe118=" + }, + "babel-runtime": { + "version": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.23.0.tgz", + "integrity": "sha1-CpSJ8UTecO+zzkMArM2zKeL8VDs=" + }, + "babel-template": { + "version": "https://registry.npmjs.org/babel-template/-/babel-template-6.24.1.tgz", + "integrity": "sha1-BK5RTx+Ts6JTfyoPYKWkX7gwgzM=" + }, + "babel-traverse": { + "version": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.24.1.tgz", + "integrity": "sha1-qzZnP9NW+aCUhlnnszjV/q2zFpU=" + }, + "babel-types": { + "version": "https://registry.npmjs.org/babel-types/-/babel-types-6.24.1.tgz", + "integrity": "sha1-oTaHncFbNga9oNkMH8dDBML/CXU=" + }, + "babelify": { + "version": "https://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz", + "integrity": "sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU=" + }, + "babylon": { + "version": "https://registry.npmjs.org/babylon/-/babylon-6.17.1.tgz", + "integrity": "sha1-F/FP3fNhtpWYH+Z5OF5PHAHr2G8=" + }, + "bach": { + "version": "https://registry.npmjs.org/bach/-/bach-1.1.0.tgz", + "integrity": "sha1-z+VC25Jcs3BR/EkK0QLHO8slioQ=", + "dev": true + }, + "backbone": { + "version": "https://registry.npmjs.org/backbone/-/backbone-1.3.3.tgz", + "integrity": "sha1-TMgOp8sWMaxHSInOQPL4vGg7KZk=", + "dev": true + }, + "backo2": { + "version": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=", + "dev": true + }, + "balanced-match": { + "version": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=" + }, + "base-x": { + "version": "https://registry.npmjs.org/base-x/-/base-x-1.1.0.tgz", + "integrity": "sha1-QtPXF0dPnqAiB/bRqh9CaRPut6w=" + }, + "base64-arraybuffer": { + "version": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", + "dev": true + }, + "base64-js": { + "version": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.2.tgz", + "integrity": "sha1-Ak8Pcq+iW3X5wO5zzU9V7Bvtl4Q=" + }, + "base64id": { + "version": "https://registry.npmjs.org/base64id/-/base64id-0.1.0.tgz", + "integrity": "sha1-As4P3u4M709ACA4ec+g08LG/zj8=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true + }, + "beefy": { + "version": "https://registry.npmjs.org/beefy/-/beefy-2.1.8.tgz", + "integrity": "sha1-e8Ebmkh6mjRnnYXinTtS83T9ACk=", + "dev": true, + "dependencies": { + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "mime": { + "version": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", + "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA=", + "dev": true + }, + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "object-keys": { + "version": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=", + "dev": true + }, + "open": { + "version": "https://registry.npmjs.org/open/-/open-0.0.3.tgz", + "integrity": "sha1-+jd/T/MIIS2SqbjmOVJAhUZGpxM=", + "dev": true + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true + }, + "resolve": { + "version": "https://registry.npmjs.org/resolve/-/resolve-0.6.3.tgz", + "integrity": "sha1-3ZV5gufnNt699TtYpN2RdUV13UY=", + "dev": true + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "through": { + "version": "https://registry.npmjs.org/through/-/through-2.2.7.tgz", + "integrity": "sha1-bo4hIAGR1OtqmfbwEN9Gqhxusr0=", + "dev": true + }, + "xtend": { + "version": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", + "dev": true + } + } + }, + "beeper": { + "version": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz", + "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=" + }, + "better-assert": { + "version": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "dev": true + }, + "bignumber.js": { + "version": "git+https://github.com/debris/bignumber.js.git#94d7146671b9719e00a09c29b01a691bc85048c2" + }, + "binary-extensions": { + "version": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.8.0.tgz", + "integrity": "sha1-SOyNFt9Dd+rl+liEaCSAr02Vx3Q=", + "dev": true + }, + "binaryextensions": { + "version": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-1.0.1.tgz", + "integrity": "sha1-HmN0iLNbWL2l9HdL+WpSEqjJB1U=", + "dev": true + }, + "bindings": { + "version": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz", + "integrity": "sha1-FK1hE4EtLTfXLme0ystLtyZQXxE=" + }, + "bip39": { + "version": "https://registry.npmjs.org/bip39/-/bip39-2.3.1.tgz", + "integrity": "sha1-yCOKvAnXGcbwETbvBC2szF3DWBs=" + }, + "bip66": { + "version": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", + "integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=" + }, + "bl": { + "version": "https://registry.npmjs.org/bl/-/bl-0.7.0.tgz", + "integrity": "sha1-P7BnBgKsKHjrdw3CA58YNr5irls=", + "dependencies": { + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=" + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "blob": { + "version": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", + "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=", + "dev": true + }, + "bluebird": { + "version": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", + "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" + }, + "bn.js": { + "version": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha1-UzRK2xRhehP26N0s4okF0cC6MhU=" + }, + "body-parser": { + "version": "https://registry.npmjs.org/body-parser/-/body-parser-1.14.2.tgz", + "integrity": "sha1-EBXLH+LEQ4WCWVgdtTMy+NDPUPk=", + "dev": true, + "dependencies": { + "debug": { + "version": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true + }, + "http-errors": { + "version": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz", + "integrity": "sha1-GX4izevUGYWF6GlO9nhhl7ke2UI=", + "dev": true + }, + "iconv-lite": { + "version": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz", + "integrity": "sha1-H4irpKsLFQjoMSrMOTRfNumS4vI=", + "dev": true + }, + "ms": { + "version": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "qs": { + "version": "https://registry.npmjs.org/qs/-/qs-5.2.0.tgz", + "integrity": "sha1-qfMRQq9GjLcrJbMBNrokVoNJFr4=", + "dev": true + } + } + }, + "boolbase": { + "version": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "boom": { + "version": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=" + }, + "bops": { + "version": "https://registry.npmjs.org/bops/-/bops-0.0.6.tgz", + "integrity": "sha1-CC0dVfoB5g29wuvC26N/ZZVUzzo=" + }, + "brace-expansion": { + "version": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz", + "integrity": "sha1-Pv/DxQ4ABTH7cg6v+A8K6O8jz1k=" + }, + "braces": { + "version": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true + }, + "brfs": { + "version": "https://registry.npmjs.org/brfs/-/brfs-1.4.3.tgz", + "integrity": "sha1-22ddb16SPm3wh/ylhZyQkKrtMhY=" + }, + "brorand": { + "version": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + }, + "browser-pack": { + "version": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.0.2.tgz", + "integrity": "sha1-+GzWzvT1MAyOY+B6TVEvZfv/RTE=", + "dev": true + }, + "browser-passworder": { + "version": "https://registry.npmjs.org/browser-passworder/-/browser-passworder-2.0.3.tgz", + "integrity": "sha1-b90gguUWoXbtvLPc7gt/n85PeRc=" + }, + "browser-resolve": { + "version": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.2.tgz", + "integrity": "sha1-j/CbCixCFxihBRwmCzLkj0QpOM4=", + "dev": true + }, + "browser-unpack": { + "version": "https://registry.npmjs.org/browser-unpack/-/browser-unpack-0.2.3.tgz", + "integrity": "sha1-iP4EzCZiV+UmUAlc2OBYXce24vE=", + "dependencies": { + "concat-stream": { + "version": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.2.1.tgz", + "integrity": "sha1-81EAtsRjeL+6i2uA+fDQzN8T3GA=" + } + } + }, + "browserify": { + "version": "https://registry.npmjs.org/browserify/-/browserify-13.3.0.tgz", + "integrity": "sha1-tanJAgJD8McORnW+yCI7xifkFc4=", + "dev": true, + "dependencies": { + "concat-stream": { + "version": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", + "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", + "dev": true, + "dependencies": { + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true + } + } + }, + "duplexer2": { + "version": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "dev": true + }, + "process": { + "version": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "punycode": { + "version": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "browserify-aes": { + "version": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.0.6.tgz", + "integrity": "sha1-Xncl297x/Vkw1OurSFZ85FHEigo=" + }, + "browserify-cipher": { + "version": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.0.tgz", + "integrity": "sha1-mYgkSHS/XtTijalWZtzWasj8Njo=", + "dev": true + }, + "browserify-derequire": { + "version": "https://registry.npmjs.org/browserify-derequire/-/browserify-derequire-0.9.4.tgz", + "integrity": "sha1-ZNYeVs/f8LjxdP2MV/i0Az4oeJU=", + "dependencies": { + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=" + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "through2": { + "version": "https://registry.npmjs.org/through2/-/through2-1.1.1.tgz", + "integrity": "sha1-CEfLxESfNAVXTb3M2buEG4OsNUU=" + } + } + }, + "browserify-des": { + "version": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.0.tgz", + "integrity": "sha1-2qJ3cXRwki7S/hhZQRihdUOXId0=", + "dev": true + }, + "browserify-rsa": { + "version": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true + }, + "browserify-sha3": { + "version": "https://registry.npmjs.org/browserify-sha3/-/browserify-sha3-0.0.1.tgz", + "integrity": "sha1-P/NKMAbvFcD7NWflQbkaI0ASPRE=" + }, + "browserify-sign": { + "version": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true + }, + "browserify-unibabel": { + "version": "https://registry.npmjs.org/browserify-unibabel/-/browserify-unibabel-3.0.0.tgz", + "integrity": "sha1-WmuPD3BM44jTkn30czfiWDD3Hdo=" + }, + "browserify-zlib": { + "version": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", + "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", + "dev": true + }, + "bs58": { + "version": "https://registry.npmjs.org/bs58/-/bs58-3.1.0.tgz", + "integrity": "sha1-1MJjiL9IBMrHFBQbGUWqR+XrJI4=" + }, + "bs58check": { + "version": "https://registry.npmjs.org/bs58check/-/bs58check-1.3.4.tgz", + "integrity": "sha1-xSVABzdJEXcU+gQsMEfrj5FRy/g=" + }, + "buffer": { + "version": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "dev": true, + "dependencies": { + "base64-js": { + "version": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.0.tgz", + "integrity": "sha1-o5mS1yNYSBGYK+XikLtqU9hnAPE=", + "dev": true + } + } + }, + "buffer-crc32": { + "version": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true + }, + "buffer-equal": { + "version": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", + "integrity": "sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs=" + }, + "buffer-shims": { + "version": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", + "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" + }, + "buffer-xor": { + "version": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" + }, + "bufferstreams": { + "version": "https://registry.npmjs.org/bufferstreams/-/bufferstreams-1.1.1.tgz", + "integrity": "sha1-AWE3MGCsWYjv+ZBYcxEU9uGV1R4=" + }, + "builtin-modules": { + "version": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" + }, + "builtin-status-codes": { + "version": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "builtins": { + "version": "https://registry.npmjs.org/builtins/-/builtins-0.0.3.tgz", + "integrity": "sha1-XQBhZtpxYQvCvPcwGfDwzEMwl1U=" + }, + "bytes": { + "version": "https://registry.npmjs.org/bytes/-/bytes-2.2.0.tgz", + "integrity": "sha1-/TVGSkA/b5EXwt42Cez/nK4ABYg=", + "dev": true + }, + "cached-path-relative": { + "version": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.1.tgz", + "integrity": "sha1-0JxLUoAKpMB44t2BqGmqyQ0uVOc=", + "dev": true + }, + "caller-path": { + "version": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=" + }, + "callsite": { + "version": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", + "dev": true + }, + "callsites": { + "version": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=" + }, + "camelcase": { + "version": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" + }, + "caseless": { + "version": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chai": { + "version": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", + "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", + "dev": true + }, + "chalk": { + "version": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=" + }, + "charm": { + "version": "https://registry.npmjs.org/charm/-/charm-1.0.2.tgz", + "integrity": "sha1-it02cVOm2aWBMxBSxAkJkdqZXjU=", + "dev": true + }, + "checkpoint-store": { + "version": "https://registry.npmjs.org/checkpoint-store/-/checkpoint-store-1.1.0.tgz", + "integrity": "sha1-BOTLUWuRQziTWB5tRgGnjpVS6gY=" + }, + "cheerio": { + "version": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", + "integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=", + "dev": true + }, + "chokidar": { + "version": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true + }, + "chownr": { + "version": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", + "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=" + }, + "cipher-base": { + "version": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.3.tgz", + "integrity": "sha1-7qvxlEGc6QDaMBjCB9IS8qbfCgc=" + }, + "circular-json": { + "version": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.1.tgz", + "integrity": "sha1-vos2rvzN6LPKeqLWr8B6NyQsDS0=" + }, + "classnames": { + "version": "https://registry.npmjs.org/classnames/-/classnames-2.2.5.tgz", + "integrity": "sha1-+zgB1FNGdknvNgPH1hoCvRKb3m0=" + }, + "cli-cursor": { + "version": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=" + }, + "cli-table": { + "version": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", + "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", + "dev": true, + "dependencies": { + "colors": { + "version": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "dev": true + } + } + }, + "cli-width": { + "version": "https://registry.npmjs.org/cli-width/-/cli-width-2.1.0.tgz", + "integrity": "sha1-sjTKIJsp72b8UY2bmNWEewDt8Ao=" + }, + "client-sw-ready-event": { + "version": "https://registry.npmjs.org/client-sw-ready-event/-/client-sw-ready-event-3.1.0.tgz", + "integrity": "sha1-SR1E+BFiEH/TfKBDWLSzPs/gpy4=" + }, + "cliui": { + "version": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=" + }, + "clone": { + "version": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz", + "integrity": "sha1-Jgt6meux7f4kdTgXX3gyQ8sZ0Uk=" + }, + "clone-stats": { + "version": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", + "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=" + }, + "co": { + "version": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "code-point-at": { + "version": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "coinstring": { + "version": "https://registry.npmjs.org/coinstring/-/coinstring-2.3.0.tgz", + "integrity": "sha1-zbYzY6lhUCQEolr7gsLibV/2J6Q=", + "dependencies": { + "bs58": { + "version": "https://registry.npmjs.org/bs58/-/bs58-2.0.1.tgz", + "integrity": "sha1-VZCNWPGYKrogCPob7Y+RmYopv40=" + } + } + }, + "collection-map": { + "version": "https://registry.npmjs.org/collection-map/-/collection-map-0.1.0.tgz", + "integrity": "sha1-TP+R0lEI159O3uzObs7j5IjyZ8I=", + "dev": true, + "dependencies": { + "make-iterator": { + "version": "https://registry.npmjs.org/make-iterator/-/make-iterator-0.1.1.tgz", + "integrity": "sha1-hz0nuBmKRlqBSDtvXRbaToY+z1s=", + "dev": true + } + } + }, + "color": { + "version": "https://registry.npmjs.org/color/-/color-0.11.4.tgz", + "integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=" + }, + "color-convert": { + "version": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", + "integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=" + }, + "color-name": { + "version": "https://registry.npmjs.org/color-name/-/color-name-1.1.2.tgz", + "integrity": "sha1-XIq3K2S9IhXWF66VWeuxSEdc+Y0=" + }, + "color-string": { + "version": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz", + "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=" + }, + "colors": { + "version": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true + }, + "combine-source-map": { + "version": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.7.2.tgz", + "integrity": "sha1-CHAxKFazB6h8xKxIbzqaYq7MwJ4=", + "dev": true, + "dependencies": { + "convert-source-map": { + "version": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", + "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", + "dev": true + } + } + }, + "combined-stream": { + "version": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=" + }, + "commander": { + "version": "https://registry.npmjs.org/commander/-/commander-2.3.0.tgz", + "integrity": "sha1-/UMOiJgy7DU7ms0d4hfBHLPu+HM=", + "dev": true + }, + "commondir": { + "version": "https://registry.npmjs.org/commondir/-/commondir-0.0.1.tgz", + "integrity": "sha1-ifAP3NUbUZxXhzP+xWPmptp/W+I=" + }, + "commonmark": { + "version": "https://registry.npmjs.org/commonmark/-/commonmark-0.24.0.tgz", + "integrity": "sha1-uA3gGCxUY1VkOqFdsSv7KCNoJ48=" + }, + "commonmark-react-renderer": { + "version": "https://registry.npmjs.org/commonmark-react-renderer/-/commonmark-react-renderer-4.3.3.tgz", + "integrity": "sha1-nEvKE4vIMoe655LM8TNzi+nLxvo=" + }, + "component-bind": { + "version": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=", + "dev": true + }, + "component-emitter": { + "version": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz", + "integrity": "sha1-KWWU8nU9qmOZbSrwjRWpURbJrsM=", + "dev": true + }, + "component-inherit": { + "version": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", + "dev": true + }, + "concat-map": { + "version": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=" + }, + "config-chain": { + "version": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.11.tgz", + "integrity": "sha1-q6CXR9++TD5w52am5BWG4YWfxvI=", + "dev": true + }, + "console-browserify": { + "version": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true + }, + "console-control-strings": { + "version": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "consolidate": { + "version": "https://registry.npmjs.org/consolidate/-/consolidate-0.14.5.tgz", + "integrity": "sha1-WiUEe8dvcwcmZ8jLUsmJiI9JTGM=", + "dev": true + }, + "constants-browserify": { + "version": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "content-disposition": { + "version": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz", + "integrity": "sha1-t9ETrueo3Se9IRM8TcJSnfFyHu0=" + }, + "convert-source-map": { + "version": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.0.tgz", + "integrity": "sha1-ms1whRxtXf3ZPZKC5e35SgP/RrU=" + }, + "cookie": { + "version": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-signature": { + "version": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "copy-props": { + "version": "https://registry.npmjs.org/copy-props/-/copy-props-1.6.0.tgz", + "integrity": "sha1-8DJLvumXcRAeezraES8xPDk9uO0=", + "dev": true + }, + "copy-to-clipboard": { + "version": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-2.1.0.tgz", + "integrity": "sha1-Az8WUqU9gpBYU3sPCNK86STt/FM=" + }, + "core-js": { + "version": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", + "integrity": "sha1-TekR5mew6ukSTjQlS1OupvxhjT4=" + }, + "core-util-is": { + "version": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "create-ecdh": { + "version": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz", + "integrity": "sha1-iIxyNZbN92EvZJgjPuvXo1MBc30=", + "dev": true + }, + "create-hash": { + "version": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", + "integrity": "sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=" + }, + "create-hmac": { + "version": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.6.tgz", + "integrity": "sha1-rLniIaThe9sHbpBlfEK5PjcmzwY=" + }, + "create-react-class": { + "version": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.5.3.tgz", + "integrity": "sha1-+w98rnkznpoXnhlO9Gbvo5I4IP4=" + }, + "cross-spawn": { + "version": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "dependencies": { + "lru-cache": { + "version": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha1-HRdnnAac2l0ECZGgnbwsDbN35V4=", + "dev": true + }, + "which": { + "version": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", + "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", + "dev": true + } + } + }, + "cryptiles": { + "version": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=" + }, + "crypto-browserify": { + "version": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.11.0.tgz", + "integrity": "sha1-NlKgkGq5sqfgw85mpAjpV6JIVSI=", + "dev": true + }, + "crypto-js": { + "version": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.8.tgz", + "integrity": "sha1-cV8HC/YBTyrpkqmLOSkli3E/CNU=" + }, + "css": { + "version": "https://registry.npmjs.org/css/-/css-2.2.1.tgz", + "integrity": "sha1-c6TIHehdtmTU7mdPfUcIXjstVdw=", + "dev": true, + "dependencies": { + "source-map": { + "version": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "dev": true + } + } + }, + "css-select": { + "version": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "dev": true + }, + "css-what": { + "version": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", + "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=", + "dev": true + }, + "cssauron": { + "version": "https://registry.npmjs.org/cssauron/-/cssauron-1.4.0.tgz", + "integrity": "sha1-pmAt/34EqDBtwNuaVR6S6LVmKtg=", + "dev": true + }, + "cssom": { + "version": "https://registry.npmjs.org/cssom/-/cssom-0.3.2.tgz", + "integrity": "sha1-uANhcMefB6kP8vFuIihAJ6JDhIs=", + "dev": true + }, + "cssstyle": { + "version": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.37.tgz", + "integrity": "sha1-VBCXI0yyUTyDzu06zdwn/yeYfVQ=", + "dev": true + }, + "cycle": { + "version": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=", + "dev": true + }, + "cyclist": { + "version": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", + "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=" + }, + "d": { + "version": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=" + }, + "d3": { + "version": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz", + "integrity": "sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g=" + }, + "dashdash": { + "version": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dependencies": { + "assert-plus": { + "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "date-now": { + "version": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "dateformat": { + "version": "https://registry.npmjs.org/dateformat/-/dateformat-2.0.0.tgz", + "integrity": "sha1-J0Pjq7XD/CRi5SfcpEXgTp9N7hc=" + }, + "debounce": { + "version": "https://registry.npmjs.org/debounce/-/debounce-1.0.2.tgz", + "integrity": "sha1-UDzGdNjX9zcJlmT7dd29NrlibcY=" + }, + "debug": { + "version": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz", + "integrity": "sha1-krrR9tBbu2u6Isyoi80OyJTChh4=" + }, + "debug-fabulous": { + "version": "https://registry.npmjs.org/debug-fabulous/-/debug-fabulous-0.0.4.tgz", + "integrity": "sha1-+gccXYdIRoVCSAdCHKSxawsaB2M=", + "dev": true, + "dependencies": { + "object-assign": { + "version": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz", + "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=", + "dev": true + } + } + }, + "decamelize": { + "version": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "deep-diff": { + "version": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.4.tgz", + "integrity": "sha1-qsXDmVIjar5fA3ojSQYLoBsArkg=" + }, + "deep-eql": { + "version": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", + "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", + "dev": true, + "dependencies": { + "type-detect": { + "version": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", + "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", + "dev": true + } + } + }, + "deep-equal": { + "version": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" + }, + "deep-extend": { + "version": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", + "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=" + }, + "deep-freeze-strict": { + "version": "https://registry.npmjs.org/deep-freeze-strict/-/deep-freeze-strict-1.1.1.tgz", + "integrity": "sha1-d9BYPKJKab5LvZrC+uQV1VUj5bA=", + "dev": true + }, + "deep-is": { + "version": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, + "deepmerge": { + "version": "https://registry.npmjs.org/deepmerge/-/deepmerge-0.2.10.tgz", + "integrity": "sha1-iQa/nlJaT78bIDsq/LRkAkmCEhk=", + "dev": true + }, + "default-resolution": { + "version": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", + "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=", + "dev": true + }, + "deferred-leveldown": { + "version": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-1.2.1.tgz", + "integrity": "sha1-XSXDMQ9f6QmUb2JA3J+Q3RCace8=" + }, + "define-properties": { + "version": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", + "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=" + }, + "defined": { + "version": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" + }, + "del": { + "version": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=" + }, + "delayed-stream": { + "version": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "delegates": { + "version": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "denodeify": { + "version": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz", + "integrity": "sha1-OjYof1A05pnnV3kBBSwubJQlFjE=" + }, + "depd": { + "version": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz", + "integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM=" + }, + "deps-sort": { + "version": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.0.tgz", + "integrity": "sha1-CRckkC6EZYJg65EHSMzNGvbiH7U=", + "dev": true + }, + "derequire": { + "version": "https://registry.npmjs.org/derequire/-/derequire-2.0.6.tgz", + "integrity": "sha1-MaQUu3yhdiOfp4sRZjbvd9UX52g=" + }, + "des.js": { + "version": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true + }, + "destroy": { + "version": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "detect-file": { + "version": "https://registry.npmjs.org/detect-file/-/detect-file-0.1.0.tgz", + "integrity": "sha1-STXe39lIhkjgBrASlWbpOGcR6mM=", + "dev": true + }, + "detect-indent": { + "version": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=" + }, + "detect-newline": { + "version": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", + "dev": true + }, + "detect-node": { + "version": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.3.tgz", + "integrity": "sha1-ogM8CcyOFY03dI+951B4Mr1s4Sc=" + }, + "detective": { + "version": "https://registry.npmjs.org/detective/-/detective-4.5.0.tgz", + "integrity": "sha1-blqMaybmx6JUsca210kNmOyR7dE=", + "dev": true + }, + "diff": { + "version": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", + "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", + "dev": true + }, + "diffie-hellman": { + "version": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz", + "integrity": "sha1-tYNXOScM/ias9jIJn97SoH8gnl4=", + "dev": true + }, + "disc": { + "version": "https://registry.npmjs.org/disc/-/disc-1.3.2.tgz", + "integrity": "sha1-MqbwLkhu33eGClNj0icYQl0pbkA=" + }, + "dnode": { + "version": "https://registry.npmjs.org/dnode/-/dnode-1.2.2.tgz", + "integrity": "sha1-SsPP4m4pKzs5uCWK59lO3FgTLvo=" + }, + "dnode-protocol": { + "version": "https://registry.npmjs.org/dnode-protocol/-/dnode-protocol-0.2.2.tgz", + "integrity": "sha1-URUdFvw7X4SBXuC5SXoQYdDRlJ0=" + }, + "doctrine": { + "version": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=" + }, + "dom-serializer": { + "version": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", + "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", + "dev": true, + "dependencies": { + "domelementtype": { + "version": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", + "dev": true + } + } + }, + "dom-walk": { + "version": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz", + "integrity": "sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg=" + }, + "domain-browser": { + "version": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", + "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=", + "dev": true + }, + "domelementtype": { + "version": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", + "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=", + "dev": true + }, + "domhandler": { + "version": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.1.tgz", + "integrity": "sha1-iS5HAAqZvlW783dP/qBWHYh5wlk=", + "dev": true + }, + "domutils": { + "version": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dev": true + }, + "drbg.js": { + "version": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", + "integrity": "sha1-Pja2xCs3BDgjzbwzLVjzHiRFSAs=" + }, + "duplexer": { + "version": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" + }, + "duplexer2": { + "version": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", + "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", + "dependencies": { + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=" + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "duplexify": { + "version": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.0.tgz", + "integrity": "sha1-GqdzAC4VeEV+nZ1KULDMquvL1gQ=", + "dependencies": { + "end-of-stream": { + "version": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.0.0.tgz", + "integrity": "sha1-1FlucCc0qT5A6a+GQxnqvZn/Lw4=" + }, + "once": { + "version": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=" + } + } + }, + "each-props": { + "version": "https://registry.npmjs.org/each-props/-/each-props-1.3.0.tgz", + "integrity": "sha1-ftgDHJJ2iK7bSoluuRSFtEh7kOo=", + "dev": true + }, + "ecc-jsbn": { + "version": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true + }, + "ee-first": { + "version": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "elliptic": { + "version": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", + "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=" + }, + "encodeurl": { + "version": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", + "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" + }, + "encoding": { + "version": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=" + }, + "end-of-stream": { + "version": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.0.tgz", + "integrity": "sha1-epDYM+/abPpurA9JSduw+tOmMgY=" + }, + "engine.io": { + "version": "https://registry.npmjs.org/engine.io/-/engine.io-1.8.0.tgz", + "integrity": "sha1-PutfJky3XbvsG6rqJtYfWk6s4qo=", + "dev": true, + "dependencies": { + "debug": { + "version": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true + }, + "ms": { + "version": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "engine.io-client": { + "version": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.8.0.tgz", + "integrity": "sha1-e3MOQSdBQIdZbZvjyI0rxf22z1w=", + "dev": true, + "dependencies": { + "component-emitter": { + "version": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "debug": { + "version": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true + }, + "ms": { + "version": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "engine.io-parser": { + "version": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.3.1.tgz", + "integrity": "sha1-lVTxrjMQfW+9FwylRm0vgz9qB88=", + "dev": true, + "dependencies": { + "has-binary": { + "version": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.6.tgz", + "integrity": "sha1-JTJvOc+k9hath4eJTjryz7x7bhA=", + "dev": true + }, + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, + "ensnare": { + "version": "https://registry.npmjs.org/ensnare/-/ensnare-1.0.0.tgz", + "integrity": "sha1-ctK/fvSKuiH2at8p0AoJBO3bYcc=" + }, + "entities": { + "version": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", + "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=" + }, + "envify": { + "version": "https://registry.npmjs.org/envify/-/envify-4.0.0.tgz", + "integrity": "sha1-95E0Pj0RzCnM5BFQMAqK9hxmyrA=", + "dev": true, + "dependencies": { + "esprima": { + "version": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", + "dev": true + } + } + }, + "enzyme": { + "version": "https://registry.npmjs.org/enzyme/-/enzyme-2.8.2.tgz", + "integrity": "sha1-bIvLBQEqvEqkvDIT+yN4C5tbFxQ=", + "dev": true + }, + "errno": { + "version": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz", + "integrity": "sha1-uJbiOp5ei6M4cfyZar02NfyaHH0=", + "dependencies": { + "prr": { + "version": "https://registry.npmjs.org/prr/-/prr-0.0.0.tgz", + "integrity": "sha1-GoS4WQgyVQFBGFPQCB7j+obikmo=" + } + } + }, + "error-ex": { + "version": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=" + }, + "es-abstract": { + "version": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.7.0.tgz", + "integrity": "sha1-363ndOAb/Nl/lhgCmMRJyGI/uUw=" + }, + "es-to-primitive": { + "version": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", + "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=" + }, + "es5-ext": { + "version": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.21.tgz", + "integrity": "sha1-Gacl+eUdAwC7wejoIRCf2dr1WSU=" + }, + "es6-iterator": { + "version": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.1.tgz", + "integrity": "sha1-jjGcnwRTv1ddN0lAplWSDlnKVRI=" + }, + "es6-map": { + "version": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=" + }, + "es6-set": { + "version": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=" + }, + "es6-symbol": { + "version": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=" + }, + "es6-weak-map": { + "version": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", + "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=" + }, + "escape-html": { + "version": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "escodegen": { + "version": "https://registry.npmjs.org/escodegen/-/escodegen-1.3.3.tgz", + "integrity": "sha1-8CQBb1qI4Eb9EgBQVek5gC5sXyM=", + "dependencies": { + "esprima": { + "version": "https://registry.npmjs.org/esprima/-/esprima-1.1.1.tgz", + "integrity": "sha1-W28VR/TRAuZw4UDFCb5ncdautUk=" + }, + "estraverse": { + "version": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz", + "integrity": "sha1-hno+jlip+EYYr7bC3bzZFrfLr3E=" + }, + "esutils": { + "version": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz", + "integrity": "sha1-gVHTWOIMisx/t0XnRywAJf5JZXA=" + }, + "source-map": { + "version": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "optional": true + } + } + }, + "escope": { + "version": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", + "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=" + }, + "eslint": { + "version": "https://registry.npmjs.org/eslint/-/eslint-2.13.1.tgz", + "integrity": "sha1-5MyPoPAJ+4KaquI4VaKTYL4fbBE=", + "dependencies": { + "strip-json-comments": { + "version": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", + "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=" + } + } + }, + "eslint-plugin-chai": { + "version": "https://registry.npmjs.org/eslint-plugin-chai/-/eslint-plugin-chai-0.0.1.tgz", + "integrity": "sha1-mh3qWLM1wxJCIZ0Fmzf/sUMJ9uE=", + "dev": true + }, + "eslint-plugin-mocha": { + "version": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-4.9.0.tgz", + "integrity": "sha1-kXqLSZq40MAdacbk+B02LuCZtv0=", + "dev": true + }, + "espree": { + "version": "https://registry.npmjs.org/espree/-/espree-3.4.3.tgz", + "integrity": "sha1-KRC1zNSc6JPC//+qtP2LOjG4I3Q=", + "dependencies": { + "acorn": { + "version": "https://registry.npmjs.org/acorn/-/acorn-5.0.3.tgz", + "integrity": "sha1-xGDfCEkUY/AozLguqzcwvwEIez0=" + } + } + }, + "esprima-fb": { + "version": "https://registry.npmjs.org/esprima-fb/-/esprima-fb-3001.0001.0000-dev-harmony-fb.tgz", + "integrity": "sha1-t303q8046gt3Qmu4vCkizmtCZBE=" + }, + "esrecurse": { + "version": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.1.0.tgz", + "integrity": "sha1-RxO2U2rffyrE8yfVWed1a/9kgiA=", + "dependencies": { + "estraverse": { + "version": "https://registry.npmjs.org/estraverse/-/estraverse-4.1.1.tgz", + "integrity": "sha1-9srKcokzqFDvkGYdDheYK6RxEaI=" + } + } + }, + "estraverse": { + "version": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" + }, + "esutils": { + "version": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + }, + "etag": { + "version": "https://registry.npmjs.org/etag/-/etag-1.8.0.tgz", + "integrity": "sha1-b2Ma7zNtbEY2K1F2QETOIWvjwFE=" + }, + "eth-bin-to-ops": { + "version": "https://registry.npmjs.org/eth-bin-to-ops/-/eth-bin-to-ops-1.0.1.tgz", + "integrity": "sha1-TScDuYeIJbw4xiWZEOkLTbAFx94=" + }, + "eth-block-tracker": { + "version": "https://registry.npmjs.org/eth-block-tracker/-/eth-block-tracker-1.0.17.tgz", + "integrity": "sha1-9jwEkCyB2N+mIk1EARWhaRa6w1w=" + }, + "eth-contract-metadata": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eth-contract-metadata/-/eth-contract-metadata-1.0.0.tgz", + "integrity": "sha1-YV/Z1jvjpwU0FDJdt7hdv/5tFWg=" + }, + "eth-ens-namehash": { + "version": "https://registry.npmjs.org/eth-ens-namehash/-/eth-ens-namehash-1.0.2.tgz", + "integrity": "sha1-Bezda6wtf9e8XKhKmTxrrZ2k7bk=", + "dependencies": { + "js-sha3": { + "version": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.7.tgz", + "integrity": "sha1-DU/9gALVMzqrr0oj7tL2N0yfKOc=" + } + } + }, + "eth-hd-keyring": { + "version": "https://registry.npmjs.org/eth-hd-keyring/-/eth-hd-keyring-1.2.0.tgz", + "integrity": "sha1-QLzH6od+9cdG9UwMh6aznOte3eM=", + "dependencies": { + "ethereumjs-util": { + "version": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.1.1.tgz", + "integrity": "sha1-Ei+zjep0fcYrOuv8Nl0b1Ivktz4=" + } + } + }, + "eth-query": { + "version": "https://registry.npmjs.org/eth-query/-/eth-query-2.1.1.tgz", + "integrity": "sha1-Co7lvSTHtcBKDMyLpH/Gq6QpjZI=" + }, + "eth-sig-util": { + "version": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.2.1.tgz", + "integrity": "sha1-JUo+csXCzLYMncXmRl/H4XS2v5E=", + "dependencies": { + "ethereumjs-util": { + "version": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.1.1.tgz", + "integrity": "sha1-Ei+zjep0fcYrOuv8Nl0b1Ivktz4=" + } + } + }, + "eth-simple-keyring": { + "version": "https://registry.npmjs.org/eth-simple-keyring/-/eth-simple-keyring-1.1.1.tgz", + "integrity": "sha1-bdddfMbt6nx4jPGe+UMcgwzZYa4=", + "dependencies": { + "ethereumjs-util": { + "version": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.1.1.tgz", + "integrity": "sha1-Ei+zjep0fcYrOuv8Nl0b1Ivktz4=" + } + } + }, + "ethereum-common": { + "version": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.0.18.tgz", + "integrity": "sha1-L9w1dvIykDNYl26znaeDIT/5Uj8=" + }, + "ethereum-ens-network-map": { + "version": "https://registry.npmjs.org/ethereum-ens-network-map/-/ethereum-ens-network-map-1.0.0.tgz", + "integrity": "sha1-Q812ac6VCnieFRABEY1NZfIQ7rc=" + }, + "ethereumjs-account": { + "version": "https://registry.npmjs.org/ethereumjs-account/-/ethereumjs-account-2.0.4.tgz", + "integrity": "sha1-+MMCMby3B/RRTYoFLB+doQNiTUc=", + "dependencies": { + "ethereumjs-util": { + "version": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-4.5.0.tgz", + "integrity": "sha1-PpQosxfuvaPXJg2FT93alUsfG8Y=" + } + } + }, + "ethereumjs-block": { + "version": "https://registry.npmjs.org/ethereumjs-block/-/ethereumjs-block-1.5.0.tgz", + "integrity": "sha1-sLkBjpzXMUbGAdx9svaypFYeRow=", + "dependencies": { + "async": { + "version": "https://registry.npmjs.org/async/-/async-2.4.1.tgz", + "integrity": "sha1-YqVrJ5yYoR0JhwlqAcw+6463u9c=" + } + } + }, + "ethereumjs-tx": { + "version": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-1.3.1.tgz", + "integrity": "sha1-1pCavPs32mQE/BgSTTUe2iAEfaw=" + }, + "ethereumjs-util": { + "version": "git://github.com/ethereumjs/ethereumjs-util.git#ac5d0908536b447083ea422b435da27f26615de9" + }, + "ethereumjs-vm": { + "version": "https://registry.npmjs.org/ethereumjs-vm/-/ethereumjs-vm-2.0.2.tgz", + "integrity": "sha1-hOI3KlcVqApi9/KjEvjGRTfoqEI=", + "dependencies": { + "async": { + "version": "https://registry.npmjs.org/async/-/async-2.4.1.tgz", + "integrity": "sha1-YqVrJ5yYoR0JhwlqAcw+6463u9c=" + }, + "ethereumjs-util": { + "version": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-4.5.0.tgz", + "integrity": "sha1-PpQosxfuvaPXJg2FT93alUsfG8Y=" + } + } + }, + "ethereumjs-wallet": { + "version": "https://registry.npmjs.org/ethereumjs-wallet/-/ethereumjs-wallet-0.6.0.tgz", + "integrity": "sha1-gnY7Fpfuenlr5xVdqd+0my+Yz9s=", + "dependencies": { + "ethereumjs-util": { + "version": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-4.5.0.tgz", + "integrity": "sha1-PpQosxfuvaPXJg2FT93alUsfG8Y=" + } + } + }, + "ethjs-abi": { + "version": "https://registry.npmjs.org/ethjs-abi/-/ethjs-abi-0.2.0.tgz", + "integrity": "sha1-0+LCIQEVIPxJm3FoIDbBT8wvWyU=", + "dependencies": { + "js-sha3": { + "version": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.5.tgz", + "integrity": "sha1-uvDA6MVK1ZA0R9+Wreekobynmko=" + } + } + }, + "ethjs-contract": { + "version": "https://registry.npmjs.org/ethjs-contract/-/ethjs-contract-0.1.9.tgz", + "integrity": "sha1-HCdmiWpW1H7B1tZhgpxJzDilUgo=", + "dependencies": { + "ethjs-util": { + "version": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.3.tgz", + "integrity": "sha1-39XqSkANxeQhqInK9H4IGtp4u1U=" + }, + "js-sha3": { + "version": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.5.tgz", + "integrity": "sha1-uvDA6MVK1ZA0R9+Wreekobynmko=" + } + } + }, + "ethjs-ens": { + "version": "https://registry.npmjs.org/ethjs-ens/-/ethjs-ens-2.0.0.tgz", + "integrity": "sha1-ZyLvx4fBe5pbJ+a0Jc2ZhvYlFOo=" + }, + "ethjs-filter": { + "version": "https://registry.npmjs.org/ethjs-filter/-/ethjs-filter-0.1.5.tgz", + "integrity": "sha1-ARKvYBfCRnfjK4/esg5hlgGbdZg=" + }, + "ethjs-format": { + "version": "https://registry.npmjs.org/ethjs-format/-/ethjs-format-0.2.0.tgz", + "integrity": "sha1-tKpRP8HVAnDY8QK/BvA8lJDTE5E=", + "dependencies": { + "ethjs-util": { + "version": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.3.tgz", + "integrity": "sha1-39XqSkANxeQhqInK9H4IGtp4u1U=" + } + } + }, + "ethjs-query": { + "version": "https://registry.npmjs.org/ethjs-query/-/ethjs-query-0.2.6.tgz", + "integrity": "sha1-nY5gRLi/dt0zQPhDcWoiWbnJHTw=" + }, + "ethjs-rpc": { + "version": "https://registry.npmjs.org/ethjs-rpc/-/ethjs-rpc-0.1.5.tgz", + "integrity": "sha1-CZ4i8n3EwYtpeKSF/DaxsPeWkIA=" + }, + "ethjs-schema": { + "version": "https://registry.npmjs.org/ethjs-schema/-/ethjs-schema-0.1.5.tgz", + "integrity": "sha1-WXQOOzl3vNu5sRvDBoIB6Kzquw0=" + }, + "ethjs-util": { + "version": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.4.tgz", + "integrity": "sha1-HItoeSV0RO9NPz+7rC3tEs2ZfZM=" + }, + "eve-raphael": { + "version": "https://registry.npmjs.org/eve-raphael/-/eve-raphael-0.5.0.tgz", + "integrity": "sha1-F8dUt5K+7z+maE15z1pHxjxM2jA=" + }, + "event-emitter": { + "version": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=" + }, + "event-stream": { + "version": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", + "dev": true + }, + "eventemitter3": { + "version": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz", + "integrity": "sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg=", + "dev": true + }, + "events": { + "version": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, + "events-to-array": { + "version": "https://registry.npmjs.org/events-to-array/-/events-to-array-1.1.2.tgz", + "integrity": "sha1-LUH1Y+H+QA7Uli/hpNXGp1Od9/Y=", + "dev": true + }, + "evp_bytestokey": { + "version": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.0.tgz", + "integrity": "sha1-SXtmrZ/vZc18CKYYCCS6FHa2blM=" + }, + "exit-hook": { + "version": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=" + }, + "expand-brackets": { + "version": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true + }, + "expand-range": { + "version": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true + }, + "expand-template": { + "version": "https://registry.npmjs.org/expand-template/-/expand-template-1.0.3.tgz", + "integrity": "sha1-bDAzIxd6YrGyLAcCefeGEoe2mxo=" + }, + "expand-tilde": { + "version": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz", + "integrity": "sha1-C4HrqJflo9MdHD0QL48BRB5VlEk=", + "dev": true + }, + "express": { + "version": "https://registry.npmjs.org/express/-/express-4.15.3.tgz", + "integrity": "sha1-urZdDwOqgMNYQIly/HAPkWlEtmI=" + }, + "extend": { + "version": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extend-shallow": { + "version": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true + }, + "extension-link-enabler": { + "version": "https://registry.npmjs.org/extension-link-enabler/-/extension-link-enabler-1.0.0.tgz", + "integrity": "sha1-V7kZru7fOL6XJwuYmM7nimN+RvM=" + }, + "extensionizer": { + "version": "https://registry.npmjs.org/extensionizer/-/extensionizer-1.0.0.tgz", + "integrity": "sha1-AcIJu+ptnArLp3Epw6pKmpj8NTg=" + }, + "extglob": { + "version": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true + }, + "extsprintf": { + "version": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", + "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=" + }, + "eyes": { + "version": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=", + "dev": true + }, + "fake-merkle-patricia-tree": { + "version": "https://registry.npmjs.org/fake-merkle-patricia-tree/-/fake-merkle-patricia-tree-1.0.1.tgz", + "integrity": "sha1-S4w6z7Ugr635hgsfFM2M40As3dM=" + }, + "falafel": { + "version": "https://registry.npmjs.org/falafel/-/falafel-1.2.0.tgz", + "integrity": "sha1-wY0k71CRF0pJfzGM0ksCaiXN2rQ=", + "dependencies": { + "acorn": { + "version": "https://registry.npmjs.org/acorn/-/acorn-1.2.2.tgz", + "integrity": "sha1-yM4n3grMdtiW0rH6099YjZ6C8BQ=" + }, + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + } + } + }, + "fancy-log": { + "version": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.0.tgz", + "integrity": "sha1-Rb4X0Cu5kX1gzP/UmVyZnmyMmUg=" + }, + "fast-levenshtein": { + "version": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, + "faye-websocket": { + "version": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.7.3.tgz", + "integrity": "sha1-zEB0x/Sk39A69U3WXDVLE1EyzhE=", + "dev": true + }, + "fbjs": { + "version": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.12.tgz", + "integrity": "sha1-ELXZL3bUVXX9Y6IX1OoCvqL47QQ=", + "dependencies": { + "core-js": { + "version": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" + } + } + }, + "fetch-ponyfill": { + "version": "https://registry.npmjs.org/fetch-ponyfill/-/fetch-ponyfill-4.0.0.tgz", + "integrity": "sha1-GM/jjWnN5Crsccs6znnh8idik9o=", + "dependencies": { + "node-fetch": { + "version": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.6.3.tgz", + "integrity": "sha1-3CNO3WSJmC1Y6PDbT2lQKavNjAQ=" + } + } + }, + "figures": { + "version": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=" + }, + "file-entry-cache": { + "version": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-1.3.1.tgz", + "integrity": "sha1-RMYepgeuS+nBQC9B9EJwy/4zT/g=" + }, + "file-tree": { + "version": "https://registry.npmjs.org/file-tree/-/file-tree-1.0.0.tgz", + "integrity": "sha1-/a2ZnLf6REODULUUx4+TWzBuk+M=" + }, + "filename-regex": { + "version": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "fileset": { + "version": "https://registry.npmjs.org/fileset/-/fileset-0.1.8.tgz", + "integrity": "sha1-UGuRqTluqn4y+0KoQHfHoMc2t0E=", + "dev": true, + "optional": true, + "dependencies": { + "glob": { + "version": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "dev": true, + "optional": true, + "dependencies": { + "minimatch": { + "version": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "dev": true, + "optional": true + } + } + }, + "minimatch": { + "version": "https://registry.npmjs.org/minimatch/-/minimatch-0.4.0.tgz", + "integrity": "sha1-vSx9Bg0sjI/Xzefx8u0tWycP2xs=", + "dev": true, + "optional": true + } + } + }, + "fill-range": { + "version": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "dev": true + }, + "finalhandler": { + "version": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.0.3.tgz", + "integrity": "sha1-70fneVDpmXgOhgIqVg4yF+DQzIk=" + }, + "find-global-packages": { + "version": "https://registry.npmjs.org/find-global-packages/-/find-global-packages-0.0.1.tgz", + "integrity": "sha1-S6f9/xfun6fagzCV94tejNvfPis=", + "dev": true + }, + "find-up": { + "version": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=" + }, + "findup-sync": { + "version": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.4.3.tgz", + "integrity": "sha1-QAQ5Kee8YK3wt/SCfExudaDeyhI=", + "dev": true + }, + "fined": { + "version": "https://registry.npmjs.org/fined/-/fined-1.0.2.tgz", + "integrity": "sha1-WyhCS3YNdZiWC374SA3/itNmDpc=", + "dev": true + }, + "fireworm": { + "version": "https://registry.npmjs.org/fireworm/-/fireworm-0.7.1.tgz", + "integrity": "sha1-zPIPeUHxCIg/zduZOD2+bhhhx1g=", + "dev": true, + "dependencies": { + "async": { + "version": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", + "dev": true + }, + "lodash.debounce": { + "version": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-3.1.1.tgz", + "integrity": "sha1-gSIRw3ipTMKdWqTjNGzwv846ffU=", + "dev": true + }, + "lodash.flatten": { + "version": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-3.0.2.tgz", + "integrity": "sha1-3hz1d1j49EeTGdNcPpzGDEUBk4w=", + "dev": true + } + } + }, + "first-chunk-stream": { + "version": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz", + "integrity": "sha1-Wb+1DNkF9g18OUzT2ayqtOatk04=", + "dev": true + }, + "flagged-respawn": { + "version": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-0.3.2.tgz", + "integrity": "sha1-/xke3c1wiKZ1smEP/8l2vpuAdLU=", + "dev": true + }, + "flat": { + "version": "https://registry.npmjs.org/flat/-/flat-1.0.0.tgz", + "integrity": "sha1-Ad/dW8vBScZrNe1AHh11PxqtjVk=" + }, + "flat-cache": { + "version": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.2.2.tgz", + "integrity": "sha1-+oZxTnLCHbiGAXYezy9VXRq8a5Y=" + }, + "flatten": { + "version": "https://registry.npmjs.org/flatten/-/flatten-0.0.1.tgz", + "integrity": "sha1-VURAdm2goNYDmZ9DNFP2wvxqdcE=" + }, + "flush-write-stream": { + "version": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.2.tgz", + "integrity": "sha1-yBuQ2HRnZvGmCaRoCZRsRd2K5Bc=" + }, + "for-each": { + "version": "https://registry.npmjs.org/for-each/-/for-each-0.3.2.tgz", + "integrity": "sha1-LEBFC5NI6X8oEyJZO6lnBLmr1NQ=" + }, + "for-in": { + "version": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true + }, + "foreach": { + "version": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" + }, + "forever-agent": { + "version": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "fork-stream": { + "version": "https://registry.npmjs.org/fork-stream/-/fork-stream-0.0.4.tgz", + "integrity": "sha1-24Sfznf2cIpfjzhq5TOgkHtUrnA=", + "dev": true + }, + "form-data": { + "version": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=" + }, + "formatio": { + "version": "https://registry.npmjs.org/formatio/-/formatio-1.1.1.tgz", + "integrity": "sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek=", + "dev": true + }, + "forwarded": { + "version": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz", + "integrity": "sha1-Ge+YdMSuHCl7zweP3mOgm2aoQ2M=" + }, + "fresh": { + "version": "https://registry.npmjs.org/fresh/-/fresh-0.5.0.tgz", + "integrity": "sha1-9HTKXmqSRtb9jglTz6m5yAWvp44=" + }, + "from": { + "version": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", + "dev": true + }, + "from2": { + "version": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=" + }, + "fs-exists-sync": { + "version": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", + "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=", + "dev": true + }, + "fs-extra": { + "version": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz", + "integrity": "sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=" + }, + "fs-promise": { + "version": "https://registry.npmjs.org/fs-promise/-/fs-promise-1.0.0.tgz", + "integrity": "sha1-QkakzUVJfS7Vfm5LIhZ9OGSyNnk=", + "dev": true, + "dependencies": { + "any-promise": { + "version": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", + "dev": true + }, + "fs-extra": { + "version": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", + "integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=", + "dev": true + } + } + }, + "fs.realpath": { + "version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.1.tgz", + "integrity": "sha1-8Z/Sj0Pur3YWgOUZogPE0LPTGv8=", + "dev": true, + "optional": true, + "dependencies": { + "abbrev": { + "version": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz", + "integrity": "sha1-0FVMIlZjbi9W58LlrRg/hZQo2B8=", + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true, + "optional": true + }, + "aproba": { + "version": "https://registry.npmjs.org/aproba/-/aproba-1.1.1.tgz", + "integrity": "sha1-ldNgDwdxCqDpKYxyatXs8urLq6s=", + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz", + "integrity": "sha1-gORw6VoIR5T+GJkmLFZnxuiN4bM=", + "dev": true, + "optional": true + }, + "asn1": { + "version": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true, + "optional": true + }, + "assert-plus": { + "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true, + "optional": true + }, + "asynckit": { + "version": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true, + "optional": true + }, + "aws-sign2": { + "version": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true, + "optional": true + }, + "aws4": { + "version": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", + "dev": true, + "optional": true + }, + "balanced-match": { + "version": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true + }, + "block-stream": { + "version": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true + }, + "boom": { + "version": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true + }, + "brace-expansion": { + "version": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", + "integrity": "sha1-cZfX6qm4fmSDkOph/GbIRCdCDfk=", + "dev": true + }, + "buffer-shims": { + "version": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", + "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=", + "dev": true + }, + "caseless": { + "version": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", + "dev": true, + "optional": true + }, + "chalk": { + "version": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "combined-stream": { + "version": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "dev": true + }, + "commander": { + "version": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "dev": true, + "optional": true + }, + "concat-map": { + "version": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "console-control-strings": { + "version": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "core-util-is": { + "version": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cryptiles": { + "version": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true, + "optional": true + }, + "dashdash": { + "version": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "optional": true, + "dependencies": { + "assert-plus": { + "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "debug": { + "version": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "optional": true + }, + "deep-extend": { + "version": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.1.tgz", + "integrity": "sha1-7+QRPQgIX05vlod1mBD4B0aeIlM=", + "dev": true, + "optional": true + }, + "delayed-stream": { + "version": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegates": { + "version": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true, + "optional": true + }, + "ecc-jsbn": { + "version": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true + }, + "escape-string-regexp": { + "version": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "optional": true + }, + "extend": { + "version": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz", + "integrity": "sha1-WkdDU7nzNT3dgXbf03uRyDpG8dQ=", + "dev": true, + "optional": true + }, + "extsprintf": { + "version": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", + "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=", + "dev": true + }, + "forever-agent": { + "version": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true, + "optional": true + }, + "form-data": { + "version": "https://registry.npmjs.org/form-data/-/form-data-2.1.2.tgz", + "integrity": "sha1-icNTQAi5fq2ky7FX1Y9vXfAl6uQ=", + "dev": true, + "optional": true + }, + "fs.realpath": { + "version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fstream": { + "version": "https://registry.npmjs.org/fstream/-/fstream-1.0.10.tgz", + "integrity": "sha1-YE6Kkv4m/9n2+uMDmdSYThqyKCI=", + "dev": true + }, + "fstream-ignore": { + "version": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", + "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", + "dev": true, + "optional": true + }, + "gauge": { + "version": "https://registry.npmjs.org/gauge/-/gauge-2.7.3.tgz", + "integrity": "sha1-HCOFX5YvF7OtPQ3HRD8wRULt/gk=", + "dev": true, + "optional": true + }, + "generate-function": { + "version": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", + "dev": true, + "optional": true + }, + "generate-object-property": { + "version": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "optional": true + }, + "getpass": { + "version": "https://registry.npmjs.org/getpass/-/getpass-0.1.6.tgz", + "integrity": "sha1-KD/9n8ElaECHUxHBtg6MQBhxEOY=", + "dev": true, + "optional": true, + "dependencies": { + "assert-plus": { + "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "glob": { + "version": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", + "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", + "dev": true + }, + "graceful-fs": { + "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "graceful-readlink": { + "version": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true, + "optional": true + }, + "har-validator": { + "version": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", + "dev": true, + "optional": true + }, + "has-ansi": { + "version": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "optional": true + }, + "has-unicode": { + "version": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true, + "optional": true + }, + "hawk": { + "version": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true, + "optional": true + }, + "hoek": { + "version": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "http-signature": { + "version": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true, + "optional": true + }, + "inflight": { + "version": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true + }, + "inherits": { + "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ini": { + "version": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", + "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=", + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true + }, + "is-my-json-valid": { + "version": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz", + "integrity": "sha1-k27do8o8IR/ZjzstPgjaQ/eykVs=", + "dev": true, + "optional": true + }, + "is-property": { + "version": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true, + "optional": true + }, + "is-typedarray": { + "version": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true, + "optional": true + }, + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isstream": { + "version": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true, + "optional": true + }, + "jodid25519": { + "version": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz", + "integrity": "sha1-BtSRIlUJNBlHfUJWM2BuDpB4KWc=", + "dev": true, + "optional": true + }, + "jsbn": { + "version": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "json-schema": { + "version": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true, + "optional": true + }, + "json-stringify-safe": { + "version": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true, + "optional": true + }, + "jsonpointer": { + "version": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true, + "optional": true + }, + "jsprim": { + "version": "https://registry.npmjs.org/jsprim/-/jsprim-1.3.1.tgz", + "integrity": "sha1-KnJW9wQSop7jZwqspiWZTE3P8lI=", + "dev": true, + "optional": true + }, + "mime-db": { + "version": "https://registry.npmjs.org/mime-db/-/mime-db-1.26.0.tgz", + "integrity": "sha1-6v/NDk/Gk1z4E02iRuLmw1MFrf8=", + "dev": true + }, + "mime-types": { + "version": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.14.tgz", + "integrity": "sha1-9+99l1g/yvO30oK2+LVnnaselO4=", + "dev": true + }, + "minimatch": { + "version": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha1-Kk5AkLlrLbBqnX3wEFWmKnfJt3Q=", + "dev": true + }, + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true + }, + "ms": { + "version": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true, + "optional": true + }, + "node-pre-gyp": { + "version": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.33.tgz", + "integrity": "sha1-ZArFUZj2qSWXLgwWxKwmoDTV7Mk=", + "dev": true, + "optional": true + }, + "nopt": { + "version": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "optional": true + }, + "npmlog": { + "version": "https://registry.npmjs.org/npmlog/-/npmlog-4.0.2.tgz", + "integrity": "sha1-0DlQ4OeM4VJ7om0qdZLpNIrD518=", + "dev": true, + "optional": true + }, + "number-is-nan": { + "version": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true, + "optional": true + }, + "object-assign": { + "version": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true, + "optional": true + }, + "once": { + "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true + }, + "path-is-absolute": { + "version": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "pinkie": { + "version": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true, + "optional": true + }, + "pinkie-promise": { + "version": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "punycode": { + "version": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true, + "optional": true + }, + "qs": { + "version": "https://registry.npmjs.org/qs/-/qs-6.3.1.tgz", + "integrity": "sha1-kYwLO802Z5dyuvE1say0wWUe150=", + "dev": true, + "optional": true + }, + "rc": { + "version": "https://registry.npmjs.org/rc/-/rc-1.1.7.tgz", + "integrity": "sha1-xepWS7B6/5/TpbMukGwdOmWUD+o=", + "dev": true, + "optional": true, + "dependencies": { + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.2.tgz", + "integrity": "sha1-qeb+w8fdqF+LsbO6cChgRVb8gl4=", + "dev": true, + "optional": true + }, + "request": { + "version": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", + "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", + "dev": true, + "optional": true + }, + "rimraf": { + "version": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", + "integrity": "sha1-loAAk8vxoMhr2VtGJUZ1NcKd+gQ=", + "dev": true + }, + "semver": { + "version": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true, + "optional": true + }, + "sntp": { + "version": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true, + "optional": true + }, + "sshpk": { + "version": "https://registry.npmjs.org/sshpk/-/sshpk-1.10.2.tgz", + "integrity": "sha1-1agEziJpVRVjjnmNviMnPeBwpfo=", + "dev": true, + "optional": true, + "dependencies": { + "assert-plus": { + "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "string-width": { + "version": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true + }, + "stringstream": { + "version": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "dev": true, + "optional": true + }, + "strip-ansi": { + "version": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true + }, + "strip-json-comments": { + "version": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true, + "optional": true + }, + "supports-color": { + "version": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true, + "optional": true + }, + "tar": { + "version": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "dev": true + }, + "tar-pack": { + "version": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.3.0.tgz", + "integrity": "sha1-MJMYFkGPVa/E0hd1r91nIM7kXa4=", + "dev": true, + "optional": true, + "dependencies": { + "once": { + "version": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=", + "dev": true, + "optional": true + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.5.tgz", + "integrity": "sha1-ZvqLcg4UOLNkaB8q0aY8YYRIydA=", + "dev": true, + "optional": true + } + } + }, + "tough-cookie": { + "version": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", + "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", + "dev": true, + "optional": true + }, + "tunnel-agent": { + "version": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", + "dev": true, + "optional": true + }, + "tweetnacl": { + "version": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "uid-number": { + "version": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", + "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=", + "dev": true, + "optional": true + }, + "util-deprecate": { + "version": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", + "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=", + "dev": true, + "optional": true + }, + "verror": { + "version": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", + "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", + "dev": true, + "optional": true + }, + "wide-align": { + "version": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.0.tgz", + "integrity": "sha1-QO3egCpx/qHwcNo+YtzaLnrdlq0=", + "dev": true, + "optional": true + }, + "wrappy": { + "version": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xtend": { + "version": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true, + "optional": true + } + } + }, + "function-bind": { + "version": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz", + "integrity": "sha1-FhdnFMgBeY5Ojyz391KUZ7tKV3E=" + }, + "function.prototype.name": { + "version": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.0.0.tgz", + "integrity": "sha1-X1I8pk5JGl+Vq6gMweORCAoUSC4=", + "dev": true + }, + "functional-red-black-tree": { + "version": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" + }, + "gauge": { + "version": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=" + }, + "generate-function": { + "version": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=" + }, + "generate-object-property": { + "version": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=" + }, + "get-caller-file": { + "version": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=" + }, + "get-stdin": { + "version": "https://registry.npmjs.org/get-stdin/-/get-stdin-3.0.2.tgz", + "integrity": "sha1-wc7SS5A5s43thb3xYeV3E7bdSr4=", + "dev": true + }, + "get-values": { + "version": "https://registry.npmjs.org/get-values/-/get-values-0.1.0.tgz", + "integrity": "sha1-OsA1tlpEkj012y/Ct7ojIrbD8p4=", + "dev": true + }, + "getpass": { + "version": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dependencies": { + "assert-plus": { + "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "github-from-package": { + "version": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" + }, + "gl-mat4": { + "version": "https://registry.npmjs.org/gl-mat4/-/gl-mat4-1.1.4.tgz", + "integrity": "sha1-HolbVYkuVqiWhnq9g30483oXgIY=" + }, + "gl-vec3": { + "version": "https://registry.npmjs.org/gl-vec3/-/gl-vec3-1.0.3.tgz", + "integrity": "sha1-EQ/Yl9Byn2OYMHOBVn0JRJQb8is=" + }, + "glob": { + "version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=" + }, + "glob-all": { + "version": "https://registry.npmjs.org/glob-all/-/glob-all-3.1.0.tgz", + "integrity": "sha1-iRPd+17hrHgSZWJBsD1SF8ZLAqs=", + "dev": true, + "dependencies": { + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-0.1.0.tgz", + "integrity": "sha1-md9lelJXTCHJBXSX33QnkLK0wN4=", + "dev": true + }, + "yargs": { + "version": "https://registry.npmjs.org/yargs/-/yargs-1.2.6.tgz", + "integrity": "sha1-nHtKgv1dWVsr8Xq23MQxNUMv40s=", + "dev": true + } + } + }, + "glob-base": { + "version": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true + }, + "glob-parent": { + "version": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true + }, + "glob-stream": { + "version": "https://registry.npmjs.org/glob-stream/-/glob-stream-5.3.5.tgz", + "integrity": "sha1-pVZlqajM3EGRWofHAeMtTgFvrSI=", + "dev": true, + "dependencies": { + "glob": { + "version": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true + }, + "glob-parent": { + "version": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true + }, + "is-extglob": { + "version": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true + }, + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "through2": { + "version": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true + } + } + }, + "glob-watcher": { + "version": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-3.2.0.tgz", + "integrity": "sha1-/8Gi09B3g7Zy9eIXmaTQs/7ZLa8=", + "dev": true + }, + "global": { + "version": "https://registry.npmjs.org/global/-/global-4.3.2.tgz", + "integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=" + }, + "global-modules": { + "version": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", + "integrity": "sha1-6lo77ULG1s6ZWk+KEmm12uIjgo0=", + "dev": true + }, + "global-prefix": { + "version": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", + "integrity": "sha1-jTvGuNo8qBEqFg2NSW/wRiv+948=", + "dev": true, + "dependencies": { + "which": { + "version": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", + "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", + "dev": true + } + } + }, + "globals": { + "version": "https://registry.npmjs.org/globals/-/globals-9.17.0.tgz", + "integrity": "sha1-DAymltm5u2lNLlRwvTd3fKrVAoY=" + }, + "globby": { + "version": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=" + }, + "glogg": { + "version": "https://registry.npmjs.org/glogg/-/glogg-1.0.0.tgz", + "integrity": "sha1-f+DxmfV6yQbPUS/urY+Q7kooT8U=" + }, + "graceful-fs": { + "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "graceful-readlink": { + "version": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, + "growl": { + "version": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", + "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "dev": true + }, + "growly": { + "version": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", + "dev": true + }, + "gulp": { + "version": "git://github.com/gulpjs/gulp.git#38246c3f8b6dbb8d4ef657183e92d90c8299e22f", + "dev": true, + "dependencies": { + "camelcase": { + "version": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + }, + "gulp-cli": { + "version": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-1.3.0.tgz", + "integrity": "sha1-pr+7i+NTQb4pCuRc0+QBBxIW7dQ=", + "dev": true + }, + "window-size": { + "version": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", + "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=", + "dev": true + }, + "yargs": { + "version": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", + "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=", + "dev": true + } + } + }, + "gulp-eslint": { + "version": "https://registry.npmjs.org/gulp-eslint/-/gulp-eslint-2.1.0.tgz", + "integrity": "sha1-P9X+C3I2ZR8VuNS/sUB8O3TQE2w=" + }, + "gulp-if": { + "version": "https://registry.npmjs.org/gulp-if/-/gulp-if-2.0.2.tgz", + "integrity": "sha1-pJe351cwBQQcqivIt92jyARE1ik=", + "dev": true + }, + "gulp-json-editor": { + "version": "https://registry.npmjs.org/gulp-json-editor/-/gulp-json-editor-2.2.1.tgz", + "integrity": "sha1-fE3XR36NBtxdxJwLgedFzbBPl7s=", + "dev": true, + "dependencies": { + "detect-indent": { + "version": "https://registry.npmjs.org/detect-indent/-/detect-indent-2.0.0.tgz", + "integrity": "sha1-cg/1Hk2Xt2iE9r9XKSNIsT396Tk=", + "dev": true + }, + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true + }, + "repeating": { + "version": "https://registry.npmjs.org/repeating/-/repeating-1.1.3.tgz", + "integrity": "sha1-PUEUIYh3U3SU+X93+Xhfq4EPpKw=", + "dev": true + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "through2": { + "version": "https://registry.npmjs.org/through2/-/through2-0.5.1.tgz", + "integrity": "sha1-390BLrnHAOIyP9M084rGIqs3Lac=", + "dev": true + }, + "xtend": { + "version": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", + "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=", + "dev": true + } + } + }, + "gulp-livereload": { + "version": "https://registry.npmjs.org/gulp-livereload/-/gulp-livereload-3.8.1.tgz", + "integrity": "sha1-APdEstdJ0+njdGWJyKRKysd5tQ8=", + "dev": true, + "dependencies": { + "ansi-regex": { + "version": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", + "dev": true + }, + "ansi-styles": { + "version": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", + "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", + "dev": true + }, + "chalk": { + "version": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", + "dev": true + }, + "has-ansi": { + "version": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", + "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", + "dev": true + }, + "lodash.assign": { + "version": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-3.2.0.tgz", + "integrity": "sha1-POnwI0tLIiPilrj6CsH+6OvKZPo=", + "dev": true + }, + "strip-ansi": { + "version": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", + "dev": true + }, + "supports-color": { + "version": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", + "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", + "dev": true + } + } + }, + "gulp-match": { + "version": "https://registry.npmjs.org/gulp-match/-/gulp-match-1.0.3.tgz", + "integrity": "sha1-kcfA1/Kb7NZgbVfYCn+Hdqh6uo4=", + "dev": true + }, + "gulp-replace": { + "version": "https://registry.npmjs.org/gulp-replace/-/gulp-replace-0.5.4.tgz", + "integrity": "sha1-aaZ5FLvRPFYr/xT1BKQDeWqg2qk=", + "dev": true + }, + "gulp-sourcemaps": { + "version": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-1.12.0.tgz", + "integrity": "sha1-eG+XyUoPloSSRl1wVY4EJCxnlZg=", + "dev": true, + "dependencies": { + "vinyl": { + "version": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "dev": true + } + } + }, + "gulp-util": { + "version": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz", + "integrity": "sha1-AFTh50RQLifATBh8PsxQXdVLu08=", + "dependencies": { + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "object-assign": { + "version": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=" + } + } + }, + "gulp-watch": { + "version": "https://registry.npmjs.org/gulp-watch/-/gulp-watch-4.3.11.tgz", + "integrity": "sha1-Fi/FY96fx3DpH5p845VVE6mhGMA=", + "dev": true, + "dependencies": { + "glob-parent": { + "version": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true + }, + "is-extglob": { + "version": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true + }, + "vinyl": { + "version": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "dev": true + } + } + }, + "gulp-zip": { + "version": "https://registry.npmjs.org/gulp-zip/-/gulp-zip-3.2.0.tgz", + "integrity": "sha1-69GY2ubcLV9E2BRWnI7EIRipPvk=", + "dev": true + }, + "gulplog": { + "version": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", + "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=" + }, + "handlebars": { + "version": "https://registry.npmjs.org/handlebars/-/handlebars-1.3.0.tgz", + "integrity": "sha1-npsTCpPjiUkTItl1zz7BgYw3zjQ=", + "dev": true, + "optional": true, + "dependencies": { + "optimist": { + "version": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", + "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=", + "dev": true, + "optional": true + } + } + }, + "har-schema": { + "version": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=" + }, + "har-validator": { + "version": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=" + }, + "has": { + "version": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", + "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=" + }, + "has-ansi": { + "version": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=" + }, + "has-binary": { + "version": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.7.tgz", + "integrity": "sha1-aOYesWIQyVRaClzOBqhzkS/h5ow=", + "dev": true, + "dependencies": { + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, + "has-color": { + "version": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", + "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=", + "dev": true + }, + "has-cors": { + "version": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=", + "dev": true + }, + "has-gulplog": { + "version": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", + "integrity": "sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4=" + }, + "has-unicode": { + "version": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "hash-base": { + "version": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", + "integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=" + }, + "hash.js": { + "version": "https://registry.npmjs.org/hash.js/-/hash.js-1.0.3.tgz", + "integrity": "sha1-EzL/ABVsCg/92CNgE9B7d6BFFXM=" + }, + "hat": { + "version": "https://registry.npmjs.org/hat/-/hat-0.0.3.tgz", + "integrity": "sha1-uwFKnmSzeIrtgAWRdBPU/z1QLYo=" + }, + "hawk": { + "version": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=" + }, + "hdkey": { + "version": "https://registry.npmjs.org/hdkey/-/hdkey-0.7.1.tgz", + "integrity": "sha1-yu5L6BqneSHpCbjSKN0PKayu5jI=" + }, + "hmac-drbg": { + "version": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=" + }, + "hoek": { + "version": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + }, + "hoist-non-react-statics": { + "version": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", + "integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs=" + }, + "home-or-tmp": { + "version": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=" + }, + "homedir-polyfill": { + "version": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", + "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", + "dev": true + }, + "hosted-git-info": { + "version": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.4.2.tgz", + "integrity": "sha1-AHa59GonBQbduq6lZJaJdGBhKmc=" + }, + "html-select": { + "version": "https://registry.npmjs.org/html-select/-/html-select-2.3.24.tgz", + "integrity": "sha1-Rq1tcS5zLPMcZznV0BEKX6vxdYU=", + "dev": true, + "dependencies": { + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "through2": { + "version": "https://registry.npmjs.org/through2/-/through2-1.1.1.tgz", + "integrity": "sha1-CEfLxESfNAVXTb3M2buEG4OsNUU=", + "dev": true + } + } + }, + "html-tokenize": { + "version": "https://registry.npmjs.org/html-tokenize/-/html-tokenize-1.2.5.tgz", + "integrity": "sha1-flupnstR75Buyaf83ubKMmfHiX4=", + "dev": true, + "dependencies": { + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + }, + "object-keys": { + "version": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=", + "dev": true + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "through2": { + "version": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz", + "integrity": "sha1-2/WGYDEVHsg1K7bE22SiKSqEC5s=", + "dev": true + }, + "xtend": { + "version": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", + "dev": true + } + } + }, + "htmlescape": { + "version": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", + "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=", + "dev": true + }, + "htmlparser2": { + "version": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", + "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=", + "dev": true + }, + "http-errors": { + "version": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.1.tgz", + "integrity": "sha1-X4uO2YrKVFZWv1cplzh/kEpyIlc=" + }, + "http-proxy": { + "version": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.16.2.tgz", + "integrity": "sha1-Bt/ykpUr9k2+hHH6nfcwZtTzd0I=", + "dev": true + }, + "http-signature": { + "version": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=" + }, + "https-browserify": { + "version": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.1.tgz", + "integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=", + "dev": true + }, + "i": { + "version": "https://registry.npmjs.org/i/-/i-0.3.5.tgz", + "integrity": "sha1-HSuFQVjsgWkRPGy39raAHpniEdU=", + "dev": true + }, + "iconv-lite": { + "version": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.17.tgz", + "integrity": "sha1-T9qjs4rLwsAxsEXQ7c3+HsqxjI0=" + }, + "idb-global": { + "version": "https://registry.npmjs.org/idb-global/-/idb-global-1.0.0.tgz", + "integrity": "sha1-srLkgDqO+mN2yTrzBW5qQ1atiW4=" + }, + "identicon.js": { + "version": "https://registry.npmjs.org/identicon.js/-/identicon.js-1.3.0.tgz", + "integrity": "sha1-e/uzhrd14HgalVV4urcegk5oaeA=" + }, + "idna-uts46": { + "version": "https://registry.npmjs.org/idna-uts46/-/idna-uts46-1.1.0.tgz", + "integrity": "sha1-vgmLK3wcq/vvh6i4D2JvrDc2auo=" + }, + "ieee754": { + "version": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=", + "dev": true + }, + "iframe": { + "version": "https://registry.npmjs.org/iframe/-/iframe-1.0.0.tgz", + "integrity": "sha1-WOdIIrF4oFedCc0WlkD7lTdHDvU=" + }, + "iframe-stream": { + "version": "https://registry.npmjs.org/iframe-stream/-/iframe-stream-1.0.2.tgz", + "integrity": "sha1-PH622TTnXX3V5l0l76XPR8sGPAs=" + }, + "ignore": { + "version": "https://registry.npmjs.org/ignore/-/ignore-3.3.3.tgz", + "integrity": "sha1-QyNS5XrM2HqzEQ6C0/6g5HgSFW0=" + }, + "ignorepatterns": { + "version": "https://registry.npmjs.org/ignorepatterns/-/ignorepatterns-1.1.0.tgz", + "integrity": "sha1-rI9DbyI5td+2bV8NOpBKh6xnzF4=", + "dev": true + }, + "immediate": { + "version": "https://registry.npmjs.org/immediate/-/immediate-3.2.3.tgz", + "integrity": "sha1-0UD6j2FGWb1lQSMwl92qwlzdmRw=" + }, + "imurmurhash": { + "version": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "in-publish": { + "version": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", + "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=" + }, + "indexof": { + "version": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, + "inflight": { + "version": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=" + }, + "inherits": { + "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", + "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=" + }, + "inject-css": { + "version": "https://registry.npmjs.org/inject-css/-/inject-css-0.1.1.tgz", + "integrity": "sha1-7z/8eOwCbJbiNV2g3zKRfjUmQVw=" + }, + "inline-source-map": { + "version": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", + "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=", + "dev": true + }, + "inquirer": { + "version": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", + "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=" + }, + "insert-module-globals": { + "version": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.0.1.tgz", + "integrity": "sha1-wDv04BywhtW15azorQr+eInWOMM=", + "dev": true, + "dependencies": { + "concat-stream": { + "version": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", + "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", + "dev": true + }, + "process": { + "version": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "interpret": { + "version": "https://registry.npmjs.org/interpret/-/interpret-1.0.3.tgz", + "integrity": "sha1-y8NcYu7uc/Gat7EKgBURQBr8D5A=", + "dev": true + }, + "invariant": { + "version": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", + "integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=" + }, + "invert-kv": { + "version": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, + "ipaddr.js": { + "version": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.3.0.tgz", + "integrity": "sha1-HgOlL9rYOou7KyXL9JmLTP/NPew=" + }, + "is-absolute": { + "version": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.2.6.tgz", + "integrity": "sha1-IN5p89uULvLYe5wto28XIjWxtes=", + "dev": true + }, + "is-arrayish": { + "version": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-binary-path": { + "version": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true + }, + "is-buffer": { + "version": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", + "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=", + "dev": true + }, + "is-builtin-module": { + "version": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=" + }, + "is-callable": { + "version": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", + "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=" + }, + "is-date-object": { + "version": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" + }, + "is-dotfile": { + "version": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true + }, + "is-extendable": { + "version": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-finite": { + "version": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=" + }, + "is-fn": { + "version": "https://registry.npmjs.org/is-fn/-/is-fn-1.0.0.tgz", + "integrity": "sha1-lUPV3nvPWwiiLsiiC65uKG1RDYw=" + }, + "is-fullwidth-code-point": { + "version": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=" + }, + "is-function": { + "version": "https://registry.npmjs.org/is-function/-/is-function-1.0.1.tgz", + "integrity": "sha1-Es+5i2W1fdPRk6MSH19uL0N2ArU=" + }, + "is-glob": { + "version": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true + }, + "is-hex-prefixed": { + "version": "https://registry.npmjs.org/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz", + "integrity": "sha1-fY035q135dEnFIkTxXPggtd39VQ=" + }, + "is-my-json-valid": { + "version": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz", + "integrity": "sha1-8Hndm/2uZe4gOKrorLyGqxCeNpM=" + }, + "is-number": { + "version": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true + }, + "is-path-cwd": { + "version": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=" + }, + "is-path-in-cwd": { + "version": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", + "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=" + }, + "is-path-inside": { + "version": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.0.tgz", + "integrity": "sha1-/AbloWg/vaE95mev9xe7wQpI838=" + }, + "is-plain-object": { + "version": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.3.tgz", + "integrity": "sha1-wVvz5LZrYtcu+vKSWEhmPsvGGbY=", + "dev": true, + "dependencies": { + "isobject": { + "version": "https://registry.npmjs.org/isobject/-/isobject-3.0.0.tgz", + "integrity": "sha1-OVZSF/NmF4nooKDAgNX35rxG4aA=", + "dev": true + } + } + }, + "is-posix-bracket": { + "version": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-property": { + "version": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" + }, + "is-regex": { + "version": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=" + }, + "is-relative": { + "version": "https://registry.npmjs.org/is-relative/-/is-relative-0.2.1.tgz", + "integrity": "sha1-0n9MfVFtF1+2ENuEu+7yPDvJeqU=", + "dev": true + }, + "is-resolvable": { + "version": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz", + "integrity": "sha1-jfV8YeouPFAUCNEA+wE8+NbgzGI=" + }, + "is-stream": { + "version": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-subset": { + "version": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", + "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=", + "dev": true + }, + "is-symbol": { + "version": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", + "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=" + }, + "is-type": { + "version": "https://registry.npmjs.org/is-type/-/is-type-0.0.1.tgz", + "integrity": "sha1-9lHYXDZdRJVdFKUdjXBh8/a0d5w=", + "dev": true + }, + "is-typedarray": { + "version": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-unc-path": { + "version": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-0.1.2.tgz", + "integrity": "sha1-arBTpyVzwQJQ/0FqOBTDUXivObk=", + "dev": true + }, + "is-utf8": { + "version": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" + }, + "is-valid-glob": { + "version": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-0.3.0.tgz", + "integrity": "sha1-1LVcafUYhvm2XHDWwmItN+KfSP4=", + "dev": true + }, + "is-windows": { + "version": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", + "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=", + "dev": true + }, + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true + }, + "isomorphic-fetch": { + "version": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=" + }, + "isstream": { + "version": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "istanbul": { + "version": "https://github.com/gotwarlost/istanbul/tarball/harmony", + "integrity": "sha1-Atq/03Q7aVlC0XCoCRcdOSRGYzE=", + "dev": true, + "optional": true, + "dependencies": { + "abbrev": { + "version": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "async": { + "version": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", + "dev": true, + "optional": true + }, + "escodegen": { + "version": "https://registry.npmjs.org/escodegen/-/escodegen-1.2.0.tgz", + "integrity": "sha1-Cd55Z3kcyVi3+Jot220jRRrzJ+E=", + "dev": true, + "optional": true, + "dependencies": { + "esprima": { + "version": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=", + "dev": true, + "optional": true + } + } + }, + "esprima": { + "version": "git://github.com/ariya/esprima.git#a65a3eb93b9a5dce9a1184ca2d1bd0b184c6b8fd", + "dev": true, + "optional": true + }, + "estraverse": { + "version": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz", + "integrity": "sha1-hno+jlip+EYYr7bC3bzZFrfLr3E=", + "dev": true, + "optional": true + }, + "esutils": { + "version": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz", + "integrity": "sha1-gVHTWOIMisx/t0XnRywAJf5JZXA=", + "dev": true, + "optional": true + }, + "mkdirp": { + "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", + "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=", + "dev": true, + "optional": true + }, + "nopt": { + "version": "https://registry.npmjs.org/nopt/-/nopt-2.2.1.tgz", + "integrity": "sha1-KqCbfRdoSHs7ianFqlIzW/8Lrqc=", + "dev": true, + "optional": true + }, + "resolve": { + "version": "https://registry.npmjs.org/resolve/-/resolve-0.6.3.tgz", + "integrity": "sha1-3ZV5gufnNt699TtYpN2RdUV13UY=", + "dev": true, + "optional": true + }, + "source-map": { + "version": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "dev": true, + "optional": true + } + } + }, + "istextorbinary": { + "version": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-1.0.2.tgz", + "integrity": "sha1-rOGTVNGpoBc+/rEITOD4ewrX3s8=", + "dev": true + }, + "jade": { + "version": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", + "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", + "dev": true, + "dependencies": { + "commander": { + "version": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", + "dev": true + }, + "mkdirp": { + "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", + "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=", + "dev": true + } + } + }, + "jazzicon": { + "version": "https://registry.npmjs.org/jazzicon/-/jazzicon-1.5.0.tgz", + "integrity": "sha1-1/NrUWAj2znubqwRf0BU6Te2Xpk=" + }, + "jodid25519": { + "version": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz", + "integrity": "sha1-BtSRIlUJNBlHfUJWM2BuDpB4KWc=", + "optional": true + }, + "js-beautify": { + "version": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.5.10.tgz", + "integrity": "sha1-TZU3FwJpk0SlFsomv1nwonu3Vxk=", + "dev": true + }, + "js-sha3": { + "version": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.3.1.tgz", + "integrity": "sha1-hhIoAhQvCChQKg0d7h2V4lO7AkM=" + }, + "js-tokens": { + "version": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.1.tgz", + "integrity": "sha1-COnxMkhKLEWjCQfp3E1VZ7fxFNc=" + }, + "js-yaml": { + "version": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.8.4.tgz", + "integrity": "sha1-UgtFZPhlc7qWZir4Woyvp7S1pvY=", + "dependencies": { + "esprima": { + "version": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" + } + } + }, + "jsbn": { + "version": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "jsdom": { + "version": "https://registry.npmjs.org/jsdom/-/jsdom-8.5.0.tgz", + "integrity": "sha1-1Nj12/J2hjW2KmKCO5R89wcevJg=", + "dev": true, + "dependencies": { + "acorn": { + "version": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", + "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=", + "dev": true + }, + "escodegen": { + "version": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true + }, + "esprima": { + "version": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "estraverse": { + "version": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "source-map": { + "version": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true + } + } + }, + "jsdom-global": { + "version": "https://registry.npmjs.org/jsdom-global/-/jsdom-global-1.7.0.tgz", + "integrity": "sha1-mWe0Cb5xXPf88Ev9N5RblFtJTVI=", + "dev": true + }, + "jsesc": { + "version": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + }, + "jshint-stylish": { + "version": "https://registry.npmjs.org/jshint-stylish/-/jshint-stylish-0.1.5.tgz", + "integrity": "sha1-1Btu744GpN37NlQL9lk/4xuYcjY=", + "dev": true, + "dependencies": { + "ansi-styles": { + "version": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", + "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=", + "dev": true + }, + "chalk": { + "version": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", + "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", + "dev": true + }, + "strip-ansi": { + "version": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", + "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=", + "dev": true + } + } + }, + "json-rpc-error": { + "version": "https://registry.npmjs.org/json-rpc-error/-/json-rpc-error-2.0.0.tgz", + "integrity": "sha1-p6+cICg4tekFxyUOVH8a/3cligI=" + }, + "json-rpc-random-id": { + "version": "https://registry.npmjs.org/json-rpc-random-id/-/json-rpc-random-id-1.0.1.tgz", + "integrity": "sha1-uknZat7RRE27jaPSA3SKy7zeyMg=" + }, + "json-schema": { + "version": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-stable-stringify": { + "version": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=" + }, + "json-stringify-safe": { + "version": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "json3": { + "version": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "json5": { + "version": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" + }, + "jsonfile": { + "version": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=" + }, + "jsonify": { + "version": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, + "jsonparse": { + "version": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true + }, + "jsonpointer": { + "version": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=" + }, + "JSONStream": { + "version": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz", + "integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=", + "dev": true + }, + "jsprim": { + "version": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", + "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=", + "dependencies": { + "assert-plus": { + "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "keccak": { + "version": "https://registry.npmjs.org/keccak/-/keccak-1.2.0.tgz", + "integrity": "sha1-tTYY/HlhtkL25z8VRu7DMp9+/+A=" + }, + "keccakjs": { + "version": "https://registry.npmjs.org/keccakjs/-/keccakjs-0.2.1.tgz", + "integrity": "sha1-HWM6+QfvMFu/ny+mFtVsRFYd+k0=" + }, + "kind-of": { + "version": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true + }, + "klaw": { + "version": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", + "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=" + }, + "labeled-stream-splicer": { + "version": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.0.tgz", + "integrity": "sha1-pS4dE4AkwAuGscDJH2d5GLiuClk=", + "dev": true, + "dependencies": { + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "stream-splicer": { + "version": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.0.tgz", + "integrity": "sha1-G2O+Q4oTPktnHMGTUZdgAXWRDYM=", + "dev": true + } + } + }, + "last-run": { + "version": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", + "integrity": "sha1-RblpQsF7HHnHchmCWbqUO+v4yls=", + "dev": true + }, + "lazy-debug-legacy": { + "version": "https://registry.npmjs.org/lazy-debug-legacy/-/lazy-debug-legacy-0.0.1.tgz", + "integrity": "sha1-U3cWwHduTPeePtG2IfdljCkRsbE=", + "dev": true + }, + "lazystream": { + "version": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "dev": true + }, + "lcid": { + "version": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=" + }, + "leftpad": { + "version": "https://registry.npmjs.org/leftpad/-/leftpad-0.0.0.tgz", + "integrity": "sha1-Agya0HhyFroPMNedR5tLNV19OcM=", + "dev": true + }, + "level-codec": { + "version": "https://registry.npmjs.org/level-codec/-/level-codec-6.1.0.tgz", + "integrity": "sha1-9d8KmVgvdtrEOFUVGrb05NDWAEU=" + }, + "level-errors": { + "version": "https://registry.npmjs.org/level-errors/-/level-errors-1.0.4.tgz", + "integrity": "sha1-NYXmI5dMc3qTdVSSpDwCZ82kQl8=" + }, + "level-iterator-stream": { + "version": "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-1.3.1.tgz", + "integrity": "sha1-5Dt4sagUPm+pek9IXrjqUwNS8u0=", + "dependencies": { + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=" + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "level-ws": { + "version": "https://registry.npmjs.org/level-ws/-/level-ws-0.0.0.tgz", + "integrity": "sha1-Ny5RIXeSSgBCSwtDrvK7QkltIos=", + "dependencies": { + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "object-keys": { + "version": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=" + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=" + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "xtend": { + "version": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=" + } + } + }, + "levelup": { + "version": "https://registry.npmjs.org/levelup/-/levelup-1.3.8.tgz", + "integrity": "sha1-+0QsSI776hBD9+uZKaeSp0+9HaY=", + "dependencies": { + "semver": { + "version": "https://registry.npmjs.org/semver/-/semver-5.1.1.tgz", + "integrity": "sha1-oykqNz5vPgeY2gsgZBuanFvEfhk=" + } + } + }, + "levn": { + "version": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=" + }, + "lexical-scope": { + "version": "https://registry.npmjs.org/lexical-scope/-/lexical-scope-1.2.0.tgz", + "integrity": "sha1-/Ope3HBKSzqHls3KQZw6CvryLfQ=", + "dev": true + }, + "liftoff": { + "version": "https://registry.npmjs.org/liftoff/-/liftoff-2.3.0.tgz", + "integrity": "sha1-qY8v9nGD2Lp8+soQVIvX/wVQs4U=", + "dev": true + }, + "livereload-js": { + "version": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.2.2.tgz", + "integrity": "sha1-bIclfmSKtHW8JOoldFftzB+NC8I=", + "dev": true + }, + "load-json-file": { + "version": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=" + }, + "lodash": { + "version": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + }, + "lodash-es": { + "version": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.4.tgz", + "integrity": "sha1-3MHXVS4VCgZABzupyzHXDwMpUOc=" + }, + "lodash._baseassign": { + "version": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true + }, + "lodash._basecopy": { + "version": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=" + }, + "lodash._baseflatten": { + "version": "https://registry.npmjs.org/lodash._baseflatten/-/lodash._baseflatten-3.1.4.tgz", + "integrity": "sha1-B3D/gBMa9uNPO1EXlqe6UhTmX/c=", + "dev": true + }, + "lodash._basetostring": { + "version": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz", + "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=" + }, + "lodash._basevalues": { + "version": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz", + "integrity": "sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc=" + }, + "lodash._bindcallback": { + "version": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", + "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=", + "dev": true + }, + "lodash._createassigner": { + "version": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz", + "integrity": "sha1-g4pbri/aymOsIt7o4Z+k5taXCxE=", + "dev": true + }, + "lodash._getnative": { + "version": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=" + }, + "lodash._isiterateecall": { + "version": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=" + }, + "lodash._reescape": { + "version": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz", + "integrity": "sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo=" + }, + "lodash._reevaluate": { + "version": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz", + "integrity": "sha1-WLx0xAZklTrgsSTYBpltrKQx4u0=" + }, + "lodash._reinterpolate": { + "version": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" + }, + "lodash._root": { + "version": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", + "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=" + }, + "lodash.assign": { + "version": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=" + }, + "lodash.assignin": { + "version": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", + "integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI=", + "dev": true + }, + "lodash.assignwith": { + "version": "https://registry.npmjs.org/lodash.assignwith/-/lodash.assignwith-4.2.0.tgz", + "integrity": "sha1-EnqX8CrcQXUalU0ksN4X4QDgOOs=", + "dev": true + }, + "lodash.bind": { + "version": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", + "integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=", + "dev": true + }, + "lodash.clonedeep": { + "version": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, + "lodash.debounce": { + "version": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", + "dev": true + }, + "lodash.defaults": { + "version": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=", + "dev": true + }, + "lodash.escape": { + "version": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", + "integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=" + }, + "lodash.filter": { + "version": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", + "integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=", + "dev": true + }, + "lodash.find": { + "version": "https://registry.npmjs.org/lodash.find/-/lodash.find-4.6.0.tgz", + "integrity": "sha1-ywcE1Hq3F4n/oN6Ll92Sb7iLE7E=", + "dev": true + }, + "lodash.flatten": { + "version": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", + "dev": true + }, + "lodash.foreach": { + "version": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=", + "dev": true + }, + "lodash.isarguments": { + "version": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=" + }, + "lodash.isarray": { + "version": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=" + }, + "lodash.isempty": { + "version": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", + "integrity": "sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=", + "dev": true + }, + "lodash.isequal": { + "version": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", + "dev": true + }, + "lodash.isfunction": { + "version": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.8.tgz", + "integrity": "sha1-TbcJ/IG8So/XEnpFilNGxc3OLGs=", + "dev": true + }, + "lodash.isplainobject": { + "version": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=", + "dev": true + }, + "lodash.keys": { + "version": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=" + }, + "lodash.map": { + "version": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=", + "dev": true + }, + "lodash.mapvalues": { + "version": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", + "integrity": "sha1-G6+lAF3p3W9PJmaMMMo3IwzJaJw=", + "dev": true + }, + "lodash.memoize": { + "version": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", + "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=", + "dev": true + }, + "lodash.merge": { + "version": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.0.tgz", + "integrity": "sha1-aYhLoUSsM/5plzemCG3v+t0PicU=", + "dev": true + }, + "lodash.pick": { + "version": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=", + "dev": true + }, + "lodash.pickby": { + "version": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz", + "integrity": "sha1-feoh2MGNdwOifHBMFdO4SmfjOv8=", + "dev": true + }, + "lodash.reduce": { + "version": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=", + "dev": true + }, + "lodash.reject": { + "version": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", + "integrity": "sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=", + "dev": true + }, + "lodash.restparam": { + "version": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", + "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=" + }, + "lodash.some": { + "version": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=", + "dev": true + }, + "lodash.sortby": { + "version": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, + "lodash.template": { + "version": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", + "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=" + }, + "lodash.templatesettings": { + "version": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz", + "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=" + }, + "lodash.uniqby": { + "version": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", + "integrity": "sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI=", + "dev": true + }, + "loglevel": { + "version": "https://registry.npmjs.org/loglevel/-/loglevel-1.4.1.tgz", + "integrity": "sha1-lbOD+Ro8J1b9SrCTZn5DCRYfK80=" + }, + "lolex": { + "version": "https://registry.npmjs.org/lolex/-/lolex-1.3.2.tgz", + "integrity": "sha1-fD2mL/yzDw9agKJWbKJORdigHzE=", + "dev": true + }, + "loose-envify": { + "version": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=" + }, + "lru-cache": { + "version": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "ltgt": { + "version": "https://registry.npmjs.org/ltgt/-/ltgt-2.1.3.tgz", + "integrity": "sha1-EIUaBtmWS5cReEQcI8nlJpjuzjQ=" + }, + "make-iterator": { + "version": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.0.tgz", + "integrity": "sha1-V7713IXSOSO6I3ZzJNjo+PPZaUs=", + "dev": true + }, + "map-async": { + "version": "https://registry.npmjs.org/map-async/-/map-async-0.1.1.tgz", + "integrity": "sha1-yJfARJ+Fhkx0taPxlu20IVZDF0U=" + }, + "map-cache": { + "version": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-stream": { + "version": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", + "dev": true + }, + "matchdep": { + "version": "https://registry.npmjs.org/matchdep/-/matchdep-1.0.1.tgz", + "integrity": "sha1-pXozgESR+64girqPaDgEN6vC3KU=", + "dev": true, + "dependencies": { + "findup-sync": { + "version": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz", + "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=", + "dev": true + }, + "glob": { + "version": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true + } + } + }, + "mdurl": { + "version": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" + }, + "media-typer": { + "version": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "memdown": { + "version": "https://registry.npmjs.org/memdown/-/memdown-1.2.4.tgz", + "integrity": "sha1-zZo0qvB01TRFonEQjrS43U7A8n8=" + }, + "memorystream": { + "version": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=" + }, + "menu-droppo": { + "version": "https://registry.npmjs.org/menu-droppo/-/menu-droppo-1.1.5.tgz", + "integrity": "sha1-qHqOfjcA7AK+A1+f3B2siTVFCVQ=" + }, + "merge-descriptors": { + "version": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "merge-stream": { + "version": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", + "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", + "dev": true + }, + "merkle-patricia-tree": { + "version": "https://registry.npmjs.org/merkle-patricia-tree/-/merkle-patricia-tree-2.1.2.tgz", + "integrity": "sha1-ckSD1Ut1YxpI/t2lXhFAUXBqcpE=", + "dependencies": { + "ethereumjs-util": { + "version": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-4.5.0.tgz", + "integrity": "sha1-PpQosxfuvaPXJg2FT93alUsfG8Y=" + } + } + }, + "mersenne-twister": { + "version": "https://registry.npmjs.org/mersenne-twister/-/mersenne-twister-1.1.0.tgz", + "integrity": "sha1-+RZhjuQ9cXnvz2Qb7EUx65Zwl4o=" + }, + "metamask-logo": { + "version": "https://registry.npmjs.org/metamask-logo/-/metamask-logo-2.1.3.tgz", + "integrity": "sha1-F1zleuUMc0Szsdwy0v0LCOOXj9A=" + }, + "methods": { + "version": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "micromatch": { + "version": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true + }, + "miller-rabin": { + "version": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.0.tgz", + "integrity": "sha1-SmL7HUKTPAVYOYL0xxb2+55sbT0=", + "dev": true + }, + "mime": { + "version": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", + "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=" + }, + "mime-db": { + "version": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", + "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=" + }, + "mime-types": { + "version": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", + "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=" + }, + "min-document": { + "version": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=" + }, + "mini-lr": { + "version": "https://registry.npmjs.org/mini-lr/-/mini-lr-0.1.9.tgz", + "integrity": "sha1-AhmdJzR5U9H9HW297UJh8Yey0PY=", + "dev": true, + "dependencies": { + "qs": { + "version": "https://registry.npmjs.org/qs/-/qs-2.2.5.tgz", + "integrity": "sha1-EIirr53MCuWuRbcJ5sa1iIsjkjw=", + "dev": true + } + } + }, + "minimalistic-assert": { + "version": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", + "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=" + }, + "minimalistic-crypto-utils": { + "version": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" + }, + "minimatch": { + "version": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=" + }, + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.5.tgz", + "integrity": "sha1-16oye87PUY+RBqxrjwA/o7zqhWY=" + }, + "mississippi": { + "version": "https://registry.npmjs.org/mississippi/-/mississippi-1.3.0.tgz", + "integrity": "sha1-0gFYPrEjJ+PFwWQqQEqcrPlONPU=" + }, + "mkdirp": { + "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dependencies": { + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } + } + }, + "mocha": { + "version": "https://registry.npmjs.org/mocha/-/mocha-2.5.3.tgz", + "integrity": "sha1-FhvlvetJZ3HrmzV0UFC2IrWu/Fg=", + "dev": true, + "dependencies": { + "debug": { + "version": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true + }, + "escape-string-regexp": { + "version": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz", + "integrity": "sha1-Tbwv5nTnGUnK8/smlc5/LcHZqNE=", + "dev": true + }, + "glob": { + "version": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "dev": true + }, + "minimatch": { + "version": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "dev": true + }, + "ms": { + "version": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "supports-color": { + "version": "https://registry.npmjs.org/supports-color/-/supports-color-1.2.0.tgz", + "integrity": "sha1-/x7R5hFp0Gs88tWI4YixjYhH4X4=", + "dev": true + } + } + }, + "mocha-eslint": { + "version": "https://registry.npmjs.org/mocha-eslint/-/mocha-eslint-2.1.1.tgz", + "integrity": "sha1-S0drRAPwPXANmAEMsxbasCqeFuA=", + "dev": true + }, + "mocha-jsdom": { + "version": "https://registry.npmjs.org/mocha-jsdom/-/mocha-jsdom-1.1.0.tgz", + "integrity": "sha1-4VdvvQYBzInTWKIToOVYXRt8egE=", + "dev": true + }, + "mocha-sinon": { + "version": "https://registry.npmjs.org/mocha-sinon/-/mocha-sinon-1.2.0.tgz", + "integrity": "sha1-lfA0qNreTalmoPOvyJwSbYtYkSM=", + "dev": true + }, + "module-deps": { + "version": "https://registry.npmjs.org/module-deps/-/module-deps-4.1.1.tgz", + "integrity": "sha1-IyFYM/HaE/1gbMuAh7RIUty4If0=", + "dev": true, + "dependencies": { + "concat-stream": { + "version": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", + "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", + "dev": true, + "dependencies": { + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true + } + } + }, + "duplexer2": { + "version": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "dev": true + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "ms": { + "version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "multipipe": { + "version": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", + "integrity": "sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s=" + }, + "multiplex": { + "version": "https://registry.npmjs.org/multiplex/-/multiplex-6.7.0.tgz", + "integrity": "sha1-/3Pk5AB5FwxEQtFgllZY+N75YMI=" + }, + "mustache": { + "version": "https://registry.npmjs.org/mustache/-/mustache-2.3.0.tgz", + "integrity": "sha1-QCj3d4sXcIpImTCm5SrDvKDaQdA=", + "dev": true + }, + "mute-stdout": { + "version": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.0.tgz", + "integrity": "sha1-WzLqB+tDyd7WEwQ0z5JvRrKn/U0=", + "dev": true + }, + "mute-stream": { + "version": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", + "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=" + }, + "mz": { + "version": "https://registry.npmjs.org/mz/-/mz-2.6.0.tgz", + "integrity": "sha1-yLhSHZWN8KTydoAl22nHGe5O8c4=", + "dev": true, + "dependencies": { + "any-promise": { + "version": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", + "dev": true + } + } + }, + "nan": { + "version": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz", + "integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=" + }, + "ncp": { + "version": "https://registry.npmjs.org/ncp/-/ncp-1.0.1.tgz", + "integrity": "sha1-0VNn5cuHQyuhF9K/gP30Wuz7QkY=", + "dev": true + }, + "negotiator": { + "version": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "next-tick": { + "version": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", + "dev": true + }, + "nock": { + "version": "https://registry.npmjs.org/nock/-/nock-8.2.1.tgz", + "integrity": "sha1-ZMxl4b3TiT9Yy6fhq/3Dj0DwNko=", + "dev": true, + "dependencies": { + "lodash": { + "version": "https://registry.npmjs.org/lodash/-/lodash-4.9.0.tgz", + "integrity": "sha1-TCDXQvA86F3HAODderm8q4Xm/BQ=", + "dev": true + } + } + }, + "node-abi": { + "version": "https://registry.npmjs.org/node-abi/-/node-abi-2.0.3.tgz", + "integrity": "sha1-DKZ+XmZ7jhNDVJyhcVOoFdC7/ao=" + }, + "node-fetch": { + "version": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.0.tgz", + "integrity": "sha1-P/bFZUT5t/sAaCM4u1Xub1SooO8=" + }, + "node-notifier": { + "version": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.1.2.tgz", + "integrity": "sha1-L6nhJgX6EACdRFSdb82KY93g5P8=", + "dev": true, + "dependencies": { + "which": { + "version": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", + "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", + "dev": true + } + } + }, + "noop-logger": { + "version": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", + "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=" + }, + "nopt": { + "version": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true + }, + "normalize-package-data": { + "version": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.3.8.tgz", + "integrity": "sha1-2Bntoqne29H/pWPqQHHZNngilbs=" + }, + "normalize-path": { + "version": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true + }, + "now-and-later": { + "version": "https://registry.npmjs.org/now-and-later/-/now-and-later-1.0.0.tgz", + "integrity": "sha1-I+eYzKrw6Ky+8Gh/gghidHRuCJM=", + "dev": true + }, + "npmlog": { + "version": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.0.tgz", + "integrity": "sha1-3Fm+6F9k8A7UJO+yrweD3yXRwLU=" + }, + "nth-check": { + "version": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", + "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", + "dev": true + }, + "number-is-nan": { + "version": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "number-to-bn": { + "version": "https://registry.npmjs.org/number-to-bn/-/number-to-bn-1.7.0.tgz", + "integrity": "sha1-uzYjWS9+X54AMLGXe9QaDFP+HqA=" + }, + "nwmatcher": { + "version": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.4.0.tgz", + "integrity": "sha1-tDiTYhcOfvl5jDx3FtgOvAEG/M8=", + "dev": true + }, + "oauth-sign": { + "version": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "object-assign": { + "version": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-component": { + "version": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", + "dev": true + }, + "object-inspect": { + "version": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.2.2.tgz", + "integrity": "sha1-yCEV5PzIiK6hTWTCLk8X9qcNXlo=" + }, + "object-is": { + "version": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", + "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=", + "dev": true + }, + "object-keys": { + "version": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", + "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=" + }, + "object.assign": { + "version": "https://registry.npmjs.org/object.assign/-/object.assign-4.0.4.tgz", + "integrity": "sha1-scnMBE7xuf5jYG/BQau7MuFHMMw=", + "dev": true + }, + "object.defaults": { + "version": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "dev": true, + "dependencies": { + "for-own": { + "version": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true + }, + "isobject": { + "version": "https://registry.npmjs.org/isobject/-/isobject-3.0.0.tgz", + "integrity": "sha1-OVZSF/NmF4nooKDAgNX35rxG4aA=", + "dev": true + } + } + }, + "object.entries": { + "version": "https://registry.npmjs.org/object.entries/-/object.entries-1.0.4.tgz", + "integrity": "sha1-G/mk3SKI9bM/Opk9JXZh8F0WGl8=", + "dev": true + }, + "object.omit": { + "version": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true + }, + "object.reduce": { + "version": "https://registry.npmjs.org/object.reduce/-/object.reduce-0.1.7.tgz", + "integrity": "sha1-0YDoT3LSGDSK9FNStVFlJGuVBG0=", + "dev": true + }, + "object.values": { + "version": "https://registry.npmjs.org/object.values/-/object.values-1.0.4.tgz", + "integrity": "sha1-5STaCbT2b/Bd9FdUbscqyZ8TBpo=", + "dev": true + }, + "obs-store": { + "version": "https://registry.npmjs.org/obs-store/-/obs-store-2.3.2.tgz", + "integrity": "sha1-JA0Ga1zNZMhj3ob0sOvVH4MQglY=" + }, + "on-finished": { + "version": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=" + }, + "once": { + "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=" + }, + "onetime": { + "version": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=" + }, + "open": { + "version": "https://registry.npmjs.org/open/-/open-0.0.5.tgz", + "integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw=", + "dev": true + }, + "opener": { + "version": "https://registry.npmjs.org/opener/-/opener-1.4.3.tgz", + "integrity": "sha1-XG2ixdflgx6P+jlklQ+NZnSskLg=" + }, + "optimist": { + "version": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=" + }, + "optionator": { + "version": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dependencies": { + "wordwrap": { + "version": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + } + } + }, + "options": { + "version": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", + "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=", + "dev": true + }, + "ordered-read-streams": { + "version": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz", + "integrity": "sha1-cTfmmzKYuzQiR6G77jiByA4v14s=", + "dev": true + }, + "os-browserify": { + "version": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.1.2.tgz", + "integrity": "sha1-ScoCk+CxlZCl9d4Qx/JlphfY/lQ=", + "dev": true + }, + "os-homedir": { + "version": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-locale": { + "version": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=" + }, + "os-tmpdir": { + "version": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "outpipe": { + "version": "https://registry.npmjs.org/outpipe/-/outpipe-1.1.1.tgz", + "integrity": "sha1-UM+GFjZeh+Ax4ppeyTOaPaRyX6I=", + "dev": true + }, + "pako": { + "version": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=", + "dev": true + }, + "parallel-transform": { + "version": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", + "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=" + }, + "parents": { + "version": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", + "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=", + "dev": true + }, + "parse-asn1": { + "version": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.0.tgz", + "integrity": "sha1-N8T5t+06tlx0gXtfJICTf7+XxxI=", + "dev": true + }, + "parse-filepath": { + "version": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.1.tgz", + "integrity": "sha1-FZ1hVdQ5BNFsEO9piRHaHpGWm3M=", + "dev": true + }, + "parse-glob": { + "version": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true + }, + "parse-headers": { + "version": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.1.tgz", + "integrity": "sha1-aug6eqJanZtwCswoaYzR8e1+lTY=" + }, + "parse-json": { + "version": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=" + }, + "parse-passwd": { + "version": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true + }, + "parse5": { + "version": "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz", + "integrity": "sha1-m387DeMr543CQBsXVzzK8Pb1nZQ=", + "dev": true + }, + "parsejson": { + "version": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.3.tgz", + "integrity": "sha1-q343WfIJ7OmUN5c/fQ8fZK4OZKs=", + "dev": true + }, + "parseqs": { + "version": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "dev": true + }, + "parseuri": { + "version": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "dev": true + }, + "parseurl": { + "version": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz", + "integrity": "sha1-yKuMkiO6NIiKpkopeyiFO+wY2lY=" + }, + "pascalcase": { + "version": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" + }, + "path-browserify": { + "version": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", + "dev": true + }, + "path-dirname": { + "version": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=" + }, + "path-is-absolute": { + "version": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-is-inside": { + "version": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" + }, + "path-platform": { + "version": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", + "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=", + "dev": true + }, + "path-root": { + "version": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "dev": true + }, + "path-root-regex": { + "version": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", + "dev": true + }, + "path-to-regexp": { + "version": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "path-type": { + "version": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=" + }, + "pause-stream": { + "version": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", + "dev": true + }, + "pbkdf2": { + "version": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.12.tgz", + "integrity": "sha1-vjZ4XFBn6kjYBv+SMojF91C2uKI=" + }, + "performance-now": { + "version": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" + }, + "pify": { + "version": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "ping-pong-stream": { + "version": "https://registry.npmjs.org/ping-pong-stream/-/ping-pong-stream-1.0.0.tgz", + "integrity": "sha1-TF6wm6atsCGInawNyr+45XcGhUo=" + }, + "pinkie": { + "version": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=" + }, + "pkginfo": { + "version": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.4.0.tgz", + "integrity": "sha1-NJ27f/04CB/K3AhT32h/DHdEzWU=", + "dev": true + }, + "plucker": { + "version": "https://registry.npmjs.org/plucker/-/plucker-0.0.0.tgz", + "integrity": "sha1-L/ok4Dqyz/pOda3B33DyViPEXQk=" + }, + "pluralize": { + "version": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", + "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=" + }, + "pojo-migrator": { + "version": "https://registry.npmjs.org/pojo-migrator/-/pojo-migrator-2.1.0.tgz", + "integrity": "sha1-PCo7n4C6Wp+367kh0zRNtO+l9mk=" + }, + "polyfill-crypto.getrandomvalues": { + "version": "https://registry.npmjs.org/polyfill-crypto.getrandomvalues/-/polyfill-crypto.getrandomvalues-1.0.0.tgz", + "integrity": "sha1-XJVgKXbrthVbFjy2XXe57t47YaQ=" + }, + "portfinder": { + "version": "https://registry.npmjs.org/portfinder/-/portfinder-0.2.1.tgz", + "integrity": "sha1-srmwFk+eF/o6nH2yME0KdRQMca0=", + "dev": true, + "dependencies": { + "mkdirp": { + "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.0.7.tgz", + "integrity": "sha1-2JtPDkw+XlylQjWTFnXglP4aUHI=", + "dev": true + } + } + }, + "post-message-stream": { + "version": "https://registry.npmjs.org/post-message-stream/-/post-message-stream-1.0.0.tgz", + "integrity": "sha1-UO/gVjKuQza1HpSjFuVS6fFtqpw=" + }, + "prebuild-install": { + "version": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-2.1.2.tgz", + "integrity": "sha1-2a4MqFMw4Dli2TKS+VqLRMLr9QU=", + "dependencies": { + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "prelude-ls": { + "version": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, + "preserve": { + "version": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "pretty-bytes": { + "version": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-0.1.2.tgz", + "integrity": "sha1-zZApTVihyk6KXQ+5yCJZmIgazwA=", + "dev": true + }, + "pretty-hrtime": { + "version": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", + "dev": true + }, + "printf": { + "version": "https://registry.npmjs.org/printf/-/printf-0.2.5.tgz", + "integrity": "sha1-xDjKLKM+OSdnHbSracDlL5NqTw8=", + "dev": true + }, + "private": { + "version": "https://registry.npmjs.org/private/-/private-0.1.7.tgz", + "integrity": "sha1-aM5eih7woju1cMwoU3tTMqumPvE=" + }, + "process": { + "version": "https://registry.npmjs.org/process/-/process-0.5.2.tgz", + "integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=" + }, + "process-nextick-args": { + "version": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "progress": { + "version": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=" + }, + "promise": { + "version": "https://registry.npmjs.org/promise/-/promise-7.1.1.tgz", + "integrity": "sha1-SJZUxpJha4qlWwck+oCbt9tJxb8=" + }, + "promise-filter": { + "version": "https://registry.npmjs.org/promise-filter/-/promise-filter-1.1.0.tgz", + "integrity": "sha1-fsPOmQyGfMud6GONvRnuF6UqS1k=" + }, + "promise-to-callback": { + "version": "https://registry.npmjs.org/promise-to-callback/-/promise-to-callback-1.0.0.tgz", + "integrity": "sha1-XSp0kBC/tn2WNZj805YHRqaP7vc=" + }, + "prompt": { + "version": "https://registry.npmjs.org/prompt/-/prompt-1.0.0.tgz", + "integrity": "sha1-jlcSPDlquYiJf7Mn/Trtw+c15P4=", + "dev": true + }, + "prop-types": { + "version": "https://registry.npmjs.org/prop-types/-/prop-types-15.5.10.tgz", + "integrity": "sha1-J5ffwxJhguOpXj37suiT3ddFYVQ=" + }, + "propagate": { + "version": "https://registry.npmjs.org/propagate/-/propagate-0.4.0.tgz", + "integrity": "sha1-8/zKCm/gZzanulcpZgaWF8EwtIE=", + "dev": true + }, + "proto-list": { + "version": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", + "dev": true + }, + "proxy-addr": { + "version": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.1.4.tgz", + "integrity": "sha1-J+VF9pYKRKYn2bREZ+NcG2tM4vM=" + }, + "prr": { + "version": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" + }, + "pseudomap": { + "version": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "public-encrypt": { + "version": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz", + "integrity": "sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=", + "dev": true + }, + "pump": { + "version": "https://registry.npmjs.org/pump/-/pump-1.0.2.tgz", + "integrity": "sha1-Oz7mUS+U8OV1U4wXmV+fFpkKXVE=" + }, + "pumpify": { + "version": "https://registry.npmjs.org/pumpify/-/pumpify-1.3.5.tgz", + "integrity": "sha1-G2ccYZlAq8rqwK0OOjwWS+dgmTs=" + }, + "punycode": { + "version": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz", + "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0=" + }, + "qrcode-npm": { + "version": "https://registry.npmjs.org/qrcode-npm/-/qrcode-npm-0.0.3.tgz", + "integrity": "sha1-d+5vvvqcDyn6CdTRUggHxqYEK5o=" + }, + "qs": { + "version": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" + }, + "querystring": { + "version": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "qunit": { + "version": "https://registry.npmjs.org/qunit/-/qunit-0.9.3.tgz", + "integrity": "sha1-qR8HM06FR7rbqmrhhBwSaZC4EpU=", + "dev": true, + "dependencies": { + "co": { + "version": "https://registry.npmjs.org/co/-/co-3.1.0.tgz", + "integrity": "sha1-TqVOpaCJOBUxheFSEMaNkJK8G3g=", + "dev": true + } + } + }, + "qunitjs": { + "version": "https://registry.npmjs.org/qunitjs/-/qunitjs-1.23.1.tgz", + "integrity": "sha1-GXHPl6yb4Bpk0jFVCNLkjm/U5xk=", + "dev": true + }, + "quote-stream": { + "version": "https://registry.npmjs.org/quote-stream/-/quote-stream-1.0.2.tgz", + "integrity": "sha1-hJY/jJwmuULhU/7rU6rnRlK34LI=", + "dependencies": { + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "ramda": { + "version": "https://registry.npmjs.org/ramda/-/ramda-0.23.0.tgz", + "integrity": "sha1-zNE//3NJepOXTj6GMnv9h71ujis=", + "dev": true + }, + "randomatic": { + "version": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.6.tgz", + "integrity": "sha1-EQ3Kv/OX6dz/fAeJzMCkmt8exbs=", + "dev": true + }, + "randombytes": { + "version": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.3.tgz", + "integrity": "sha1-Z0yZdgkBw8QRJ3GjHlIdw0nMCew=" + }, + "range-parser": { + "version": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + }, + "raphael": { + "version": "https://registry.npmjs.org/raphael/-/raphael-2.2.7.tgz", + "integrity": "sha1-IxsZFB+NCGmG2PrOtm+LVi7iyBA=" + }, + "raw-body": { + "version": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.7.tgz", + "integrity": "sha1-rf6s4uT7MJgFgBTQjActzFl1h3Q=", + "dev": true, + "dependencies": { + "bytes": { + "version": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz", + "integrity": "sha1-fZcZb51br39pNeJZhVSe3SpsIzk=", + "dev": true + }, + "iconv-lite": { + "version": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz", + "integrity": "sha1-H4irpKsLFQjoMSrMOTRfNumS4vI=", + "dev": true + } + } + }, + "rc": { + "version": "https://registry.npmjs.org/rc/-/rc-1.2.1.tgz", + "integrity": "sha1-LgPo5C7kULjLPc5lvhv4l04d/ZU=", + "dependencies": { + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "react": { + "version": "https://registry.npmjs.org/react/-/react-15.5.4.tgz", + "integrity": "sha1-+oPrAVBqsjfNwcjDsc6o3gEr8Ec=" + }, + "react-addons-css-transition-group": { + "version": "https://registry.npmjs.org/react-addons-css-transition-group/-/react-addons-css-transition-group-15.5.2.tgz", + "integrity": "sha1-6n4Knw4cJ8pCbaTv01WZFb1C6tI=" + }, + "react-addons-test-utils": { + "version": "https://registry.npmjs.org/react-addons-test-utils/-/react-addons-test-utils-15.5.1.tgz", + "integrity": "sha1-4NJYzaKhIq0N/2n4OCYNDDlY9fc=", + "dev": true + }, + "react-dom": { + "version": "https://registry.npmjs.org/react-dom/-/react-dom-15.5.4.tgz", + "integrity": "sha1-ugwoeG/VLtfk8hNf4CiNRirvk9o=" + }, + "react-hyperscript": { + "version": "https://registry.npmjs.org/react-hyperscript/-/react-hyperscript-2.4.2.tgz", + "integrity": "sha1-wZsfWhYcot8QvM5t0imehUepgv4=" + }, + "react-input-autosize": { + "version": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-1.1.4.tgz", + "integrity": "sha1-y8RQctQITdxXgG2447NOZEuDZqw=" + }, + "react-markdown": { + "version": "https://registry.npmjs.org/react-markdown/-/react-markdown-2.5.0.tgz", + "integrity": "sha1-scYZBP7liViGgDvZ332yPD3DqJ4=" + }, + "react-redux": { + "version": "https://registry.npmjs.org/react-redux/-/react-redux-4.4.8.tgz", + "integrity": "sha1-57wd0QDotk6WrIIS2xEyObni4I8=" + }, + "react-select": { + "version": "https://registry.npmjs.org/react-select/-/react-select-1.0.0-rc.5.tgz", + "integrity": "sha1-nTFvJSsa3Dct21zfHxGca3z9tdY=" + }, + "react-simple-file-input": { + "version": "https://registry.npmjs.org/react-simple-file-input/-/react-simple-file-input-1.0.0.tgz", + "integrity": "sha1-DVmJtRub8sJbtIoMP9fnPkE+qkg=" + }, + "react-test-renderer": { + "version": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-15.5.4.tgz", + "integrity": "sha1-1OuyP2E9aF6o9TkBCcLSD798g7w=", + "dev": true + }, + "react-testutils-additions": { + "version": "https://registry.npmjs.org/react-testutils-additions/-/react-testutils-additions-15.2.0.tgz", + "integrity": "sha1-eAKm8o3/nPtnPL6vMoAc1qBU5rc=", + "dev": true, + "dependencies": { + "object-assign": { + "version": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", + "dev": true + } + } + }, + "react-tooltip-component": { + "version": "https://registry.npmjs.org/react-tooltip-component/-/react-tooltip-component-0.3.0.tgz", + "integrity": "sha1-+z7HjDJw/pGWkrwx8UBBCLz0eF4=" + }, + "read": { + "version": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", + "dev": true + }, + "read-only-stream": { + "version": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", + "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=", + "dev": true + }, + "read-pkg": { + "version": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=" + }, + "read-pkg-up": { + "version": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=" + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.9.tgz", + "integrity": "sha1-z3jsb0ptHrQ9JkiMrJfwQudLf8g=" + }, + "readable-wrap": { + "version": "https://registry.npmjs.org/readable-wrap/-/readable-wrap-1.0.0.tgz", + "integrity": "sha1-O1ohHGMeEjA6VJkcgGwX564ga/8=", + "dev": true, + "dependencies": { + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "readdirp": { + "version": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "dev": true + }, + "readline2": { + "version": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", + "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=" + }, + "rechoir": { + "version": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true + }, + "redux": { + "version": "https://registry.npmjs.org/redux/-/redux-3.6.0.tgz", + "integrity": "sha1-iHwrPQub2G7KK+cFccJ2VMGeGI0=" + }, + "redux-logger": { + "version": "https://registry.npmjs.org/redux-logger/-/redux-logger-2.10.2.tgz", + "integrity": "sha1-PFpfCm8yV3wd6t9mVfJX+CxsOTc=" + }, + "redux-thunk": { + "version": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-1.0.3.tgz", + "integrity": "sha1-d4qgCZ7qBZUDGrazkWX2Zw2NJr0=" + }, + "regenerate": { + "version": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.2.tgz", + "integrity": "sha1-0ZQcZ7rUN+G+dkM63Vs4X5WxkmA=" + }, + "regenerator-runtime": { + "version": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=" + }, + "regenerator-transform": { + "version": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.9.11.tgz", + "integrity": "sha1-On0GdSDLe3F2dp61/4aGkb7+EoM=" + }, + "regex-cache": { + "version": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.3.tgz", + "integrity": "sha1-mxpsNdTQ3871cRrmUejp09cRQUU=", + "dev": true + }, + "regexpu-core": { + "version": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", + "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=" + }, + "regjsgen": { + "version": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=" + }, + "regjsparser": { + "version": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=" + }, + "remove-trailing-separator": { + "version": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.0.1.tgz", + "integrity": "sha1-YV67lq9VlVLUv0BXyENtSGq2PMQ=", + "dev": true + }, + "repeat-element": { + "version": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true + }, + "repeat-string": { + "version": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=" + }, + "replace-ext": { + "version": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", + "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=" + }, + "replaceall": { + "version": "https://registry.npmjs.org/replaceall/-/replaceall-0.1.6.tgz", + "integrity": "sha1-gdgax663LX9cSUKt8ml6MiBojY4=", + "dev": true + }, + "replacestream": { + "version": "https://registry.npmjs.org/replacestream/-/replacestream-4.0.2.tgz", + "integrity": "sha1-DEFAcH5PAyP1DeBEhRcIz1i8N70=", + "dev": true + }, + "request": { + "version": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "dependencies": { + "tunnel-agent": { + "version": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=" + }, + "uuid": { + "version": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", + "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=" + } + } + }, + "request-promise": { + "version": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.1.tgz", + "integrity": "sha1-fuxWyJMXqCLL/qmbA5zlQ8LhX2c=" + }, + "request-promise-core": { + "version": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", + "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=" + }, + "require-directory": { + "version": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-from-string": { + "version": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz", + "integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=" + }, + "require-main-filename": { + "version": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, + "require-uncached": { + "version": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=" + }, + "requires-port": { + "version": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "resolve": { + "version": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" + }, + "resolve-dir": { + "version": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", + "integrity": "sha1-shklmlYC+sXFxJatiUpujMQwJh4=", + "dev": true + }, + "resolve-from": { + "version": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=" + }, + "resolve-url": { + "version": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "response-stream": { + "version": "https://registry.npmjs.org/response-stream/-/response-stream-0.0.0.tgz", + "integrity": "sha1-2ksXzHaEyYyWK+tNlfZoyNytCdU=", + "dev": true + }, + "restore-cursor": { + "version": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=" + }, + "resumer": { + "version": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", + "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=" + }, + "revalidator": { + "version": "https://registry.npmjs.org/revalidator/-/revalidator-0.1.8.tgz", + "integrity": "sha1-/s5hv6DBtSoga9axgZgYS91SOjs=", + "dev": true + }, + "rimraf": { + "version": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", + "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=" + }, + "ripemd160": { + "version": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", + "integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=" + }, + "rlp": { + "version": "https://registry.npmjs.org/rlp/-/rlp-2.0.0.tgz", + "integrity": "sha1-nbOE/0uJqPYVY9kjldhiWxjzr7A=" + }, + "run-async": { + "version": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", + "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=" + }, + "rx-lite": { + "version": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", + "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=" + }, + "safe-buffer": { + "version": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", + "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=" + }, + "samsam": { + "version": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz", + "integrity": "sha1-vsEf3IOp/aBjQBIQ5AF2wwJNFWc=", + "dev": true + }, + "sandwich-expando": { + "version": "https://registry.npmjs.org/sandwich-expando/-/sandwich-expando-1.1.1.tgz", + "integrity": "sha1-g4BvzKI3Wvi2ww5vUu1PmJ3rsWU=" + }, + "sax": { + "version": "https://registry.npmjs.org/sax/-/sax-1.2.2.tgz", + "integrity": "sha1-/YYxojvHgmvvXYcb24c3jJVkeCg=", + "dev": true + }, + "script-injector": { + "version": "https://registry.npmjs.org/script-injector/-/script-injector-1.0.0.tgz", + "integrity": "sha1-9vTH9qXcxZ4IJG52vfyDoKFAaSY=", + "dev": true, + "dependencies": { + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "through2": { + "version": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true + } + } + }, + "scrypt": { + "version": "https://registry.npmjs.org/scrypt/-/scrypt-6.0.3.tgz", + "integrity": "sha1-BOAUpWgrU/pQwtXM4WfXGcBthw0=" + }, + "scrypt.js": { + "version": "https://registry.npmjs.org/scrypt.js/-/scrypt.js-0.2.0.tgz", + "integrity": "sha1-r40UZbcemZARC+38WTuUeeA6ito=" + }, + "scryptsy": { + "version": "https://registry.npmjs.org/scryptsy/-/scryptsy-1.2.1.tgz", + "integrity": "sha1-oyJfpLJST4AnAHYeKFW987LZIWM=" + }, + "secp256k1": { + "version": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.2.5.tgz", + "integrity": "sha1-Dd5bJ+UCFmX23/ynssPgEMbBPJM=" + }, + "semaphore": { + "version": "https://registry.npmjs.org/semaphore/-/semaphore-1.0.5.tgz", + "integrity": "sha1-tJJXbmavGT25XWXiXsU/Xxl5jWA=" + }, + "semver": { + "version": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" + }, + "semver-greatest-satisfied-range": { + "version": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.0.0.tgz", + "integrity": "sha1-T7RB4qjSbEC1mDJ1VzGN4nKlWKA=", + "dev": true, + "dependencies": { + "semver": { + "version": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", + "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", + "dev": true + } + } + }, + "semver-regex": { + "version": "https://registry.npmjs.org/semver-regex/-/semver-regex-1.0.0.tgz", + "integrity": "sha1-kqSWkGX5xwxpR1PVUkj8aPj2Usk=", + "dev": true + }, + "send": { + "version": "https://registry.npmjs.org/send/-/send-0.15.3.tgz", + "integrity": "sha1-UBP5+ZAj31DRvZiSwZ4979HVMwk=" + }, + "serve-static": { + "version": "https://registry.npmjs.org/serve-static/-/serve-static-1.12.3.tgz", + "integrity": "sha1-n0uhni8wMMVH+K+ZEHg47DjVseI=" + }, + "set-blocking": { + "version": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-immediate-shim": { + "version": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + }, + "setimmediate": { + "version": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, + "setprototypeof": { + "version": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + }, + "sha.js": { + "version": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.8.tgz", + "integrity": "sha1-NwaMLEdra69ALRSknGf1l5IfY08=" + }, + "sha3": { + "version": "https://registry.npmjs.org/sha3/-/sha3-1.2.0.tgz", + "integrity": "sha1-aYnxtwpJhwWHajc+LGKs6WqpOZo=" + }, + "shallow-copy": { + "version": "https://registry.npmjs.org/shallow-copy/-/shallow-copy-0.0.1.tgz", + "integrity": "sha1-QV9CcC1z2BAzApLMXuhurhoRoXA=" + }, + "shasum": { + "version": "https://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz", + "integrity": "sha1-5wEjENj0F/TetXEhUOVni4euVl8=", + "dev": true, + "dependencies": { + "json-stable-stringify": { + "version": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz", + "integrity": "sha1-YRwj6BTbN1Un34URk9tZ3Sryf0U=", + "dev": true + } + } + }, + "shebang-command": { + "version": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true + }, + "shebang-regex": { + "version": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shell-quote": { + "version": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", + "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", + "dev": true + }, + "shelljs": { + "version": "https://registry.npmjs.org/shelljs/-/shelljs-0.6.1.tgz", + "integrity": "sha1-7GIRvtGSBEIIj+D3Cyg3Iy7SyKg=" + }, + "shellwords": { + "version": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.0.tgz", + "integrity": "sha1-Zq/Ue2oSky2Qccv9mKUueFzQuhQ=", + "dev": true + }, + "sigmund": { + "version": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + }, + "signal-exit": { + "version": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "simple-get": { + "version": "https://registry.npmjs.org/simple-get/-/simple-get-1.4.3.tgz", + "integrity": "sha1-6XVe2kB+ltpAxeUVjJ6jezO+y+s=" + }, + "sinon": { + "version": "https://registry.npmjs.org/sinon/-/sinon-1.17.7.tgz", + "integrity": "sha1-RUKk9JugxFwF6y6d2dID4rjv4L8=", + "dev": true + }, + "sizzle": { + "version": "https://registry.npmjs.org/sizzle/-/sizzle-2.3.3.tgz", + "integrity": "sha1-TrB4w3IxpWtS5Bk/cB5++JN+YGs=", + "dev": true + }, + "slash": { + "version": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" + }, + "slice-ansi": { + "version": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=" + }, + "sntp": { + "version": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=" + }, + "socket.io": { + "version": "https://registry.npmjs.org/socket.io/-/socket.io-1.6.0.tgz", + "integrity": "sha1-PkDZMmN+a9kjmBslyvfFPoO24uE=", + "dev": true, + "dependencies": { + "debug": { + "version": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true + }, + "ms": { + "version": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + }, + "object-assign": { + "version": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz", + "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=", + "dev": true + } + } + }, + "socket.io-adapter": { + "version": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz", + "integrity": "sha1-y21LuL7IHhB4uZZ3+c7QBGBmu4s=", + "dev": true, + "dependencies": { + "debug": { + "version": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true + }, + "ms": { + "version": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "socket.io-client": { + "version": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.6.0.tgz", + "integrity": "sha1-W2aPT3cTBN/u0XkGRwg4b6ZxeFM=", + "dev": true, + "dependencies": { + "component-emitter": { + "version": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "debug": { + "version": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true + }, + "ms": { + "version": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "socket.io-parser": { + "version": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.3.1.tgz", + "integrity": "sha1-3VMgJRA85Clpcya+/WQAX8/ltKA=", + "dev": true, + "dependencies": { + "debug": { + "version": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true + }, + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "ms": { + "version": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + } + } + }, + "solc": { + "version": "https://registry.npmjs.org/solc/-/solc-0.4.11.tgz", + "integrity": "sha1-JSLrQ+fAQZusIGC5biCiWTv7Xos=", + "dependencies": { + "yargs": { + "version": "https://registry.npmjs.org/yargs/-/yargs-4.8.1.tgz", + "integrity": "sha1-wMQpJMpKqmsObaFznfshZDn53cA=" + }, + "yargs-parser": { + "version": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-2.4.1.tgz", + "integrity": "sha1-hVaN488VD/SfpRgl8DqMiA3cxcQ=" + } + } + }, + "source-map": { + "version": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=" + }, + "source-map-resolve": { + "version": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.3.1.tgz", + "integrity": "sha1-YQ9hIqRFuN1RU1oqcbeD38Ekh2E=", + "dev": true + }, + "source-map-support": { + "version": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.15.tgz", + "integrity": "sha1-AyAt9lwG0r2MfsI2KhkwVv7407E=" + }, + "source-map-url": { + "version": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.3.0.tgz", + "integrity": "sha1-fsrxO1e80J2opAxdJp2zN5nUqvk=", + "dev": true + }, + "sparkles": { + "version": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.0.tgz", + "integrity": "sha1-Gsu/tZJDbRC76PeFt8xvgoFQEsM=" + }, + "spawn-args": { + "version": "https://registry.npmjs.org/spawn-args/-/spawn-args-0.2.0.tgz", + "integrity": "sha1-+30L0dcP1DFr2ePew4nmX51jYbs=", + "dev": true + }, + "spdx-correct": { + "version": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=" + }, + "spdx-expression-parse": { + "version": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", + "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=" + }, + "spdx-license-ids": { + "version": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", + "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=" + }, + "split": { + "version": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", + "dev": true + }, + "sprintf-js": { + "version": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "sshpk": { + "version": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.0.tgz", + "integrity": "sha1-/yo+T9BEl1Vf7Zezmg/YL6+zozw=", + "dependencies": { + "assert-plus": { + "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "stack-trace": { + "version": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", + "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=", + "dev": true + }, + "static-eval": { + "version": "https://registry.npmjs.org/static-eval/-/static-eval-0.2.4.tgz", + "integrity": "sha1-t9NNg4k3uWn5ZBygfUj47eJj6ns=", + "dependencies": { + "escodegen": { + "version": "https://registry.npmjs.org/escodegen/-/escodegen-0.0.28.tgz", + "integrity": "sha1-Dk/xcV8yh3XWyrUaxEpAbNer/9M=" + }, + "esprima": { + "version": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=" + }, + "estraverse": { + "version": "https://registry.npmjs.org/estraverse/-/estraverse-1.3.2.tgz", + "integrity": "sha1-N8K4k+8T1yPydth41g2FNRUqbEI=" + } + } + }, + "static-module": { + "version": "https://registry.npmjs.org/static-module/-/static-module-1.3.2.tgz", + "integrity": "sha1-Mp+58iOlZiZr2nGEO32TLHZxdPM=", + "dependencies": { + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "object-inspect": { + "version": "https://registry.npmjs.org/object-inspect/-/object-inspect-0.4.0.tgz", + "integrity": "sha1-9RV8EWwUVbJDsG7pdwM5LFrYn+w=" + }, + "object-keys": { + "version": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=" + }, + "quote-stream": { + "version": "https://registry.npmjs.org/quote-stream/-/quote-stream-0.0.0.tgz", + "integrity": "sha1-zeKelMQJsW4Z3HCYuJtmWPlyHTs=" + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=" + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "through2": { + "version": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz", + "integrity": "sha1-2/WGYDEVHsg1K7bE22SiKSqEC5s=" + }, + "xtend": { + "version": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=" + } + } + }, + "statuses": { + "version": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" + }, + "stealthy-require": { + "version": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" + }, + "stream-browserify": { + "version": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "dev": true + }, + "stream-combiner": { + "version": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "dev": true + }, + "stream-combiner2": { + "version": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", + "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", + "dev": true, + "dependencies": { + "duplexer2": { + "version": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "dev": true + } + } + }, + "stream-each": { + "version": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.0.tgz", + "integrity": "sha1-HpXUdXP1gNgU3A/4zQ9m8c5TyZE=" + }, + "stream-exhaust": { + "version": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.1.tgz", + "integrity": "sha1-wMRFXlTOWhecqHNuczNLTn/WdVM=", + "dev": true + }, + "stream-http": { + "version": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.1.tgz", + "integrity": "sha1-VGpRdBrVprB+njGwsQRBqRffUoo=", + "dev": true + }, + "stream-shift": { + "version": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" + }, + "stream-splicer": { + "version": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-1.3.2.tgz", + "integrity": "sha1-PARBvhW5v04iYnXm3IOWR0VUZmE=", + "dev": true, + "dependencies": { + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "through2": { + "version": "https://registry.npmjs.org/through2/-/through2-1.1.1.tgz", + "integrity": "sha1-CEfLxESfNAVXTb3M2buEG4OsNUU=", + "dev": true + } + } + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.1.tgz", + "integrity": "sha1-YuIA8DmVWmgQ2N8KM//A8BNmLZg=" + }, + "string-width": { + "version": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=" + }, + "string.prototype.repeat": { + "version": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-0.2.0.tgz", + "integrity": "sha1-q6Nt4I3O5qWjN9SbLqHaGyj8Ds8=" + }, + "string.prototype.trim": { + "version": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz", + "integrity": "sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo=" + }, + "stringstream": { + "version": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, + "strip-ansi": { + "version": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=" + }, + "strip-bom": { + "version": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=" + }, + "strip-bom-stream": { + "version": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz", + "integrity": "sha1-5xRDmFd9Uaa+0PoZlPoF9D/ZiO4=", + "dev": true + }, + "strip-hex-prefix": { + "version": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz", + "integrity": "sha1-DF8VX+8RUTczd96du1iNoFUA428=" + }, + "strip-json-comments": { + "version": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "styled_string": { + "version": "https://registry.npmjs.org/styled_string/-/styled_string-0.0.1.tgz", + "integrity": "sha1-0ieCvYEpVFm8Tx3xjEutjpTdEko=", + "dev": true + }, + "subarg": { + "version": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", + "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", + "dev": true, + "dependencies": { + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "supports-color": { + "version": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + }, + "sw-stream": { + "version": "https://registry.npmjs.org/sw-stream/-/sw-stream-2.0.0.tgz", + "integrity": "sha1-Yo677rnu4LZrA+xS/FX8xO6yPPM=" + }, + "symbol-observable": { + "version": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.4.tgz", + "integrity": "sha1-Kb9hXUqnEhvdiYsi1LP5vE4qoD0=" + }, + "symbol-tree": { + "version": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", + "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", + "dev": true + }, + "syntax-error": { + "version": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.3.0.tgz", + "integrity": "sha1-HtkmbE1AvnXcVb+bsct3Biu5bKE=", + "dev": true + }, + "table": { + "version": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", + "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", + "dependencies": { + "is-fullwidth-code-point": { + "version": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "string-width": { + "version": "https://registry.npmjs.org/string-width/-/string-width-2.0.0.tgz", + "integrity": "sha1-Y1xUNsxypuDDh87KJ41OLuxSaH4=" + } + } + }, + "tap-parser": { + "version": "https://registry.npmjs.org/tap-parser/-/tap-parser-5.3.3.tgz", + "integrity": "sha1-U+yKkPJ11v/0PxaeVqZ5UCp0EYU=", + "dev": true + }, + "tape": { + "version": "https://registry.npmjs.org/tape/-/tape-4.6.3.tgz", + "integrity": "sha1-Y353WB6ass4XV36b1M5PV1gG2LY=", + "dependencies": { + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "tar-fs": { + "version": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.15.2.tgz", + "integrity": "sha1-dh9bMpMsezlGGmDVN/rqDYCEgww=" + }, + "tar-stream": { + "version": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.5.4.tgz", + "integrity": "sha1-NlSc8E7RrumyowwBQyUiONr5QBY=", + "dependencies": { + "bl": { + "version": "https://registry.npmjs.org/bl/-/bl-1.2.1.tgz", + "integrity": "sha1-ysMo977kVzDUBLaSID/LWQ4XLV4=" + } + } + }, + "ternary-stream": { + "version": "https://registry.npmjs.org/ternary-stream/-/ternary-stream-2.0.1.tgz", + "integrity": "sha1-Bk5Im0tb9gumpre8fy9cJ07Pgmk=", + "dev": true + }, + "testem": { + "version": "https://registry.npmjs.org/testem/-/testem-1.16.2.tgz", + "integrity": "sha1-lURtMQoQ6FLT69vAzis/11N4uik=", + "dev": true, + "dependencies": { + "commander": { + "version": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "dev": true + } + } + }, + "text-table": { + "version": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" + }, + "textarea-caret": { + "version": "https://registry.npmjs.org/textarea-caret/-/textarea-caret-3.0.2.tgz", + "integrity": "sha1-82DEhpmqGr9xhoCkOjGoUGZcLK8=" + }, + "textextensions": { + "version": "https://registry.npmjs.org/textextensions/-/textextensions-1.0.2.tgz", + "integrity": "sha1-ZUhjk+4fK7A5pgy7oFsLaL2VAdI=", + "dev": true + }, + "thenify": { + "version": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", + "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", + "dev": true, + "dependencies": { + "any-promise": { + "version": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", + "dev": true + } + } + }, + "thenify-all": { + "version": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", + "dev": true + }, + "three": { + "version": "https://registry.npmjs.org/three/-/three-0.73.0.tgz", + "integrity": "sha1-jyWKxG2BVsRa8kT+E6T4u3oEUSk=" + }, + "three.js": { + "version": "https://registry.npmjs.org/three.js/-/three.js-0.73.2.tgz", + "integrity": "sha1-3JARPxgT9AShjhYkqajjnGwZSpY=" + }, + "through": { + "version": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "through2": { + "version": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=" + }, + "through2-filter": { + "version": "https://registry.npmjs.org/through2-filter/-/through2-filter-2.0.0.tgz", + "integrity": "sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=", + "dev": true + }, + "tildify": { + "version": "https://registry.npmjs.org/tildify/-/tildify-1.2.0.tgz", + "integrity": "sha1-3OwD9V3Km3qj5bBPIYF+tW5jWIo=", + "dev": true + }, + "time-stamp": { + "version": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", + "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=" + }, + "timers-browserify": { + "version": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", + "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", + "dev": true, + "dependencies": { + "process": { + "version": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + } + } + }, + "to-absolute-glob": { + "version": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-0.1.1.tgz", + "integrity": "sha1-HN+kcqnvUMI57maZm2YsoOs5k38=", + "dev": true + }, + "to-array": { + "version": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", + "dev": true + }, + "to-arraybuffer": { + "version": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" + }, + "to-iso-string": { + "version": "https://registry.npmjs.org/to-iso-string/-/to-iso-string-0.0.2.tgz", + "integrity": "sha1-TcGeZk38y+Jb2NtQiwDG2hWCVdE=", + "dev": true + }, + "to-utf8": { + "version": "https://registry.npmjs.org/to-utf8/-/to-utf8-0.0.1.tgz", + "integrity": "sha1-0Xrqcv8vujm55DYBvns/9y4ImFI=" + }, + "toggle-selection": { + "version": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.5.tgz", + "integrity": "sha1-cmxwPeYHGTpzwyx99JzSSVD8V08=" + }, + "tough-cookie": { + "version": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", + "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", + "dependencies": { + "punycode": { + "version": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "tr46": { + "version": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "dev": true + }, + "tracejs": { + "version": "https://registry.npmjs.org/tracejs/-/tracejs-0.1.8.tgz", + "integrity": "sha1-bCZ4exhT8TcWNGIsHIC8RAJsXXA=", + "dev": true + }, + "traverse": { + "version": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", + "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=" + }, + "trim": { + "version": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", + "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=" + }, + "trim-right": { + "version": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" + }, + "trumpet": { + "version": "https://registry.npmjs.org/trumpet/-/trumpet-1.7.2.tgz", + "integrity": "sha1-sCxp5GXRcfVeRJJL+bW90gl0yDA=", + "dev": true, + "dependencies": { + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "through2": { + "version": "https://registry.npmjs.org/through2/-/through2-1.1.1.tgz", + "integrity": "sha1-CEfLxESfNAVXTb3M2buEG4OsNUU=", + "dev": true + } + } + }, + "tryit": { + "version": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz", + "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=" + }, + "tty-browserify": { + "version": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "tunnel-agent": { + "version": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=" + }, + "tweetnacl": { + "version": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "type-check": { + "version": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=" + }, + "type-detect": { + "version": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", + "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=", + "dev": true + }, + "type-is": { + "version": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=" + }, + "typedarray": { + "version": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "ua-parser-js": { + "version": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.12.tgz", + "integrity": "sha1-BMgamb3V3FImPqKdJMa/jUgYpLs=" + }, + "uglify-js": { + "version": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.3.6.tgz", + "integrity": "sha1-+gmEdwtCi3qbKoBY9GNV0U/vIRo=", + "dev": true, + "dependencies": { + "async": { + "version": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", + "dev": true + }, + "optimist": { + "version": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", + "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=", + "dev": true + }, + "source-map": { + "version": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "dev": true + } + } + }, + "uglifyify": { + "version": "https://registry.npmjs.org/uglifyify/-/uglifyify-3.0.4.tgz", + "integrity": "sha1-SH4IClp3mIgOaOkN75sGaB+xO9I=", + "dev": true, + "dependencies": { + "convert-source-map": { + "version": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", + "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", + "dev": true + }, + "extend": { + "version": "https://registry.npmjs.org/extend/-/extend-1.3.0.tgz", + "integrity": "sha1-0VFvsP9WJNLr+RI+odrFoZlABPg=", + "dev": true + } + } + }, + "ultron": { + "version": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", + "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=", + "dev": true + }, + "umd": { + "version": "https://registry.npmjs.org/umd/-/umd-3.0.1.tgz", + "integrity": "sha1-iuVW4RAR9jwllnCKiDclnwGz1g4=", + "dev": true + }, + "unc-path-regex": { + "version": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", + "dev": true + }, + "underscore": { + "version": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=", + "dev": true + }, + "undertaker": { + "version": "https://registry.npmjs.org/undertaker/-/undertaker-1.1.0.tgz", + "integrity": "sha1-C6AOb7aor+HpKGMVZar226YRGus=", + "dev": true, + "dependencies": { + "array-each": { + "version": "https://registry.npmjs.org/array-each/-/array-each-0.1.1.tgz", + "integrity": "sha1-xdUrqCJfNtcoF4unrsQTrPrd0Pk=", + "dev": true + }, + "array-slice": { + "version": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", + "dev": true + }, + "isobject": { + "version": "https://registry.npmjs.org/isobject/-/isobject-1.0.2.tgz", + "integrity": "sha1-8Pm4zpLdVA+gdAiC44NaLgIux4o=", + "dev": true + }, + "object.defaults": { + "version": "https://registry.npmjs.org/object.defaults/-/object.defaults-0.3.0.tgz", + "integrity": "sha1-seucvHjEx71WysbK496tWnETiCo=", + "dev": true + } + } + }, + "undertaker-registry": { + "version": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.0.tgz", + "integrity": "sha1-LacWx2WZnYyUufntLABt9JI7BSs=", + "dev": true + }, + "uniq": { + "version": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=" + }, + "unique-stream": { + "version": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.2.1.tgz", + "integrity": "sha1-WqADz76Uxf+GbE59ZouxxNuts2k=", + "dev": true + }, + "unorm": { + "version": "https://registry.npmjs.org/unorm/-/unorm-1.4.1.tgz", + "integrity": "sha1-NkIA1fE2RsqLzURJAnEzVhR5IwA=" + }, + "unpipe": { + "version": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "unzip-response": { + "version": "https://registry.npmjs.org/unzip-response/-/unzip-response-1.0.2.tgz", + "integrity": "sha1-uYTwh3/AqJwsdzzB73tbIytbBv4=" + }, + "urix": { + "version": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "dependencies": { + "punycode": { + "version": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "user-home": { + "version": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", + "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=" + }, + "utf8": { + "version": "https://registry.npmjs.org/utf8/-/utf8-2.1.2.tgz", + "integrity": "sha1-H6DZJw6b6FDZsFAn9jUZv0ZFfZY=" + }, + "util": { + "version": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "dependencies": { + "inherits": { + "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + } + } + }, + "util-deprecate": { + "version": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utile": { + "version": "https://registry.npmjs.org/utile/-/utile-0.3.0.tgz", + "integrity": "sha1-E1LDQOuCDk2N26A5pPv6oy7U7zo=", + "dev": true, + "dependencies": { + "async": { + "version": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", + "dev": true + }, + "deep-equal": { + "version": "https://registry.npmjs.org/deep-equal/-/deep-equal-0.2.2.tgz", + "integrity": "sha1-hLdFiW80xoTpjyzg5Cq69Du6AX0=", + "dev": true + } + } + }, + "utils-merge": { + "version": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", + "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=" + }, + "uuid": { + "version": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=" + }, + "v8flags": { + "version": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz", + "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=", + "dev": true, + "dependencies": { + "user-home": { + "version": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", + "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=", + "dev": true + } + } + }, + "vali-date": { + "version": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz", + "integrity": "sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=", + "dev": true + }, + "valid-url": { + "version": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", + "integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=" + }, + "validate-npm-package-license": { + "version": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=" + }, + "varint": { + "version": "https://registry.npmjs.org/varint/-/varint-4.0.1.tgz", + "integrity": "sha1-SQgpuULSSEY7KzUJeZXDv3NxmOk=" + }, + "vary": { + "version": "https://registry.npmjs.org/vary/-/vary-1.1.1.tgz", + "integrity": "sha1-Z1Neu2lMHVIldFeYRmUyP1h+jTc=" + }, + "verror": { + "version": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", + "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=" + }, + "vinyl": { + "version": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", + "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=" + }, + "vinyl-buffer": { + "version": "https://registry.npmjs.org/vinyl-buffer/-/vinyl-buffer-1.0.0.tgz", + "integrity": "sha1-ygZ+oIQx1QdyKx3lCD9gJhbrwjQ=", + "dev": true, + "dependencies": { + "bl": { + "version": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz", + "integrity": "sha1-wGt5evCF6gC8Unr8jvzxHeIjIFQ=", + "dev": true + }, + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "through2": { + "version": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true + } + } + }, + "vinyl-file": { + "version": "https://registry.npmjs.org/vinyl-file/-/vinyl-file-2.0.0.tgz", + "integrity": "sha1-p+v1/779obfRjRQPyweyI++2dRo=", + "dev": true, + "dependencies": { + "first-chunk-stream": { + "version": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz", + "integrity": "sha1-G97NuOCDwGZLkZRVgVd6Q6nzHXA=", + "dev": true + }, + "strip-bom-stream": { + "version": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-2.0.0.tgz", + "integrity": "sha1-+H217yYT9paKpUWr/h7HKLaoKco=", + "dev": true + }, + "vinyl": { + "version": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "dev": true + } + } + }, + "vinyl-fs": { + "version": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-2.4.4.tgz", + "integrity": "sha1-vm/zJwy1Xf19MGNkDegfJddTIjk=", + "dev": true, + "dependencies": { + "gulp-sourcemaps": { + "version": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-1.6.0.tgz", + "integrity": "sha1-uG/zSdgBzrVuHZ59x7vLS33uYAw=", + "dev": true + }, + "vinyl": { + "version": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "dev": true + } + } + }, + "vinyl-source-stream": { + "version": "https://registry.npmjs.org/vinyl-source-stream/-/vinyl-source-stream-1.1.0.tgz", + "integrity": "sha1-RMvlEIIFJ53rDFZTwJSiiHk4sas=", + "dev": true, + "dependencies": { + "clone": { + "version": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", + "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", + "dev": true + }, + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "through2": { + "version": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true + }, + "vinyl": { + "version": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", + "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", + "dev": true + } + } + }, + "vm-browserify": { + "version": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "dev": true + }, + "vreme": { + "version": "https://registry.npmjs.org/vreme/-/vreme-3.0.2.tgz", + "integrity": "sha1-RyE3a0SUV/796KhJ0zQJM7kLVoY=" + }, + "watchify": { + "version": "https://registry.npmjs.org/watchify/-/watchify-3.9.0.tgz", + "integrity": "sha1-8HX9LoqGrN6Eztum5cKgvt1SPZ4=", + "dev": true, + "dependencies": { + "base64-js": { + "version": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.0.tgz", + "integrity": "sha1-o5mS1yNYSBGYK+XikLtqU9hnAPE=", + "dev": true + }, + "browserify": { + "version": "https://registry.npmjs.org/browserify/-/browserify-14.4.0.tgz", + "integrity": "sha1-CJo0Y69Y0OSNjNQHCz90ZU1avKk=", + "dev": true + }, + "buffer": { + "version": "https://registry.npmjs.org/buffer/-/buffer-5.0.6.tgz", + "integrity": "sha1-LqZp9+7Atu2gWwj4tf9mGyhXNYg=", + "dev": true + }, + "concat-stream": { + "version": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", + "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", + "dev": true, + "dependencies": { + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "duplexer2": { + "version": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "dev": true + }, + "https-browserify": { + "version": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "process": { + "version": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "punycode": { + "version": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + } + } + }, + "weak": { + "version": "https://registry.npmjs.org/weak/-/weak-1.0.1.tgz", + "integrity": "sha1-q5mqswcGlZqgIAy4z1RbucszuZ4=", + "optional": true + }, + "web3": { + "version": "https://registry.npmjs.org/web3/-/web3-0.18.2.tgz", + "integrity": "sha1-YbGm7fUFaCDiLh7wgvVMJ59L91g=" + }, + "web3-provider-engine": { + "version": "https://registry.npmjs.org/web3-provider-engine/-/web3-provider-engine-12.2.1.tgz", + "integrity": "sha1-hIwu4Yf5cBsKOC4iB8mxDxdKjXI=", + "dependencies": { + "async": { + "version": "https://registry.npmjs.org/async/-/async-2.4.1.tgz", + "integrity": "sha1-YqVrJ5yYoR0JhwlqAcw+6463u9c=" + }, + "clone": { + "version": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", + "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=" + }, + "ethereumjs-util": { + "version": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.1.1.tgz", + "integrity": "sha1-Ei+zjep0fcYrOuv8Nl0b1Ivktz4=" + } + } + }, + "web3-stream-provider": { + "version": "https://registry.npmjs.org/web3-stream-provider/-/web3-stream-provider-2.0.8.tgz", + "integrity": "sha1-AgNxn9XtoWwsr1hQ+krtVRjt7qo=" + }, + "webidl-conversions": { + "version": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "dev": true + }, + "websocket-driver": { + "version": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz", + "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=", + "dev": true + }, + "websocket-extensions": { + "version": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.1.tgz", + "integrity": "sha1-domUmcGEtu91Q3fC27DNbLVdKec=", + "dev": true + }, + "whatwg-fetch": { + "version": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", + "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=" + }, + "whatwg-url": { + "version": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-2.0.1.tgz", + "integrity": "sha1-U5ayBD8CDub3BNnEXqhRnnJN5lk=", + "dev": true + }, + "which": { + "version": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", + "integrity": "sha1-RgwdoPgQED0DIam2M6+eV15kSG8=", + "dev": true + }, + "which-module": { + "version": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=" + }, + "wide-align": { + "version": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", + "integrity": "sha1-Vx4PGwYEY268DfwhsDObvjE0FxA=" + }, + "window-size": { + "version": "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz", + "integrity": "sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU=" + }, + "winston": { + "version": "https://registry.npmjs.org/winston/-/winston-2.1.1.tgz", + "integrity": "sha1-PJNJ0ZYgf9G9/51LxD73JRDjoS4=", + "dev": true, + "dependencies": { + "async": { + "version": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", + "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=", + "dev": true + }, + "colors": { + "version": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "dev": true + }, + "pkginfo": { + "version": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.3.1.tgz", + "integrity": "sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE=", + "dev": true + } + } + }, + "wordwrap": { + "version": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + }, + "wrap-ansi": { + "version": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=" + }, + "wrappy": { + "version": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "wreck": { + "version": "https://registry.npmjs.org/wreck/-/wreck-6.3.0.tgz", + "integrity": "sha1-oTaXafB7u2LWo3gzanhx/Hc8dAs=", + "dev": true + }, + "write": { + "version": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=" + }, + "ws": { + "version": "https://registry.npmjs.org/ws/-/ws-1.1.1.tgz", + "integrity": "sha1-CC3bbGQehdS7RR8D1S8G6r2x8Bg=", + "dev": true + }, + "wtf-8": { + "version": "https://registry.npmjs.org/wtf-8/-/wtf-8-1.0.0.tgz", + "integrity": "sha1-OS2LotDxw00e4tYw8V0O+2jhBIo=", + "dev": true + }, + "xhr": { + "version": "https://registry.npmjs.org/xhr/-/xhr-2.4.0.tgz", + "integrity": "sha1-4W5mpF+GmGHu76tBbV7/ci3ECZM=" + }, + "xhr2": { + "version": "https://registry.npmjs.org/xhr2/-/xhr2-0.1.4.tgz", + "integrity": "sha1-f4dliEdxbbUCYyOBL4GMras4el8=" + }, + "xml-name-validator": { + "version": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz", + "integrity": "sha1-TYuPHszTQZqjYgYb7O9RXh5VljU=", + "dev": true + }, + "xmldom": { + "version": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", + "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk=", + "dev": true + }, + "xmlhttprequest": { + "version": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", + "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=" + }, + "xmlhttprequest-ssl": { + "version": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz", + "integrity": "sha1-GFqIjATspGw+QHDZn3tJ3jUomS0=", + "dev": true + }, + "xss-filters": { + "version": "https://registry.npmjs.org/xss-filters/-/xss-filters-1.2.7.tgz", + "integrity": "sha1-Wfod4gHzby80cNysX1jMwoMLCpo=" + }, + "xtend": { + "version": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, + "y18n": { + "version": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yallist": { + "version": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", + "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=" + }, + "yargs-parser": { + "version": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", + "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=" + }, + "yazl": { + "version": "https://registry.npmjs.org/yazl/-/yazl-2.4.2.tgz", + "integrity": "sha1-FMsZCD4eJacAksFYiqvg9OTdTYg=", + "dev": true + }, + "yeast": { + "version": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", + "dev": true + } + } +} -- cgit v1.2.3 From 82cbfaa826cc4d731dfbeab7482420c66c0e832b Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Thu, 1 Jun 2017 12:53:16 -0700 Subject: Convert gasLimit to not use muln in BN --- app/scripts/lib/tx-utils.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js index 8cf304d0b..658f3bedc 100644 --- a/app/scripts/lib/tx-utils.js +++ b/app/scripts/lib/tx-utils.js @@ -30,7 +30,7 @@ module.exports = class txProviderUtils { setBlockGasLimit (txMeta, blockGasLimitHex, cb) { const blockGasLimitBN = hexToBn(blockGasLimitHex) - const saferGasLimitBN = blockGasLimitBN.muln(0.95) + const saferGasLimitBN = BnMultiplyByFraction(blockGasLimitBN, 19, 20) txMeta.blockGasLimit = bnToHex(saferGasLimitBN) cb() return @@ -43,7 +43,7 @@ module.exports = class txProviderUtils { // if not, fallback to block gasLimit if (!txMeta.gasLimitSpecified) { const blockGasLimitBN = hexToBn(blockGasLimitHex) - const saferGasLimitBN = blockGasLimitBN.muln(0.95) + const saferGasLimitBN = BnMultiplyByFraction(blockGasLimitBN, 19, 20) txParams.gas = bnToHex(saferGasLimitBN) } // run tx, see if it will OOG @@ -143,3 +143,9 @@ function bnToHex (inputBn) { function hexToBn (inputHex) { return new BN(ethUtil.stripHexPrefix(inputHex), 16) } + +function BnMultiplyByFraction (targetBN, numerator, denominator) { + const numBN = new BN(numerator) + const denomBN = new BN(denominator) + return targetBN.mul(numBN).div(denomBN) +} -- cgit v1.2.3 From 611cb7ad930955bbc1691eafbe961a313557f17b Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 2 Jun 2017 11:08:59 -0700 Subject: Version 3.7.4 --- CHANGELOG.md | 5 +++++ app/manifest.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b48889f5d..0e7b292b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Current Master +## 3.7.4 2017-6-2 + +- Fix bug with inflight cache that caused some block lookups to return bad values (affected OasisDex). +- Fixed bug with gas limit calculation that would sometimes create unsubmittable gas limits. + ## 3.7.3 2017-6-1 - Rebuilt to fix cache clearing bug. diff --git a/app/manifest.json b/app/manifest.json index 4dcd6df31..99a083f0f 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.7.3", + "version": "3.7.4", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From 2b7d8424981fbbd0f6306b5ee7abf8754f9f7092 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Fri, 2 Jun 2017 15:18:14 -0700 Subject: Update gasblocklimit params with every block. --- app/scripts/lib/eth-store.js | 2 ++ app/scripts/lib/tx-utils.js | 9 --------- ui/app/components/pending-tx.js | 11 +++++++++-- ui/app/conf-tx.js | 4 +++- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/app/scripts/lib/eth-store.js b/app/scripts/lib/eth-store.js index 6f04a9dd6..ebba98f5c 100644 --- a/app/scripts/lib/eth-store.js +++ b/app/scripts/lib/eth-store.js @@ -21,6 +21,7 @@ class EthereumStore extends ObservableStore { transactions: {}, currentBlockNumber: '0', currentBlockHash: '', + currentBlockGasLimit: '', }) this._provider = opts.provider this._query = new EthQuery(this._provider) @@ -73,6 +74,7 @@ class EthereumStore extends ObservableStore { this._currentBlockNumber = blockNumber this.updateState({ currentBlockNumber: parseInt(blockNumber) }) this.updateState({ currentBlockHash: `0x${block.hash.toString('hex')}`}) + this.updateState({ currentBlockGasLimit: `0x${block.gasLimit.toString('hex')}` }) async.parallel([ this._updateAccounts.bind(this), this._updateTransactions.bind(this, blockNumber), diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js index 658f3bedc..149d93102 100644 --- a/app/scripts/lib/tx-utils.js +++ b/app/scripts/lib/tx-utils.js @@ -21,21 +21,12 @@ module.exports = class txProviderUtils { this.query.getBlockByNumber('latest', true, (err, block) => { if (err) return cb(err) async.waterfall([ - self.setBlockGasLimit.bind(self, txMeta, block.gasLimit), self.estimateTxGas.bind(self, txMeta, block.gasLimit), self.setTxGas.bind(self, txMeta, block.gasLimit), ], cb) }) } - setBlockGasLimit (txMeta, blockGasLimitHex, cb) { - const blockGasLimitBN = hexToBn(blockGasLimitHex) - const saferGasLimitBN = BnMultiplyByFraction(blockGasLimitBN, 19, 20) - txMeta.blockGasLimit = bnToHex(saferGasLimitBN) - cb() - return - } - estimateTxGas (txMeta, blockGasLimitHex, cb) { const txParams = txMeta.txParams // check if gasLimit is already specified diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index b46f715bc..0847a8d4c 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -32,7 +32,7 @@ function PendingTx () { PendingTx.prototype.render = function () { const props = this.props - const { currentCurrency } = props + const { currentCurrency, blockGasLimit } = props const conversionRate = props.conversionRate const txMeta = this.gatherTxMeta() @@ -47,7 +47,8 @@ PendingTx.prototype.render = function () { // Gas const gas = txParams.gas const gasBn = hexToBn(gas) - const safeGasLimit = parseInt(txMeta.blockGasLimit) + const gasLimit = new BN(parseInt(blockGasLimit)) + const safeGasLimit = this.bnMultiplyByFraction(gasLimit, 19, 20).toString(10) // Gas Price const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) @@ -434,6 +435,12 @@ PendingTx.prototype._notZeroOrEmptyString = function (obj) { return obj !== '' && obj !== '0x0' } +PendingTx.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) { + const numBN = new BN(numerator) + const denomBN = new BN(denominator) + return targetBN.mul(numBN).div(denomBN) +} + function forwardCarrat () { return ( h('img', { diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 008627ce6..c002019e2 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -29,6 +29,7 @@ function mapStateToProps (state) { provider: state.metamask.provider, conversionRate: state.metamask.conversionRate, currentCurrency: state.metamask.currentCurrency, + blockGasLimit: state.metamask.currentBlockGasLimit, } } @@ -40,7 +41,7 @@ function ConfirmTxScreen () { ConfirmTxScreen.prototype.render = function () { const props = this.props const { network, provider, unapprovedTxs, currentCurrency, - unapprovedMsgs, unapprovedPersonalMsgs, conversionRate } = props + unapprovedMsgs, unapprovedPersonalMsgs, conversionRate, blockGasLimit } = props var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) @@ -106,6 +107,7 @@ ConfirmTxScreen.prototype.render = function () { identities: props.identities, conversionRate, currentCurrency, + blockGasLimit, // Actions buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), sendTransaction: this.sendTransaction.bind(this), -- cgit v1.2.3 From 8d8eb0d8adb5edbfb9f34ed9bacd28ffd03b1e1d Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Fri, 2 Jun 2017 15:20:41 -0700 Subject: bump changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e7b292b5..7f8b8ad0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Update gasLimit params with every new block seen. + ## 3.7.4 2017-6-2 - Fix bug with inflight cache that caused some block lookups to return bad values (affected OasisDex). -- cgit v1.2.3 From ec097c8e3473826f29d988bb6e754345f494913e Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Sun, 4 Jun 2017 22:13:28 -0700 Subject: Add copy links to mini tx panels --- ui/app/components/pending-tx.js | 48 ++++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index b46f715bc..4a62746d6 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -9,6 +9,8 @@ const BN = ethUtil.BN const hexToBn = require('../../../app/scripts/lib/hex-to-bn') const MiniAccountPanel = require('./mini-account-panel') +const Tooltip = require('./tooltip') +const copyToClipboard = require('copy-to-clipboard') const EthBalance = require('./eth-balance') const util = require('../util') const addressSummary = util.addressSummary @@ -93,11 +95,23 @@ PendingTx.prototype.render = function () { fontFamily: 'Montserrat Bold, Montserrat, sans-serif', }, }, identity.name), - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, addressSummary(address, 6, 4, false)), + + h(Tooltip, { + title: 'Copy address', + position: 'bottom', + }, [ + h('span.font-small', { + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(ethUtil.toChecksumAddress(address)) + }, + style: { + cursor: 'pointer', + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, addressSummary(address, 6, 4, false)), + ]), h('span.font-small', { style: { @@ -322,16 +336,30 @@ PendingTx.prototype.miniAccountPanelForRecipient = function () { imageSeed: txParams.to, picOrder: 'left', }, [ + h('span.font-small', { style: { fontFamily: 'Montserrat Bold, Montserrat, sans-serif', }, }, nameForAddress(txParams.to, props.identities)), - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, addressSummary(txParams.to, 6, 4, false)), + + h(Tooltip, { + title: 'Copy address', + position: 'bottom', + }, [ + h('span.font-small', { + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(ethUtil.toChecksumAddress(txParams.to)) + }, + style: { + cursor: 'pointer', + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, addressSummary(txParams.to, 6, 4, false)), + ]), + ]) } else { return h(MiniAccountPanel, { -- cgit v1.2.3 From 773b36b0de5613f1f6bda1caba08ee240a14ab32 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Sun, 4 Jun 2017 22:21:37 -0700 Subject: Move address copying into reusable component "copyable" component allows any elements to be wrapped to include: - a tool tip that changes/debounces its label when clicked. - a customizable copyable value. Fixes #1539 --- ui/app/components/copyable.js | 49 +++++++++++++++++++++++++++++++++++++++++ ui/app/components/pending-tx.js | 25 +++++---------------- 2 files changed, 54 insertions(+), 20 deletions(-) create mode 100644 ui/app/components/copyable.js diff --git a/ui/app/components/copyable.js b/ui/app/components/copyable.js new file mode 100644 index 000000000..9b785a77e --- /dev/null +++ b/ui/app/components/copyable.js @@ -0,0 +1,49 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const Tooltip = require('./tooltip') +const copyToClipboard = require('copy-to-clipboard') + +module.exports = Copyable + +inherits(Copyable, Component) +function Copyable () { + Component.call(this) + this.state = { + copied: false, + } +} + +Copyable.prototype.render = function () { + const props = this.props + const state = this.state + const { value, children } = props + const { copied } = state + + return h(Tooltip, { + title: copied ? 'Copied!' : 'Copy', + position: 'bottom', + style: { + cursor: 'pointer', + }, + }, h('span', { + style: { + cursor: 'pointer', + }, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(value) + this.debounceRestore() + }, + }, children)) +} + +Copyable.prototype.debounceRestore = function () { + this.setState({ copied: true }) + clearTimeout(this.timeout) + this.timeout = setTimeout(() => { + this.setState({ copied: false }) + }, 850) +} diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 4a62746d6..4961db5de 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -9,8 +9,7 @@ const BN = ethUtil.BN const hexToBn = require('../../../app/scripts/lib/hex-to-bn') const MiniAccountPanel = require('./mini-account-panel') -const Tooltip = require('./tooltip') -const copyToClipboard = require('copy-to-clipboard') +const Copyable = require('./copyable') const EthBalance = require('./eth-balance') const util = require('../util') const addressSummary = util.addressSummary @@ -96,18 +95,11 @@ PendingTx.prototype.render = function () { }, }, identity.name), - h(Tooltip, { - title: 'Copy address', - position: 'bottom', + h(Copyable, { + value: ethUtil.toChecksumAddress(address), }, [ h('span.font-small', { - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - copyToClipboard(ethUtil.toChecksumAddress(address)) - }, style: { - cursor: 'pointer', fontFamily: 'Montserrat Light, Montserrat, sans-serif', }, }, addressSummary(address, 6, 4, false)), @@ -343,18 +335,11 @@ PendingTx.prototype.miniAccountPanelForRecipient = function () { }, }, nameForAddress(txParams.to, props.identities)), - h(Tooltip, { - title: 'Copy address', - position: 'bottom', + h(Copyable, { + value: ethUtil.toChecksumAddress(txParams.to), }, [ h('span.font-small', { - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - copyToClipboard(ethUtil.toChecksumAddress(txParams.to)) - }, style: { - cursor: 'pointer', fontFamily: 'Montserrat Light, Montserrat, sans-serif', }, }, addressSummary(txParams.to, 6, 4, false)), -- cgit v1.2.3 From 8dc6aa9c4c4f11e08eee0688c210324b313b710b Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Sun, 4 Jun 2017 22:26:32 -0700 Subject: Remove dead style code --- ui/app/components/copyable.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/ui/app/components/copyable.js b/ui/app/components/copyable.js index 9b785a77e..a4f6f4bc6 100644 --- a/ui/app/components/copyable.js +++ b/ui/app/components/copyable.js @@ -24,9 +24,6 @@ Copyable.prototype.render = function () { return h(Tooltip, { title: copied ? 'Copied!' : 'Copy', position: 'bottom', - style: { - cursor: 'pointer', - }, }, h('span', { style: { cursor: 'pointer', -- cgit v1.2.3 From bb6e41963d42a91ecc34a728b7c0c18d26e6cd9f Mon Sep 17 00:00:00 2001 From: frankiebee Date: Mon, 5 Jun 2017 11:40:20 -0700 Subject: Dissallow transactions to be sent to 0x0000000000000000000000000000000000000000 --- ui/app/components/ens-input.js | 1 + ui/app/components/pending-tx.js | 14 +++++++++++++- ui/app/conf-tx.js | 1 + ui/app/send.js | 2 +- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js index 3e44d83af..11e0fb36d 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/ens-input.js @@ -95,6 +95,7 @@ EnsInput.prototype.lookupEnsName = function () { log.info(`ENS attempting to resolve name: ${recipient}`) this.ens.lookup(recipient.trim()) .then((address) => { + if (address === '0x0000000000000000000000000000000000000000') throw new Error('No address has been set for this name.') if (address !== ensResolution) { this.setState({ loadingEns: false, diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index b46f715bc..e8bf32d92 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -44,6 +44,9 @@ PendingTx.prototype.render = function () { const account = props.accounts[address] const balance = account ? account.balance : '0x0' + // recipient check + const isValidAddress = !(txParams.to === '0x0000000000000000000000000000000000000000') + // Gas const gas = txParams.gas const gasBn = hexToBn(gas) @@ -261,6 +264,15 @@ PendingTx.prototype.render = function () { }, 'Transaction Error. Exception thrown in contract code.') : null, + !isValidAddress ? + h('.error', { + style: { + marginLeft: 50, + fontSize: '0.9em', + }, + }, 'Recipient address is invalid sending this transaction will result in a loss of ETH.') + : null, + insufficientBalance ? h('span.error', { style: { @@ -298,7 +310,7 @@ PendingTx.prototype.render = function () { type: 'submit', value: 'ACCEPT', style: { marginLeft: '10px' }, - disabled: insufficientBalance || !this.state.valid, + disabled: insufficientBalance || !this.state.valid || !isValidAddress, }), h('button.cancel.btn-red', { diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 008627ce6..4ae81f35f 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -48,6 +48,7 @@ ConfirmTxScreen.prototype.render = function () { var txParams = txData.params || {} var isNotification = isPopupOrNotification() === 'notification' + log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`) if (unconfTxList.length === 0) return h(Loading, { isLoading: true }) diff --git a/ui/app/send.js b/ui/app/send.js index fd6994145..e0896035e 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -262,7 +262,7 @@ SendTransactionScreen.prototype.onSubmit = function () { return this.props.dispatch(actions.displayWarning(message)) } - if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) { + if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData) || (recipient === '0x0000000000000000000000000000000000000000')) { message = 'Recipient address is invalid.' return this.props.dispatch(actions.displayWarning(message)) } -- cgit v1.2.3 From 37fd32025f9cb5dffb601011e2442efee59e3595 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Mon, 5 Jun 2017 12:00:01 -0700 Subject: Fix punctuation --- ui/app/components/pending-tx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index e8bf32d92..2d4dc26a2 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -270,7 +270,7 @@ PendingTx.prototype.render = function () { marginLeft: 50, fontSize: '0.9em', }, - }, 'Recipient address is invalid sending this transaction will result in a loss of ETH.') + }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.') : null, insufficientBalance ? -- cgit v1.2.3 From 653319be1055b8ff0a36cb334c93ac7435f1fc5c Mon Sep 17 00:00:00 2001 From: frankiebee Date: Mon, 5 Jun 2017 12:09:19 -0700 Subject: move address check to util.isValidAddress --- ui/app/components/pending-tx.js | 4 ++-- ui/app/send.js | 2 +- ui/app/util.js | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 2d4dc26a2..56c466506 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -7,7 +7,7 @@ const clone = require('clone') const ethUtil = require('ethereumjs-util') const BN = ethUtil.BN const hexToBn = require('../../../app/scripts/lib/hex-to-bn') - +const util = require('../util') const MiniAccountPanel = require('./mini-account-panel') const EthBalance = require('./eth-balance') const util = require('../util') @@ -45,7 +45,7 @@ PendingTx.prototype.render = function () { const balance = account ? account.balance : '0x0' // recipient check - const isValidAddress = !(txParams.to === '0x0000000000000000000000000000000000000000') + const isValidAddress = util.isValidAddress(txParams.to) // Gas const gas = txParams.gas diff --git a/ui/app/send.js b/ui/app/send.js index e0896035e..75a600dee 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -262,7 +262,7 @@ SendTransactionScreen.prototype.onSubmit = function () { return this.props.dispatch(actions.displayWarning(message)) } - if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData) || (recipient === '0x0000000000000000000000000000000000000000')) { + if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData) { message = 'Recipient address is invalid.' return this.props.dispatch(actions.displayWarning(message)) } diff --git a/ui/app/util.js b/ui/app/util.js index 7a56bf6a0..ac3f42c6b 100644 --- a/ui/app/util.js +++ b/ui/app/util.js @@ -61,6 +61,7 @@ function miniAddressSummary (address) { function isValidAddress (address) { var prefixed = ethUtil.addHexPrefix(address) + if (address === '0x0000000000000000000000000000000000000000') return false return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed) } -- cgit v1.2.3 From 0f69a09823928ec6aaf5189cf4d4f50c52c9debb Mon Sep 17 00:00:00 2001 From: frankiebee Date: Mon, 5 Jun 2017 12:22:02 -0700 Subject: Fix linting error --- ui/app/components/pending-tx.js | 1 - ui/app/send.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 56c466506..18c378781 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -10,7 +10,6 @@ const hexToBn = require('../../../app/scripts/lib/hex-to-bn') const util = require('../util') const MiniAccountPanel = require('./mini-account-panel') const EthBalance = require('./eth-balance') -const util = require('../util') const addressSummary = util.addressSummary const nameForAddress = require('../../lib/contract-namer') const BNInput = require('./bn-as-decimal-input') diff --git a/ui/app/send.js b/ui/app/send.js index 75a600dee..fd6994145 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -262,7 +262,7 @@ SendTransactionScreen.prototype.onSubmit = function () { return this.props.dispatch(actions.displayWarning(message)) } - if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData) { + if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) { message = 'Recipient address is invalid.' return this.props.dispatch(actions.displayWarning(message)) } -- cgit v1.2.3 From ec99bfd5531922e7d153709c1a77f1c40cae9e99 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Mon, 5 Jun 2017 12:37:29 -0700 Subject: set the ensResolution to an invalid address if an error ocurs durring look up --- ui/app/components/ens-input.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js index 11e0fb36d..16a3a684c 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/ens-input.js @@ -109,6 +109,7 @@ EnsInput.prototype.lookupEnsName = function () { log.error(reason) return this.setState({ loadingEns: false, + ensResolution: '0x0000000000000000000000000000000000000000', ensFailure: true, hoverText: reason.message, }) -- cgit v1.2.3 From 94fedd1fc9d44054670b9f60ae6f92b409e7b25e Mon Sep 17 00:00:00 2001 From: frankiebee Date: Mon, 5 Jun 2017 13:00:15 -0700 Subject: Fix for quick switch on ENS names --- ui/app/components/ens-input.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js index 16a3a684c..43bb7ab22 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/ens-input.js @@ -21,6 +21,7 @@ EnsInput.prototype.render = function () { const opts = extend(props, { list: 'addresses', onChange: () => { + this.setState({ ensResolution: '0x0000000000000000000000000000000000000000' }) const network = this.props.network const networkHasEnsSupport = getNetworkEnsSupport(network) if (!networkHasEnsSupport) return @@ -102,6 +103,7 @@ EnsInput.prototype.lookupEnsName = function () { ensResolution: address, nickname: recipient.trim(), hoverText: address + '\nClick to Copy', + ensFailure: false, }) } }) -- cgit v1.2.3 From c92afef91dc08982ab12b19fcc81c14439aaa808 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 5 Jun 2017 13:40:26 -0700 Subject: Version 3.7.5 --- CHANGELOG.md | 6 ++++++ app/manifest.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e7b292b5..14a6ef3a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Current Master +## 3.7.5 2017-6-5 + +- Prevent users from sending to the `0x0` address. +- Provide useful errors when entering bad characters in ENS name. +- Add ability to copy addresses from transaction confirmation view. + ## 3.7.4 2017-6-2 - Fix bug with inflight cache that caused some block lookups to return bad values (affected OasisDex). diff --git a/app/manifest.json b/app/manifest.json index 99a083f0f..acdb3795e 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.7.4", + "version": "3.7.5", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From e99ce3763ab81ed94eb22db66aadef343c183a00 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 5 Jun 2017 13:42:41 -0700 Subject: Add publishing guide to readme --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 821e1cdfd..7997cb52b 100644 --- a/README.md +++ b/README.md @@ -193,3 +193,8 @@ You will need: + An RPC Endpoint url + An explorer link + CSS for the display icon + +## Other Guides + +- [Publishing Guide](./docs/publishing.md) + -- cgit v1.2.3 From 7f991e5574b7247f7b2f4c0f14e3277200c3e526 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 5 Jun 2017 13:46:18 -0700 Subject: Add publishing guide --- docs/publishing.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 docs/publishing.md diff --git a/docs/publishing.md b/docs/publishing.md new file mode 100644 index 000000000..e546f327b --- /dev/null +++ b/docs/publishing.md @@ -0,0 +1,10 @@ +# Publishing Guide + +When publishing a new version of MetaMask, we follow this procedure: + +1. `npm run dist` to generate the latest build. +2. Publish to chrome store. +3. Publish to firefox addon marketplace. +4. Post on Github releases page. +5. `npm run announce`, post that announcement in our public places. + -- cgit v1.2.3 From d0144c285308da1dfe04dc07d0be377a81b094cc Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 5 Jun 2017 13:55:48 -0700 Subject: Break docs up into individual files --- README.md | 168 ++++---------------------------------- docs/add-to-chrome.md | 14 ++++ docs/add-to-firef.md | 14 ++++ docs/adding-new-networks.md | 25 ++++++ docs/developing-on-deps.md | 10 +++ docs/development-visualization.md | 35 ++++++++ docs/notices.md | 15 ++++ docs/publishing.md | 9 ++ docs/ui-dev-mode.md | 6 ++ docs/ui-mock-mode.md | 8 ++ 10 files changed, 153 insertions(+), 151 deletions(-) create mode 100644 docs/add-to-chrome.md create mode 100644 docs/add-to-firef.md create mode 100644 docs/adding-new-networks.md create mode 100644 docs/developing-on-deps.md create mode 100644 docs/development-visualization.md create mode 100644 docs/notices.md create mode 100644 docs/ui-dev-mode.md create mode 100644 docs/ui-mock-mode.md diff --git a/README.md b/README.md index 7997cb52b..afeb96ae5 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,15 @@ If you're a web dapp developer, we've got two types of guides for you: Uncompressed builds can be found in `/dist`, compressed builds can be found in `/builds` once they're built. -## Installing Local Builds on Chrome +### Running Tests -To install your locally built extension on Chrome, [follow this guide](http://stackoverflow.com/a/24577660/272576). +Requires `mocha` installed. Run `npm install -g mocha`. -The built extension is stored in `./dist/chrome/`. +Then just run `npm test`. + +You can also test with a continuously watching process, via `npm run watch`. + +You can run the linter by itself with `gulp lint`. ## Architecture @@ -41,160 +45,22 @@ npm start npm run dist ``` -#### In Chrome - -Open `Settings` > `Extensions`. - -Check "Developer mode". - -At the top, click `Load Unpacked Extension`. - -Navigate to your `metamask-plugin/dist/chrome` folder. - -Click `Select`. - -You now have the plugin, and can click 'inspect views: background plugin' to view its dev console. - -#### In Firefox - -Go to the url `about:debugging`. - -Click the button `Load Temporary Add-On`. - -Select the file `dist/firefox/manifest.json`. - -You can optionally enable debugging, and click `Debug`, for a console window that logs all of Metamask's processes to a single console. - -If you have problems debugging, try connecting to the IRC channel `#webextensions` on `irc.mozilla.org`. - -For longer questions, use the StackOverfow tag `firefox-addons`. - -### Developing on UI Only - -You can run `npm run ui`, and your browser should open a live-reloading demo version of the plugin UI. - -Some actions will crash the app, so this is only for tuning aesthetics, but it allows live-reloading styles, which is a much faster feedback loop than reloading the full extension. - -### Developing on UI with Mocked Background Process - -You can run `npm run mock` and your browser should open a live-reloading demo version of the plugin UI, just like the `npm run ui`, except that it tries to actually perform all normal operations. - -It does not yet connect to a real blockchain (this could be a good test feature later, connecting to a test blockchain), so only local operations work. - -You can reset the mock ui at any time with the `Reset` button at the top of the screen. - -### Developing on Dependencies - -To enjoy the live-reloading that `gulp dev` offers while working on the `web3-provider-engine` or other dependencies: - - 1. Clone the dependency locally. - 2. `npm install` in its folder. - 3. Run `npm link` in its folder. - 4. Run `npm link $DEP_NAME` in this project folder. - 5. Next time you `npm start` it will watch the dependency for changes as well! - -### Running Tests - -Requires `mocha` installed. Run `npm install -g mocha`. - -Then just run `npm test`. - -You can also test with a continuously watching process, via `npm run watch`. - -You can run the linter by itself with `gulp lint`. - #### Writing Browser Tests To write tests that will be run in the browser using QUnit, add your test files to `test/integration/lib`. -### Deploying the UI +## Other Docs - You must be authorized already on the MetaMask plugin. - - 0. Update the version in `app/manifest.json` and the Changelog in `CHANGELOG.md`. - 1. Visit [the chrome developer dashboard](https://chrome.google.com/webstore/developer/dashboard?authuser=2). - 2. Run `gulp dist` (or `gulp zip` if you've already built) - 3. Upload the latest zip file from `builds/metamask-$PLATFORM-$VERSION.zip` as the updated package. +- [How to add custom build to Chrome](./docs/add-to-chrome.md) +- [How to add custom build to Firefox](./docs/add-to-firefox.md) +- [How to develop a live-reloading UI](./docs/ui-dev-mode.md) +- [Publishing Guide](./docs/publishing.md) +- [How to develop an in-browser mocked UI](./docs/ui-mock-mode.md) +- [How to live reload on local dependency changes](./docs/developing-on-deps.md) +- [How to add new networks to the Provider Menu](./docs/adding-new-networks.md) +- [How to manage notices that appear when the app starts up](./docs/notices.md) +- [How to generate a visualization of this repository's development](./docs/development-visualization.md) [1]: http://www.nomnoml.com/#view/%5B%3Cactor%3Euser%5D%0A%0A%5Bmetamask-ui%7C%0A%20%20%20%5Btools%7C%0A%20%20%20%20%20react%0A%20%20%20%20%20redux%0A%20%20%20%20%20thunk%0A%20%20%20%20%20ethUtils%0A%20%20%20%20%20jazzicon%0A%20%20%20%5D%0A%20%20%20%5Bcomponents%7C%0A%20%20%20%20%20app%0A%20%20%20%20%20account-detail%0A%20%20%20%20%20accounts%0A%20%20%20%20%20locked-screen%0A%20%20%20%20%20restore-vault%0A%20%20%20%20%20identicon%0A%20%20%20%20%20config%0A%20%20%20%20%20info%0A%20%20%20%5D%0A%20%20%20%5Breducers%7C%0A%20%20%20%20%20app%0A%20%20%20%20%20metamask%0A%20%20%20%20%20identities%0A%20%20%20%5D%0A%20%20%20%5Bactions%7C%0A%20%20%20%20%20%5BaccountManager%5D%0A%20%20%20%5D%0A%20%20%20%5Bcomponents%5D%3A-%3E%5Bactions%5D%0A%20%20%20%5Bactions%5D%3A-%3E%5Breducers%5D%0A%20%20%20%5Breducers%5D%3A-%3E%5Bcomponents%5D%0A%5D%0A%0A%5Bweb%20dapp%7C%0A%20%20%5Bui%20code%5D%0A%20%20%5Bweb3%5D%0A%20%20%5Bmetamask-inpage%5D%0A%20%20%0A%20%20%5B%3Cactor%3Eui%20developer%5D%0A%20%20%5Bui%20developer%5D-%3E%5Bui%20code%5D%0A%20%20%5Bui%20code%5D%3C-%3E%5Bweb3%5D%0A%20%20%5Bweb3%5D%3C-%3E%5Bmetamask-inpage%5D%0A%5D%0A%0A%5Bmetamask-background%7C%0A%20%20%5Bprovider-engine%5D%0A%20%20%5Bhooked%20wallet%20subprovider%5D%0A%20%20%5Bid%20store%5D%0A%20%20%0A%20%20%5Bprovider-engine%5D%3C-%3E%5Bhooked%20wallet%20subprovider%5D%0A%20%20%5Bhooked%20wallet%20subprovider%5D%3C-%3E%5Bid%20store%5D%0A%20%20%5Bconfig%20manager%7C%0A%20%20%20%20%5Brpc%20configuration%5D%0A%20%20%20%20%5Bencrypted%20keys%5D%0A%20%20%20%20%5Bwallet%20nicknames%5D%0A%20%20%5D%0A%20%20%0A%20%20%5Bprovider-engine%5D%3C-%5Bconfig%20manager%5D%0A%20%20%5Bid%20store%5D%3C-%3E%5Bconfig%20manager%5D%0A%5D%0A%0A%5Buser%5D%3C-%3E%5Bmetamask-ui%5D%0A%0A%5Buser%5D%3C%3A--%3A%3E%5Bweb%20dapp%5D%0A%0A%5Bmetamask-contentscript%7C%0A%20%20%5Bplugin%20restart%20detector%5D%0A%20%20%5Brpc%20passthrough%5D%0A%5D%0A%0A%5Brpc%20%7C%0A%20%20%5Bethereum%20blockchain%20%7C%0A%20%20%20%20%5Bcontracts%5D%0A%20%20%20%20%5Baccounts%5D%0A%20%20%5D%0A%5D%0A%0A%5Bweb%20dapp%5D%3C%3A--%3A%3E%5Bmetamask-contentscript%5D%0A%5Bmetamask-contentscript%5D%3C-%3E%5Bmetamask-background%5D%0A%5Bmetamask-background%5D%3C-%3E%5Bmetamask-ui%5D%0A%5Bmetamask-background%5D%3C-%3E%5Brpc%5D%0A -### Generate Development Visualization - -This will generate a video of the repo commit history. - -Install preqs: -``` -brew install gource -brew install ffmpeg -``` - -From the repo dir, pipe `gource` into `ffmpeg`: -``` -gource \ - --seconds-per-day .1 \ - --user-scale 1.5 \ - --default-user-image "./images/icon-512.png" \ - --viewport 1280x720 \ - --auto-skip-seconds .1 \ - --multi-sampling \ - --stop-at-end \ - --highlight-users \ - --hide mouse,progress \ - --file-idle-time 0 \ - --max-files 0 \ - --background-colour 000000 \ - --font-size 18 \ - --date-format "%b %d, %Y" \ - --highlight-dirs \ - --user-friction 0.1 \ - --title "MetaMask Development History" \ - --output-ppm-stream - \ - --output-framerate 30 \ - | ffmpeg -y -r 30 -f image2pipe -vcodec ppm -i - -b 65536K metamask-dev-history.mp4 -``` - -## Generating Notices - -To add a notice: -``` -npm run generateNotice -``` -Enter the body of your notice into the text editor that pops up, without including the body. Be sure to save the file before closing the window! -Afterwards, enter the title of the notice in the command line and press enter. Afterwards, add and commit the new changes made. - -To delete a notice: -``` -npm run deleteNotice -``` -A list of active notices will pop up. Enter the corresponding id in the command line prompt and add and commit the new changes afterwards. - -## Adding Custom Networks - -To add another network to our dropdown menu, make sure the following files are adjusted properly: - -``` -app/scripts/config.js -app/scripts/lib/buy-eth-url.js -app/scripts/lib/config-manager.js -ui/app/app.js -ui/app/components/buy-button-subview.js -ui/app/components/drop-menu-item.js -ui/app/components/network.js -ui/app/components/transaction-list-item.js -ui/app/config.js -ui/app/css/lib.css -ui/lib/account-link.js -ui/lib/explorer-link.js -``` - -You will need: -+ The network ID -+ An RPC Endpoint url -+ An explorer link -+ CSS for the display icon - -## Other Guides - -- [Publishing Guide](./docs/publishing.md) - diff --git a/docs/add-to-chrome.md b/docs/add-to-chrome.md new file mode 100644 index 000000000..ea5213182 --- /dev/null +++ b/docs/add-to-chrome.md @@ -0,0 +1,14 @@ +## Add Custom Build to Chrome + +Open `Settings` > `Extensions`. + +Check "Developer mode". + +At the top, click `Load Unpacked Extension`. + +Navigate to your `metamask-plugin/dist/chrome` folder. + +Click `Select`. + +You now have the plugin, and can click 'inspect views: background plugin' to view its dev console. + diff --git a/docs/add-to-firef.md b/docs/add-to-firef.md new file mode 100644 index 000000000..593d06170 --- /dev/null +++ b/docs/add-to-firef.md @@ -0,0 +1,14 @@ +# Add Custom Build to Firefox + +Go to the url `about:debugging`. + +Click the button `Load Temporary Add-On`. + +Select the file `dist/firefox/manifest.json`. + +You can optionally enable debugging, and click `Debug`, for a console window that logs all of Metamask's processes to a single console. + +If you have problems debugging, try connecting to the IRC channel `#webextensions` on `irc.mozilla.org`. + +For longer questions, use the StackOverfow tag `firefox-addons`. + diff --git a/docs/adding-new-networks.md b/docs/adding-new-networks.md new file mode 100644 index 000000000..ea1453c21 --- /dev/null +++ b/docs/adding-new-networks.md @@ -0,0 +1,25 @@ +## Adding Custom Networks + +To add another network to our dropdown menu, make sure the following files are adjusted properly: + +``` +app/scripts/config.js +app/scripts/lib/buy-eth-url.js +app/scripts/lib/config-manager.js +ui/app/app.js +ui/app/components/buy-button-subview.js +ui/app/components/drop-menu-item.js +ui/app/components/network.js +ui/app/components/transaction-list-item.js +ui/app/config.js +ui/app/css/lib.css +ui/lib/account-link.js +ui/lib/explorer-link.js +``` + +You will need: ++ The network ID ++ An RPC Endpoint url ++ An explorer link ++ CSS for the display icon + diff --git a/docs/developing-on-deps.md b/docs/developing-on-deps.md new file mode 100644 index 000000000..7de3f67a8 --- /dev/null +++ b/docs/developing-on-deps.md @@ -0,0 +1,10 @@ +### Developing on Dependencies + +To enjoy the live-reloading that `gulp dev` offers while working on the `web3-provider-engine` or other dependencies: + + 1. Clone the dependency locally. + 2. `npm install` in its folder. + 3. Run `npm link` in its folder. + 4. Run `npm link $DEP_NAME` in this project folder. + 5. Next time you `npm start` it will watch the dependency for changes as well! + diff --git a/docs/development-visualization.md b/docs/development-visualization.md new file mode 100644 index 000000000..95847300d --- /dev/null +++ b/docs/development-visualization.md @@ -0,0 +1,35 @@ +### Generate Development Visualization + +This will generate a video of the repo commit history. + +Install preqs: +``` +brew install gource +brew install ffmpeg +``` + +From the repo dir, pipe `gource` into `ffmpeg`: +``` +gource \ + --seconds-per-day .1 \ + --user-scale 1.5 \ + --default-user-image "./images/icon-512.png" \ + --viewport 1280x720 \ + --auto-skip-seconds .1 \ + --multi-sampling \ + --stop-at-end \ + --highlight-users \ + --hide mouse,progress \ + --file-idle-time 0 \ + --max-files 0 \ + --background-colour 000000 \ + --font-size 18 \ + --date-format "%b %d, %Y" \ + --highlight-dirs \ + --user-friction 0.1 \ + --title "MetaMask Development History" \ + --output-ppm-stream - \ + --output-framerate 30 \ + | ffmpeg -y -r 30 -f image2pipe -vcodec ppm -i - -b 65536K metamask-dev-history.mp4 +``` + diff --git a/docs/notices.md b/docs/notices.md new file mode 100644 index 000000000..826e6e84e --- /dev/null +++ b/docs/notices.md @@ -0,0 +1,15 @@ +## Generating Notices + +To add a notice: +``` +npm run generateNotice +``` +Enter the body of your notice into the text editor that pops up, without including the body. Be sure to save the file before closing the window! +Afterwards, enter the title of the notice in the command line and press enter. Afterwards, add and commit the new changes made. + +To delete a notice: +``` +npm run deleteNotice +``` +A list of active notices will pop up. Enter the corresponding id in the command line prompt and add and commit the new changes afterwards. + diff --git a/docs/publishing.md b/docs/publishing.md index e546f327b..00369acf9 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -2,6 +2,15 @@ When publishing a new version of MetaMask, we follow this procedure: +## Incrementing Version & Changelog + + You must be authorized already on the MetaMask plugin. + +1. Update the version in `app/manifest.json` and the Changelog in `CHANGELOG.md`. +2. Visit [the chrome developer dashboard](https://chrome.google.com/webstore/developer/dashboard?authuser=2). + +## Publishing + 1. `npm run dist` to generate the latest build. 2. Publish to chrome store. 3. Publish to firefox addon marketplace. diff --git a/docs/ui-dev-mode.md b/docs/ui-dev-mode.md new file mode 100644 index 000000000..df49d8b04 --- /dev/null +++ b/docs/ui-dev-mode.md @@ -0,0 +1,6 @@ +# Running UI Dev Mode + +You can run `npm run ui`, and your browser should open a live-reloading demo version of the plugin UI. + +Some actions will crash the app, so this is only for tuning aesthetics, but it allows live-reloading styles, which is a much faster feedback loop than reloading the full extension. + diff --git a/docs/ui-mock-mode.md b/docs/ui-mock-mode.md new file mode 100644 index 000000000..bb54dc471 --- /dev/null +++ b/docs/ui-mock-mode.md @@ -0,0 +1,8 @@ +### Developing on UI with Mocked Background Process + +You can run `npm run mock` and your browser should open a live-reloading demo version of the plugin UI, just like the `npm run ui`, except that it tries to actually perform all normal operations. + +It does not yet connect to a real blockchain (this could be a good test feature later, connecting to a test blockchain), so only local operations work. + +You can reset the mock ui at any time with the `Reset` button at the top of the screen. + -- cgit v1.2.3 From c8f0802a8e63ca29c643d08ebc1b777520645246 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 5 Jun 2017 15:35:52 -0700 Subject: Fix bug that prevented publishing contracts --- CHANGELOG.md | 2 ++ ui/app/components/pending-tx.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14a6ef3a7..6b0f27e72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Fix bug that prevented publishing contracts. + ## 3.7.5 2017-6-5 - Prevent users from sending to the `0x0` address. diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index a106f245b..e3b307b0b 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -45,7 +45,7 @@ PendingTx.prototype.render = function () { const balance = account ? account.balance : '0x0' // recipient check - const isValidAddress = util.isValidAddress(txParams.to) + const isValidAddress = !txParams.to || util.isValidAddress(txParams.to) // Gas const gas = txParams.gas -- cgit v1.2.3 From 838ffb62ee59a36d1dbfdafca0dc6727aeb6985d Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 5 Jun 2017 15:36:18 -0700 Subject: Version 3.7.6 --- CHANGELOG.md | 2 ++ app/manifest.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b0f27e72..cbab1c71a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +## 3.7.6 2017-6-5 + - Fix bug that prevented publishing contracts. ## 3.7.5 2017-6-5 diff --git a/app/manifest.json b/app/manifest.json index acdb3795e..2d321e862 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.7.5", + "version": "3.7.6", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From 203a573f3fa8cf636c846a0a467318f6767d05fe Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Mon, 5 Jun 2017 16:23:56 -0700 Subject: Use new URL for currency API from cryptonator. --- app/manifest.json | 2 +- app/scripts/controllers/currency.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/manifest.json b/app/manifest.json index 99a083f0f..01ee173a6 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -58,7 +58,7 @@ "storage", "clipboardWrite", "http://localhost:8545/", - "https://www.cryptonator.com/" + "https://api.cryptonator.com/" ], "web_accessible_resources": [ "scripts/inpage.js" diff --git a/app/scripts/controllers/currency.js b/app/scripts/controllers/currency.js index fb130ed76..1f20dc005 100644 --- a/app/scripts/controllers/currency.js +++ b/app/scripts/controllers/currency.js @@ -45,7 +45,7 @@ class CurrencyController { updateConversionRate () { const currentCurrency = this.getCurrentCurrency() - return fetch(`https://www.cryptonator.com/api/ticker/eth-${currentCurrency}`) + return fetch(`https://api.cryptonator.com/api/ticker/eth-${currentCurrency}`) .then(response => response.json()) .then((parsedResponse) => { this.setConversionRate(Number(parsedResponse.ticker.price)) -- cgit v1.2.3 From f7773538ebb583905b5385096abea48c58e7673c Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Mon, 5 Jun 2017 16:24:32 -0700 Subject: Modify tests for new api mock.' --- test/unit/currency-controller-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/currency-controller-test.js b/test/unit/currency-controller-test.js index cfbce7fb3..5eeaf9bcc 100644 --- a/test/unit/currency-controller-test.js +++ b/test/unit/currency-controller-test.js @@ -36,7 +36,7 @@ describe('currency-controller', function () { describe('#updateConversionRate', function () { it('should retrieve an update for ETH to USD and set it in memory', function (done) { this.timeout(15000) - nock('https://www.cryptonator.com') + nock('https://api.cryptonator.com') .get('/api/ticker/eth-USD') .reply(200, '{"ticker":{"base":"ETH","target":"USD","price":"11.02456145","volume":"44948.91745289","change":"-0.01472534"},"timestamp":1472072136,"success":true,"error":""}') @@ -57,7 +57,7 @@ describe('currency-controller', function () { this.timeout(15000) assert.equal(currencyController.getConversionRate(), 0) - nock('https://www.cryptonator.com') + nock('https://api.cryptonator.com') .get('/api/ticker/eth-JPY') .reply(200, '{"ticker":{"base":"ETH","target":"JPY","price":"11.02456145","volume":"44948.91745289","change":"-0.01472534"},"timestamp":1472072136,"success":true,"error":""}') -- cgit v1.2.3 From 0fb38632befc8a09dc12de8f609edfb0ffab9d35 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Mon, 5 Jun 2017 16:25:02 -0700 Subject: Bump changelog. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e7b292b5..52d187ccf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Fix currency API URL from cryptonator. + ## 3.7.4 2017-6-2 - Fix bug with inflight cache that caused some block lookups to return bad values (affected OasisDex). -- cgit v1.2.3 From f788ba7244d78a60233b9516423524ebd931fbb6 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Mon, 5 Jun 2017 16:27:15 -0700 Subject: Resolve changelog merging. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d9de93d3..7bfa441b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Current Master - Fix currency API URL from cryptonator. + ## 3.7.6 2017-6-5 - Fix bug that prevented publishing contracts. -- cgit v1.2.3 From a37c8f3ed07d62948f26444ddce6ac148ed6e458 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Thu, 8 Jun 2017 16:02:23 -0700 Subject: Modify FAQ to be more visible. --- ui/app/app.js | 2 +- ui/app/info.js | 19 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index 6e5aa57cd..53dbc3354 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -341,7 +341,7 @@ App.prototype.renderDropdown = function () { }), h(DropMenuItem, { - label: 'Info', + label: 'Info/Help', closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), action: () => this.props.dispatch(actions.showInfoPage()), icon: h('i.fa.fa-question.fa-lg'), diff --git a/ui/app/info.js b/ui/app/info.js index a6fdeb315..aa4503b62 100644 --- a/ui/app/info.js +++ b/ui/app/info.js @@ -52,7 +52,7 @@ InfoScreen.prototype.render = function () { h('div', { style: { - marginBottom: '10px', + marginBottom: '5px', }}, [ h('div', [ @@ -87,7 +87,7 @@ InfoScreen.prototype.render = function () { h('hr', { style: { - margin: '20px 0 ', + margin: '10px 0 ', width: '7em', }, }), @@ -97,6 +97,13 @@ InfoScreen.prototype.render = function () { paddingLeft: '30px', }}, [ + h('div.fa.fa-github', [ + h('a.info', { + href: 'https://github.com/MetaMask/faq', + target: '_blank', + onClick (event) { this.navigateTo(event.target.href) }, + }, 'Need Help? Read our FAQ!'), + ]), h('div', [ h('a', { href: 'https://metamask.io/', @@ -138,14 +145,6 @@ InfoScreen.prototype.render = function () { onClick () { this.navigateTo('mailto:help@metamask.io?subject=Feedback') }, }, 'Email us!'), ]), - - h('div.fa.fa-github', [ - h('a.info', { - href: 'https://github.com/MetaMask/metamask-plugin/issues', - target: '_blank', - onClick (event) { this.navigateTo(event.target.href) }, - }, 'Start a thread on GitHub'), - ]), ]), ]), ]), -- cgit v1.2.3 From 57a7fc4425fbd87a89d4bc5c61eac3dc1ffa250e Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 8 Jun 2017 16:07:05 -0700 Subject: deps - bump provider engine for warp feature --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9efba3866..c6c3add8e 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "valid-url": "^1.0.9", "vreme": "^3.0.2", "web3": "0.18.2", - "web3-provider-engine": "^12.1.0", + "web3-provider-engine": "^12.2.3", "web3-stream-provider": "^2.0.6", "xtend": "^4.0.1" }, -- cgit v1.2.3 From f994461763057a15a51b69b805faacd891e08db4 Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 8 Jun 2017 16:21:16 -0700 Subject: changelog - note on block-tracker warp --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbab1c71a..1e98b17d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Fix bug where metamask would show old data after computer being asleep or disconnected from the internet. + ## 3.7.6 2017-6-5 - Fix bug that prevented publishing contracts. -- cgit v1.2.3 From 017c7c4c006d3e6041d33ae815d83faf43ec21bb Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 8 Jun 2017 16:42:00 -0700 Subject: 3.7.7 --- app/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/manifest.json b/app/manifest.json index 2d321e862..a0d1500c2 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.7.6", + "version": "3.7.7", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From e9c693eae4d5ea9c44117b26260c7e05261b3eb9 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 8 Jun 2017 16:44:11 -0700 Subject: Version bump changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e98b17d8..b1fa344f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +## 3.7.7 2017-6-8 + - Fix bug where metamask would show old data after computer being asleep or disconnected from the internet. ## 3.7.6 2017-6-5 -- cgit v1.2.3 From 1a70141e8bbfce0881aebd3afe431b3d38880167 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 8 Jun 2017 17:32:50 -0700 Subject: clean up code --- mascara/src/background.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/mascara/src/background.js b/mascara/src/background.js index dff5e6a7c..d9dbf593a 100644 --- a/mascara/src/background.js +++ b/mascara/src/background.js @@ -33,11 +33,6 @@ self.addEventListener('install', function(event) { }) self.addEventListener('activate', function(event) { event.waitUntil(self.clients.claim()) - self.clients.matchAll() - .then((clients) => { - if (connectedClientCount < clients.length) sendMessageToAllClients('reconnect') - }) - }) console.log('inside:open') -- cgit v1.2.3 From 4941b5ab118e4ab99ae94b2a714c7fceab24adcf Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 8 Jun 2017 17:33:27 -0700 Subject: bump cswready event --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9efba3866..c58202ef0 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "bluebird": "^3.5.0", "browser-passworder": "^2.0.3", "browserify-derequire": "^0.9.4", - "client-sw-ready-event": "^3.0.3", + "client-sw-ready-event": "^3.3.0", "clone": "^1.0.2", "copy-to-clipboard": "^2.0.0", "debounce": "^1.0.0", -- cgit v1.2.3 From a0a19468a805994f3897a63489c344e5f3f89dc9 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 8 Jun 2017 17:34:51 -0700 Subject: reload the page when switching networks --- mascara/src/mascara.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mascara/src/mascara.js b/mascara/src/mascara.js index 0fc2868e1..1655d1f64 100644 --- a/mascara/src/mascara.js +++ b/mascara/src/mascara.js @@ -1,6 +1,6 @@ const Web3 = require('web3') const setupProvider = require('./lib/setup-provider.js') - +const setupDappAutoReload = require('../../app/scripts/lib/auto-reload.js') const MASCARA_ORIGIN = process.env.MASCARA_ORIGIN || 'http://localhost:9001' console.log('MASCARA_ORIGIN:', MASCARA_ORIGIN) @@ -14,8 +14,7 @@ const provider = setupProvider({ instrumentForUserInteractionTriggers(provider) const web3 = new Web3(provider) -global.web3 = web3 - +setupDappAutoReload(web3, provider.publicConfigStore) // // ui stuff // -- cgit v1.2.3 From 3d1d38a2c0cd04d637b878d0a0be7f505f406a97 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 8 Jun 2017 17:35:21 -0700 Subject: reload page if ui is not present --- mascara/src/ui.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mascara/src/ui.js b/mascara/src/ui.js index e798847a7..5f9be542f 100644 --- a/mascara/src/ui.js +++ b/mascara/src/ui.js @@ -40,7 +40,6 @@ const connectApp = function (readSw) { }) }) } - background.on('ready', (sw) => { background.removeListener('updatefound', connectApp) connectApp(sw) @@ -48,5 +47,10 @@ background.on('ready', (sw) => { background.on('updatefound', () => window.location.reload()) background.startWorker() -// background.startWorker() +.then(() => { + setTimeout(() => { + const appContent = document.getElementById(`app-content`) + if (!appContent.children.length) window.location.reload() + }, 2000) +}) console.log('hello from MetaMascara ui!') -- cgit v1.2.3 From 2fcf3d843985e0675fc9780043b0331d45ba7190 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Fri, 9 Jun 2017 10:48:28 -0700 Subject: Modify wording to new accept. --- mascara/test/lib/first-time.js | 2 +- test/integration/lib/first-time.js | 2 +- ui/app/components/notice.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mascara/test/lib/first-time.js b/mascara/test/lib/first-time.js index 76a4545bf..e42c9e39d 100644 --- a/mascara/test/lib/first-time.js +++ b/mascara/test/lib/first-time.js @@ -10,7 +10,7 @@ QUnit.test('render init screen', function (assert) { app = $('#app-content').contents() const recurseNotices = function () { let button = app.find('button') - if (button.html() === 'Agree') { + if (button.html() === 'Accept') { let termsPage = app.find('.markdown')[0] termsPage.scrollTop = termsPage.scrollHeight return wait().then(() => { diff --git a/test/integration/lib/first-time.js b/test/integration/lib/first-time.js index f0fa4ee3f..6c8cedbac 100644 --- a/test/integration/lib/first-time.js +++ b/test/integration/lib/first-time.js @@ -11,7 +11,7 @@ QUnit.test('render init screen', function (assert) { const recurseNotices = function () { let button = app.find('button') - if (button.html() === 'Agree') { + if (button.html() === 'Accept') { let termsPage = app.find('.markdown')[0] termsPage.scrollTop = termsPage.scrollHeight return wait().then(() => { diff --git a/ui/app/components/notice.js b/ui/app/components/notice.js index 7fe41fa88..d9f0067cd 100644 --- a/ui/app/components/notice.js +++ b/ui/app/components/notice.js @@ -107,7 +107,7 @@ Notice.prototype.render = function () { style: { marginTop: '18px', }, - }, 'Agree'), + }, 'Accept'), ]) ) } -- cgit v1.2.3 From 13e667202835b082eb840ec2fb59511d687acdba Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Sun, 28 May 2017 11:18:07 -0700 Subject: Linting --- app/scripts/background.js | 2 +- app/scripts/lib/migrator/index.js | 4 +-- test/lib/mock-store.js | 2 +- test/unit/components/bn-as-decimal-input-test.js | 6 ++--- test/unit/components/pending-tx-test.js | 31 +++++++++++------------- test/unit/explorer-link-test.js | 1 - test/unit/migrator-test.js | 2 +- test/unit/network-contoller-test.js | 8 +++--- test/unit/tx-controller-test.js | 1 - 9 files changed, 26 insertions(+), 31 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index 63c8a7252..1dbfb1b98 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -31,7 +31,7 @@ const diskStore = new LocalStorageStore({ storageKey: STORAGE_KEY }) // initialization flow initialize().catch(console.error) -async function initialize() { +async function initialize () { const initState = await loadStateFromPersistence() await setupController(initState) console.log('MetaMask initialization complete.') diff --git a/app/scripts/lib/migrator/index.js b/app/scripts/lib/migrator/index.js index de6f5d5cd..4fd2cae92 100644 --- a/app/scripts/lib/migrator/index.js +++ b/app/scripts/lib/migrator/index.js @@ -14,8 +14,8 @@ class Migrator { async migrateData (versionedData = this.generateInitialState()) { const pendingMigrations = this.migrations.filter(migrationIsPending) - for (let index in pendingMigrations) { - let migration = pendingMigrations[index] + for (const index in pendingMigrations) { + const migration = pendingMigrations[index] versionedData = await migration.migrate(versionedData) if (!versionedData.data) throw new Error('Migrator - migration returned empty data') if (versionedData.version !== undefined && versionedData.meta.version !== migration.version) throw new Error('Migrator - Migration did not update version number correctly') diff --git a/test/lib/mock-store.js b/test/lib/mock-store.js index 4714c3485..0d50e2d9c 100644 --- a/test/lib/mock-store.js +++ b/test/lib/mock-store.js @@ -2,7 +2,7 @@ const createStore = require('redux').createStore const applyMiddleware = require('redux').applyMiddleware const thunkMiddleware = require('redux-thunk') const createLogger = require('redux-logger') -const rootReducer = function() {} +const rootReducer = function () {} module.exports = configureStore diff --git a/test/unit/components/bn-as-decimal-input-test.js b/test/unit/components/bn-as-decimal-input-test.js index b3365b6f9..106b3a871 100644 --- a/test/unit/components/bn-as-decimal-input-test.js +++ b/test/unit/components/bn-as-decimal-input-test.js @@ -18,7 +18,7 @@ describe('BnInput', function () { } const value = new BN(valueStr, 10) - let inputStr = '2.3' + const inputStr = '2.3' let targetStr = '23' while (targetStr.length < 19) { @@ -43,9 +43,9 @@ describe('BnInput', function () { const component = additions.renderIntoDocument(inputComponent) renderer.render(inputComponent) const input = additions.find(component, 'input.hex-input')[0] - ReactTestUtils.Simulate.change(input, { preventDefault() {}, target: { + ReactTestUtils.Simulate.change(input, { preventDefault () {}, target: { value: inputStr, - checkValidity() { return true } }, + checkValidity () { return true } }, }) }) }) diff --git a/test/unit/components/pending-tx-test.js b/test/unit/components/pending-tx-test.js index 52e5e5910..22a98bc93 100644 --- a/test/unit/components/pending-tx-test.js +++ b/test/unit/components/pending-tx-test.js @@ -15,23 +15,22 @@ describe('PendingTx', function () { const gasPrice = '0x4A817C800' // 20 Gwei const txData = { - 'id':5021615666270214, - 'time':1494458763011, - 'status':'unapproved', - 'metamaskNetworkId':'1494442339676', - 'txParams':{ - 'from':'0xfdea65c8e26263f6d9a1b5de9555d2931a33b826', - 'to':'0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', - 'value':'0xde0b6b3a7640000', + 'id': 5021615666270214, + 'time': 1494458763011, + 'status': 'unapproved', + 'metamaskNetworkId': '1494442339676', + 'txParams': { + 'from': '0xfdea65c8e26263f6d9a1b5de9555d2931a33b826', + 'to': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', + 'value': '0xde0b6b3a7640000', gasPrice, - 'gas':'0x7b0c'}, - 'gasLimitSpecified':false, - 'estimatedGas':'0x5208', + 'gas': '0x7b0c'}, + 'gasLimitSpecified': false, + 'estimatedGas': '0x5208', } it('should use updated values when edited.', function (done) { - const renderer = ReactTestUtils.createRenderer() const newGasPrice = '0x77359400' @@ -40,7 +39,6 @@ describe('PendingTx', function () { accounts: identities, txData, sendTransaction: (txMeta, event) => { - // Assert changes: const result = ethUtil.addHexPrefix(txMeta.txParams.gasPrice) assert.notEqual(result, gasPrice, 'gas price should change') @@ -60,17 +58,16 @@ describe('PendingTx', function () { ReactTestUtils.Simulate.change(input, { target: { value: 2, - checkValidity() { return true }, + checkValidity () { return true }, }, }) const form = additions.find(component, 'form')[0] form.checkValidity = () => true - form.getFormEl = () => { return { checkValidity() { return true } } } - ReactTestUtils.Simulate.submit(form, { preventDefault() {}, target: { checkValidity() { + form.getFormEl = () => { return { checkValidity () { return true } } } + ReactTestUtils.Simulate.submit(form, { preventDefault () {}, target: { checkValidity () { return true } } }) - } catch (e) { console.log('WHAAAA') console.error(e) diff --git a/test/unit/explorer-link-test.js b/test/unit/explorer-link-test.js index e672b36ed..a02564509 100644 --- a/test/unit/explorer-link-test.js +++ b/test/unit/explorer-link-test.js @@ -11,5 +11,4 @@ describe('explorer-link', function () { var result = linkGen('hash', '42') assert.notEqual(result.indexOf('kovan'), -1, 'kovan injected') }) - }) diff --git a/test/unit/migrator-test.js b/test/unit/migrator-test.js index ece95b9f6..16066fefe 100644 --- a/test/unit/migrator-test.js +++ b/test/unit/migrator-test.js @@ -28,7 +28,7 @@ const migrations = [ }, }, ] -const versionedData = {meta: {version: 0}, data:{hello:'world'}} +const versionedData = {meta: {version: 0}, data: {hello: 'world'}} describe('Migrator', () => { const migrator = new Migrator({ migrations }) it('migratedData version should be version 3', (done) => { diff --git a/test/unit/network-contoller-test.js b/test/unit/network-contoller-test.js index 76452b303..0c7ee9d70 100644 --- a/test/unit/network-contoller-test.js +++ b/test/unit/network-contoller-test.js @@ -27,7 +27,7 @@ describe('# Network Controller', function () { }) }) describe('network', function () { - describe('#provider', function() { + describe('#provider', function () { it('provider should be updatable without reassignment', function () { networkController.initializeProvider(networkController.providerInit) const provider = networkController.provider @@ -37,7 +37,7 @@ describe('# Network Controller', function () { }) describe('#getNetworkState', function () { it('should return loading when new', function () { - let networkState = networkController.getNetworkState() + const networkState = networkController.getNetworkState() assert.equal(networkState, 'loading', 'network is loading') }) }) @@ -45,14 +45,14 @@ describe('# Network Controller', function () { describe('#setNetworkState', function () { it('should update the network', function () { networkController.setNetworkState(1) - let networkState = networkController.getNetworkState() + const networkState = networkController.getNetworkState() assert.equal(networkState, 1, 'network is 1') }) }) describe('#getRpcAddressForType', function () { it('should return the right rpc address', function () { - let rpcTarget = networkController.getRpcAddressForType('mainnet') + const rpcTarget = networkController.getRpcAddressForType('mainnet') assert.equal(rpcTarget, 'https://mainnet.infura.io/metamask', 'returns the right rpcAddress') }) }) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index 711e1ea79..3954300a8 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -320,5 +320,4 @@ describe('Transaction Controller', function () { }) }) }) - }) -- cgit v1.2.3 From 997f38c2195bee1d5623d6d18eae0b409587ff3b Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 12 Jun 2017 13:30:59 -0700 Subject: gitignore - add package-lock and re-arrange by category --- .gitignore | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index cc0ba4fd1..85c2d15d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,27 @@ -dist npm-debug.log node_modules -temp -.tmp -.sass-cache +package-lock.json + app/bower_components test/bower_components package + +temp +.tmp +.sass-cache .DS_Store +app/.DS_Store + +dist builds/ disc/ -notes.txt -app/.DS_Store -development/bundle.js builds.zip -test/integration/bundle.js + +development/bundle.js development/states.js +test/integration/bundle.js test/background.js test/bundle.js test/test-bundle.js + +notes.txt \ No newline at end of file -- cgit v1.2.3 From e8d5d85a16619b808c897a332d8b53b5b64066e1 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 12 Jun 2017 13:34:02 -0700 Subject: gitignore - remove cached package-lock --- package-lock.json | 7348 ----------------------------------------------------- 1 file changed, 7348 deletions(-) delete mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index cd2a6ee8c..000000000 --- a/package-lock.json +++ /dev/null @@ -1,7348 +0,0 @@ -{ - "name": "metamask-crx", - "version": "0.0.0", - "lockfileVersion": 1, - "dependencies": { - "@gulp-sourcemaps/map-sources": { - "version": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", - "integrity": "sha1-iQrnxdjId/bThIYCFazp1+yUW9o=", - "dev": true - }, - "abab": { - "version": "https://registry.npmjs.org/abab/-/abab-1.0.3.tgz", - "integrity": "sha1-uB3l9ydOxOdW15fNg08wNkJyTl0=", - "dev": true - }, - "abbrev": { - "version": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz", - "integrity": "sha1-0FVMIlZjbi9W58LlrRg/hZQo2B8=", - "dev": true - }, - "abstract-leveldown": { - "version": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-2.4.1.tgz", - "integrity": "sha1-s7/tuITraToSd18MVenwpCDM7mQ=" - }, - "accepts": { - "version": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz", - "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=" - }, - "acorn": { - "version": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" - }, - "acorn-globals": { - "version": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz", - "integrity": "sha1-VbtemGkVB7dFedBRNBMhfDgMVM8=", - "dev": true, - "dependencies": { - "acorn": { - "version": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", - "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=", - "dev": true - } - } - }, - "acorn-jsx": { - "version": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", - "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", - "dependencies": { - "acorn": { - "version": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=" - } - } - }, - "aes-js": { - "version": "https://registry.npmjs.org/aes-js/-/aes-js-0.2.4.tgz", - "integrity": "sha1-lLiBq3FyhtAV+iGeCPtmcJ3aWj0=" - }, - "after": { - "version": "https://registry.npmjs.org/after/-/after-0.8.1.tgz", - "integrity": "sha1-q11PuIP1loFtNRX495HAr0ht1ic=", - "dev": true - }, - "ajv": { - "version": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=" - }, - "ajv-keywords": { - "version": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", - "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=" - }, - "amdefine": { - "version": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" - }, - "ansi-escapes": { - "version": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=" - }, - "ansi-regex": { - "version": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "ansicolors": { - "version": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", - "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=", - "dev": true - }, - "any-promise": { - "version": "https://registry.npmjs.org/any-promise/-/any-promise-0.1.0.tgz", - "integrity": "sha1-gwtoCqflbzNFHUsEnzvYBESY7ic=" - }, - "anymatch": { - "version": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.0.tgz", - "integrity": "sha1-o+Uvo5FoyCX/V7AkgSbOWo/5VQc=", - "dev": true - }, - "aproba": { - "version": "https://registry.npmjs.org/aproba/-/aproba-1.1.1.tgz", - "integrity": "sha1-ldNgDwdxCqDpKYxyatXs8urLq6s=" - }, - "archy": { - "version": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", - "dev": true - }, - "are-we-there-yet": { - "version": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", - "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=" - }, - "argparse": { - "version": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", - "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=" - }, - "argsparser": { - "version": "https://registry.npmjs.org/argsparser/-/argsparser-0.0.6.tgz", - "integrity": "sha1-/0XrW5LABCJc8UalHTOdzLJlvmM=", - "dev": true - }, - "arr-diff": { - "version": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", - "dev": true - }, - "arr-filter": { - "version": "https://registry.npmjs.org/arr-filter/-/arr-filter-1.1.2.tgz", - "integrity": "sha1-Q/3d0JHo7xGqTEXZzcGOLf8XEe4=", - "dev": true - }, - "arr-flatten": { - "version": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.0.3.tgz", - "integrity": "sha1-onTthawIhJtr14R8RYB0XcUa37E=", - "dev": true - }, - "arr-map": { - "version": "https://registry.npmjs.org/arr-map/-/arr-map-2.0.2.tgz", - "integrity": "sha1-Onc0X/wc814qkYJWAfnljy4kysQ=", - "dev": true - }, - "array-differ": { - "version": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", - "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=" - }, - "array-each": { - "version": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", - "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", - "dev": true - }, - "array-equal": { - "version": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", - "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", - "dev": true - }, - "array-filter": { - "version": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", - "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", - "dev": true - }, - "array-flatten": { - "version": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "array-initial": { - "version": "https://registry.npmjs.org/array-initial/-/array-initial-1.0.0.tgz", - "integrity": "sha1-CbE8WNVqBQNC53erb/zllbEI2tk=", - "dev": true, - "dependencies": { - "array-slice": { - "version": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", - "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", - "dev": true - }, - "is-number": { - "version": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz", - "integrity": "sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY=", - "dev": true - } - } - }, - "array-last": { - "version": "https://registry.npmjs.org/array-last/-/array-last-1.1.1.tgz", - "integrity": "sha1-9GWPmI2SEya1itARPPdtM3x7IKo=", - "dev": true, - "dependencies": { - "is-number": { - "version": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz", - "integrity": "sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY=", - "dev": true - } - } - }, - "array-map": { - "version": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", - "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", - "dev": true - }, - "array-reduce": { - "version": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", - "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", - "dev": true - }, - "array-slice": { - "version": "https://registry.npmjs.org/array-slice/-/array-slice-1.0.0.tgz", - "integrity": "sha1-5zA08A3MH0CHYAj9IP6ud71LfC8=", - "dev": true - }, - "array-union": { - "version": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=" - }, - "array-uniq": { - "version": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" - }, - "array-unique": { - "version": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", - "dev": true - }, - "arraybuffer.slice": { - "version": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz", - "integrity": "sha1-8zshWfBTKj8xB6JywMz70a0peco=", - "dev": true - }, - "arrify": { - "version": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" - }, - "asap": { - "version": "https://registry.npmjs.org/asap/-/asap-2.0.5.tgz", - "integrity": "sha1-UidltQw1EEkOUtfc/ghe+bqWlY8=" - }, - "asn1": { - "version": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" - }, - "asn1.js": { - "version": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.9.1.tgz", - "integrity": "sha1-SLokC0WpKA6UdImQull9IWYX/UA=", - "dev": true - }, - "assert": { - "version": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", - "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", - "dev": true - }, - "assert-plus": { - "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" - }, - "assertion-error": { - "version": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", - "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", - "dev": true - }, - "astw": { - "version": "https://registry.npmjs.org/astw/-/astw-2.2.0.tgz", - "integrity": "sha1-e9QXhNMkk5h66yOba04cV6hzuRc=", - "dev": true - }, - "async": { - "version": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - }, - "async-done": { - "version": "https://registry.npmjs.org/async-done/-/async-done-1.2.2.tgz", - "integrity": "sha1-ukKA2lWhbhX0u4vzqESpGHh0DjE=", - "dev": true - }, - "async-each": { - "version": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", - "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", - "dev": true - }, - "async-eventemitter": { - "version": "https://registry.npmjs.org/async-eventemitter/-/async-eventemitter-0.2.3.tgz", - "integrity": "sha1-959IDf2mZFqXvWFCwBcVDWO05w4=", - "dependencies": { - "async": { - "version": "https://registry.npmjs.org/async/-/async-2.4.1.tgz", - "integrity": "sha1-YqVrJ5yYoR0JhwlqAcw+6463u9c=" - } - } - }, - "async-reduce": { - "version": "https://registry.npmjs.org/async-reduce/-/async-reduce-0.0.1.tgz", - "integrity": "sha1-sja183bW+uOBze2QBqp/LHOxfzE=" - }, - "async-settle": { - "version": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", - "integrity": "sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs=", - "dev": true - }, - "asynckit": { - "version": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "atob": { - "version": "https://registry.npmjs.org/atob/-/atob-1.1.3.tgz", - "integrity": "sha1-lfE2KbEsOlGl0hWr3OKqnzL4B3M=", - "dev": true - }, - "aws-sign2": { - "version": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" - }, - "aws4": { - "version": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" - }, - "babel-code-frame": { - "version": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.22.0.tgz", - "integrity": "sha1-AnYgvuVnqIwyVhV05/0IAdMxGOQ=" - }, - "babel-core": { - "version": "https://registry.npmjs.org/babel-core/-/babel-core-6.24.1.tgz", - "integrity": "sha1-jEKFZNzh4fQfszfsNPTDsCK1rYM=" - }, - "babel-eslint": { - "version": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-6.1.2.tgz", - "integrity": "sha1-UpNBn+NnLWZZjTJ9qWlFZ7pqXy8=", - "dev": true - }, - "babel-generator": { - "version": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.24.1.tgz", - "integrity": "sha1-5xX0hsWN7SVknYiJRNUqoHxdlJc=", - "dependencies": { - "jsesc": { - "version": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=" - } - } - }, - "babel-helper-bindify-decorators": { - "version": "https://registry.npmjs.org/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz", - "integrity": "sha1-FMGeXxQte0fxmlJDHlKxzLxAozA=", - "dev": true - }, - "babel-helper-builder-binary-assignment-operator-visitor": { - "version": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", - "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", - "dev": true - }, - "babel-helper-call-delegate": { - "version": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", - "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=" - }, - "babel-helper-define-map": { - "version": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.24.1.tgz", - "integrity": "sha1-epdH8ljYlH0y1RX2qhx70CIEoIA=" - }, - "babel-helper-explode-assignable-expression": { - "version": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", - "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", - "dev": true - }, - "babel-helper-explode-class": { - "version": "https://registry.npmjs.org/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz", - "integrity": "sha1-fcKjkQ3uAHBW4eMdZAztPVTqqes=", - "dev": true - }, - "babel-helper-function-name": { - "version": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", - "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=" - }, - "babel-helper-get-function-arity": { - "version": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", - "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=" - }, - "babel-helper-hoist-variables": { - "version": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", - "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=" - }, - "babel-helper-optimise-call-expression": { - "version": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", - "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=" - }, - "babel-helper-regex": { - "version": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.24.1.tgz", - "integrity": "sha1-024i+rEAjXnYhkjjIRaGgShFbOg=" - }, - "babel-helper-remap-async-to-generator": { - "version": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", - "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", - "dev": true - }, - "babel-helper-replace-supers": { - "version": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", - "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=" - }, - "babel-helpers": { - "version": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", - "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=" - }, - "babel-messages": { - "version": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=" - }, - "babel-plugin-check-es2015-constants": { - "version": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", - "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=" - }, - "babel-plugin-syntax-async-functions": { - "version": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", - "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", - "dev": true - }, - "babel-plugin-syntax-async-generators": { - "version": "https://registry.npmjs.org/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz", - "integrity": "sha1-a8lj67FuzLrmuStZbrfzXDQqi5o=", - "dev": true - }, - "babel-plugin-syntax-class-constructor-call": { - "version": "https://registry.npmjs.org/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz", - "integrity": "sha1-nLnTn+Q8hgC+yBRkVt3L1OGnZBY=", - "dev": true - }, - "babel-plugin-syntax-class-properties": { - "version": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", - "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=", - "dev": true - }, - "babel-plugin-syntax-decorators": { - "version": "https://registry.npmjs.org/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz", - "integrity": "sha1-MSVjtNvePMgGzuPkFszurd0RrAs=", - "dev": true - }, - "babel-plugin-syntax-do-expressions": { - "version": "https://registry.npmjs.org/babel-plugin-syntax-do-expressions/-/babel-plugin-syntax-do-expressions-6.13.0.tgz", - "integrity": "sha1-V0d1YTmqJtOQ0JQQsDdEugfkeW0=", - "dev": true - }, - "babel-plugin-syntax-dynamic-import": { - "version": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", - "integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo=", - "dev": true - }, - "babel-plugin-syntax-exponentiation-operator": { - "version": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", - "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=", - "dev": true - }, - "babel-plugin-syntax-export-extensions": { - "version": "https://registry.npmjs.org/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz", - "integrity": "sha1-cKFITw+QiaToStRLrDU8lbmxJyE=", - "dev": true - }, - "babel-plugin-syntax-function-bind": { - "version": "https://registry.npmjs.org/babel-plugin-syntax-function-bind/-/babel-plugin-syntax-function-bind-6.13.0.tgz", - "integrity": "sha1-SMSV8Xe98xqYHnMvVa3AvdJgH0Y=", - "dev": true - }, - "babel-plugin-syntax-object-rest-spread": { - "version": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", - "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", - "dev": true - }, - "babel-plugin-syntax-trailing-function-commas": { - "version": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", - "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=", - "dev": true - }, - "babel-plugin-transform-async-generator-functions": { - "version": "https://registry.npmjs.org/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz", - "integrity": "sha1-8FiQAUX9PpkHpt3yjaWfIVJYpds=", - "dev": true - }, - "babel-plugin-transform-async-to-generator": { - "version": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", - "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", - "dev": true - }, - "babel-plugin-transform-class-constructor-call": { - "version": "https://registry.npmjs.org/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz", - "integrity": "sha1-gNwoVQWsBn3LjWxl4vbxGrd2Xvk=", - "dev": true - }, - "babel-plugin-transform-class-properties": { - "version": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", - "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", - "dev": true - }, - "babel-plugin-transform-decorators": { - "version": "https://registry.npmjs.org/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz", - "integrity": "sha1-eIAT2PjGtSIr33s0Q5Df13Vp4k0=", - "dev": true - }, - "babel-plugin-transform-do-expressions": { - "version": "https://registry.npmjs.org/babel-plugin-transform-do-expressions/-/babel-plugin-transform-do-expressions-6.22.0.tgz", - "integrity": "sha1-KMyvkoEtlJws0SgfaQyP3EaK6bs=", - "dev": true - }, - "babel-plugin-transform-es2015-arrow-functions": { - "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", - "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=" - }, - "babel-plugin-transform-es2015-block-scoped-functions": { - "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", - "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=" - }, - "babel-plugin-transform-es2015-block-scoping": { - "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.24.1.tgz", - "integrity": "sha1-dsKV3DpHQbFmWt/TFnIV3P8ypXY=" - }, - "babel-plugin-transform-es2015-classes": { - "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", - "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=" - }, - "babel-plugin-transform-es2015-computed-properties": { - "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", - "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=" - }, - "babel-plugin-transform-es2015-destructuring": { - "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", - "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=" - }, - "babel-plugin-transform-es2015-duplicate-keys": { - "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", - "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=" - }, - "babel-plugin-transform-es2015-for-of": { - "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", - "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=" - }, - "babel-plugin-transform-es2015-function-name": { - "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", - "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=" - }, - "babel-plugin-transform-es2015-literals": { - "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", - "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=" - }, - "babel-plugin-transform-es2015-modules-amd": { - "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", - "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=" - }, - "babel-plugin-transform-es2015-modules-commonjs": { - "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.1.tgz", - "integrity": "sha1-0+MQtA72ZKNmIiAAl8bUQCmPK/4=" - }, - "babel-plugin-transform-es2015-modules-systemjs": { - "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", - "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=" - }, - "babel-plugin-transform-es2015-modules-umd": { - "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", - "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=" - }, - "babel-plugin-transform-es2015-object-super": { - "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", - "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=" - }, - "babel-plugin-transform-es2015-parameters": { - "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", - "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=" - }, - "babel-plugin-transform-es2015-shorthand-properties": { - "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", - "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=" - }, - "babel-plugin-transform-es2015-spread": { - "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", - "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=" - }, - "babel-plugin-transform-es2015-sticky-regex": { - "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", - "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=" - }, - "babel-plugin-transform-es2015-template-literals": { - "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", - "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=" - }, - "babel-plugin-transform-es2015-typeof-symbol": { - "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", - "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=" - }, - "babel-plugin-transform-es2015-unicode-regex": { - "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", - "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=" - }, - "babel-plugin-transform-exponentiation-operator": { - "version": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", - "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", - "dev": true - }, - "babel-plugin-transform-export-extensions": { - "version": "https://registry.npmjs.org/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz", - "integrity": "sha1-U3OLR+deghhYnuqUbLvTkQm75lM=", - "dev": true - }, - "babel-plugin-transform-function-bind": { - "version": "https://registry.npmjs.org/babel-plugin-transform-function-bind/-/babel-plugin-transform-function-bind-6.22.0.tgz", - "integrity": "sha1-xvuOlqwpajELjPjqQBRiQH3fapc=", - "dev": true - }, - "babel-plugin-transform-object-rest-spread": { - "version": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.23.0.tgz", - "integrity": "sha1-h11ryb52HFiirj/u5dxIldjH+SE=", - "dev": true - }, - "babel-plugin-transform-regenerator": { - "version": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.24.1.tgz", - "integrity": "sha1-uNowWtQ8PJm0hI5P5AN7dw0jxBg=" - }, - "babel-plugin-transform-runtime": { - "version": "https://registry.npmjs.org/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz", - "integrity": "sha1-iEkNRGUC6puOfvsP4J7E2ZR5se4=", - "dev": true - }, - "babel-plugin-transform-strict-mode": { - "version": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", - "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=" - }, - "babel-preset-es2015": { - "version": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz", - "integrity": "sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk=" - }, - "babel-preset-stage-0": { - "version": "https://registry.npmjs.org/babel-preset-stage-0/-/babel-preset-stage-0-6.24.1.tgz", - "integrity": "sha1-VkLRUEL5E4TX5a+LyIsduVsDnmo=", - "dev": true - }, - "babel-preset-stage-1": { - "version": "https://registry.npmjs.org/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz", - "integrity": "sha1-dpLNfc1oSZB+auSgqFWJz7niv7A=", - "dev": true - }, - "babel-preset-stage-2": { - "version": "https://registry.npmjs.org/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz", - "integrity": "sha1-2eKWD7PXEYfw5k7sYrwHdnIZvcE=", - "dev": true - }, - "babel-preset-stage-3": { - "version": "https://registry.npmjs.org/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz", - "integrity": "sha1-g2raCp56f6N8sTj7kyb4eTSkg5U=", - "dev": true - }, - "babel-register": { - "version": "https://registry.npmjs.org/babel-register/-/babel-register-6.24.1.tgz", - "integrity": "sha1-fhDhOi9xBlvfrVoXh7pFvKbe118=" - }, - "babel-runtime": { - "version": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.23.0.tgz", - "integrity": "sha1-CpSJ8UTecO+zzkMArM2zKeL8VDs=" - }, - "babel-template": { - "version": "https://registry.npmjs.org/babel-template/-/babel-template-6.24.1.tgz", - "integrity": "sha1-BK5RTx+Ts6JTfyoPYKWkX7gwgzM=" - }, - "babel-traverse": { - "version": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.24.1.tgz", - "integrity": "sha1-qzZnP9NW+aCUhlnnszjV/q2zFpU=" - }, - "babel-types": { - "version": "https://registry.npmjs.org/babel-types/-/babel-types-6.24.1.tgz", - "integrity": "sha1-oTaHncFbNga9oNkMH8dDBML/CXU=" - }, - "babelify": { - "version": "https://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz", - "integrity": "sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU=" - }, - "babylon": { - "version": "https://registry.npmjs.org/babylon/-/babylon-6.17.1.tgz", - "integrity": "sha1-F/FP3fNhtpWYH+Z5OF5PHAHr2G8=" - }, - "bach": { - "version": "https://registry.npmjs.org/bach/-/bach-1.1.0.tgz", - "integrity": "sha1-z+VC25Jcs3BR/EkK0QLHO8slioQ=", - "dev": true - }, - "backbone": { - "version": "https://registry.npmjs.org/backbone/-/backbone-1.3.3.tgz", - "integrity": "sha1-TMgOp8sWMaxHSInOQPL4vGg7KZk=", - "dev": true - }, - "backo2": { - "version": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", - "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=", - "dev": true - }, - "balanced-match": { - "version": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", - "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=" - }, - "base-x": { - "version": "https://registry.npmjs.org/base-x/-/base-x-1.1.0.tgz", - "integrity": "sha1-QtPXF0dPnqAiB/bRqh9CaRPut6w=" - }, - "base64-arraybuffer": { - "version": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", - "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", - "dev": true - }, - "base64-js": { - "version": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.2.tgz", - "integrity": "sha1-Ak8Pcq+iW3X5wO5zzU9V7Bvtl4Q=" - }, - "base64id": { - "version": "https://registry.npmjs.org/base64id/-/base64id-0.1.0.tgz", - "integrity": "sha1-As4P3u4M709ACA4ec+g08LG/zj8=", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "optional": true - }, - "beefy": { - "version": "https://registry.npmjs.org/beefy/-/beefy-2.1.8.tgz", - "integrity": "sha1-e8Ebmkh6mjRnnYXinTtS83T9ACk=", - "dev": true, - "dependencies": { - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "mime": { - "version": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", - "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA=", - "dev": true - }, - "minimist": { - "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "object-keys": { - "version": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", - "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=", - "dev": true - }, - "open": { - "version": "https://registry.npmjs.org/open/-/open-0.0.3.tgz", - "integrity": "sha1-+jd/T/MIIS2SqbjmOVJAhUZGpxM=", - "dev": true - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true - }, - "resolve": { - "version": "https://registry.npmjs.org/resolve/-/resolve-0.6.3.tgz", - "integrity": "sha1-3ZV5gufnNt699TtYpN2RdUV13UY=", - "dev": true - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "through": { - "version": "https://registry.npmjs.org/through/-/through-2.2.7.tgz", - "integrity": "sha1-bo4hIAGR1OtqmfbwEN9Gqhxusr0=", - "dev": true - }, - "xtend": { - "version": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", - "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", - "dev": true - } - } - }, - "beeper": { - "version": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz", - "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=" - }, - "better-assert": { - "version": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", - "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", - "dev": true - }, - "bignumber.js": { - "version": "git+https://github.com/debris/bignumber.js.git#94d7146671b9719e00a09c29b01a691bc85048c2" - }, - "binary-extensions": { - "version": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.8.0.tgz", - "integrity": "sha1-SOyNFt9Dd+rl+liEaCSAr02Vx3Q=", - "dev": true - }, - "binaryextensions": { - "version": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-1.0.1.tgz", - "integrity": "sha1-HmN0iLNbWL2l9HdL+WpSEqjJB1U=", - "dev": true - }, - "bindings": { - "version": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz", - "integrity": "sha1-FK1hE4EtLTfXLme0ystLtyZQXxE=" - }, - "bip39": { - "version": "https://registry.npmjs.org/bip39/-/bip39-2.3.1.tgz", - "integrity": "sha1-yCOKvAnXGcbwETbvBC2szF3DWBs=" - }, - "bip66": { - "version": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", - "integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=" - }, - "bl": { - "version": "https://registry.npmjs.org/bl/-/bl-0.7.0.tgz", - "integrity": "sha1-P7BnBgKsKHjrdw3CA58YNr5irls=", - "dependencies": { - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=" - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "blob": { - "version": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", - "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=", - "dev": true - }, - "bluebird": { - "version": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", - "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" - }, - "bn.js": { - "version": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", - "integrity": "sha1-UzRK2xRhehP26N0s4okF0cC6MhU=" - }, - "body-parser": { - "version": "https://registry.npmjs.org/body-parser/-/body-parser-1.14.2.tgz", - "integrity": "sha1-EBXLH+LEQ4WCWVgdtTMy+NDPUPk=", - "dev": true, - "dependencies": { - "debug": { - "version": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", - "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", - "dev": true - }, - "http-errors": { - "version": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz", - "integrity": "sha1-GX4izevUGYWF6GlO9nhhl7ke2UI=", - "dev": true - }, - "iconv-lite": { - "version": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz", - "integrity": "sha1-H4irpKsLFQjoMSrMOTRfNumS4vI=", - "dev": true - }, - "ms": { - "version": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", - "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", - "dev": true - }, - "qs": { - "version": "https://registry.npmjs.org/qs/-/qs-5.2.0.tgz", - "integrity": "sha1-qfMRQq9GjLcrJbMBNrokVoNJFr4=", - "dev": true - } - } - }, - "boolbase": { - "version": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", - "dev": true - }, - "boom": { - "version": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=" - }, - "bops": { - "version": "https://registry.npmjs.org/bops/-/bops-0.0.6.tgz", - "integrity": "sha1-CC0dVfoB5g29wuvC26N/ZZVUzzo=" - }, - "brace-expansion": { - "version": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz", - "integrity": "sha1-Pv/DxQ4ABTH7cg6v+A8K6O8jz1k=" - }, - "braces": { - "version": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", - "dev": true - }, - "brfs": { - "version": "https://registry.npmjs.org/brfs/-/brfs-1.4.3.tgz", - "integrity": "sha1-22ddb16SPm3wh/ylhZyQkKrtMhY=" - }, - "brorand": { - "version": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" - }, - "browser-pack": { - "version": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.0.2.tgz", - "integrity": "sha1-+GzWzvT1MAyOY+B6TVEvZfv/RTE=", - "dev": true - }, - "browser-passworder": { - "version": "https://registry.npmjs.org/browser-passworder/-/browser-passworder-2.0.3.tgz", - "integrity": "sha1-b90gguUWoXbtvLPc7gt/n85PeRc=" - }, - "browser-resolve": { - "version": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.2.tgz", - "integrity": "sha1-j/CbCixCFxihBRwmCzLkj0QpOM4=", - "dev": true - }, - "browser-unpack": { - "version": "https://registry.npmjs.org/browser-unpack/-/browser-unpack-0.2.3.tgz", - "integrity": "sha1-iP4EzCZiV+UmUAlc2OBYXce24vE=", - "dependencies": { - "concat-stream": { - "version": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.2.1.tgz", - "integrity": "sha1-81EAtsRjeL+6i2uA+fDQzN8T3GA=" - } - } - }, - "browserify": { - "version": "https://registry.npmjs.org/browserify/-/browserify-13.3.0.tgz", - "integrity": "sha1-tanJAgJD8McORnW+yCI7xifkFc4=", - "dev": true, - "dependencies": { - "concat-stream": { - "version": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", - "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", - "dev": true, - "dependencies": { - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", - "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", - "dev": true - } - } - }, - "duplexer2": { - "version": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true - }, - "process": { - "version": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", - "dev": true - }, - "punycode": { - "version": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } - } - }, - "browserify-aes": { - "version": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.0.6.tgz", - "integrity": "sha1-Xncl297x/Vkw1OurSFZ85FHEigo=" - }, - "browserify-cipher": { - "version": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.0.tgz", - "integrity": "sha1-mYgkSHS/XtTijalWZtzWasj8Njo=", - "dev": true - }, - "browserify-derequire": { - "version": "https://registry.npmjs.org/browserify-derequire/-/browserify-derequire-0.9.4.tgz", - "integrity": "sha1-ZNYeVs/f8LjxdP2MV/i0Az4oeJU=", - "dependencies": { - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=" - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "through2": { - "version": "https://registry.npmjs.org/through2/-/through2-1.1.1.tgz", - "integrity": "sha1-CEfLxESfNAVXTb3M2buEG4OsNUU=" - } - } - }, - "browserify-des": { - "version": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.0.tgz", - "integrity": "sha1-2qJ3cXRwki7S/hhZQRihdUOXId0=", - "dev": true - }, - "browserify-rsa": { - "version": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", - "dev": true - }, - "browserify-sha3": { - "version": "https://registry.npmjs.org/browserify-sha3/-/browserify-sha3-0.0.1.tgz", - "integrity": "sha1-P/NKMAbvFcD7NWflQbkaI0ASPRE=" - }, - "browserify-sign": { - "version": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", - "dev": true - }, - "browserify-unibabel": { - "version": "https://registry.npmjs.org/browserify-unibabel/-/browserify-unibabel-3.0.0.tgz", - "integrity": "sha1-WmuPD3BM44jTkn30czfiWDD3Hdo=" - }, - "browserify-zlib": { - "version": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", - "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", - "dev": true - }, - "bs58": { - "version": "https://registry.npmjs.org/bs58/-/bs58-3.1.0.tgz", - "integrity": "sha1-1MJjiL9IBMrHFBQbGUWqR+XrJI4=" - }, - "bs58check": { - "version": "https://registry.npmjs.org/bs58check/-/bs58check-1.3.4.tgz", - "integrity": "sha1-xSVABzdJEXcU+gQsMEfrj5FRy/g=" - }, - "buffer": { - "version": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", - "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", - "dev": true, - "dependencies": { - "base64-js": { - "version": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.0.tgz", - "integrity": "sha1-o5mS1yNYSBGYK+XikLtqU9hnAPE=", - "dev": true - } - } - }, - "buffer-crc32": { - "version": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", - "dev": true - }, - "buffer-equal": { - "version": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", - "integrity": "sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs=" - }, - "buffer-shims": { - "version": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", - "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" - }, - "buffer-xor": { - "version": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" - }, - "bufferstreams": { - "version": "https://registry.npmjs.org/bufferstreams/-/bufferstreams-1.1.1.tgz", - "integrity": "sha1-AWE3MGCsWYjv+ZBYcxEU9uGV1R4=" - }, - "builtin-modules": { - "version": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" - }, - "builtin-status-codes": { - "version": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", - "dev": true - }, - "builtins": { - "version": "https://registry.npmjs.org/builtins/-/builtins-0.0.3.tgz", - "integrity": "sha1-XQBhZtpxYQvCvPcwGfDwzEMwl1U=" - }, - "bytes": { - "version": "https://registry.npmjs.org/bytes/-/bytes-2.2.0.tgz", - "integrity": "sha1-/TVGSkA/b5EXwt42Cez/nK4ABYg=", - "dev": true - }, - "cached-path-relative": { - "version": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.1.tgz", - "integrity": "sha1-0JxLUoAKpMB44t2BqGmqyQ0uVOc=", - "dev": true - }, - "caller-path": { - "version": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", - "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=" - }, - "callsite": { - "version": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", - "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", - "dev": true - }, - "callsites": { - "version": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=" - }, - "camelcase": { - "version": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" - }, - "caseless": { - "version": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "chai": { - "version": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", - "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", - "dev": true - }, - "chalk": { - "version": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=" - }, - "charm": { - "version": "https://registry.npmjs.org/charm/-/charm-1.0.2.tgz", - "integrity": "sha1-it02cVOm2aWBMxBSxAkJkdqZXjU=", - "dev": true - }, - "checkpoint-store": { - "version": "https://registry.npmjs.org/checkpoint-store/-/checkpoint-store-1.1.0.tgz", - "integrity": "sha1-BOTLUWuRQziTWB5tRgGnjpVS6gY=" - }, - "cheerio": { - "version": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", - "integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=", - "dev": true - }, - "chokidar": { - "version": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", - "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", - "dev": true - }, - "chownr": { - "version": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", - "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=" - }, - "cipher-base": { - "version": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.3.tgz", - "integrity": "sha1-7qvxlEGc6QDaMBjCB9IS8qbfCgc=" - }, - "circular-json": { - "version": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.1.tgz", - "integrity": "sha1-vos2rvzN6LPKeqLWr8B6NyQsDS0=" - }, - "classnames": { - "version": "https://registry.npmjs.org/classnames/-/classnames-2.2.5.tgz", - "integrity": "sha1-+zgB1FNGdknvNgPH1hoCvRKb3m0=" - }, - "cli-cursor": { - "version": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=" - }, - "cli-table": { - "version": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", - "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", - "dev": true, - "dependencies": { - "colors": { - "version": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", - "dev": true - } - } - }, - "cli-width": { - "version": "https://registry.npmjs.org/cli-width/-/cli-width-2.1.0.tgz", - "integrity": "sha1-sjTKIJsp72b8UY2bmNWEewDt8Ao=" - }, - "client-sw-ready-event": { - "version": "https://registry.npmjs.org/client-sw-ready-event/-/client-sw-ready-event-3.1.0.tgz", - "integrity": "sha1-SR1E+BFiEH/TfKBDWLSzPs/gpy4=" - }, - "cliui": { - "version": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=" - }, - "clone": { - "version": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz", - "integrity": "sha1-Jgt6meux7f4kdTgXX3gyQ8sZ0Uk=" - }, - "clone-stats": { - "version": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", - "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=" - }, - "co": { - "version": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "code-point-at": { - "version": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "coinstring": { - "version": "https://registry.npmjs.org/coinstring/-/coinstring-2.3.0.tgz", - "integrity": "sha1-zbYzY6lhUCQEolr7gsLibV/2J6Q=", - "dependencies": { - "bs58": { - "version": "https://registry.npmjs.org/bs58/-/bs58-2.0.1.tgz", - "integrity": "sha1-VZCNWPGYKrogCPob7Y+RmYopv40=" - } - } - }, - "collection-map": { - "version": "https://registry.npmjs.org/collection-map/-/collection-map-0.1.0.tgz", - "integrity": "sha1-TP+R0lEI159O3uzObs7j5IjyZ8I=", - "dev": true, - "dependencies": { - "make-iterator": { - "version": "https://registry.npmjs.org/make-iterator/-/make-iterator-0.1.1.tgz", - "integrity": "sha1-hz0nuBmKRlqBSDtvXRbaToY+z1s=", - "dev": true - } - } - }, - "color": { - "version": "https://registry.npmjs.org/color/-/color-0.11.4.tgz", - "integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=" - }, - "color-convert": { - "version": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", - "integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=" - }, - "color-name": { - "version": "https://registry.npmjs.org/color-name/-/color-name-1.1.2.tgz", - "integrity": "sha1-XIq3K2S9IhXWF66VWeuxSEdc+Y0=" - }, - "color-string": { - "version": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz", - "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=" - }, - "colors": { - "version": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", - "dev": true - }, - "combine-source-map": { - "version": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.7.2.tgz", - "integrity": "sha1-CHAxKFazB6h8xKxIbzqaYq7MwJ4=", - "dev": true, - "dependencies": { - "convert-source-map": { - "version": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", - "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", - "dev": true - } - } - }, - "combined-stream": { - "version": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=" - }, - "commander": { - "version": "https://registry.npmjs.org/commander/-/commander-2.3.0.tgz", - "integrity": "sha1-/UMOiJgy7DU7ms0d4hfBHLPu+HM=", - "dev": true - }, - "commondir": { - "version": "https://registry.npmjs.org/commondir/-/commondir-0.0.1.tgz", - "integrity": "sha1-ifAP3NUbUZxXhzP+xWPmptp/W+I=" - }, - "commonmark": { - "version": "https://registry.npmjs.org/commonmark/-/commonmark-0.24.0.tgz", - "integrity": "sha1-uA3gGCxUY1VkOqFdsSv7KCNoJ48=" - }, - "commonmark-react-renderer": { - "version": "https://registry.npmjs.org/commonmark-react-renderer/-/commonmark-react-renderer-4.3.3.tgz", - "integrity": "sha1-nEvKE4vIMoe655LM8TNzi+nLxvo=" - }, - "component-bind": { - "version": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", - "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=", - "dev": true - }, - "component-emitter": { - "version": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz", - "integrity": "sha1-KWWU8nU9qmOZbSrwjRWpURbJrsM=", - "dev": true - }, - "component-inherit": { - "version": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", - "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", - "dev": true - }, - "concat-map": { - "version": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", - "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=" - }, - "config-chain": { - "version": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.11.tgz", - "integrity": "sha1-q6CXR9++TD5w52am5BWG4YWfxvI=", - "dev": true - }, - "console-browserify": { - "version": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", - "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", - "dev": true - }, - "console-control-strings": { - "version": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, - "consolidate": { - "version": "https://registry.npmjs.org/consolidate/-/consolidate-0.14.5.tgz", - "integrity": "sha1-WiUEe8dvcwcmZ8jLUsmJiI9JTGM=", - "dev": true - }, - "constants-browserify": { - "version": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", - "dev": true - }, - "content-disposition": { - "version": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" - }, - "content-type": { - "version": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz", - "integrity": "sha1-t9ETrueo3Se9IRM8TcJSnfFyHu0=" - }, - "convert-source-map": { - "version": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.0.tgz", - "integrity": "sha1-ms1whRxtXf3ZPZKC5e35SgP/RrU=" - }, - "cookie": { - "version": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" - }, - "cookie-signature": { - "version": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "copy-props": { - "version": "https://registry.npmjs.org/copy-props/-/copy-props-1.6.0.tgz", - "integrity": "sha1-8DJLvumXcRAeezraES8xPDk9uO0=", - "dev": true - }, - "copy-to-clipboard": { - "version": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-2.1.0.tgz", - "integrity": "sha1-Az8WUqU9gpBYU3sPCNK86STt/FM=" - }, - "core-js": { - "version": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", - "integrity": "sha1-TekR5mew6ukSTjQlS1OupvxhjT4=" - }, - "core-util-is": { - "version": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "create-ecdh": { - "version": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz", - "integrity": "sha1-iIxyNZbN92EvZJgjPuvXo1MBc30=", - "dev": true - }, - "create-hash": { - "version": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", - "integrity": "sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=" - }, - "create-hmac": { - "version": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.6.tgz", - "integrity": "sha1-rLniIaThe9sHbpBlfEK5PjcmzwY=" - }, - "create-react-class": { - "version": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.5.3.tgz", - "integrity": "sha1-+w98rnkznpoXnhlO9Gbvo5I4IP4=" - }, - "cross-spawn": { - "version": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "dev": true, - "dependencies": { - "lru-cache": { - "version": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", - "integrity": "sha1-HRdnnAac2l0ECZGgnbwsDbN35V4=", - "dev": true - }, - "which": { - "version": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", - "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", - "dev": true - } - } - }, - "cryptiles": { - "version": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=" - }, - "crypto-browserify": { - "version": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.11.0.tgz", - "integrity": "sha1-NlKgkGq5sqfgw85mpAjpV6JIVSI=", - "dev": true - }, - "crypto-js": { - "version": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.8.tgz", - "integrity": "sha1-cV8HC/YBTyrpkqmLOSkli3E/CNU=" - }, - "css": { - "version": "https://registry.npmjs.org/css/-/css-2.2.1.tgz", - "integrity": "sha1-c6TIHehdtmTU7mdPfUcIXjstVdw=", - "dev": true, - "dependencies": { - "source-map": { - "version": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", - "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", - "dev": true - } - } - }, - "css-select": { - "version": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", - "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", - "dev": true - }, - "css-what": { - "version": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", - "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=", - "dev": true - }, - "cssauron": { - "version": "https://registry.npmjs.org/cssauron/-/cssauron-1.4.0.tgz", - "integrity": "sha1-pmAt/34EqDBtwNuaVR6S6LVmKtg=", - "dev": true - }, - "cssom": { - "version": "https://registry.npmjs.org/cssom/-/cssom-0.3.2.tgz", - "integrity": "sha1-uANhcMefB6kP8vFuIihAJ6JDhIs=", - "dev": true - }, - "cssstyle": { - "version": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.37.tgz", - "integrity": "sha1-VBCXI0yyUTyDzu06zdwn/yeYfVQ=", - "dev": true - }, - "cycle": { - "version": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", - "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=", - "dev": true - }, - "cyclist": { - "version": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", - "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=" - }, - "d": { - "version": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", - "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=" - }, - "d3": { - "version": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz", - "integrity": "sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g=" - }, - "dashdash": { - "version": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dependencies": { - "assert-plus": { - "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "date-now": { - "version": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", - "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", - "dev": true - }, - "dateformat": { - "version": "https://registry.npmjs.org/dateformat/-/dateformat-2.0.0.tgz", - "integrity": "sha1-J0Pjq7XD/CRi5SfcpEXgTp9N7hc=" - }, - "debounce": { - "version": "https://registry.npmjs.org/debounce/-/debounce-1.0.2.tgz", - "integrity": "sha1-UDzGdNjX9zcJlmT7dd29NrlibcY=" - }, - "debug": { - "version": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz", - "integrity": "sha1-krrR9tBbu2u6Isyoi80OyJTChh4=" - }, - "debug-fabulous": { - "version": "https://registry.npmjs.org/debug-fabulous/-/debug-fabulous-0.0.4.tgz", - "integrity": "sha1-+gccXYdIRoVCSAdCHKSxawsaB2M=", - "dev": true, - "dependencies": { - "object-assign": { - "version": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz", - "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=", - "dev": true - } - } - }, - "decamelize": { - "version": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "deep-diff": { - "version": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.4.tgz", - "integrity": "sha1-qsXDmVIjar5fA3ojSQYLoBsArkg=" - }, - "deep-eql": { - "version": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", - "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", - "dev": true, - "dependencies": { - "type-detect": { - "version": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", - "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", - "dev": true - } - } - }, - "deep-equal": { - "version": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" - }, - "deep-extend": { - "version": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", - "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=" - }, - "deep-freeze-strict": { - "version": "https://registry.npmjs.org/deep-freeze-strict/-/deep-freeze-strict-1.1.1.tgz", - "integrity": "sha1-d9BYPKJKab5LvZrC+uQV1VUj5bA=", - "dev": true - }, - "deep-is": { - "version": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" - }, - "deepmerge": { - "version": "https://registry.npmjs.org/deepmerge/-/deepmerge-0.2.10.tgz", - "integrity": "sha1-iQa/nlJaT78bIDsq/LRkAkmCEhk=", - "dev": true - }, - "default-resolution": { - "version": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", - "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=", - "dev": true - }, - "deferred-leveldown": { - "version": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-1.2.1.tgz", - "integrity": "sha1-XSXDMQ9f6QmUb2JA3J+Q3RCace8=" - }, - "define-properties": { - "version": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", - "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=" - }, - "defined": { - "version": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" - }, - "del": { - "version": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", - "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=" - }, - "delayed-stream": { - "version": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "delegates": { - "version": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "denodeify": { - "version": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz", - "integrity": "sha1-OjYof1A05pnnV3kBBSwubJQlFjE=" - }, - "depd": { - "version": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz", - "integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM=" - }, - "deps-sort": { - "version": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.0.tgz", - "integrity": "sha1-CRckkC6EZYJg65EHSMzNGvbiH7U=", - "dev": true - }, - "derequire": { - "version": "https://registry.npmjs.org/derequire/-/derequire-2.0.6.tgz", - "integrity": "sha1-MaQUu3yhdiOfp4sRZjbvd9UX52g=" - }, - "des.js": { - "version": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", - "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", - "dev": true - }, - "destroy": { - "version": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "detect-file": { - "version": "https://registry.npmjs.org/detect-file/-/detect-file-0.1.0.tgz", - "integrity": "sha1-STXe39lIhkjgBrASlWbpOGcR6mM=", - "dev": true - }, - "detect-indent": { - "version": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=" - }, - "detect-newline": { - "version": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", - "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", - "dev": true - }, - "detect-node": { - "version": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.3.tgz", - "integrity": "sha1-ogM8CcyOFY03dI+951B4Mr1s4Sc=" - }, - "detective": { - "version": "https://registry.npmjs.org/detective/-/detective-4.5.0.tgz", - "integrity": "sha1-blqMaybmx6JUsca210kNmOyR7dE=", - "dev": true - }, - "diff": { - "version": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", - "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", - "dev": true - }, - "diffie-hellman": { - "version": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz", - "integrity": "sha1-tYNXOScM/ias9jIJn97SoH8gnl4=", - "dev": true - }, - "disc": { - "version": "https://registry.npmjs.org/disc/-/disc-1.3.2.tgz", - "integrity": "sha1-MqbwLkhu33eGClNj0icYQl0pbkA=" - }, - "dnode": { - "version": "https://registry.npmjs.org/dnode/-/dnode-1.2.2.tgz", - "integrity": "sha1-SsPP4m4pKzs5uCWK59lO3FgTLvo=" - }, - "dnode-protocol": { - "version": "https://registry.npmjs.org/dnode-protocol/-/dnode-protocol-0.2.2.tgz", - "integrity": "sha1-URUdFvw7X4SBXuC5SXoQYdDRlJ0=" - }, - "doctrine": { - "version": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", - "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=" - }, - "dom-serializer": { - "version": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", - "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", - "dev": true, - "dependencies": { - "domelementtype": { - "version": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", - "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", - "dev": true - } - } - }, - "dom-walk": { - "version": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz", - "integrity": "sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg=" - }, - "domain-browser": { - "version": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", - "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=", - "dev": true - }, - "domelementtype": { - "version": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", - "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=", - "dev": true - }, - "domhandler": { - "version": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.1.tgz", - "integrity": "sha1-iS5HAAqZvlW783dP/qBWHYh5wlk=", - "dev": true - }, - "domutils": { - "version": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "dev": true - }, - "drbg.js": { - "version": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", - "integrity": "sha1-Pja2xCs3BDgjzbwzLVjzHiRFSAs=" - }, - "duplexer": { - "version": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" - }, - "duplexer2": { - "version": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", - "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", - "dependencies": { - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=" - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "duplexify": { - "version": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.0.tgz", - "integrity": "sha1-GqdzAC4VeEV+nZ1KULDMquvL1gQ=", - "dependencies": { - "end-of-stream": { - "version": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.0.0.tgz", - "integrity": "sha1-1FlucCc0qT5A6a+GQxnqvZn/Lw4=" - }, - "once": { - "version": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", - "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=" - } - } - }, - "each-props": { - "version": "https://registry.npmjs.org/each-props/-/each-props-1.3.0.tgz", - "integrity": "sha1-ftgDHJJ2iK7bSoluuRSFtEh7kOo=", - "dev": true - }, - "ecc-jsbn": { - "version": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true - }, - "ee-first": { - "version": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "elliptic": { - "version": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", - "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=" - }, - "encodeurl": { - "version": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", - "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" - }, - "encoding": { - "version": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", - "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=" - }, - "end-of-stream": { - "version": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.0.tgz", - "integrity": "sha1-epDYM+/abPpurA9JSduw+tOmMgY=" - }, - "engine.io": { - "version": "https://registry.npmjs.org/engine.io/-/engine.io-1.8.0.tgz", - "integrity": "sha1-PutfJky3XbvsG6rqJtYfWk6s4qo=", - "dev": true, - "dependencies": { - "debug": { - "version": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", - "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", - "dev": true - }, - "ms": { - "version": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", - "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", - "dev": true - } - } - }, - "engine.io-client": { - "version": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.8.0.tgz", - "integrity": "sha1-e3MOQSdBQIdZbZvjyI0rxf22z1w=", - "dev": true, - "dependencies": { - "component-emitter": { - "version": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "debug": { - "version": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", - "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", - "dev": true - }, - "ms": { - "version": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", - "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", - "dev": true - } - } - }, - "engine.io-parser": { - "version": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.3.1.tgz", - "integrity": "sha1-lVTxrjMQfW+9FwylRm0vgz9qB88=", - "dev": true, - "dependencies": { - "has-binary": { - "version": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.6.tgz", - "integrity": "sha1-JTJvOc+k9hath4eJTjryz7x7bhA=", - "dev": true - }, - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - } - } - }, - "ensnare": { - "version": "https://registry.npmjs.org/ensnare/-/ensnare-1.0.0.tgz", - "integrity": "sha1-ctK/fvSKuiH2at8p0AoJBO3bYcc=" - }, - "entities": { - "version": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", - "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=" - }, - "envify": { - "version": "https://registry.npmjs.org/envify/-/envify-4.0.0.tgz", - "integrity": "sha1-95E0Pj0RzCnM5BFQMAqK9hxmyrA=", - "dev": true, - "dependencies": { - "esprima": { - "version": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - } - } - }, - "enzyme": { - "version": "https://registry.npmjs.org/enzyme/-/enzyme-2.8.2.tgz", - "integrity": "sha1-bIvLBQEqvEqkvDIT+yN4C5tbFxQ=", - "dev": true - }, - "errno": { - "version": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz", - "integrity": "sha1-uJbiOp5ei6M4cfyZar02NfyaHH0=", - "dependencies": { - "prr": { - "version": "https://registry.npmjs.org/prr/-/prr-0.0.0.tgz", - "integrity": "sha1-GoS4WQgyVQFBGFPQCB7j+obikmo=" - } - } - }, - "error-ex": { - "version": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", - "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=" - }, - "es-abstract": { - "version": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.7.0.tgz", - "integrity": "sha1-363ndOAb/Nl/lhgCmMRJyGI/uUw=" - }, - "es-to-primitive": { - "version": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", - "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=" - }, - "es5-ext": { - "version": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.21.tgz", - "integrity": "sha1-Gacl+eUdAwC7wejoIRCf2dr1WSU=" - }, - "es6-iterator": { - "version": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.1.tgz", - "integrity": "sha1-jjGcnwRTv1ddN0lAplWSDlnKVRI=" - }, - "es6-map": { - "version": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", - "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=" - }, - "es6-set": { - "version": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", - "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=" - }, - "es6-symbol": { - "version": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=" - }, - "es6-weak-map": { - "version": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", - "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=" - }, - "escape-html": { - "version": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "escape-string-regexp": { - "version": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "escodegen": { - "version": "https://registry.npmjs.org/escodegen/-/escodegen-1.3.3.tgz", - "integrity": "sha1-8CQBb1qI4Eb9EgBQVek5gC5sXyM=", - "dependencies": { - "esprima": { - "version": "https://registry.npmjs.org/esprima/-/esprima-1.1.1.tgz", - "integrity": "sha1-W28VR/TRAuZw4UDFCb5ncdautUk=" - }, - "estraverse": { - "version": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz", - "integrity": "sha1-hno+jlip+EYYr7bC3bzZFrfLr3E=" - }, - "esutils": { - "version": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz", - "integrity": "sha1-gVHTWOIMisx/t0XnRywAJf5JZXA=" - }, - "source-map": { - "version": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", - "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", - "optional": true - } - } - }, - "escope": { - "version": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", - "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=" - }, - "eslint": { - "version": "https://registry.npmjs.org/eslint/-/eslint-2.13.1.tgz", - "integrity": "sha1-5MyPoPAJ+4KaquI4VaKTYL4fbBE=", - "dependencies": { - "strip-json-comments": { - "version": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", - "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=" - } - } - }, - "eslint-plugin-chai": { - "version": "https://registry.npmjs.org/eslint-plugin-chai/-/eslint-plugin-chai-0.0.1.tgz", - "integrity": "sha1-mh3qWLM1wxJCIZ0Fmzf/sUMJ9uE=", - "dev": true - }, - "eslint-plugin-mocha": { - "version": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-4.9.0.tgz", - "integrity": "sha1-kXqLSZq40MAdacbk+B02LuCZtv0=", - "dev": true - }, - "espree": { - "version": "https://registry.npmjs.org/espree/-/espree-3.4.3.tgz", - "integrity": "sha1-KRC1zNSc6JPC//+qtP2LOjG4I3Q=", - "dependencies": { - "acorn": { - "version": "https://registry.npmjs.org/acorn/-/acorn-5.0.3.tgz", - "integrity": "sha1-xGDfCEkUY/AozLguqzcwvwEIez0=" - } - } - }, - "esprima-fb": { - "version": "https://registry.npmjs.org/esprima-fb/-/esprima-fb-3001.0001.0000-dev-harmony-fb.tgz", - "integrity": "sha1-t303q8046gt3Qmu4vCkizmtCZBE=" - }, - "esrecurse": { - "version": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.1.0.tgz", - "integrity": "sha1-RxO2U2rffyrE8yfVWed1a/9kgiA=", - "dependencies": { - "estraverse": { - "version": "https://registry.npmjs.org/estraverse/-/estraverse-4.1.1.tgz", - "integrity": "sha1-9srKcokzqFDvkGYdDheYK6RxEaI=" - } - } - }, - "estraverse": { - "version": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" - }, - "esutils": { - "version": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" - }, - "etag": { - "version": "https://registry.npmjs.org/etag/-/etag-1.8.0.tgz", - "integrity": "sha1-b2Ma7zNtbEY2K1F2QETOIWvjwFE=" - }, - "eth-bin-to-ops": { - "version": "https://registry.npmjs.org/eth-bin-to-ops/-/eth-bin-to-ops-1.0.1.tgz", - "integrity": "sha1-TScDuYeIJbw4xiWZEOkLTbAFx94=" - }, - "eth-block-tracker": { - "version": "https://registry.npmjs.org/eth-block-tracker/-/eth-block-tracker-1.0.17.tgz", - "integrity": "sha1-9jwEkCyB2N+mIk1EARWhaRa6w1w=" - }, - "eth-contract-metadata": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eth-contract-metadata/-/eth-contract-metadata-1.0.0.tgz", - "integrity": "sha1-YV/Z1jvjpwU0FDJdt7hdv/5tFWg=" - }, - "eth-ens-namehash": { - "version": "https://registry.npmjs.org/eth-ens-namehash/-/eth-ens-namehash-1.0.2.tgz", - "integrity": "sha1-Bezda6wtf9e8XKhKmTxrrZ2k7bk=", - "dependencies": { - "js-sha3": { - "version": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.7.tgz", - "integrity": "sha1-DU/9gALVMzqrr0oj7tL2N0yfKOc=" - } - } - }, - "eth-hd-keyring": { - "version": "https://registry.npmjs.org/eth-hd-keyring/-/eth-hd-keyring-1.2.0.tgz", - "integrity": "sha1-QLzH6od+9cdG9UwMh6aznOte3eM=", - "dependencies": { - "ethereumjs-util": { - "version": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.1.1.tgz", - "integrity": "sha1-Ei+zjep0fcYrOuv8Nl0b1Ivktz4=" - } - } - }, - "eth-query": { - "version": "https://registry.npmjs.org/eth-query/-/eth-query-2.1.1.tgz", - "integrity": "sha1-Co7lvSTHtcBKDMyLpH/Gq6QpjZI=" - }, - "eth-sig-util": { - "version": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.2.1.tgz", - "integrity": "sha1-JUo+csXCzLYMncXmRl/H4XS2v5E=", - "dependencies": { - "ethereumjs-util": { - "version": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.1.1.tgz", - "integrity": "sha1-Ei+zjep0fcYrOuv8Nl0b1Ivktz4=" - } - } - }, - "eth-simple-keyring": { - "version": "https://registry.npmjs.org/eth-simple-keyring/-/eth-simple-keyring-1.1.1.tgz", - "integrity": "sha1-bdddfMbt6nx4jPGe+UMcgwzZYa4=", - "dependencies": { - "ethereumjs-util": { - "version": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.1.1.tgz", - "integrity": "sha1-Ei+zjep0fcYrOuv8Nl0b1Ivktz4=" - } - } - }, - "ethereum-common": { - "version": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.0.18.tgz", - "integrity": "sha1-L9w1dvIykDNYl26znaeDIT/5Uj8=" - }, - "ethereum-ens-network-map": { - "version": "https://registry.npmjs.org/ethereum-ens-network-map/-/ethereum-ens-network-map-1.0.0.tgz", - "integrity": "sha1-Q812ac6VCnieFRABEY1NZfIQ7rc=" - }, - "ethereumjs-account": { - "version": "https://registry.npmjs.org/ethereumjs-account/-/ethereumjs-account-2.0.4.tgz", - "integrity": "sha1-+MMCMby3B/RRTYoFLB+doQNiTUc=", - "dependencies": { - "ethereumjs-util": { - "version": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-4.5.0.tgz", - "integrity": "sha1-PpQosxfuvaPXJg2FT93alUsfG8Y=" - } - } - }, - "ethereumjs-block": { - "version": "https://registry.npmjs.org/ethereumjs-block/-/ethereumjs-block-1.5.0.tgz", - "integrity": "sha1-sLkBjpzXMUbGAdx9svaypFYeRow=", - "dependencies": { - "async": { - "version": "https://registry.npmjs.org/async/-/async-2.4.1.tgz", - "integrity": "sha1-YqVrJ5yYoR0JhwlqAcw+6463u9c=" - } - } - }, - "ethereumjs-tx": { - "version": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-1.3.1.tgz", - "integrity": "sha1-1pCavPs32mQE/BgSTTUe2iAEfaw=" - }, - "ethereumjs-util": { - "version": "git://github.com/ethereumjs/ethereumjs-util.git#ac5d0908536b447083ea422b435da27f26615de9" - }, - "ethereumjs-vm": { - "version": "https://registry.npmjs.org/ethereumjs-vm/-/ethereumjs-vm-2.0.2.tgz", - "integrity": "sha1-hOI3KlcVqApi9/KjEvjGRTfoqEI=", - "dependencies": { - "async": { - "version": "https://registry.npmjs.org/async/-/async-2.4.1.tgz", - "integrity": "sha1-YqVrJ5yYoR0JhwlqAcw+6463u9c=" - }, - "ethereumjs-util": { - "version": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-4.5.0.tgz", - "integrity": "sha1-PpQosxfuvaPXJg2FT93alUsfG8Y=" - } - } - }, - "ethereumjs-wallet": { - "version": "https://registry.npmjs.org/ethereumjs-wallet/-/ethereumjs-wallet-0.6.0.tgz", - "integrity": "sha1-gnY7Fpfuenlr5xVdqd+0my+Yz9s=", - "dependencies": { - "ethereumjs-util": { - "version": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-4.5.0.tgz", - "integrity": "sha1-PpQosxfuvaPXJg2FT93alUsfG8Y=" - } - } - }, - "ethjs-abi": { - "version": "https://registry.npmjs.org/ethjs-abi/-/ethjs-abi-0.2.0.tgz", - "integrity": "sha1-0+LCIQEVIPxJm3FoIDbBT8wvWyU=", - "dependencies": { - "js-sha3": { - "version": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.5.tgz", - "integrity": "sha1-uvDA6MVK1ZA0R9+Wreekobynmko=" - } - } - }, - "ethjs-contract": { - "version": "https://registry.npmjs.org/ethjs-contract/-/ethjs-contract-0.1.9.tgz", - "integrity": "sha1-HCdmiWpW1H7B1tZhgpxJzDilUgo=", - "dependencies": { - "ethjs-util": { - "version": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.3.tgz", - "integrity": "sha1-39XqSkANxeQhqInK9H4IGtp4u1U=" - }, - "js-sha3": { - "version": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.5.tgz", - "integrity": "sha1-uvDA6MVK1ZA0R9+Wreekobynmko=" - } - } - }, - "ethjs-ens": { - "version": "https://registry.npmjs.org/ethjs-ens/-/ethjs-ens-2.0.0.tgz", - "integrity": "sha1-ZyLvx4fBe5pbJ+a0Jc2ZhvYlFOo=" - }, - "ethjs-filter": { - "version": "https://registry.npmjs.org/ethjs-filter/-/ethjs-filter-0.1.5.tgz", - "integrity": "sha1-ARKvYBfCRnfjK4/esg5hlgGbdZg=" - }, - "ethjs-format": { - "version": "https://registry.npmjs.org/ethjs-format/-/ethjs-format-0.2.0.tgz", - "integrity": "sha1-tKpRP8HVAnDY8QK/BvA8lJDTE5E=", - "dependencies": { - "ethjs-util": { - "version": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.3.tgz", - "integrity": "sha1-39XqSkANxeQhqInK9H4IGtp4u1U=" - } - } - }, - "ethjs-query": { - "version": "https://registry.npmjs.org/ethjs-query/-/ethjs-query-0.2.6.tgz", - "integrity": "sha1-nY5gRLi/dt0zQPhDcWoiWbnJHTw=" - }, - "ethjs-rpc": { - "version": "https://registry.npmjs.org/ethjs-rpc/-/ethjs-rpc-0.1.5.tgz", - "integrity": "sha1-CZ4i8n3EwYtpeKSF/DaxsPeWkIA=" - }, - "ethjs-schema": { - "version": "https://registry.npmjs.org/ethjs-schema/-/ethjs-schema-0.1.5.tgz", - "integrity": "sha1-WXQOOzl3vNu5sRvDBoIB6Kzquw0=" - }, - "ethjs-util": { - "version": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.4.tgz", - "integrity": "sha1-HItoeSV0RO9NPz+7rC3tEs2ZfZM=" - }, - "eve-raphael": { - "version": "https://registry.npmjs.org/eve-raphael/-/eve-raphael-0.5.0.tgz", - "integrity": "sha1-F8dUt5K+7z+maE15z1pHxjxM2jA=" - }, - "event-emitter": { - "version": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=" - }, - "event-stream": { - "version": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", - "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", - "dev": true - }, - "eventemitter3": { - "version": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz", - "integrity": "sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg=", - "dev": true - }, - "events": { - "version": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" - }, - "events-to-array": { - "version": "https://registry.npmjs.org/events-to-array/-/events-to-array-1.1.2.tgz", - "integrity": "sha1-LUH1Y+H+QA7Uli/hpNXGp1Od9/Y=", - "dev": true - }, - "evp_bytestokey": { - "version": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.0.tgz", - "integrity": "sha1-SXtmrZ/vZc18CKYYCCS6FHa2blM=" - }, - "exit-hook": { - "version": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", - "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=" - }, - "expand-brackets": { - "version": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", - "dev": true - }, - "expand-range": { - "version": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", - "dev": true - }, - "expand-template": { - "version": "https://registry.npmjs.org/expand-template/-/expand-template-1.0.3.tgz", - "integrity": "sha1-bDAzIxd6YrGyLAcCefeGEoe2mxo=" - }, - "expand-tilde": { - "version": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz", - "integrity": "sha1-C4HrqJflo9MdHD0QL48BRB5VlEk=", - "dev": true - }, - "express": { - "version": "https://registry.npmjs.org/express/-/express-4.15.3.tgz", - "integrity": "sha1-urZdDwOqgMNYQIly/HAPkWlEtmI=" - }, - "extend": { - "version": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" - }, - "extend-shallow": { - "version": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true - }, - "extension-link-enabler": { - "version": "https://registry.npmjs.org/extension-link-enabler/-/extension-link-enabler-1.0.0.tgz", - "integrity": "sha1-V7kZru7fOL6XJwuYmM7nimN+RvM=" - }, - "extensionizer": { - "version": "https://registry.npmjs.org/extensionizer/-/extensionizer-1.0.0.tgz", - "integrity": "sha1-AcIJu+ptnArLp3Epw6pKmpj8NTg=" - }, - "extglob": { - "version": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", - "dev": true - }, - "extsprintf": { - "version": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", - "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=" - }, - "eyes": { - "version": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", - "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=", - "dev": true - }, - "fake-merkle-patricia-tree": { - "version": "https://registry.npmjs.org/fake-merkle-patricia-tree/-/fake-merkle-patricia-tree-1.0.1.tgz", - "integrity": "sha1-S4w6z7Ugr635hgsfFM2M40As3dM=" - }, - "falafel": { - "version": "https://registry.npmjs.org/falafel/-/falafel-1.2.0.tgz", - "integrity": "sha1-wY0k71CRF0pJfzGM0ksCaiXN2rQ=", - "dependencies": { - "acorn": { - "version": "https://registry.npmjs.org/acorn/-/acorn-1.2.2.tgz", - "integrity": "sha1-yM4n3grMdtiW0rH6099YjZ6C8BQ=" - }, - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - } - } - }, - "fancy-log": { - "version": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.0.tgz", - "integrity": "sha1-Rb4X0Cu5kX1gzP/UmVyZnmyMmUg=" - }, - "fast-levenshtein": { - "version": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" - }, - "faye-websocket": { - "version": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.7.3.tgz", - "integrity": "sha1-zEB0x/Sk39A69U3WXDVLE1EyzhE=", - "dev": true - }, - "fbjs": { - "version": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.12.tgz", - "integrity": "sha1-ELXZL3bUVXX9Y6IX1OoCvqL47QQ=", - "dependencies": { - "core-js": { - "version": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", - "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" - } - } - }, - "fetch-ponyfill": { - "version": "https://registry.npmjs.org/fetch-ponyfill/-/fetch-ponyfill-4.0.0.tgz", - "integrity": "sha1-GM/jjWnN5Crsccs6znnh8idik9o=", - "dependencies": { - "node-fetch": { - "version": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.6.3.tgz", - "integrity": "sha1-3CNO3WSJmC1Y6PDbT2lQKavNjAQ=" - } - } - }, - "figures": { - "version": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=" - }, - "file-entry-cache": { - "version": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-1.3.1.tgz", - "integrity": "sha1-RMYepgeuS+nBQC9B9EJwy/4zT/g=" - }, - "file-tree": { - "version": "https://registry.npmjs.org/file-tree/-/file-tree-1.0.0.tgz", - "integrity": "sha1-/a2ZnLf6REODULUUx4+TWzBuk+M=" - }, - "filename-regex": { - "version": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", - "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", - "dev": true - }, - "fileset": { - "version": "https://registry.npmjs.org/fileset/-/fileset-0.1.8.tgz", - "integrity": "sha1-UGuRqTluqn4y+0KoQHfHoMc2t0E=", - "dev": true, - "optional": true, - "dependencies": { - "glob": { - "version": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", - "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", - "dev": true, - "optional": true, - "dependencies": { - "minimatch": { - "version": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", - "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", - "dev": true, - "optional": true - } - } - }, - "minimatch": { - "version": "https://registry.npmjs.org/minimatch/-/minimatch-0.4.0.tgz", - "integrity": "sha1-vSx9Bg0sjI/Xzefx8u0tWycP2xs=", - "dev": true, - "optional": true - } - } - }, - "fill-range": { - "version": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", - "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", - "dev": true - }, - "finalhandler": { - "version": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.0.3.tgz", - "integrity": "sha1-70fneVDpmXgOhgIqVg4yF+DQzIk=" - }, - "find-global-packages": { - "version": "https://registry.npmjs.org/find-global-packages/-/find-global-packages-0.0.1.tgz", - "integrity": "sha1-S6f9/xfun6fagzCV94tejNvfPis=", - "dev": true - }, - "find-up": { - "version": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=" - }, - "findup-sync": { - "version": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.4.3.tgz", - "integrity": "sha1-QAQ5Kee8YK3wt/SCfExudaDeyhI=", - "dev": true - }, - "fined": { - "version": "https://registry.npmjs.org/fined/-/fined-1.0.2.tgz", - "integrity": "sha1-WyhCS3YNdZiWC374SA3/itNmDpc=", - "dev": true - }, - "fireworm": { - "version": "https://registry.npmjs.org/fireworm/-/fireworm-0.7.1.tgz", - "integrity": "sha1-zPIPeUHxCIg/zduZOD2+bhhhx1g=", - "dev": true, - "dependencies": { - "async": { - "version": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", - "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", - "dev": true - }, - "lodash.debounce": { - "version": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-3.1.1.tgz", - "integrity": "sha1-gSIRw3ipTMKdWqTjNGzwv846ffU=", - "dev": true - }, - "lodash.flatten": { - "version": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-3.0.2.tgz", - "integrity": "sha1-3hz1d1j49EeTGdNcPpzGDEUBk4w=", - "dev": true - } - } - }, - "first-chunk-stream": { - "version": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz", - "integrity": "sha1-Wb+1DNkF9g18OUzT2ayqtOatk04=", - "dev": true - }, - "flagged-respawn": { - "version": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-0.3.2.tgz", - "integrity": "sha1-/xke3c1wiKZ1smEP/8l2vpuAdLU=", - "dev": true - }, - "flat": { - "version": "https://registry.npmjs.org/flat/-/flat-1.0.0.tgz", - "integrity": "sha1-Ad/dW8vBScZrNe1AHh11PxqtjVk=" - }, - "flat-cache": { - "version": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.2.2.tgz", - "integrity": "sha1-+oZxTnLCHbiGAXYezy9VXRq8a5Y=" - }, - "flatten": { - "version": "https://registry.npmjs.org/flatten/-/flatten-0.0.1.tgz", - "integrity": "sha1-VURAdm2goNYDmZ9DNFP2wvxqdcE=" - }, - "flush-write-stream": { - "version": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.2.tgz", - "integrity": "sha1-yBuQ2HRnZvGmCaRoCZRsRd2K5Bc=" - }, - "for-each": { - "version": "https://registry.npmjs.org/for-each/-/for-each-0.3.2.tgz", - "integrity": "sha1-LEBFC5NI6X8oEyJZO6lnBLmr1NQ=" - }, - "for-in": { - "version": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "for-own": { - "version": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", - "dev": true - }, - "foreach": { - "version": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" - }, - "forever-agent": { - "version": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "fork-stream": { - "version": "https://registry.npmjs.org/fork-stream/-/fork-stream-0.0.4.tgz", - "integrity": "sha1-24Sfznf2cIpfjzhq5TOgkHtUrnA=", - "dev": true - }, - "form-data": { - "version": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", - "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=" - }, - "formatio": { - "version": "https://registry.npmjs.org/formatio/-/formatio-1.1.1.tgz", - "integrity": "sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek=", - "dev": true - }, - "forwarded": { - "version": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz", - "integrity": "sha1-Ge+YdMSuHCl7zweP3mOgm2aoQ2M=" - }, - "fresh": { - "version": "https://registry.npmjs.org/fresh/-/fresh-0.5.0.tgz", - "integrity": "sha1-9HTKXmqSRtb9jglTz6m5yAWvp44=" - }, - "from": { - "version": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", - "dev": true - }, - "from2": { - "version": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=" - }, - "fs-exists-sync": { - "version": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", - "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=", - "dev": true - }, - "fs-extra": { - "version": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz", - "integrity": "sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=" - }, - "fs-promise": { - "version": "https://registry.npmjs.org/fs-promise/-/fs-promise-1.0.0.tgz", - "integrity": "sha1-QkakzUVJfS7Vfm5LIhZ9OGSyNnk=", - "dev": true, - "dependencies": { - "any-promise": { - "version": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", - "dev": true - }, - "fs-extra": { - "version": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", - "integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=", - "dev": true - } - } - }, - "fs.realpath": { - "version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.1.tgz", - "integrity": "sha1-8Z/Sj0Pur3YWgOUZogPE0LPTGv8=", - "dev": true, - "optional": true, - "dependencies": { - "abbrev": { - "version": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz", - "integrity": "sha1-0FVMIlZjbi9W58LlrRg/hZQo2B8=", - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true, - "optional": true - }, - "aproba": { - "version": "https://registry.npmjs.org/aproba/-/aproba-1.1.1.tgz", - "integrity": "sha1-ldNgDwdxCqDpKYxyatXs8urLq6s=", - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz", - "integrity": "sha1-gORw6VoIR5T+GJkmLFZnxuiN4bM=", - "dev": true, - "optional": true - }, - "asn1": { - "version": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", - "dev": true, - "optional": true - }, - "assert-plus": { - "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", - "dev": true, - "optional": true - }, - "asynckit": { - "version": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true, - "optional": true - }, - "aws-sign2": { - "version": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", - "dev": true, - "optional": true - }, - "aws4": { - "version": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", - "dev": true, - "optional": true - }, - "balanced-match": { - "version": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", - "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "dev": true, - "optional": true - }, - "block-stream": { - "version": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "dev": true - }, - "boom": { - "version": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", - "dev": true - }, - "brace-expansion": { - "version": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", - "integrity": "sha1-cZfX6qm4fmSDkOph/GbIRCdCDfk=", - "dev": true - }, - "buffer-shims": { - "version": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", - "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=", - "dev": true - }, - "caseless": { - "version": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", - "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", - "dev": true, - "optional": true - }, - "chalk": { - "version": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "combined-stream": { - "version": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", - "dev": true - }, - "commander": { - "version": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", - "dev": true, - "optional": true - }, - "concat-map": { - "version": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "console-control-strings": { - "version": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true - }, - "core-util-is": { - "version": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "cryptiles": { - "version": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", - "dev": true, - "optional": true - }, - "dashdash": { - "version": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "optional": true, - "dependencies": { - "assert-plus": { - "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true, - "optional": true - } - } - }, - "debug": { - "version": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", - "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", - "dev": true, - "optional": true - }, - "deep-extend": { - "version": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.1.tgz", - "integrity": "sha1-7+QRPQgIX05vlod1mBD4B0aeIlM=", - "dev": true, - "optional": true - }, - "delayed-stream": { - "version": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "delegates": { - "version": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true, - "optional": true - }, - "ecc-jsbn": { - "version": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "dev": true, - "optional": true - }, - "escape-string-regexp": { - "version": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true, - "optional": true - }, - "extend": { - "version": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz", - "integrity": "sha1-WkdDU7nzNT3dgXbf03uRyDpG8dQ=", - "dev": true, - "optional": true - }, - "extsprintf": { - "version": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", - "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=", - "dev": true - }, - "forever-agent": { - "version": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true, - "optional": true - }, - "form-data": { - "version": "https://registry.npmjs.org/form-data/-/form-data-2.1.2.tgz", - "integrity": "sha1-icNTQAi5fq2ky7FX1Y9vXfAl6uQ=", - "dev": true, - "optional": true - }, - "fs.realpath": { - "version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fstream": { - "version": "https://registry.npmjs.org/fstream/-/fstream-1.0.10.tgz", - "integrity": "sha1-YE6Kkv4m/9n2+uMDmdSYThqyKCI=", - "dev": true - }, - "fstream-ignore": { - "version": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", - "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", - "dev": true, - "optional": true - }, - "gauge": { - "version": "https://registry.npmjs.org/gauge/-/gauge-2.7.3.tgz", - "integrity": "sha1-HCOFX5YvF7OtPQ3HRD8wRULt/gk=", - "dev": true, - "optional": true - }, - "generate-function": { - "version": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", - "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", - "dev": true, - "optional": true - }, - "generate-object-property": { - "version": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", - "dev": true, - "optional": true - }, - "getpass": { - "version": "https://registry.npmjs.org/getpass/-/getpass-0.1.6.tgz", - "integrity": "sha1-KD/9n8ElaECHUxHBtg6MQBhxEOY=", - "dev": true, - "optional": true, - "dependencies": { - "assert-plus": { - "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true, - "optional": true - } - } - }, - "glob": { - "version": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", - "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", - "dev": true - }, - "graceful-fs": { - "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true - }, - "graceful-readlink": { - "version": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", - "dev": true, - "optional": true - }, - "har-validator": { - "version": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", - "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", - "dev": true, - "optional": true - }, - "has-ansi": { - "version": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "optional": true - }, - "has-unicode": { - "version": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true, - "optional": true - }, - "hawk": { - "version": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", - "dev": true, - "optional": true - }, - "hoek": { - "version": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", - "dev": true - }, - "http-signature": { - "version": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", - "dev": true, - "optional": true - }, - "inflight": { - "version": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true - }, - "inherits": { - "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "ini": { - "version": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", - "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=", - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true - }, - "is-my-json-valid": { - "version": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz", - "integrity": "sha1-k27do8o8IR/ZjzstPgjaQ/eykVs=", - "dev": true, - "optional": true - }, - "is-property": { - "version": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", - "dev": true, - "optional": true - }, - "is-typedarray": { - "version": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true, - "optional": true - }, - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isstream": { - "version": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true, - "optional": true - }, - "jodid25519": { - "version": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz", - "integrity": "sha1-BtSRIlUJNBlHfUJWM2BuDpB4KWc=", - "dev": true, - "optional": true - }, - "jsbn": { - "version": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true, - "optional": true - }, - "json-schema": { - "version": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true, - "optional": true - }, - "json-stringify-safe": { - "version": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true, - "optional": true - }, - "jsonpointer": { - "version": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", - "dev": true, - "optional": true - }, - "jsprim": { - "version": "https://registry.npmjs.org/jsprim/-/jsprim-1.3.1.tgz", - "integrity": "sha1-KnJW9wQSop7jZwqspiWZTE3P8lI=", - "dev": true, - "optional": true - }, - "mime-db": { - "version": "https://registry.npmjs.org/mime-db/-/mime-db-1.26.0.tgz", - "integrity": "sha1-6v/NDk/Gk1z4E02iRuLmw1MFrf8=", - "dev": true - }, - "mime-types": { - "version": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.14.tgz", - "integrity": "sha1-9+99l1g/yvO30oK2+LVnnaselO4=", - "dev": true - }, - "minimatch": { - "version": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha1-Kk5AkLlrLbBqnX3wEFWmKnfJt3Q=", - "dev": true - }, - "minimist": { - "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "mkdirp": { - "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true - }, - "ms": { - "version": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", - "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", - "dev": true, - "optional": true - }, - "node-pre-gyp": { - "version": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.33.tgz", - "integrity": "sha1-ZArFUZj2qSWXLgwWxKwmoDTV7Mk=", - "dev": true, - "optional": true - }, - "nopt": { - "version": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "dev": true, - "optional": true - }, - "npmlog": { - "version": "https://registry.npmjs.org/npmlog/-/npmlog-4.0.2.tgz", - "integrity": "sha1-0DlQ4OeM4VJ7om0qdZLpNIrD518=", - "dev": true, - "optional": true - }, - "number-is-nan": { - "version": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "oauth-sign": { - "version": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", - "dev": true, - "optional": true - }, - "object-assign": { - "version": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, - "optional": true - }, - "once": { - "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true - }, - "path-is-absolute": { - "version": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "pinkie": { - "version": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true, - "optional": true - }, - "pinkie-promise": { - "version": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", - "dev": true - }, - "punycode": { - "version": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true, - "optional": true - }, - "qs": { - "version": "https://registry.npmjs.org/qs/-/qs-6.3.1.tgz", - "integrity": "sha1-kYwLO802Z5dyuvE1say0wWUe150=", - "dev": true, - "optional": true - }, - "rc": { - "version": "https://registry.npmjs.org/rc/-/rc-1.1.7.tgz", - "integrity": "sha1-xepWS7B6/5/TpbMukGwdOmWUD+o=", - "dev": true, - "optional": true, - "dependencies": { - "minimist": { - "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.2.tgz", - "integrity": "sha1-qeb+w8fdqF+LsbO6cChgRVb8gl4=", - "dev": true, - "optional": true - }, - "request": { - "version": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", - "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", - "dev": true, - "optional": true - }, - "rimraf": { - "version": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", - "integrity": "sha1-loAAk8vxoMhr2VtGJUZ1NcKd+gQ=", - "dev": true - }, - "semver": { - "version": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true, - "optional": true - }, - "sntp": { - "version": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", - "dev": true, - "optional": true - }, - "sshpk": { - "version": "https://registry.npmjs.org/sshpk/-/sshpk-1.10.2.tgz", - "integrity": "sha1-1agEziJpVRVjjnmNviMnPeBwpfo=", - "dev": true, - "optional": true, - "dependencies": { - "assert-plus": { - "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true, - "optional": true - } - } - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "string-width": { - "version": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true - }, - "stringstream": { - "version": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", - "dev": true, - "optional": true - }, - "strip-ansi": { - "version": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true - }, - "strip-json-comments": { - "version": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true, - "optional": true - }, - "supports-color": { - "version": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true, - "optional": true - }, - "tar": { - "version": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", - "dev": true - }, - "tar-pack": { - "version": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.3.0.tgz", - "integrity": "sha1-MJMYFkGPVa/E0hd1r91nIM7kXa4=", - "dev": true, - "optional": true, - "dependencies": { - "once": { - "version": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", - "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=", - "dev": true, - "optional": true - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.5.tgz", - "integrity": "sha1-ZvqLcg4UOLNkaB8q0aY8YYRIydA=", - "dev": true, - "optional": true - } - } - }, - "tough-cookie": { - "version": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", - "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", - "dev": true, - "optional": true - }, - "tunnel-agent": { - "version": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", - "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", - "dev": true, - "optional": true - }, - "tweetnacl": { - "version": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true, - "optional": true - }, - "uid-number": { - "version": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", - "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=", - "dev": true, - "optional": true - }, - "util-deprecate": { - "version": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "uuid": { - "version": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", - "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=", - "dev": true, - "optional": true - }, - "verror": { - "version": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", - "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", - "dev": true, - "optional": true - }, - "wide-align": { - "version": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.0.tgz", - "integrity": "sha1-QO3egCpx/qHwcNo+YtzaLnrdlq0=", - "dev": true, - "optional": true - }, - "wrappy": { - "version": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "xtend": { - "version": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", - "dev": true, - "optional": true - } - } - }, - "function-bind": { - "version": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz", - "integrity": "sha1-FhdnFMgBeY5Ojyz391KUZ7tKV3E=" - }, - "function.prototype.name": { - "version": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.0.0.tgz", - "integrity": "sha1-X1I8pk5JGl+Vq6gMweORCAoUSC4=", - "dev": true - }, - "functional-red-black-tree": { - "version": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" - }, - "gauge": { - "version": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=" - }, - "generate-function": { - "version": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", - "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=" - }, - "generate-object-property": { - "version": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=" - }, - "get-caller-file": { - "version": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", - "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=" - }, - "get-stdin": { - "version": "https://registry.npmjs.org/get-stdin/-/get-stdin-3.0.2.tgz", - "integrity": "sha1-wc7SS5A5s43thb3xYeV3E7bdSr4=", - "dev": true - }, - "get-values": { - "version": "https://registry.npmjs.org/get-values/-/get-values-0.1.0.tgz", - "integrity": "sha1-OsA1tlpEkj012y/Ct7ojIrbD8p4=", - "dev": true - }, - "getpass": { - "version": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dependencies": { - "assert-plus": { - "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "github-from-package": { - "version": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" - }, - "gl-mat4": { - "version": "https://registry.npmjs.org/gl-mat4/-/gl-mat4-1.1.4.tgz", - "integrity": "sha1-HolbVYkuVqiWhnq9g30483oXgIY=" - }, - "gl-vec3": { - "version": "https://registry.npmjs.org/gl-vec3/-/gl-vec3-1.0.3.tgz", - "integrity": "sha1-EQ/Yl9Byn2OYMHOBVn0JRJQb8is=" - }, - "glob": { - "version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=" - }, - "glob-all": { - "version": "https://registry.npmjs.org/glob-all/-/glob-all-3.1.0.tgz", - "integrity": "sha1-iRPd+17hrHgSZWJBsD1SF8ZLAqs=", - "dev": true, - "dependencies": { - "minimist": { - "version": "https://registry.npmjs.org/minimist/-/minimist-0.1.0.tgz", - "integrity": "sha1-md9lelJXTCHJBXSX33QnkLK0wN4=", - "dev": true - }, - "yargs": { - "version": "https://registry.npmjs.org/yargs/-/yargs-1.2.6.tgz", - "integrity": "sha1-nHtKgv1dWVsr8Xq23MQxNUMv40s=", - "dev": true - } - } - }, - "glob-base": { - "version": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", - "dev": true - }, - "glob-parent": { - "version": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", - "dev": true - }, - "glob-stream": { - "version": "https://registry.npmjs.org/glob-stream/-/glob-stream-5.3.5.tgz", - "integrity": "sha1-pVZlqajM3EGRWofHAeMtTgFvrSI=", - "dev": true, - "dependencies": { - "glob": { - "version": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "dev": true - }, - "glob-parent": { - "version": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true - }, - "is-extglob": { - "version": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true - }, - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "through2": { - "version": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", - "dev": true - } - } - }, - "glob-watcher": { - "version": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-3.2.0.tgz", - "integrity": "sha1-/8Gi09B3g7Zy9eIXmaTQs/7ZLa8=", - "dev": true - }, - "global": { - "version": "https://registry.npmjs.org/global/-/global-4.3.2.tgz", - "integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=" - }, - "global-modules": { - "version": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", - "integrity": "sha1-6lo77ULG1s6ZWk+KEmm12uIjgo0=", - "dev": true - }, - "global-prefix": { - "version": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", - "integrity": "sha1-jTvGuNo8qBEqFg2NSW/wRiv+948=", - "dev": true, - "dependencies": { - "which": { - "version": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", - "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", - "dev": true - } - } - }, - "globals": { - "version": "https://registry.npmjs.org/globals/-/globals-9.17.0.tgz", - "integrity": "sha1-DAymltm5u2lNLlRwvTd3fKrVAoY=" - }, - "globby": { - "version": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", - "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=" - }, - "glogg": { - "version": "https://registry.npmjs.org/glogg/-/glogg-1.0.0.tgz", - "integrity": "sha1-f+DxmfV6yQbPUS/urY+Q7kooT8U=" - }, - "graceful-fs": { - "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" - }, - "graceful-readlink": { - "version": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", - "dev": true - }, - "growl": { - "version": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", - "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", - "dev": true - }, - "growly": { - "version": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true - }, - "gulp": { - "version": "git://github.com/gulpjs/gulp.git#38246c3f8b6dbb8d4ef657183e92d90c8299e22f", - "dev": true, - "dependencies": { - "camelcase": { - "version": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", - "dev": true - }, - "gulp-cli": { - "version": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-1.3.0.tgz", - "integrity": "sha1-pr+7i+NTQb4pCuRc0+QBBxIW7dQ=", - "dev": true - }, - "window-size": { - "version": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", - "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=", - "dev": true - }, - "yargs": { - "version": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", - "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=", - "dev": true - } - } - }, - "gulp-eslint": { - "version": "https://registry.npmjs.org/gulp-eslint/-/gulp-eslint-2.1.0.tgz", - "integrity": "sha1-P9X+C3I2ZR8VuNS/sUB8O3TQE2w=" - }, - "gulp-if": { - "version": "https://registry.npmjs.org/gulp-if/-/gulp-if-2.0.2.tgz", - "integrity": "sha1-pJe351cwBQQcqivIt92jyARE1ik=", - "dev": true - }, - "gulp-json-editor": { - "version": "https://registry.npmjs.org/gulp-json-editor/-/gulp-json-editor-2.2.1.tgz", - "integrity": "sha1-fE3XR36NBtxdxJwLgedFzbBPl7s=", - "dev": true, - "dependencies": { - "detect-indent": { - "version": "https://registry.npmjs.org/detect-indent/-/detect-indent-2.0.0.tgz", - "integrity": "sha1-cg/1Hk2Xt2iE9r9XKSNIsT396Tk=", - "dev": true - }, - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "minimist": { - "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true - }, - "repeating": { - "version": "https://registry.npmjs.org/repeating/-/repeating-1.1.3.tgz", - "integrity": "sha1-PUEUIYh3U3SU+X93+Xhfq4EPpKw=", - "dev": true - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "through2": { - "version": "https://registry.npmjs.org/through2/-/through2-0.5.1.tgz", - "integrity": "sha1-390BLrnHAOIyP9M084rGIqs3Lac=", - "dev": true - }, - "xtend": { - "version": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", - "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=", - "dev": true - } - } - }, - "gulp-livereload": { - "version": "https://registry.npmjs.org/gulp-livereload/-/gulp-livereload-3.8.1.tgz", - "integrity": "sha1-APdEstdJ0+njdGWJyKRKysd5tQ8=", - "dev": true, - "dependencies": { - "ansi-regex": { - "version": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", - "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", - "dev": true - }, - "ansi-styles": { - "version": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", - "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", - "dev": true - }, - "chalk": { - "version": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", - "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", - "dev": true - }, - "has-ansi": { - "version": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", - "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", - "dev": true - }, - "lodash.assign": { - "version": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-3.2.0.tgz", - "integrity": "sha1-POnwI0tLIiPilrj6CsH+6OvKZPo=", - "dev": true - }, - "strip-ansi": { - "version": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", - "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", - "dev": true - }, - "supports-color": { - "version": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", - "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", - "dev": true - } - } - }, - "gulp-match": { - "version": "https://registry.npmjs.org/gulp-match/-/gulp-match-1.0.3.tgz", - "integrity": "sha1-kcfA1/Kb7NZgbVfYCn+Hdqh6uo4=", - "dev": true - }, - "gulp-replace": { - "version": "https://registry.npmjs.org/gulp-replace/-/gulp-replace-0.5.4.tgz", - "integrity": "sha1-aaZ5FLvRPFYr/xT1BKQDeWqg2qk=", - "dev": true - }, - "gulp-sourcemaps": { - "version": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-1.12.0.tgz", - "integrity": "sha1-eG+XyUoPloSSRl1wVY4EJCxnlZg=", - "dev": true, - "dependencies": { - "vinyl": { - "version": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", - "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", - "dev": true - } - } - }, - "gulp-util": { - "version": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz", - "integrity": "sha1-AFTh50RQLifATBh8PsxQXdVLu08=", - "dependencies": { - "minimist": { - "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - }, - "object-assign": { - "version": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", - "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=" - } - } - }, - "gulp-watch": { - "version": "https://registry.npmjs.org/gulp-watch/-/gulp-watch-4.3.11.tgz", - "integrity": "sha1-Fi/FY96fx3DpH5p845VVE6mhGMA=", - "dev": true, - "dependencies": { - "glob-parent": { - "version": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true - }, - "is-extglob": { - "version": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true - }, - "vinyl": { - "version": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", - "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", - "dev": true - } - } - }, - "gulp-zip": { - "version": "https://registry.npmjs.org/gulp-zip/-/gulp-zip-3.2.0.tgz", - "integrity": "sha1-69GY2ubcLV9E2BRWnI7EIRipPvk=", - "dev": true - }, - "gulplog": { - "version": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", - "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=" - }, - "handlebars": { - "version": "https://registry.npmjs.org/handlebars/-/handlebars-1.3.0.tgz", - "integrity": "sha1-npsTCpPjiUkTItl1zz7BgYw3zjQ=", - "dev": true, - "optional": true, - "dependencies": { - "optimist": { - "version": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", - "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=", - "dev": true, - "optional": true - } - } - }, - "har-schema": { - "version": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", - "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=" - }, - "har-validator": { - "version": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", - "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=" - }, - "has": { - "version": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", - "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=" - }, - "has-ansi": { - "version": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=" - }, - "has-binary": { - "version": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.7.tgz", - "integrity": "sha1-aOYesWIQyVRaClzOBqhzkS/h5ow=", - "dev": true, - "dependencies": { - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - } - } - }, - "has-color": { - "version": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", - "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=", - "dev": true - }, - "has-cors": { - "version": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", - "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=", - "dev": true - }, - "has-gulplog": { - "version": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", - "integrity": "sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4=" - }, - "has-unicode": { - "version": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, - "hash-base": { - "version": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", - "integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=" - }, - "hash.js": { - "version": "https://registry.npmjs.org/hash.js/-/hash.js-1.0.3.tgz", - "integrity": "sha1-EzL/ABVsCg/92CNgE9B7d6BFFXM=" - }, - "hat": { - "version": "https://registry.npmjs.org/hat/-/hat-0.0.3.tgz", - "integrity": "sha1-uwFKnmSzeIrtgAWRdBPU/z1QLYo=" - }, - "hawk": { - "version": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=" - }, - "hdkey": { - "version": "https://registry.npmjs.org/hdkey/-/hdkey-0.7.1.tgz", - "integrity": "sha1-yu5L6BqneSHpCbjSKN0PKayu5jI=" - }, - "hmac-drbg": { - "version": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=" - }, - "hoek": { - "version": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" - }, - "hoist-non-react-statics": { - "version": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", - "integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs=" - }, - "home-or-tmp": { - "version": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", - "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=" - }, - "homedir-polyfill": { - "version": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", - "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", - "dev": true - }, - "hosted-git-info": { - "version": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.4.2.tgz", - "integrity": "sha1-AHa59GonBQbduq6lZJaJdGBhKmc=" - }, - "html-select": { - "version": "https://registry.npmjs.org/html-select/-/html-select-2.3.24.tgz", - "integrity": "sha1-Rq1tcS5zLPMcZznV0BEKX6vxdYU=", - "dev": true, - "dependencies": { - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "minimist": { - "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "through2": { - "version": "https://registry.npmjs.org/through2/-/through2-1.1.1.tgz", - "integrity": "sha1-CEfLxESfNAVXTb3M2buEG4OsNUU=", - "dev": true - } - } - }, - "html-tokenize": { - "version": "https://registry.npmjs.org/html-tokenize/-/html-tokenize-1.2.5.tgz", - "integrity": "sha1-flupnstR75Buyaf83ubKMmfHiX4=", - "dev": true, - "dependencies": { - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "minimist": { - "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true - }, - "object-keys": { - "version": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", - "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=", - "dev": true - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "through2": { - "version": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz", - "integrity": "sha1-2/WGYDEVHsg1K7bE22SiKSqEC5s=", - "dev": true - }, - "xtend": { - "version": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", - "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", - "dev": true - } - } - }, - "htmlescape": { - "version": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", - "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=", - "dev": true - }, - "htmlparser2": { - "version": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", - "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=", - "dev": true - }, - "http-errors": { - "version": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.1.tgz", - "integrity": "sha1-X4uO2YrKVFZWv1cplzh/kEpyIlc=" - }, - "http-proxy": { - "version": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.16.2.tgz", - "integrity": "sha1-Bt/ykpUr9k2+hHH6nfcwZtTzd0I=", - "dev": true - }, - "http-signature": { - "version": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=" - }, - "https-browserify": { - "version": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.1.tgz", - "integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=", - "dev": true - }, - "i": { - "version": "https://registry.npmjs.org/i/-/i-0.3.5.tgz", - "integrity": "sha1-HSuFQVjsgWkRPGy39raAHpniEdU=", - "dev": true - }, - "iconv-lite": { - "version": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.17.tgz", - "integrity": "sha1-T9qjs4rLwsAxsEXQ7c3+HsqxjI0=" - }, - "idb-global": { - "version": "https://registry.npmjs.org/idb-global/-/idb-global-1.0.0.tgz", - "integrity": "sha1-srLkgDqO+mN2yTrzBW5qQ1atiW4=" - }, - "identicon.js": { - "version": "https://registry.npmjs.org/identicon.js/-/identicon.js-1.3.0.tgz", - "integrity": "sha1-e/uzhrd14HgalVV4urcegk5oaeA=" - }, - "idna-uts46": { - "version": "https://registry.npmjs.org/idna-uts46/-/idna-uts46-1.1.0.tgz", - "integrity": "sha1-vgmLK3wcq/vvh6i4D2JvrDc2auo=" - }, - "ieee754": { - "version": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", - "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=", - "dev": true - }, - "iframe": { - "version": "https://registry.npmjs.org/iframe/-/iframe-1.0.0.tgz", - "integrity": "sha1-WOdIIrF4oFedCc0WlkD7lTdHDvU=" - }, - "iframe-stream": { - "version": "https://registry.npmjs.org/iframe-stream/-/iframe-stream-1.0.2.tgz", - "integrity": "sha1-PH622TTnXX3V5l0l76XPR8sGPAs=" - }, - "ignore": { - "version": "https://registry.npmjs.org/ignore/-/ignore-3.3.3.tgz", - "integrity": "sha1-QyNS5XrM2HqzEQ6C0/6g5HgSFW0=" - }, - "ignorepatterns": { - "version": "https://registry.npmjs.org/ignorepatterns/-/ignorepatterns-1.1.0.tgz", - "integrity": "sha1-rI9DbyI5td+2bV8NOpBKh6xnzF4=", - "dev": true - }, - "immediate": { - "version": "https://registry.npmjs.org/immediate/-/immediate-3.2.3.tgz", - "integrity": "sha1-0UD6j2FGWb1lQSMwl92qwlzdmRw=" - }, - "imurmurhash": { - "version": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" - }, - "in-publish": { - "version": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", - "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=" - }, - "indexof": { - "version": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", - "dev": true - }, - "inflight": { - "version": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=" - }, - "inherits": { - "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", - "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=" - }, - "inject-css": { - "version": "https://registry.npmjs.org/inject-css/-/inject-css-0.1.1.tgz", - "integrity": "sha1-7z/8eOwCbJbiNV2g3zKRfjUmQVw=" - }, - "inline-source-map": { - "version": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", - "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=", - "dev": true - }, - "inquirer": { - "version": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", - "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=" - }, - "insert-module-globals": { - "version": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.0.1.tgz", - "integrity": "sha1-wDv04BywhtW15azorQr+eInWOMM=", - "dev": true, - "dependencies": { - "concat-stream": { - "version": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", - "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", - "dev": true - }, - "process": { - "version": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", - "dev": true - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", - "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", - "dev": true - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } - } - }, - "interpret": { - "version": "https://registry.npmjs.org/interpret/-/interpret-1.0.3.tgz", - "integrity": "sha1-y8NcYu7uc/Gat7EKgBURQBr8D5A=", - "dev": true - }, - "invariant": { - "version": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", - "integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=" - }, - "invert-kv": { - "version": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" - }, - "ipaddr.js": { - "version": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.3.0.tgz", - "integrity": "sha1-HgOlL9rYOou7KyXL9JmLTP/NPew=" - }, - "is-absolute": { - "version": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.2.6.tgz", - "integrity": "sha1-IN5p89uULvLYe5wto28XIjWxtes=", - "dev": true - }, - "is-arrayish": { - "version": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" - }, - "is-binary-path": { - "version": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true - }, - "is-buffer": { - "version": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", - "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=", - "dev": true - }, - "is-builtin-module": { - "version": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=" - }, - "is-callable": { - "version": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", - "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=" - }, - "is-date-object": { - "version": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" - }, - "is-dotfile": { - "version": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", - "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", - "dev": true - }, - "is-equal-shallow": { - "version": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", - "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", - "dev": true - }, - "is-extendable": { - "version": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true - }, - "is-finite": { - "version": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=" - }, - "is-fn": { - "version": "https://registry.npmjs.org/is-fn/-/is-fn-1.0.0.tgz", - "integrity": "sha1-lUPV3nvPWwiiLsiiC65uKG1RDYw=" - }, - "is-fullwidth-code-point": { - "version": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=" - }, - "is-function": { - "version": "https://registry.npmjs.org/is-function/-/is-function-1.0.1.tgz", - "integrity": "sha1-Es+5i2W1fdPRk6MSH19uL0N2ArU=" - }, - "is-glob": { - "version": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "dev": true - }, - "is-hex-prefixed": { - "version": "https://registry.npmjs.org/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz", - "integrity": "sha1-fY035q135dEnFIkTxXPggtd39VQ=" - }, - "is-my-json-valid": { - "version": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz", - "integrity": "sha1-8Hndm/2uZe4gOKrorLyGqxCeNpM=" - }, - "is-number": { - "version": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", - "dev": true - }, - "is-path-cwd": { - "version": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=" - }, - "is-path-in-cwd": { - "version": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", - "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=" - }, - "is-path-inside": { - "version": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.0.tgz", - "integrity": "sha1-/AbloWg/vaE95mev9xe7wQpI838=" - }, - "is-plain-object": { - "version": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.3.tgz", - "integrity": "sha1-wVvz5LZrYtcu+vKSWEhmPsvGGbY=", - "dev": true, - "dependencies": { - "isobject": { - "version": "https://registry.npmjs.org/isobject/-/isobject-3.0.0.tgz", - "integrity": "sha1-OVZSF/NmF4nooKDAgNX35rxG4aA=", - "dev": true - } - } - }, - "is-posix-bracket": { - "version": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", - "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", - "dev": true - }, - "is-primitive": { - "version": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", - "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", - "dev": true - }, - "is-property": { - "version": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" - }, - "is-regex": { - "version": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=" - }, - "is-relative": { - "version": "https://registry.npmjs.org/is-relative/-/is-relative-0.2.1.tgz", - "integrity": "sha1-0n9MfVFtF1+2ENuEu+7yPDvJeqU=", - "dev": true - }, - "is-resolvable": { - "version": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz", - "integrity": "sha1-jfV8YeouPFAUCNEA+wE8+NbgzGI=" - }, - "is-stream": { - "version": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" - }, - "is-subset": { - "version": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", - "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=", - "dev": true - }, - "is-symbol": { - "version": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", - "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=" - }, - "is-type": { - "version": "https://registry.npmjs.org/is-type/-/is-type-0.0.1.tgz", - "integrity": "sha1-9lHYXDZdRJVdFKUdjXBh8/a0d5w=", - "dev": true - }, - "is-typedarray": { - "version": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "is-unc-path": { - "version": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-0.1.2.tgz", - "integrity": "sha1-arBTpyVzwQJQ/0FqOBTDUXivObk=", - "dev": true - }, - "is-utf8": { - "version": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" - }, - "is-valid-glob": { - "version": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-0.3.0.tgz", - "integrity": "sha1-1LVcafUYhvm2XHDWwmItN+KfSP4=", - "dev": true - }, - "is-windows": { - "version": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", - "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=", - "dev": true - }, - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true - }, - "isomorphic-fetch": { - "version": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", - "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=" - }, - "isstream": { - "version": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "istanbul": { - "version": "https://github.com/gotwarlost/istanbul/tarball/harmony", - "integrity": "sha1-Atq/03Q7aVlC0XCoCRcdOSRGYzE=", - "dev": true, - "optional": true, - "dependencies": { - "abbrev": { - "version": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", - "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", - "dev": true - }, - "async": { - "version": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", - "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", - "dev": true, - "optional": true - }, - "escodegen": { - "version": "https://registry.npmjs.org/escodegen/-/escodegen-1.2.0.tgz", - "integrity": "sha1-Cd55Z3kcyVi3+Jot220jRRrzJ+E=", - "dev": true, - "optional": true, - "dependencies": { - "esprima": { - "version": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", - "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=", - "dev": true, - "optional": true - } - } - }, - "esprima": { - "version": "git://github.com/ariya/esprima.git#a65a3eb93b9a5dce9a1184ca2d1bd0b184c6b8fd", - "dev": true, - "optional": true - }, - "estraverse": { - "version": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz", - "integrity": "sha1-hno+jlip+EYYr7bC3bzZFrfLr3E=", - "dev": true, - "optional": true - }, - "esutils": { - "version": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz", - "integrity": "sha1-gVHTWOIMisx/t0XnRywAJf5JZXA=", - "dev": true, - "optional": true - }, - "mkdirp": { - "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", - "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=", - "dev": true, - "optional": true - }, - "nopt": { - "version": "https://registry.npmjs.org/nopt/-/nopt-2.2.1.tgz", - "integrity": "sha1-KqCbfRdoSHs7ianFqlIzW/8Lrqc=", - "dev": true, - "optional": true - }, - "resolve": { - "version": "https://registry.npmjs.org/resolve/-/resolve-0.6.3.tgz", - "integrity": "sha1-3ZV5gufnNt699TtYpN2RdUV13UY=", - "dev": true, - "optional": true - }, - "source-map": { - "version": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", - "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", - "dev": true, - "optional": true - } - } - }, - "istextorbinary": { - "version": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-1.0.2.tgz", - "integrity": "sha1-rOGTVNGpoBc+/rEITOD4ewrX3s8=", - "dev": true - }, - "jade": { - "version": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", - "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", - "dev": true, - "dependencies": { - "commander": { - "version": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", - "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", - "dev": true - }, - "mkdirp": { - "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", - "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=", - "dev": true - } - } - }, - "jazzicon": { - "version": "https://registry.npmjs.org/jazzicon/-/jazzicon-1.5.0.tgz", - "integrity": "sha1-1/NrUWAj2znubqwRf0BU6Te2Xpk=" - }, - "jodid25519": { - "version": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz", - "integrity": "sha1-BtSRIlUJNBlHfUJWM2BuDpB4KWc=", - "optional": true - }, - "js-beautify": { - "version": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.5.10.tgz", - "integrity": "sha1-TZU3FwJpk0SlFsomv1nwonu3Vxk=", - "dev": true - }, - "js-sha3": { - "version": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.3.1.tgz", - "integrity": "sha1-hhIoAhQvCChQKg0d7h2V4lO7AkM=" - }, - "js-tokens": { - "version": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.1.tgz", - "integrity": "sha1-COnxMkhKLEWjCQfp3E1VZ7fxFNc=" - }, - "js-yaml": { - "version": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.8.4.tgz", - "integrity": "sha1-UgtFZPhlc7qWZir4Woyvp7S1pvY=", - "dependencies": { - "esprima": { - "version": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" - } - } - }, - "jsbn": { - "version": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, - "jsdom": { - "version": "https://registry.npmjs.org/jsdom/-/jsdom-8.5.0.tgz", - "integrity": "sha1-1Nj12/J2hjW2KmKCO5R89wcevJg=", - "dev": true, - "dependencies": { - "acorn": { - "version": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", - "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=", - "dev": true - }, - "escodegen": { - "version": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", - "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", - "dev": true - }, - "esprima": { - "version": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", - "dev": true - }, - "estraverse": { - "version": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", - "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", - "dev": true - }, - "source-map": { - "version": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", - "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", - "dev": true, - "optional": true - } - } - }, - "jsdom-global": { - "version": "https://registry.npmjs.org/jsdom-global/-/jsdom-global-1.7.0.tgz", - "integrity": "sha1-mWe0Cb5xXPf88Ev9N5RblFtJTVI=", - "dev": true - }, - "jsesc": { - "version": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" - }, - "jshint-stylish": { - "version": "https://registry.npmjs.org/jshint-stylish/-/jshint-stylish-0.1.5.tgz", - "integrity": "sha1-1Btu744GpN37NlQL9lk/4xuYcjY=", - "dev": true, - "dependencies": { - "ansi-styles": { - "version": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", - "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=", - "dev": true - }, - "chalk": { - "version": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", - "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", - "dev": true - }, - "strip-ansi": { - "version": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", - "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=", - "dev": true - } - } - }, - "json-rpc-error": { - "version": "https://registry.npmjs.org/json-rpc-error/-/json-rpc-error-2.0.0.tgz", - "integrity": "sha1-p6+cICg4tekFxyUOVH8a/3cligI=" - }, - "json-rpc-random-id": { - "version": "https://registry.npmjs.org/json-rpc-random-id/-/json-rpc-random-id-1.0.1.tgz", - "integrity": "sha1-uknZat7RRE27jaPSA3SKy7zeyMg=" - }, - "json-schema": { - "version": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-stable-stringify": { - "version": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=" - }, - "json-stringify-safe": { - "version": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "json3": { - "version": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", - "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", - "dev": true - }, - "json5": { - "version": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" - }, - "jsonfile": { - "version": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=" - }, - "jsonify": { - "version": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" - }, - "jsonparse": { - "version": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", - "dev": true - }, - "jsonpointer": { - "version": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=" - }, - "JSONStream": { - "version": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz", - "integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=", - "dev": true - }, - "jsprim": { - "version": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", - "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=", - "dependencies": { - "assert-plus": { - "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "keccak": { - "version": "https://registry.npmjs.org/keccak/-/keccak-1.2.0.tgz", - "integrity": "sha1-tTYY/HlhtkL25z8VRu7DMp9+/+A=" - }, - "keccakjs": { - "version": "https://registry.npmjs.org/keccakjs/-/keccakjs-0.2.1.tgz", - "integrity": "sha1-HWM6+QfvMFu/ny+mFtVsRFYd+k0=" - }, - "kind-of": { - "version": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true - }, - "klaw": { - "version": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", - "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=" - }, - "labeled-stream-splicer": { - "version": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.0.tgz", - "integrity": "sha1-pS4dE4AkwAuGscDJH2d5GLiuClk=", - "dev": true, - "dependencies": { - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "stream-splicer": { - "version": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.0.tgz", - "integrity": "sha1-G2O+Q4oTPktnHMGTUZdgAXWRDYM=", - "dev": true - } - } - }, - "last-run": { - "version": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", - "integrity": "sha1-RblpQsF7HHnHchmCWbqUO+v4yls=", - "dev": true - }, - "lazy-debug-legacy": { - "version": "https://registry.npmjs.org/lazy-debug-legacy/-/lazy-debug-legacy-0.0.1.tgz", - "integrity": "sha1-U3cWwHduTPeePtG2IfdljCkRsbE=", - "dev": true - }, - "lazystream": { - "version": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", - "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", - "dev": true - }, - "lcid": { - "version": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=" - }, - "leftpad": { - "version": "https://registry.npmjs.org/leftpad/-/leftpad-0.0.0.tgz", - "integrity": "sha1-Agya0HhyFroPMNedR5tLNV19OcM=", - "dev": true - }, - "level-codec": { - "version": "https://registry.npmjs.org/level-codec/-/level-codec-6.1.0.tgz", - "integrity": "sha1-9d8KmVgvdtrEOFUVGrb05NDWAEU=" - }, - "level-errors": { - "version": "https://registry.npmjs.org/level-errors/-/level-errors-1.0.4.tgz", - "integrity": "sha1-NYXmI5dMc3qTdVSSpDwCZ82kQl8=" - }, - "level-iterator-stream": { - "version": "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-1.3.1.tgz", - "integrity": "sha1-5Dt4sagUPm+pek9IXrjqUwNS8u0=", - "dependencies": { - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=" - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "level-ws": { - "version": "https://registry.npmjs.org/level-ws/-/level-ws-0.0.0.tgz", - "integrity": "sha1-Ny5RIXeSSgBCSwtDrvK7QkltIos=", - "dependencies": { - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "object-keys": { - "version": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", - "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=" - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=" - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "xtend": { - "version": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", - "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=" - } - } - }, - "levelup": { - "version": "https://registry.npmjs.org/levelup/-/levelup-1.3.8.tgz", - "integrity": "sha1-+0QsSI776hBD9+uZKaeSp0+9HaY=", - "dependencies": { - "semver": { - "version": "https://registry.npmjs.org/semver/-/semver-5.1.1.tgz", - "integrity": "sha1-oykqNz5vPgeY2gsgZBuanFvEfhk=" - } - } - }, - "levn": { - "version": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=" - }, - "lexical-scope": { - "version": "https://registry.npmjs.org/lexical-scope/-/lexical-scope-1.2.0.tgz", - "integrity": "sha1-/Ope3HBKSzqHls3KQZw6CvryLfQ=", - "dev": true - }, - "liftoff": { - "version": "https://registry.npmjs.org/liftoff/-/liftoff-2.3.0.tgz", - "integrity": "sha1-qY8v9nGD2Lp8+soQVIvX/wVQs4U=", - "dev": true - }, - "livereload-js": { - "version": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.2.2.tgz", - "integrity": "sha1-bIclfmSKtHW8JOoldFftzB+NC8I=", - "dev": true - }, - "load-json-file": { - "version": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=" - }, - "lodash": { - "version": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" - }, - "lodash-es": { - "version": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.4.tgz", - "integrity": "sha1-3MHXVS4VCgZABzupyzHXDwMpUOc=" - }, - "lodash._baseassign": { - "version": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", - "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", - "dev": true - }, - "lodash._basecopy": { - "version": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", - "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=" - }, - "lodash._baseflatten": { - "version": "https://registry.npmjs.org/lodash._baseflatten/-/lodash._baseflatten-3.1.4.tgz", - "integrity": "sha1-B3D/gBMa9uNPO1EXlqe6UhTmX/c=", - "dev": true - }, - "lodash._basetostring": { - "version": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz", - "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=" - }, - "lodash._basevalues": { - "version": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz", - "integrity": "sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc=" - }, - "lodash._bindcallback": { - "version": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", - "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=", - "dev": true - }, - "lodash._createassigner": { - "version": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz", - "integrity": "sha1-g4pbri/aymOsIt7o4Z+k5taXCxE=", - "dev": true - }, - "lodash._getnative": { - "version": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=" - }, - "lodash._isiterateecall": { - "version": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", - "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=" - }, - "lodash._reescape": { - "version": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz", - "integrity": "sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo=" - }, - "lodash._reevaluate": { - "version": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz", - "integrity": "sha1-WLx0xAZklTrgsSTYBpltrKQx4u0=" - }, - "lodash._reinterpolate": { - "version": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" - }, - "lodash._root": { - "version": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", - "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=" - }, - "lodash.assign": { - "version": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=" - }, - "lodash.assignin": { - "version": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", - "integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI=", - "dev": true - }, - "lodash.assignwith": { - "version": "https://registry.npmjs.org/lodash.assignwith/-/lodash.assignwith-4.2.0.tgz", - "integrity": "sha1-EnqX8CrcQXUalU0ksN4X4QDgOOs=", - "dev": true - }, - "lodash.bind": { - "version": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", - "integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=", - "dev": true - }, - "lodash.clonedeep": { - "version": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true - }, - "lodash.debounce": { - "version": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", - "dev": true - }, - "lodash.defaults": { - "version": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=", - "dev": true - }, - "lodash.escape": { - "version": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", - "integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=" - }, - "lodash.filter": { - "version": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", - "integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=", - "dev": true - }, - "lodash.find": { - "version": "https://registry.npmjs.org/lodash.find/-/lodash.find-4.6.0.tgz", - "integrity": "sha1-ywcE1Hq3F4n/oN6Ll92Sb7iLE7E=", - "dev": true - }, - "lodash.flatten": { - "version": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", - "dev": true - }, - "lodash.foreach": { - "version": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", - "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=", - "dev": true - }, - "lodash.isarguments": { - "version": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=" - }, - "lodash.isarray": { - "version": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=" - }, - "lodash.isempty": { - "version": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", - "integrity": "sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=", - "dev": true - }, - "lodash.isequal": { - "version": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", - "dev": true - }, - "lodash.isfunction": { - "version": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.8.tgz", - "integrity": "sha1-TbcJ/IG8So/XEnpFilNGxc3OLGs=", - "dev": true - }, - "lodash.isplainobject": { - "version": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" - }, - "lodash.isstring": { - "version": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=", - "dev": true - }, - "lodash.keys": { - "version": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=" - }, - "lodash.map": { - "version": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", - "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=", - "dev": true - }, - "lodash.mapvalues": { - "version": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", - "integrity": "sha1-G6+lAF3p3W9PJmaMMMo3IwzJaJw=", - "dev": true - }, - "lodash.memoize": { - "version": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", - "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=", - "dev": true - }, - "lodash.merge": { - "version": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.0.tgz", - "integrity": "sha1-aYhLoUSsM/5plzemCG3v+t0PicU=", - "dev": true - }, - "lodash.pick": { - "version": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", - "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=", - "dev": true - }, - "lodash.pickby": { - "version": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz", - "integrity": "sha1-feoh2MGNdwOifHBMFdO4SmfjOv8=", - "dev": true - }, - "lodash.reduce": { - "version": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", - "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=", - "dev": true - }, - "lodash.reject": { - "version": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", - "integrity": "sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=", - "dev": true - }, - "lodash.restparam": { - "version": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", - "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=" - }, - "lodash.some": { - "version": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", - "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=", - "dev": true - }, - "lodash.sortby": { - "version": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", - "dev": true - }, - "lodash.template": { - "version": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", - "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=" - }, - "lodash.templatesettings": { - "version": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz", - "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=" - }, - "lodash.uniqby": { - "version": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", - "integrity": "sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI=", - "dev": true - }, - "loglevel": { - "version": "https://registry.npmjs.org/loglevel/-/loglevel-1.4.1.tgz", - "integrity": "sha1-lbOD+Ro8J1b9SrCTZn5DCRYfK80=" - }, - "lolex": { - "version": "https://registry.npmjs.org/lolex/-/lolex-1.3.2.tgz", - "integrity": "sha1-fD2mL/yzDw9agKJWbKJORdigHzE=", - "dev": true - }, - "loose-envify": { - "version": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=" - }, - "lru-cache": { - "version": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", - "dev": true - }, - "ltgt": { - "version": "https://registry.npmjs.org/ltgt/-/ltgt-2.1.3.tgz", - "integrity": "sha1-EIUaBtmWS5cReEQcI8nlJpjuzjQ=" - }, - "make-iterator": { - "version": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.0.tgz", - "integrity": "sha1-V7713IXSOSO6I3ZzJNjo+PPZaUs=", - "dev": true - }, - "map-async": { - "version": "https://registry.npmjs.org/map-async/-/map-async-0.1.1.tgz", - "integrity": "sha1-yJfARJ+Fhkx0taPxlu20IVZDF0U=" - }, - "map-cache": { - "version": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-stream": { - "version": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", - "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", - "dev": true - }, - "matchdep": { - "version": "https://registry.npmjs.org/matchdep/-/matchdep-1.0.1.tgz", - "integrity": "sha1-pXozgESR+64girqPaDgEN6vC3KU=", - "dev": true, - "dependencies": { - "findup-sync": { - "version": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz", - "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=", - "dev": true - }, - "glob": { - "version": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "dev": true - } - } - }, - "mdurl": { - "version": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" - }, - "media-typer": { - "version": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "memdown": { - "version": "https://registry.npmjs.org/memdown/-/memdown-1.2.4.tgz", - "integrity": "sha1-zZo0qvB01TRFonEQjrS43U7A8n8=" - }, - "memorystream": { - "version": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", - "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=" - }, - "menu-droppo": { - "version": "https://registry.npmjs.org/menu-droppo/-/menu-droppo-1.1.5.tgz", - "integrity": "sha1-qHqOfjcA7AK+A1+f3B2siTVFCVQ=" - }, - "merge-descriptors": { - "version": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "merge-stream": { - "version": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", - "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", - "dev": true - }, - "merkle-patricia-tree": { - "version": "https://registry.npmjs.org/merkle-patricia-tree/-/merkle-patricia-tree-2.1.2.tgz", - "integrity": "sha1-ckSD1Ut1YxpI/t2lXhFAUXBqcpE=", - "dependencies": { - "ethereumjs-util": { - "version": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-4.5.0.tgz", - "integrity": "sha1-PpQosxfuvaPXJg2FT93alUsfG8Y=" - } - } - }, - "mersenne-twister": { - "version": "https://registry.npmjs.org/mersenne-twister/-/mersenne-twister-1.1.0.tgz", - "integrity": "sha1-+RZhjuQ9cXnvz2Qb7EUx65Zwl4o=" - }, - "metamask-logo": { - "version": "https://registry.npmjs.org/metamask-logo/-/metamask-logo-2.1.3.tgz", - "integrity": "sha1-F1zleuUMc0Szsdwy0v0LCOOXj9A=" - }, - "methods": { - "version": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "micromatch": { - "version": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", - "dev": true - }, - "miller-rabin": { - "version": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.0.tgz", - "integrity": "sha1-SmL7HUKTPAVYOYL0xxb2+55sbT0=", - "dev": true - }, - "mime": { - "version": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", - "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=" - }, - "mime-db": { - "version": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", - "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=" - }, - "mime-types": { - "version": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", - "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=" - }, - "min-document": { - "version": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", - "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=" - }, - "mini-lr": { - "version": "https://registry.npmjs.org/mini-lr/-/mini-lr-0.1.9.tgz", - "integrity": "sha1-AhmdJzR5U9H9HW297UJh8Yey0PY=", - "dev": true, - "dependencies": { - "qs": { - "version": "https://registry.npmjs.org/qs/-/qs-2.2.5.tgz", - "integrity": "sha1-EIirr53MCuWuRbcJ5sa1iIsjkjw=", - "dev": true - } - } - }, - "minimalistic-assert": { - "version": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", - "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=" - }, - "minimalistic-crypto-utils": { - "version": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" - }, - "minimatch": { - "version": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=" - }, - "minimist": { - "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.5.tgz", - "integrity": "sha1-16oye87PUY+RBqxrjwA/o7zqhWY=" - }, - "mississippi": { - "version": "https://registry.npmjs.org/mississippi/-/mississippi-1.3.0.tgz", - "integrity": "sha1-0gFYPrEjJ+PFwWQqQEqcrPlONPU=" - }, - "mkdirp": { - "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dependencies": { - "minimist": { - "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - } - } - }, - "mocha": { - "version": "https://registry.npmjs.org/mocha/-/mocha-2.5.3.tgz", - "integrity": "sha1-FhvlvetJZ3HrmzV0UFC2IrWu/Fg=", - "dev": true, - "dependencies": { - "debug": { - "version": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", - "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", - "dev": true - }, - "escape-string-regexp": { - "version": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz", - "integrity": "sha1-Tbwv5nTnGUnK8/smlc5/LcHZqNE=", - "dev": true - }, - "glob": { - "version": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", - "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", - "dev": true - }, - "minimatch": { - "version": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", - "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", - "dev": true - }, - "ms": { - "version": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", - "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", - "dev": true - }, - "supports-color": { - "version": "https://registry.npmjs.org/supports-color/-/supports-color-1.2.0.tgz", - "integrity": "sha1-/x7R5hFp0Gs88tWI4YixjYhH4X4=", - "dev": true - } - } - }, - "mocha-eslint": { - "version": "https://registry.npmjs.org/mocha-eslint/-/mocha-eslint-2.1.1.tgz", - "integrity": "sha1-S0drRAPwPXANmAEMsxbasCqeFuA=", - "dev": true - }, - "mocha-jsdom": { - "version": "https://registry.npmjs.org/mocha-jsdom/-/mocha-jsdom-1.1.0.tgz", - "integrity": "sha1-4VdvvQYBzInTWKIToOVYXRt8egE=", - "dev": true - }, - "mocha-sinon": { - "version": "https://registry.npmjs.org/mocha-sinon/-/mocha-sinon-1.2.0.tgz", - "integrity": "sha1-lfA0qNreTalmoPOvyJwSbYtYkSM=", - "dev": true - }, - "module-deps": { - "version": "https://registry.npmjs.org/module-deps/-/module-deps-4.1.1.tgz", - "integrity": "sha1-IyFYM/HaE/1gbMuAh7RIUty4If0=", - "dev": true, - "dependencies": { - "concat-stream": { - "version": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", - "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", - "dev": true, - "dependencies": { - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", - "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", - "dev": true - } - } - }, - "duplexer2": { - "version": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } - } - }, - "ms": { - "version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "multipipe": { - "version": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", - "integrity": "sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s=" - }, - "multiplex": { - "version": "https://registry.npmjs.org/multiplex/-/multiplex-6.7.0.tgz", - "integrity": "sha1-/3Pk5AB5FwxEQtFgllZY+N75YMI=" - }, - "mustache": { - "version": "https://registry.npmjs.org/mustache/-/mustache-2.3.0.tgz", - "integrity": "sha1-QCj3d4sXcIpImTCm5SrDvKDaQdA=", - "dev": true - }, - "mute-stdout": { - "version": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.0.tgz", - "integrity": "sha1-WzLqB+tDyd7WEwQ0z5JvRrKn/U0=", - "dev": true - }, - "mute-stream": { - "version": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", - "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=" - }, - "mz": { - "version": "https://registry.npmjs.org/mz/-/mz-2.6.0.tgz", - "integrity": "sha1-yLhSHZWN8KTydoAl22nHGe5O8c4=", - "dev": true, - "dependencies": { - "any-promise": { - "version": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", - "dev": true - } - } - }, - "nan": { - "version": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz", - "integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=" - }, - "ncp": { - "version": "https://registry.npmjs.org/ncp/-/ncp-1.0.1.tgz", - "integrity": "sha1-0VNn5cuHQyuhF9K/gP30Wuz7QkY=", - "dev": true - }, - "negotiator": { - "version": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" - }, - "next-tick": { - "version": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", - "dev": true - }, - "nock": { - "version": "https://registry.npmjs.org/nock/-/nock-8.2.1.tgz", - "integrity": "sha1-ZMxl4b3TiT9Yy6fhq/3Dj0DwNko=", - "dev": true, - "dependencies": { - "lodash": { - "version": "https://registry.npmjs.org/lodash/-/lodash-4.9.0.tgz", - "integrity": "sha1-TCDXQvA86F3HAODderm8q4Xm/BQ=", - "dev": true - } - } - }, - "node-abi": { - "version": "https://registry.npmjs.org/node-abi/-/node-abi-2.0.3.tgz", - "integrity": "sha1-DKZ+XmZ7jhNDVJyhcVOoFdC7/ao=" - }, - "node-fetch": { - "version": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.0.tgz", - "integrity": "sha1-P/bFZUT5t/sAaCM4u1Xub1SooO8=" - }, - "node-notifier": { - "version": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.1.2.tgz", - "integrity": "sha1-L6nhJgX6EACdRFSdb82KY93g5P8=", - "dev": true, - "dependencies": { - "which": { - "version": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", - "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", - "dev": true - } - } - }, - "noop-logger": { - "version": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", - "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=" - }, - "nopt": { - "version": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "dev": true - }, - "normalize-package-data": { - "version": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.3.8.tgz", - "integrity": "sha1-2Bntoqne29H/pWPqQHHZNngilbs=" - }, - "normalize-path": { - "version": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true - }, - "now-and-later": { - "version": "https://registry.npmjs.org/now-and-later/-/now-and-later-1.0.0.tgz", - "integrity": "sha1-I+eYzKrw6Ky+8Gh/gghidHRuCJM=", - "dev": true - }, - "npmlog": { - "version": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.0.tgz", - "integrity": "sha1-3Fm+6F9k8A7UJO+yrweD3yXRwLU=" - }, - "nth-check": { - "version": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", - "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", - "dev": true - }, - "number-is-nan": { - "version": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "number-to-bn": { - "version": "https://registry.npmjs.org/number-to-bn/-/number-to-bn-1.7.0.tgz", - "integrity": "sha1-uzYjWS9+X54AMLGXe9QaDFP+HqA=" - }, - "nwmatcher": { - "version": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.4.0.tgz", - "integrity": "sha1-tDiTYhcOfvl5jDx3FtgOvAEG/M8=", - "dev": true - }, - "oauth-sign": { - "version": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" - }, - "object-assign": { - "version": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-component": { - "version": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", - "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", - "dev": true - }, - "object-inspect": { - "version": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.2.2.tgz", - "integrity": "sha1-yCEV5PzIiK6hTWTCLk8X9qcNXlo=" - }, - "object-is": { - "version": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", - "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=", - "dev": true - }, - "object-keys": { - "version": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", - "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=" - }, - "object.assign": { - "version": "https://registry.npmjs.org/object.assign/-/object.assign-4.0.4.tgz", - "integrity": "sha1-scnMBE7xuf5jYG/BQau7MuFHMMw=", - "dev": true - }, - "object.defaults": { - "version": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", - "dev": true, - "dependencies": { - "for-own": { - "version": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", - "dev": true - }, - "isobject": { - "version": "https://registry.npmjs.org/isobject/-/isobject-3.0.0.tgz", - "integrity": "sha1-OVZSF/NmF4nooKDAgNX35rxG4aA=", - "dev": true - } - } - }, - "object.entries": { - "version": "https://registry.npmjs.org/object.entries/-/object.entries-1.0.4.tgz", - "integrity": "sha1-G/mk3SKI9bM/Opk9JXZh8F0WGl8=", - "dev": true - }, - "object.omit": { - "version": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", - "dev": true - }, - "object.reduce": { - "version": "https://registry.npmjs.org/object.reduce/-/object.reduce-0.1.7.tgz", - "integrity": "sha1-0YDoT3LSGDSK9FNStVFlJGuVBG0=", - "dev": true - }, - "object.values": { - "version": "https://registry.npmjs.org/object.values/-/object.values-1.0.4.tgz", - "integrity": "sha1-5STaCbT2b/Bd9FdUbscqyZ8TBpo=", - "dev": true - }, - "obs-store": { - "version": "https://registry.npmjs.org/obs-store/-/obs-store-2.3.2.tgz", - "integrity": "sha1-JA0Ga1zNZMhj3ob0sOvVH4MQglY=" - }, - "on-finished": { - "version": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=" - }, - "once": { - "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=" - }, - "onetime": { - "version": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=" - }, - "open": { - "version": "https://registry.npmjs.org/open/-/open-0.0.5.tgz", - "integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw=", - "dev": true - }, - "opener": { - "version": "https://registry.npmjs.org/opener/-/opener-1.4.3.tgz", - "integrity": "sha1-XG2ixdflgx6P+jlklQ+NZnSskLg=" - }, - "optimist": { - "version": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=" - }, - "optionator": { - "version": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dependencies": { - "wordwrap": { - "version": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" - } - } - }, - "options": { - "version": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", - "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=", - "dev": true - }, - "ordered-read-streams": { - "version": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz", - "integrity": "sha1-cTfmmzKYuzQiR6G77jiByA4v14s=", - "dev": true - }, - "os-browserify": { - "version": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.1.2.tgz", - "integrity": "sha1-ScoCk+CxlZCl9d4Qx/JlphfY/lQ=", - "dev": true - }, - "os-homedir": { - "version": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-locale": { - "version": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=" - }, - "os-tmpdir": { - "version": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "outpipe": { - "version": "https://registry.npmjs.org/outpipe/-/outpipe-1.1.1.tgz", - "integrity": "sha1-UM+GFjZeh+Ax4ppeyTOaPaRyX6I=", - "dev": true - }, - "pako": { - "version": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", - "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=", - "dev": true - }, - "parallel-transform": { - "version": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", - "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=" - }, - "parents": { - "version": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", - "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=", - "dev": true - }, - "parse-asn1": { - "version": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.0.tgz", - "integrity": "sha1-N8T5t+06tlx0gXtfJICTf7+XxxI=", - "dev": true - }, - "parse-filepath": { - "version": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.1.tgz", - "integrity": "sha1-FZ1hVdQ5BNFsEO9piRHaHpGWm3M=", - "dev": true - }, - "parse-glob": { - "version": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", - "dev": true - }, - "parse-headers": { - "version": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.1.tgz", - "integrity": "sha1-aug6eqJanZtwCswoaYzR8e1+lTY=" - }, - "parse-json": { - "version": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=" - }, - "parse-passwd": { - "version": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", - "dev": true - }, - "parse5": { - "version": "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz", - "integrity": "sha1-m387DeMr543CQBsXVzzK8Pb1nZQ=", - "dev": true - }, - "parsejson": { - "version": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.3.tgz", - "integrity": "sha1-q343WfIJ7OmUN5c/fQ8fZK4OZKs=", - "dev": true - }, - "parseqs": { - "version": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", - "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", - "dev": true - }, - "parseuri": { - "version": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", - "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", - "dev": true - }, - "parseurl": { - "version": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz", - "integrity": "sha1-yKuMkiO6NIiKpkopeyiFO+wY2lY=" - }, - "pascalcase": { - "version": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" - }, - "path-browserify": { - "version": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", - "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", - "dev": true - }, - "path-dirname": { - "version": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, - "path-exists": { - "version": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=" - }, - "path-is-absolute": { - "version": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-is-inside": { - "version": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" - }, - "path-platform": { - "version": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", - "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=", - "dev": true - }, - "path-root": { - "version": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", - "dev": true - }, - "path-root-regex": { - "version": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", - "dev": true - }, - "path-to-regexp": { - "version": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "path-type": { - "version": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=" - }, - "pause-stream": { - "version": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", - "dev": true - }, - "pbkdf2": { - "version": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.12.tgz", - "integrity": "sha1-vjZ4XFBn6kjYBv+SMojF91C2uKI=" - }, - "performance-now": { - "version": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", - "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" - }, - "pify": { - "version": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - }, - "ping-pong-stream": { - "version": "https://registry.npmjs.org/ping-pong-stream/-/ping-pong-stream-1.0.0.tgz", - "integrity": "sha1-TF6wm6atsCGInawNyr+45XcGhUo=" - }, - "pinkie": { - "version": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" - }, - "pinkie-promise": { - "version": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=" - }, - "pkginfo": { - "version": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.4.0.tgz", - "integrity": "sha1-NJ27f/04CB/K3AhT32h/DHdEzWU=", - "dev": true - }, - "plucker": { - "version": "https://registry.npmjs.org/plucker/-/plucker-0.0.0.tgz", - "integrity": "sha1-L/ok4Dqyz/pOda3B33DyViPEXQk=" - }, - "pluralize": { - "version": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", - "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=" - }, - "pojo-migrator": { - "version": "https://registry.npmjs.org/pojo-migrator/-/pojo-migrator-2.1.0.tgz", - "integrity": "sha1-PCo7n4C6Wp+367kh0zRNtO+l9mk=" - }, - "polyfill-crypto.getrandomvalues": { - "version": "https://registry.npmjs.org/polyfill-crypto.getrandomvalues/-/polyfill-crypto.getrandomvalues-1.0.0.tgz", - "integrity": "sha1-XJVgKXbrthVbFjy2XXe57t47YaQ=" - }, - "portfinder": { - "version": "https://registry.npmjs.org/portfinder/-/portfinder-0.2.1.tgz", - "integrity": "sha1-srmwFk+eF/o6nH2yME0KdRQMca0=", - "dev": true, - "dependencies": { - "mkdirp": { - "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.0.7.tgz", - "integrity": "sha1-2JtPDkw+XlylQjWTFnXglP4aUHI=", - "dev": true - } - } - }, - "post-message-stream": { - "version": "https://registry.npmjs.org/post-message-stream/-/post-message-stream-1.0.0.tgz", - "integrity": "sha1-UO/gVjKuQza1HpSjFuVS6fFtqpw=" - }, - "prebuild-install": { - "version": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-2.1.2.tgz", - "integrity": "sha1-2a4MqFMw4Dli2TKS+VqLRMLr9QU=", - "dependencies": { - "minimist": { - "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "prelude-ls": { - "version": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" - }, - "preserve": { - "version": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", - "dev": true - }, - "pretty-bytes": { - "version": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-0.1.2.tgz", - "integrity": "sha1-zZApTVihyk6KXQ+5yCJZmIgazwA=", - "dev": true - }, - "pretty-hrtime": { - "version": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", - "dev": true - }, - "printf": { - "version": "https://registry.npmjs.org/printf/-/printf-0.2.5.tgz", - "integrity": "sha1-xDjKLKM+OSdnHbSracDlL5NqTw8=", - "dev": true - }, - "private": { - "version": "https://registry.npmjs.org/private/-/private-0.1.7.tgz", - "integrity": "sha1-aM5eih7woju1cMwoU3tTMqumPvE=" - }, - "process": { - "version": "https://registry.npmjs.org/process/-/process-0.5.2.tgz", - "integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=" - }, - "process-nextick-args": { - "version": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" - }, - "progress": { - "version": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", - "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=" - }, - "promise": { - "version": "https://registry.npmjs.org/promise/-/promise-7.1.1.tgz", - "integrity": "sha1-SJZUxpJha4qlWwck+oCbt9tJxb8=" - }, - "promise-filter": { - "version": "https://registry.npmjs.org/promise-filter/-/promise-filter-1.1.0.tgz", - "integrity": "sha1-fsPOmQyGfMud6GONvRnuF6UqS1k=" - }, - "promise-to-callback": { - "version": "https://registry.npmjs.org/promise-to-callback/-/promise-to-callback-1.0.0.tgz", - "integrity": "sha1-XSp0kBC/tn2WNZj805YHRqaP7vc=" - }, - "prompt": { - "version": "https://registry.npmjs.org/prompt/-/prompt-1.0.0.tgz", - "integrity": "sha1-jlcSPDlquYiJf7Mn/Trtw+c15P4=", - "dev": true - }, - "prop-types": { - "version": "https://registry.npmjs.org/prop-types/-/prop-types-15.5.10.tgz", - "integrity": "sha1-J5ffwxJhguOpXj37suiT3ddFYVQ=" - }, - "propagate": { - "version": "https://registry.npmjs.org/propagate/-/propagate-0.4.0.tgz", - "integrity": "sha1-8/zKCm/gZzanulcpZgaWF8EwtIE=", - "dev": true - }, - "proto-list": { - "version": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", - "dev": true - }, - "proxy-addr": { - "version": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.1.4.tgz", - "integrity": "sha1-J+VF9pYKRKYn2bREZ+NcG2tM4vM=" - }, - "prr": { - "version": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" - }, - "pseudomap": { - "version": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true - }, - "public-encrypt": { - "version": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz", - "integrity": "sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=", - "dev": true - }, - "pump": { - "version": "https://registry.npmjs.org/pump/-/pump-1.0.2.tgz", - "integrity": "sha1-Oz7mUS+U8OV1U4wXmV+fFpkKXVE=" - }, - "pumpify": { - "version": "https://registry.npmjs.org/pumpify/-/pumpify-1.3.5.tgz", - "integrity": "sha1-G2ccYZlAq8rqwK0OOjwWS+dgmTs=" - }, - "punycode": { - "version": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz", - "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0=" - }, - "qrcode-npm": { - "version": "https://registry.npmjs.org/qrcode-npm/-/qrcode-npm-0.0.3.tgz", - "integrity": "sha1-d+5vvvqcDyn6CdTRUggHxqYEK5o=" - }, - "qs": { - "version": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", - "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" - }, - "querystring": { - "version": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "dev": true - }, - "querystring-es3": { - "version": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", - "dev": true - }, - "qunit": { - "version": "https://registry.npmjs.org/qunit/-/qunit-0.9.3.tgz", - "integrity": "sha1-qR8HM06FR7rbqmrhhBwSaZC4EpU=", - "dev": true, - "dependencies": { - "co": { - "version": "https://registry.npmjs.org/co/-/co-3.1.0.tgz", - "integrity": "sha1-TqVOpaCJOBUxheFSEMaNkJK8G3g=", - "dev": true - } - } - }, - "qunitjs": { - "version": "https://registry.npmjs.org/qunitjs/-/qunitjs-1.23.1.tgz", - "integrity": "sha1-GXHPl6yb4Bpk0jFVCNLkjm/U5xk=", - "dev": true - }, - "quote-stream": { - "version": "https://registry.npmjs.org/quote-stream/-/quote-stream-1.0.2.tgz", - "integrity": "sha1-hJY/jJwmuULhU/7rU6rnRlK34LI=", - "dependencies": { - "minimist": { - "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "ramda": { - "version": "https://registry.npmjs.org/ramda/-/ramda-0.23.0.tgz", - "integrity": "sha1-zNE//3NJepOXTj6GMnv9h71ujis=", - "dev": true - }, - "randomatic": { - "version": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.6.tgz", - "integrity": "sha1-EQ3Kv/OX6dz/fAeJzMCkmt8exbs=", - "dev": true - }, - "randombytes": { - "version": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.3.tgz", - "integrity": "sha1-Z0yZdgkBw8QRJ3GjHlIdw0nMCew=" - }, - "range-parser": { - "version": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" - }, - "raphael": { - "version": "https://registry.npmjs.org/raphael/-/raphael-2.2.7.tgz", - "integrity": "sha1-IxsZFB+NCGmG2PrOtm+LVi7iyBA=" - }, - "raw-body": { - "version": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.7.tgz", - "integrity": "sha1-rf6s4uT7MJgFgBTQjActzFl1h3Q=", - "dev": true, - "dependencies": { - "bytes": { - "version": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz", - "integrity": "sha1-fZcZb51br39pNeJZhVSe3SpsIzk=", - "dev": true - }, - "iconv-lite": { - "version": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz", - "integrity": "sha1-H4irpKsLFQjoMSrMOTRfNumS4vI=", - "dev": true - } - } - }, - "rc": { - "version": "https://registry.npmjs.org/rc/-/rc-1.2.1.tgz", - "integrity": "sha1-LgPo5C7kULjLPc5lvhv4l04d/ZU=", - "dependencies": { - "minimist": { - "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "react": { - "version": "https://registry.npmjs.org/react/-/react-15.5.4.tgz", - "integrity": "sha1-+oPrAVBqsjfNwcjDsc6o3gEr8Ec=" - }, - "react-addons-css-transition-group": { - "version": "https://registry.npmjs.org/react-addons-css-transition-group/-/react-addons-css-transition-group-15.5.2.tgz", - "integrity": "sha1-6n4Knw4cJ8pCbaTv01WZFb1C6tI=" - }, - "react-addons-test-utils": { - "version": "https://registry.npmjs.org/react-addons-test-utils/-/react-addons-test-utils-15.5.1.tgz", - "integrity": "sha1-4NJYzaKhIq0N/2n4OCYNDDlY9fc=", - "dev": true - }, - "react-dom": { - "version": "https://registry.npmjs.org/react-dom/-/react-dom-15.5.4.tgz", - "integrity": "sha1-ugwoeG/VLtfk8hNf4CiNRirvk9o=" - }, - "react-hyperscript": { - "version": "https://registry.npmjs.org/react-hyperscript/-/react-hyperscript-2.4.2.tgz", - "integrity": "sha1-wZsfWhYcot8QvM5t0imehUepgv4=" - }, - "react-input-autosize": { - "version": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-1.1.4.tgz", - "integrity": "sha1-y8RQctQITdxXgG2447NOZEuDZqw=" - }, - "react-markdown": { - "version": "https://registry.npmjs.org/react-markdown/-/react-markdown-2.5.0.tgz", - "integrity": "sha1-scYZBP7liViGgDvZ332yPD3DqJ4=" - }, - "react-redux": { - "version": "https://registry.npmjs.org/react-redux/-/react-redux-4.4.8.tgz", - "integrity": "sha1-57wd0QDotk6WrIIS2xEyObni4I8=" - }, - "react-select": { - "version": "https://registry.npmjs.org/react-select/-/react-select-1.0.0-rc.5.tgz", - "integrity": "sha1-nTFvJSsa3Dct21zfHxGca3z9tdY=" - }, - "react-simple-file-input": { - "version": "https://registry.npmjs.org/react-simple-file-input/-/react-simple-file-input-1.0.0.tgz", - "integrity": "sha1-DVmJtRub8sJbtIoMP9fnPkE+qkg=" - }, - "react-test-renderer": { - "version": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-15.5.4.tgz", - "integrity": "sha1-1OuyP2E9aF6o9TkBCcLSD798g7w=", - "dev": true - }, - "react-testutils-additions": { - "version": "https://registry.npmjs.org/react-testutils-additions/-/react-testutils-additions-15.2.0.tgz", - "integrity": "sha1-eAKm8o3/nPtnPL6vMoAc1qBU5rc=", - "dev": true, - "dependencies": { - "object-assign": { - "version": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", - "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", - "dev": true - } - } - }, - "react-tooltip-component": { - "version": "https://registry.npmjs.org/react-tooltip-component/-/react-tooltip-component-0.3.0.tgz", - "integrity": "sha1-+z7HjDJw/pGWkrwx8UBBCLz0eF4=" - }, - "read": { - "version": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", - "dev": true - }, - "read-only-stream": { - "version": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", - "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=", - "dev": true - }, - "read-pkg": { - "version": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=" - }, - "read-pkg-up": { - "version": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=" - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.9.tgz", - "integrity": "sha1-z3jsb0ptHrQ9JkiMrJfwQudLf8g=" - }, - "readable-wrap": { - "version": "https://registry.npmjs.org/readable-wrap/-/readable-wrap-1.0.0.tgz", - "integrity": "sha1-O1ohHGMeEjA6VJkcgGwX564ga/8=", - "dev": true, - "dependencies": { - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } - } - }, - "readdirp": { - "version": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", - "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", - "dev": true - }, - "readline2": { - "version": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", - "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=" - }, - "rechoir": { - "version": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", - "dev": true - }, - "redux": { - "version": "https://registry.npmjs.org/redux/-/redux-3.6.0.tgz", - "integrity": "sha1-iHwrPQub2G7KK+cFccJ2VMGeGI0=" - }, - "redux-logger": { - "version": "https://registry.npmjs.org/redux-logger/-/redux-logger-2.10.2.tgz", - "integrity": "sha1-PFpfCm8yV3wd6t9mVfJX+CxsOTc=" - }, - "redux-thunk": { - "version": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-1.0.3.tgz", - "integrity": "sha1-d4qgCZ7qBZUDGrazkWX2Zw2NJr0=" - }, - "regenerate": { - "version": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.2.tgz", - "integrity": "sha1-0ZQcZ7rUN+G+dkM63Vs4X5WxkmA=" - }, - "regenerator-runtime": { - "version": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", - "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=" - }, - "regenerator-transform": { - "version": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.9.11.tgz", - "integrity": "sha1-On0GdSDLe3F2dp61/4aGkb7+EoM=" - }, - "regex-cache": { - "version": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.3.tgz", - "integrity": "sha1-mxpsNdTQ3871cRrmUejp09cRQUU=", - "dev": true - }, - "regexpu-core": { - "version": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", - "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=" - }, - "regjsgen": { - "version": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", - "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=" - }, - "regjsparser": { - "version": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", - "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=" - }, - "remove-trailing-separator": { - "version": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.0.1.tgz", - "integrity": "sha1-YV67lq9VlVLUv0BXyENtSGq2PMQ=", - "dev": true - }, - "repeat-element": { - "version": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", - "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", - "dev": true - }, - "repeat-string": { - "version": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "repeating": { - "version": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=" - }, - "replace-ext": { - "version": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", - "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=" - }, - "replaceall": { - "version": "https://registry.npmjs.org/replaceall/-/replaceall-0.1.6.tgz", - "integrity": "sha1-gdgax663LX9cSUKt8ml6MiBojY4=", - "dev": true - }, - "replacestream": { - "version": "https://registry.npmjs.org/replacestream/-/replacestream-4.0.2.tgz", - "integrity": "sha1-DEFAcH5PAyP1DeBEhRcIz1i8N70=", - "dev": true - }, - "request": { - "version": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", - "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", - "dependencies": { - "tunnel-agent": { - "version": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=" - }, - "uuid": { - "version": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", - "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=" - } - } - }, - "request-promise": { - "version": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.1.tgz", - "integrity": "sha1-fuxWyJMXqCLL/qmbA5zlQ8LhX2c=" - }, - "request-promise-core": { - "version": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", - "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=" - }, - "require-directory": { - "version": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" - }, - "require-from-string": { - "version": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz", - "integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=" - }, - "require-main-filename": { - "version": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" - }, - "require-uncached": { - "version": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", - "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=" - }, - "requires-port": { - "version": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true - }, - "resolve": { - "version": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" - }, - "resolve-dir": { - "version": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", - "integrity": "sha1-shklmlYC+sXFxJatiUpujMQwJh4=", - "dev": true - }, - "resolve-from": { - "version": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=" - }, - "resolve-url": { - "version": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "response-stream": { - "version": "https://registry.npmjs.org/response-stream/-/response-stream-0.0.0.tgz", - "integrity": "sha1-2ksXzHaEyYyWK+tNlfZoyNytCdU=", - "dev": true - }, - "restore-cursor": { - "version": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=" - }, - "resumer": { - "version": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", - "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=" - }, - "revalidator": { - "version": "https://registry.npmjs.org/revalidator/-/revalidator-0.1.8.tgz", - "integrity": "sha1-/s5hv6DBtSoga9axgZgYS91SOjs=", - "dev": true - }, - "rimraf": { - "version": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", - "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=" - }, - "ripemd160": { - "version": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", - "integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=" - }, - "rlp": { - "version": "https://registry.npmjs.org/rlp/-/rlp-2.0.0.tgz", - "integrity": "sha1-nbOE/0uJqPYVY9kjldhiWxjzr7A=" - }, - "run-async": { - "version": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", - "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=" - }, - "rx-lite": { - "version": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", - "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=" - }, - "safe-buffer": { - "version": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", - "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=" - }, - "samsam": { - "version": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz", - "integrity": "sha1-vsEf3IOp/aBjQBIQ5AF2wwJNFWc=", - "dev": true - }, - "sandwich-expando": { - "version": "https://registry.npmjs.org/sandwich-expando/-/sandwich-expando-1.1.1.tgz", - "integrity": "sha1-g4BvzKI3Wvi2ww5vUu1PmJ3rsWU=" - }, - "sax": { - "version": "https://registry.npmjs.org/sax/-/sax-1.2.2.tgz", - "integrity": "sha1-/YYxojvHgmvvXYcb24c3jJVkeCg=", - "dev": true - }, - "script-injector": { - "version": "https://registry.npmjs.org/script-injector/-/script-injector-1.0.0.tgz", - "integrity": "sha1-9vTH9qXcxZ4IJG52vfyDoKFAaSY=", - "dev": true, - "dependencies": { - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "through2": { - "version": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", - "dev": true - } - } - }, - "scrypt": { - "version": "https://registry.npmjs.org/scrypt/-/scrypt-6.0.3.tgz", - "integrity": "sha1-BOAUpWgrU/pQwtXM4WfXGcBthw0=" - }, - "scrypt.js": { - "version": "https://registry.npmjs.org/scrypt.js/-/scrypt.js-0.2.0.tgz", - "integrity": "sha1-r40UZbcemZARC+38WTuUeeA6ito=" - }, - "scryptsy": { - "version": "https://registry.npmjs.org/scryptsy/-/scryptsy-1.2.1.tgz", - "integrity": "sha1-oyJfpLJST4AnAHYeKFW987LZIWM=" - }, - "secp256k1": { - "version": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.2.5.tgz", - "integrity": "sha1-Dd5bJ+UCFmX23/ynssPgEMbBPJM=" - }, - "semaphore": { - "version": "https://registry.npmjs.org/semaphore/-/semaphore-1.0.5.tgz", - "integrity": "sha1-tJJXbmavGT25XWXiXsU/Xxl5jWA=" - }, - "semver": { - "version": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" - }, - "semver-greatest-satisfied-range": { - "version": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.0.0.tgz", - "integrity": "sha1-T7RB4qjSbEC1mDJ1VzGN4nKlWKA=", - "dev": true, - "dependencies": { - "semver": { - "version": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", - "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", - "dev": true - } - } - }, - "semver-regex": { - "version": "https://registry.npmjs.org/semver-regex/-/semver-regex-1.0.0.tgz", - "integrity": "sha1-kqSWkGX5xwxpR1PVUkj8aPj2Usk=", - "dev": true - }, - "send": { - "version": "https://registry.npmjs.org/send/-/send-0.15.3.tgz", - "integrity": "sha1-UBP5+ZAj31DRvZiSwZ4979HVMwk=" - }, - "serve-static": { - "version": "https://registry.npmjs.org/serve-static/-/serve-static-1.12.3.tgz", - "integrity": "sha1-n0uhni8wMMVH+K+ZEHg47DjVseI=" - }, - "set-blocking": { - "version": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "set-immediate-shim": { - "version": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" - }, - "setimmediate": { - "version": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" - }, - "setprototypeof": { - "version": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" - }, - "sha.js": { - "version": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.8.tgz", - "integrity": "sha1-NwaMLEdra69ALRSknGf1l5IfY08=" - }, - "sha3": { - "version": "https://registry.npmjs.org/sha3/-/sha3-1.2.0.tgz", - "integrity": "sha1-aYnxtwpJhwWHajc+LGKs6WqpOZo=" - }, - "shallow-copy": { - "version": "https://registry.npmjs.org/shallow-copy/-/shallow-copy-0.0.1.tgz", - "integrity": "sha1-QV9CcC1z2BAzApLMXuhurhoRoXA=" - }, - "shasum": { - "version": "https://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz", - "integrity": "sha1-5wEjENj0F/TetXEhUOVni4euVl8=", - "dev": true, - "dependencies": { - "json-stable-stringify": { - "version": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz", - "integrity": "sha1-YRwj6BTbN1Un34URk9tZ3Sryf0U=", - "dev": true - } - } - }, - "shebang-command": { - "version": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true - }, - "shebang-regex": { - "version": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "shell-quote": { - "version": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", - "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", - "dev": true - }, - "shelljs": { - "version": "https://registry.npmjs.org/shelljs/-/shelljs-0.6.1.tgz", - "integrity": "sha1-7GIRvtGSBEIIj+D3Cyg3Iy7SyKg=" - }, - "shellwords": { - "version": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.0.tgz", - "integrity": "sha1-Zq/Ue2oSky2Qccv9mKUueFzQuhQ=", - "dev": true - }, - "sigmund": { - "version": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", - "dev": true - }, - "signal-exit": { - "version": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "simple-get": { - "version": "https://registry.npmjs.org/simple-get/-/simple-get-1.4.3.tgz", - "integrity": "sha1-6XVe2kB+ltpAxeUVjJ6jezO+y+s=" - }, - "sinon": { - "version": "https://registry.npmjs.org/sinon/-/sinon-1.17.7.tgz", - "integrity": "sha1-RUKk9JugxFwF6y6d2dID4rjv4L8=", - "dev": true - }, - "sizzle": { - "version": "https://registry.npmjs.org/sizzle/-/sizzle-2.3.3.tgz", - "integrity": "sha1-TrB4w3IxpWtS5Bk/cB5++JN+YGs=", - "dev": true - }, - "slash": { - "version": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" - }, - "slice-ansi": { - "version": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=" - }, - "sntp": { - "version": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=" - }, - "socket.io": { - "version": "https://registry.npmjs.org/socket.io/-/socket.io-1.6.0.tgz", - "integrity": "sha1-PkDZMmN+a9kjmBslyvfFPoO24uE=", - "dev": true, - "dependencies": { - "debug": { - "version": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", - "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", - "dev": true - }, - "ms": { - "version": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", - "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", - "dev": true - }, - "object-assign": { - "version": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz", - "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=", - "dev": true - } - } - }, - "socket.io-adapter": { - "version": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz", - "integrity": "sha1-y21LuL7IHhB4uZZ3+c7QBGBmu4s=", - "dev": true, - "dependencies": { - "debug": { - "version": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", - "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", - "dev": true - }, - "ms": { - "version": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", - "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", - "dev": true - } - } - }, - "socket.io-client": { - "version": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.6.0.tgz", - "integrity": "sha1-W2aPT3cTBN/u0XkGRwg4b6ZxeFM=", - "dev": true, - "dependencies": { - "component-emitter": { - "version": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "debug": { - "version": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", - "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", - "dev": true - }, - "ms": { - "version": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", - "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", - "dev": true - } - } - }, - "socket.io-parser": { - "version": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.3.1.tgz", - "integrity": "sha1-3VMgJRA85Clpcya+/WQAX8/ltKA=", - "dev": true, - "dependencies": { - "debug": { - "version": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", - "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", - "dev": true - }, - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "ms": { - "version": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", - "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", - "dev": true - } - } - }, - "solc": { - "version": "https://registry.npmjs.org/solc/-/solc-0.4.11.tgz", - "integrity": "sha1-JSLrQ+fAQZusIGC5biCiWTv7Xos=", - "dependencies": { - "yargs": { - "version": "https://registry.npmjs.org/yargs/-/yargs-4.8.1.tgz", - "integrity": "sha1-wMQpJMpKqmsObaFznfshZDn53cA=" - }, - "yargs-parser": { - "version": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-2.4.1.tgz", - "integrity": "sha1-hVaN488VD/SfpRgl8DqMiA3cxcQ=" - } - } - }, - "source-map": { - "version": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", - "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=" - }, - "source-map-resolve": { - "version": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.3.1.tgz", - "integrity": "sha1-YQ9hIqRFuN1RU1oqcbeD38Ekh2E=", - "dev": true - }, - "source-map-support": { - "version": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.15.tgz", - "integrity": "sha1-AyAt9lwG0r2MfsI2KhkwVv7407E=" - }, - "source-map-url": { - "version": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.3.0.tgz", - "integrity": "sha1-fsrxO1e80J2opAxdJp2zN5nUqvk=", - "dev": true - }, - "sparkles": { - "version": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.0.tgz", - "integrity": "sha1-Gsu/tZJDbRC76PeFt8xvgoFQEsM=" - }, - "spawn-args": { - "version": "https://registry.npmjs.org/spawn-args/-/spawn-args-0.2.0.tgz", - "integrity": "sha1-+30L0dcP1DFr2ePew4nmX51jYbs=", - "dev": true - }, - "spdx-correct": { - "version": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", - "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=" - }, - "spdx-expression-parse": { - "version": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", - "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=" - }, - "spdx-license-ids": { - "version": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", - "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=" - }, - "split": { - "version": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", - "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", - "dev": true - }, - "sprintf-js": { - "version": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "sshpk": { - "version": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.0.tgz", - "integrity": "sha1-/yo+T9BEl1Vf7Zezmg/YL6+zozw=", - "dependencies": { - "assert-plus": { - "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "stack-trace": { - "version": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", - "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=", - "dev": true - }, - "static-eval": { - "version": "https://registry.npmjs.org/static-eval/-/static-eval-0.2.4.tgz", - "integrity": "sha1-t9NNg4k3uWn5ZBygfUj47eJj6ns=", - "dependencies": { - "escodegen": { - "version": "https://registry.npmjs.org/escodegen/-/escodegen-0.0.28.tgz", - "integrity": "sha1-Dk/xcV8yh3XWyrUaxEpAbNer/9M=" - }, - "esprima": { - "version": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", - "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=" - }, - "estraverse": { - "version": "https://registry.npmjs.org/estraverse/-/estraverse-1.3.2.tgz", - "integrity": "sha1-N8K4k+8T1yPydth41g2FNRUqbEI=" - } - } - }, - "static-module": { - "version": "https://registry.npmjs.org/static-module/-/static-module-1.3.2.tgz", - "integrity": "sha1-Mp+58iOlZiZr2nGEO32TLHZxdPM=", - "dependencies": { - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "minimist": { - "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "object-inspect": { - "version": "https://registry.npmjs.org/object-inspect/-/object-inspect-0.4.0.tgz", - "integrity": "sha1-9RV8EWwUVbJDsG7pdwM5LFrYn+w=" - }, - "object-keys": { - "version": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", - "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=" - }, - "quote-stream": { - "version": "https://registry.npmjs.org/quote-stream/-/quote-stream-0.0.0.tgz", - "integrity": "sha1-zeKelMQJsW4Z3HCYuJtmWPlyHTs=" - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=" - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "through2": { - "version": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz", - "integrity": "sha1-2/WGYDEVHsg1K7bE22SiKSqEC5s=" - }, - "xtend": { - "version": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", - "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=" - } - } - }, - "statuses": { - "version": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" - }, - "stealthy-require": { - "version": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" - }, - "stream-browserify": { - "version": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", - "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", - "dev": true - }, - "stream-combiner": { - "version": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", - "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", - "dev": true - }, - "stream-combiner2": { - "version": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", - "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", - "dev": true, - "dependencies": { - "duplexer2": { - "version": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true - } - } - }, - "stream-each": { - "version": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.0.tgz", - "integrity": "sha1-HpXUdXP1gNgU3A/4zQ9m8c5TyZE=" - }, - "stream-exhaust": { - "version": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.1.tgz", - "integrity": "sha1-wMRFXlTOWhecqHNuczNLTn/WdVM=", - "dev": true - }, - "stream-http": { - "version": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.1.tgz", - "integrity": "sha1-VGpRdBrVprB+njGwsQRBqRffUoo=", - "dev": true - }, - "stream-shift": { - "version": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" - }, - "stream-splicer": { - "version": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-1.3.2.tgz", - "integrity": "sha1-PARBvhW5v04iYnXm3IOWR0VUZmE=", - "dev": true, - "dependencies": { - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "through2": { - "version": "https://registry.npmjs.org/through2/-/through2-1.1.1.tgz", - "integrity": "sha1-CEfLxESfNAVXTb3M2buEG4OsNUU=", - "dev": true - } - } - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.1.tgz", - "integrity": "sha1-YuIA8DmVWmgQ2N8KM//A8BNmLZg=" - }, - "string-width": { - "version": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=" - }, - "string.prototype.repeat": { - "version": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-0.2.0.tgz", - "integrity": "sha1-q6Nt4I3O5qWjN9SbLqHaGyj8Ds8=" - }, - "string.prototype.trim": { - "version": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz", - "integrity": "sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo=" - }, - "stringstream": { - "version": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" - }, - "strip-ansi": { - "version": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=" - }, - "strip-bom": { - "version": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=" - }, - "strip-bom-stream": { - "version": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz", - "integrity": "sha1-5xRDmFd9Uaa+0PoZlPoF9D/ZiO4=", - "dev": true - }, - "strip-hex-prefix": { - "version": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz", - "integrity": "sha1-DF8VX+8RUTczd96du1iNoFUA428=" - }, - "strip-json-comments": { - "version": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "styled_string": { - "version": "https://registry.npmjs.org/styled_string/-/styled_string-0.0.1.tgz", - "integrity": "sha1-0ieCvYEpVFm8Tx3xjEutjpTdEko=", - "dev": true - }, - "subarg": { - "version": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", - "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", - "dev": true, - "dependencies": { - "minimist": { - "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } - } - }, - "supports-color": { - "version": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - }, - "sw-stream": { - "version": "https://registry.npmjs.org/sw-stream/-/sw-stream-2.0.0.tgz", - "integrity": "sha1-Yo677rnu4LZrA+xS/FX8xO6yPPM=" - }, - "symbol-observable": { - "version": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.4.tgz", - "integrity": "sha1-Kb9hXUqnEhvdiYsi1LP5vE4qoD0=" - }, - "symbol-tree": { - "version": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", - "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", - "dev": true - }, - "syntax-error": { - "version": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.3.0.tgz", - "integrity": "sha1-HtkmbE1AvnXcVb+bsct3Biu5bKE=", - "dev": true - }, - "table": { - "version": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", - "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", - "dependencies": { - "is-fullwidth-code-point": { - "version": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "https://registry.npmjs.org/string-width/-/string-width-2.0.0.tgz", - "integrity": "sha1-Y1xUNsxypuDDh87KJ41OLuxSaH4=" - } - } - }, - "tap-parser": { - "version": "https://registry.npmjs.org/tap-parser/-/tap-parser-5.3.3.tgz", - "integrity": "sha1-U+yKkPJ11v/0PxaeVqZ5UCp0EYU=", - "dev": true - }, - "tape": { - "version": "https://registry.npmjs.org/tape/-/tape-4.6.3.tgz", - "integrity": "sha1-Y353WB6ass4XV36b1M5PV1gG2LY=", - "dependencies": { - "minimist": { - "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "tar-fs": { - "version": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.15.2.tgz", - "integrity": "sha1-dh9bMpMsezlGGmDVN/rqDYCEgww=" - }, - "tar-stream": { - "version": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.5.4.tgz", - "integrity": "sha1-NlSc8E7RrumyowwBQyUiONr5QBY=", - "dependencies": { - "bl": { - "version": "https://registry.npmjs.org/bl/-/bl-1.2.1.tgz", - "integrity": "sha1-ysMo977kVzDUBLaSID/LWQ4XLV4=" - } - } - }, - "ternary-stream": { - "version": "https://registry.npmjs.org/ternary-stream/-/ternary-stream-2.0.1.tgz", - "integrity": "sha1-Bk5Im0tb9gumpre8fy9cJ07Pgmk=", - "dev": true - }, - "testem": { - "version": "https://registry.npmjs.org/testem/-/testem-1.16.2.tgz", - "integrity": "sha1-lURtMQoQ6FLT69vAzis/11N4uik=", - "dev": true, - "dependencies": { - "commander": { - "version": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", - "dev": true - } - } - }, - "text-table": { - "version": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" - }, - "textarea-caret": { - "version": "https://registry.npmjs.org/textarea-caret/-/textarea-caret-3.0.2.tgz", - "integrity": "sha1-82DEhpmqGr9xhoCkOjGoUGZcLK8=" - }, - "textextensions": { - "version": "https://registry.npmjs.org/textextensions/-/textextensions-1.0.2.tgz", - "integrity": "sha1-ZUhjk+4fK7A5pgy7oFsLaL2VAdI=", - "dev": true - }, - "thenify": { - "version": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", - "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", - "dev": true, - "dependencies": { - "any-promise": { - "version": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", - "dev": true - } - } - }, - "thenify-all": { - "version": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", - "dev": true - }, - "three": { - "version": "https://registry.npmjs.org/three/-/three-0.73.0.tgz", - "integrity": "sha1-jyWKxG2BVsRa8kT+E6T4u3oEUSk=" - }, - "three.js": { - "version": "https://registry.npmjs.org/three.js/-/three.js-0.73.2.tgz", - "integrity": "sha1-3JARPxgT9AShjhYkqajjnGwZSpY=" - }, - "through": { - "version": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "through2": { - "version": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=" - }, - "through2-filter": { - "version": "https://registry.npmjs.org/through2-filter/-/through2-filter-2.0.0.tgz", - "integrity": "sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=", - "dev": true - }, - "tildify": { - "version": "https://registry.npmjs.org/tildify/-/tildify-1.2.0.tgz", - "integrity": "sha1-3OwD9V3Km3qj5bBPIYF+tW5jWIo=", - "dev": true - }, - "time-stamp": { - "version": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", - "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=" - }, - "timers-browserify": { - "version": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", - "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", - "dev": true, - "dependencies": { - "process": { - "version": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", - "dev": true - } - } - }, - "to-absolute-glob": { - "version": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-0.1.1.tgz", - "integrity": "sha1-HN+kcqnvUMI57maZm2YsoOs5k38=", - "dev": true - }, - "to-array": { - "version": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", - "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", - "dev": true - }, - "to-arraybuffer": { - "version": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", - "dev": true - }, - "to-fast-properties": { - "version": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" - }, - "to-iso-string": { - "version": "https://registry.npmjs.org/to-iso-string/-/to-iso-string-0.0.2.tgz", - "integrity": "sha1-TcGeZk38y+Jb2NtQiwDG2hWCVdE=", - "dev": true - }, - "to-utf8": { - "version": "https://registry.npmjs.org/to-utf8/-/to-utf8-0.0.1.tgz", - "integrity": "sha1-0Xrqcv8vujm55DYBvns/9y4ImFI=" - }, - "toggle-selection": { - "version": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.5.tgz", - "integrity": "sha1-cmxwPeYHGTpzwyx99JzSSVD8V08=" - }, - "tough-cookie": { - "version": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", - "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", - "dependencies": { - "punycode": { - "version": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - } - } - }, - "tr46": { - "version": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", - "dev": true - }, - "tracejs": { - "version": "https://registry.npmjs.org/tracejs/-/tracejs-0.1.8.tgz", - "integrity": "sha1-bCZ4exhT8TcWNGIsHIC8RAJsXXA=", - "dev": true - }, - "traverse": { - "version": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", - "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=" - }, - "trim": { - "version": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", - "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=" - }, - "trim-right": { - "version": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" - }, - "trumpet": { - "version": "https://registry.npmjs.org/trumpet/-/trumpet-1.7.2.tgz", - "integrity": "sha1-sCxp5GXRcfVeRJJL+bW90gl0yDA=", - "dev": true, - "dependencies": { - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "through2": { - "version": "https://registry.npmjs.org/through2/-/through2-1.1.1.tgz", - "integrity": "sha1-CEfLxESfNAVXTb3M2buEG4OsNUU=", - "dev": true - } - } - }, - "tryit": { - "version": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz", - "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=" - }, - "tty-browserify": { - "version": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", - "dev": true - }, - "tunnel-agent": { - "version": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", - "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=" - }, - "tweetnacl": { - "version": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, - "type-check": { - "version": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=" - }, - "type-detect": { - "version": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", - "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=", - "dev": true - }, - "type-is": { - "version": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", - "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=" - }, - "typedarray": { - "version": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "ua-parser-js": { - "version": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.12.tgz", - "integrity": "sha1-BMgamb3V3FImPqKdJMa/jUgYpLs=" - }, - "uglify-js": { - "version": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.3.6.tgz", - "integrity": "sha1-+gmEdwtCi3qbKoBY9GNV0U/vIRo=", - "dev": true, - "dependencies": { - "async": { - "version": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", - "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", - "dev": true - }, - "optimist": { - "version": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", - "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=", - "dev": true - }, - "source-map": { - "version": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", - "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", - "dev": true - } - } - }, - "uglifyify": { - "version": "https://registry.npmjs.org/uglifyify/-/uglifyify-3.0.4.tgz", - "integrity": "sha1-SH4IClp3mIgOaOkN75sGaB+xO9I=", - "dev": true, - "dependencies": { - "convert-source-map": { - "version": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", - "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", - "dev": true - }, - "extend": { - "version": "https://registry.npmjs.org/extend/-/extend-1.3.0.tgz", - "integrity": "sha1-0VFvsP9WJNLr+RI+odrFoZlABPg=", - "dev": true - } - } - }, - "ultron": { - "version": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", - "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=", - "dev": true - }, - "umd": { - "version": "https://registry.npmjs.org/umd/-/umd-3.0.1.tgz", - "integrity": "sha1-iuVW4RAR9jwllnCKiDclnwGz1g4=", - "dev": true - }, - "unc-path-regex": { - "version": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", - "dev": true - }, - "underscore": { - "version": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", - "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=", - "dev": true - }, - "undertaker": { - "version": "https://registry.npmjs.org/undertaker/-/undertaker-1.1.0.tgz", - "integrity": "sha1-C6AOb7aor+HpKGMVZar226YRGus=", - "dev": true, - "dependencies": { - "array-each": { - "version": "https://registry.npmjs.org/array-each/-/array-each-0.1.1.tgz", - "integrity": "sha1-xdUrqCJfNtcoF4unrsQTrPrd0Pk=", - "dev": true - }, - "array-slice": { - "version": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", - "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", - "dev": true - }, - "isobject": { - "version": "https://registry.npmjs.org/isobject/-/isobject-1.0.2.tgz", - "integrity": "sha1-8Pm4zpLdVA+gdAiC44NaLgIux4o=", - "dev": true - }, - "object.defaults": { - "version": "https://registry.npmjs.org/object.defaults/-/object.defaults-0.3.0.tgz", - "integrity": "sha1-seucvHjEx71WysbK496tWnETiCo=", - "dev": true - } - } - }, - "undertaker-registry": { - "version": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.0.tgz", - "integrity": "sha1-LacWx2WZnYyUufntLABt9JI7BSs=", - "dev": true - }, - "uniq": { - "version": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", - "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=" - }, - "unique-stream": { - "version": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.2.1.tgz", - "integrity": "sha1-WqADz76Uxf+GbE59ZouxxNuts2k=", - "dev": true - }, - "unorm": { - "version": "https://registry.npmjs.org/unorm/-/unorm-1.4.1.tgz", - "integrity": "sha1-NkIA1fE2RsqLzURJAnEzVhR5IwA=" - }, - "unpipe": { - "version": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "unzip-response": { - "version": "https://registry.npmjs.org/unzip-response/-/unzip-response-1.0.2.tgz", - "integrity": "sha1-uYTwh3/AqJwsdzzB73tbIytbBv4=" - }, - "urix": { - "version": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "url": { - "version": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "dev": true, - "dependencies": { - "punycode": { - "version": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true - } - } - }, - "user-home": { - "version": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", - "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=" - }, - "utf8": { - "version": "https://registry.npmjs.org/utf8/-/utf8-2.1.2.tgz", - "integrity": "sha1-H6DZJw6b6FDZsFAn9jUZv0ZFfZY=" - }, - "util": { - "version": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "dev": true, - "dependencies": { - "inherits": { - "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", - "dev": true - } - } - }, - "util-deprecate": { - "version": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "utile": { - "version": "https://registry.npmjs.org/utile/-/utile-0.3.0.tgz", - "integrity": "sha1-E1LDQOuCDk2N26A5pPv6oy7U7zo=", - "dev": true, - "dependencies": { - "async": { - "version": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", - "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", - "dev": true - }, - "deep-equal": { - "version": "https://registry.npmjs.org/deep-equal/-/deep-equal-0.2.2.tgz", - "integrity": "sha1-hLdFiW80xoTpjyzg5Cq69Du6AX0=", - "dev": true - } - } - }, - "utils-merge": { - "version": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", - "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=" - }, - "uuid": { - "version": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", - "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=" - }, - "v8flags": { - "version": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz", - "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=", - "dev": true, - "dependencies": { - "user-home": { - "version": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", - "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=", - "dev": true - } - } - }, - "vali-date": { - "version": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz", - "integrity": "sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=", - "dev": true - }, - "valid-url": { - "version": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", - "integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=" - }, - "validate-npm-package-license": { - "version": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", - "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=" - }, - "varint": { - "version": "https://registry.npmjs.org/varint/-/varint-4.0.1.tgz", - "integrity": "sha1-SQgpuULSSEY7KzUJeZXDv3NxmOk=" - }, - "vary": { - "version": "https://registry.npmjs.org/vary/-/vary-1.1.1.tgz", - "integrity": "sha1-Z1Neu2lMHVIldFeYRmUyP1h+jTc=" - }, - "verror": { - "version": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", - "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=" - }, - "vinyl": { - "version": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", - "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=" - }, - "vinyl-buffer": { - "version": "https://registry.npmjs.org/vinyl-buffer/-/vinyl-buffer-1.0.0.tgz", - "integrity": "sha1-ygZ+oIQx1QdyKx3lCD9gJhbrwjQ=", - "dev": true, - "dependencies": { - "bl": { - "version": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz", - "integrity": "sha1-wGt5evCF6gC8Unr8jvzxHeIjIFQ=", - "dev": true - }, - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "through2": { - "version": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", - "dev": true - } - } - }, - "vinyl-file": { - "version": "https://registry.npmjs.org/vinyl-file/-/vinyl-file-2.0.0.tgz", - "integrity": "sha1-p+v1/779obfRjRQPyweyI++2dRo=", - "dev": true, - "dependencies": { - "first-chunk-stream": { - "version": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz", - "integrity": "sha1-G97NuOCDwGZLkZRVgVd6Q6nzHXA=", - "dev": true - }, - "strip-bom-stream": { - "version": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-2.0.0.tgz", - "integrity": "sha1-+H217yYT9paKpUWr/h7HKLaoKco=", - "dev": true - }, - "vinyl": { - "version": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", - "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", - "dev": true - } - } - }, - "vinyl-fs": { - "version": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-2.4.4.tgz", - "integrity": "sha1-vm/zJwy1Xf19MGNkDegfJddTIjk=", - "dev": true, - "dependencies": { - "gulp-sourcemaps": { - "version": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-1.6.0.tgz", - "integrity": "sha1-uG/zSdgBzrVuHZ59x7vLS33uYAw=", - "dev": true - }, - "vinyl": { - "version": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", - "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", - "dev": true - } - } - }, - "vinyl-source-stream": { - "version": "https://registry.npmjs.org/vinyl-source-stream/-/vinyl-source-stream-1.1.0.tgz", - "integrity": "sha1-RMvlEIIFJ53rDFZTwJSiiHk4sas=", - "dev": true, - "dependencies": { - "clone": { - "version": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", - "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", - "dev": true - }, - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "through2": { - "version": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", - "dev": true - }, - "vinyl": { - "version": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", - "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", - "dev": true - } - } - }, - "vm-browserify": { - "version": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", - "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", - "dev": true - }, - "vreme": { - "version": "https://registry.npmjs.org/vreme/-/vreme-3.0.2.tgz", - "integrity": "sha1-RyE3a0SUV/796KhJ0zQJM7kLVoY=" - }, - "watchify": { - "version": "https://registry.npmjs.org/watchify/-/watchify-3.9.0.tgz", - "integrity": "sha1-8HX9LoqGrN6Eztum5cKgvt1SPZ4=", - "dev": true, - "dependencies": { - "base64-js": { - "version": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.0.tgz", - "integrity": "sha1-o5mS1yNYSBGYK+XikLtqU9hnAPE=", - "dev": true - }, - "browserify": { - "version": "https://registry.npmjs.org/browserify/-/browserify-14.4.0.tgz", - "integrity": "sha1-CJo0Y69Y0OSNjNQHCz90ZU1avKk=", - "dev": true - }, - "buffer": { - "version": "https://registry.npmjs.org/buffer/-/buffer-5.0.6.tgz", - "integrity": "sha1-LqZp9+7Atu2gWwj4tf9mGyhXNYg=", - "dev": true - }, - "concat-stream": { - "version": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", - "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", - "dev": true, - "dependencies": { - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", - "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", - "dev": true - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } - } - }, - "duplexer2": { - "version": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true - }, - "https-browserify": { - "version": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", - "dev": true - }, - "process": { - "version": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", - "dev": true - }, - "punycode": { - "version": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - } - } - }, - "weak": { - "version": "https://registry.npmjs.org/weak/-/weak-1.0.1.tgz", - "integrity": "sha1-q5mqswcGlZqgIAy4z1RbucszuZ4=", - "optional": true - }, - "web3": { - "version": "https://registry.npmjs.org/web3/-/web3-0.18.2.tgz", - "integrity": "sha1-YbGm7fUFaCDiLh7wgvVMJ59L91g=" - }, - "web3-provider-engine": { - "version": "https://registry.npmjs.org/web3-provider-engine/-/web3-provider-engine-12.2.1.tgz", - "integrity": "sha1-hIwu4Yf5cBsKOC4iB8mxDxdKjXI=", - "dependencies": { - "async": { - "version": "https://registry.npmjs.org/async/-/async-2.4.1.tgz", - "integrity": "sha1-YqVrJ5yYoR0JhwlqAcw+6463u9c=" - }, - "clone": { - "version": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", - "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=" - }, - "ethereumjs-util": { - "version": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.1.1.tgz", - "integrity": "sha1-Ei+zjep0fcYrOuv8Nl0b1Ivktz4=" - } - } - }, - "web3-stream-provider": { - "version": "https://registry.npmjs.org/web3-stream-provider/-/web3-stream-provider-2.0.8.tgz", - "integrity": "sha1-AgNxn9XtoWwsr1hQ+krtVRjt7qo=" - }, - "webidl-conversions": { - "version": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", - "dev": true - }, - "websocket-driver": { - "version": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz", - "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=", - "dev": true - }, - "websocket-extensions": { - "version": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.1.tgz", - "integrity": "sha1-domUmcGEtu91Q3fC27DNbLVdKec=", - "dev": true - }, - "whatwg-fetch": { - "version": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", - "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=" - }, - "whatwg-url": { - "version": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-2.0.1.tgz", - "integrity": "sha1-U5ayBD8CDub3BNnEXqhRnnJN5lk=", - "dev": true - }, - "which": { - "version": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", - "integrity": "sha1-RgwdoPgQED0DIam2M6+eV15kSG8=", - "dev": true - }, - "which-module": { - "version": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=" - }, - "wide-align": { - "version": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", - "integrity": "sha1-Vx4PGwYEY268DfwhsDObvjE0FxA=" - }, - "window-size": { - "version": "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz", - "integrity": "sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU=" - }, - "winston": { - "version": "https://registry.npmjs.org/winston/-/winston-2.1.1.tgz", - "integrity": "sha1-PJNJ0ZYgf9G9/51LxD73JRDjoS4=", - "dev": true, - "dependencies": { - "async": { - "version": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", - "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=", - "dev": true - }, - "colors": { - "version": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", - "dev": true - }, - "pkginfo": { - "version": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.3.1.tgz", - "integrity": "sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE=", - "dev": true - } - } - }, - "wordwrap": { - "version": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" - }, - "wrap-ansi": { - "version": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=" - }, - "wrappy": { - "version": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "wreck": { - "version": "https://registry.npmjs.org/wreck/-/wreck-6.3.0.tgz", - "integrity": "sha1-oTaXafB7u2LWo3gzanhx/Hc8dAs=", - "dev": true - }, - "write": { - "version": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=" - }, - "ws": { - "version": "https://registry.npmjs.org/ws/-/ws-1.1.1.tgz", - "integrity": "sha1-CC3bbGQehdS7RR8D1S8G6r2x8Bg=", - "dev": true - }, - "wtf-8": { - "version": "https://registry.npmjs.org/wtf-8/-/wtf-8-1.0.0.tgz", - "integrity": "sha1-OS2LotDxw00e4tYw8V0O+2jhBIo=", - "dev": true - }, - "xhr": { - "version": "https://registry.npmjs.org/xhr/-/xhr-2.4.0.tgz", - "integrity": "sha1-4W5mpF+GmGHu76tBbV7/ci3ECZM=" - }, - "xhr2": { - "version": "https://registry.npmjs.org/xhr2/-/xhr2-0.1.4.tgz", - "integrity": "sha1-f4dliEdxbbUCYyOBL4GMras4el8=" - }, - "xml-name-validator": { - "version": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz", - "integrity": "sha1-TYuPHszTQZqjYgYb7O9RXh5VljU=", - "dev": true - }, - "xmldom": { - "version": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", - "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk=", - "dev": true - }, - "xmlhttprequest": { - "version": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", - "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=" - }, - "xmlhttprequest-ssl": { - "version": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz", - "integrity": "sha1-GFqIjATspGw+QHDZn3tJ3jUomS0=", - "dev": true - }, - "xss-filters": { - "version": "https://registry.npmjs.org/xss-filters/-/xss-filters-1.2.7.tgz", - "integrity": "sha1-Wfod4gHzby80cNysX1jMwoMLCpo=" - }, - "xtend": { - "version": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" - }, - "y18n": { - "version": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" - }, - "yallist": { - "version": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - }, - "yargs": { - "version": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", - "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=" - }, - "yargs-parser": { - "version": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", - "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=" - }, - "yazl": { - "version": "https://registry.npmjs.org/yazl/-/yazl-2.4.2.tgz", - "integrity": "sha1-FMsZCD4eJacAksFYiqvg9OTdTYg=", - "dev": true - }, - "yeast": { - "version": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", - "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", - "dev": true - } - } -} -- cgit v1.2.3 From c0a023ffdb6e04646e45b2a3c96241f5fb006326 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 12 Jun 2017 13:46:56 -0700 Subject: default testnet - fix typo --- app/scripts/first-time-state.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/scripts/first-time-state.js b/app/scripts/first-time-state.js index 5d89a0a21..5e8577100 100644 --- a/app/scripts/first-time-state.js +++ b/app/scripts/first-time-state.js @@ -10,7 +10,6 @@ module.exports = { NetworkController: { provider: { type: (METAMASK_DEBUG || env === 'test') ? 'rinkeby' : 'mainnet', - type: 'rinkeby', }, }, } -- cgit v1.2.3 From 5668910244a44874de93547d4037ee060ed5a264 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Mon, 12 Jun 2017 14:05:08 -0700 Subject: prefix the address with "ethereum:" --- ui/app/components/qr-code.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/app/components/qr-code.js b/ui/app/components/qr-code.js index 5488599eb..ac40e9ced 100644 --- a/ui/app/components/qr-code.js +++ b/ui/app/components/qr-code.js @@ -25,8 +25,7 @@ QrCodeView.prototype.render = function () { var props = this.props var Qr = props.Qr var qrImage = qrCode(4, 'M') - - qrImage.addData(Qr.data) + qrImage.addData(`ethereum:${Qr.data}`) qrImage.make() return h('.main-container.flex-column', { -- cgit v1.2.3 From 9fd9c34574aae66652c08d20e97dfe7516276f75 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 12 Jun 2017 14:09:38 -0700 Subject: deps - prov-eng 12.2.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 341af49a7..2c23d9e10 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "valid-url": "^1.0.9", "vreme": "^3.0.2", "web3": "0.18.2", - "web3-provider-engine": "^12.2.3", + "web3-provider-engine": "^12.2.4", "web3-stream-provider": "^2.0.6", "xtend": "^4.0.1" }, -- cgit v1.2.3 From d1fa3c6de16e5335ea9c21938029312df95c3d51 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Mon, 12 Jun 2017 14:37:32 -0700 Subject: only prefix ethereum address --- ui/app/components/qr-code.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ui/app/components/qr-code.js b/ui/app/components/qr-code.js index ac40e9ced..06b9aed9b 100644 --- a/ui/app/components/qr-code.js +++ b/ui/app/components/qr-code.js @@ -3,6 +3,7 @@ const h = require('react-hyperscript') const qrCode = require('qrcode-npm').qrcode const inherits = require('util').inherits const connect = require('react-redux').connect +const isHexPrefixed = require('ethereumjs-util').isHexPrefixed const CopyButton = require('./copyButton') module.exports = connect(mapStateToProps)(QrCodeView) @@ -22,12 +23,12 @@ function QrCodeView () { } QrCodeView.prototype.render = function () { - var props = this.props - var Qr = props.Qr - var qrImage = qrCode(4, 'M') - qrImage.addData(`ethereum:${Qr.data}`) + const props = this.props + const Qr = props.Qr + const address = `${isHexPrefixed(Qr.data) ? 'ethereum:' : ''}${Qr.data}` + const qrImage = qrCode(4, 'M') + qrImage.addData(address) qrImage.make() - return h('.main-container.flex-column', { key: 'qr', style: { -- cgit v1.2.3 From b712e7ce6498b2d8d26f506f40a1e00ef1b7462c Mon Sep 17 00:00:00 2001 From: frankiebee Date: Mon, 12 Jun 2017 14:48:55 -0700 Subject: add to CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a0d0a579..834c0b134 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Current Master +- Add a `ethereum:` prefix to the QR code address - Fix currency API URL from cryptonator. - Update gasLimit params with every new block seen. -- cgit v1.2.3 From 844159cb18c7dbf6b7a705a8e988477b0b67850a Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 12 Jun 2017 15:57:01 -0700 Subject: Version 3.7.8 --- CHANGELOG.md | 2 ++ app/manifest.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3823109d..167a34828 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +## 3.7.8 2017-6-12 + - Add a `ethereum:` prefix to the QR code address - The default network on installation is now MainNet - Fix currency API URL from cryptonator. diff --git a/app/manifest.json b/app/manifest.json index a610d9e75..7ae20158c 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.7.7", + "version": "3.7.8", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From 96fa29ffbce841977ae02acaba2d8114436262d4 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Mon, 12 Jun 2017 16:11:37 -0700 Subject: put tx resubmission on the block event --- app/scripts/controllers/transactions.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index faccf1ab1..bf24523cc 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -9,7 +9,6 @@ const createId = require('../lib/random-id') const denodeify = require('denodeify') const RETRY_LIMIT = 200 -const RESUBMIT_INTERVAL = 10000 // Ten seconds module.exports = class TransactionController extends EventEmitter { constructor (opts) { @@ -26,6 +25,7 @@ module.exports = class TransactionController extends EventEmitter { this.query = opts.ethQuery this.txProviderUtils = new TxProviderUtil(this.query) this.blockTracker.on('block', this.checkForTxInBlock.bind(this)) + this.blockTracker.on('block', this.continuallyResubmitPendingTxs.bind(this)) this.signEthTx = opts.signTransaction this.nonceLock = Semaphore(1) @@ -34,8 +34,6 @@ module.exports = class TransactionController extends EventEmitter { this.store.subscribe(() => this._updateMemstore()) this.networkStore.subscribe(() => this._updateMemstore()) this.preferencesStore.subscribe(() => this._updateMemstore()) - - this.continuallyResubmitPendingTxs() } getState () { @@ -411,16 +409,13 @@ module.exports = class TransactionController extends EventEmitter { continuallyResubmitPendingTxs () { const pending = this.getTxsByMetaData('status', 'submitted') + // only try resubmitting if their are transactions to resubmit + if (!pending.length) return const resubmit = denodeify(this.resubmitTx.bind(this)) Promise.all(pending.map(txMeta => resubmit(txMeta))) .catch((reason) => { log.info('Problem resubmitting tx', reason) }) - .then(() => { - global.setTimeout(() => { - this.continuallyResubmitPendingTxs() - }, RESUBMIT_INTERVAL) - }) } resubmitTx (txMeta, cb) { -- cgit v1.2.3 From 5e03b69892375b24bf6b7a59bb3379aac2c126da Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Mon, 12 Jun 2017 16:56:37 -0700 Subject: Fix check icon appearing in inappropriate situations. --- ui/app/components/ens-input.js | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js index 43bb7ab22..52ad8943b 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/ens-input.js @@ -21,7 +21,6 @@ EnsInput.prototype.render = function () { const opts = extend(props, { list: 'addresses', onChange: () => { - this.setState({ ensResolution: '0x0000000000000000000000000000000000000000' }) const network = this.props.network const networkHasEnsSupport = getNetworkEnsSupport(network) if (!networkHasEnsSupport) return @@ -73,6 +72,7 @@ EnsInput.prototype.render = function () { EnsInput.prototype.componentDidMount = function () { const network = this.props.network const networkHasEnsSupport = getNetworkEnsSupport(network) + this.setState({ ensResolution: '0x0000000000000000000000000000000000000000' }) if (networkHasEnsSupport) { const provider = global.ethereumProvider @@ -85,14 +85,6 @@ EnsInput.prototype.lookupEnsName = function () { const recipient = document.querySelector('input[name="address"]').value const { ensResolution } = this.state - if (!this.ens) { - return this.setState({ - loadingEns: false, - ensFailure: true, - hoverText: 'ENS is not supported on your current network.', - }) - } - log.info(`ENS attempting to resolve name: ${recipient}`) this.ens.lookup(recipient.trim()) .then((address) => { @@ -124,7 +116,7 @@ EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) { // If an address is sent without a nickname, meaning not from ENS or from // the user's own accounts, a default of a one-space string is used. const nickname = state.nickname || ' ' - if (ensResolution && this.props.onChange && + if (prevState && ensResolution && this.props.onChange && ensResolution !== prevState.ensResolution) { this.props.onChange(ensResolution, nickname) } @@ -143,7 +135,7 @@ EnsInput.prototype.ensIcon = function (recipient) { } EnsInput.prototype.ensIconContents = function (recipient) { - const { loadingEns, ensFailure, ensResolution } = this.state || {} + const { loadingEns, ensFailure, ensResolution } = this.state || { ensResolution: '0x0000000000000000000000000000000000000000'} if (loadingEns) { return h('img', { @@ -160,7 +152,7 @@ EnsInput.prototype.ensIconContents = function (recipient) { return h('i.fa.fa-warning.fa-lg.warning') } - if (ensResolution) { + if (ensResolution && (ensResolution !== '0x0000000000000000000000000000000000000000')) { return h('i.fa.fa-check-circle.fa-lg.cursor-pointer', { style: { color: 'green' }, onClick: (event) => { @@ -175,4 +167,3 @@ EnsInput.prototype.ensIconContents = function (recipient) { function getNetworkEnsSupport (network) { return Boolean(networkMap[network]) } - -- cgit v1.2.3 From e9e43637df0132804a4a1fe031d1734029c19bf5 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Mon, 12 Jun 2017 16:57:31 -0700 Subject: Bump changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38aebcbd4..43aaa76d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - The default network on installation is now MainNet - Fix currency API URL from cryptonator. - Update gasLimit params with every new block seen. +- Fix ENS resolver symbol UI. ## 3.7.7 2017-6-8 -- cgit v1.2.3 From dbc48c3992b9592ae6984c0dc402f06ad60d7814 Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Mon, 12 Jun 2017 17:42:54 -0700 Subject: Make test Async --- test/unit/tx-controller-test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index 3954300a8..f05ae479b 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -311,12 +311,13 @@ describe('Transaction Controller', function () { }) describe('#sign replay-protected tx', function () { - it('prepares a tx with the chainId set', function () { + it('prepares a tx with the chainId set', function (done) { txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) txController.signTransaction('1', (err, rawTx) => { if (err) return assert.fail('it should not fail') const ethTx = new EthTx(ethUtil.toBuffer(rawTx)) assert.equal(ethTx.getChainId(), currentNetworkId) + done() }) }) }) -- cgit v1.2.3 From ae7c296669ad5c81e85071de071e2b0f2b1d4463 Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Mon, 12 Jun 2017 17:44:11 -0700 Subject: Fix networkState in chain id --- app/scripts/controllers/transactions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index faccf1ab1..d4603aafa 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -212,7 +212,7 @@ module.exports = class TransactionController extends EventEmitter { getChainId () { const networkState = this.networkStore.getState() - const getChainId = parseInt(networkState.network) + const getChainId = parseInt(networkState) if (Number.isNaN(getChainId)) { return 0 } else { -- cgit v1.2.3 From c0f07844af434666ae1e511e90e36ca493e8bb91 Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Mon, 12 Jun 2017 17:52:37 -0700 Subject: Finish async when failing async test --- test/unit/tx-controller-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index f05ae479b..f0d8a706e 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -314,7 +314,7 @@ describe('Transaction Controller', function () { it('prepares a tx with the chainId set', function (done) { txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) txController.signTransaction('1', (err, rawTx) => { - if (err) return assert.fail('it should not fail') + if (err) return done('it should not fail') const ethTx = new EthTx(ethUtil.toBuffer(rawTx)) assert.equal(ethTx.getChainId(), currentNetworkId) done() -- cgit v1.2.3 From ec3383c16275f8d4594323b7b4ec38b447844e68 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 13 Jun 2017 09:50:01 -0700 Subject: rename continuallyResubmitPendingTxs to resubmitPendingTxs --- app/scripts/controllers/transactions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index bf24523cc..9f621747f 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -25,7 +25,7 @@ module.exports = class TransactionController extends EventEmitter { this.query = opts.ethQuery this.txProviderUtils = new TxProviderUtil(this.query) this.blockTracker.on('block', this.checkForTxInBlock.bind(this)) - this.blockTracker.on('block', this.continuallyResubmitPendingTxs.bind(this)) + this.blockTracker.on('block', this.resubmitPendingTxs.bind(this)) this.signEthTx = opts.signTransaction this.nonceLock = Semaphore(1) @@ -407,7 +407,7 @@ module.exports = class TransactionController extends EventEmitter { this.memStore.updateState({ unapprovedTxs, selectedAddressTxList }) } - continuallyResubmitPendingTxs () { + resubmitPendingTxs () { const pending = this.getTxsByMetaData('status', 'submitted') // only try resubmitting if their are transactions to resubmit if (!pending.length) return -- cgit v1.2.3 From 790712e6fd4b76017e11de08ccfa474c9a7e4a6f Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Tue, 13 Jun 2017 13:38:27 -0700 Subject: Cleanup zero addresses. --- ui/app/components/ens-input.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js index 52ad8943b..16c50db84 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/ens-input.js @@ -7,6 +7,7 @@ const copyToClipboard = require('copy-to-clipboard') const ENS = require('ethjs-ens') const networkMap = require('ethjs-ens/lib/network-map.json') const ensRE = /.+\.eth$/ +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' module.exports = EnsInput @@ -72,7 +73,7 @@ EnsInput.prototype.render = function () { EnsInput.prototype.componentDidMount = function () { const network = this.props.network const networkHasEnsSupport = getNetworkEnsSupport(network) - this.setState({ ensResolution: '0x0000000000000000000000000000000000000000' }) + this.setState({ ensResolution: ZERO_ADDRESS }) if (networkHasEnsSupport) { const provider = global.ethereumProvider @@ -88,7 +89,7 @@ EnsInput.prototype.lookupEnsName = function () { log.info(`ENS attempting to resolve name: ${recipient}`) this.ens.lookup(recipient.trim()) .then((address) => { - if (address === '0x0000000000000000000000000000000000000000') throw new Error('No address has been set for this name.') + if (address === ZERO_ADDRESS) throw new Error('No address has been set for this name.') if (address !== ensResolution) { this.setState({ loadingEns: false, @@ -103,7 +104,7 @@ EnsInput.prototype.lookupEnsName = function () { log.error(reason) return this.setState({ loadingEns: false, - ensResolution: '0x0000000000000000000000000000000000000000', + ensResolution: ZERO_ADDRESS, ensFailure: true, hoverText: reason.message, }) @@ -135,7 +136,7 @@ EnsInput.prototype.ensIcon = function (recipient) { } EnsInput.prototype.ensIconContents = function (recipient) { - const { loadingEns, ensFailure, ensResolution } = this.state || { ensResolution: '0x0000000000000000000000000000000000000000'} + const { loadingEns, ensFailure, ensResolution } = this.state || { ensResolution: ZERO_ADDRESS} if (loadingEns) { return h('img', { @@ -152,7 +153,7 @@ EnsInput.prototype.ensIconContents = function (recipient) { return h('i.fa.fa-warning.fa-lg.warning') } - if (ensResolution && (ensResolution !== '0x0000000000000000000000000000000000000000')) { + if (ensResolution && (ensResolution !== ZERO_ADDRESS)) { return h('i.fa.fa-check-circle.fa-lg.cursor-pointer', { style: { color: 'green' }, onClick: (event) => { -- cgit v1.2.3 From de500250c463f51f68abff44c8ed6c20912b48c0 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 13 Jun 2017 17:46:47 -0700 Subject: Fix build for eth-contract-metadata --- gulpfile.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 5bba1b9b3..3f235396c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -53,7 +53,7 @@ gulp.task('copy:images', copyTask({ ], })) gulp.task('copy:contractImages', copyTask({ - source: './node_modules/ethereum-contract-icons/images/', + source: './node_modules/eth-contract-metadata/images/', destinations: [ './dist/firefox/images/contract', './dist/chrome/images/contract', diff --git a/package.json b/package.json index 07f2c488c..cb4c05aaf 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "end-of-stream": "^1.1.0", "ensnare": "^1.0.0", "eth-bin-to-ops": "^1.0.1", - "eth-contract-metadata": "^1.0.0", + "eth-contract-metadata": "^1.1.0", "eth-hd-keyring": "^1.1.1", "eth-query": "^2.1.1", "eth-sig-util": "^1.1.1", -- cgit v1.2.3 From 108c4ab2c58074aa8148828fbbef8cbf3a4e23f5 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 13 Jun 2017 17:47:56 -0700 Subject: Auto populate token list with popular token balances Half implements #175 Things to do: - Add ability to add tokens to the list. - Persist the token tab selection (so it is an implicit preference) - Check what's up with the token-tracker polling, it seems like it is not waiting the interval. --- ui/app/account-detail.js | 17 +++++--------- ui/app/components/identicon.js | 6 +++-- ui/app/components/tab-bar.js | 1 + ui/app/components/token-cell.js | 22 +++++++++++++++--- ui/app/components/token-list.js | 49 +++++++++++++++++++++++++++++------------ ui/app/info.js | 1 + ui/lib/icon-factory.js | 4 ++-- 7 files changed, 68 insertions(+), 32 deletions(-) diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index 5b2588ec5..8234a8438 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -39,7 +39,7 @@ function mapStateToProps (state) { inherits(AccountDetailScreen, Component) function AccountDetailScreen () { - this.state = {} + this.state = { tabSelection: 'history' } Component.call(this) } @@ -251,13 +251,7 @@ AccountDetailScreen.prototype.subview = function () { } AccountDetailScreen.prototype.tabSections = function () { - var subview - try { - subview = this.props.accountDetail.subview - } catch (e) { - subview = null - } - + const tabSelection = this.state.tabSelection return h('section.tabSection', [ h(TabBar, { @@ -265,7 +259,7 @@ AccountDetailScreen.prototype.tabSections = function () { { content: 'History', key: 'history' }, { content: 'Tokens', key: 'tokens' }, ], - defaultTab: subview || 'history', + defaultTab: tabSelection || 'history', tabSelected: (key) => { this.setState({ tabSelection: key }) }, @@ -276,12 +270,13 @@ AccountDetailScreen.prototype.tabSections = function () { } AccountDetailScreen.prototype.tabSwitchView = function () { - const userAddress = this.props.address + const props = this.props + const { address, network } = props const tabSelection = this.state.tabSelection || 'history' switch (tabSelection) { case 'tokens': - return h(TokenList, { userAddress }) + return h(TokenList, { userAddress: address, network }) default: return this.transactionList() } diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js index 58bd2bdc4..c754bc6ba 100644 --- a/ui/app/components/identicon.js +++ b/ui/app/components/identicon.js @@ -23,7 +23,9 @@ IdenticonComponent.prototype.render = function () { h('div', { key: 'identicon-' + this.props.address, style: { - display: 'inline-block', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', height: diameter, width: diameter, borderRadius: diameter / 2, @@ -40,8 +42,8 @@ IdenticonComponent.prototype.componentDidMount = function () { if (!address) return var container = findDOMNode(this) - var diameter = props.diameter || this.defaultDiameter + var diameter = props.diameter || this.defaultDiameter if (!isNode) { var img = iconFactory.iconForAddress(address, diameter) container.appendChild(img) diff --git a/ui/app/components/tab-bar.js b/ui/app/components/tab-bar.js index 65078e0a4..6295e7dd9 100644 --- a/ui/app/components/tab-bar.js +++ b/ui/app/components/tab-bar.js @@ -33,3 +33,4 @@ TabBar.prototype.render = function () { })) ) } + diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js index 879dc01d1..ad7f55345 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/token-cell.js @@ -12,11 +12,19 @@ function TokenCell () { TokenCell.prototype.render = function () { const props = this.props - const { address, symbol, string, network } = props - log.info({ address, symbol, string }) + const { address, symbol, string, network, userAddress } = props + log.info({ address, symbol, string, network }) return ( - h('li.token-cell', [ + h('li.token-cell', { + style: { cursor: network === '1' ? 'pointer' : 'default' }, + onClick: (event) => { + const url = urlFor(address, userAddress, network) + if (url) { + navigateTo(url) + } + }, + }, [ h(Identicon, { diameter: 50, @@ -29,3 +37,11 @@ TokenCell.prototype.render = function () { ) } +function navigateTo (url) { + global.platform.openWindow({ url }) +} + +function urlFor (tokenAddress, address, network) { + return `https://etherscan.io/token/${tokenAddress}?a=${address}` +} + diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index 6589dea62..b79fbccf3 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -3,35 +3,49 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const TokenTracker = require('eth-token-tracker') const TokenCell = require('./token-cell.js') +const contracts = require('eth-contract-metadata') +const Loading = require('./loading') + +const tokens = [] +for (let address in contracts) { + const contract = contracts[address] + if (contract.erc20) { + contract.address = address + tokens.push(contract) + } +} module.exports = TokenList inherits(TokenList, Component) function TokenList () { - - // Hard coded for development for now: - const tokens = [ - { address: '0x48c80F1f4D53D5951e5D5438B54Cba84f29F32a5', symbol: 'REP', balance: 'aa'}, - { address: '0xc66ea802717bfb9833400264dd12c2bceaa34a6d', symbol: 'MKR', balance: '1000', decimals: 18}, - { address: '0xa74476443119A942dE498590Fe1f2454d7D4aC0d', symbol: 'GOL', balance: 'ff'}, - { address: '0xaec2e87e0a235266d9c5adc9deb4b2e29b54d009', symbol: 'SNGLS', balance: '0' }, - ] - - this.state = { tokens } + this.state = { tokens, isLoading: true } Component.call(this) } TokenList.prototype.render = function () { - const tokens = this.state.tokens + const state = this.state + const { tokens, isLoading } = state + + const { userAddress } = this.props + + if (isLoading) return h(Loading, { isLoading }) + const network = this.props.network const tokenViews = tokens.map((tokenData) => { tokenData.network = network + tokenData.userAddress = userAddress return h(TokenCell, tokenData) }) return ( - h('ol', [h('style', ` + h('ol', { + style: { + height: '302px', + overflowY: 'auto', + }, + }, [h('style', ` li.token-cell { display: flex; @@ -54,19 +68,26 @@ TokenList.prototype.render = function () { } TokenList.prototype.componentDidMount = function () { + if (!global.ethereumProvider) return const { userAddress } = this.props this.tracker = new TokenTracker({ userAddress, - provider: web3.currentProvider, + provider: global.ethereumProvider, tokens: this.state.tokens, + pollingInterval: 8000, }) this.setState({ tokens: this.tracker.serialize() }) - this.tracker.on('update', (tokenData) => this.setState({ tokens: tokenData })) + this.tracker.on('update', (tokenData) => { + const heldTokens = tokenData.filter(token => token.balance !== '0' && token.string !== '0.000') + this.setState({ tokens: heldTokens, isLoading: false }) + }) this.tracker.updateBalances() } TokenList.prototype.componentWillUnmount = function () { + if (!this.tracker) return this.tracker.stop() } + diff --git a/ui/app/info.js b/ui/app/info.js index aa4503b62..825796ed6 100644 --- a/ui/app/info.js +++ b/ui/app/info.js @@ -155,3 +155,4 @@ InfoScreen.prototype.render = function () { InfoScreen.prototype.navigateTo = function (url) { global.platform.openWindow({ url }) } + diff --git a/ui/lib/icon-factory.js b/ui/lib/icon-factory.js index 4ee6b600b..27a74de66 100644 --- a/ui/lib/icon-factory.js +++ b/ui/lib/icon-factory.js @@ -44,7 +44,7 @@ IconFactory.prototype.generateNewIdenticon = function (address, diameter) { // util function iconExistsFor (address) { - return (contractMap.address) && isValidAddress(address) && (contractMap[address].logo) + return contractMap[address] && isValidAddress(address) && contractMap[address].logo } function imageElFor (address) { @@ -53,7 +53,7 @@ function imageElFor (address) { const path = `images/contract/${fileName}` const img = document.createElement('img') img.src = path - img.style.width = '100%' + img.style.width = '75%' return img } -- cgit v1.2.3 From 5c9a228848144288488e218df4708b940c6e0487 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 13 Jun 2017 17:57:18 -0700 Subject: Add token list note to changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 167a34828..f30f551ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Add list of popular tokens held to the account detail view. + ## 3.7.8 2017-6-12 - Add a `ethereum:` prefix to the QR code address -- cgit v1.2.3 From 42f8f32a52b1ed621179a253a7a3da7641b55e04 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 13 Jun 2017 17:58:51 -0700 Subject: Remove bad state file --- development/states/token-list.json | 93 -------------------------------------- 1 file changed, 93 deletions(-) delete mode 100644 development/states/token-list.json diff --git a/development/states/token-list.json b/development/states/token-list.json deleted file mode 100644 index 404f1aedd..000000000 --- a/development/states/token-list.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "metamask": { - "isInitialized": true, - "isUnlocked": true, - "rpcTarget": "https://rawtestrpc.metamask.io/", - "identities": { - "0x55e2780588aa5000f464f700d2676fd0a22ee160": { - "address": "0x55e2780588aa5000f464f700d2676fd0a22ee160", - "name": "Account 1" - }, - "0x1c3d6d41dcb245c11a449ec46c9cf9eb7dada4af": { - "address": "0x1c3d6d41dcb245c11a449ec46c9cf9eb7dada4af", - "name": "Account 2" - }, - "0xe34b1ac3074121418152c7a68b4ae6cb7803d725": { - "address": "0xe34b1ac3074121418152c7a68b4ae6cb7803d725", - "name": "Account 3" - } - }, - "unapprovedTxs": {}, - "noActiveNotices": true, - "frequentRpcList": [], - "addressBook": [], - "network": "1", - "accounts": { - "0x55e2780588aa5000f464f700d2676fd0a22ee160": { - "balance": "0x4622f471c28b8a53", - "nonce": "0x17", - "code": "0x", - "address": "0x55e2780588aa5000f464f700d2676fd0a22ee160" - }, - "0x1c3d6d41dcb245c11a449ec46c9cf9eb7dada4af": { - "balance": "0x0", - "nonce": "0x0", - "code": "0x", - "address": "0x1c3d6d41dcb245c11a449ec46c9cf9eb7dada4af" - }, - "0xe34b1ac3074121418152c7a68b4ae6cb7803d725": { - "balance": "0x0", - "nonce": "0x0", - "code": "0x", - "address": "0xe34b1ac3074121418152c7a68b4ae6cb7803d725" - } - }, - "transactions": {}, - "currentBlockNumber": 3575443, - "currentBlockHash": "0xf03bdb8ad844336723473865a5368fa618de837d8290ad380fadbc9fa2bf87f6", - "selectedAddressTxList": [], - "unapprovedMsgs": {}, - "unapprovedMsgCount": 0, - "unapprovedPersonalMsgs": {}, - "unapprovedPersonalMsgCount": 0, - "keyringTypes": [ - "Simple Key Pair", - "HD Key Tree" - ], - "keyrings": [ - { - "type": "HD Key Tree", - "accounts": [ - "55e2780588aa5000f464f700d2676fd0a22ee160", - "1c3d6d41dcb245c11a449ec46c9cf9eb7dada4af", - "e34b1ac3074121418152c7a68b4ae6cb7803d725" - ] - } - ], - "selectedAddress": "0x55e2780588aa5000f464f700d2676fd0a22ee160", - "currentCurrency": "USD", - "conversionRate": 51.12009214, - "conversionDate": 1492788481, - "provider": { - "type": "mainnet" - }, - "shapeShiftTxList": [], - "lostAccounts": [] - }, - "appState": { - "shouldClose": false, - "menuOpen": false, - "currentView": { - "name": "accountDetail", - "detailView": "tokens", - "context": "0x55e2780588aa5000f464f700d2676fd0a22ee160" - }, - "accountDetail": { - "subview": "transactions" - }, - "transForward": true, - "isLoading": false, - "warning": null - }, - "identities": {} -} -- cgit v1.2.3 From 3df2f2b2d4239a033ba23d14a75e0a10ece584aa Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 13 Jun 2017 18:00:06 -0700 Subject: Rename history to sent --- ui/app/account-detail.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index 8234a8438..2e7f3b1be 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -256,7 +256,7 @@ AccountDetailScreen.prototype.tabSections = function () { h(TabBar, { tabs: [ - { content: 'History', key: 'history' }, + { content: 'Sent', key: 'history' }, { content: 'Tokens', key: 'tokens' }, ], defaultTab: tabSelection || 'history', -- cgit v1.2.3 From a8ababe2f495ef91bd51436d41709cadb7b2f48c Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 14 Jun 2017 10:13:07 -0700 Subject: Add a new warning for file import JSON --- ui/app/accounts/import/json.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/app/accounts/import/json.js b/ui/app/accounts/import/json.js index 5ed31ab0a..158a3c923 100644 --- a/ui/app/accounts/import/json.js +++ b/ui/app/accounts/import/json.js @@ -5,6 +5,8 @@ const connect = require('react-redux').connect const actions = require('../../actions') const FileInput = require('react-simple-file-input').default +const HELP_LINK = 'https://github.com/MetaMask/faq/blob/master/README.md#q-i-cant-use-the-import-feature-for-uploading-a-json-file-the-window-keeps-closing-when-i-try-to-select-a-file' + module.exports = connect(mapStateToProps)(JsonImportSubview) function mapStateToProps (state) { @@ -32,6 +34,7 @@ JsonImportSubview.prototype.render = function () { }, [ h('p', 'Used by a variety of different clients'), + h('a.warning', { href: HELP_LINK, target: '_blank' }, 'File import not working? Click here!'), h(FileInput, { readAs: 'text', -- cgit v1.2.3 From 74d15f5cb0bae86cfbc48019d40ac68943f72b18 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 14 Jun 2017 10:13:34 -0700 Subject: Bump changelog. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8afa1eec4..7f99e680e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Add a warning to JSON file import. + ## 3.7.8 2017-6-12 - Add a `ethereum:` prefix to the QR code address -- cgit v1.2.3 From 0fd32e67d4c8e911cd5cd88b81f04d11b2202609 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 14 Jun 2017 12:01:45 -0700 Subject: Do not mark slowly mined txs as failed. Fixes #1567 Also seems to fix #1556 Also improves resubmit performance by only resubmitting on `latest`. --- CHANGELOG.md | 1 + app/scripts/controllers/transactions.js | 12 ++++-------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f99e680e..0d3e86342 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Current Master - Add a warning to JSON file import. +- Fix bug where slowly mined txs would sometimes be incorrectly marked as failed. ## 3.7.8 2017-6-12 diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 2db8041eb..931f01855 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -25,7 +25,7 @@ module.exports = class TransactionController extends EventEmitter { this.query = opts.ethQuery this.txProviderUtils = new TxProviderUtil(this.query) this.blockTracker.on('block', this.checkForTxInBlock.bind(this)) - this.blockTracker.on('block', this.resubmitPendingTxs.bind(this)) + this.blockTracker.on('latest', this.resubmitPendingTxs.bind(this)) this.signEthTx = opts.signTransaction this.nonceLock = Semaphore(1) @@ -339,7 +339,7 @@ module.exports = class TransactionController extends EventEmitter { // checks if a signed tx is in a block and // if included sets the tx status as 'confirmed' checkForTxInBlock () { - var signedTxList = this.getFilteredTxList({status: 'submitted'}) + var signedTxList = this.getFilteredTxList({ status: 'submitted' }) if (!signedTxList.length) return signedTxList.forEach((txMeta) => { var txHash = txMeta.hash @@ -430,12 +430,8 @@ module.exports = class TransactionController extends EventEmitter { } if (txMeta.retryCount > RETRY_LIMIT) { - txMeta.err = { - isWarning: true, - message: 'Gave up submitting tx.', - } - this.updateTx(txMeta) - return log.error(txMeta.err.message) + const message = 'Gave up submitting tx ' + txMeta.hash + return log.warning(message) } txMeta.retryCount++ -- cgit v1.2.3 From b7b9e0c1ac203d39196753f39f17a1fe2f4751e5 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 14 Jun 2017 14:21:50 -0700 Subject: Persist selected account tab Also improve error handling with token balances. --- app/scripts/controllers/preferences.js | 8 ++++++++ app/scripts/metamask-controller.js | 1 + package.json | 4 ++-- ui/app/account-detail.js | 13 +++++++------ ui/app/actions.js | 25 +++++++++++++++++++++---- ui/app/components/token-list.js | 15 +++++++++++++-- 6 files changed, 52 insertions(+), 14 deletions(-) diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 7212c7c43..aa8e05fcc 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -7,6 +7,7 @@ class PreferencesController { constructor (opts = {}) { const initState = extend({ frequentRpcList: [], + currentAccountTab: 'history', }, opts.initState) this.store = new ObservableStore(initState) } @@ -35,6 +36,13 @@ class PreferencesController { }) } + setCurrentAccountTab (currentAccountTab) { + return new Promise((resolve, reject) => { + this.store.updateState({ currentAccountTab }) + resolve() + }) + } + addToFrequentRpcList (_url) { const rpcList = this.getFrequentRpcList() const index = rpcList.findIndex((element) => { return element === _url }) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index a7eb3d056..410693df4 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -275,6 +275,7 @@ module.exports = class MetamaskController extends EventEmitter { // PreferencesController setSelectedAddress: nodeify(preferencesController.setSelectedAddress).bind(preferencesController), + setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab).bind(preferencesController), setDefaultRpc: nodeify(this.setDefaultRpc).bind(this), setCustomRpc: nodeify(this.setCustomRpc).bind(this), diff --git a/package.json b/package.json index cb4c05aaf..edef4753f 100644 --- a/package.json +++ b/package.json @@ -62,12 +62,12 @@ "end-of-stream": "^1.1.0", "ensnare": "^1.0.0", "eth-bin-to-ops": "^1.0.1", - "eth-contract-metadata": "^1.1.0", + "eth-contract-metadata": "^1.1.1", "eth-hd-keyring": "^1.1.1", "eth-query": "^2.1.1", "eth-sig-util": "^1.1.1", "eth-simple-keyring": "^1.1.1", - "eth-token-tracker": "^1.0.4", + "eth-token-tracker": "^1.0.6", "ethereumjs-tx": "^1.3.0", "ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index 2e7f3b1be..836032b3c 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -34,12 +34,12 @@ function mapStateToProps (state) { transactions: state.metamask.selectedAddressTxList || [], conversionRate: state.metamask.conversionRate, currentCurrency: state.metamask.currentCurrency, + currentAccountTab: state.metamask.currentAccountTab, } } inherits(AccountDetailScreen, Component) function AccountDetailScreen () { - this.state = { tabSelection: 'history' } Component.call(this) } @@ -251,7 +251,8 @@ AccountDetailScreen.prototype.subview = function () { } AccountDetailScreen.prototype.tabSections = function () { - const tabSelection = this.state.tabSelection + const { currentAccountTab } = this.props + return h('section.tabSection', [ h(TabBar, { @@ -259,9 +260,9 @@ AccountDetailScreen.prototype.tabSections = function () { { content: 'Sent', key: 'history' }, { content: 'Tokens', key: 'tokens' }, ], - defaultTab: tabSelection || 'history', + defaultTab: currentAccountTab || 'history', tabSelected: (key) => { - this.setState({ tabSelection: key }) + this.props.dispatch(actions.setCurrentAccountTab(key)) }, }), @@ -272,9 +273,9 @@ AccountDetailScreen.prototype.tabSections = function () { AccountDetailScreen.prototype.tabSwitchView = function () { const props = this.props const { address, network } = props - const tabSelection = this.state.tabSelection || 'history' + const { currentAccountTab } = this.props - switch (tabSelection) { + switch (currentAccountTab) { case 'tokens': return h(TokenList, { userAddress: address, network }) default: diff --git a/ui/app/actions.js b/ui/app/actions.js index 1a3557cb4..b6b5d6eb1 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -74,6 +74,7 @@ var actions = { SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE', SET_CURRENT_FIAT: 'SET_CURRENT_FIAT', setCurrentCurrency: setCurrentCurrency, + setCurrentAccountTab, // account detail screen SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', showSendPage: showSendPage, @@ -218,7 +219,7 @@ function confirmSeedWords () { return dispatch(actions.displayWarning(err.message)) } - console.log('Seed word cache cleared. ' + account) + log.info('Seed word cache cleared. ' + account) dispatch(actions.showAccountDetail(account)) }) } @@ -338,7 +339,7 @@ function setCurrentCurrency (currencyCode) { background.setCurrentCurrency(currencyCode, (err, data) => { dispatch(this.hideLoadingIndication()) if (err) { - console.error(err.stack) + log.error(err.stack) return dispatch(actions.displayWarning(err.message)) } dispatch({ @@ -409,7 +410,7 @@ function sendTx (txData) { background.approveTransaction(txData.id, (err) => { if (err) { dispatch(actions.txError(err)) - return console.error(err.message) + return log.error(err.message) } dispatch(actions.completedTx(txData.id)) }) @@ -424,7 +425,7 @@ function updateAndApproveTx (txData) { dispatch(actions.hideLoadingIndication()) if (err) { dispatch(actions.txError(err)) - return console.error(err.message) + return log.error(err.message) } dispatch(actions.completedTx(txData.id)) }) @@ -558,6 +559,11 @@ function lockMetamask () { return callBackgroundThenUpdate(background.setLocked) } +function setCurrentAccountTab (newTabName) { + log.debug(`background.setCurrentAccountTab: ${newTabName}`) + return callBackgroundThenUpdateNoSpinner(background.setCurrentAccountTab, newTabName) +} + function showAccountDetail (address) { return (dispatch) => { dispatch(actions.showLoadingIndication()) @@ -965,6 +971,17 @@ function shapeShiftRequest (query, options, cb) { // We hide loading indication. // If it errored, we show a warning. // If it didn't, we update the state. +function callBackgroundThenUpdateNoSpinner (method, ...args) { + return (dispatch) => { + method.call(background, ...args, (err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + forceUpdateMetamaskState(dispatch) + }) + } +} + function callBackgroundThenUpdate (method, ...args) { return (dispatch) => { dispatch(actions.showLoadingIndication()) diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index b79fbccf3..66cbddeda 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -80,10 +80,21 @@ TokenList.prototype.componentDidMount = function () { this.setState({ tokens: this.tracker.serialize() }) this.tracker.on('update', (tokenData) => { - const heldTokens = tokenData.filter(token => token.balance !== '0' && token.string !== '0.000') - this.setState({ tokens: heldTokens, isLoading: false }) + this.updateBalances(tokenData) }) this.tracker.updateBalances() + .then(() => { + this.updateBalances(this.tracker.serialize()) + }) + .catch((reason) => { + log.error(`Problem updating balances`, reason) + this.setState({ isLoading: false }) + }) +} + +TokenList.prototype.updateBalances = function (tokenData) { + const heldTokens = tokenData.filter(token => token.balance !== '0' && token.string !== '0.000') + this.setState({ tokens: heldTokens, isLoading: false }) } TokenList.prototype.componentWillUnmount = function () { -- cgit v1.2.3 From 6fda78cd2b850c7414d598227a0ef6b4235f241e Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 14 Jun 2017 15:17:46 -0700 Subject: Refresh token balance on network change --- app/scripts/controllers/transactions.js | 1 - package.json | 2 +- ui/app/components/token-list.js | 27 ++++++++++++++++++++++----- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 8be73fad8..d546615ed 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -389,7 +389,6 @@ module.exports = class TransactionController extends EventEmitter { this.emit(`${txMeta.id}:${status}`, txId) if (status === 'submitted' || status === 'rejected') { this.emit(`${txMeta.id}:finished`, txMeta) - } this.updateTx(txMeta) this.emit('updateBadge') diff --git a/package.json b/package.json index edef4753f..8c4ef3dc4 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "eth-query": "^2.1.1", "eth-sig-util": "^1.1.1", "eth-simple-keyring": "^1.1.1", - "eth-token-tracker": "^1.0.6", + "eth-token-tracker": "^1.0.7", "ethereumjs-tx": "^1.3.0", "ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index 66cbddeda..90e7e876e 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -7,7 +7,7 @@ const contracts = require('eth-contract-metadata') const Loading = require('./loading') const tokens = [] -for (let address in contracts) { +for (const address in contracts) { const contract = contracts[address] if (contract.erc20) { contract.address = address @@ -19,7 +19,7 @@ module.exports = TokenList inherits(TokenList, Component) function TokenList () { - this.state = { tokens, isLoading: true } + this.state = { tokens, isLoading: true, network: null } Component.call(this) } @@ -68,17 +68,23 @@ TokenList.prototype.render = function () { } TokenList.prototype.componentDidMount = function () { + this.createFreshTokenTracker() +} + +TokenList.prototype.createFreshTokenTracker = function () { + if (this.tracker) { + this.tracker.stop() + } + if (!global.ethereumProvider) return const { userAddress } = this.props - this.tracker = new TokenTracker({ userAddress, provider: global.ethereumProvider, - tokens: this.state.tokens, + tokens: tokens, pollingInterval: 8000, }) - this.setState({ tokens: this.tracker.serialize() }) this.tracker.on('update', (tokenData) => { this.updateBalances(tokenData) }) @@ -92,6 +98,17 @@ TokenList.prototype.componentDidMount = function () { }) } +TokenList.prototype.componentWillUpdate = function (nextProps) { + if (nextProps.network === 'loading') return + const oldNet = this.props.network + const newNet = nextProps.network + + if (oldNet && newNet && newNet !== oldNet) { + this.setState({ isLoading: true }) + this.createFreshTokenTracker() + } +} + TokenList.prototype.updateBalances = function (tokenData) { const heldTokens = tokenData.filter(token => token.balance !== '0' && token.string !== '0.000') this.setState({ tokens: heldTokens, isLoading: false }) -- cgit v1.2.3 From a025cd178d8741dc41c31d3823004d5c5628a15a Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 14 Jun 2017 15:23:13 -0700 Subject: Add issue template. --- ISSUE_TEMPLATE | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 ISSUE_TEMPLATE diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE new file mode 100644 index 000000000..9c17ec3cd --- /dev/null +++ b/ISSUE_TEMPLATE @@ -0,0 +1,17 @@ + -- cgit v1.2.3 From 96d416c486c4efd3698d41a38a02c6379fbb61b1 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 14 Jun 2017 15:30:03 -0700 Subject: Vertically center loading indication --- ui/app/components/token-list.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index 90e7e876e..c9e86dd22 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -4,7 +4,6 @@ const inherits = require('util').inherits const TokenTracker = require('eth-token-tracker') const TokenCell = require('./token-cell.js') const contracts = require('eth-contract-metadata') -const Loading = require('./loading') const tokens = [] for (const address in contracts) { @@ -29,7 +28,16 @@ TokenList.prototype.render = function () { const { userAddress } = this.props - if (isLoading) return h(Loading, { isLoading }) + if (isLoading) { + return h('div', { + style: { + display: 'flex', + height: '250px', + alignItems: 'center', + justifyContent: 'center', + }, + }, 'Loading') + } const network = this.props.network -- cgit v1.2.3 From 0b18a69679e2119fb2bc7ebe027312793504bd5f Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 14 Jun 2017 15:52:46 -0700 Subject: Bump token-tracker version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8c4ef3dc4..7f0089d8a 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "eth-query": "^2.1.1", "eth-sig-util": "^1.1.1", "eth-simple-keyring": "^1.1.1", - "eth-token-tracker": "^1.0.7", + "eth-token-tracker": "^1.0.8", "ethereumjs-tx": "^1.3.0", "ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", -- cgit v1.2.3 From 56490c6468bb83dccf04941ded5fec1017e5fe2c Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 14 Jun 2017 16:14:15 -0700 Subject: Bump provider-engine --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2c23d9e10..127211374 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "valid-url": "^1.0.9", "vreme": "^3.0.2", "web3": "0.18.2", - "web3-provider-engine": "^12.2.4", + "web3-provider-engine": "^13.0.0", "web3-stream-provider": "^2.0.6", "xtend": "^4.0.1" }, -- cgit v1.2.3 From 6ae97290f0e744479a41e31507f79309137d94c0 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 14 Jun 2017 16:14:55 -0700 Subject: check for the tx in the block that provider engine gives us --- app/scripts/controllers/transactions.js | 20 +++++--------------- package.json | 2 +- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 2db8041eb..71f90c2cd 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -338,12 +338,13 @@ module.exports = class TransactionController extends EventEmitter { // checks if a signed tx is in a block and // if included sets the tx status as 'confirmed' - checkForTxInBlock () { + checkForTxInBlock (block) { var signedTxList = this.getFilteredTxList({status: 'submitted'}) if (!signedTxList.length) return signedTxList.forEach((txMeta) => { var txHash = txMeta.hash var txId = txMeta.id + if (!txHash) { const errReason = { errCode: 'No hash was provided', @@ -351,20 +352,9 @@ module.exports = class TransactionController extends EventEmitter { } return this.setTxStatusFailed(txId, errReason) } - this.query.getTransactionByHash(txHash, (err, txParams) => { - if (err || !txParams) { - if (!txParams) return - txMeta.err = { - isWarning: true, - errorCode: err, - message: 'There was a problem loading this transaction.', - } - this.updateTx(txMeta) - return log.error(err) - } - if (txParams.blockNumber) { - this.setTxStatusConfirmed(txId) - } + + block.transactions.forEach((tx) => { + if (tx.hash === txHash) this.setTxStatusConfirmed(txId) }) }) } diff --git a/package.json b/package.json index 127211374..7ee5dc5be 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "valid-url": "^1.0.9", "vreme": "^3.0.2", "web3": "0.18.2", - "web3-provider-engine": "^13.0.0", + "web3-provider-engine": "^13.0.1", "web3-stream-provider": "^2.0.6", "xtend": "^4.0.1" }, -- cgit v1.2.3 From 1814721e80c057dd5da6f89ece3f2d376ca59bc1 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 14 Jun 2017 18:08:03 -0700 Subject: Add no tokens message --- ui/app/components/token-list.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index c9e86dd22..c560a6072 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -29,14 +29,7 @@ TokenList.prototype.render = function () { const { userAddress } = this.props if (isLoading) { - return h('div', { - style: { - display: 'flex', - height: '250px', - alignItems: 'center', - justifyContent: 'center', - }, - }, 'Loading') + return this.message('Loading') } const network = this.props.network @@ -71,10 +64,21 @@ TokenList.prototype.render = function () { cursor: pointer; } - `)].concat(tokenViews)) + `)].concat(tokenViews.length ? tokenViews : this.message('No Tokens Found.'))) ) } +TokenList.prototype.message = function (body) { + return h('div', { + style: { + display: 'flex', + height: '250px', + alignItems: 'center', + justifyContent: 'center', + }, + }, body) +} + TokenList.prototype.componentDidMount = function () { this.createFreshTokenTracker() } -- cgit v1.2.3 From 68389d5d496dc11a25ce07b5f95b0e10954e847f Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 14 Jun 2017 18:12:41 -0700 Subject: Remove excessive log --- ui/app/components/token-cell.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js index ad7f55345..d3a895d36 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/token-cell.js @@ -13,7 +13,6 @@ function TokenCell () { TokenCell.prototype.render = function () { const props = this.props const { address, symbol, string, network, userAddress } = props - log.info({ address, symbol, string, network }) return ( h('li.token-cell', { -- cgit v1.2.3 From 1ed5804e4dd2f85549abcb8dfd8981dab3f6868c Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 14 Jun 2017 19:15:50 -0700 Subject: Multiple loading style improvements - When seeking network, show a full screen loading indication + message. - Network menu is still accessible "over" this indication. - Top Menu-Droppo components now slide *under* the menu bar like they should. - Loading indication opacity increased to increase message legibility. --- ui/app/app.js | 19 ++++++++++++------- ui/app/components/loading.js | 5 ++++- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index 53dbc3354..17a0f8ef3 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -21,7 +21,7 @@ const generateLostAccountsNotice = require('../lib/lost-accounts-notice') const ConfigScreen = require('./config') const Import = require('./accounts/import') const InfoScreen = require('./info') -const LoadingIndicator = require('./components/loading') +const Loading = require('./components/loading') const SandwichExpando = require('sandwich-expando') const MenuDroppo = require('menu-droppo') const DropMenuItem = require('./components/drop-menu-item') @@ -64,7 +64,9 @@ function mapStateToProps (state) { App.prototype.render = function () { var props = this.props - const { isLoading, loadingMessage, transForward } = props + const { isLoading, loadingMessage, transForward, network } = props + const isLoadingNetwork = network === 'loading' + log.debug('Main ui render function') return ( @@ -77,13 +79,16 @@ App.prototype.render = function () { }, }, [ - h(LoadingIndicator, { isLoading, loadingMessage }), - // app bar this.renderAppBar(), this.renderNetworkDropdown(), this.renderDropdown(), + h(Loading, { + isLoading: isLoading || isLoadingNetwork, + loadingMessage: loadingMessage || 'Searching for Network', + }), + // panel content h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), { style: { @@ -124,7 +129,7 @@ App.prototype.renderAppBar = function () { background: props.isUnlocked ? 'white' : 'none', height: '36px', position: 'relative', - zIndex: 10, + zIndex: 12, }, }, [ @@ -221,7 +226,7 @@ App.prototype.renderNetworkDropdown = function () { onClickOutside: (event) => { this.setState({ isNetworkMenuOpen: !isOpen }) }, - zIndex: 1, + zIndex: 11, style: { position: 'absolute', left: 0, @@ -300,7 +305,7 @@ App.prototype.renderDropdown = function () { return h(MenuDroppo, { isOpen: isOpen, - zIndex: 1, + zIndex: 11, onClickOutside: (event) => { this.setState({ isMainMenuOpen: !isOpen }) }, diff --git a/ui/app/components/loading.js b/ui/app/components/loading.js index 88dc535df..87d6f5d20 100644 --- a/ui/app/components/loading.js +++ b/ui/app/components/loading.js @@ -26,18 +26,21 @@ LoadingIndicator.prototype.render = function () { style: { zIndex: 10, position: 'absolute', + flexDirection: 'column', display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%', width: '100%', - background: 'rgba(255, 255, 255, 0.5)', + background: 'rgba(255, 255, 255, 0.8)', }, }, [ h('img', { src: 'images/loading.svg', }), + h('br'), + showMessageIfAny(loadingMessage), ]) : null, ]) -- cgit v1.2.3 From a80945e73075b8c0dc43a68ba73da65d7074e098 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 14 Jun 2017 19:36:37 -0700 Subject: Hide message on normal loading --- ui/app/app.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/app/app.js b/ui/app/app.js index 17a0f8ef3..d444a8349 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -66,6 +66,8 @@ App.prototype.render = function () { var props = this.props const { isLoading, loadingMessage, transForward, network } = props const isLoadingNetwork = network === 'loading' + const loadMessage = loadingMessage || isLoadingNetwork ? + 'Searching for Network' : null log.debug('Main ui render function') @@ -86,7 +88,7 @@ App.prototype.render = function () { h(Loading, { isLoading: isLoading || isLoadingNetwork, - loadingMessage: loadingMessage || 'Searching for Network', + loadingMessage: loadMessage, }), // panel content -- cgit v1.2.3 From a10740af7e35aa60e0445598403e6bda22382c2f Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 14 Jun 2017 20:17:59 -0700 Subject: add a check for weather a tx is included in a block when jumping blocks --- app/scripts/controllers/transactions.js | 41 +++++++++++++++++++++++++++++++++ test/unit/tx-controller-test.js | 1 + 2 files changed, 42 insertions(+) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 2db8041eb..41f651458 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -26,6 +26,7 @@ module.exports = class TransactionController extends EventEmitter { this.txProviderUtils = new TxProviderUtil(this.query) this.blockTracker.on('block', this.checkForTxInBlock.bind(this)) this.blockTracker.on('block', this.resubmitPendingTxs.bind(this)) + this.provider._blockTracker.on('sync', this.queryPendingTxs.bind(this)) this.signEthTx = opts.signTransaction this.nonceLock = Semaphore(1) @@ -369,6 +370,15 @@ module.exports = class TransactionController extends EventEmitter { }) } + queryPendingTxs ({oldBlock, newBlock}) { + if (!oldBlock) { + this._checkPendingTxs() + return + } + const diff = Number.parseInt(newBlock.number) - Number.parseInt(oldBlock.number) + if (diff > 1) this._checkPendingTxs() + } + // PRIVATE METHODS // Should find the tx in the tx list and @@ -443,6 +453,37 @@ module.exports = class TransactionController extends EventEmitter { this.txProviderUtils.publishTransaction(rawTx, cb) } + _checkPendingTxs () { + var signedTxList = this.getFilteredTxList({status: 'submitted'}) + if (!signedTxList.length) return + signedTxList.forEach((txMeta) => { + var txHash = txMeta.hash + var txId = txMeta.id + if (!txHash) { + const errReason = { + errCode: 'No hash was provided', + message: 'We had an error while submitting this transaction, please try again.', + } + return this.setTxStatusFailed(txId, errReason) + } + this.query.getTransactionByHash(txHash, (err, txParams) => { + if (err || !txParams) { + if (!txParams) return + txMeta.err = { + isWarning: true, + errorCode: err, + message: 'There was a problem loading this transaction.', + } + this.updateTx(txMeta) + return log.error(err) + } + if (txParams.blockNumber) { + this.setTxStatusConfirmed(txId) + } + }) + }) + } + } diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index f0d8a706e..0d35cd62c 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -19,6 +19,7 @@ describe('Transaction Controller', function () { txController = new TransactionController({ networkStore: new ObservableStore(currentNetworkId), txHistoryLimit: 10, + provider: { _blockTracker: new EventEmitter()}, blockTracker: new EventEmitter(), ethQuery: new EthQuery(new EventEmitter()), signTransaction: (ethTx) => new Promise((resolve) => { -- cgit v1.2.3 From 0e1e0aa32398b0b9d19cd6ae3fb06d577aae6cc6 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 14 Jun 2017 20:42:48 -0700 Subject: Create add token button and template view --- ui/app/account-detail.js | 6 ++- ui/app/actions.js | 11 ++++- ui/app/add-token.js | 91 +++++++++++++++++++++++++++++++++++++++++ ui/app/app.js | 5 +++ ui/app/components/token-list.js | 71 ++++++++++++++++++++++---------- ui/app/reducers/app.js | 10 +++++ 6 files changed, 170 insertions(+), 24 deletions(-) create mode 100644 ui/app/add-token.js diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index 836032b3c..19f2cba59 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -277,7 +277,11 @@ AccountDetailScreen.prototype.tabSwitchView = function () { switch (currentAccountTab) { case 'tokens': - return h(TokenList, { userAddress: address, network }) + return h(TokenList, { + userAddress: address, + network, + addToken: () => this.props.dispatch(actions.showAddTokenPage()), + }) default: return this.transactionList() } diff --git a/ui/app/actions.js b/ui/app/actions.js index b6b5d6eb1..d17d4610e 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -121,7 +121,9 @@ var actions = { SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE', USE_ETHERSCAN_PROVIDER: 'USE_ETHERSCAN_PROVIDER', useEtherscanProvider: useEtherscanProvider, - showConfigPage: showConfigPage, + showConfigPage, + SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', + showAddTokenPage, setRpcTarget: setRpcTarget, setDefaultRpcTarget: setDefaultRpcTarget, setProviderType: setProviderType, @@ -627,6 +629,13 @@ function showConfigPage (transitionForward = true) { } } +function showAddTokenPage (transitionForward = true) { + return { + type: actions.SHOW_ADD_TOKEN_PAGE, + value: transitionForward, + } +} + function goBackToInitView () { return { type: actions.BACK_TO_INIT_MENU, diff --git a/ui/app/add-token.js b/ui/app/add-token.js new file mode 100644 index 000000000..5356b6a0b --- /dev/null +++ b/ui/app/add-token.js @@ -0,0 +1,91 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') + +module.exports = connect(mapStateToProps)(AddTokenScreen) + +function mapStateToProps (state) { + return {} +} + +inherits(AddTokenScreen, Component) +function AddTokenScreen () { + this.state = { warning: null } + Component.call(this) +} + +AddTokenScreen.prototype.render = function () { + const state = this.state + const { warning } = state + return ( + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + state.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Add Token'), + ]), + + h('.error', { + style: { + display: warning ? 'block' : 'none', + padding: '0 20px', + textAlign: 'center', + }, + }, warning), + + // conf view + h('.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-space-around', { + style: { + padding: '20px', + }, + }, [ + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Token Sybmol'), + ]), + + h('div', { style: {display: 'flex'} }, [ + h('input#token_symbol', { + placeholder: `Like "ETH"`, + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onKeyPress (event) { + if (event.key === 'Enter') { + var element = event.target + var newRpc = element.value + } + }, + }), + ]), + + h('button', { + style: { + alignSelf: 'center', + }, + onClick (event) { + event.preventDefault() + var tokenSymbolEl = document.querySelector('input#token_symbol') + var tokenSymbol = tokenSymbolEl.value + console.log(tokenSymbol) + }, + }, 'Add'), + ]), + ]), + ]) + ) +} + diff --git a/ui/app/app.js b/ui/app/app.js index d444a8349..8bf69b5ad 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -19,6 +19,7 @@ const NoticeScreen = require('./components/notice') const generateLostAccountsNotice = require('../lib/lost-accounts-notice') // other views const ConfigScreen = require('./config') +const AddTokenScreen = require('./add-token') const Import = require('./accounts/import') const InfoScreen = require('./info') const Loading = require('./components/loading') @@ -458,6 +459,10 @@ App.prototype.renderPrimary = function () { log.debug('rendering confirm tx screen') return h(ConfirmTxScreen, {key: 'confirm-tx'}) + case 'add-token': + log.debug('rendering add-token screen from unlock screen.') + return h(AddTokenScreen, {key: 'add-token'}) + case 'config': log.debug('rendering config screen') return h(ConfigScreen, {key: 'config'}) diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index c560a6072..d230ce74a 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -40,32 +40,59 @@ TokenList.prototype.render = function () { return h(TokenCell, tokenData) }) - return ( + return h('div', [ h('ol', { style: { - height: '302px', + height: '260px', overflowY: 'auto', + display: 'flex', + flexDirection: 'column', }, - }, [h('style', ` - - li.token-cell { - display: flex; - flex-direction: row; - align-items: center; - padding: 10px; - } - - li.token-cell > h3 { - margin-left: 12px; - } - - li.token-cell:hover { - background: white; - cursor: pointer; - } - - `)].concat(tokenViews.length ? tokenViews : this.message('No Tokens Found.'))) - ) + }, [ + h('style', ` + + li.token-cell { + display: flex; + flex-direction: row; + align-items: center; + padding: 10px; + } + + li.token-cell > h3 { + margin-left: 12px; + } + + li.token-cell:hover { + background: white; + cursor: pointer; + } + + `), + ...tokenViews, + tokenViews.length ? null : this.message('No Tokens Found.'), + ]), + this.addTokenButtonElement(), + ]) +} + +TokenList.prototype.addTokenButtonElement = function () { + return h('div', [ + h('div.footer.hover-white.pointer', { + key: 'reveal-account-bar', + onClick: () => { + this.props.addToken() + }, + style: { + display: 'flex', + height: '40px', + padding: '10px', + justifyContent: 'center', + alignItems: 'center', + }, + }, [ + h('i.fa.fa-plus.fa-lg'), + ]), + ]) } TokenList.prototype.message = function (body) { diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index deacad0a7..2fcc9bfe0 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -103,7 +103,17 @@ function reduceApp (state, action) { transForward: action.value, }) + case actions.SHOW_ADD_TOKEN_PAGE: + return extend(appState, { + currentView: { + name: 'add-token', + context: appState.currentView.context, + }, + transForward: action.value, + }) + case actions.SHOW_IMPORT_PAGE: + return extend(appState, { currentView: { name: 'import-menu', -- cgit v1.2.3 From da33efe77515bbfff7100f3205b4d0d907e9296b Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 14 Jun 2017 21:42:29 -0700 Subject: bump eth-query for quiter logs --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2c23d9e10..c60aff821 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "eth-bin-to-ops": "^1.0.1", "eth-contract-metadata": "^1.0.0", "eth-hd-keyring": "^1.1.1", - "eth-query": "^2.1.1", + "eth-query": "^2.1.2", "eth-sig-util": "^1.1.1", "eth-simple-keyring": "^1.1.1", "ethereumjs-tx": "^1.3.0", -- cgit v1.2.3 From 07539a63e4378153778b5bb264aaffad7e46cf34 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 14 Jun 2017 21:52:49 -0700 Subject: remove unnecessary log --- app/scripts/controllers/transactions.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 931f01855..2769a8d59 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -429,10 +429,7 @@ module.exports = class TransactionController extends EventEmitter { return cb() } - if (txMeta.retryCount > RETRY_LIMIT) { - const message = 'Gave up submitting tx ' + txMeta.hash - return log.warning(message) - } + if (txMeta.retryCount > RETRY_LIMIT) return txMeta.retryCount++ const rawTx = txMeta.rawTx -- cgit v1.2.3 From dab2fccc78ad76095cc12ce0a1056d9b7a9d6001 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 14 Jun 2017 22:16:14 -0700 Subject: introduce nonce-tracker --- app/scripts/lib/nonce-tracker.js | 49 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 app/scripts/lib/nonce-tracker.js diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js new file mode 100644 index 000000000..6e9d094bc --- /dev/null +++ b/app/scripts/lib/nonce-tracker.js @@ -0,0 +1,49 @@ +const EthQuery = require('ethjs-query') + +class NonceTracker { + + constructor({ blockTracker, provider, getPendingTransactions }) { + this.blockTracker = blockTracker + this.ethQuery = new EthQuery(provider) + this.getPendingTransactions = getPendingTransactions + this.lockMap = {} + } + + // releaseLock must be called + // releaseLock must be called after adding signed tx to pending transactions (or discarding) + async getNonceLock(address) { + // await lock free + await this.lockMap[address] + // take lock + const releaseLock = this._takeLock(address) + // calculate next nonce + const currentBlock = await this._getCurrentBlock() + const blockNumber = currentBlock.number + const pendingTransactions = this.getPendingTransactions(address) + const baseCount = await this.ethQuery.getTransactionCount(address, blockNumber) + const nextNonce = baseCount + pendingTransactions + // return next nonce and release cb + return { nextNonce, releaseLock } + } + + async _getCurrentBlock() { + const currentBlock = this.blockTracker.getCurrentBlock() + if (currentBlock) return currentBlock + return await Promise((reject, resolve) => { + this.blockTracker.once('latest', resolve) + }) + } + + _takeLock(lockId) { + let releaseLock = null + // create and store lock + const lock = new Promise((reject, resolve) => { releaseLock = resolve }) + this.lockMap[lockId] = lock + // setup lock teardown + lock.then(() => delete this.lockMap[lockId]) + return releaseLock + } + +} + +module.exports = NonceTracker -- cgit v1.2.3 From b3492d9c17e62332c17bb082c23db30512e2b881 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 14 Jun 2017 23:44:02 -0700 Subject: transaction controller - use nonce-tracker --- app/scripts/controllers/transactions.js | 90 +++++++++++++++++++++------------ app/scripts/metamask-controller.js | 2 +- test/unit/tx-controller-test.js | 7 +-- 3 files changed, 63 insertions(+), 36 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 2db8041eb..e7fe9927e 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -4,9 +4,10 @@ const extend = require('xtend') const Semaphore = require('semaphore') const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') +const denodeify = require('denodeify') const TxProviderUtil = require('../lib/tx-utils') const createId = require('../lib/random-id') -const denodeify = require('denodeify') +const NonceTracker = require('../lib/nonce-tracker') const RETRY_LIMIT = 200 @@ -22,6 +23,11 @@ module.exports = class TransactionController extends EventEmitter { this.txHistoryLimit = opts.txHistoryLimit this.provider = opts.provider this.blockTracker = opts.blockTracker + this.nonceTracker = new NonceTracker({ + provider: this.provider, + blockTracker: this.blockTracker, + getPendingTransactions: (address) => this.getFilteredTxList({ from: address, status: 'submitted' }), + }) this.query = opts.ethQuery this.txProviderUtils = new TxProviderUtil(this.query) this.blockTracker.on('block', this.checkForTxInBlock.bind(this)) @@ -169,29 +175,58 @@ module.exports = class TransactionController extends EventEmitter { }, {}) } - approveTransaction (txId, cb = warn) { - const self = this - // approve - self.setTxStatusApproved(txId) - // only allow one tx at a time for atomic nonce usage - self.nonceLock.take(() => { - // begin signature process - async.waterfall([ - (cb) => self.fillInTxParams(txId, cb), - (cb) => self.signTransaction(txId, cb), - (rawTx, cb) => self.publishTransaction(txId, rawTx, cb), - ], (err) => { - self.nonceLock.leave() - if (err) { - this.setTxStatusFailed(txId, { - errCode: err.errCode || err, - message: err.message || 'Transaction failed during approval', - }) - return cb(err) - } - cb() + // approveTransaction (txId, cb = warn) { + // promiseToCallback((async () => { + // // approve + // self.setTxStatusApproved(txId) + // // get next nonce + // const txMeta = this.getTx(txId) + // const fromAddress = txMeta.txParams.from + // const { nextNonce, releaseLock } = await this.nonceTracker.getNonceLock(fromAddress) + // txMeta.txParams.nonce = nonce + // this.updateTx(txMeta) + // // sign transaction + // const rawTx = await denodeify(self.signTransaction.bind(self))(txId) + // await denodeify(self.publishTransaction.bind(self))(txId, rawTx) + // })())((err) => { + // if (err) { + // this.setTxStatusFailed(txId, { + // errCode: err.errCode || err, + // message: err.message || 'Transaction failed during approval', + // }) + // } + // // must set transaction to submitted/failed before releasing lock + // releaseLock() + // cb(err) + // }) + // } + + async approveTransaction (txId) { + let nonceLock + try { + // approve + this.setTxStatusApproved(txId) + // get next nonce + const txMeta = this.getTx(txId) + const fromAddress = txMeta.txParams.from + nonceLock = await this.nonceTracker.getNonceLock(fromAddress) + txMeta.txParams.nonce = nonceLock.nextNonce + this.updateTx(txMeta) + // sign transaction + const rawTx = await denodeify(this.signTransaction.bind(this))(txId) + await denodeify(this.publishTransaction.bind(this))(txId, rawTx) + // must set transaction to submitted/failed before releasing lock + nonceLock.releaseLock() + } catch (err) { + this.setTxStatusFailed(txId, { + errCode: err.errCode || err, + message: err.message || 'Transaction failed during approval', }) - }) + // must set transaction to submitted/failed before releasing lock + if (nonceLock) nonceLock.releaseLock() + // continue with error chain + throw err + } } cancelTransaction (txId, cb = warn) { @@ -199,15 +234,6 @@ module.exports = class TransactionController extends EventEmitter { cb() } - fillInTxParams (txId, cb) { - const txMeta = this.getTx(txId) - this.txProviderUtils.fillInTxParams(txMeta.txParams, (err) => { - if (err) return cb(err) - this.updateTx(txMeta) - cb() - }) - } - getChainId () { const networkState = this.networkStore.getState() const getChainId = parseInt(networkState) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index a7eb3d056..006a32eac 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -290,7 +290,7 @@ module.exports = class MetamaskController extends EventEmitter { exportAccount: nodeify(keyringController.exportAccount).bind(keyringController), // txController - approveTransaction: txController.approveTransaction.bind(txController), + approveTransaction: nodeify(txController.approveTransaction).bind(txController), cancelTransaction: txController.cancelTransaction.bind(txController), updateAndApproveTransaction: this.updateAndApproveTx.bind(this), diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index f0d8a706e..7c8d1761d 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -1,5 +1,4 @@ const assert = require('assert') -const EventEmitter = require('events') const ethUtil = require('ethereumjs-util') const EthTx = require('ethereumjs-tx') const EthQuery = require('eth-query') @@ -19,13 +18,15 @@ describe('Transaction Controller', function () { txController = new TransactionController({ networkStore: new ObservableStore(currentNetworkId), txHistoryLimit: 10, - blockTracker: new EventEmitter(), - ethQuery: new EthQuery(new EventEmitter()), + blockTracker: { getCurrentBlock: noop, on: noop }, + provider: { sendAsync: noop }, + ethQuery: new EthQuery({ sendAsync: noop }), signTransaction: (ethTx) => new Promise((resolve) => { ethTx.sign(privKey) resolve() }), }) + txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop }) }) describe('#validateTxParams', function () { -- cgit v1.2.3 From f87c5f8a14fb892325735ed6377ed5b9464fe512 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 15 Jun 2017 13:31:37 -0700 Subject: Remove log --- ui/app/add-token.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/app/add-token.js b/ui/app/add-token.js index 5356b6a0b..cd47709ab 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -80,7 +80,6 @@ AddTokenScreen.prototype.render = function () { event.preventDefault() var tokenSymbolEl = document.querySelector('input#token_symbol') var tokenSymbol = tokenSymbolEl.value - console.log(tokenSymbol) }, }, 'Add'), ]), -- cgit v1.2.3 From 2e5deef2b0bdaaa67ee1584da52b7001cc9e849b Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 15 Jun 2017 13:48:48 -0700 Subject: check nonce and balance when resubmiting tx --- app/scripts/controllers/transactions.js | 27 ++++++++++++++++----------- app/scripts/metamask-controller.js | 1 + test/unit/tx-controller-test.js | 1 + 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 2769a8d59..58dc8a6ab 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -25,10 +25,10 @@ module.exports = class TransactionController extends EventEmitter { this.query = opts.ethQuery this.txProviderUtils = new TxProviderUtil(this.query) this.blockTracker.on('block', this.checkForTxInBlock.bind(this)) - this.blockTracker.on('latest', this.resubmitPendingTxs.bind(this)) + this.provider._blockTracker.on('latest', this.resubmitPendingTxs.bind(this)) this.signEthTx = opts.signTransaction this.nonceLock = Semaphore(1) - + this.ethStore = opts.ethStore // memstore is computed from a few different stores this._updateMemstore() this.store.subscribe(() => this._updateMemstore()) @@ -411,26 +411,31 @@ module.exports = class TransactionController extends EventEmitter { const pending = this.getTxsByMetaData('status', 'submitted') // only try resubmitting if their are transactions to resubmit if (!pending.length) return - const resubmit = denodeify(this.resubmitTx.bind(this)) + const resubmit = denodeify(this._resubmitTx.bind(this)) Promise.all(pending.map(txMeta => resubmit(txMeta))) .catch((reason) => { log.info('Problem resubmitting tx', reason) }) } - resubmitTx (txMeta, cb) { - // Increment a try counter. - if (!('retryCount' in txMeta)) { - txMeta.retryCount = 0 - } + _resubmitTx (txMeta, cb) { + const address = txMeta.txParams.from + const balance = this.ethStore.getState().accounts[address].balance + const nonce = Number.parseInt(this.ethStore.getState().accounts[address].nonce) + const txNonce = Number.parseInt(txMeta.txParams.nonce) + const gtBalance = Number.parseInt(txMeta.txParams.value) > Number.parseInt(balance) + if (!('retryCount' in txMeta)) txMeta.retryCount = 0 + // if the value of the transaction is greater then the balance + // or the nonce of the transaction is lower then the accounts nonce + // dont resubmit the tx + if (gtBalance || txNonce < nonce) return cb() // Only auto-submit already-signed txs: - if (!('rawTx' in txMeta)) { - return cb() - } + if (!('rawTx' in txMeta)) return cb() if (txMeta.retryCount > RETRY_LIMIT) return + // Increment a try counter. txMeta.retryCount++ const rawTx = txMeta.rawTx this.txProviderUtils.publishTransaction(rawTx, cb) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index a7eb3d056..727c19fb7 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -98,6 +98,7 @@ module.exports = class MetamaskController extends EventEmitter { provider: this.provider, blockTracker: this.provider, ethQuery: this.ethQuery, + ethStore: this.ethStore, }) // notices diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index f0d8a706e..702bdd03d 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -19,6 +19,7 @@ describe('Transaction Controller', function () { txController = new TransactionController({ networkStore: new ObservableStore(currentNetworkId), txHistoryLimit: 10, + provider: { _blockTracker: new EventEmitter() }, blockTracker: new EventEmitter(), ethQuery: new EthQuery(new EventEmitter()), signTransaction: (ethTx) => new Promise((resolve) => { -- cgit v1.2.3 From 27b874f2c48fd1cb9dc0984646cb739173ddaf2c Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 15 Jun 2017 14:08:07 -0700 Subject: transactions controller - add comments --- app/scripts/controllers/transactions.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 41f651458..aa168b736 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -26,6 +26,7 @@ module.exports = class TransactionController extends EventEmitter { this.txProviderUtils = new TxProviderUtil(this.query) this.blockTracker.on('block', this.checkForTxInBlock.bind(this)) this.blockTracker.on('block', this.resubmitPendingTxs.bind(this)) + // provider-engine only exploses the 'block' event, not 'latest' for 'sync' this.provider._blockTracker.on('sync', this.queryPendingTxs.bind(this)) this.signEthTx = opts.signTransaction this.nonceLock = Semaphore(1) @@ -371,10 +372,12 @@ module.exports = class TransactionController extends EventEmitter { } queryPendingTxs ({oldBlock, newBlock}) { + // check pending transactions on start if (!oldBlock) { this._checkPendingTxs() return } + // if we synced by more than one block, check for missed pending transactions const diff = Number.parseInt(newBlock.number) - Number.parseInt(oldBlock.number) if (diff > 1) this._checkPendingTxs() } @@ -453,6 +456,8 @@ module.exports = class TransactionController extends EventEmitter { this.txProviderUtils.publishTransaction(rawTx, cb) } + // checks the network for signed txs and + // if confirmed sets the tx status as 'confirmed' _checkPendingTxs () { var signedTxList = this.getFilteredTxList({status: 'submitted'}) if (!signedTxList.length) return -- cgit v1.2.3 From b67bc7043ee231bb9ed4781aa4ac29d4e3107481 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 15 Jun 2017 15:25:22 -0700 Subject: Fix test to call done --- test/unit/tx-controller-test.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index 7c8d1761d..908b060d4 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -270,26 +270,28 @@ describe('Transaction Controller', function () { it('does not overwrite set values', function (done) { + this.timeout(15000) const wrongValue = '0x05' txController.addTx(txMeta) const estimateStub = sinon.stub(txController.txProviderUtils.query, 'estimateGas') - .callsArgWith(1, null, wrongValue) + .callsArgWithAsync(1, null, wrongValue) const priceStub = sinon.stub(txController.txProviderUtils.query, 'gasPrice') - .callsArgWith(0, null, wrongValue) + .callsArgWithAsync(0, null, wrongValue) const nonceStub = sinon.stub(txController.txProviderUtils.query, 'getTransactionCount') - .callsArgWith(2, null, wrongValue) + .callsArgWithAsync(2, null, wrongValue) const signStub = sinon.stub(txController, 'signTransaction') - .callsArgWith(1, null, noop) + .callsArgWithAsync(1, null, noop) const pubStub = sinon.stub(txController.txProviderUtils, 'publishTransaction') - .callsArgWith(1, null, originalValue) + .callsArgWithAsync(1, null, originalValue) + console.log('HERE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') - txController.approveTransaction(txMeta.id, (err) => { + txController.approveTransaction(txMeta.id).then((err) => { assert.ifError(err, 'should not error') const result = txController.getTx(txMeta.id) @@ -305,7 +307,6 @@ describe('Transaction Controller', function () { signStub.restore() nonceStub.restore() pubStub.restore() - done() }) }) -- cgit v1.2.3 From 711a4def86d9e8c626ee554a976d9eab6abdcbee Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 15 Jun 2017 15:38:23 -0700 Subject: Make add token screen a fully working form Entering the address of a valid HumanStandardToken even auto-fills the other fields! --- ui/app/add-token.js | 152 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 141 insertions(+), 11 deletions(-) diff --git a/ui/app/add-token.js b/ui/app/add-token.js index cd47709ab..fdbb5fe53 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -4,21 +4,37 @@ const h = require('react-hyperscript') const connect = require('react-redux').connect const actions = require('./actions') +const ethUtil = require('ethereumjs-util') +const abi = require('human-standard-token-abi') +const Eth = require('ethjs-query') +const EthContract = require('ethjs-contract') + +const emptyAddr = '0x0000000000000000000000000000000000000000' + module.exports = connect(mapStateToProps)(AddTokenScreen) function mapStateToProps (state) { - return {} + return { + } } inherits(AddTokenScreen, Component) function AddTokenScreen () { - this.state = { warning: null } + this.state = { + warning: null, + address: null, + symbol: 'TOKEN', + decimals: 18, + } Component.call(this) } AddTokenScreen.prototype.render = function () { + const props = this.props + const state = this.state - const { warning } = state + const { warning, address, symbol, decimals } = state + return ( h('.flex-column.flex-grow', [ @@ -48,6 +64,26 @@ AddTokenScreen.prototype.render = function () { }, }, [ + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Token Address'), + ]), + + h('section.flex-row.flex-center', [ + h('input#token-address', { + name: 'address', + placeholder: 'Token Address', + onChange: this.tokenAddressDidChange.bind(this), + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + }), + ]), + h('div', [ h('span', { style: { fontWeight: 'bold', paddingRight: '10px'}, @@ -57,17 +93,43 @@ AddTokenScreen.prototype.render = function () { h('div', { style: {display: 'flex'} }, [ h('input#token_symbol', { placeholder: `Like "ETH"`, + value: symbol, style: { width: 'inherit', flex: '1 0 auto', height: '30px', margin: '8px', }, - onKeyPress (event) { - if (event.key === 'Enter') { - var element = event.target - var newRpc = element.value - } + onChange: (event) => { + var element = event.target + var symbol = element.value + this.setState({ symbol }) + }, + }), + ]), + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Decimals of Precision'), + ]), + + h('div', { style: {display: 'flex'} }, [ + h('input#token_decimals', { + value: decimals, + type: 'number', + min: 0, + max: 36, + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onChange: (event) => { + var element = event.target + var decimals = element.value.trim() + this.setState({ decimals }) }, }), ]), @@ -77,9 +139,11 @@ AddTokenScreen.prototype.render = function () { alignSelf: 'center', }, onClick (event) { - event.preventDefault() - var tokenSymbolEl = document.querySelector('input#token_symbol') - var tokenSymbol = tokenSymbolEl.value + const valid = this.validateInputs() + if (!valid) return + + const { address, symbol, decimals } = state + this.props.dispatch(addToken(address.trim(), symbol.trim(), decimals)) }, }, 'Add'), ]), @@ -88,3 +152,69 @@ AddTokenScreen.prototype.render = function () { ) } +AddTokenScreen.prototype.componentWillMount = function () { + if (typeof global.ethereumProvider === 'undefined') return + + this.eth = new Eth(global.ethereumProvider) + this.contract = new EthContract(this.eth) + this.TokenContract = this.contract(abi) +} + +AddTokenScreen.prototype.tokenAddressDidChange = function (event) { + const el = event.target + const address = el.value.trim() + if (ethUtil.isValidAddress(address) && address !== emptyAddr) { + this.setState({ address }) + this.attemptToAutoFillTokenParams(address) + } +} + +AddTokenScreen.prototype.validateInputs = function () { + let msg = '' + const state = this.state + const { address, symbol, decimals } = state + + const validAddress = ethUtil.isValidAddress(address) + if (!validAddress) { + msg += 'Address is invalid. ' + } + + const validDecimals = decimals >= 0 && decimals < 36 + if (!validDecimals) { + msg += 'Decimals must be at least 0, and not over 36. ' + } + + const symbolLen = symbol.trim().length + const validSymbol = symbolLen > 0 && symbolLen < 10 + if (!validSymbol) { + msg += 'Symbol must be between 0 and 10 characters.' + } + + const isValid = validAddress && validDecimals + + if (!isValid) { + this.setState({ + warning: msg, + }) + } else { + this.setState({ warning: null }) + } + + return isValid +} + +AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) { + const contract = this.TokenContract.at(address) + + const results = await Promise.all([ + contract.symbol(), + contract.decimals(), + ]) + + const [ symbol, decimals ] = results + if (symbol && decimals) { + console.log('SETTING SYMBOL AND DECIMALS', { symbol, decimals }) + this.setState({ symbol: symbol[0], decimals: decimals[0].toString() }) + } +} + -- cgit v1.2.3 From 3062254c5660cc4f3fa887c58f0bba6a6b533ca7 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 15 Jun 2017 16:20:19 -0700 Subject: Do not recommend posting logs publicly on github. This exposes all user accounts, and not all users will want to do this, so we should not recommend this. --- ISSUE_TEMPLATE | 2 -- 1 file changed, 2 deletions(-) diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE index 9c17ec3cd..d0ff3c08e 100644 --- a/ISSUE_TEMPLATE +++ b/ISSUE_TEMPLATE @@ -4,8 +4,6 @@ FAQ: Common questions such as "Where is my ether?" or "Where did my tokens go?" are answered in the FAQ. Bug Reports: - In order to quickly expedite your issue, please follow the directions here and paste the results into your issue. - https://github.com/MetaMask/faq/blob/master/LOGS.md Briefly describe the issue you've encountered * Expected Behavior -- cgit v1.2.3 From 48789f2a3df2c820b61902fb49057f9f7b6cbd8c Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 15 Jun 2017 16:22:53 -0700 Subject: Add ability to add tokens to token list Fiex #1616 --- CHANGELOG.md | 1 + app/scripts/controllers/preferences.js | 29 ++++++++++++++++++++---- app/scripts/metamask-controller.js | 1 + ui/app/account-detail.js | 4 +++- ui/app/actions.js | 14 ++++++++++++ ui/app/add-token.js | 10 ++++----- ui/app/components/token-list.js | 40 +++++++++++++++++++++++++--------- 7 files changed, 78 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37f334d23..567479862 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Current Master - Add list of popular tokens held to the account detail view. +- Add ability to add Tokens to token list. - Add a warning to JSON file import. ## 3.7.8 2017-6-12 diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index aa8e05fcc..e45224593 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -8,13 +8,11 @@ class PreferencesController { const initState = extend({ frequentRpcList: [], currentAccountTab: 'history', + tokens: [], }, opts.initState) this.store = new ObservableStore(initState) } - - // - // PUBLIC METHODS - // +// PUBLIC METHODS setSelectedAddress (_address) { return new Promise((resolve, reject) => { @@ -28,6 +26,29 @@ class PreferencesController { return this.store.getState().selectedAddress } + addToken (rawAddress, symbol, decimals) { + const address = normalizeAddress(rawAddress) + const newEntry = { address, symbol, decimals } + + const tokens = this.store.getState().tokens + const previousIndex = tokens.find((token, index) => { + return token.address === address + }) + + if (previousIndex) { + tokens[previousIndex] = newEntry + } else { + tokens.push(newEntry) + } + + this.store.updateState({ tokens }) + return Promise.resolve() + } + + getTokens () { + return this.store.getState().tokens + } + updateFrequentRpcList (_url) { return this.addToFrequentRpcList(_url) .then((rpcList) => { diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 410693df4..e4267381d 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -275,6 +275,7 @@ module.exports = class MetamaskController extends EventEmitter { // PreferencesController setSelectedAddress: nodeify(preferencesController.setSelectedAddress).bind(preferencesController), + addToken: nodeify(preferencesController.addToken).bind(preferencesController), setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab).bind(preferencesController), setDefaultRpc: nodeify(this.setDefaultRpc).bind(this), setCustomRpc: nodeify(this.setCustomRpc).bind(this), diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index 19f2cba59..bed05a7fb 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -35,6 +35,7 @@ function mapStateToProps (state) { conversionRate: state.metamask.conversionRate, currentCurrency: state.metamask.currentCurrency, currentAccountTab: state.metamask.currentAccountTab, + tokens: state.metamask.tokens, } } @@ -273,13 +274,14 @@ AccountDetailScreen.prototype.tabSections = function () { AccountDetailScreen.prototype.tabSwitchView = function () { const props = this.props const { address, network } = props - const { currentAccountTab } = this.props + const { currentAccountTab, tokens } = this.props switch (currentAccountTab) { case 'tokens': return h(TokenList, { userAddress: address, network, + tokens, addToken: () => this.props.dispatch(actions.showAddTokenPage()), }) default: diff --git a/ui/app/actions.js b/ui/app/actions.js index d17d4610e..6ff28f32f 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -124,6 +124,7 @@ var actions = { showConfigPage, SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', showAddTokenPage, + addToken, setRpcTarget: setRpcTarget, setDefaultRpcTarget: setDefaultRpcTarget, setProviderType: setProviderType, @@ -636,6 +637,19 @@ function showAddTokenPage (transitionForward = true) { } } +function addToken (address, symbol, decimals) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + background.addToken(address, symbol, decimals, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch(actions.goHome()) + }) + } +} + function goBackToInitView () { return { type: actions.BACK_TO_INIT_MENU, diff --git a/ui/app/add-token.js b/ui/app/add-token.js index fdbb5fe53..025cfacb5 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -30,10 +30,8 @@ function AddTokenScreen () { } AddTokenScreen.prototype.render = function () { - const props = this.props - const state = this.state - const { warning, address, symbol, decimals } = state + const { warning, symbol, decimals } = state return ( h('.flex-column.flex-grow', [ @@ -138,12 +136,12 @@ AddTokenScreen.prototype.render = function () { style: { alignSelf: 'center', }, - onClick (event) { + onClick: (event) => { const valid = this.validateInputs() if (!valid) return - const { address, symbol, decimals } = state - this.props.dispatch(addToken(address.trim(), symbol.trim(), decimals)) + const { address, symbol, decimals } = this.state + this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) }, }, 'Add'), ]), diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index d230ce74a..100e596ed 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -4,13 +4,14 @@ const inherits = require('util').inherits const TokenTracker = require('eth-token-tracker') const TokenCell = require('./token-cell.js') const contracts = require('eth-contract-metadata') +const normalizeAddress = require('eth-sig-util').normalize -const tokens = [] +const defaultTokens = [] for (const address in contracts) { const contract = contracts[address] if (contract.erc20) { contract.address = address - tokens.push(contract) + defaultTokens.push(contract) } } @@ -18,22 +19,23 @@ module.exports = TokenList inherits(TokenList, Component) function TokenList () { - this.state = { tokens, isLoading: true, network: null } + this.state = { + tokens: null, + isLoading: true, + network: null, + } Component.call(this) } TokenList.prototype.render = function () { const state = this.state - const { tokens, isLoading } = state - - const { userAddress } = this.props + const { isLoading, tokens } = state + const { userAddress, network } = this.props if (isLoading) { return this.message('Loading') } - const network = this.props.network - const tokenViews = tokens.map((tokenData) => { tokenData.network = network tokenData.userAddress = userAddress @@ -120,7 +122,7 @@ TokenList.prototype.createFreshTokenTracker = function () { this.tracker = new TokenTracker({ userAddress, provider: global.ethereumProvider, - tokens: tokens, + tokens: uniqueMergeTokens(defaultTokens, this.props.tokens), pollingInterval: 8000, }) @@ -149,7 +151,12 @@ TokenList.prototype.componentWillUpdate = function (nextProps) { } TokenList.prototype.updateBalances = function (tokenData) { - const heldTokens = tokenData.filter(token => token.balance !== '0' && token.string !== '0.000') + const desired = this.props.tokens.map(token => token.address) + const heldTokens = tokenData.filter(token => { + const held = token.balance !== '0' && token.string !== '0.000' + const preferred = desired.includes(normalizeAddress(token.address)) + return held || preferred + }) this.setState({ tokens: heldTokens, isLoading: false }) } @@ -158,3 +165,16 @@ TokenList.prototype.componentWillUnmount = function () { this.tracker.stop() } +function uniqueMergeTokens (tokensA, tokensB) { + const uniqueAddresses = [] + const result = [] + tokensA.concat(tokensB).forEach((token) => { + const normal = normalizeAddress(token.address) + if (!uniqueAddresses.includes(normal)) { + uniqueAddresses.push(normal) + result.push(token) + } + }) + return result +} + -- cgit v1.2.3 From 06f6aa7a00f57004746d1e21759ac56396d9b855 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 15 Jun 2017 18:00:24 -0700 Subject: Debounce background updates Our background sometimes emits absurd quantities of updates very quickly. This PR reduces the amount of inter-process traffic by ensuring the `sendUpdate` method does not fire more than every 200 ms. Fixes #1621 --- app/scripts/metamask-controller.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index a7eb3d056..d745d29dc 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -23,6 +23,7 @@ const autoFaucet = require('./lib/auto-faucet') const nodeify = require('./lib/nodeify') const accountImporter = require('./account-import-strategies') const getBuyEthUrl = require('./lib/buy-eth-url') +const debounce = require('debounce') const version = require('../manifest.json').version @@ -30,6 +31,9 @@ module.exports = class MetamaskController extends EventEmitter { constructor (opts) { super() + + this.sendUpdate = debounce(this.privateSendUpdate.bind(this), 200) + this.opts = opts const initState = opts.initState || {} @@ -354,7 +358,7 @@ module.exports = class MetamaskController extends EventEmitter { ) } - sendUpdate () { + privateSendUpdate () { this.emit('update', this.getState()) } -- cgit v1.2.3 From 3e4f2cf3d3c13c876f7ba1d77e8075ae973b1755 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Fri, 16 Jun 2017 16:34:38 -0700 Subject: bump provider engine --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7ee5dc5be..3cace3250 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "valid-url": "^1.0.9", "vreme": "^3.0.2", "web3": "0.18.2", - "web3-provider-engine": "^13.0.1", + "web3-provider-engine": "^13.0.3", "web3-stream-provider": "^2.0.6", "xtend": "^4.0.1" }, -- cgit v1.2.3 From 5f8e74e0aa7eed37fc86a84f1495cc65030e9136 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Fri, 16 Jun 2017 16:36:32 -0700 Subject: put the block listeners back on the provider --- app/scripts/controllers/transactions.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 9c20a7f1a..d56a81150 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -24,10 +24,9 @@ module.exports = class TransactionController extends EventEmitter { this.blockTracker = opts.blockTracker this.query = opts.ethQuery this.txProviderUtils = new TxProviderUtil(this.query) - this.blockTracker.on('block', this.checkForTxInBlock.bind(this)) + this.blockTracker.on('rawBlock', this.checkForTxInBlock.bind(this)) this.blockTracker.on('block', this.resubmitPendingTxs.bind(this)) - // provider-engine only exploses the 'block' event, not 'latest' for 'sync' - this.provider._blockTracker.on('sync', this.queryPendingTxs.bind(this)) + this.blockTracker.on('sync', this.queryPendingTxs.bind(this)) this.signEthTx = opts.signTransaction this.nonceLock = Semaphore(1) -- cgit v1.2.3 From 9c2ead3d528046e4a6eed5bc45e82c972000354d Mon Sep 17 00:00:00 2001 From: frankiebee Date: Fri, 16 Jun 2017 16:43:38 -0700 Subject: put event back on the "blockTracker:/provider" --- app/scripts/controllers/transactions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index f6eb4e4d9..042d8e66d 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -27,7 +27,7 @@ module.exports = class TransactionController extends EventEmitter { this.blockTracker.on('block', this.checkForTxInBlock.bind(this)) // provider-engine only exploses the 'block' event, not 'latest' for 'sync' this.provider._blockTracker.on('sync', this.queryPendingTxs.bind(this)) - this.provider._blockTracker.on('latest', this.resubmitPendingTxs.bind(this)) + this.blockTracker.on('latest', this.resubmitPendingTxs.bind(this)) this.signEthTx = opts.signTransaction this.nonceLock = Semaphore(1) this.ethStore = opts.ethStore -- cgit v1.2.3 From e672f2da0d74bc1e001acb35be0345e49663463e Mon Sep 17 00:00:00 2001 From: frankiebee Date: Fri, 16 Jun 2017 17:04:56 -0700 Subject: remove irrelevant test --- test/unit/tx-controller-test.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index 908b060d4..8ce6a5a65 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -281,15 +281,12 @@ describe('Transaction Controller', function () { const priceStub = sinon.stub(txController.txProviderUtils.query, 'gasPrice') .callsArgWithAsync(0, null, wrongValue) - const nonceStub = sinon.stub(txController.txProviderUtils.query, 'getTransactionCount') - .callsArgWithAsync(2, null, wrongValue) const signStub = sinon.stub(txController, 'signTransaction') .callsArgWithAsync(1, null, noop) const pubStub = sinon.stub(txController.txProviderUtils, 'publishTransaction') .callsArgWithAsync(1, null, originalValue) - console.log('HERE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') txController.approveTransaction(txMeta.id).then((err) => { assert.ifError(err, 'should not error') @@ -299,13 +296,11 @@ describe('Transaction Controller', function () { assert.equal(params.gas, originalValue, 'gas unmodified') assert.equal(params.gasPrice, originalValue, 'gas price unmodified') - assert.equal(params.nonce, originalValue, 'nonce unmodified') assert.equal(result.hash, originalValue, 'hash was set') estimateStub.restore() priceStub.restore() signStub.restore() - nonceStub.restore() pubStub.restore() done() }) -- cgit v1.2.3 From 88844946fe9baca1c057667e1ba27a5ca54b0d2f Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Sat, 17 Jun 2017 12:14:35 -0700 Subject: Remove second linting script for tests --- test/unit/linting_test.js | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 test/unit/linting_test.js diff --git a/test/unit/linting_test.js b/test/unit/linting_test.js deleted file mode 100644 index 45578fc36..000000000 --- a/test/unit/linting_test.js +++ /dev/null @@ -1,9 +0,0 @@ -// LINTING: -const lint = require('mocha-eslint') -const lintPaths = ['app/**/*.js', 'ui/**/*.js', 'test/**/*.js', '!node_modules/**', '!dist/**', '!docs/**', '!app/scripts/chromereload.js'] - -const lintOptions = { - strict: false, -} - -lint(lintPaths, lintOptions) -- cgit v1.2.3 From 1c05c82867f2162db1b1d28be24a051161661517 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 19 Jun 2017 15:22:58 -0700 Subject: Add MetaMark support --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7f0089d8a..f8f77aaa3 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "end-of-stream": "^1.1.0", "ensnare": "^1.0.0", "eth-bin-to-ops": "^1.0.1", - "eth-contract-metadata": "^1.1.1", + "eth-contract-metadata": "^1.1.3", "eth-hd-keyring": "^1.1.1", "eth-query": "^2.1.1", "eth-sig-util": "^1.1.1", -- cgit v1.2.3 From 235cb1f2d790a7bda349ab0d33ad1009751a8536 Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Mon, 19 Jun 2017 17:50:06 -0700 Subject: Keeps dapp gas price if set --- app/scripts/controllers/transactions.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index d9d9849b1..e3c2d74d3 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -152,13 +152,15 @@ module.exports = class TransactionController extends EventEmitter { const txParams = txMeta.txParams // ensure value txParams.value = txParams.value || '0x0' - this.query.gasPrice((err, gasPrice) => { - if (err) return cb(err) - // set gasPrice - txParams.gasPrice = gasPrice - // set gasLimit - this.txProviderUtils.analyzeGasUsage(txMeta, cb) - }) + if (!txParams.gasPrice) { + this.query.gasPrice((err, gasPrice) => { + if (err) return cb(err) + // set gasPrice + txParams.gasPrice = gasPrice + }) + } + // set gasLimit + this.txProviderUtils.analyzeGasUsage(txMeta, cb) } getUnapprovedTxList () { -- cgit v1.2.3 From 60855b05106899149824feecbd0f5d54907b0451 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 19 Jun 2017 16:12:34 -0700 Subject: Add send button to TokenFactory A simple solution to a temporary token send screen: Linking to EtherScan. Will hold us over until we make our own token send view. --- CHANGELOG.md | 1 + ui/app/components/token-cell.js | 36 +++++++++++++++++++++++++++++------- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 567479862..6d22da332 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Add list of popular tokens held to the account detail view. - Add ability to add Tokens to token list. - Add a warning to JSON file import. +- Add "send" link to token list, which goes to TokenFactory. ## 3.7.8 2017-6-12 diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js index d3a895d36..1b226983b 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/token-cell.js @@ -17,12 +17,7 @@ TokenCell.prototype.render = function () { return ( h('li.token-cell', { style: { cursor: network === '1' ? 'pointer' : 'default' }, - onClick: (event) => { - const url = urlFor(address, userAddress, network) - if (url) { - navigateTo(url) - } - }, + onClick: this.view.bind(this, address, userAddress, network), }, [ h(Identicon, { @@ -32,15 +27,42 @@ TokenCell.prototype.render = function () { }), h('h3', `${string || 0} ${symbol}`), + + h('span', { style: { flex: '1 0 auto' } }), + + h('button', { + onClick: this.send.bind(this, address), + }, 'SEND'), + ]) ) } +TokenCell.prototype.send = function (address, event) { + event.preventDefault() + event.stopPropagation + const url = tokenFactoryFor(address) + if (url) { + navigateTo(url) + } +} + +TokenCell.prototype.view = function (address, userAddress, network, event) { + const url = etherscanLinkFor(address, userAddress, network) + if (url) { + navigateTo(url) + } +} + function navigateTo (url) { global.platform.openWindow({ url }) } -function urlFor (tokenAddress, address, network) { +function etherscanLinkFor (tokenAddress, address, network) { return `https://etherscan.io/token/${tokenAddress}?a=${address}` } +function tokenFactoryFor (tokenAddress) { + return `https://tokenfactory.surge.sh/#/token/${tokenAddress}` +} + -- cgit v1.2.3 From 0799e5edf5b508f588af93431db0df3bd7e6c27d Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 19 Jun 2017 19:02:38 -0700 Subject: Fix token balance rendering --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b0024e901..01c256ef3 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "eth-query": "^2.1.2", "eth-sig-util": "^1.1.1", "eth-simple-keyring": "^1.1.1", - "eth-token-tracker": "^1.0.8", + "eth-token-tracker": "^1.0.9", "ethereumjs-tx": "^1.3.0", "ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", -- cgit v1.2.3 From a2781df8b4ce8d1fc03080eb3361217a236ec82d Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 19 Jun 2017 19:11:55 -0700 Subject: Add better event lifecycle management to token list. Token list now renders errors when a token lookup fails. Also now cleans up event listeners when re-initializing the token list. --- ui/app/components/token-list.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index c560a6072..633d3ccfe 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -24,7 +24,7 @@ function TokenList () { TokenList.prototype.render = function () { const state = this.state - const { tokens, isLoading } = state + const { tokens, isLoading, error } = state const { userAddress } = this.props @@ -32,6 +32,11 @@ TokenList.prototype.render = function () { return this.message('Loading') } + if (error) { + log.error(error) + return this.message('There was a problem loading your token balances.') + } + const network = this.props.network const tokenViews = tokens.map((tokenData) => { @@ -85,7 +90,10 @@ TokenList.prototype.componentDidMount = function () { TokenList.prototype.createFreshTokenTracker = function () { if (this.tracker) { + // Clean up old trackers when refreshing: this.tracker.stop() + this.tracker.removeListener('update', this.balanceUpdater) + this.tracker.removeListener('error', this.showError) } if (!global.ethereumProvider) return @@ -97,9 +105,15 @@ TokenList.prototype.createFreshTokenTracker = function () { pollingInterval: 8000, }) - this.tracker.on('update', (tokenData) => { - this.updateBalances(tokenData) - }) + + // Set up listener instances for cleaning up + this.balanceUpdater = this.updateBalances.bind(this) + this.showError = (error) => { + this.setState({ error, isLoading: false }) + } + this.tracker.on('update', this.balanceUpdater) + this.tracker.on('error', this.showError) + this.tracker.updateBalances() .then(() => { this.updateBalances(this.tracker.serialize()) -- cgit v1.2.3 From 97ab48ba0d5c0699b4ec0ad4bbc3d9c8805ef048 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 20 Jun 2017 08:01:00 -0700 Subject: Fix propagation --- ui/app/components/token-cell.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js index 1b226983b..67558ad87 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/token-cell.js @@ -40,7 +40,7 @@ TokenCell.prototype.render = function () { TokenCell.prototype.send = function (address, event) { event.preventDefault() - event.stopPropagation + event.stopPropagation() const url = tokenFactoryFor(address) if (url) { navigateTo(url) -- cgit v1.2.3 From 027394b2058b31daa399c582c82f0c0b01571144 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 20 Jun 2017 08:58:25 -0700 Subject: Reduce token list clutter by only showing held tokens We could change this when we allow hiding/removing tokens, but for now, this is a simple and pleasant solution. --- ui/app/components/token-list.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index 100e596ed..bc41c5270 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -20,7 +20,7 @@ module.exports = TokenList inherits(TokenList, Component) function TokenList () { this.state = { - tokens: null, + tokens: [], isLoading: true, network: null, } @@ -150,12 +150,9 @@ TokenList.prototype.componentWillUpdate = function (nextProps) { } } -TokenList.prototype.updateBalances = function (tokenData) { - const desired = this.props.tokens.map(token => token.address) - const heldTokens = tokenData.filter(token => { - const held = token.balance !== '0' && token.string !== '0.000' - const preferred = desired.includes(normalizeAddress(token.address)) - return held || preferred +TokenList.prototype.updateBalances = function (tokens) { + const heldTokens = tokens.filter(token => { + return token.balance !== '0' && token.string !== '0.000' }) this.setState({ tokens: heldTokens, isLoading: false }) } -- cgit v1.2.3 From c7780727eb26cd35806f36aaf91cfb6865dd9693 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 20 Jun 2017 09:01:55 -0700 Subject: Bump circleCI node version to 8.0.0 --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 4305ca3b4..1f018ac24 100644 --- a/circle.yml +++ b/circle.yml @@ -1,6 +1,6 @@ machine: node: - version: 7.6.0 + version: 8.0.0 dependencies: pre: - "npm i -g testem" -- cgit v1.2.3 From 044c16219bff7c583b57edeee913dd576930031e Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Tue, 20 Jun 2017 15:38:23 -0700 Subject: Fix badge number to include personal_sign --- app/scripts/background.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index 1dbfb1b98..e8987394f 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -116,13 +116,15 @@ function setupController (initState) { updateBadge() controller.txController.on('updateBadge', updateBadge) controller.messageManager.on('updateBadge', updateBadge) + controller.personalMessageManager.on('updateBadge', updateBadge) // plugin badge text function updateBadge () { var label = '' var unapprovedTxCount = controller.txController.unapprovedTxCount var unapprovedMsgCount = controller.messageManager.unapprovedMsgCount - var count = unapprovedTxCount + unapprovedMsgCount + var unapprovedPersonalMsgs = controller.personalMessageManager.unapprovedPersonalMsgCount + var count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgs if (count) { label = String(count) } -- cgit v1.2.3 From b3a6df2dca7b5b9fef883fded020c6bd7a7320fa Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Tue, 20 Jun 2017 15:41:34 -0700 Subject: Bump changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d3e86342..c040b630c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Add a warning to JSON file import. - Fix bug where slowly mined txs would sometimes be incorrectly marked as failed. +- Fix bug where badge count did not reflect personal_sign pending messages. ## 3.7.8 2017-6-12 -- cgit v1.2.3 From d0294720e22c3c616b3fb049ae22a69dcbe8620f Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 20 Jun 2017 18:23:49 -0700 Subject: Make seed confirmation wording scarier --- ui/app/keychains/hd/create-vault-complete.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/keychains/hd/create-vault-complete.js b/ui/app/keychains/hd/create-vault-complete.js index 5230797ad..9741155f7 100644 --- a/ui/app/keychains/hd/create-vault-complete.js +++ b/ui/app/keychains/hd/create-vault-complete.js @@ -54,7 +54,7 @@ CreateVaultCompleteScreen.prototype.render = function () { textAlign: 'center', }, }, [ - h('span.error', 'These 12 words can restore all of your MetaMask accounts for this vault.\nSave them somewhere safe and secret.'), + h('span.error', 'These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret.'), ]), h('textarea.twelve-word-phrase', { -- cgit v1.2.3 From 2f4ebea30654c7c18f601e3136914d74edfdeca0 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 20 Jun 2017 18:26:08 -0700 Subject: Bump changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c040b630c..8200c0d3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Add a warning to JSON file import. - Fix bug where slowly mined txs would sometimes be incorrectly marked as failed. - Fix bug where badge count did not reflect personal_sign pending messages. +- Seed word confirmation wording is now scarier. ## 3.7.8 2017-6-12 -- cgit v1.2.3 From eb7a9d7517777e16f01b45a27a82100ea47bf3aa Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 21 Jun 2017 15:55:22 -0700 Subject: Fix broken malito link. --- ui/app/info.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ui/app/info.js b/ui/app/info.js index aa4503b62..89cb7854d 100644 --- a/ui/app/info.js +++ b/ui/app/info.js @@ -101,14 +101,12 @@ InfoScreen.prototype.render = function () { h('a.info', { href: 'https://github.com/MetaMask/faq', target: '_blank', - onClick (event) { this.navigateTo(event.target.href) }, }, 'Need Help? Read our FAQ!'), ]), h('div', [ h('a', { href: 'https://metamask.io/', target: '_blank', - onClick (event) { this.navigateTo(event.target.href) }, }, [ h('img.icon-size', { src: 'images/icon-128.png', @@ -126,7 +124,6 @@ InfoScreen.prototype.render = function () { h('a.info', { href: 'http://slack.metamask.io', target: '_blank', - onClick (event) { this.navigateTo(event.target.href) }, }, 'Join the conversation on Slack'), ]), @@ -134,7 +131,6 @@ InfoScreen.prototype.render = function () { h('a.info', { href: 'https://twitter.com/metamask_io', target: '_blank', - onClick (event) { this.navigateTo(event.target.href) }, }, 'Follow us on Twitter'), ]), @@ -142,7 +138,7 @@ InfoScreen.prototype.render = function () { h('a.info', { target: '_blank', style: { width: '85vw' }, - onClick () { this.navigateTo('mailto:help@metamask.io?subject=Feedback') }, + href: 'mailto:help@metamask.io?subject=Feedback', }, 'Email us!'), ]), ]), -- cgit v1.2.3 From fa8c74fe9b19229580224815cc131611ee29027c Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 21 Jun 2017 17:28:19 -0700 Subject: add a test for #getNonceLock --- app/scripts/lib/nonce-tracker.js | 22 +++++++++++++------ package.json | 1 + test/unit/nonce-tracker-test.js | 46 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 test/unit/nonce-tracker-test.js diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js index 6e9d094bc..ff2317a91 100644 --- a/app/scripts/lib/nonce-tracker.js +++ b/app/scripts/lib/nonce-tracker.js @@ -1,4 +1,4 @@ -const EthQuery = require('ethjs-query') +const EthQuery = require('eth-query') class NonceTracker { @@ -20,10 +20,10 @@ class NonceTracker { const currentBlock = await this._getCurrentBlock() const blockNumber = currentBlock.number const pendingTransactions = this.getPendingTransactions(address) - const baseCount = await this.ethQuery.getTransactionCount(address, blockNumber) - const nextNonce = baseCount + pendingTransactions + const baseCount = await this._getTxCount(address, blockNumber) + const nextNonce = parseInt(baseCount) + pendingTransactions.length + 1 // return next nonce and release cb - return { nextNonce, releaseLock } + return { nextNonce: nextNonce.toString(16), releaseLock } } async _getCurrentBlock() { @@ -37,13 +37,23 @@ class NonceTracker { _takeLock(lockId) { let releaseLock = null // create and store lock - const lock = new Promise((reject, resolve) => { releaseLock = resolve }) + const lock = new Promise((resolve, reject) => { releaseLock = resolve }) this.lockMap[lockId] = lock // setup lock teardown - lock.then(() => delete this.lockMap[lockId]) + lock.then(() => { + delete this.lockMap[lockId] + }) return releaseLock } + _getTxCount (address, blockNumber) { + return new Promise((resolve, reject) => { + this.ethQuery.getTransactionCount(address, blockNumber, (err, result) => { + err ? reject(err) : resolve(result) + }) + }) + } + } module.exports = NonceTracker diff --git a/package.json b/package.json index 9ed2e7ae0..9085e4565 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "dist": "npm install && gulp dist --disableLiveReload", "test": "npm run lint && npm run test-unit && npm run test-integration", "test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"", + "single-test": "METAMASK_ENV=test mocha --require test/helper.js", "test-integration": "npm run buildMock && npm run buildCiUnits && testem ci -P 2", "lint": "gulp lint", "buildCiUnits": "node test/integration/index.js", diff --git a/test/unit/nonce-tracker-test.js b/test/unit/nonce-tracker-test.js new file mode 100644 index 000000000..5a05d6170 --- /dev/null +++ b/test/unit/nonce-tracker-test.js @@ -0,0 +1,46 @@ +const assert = require('assert') +const NonceTracker = require('../../app/scripts/lib/nonce-tracker') + +describe('Nonce Tracker', function () { + let nonceTracker, provider, getPendingTransactions, pendingTxs + const noop = () => {} + + + beforeEach(function () { + pendingTxs =[{ + 'status': 'submitted', + 'txParams': { + 'from': '0x7d3517b0d011698406d6e0aed8453f0be2697926', + 'gas': '0x30d40', + 'value': '0x0', + 'nonce': '0x1', + }, + }] + + + getPendingTransactions = () => pendingTxs + provider = { sendAsync: (_, cb) => { cb(undefined , {result: '0x0'}) }, } + nonceTracker = new NonceTracker({ + blockTracker: { + getCurrentBlock: () => '0x11b568', + once: (...args) => { + setTimeout(() => { + args.pop()() + }, 5000) + } + }, + provider, + getPendingTransactions, + }) + }) + + describe('#getNonceLock', function () { + it('should work', async function (done) { + this.timeout(15000) + const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926') + assert.equal(nonceLock.nextNonce, '2', 'nonce should be 2') + nonceLock.releaseLock() + done() + }) + }) +}) -- cgit v1.2.3 From 92df9965ebd4a833817c32fd32f7e4533ec7fe19 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 21 Jun 2017 19:51:00 -0700 Subject: fix nonceTracker --- app/scripts/controllers/transactions.js | 51 +++++++++------------------------ app/scripts/lib/nonce-tracker.js | 14 ++++----- test/unit/nonce-tracker-test.js | 18 ++++-------- 3 files changed, 25 insertions(+), 58 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index c2f98e66a..59c8be8b7 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -25,7 +25,7 @@ module.exports = class TransactionController extends EventEmitter { this.blockTracker = opts.blockTracker this.nonceTracker = new NonceTracker({ provider: this.provider, - blockTracker: this.blockTracker, + blockTracker: this.provider._blockTracker, getPendingTransactions: (address) => this.getFilteredTxList({ from: address, status: 'submitted' }), }) this.query = opts.ethQuery @@ -176,33 +176,7 @@ module.exports = class TransactionController extends EventEmitter { }, {}) } - // approveTransaction (txId, cb = warn) { - // promiseToCallback((async () => { - // // approve - // self.setTxStatusApproved(txId) - // // get next nonce - // const txMeta = this.getTx(txId) - // const fromAddress = txMeta.txParams.from - // const { nextNonce, releaseLock } = await this.nonceTracker.getNonceLock(fromAddress) - // txMeta.txParams.nonce = nonce - // this.updateTx(txMeta) - // // sign transaction - // const rawTx = await denodeify(self.signTransaction.bind(self))(txId) - // await denodeify(self.publishTransaction.bind(self))(txId, rawTx) - // })())((err) => { - // if (err) { - // this.setTxStatusFailed(txId, { - // errCode: err.errCode || err, - // message: err.message || 'Transaction failed during approval', - // }) - // } - // // must set transaction to submitted/failed before releasing lock - // releaseLock() - // cb(err) - // }) - // } - - async approveTransaction (txId) { + async approveTransaction (txId, cb = warn) { let nonceLock try { // approve @@ -215,9 +189,10 @@ module.exports = class TransactionController extends EventEmitter { this.updateTx(txMeta) // sign transaction const rawTx = await denodeify(this.signTransaction.bind(this))(txId) - await denodeify(this.publishTransaction.bind(this))(txId, rawTx) + await this.publishTransaction(txId, rawTx) // must set transaction to submitted/failed before releasing lock nonceLock.releaseLock() + cb() } catch (err) { this.setTxStatusFailed(txId, { errCode: err.errCode || err, @@ -226,7 +201,7 @@ module.exports = class TransactionController extends EventEmitter { // must set transaction to submitted/failed before releasing lock if (nonceLock) nonceLock.releaseLock() // continue with error chain - throw err + cb(err) } } @@ -260,16 +235,17 @@ module.exports = class TransactionController extends EventEmitter { }) } - publishTransaction (txId, rawTx, cb = warn) { + publishTransaction (txId, rawTx) { const txMeta = this.getTx(txId) txMeta.rawTx = rawTx this.updateTx(txMeta) - - this.txProviderUtils.publishTransaction(rawTx, (err, txHash) => { - if (err) return cb(err) - this.setTxHash(txId, txHash) - this.setTxStatusSubmitted(txId) - cb() + return new Promise((resolve, reject) => { + this.txProviderUtils.publishTransaction(rawTx, (err, txHash) => { + if (err) reject(err) + this.setTxHash(txId, txHash) + this.setTxStatusSubmitted(txId) + resolve() + }) }) } @@ -414,7 +390,6 @@ module.exports = class TransactionController extends EventEmitter { this.emit(`${txMeta.id}:${status}`, txId) if (status === 'submitted' || status === 'rejected') { this.emit(`${txMeta.id}:finished`, txMeta) - } this.updateTx(txMeta) this.emit('updateBadge') diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js index ff2317a91..4ea511dec 100644 --- a/app/scripts/lib/nonce-tracker.js +++ b/app/scripts/lib/nonce-tracker.js @@ -2,7 +2,7 @@ const EthQuery = require('eth-query') class NonceTracker { - constructor({ blockTracker, provider, getPendingTransactions }) { + constructor ({ blockTracker, provider, getPendingTransactions }) { this.blockTracker = blockTracker this.ethQuery = new EthQuery(provider) this.getPendingTransactions = getPendingTransactions @@ -11,7 +11,7 @@ class NonceTracker { // releaseLock must be called // releaseLock must be called after adding signed tx to pending transactions (or discarding) - async getNonceLock(address) { + async getNonceLock (address) { // await lock free await this.lockMap[address] // take lock @@ -21,12 +21,12 @@ class NonceTracker { const blockNumber = currentBlock.number const pendingTransactions = this.getPendingTransactions(address) const baseCount = await this._getTxCount(address, blockNumber) - const nextNonce = parseInt(baseCount) + pendingTransactions.length + 1 + const nextNonce = parseInt(baseCount) + pendingTransactions.length // return next nonce and release cb return { nextNonce: nextNonce.toString(16), releaseLock } } - async _getCurrentBlock() { + async _getCurrentBlock () { const currentBlock = this.blockTracker.getCurrentBlock() if (currentBlock) return currentBlock return await Promise((reject, resolve) => { @@ -34,15 +34,13 @@ class NonceTracker { }) } - _takeLock(lockId) { + _takeLock (lockId) { let releaseLock = null // create and store lock const lock = new Promise((resolve, reject) => { releaseLock = resolve }) this.lockMap[lockId] = lock // setup lock teardown - lock.then(() => { - delete this.lockMap[lockId] - }) + lock.then(() => delete this.lockMap[lockId]) return releaseLock } diff --git a/test/unit/nonce-tracker-test.js b/test/unit/nonce-tracker-test.js index 5a05d6170..16cd6d008 100644 --- a/test/unit/nonce-tracker-test.js +++ b/test/unit/nonce-tracker-test.js @@ -3,31 +3,25 @@ const NonceTracker = require('../../app/scripts/lib/nonce-tracker') describe('Nonce Tracker', function () { let nonceTracker, provider, getPendingTransactions, pendingTxs - const noop = () => {} beforeEach(function () { - pendingTxs =[{ + pendingTxs = [{ 'status': 'submitted', 'txParams': { 'from': '0x7d3517b0d011698406d6e0aed8453f0be2697926', 'gas': '0x30d40', 'value': '0x0', - 'nonce': '0x1', + 'nonce': '0x0', }, }] getPendingTransactions = () => pendingTxs - provider = { sendAsync: (_, cb) => { cb(undefined , {result: '0x0'}) }, } + provider = { sendAsync: (_, cb) => { cb(undefined, {result: '0x0'}) } } nonceTracker = new NonceTracker({ blockTracker: { - getCurrentBlock: () => '0x11b568', - once: (...args) => { - setTimeout(() => { - args.pop()() - }, 5000) - } + getCurrentBlock: () => '0x11b568', }, provider, getPendingTransactions, @@ -38,8 +32,8 @@ describe('Nonce Tracker', function () { it('should work', async function (done) { this.timeout(15000) const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926') - assert.equal(nonceLock.nextNonce, '2', 'nonce should be 2') - nonceLock.releaseLock() + assert.equal(nonceLock.nextNonce, '1', 'nonce should be 1') + await nonceLock.releaseLock() done() }) }) -- cgit v1.2.3 From c0c588053a29a4406ef30de8628065429ff99595 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 22 Jun 2017 09:46:03 -0400 Subject: Print integration build errors --- test/integration/index.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/test/integration/index.js b/test/integration/index.js index f2d656b0b..85f91d92b 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -9,13 +9,15 @@ var b = browserify() // Remove old bundle try { fs.unlinkSync(bundlePath) -} catch (e) {} -var writeStream = fs.createWriteStream(bundlePath) + var writeStream = fs.createWriteStream(bundlePath) -tests.forEach(function (fileName) { - b.add(path.join(__dirname, 'lib', fileName)) -}) + tests.forEach(function (fileName) { + b.add(path.join(__dirname, 'lib', fileName)) + }) -b.bundle().pipe(writeStream) + b.bundle().pipe(writeStream) +} catch (e) { + console.error('Integration build failure', e) +} -- cgit v1.2.3 From b7f8657ab5c01c70eff3e19afd334f07b6415c34 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Thu, 22 Jun 2017 12:32:08 -0700 Subject: Add infura network status to our UI state. --- app/scripts/controllers/infura.js | 42 ++++++++++++++++++++++++++++++++++++++ app/scripts/metamask-controller.js | 15 +++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 app/scripts/controllers/infura.js diff --git a/app/scripts/controllers/infura.js b/app/scripts/controllers/infura.js new file mode 100644 index 000000000..e0dad0bc1 --- /dev/null +++ b/app/scripts/controllers/infura.js @@ -0,0 +1,42 @@ +const ObservableStore = require('obs-store') +const extend = require('xtend') + +// every ten minutes +const POLLING_INTERVAL = 300000 + +class InfuraController { + + constructor (opts = {}) { + const initState = extend({ + infuraNetworkStatus: {}, + }, opts.initState) + this.store = new ObservableStore(initState) + } + + // + // PUBLIC METHODS + // + + // Responsible for retrieving the status of Infura's nodes. Can return either + // ok, degraded, or down. + checkInfuraNetworkStatus () { + return fetch('https://api.infura.io/v1/status/metamask') + .then(response => response.json()) + .then((parsedResponse) => { + this.store.updateState({ + infuraNetworkStatus: parsedResponse, + }) + }) + } + + scheduleInfuraNetworkCheck() { + if (this.conversionInterval) { + clearInterval(this.conversionInterval) + } + this.conversionInterval = setInterval(() => { + this.checkInfuraNetworkStatus() + }, POLLING_INTERVAL) + } +} + +module.exports = InfuraController diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index de9a15924..05ba7ecea 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -15,6 +15,7 @@ const CurrencyController = require('./controllers/currency') const NoticeController = require('./notice-controller') const ShapeShiftController = require('./controllers/shapeshift') const AddressBookController = require('./controllers/address-book') +const InfuraController = require('./controllers/infura') const MessageManager = require('./lib/message-manager') const PersonalMessageManager = require('./lib/personal-message-manager') const TransactionController = require('./controllers/transactions') @@ -44,8 +45,8 @@ module.exports = class MetamaskController extends EventEmitter { this.store = new ObservableStore(initState) // network store - this.networkController = new NetworkController(initState.NetworkController) + // config manager this.configManager = new ConfigManager({ store: this.store, @@ -63,6 +64,13 @@ module.exports = class MetamaskController extends EventEmitter { this.currencyController.updateConversionRate() this.currencyController.scheduleConversionInterval() + // infura controller + this.infuraController = new InfuraController({ + initState: initState.InfuraController, + }) + this.infuraController.scheduleInfuraNetworkCheck() + + // rpc provider this.provider = this.initializeProvider() @@ -147,6 +155,9 @@ module.exports = class MetamaskController extends EventEmitter { this.networkController.store.subscribe((state) => { this.store.updateState({ NetworkController: state }) }) + this.infuraController.store.subscribe((state) => { + this.store.updateState({ InfuraController: state }) + }) // manual mem state subscriptions this.networkController.store.subscribe(this.sendUpdate.bind(this)) @@ -160,6 +171,7 @@ module.exports = class MetamaskController extends EventEmitter { this.currencyController.store.subscribe(this.sendUpdate.bind(this)) this.noticeController.memStore.subscribe(this.sendUpdate.bind(this)) this.shapeshiftController.store.subscribe(this.sendUpdate.bind(this)) + this.infuraController.store.subscribe(this.sendUpdate.bind(this)) } // @@ -237,6 +249,7 @@ module.exports = class MetamaskController extends EventEmitter { this.addressBookController.store.getState(), this.currencyController.store.getState(), this.noticeController.memStore.getState(), + this.infuraController.store.getState(), // config manager this.configManager.getConfig(), this.shapeshiftController.store.getState(), -- cgit v1.2.3 From f9f0f6f9ef3ec2f4e311316e3dd7339e21482f40 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Thu, 22 Jun 2017 12:32:34 -0700 Subject: Add infura network status to our UI state. --- app/scripts/controllers/infura.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/controllers/infura.js b/app/scripts/controllers/infura.js index e0dad0bc1..98375b446 100644 --- a/app/scripts/controllers/infura.js +++ b/app/scripts/controllers/infura.js @@ -29,7 +29,7 @@ class InfuraController { }) } - scheduleInfuraNetworkCheck() { + scheduleInfuraNetworkCheck () { if (this.conversionInterval) { clearInterval(this.conversionInterval) } -- cgit v1.2.3 From 199663c0a46990ef08e3e1c0faf692f868f0bc6d Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Thu, 22 Jun 2017 12:51:53 -0700 Subject: Add test for infura controller. --- test/unit/infura-controller-test.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 test/unit/infura-controller-test.js diff --git a/test/unit/infura-controller-test.js b/test/unit/infura-controller-test.js new file mode 100644 index 000000000..0665c498e --- /dev/null +++ b/test/unit/infura-controller-test.js @@ -0,0 +1,34 @@ +// polyfill fetch +global.fetch = global.fetch || require('isomorphic-fetch') + +const assert = require('assert') +const nock = require('nock') +const InfuraController = require('../../app/scripts/controllers/infura') + +describe('infura-controller', function () { + var infuraController + + beforeEach(function () { + infuraController = new InfuraController() + }) + + describe('network status queries', function () { + describe('#checkInfuraNetworkStatus', function () { + it('should return an object reflecting the network statuses', function () { + this.timeout(15000) + nock('https://api.infura.io') + .get('/v1/status/metamask') + .reply(200, '{"mainnet": "ok", "ropsten": "degraded", "kovan": "down", "rinkeby": "ok"}') + + infuraController.checkInfuraNetworkStatus() + .then(() => { + const networkStatus = infuraController.store.getState().infuraNetworkStatus + assert.equal(Object.keys(networkStatus).length, 4) + assert.equal(networkStatus.mainnet, 'ok') + assert.equal(networkStatus.ropsten, 'degraded') + assert.equal(networkStatus.kovan, 'down') + }) + }) + }) + }) +}) -- cgit v1.2.3 From 1ddcbaad5bdd554d9711a9a7cdb29e71703325f3 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 22 Jun 2017 13:46:08 -0700 Subject: add `done` and stub fetch --- test/unit/infura-controller-test.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/test/unit/infura-controller-test.js b/test/unit/infura-controller-test.js index 0665c498e..75bd031ff 100644 --- a/test/unit/infura-controller-test.js +++ b/test/unit/infura-controller-test.js @@ -1,8 +1,9 @@ // polyfill fetch -global.fetch = global.fetch || require('isomorphic-fetch') - +global.fetch = global.fetch || function () {return Promise.resolve({ + json: () => { return Promise.resolve({"mainnet": "ok", "ropsten": "degraded", "kovan": "down", "rinkeby": "ok"}) }, + }) +} const assert = require('assert') -const nock = require('nock') const InfuraController = require('../../app/scripts/controllers/infura') describe('infura-controller', function () { @@ -14,20 +15,20 @@ describe('infura-controller', function () { describe('network status queries', function () { describe('#checkInfuraNetworkStatus', function () { - it('should return an object reflecting the network statuses', function () { + it('should return an object reflecting the network statuses', function (done) { this.timeout(15000) - nock('https://api.infura.io') - .get('/v1/status/metamask') - .reply(200, '{"mainnet": "ok", "ropsten": "degraded", "kovan": "down", "rinkeby": "ok"}') - infuraController.checkInfuraNetworkStatus() .then(() => { const networkStatus = infuraController.store.getState().infuraNetworkStatus + const networkStatus2 = infuraController.store.getState() assert.equal(Object.keys(networkStatus).length, 4) assert.equal(networkStatus.mainnet, 'ok') assert.equal(networkStatus.ropsten, 'degraded') assert.equal(networkStatus.kovan, 'down') }) + .then(() => done()) + .catch(done) + }) }) }) -- cgit v1.2.3 From 42a2bcb1325bd5763a6402d5c2d6634053da908b Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 22 Jun 2017 13:58:55 -0700 Subject: remove option fetch --- test/unit/infura-controller-test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/unit/infura-controller-test.js b/test/unit/infura-controller-test.js index 75bd031ff..7a2a114f9 100644 --- a/test/unit/infura-controller-test.js +++ b/test/unit/infura-controller-test.js @@ -1,5 +1,5 @@ // polyfill fetch -global.fetch = global.fetch || function () {return Promise.resolve({ +global.fetch = function () {return Promise.resolve({ json: () => { return Promise.resolve({"mainnet": "ok", "ropsten": "degraded", "kovan": "down", "rinkeby": "ok"}) }, }) } @@ -20,7 +20,6 @@ describe('infura-controller', function () { infuraController.checkInfuraNetworkStatus() .then(() => { const networkStatus = infuraController.store.getState().infuraNetworkStatus - const networkStatus2 = infuraController.store.getState() assert.equal(Object.keys(networkStatus).length, 4) assert.equal(networkStatus.mainnet, 'ok') assert.equal(networkStatus.ropsten, 'degraded') -- cgit v1.2.3 From f21d42583983a87588963895a301f40ccc3e8c2b Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Mon, 26 Jun 2017 11:37:12 -0700 Subject: Simplify build variables. Remove maps from production. --- gulpfile.js | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 3f235396c..cc723704a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -20,7 +20,7 @@ var gulpif = require('gulp-if') var replace = require('gulp-replace') var mkdirp = require('mkdirp') -var disableLiveReload = gutil.env.disableLiveReload +var disableDebugTools = gutil.env.disableDebugTools var debug = gutil.env.debug // browser reload @@ -121,7 +121,7 @@ gulp.task('manifest:production', function() { './dist/chrome/manifest.json', './dist/edge/manifest.json', ],{base: './dist/'}) - .pipe(gulpif(disableLiveReload,jsoneditor(function(json) { + .pipe(gulpif(!debug,jsoneditor(function(json) { json.background.scripts = ["scripts/background.js"] return json }))) @@ -138,7 +138,7 @@ const staticFiles = [ var copyStrings = staticFiles.map(staticFile => `copy:${staticFile}`) copyStrings.push('copy:contractImages') -if (!disableLiveReload) { +if (debug) { copyStrings.push('copy:reload') } @@ -234,7 +234,7 @@ function copyTask(opts){ destinations.forEach(function(destination) { stream = stream.pipe(gulp.dest(destination)) }) - stream.pipe(gulpif(!disableLiveReload,livereload())) + stream.pipe(gulpif(debug,livereload())) return stream } @@ -314,16 +314,16 @@ function bundleTask(opts) { .pipe(buffer()) // sourcemaps // loads map from browserify file - .pipe(sourcemaps.init({loadMaps: true})) + .pipe(gulpif(debug, sourcemaps.init({loadMaps: true}))) // writes .map file - .pipe(sourcemaps.write('./')) + .pipe(gulpif(debug, sourcemaps.write('./'))) // write completed bundles .pipe(gulp.dest('./dist/firefox/scripts')) .pipe(gulp.dest('./dist/chrome/scripts')) .pipe(gulp.dest('./dist/edge/scripts')) .pipe(gulp.dest('./dist/opera/scripts')) // finally, trigger live reload - .pipe(gulpif(!disableLiveReload, livereload())) + .pipe(gulpif(debug, livereload())) ) } diff --git a/package.json b/package.json index 01c256ef3..3f62923f8 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "start": "npm run dev", "dev": "gulp dev --debug", "disc": "gulp disc --debug", - "dist": "npm install && gulp dist --disableLiveReload", + "dist": "npm install && gulp dist", "test": "npm run lint && npm run test-unit && npm run test-integration", "test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"", "test-integration": "npm run buildMock && npm run buildCiUnits && testem ci -P 2", -- cgit v1.2.3 From 7628a83fd67fcc0f0dca798ea6c5d34eeefb744a Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Mon, 26 Jun 2017 11:47:08 -0700 Subject: Fix Back Button on Add Token View --- ui/app/add-token.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/app/add-token.js b/ui/app/add-token.js index 025cfacb5..b303b5c0d 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -31,6 +31,7 @@ function AddTokenScreen () { AddTokenScreen.prototype.render = function () { const state = this.state + const props = this.props const { warning, symbol, decimals } = state return ( @@ -40,7 +41,7 @@ AddTokenScreen.prototype.render = function () { h('.section-title.flex-row.flex-center', [ h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { onClick: (event) => { - state.dispatch(actions.goHome()) + props.dispatch(actions.goHome()) }, }), h('h2.page-subtitle', 'Add Token'), -- cgit v1.2.3 From 31da623c21463e93737cfcb36cbf10b2d30f7e2a Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Mon, 26 Jun 2017 14:01:09 -0700 Subject: Fix react warning on create-vault-complete --- ui/app/keychains/hd/create-vault-complete.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/keychains/hd/create-vault-complete.js b/ui/app/keychains/hd/create-vault-complete.js index 9741155f7..a318a9b50 100644 --- a/ui/app/keychains/hd/create-vault-complete.js +++ b/ui/app/keychains/hd/create-vault-complete.js @@ -20,7 +20,7 @@ function mapStateToProps (state) { CreateVaultCompleteScreen.prototype.render = function () { var state = this.props - var seed = state.seed || state.cachedSeed + var seed = state.seed || state.cachedSeed || '' return ( -- cgit v1.2.3 From f925a37a9f79337951a0ffd8a106929b3f75d22b Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Mon, 26 Jun 2017 14:01:35 -0700 Subject: Fix react warning for keys on ens address book --- ui/app/components/ens-input.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js index 16c50db84..3a33ebf74 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/ens-input.js @@ -41,7 +41,6 @@ EnsInput.prototype.render = function () { this.checkName() }, }) - return h('div', { style: { width: '100%' }, }, [ @@ -55,6 +54,7 @@ EnsInput.prototype.render = function () { return h('option', { value: identity.address, label: identity.name, + key: identity.address, }) }), // Corresponds to previously sent-to addresses. -- cgit v1.2.3 From 615b8d05a15dcbc572f64d3aa7b8e9feab367bdc Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Mon, 26 Jun 2017 15:47:53 -0700 Subject: Prevent users from accidentally submitting two transactions by disabling button. --- ui/app/components/pending-tx.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 4b1a00eca..5d6954092 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -410,6 +410,8 @@ PendingTx.prototype.resetGasFields = function () { PendingTx.prototype.onSubmit = function (event) { event.preventDefault() + const acceptButton = document.querySelector('input.confirm') + acceptButton.disabled = true const txMeta = this.gatherTxMeta() const valid = this.checkValidity() this.setState({ valid }) @@ -417,6 +419,7 @@ PendingTx.prototype.onSubmit = function (event) { this.props.sendTransaction(txMeta, event) } else { this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) + acceptButton.disabled = false } } -- cgit v1.2.3 From f2cfbda1c968d4bb094cac4f80abe8eeb5225dcf Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Mon, 26 Jun 2017 15:48:41 -0700 Subject: Bump changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 754f076f8..4bfec12dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Fix bug where slowly mined txs would sometimes be incorrectly marked as failed. - Fix bug where badge count did not reflect personal_sign pending messages. - Seed word confirmation wording is now scarier. +- Prevent users from submitting two duplicate transactions by disabling submit. ## 3.7.8 2017-6-12 -- cgit v1.2.3 From 9962a3068b25283e62963338f2c686818f0baef7 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Mon, 26 Jun 2017 15:57:46 -0700 Subject: Change disabling button as state property. --- ui/app/components/pending-tx.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 5d6954092..f33a5d948 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -27,6 +27,7 @@ function PendingTx () { this.state = { valid: true, txData: null, + submitting: false, } } @@ -316,7 +317,7 @@ PendingTx.prototype.render = function () { type: 'submit', value: 'ACCEPT', style: { marginLeft: '10px' }, - disabled: insufficientBalance || !this.state.valid || !isValidAddress, + disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, }), h('button.cancel.btn-red', { @@ -410,16 +411,14 @@ PendingTx.prototype.resetGasFields = function () { PendingTx.prototype.onSubmit = function (event) { event.preventDefault() - const acceptButton = document.querySelector('input.confirm') - acceptButton.disabled = true const txMeta = this.gatherTxMeta() const valid = this.checkValidity() - this.setState({ valid }) + this.setState({ valid, submitting: true }) if (valid && this.verifyGasParams()) { this.props.sendTransaction(txMeta, event) } else { this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) - acceptButton.disabled = false + this.setState({ submitting: false }) } } -- cgit v1.2.3 From 92da8bc57889bb584d4665700072e9e8d8cdb20b Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Mon, 26 Jun 2017 16:14:35 -0700 Subject: Fix error message for invalid seed words. --- app/scripts/keyring-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js index 5b3c80e40..2edc8060e 100644 --- a/app/scripts/keyring-controller.js +++ b/app/scripts/keyring-controller.js @@ -87,7 +87,7 @@ class KeyringController extends EventEmitter { } if (!bip39.validateMnemonic(seed)) { - return Promise.reject('Seed phrase is invalid.') + return Promise.reject(new Error('Seed phrase is invalid.')) } this.clearKeyrings() -- cgit v1.2.3 From d6c14f46091f716195b89eb27233d9fc4ae8f5b0 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Mon, 26 Jun 2017 16:15:11 -0700 Subject: Bump changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 754f076f8..bcae3943e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Fix bug where slowly mined txs would sometimes be incorrectly marked as failed. - Fix bug where badge count did not reflect personal_sign pending messages. - Seed word confirmation wording is now scarier. +- Fix error for invalid seed words. ## 3.7.8 2017-6-12 -- cgit v1.2.3 From ca832959c224a184c0ad40f5dd4239ec261b7f6b Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Tue, 27 Jun 2017 10:34:02 -0700 Subject: Update change log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96dc79d9a..a6b988b4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Seed word confirmation wording is now scarier. - Fix error for invalid seed words. - Prevent users from submitting two duplicate transactions by disabling submit. +- Allow Dapps to specify gas price as hex string. ## 3.7.8 2017-6-12 -- cgit v1.2.3 From a3526906613c9047e6a2bd6bd7c11934152a32fb Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Tue, 27 Jun 2017 12:09:50 -0700 Subject: Remove trailing periods and white space --- ui/app/send.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/send.js b/ui/app/send.js index fd6994145..34e0ea70a 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -244,7 +244,7 @@ SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickna SendTransactionScreen.prototype.onSubmit = function () { const state = this.state || {} - const recipient = state.recipient || document.querySelector('input[name="address"]').value + const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '') const nickname = state.nickname || ' ' const input = document.querySelector('input[name="amount"]').value const value = util.normalizeEthStringToWei(input) -- cgit v1.2.3 From 77054e1fbb588f81403b21999217fccb4d140bf0 Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Tue, 27 Jun 2017 13:56:51 -0700 Subject: Rename Send to Next --- ui/app/send.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/send.js b/ui/app/send.js index fd6994145..70d26d50a 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -189,7 +189,7 @@ SendTransactionScreen.prototype.render = function () { style: { textTransform: 'uppercase', }, - }, 'Send'), + }, 'Next'), ]), -- cgit v1.2.3 From db2836a1ae5bfb2e641ab2b68a9853297e97b64b Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 27 Jun 2017 14:19:28 -0700 Subject: dont stop retrying brodcasting txs --- CHANGELOG.md | 1 + app/scripts/controllers/transactions.js | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96dc79d9a..872db1c1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Current Master +- No longer stop rebroadcasting transactions - Add list of popular tokens held to the account detail view. - Add a warning to JSON file import. - Fix bug where slowly mined txs would sometimes be incorrectly marked as failed. diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index f6dea34e7..31cf8239a 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -8,8 +8,6 @@ const TxProviderUtil = require('../lib/tx-utils') const createId = require('../lib/random-id') const denodeify = require('denodeify') -const RETRY_LIMIT = 200 - module.exports = class TransactionController extends EventEmitter { constructor (opts) { super() @@ -435,8 +433,6 @@ module.exports = class TransactionController extends EventEmitter { // Only auto-submit already-signed txs: if (!('rawTx' in txMeta)) return cb() - if (txMeta.retryCount > RETRY_LIMIT) return - // Increment a try counter. txMeta.retryCount++ const rawTx = txMeta.rawTx -- cgit v1.2.3 From 5cfce8c45a10db8271682b282d4c41747e01943a Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 27 Jun 2017 14:45:43 -0700 Subject: Fix token adding bug --- ui/app/actions.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index 6ff28f32f..d99291e46 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -645,7 +645,9 @@ function addToken (address, symbol, decimals) { if (err) { return dispatch(actions.displayWarning(err.message)) } - dispatch(actions.goHome()) + setTimeout(() => { + dispatch(actions.goHome()) + }, 250) }) } } -- cgit v1.2.3 From 3aff9fdd2a708768e8f4d82ad8756c62a5f3e55c Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 27 Jun 2017 14:49:41 -0700 Subject: Support other network links --- ui/app/components/token-cell.js | 4 +++- ui/lib/etherscan-prefix-for-network.js | 21 +++++++++++++++++++++ ui/lib/explorer-link.js | 21 +++------------------ 3 files changed, 27 insertions(+), 19 deletions(-) create mode 100644 ui/lib/etherscan-prefix-for-network.js diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js index d3a895d36..4d2cacb01 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/token-cell.js @@ -2,6 +2,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits const Identicon = require('./identicon') +const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') module.exports = TokenCell @@ -41,6 +42,7 @@ function navigateTo (url) { } function urlFor (tokenAddress, address, network) { - return `https://etherscan.io/token/${tokenAddress}?a=${address}` + const prefix = prefixForNetwork(network) + return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}` } diff --git a/ui/lib/etherscan-prefix-for-network.js b/ui/lib/etherscan-prefix-for-network.js new file mode 100644 index 000000000..eec658d8f --- /dev/null +++ b/ui/lib/etherscan-prefix-for-network.js @@ -0,0 +1,21 @@ +module.exports = function (hash, network) { + const net = parseInt(network) + let prefix + switch (net) { + case 1: // main net + prefix = '' + break + case 3: // ropsten test net + prefix = 'ropsten.' + break + case 4: // rinkeby test net + prefix = 'rinkeby.' + break + case 42: // kovan test net + prefix = 'kovan.' + break + default: + prefix = '' + } + return prefix +} diff --git a/ui/lib/explorer-link.js b/ui/lib/explorer-link.js index e11249551..3b82ecd5f 100644 --- a/ui/lib/explorer-link.js +++ b/ui/lib/explorer-link.js @@ -1,21 +1,6 @@ +const prefixForNetwork = require('./etherscan-prefix-for-network') + module.exports = function (hash, network) { - const net = parseInt(network) - let prefix - switch (net) { - case 1: // main net - prefix = '' - break - case 3: // ropsten test net - prefix = 'ropsten.' - break - case 4: // rinkeby test net - prefix = 'rinkeby.' - break - case 42: // kovan test net - prefix = 'kovan.' - break - default: - prefix = '' - } + const prefix = prefixForNetwork(network) return `http://${prefix}etherscan.io/tx/${hash}` } -- cgit v1.2.3 From 5440ed23d621c9ebcd24f89d54701f978e1c086e Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 27 Jun 2017 14:49:41 -0700 Subject: Support other network links --- ui/app/components/token-cell.js | 4 +++- ui/lib/etherscan-prefix-for-network.js | 21 +++++++++++++++++++++ ui/lib/explorer-link.js | 21 +++------------------ 3 files changed, 27 insertions(+), 19 deletions(-) create mode 100644 ui/lib/etherscan-prefix-for-network.js diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js index d3a895d36..4d2cacb01 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/token-cell.js @@ -2,6 +2,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits const Identicon = require('./identicon') +const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') module.exports = TokenCell @@ -41,6 +42,7 @@ function navigateTo (url) { } function urlFor (tokenAddress, address, network) { - return `https://etherscan.io/token/${tokenAddress}?a=${address}` + const prefix = prefixForNetwork(network) + return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}` } diff --git a/ui/lib/etherscan-prefix-for-network.js b/ui/lib/etherscan-prefix-for-network.js new file mode 100644 index 000000000..2c1904f1c --- /dev/null +++ b/ui/lib/etherscan-prefix-for-network.js @@ -0,0 +1,21 @@ +module.exports = function (network) { + const net = parseInt(network) + let prefix + switch (net) { + case 1: // main net + prefix = '' + break + case 3: // ropsten test net + prefix = 'ropsten.' + break + case 4: // rinkeby test net + prefix = 'rinkeby.' + break + case 42: // kovan test net + prefix = 'kovan.' + break + default: + prefix = '' + } + return prefix +} diff --git a/ui/lib/explorer-link.js b/ui/lib/explorer-link.js index e11249551..3b82ecd5f 100644 --- a/ui/lib/explorer-link.js +++ b/ui/lib/explorer-link.js @@ -1,21 +1,6 @@ +const prefixForNetwork = require('./etherscan-prefix-for-network') + module.exports = function (hash, network) { - const net = parseInt(network) - let prefix - switch (net) { - case 1: // main net - prefix = '' - break - case 3: // ropsten test net - prefix = 'ropsten.' - break - case 4: // rinkeby test net - prefix = 'rinkeby.' - break - case 42: // kovan test net - prefix = 'kovan.' - break - default: - prefix = '' - } + const prefix = prefixForNetwork(network) return `http://${prefix}etherscan.io/tx/${hash}` } -- cgit v1.2.3 From 78af771c797a3a9d57970ef94be3c3e5ecf117c6 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 27 Jun 2017 15:02:15 -0700 Subject: Do not allow adding non token addresses --- ui/app/add-token.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/ui/app/add-token.js b/ui/app/add-token.js index b303b5c0d..f21184270 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -142,7 +142,13 @@ AddTokenScreen.prototype.render = function () { if (!valid) return const { address, symbol, decimals } = this.state - this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) + this.checkIfToken(address.trim()) + .then(() => { + this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) + }) + .catch((reason) => { + this.setState({ warning: 'Not a valid token address.' }) + }) }, }, 'Add'), ]), @@ -202,6 +208,12 @@ AddTokenScreen.prototype.validateInputs = function () { return isValid } +AddTokenScreen.prototype.checkIfToken = async function (address) { + const contract = this.TokenContract.at(address) + const result = await contract.balance(address) + return result[0].toString() +} + AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) { const contract = this.TokenContract.at(address) -- cgit v1.2.3 From 4e0ec74bb7cb36e2e0fa035bf653ce0e57b7c2e7 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 27 Jun 2017 14:59:37 -0700 Subject: Create a migration for setting tx's with the message 'Gave up submitting tx.' as failed --- app/scripts/migrations/015.js | 38 ++++++++++++++++++++++++++++++++++++++ app/scripts/migrations/index.js | 1 + 2 files changed, 39 insertions(+) create mode 100644 app/scripts/migrations/015.js diff --git a/app/scripts/migrations/015.js b/app/scripts/migrations/015.js new file mode 100644 index 000000000..4b839580b --- /dev/null +++ b/app/scripts/migrations/015.js @@ -0,0 +1,38 @@ +const version = 15 + +/* + +This migration sets transactions with the 'Gave up submitting tx.' err message +to a 'failed' stated + +*/ + +const clone = require('clone') + +module.exports = { + version, + + migrate: function (originalVersionedData) { + const versionedData = clone(originalVersionedData) + versionedData.meta.version = version + try { + const state = versionedData.data + const newState = transformState(state) + versionedData.data = newState + } catch (err) { + console.warn(`MetaMask Migration #${version}` + err.stack) + } + return Promise.resolve(versionedData) + }, +} + +function transformState (state) { + const newState = state + const transactions = newState.TransactionController.transactions + newState.TransactionController.transactions = transactions.map((txMeta) => { + if (!txMeta.err) return txMeta + else if (txMeta.err.message === 'Gave up submitting tx.') txMeta.status = 'failed' + return txMeta + }) + return newState +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index fb1ad7863..651ee6a9c 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -25,4 +25,5 @@ module.exports = [ require('./012'), require('./013'), require('./014'), + require('./015'), ] -- cgit v1.2.3 From 0ee4502d716ebe28fa426a05c454a75c7f82d965 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 27 Jun 2017 15:26:04 -0700 Subject: calculate nonce based on local pending txs w/o error state. --- app/scripts/controllers/transactions.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 651565519..460280578 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -26,7 +26,7 @@ module.exports = class TransactionController extends EventEmitter { this.nonceTracker = new NonceTracker({ provider: this.provider, blockTracker: this.provider._blockTracker, - getPendingTransactions: (address) => this.getFilteredTxList({ from: address, status: 'submitted' }), + getPendingTransactions: (address) => this.getFilteredTxList({ from: address, status: 'submitted', err: undefined }), }) this.query = opts.ethQuery this.txProviderUtils = new TxProviderUtil(this.query) @@ -263,10 +263,19 @@ module.exports = class TransactionController extends EventEmitter { to: '0x0..', from: '0x0..', status: 'signed', + err: undefined, } and returns a list of tx with all options matching + ****************HINT**************** + | `err: undefined` is like looking | + | for a tx with no err | + | so you can also search txs that | + | dont have something as well by | + | setting the value as undefined | + ************************************ + this is for things like filtering a the tx list for only tx's from 1 account or for filltering for all txs from one account -- cgit v1.2.3 From 8642feee09f57d5756c682a053434a196cff4af3 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 27 Jun 2017 15:49:57 -0700 Subject: Remove token contract validation step --- ui/app/add-token.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/ui/app/add-token.js b/ui/app/add-token.js index f21184270..b303b5c0d 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -142,13 +142,7 @@ AddTokenScreen.prototype.render = function () { if (!valid) return const { address, symbol, decimals } = this.state - this.checkIfToken(address.trim()) - .then(() => { - this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) - }) - .catch((reason) => { - this.setState({ warning: 'Not a valid token address.' }) - }) + this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) }, }, 'Add'), ]), @@ -208,12 +202,6 @@ AddTokenScreen.prototype.validateInputs = function () { return isValid } -AddTokenScreen.prototype.checkIfToken = async function (address) { - const contract = this.TokenContract.at(address) - const result = await contract.balance(address) - return result[0].toString() -} - AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) { const contract = this.TokenContract.at(address) -- cgit v1.2.3 From 690685d20de310b4c4589e92a5053afd9c56e85a Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 27 Jun 2017 16:46:14 -0700 Subject: nonce-tracker: only check transactions that are not supposed to be ignored --- app/scripts/controllers/transactions.js | 9 ++++++++- app/scripts/lib/nonce-tracker.js | 13 +++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 460280578..c74427cd5 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -26,7 +26,14 @@ module.exports = class TransactionController extends EventEmitter { this.nonceTracker = new NonceTracker({ provider: this.provider, blockTracker: this.provider._blockTracker, - getPendingTransactions: (address) => this.getFilteredTxList({ from: address, status: 'submitted', err: undefined }), + getPendingTransactions: (address) => { + return this.getFilteredTxList({ + from: address, + status: 'submitted', + err: undefined, + ignore: undefined, + }) + }, }) this.query = opts.ethQuery this.txProviderUtils = new TxProviderUtil(this.query) diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js index 4ea511dec..9ef7706f9 100644 --- a/app/scripts/lib/nonce-tracker.js +++ b/app/scripts/lib/nonce-tracker.js @@ -12,15 +12,14 @@ class NonceTracker { // releaseLock must be called // releaseLock must be called after adding signed tx to pending transactions (or discarding) async getNonceLock (address) { + const pendingTransactions = this.getPendingTransactions(address) // await lock free - await this.lockMap[address] + if (pendingTransactions.length) await this.lockMap[address] + else if (this.lockMap[address]) await this.lockMap[address]() // take lock const releaseLock = this._takeLock(address) // calculate next nonce - const currentBlock = await this._getCurrentBlock() - const blockNumber = currentBlock.number - const pendingTransactions = this.getPendingTransactions(address) - const baseCount = await this._getTxCount(address, blockNumber) + const baseCount = await this._getTxCount(address) const nextNonce = parseInt(baseCount) + pendingTransactions.length // return next nonce and release cb return { nextNonce: nextNonce.toString(16), releaseLock } @@ -44,7 +43,9 @@ class NonceTracker { return releaseLock } - _getTxCount (address, blockNumber) { + async _getTxCount (address) { + const currentBlock = await this._getCurrentBlock() + const blockNumber = currentBlock.number return new Promise((resolve, reject) => { this.ethQuery.getTransactionCount(address, blockNumber, (err, result) => { err ? reject(err) : resolve(result) -- cgit v1.2.3 From d5f0ee4f5e6a0c608ea200fb8c6593641505296d Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Tue, 27 Jun 2017 18:40:47 -0700 Subject: Add Back Button for Import Screen --- ui/app/accounts/import/index.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ui/app/accounts/import/index.js b/ui/app/accounts/import/index.js index a0f0f9bdb..97b387229 100644 --- a/ui/app/accounts/import/index.js +++ b/ui/app/accounts/import/index.js @@ -2,6 +2,7 @@ const inherits = require('util').inherits const Component = require('react').Component const h = require('react-hyperscript') const connect = require('react-redux').connect +const actions = require('../../actions') import Select from 'react-select' // Subviews @@ -37,6 +38,14 @@ AccountImportSubview.prototype.render = function () { style: { }, }, [ + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + props.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Import Accounts'), + ]), h('div', { style: { padding: '10px', -- cgit v1.2.3 From 1b8a4395ab2a7b84b37676ccf08ba58f4d12fccc Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 28 Jun 2017 10:22:12 -0700 Subject: Add copy state logs button --- CHANGELOG.md | 1 + ui/app/config.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d8a814b9..19bb14e85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Fix error for invalid seed words. - Prevent users from submitting two duplicate transactions by disabling submit. - Allow Dapps to specify gas price as hex string. +- Add button for copying state logs to clipboard. ## 3.7.8 2017-6-12 diff --git a/ui/app/config.js b/ui/app/config.js index d7be26757..62785c49b 100644 --- a/ui/app/config.js +++ b/ui/app/config.js @@ -5,6 +5,7 @@ const connect = require('react-redux').connect const actions = require('./actions') const currencies = require('./conversion.json').rows const validUrl = require('valid-url') +const copyToClipboard = require('copy-to-clipboard') module.exports = connect(mapStateToProps)(ConfigScreen) @@ -85,8 +86,35 @@ ConfigScreen.prototype.render = function () { }, }, 'Save'), ]), + h('hr.horizontal-line'), + currentConversionInformation(metamaskState, state), + + h('hr.horizontal-line'), + + h('div', { + style: { + marginTop: '20px', + }, + }, [ + h('p', { + style: { + fontFamily: 'Montserrat Light', + fontSize: '13px', + }, + }, `State logs contain your public account addresses and sent transactions.`), + h('br'), + h('button', { + style: { + alignSelf: 'center', + }, + onClick (event) { + copyToClipboard(window.logState()) + }, + }, 'Copy State Logs'), + ]), + h('hr.horizontal-line'), h('div', { -- cgit v1.2.3 From d7bcd9458f994bc7599c97623bf73a0c3367d7cf Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 28 Jun 2017 10:41:58 -0700 Subject: Version 3.8.0 --- CHANGELOG.md | 2 ++ app/manifest.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0018fc76..e88d085e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +## 3.8.0 2017-6-28 + - No longer stop rebroadcasting transactions - Add list of popular tokens held to the account detail view. - Add ability to add Tokens to token list. diff --git a/app/manifest.json b/app/manifest.json index 7ae20158c..1cd909732 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.7.8", + "version": "3.8.0", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From c81a3c649a53fd09fedc9c8d0a8cab49347186d6 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 28 Jun 2017 16:36:58 -0700 Subject: Add padding to token messages --- ui/app/components/token-list.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index ac7ab8309..bf352dc11 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -109,6 +109,7 @@ TokenList.prototype.message = function (body) { height: '250px', alignItems: 'center', justifyContent: 'center', + padding: '30px', }, }, body) } -- cgit v1.2.3 From 5e8b4e3226a4b084c418c7ed709d4e0f34ab24ec Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 29 Jun 2017 12:06:22 -0700 Subject: =?UTF-8?q?change=20=E2=80=9CACCEPT=E2=80=9D=20to=20=E2=80=9CSUBMI?= =?UTF-8?q?T=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/app/components/pending-tx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index f33a5d948..d7d602f31 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -315,7 +315,7 @@ PendingTx.prototype.render = function () { // Accept Button h('input.confirm.btn-green', { type: 'submit', - value: 'ACCEPT', + value: 'SUBMIT', style: { marginLeft: '10px' }, disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, }), -- cgit v1.2.3 From f285fd5eb1b8706da7bdb694543b96b7bed819f1 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 29 Jun 2017 14:56:24 -0700 Subject: Bump web3 version to 0.9.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3f62923f8..6ab265ec2 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "through2": "^2.0.1", "valid-url": "^1.0.9", "vreme": "^3.0.2", - "web3": "0.18.2", + "web3": "0.19.1", "web3-provider-engine": "^13.0.3", "web3-stream-provider": "^2.0.6", "xtend": "^4.0.1" -- cgit v1.2.3 From 63acc0f4c8606f5f0e18dc716cecd4dbfc20a4b4 Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 29 Jun 2017 18:50:21 -0700 Subject: deps - remove duplicated dev-dependencies ``` npm WARN The package clone is included as both a dev and production dependency. npm WARN The package react-dom is included as both a dev and production dependency. ``` --- package.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/package.json b/package.json index 6ab265ec2..a8aec2f04 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "qrcode-npm": "0.0.3", "react": "^15.0.2", "react-addons-css-transition-group": "^15.0.2", - "react-dom": "^15.0.2", + "react-dom": "^15.5.4", "react-hyperscript": "^2.2.2", "react-markdown": "^2.3.0", "react-redux": "^4.4.5", @@ -141,7 +141,6 @@ "brfs": "^1.4.3", "browserify": "^13.0.0", "chai": "^3.5.0", - "clone": "^1.0.2", "deep-freeze-strict": "^1.1.1", "del": "^2.2.0", "envify": "^4.0.0", @@ -173,7 +172,6 @@ "qs": "^6.2.0", "qunit": "^0.9.1", "react-addons-test-utils": "^15.5.1", - "react-dom": "^15.5.4", "react-test-renderer": "^15.5.4", "react-testutils-additions": "^15.2.0", "sinon": "^1.17.3", -- cgit v1.2.3 From c7f2fd279d40d401260991a76787459761a453e4 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 30 Jun 2017 09:45:34 -0700 Subject: Bump token-tracker to 1.1.1 Includes a critical decimal-handling fix. Also reduces number of symbol and precision queries after initial load. --- CHANGELOG.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e88d085e0..808fe6939 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Current Master +- Fix precision bug in token balances. +- Cache token symbol and precisions to reduce network load. + ## 3.8.0 2017-6-28 - No longer stop rebroadcasting transactions diff --git a/package.json b/package.json index a8aec2f04..77d3531ff 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "eth-query": "^2.1.2", "eth-sig-util": "^1.1.1", "eth-simple-keyring": "^1.1.1", - "eth-token-tracker": "^1.0.9", + "eth-token-tracker": "^1.1.1", "ethereumjs-tx": "^1.3.0", "ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", -- cgit v1.2.3 From 8179f5f84c288d47847540c8f189e6ee8170e701 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 30 Jun 2017 10:10:53 -0700 Subject: Bump token-tracker to 1.1.2 To restore older firefox compatibility. Fixes #1696 --- CHANGELOG.md | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 808fe6939..143e7ca9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Fix precision bug in token balances. - Cache token symbol and precisions to reduce network load. +- Transpile some newer JavaScript, restores compatibility with some older browsers. ## 3.8.0 2017-6-28 diff --git a/package.json b/package.json index 77d3531ff..3b608af0e 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "eth-query": "^2.1.2", "eth-sig-util": "^1.1.1", "eth-simple-keyring": "^1.1.1", - "eth-token-tracker": "^1.1.1", + "eth-token-tracker": "^1.1.2", "ethereumjs-tx": "^1.3.0", "ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", -- cgit v1.2.3 From 8abd592034649ee0ad47eaaa33859b99d206df1f Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 30 Jun 2017 10:21:47 -0700 Subject: Stop loading popular tokens by default Improves performance when loading the token tab. --- CHANGELOG.md | 2 ++ ui/app/components/token-list.js | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e88d085e0..46a093b8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Temporarily disabled loading popular tokens by default to improve performance. + ## 3.8.0 2017-6-28 - No longer stop rebroadcasting transactions diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index bf352dc11..fed7e9f7a 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -3,10 +3,11 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const TokenTracker = require('eth-token-tracker') const TokenCell = require('./token-cell.js') -const contracts = require('eth-contract-metadata') const normalizeAddress = require('eth-sig-util').normalize const defaultTokens = [] +/* +const contracts = require('eth-contract-metadata') for (const address in contracts) { const contract = contracts[address] if (contract.erc20) { @@ -14,6 +15,7 @@ for (const address in contracts) { defaultTokens.push(contract) } } +*/ module.exports = TokenList -- cgit v1.2.3 From 0c011d0fda7268abfacf29715d73d347e9e8e676 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 30 Jun 2017 10:28:27 -0700 Subject: Remove send button Some token precisions are not respected by TokenFactory, so it's not sufficient for a default send form. Removing for now. --- CHANGELOG.md | 2 ++ ui/app/components/token-cell.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e88d085e0..23cda24fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Remove SEND token button until a better token sending form can be built, due to some precision issues. + ## 3.8.0 2017-6-28 - No longer stop rebroadcasting transactions diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js index 48f46934a..19d7139bb 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/token-cell.js @@ -31,9 +31,11 @@ TokenCell.prototype.render = function () { h('span', { style: { flex: '1 0 auto' } }), + /* h('button', { onClick: this.send.bind(this, address), }, 'SEND'), + */ ]) ) -- cgit v1.2.3 From 2e7be151c556ee672803e527f34485fc2f276e48 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 30 Jun 2017 13:55:04 -0700 Subject: Version 3.8.1 --- CHANGELOG.md | 2 ++ app/manifest.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9943a8e15..abb8f24f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +## 3.8.1 2017-6-30 + - Temporarily disabled loading popular tokens by default to improve performance. - Remove SEND token button until a better token sending form can be built, due to some precision issues. - Fix precision bug in token balances. diff --git a/app/manifest.json b/app/manifest.json index 1cd909732..c0d9af8a0 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.8.0", + "version": "3.8.1", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From 91cd849e76d81ebbb984a007979b0566e13a86c2 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 3 Jul 2017 13:48:18 -0700 Subject: Began creating new UI template --- app/scripts/background.js | 3 ++- app/scripts/send-token.js | 33 +++++++++++++++++++++++++++++++++ app/send-token.html | 11 +++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 app/scripts/send-token.js create mode 100644 app/send-token.html diff --git a/app/scripts/background.js b/app/scripts/background.js index e8987394f..7e8f9172f 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -90,7 +90,8 @@ function setupController (initState) { extension.runtime.onConnect.addListener(connectRemote) function connectRemote (remotePort) { - var isMetaMaskInternalProcess = remotePort.name === 'popup' || remotePort.name === 'notification' + const name = remotePort.name + var isMetaMaskInternalProcess = name === 'popup' || name === 'notification' || name === 'ui' var portStream = new PortStream(remotePort) if (isMetaMaskInternalProcess) { // communication with popup diff --git a/app/scripts/send-token.js b/app/scripts/send-token.js new file mode 100644 index 000000000..9e6868884 --- /dev/null +++ b/app/scripts/send-token.js @@ -0,0 +1,33 @@ +const startPopup = require('./popup-core') +const PortStream = require('./lib/port-stream.js') +const ExtensionPlatform = require('./platforms/extension') +const extension = require('extensionizer') +const NotificationManager = require('./lib/notification-manager') +const notificationManager = new NotificationManager() + +// create platform global +global.platform = new ExtensionPlatform() + +// inject css +const css = MetaMaskUiCss() +injectCss(css) + +// setup stream to background +const extensionPort = extension.runtime.connect({ name: 'ui' }) +const connectionStream = new PortStream(extensionPort) + +// start ui +const container = document.getElementById('app-content') +startPopup({ container, connectionStream }, (err, store) => { + if (err) return displayCriticalError(err) + store.subscribe(() => { + const state = store.getState() + }) +}) + +function displayCriticalError (err) { + container.innerHTML = '
The MetaMask app failed to load: please open and close MetaMask again to restart.
' + container.style.height = '80px' + log.error(err.stack) + throw err +} diff --git a/app/send-token.html b/app/send-token.html new file mode 100644 index 000000000..5f98e1072 --- /dev/null +++ b/app/send-token.html @@ -0,0 +1,11 @@ + + + + + MetaMask Plugin + + +
+ + + -- cgit v1.2.3 From 1503dba5ca3526e9750353c2db999dd50433837e Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 3 Jul 2017 13:51:39 -0700 Subject: No longer show network spinner on config screen The config screen is used to select networks, so we must not block it with network loading indication. --- CHANGELOG.md | 2 ++ ui/app/app.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abb8f24f5..16472e8e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- No longer show network loading indication on config screen, to allow selecting custom RPCs. + ## 3.8.1 2017-6-30 - Temporarily disabled loading popular tokens by default to improve performance. diff --git a/ui/app/app.js b/ui/app/app.js index 8bf69b5ad..e4f312bf4 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -66,7 +66,7 @@ function mapStateToProps (state) { App.prototype.render = function () { var props = this.props const { isLoading, loadingMessage, transForward, network } = props - const isLoadingNetwork = network === 'loading' + const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config' const loadMessage = loadingMessage || isLoadingNetwork ? 'Searching for Network' : null -- cgit v1.2.3 From 4e4d6cea403373e7f7d493775981cfa2a97da5f4 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 3 Jul 2017 15:06:26 -0700 Subject: Add menu carrat next to network searching indicator --- ui/app/app.js | 24 +++++++++--------------- ui/app/components/network.js | 23 ++++++++++++++++------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index 8bf69b5ad..ce543a082 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -136,7 +136,7 @@ App.prototype.renderAppBar = function () { }, }, [ - h('div', { + h('div.left-menu-section', { style: { display: 'flex', flexDirection: 'row', @@ -151,21 +151,15 @@ App.prototype.renderAppBar = function () { src: '/images/icon-128.png', }), - h('#network-spacer.flex-center', { - style: { - marginRight: '-72px', + h(NetworkIndicator, { + network: this.props.network, + provider: this.props.provider, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen }) }, - }, [ - h(NetworkIndicator, { - network: this.props.network, - provider: this.props.provider, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen }) - }, - }), - ]), + }), ]), // metamask name diff --git a/ui/app/components/network.js b/ui/app/components/network.js index 31a8fc17c..d5d3e18cd 100644 --- a/ui/app/components/network.js +++ b/ui/app/components/network.js @@ -22,15 +22,24 @@ Network.prototype.render = function () { let iconName, hoverText if (networkNumber === 'loading') { - return h('img.network-indicator', { - title: 'Attempting to connect to blockchain.', - onClick: (event) => this.props.onClick(event), + return h('span', { style: { - width: '27px', - marginRight: '-27px', + display: 'flex', + alignItems: 'center', + flexDirection: 'row', }, - src: 'images/loading.svg', - }) + onClick: (event) => this.props.onClick(event), + }, [ + h('img', { + title: 'Attempting to connect to blockchain.', + style: { + width: '27px', + }, + src: 'images/loading.svg', + }), + h('i.fa.fa-sort-desc'), + ]) + } else if (providerName === 'mainnet') { hoverText = 'Main Ethereum Network' iconName = 'ethereum-network' -- cgit v1.2.3 From 8f65f964ae122fb4df21e3644c8653bc1426c43c Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 3 Jul 2017 15:12:24 -0700 Subject: Indicate which network is being searched for --- ui/app/app.js | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/ui/app/app.js b/ui/app/app.js index ce543a082..8fb424786 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -68,7 +68,7 @@ App.prototype.render = function () { const { isLoading, loadingMessage, transForward, network } = props const isLoadingNetwork = network === 'loading' const loadMessage = loadingMessage || isLoadingNetwork ? - 'Searching for Network' : null + `Connecting to ${this.getNetworkName()}` : null log.debug('Main ui render function') @@ -549,6 +549,27 @@ App.prototype.renderCustomOption = function (provider) { } } +App.prototype.getNetworkName = function () { + const { provider } = this.props + const providerName = provider.type + + let name + + if (providerName === 'mainnet') { + name = 'Main Ethereum Network' + } else if (providerName === 'ropsten') { + name = 'Ropsten Test Network' + } else if (providerName === 'kovan') { + name = 'Kovan Test Network' + } else if (providerName === 'rinkeby') { + name = 'Rinkeby Test Network' + } else { + name = 'Unknown Private Network' + } + + return name +} + App.prototype.renderCommonRpc = function (rpcList, provider) { const { rpcTarget } = provider const props = this.props -- cgit v1.2.3 From d3c7ba31c5031f3bf004108505c41cd864438583 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 3 Jul 2017 15:13:26 -0700 Subject: Bump changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index abb8f24f5..ddaa8a049 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Current Master +- Visually indicate that network spinner is a menu. +- Indicate what network is being searched for when disconnected. + ## 3.8.1 2017-6-30 - Temporarily disabled loading popular tokens by default to improve performance. -- cgit v1.2.3 From 5eb3d5d485b17b98b19443d8def2f03dec9b38ef Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 3 Jul 2017 15:39:25 -0700 Subject: Make folder for responsive UI --- app/home.html | 11 + app/scripts/popup-core.js | 2 +- app/scripts/responsive-core.js | 54 + app/scripts/responsive.js | 33 + app/scripts/send-token.js | 33 - app/send-token.html | 11 - ui/.gitignore | 66 - ui/app/account-detail.js | 311 --- ui/app/accounts/account-list-item.js | 91 - ui/app/accounts/import/index.js | 100 - ui/app/accounts/import/json.js | 100 - ui/app/accounts/import/private-key.js | 67 - ui/app/accounts/import/seed.js | 30 - ui/app/accounts/index.js | 164 -- ui/app/actions.js | 1031 --------- ui/app/add-token.js | 219 -- ui/app/app.js | 591 ----- ui/app/components/account-export.js | 122 - ui/app/components/account-info-link.js | 41 - ui/app/components/account-panel.js | 86 - ui/app/components/balance.js | 89 - ui/app/components/binary-renderer.js | 46 - ui/app/components/bn-as-decimal-input.js | 174 -- ui/app/components/buy-button-subview.js | 197 -- ui/app/components/coinbase-form.js | 63 - ui/app/components/copyButton.js | 59 - ui/app/components/copyable.js | 46 - ui/app/components/custom-radio-list.js | 60 - ui/app/components/drop-menu-item.js | 59 - ui/app/components/editable-label.js | 51 - ui/app/components/ens-input.js | 170 -- ui/app/components/eth-balance.js | 89 - ui/app/components/fiat-value.js | 63 - ui/app/components/hex-as-decimal-input.js | 154 -- ui/app/components/identicon.js | 72 - ui/app/components/loading.js | 53 - ui/app/components/mascot.js | 59 - ui/app/components/mini-account-panel.js | 74 - ui/app/components/network.js | 125 - ui/app/components/notice.js | 126 -- ui/app/components/pending-msg-details.js | 50 - ui/app/components/pending-msg.js | 56 - ui/app/components/pending-personal-msg-details.js | 60 - ui/app/components/pending-personal-msg.js | 47 - ui/app/components/pending-tx.js | 480 ---- ui/app/components/qr-code.js | 79 - ui/app/components/range-slider.js | 58 - ui/app/components/shapeshift-form.js | 306 --- ui/app/components/shift-list-item.js | 204 -- ui/app/components/tab-bar.js | 36 - ui/app/components/template.js | 18 - ui/app/components/token-cell.js | 72 - ui/app/components/token-list.js | 194 -- ui/app/components/tooltip.js | 22 - ui/app/components/transaction-list-item-icon.js | 68 - ui/app/components/transaction-list-item.js | 165 -- ui/app/components/transaction-list.js | 79 - ui/app/conf-tx.js | 213 -- ui/app/config.js | 211 -- ui/app/conversion.json | 207 -- ui/app/css/debug.css | 21 - ui/app/css/fonts.css | 36 - ui/app/css/index.css | 667 ------ ui/app/css/lib.css | 268 --- ui/app/css/reset.css | 48 - ui/app/css/transitions.css | 42 - ui/app/first-time/init-menu.js | 179 -- ui/app/img/identicon-tardigrade.png | Bin 141119 -> 0 bytes ui/app/img/identicon-walrus.png | Bin 388973 -> 0 bytes ui/app/info.js | 154 -- ui/app/keychains/hd/create-vault-complete.js | 78 - ui/app/keychains/hd/recover-seed/confirmation.js | 118 - ui/app/keychains/hd/restore-vault.js | 152 -- ui/app/new-keychain.js | 29 - ui/app/reducers.js | 52 - ui/app/reducers/app.js | 585 ----- ui/app/reducers/identities.js | 15 - ui/app/reducers/metamask.js | 137 -- ui/app/root.js | 22 - ui/app/send.js | 288 --- ui/app/settings.js | 59 - ui/app/store.js | 21 - ui/app/template.js | 30 - ui/app/unlock.js | 118 - ui/app/util.js | 217 -- ui/classic/.gitignore | 66 + ui/classic/app/account-detail.js | 311 +++ ui/classic/app/accounts/account-list-item.js | 91 + ui/classic/app/accounts/import/index.js | 100 + ui/classic/app/accounts/import/json.js | 100 + ui/classic/app/accounts/import/private-key.js | 67 + ui/classic/app/accounts/import/seed.js | 30 + ui/classic/app/accounts/index.js | 164 ++ ui/classic/app/actions.js | 1031 +++++++++ ui/classic/app/add-token.js | 219 ++ ui/classic/app/app.js | 591 +++++ ui/classic/app/components/account-export.js | 122 + ui/classic/app/components/account-info-link.js | 41 + ui/classic/app/components/account-panel.js | 86 + ui/classic/app/components/balance.js | 89 + ui/classic/app/components/binary-renderer.js | 46 + ui/classic/app/components/bn-as-decimal-input.js | 174 ++ ui/classic/app/components/buy-button-subview.js | 197 ++ ui/classic/app/components/coinbase-form.js | 63 + ui/classic/app/components/copyButton.js | 59 + ui/classic/app/components/copyable.js | 46 + ui/classic/app/components/custom-radio-list.js | 60 + ui/classic/app/components/drop-menu-item.js | 59 + ui/classic/app/components/editable-label.js | 51 + ui/classic/app/components/ens-input.js | 170 ++ ui/classic/app/components/eth-balance.js | 89 + ui/classic/app/components/fiat-value.js | 63 + ui/classic/app/components/hex-as-decimal-input.js | 154 ++ ui/classic/app/components/identicon.js | 72 + ui/classic/app/components/loading.js | 53 + ui/classic/app/components/mascot.js | 59 + ui/classic/app/components/mini-account-panel.js | 74 + ui/classic/app/components/network.js | 125 + ui/classic/app/components/notice.js | 126 ++ ui/classic/app/components/pending-msg-details.js | 50 + ui/classic/app/components/pending-msg.js | 56 + .../app/components/pending-personal-msg-details.js | 60 + ui/classic/app/components/pending-personal-msg.js | 47 + ui/classic/app/components/pending-tx.js | 480 ++++ ui/classic/app/components/qr-code.js | 79 + ui/classic/app/components/range-slider.js | 58 + ui/classic/app/components/shapeshift-form.js | 306 +++ ui/classic/app/components/shift-list-item.js | 204 ++ ui/classic/app/components/tab-bar.js | 36 + ui/classic/app/components/template.js | 18 + ui/classic/app/components/token-cell.js | 72 + ui/classic/app/components/token-list.js | 194 ++ ui/classic/app/components/tooltip.js | 22 + .../app/components/transaction-list-item-icon.js | 68 + ui/classic/app/components/transaction-list-item.js | 165 ++ ui/classic/app/components/transaction-list.js | 79 + ui/classic/app/conf-tx.js | 213 ++ ui/classic/app/config.js | 211 ++ ui/classic/app/conversion.json | 207 ++ ui/classic/app/css/debug.css | 21 + ui/classic/app/css/fonts.css | 36 + ui/classic/app/css/index.css | 667 ++++++ ui/classic/app/css/lib.css | 268 +++ ui/classic/app/css/reset.css | 48 + ui/classic/app/css/transitions.css | 42 + ui/classic/app/first-time/init-menu.js | 179 ++ ui/classic/app/img/identicon-tardigrade.png | Bin 0 -> 141119 bytes ui/classic/app/img/identicon-walrus.png | Bin 0 -> 388973 bytes ui/classic/app/info.js | 154 ++ .../app/keychains/hd/create-vault-complete.js | 78 + .../app/keychains/hd/recover-seed/confirmation.js | 118 + ui/classic/app/keychains/hd/restore-vault.js | 152 ++ ui/classic/app/new-keychain.js | 29 + ui/classic/app/reducers.js | 52 + ui/classic/app/reducers/app.js | 585 +++++ ui/classic/app/reducers/identities.js | 15 + ui/classic/app/reducers/metamask.js | 137 ++ ui/classic/app/root.js | 22 + ui/classic/app/send.js | 288 +++ ui/classic/app/settings.js | 59 + ui/classic/app/store.js | 21 + ui/classic/app/template.js | 30 + ui/classic/app/unlock.js | 118 + ui/classic/app/util.js | 217 ++ ui/classic/css.js | 29 + ui/classic/design/00-metamask-SignIn.jpg | Bin 0 -> 57848 bytes ui/classic/design/01-metamask-SelectAcc.jpg | Bin 0 -> 76063 bytes ui/classic/design/02-metamask-AccDetails.jpg | Bin 0 -> 75780 bytes .../design/02a-metamask-AccDetails-OverToken.jpg | Bin 0 -> 121847 bytes .../02a-metamask-AccDetails-OverTransaction.jpg | Bin 0 -> 122075 bytes ui/classic/design/02a-metamask-AccDetails.jpg | Bin 0 -> 117570 bytes ui/classic/design/02b-metamask-AccDetails-Send.jpg | Bin 0 -> 110143 bytes ui/classic/design/03-metamask-Qr.jpg | Bin 0 -> 66052 bytes ui/classic/design/05-metamask-Menu.jpg | Bin 0 -> 130264 bytes .../chromeStorePics/final_screen_dao_accounts.png | Bin 0 -> 249708 bytes .../chromeStorePics/final_screen_dao_locked.png | Bin 0 -> 220295 bytes .../final_screen_dao_notification.png | Bin 0 -> 214405 bytes .../chromeStorePics/final_screen_wei_account.png | Bin 0 -> 253382 bytes .../final_screen_wei_notification.png | Bin 0 -> 193865 bytes ui/classic/design/chromeStorePics/icon-128.png | Bin 0 -> 5770 bytes ui/classic/design/chromeStorePics/icon-64.png | Bin 0 -> 3573 bytes ui/classic/design/chromeStorePics/metamask_icon.ai | 2383 ++++++++++++++++++++ ui/classic/design/chromeStorePics/promo1400560.png | Bin 0 -> 261644 bytes ui/classic/design/chromeStorePics/promo440280.png | Bin 0 -> 57471 bytes ui/classic/design/chromeStorePics/promo920680.png | Bin 0 -> 206713 bytes .../design/chromeStorePics/screen_dao_accounts.png | Bin 0 -> 517598 bytes .../design/chromeStorePics/screen_dao_locked.png | Bin 0 -> 287108 bytes .../chromeStorePics/screen_dao_notification.png | Bin 0 -> 296498 bytes .../design/chromeStorePics/screen_wei_account.png | Bin 0 -> 653633 bytes .../chromeStorePics/screen_wei_notification.png | Bin 0 -> 402486 bytes ui/classic/design/metamask-logo-eyes.png | Bin 0 -> 146076 bytes ui/classic/design/wireframes/1st_time_use.png | Bin 0 -> 937556 bytes .../design/wireframes/metamask_wfs_jan_13.pdf | Bin 0 -> 452413 bytes .../design/wireframes/metamask_wfs_jan_13.png | Bin 0 -> 419066 bytes .../design/wireframes/metamask_wfs_jan_18.pdf | Bin 0 -> 612778 bytes ui/classic/example.js | 123 + ui/classic/index.html | 20 + ui/classic/index.js | 58 + ui/classic/lib/account-link.js | 26 + ui/classic/lib/contract-namer.js | 33 + ui/classic/lib/etherscan-prefix-for-network.js | 21 + ui/classic/lib/explorer-link.js | 6 + ui/classic/lib/icon-factory.js | 65 + ui/classic/lib/lost-accounts-notice.js | 23 + ui/classic/lib/persistent-form.js | 61 + ui/classic/lib/tx-helper.js | 17 + ui/css.js | 29 - ui/design/00-metamask-SignIn.jpg | Bin 57848 -> 0 bytes ui/design/01-metamask-SelectAcc.jpg | Bin 76063 -> 0 bytes ui/design/02-metamask-AccDetails.jpg | Bin 75780 -> 0 bytes ui/design/02a-metamask-AccDetails-OverToken.jpg | Bin 121847 -> 0 bytes .../02a-metamask-AccDetails-OverTransaction.jpg | Bin 122075 -> 0 bytes ui/design/02a-metamask-AccDetails.jpg | Bin 117570 -> 0 bytes ui/design/02b-metamask-AccDetails-Send.jpg | Bin 110143 -> 0 bytes ui/design/03-metamask-Qr.jpg | Bin 66052 -> 0 bytes ui/design/05-metamask-Menu.jpg | Bin 130264 -> 0 bytes .../chromeStorePics/final_screen_dao_accounts.png | Bin 249708 -> 0 bytes .../chromeStorePics/final_screen_dao_locked.png | Bin 220295 -> 0 bytes .../final_screen_dao_notification.png | Bin 214405 -> 0 bytes .../chromeStorePics/final_screen_wei_account.png | Bin 253382 -> 0 bytes .../final_screen_wei_notification.png | Bin 193865 -> 0 bytes ui/design/chromeStorePics/icon-128.png | Bin 5770 -> 0 bytes ui/design/chromeStorePics/icon-64.png | Bin 3573 -> 0 bytes ui/design/chromeStorePics/metamask_icon.ai | 2383 -------------------- ui/design/chromeStorePics/promo1400560.png | Bin 261644 -> 0 bytes ui/design/chromeStorePics/promo440280.png | Bin 57471 -> 0 bytes ui/design/chromeStorePics/promo920680.png | Bin 206713 -> 0 bytes ui/design/chromeStorePics/screen_dao_accounts.png | Bin 517598 -> 0 bytes ui/design/chromeStorePics/screen_dao_locked.png | Bin 287108 -> 0 bytes .../chromeStorePics/screen_dao_notification.png | Bin 296498 -> 0 bytes ui/design/chromeStorePics/screen_wei_account.png | Bin 653633 -> 0 bytes .../chromeStorePics/screen_wei_notification.png | Bin 402486 -> 0 bytes ui/design/metamask-logo-eyes.png | Bin 146076 -> 0 bytes ui/design/wireframes/1st_time_use.png | Bin 937556 -> 0 bytes ui/design/wireframes/metamask_wfs_jan_13.pdf | Bin 452413 -> 0 bytes ui/design/wireframes/metamask_wfs_jan_13.png | Bin 419066 -> 0 bytes ui/design/wireframes/metamask_wfs_jan_18.pdf | Bin 612778 -> 0 bytes ui/example.js | 123 - ui/index.html | 20 - ui/index.js | 58 - ui/lib/account-link.js | 26 - ui/lib/contract-namer.js | 33 - ui/lib/etherscan-prefix-for-network.js | 21 - ui/lib/explorer-link.js | 6 - ui/lib/icon-factory.js | 65 - ui/lib/lost-accounts-notice.js | 23 - ui/lib/persistent-form.js | 61 - ui/lib/tx-helper.js | 17 - 248 files changed, 13773 insertions(+), 13719 deletions(-) create mode 100644 app/home.html create mode 100644 app/scripts/responsive-core.js create mode 100644 app/scripts/responsive.js delete mode 100644 app/scripts/send-token.js delete mode 100644 app/send-token.html delete mode 100644 ui/.gitignore delete mode 100644 ui/app/account-detail.js delete mode 100644 ui/app/accounts/account-list-item.js delete mode 100644 ui/app/accounts/import/index.js delete mode 100644 ui/app/accounts/import/json.js delete mode 100644 ui/app/accounts/import/private-key.js delete mode 100644 ui/app/accounts/import/seed.js delete mode 100644 ui/app/accounts/index.js delete mode 100644 ui/app/actions.js delete mode 100644 ui/app/add-token.js delete mode 100644 ui/app/app.js delete mode 100644 ui/app/components/account-export.js delete mode 100644 ui/app/components/account-info-link.js delete mode 100644 ui/app/components/account-panel.js delete mode 100644 ui/app/components/balance.js delete mode 100644 ui/app/components/binary-renderer.js delete mode 100644 ui/app/components/bn-as-decimal-input.js delete mode 100644 ui/app/components/buy-button-subview.js delete mode 100644 ui/app/components/coinbase-form.js delete mode 100644 ui/app/components/copyButton.js delete mode 100644 ui/app/components/copyable.js delete mode 100644 ui/app/components/custom-radio-list.js delete mode 100644 ui/app/components/drop-menu-item.js delete mode 100644 ui/app/components/editable-label.js delete mode 100644 ui/app/components/ens-input.js delete mode 100644 ui/app/components/eth-balance.js delete mode 100644 ui/app/components/fiat-value.js delete mode 100644 ui/app/components/hex-as-decimal-input.js delete mode 100644 ui/app/components/identicon.js delete mode 100644 ui/app/components/loading.js delete mode 100644 ui/app/components/mascot.js delete mode 100644 ui/app/components/mini-account-panel.js delete mode 100644 ui/app/components/network.js delete mode 100644 ui/app/components/notice.js delete mode 100644 ui/app/components/pending-msg-details.js delete mode 100644 ui/app/components/pending-msg.js delete mode 100644 ui/app/components/pending-personal-msg-details.js delete mode 100644 ui/app/components/pending-personal-msg.js delete mode 100644 ui/app/components/pending-tx.js delete mode 100644 ui/app/components/qr-code.js delete mode 100644 ui/app/components/range-slider.js delete mode 100644 ui/app/components/shapeshift-form.js delete mode 100644 ui/app/components/shift-list-item.js delete mode 100644 ui/app/components/tab-bar.js delete mode 100644 ui/app/components/template.js delete mode 100644 ui/app/components/token-cell.js delete mode 100644 ui/app/components/token-list.js delete mode 100644 ui/app/components/tooltip.js delete mode 100644 ui/app/components/transaction-list-item-icon.js delete mode 100644 ui/app/components/transaction-list-item.js delete mode 100644 ui/app/components/transaction-list.js delete mode 100644 ui/app/conf-tx.js delete mode 100644 ui/app/config.js delete mode 100644 ui/app/conversion.json delete mode 100644 ui/app/css/debug.css delete mode 100644 ui/app/css/fonts.css delete mode 100644 ui/app/css/index.css delete mode 100644 ui/app/css/lib.css delete mode 100644 ui/app/css/reset.css delete mode 100644 ui/app/css/transitions.css delete mode 100644 ui/app/first-time/init-menu.js delete mode 100644 ui/app/img/identicon-tardigrade.png delete mode 100644 ui/app/img/identicon-walrus.png delete mode 100644 ui/app/info.js delete mode 100644 ui/app/keychains/hd/create-vault-complete.js delete mode 100644 ui/app/keychains/hd/recover-seed/confirmation.js delete mode 100644 ui/app/keychains/hd/restore-vault.js delete mode 100644 ui/app/new-keychain.js delete mode 100644 ui/app/reducers.js delete mode 100644 ui/app/reducers/app.js delete mode 100644 ui/app/reducers/identities.js delete mode 100644 ui/app/reducers/metamask.js delete mode 100644 ui/app/root.js delete mode 100644 ui/app/send.js delete mode 100644 ui/app/settings.js delete mode 100644 ui/app/store.js delete mode 100644 ui/app/template.js delete mode 100644 ui/app/unlock.js delete mode 100644 ui/app/util.js create mode 100644 ui/classic/.gitignore create mode 100644 ui/classic/app/account-detail.js create mode 100644 ui/classic/app/accounts/account-list-item.js create mode 100644 ui/classic/app/accounts/import/index.js create mode 100644 ui/classic/app/accounts/import/json.js create mode 100644 ui/classic/app/accounts/import/private-key.js create mode 100644 ui/classic/app/accounts/import/seed.js create mode 100644 ui/classic/app/accounts/index.js create mode 100644 ui/classic/app/actions.js create mode 100644 ui/classic/app/add-token.js create mode 100644 ui/classic/app/app.js create mode 100644 ui/classic/app/components/account-export.js create mode 100644 ui/classic/app/components/account-info-link.js create mode 100644 ui/classic/app/components/account-panel.js create mode 100644 ui/classic/app/components/balance.js create mode 100644 ui/classic/app/components/binary-renderer.js create mode 100644 ui/classic/app/components/bn-as-decimal-input.js create mode 100644 ui/classic/app/components/buy-button-subview.js create mode 100644 ui/classic/app/components/coinbase-form.js create mode 100644 ui/classic/app/components/copyButton.js create mode 100644 ui/classic/app/components/copyable.js create mode 100644 ui/classic/app/components/custom-radio-list.js create mode 100644 ui/classic/app/components/drop-menu-item.js create mode 100644 ui/classic/app/components/editable-label.js create mode 100644 ui/classic/app/components/ens-input.js create mode 100644 ui/classic/app/components/eth-balance.js create mode 100644 ui/classic/app/components/fiat-value.js create mode 100644 ui/classic/app/components/hex-as-decimal-input.js create mode 100644 ui/classic/app/components/identicon.js create mode 100644 ui/classic/app/components/loading.js create mode 100644 ui/classic/app/components/mascot.js create mode 100644 ui/classic/app/components/mini-account-panel.js create mode 100644 ui/classic/app/components/network.js create mode 100644 ui/classic/app/components/notice.js create mode 100644 ui/classic/app/components/pending-msg-details.js create mode 100644 ui/classic/app/components/pending-msg.js create mode 100644 ui/classic/app/components/pending-personal-msg-details.js create mode 100644 ui/classic/app/components/pending-personal-msg.js create mode 100644 ui/classic/app/components/pending-tx.js create mode 100644 ui/classic/app/components/qr-code.js create mode 100644 ui/classic/app/components/range-slider.js create mode 100644 ui/classic/app/components/shapeshift-form.js create mode 100644 ui/classic/app/components/shift-list-item.js create mode 100644 ui/classic/app/components/tab-bar.js create mode 100644 ui/classic/app/components/template.js create mode 100644 ui/classic/app/components/token-cell.js create mode 100644 ui/classic/app/components/token-list.js create mode 100644 ui/classic/app/components/tooltip.js create mode 100644 ui/classic/app/components/transaction-list-item-icon.js create mode 100644 ui/classic/app/components/transaction-list-item.js create mode 100644 ui/classic/app/components/transaction-list.js create mode 100644 ui/classic/app/conf-tx.js create mode 100644 ui/classic/app/config.js create mode 100644 ui/classic/app/conversion.json create mode 100644 ui/classic/app/css/debug.css create mode 100644 ui/classic/app/css/fonts.css create mode 100644 ui/classic/app/css/index.css create mode 100644 ui/classic/app/css/lib.css create mode 100644 ui/classic/app/css/reset.css create mode 100644 ui/classic/app/css/transitions.css create mode 100644 ui/classic/app/first-time/init-menu.js create mode 100644 ui/classic/app/img/identicon-tardigrade.png create mode 100644 ui/classic/app/img/identicon-walrus.png create mode 100644 ui/classic/app/info.js create mode 100644 ui/classic/app/keychains/hd/create-vault-complete.js create mode 100644 ui/classic/app/keychains/hd/recover-seed/confirmation.js create mode 100644 ui/classic/app/keychains/hd/restore-vault.js create mode 100644 ui/classic/app/new-keychain.js create mode 100644 ui/classic/app/reducers.js create mode 100644 ui/classic/app/reducers/app.js create mode 100644 ui/classic/app/reducers/identities.js create mode 100644 ui/classic/app/reducers/metamask.js create mode 100644 ui/classic/app/root.js create mode 100644 ui/classic/app/send.js create mode 100644 ui/classic/app/settings.js create mode 100644 ui/classic/app/store.js create mode 100644 ui/classic/app/template.js create mode 100644 ui/classic/app/unlock.js create mode 100644 ui/classic/app/util.js create mode 100644 ui/classic/css.js create mode 100644 ui/classic/design/00-metamask-SignIn.jpg create mode 100644 ui/classic/design/01-metamask-SelectAcc.jpg create mode 100644 ui/classic/design/02-metamask-AccDetails.jpg create mode 100644 ui/classic/design/02a-metamask-AccDetails-OverToken.jpg create mode 100644 ui/classic/design/02a-metamask-AccDetails-OverTransaction.jpg create mode 100644 ui/classic/design/02a-metamask-AccDetails.jpg create mode 100644 ui/classic/design/02b-metamask-AccDetails-Send.jpg create mode 100644 ui/classic/design/03-metamask-Qr.jpg create mode 100644 ui/classic/design/05-metamask-Menu.jpg create mode 100644 ui/classic/design/chromeStorePics/final_screen_dao_accounts.png create mode 100644 ui/classic/design/chromeStorePics/final_screen_dao_locked.png create mode 100644 ui/classic/design/chromeStorePics/final_screen_dao_notification.png create mode 100644 ui/classic/design/chromeStorePics/final_screen_wei_account.png create mode 100644 ui/classic/design/chromeStorePics/final_screen_wei_notification.png create mode 100644 ui/classic/design/chromeStorePics/icon-128.png create mode 100644 ui/classic/design/chromeStorePics/icon-64.png create mode 100644 ui/classic/design/chromeStorePics/metamask_icon.ai create mode 100644 ui/classic/design/chromeStorePics/promo1400560.png create mode 100644 ui/classic/design/chromeStorePics/promo440280.png create mode 100644 ui/classic/design/chromeStorePics/promo920680.png create mode 100644 ui/classic/design/chromeStorePics/screen_dao_accounts.png create mode 100644 ui/classic/design/chromeStorePics/screen_dao_locked.png create mode 100644 ui/classic/design/chromeStorePics/screen_dao_notification.png create mode 100644 ui/classic/design/chromeStorePics/screen_wei_account.png create mode 100644 ui/classic/design/chromeStorePics/screen_wei_notification.png create mode 100644 ui/classic/design/metamask-logo-eyes.png create mode 100644 ui/classic/design/wireframes/1st_time_use.png create mode 100644 ui/classic/design/wireframes/metamask_wfs_jan_13.pdf create mode 100644 ui/classic/design/wireframes/metamask_wfs_jan_13.png create mode 100644 ui/classic/design/wireframes/metamask_wfs_jan_18.pdf create mode 100644 ui/classic/example.js create mode 100644 ui/classic/index.html create mode 100644 ui/classic/index.js create mode 100644 ui/classic/lib/account-link.js create mode 100644 ui/classic/lib/contract-namer.js create mode 100644 ui/classic/lib/etherscan-prefix-for-network.js create mode 100644 ui/classic/lib/explorer-link.js create mode 100644 ui/classic/lib/icon-factory.js create mode 100644 ui/classic/lib/lost-accounts-notice.js create mode 100644 ui/classic/lib/persistent-form.js create mode 100644 ui/classic/lib/tx-helper.js delete mode 100644 ui/css.js delete mode 100644 ui/design/00-metamask-SignIn.jpg delete mode 100644 ui/design/01-metamask-SelectAcc.jpg delete mode 100644 ui/design/02-metamask-AccDetails.jpg delete mode 100644 ui/design/02a-metamask-AccDetails-OverToken.jpg delete mode 100644 ui/design/02a-metamask-AccDetails-OverTransaction.jpg delete mode 100644 ui/design/02a-metamask-AccDetails.jpg delete mode 100644 ui/design/02b-metamask-AccDetails-Send.jpg delete mode 100644 ui/design/03-metamask-Qr.jpg delete mode 100644 ui/design/05-metamask-Menu.jpg delete mode 100644 ui/design/chromeStorePics/final_screen_dao_accounts.png delete mode 100644 ui/design/chromeStorePics/final_screen_dao_locked.png delete mode 100644 ui/design/chromeStorePics/final_screen_dao_notification.png delete mode 100644 ui/design/chromeStorePics/final_screen_wei_account.png delete mode 100644 ui/design/chromeStorePics/final_screen_wei_notification.png delete mode 100644 ui/design/chromeStorePics/icon-128.png delete mode 100644 ui/design/chromeStorePics/icon-64.png delete mode 100644 ui/design/chromeStorePics/metamask_icon.ai delete mode 100644 ui/design/chromeStorePics/promo1400560.png delete mode 100644 ui/design/chromeStorePics/promo440280.png delete mode 100644 ui/design/chromeStorePics/promo920680.png delete mode 100644 ui/design/chromeStorePics/screen_dao_accounts.png delete mode 100644 ui/design/chromeStorePics/screen_dao_locked.png delete mode 100644 ui/design/chromeStorePics/screen_dao_notification.png delete mode 100644 ui/design/chromeStorePics/screen_wei_account.png delete mode 100644 ui/design/chromeStorePics/screen_wei_notification.png delete mode 100644 ui/design/metamask-logo-eyes.png delete mode 100644 ui/design/wireframes/1st_time_use.png delete mode 100644 ui/design/wireframes/metamask_wfs_jan_13.pdf delete mode 100644 ui/design/wireframes/metamask_wfs_jan_13.png delete mode 100644 ui/design/wireframes/metamask_wfs_jan_18.pdf delete mode 100644 ui/example.js delete mode 100644 ui/index.html delete mode 100644 ui/index.js delete mode 100644 ui/lib/account-link.js delete mode 100644 ui/lib/contract-namer.js delete mode 100644 ui/lib/etherscan-prefix-for-network.js delete mode 100644 ui/lib/explorer-link.js delete mode 100644 ui/lib/icon-factory.js delete mode 100644 ui/lib/lost-accounts-notice.js delete mode 100644 ui/lib/persistent-form.js delete mode 100644 ui/lib/tx-helper.js diff --git a/app/home.html b/app/home.html new file mode 100644 index 000000000..b7b8adbeb --- /dev/null +++ b/app/home.html @@ -0,0 +1,11 @@ + + + + + MetaMask Plugin + + +
+ + + diff --git a/app/scripts/popup-core.js b/app/scripts/popup-core.js index f1eb394d7..156be795a 100644 --- a/app/scripts/popup-core.js +++ b/app/scripts/popup-core.js @@ -2,7 +2,7 @@ const EventEmitter = require('events').EventEmitter const async = require('async') const Dnode = require('dnode') const EthQuery = require('eth-query') -const launchMetamaskUi = require('../../ui') +const launchMetamaskUi = require('../../ui/classic') const StreamProvider = require('web3-stream-provider') const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex diff --git a/app/scripts/responsive-core.js b/app/scripts/responsive-core.js new file mode 100644 index 000000000..3760facfa --- /dev/null +++ b/app/scripts/responsive-core.js @@ -0,0 +1,54 @@ +const EventEmitter = require('events').EventEmitter +const async = require('async') +const Dnode = require('dnode') +const EthQuery = require('eth-query') +const launchMetamaskUi = require('../../ui/responsive') +const StreamProvider = require('web3-stream-provider') +const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex + + +module.exports = initializePopup + + +function initializePopup ({ container, connectionStream }, cb) { + // setup app + async.waterfall([ + (cb) => connectToAccountManager(connectionStream, cb), + (accountManager, cb) => launchMetamaskUi({ container, accountManager }, cb), + ], cb) +} + +function connectToAccountManager (connectionStream, cb) { + // setup communication with background + // setup multiplexing + var mx = setupMultiplex(connectionStream) + // connect features + setupControllerConnection(mx.createStream('controller'), cb) + setupWeb3Connection(mx.createStream('provider')) +} + +function setupWeb3Connection (connectionStream) { + var providerStream = new StreamProvider() + providerStream.pipe(connectionStream).pipe(providerStream) + connectionStream.on('error', console.error.bind(console)) + providerStream.on('error', console.error.bind(console)) + global.ethereumProvider = providerStream + global.ethQuery = new EthQuery(providerStream) +} + +function setupControllerConnection (connectionStream, cb) { + // this is a really sneaky way of adding EventEmitter api + // to a bi-directional dnode instance + var eventEmitter = new EventEmitter() + var accountManagerDnode = Dnode({ + sendUpdate: function (state) { + eventEmitter.emit('update', state) + }, + }) + connectionStream.pipe(accountManagerDnode).pipe(connectionStream) + accountManagerDnode.once('remote', function (accountManager) { + // setup push events + accountManager.on = eventEmitter.on.bind(eventEmitter) + cb(null, accountManager) + }) +} diff --git a/app/scripts/responsive.js b/app/scripts/responsive.js new file mode 100644 index 000000000..512065309 --- /dev/null +++ b/app/scripts/responsive.js @@ -0,0 +1,33 @@ +const startPopup = require('./responsive-core') +const PortStream = require('./lib/port-stream.js') +const ExtensionPlatform = require('./platforms/extension') +const extension = require('extensionizer') +const NotificationManager = require('./lib/notification-manager') +const notificationManager = new NotificationManager() + +// create platform global +global.platform = new ExtensionPlatform() + +// inject css +const css = MetaMaskUiCss() +injectCss(css) + +// setup stream to background +const extensionPort = extension.runtime.connect({ name: 'ui' }) +const connectionStream = new PortStream(extensionPort) + +// start ui +const container = document.getElementById('app-content') +startPopup({ container, connectionStream }, (err, store) => { + if (err) return displayCriticalError(err) + store.subscribe(() => { + const state = store.getState() + }) +}) + +function displayCriticalError (err) { + container.innerHTML = '
The MetaMask app failed to load: please open and close MetaMask again to restart.
' + container.style.height = '80px' + log.error(err.stack) + throw err +} diff --git a/app/scripts/send-token.js b/app/scripts/send-token.js deleted file mode 100644 index 9e6868884..000000000 --- a/app/scripts/send-token.js +++ /dev/null @@ -1,33 +0,0 @@ -const startPopup = require('./popup-core') -const PortStream = require('./lib/port-stream.js') -const ExtensionPlatform = require('./platforms/extension') -const extension = require('extensionizer') -const NotificationManager = require('./lib/notification-manager') -const notificationManager = new NotificationManager() - -// create platform global -global.platform = new ExtensionPlatform() - -// inject css -const css = MetaMaskUiCss() -injectCss(css) - -// setup stream to background -const extensionPort = extension.runtime.connect({ name: 'ui' }) -const connectionStream = new PortStream(extensionPort) - -// start ui -const container = document.getElementById('app-content') -startPopup({ container, connectionStream }, (err, store) => { - if (err) return displayCriticalError(err) - store.subscribe(() => { - const state = store.getState() - }) -}) - -function displayCriticalError (err) { - container.innerHTML = '
The MetaMask app failed to load: please open and close MetaMask again to restart.
' - container.style.height = '80px' - log.error(err.stack) - throw err -} diff --git a/app/send-token.html b/app/send-token.html deleted file mode 100644 index 5f98e1072..000000000 --- a/app/send-token.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - MetaMask Plugin - - -
- - - diff --git a/ui/.gitignore b/ui/.gitignore deleted file mode 100644 index c6b1254b5..000000000 --- a/ui/.gitignore +++ /dev/null @@ -1,66 +0,0 @@ - -# Created by https://www.gitignore.io/api/osx,node - -### OSX ### -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - - -### Node ### -# Logs -logs -*.log -npm-debug.log* - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directory -# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git -node_modules - -# Optional npm cache directory -.npm - -# Optional REPL history -.node_repl_history - diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js deleted file mode 100644 index bed05a7fb..000000000 --- a/ui/app/account-detail.js +++ /dev/null @@ -1,311 +0,0 @@ -const inherits = require('util').inherits -const extend = require('xtend') -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const CopyButton = require('./components/copyButton') -const AccountInfoLink = require('./components/account-info-link') -const actions = require('./actions') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') -const valuesFor = require('./util').valuesFor - -const Identicon = require('./components/identicon') -const EthBalance = require('./components/eth-balance') -const TransactionList = require('./components/transaction-list') -const ExportAccountView = require('./components/account-export') -const ethUtil = require('ethereumjs-util') -const EditableLabel = require('./components/editable-label') -const Tooltip = require('./components/tooltip') -const TabBar = require('./components/tab-bar') -const TokenList = require('./components/token-list') - -module.exports = connect(mapStateToProps)(AccountDetailScreen) - -function mapStateToProps (state) { - return { - metamask: state.metamask, - identities: state.metamask.identities, - accounts: state.metamask.accounts, - address: state.metamask.selectedAddress, - accountDetail: state.appState.accountDetail, - network: state.metamask.network, - unapprovedMsgs: valuesFor(state.metamask.unapprovedMsgs), - shapeShiftTxList: state.metamask.shapeShiftTxList, - transactions: state.metamask.selectedAddressTxList || [], - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - currentAccountTab: state.metamask.currentAccountTab, - tokens: state.metamask.tokens, - } -} - -inherits(AccountDetailScreen, Component) -function AccountDetailScreen () { - Component.call(this) -} - -AccountDetailScreen.prototype.render = function () { - var props = this.props - var selected = props.address || Object.keys(props.accounts)[0] - var checksumAddress = selected && ethUtil.toChecksumAddress(selected) - var identity = props.identities[selected] - var account = props.accounts[selected] - const { network, conversionRate, currentCurrency } = props - - return ( - - h('.account-detail-section', [ - - // identicon, label, balance, etc - h('.account-data-subsection', { - style: { - margin: '0 20px', - }, - }, [ - - // header - identicon + nav - h('div', { - style: { - paddingTop: '20px', - display: 'flex', - justifyContent: 'flex-start', - alignItems: 'flex-start', - }, - }, [ - - // large identicon and addresses - h('.identicon-wrapper.select-none', [ - h(Identicon, { - diameter: 62, - address: selected, - }), - ]), - h('flex-column', { - style: { - lineHeight: '10px', - marginLeft: '15px', - }, - }, [ - h(EditableLabel, { - textValue: identity ? identity.name : '', - state: { - isEditingLabel: false, - }, - saveText: (text) => { - props.dispatch(actions.saveAccountLabel(selected, text)) - }, - }, [ - - // What is shown when not editing + edit text: - h('label.editing-label', [h('.edit-text', 'edit')]), - h('h2.font-medium.color-forest', {name: 'edit'}, identity && identity.name), - ]), - h('.flex-row', { - style: { - width: '15em', - justifyContent: 'space-between', - alignItems: 'baseline', - }, - }, [ - - // address - - h('div', { - style: { - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingTop: '3px', - width: '5em', - fontSize: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - marginTop: '10px', - marginBottom: '15px', - color: '#AEAEAE', - }, - }, checksumAddress), - - // copy and export - - h('.flex-row', { - style: { - justifyContent: 'flex-end', - }, - }, [ - - h(AccountInfoLink, { selected, network }), - - h(CopyButton, { - value: checksumAddress, - }), - - h(Tooltip, { - title: 'QR Code', - }, [ - h('i.fa.fa-qrcode.pointer.pop-hover', { - onClick: () => props.dispatch(actions.showQrView(selected, identity ? identity.name : '')), - style: { - fontSize: '18px', - position: 'relative', - color: 'rgb(247, 134, 28)', - top: '5px', - marginLeft: '3px', - marginRight: '3px', - }, - }), - ]), - - h(Tooltip, { - title: 'Export Private Key', - }, [ - h('div', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - h('img.cursor-pointer.color-orange', { - src: 'images/key-32.png', - onClick: () => this.requestAccountExport(selected), - style: { - height: '19px', - }, - }), - ]), - ]), - ]), - ]), - - // account ballence - - ]), - ]), - h('.flex-row', { - style: { - justifyContent: 'space-between', - alignItems: 'flex-start', - }, - }, [ - - h(EthBalance, { - value: account && account.balance, - conversionRate, - currentCurrency, - style: { - lineHeight: '7px', - marginTop: '10px', - }, - }), - - h('button', { - onClick: () => props.dispatch(actions.buyEthView(selected)), - style: { - marginBottom: '20px', - marginRight: '8px', - position: 'absolute', - left: '219px', - }, - }, 'BUY'), - - h('button', { - onClick: () => props.dispatch(actions.showSendPage()), - style: { - marginBottom: '20px', - marginRight: '8px', - }, - }, 'SEND'), - - ]), - ]), - - // subview (tx history, pk export confirm, buy eth warning) - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.subview(), - ]), - - ]) - ) -} - -AccountDetailScreen.prototype.subview = function () { - var subview - try { - subview = this.props.accountDetail.subview - } catch (e) { - subview = null - } - - switch (subview) { - case 'transactions': - return this.tabSections() - case 'export': - var state = extend({key: 'export'}, this.props) - return h(ExportAccountView, state) - default: - return this.tabSections() - } -} - -AccountDetailScreen.prototype.tabSections = function () { - const { currentAccountTab } = this.props - - return h('section.tabSection', [ - - h(TabBar, { - tabs: [ - { content: 'Sent', key: 'history' }, - { content: 'Tokens', key: 'tokens' }, - ], - defaultTab: currentAccountTab || 'history', - tabSelected: (key) => { - this.props.dispatch(actions.setCurrentAccountTab(key)) - }, - }), - - this.tabSwitchView(), - ]) -} - -AccountDetailScreen.prototype.tabSwitchView = function () { - const props = this.props - const { address, network } = props - const { currentAccountTab, tokens } = this.props - - switch (currentAccountTab) { - case 'tokens': - return h(TokenList, { - userAddress: address, - network, - tokens, - addToken: () => this.props.dispatch(actions.showAddTokenPage()), - }) - default: - return this.transactionList() - } -} - -AccountDetailScreen.prototype.transactionList = function () { - const {transactions, unapprovedMsgs, address, - network, shapeShiftTxList, conversionRate } = this.props - - return h(TransactionList, { - transactions: transactions.sort((a, b) => b.time - a.time), - network, - unapprovedMsgs, - conversionRate, - address, - shapeShiftTxList, - viewPendingTx: (txId) => { - this.props.dispatch(actions.viewPendingTx(txId)) - }, - }) -} - -AccountDetailScreen.prototype.requestAccountExport = function () { - this.props.dispatch(actions.requestExportAccount()) -} diff --git a/ui/app/accounts/account-list-item.js b/ui/app/accounts/account-list-item.js deleted file mode 100644 index 10a0b6cc7..000000000 --- a/ui/app/accounts/account-list-item.js +++ /dev/null @@ -1,91 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ethUtil = require('ethereumjs-util') - -const EthBalance = require('../components/eth-balance') -const CopyButton = require('../components/copyButton') -const Identicon = require('../components/identicon') - -module.exports = AccountListItem - -inherits(AccountListItem, Component) -function AccountListItem () { - Component.call(this) -} - -AccountListItem.prototype.render = function () { - const { identity, selectedAddress, accounts, onShowDetail, - conversionRate, currentCurrency } = this.props - - const checksumAddress = identity && identity.address && ethUtil.toChecksumAddress(identity.address) - const isSelected = selectedAddress === identity.address - const account = accounts[identity.address] - const selectedClass = isSelected ? '.selected' : '' - - return ( - h(`.accounts-list-option.flex-row.flex-space-between.pointer.hover-white${selectedClass}`, { - key: `account-panel-${identity.address}`, - onClick: (event) => onShowDetail(identity.address, event), - }, [ - - h('.identicon-wrapper.flex-column.flex-center.select-none', [ - this.pendingOrNot(), - this.indicateIfLoose(), - h(Identicon, { - address: identity.address, - imageify: true, - }), - ]), - - // account address, balance - h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', { - style: { - width: '200px', - }, - }, [ - h('span', identity.name), - h('span.font-small', { - style: { - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - }, checksumAddress), - h(EthBalance, { - value: account && account.balance, - currentCurrency, - conversionRate, - style: { - lineHeight: '7px', - marginTop: '10px', - }, - }), - ]), - - // copy button - h('.identity-copy.flex-column', { - style: { - margin: '0 20px', - }, - }, [ - h(CopyButton, { - value: checksumAddress, - }), - ]), - ]) - ) -} - -AccountListItem.prototype.indicateIfLoose = function () { - try { // Sometimes keyrings aren't loaded yet: - const type = this.props.keyring.type - const isLoose = type !== 'HD Key Tree' - return isLoose ? h('.keyring-label', 'LOOSE') : null - } catch (e) { return } -} - -AccountListItem.prototype.pendingOrNot = function () { - const pending = this.props.pending - if (pending.length === 0) return null - return h('.pending-dot', pending.length) -} diff --git a/ui/app/accounts/import/index.js b/ui/app/accounts/import/index.js deleted file mode 100644 index 97b387229..000000000 --- a/ui/app/accounts/import/index.js +++ /dev/null @@ -1,100 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../../actions') -import Select from 'react-select' - -// Subviews -const JsonImportView = require('./json.js') -const PrivateKeyImportView = require('./private-key.js') - -const menuItems = [ - 'Private Key', - 'JSON File', -] - -module.exports = connect(mapStateToProps)(AccountImportSubview) - -function mapStateToProps (state) { - return { - menuItems, - } -} - -inherits(AccountImportSubview, Component) -function AccountImportSubview () { - Component.call(this) -} - -AccountImportSubview.prototype.render = function () { - const props = this.props - const state = this.state || {} - const { menuItems } = props - const { type } = state - - return ( - h('div', { - style: { - }, - }, [ - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: (event) => { - props.dispatch(actions.goHome()) - }, - }), - h('h2.page-subtitle', 'Import Accounts'), - ]), - h('div', { - style: { - padding: '10px', - color: 'rgb(174, 174, 174)', - }, - }, [ - - h('h3', { style: { padding: '3px' } }, 'SELECT TYPE'), - - h('style', ` - .has-value.Select--single > .Select-control .Select-value .Select-value-label, .Select-value-label { - color: rgb(174,174,174); - } - `), - - h(Select, { - name: 'import-type-select', - clearable: false, - value: type || menuItems[0], - options: menuItems.map((type) => { - return { - value: type, - label: type, - } - }), - onChange: (opt) => { - this.setState({ type: opt.value }) - }, - }), - ]), - - this.renderImportView(), - ]) - ) -} - -AccountImportSubview.prototype.renderImportView = function () { - const props = this.props - const state = this.state || {} - const { type } = state - const { menuItems } = props - const current = type || menuItems[0] - - switch (current) { - case 'Private Key': - return h(PrivateKeyImportView) - case 'JSON File': - return h(JsonImportView) - default: - return h(JsonImportView) - } -} diff --git a/ui/app/accounts/import/json.js b/ui/app/accounts/import/json.js deleted file mode 100644 index 158a3c923..000000000 --- a/ui/app/accounts/import/json.js +++ /dev/null @@ -1,100 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../../actions') -const FileInput = require('react-simple-file-input').default - -const HELP_LINK = 'https://github.com/MetaMask/faq/blob/master/README.md#q-i-cant-use-the-import-feature-for-uploading-a-json-file-the-window-keeps-closing-when-i-try-to-select-a-file' - -module.exports = connect(mapStateToProps)(JsonImportSubview) - -function mapStateToProps (state) { - return { - error: state.appState.warning, - } -} - -inherits(JsonImportSubview, Component) -function JsonImportSubview () { - Component.call(this) -} - -JsonImportSubview.prototype.render = function () { - const { error } = this.props - - return ( - h('div', { - style: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - padding: '5px 15px 0px 15px', - }, - }, [ - - h('p', 'Used by a variety of different clients'), - h('a.warning', { href: HELP_LINK, target: '_blank' }, 'File import not working? Click here!'), - - h(FileInput, { - readAs: 'text', - onLoad: this.onLoad.bind(this), - style: { - margin: '20px 0px 12px 20px', - fontSize: '15px', - }, - }), - - h('input.large-input.letter-spacey', { - type: 'password', - placeholder: 'Enter password', - id: 'json-password-box', - onKeyPress: this.createKeyringOnEnter.bind(this), - style: { - width: 260, - marginTop: 12, - }, - }), - - h('button.primary', { - onClick: this.createNewKeychain.bind(this), - style: { - margin: 12, - }, - }, 'Import'), - - error ? h('span.error', error) : null, - ]) - ) -} - -JsonImportSubview.prototype.onLoad = function (event, file) { - this.setState({file: file, fileContents: event.target.result}) -} - -JsonImportSubview.prototype.createKeyringOnEnter = function (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.createNewKeychain() - } -} - -JsonImportSubview.prototype.createNewKeychain = function () { - const state = this.state - const { fileContents } = state - - if (!fileContents) { - const message = 'You must select a file to import.' - return this.props.dispatch(actions.displayWarning(message)) - } - - const passwordInput = document.getElementById('json-password-box') - const password = passwordInput.value - - if (!password) { - const message = 'You must enter a password for the selected file.' - return this.props.dispatch(actions.displayWarning(message)) - } - - this.props.dispatch(actions.importNewAccount('JSON File', [ fileContents, password ])) -} diff --git a/ui/app/accounts/import/private-key.js b/ui/app/accounts/import/private-key.js deleted file mode 100644 index 68ccee58e..000000000 --- a/ui/app/accounts/import/private-key.js +++ /dev/null @@ -1,67 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../../actions') - -module.exports = connect(mapStateToProps)(PrivateKeyImportView) - -function mapStateToProps (state) { - return { - error: state.appState.warning, - } -} - -inherits(PrivateKeyImportView, Component) -function PrivateKeyImportView () { - Component.call(this) -} - -PrivateKeyImportView.prototype.render = function () { - const { error } = this.props - - return ( - h('div', { - style: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - padding: '5px 15px 0px 15px', - }, - }, [ - h('span', 'Paste your private key string here'), - - h('input.large-input.letter-spacey', { - type: 'password', - id: 'private-key-box', - onKeyPress: this.createKeyringOnEnter.bind(this), - style: { - width: 260, - marginTop: 12, - }, - }), - - h('button.primary', { - onClick: this.createNewKeychain.bind(this), - style: { - margin: 12, - }, - }, 'Import'), - - error ? h('span.error', error) : null, - ]) - ) -} - -PrivateKeyImportView.prototype.createKeyringOnEnter = function (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.createNewKeychain() - } -} - -PrivateKeyImportView.prototype.createNewKeychain = function () { - const input = document.getElementById('private-key-box') - const privateKey = input.value - this.props.dispatch(actions.importNewAccount('Private Key', [ privateKey ])) -} diff --git a/ui/app/accounts/import/seed.js b/ui/app/accounts/import/seed.js deleted file mode 100644 index b4a7c0afa..000000000 --- a/ui/app/accounts/import/seed.js +++ /dev/null @@ -1,30 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect - -module.exports = connect(mapStateToProps)(SeedImportSubview) - -function mapStateToProps (state) { - return {} -} - -inherits(SeedImportSubview, Component) -function SeedImportSubview () { - Component.call(this) -} - -SeedImportSubview.prototype.render = function () { - return ( - h('div', { - style: { - }, - }, [ - `Paste your seed phrase here!`, - h('textarea'), - h('br'), - h('button', 'Submit'), - ]) - ) -} - diff --git a/ui/app/accounts/index.js b/ui/app/accounts/index.js deleted file mode 100644 index ac2615cd7..000000000 --- a/ui/app/accounts/index.js +++ /dev/null @@ -1,164 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../actions') -const valuesFor = require('../util').valuesFor -const findDOMNode = require('react-dom').findDOMNode -const AccountListItem = require('./account-list-item') - -module.exports = connect(mapStateToProps)(AccountsScreen) - -function mapStateToProps (state) { - const pendingTxs = valuesFor(state.metamask.unapprovedTxs) - .filter(txMeta => txMeta.metamaskNetworkId === state.metamask.network) - const pendingMsgs = valuesFor(state.metamask.unapprovedMsgs) - const pending = pendingTxs.concat(pendingMsgs) - - return { - accounts: state.metamask.accounts, - identities: state.metamask.identities, - unapprovedTxs: state.metamask.unapprovedTxs, - selectedAddress: state.metamask.selectedAddress, - scrollToBottom: state.appState.scrollToBottom, - pending, - keyrings: state.metamask.keyrings, - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - } -} - -inherits(AccountsScreen, Component) -function AccountsScreen () { - Component.call(this) -} - -AccountsScreen.prototype.render = function () { - const props = this.props - const { keyrings, conversionRate, currentCurrency } = props - const identityList = valuesFor(props.identities) - const unapprovedTxList = valuesFor(props.unapprovedTxs) - - return ( - - h('.accounts-section.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: this.goHome.bind(this), - }), - h('h2.page-subtitle', 'Select Account'), - ]), - - h('hr.horizontal-line'), - - // identity selection - h('section.identity-section', { - style: { - height: '418px', - overflowY: 'auto', - overflowX: 'hidden', - }, - }, - [ - identityList.map((identity) => { - const pending = this.props.pending.filter((txOrMsg) => { - if ('txParams' in txOrMsg) { - return txOrMsg.txParams.from === identity.address - } else if ('msgParams' in txOrMsg) { - return txOrMsg.msgParams.from === identity.address - } else { - return false - } - }) - - const simpleAddress = identity.address.substring(2).toLowerCase() - const keyring = keyrings.find((kr) => { - return kr.accounts.includes(simpleAddress) || - kr.accounts.includes(identity.address) - }) - - return h(AccountListItem, { - key: `acct-panel-${identity.address}`, - identity, - selectedAddress: this.props.selectedAddress, - conversionRate, - currentCurrency, - accounts: this.props.accounts, - onShowDetail: this.onShowDetail.bind(this), - pending, - keyring, - }) - }), - - h('hr.horizontal-line'), - h('div.footer.hover-white.pointer', { - key: 'reveal-account-bar', - onClick: () => { - this.addNewAccount() - }, - style: { - display: 'flex', - height: '40px', - padding: '10px', - justifyContent: 'center', - alignItems: 'center', - }, - }, [ - h('i.fa.fa-plus.fa-lg', {key: ''}), - ]), - h('hr.horizontal-line'), - ]), - - unapprovedTxList.length ? ( - - h('.unconftx-link.flex-row.flex-center', { - onClick: this.navigateToConfTx.bind(this), - }, [ - h('span', 'Unconfirmed Txs'), - h('i.fa.fa-arrow-right.fa-lg'), - ]) - - ) : ( - null - ), - ]) - ) -} - -// If a new account was revealed, scroll to the bottom -AccountsScreen.prototype.componentDidUpdate = function () { - const scrollToBottom = this.props.scrollToBottom - - if (scrollToBottom) { - var container = findDOMNode(this) - var scrollable = container.querySelector('.identity-section') - scrollable.scrollTop = scrollable.scrollHeight - } -} - -AccountsScreen.prototype.navigateToConfTx = function () { - event.stopPropagation() - this.props.dispatch(actions.showConfTxPage()) -} - -AccountsScreen.prototype.onShowDetail = function (address, event) { - event.stopPropagation() - this.props.dispatch(actions.showAccountDetail(address)) -} - -AccountsScreen.prototype.addNewAccount = function () { - this.props.dispatch(actions.addNewAccount(0)) -} - -/* An optional view proposed in this design: - * https://consensys.quip.com/zZVrAysM5znY -AccountsScreen.prototype.addNewAccount = function () { - this.props.dispatch(actions.navigateToNewAccountScreen()) -} -*/ - -AccountsScreen.prototype.goHome = function () { - this.props.dispatch(actions.goHome()) -} diff --git a/ui/app/actions.js b/ui/app/actions.js deleted file mode 100644 index d99291e46..000000000 --- a/ui/app/actions.js +++ /dev/null @@ -1,1031 +0,0 @@ -const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url') - -var actions = { - _setBackgroundConnection: _setBackgroundConnection, - - GO_HOME: 'GO_HOME', - goHome: goHome, - // menu state - getNetworkStatus: 'getNetworkStatus', - // transition state - TRANSITION_FORWARD: 'TRANSITION_FORWARD', - TRANSITION_BACKWARD: 'TRANSITION_BACKWARD', - transitionForward, - transitionBackward, - // remote state - UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE', - updateMetamaskState: updateMetamaskState, - // notices - MARK_NOTICE_READ: 'MARK_NOTICE_READ', - markNoticeRead: markNoticeRead, - SHOW_NOTICE: 'SHOW_NOTICE', - showNotice: showNotice, - CLEAR_NOTICES: 'CLEAR_NOTICES', - clearNotices: clearNotices, - markAccountsFound, - // intialize screen - CREATE_NEW_VAULT_IN_PROGRESS: 'CREATE_NEW_VAULT_IN_PROGRESS', - SHOW_CREATE_VAULT: 'SHOW_CREATE_VAULT', - SHOW_RESTORE_VAULT: 'SHOW_RESTORE_VAULT', - FORGOT_PASSWORD: 'FORGOT_PASSWORD', - forgotPassword: forgotPassword, - SHOW_INIT_MENU: 'SHOW_INIT_MENU', - SHOW_NEW_VAULT_SEED: 'SHOW_NEW_VAULT_SEED', - SHOW_INFO_PAGE: 'SHOW_INFO_PAGE', - SHOW_IMPORT_PAGE: 'SHOW_IMPORT_PAGE', - unlockMetamask: unlockMetamask, - unlockFailed: unlockFailed, - showCreateVault: showCreateVault, - showRestoreVault: showRestoreVault, - showInitializeMenu: showInitializeMenu, - showImportPage, - createNewVaultAndKeychain: createNewVaultAndKeychain, - createNewVaultAndRestore: createNewVaultAndRestore, - createNewVaultInProgress: createNewVaultInProgress, - addNewKeyring, - importNewAccount, - addNewAccount, - NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN', - navigateToNewAccountScreen, - showNewVaultSeed: showNewVaultSeed, - showInfoPage: showInfoPage, - // seed recovery actions - REVEAL_SEED_CONFIRMATION: 'REVEAL_SEED_CONFIRMATION', - revealSeedConfirmation: revealSeedConfirmation, - requestRevealSeed: requestRevealSeed, - // unlock screen - UNLOCK_IN_PROGRESS: 'UNLOCK_IN_PROGRESS', - UNLOCK_FAILED: 'UNLOCK_FAILED', - UNLOCK_METAMASK: 'UNLOCK_METAMASK', - LOCK_METAMASK: 'LOCK_METAMASK', - tryUnlockMetamask: tryUnlockMetamask, - lockMetamask: lockMetamask, - unlockInProgress: unlockInProgress, - // error handling - displayWarning: displayWarning, - DISPLAY_WARNING: 'DISPLAY_WARNING', - HIDE_WARNING: 'HIDE_WARNING', - hideWarning: hideWarning, - // accounts screen - SET_SELECTED_ACCOUNT: 'SET_SELECTED_ACCOUNT', - SHOW_ACCOUNT_DETAIL: 'SHOW_ACCOUNT_DETAIL', - SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE', - SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE', - SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE', - SET_CURRENT_FIAT: 'SET_CURRENT_FIAT', - setCurrentCurrency: setCurrentCurrency, - setCurrentAccountTab, - // account detail screen - SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', - showSendPage: showSendPage, - ADD_TO_ADDRESS_BOOK: 'ADD_TO_ADDRESS_BOOK', - addToAddressBook: addToAddressBook, - REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT', - requestExportAccount: requestExportAccount, - EXPORT_ACCOUNT: 'EXPORT_ACCOUNT', - exportAccount: exportAccount, - SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY', - showPrivateKey: showPrivateKey, - SAVE_ACCOUNT_LABEL: 'SAVE_ACCOUNT_LABEL', - saveAccountLabel: saveAccountLabel, - // tx conf screen - COMPLETED_TX: 'COMPLETED_TX', - TRANSACTION_ERROR: 'TRANSACTION_ERROR', - NEXT_TX: 'NEXT_TX', - PREVIOUS_TX: 'PREV_TX', - signMsg: signMsg, - cancelMsg: cancelMsg, - signPersonalMsg, - cancelPersonalMsg, - sendTx: sendTx, - signTx: signTx, - updateAndApproveTx, - cancelTx: cancelTx, - completedTx: completedTx, - txError: txError, - nextTx: nextTx, - previousTx: previousTx, - viewPendingTx: viewPendingTx, - VIEW_PENDING_TX: 'VIEW_PENDING_TX', - // app messages - confirmSeedWords: confirmSeedWords, - showAccountDetail: showAccountDetail, - BACK_TO_ACCOUNT_DETAIL: 'BACK_TO_ACCOUNT_DETAIL', - backToAccountDetail: backToAccountDetail, - showAccountsPage: showAccountsPage, - showConfTxPage: showConfTxPage, - // config screen - SHOW_CONFIG_PAGE: 'SHOW_CONFIG_PAGE', - SET_RPC_TARGET: 'SET_RPC_TARGET', - SET_DEFAULT_RPC_TARGET: 'SET_DEFAULT_RPC_TARGET', - SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE', - USE_ETHERSCAN_PROVIDER: 'USE_ETHERSCAN_PROVIDER', - useEtherscanProvider: useEtherscanProvider, - showConfigPage, - SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', - showAddTokenPage, - addToken, - setRpcTarget: setRpcTarget, - setDefaultRpcTarget: setDefaultRpcTarget, - setProviderType: setProviderType, - // loading overlay - SHOW_LOADING: 'SHOW_LOADING_INDICATION', - HIDE_LOADING: 'HIDE_LOADING_INDICATION', - showLoadingIndication: showLoadingIndication, - hideLoadingIndication: hideLoadingIndication, - // buy Eth with coinbase - BUY_ETH: 'BUY_ETH', - buyEth: buyEth, - buyEthView: buyEthView, - BUY_ETH_VIEW: 'BUY_ETH_VIEW', - COINBASE_SUBVIEW: 'COINBASE_SUBVIEW', - coinBaseSubview: coinBaseSubview, - SHAPESHIFT_SUBVIEW: 'SHAPESHIFT_SUBVIEW', - shapeShiftSubview: shapeShiftSubview, - PAIR_UPDATE: 'PAIR_UPDATE', - pairUpdate: pairUpdate, - coinShiftRquest: coinShiftRquest, - SHOW_SUB_LOADING_INDICATION: 'SHOW_SUB_LOADING_INDICATION', - showSubLoadingIndication: showSubLoadingIndication, - HIDE_SUB_LOADING_INDICATION: 'HIDE_SUB_LOADING_INDICATION', - hideSubLoadingIndication: hideSubLoadingIndication, -// QR STUFF: - SHOW_QR: 'SHOW_QR', - showQrView: showQrView, - reshowQrCode: reshowQrCode, - SHOW_QR_VIEW: 'SHOW_QR_VIEW', -// FORGOT PASSWORD: - BACK_TO_INIT_MENU: 'BACK_TO_INIT_MENU', - goBackToInitView: goBackToInitView, - RECOVERY_IN_PROGRESS: 'RECOVERY_IN_PROGRESS', - BACK_TO_UNLOCK_VIEW: 'BACK_TO_UNLOCK_VIEW', - backToUnlockView: backToUnlockView, - // SHOWING KEYCHAIN - SHOW_NEW_KEYCHAIN: 'SHOW_NEW_KEYCHAIN', - showNewKeychain: showNewKeychain, - - callBackgroundThenUpdate, - forceUpdateMetamaskState, -} - -module.exports = actions - -var background = null -function _setBackgroundConnection (backgroundConnection) { - background = backgroundConnection -} - -function goHome () { - return { - type: actions.GO_HOME, - } -} - -// async actions - -function tryUnlockMetamask (password) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - dispatch(actions.unlockInProgress()) - log.debug(`background.submitPassword`) - background.submitPassword(password, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - dispatch(actions.unlockFailed(err.message)) - } else { - dispatch(actions.transitionForward()) - forceUpdateMetamaskState(dispatch) - } - }) - } -} - -function transitionForward () { - return { - type: this.TRANSITION_FORWARD, - } -} - -function transitionBackward () { - return { - type: this.TRANSITION_BACKWARD, - } -} - -function confirmSeedWords () { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.clearSeedWordCache`) - background.clearSeedWordCache((err, account) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - - log.info('Seed word cache cleared. ' + account) - dispatch(actions.showAccountDetail(account)) - }) - } -} - -function createNewVaultAndRestore (password, seed) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.createNewVaultAndRestore`) - background.createNewVaultAndRestore(password, seed, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.showAccountsPage()) - }) - } -} - -function createNewVaultAndKeychain (password) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.createNewVaultAndKeychain`) - background.createNewVaultAndKeychain(password, (err) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - log.debug(`background.placeSeedWords`) - background.placeSeedWords((err) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch(actions.hideLoadingIndication()) - forceUpdateMetamaskState(dispatch) - }) - }) - } -} - -function revealSeedConfirmation () { - return { - type: this.REVEAL_SEED_CONFIRMATION, - } -} - -function requestRevealSeed (password) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.submitPassword`) - background.submitPassword(password, (err) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - log.debug(`background.placeSeedWords`) - background.placeSeedWords((err, result) => { - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.hideLoadingIndication()) - dispatch(actions.showNewVaultSeed(result)) - }) - }) - } -} - -function addNewKeyring (type, opts) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.addNewKeyring`) - background.addNewKeyring(type, opts, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.showAccountsPage()) - }) - } -} - -function importNewAccount (strategy, args) { - return (dispatch) => { - dispatch(actions.showLoadingIndication('This may take a while, be patient.')) - log.debug(`background.importAccountWithStrategy`) - background.importAccountWithStrategy(strategy, args, (err) => { - if (err) return dispatch(actions.displayWarning(err.message)) - log.debug(`background.getState`) - background.getState((err, newState) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch(actions.updateMetamaskState(newState)) - dispatch({ - type: actions.SHOW_ACCOUNT_DETAIL, - value: newState.selectedAddress, - }) - }) - }) - } -} - -function navigateToNewAccountScreen () { - return { - type: this.NEW_ACCOUNT_SCREEN, - } -} - -function addNewAccount () { - log.debug(`background.addNewAccount`) - return callBackgroundThenUpdate(background.addNewAccount) -} - -function showInfoPage () { - return { - type: actions.SHOW_INFO_PAGE, - } -} - -function setCurrentCurrency (currencyCode) { - return (dispatch) => { - dispatch(this.showLoadingIndication()) - log.debug(`background.setCurrentCurrency`) - background.setCurrentCurrency(currencyCode, (err, data) => { - dispatch(this.hideLoadingIndication()) - if (err) { - log.error(err.stack) - return dispatch(actions.displayWarning(err.message)) - } - dispatch({ - type: this.SET_CURRENT_FIAT, - value: { - currentCurrency: data.currentCurrency, - conversionRate: data.conversionRate, - conversionDate: data.conversionDate, - }, - }) - }) - } -} - -function signMsg (msgData) { - log.debug('action - signMsg') - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - - log.debug(`actions calling background.signMessage`) - background.signMessage(msgData, (err, newState) => { - log.debug('signMessage called back') - dispatch(actions.updateMetamaskState(newState)) - dispatch(actions.hideLoadingIndication()) - - if (err) log.error(err) - if (err) return dispatch(actions.displayWarning(err.message)) - - dispatch(actions.completedTx(msgData.metamaskId)) - }) - } -} - -function signPersonalMsg (msgData) { - log.debug('action - signPersonalMsg') - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - - log.debug(`actions calling background.signPersonalMessage`) - background.signPersonalMessage(msgData, (err, newState) => { - log.debug('signPersonalMessage called back') - dispatch(actions.updateMetamaskState(newState)) - dispatch(actions.hideLoadingIndication()) - - if (err) log.error(err) - if (err) return dispatch(actions.displayWarning(err.message)) - - dispatch(actions.completedTx(msgData.metamaskId)) - }) - } -} - -function signTx (txData) { - return (dispatch) => { - global.ethQuery.sendTransaction(txData, (err, data) => { - dispatch(actions.hideLoadingIndication()) - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.hideWarning()) - }) - dispatch(this.showConfTxPage()) - } -} - -function sendTx (txData) { - log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`) - return (dispatch) => { - log.debug(`actions calling background.approveTransaction`) - background.approveTransaction(txData.id, (err) => { - if (err) { - dispatch(actions.txError(err)) - return log.error(err.message) - } - dispatch(actions.completedTx(txData.id)) - }) - } -} - -function updateAndApproveTx (txData) { - log.info('actions: updateAndApproveTx: ' + JSON.stringify(txData)) - return (dispatch) => { - log.debug(`actions calling background.updateAndApproveTx`) - background.updateAndApproveTransaction(txData, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - dispatch(actions.txError(err)) - return log.error(err.message) - } - dispatch(actions.completedTx(txData.id)) - }) - } -} - -function completedTx (id) { - return { - type: actions.COMPLETED_TX, - value: id, - } -} - -function txError (err) { - return { - type: actions.TRANSACTION_ERROR, - message: err.message, - } -} - -function cancelMsg (msgData) { - log.debug(`background.cancelMessage`) - background.cancelMessage(msgData.id) - return actions.completedTx(msgData.id) -} - -function cancelPersonalMsg (msgData) { - const id = msgData.id - background.cancelPersonalMessage(id) - return actions.completedTx(id) -} - -function cancelTx (txData) { - log.debug(`background.cancelTransaction`) - background.cancelTransaction(txData.id) - return actions.completedTx(txData.id) -} - -// -// initialize screen -// - -function showCreateVault () { - return { - type: actions.SHOW_CREATE_VAULT, - } -} - -function showRestoreVault () { - return { - type: actions.SHOW_RESTORE_VAULT, - } -} - -function forgotPassword () { - return { - type: actions.FORGOT_PASSWORD, - } -} - -function showInitializeMenu () { - return { - type: actions.SHOW_INIT_MENU, - } -} - -function showImportPage () { - return { - type: actions.SHOW_IMPORT_PAGE, - } -} - -function createNewVaultInProgress () { - return { - type: actions.CREATE_NEW_VAULT_IN_PROGRESS, - } -} - -function showNewVaultSeed (seed) { - return { - type: actions.SHOW_NEW_VAULT_SEED, - value: seed, - } -} - -function backToUnlockView () { - return { - type: actions.BACK_TO_UNLOCK_VIEW, - } -} - -function showNewKeychain () { - return { - type: actions.SHOW_NEW_KEYCHAIN, - } -} - -// -// unlock screen -// - -function unlockInProgress () { - return { - type: actions.UNLOCK_IN_PROGRESS, - } -} - -function unlockFailed (message) { - return { - type: actions.UNLOCK_FAILED, - value: message, - } -} - -function unlockMetamask (account) { - return { - type: actions.UNLOCK_METAMASK, - value: account, - } -} - -function updateMetamaskState (newState) { - return { - type: actions.UPDATE_METAMASK_STATE, - value: newState, - } -} - -function lockMetamask () { - log.debug(`background.setLocked`) - return callBackgroundThenUpdate(background.setLocked) -} - -function setCurrentAccountTab (newTabName) { - log.debug(`background.setCurrentAccountTab: ${newTabName}`) - return callBackgroundThenUpdateNoSpinner(background.setCurrentAccountTab, newTabName) -} - -function showAccountDetail (address) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.setSelectedAddress`) - background.setSelectedAddress(address, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch({ - type: actions.SHOW_ACCOUNT_DETAIL, - value: address, - }) - }) - } -} - -function backToAccountDetail (address) { - return { - type: actions.BACK_TO_ACCOUNT_DETAIL, - value: address, - } -} - -function showAccountsPage () { - return { - type: actions.SHOW_ACCOUNTS_PAGE, - } -} - -function showConfTxPage (transForward = true) { - return { - type: actions.SHOW_CONF_TX_PAGE, - transForward: transForward, - } -} - -function nextTx () { - return { - type: actions.NEXT_TX, - } -} - -function viewPendingTx (txId) { - return { - type: actions.VIEW_PENDING_TX, - value: txId, - } -} - -function previousTx () { - return { - type: actions.PREVIOUS_TX, - } -} - -function showConfigPage (transitionForward = true) { - return { - type: actions.SHOW_CONFIG_PAGE, - value: transitionForward, - } -} - -function showAddTokenPage (transitionForward = true) { - return { - type: actions.SHOW_ADD_TOKEN_PAGE, - value: transitionForward, - } -} - -function addToken (address, symbol, decimals) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - background.addToken(address, symbol, decimals, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - setTimeout(() => { - dispatch(actions.goHome()) - }, 250) - }) - } -} - -function goBackToInitView () { - return { - type: actions.BACK_TO_INIT_MENU, - } -} - -// -// notice -// - -function markNoticeRead (notice) { - return (dispatch) => { - dispatch(this.showLoadingIndication()) - log.debug(`background.markNoticeRead`) - background.markNoticeRead(notice, (err, notice) => { - dispatch(this.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err)) - } - if (notice) { - return dispatch(actions.showNotice(notice)) - } else { - dispatch(this.clearNotices()) - return { - type: actions.SHOW_ACCOUNTS_PAGE, - } - } - }) - } -} - -function showNotice (notice) { - return { - type: actions.SHOW_NOTICE, - value: notice, - } -} - -function clearNotices () { - return { - type: actions.CLEAR_NOTICES, - } -} - -function markAccountsFound () { - log.debug(`background.markAccountsFound`) - return callBackgroundThenUpdate(background.markAccountsFound) -} - -// -// config -// - -// default rpc target refers to localhost:8545 in this instance. -function setDefaultRpcTarget (rpcList) { - log.debug(`background.setDefaultRpcTarget`) - return (dispatch) => { - background.setDefaultRpc((err, result) => { - if (err) { - log.error(err) - return dispatch(self.displayWarning('Had a problem changing networks.')) - } - }) - } -} - -function setRpcTarget (newRpc) { - log.debug(`background.setRpcTarget`) - return (dispatch) => { - background.setCustomRpc(newRpc, (err, result) => { - if (err) { - log.error(err) - return dispatch(self.displayWarning('Had a problem changing networks!')) - } - }) - } -} - -// Calls the addressBookController to add a new address. -function addToAddressBook (recipient, nickname) { - log.debug(`background.addToAddressBook`) - return (dispatch) => { - background.setAddressBook(recipient, nickname, (err, result) => { - if (err) { - log.error(err) - return dispatch(self.displayWarning('Address book failed to update')) - } - }) - } -} - -function setProviderType (type) { - log.debug(`background.setProviderType`) - background.setProviderType(type) - return { - type: actions.SET_PROVIDER_TYPE, - value: type, - } -} - -function useEtherscanProvider () { - log.debug(`background.useEtherscanProvider`) - background.useEtherscanProvider() - return { - type: actions.USE_ETHERSCAN_PROVIDER, - } -} - -function showLoadingIndication (message) { - return { - type: actions.SHOW_LOADING, - value: message, - } -} - -function hideLoadingIndication () { - return { - type: actions.HIDE_LOADING, - } -} - -function showSubLoadingIndication () { - return { - type: actions.SHOW_SUB_LOADING_INDICATION, - } -} - -function hideSubLoadingIndication () { - return { - type: actions.HIDE_SUB_LOADING_INDICATION, - } -} - -function displayWarning (text) { - return { - type: actions.DISPLAY_WARNING, - value: text, - } -} - -function hideWarning () { - return { - type: actions.HIDE_WARNING, - } -} - -function requestExportAccount () { - return { - type: actions.REQUEST_ACCOUNT_EXPORT, - } -} - -function exportAccount (password, address) { - var self = this - - return function (dispatch) { - dispatch(self.showLoadingIndication()) - - log.debug(`background.submitPassword`) - background.submitPassword(password, function (err) { - if (err) { - log.error('Error in submiting password.') - dispatch(self.hideLoadingIndication()) - return dispatch(self.displayWarning('Incorrect Password.')) - } - log.debug(`background.exportAccount`) - background.exportAccount(address, function (err, result) { - dispatch(self.hideLoadingIndication()) - - if (err) { - log.error(err) - return dispatch(self.displayWarning('Had a problem exporting the account.')) - } - - dispatch(self.showPrivateKey(result)) - }) - }) - } -} - -function showPrivateKey (key) { - return { - type: actions.SHOW_PRIVATE_KEY, - value: key, - } -} - -function saveAccountLabel (account, label) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.saveAccountLabel`) - background.saveAccountLabel(account, label, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch({ - type: actions.SAVE_ACCOUNT_LABEL, - value: { account, label }, - }) - }) - } -} - -function showSendPage () { - return { - type: actions.SHOW_SEND_PAGE, - } -} - -function buyEth (opts) { - return (dispatch) => { - const url = getBuyEthUrl(opts) - global.platform.openWindow({ url }) - dispatch({ - type: actions.BUY_ETH, - }) - } -} - -function buyEthView (address) { - return { - type: actions.BUY_ETH_VIEW, - value: address, - } -} - -function coinBaseSubview () { - return { - type: actions.COINBASE_SUBVIEW, - } -} - -function pairUpdate (coin) { - return (dispatch) => { - dispatch(actions.showSubLoadingIndication()) - dispatch(actions.hideWarning()) - shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { - dispatch(actions.hideSubLoadingIndication()) - dispatch({ - type: actions.PAIR_UPDATE, - value: { - marketinfo: mktResponse, - }, - }) - }) - } -} - -function shapeShiftSubview (network) { - var pair = 'btc_eth' - - return (dispatch) => { - dispatch(actions.showSubLoadingIndication()) - shapeShiftRequest('marketinfo', {pair}, (mktResponse) => { - shapeShiftRequest('getcoins', {}, (response) => { - dispatch(actions.hideSubLoadingIndication()) - if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) - dispatch({ - type: actions.SHAPESHIFT_SUBVIEW, - value: { - marketinfo: mktResponse, - coinOptions: response, - }, - }) - }) - }) - } -} - -function coinShiftRquest (data, marketData) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - shapeShiftRequest('shift', { method: 'POST', data}, (response) => { - dispatch(actions.hideLoadingIndication()) - if (response.error) return dispatch(actions.displayWarning(response.error)) - var message = ` - Deposit your ${response.depositType} to the address bellow:` - log.debug(`background.createShapeShiftTx`) - background.createShapeShiftTx(response.deposit, response.depositType) - dispatch(actions.showQrView(response.deposit, [message].concat(marketData))) - }) - } -} - -function showQrView (data, message) { - return { - type: actions.SHOW_QR_VIEW, - value: { - message: message, - data: data, - }, - } -} -function reshowQrCode (data, coin) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { - if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) - - var message = [ - `Deposit your ${coin} to the address bellow:`, - `Deposit Limit: ${mktResponse.limit}`, - `Deposit Minimum:${mktResponse.minimum}`, - ] - - dispatch(actions.hideLoadingIndication()) - return dispatch(actions.showQrView(data, message)) - }) - } -} - -function shapeShiftRequest (query, options, cb) { - var queryResponse, method - !options ? options = {} : null - options.method ? method = options.method : method = 'GET' - - var requestListner = function (request) { - queryResponse = JSON.parse(this.responseText) - cb ? cb(queryResponse) : null - return queryResponse - } - - var shapShiftReq = new XMLHttpRequest() - shapShiftReq.addEventListener('load', requestListner) - shapShiftReq.open(method, `https://shapeshift.io/${query}/${options.pair ? options.pair : ''}`, true) - - if (options.method === 'POST') { - var jsonObj = JSON.stringify(options.data) - shapShiftReq.setRequestHeader('Content-Type', 'application/json') - return shapShiftReq.send(jsonObj) - } else { - return shapShiftReq.send() - } -} - -// Call Background Then Update -// -// A function generator for a common pattern wherein: -// We show loading indication. -// We call a background method. -// We hide loading indication. -// If it errored, we show a warning. -// If it didn't, we update the state. -function callBackgroundThenUpdateNoSpinner (method, ...args) { - return (dispatch) => { - method.call(background, ...args, (err) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - forceUpdateMetamaskState(dispatch) - }) - } -} - -function callBackgroundThenUpdate (method, ...args) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - method.call(background, ...args, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - forceUpdateMetamaskState(dispatch) - }) - } -} - -function forceUpdateMetamaskState (dispatch) { - log.debug(`background.getState`) - background.getState((err, newState) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch(actions.updateMetamaskState(newState)) - }) -} diff --git a/ui/app/add-token.js b/ui/app/add-token.js deleted file mode 100644 index b303b5c0d..000000000 --- a/ui/app/add-token.js +++ /dev/null @@ -1,219 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') - -const ethUtil = require('ethereumjs-util') -const abi = require('human-standard-token-abi') -const Eth = require('ethjs-query') -const EthContract = require('ethjs-contract') - -const emptyAddr = '0x0000000000000000000000000000000000000000' - -module.exports = connect(mapStateToProps)(AddTokenScreen) - -function mapStateToProps (state) { - return { - } -} - -inherits(AddTokenScreen, Component) -function AddTokenScreen () { - this.state = { - warning: null, - address: null, - symbol: 'TOKEN', - decimals: 18, - } - Component.call(this) -} - -AddTokenScreen.prototype.render = function () { - const state = this.state - const props = this.props - const { warning, symbol, decimals } = state - - return ( - h('.flex-column.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: (event) => { - props.dispatch(actions.goHome()) - }, - }), - h('h2.page-subtitle', 'Add Token'), - ]), - - h('.error', { - style: { - display: warning ? 'block' : 'none', - padding: '0 20px', - textAlign: 'center', - }, - }, warning), - - // conf view - h('.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-space-around', { - style: { - padding: '20px', - }, - }, [ - - h('div', [ - h('span', { - style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Token Address'), - ]), - - h('section.flex-row.flex-center', [ - h('input#token-address', { - name: 'address', - placeholder: 'Token Address', - onChange: this.tokenAddressDidChange.bind(this), - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - }), - ]), - - h('div', [ - h('span', { - style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Token Sybmol'), - ]), - - h('div', { style: {display: 'flex'} }, [ - h('input#token_symbol', { - placeholder: `Like "ETH"`, - value: symbol, - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - onChange: (event) => { - var element = event.target - var symbol = element.value - this.setState({ symbol }) - }, - }), - ]), - - h('div', [ - h('span', { - style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Decimals of Precision'), - ]), - - h('div', { style: {display: 'flex'} }, [ - h('input#token_decimals', { - value: decimals, - type: 'number', - min: 0, - max: 36, - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - onChange: (event) => { - var element = event.target - var decimals = element.value.trim() - this.setState({ decimals }) - }, - }), - ]), - - h('button', { - style: { - alignSelf: 'center', - }, - onClick: (event) => { - const valid = this.validateInputs() - if (!valid) return - - const { address, symbol, decimals } = this.state - this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) - }, - }, 'Add'), - ]), - ]), - ]) - ) -} - -AddTokenScreen.prototype.componentWillMount = function () { - if (typeof global.ethereumProvider === 'undefined') return - - this.eth = new Eth(global.ethereumProvider) - this.contract = new EthContract(this.eth) - this.TokenContract = this.contract(abi) -} - -AddTokenScreen.prototype.tokenAddressDidChange = function (event) { - const el = event.target - const address = el.value.trim() - if (ethUtil.isValidAddress(address) && address !== emptyAddr) { - this.setState({ address }) - this.attemptToAutoFillTokenParams(address) - } -} - -AddTokenScreen.prototype.validateInputs = function () { - let msg = '' - const state = this.state - const { address, symbol, decimals } = state - - const validAddress = ethUtil.isValidAddress(address) - if (!validAddress) { - msg += 'Address is invalid. ' - } - - const validDecimals = decimals >= 0 && decimals < 36 - if (!validDecimals) { - msg += 'Decimals must be at least 0, and not over 36. ' - } - - const symbolLen = symbol.trim().length - const validSymbol = symbolLen > 0 && symbolLen < 10 - if (!validSymbol) { - msg += 'Symbol must be between 0 and 10 characters.' - } - - const isValid = validAddress && validDecimals - - if (!isValid) { - this.setState({ - warning: msg, - }) - } else { - this.setState({ warning: null }) - } - - return isValid -} - -AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) { - const contract = this.TokenContract.at(address) - - const results = await Promise.all([ - contract.symbol(), - contract.decimals(), - ]) - - const [ symbol, decimals ] = results - if (symbol && decimals) { - console.log('SETTING SYMBOL AND DECIMALS', { symbol, decimals }) - this.setState({ symbol: symbol[0], decimals: decimals[0].toString() }) - } -} - diff --git a/ui/app/app.js b/ui/app/app.js deleted file mode 100644 index 1a63002e1..000000000 --- a/ui/app/app.js +++ /dev/null @@ -1,591 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('./actions') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') -// init -const InitializeMenuScreen = require('./first-time/init-menu') -const NewKeyChainScreen = require('./new-keychain') -// unlock -const UnlockScreen = require('./unlock') -// accounts -const AccountsScreen = require('./accounts') -const AccountDetailScreen = require('./account-detail') -const SendTransactionScreen = require('./send') -const ConfirmTxScreen = require('./conf-tx') -// notice -const NoticeScreen = require('./components/notice') -const generateLostAccountsNotice = require('../lib/lost-accounts-notice') -// other views -const ConfigScreen = require('./config') -const AddTokenScreen = require('./add-token') -const Import = require('./accounts/import') -const InfoScreen = require('./info') -const Loading = require('./components/loading') -const SandwichExpando = require('sandwich-expando') -const MenuDroppo = require('menu-droppo') -const DropMenuItem = require('./components/drop-menu-item') -const NetworkIndicator = require('./components/network') -const Tooltip = require('./components/tooltip') -const BuyView = require('./components/buy-button-subview') -const QrView = require('./components/qr-code') -const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete') -const HDRestoreVaultScreen = require('./keychains/hd/restore-vault') -const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation') - -module.exports = connect(mapStateToProps)(App) - -inherits(App, Component) -function App () { Component.call(this) } - -function mapStateToProps (state) { - return { - // state from plugin - isLoading: state.appState.isLoading, - loadingMessage: state.appState.loadingMessage, - noActiveNotices: state.metamask.noActiveNotices, - isInitialized: state.metamask.isInitialized, - isUnlocked: state.metamask.isUnlocked, - currentView: state.appState.currentView, - activeAddress: state.appState.activeAddress, - transForward: state.appState.transForward, - seedWords: state.metamask.seedWords, - unapprovedTxs: state.metamask.unapprovedTxs, - unapprovedMsgs: state.metamask.unapprovedMsgs, - menuOpen: state.appState.menuOpen, - network: state.metamask.network, - provider: state.metamask.provider, - forgottenPassword: state.appState.forgottenPassword, - lastUnreadNotice: state.metamask.lastUnreadNotice, - lostAccounts: state.metamask.lostAccounts, - frequentRpcList: state.metamask.frequentRpcList || [], - } -} - -App.prototype.render = function () { - var props = this.props - const { isLoading, loadingMessage, transForward, network } = props - const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config' - const loadMessage = loadingMessage || isLoadingNetwork ? - `Connecting to ${this.getNetworkName()}` : null - - log.debug('Main ui render function') - - return ( - - h('.flex-column.flex-grow.full-height', { - style: { - // Windows was showing a vertical scroll bar: - overflow: 'hidden', - position: 'relative', - }, - }, [ - - // app bar - this.renderAppBar(), - this.renderNetworkDropdown(), - this.renderDropdown(), - - h(Loading, { - isLoading: isLoading || isLoadingNetwork, - loadingMessage: loadMessage, - }), - - // panel content - h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), { - style: { - height: '380px', - width: '360px', - }, - }, [ - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.renderPrimary(), - ]), - ]), - ]) - ) -} - -App.prototype.renderAppBar = function () { - if (window.METAMASK_UI_TYPE === 'notification') { - return null - } - - const props = this.props - const state = this.state || {} - const isNetworkMenuOpen = state.isNetworkMenuOpen || false - - return ( - - h('div', [ - - h('.app-header.flex-row.flex-space-between', { - style: { - alignItems: 'center', - visibility: props.isUnlocked ? 'visible' : 'none', - background: props.isUnlocked ? 'white' : 'none', - height: '36px', - position: 'relative', - zIndex: 12, - }, - }, [ - - h('div.left-menu-section', { - style: { - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - }, - }, [ - - // mini logo - h('img', { - height: 24, - width: 24, - src: '/images/icon-128.png', - }), - - h(NetworkIndicator, { - network: this.props.network, - provider: this.props.provider, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen }) - }, - }), - ]), - - // metamask name - props.isUnlocked && h('h1', { - style: { - position: 'relative', - left: '9px', - }, - }, 'MetaMask'), - - props.isUnlocked && h('div', { - style: { - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - }, - }, [ - - // small accounts nav - props.isUnlocked && h(Tooltip, { title: 'Switch Accounts' }, [ - h('img.cursor-pointer.color-orange', { - src: 'images/switch_acc.svg', - style: { - width: '23.5px', - marginRight: '8px', - }, - onClick: (event) => { - event.stopPropagation() - this.props.dispatch(actions.showAccountsPage()) - }, - }), - ]), - - // hamburger - props.isUnlocked && h(SandwichExpando, { - width: 16, - barHeight: 2, - padding: 0, - isOpen: state.isMainMenuOpen, - color: 'rgb(247,146,30)', - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - this.setState({ isMainMenuOpen: !state.isMainMenuOpen }) - }, - }), - ]), - ]), - ]) - ) -} - -App.prototype.renderNetworkDropdown = function () { - const props = this.props - const rpcList = props.frequentRpcList - const state = this.state || {} - const isOpen = state.isNetworkMenuOpen - - return h(MenuDroppo, { - isOpen, - onClickOutside: (event) => { - this.setState({ isNetworkMenuOpen: !isOpen }) - }, - zIndex: 11, - style: { - position: 'absolute', - left: 0, - top: '36px', - }, - innerStyle: { - background: 'white', - boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', - }, - }, [ // DROP MENU ITEMS - h('style', ` - .drop-menu-item:hover { background:rgb(235, 235, 235); } - .drop-menu-item i { margin: 11px; } - `), - - h(DropMenuItem, { - label: 'Main Ethereum Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setProviderType('mainnet')), - icon: h('.menu-icon.diamond'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Ropsten Test Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setProviderType('ropsten')), - icon: h('.menu-icon.red-dot'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Kovan Test Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false}), - action: () => props.dispatch(actions.setProviderType('kovan')), - icon: h('.menu-icon.hollow-diamond'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Rinkeby Test Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false}), - action: () => props.dispatch(actions.setProviderType('rinkeby')), - icon: h('.menu-icon.golden-square'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Localhost 8545', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setDefaultRpcTarget(rpcList)), - icon: h('i.fa.fa-question-circle.fa-lg'), - activeNetworkRender: props.provider.rpcTarget, - }), - - this.renderCustomOption(props.provider), - this.renderCommonRpc(rpcList, props.provider), - - h(DropMenuItem, { - label: 'Custom RPC', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => this.props.dispatch(actions.showConfigPage()), - icon: h('i.fa.fa-question-circle.fa-lg'), - }), - - ]) -} - -App.prototype.renderDropdown = function () { - const state = this.state || {} - const isOpen = state.isMainMenuOpen - - return h(MenuDroppo, { - isOpen: isOpen, - zIndex: 11, - onClickOutside: (event) => { - this.setState({ isMainMenuOpen: !isOpen }) - }, - style: { - position: 'absolute', - right: 0, - top: '36px', - }, - innerStyle: { - background: 'white', - boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', - }, - }, [ // DROP MENU ITEMS - h('style', ` - .drop-menu-item:hover { background:rgb(235, 235, 235); } - .drop-menu-item i { margin: 11px; } - `), - - h(DropMenuItem, { - label: 'Settings', - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.showConfigPage()), - icon: h('i.fa.fa-gear.fa-lg'), - }), - - h(DropMenuItem, { - label: 'Import Account', - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.showImportPage()), - icon: h('i.fa.fa-arrow-circle-o-up.fa-lg'), - }), - - h(DropMenuItem, { - label: 'Lock', - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.lockMetamask()), - icon: h('i.fa.fa-lock.fa-lg'), - }), - - h(DropMenuItem, { - label: 'Info/Help', - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.showInfoPage()), - icon: h('i.fa.fa-question.fa-lg'), - }), - ]) -} - -App.prototype.renderBackButton = function (style, justArrow = false) { - var props = this.props - return ( - h('.flex-row', { - key: 'leftArrow', - style: style, - onClick: () => props.dispatch(actions.goBackToInitView()), - }, [ - h('i.fa.fa-arrow-left.cursor-pointer'), - justArrow ? null : h('div.cursor-pointer', { - style: { - marginLeft: '3px', - }, - onClick: () => props.dispatch(actions.goBackToInitView()), - }, 'BACK'), - ]) - ) -} - -App.prototype.renderPrimary = function () { - log.debug('rendering primary') - var props = this.props - - // notices - if (!props.noActiveNotices) { - log.debug('rendering notice screen for unread notices.') - return h(NoticeScreen, { - notice: props.lastUnreadNotice, - key: 'NoticeScreen', - onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)), - }) - } else if (props.lostAccounts && props.lostAccounts.length > 0) { - log.debug('rendering notice screen for lost accounts view.') - return h(NoticeScreen, { - notice: generateLostAccountsNotice(props.lostAccounts), - key: 'LostAccountsNotice', - onConfirm: () => props.dispatch(actions.markAccountsFound()), - }) - } - - if (props.seedWords) { - log.debug('rendering seed words') - return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'}) - } - - // show initialize screen - if (!props.isInitialized || props.forgottenPassword) { - // show current view - log.debug('rendering an initialize screen') - switch (props.currentView.name) { - - case 'restoreVault': - log.debug('rendering restore vault screen') - return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) - - default: - log.debug('rendering menu screen') - return h(InitializeMenuScreen, {key: 'menuScreenInit'}) - } - } - - // show unlock screen - if (!props.isUnlocked) { - switch (props.currentView.name) { - - case 'restoreVault': - log.debug('rendering restore vault screen') - return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) - - case 'config': - log.debug('rendering config screen from unlock screen.') - return h(ConfigScreen, {key: 'config'}) - - default: - log.debug('rendering locked screen') - return h(UnlockScreen, {key: 'locked'}) - } - } - - // show current view - switch (props.currentView.name) { - - case 'accounts': - log.debug('rendering accounts screen') - return h(AccountsScreen, {key: 'accounts'}) - - case 'accountDetail': - log.debug('rendering account detail screen') - return h(AccountDetailScreen, {key: 'account-detail'}) - - case 'sendTransaction': - log.debug('rendering send tx screen') - return h(SendTransactionScreen, {key: 'send-transaction'}) - - case 'newKeychain': - log.debug('rendering new keychain screen') - return h(NewKeyChainScreen, {key: 'new-keychain'}) - - case 'confTx': - log.debug('rendering confirm tx screen') - return h(ConfirmTxScreen, {key: 'confirm-tx'}) - - case 'add-token': - log.debug('rendering add-token screen from unlock screen.') - return h(AddTokenScreen, {key: 'add-token'}) - - case 'config': - log.debug('rendering config screen') - return h(ConfigScreen, {key: 'config'}) - - case 'import-menu': - log.debug('rendering import screen') - return h(Import, {key: 'import-menu'}) - - case 'reveal-seed-conf': - log.debug('rendering reveal seed confirmation screen') - return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'}) - - case 'info': - log.debug('rendering info screen') - return h(InfoScreen, {key: 'info'}) - - case 'buyEth': - log.debug('rendering buy ether screen') - return h(BuyView, {key: 'buyEthView'}) - - case 'qr': - log.debug('rendering show qr screen') - return h('div', { - style: { - position: 'absolute', - height: '100%', - top: '0px', - left: '0px', - }, - }, [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { - onClick: () => props.dispatch(actions.backToAccountDetail(props.activeAddress)), - style: { - marginLeft: '10px', - marginTop: '50px', - }, - }), - h('div', { - style: { - position: 'absolute', - left: '44px', - width: '285px', - }, - }, [ - h(QrView, {key: 'qr'}), - ]), - ]) - - default: - log.debug('rendering default, account detail screen') - return h(AccountDetailScreen, {key: 'account-detail'}) - } -} - -App.prototype.toggleMetamaskActive = function () { - if (!this.props.isUnlocked) { - // currently inactive: redirect to password box - var passwordBox = document.querySelector('input[type=password]') - if (!passwordBox) return - passwordBox.focus() - } else { - // currently active: deactivate - this.props.dispatch(actions.lockMetamask(false)) - } -} - -App.prototype.renderCustomOption = function (provider) { - const { rpcTarget, type } = provider - if (type !== 'rpc') return null - - // Concatenate long URLs - let label = rpcTarget - if (rpcTarget.length > 31) { - label = label.substr(0, 34) + '...' - } - - switch (rpcTarget) { - - case 'http://localhost:8545': - return null - - default: - return h(DropMenuItem, { - label, - key: rpcTarget, - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - icon: h('i.fa.fa-question-circle.fa-lg'), - activeNetworkRender: 'custom', - }) - } -} - -App.prototype.getNetworkName = function () { - const { provider } = this.props - const providerName = provider.type - - let name - - if (providerName === 'mainnet') { - name = 'Main Ethereum Network' - } else if (providerName === 'ropsten') { - name = 'Ropsten Test Network' - } else if (providerName === 'kovan') { - name = 'Kovan Test Network' - } else if (providerName === 'rinkeby') { - name = 'Rinkeby Test Network' - } else { - name = 'Unknown Private Network' - } - - return name -} - -App.prototype.renderCommonRpc = function (rpcList, provider) { - const { rpcTarget } = provider - const props = this.props - - return rpcList.map((rpc) => { - if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { - return null - } else { - return h(DropMenuItem, { - label: rpc, - key: rpc, - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setRpcTarget(rpc)), - icon: h('i.fa.fa-question-circle.fa-lg'), - activeNetworkRender: rpc, - }) - } - }) -} diff --git a/ui/app/components/account-export.js b/ui/app/components/account-export.js deleted file mode 100644 index 394d878f7..000000000 --- a/ui/app/components/account-export.js +++ /dev/null @@ -1,122 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const copyToClipboard = require('copy-to-clipboard') -const actions = require('../actions') -const ethUtil = require('ethereumjs-util') -const connect = require('react-redux').connect - -module.exports = connect(mapStateToProps)(ExportAccountView) - -inherits(ExportAccountView, Component) -function ExportAccountView () { - Component.call(this) -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -ExportAccountView.prototype.render = function () { - var state = this.props - var accountDetail = state.accountDetail - - if (!accountDetail) return h('div') - var accountExport = accountDetail.accountExport - - var notExporting = accountExport === 'none' - var exportRequested = accountExport === 'requested' - var accountExported = accountExport === 'completed' - - if (notExporting) return h('div') - - if (exportRequested) { - var warning = `Export private keys at your own risk.` - return ( - h('div', { - style: { - display: 'inline-block', - textAlign: 'center', - }, - }, - [ - h('div', { - key: 'exporting', - style: { - margin: '0 20px', - }, - }, [ - h('p.error', warning), - h('input#exportAccount.sizing-input', { - type: 'password', - placeholder: 'confirm password', - onKeyPress: this.onExportKeyPress.bind(this), - style: { - position: 'relative', - top: '1.5px', - marginBottom: '7px', - }, - }), - ]), - h('div', { - key: 'buttons', - style: { - margin: '0 20px', - }, - }, - [ - h('button', { - onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }), - style: { - marginRight: '10px', - }, - }, 'Submit'), - h('button', { - onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), - }, 'Cancel'), - ]), - (this.props.warning) && ( - h('span.error', { - style: { - margin: '20px', - }, - }, this.props.warning.split('-')) - ), - ]) - ) - } - - if (accountExported) { - return h('div.privateKey', { - style: { - margin: '0 20px', - }, - }, [ - h('label', 'Your private key (click to copy):'), - h('p.error.cursor-pointer', { - style: { - textOverflow: 'ellipsis', - overflow: 'hidden', - webkitUserSelect: 'text', - width: '100%', - }, - onClick: function (event) { - copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey)) - }, - }, ethUtil.stripHexPrefix(accountDetail.privateKey)), - h('button', { - onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), - }, 'Done'), - ]) - } -} - -ExportAccountView.prototype.onExportKeyPress = function (event) { - if (event.key !== 'Enter') return - event.preventDefault() - - var input = document.getElementById('exportAccount').value - this.props.dispatch(actions.exportAccount(input, this.props.address)) -} diff --git a/ui/app/components/account-info-link.js b/ui/app/components/account-info-link.js deleted file mode 100644 index 6526ab502..000000000 --- a/ui/app/components/account-info-link.js +++ /dev/null @@ -1,41 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const Tooltip = require('./tooltip') -const genAccountLink = require('../../lib/account-link') - -module.exports = AccountInfoLink - -inherits(AccountInfoLink, Component) -function AccountInfoLink () { - Component.call(this) -} - -AccountInfoLink.prototype.render = function () { - const { selected, network } = this.props - const title = 'View account on Etherscan' - const url = genAccountLink(selected, network) - - if (!url) { - return null - } - - return h('.account-info-link', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - - h(Tooltip, { - title, - }, [ - h('i.fa.fa-info-circle.cursor-pointer.color-orange', { - style: { - margin: '5px', - }, - onClick () { global.platform.openWindow({ url }) }, - }), - ]), - ]) -} diff --git a/ui/app/components/account-panel.js b/ui/app/components/account-panel.js deleted file mode 100644 index abaaf8163..000000000 --- a/ui/app/components/account-panel.js +++ /dev/null @@ -1,86 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const Identicon = require('./identicon') -const formatBalance = require('../util').formatBalance -const addressSummary = require('../util').addressSummary - -module.exports = AccountPanel - - -inherits(AccountPanel, Component) -function AccountPanel () { - Component.call(this) -} - -AccountPanel.prototype.render = function () { - var state = this.props - var identity = state.identity || {} - var account = state.account || {} - var isFauceting = state.isFauceting - - var panelState = { - key: `accountPanel${identity.address}`, - identiconKey: identity.address, - identiconLabel: identity.name || '', - attributes: [ - { - key: 'ADDRESS', - value: addressSummary(identity.address), - }, - balanceOrFaucetingIndication(account, isFauceting), - ], - } - - return ( - - h('.identity-panel.flex-row.flex-space-between', { - style: { - flex: '1 0 auto', - cursor: panelState.onClick ? 'pointer' : undefined, - }, - onClick: panelState.onClick, - }, [ - - // account identicon - h('.identicon-wrapper.flex-column.select-none', [ - h(Identicon, { - address: panelState.identiconKey, - imageify: state.imageifyIdenticons, - }), - h('span.font-small', panelState.identiconLabel.substring(0, 7) + '...'), - ]), - - // account address, balance - h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [ - - panelState.attributes.map((attr) => { - return h('.flex-row.flex-space-between', { - key: '' + Math.round(Math.random() * 1000000), - }, [ - h('label.font-small.no-select', attr.key), - h('span.font-small', attr.value), - ]) - }), - ]), - - ]) - - ) -} - -function balanceOrFaucetingIndication (account, isFauceting) { - // Temporarily deactivating isFauceting indication - // because it shows fauceting for empty restored accounts. - if (/* isFauceting*/ false) { - return { - key: 'Account is auto-funding.', - value: 'Please wait.', - } - } else { - return { - key: 'BALANCE', - value: formatBalance(account.balance), - } - } -} diff --git a/ui/app/components/balance.js b/ui/app/components/balance.js deleted file mode 100644 index 57ca84564..000000000 --- a/ui/app/components/balance.js +++ /dev/null @@ -1,89 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const formatBalance = require('../util').formatBalance -const generateBalanceObject = require('../util').generateBalanceObject -const Tooltip = require('./tooltip.js') -const FiatValue = require('./fiat-value.js') - -module.exports = EthBalanceComponent - -inherits(EthBalanceComponent, Component) -function EthBalanceComponent () { - Component.call(this) -} - -EthBalanceComponent.prototype.render = function () { - var props = this.props - let { value } = props - var style = props.style - var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true - value = value ? formatBalance(value, 6, needsParse) : '...' - var width = props.width - - return ( - - h('.ether-balance.ether-balance-amount', { - style: style, - }, [ - h('div', { - style: { - display: 'inline', - width: width, - }, - }, this.renderBalance(value)), - ]) - - ) -} -EthBalanceComponent.prototype.renderBalance = function (value) { - var props = this.props - if (value === 'None') return value - if (value === '...') return value - var balanceObj = generateBalanceObject(value, props.shorten ? 1 : 3) - var balance - var splitBalance = value.split(' ') - var ethNumber = splitBalance[0] - var ethSuffix = splitBalance[1] - const showFiat = 'showFiat' in props ? props.showFiat : true - - if (props.shorten) { - balance = balanceObj.shortBalance - } else { - balance = balanceObj.balance - } - - var label = balanceObj.label - - return ( - h(Tooltip, { - position: 'bottom', - title: `${ethNumber} ${ethSuffix}`, - }, h('div.flex-column', [ - h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('div', { - style: { - width: '100%', - textAlign: 'right', - }, - }, this.props.incoming ? `+${balance}` : balance), - h('div', { - style: { - color: ' #AEAEAE', - fontSize: '12px', - marginLeft: '5px', - }, - }, label), - ]), - - showFiat ? h(FiatValue, { value: props.value }) : null, - ])) - ) -} diff --git a/ui/app/components/binary-renderer.js b/ui/app/components/binary-renderer.js deleted file mode 100644 index 0b6a1f5c2..000000000 --- a/ui/app/components/binary-renderer.js +++ /dev/null @@ -1,46 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ethUtil = require('ethereumjs-util') -const extend = require('xtend') - -module.exports = BinaryRenderer - -inherits(BinaryRenderer, Component) -function BinaryRenderer () { - Component.call(this) -} - -BinaryRenderer.prototype.render = function () { - const props = this.props - const { value, style } = props - const text = this.hexToText(value) - - const defaultStyle = extend({ - width: '315px', - maxHeight: '210px', - resize: 'none', - border: 'none', - background: 'white', - padding: '3px', - }, style) - - return ( - h('textarea.font-small', { - readOnly: true, - style: defaultStyle, - defaultValue: text, - }) - ) -} - -BinaryRenderer.prototype.hexToText = function (hex) { - try { - const stripped = ethUtil.stripHexPrefix(hex) - const buff = Buffer.from(stripped, 'hex') - return buff.toString('utf8') - } catch (e) { - return hex - } -} - diff --git a/ui/app/components/bn-as-decimal-input.js b/ui/app/components/bn-as-decimal-input.js deleted file mode 100644 index f3ace4720..000000000 --- a/ui/app/components/bn-as-decimal-input.js +++ /dev/null @@ -1,174 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const extend = require('xtend') - -module.exports = BnAsDecimalInput - -inherits(BnAsDecimalInput, Component) -function BnAsDecimalInput () { - this.state = { invalid: null } - Component.call(this) -} - -/* Bn as Decimal Input - * - * A component for allowing easy, decimal editing - * of a passed in bn string value. - * - * On change, calls back its `onChange` function parameter - * and passes it an updated bn string. - */ - -BnAsDecimalInput.prototype.render = function () { - const props = this.props - const state = this.state - - const { value, scale, precision, onChange, min, max } = props - - const suffix = props.suffix - const style = props.style - const valueString = value.toString(10) - const newValue = this.downsize(valueString, scale, precision) - - return ( - h('.flex-column', [ - h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('input.hex-input', { - type: 'number', - step: 'any', - required: true, - min, - max, - style: extend({ - display: 'block', - textAlign: 'right', - backgroundColor: 'transparent', - border: '1px solid #bdbdbd', - - }, style), - value: newValue, - onBlur: (event) => { - this.updateValidity(event) - }, - onChange: (event) => { - this.updateValidity(event) - const value = (event.target.value === '') ? '' : event.target.value - - - const scaledNumber = this.upsize(value, scale, precision) - const precisionBN = new BN(scaledNumber, 10) - onChange(precisionBN, event.target.checkValidity()) - }, - onInvalid: (event) => { - const msg = this.constructWarning() - if (msg === state.invalid) { - return - } - this.setState({ invalid: msg }) - event.preventDefault() - return false - }, - }), - h('div', { - style: { - color: ' #AEAEAE', - fontSize: '12px', - marginLeft: '5px', - marginRight: '6px', - width: '20px', - }, - }, suffix), - ]), - - state.invalid ? h('span.error', { - style: { - position: 'absolute', - right: '0px', - textAlign: 'right', - transform: 'translateY(26px)', - padding: '3px', - background: 'rgba(255,255,255,0.85)', - zIndex: '1', - textTransform: 'capitalize', - border: '2px solid #E20202', - }, - }, state.invalid) : null, - ]) - ) -} - -BnAsDecimalInput.prototype.setValid = function (message) { - this.setState({ invalid: null }) -} - -BnAsDecimalInput.prototype.updateValidity = function (event) { - const target = event.target - const value = this.props.value - const newValue = target.value - - if (value === newValue) { - return - } - - const valid = target.checkValidity() - - if (valid) { - this.setState({ invalid: null }) - } -} - -BnAsDecimalInput.prototype.constructWarning = function () { - const { name, min, max } = this.props - let message = name ? name + ' ' : '' - - if (min && max) { - message += `must be greater than or equal to ${min} and less than or equal to ${max}.` - } else if (min) { - message += `must be greater than or equal to ${min}.` - } else if (max) { - message += `must be less than or equal to ${max}.` - } else { - message += 'Invalid input.' - } - - return message -} - - -BnAsDecimalInput.prototype.downsize = function (number, scale, precision) { - // if there is no scaling, simply return the number - if (scale === 0) { - return Number(number) - } else { - // if the scale is the same as the precision, account for this edge case. - var decimals = (scale === precision) ? -1 : scale - precision - return Number(number.slice(0, -scale) + '.' + number.slice(-scale, decimals)) - } -} - -BnAsDecimalInput.prototype.upsize = function (number, scale, precision) { - var stringArray = number.toString().split('.') - var decimalLength = stringArray[1] ? stringArray[1].length : 0 - var newString = stringArray[0] - - // If there is scaling and decimal parts exist, integrate them in. - if ((scale !== 0) && (decimalLength !== 0)) { - newString += stringArray[1].slice(0, precision) - } - - // Add 0s to account for the upscaling. - for (var i = decimalLength; i < scale; i++) { - newString += '0' - } - return newString -} diff --git a/ui/app/components/buy-button-subview.js b/ui/app/components/buy-button-subview.js deleted file mode 100644 index 87084f92d..000000000 --- a/ui/app/components/buy-button-subview.js +++ /dev/null @@ -1,197 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const connect = require('react-redux').connect -const actions = require('../actions') -const CoinbaseForm = require('./coinbase-form') -const ShapeshiftForm = require('./shapeshift-form') -const Loading = require('./loading') -const AccountPanel = require('./account-panel') -const RadioList = require('./custom-radio-list') - -module.exports = connect(mapStateToProps)(BuyButtonSubview) - -function mapStateToProps (state) { - return { - identity: state.appState.identity, - account: state.metamask.accounts[state.appState.buyView.buyAddress], - warning: state.appState.warning, - buyView: state.appState.buyView, - network: state.metamask.network, - provider: state.metamask.provider, - context: state.appState.currentView.context, - isSubLoading: state.appState.isSubLoading, - } -} - -inherits(BuyButtonSubview, Component) -function BuyButtonSubview () { - Component.call(this) -} - -BuyButtonSubview.prototype.render = function () { - const props = this.props - const isLoading = props.isSubLoading - - return ( - h('.buy-eth-section.flex-column', { - style: { - alignItems: 'center', - }, - }, [ - // back button - h('.flex-row', { - style: { - alignItems: 'center', - justifyContent: 'center', - }, - }, [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { - onClick: this.backButtonContext.bind(this), - style: { - position: 'absolute', - left: '10px', - }, - }), - h('h2.text-transform-uppercase.flex-center', { - style: { - width: '100vw', - background: 'rgb(235, 235, 235)', - color: 'rgb(174, 174, 174)', - paddingTop: '4px', - paddingBottom: '4px', - }, - }, 'Buy Eth'), - ]), - h('div', { - style: { - position: 'absolute', - top: '57vh', - left: '49vw', - }, - }, [ - h(Loading, {isLoading}), - ]), - h('div', { - style: { - width: '80%', - }, - }, [ - h(AccountPanel, { - showFullAddress: true, - identity: props.identity, - account: props.account, - }), - ]), - h('h3.text-transform-uppercase', { - style: { - paddingLeft: '15px', - fontFamily: 'Montserrat Light', - width: '100vw', - background: 'rgb(235, 235, 235)', - color: 'rgb(174, 174, 174)', - paddingTop: '4px', - paddingBottom: '4px', - }, - }, 'Select Service'), - h('.flex-row.selected-exchange', { - style: { - position: 'relative', - right: '35px', - marginTop: '20px', - marginBottom: '20px', - }, - }, [ - h(RadioList, { - defaultFocus: props.buyView.subview, - labels: [ - 'Coinbase', - 'ShapeShift', - ], - subtext: { - 'Coinbase': 'Crypto/FIAT (USA only)', - 'ShapeShift': 'Crypto', - }, - onClick: this.radioHandler.bind(this), - }), - ]), - h('h3.text-transform-uppercase', { - style: { - paddingLeft: '15px', - fontFamily: 'Montserrat Light', - width: '100vw', - background: 'rgb(235, 235, 235)', - color: 'rgb(174, 174, 174)', - paddingTop: '4px', - paddingBottom: '4px', - }, - }, props.buyView.subview), - this.formVersionSubview(), - ]) - ) -} - -BuyButtonSubview.prototype.formVersionSubview = function () { - const network = this.props.network - if (network === '1') { - if (this.props.buyView.formView.coinbase) { - return h(CoinbaseForm, this.props) - } else if (this.props.buyView.formView.shapeshift) { - return h(ShapeshiftForm, this.props) - } - } else { - return h('div.flex-column', { - style: { - alignItems: 'center', - margin: '50px', - }, - }, [ - h('h3.text-transform-uppercase', { - style: { - width: '225px', - marginBottom: '15px', - }, - }, 'In order to access this feature, please switch to the Main Network'), - ((network === '3') || (network === '4') || (network === '42')) ? h('h3.text-transform-uppercase', 'or go to the') : null, - (network === '3') ? h('button.text-transform-uppercase', { - onClick: () => this.props.dispatch(actions.buyEth({ network })), - style: { - marginTop: '15px', - }, - }, 'Ropsten Test Faucet') : null, - (network === '4') ? h('button.text-transform-uppercase', { - onClick: () => this.props.dispatch(actions.buyEth({ network })), - style: { - marginTop: '15px', - }, - }, 'Rinkeby Test Faucet') : null, - (network === '42') ? h('button.text-transform-uppercase', { - onClick: () => this.props.dispatch(actions.buyEth({ network })), - style: { - marginTop: '15px', - }, - }, 'Kovan Test Faucet') : null, - ]) - } -} - -BuyButtonSubview.prototype.navigateTo = function (url) { - global.platform.openWindow({ url }) -} - -BuyButtonSubview.prototype.backButtonContext = function () { - if (this.props.context === 'confTx') { - this.props.dispatch(actions.showConfTxPage(false)) - } else { - this.props.dispatch(actions.goHome()) - } -} - -BuyButtonSubview.prototype.radioHandler = function (event) { - switch (event.target.title) { - case 'Coinbase': - return this.props.dispatch(actions.coinBaseSubview()) - case 'ShapeShift': - return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type)) - } -} diff --git a/ui/app/components/coinbase-form.js b/ui/app/components/coinbase-form.js deleted file mode 100644 index f44d86045..000000000 --- a/ui/app/components/coinbase-form.js +++ /dev/null @@ -1,63 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const connect = require('react-redux').connect -const actions = require('../actions') - -module.exports = connect(mapStateToProps)(CoinbaseForm) - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -inherits(CoinbaseForm, Component) - -function CoinbaseForm () { - Component.call(this) -} - -CoinbaseForm.prototype.render = function () { - var props = this.props - - return h('.flex-column', { - style: { - marginTop: '35px', - padding: '25px', - width: '100%', - }, - }, [ - h('.flex-row', { - style: { - justifyContent: 'space-around', - margin: '33px', - marginTop: '0px', - }, - }, [ - h('button.btn-green', { - onClick: this.toCoinbase.bind(this), - }, 'Continue to Coinbase'), - - h('button.btn-red', { - onClick: () => props.dispatch(actions.backTobuyView(props.accounts.address)), - }, 'Cancel'), - ]), - ]) -} - -CoinbaseForm.prototype.toCoinbase = function () { - const props = this.props - const address = props.buyView.buyAddress - props.dispatch(actions.buyEth({ network: '1', address, amount: 0 })) -} - -CoinbaseForm.prototype.renderLoading = function () { - return h('img', { - style: { - width: '27px', - marginRight: '-27px', - }, - src: 'images/loading.svg', - }) -} diff --git a/ui/app/components/copyButton.js b/ui/app/components/copyButton.js deleted file mode 100644 index a25d0719c..000000000 --- a/ui/app/components/copyButton.js +++ /dev/null @@ -1,59 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const copyToClipboard = require('copy-to-clipboard') - -const Tooltip = require('./tooltip') - -module.exports = CopyButton - -inherits(CopyButton, Component) -function CopyButton () { - Component.call(this) -} - -// As parameters, accepts: -// "value", which is the value to copy (mandatory) -// "title", which is the text to show on hover (optional, defaults to 'Copy') -CopyButton.prototype.render = function () { - const props = this.props - const state = this.state || {} - - const value = props.value - const copied = state.copied - - const message = copied ? 'Copied' : props.title || ' Copy ' - - return h('.copy-button', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - - h(Tooltip, { - title: message, - }, [ - h('i.fa.fa-clipboard.cursor-pointer.color-orange', { - style: { - margin: '5px', - }, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - copyToClipboard(value) - this.debounceRestore() - }, - }), - ]), - - ]) -} - -CopyButton.prototype.debounceRestore = function () { - this.setState({ copied: true }) - clearTimeout(this.timeout) - this.timeout = setTimeout(() => { - this.setState({ copied: false }) - }, 850) -} diff --git a/ui/app/components/copyable.js b/ui/app/components/copyable.js deleted file mode 100644 index a4f6f4bc6..000000000 --- a/ui/app/components/copyable.js +++ /dev/null @@ -1,46 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const Tooltip = require('./tooltip') -const copyToClipboard = require('copy-to-clipboard') - -module.exports = Copyable - -inherits(Copyable, Component) -function Copyable () { - Component.call(this) - this.state = { - copied: false, - } -} - -Copyable.prototype.render = function () { - const props = this.props - const state = this.state - const { value, children } = props - const { copied } = state - - return h(Tooltip, { - title: copied ? 'Copied!' : 'Copy', - position: 'bottom', - }, h('span', { - style: { - cursor: 'pointer', - }, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - copyToClipboard(value) - this.debounceRestore() - }, - }, children)) -} - -Copyable.prototype.debounceRestore = function () { - this.setState({ copied: true }) - clearTimeout(this.timeout) - this.timeout = setTimeout(() => { - this.setState({ copied: false }) - }, 850) -} diff --git a/ui/app/components/custom-radio-list.js b/ui/app/components/custom-radio-list.js deleted file mode 100644 index a4c525396..000000000 --- a/ui/app/components/custom-radio-list.js +++ /dev/null @@ -1,60 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = RadioList - -inherits(RadioList, Component) -function RadioList () { - Component.call(this) -} - -RadioList.prototype.render = function () { - const props = this.props - const activeClass = '.custom-radio-selected' - const inactiveClass = '.custom-radio-inactive' - const { - labels, - defaultFocus, - } = props - - - return ( - h('.flex-row', { - style: { - fontSize: '12px', - }, - }, [ - h('.flex-column.custom-radios', { - style: { - marginRight: '5px', - }, - }, - labels.map((lable, i) => { - let isSelcted = (this.state !== null) - isSelcted = isSelcted ? (this.state.selected === lable) : (defaultFocus === lable) - return h(isSelcted ? activeClass : inactiveClass, { - title: lable, - onClick: (event) => { - this.setState({selected: event.target.title}) - props.onClick(event) - }, - }) - }) - ), - h('.text', {}, - labels.map((lable) => { - if (props.subtext) { - return h('.flex-row', {}, [ - h('.radio-titles', lable), - h('.radio-titles-subtext', `- ${props.subtext[lable]}`), - ]) - } else { - return h('.radio-titles', lable) - } - }) - ), - ]) - ) -} - diff --git a/ui/app/components/drop-menu-item.js b/ui/app/components/drop-menu-item.js deleted file mode 100644 index e42948209..000000000 --- a/ui/app/components/drop-menu-item.js +++ /dev/null @@ -1,59 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = DropMenuItem - -inherits(DropMenuItem, Component) -function DropMenuItem () { - Component.call(this) -} - -DropMenuItem.prototype.render = function () { - return h('li.drop-menu-item', { - onClick: () => { - this.props.closeMenu() - this.props.action() - }, - style: { - listStyle: 'none', - padding: '6px 16px 6px 5px', - fontFamily: 'Montserrat Regular', - color: 'rgb(125, 128, 130)', - cursor: 'pointer', - display: 'flex', - justifyContent: 'flex-start', - }, - }, [ - this.props.icon, - this.props.label, - this.activeNetworkRender(), - ]) -} - -DropMenuItem.prototype.activeNetworkRender = function () { - const activeNetwork = this.props.activeNetworkRender - const { provider } = this.props - const providerType = provider ? provider.type : null - if (activeNetwork === undefined) return - - switch (this.props.label) { - case 'Main Ethereum Network': - if (providerType === 'mainnet') return h('.check', '✓') - break - case 'Ropsten Test Network': - if (providerType === 'ropsten') return h('.check', '✓') - break - case 'Kovan Test Network': - if (providerType === 'kovan') return h('.check', '✓') - break - case 'Rinkeby Test Network': - if (providerType === 'rinkeby') return h('.check', '✓') - break - case 'Localhost 8545': - if (activeNetwork === 'http://localhost:8545') return h('.check', '✓') - break - default: - if (activeNetwork === 'custom') return h('.check', '✓') - } -} diff --git a/ui/app/components/editable-label.js b/ui/app/components/editable-label.js deleted file mode 100644 index 41936f5e0..000000000 --- a/ui/app/components/editable-label.js +++ /dev/null @@ -1,51 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const findDOMNode = require('react-dom').findDOMNode - -module.exports = EditableLabel - -inherits(EditableLabel, Component) -function EditableLabel () { - Component.call(this) -} - -EditableLabel.prototype.render = function () { - const props = this.props - const state = this.state - - if (state && state.isEditingLabel) { - return h('div.editable-label', [ - h('input.sizing-input', { - defaultValue: props.textValue, - maxLength: '20', - onKeyPress: (event) => { - this.saveIfEnter(event) - }, - }), - h('button.editable-button', { - onClick: () => this.saveText(), - }, 'Save'), - ]) - } else { - return h('div.name-label', { - onClick: (event) => { - this.setState({ isEditingLabel: true }) - }, - }, this.props.children) - } -} - -EditableLabel.prototype.saveIfEnter = function (event) { - if (event.key === 'Enter') { - this.saveText() - } -} - -EditableLabel.prototype.saveText = function () { - var container = findDOMNode(this) - var text = container.querySelector('.editable-label input').value - var truncatedText = text.substring(0, 20) - this.props.saveText(truncatedText) - this.setState({ isEditingLabel: false, textLabel: truncatedText }) -} diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js deleted file mode 100644 index 3a33ebf74..000000000 --- a/ui/app/components/ens-input.js +++ /dev/null @@ -1,170 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const extend = require('xtend') -const debounce = require('debounce') -const copyToClipboard = require('copy-to-clipboard') -const ENS = require('ethjs-ens') -const networkMap = require('ethjs-ens/lib/network-map.json') -const ensRE = /.+\.eth$/ -const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' - - -module.exports = EnsInput - -inherits(EnsInput, Component) -function EnsInput () { - Component.call(this) -} - -EnsInput.prototype.render = function () { - const props = this.props - const opts = extend(props, { - list: 'addresses', - onChange: () => { - const network = this.props.network - const networkHasEnsSupport = getNetworkEnsSupport(network) - if (!networkHasEnsSupport) return - - const recipient = document.querySelector('input[name="address"]').value - if (recipient.match(ensRE) === null) { - return this.setState({ - loadingEns: false, - ensResolution: null, - ensFailure: null, - }) - } - - this.setState({ - loadingEns: true, - }) - this.checkName() - }, - }) - return h('div', { - style: { width: '100%' }, - }, [ - h('input.large-input', opts), - // The address book functionality. - h('datalist#addresses', - [ - // Corresponds to the addresses owned. - Object.keys(props.identities).map((key) => { - const identity = props.identities[key] - return h('option', { - value: identity.address, - label: identity.name, - key: identity.address, - }) - }), - // Corresponds to previously sent-to addresses. - props.addressBook.map((identity) => { - return h('option', { - value: identity.address, - label: identity.name, - key: identity.address, - }) - }), - ]), - this.ensIcon(), - ]) -} - -EnsInput.prototype.componentDidMount = function () { - const network = this.props.network - const networkHasEnsSupport = getNetworkEnsSupport(network) - this.setState({ ensResolution: ZERO_ADDRESS }) - - if (networkHasEnsSupport) { - const provider = global.ethereumProvider - this.ens = new ENS({ provider, network }) - this.checkName = debounce(this.lookupEnsName.bind(this), 200) - } -} - -EnsInput.prototype.lookupEnsName = function () { - const recipient = document.querySelector('input[name="address"]').value - const { ensResolution } = this.state - - log.info(`ENS attempting to resolve name: ${recipient}`) - this.ens.lookup(recipient.trim()) - .then((address) => { - if (address === ZERO_ADDRESS) throw new Error('No address has been set for this name.') - if (address !== ensResolution) { - this.setState({ - loadingEns: false, - ensResolution: address, - nickname: recipient.trim(), - hoverText: address + '\nClick to Copy', - ensFailure: false, - }) - } - }) - .catch((reason) => { - log.error(reason) - return this.setState({ - loadingEns: false, - ensResolution: ZERO_ADDRESS, - ensFailure: true, - hoverText: reason.message, - }) - }) -} - -EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) { - const state = this.state || {} - const ensResolution = state.ensResolution - // If an address is sent without a nickname, meaning not from ENS or from - // the user's own accounts, a default of a one-space string is used. - const nickname = state.nickname || ' ' - if (prevState && ensResolution && this.props.onChange && - ensResolution !== prevState.ensResolution) { - this.props.onChange(ensResolution, nickname) - } -} - -EnsInput.prototype.ensIcon = function (recipient) { - const { hoverText } = this.state || {} - return h('span', { - title: hoverText, - style: { - position: 'absolute', - padding: '9px', - transform: 'translatex(-40px)', - }, - }, this.ensIconContents(recipient)) -} - -EnsInput.prototype.ensIconContents = function (recipient) { - const { loadingEns, ensFailure, ensResolution } = this.state || { ensResolution: ZERO_ADDRESS} - - if (loadingEns) { - return h('img', { - src: 'images/loading.svg', - style: { - width: '30px', - height: '30px', - transform: 'translateY(-6px)', - }, - }) - } - - if (ensFailure) { - return h('i.fa.fa-warning.fa-lg.warning') - } - - if (ensResolution && (ensResolution !== ZERO_ADDRESS)) { - return h('i.fa.fa-check-circle.fa-lg.cursor-pointer', { - style: { color: 'green' }, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - copyToClipboard(ensResolution) - }, - }) - } -} - -function getNetworkEnsSupport (network) { - return Boolean(networkMap[network]) -} diff --git a/ui/app/components/eth-balance.js b/ui/app/components/eth-balance.js deleted file mode 100644 index 4f538fd31..000000000 --- a/ui/app/components/eth-balance.js +++ /dev/null @@ -1,89 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const formatBalance = require('../util').formatBalance -const generateBalanceObject = require('../util').generateBalanceObject -const Tooltip = require('./tooltip.js') -const FiatValue = require('./fiat-value.js') - -module.exports = EthBalanceComponent - -inherits(EthBalanceComponent, Component) -function EthBalanceComponent () { - Component.call(this) -} - -EthBalanceComponent.prototype.render = function () { - var props = this.props - let { value } = props - const { style, width } = props - var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true - value = value ? formatBalance(value, 6, needsParse) : '...' - - return ( - - h('.ether-balance.ether-balance-amount', { - style, - }, [ - h('div', { - style: { - display: 'inline', - width, - }, - }, this.renderBalance(value)), - ]) - - ) -} -EthBalanceComponent.prototype.renderBalance = function (value) { - var props = this.props - const { conversionRate, shorten, incoming, currentCurrency } = props - if (value === 'None') return value - if (value === '...') return value - var balanceObj = generateBalanceObject(value, shorten ? 1 : 3) - var balance - var splitBalance = value.split(' ') - var ethNumber = splitBalance[0] - var ethSuffix = splitBalance[1] - const showFiat = 'showFiat' in props ? props.showFiat : true - - if (shorten) { - balance = balanceObj.shortBalance - } else { - balance = balanceObj.balance - } - - var label = balanceObj.label - - return ( - h(Tooltip, { - position: 'bottom', - title: `${ethNumber} ${ethSuffix}`, - }, h('div.flex-column', [ - h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('div', { - style: { - width: '100%', - textAlign: 'right', - }, - }, incoming ? `+${balance}` : balance), - h('div', { - style: { - color: ' #AEAEAE', - fontSize: '12px', - marginLeft: '5px', - }, - }, label), - ]), - - showFiat ? h(FiatValue, { value: props.value, conversionRate, currentCurrency }) : null, - ])) - ) -} diff --git a/ui/app/components/fiat-value.js b/ui/app/components/fiat-value.js deleted file mode 100644 index 8a64a1cfc..000000000 --- a/ui/app/components/fiat-value.js +++ /dev/null @@ -1,63 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const formatBalance = require('../util').formatBalance - -module.exports = FiatValue - -inherits(FiatValue, Component) -function FiatValue () { - Component.call(this) -} - -FiatValue.prototype.render = function () { - const props = this.props - const { conversionRate, currentCurrency } = props - - const value = formatBalance(props.value, 6) - - if (value === 'None') return value - var fiatDisplayNumber, fiatTooltipNumber - var splitBalance = value.split(' ') - - if (conversionRate !== 0) { - fiatTooltipNumber = Number(splitBalance[0]) * conversionRate - fiatDisplayNumber = fiatTooltipNumber.toFixed(2) - } else { - fiatDisplayNumber = 'N/A' - fiatTooltipNumber = 'Unknown' - } - - return fiatDisplay(fiatDisplayNumber, currentCurrency) -} - -function fiatDisplay (fiatDisplayNumber, fiatSuffix) { - if (fiatDisplayNumber !== 'N/A') { - return h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('div', { - style: { - width: '100%', - textAlign: 'right', - fontSize: '12px', - color: '#333333', - }, - }, fiatDisplayNumber), - h('div', { - style: { - color: '#AEAEAE', - marginLeft: '5px', - fontSize: '12px', - }, - }, fiatSuffix), - ]) - } else { - return h('div') - } -} diff --git a/ui/app/components/hex-as-decimal-input.js b/ui/app/components/hex-as-decimal-input.js deleted file mode 100644 index 4a71e9585..000000000 --- a/ui/app/components/hex-as-decimal-input.js +++ /dev/null @@ -1,154 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const extend = require('xtend') - -module.exports = HexAsDecimalInput - -inherits(HexAsDecimalInput, Component) -function HexAsDecimalInput () { - this.state = { invalid: null } - Component.call(this) -} - -/* Hex as Decimal Input - * - * A component for allowing easy, decimal editing - * of a passed in hex string value. - * - * On change, calls back its `onChange` function parameter - * and passes it an updated hex string. - */ - -HexAsDecimalInput.prototype.render = function () { - const props = this.props - const state = this.state - - const { value, onChange, min, max } = props - - const toEth = props.toEth - const suffix = props.suffix - const decimalValue = decimalize(value, toEth) - const style = props.style - - return ( - h('.flex-column', [ - h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('input.hex-input', { - type: 'number', - required: true, - min: min, - max: max, - style: extend({ - display: 'block', - textAlign: 'right', - backgroundColor: 'transparent', - border: '1px solid #bdbdbd', - - }, style), - value: parseInt(decimalValue), - onBlur: (event) => { - this.updateValidity(event) - }, - onChange: (event) => { - this.updateValidity(event) - const hexString = (event.target.value === '') ? '' : hexify(event.target.value) - onChange(hexString) - }, - onInvalid: (event) => { - const msg = this.constructWarning() - if (msg === state.invalid) { - return - } - this.setState({ invalid: msg }) - event.preventDefault() - return false - }, - }), - h('div', { - style: { - color: ' #AEAEAE', - fontSize: '12px', - marginLeft: '5px', - marginRight: '6px', - width: '20px', - }, - }, suffix), - ]), - - state.invalid ? h('span.error', { - style: { - position: 'absolute', - right: '0px', - textAlign: 'right', - transform: 'translateY(26px)', - padding: '3px', - background: 'rgba(255,255,255,0.85)', - zIndex: '1', - textTransform: 'capitalize', - border: '2px solid #E20202', - }, - }, state.invalid) : null, - ]) - ) -} - -HexAsDecimalInput.prototype.setValid = function (message) { - this.setState({ invalid: null }) -} - -HexAsDecimalInput.prototype.updateValidity = function (event) { - const target = event.target - const value = this.props.value - const newValue = target.value - - if (value === newValue) { - return - } - - const valid = target.checkValidity() - if (valid) { - this.setState({ invalid: null }) - } -} - -HexAsDecimalInput.prototype.constructWarning = function () { - const { name, min, max } = this.props - let message = name ? name + ' ' : '' - - if (min && max) { - message += `must be greater than or equal to ${min} and less than or equal to ${max}.` - } else if (min) { - message += `must be greater than or equal to ${min}.` - } else if (max) { - message += `must be less than or equal to ${max}.` - } else { - message += 'Invalid input.' - } - - return message -} - -function hexify (decimalString) { - const hexBN = new BN(parseInt(decimalString), 10) - return '0x' + hexBN.toString('hex') -} - -function decimalize (input, toEth) { - if (input === '') { - return '' - } else { - const strippedInput = ethUtil.stripHexPrefix(input) - const inputBN = new BN(strippedInput, 'hex') - return inputBN.toString(10) - } -} diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js deleted file mode 100644 index c754bc6ba..000000000 --- a/ui/app/components/identicon.js +++ /dev/null @@ -1,72 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const isNode = require('detect-node') -const findDOMNode = require('react-dom').findDOMNode -const jazzicon = require('jazzicon') -const iconFactoryGen = require('../../lib/icon-factory') -const iconFactory = iconFactoryGen(jazzicon) - -module.exports = IdenticonComponent - -inherits(IdenticonComponent, Component) -function IdenticonComponent () { - Component.call(this) - - this.defaultDiameter = 46 -} - -IdenticonComponent.prototype.render = function () { - var props = this.props - var diameter = props.diameter || this.defaultDiameter - return ( - h('div', { - key: 'identicon-' + this.props.address, - style: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - height: diameter, - width: diameter, - borderRadius: diameter / 2, - overflow: 'hidden', - }, - }) - ) -} - -IdenticonComponent.prototype.componentDidMount = function () { - var props = this.props - const { address } = props - - if (!address) return - - var container = findDOMNode(this) - - var diameter = props.diameter || this.defaultDiameter - if (!isNode) { - var img = iconFactory.iconForAddress(address, diameter) - container.appendChild(img) - } -} - -IdenticonComponent.prototype.componentDidUpdate = function () { - var props = this.props - const { address } = props - - if (!address) return - - var container = findDOMNode(this) - - var children = container.children - for (var i = 0; i < children.length; i++) { - container.removeChild(children[i]) - } - - var diameter = props.diameter || this.defaultDiameter - if (!isNode) { - var img = iconFactory.iconForAddress(address, diameter) - container.appendChild(img) - } -} - diff --git a/ui/app/components/loading.js b/ui/app/components/loading.js deleted file mode 100644 index 87d6f5d20..000000000 --- a/ui/app/components/loading.js +++ /dev/null @@ -1,53 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') - - -inherits(LoadingIndicator, Component) -module.exports = LoadingIndicator - -function LoadingIndicator () { - Component.call(this) -} - -LoadingIndicator.prototype.render = function () { - const { isLoading, loadingMessage } = this.props - - return ( - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'loader', - transitionEnterTimeout: 150, - transitionLeaveTimeout: 150, - }, [ - - isLoading ? h('div', { - style: { - zIndex: 10, - position: 'absolute', - flexDirection: 'column', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - height: '100%', - width: '100%', - background: 'rgba(255, 255, 255, 0.8)', - }, - }, [ - h('img', { - src: 'images/loading.svg', - }), - - h('br'), - - showMessageIfAny(loadingMessage), - ]) : null, - ]) - ) -} - -function showMessageIfAny (loadingMessage) { - if (!loadingMessage) return null - return h('span', loadingMessage) -} diff --git a/ui/app/components/mascot.js b/ui/app/components/mascot.js deleted file mode 100644 index 973ec2cad..000000000 --- a/ui/app/components/mascot.js +++ /dev/null @@ -1,59 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const metamaskLogo = require('metamask-logo') -const debounce = require('debounce') - -module.exports = Mascot - -inherits(Mascot, Component) -function Mascot () { - Component.call(this) - this.logo = metamaskLogo({ - followMouse: true, - pxNotRatio: true, - width: 200, - height: 200, - }) - - this.refollowMouse = debounce(this.logo.setFollowMouse.bind(this.logo, true), 1000) - this.unfollowMouse = this.logo.setFollowMouse.bind(this.logo, false) -} - -Mascot.prototype.render = function () { - // this is a bit hacky - // the event emitter is on `this.props` - // and we dont get that until render - this.handleAnimationEvents() - - return h('#metamask-mascot-container', { - style: { zIndex: 0 }, - }) -} - -Mascot.prototype.componentDidMount = function () { - var targetDivId = 'metamask-mascot-container' - var container = document.getElementById(targetDivId) - container.appendChild(this.logo.container) -} - -Mascot.prototype.componentWillUnmount = function () { - this.animations = this.props.animationEventEmitter - this.animations.removeAllListeners() - this.logo.container.remove() - this.logo.stopAnimation() -} - -Mascot.prototype.handleAnimationEvents = function () { - // only setup listeners once - if (this.animations) return - this.animations = this.props.animationEventEmitter - this.animations.on('point', this.lookAt.bind(this)) - this.animations.on('setFollowMouse', this.logo.setFollowMouse.bind(this.logo)) -} - -Mascot.prototype.lookAt = function (target) { - this.unfollowMouse() - this.logo.lookAt(target) - this.refollowMouse() -} diff --git a/ui/app/components/mini-account-panel.js b/ui/app/components/mini-account-panel.js deleted file mode 100644 index c09cf5b7a..000000000 --- a/ui/app/components/mini-account-panel.js +++ /dev/null @@ -1,74 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const Identicon = require('./identicon') - -module.exports = AccountPanel - - -inherits(AccountPanel, Component) -function AccountPanel () { - Component.call(this) -} - -AccountPanel.prototype.render = function () { - var props = this.props - var picOrder = props.picOrder || 'left' - const { imageSeed } = props - - return ( - - h('.identity-panel.flex-row.flex-left', { - style: { - cursor: props.onClick ? 'pointer' : undefined, - }, - onClick: props.onClick, - }, [ - - this.genIcon(imageSeed, picOrder), - - h('div.flex-column.flex-justify-center', { - style: { - lineHeight: '15px', - order: 2, - display: 'flex', - alignItems: picOrder === 'left' ? 'flex-begin' : 'flex-end', - }, - }, this.props.children), - ]) - ) -} - -AccountPanel.prototype.genIcon = function (seed, picOrder) { - const props = this.props - - // When there is no seed value, this is a contract creation. - // We then show the contract icon. - if (!seed) { - return h('.identicon-wrapper.flex-column.select-none', { - style: { - order: picOrder === 'left' ? 1 : 3, - }, - }, [ - h('i.fa.fa-file-text-o.fa-lg', { - style: { - fontSize: '42px', - transform: 'translate(0px, -16px)', - }, - }), - ]) - } - - // If there was a seed, we return an identicon for that address. - return h('.identicon-wrapper.flex-column.select-none', { - style: { - order: picOrder === 'left' ? 1 : 3, - }, - }, [ - h(Identicon, { - address: seed, - imageify: props.imageifyIdenticons, - }), - ]) -} - diff --git a/ui/app/components/network.js b/ui/app/components/network.js deleted file mode 100644 index d5d3e18cd..000000000 --- a/ui/app/components/network.js +++ /dev/null @@ -1,125 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = Network - -inherits(Network, Component) - -function Network () { - Component.call(this) -} - -Network.prototype.render = function () { - const props = this.props - const networkNumber = props.network - let providerName - try { - providerName = props.provider.type - } catch (e) { - providerName = null - } - let iconName, hoverText - - if (networkNumber === 'loading') { - return h('span', { - style: { - display: 'flex', - alignItems: 'center', - flexDirection: 'row', - }, - onClick: (event) => this.props.onClick(event), - }, [ - h('img', { - title: 'Attempting to connect to blockchain.', - style: { - width: '27px', - }, - src: 'images/loading.svg', - }), - h('i.fa.fa-sort-desc'), - ]) - - } else if (providerName === 'mainnet') { - hoverText = 'Main Ethereum Network' - iconName = 'ethereum-network' - } else if (providerName === 'ropsten') { - hoverText = 'Ropsten Test Network' - iconName = 'ropsten-test-network' - } else if (parseInt(networkNumber) === 3) { - hoverText = 'Ropsten Test Network' - iconName = 'ropsten-test-network' - } else if (providerName === 'kovan') { - hoverText = 'Kovan Test Network' - iconName = 'kovan-test-network' - } else if (providerName === 'rinkeby') { - hoverText = 'Rinkeby Test Network' - iconName = 'rinkeby-test-network' - } else { - hoverText = 'Unknown Private Network' - iconName = 'unknown-private-network' - } - - return ( - h('#network_component.pointer', { - title: hoverText, - onClick: (event) => this.props.onClick(event), - }, [ - (function () { - switch (iconName) { - case 'ethereum-network': - return h('.network-indicator', [ - h('.menu-icon.diamond'), - h('.network-name', { - style: { - color: '#039396', - }}, - 'Ethereum Main Net'), - ]) - case 'ropsten-test-network': - return h('.network-indicator', [ - h('.menu-icon.red-dot'), - h('.network-name', { - style: { - color: '#ff6666', - }}, - 'Ropsten Test Net'), - ]) - case 'kovan-test-network': - return h('.network-indicator', [ - h('.menu-icon.hollow-diamond'), - h('.network-name', { - style: { - color: '#690496', - }}, - 'Kovan Test Net'), - ]) - case 'rinkeby-test-network': - return h('.network-indicator', [ - h('.menu-icon.golden-square'), - h('.network-name', { - style: { - color: '#e7a218', - }}, - 'Rinkeby Test Net'), - ]) - default: - return h('.network-indicator', [ - h('i.fa.fa-question-circle.fa-lg', { - style: { - margin: '10px', - color: 'rgb(125, 128, 130)', - }, - }), - - h('.network-name', { - style: { - color: '#AEAEAE', - }}, - 'Private Network'), - ]) - } - })(), - ]) - ) -} diff --git a/ui/app/components/notice.js b/ui/app/components/notice.js deleted file mode 100644 index d9f0067cd..000000000 --- a/ui/app/components/notice.js +++ /dev/null @@ -1,126 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const ReactMarkdown = require('react-markdown') -const linker = require('extension-link-enabler') -const findDOMNode = require('react-dom').findDOMNode - -module.exports = Notice - -inherits(Notice, Component) -function Notice () { - Component.call(this) -} - -Notice.prototype.render = function () { - const { notice, onConfirm } = this.props - const { title, date, body } = notice - const state = this.state || { disclaimerDisabled: true } - const disabled = state.disclaimerDisabled - - return ( - h('.flex-column.flex-center.flex-grow', [ - h('h3.flex-center.text-transform-uppercase.terms-header', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - width: '100%', - fontSize: '20px', - textAlign: 'center', - padding: 6, - }, - }, [ - title, - ]), - - h('h5.flex-center.text-transform-uppercase.terms-header', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginBottom: 24, - width: '100%', - fontSize: '20px', - textAlign: 'center', - padding: 6, - }, - }, [ - date, - ]), - - h('style', ` - - .markdown { - overflow-x: hidden; - } - - .markdown h1, .markdown h2, .markdown h3 { - margin: 10px 0; - font-weight: bold; - } - - .markdown strong { - font-weight: bold; - } - .markdown em { - font-style: italic; - } - - .markdown p { - margin: 10px 0; - } - - .markdown a { - color: #df6b0e; - } - - `), - - h('div.markdown', { - onScroll: (e) => { - var object = e.currentTarget - if (object.offsetHeight + object.scrollTop + 100 >= object.scrollHeight) { - this.setState({disclaimerDisabled: false}) - } - }, - style: { - background: 'rgb(235, 235, 235)', - height: '310px', - padding: '6px', - width: '90%', - overflowY: 'scroll', - scroll: 'auto', - }, - }, [ - h(ReactMarkdown, { - className: 'notice-box', - source: body, - skipHtml: true, - }), - ]), - - h('button', { - disabled, - onClick: () => { - this.setState({disclaimerDisabled: true}) - onConfirm() - }, - style: { - marginTop: '18px', - }, - }, 'Accept'), - ]) - ) -} - -Notice.prototype.componentDidMount = function () { - var node = findDOMNode(this) - linker.setupListener(node) - if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) { - this.setState({disclaimerDisabled: false}) - } -} - -Notice.prototype.componentWillUnmount = function () { - var node = findDOMNode(this) - linker.teardownListener(node) -} diff --git a/ui/app/components/pending-msg-details.js b/ui/app/components/pending-msg-details.js deleted file mode 100644 index 16308d121..000000000 --- a/ui/app/components/pending-msg-details.js +++ /dev/null @@ -1,50 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const AccountPanel = require('./account-panel') - -module.exports = PendingMsgDetails - -inherits(PendingMsgDetails, Component) -function PendingMsgDetails () { - Component.call(this) -} - -PendingMsgDetails.prototype.render = function () { - var state = this.props - var msgData = state.txData - - var msgParams = msgData.msgParams || {} - var address = msgParams.from || state.selectedAddress - var identity = state.identities[address] || { address: address } - var account = state.accounts[address] || { address: address } - - return ( - h('div', { - key: msgData.id, - style: { - margin: '10px 20px', - }, - }, [ - - // account that will sign - h(AccountPanel, { - showFullAddress: true, - identity: identity, - account: account, - imageifyIdenticons: state.imageifyIdenticons, - }), - - // message data - h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-row.flex-space-between', [ - h('label.font-small', 'MESSAGE'), - h('span.font-small', msgParams.data), - ]), - ]), - - ]) - ) -} - diff --git a/ui/app/components/pending-msg.js b/ui/app/components/pending-msg.js deleted file mode 100644 index b2cac164a..000000000 --- a/ui/app/components/pending-msg.js +++ /dev/null @@ -1,56 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const PendingTxDetails = require('./pending-msg-details') - -module.exports = PendingMsg - -inherits(PendingMsg, Component) -function PendingMsg () { - Component.call(this) -} - -PendingMsg.prototype.render = function () { - var state = this.props - var msgData = state.txData - - return ( - - h('div', { - key: msgData.id, - }, [ - - // header - h('h3', { - style: { - fontWeight: 'bold', - textAlign: 'center', - }, - }, 'Sign Message'), - - h('.error', { - style: { - margin: '10px', - }, - }, `Signing this message can have - dangerous side effects. Only sign messages from - sites you fully trust with your entire account. - This will be fixed in a future version.`), - - // message details - h(PendingTxDetails, state), - - // sign + cancel - h('.flex-row.flex-space-around', [ - h('button', { - onClick: state.cancelMessage, - }, 'Cancel'), - h('button', { - onClick: state.signMessage, - }, 'Sign'), - ]), - ]) - - ) -} - diff --git a/ui/app/components/pending-personal-msg-details.js b/ui/app/components/pending-personal-msg-details.js deleted file mode 100644 index 1050513f2..000000000 --- a/ui/app/components/pending-personal-msg-details.js +++ /dev/null @@ -1,60 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const AccountPanel = require('./account-panel') -const BinaryRenderer = require('./binary-renderer') - -module.exports = PendingMsgDetails - -inherits(PendingMsgDetails, Component) -function PendingMsgDetails () { - Component.call(this) -} - -PendingMsgDetails.prototype.render = function () { - var state = this.props - var msgData = state.txData - - var msgParams = msgData.msgParams || {} - var address = msgParams.from || state.selectedAddress - var identity = state.identities[address] || { address: address } - var account = state.accounts[address] || { address: address } - - var { data } = msgParams - - return ( - h('div', { - key: msgData.id, - style: { - margin: '10px 20px', - }, - }, [ - - // account that will sign - h(AccountPanel, { - showFullAddress: true, - identity: identity, - account: account, - imageifyIdenticons: state.imageifyIdenticons, - }), - - // message data - h('div', { - style: { - height: '260px', - }, - }, [ - h('label.font-small', { style: { display: 'block' } }, 'MESSAGE'), - h(BinaryRenderer, { - value: data, - style: { - height: '215px', - }, - }), - ]), - - ]) - ) -} - diff --git a/ui/app/components/pending-personal-msg.js b/ui/app/components/pending-personal-msg.js deleted file mode 100644 index 4542adb28..000000000 --- a/ui/app/components/pending-personal-msg.js +++ /dev/null @@ -1,47 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const PendingTxDetails = require('./pending-personal-msg-details') - -module.exports = PendingMsg - -inherits(PendingMsg, Component) -function PendingMsg () { - Component.call(this) -} - -PendingMsg.prototype.render = function () { - var state = this.props - var msgData = state.txData - - return ( - - h('div', { - key: msgData.id, - }, [ - - // header - h('h3', { - style: { - fontWeight: 'bold', - textAlign: 'center', - }, - }, 'Sign Message'), - - // message details - h(PendingTxDetails, state), - - // sign + cancel - h('.flex-row.flex-space-around', [ - h('button', { - onClick: state.cancelPersonalMessage, - }, 'Cancel'), - h('button', { - onClick: state.signPersonalMessage, - }, 'Sign'), - ]), - ]) - - ) -} - diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js deleted file mode 100644 index d7d602f31..000000000 --- a/ui/app/components/pending-tx.js +++ /dev/null @@ -1,480 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const actions = require('../actions') -const clone = require('clone') - -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const hexToBn = require('../../../app/scripts/lib/hex-to-bn') -const util = require('../util') -const MiniAccountPanel = require('./mini-account-panel') -const Copyable = require('./copyable') -const EthBalance = require('./eth-balance') -const addressSummary = util.addressSummary -const nameForAddress = require('../../lib/contract-namer') -const BNInput = require('./bn-as-decimal-input') - -const MIN_GAS_PRICE_GWEI_BN = new BN(2) -const GWEI_FACTOR = new BN(1e9) -const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) -const MIN_GAS_LIMIT_BN = new BN(21000) - -module.exports = PendingTx -inherits(PendingTx, Component) -function PendingTx () { - Component.call(this) - this.state = { - valid: true, - txData: null, - submitting: false, - } -} - -PendingTx.prototype.render = function () { - const props = this.props - const { currentCurrency, blockGasLimit } = props - - const conversionRate = props.conversionRate - const txMeta = this.gatherTxMeta() - const txParams = txMeta.txParams || {} - - // Account Details - const address = txParams.from || props.selectedAddress - const identity = props.identities[address] || { address: address } - const account = props.accounts[address] - const balance = account ? account.balance : '0x0' - - // recipient check - const isValidAddress = !txParams.to || util.isValidAddress(txParams.to) - - // Gas - const gas = txParams.gas - const gasBn = hexToBn(gas) - const gasLimit = new BN(parseInt(blockGasLimit)) - const safeGasLimit = this.bnMultiplyByFraction(gasLimit, 19, 20).toString(10) - - // Gas Price - const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) - const gasPriceBn = hexToBn(gasPrice) - - const txFeeBn = gasBn.mul(gasPriceBn) - const valueBn = hexToBn(txParams.value) - const maxCost = txFeeBn.add(valueBn) - - const dataLength = txParams.data ? (txParams.data.length - 2) / 2 : 0 - - const balanceBn = hexToBn(balance) - const insufficientBalance = balanceBn.lt(maxCost) - - this.inputs = [] - - return ( - - h('div', { - key: txMeta.id, - }, [ - - h('form#pending-tx-form', { - onSubmit: this.onSubmit.bind(this), - - }, [ - - // tx info - h('div', [ - - h('.flex-row.flex-center', { - style: { - maxWidth: '100%', - }, - }, [ - - h(MiniAccountPanel, { - imageSeed: address, - picOrder: 'right', - }, [ - h('span.font-small', { - style: { - fontFamily: 'Montserrat Bold, Montserrat, sans-serif', - }, - }, identity.name), - - h(Copyable, { - value: ethUtil.toChecksumAddress(address), - }, [ - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, addressSummary(address, 6, 4, false)), - ]), - - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, [ - h(EthBalance, { - value: balance, - conversionRate, - currentCurrency, - inline: true, - labelColor: '#F7861C', - }), - ]), - ]), - - forwardCarrat(), - - this.miniAccountPanelForRecipient(), - ]), - - h('style', ` - .table-box { - margin: 7px 0px 0px 0px; - width: 100%; - } - .table-box .row { - margin: 0px; - background: rgb(236,236,236); - display: flex; - justify-content: space-between; - font-family: Montserrat Light, sans-serif; - font-size: 13px; - padding: 5px 25px; - } - .table-box .row .value { - font-family: Montserrat Regular; - } - `), - - h('.table-box', [ - - // Ether Value - // Currently not customizable, but easily modified - // in the way that gas and gasLimit currently are. - h('.row', [ - h('.cell.label', 'Amount'), - h(EthBalance, { value: txParams.value, currentCurrency, conversionRate }), - ]), - - // Gas Limit (customizable) - h('.cell.row', [ - h('.cell.label', 'Gas Limit'), - h('.cell.value', { - }, [ - h(BNInput, { - name: 'Gas Limit', - value: gasBn, - precision: 0, - scale: 0, - // The hard lower limit for gas. - min: MIN_GAS_LIMIT_BN.toString(10), - max: safeGasLimit, - suffix: 'UNITS', - style: { - position: 'relative', - top: '5px', - }, - onChange: this.gasLimitChanged.bind(this), - - ref: (hexInput) => { this.inputs.push(hexInput) }, - }), - ]), - ]), - - // Gas Price (customizable) - h('.cell.row', [ - h('.cell.label', 'Gas Price'), - h('.cell.value', { - }, [ - h(BNInput, { - name: 'Gas Price', - value: gasPriceBn, - precision: 9, - scale: 9, - suffix: 'GWEI', - min: MIN_GAS_PRICE_GWEI_BN.toString(10), - style: { - position: 'relative', - top: '5px', - }, - onChange: this.gasPriceChanged.bind(this), - ref: (hexInput) => { this.inputs.push(hexInput) }, - }), - ]), - ]), - - // Max Transaction Fee (calculated) - h('.cell.row', [ - h('.cell.label', 'Max Transaction Fee'), - h(EthBalance, { value: txFeeBn.toString(16), currentCurrency, conversionRate }), - ]), - - h('.cell.row', { - style: { - fontFamily: 'Montserrat Regular', - background: 'white', - padding: '10px 25px', - }, - }, [ - h('.cell.label', 'Max Total'), - h('.cell.value', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - h(EthBalance, { - value: maxCost.toString(16), - currentCurrency, - conversionRate, - inline: true, - labelColor: 'black', - fontSize: '16px', - }), - ]), - ]), - - // Data size row: - h('.cell.row', { - style: { - background: '#f7f7f7', - paddingBottom: '0px', - }, - }, [ - h('.cell.label'), - h('.cell.value', { - style: { - fontFamily: 'Montserrat Light', - fontSize: '11px', - }, - }, `Data included: ${dataLength} bytes`), - ]), - ]), // End of Table - - ]), - - h('style', ` - .conf-buttons button { - margin-left: 10px; - text-transform: uppercase; - } - `), - - txMeta.simulationFails ? - h('.error', { - style: { - marginLeft: 50, - fontSize: '0.9em', - }, - }, 'Transaction Error. Exception thrown in contract code.') - : null, - - !isValidAddress ? - h('.error', { - style: { - marginLeft: 50, - fontSize: '0.9em', - }, - }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.') - : null, - - insufficientBalance ? - h('span.error', { - style: { - marginLeft: 50, - fontSize: '0.9em', - }, - }, 'Insufficient balance for transaction') - : null, - - // send + cancel - h('.flex-row.flex-space-around.conf-buttons', { - style: { - display: 'flex', - justifyContent: 'flex-end', - margin: '14px 25px', - }, - }, [ - - - insufficientBalance ? - h('button.btn-green', { - onClick: props.buyEth, - }, 'Buy Ether') - : null, - - h('button', { - onClick: (event) => { - this.resetGasFields() - event.preventDefault() - }, - }, 'Reset'), - - // Accept Button - h('input.confirm.btn-green', { - type: 'submit', - value: 'SUBMIT', - style: { marginLeft: '10px' }, - disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, - }), - - h('button.cancel.btn-red', { - onClick: props.cancelTransaction, - }, 'Reject'), - ]), - ]), - ]) - ) -} - -PendingTx.prototype.miniAccountPanelForRecipient = function () { - const props = this.props - const txData = props.txData - const txParams = txData.txParams || {} - const isContractDeploy = !('to' in txParams) - - // If it's not a contract deploy, send to the account - if (!isContractDeploy) { - return h(MiniAccountPanel, { - imageSeed: txParams.to, - picOrder: 'left', - }, [ - - h('span.font-small', { - style: { - fontFamily: 'Montserrat Bold, Montserrat, sans-serif', - }, - }, nameForAddress(txParams.to, props.identities)), - - h(Copyable, { - value: ethUtil.toChecksumAddress(txParams.to), - }, [ - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, addressSummary(txParams.to, 6, 4, false)), - ]), - - ]) - } else { - return h(MiniAccountPanel, { - picOrder: 'left', - }, [ - - h('span.font-small', { - style: { - fontFamily: 'Montserrat Bold, Montserrat, sans-serif', - }, - }, 'New Contract'), - - ]) - } -} - -PendingTx.prototype.gasPriceChanged = function (newBN, valid) { - log.info(`Gas price changed to: ${newBN.toString(10)}`) - const txMeta = this.gatherTxMeta() - txMeta.txParams.gasPrice = '0x' + newBN.toString('hex') - this.setState({ - txData: clone(txMeta), - valid, - }) -} - -PendingTx.prototype.gasLimitChanged = function (newBN, valid) { - log.info(`Gas limit changed to ${newBN.toString(10)}`) - const txMeta = this.gatherTxMeta() - txMeta.txParams.gas = '0x' + newBN.toString('hex') - this.setState({ - txData: clone(txMeta), - valid, - }) -} - -PendingTx.prototype.resetGasFields = function () { - log.debug(`pending-tx resetGasFields`) - - this.inputs.forEach((hexInput) => { - if (hexInput) { - hexInput.setValid() - } - }) - - this.setState({ - txData: null, - valid: true, - }) -} - -PendingTx.prototype.onSubmit = function (event) { - event.preventDefault() - const txMeta = this.gatherTxMeta() - const valid = this.checkValidity() - this.setState({ valid, submitting: true }) - if (valid && this.verifyGasParams()) { - this.props.sendTransaction(txMeta, event) - } else { - this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) - this.setState({ submitting: false }) - } -} - -PendingTx.prototype.checkValidity = function () { - const form = this.getFormEl() - const valid = form.checkValidity() - return valid -} - -PendingTx.prototype.getFormEl = function () { - const form = document.querySelector('form#pending-tx-form') - // Stub out form for unit tests: - if (!form) { - return { checkValidity () { return true } } - } - return form -} - -// After a customizable state value has been updated, -PendingTx.prototype.gatherTxMeta = function () { - log.debug(`pending-tx gatherTxMeta`) - const props = this.props - const state = this.state - const txData = clone(state.txData) || clone(props.txData) - - log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) - return txData -} - -PendingTx.prototype.verifyGasParams = function () { - // We call this in case the gas has not been modified at all - if (!this.state) { return true } - return ( - this._notZeroOrEmptyString(this.state.gas) && - this._notZeroOrEmptyString(this.state.gasPrice) - ) -} - -PendingTx.prototype._notZeroOrEmptyString = function (obj) { - return obj !== '' && obj !== '0x0' -} - -PendingTx.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) { - const numBN = new BN(numerator) - const denomBN = new BN(denominator) - return targetBN.mul(numBN).div(denomBN) -} - -function forwardCarrat () { - return ( - h('img', { - src: 'images/forward-carrat.svg', - style: { - padding: '5px 6px 0px 10px', - height: '37px', - }, - }) - ) -} diff --git a/ui/app/components/qr-code.js b/ui/app/components/qr-code.js deleted file mode 100644 index 06b9aed9b..000000000 --- a/ui/app/components/qr-code.js +++ /dev/null @@ -1,79 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const qrCode = require('qrcode-npm').qrcode -const inherits = require('util').inherits -const connect = require('react-redux').connect -const isHexPrefixed = require('ethereumjs-util').isHexPrefixed -const CopyButton = require('./copyButton') - -module.exports = connect(mapStateToProps)(QrCodeView) - -function mapStateToProps (state) { - return { - Qr: state.appState.Qr, - buyView: state.appState.buyView, - warning: state.appState.warning, - } -} - -inherits(QrCodeView, Component) - -function QrCodeView () { - Component.call(this) -} - -QrCodeView.prototype.render = function () { - const props = this.props - const Qr = props.Qr - const address = `${isHexPrefixed(Qr.data) ? 'ethereum:' : ''}${Qr.data}` - const qrImage = qrCode(4, 'M') - qrImage.addData(address) - qrImage.make() - return h('.main-container.flex-column', { - key: 'qr', - style: { - justifyContent: 'center', - paddingBottom: '45px', - paddingLeft: '45px', - paddingRight: '45px', - alignItems: 'center', - }, - }, [ - Array.isArray(Qr.message) ? h('.message-container', this.renderMultiMessage()) : h('.qr-header', Qr.message), - - this.props.warning ? this.props.warning && h('span.error.flex-center', { - style: { - textAlign: 'center', - width: '229px', - height: '82px', - }, - }, - this.props.warning) : null, - - h('#qr-container.flex-column', { - style: { - marginTop: '25px', - marginBottom: '15px', - }, - dangerouslySetInnerHTML: { - __html: qrImage.createTableTag(4), - }, - }), - h('.flex-row', [ - h('h3.ellip-address', { - style: { - width: '247px', - }, - }, Qr.data), - h(CopyButton, { - value: Qr.data, - }), - ]), - ]) -} - -QrCodeView.prototype.renderMultiMessage = function () { - var Qr = this.props.Qr - var multiMessage = Qr.message.map((message) => h('.qr-message', message)) - return multiMessage -} diff --git a/ui/app/components/range-slider.js b/ui/app/components/range-slider.js deleted file mode 100644 index 823f5eb01..000000000 --- a/ui/app/components/range-slider.js +++ /dev/null @@ -1,58 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = RangeSlider - -inherits(RangeSlider, Component) -function RangeSlider () { - Component.call(this) -} - -RangeSlider.prototype.render = function () { - const state = this.state || {} - const props = this.props - const onInput = props.onInput || function () {} - const name = props.name - const { - min = 0, - max = 100, - increment = 1, - defaultValue = 50, - mirrorInput = false, - } = this.props.options - const {container, input, range} = props.style - - return ( - h('.flex-row', { - style: container, - }, [ - h('input', { - type: 'range', - name: name, - min: min, - max: max, - step: increment, - style: range, - value: state.value || defaultValue, - onChange: mirrorInput ? this.mirrorInputs.bind(this, event) : onInput, - }), - - // Mirrored input for range - mirrorInput ? h('input.large-input', { - type: 'number', - name: `${name}Mirror`, - min: min, - max: max, - value: state.value || defaultValue, - step: increment, - style: input, - onChange: this.mirrorInputs.bind(this, event), - }) : null, - ]) - ) -} - -RangeSlider.prototype.mirrorInputs = function (event) { - this.setState({value: event.target.value}) -} diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js deleted file mode 100644 index e0a720426..000000000 --- a/ui/app/components/shapeshift-form.js +++ /dev/null @@ -1,306 +0,0 @@ -const PersistentForm = require('../../lib/persistent-form') -const h = require('react-hyperscript') -const inherits = require('util').inherits -const connect = require('react-redux').connect -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') -const actions = require('../actions') -const Qr = require('./qr-code') -const isValidAddress = require('../util').isValidAddress -module.exports = connect(mapStateToProps)(ShapeshiftForm) - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - isSubLoading: state.appState.isSubLoading, - qrRequested: state.appState.qrRequested, - } -} - -inherits(ShapeshiftForm, PersistentForm) - -function ShapeshiftForm () { - PersistentForm.call(this) - this.persistentFormParentId = 'shapeshift-buy-form' -} - -ShapeshiftForm.prototype.render = function () { - return h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.props.qrRequested ? h(Qr, {key: 'qr'}) : this.renderMain(), - ]) -} - -ShapeshiftForm.prototype.renderMain = function () { - const marketinfo = this.props.buyView.formView.marketinfo - const coinOptions = this.props.buyView.formView.coinOptions - var coin = marketinfo.pair.split('_')[0].toUpperCase() - - return h('.flex-column', { - style: { - // marginTop: '10px', - padding: '25px', - paddingTop: '5px', - width: '100%', - minHeight: '215px', - alignItems: 'center', - overflowY: 'auto', - }, - }, [ - h('.flex-row', { - style: { - justifyContent: 'center', - alignItems: 'baseline', - height: '42px', - }, - }, [ - h('img', { - src: coinOptions[coin].image, - width: '25px', - height: '25px', - style: { - marginRight: '5px', - }, - }), - - h('.input-container', [ - h('input#fromCoin.buy-inputs.ex-coins', { - type: 'text', - list: 'coinList', - autoFocus: true, - dataset: { - persistentFormId: 'input-coin', - }, - style: { - boxSizing: 'border-box', - }, - onChange: this.handleLiveInput.bind(this), - defaultValue: 'BTC', - }), - - this.renderCoinList(), - - h('i.fa.fa-pencil-square-o.edit-text', { - style: { - fontSize: '12px', - color: '#F7861C', - position: 'relative', - bottom: '48px', - left: '106px', - }, - }), - ]), - - h('.icon-control', [ - h('i.fa.fa-refresh.fa-4.orange', { - style: { - bottom: '5px', - left: '5px', - color: '#F7861C', - }, - onClick: this.updateCoin.bind(this), - }), - h('i.fa.fa-chevron-right.fa-4.orange', { - style: { - position: 'relative', - bottom: '26px', - left: '10px', - color: '#F7861C', - }, - onClick: this.updateCoin.bind(this), - }), - ]), - - h('#toCoin.ex-coins', marketinfo.pair.split('_')[1].toUpperCase()), - - h('img', { - src: coinOptions[marketinfo.pair.split('_')[1].toUpperCase()].image, - width: '25px', - height: '25px', - style: { - marginLeft: '5px', - }, - }), - ]), - h('.flex-column', { - style: { - alignItems: 'flex-start', - }, - }, [ - this.props.warning ? this.props.warning && h('span.error.flex-center', { - style: { - textAlign: 'center', - width: '229px', - height: '82px', - }, - }, - this.props.warning) : this.renderInfo(), - ]), - - h(this.activeToggle('.input-container'), { - style: { - padding: '10px', - paddingTop: '0px', - width: '100%', - }, - }, [ - - h('div', `${coin} Address:`), - - h('input#fromCoinAddress.buy-inputs', { - type: 'text', - placeholder: `Your ${coin} Refund Address`, - dataset: { - persistentFormId: 'refund-address', - }, - style: { - boxSizing: 'border-box', - width: '227px', - height: '30px', - padding: ' 5px ', - }, - }), - - h('i.fa.fa-pencil-square-o.edit-text', { - style: { - fontSize: '12px', - color: '#F7861C', - position: 'relative', - bottom: '10px', - right: '11px', - }, - }), - h('.flex-row', { - style: { - justifyContent: 'flex-end', - }, - }, [ - h('button', { - onClick: this.shift.bind(this), - style: { - marginTop: '10px', - position: 'relative', - bottom: '40px', - }, - }, - 'Submit'), - ]), - ]), - ]) -} - -ShapeshiftForm.prototype.shift = function () { - var props = this.props - var withdrawal = this.props.buyView.buyAddress - var returnAddress = document.getElementById('fromCoinAddress').value - var pair = this.props.buyView.formView.marketinfo.pair - var data = { - 'withdrawal': withdrawal, - 'pair': pair, - 'returnAddress': returnAddress, - // Public api key - 'apiKey': '803d1f5df2ed1b1476e4b9e6bcd089e34d8874595dda6a23b67d93c56ea9cc2445e98a6748b219b2b6ad654d9f075f1f1db139abfa93158c04e825db122c14b6', - } - var message = [ - `Deposit Limit: ${props.buyView.formView.marketinfo.limit}`, - `Deposit Minimum:${props.buyView.formView.marketinfo.minimum}`, - ] - if (isValidAddress(withdrawal)) { - this.props.dispatch(actions.coinShiftRquest(data, message)) - } -} - -ShapeshiftForm.prototype.renderCoinList = function () { - var list = Object.keys(this.props.buyView.formView.coinOptions).map((item) => { - return h('option', { - value: item, - }, item) - }) - - return h('datalist#coinList', { - onClick: (event) => { - event.preventDefault() - }, - }, list) -} - -ShapeshiftForm.prototype.updateCoin = function (event) { - event.preventDefault() - const props = this.props - var coinOptions = this.props.buyView.formView.coinOptions - var coin = document.getElementById('fromCoin').value - - if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { - var message = 'Not a valid coin' - return props.dispatch(actions.displayWarning(message)) - } else { - return props.dispatch(actions.pairUpdate(coin)) - } -} - -ShapeshiftForm.prototype.handleLiveInput = function () { - const props = this.props - var coinOptions = this.props.buyView.formView.coinOptions - var coin = document.getElementById('fromCoin').value - - if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { - return null - } else { - return props.dispatch(actions.pairUpdate(coin)) - } -} - -ShapeshiftForm.prototype.renderInfo = function () { - const marketinfo = this.props.buyView.formView.marketinfo - const coinOptions = this.props.buyView.formView.coinOptions - var coin = marketinfo.pair.split('_')[0].toUpperCase() - - return h('span', { - style: { - }, - }, [ - h('h3.flex-row.text-transform-uppercase', { - style: { - color: '#868686', - paddingTop: '4px', - justifyContent: 'space-around', - textAlign: 'center', - fontSize: '17px', - }, - }, `Market Info for ${marketinfo.pair.replace('_', ' to ').toUpperCase()}:`), - h('.marketinfo', ['Status : ', `${coinOptions[coin].status}`]), - h('.marketinfo', ['Exchange Rate: ', `${marketinfo.rate}`]), - h('.marketinfo', ['Limit: ', `${marketinfo.limit}`]), - h('.marketinfo', ['Minimum : ', `${marketinfo.minimum}`]), - ]) -} - -ShapeshiftForm.prototype.activeToggle = function (elementType) { - if (!this.props.buyView.formView.response || this.props.warning) return elementType - return `${elementType}.inactive` -} - -ShapeshiftForm.prototype.renderLoading = function () { - return h('span', { - style: { - position: 'absolute', - left: '70px', - bottom: '194px', - background: 'transparent', - width: '229px', - height: '82px', - display: 'flex', - justifyContent: 'center', - }, - }, [ - h('img', { - style: { - width: '60px', - }, - src: 'images/loading.svg', - }), - ]) -} diff --git a/ui/app/components/shift-list-item.js b/ui/app/components/shift-list-item.js deleted file mode 100644 index 32bfbeda4..000000000 --- a/ui/app/components/shift-list-item.js +++ /dev/null @@ -1,204 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const vreme = new (require('vreme')) -const explorerLink = require('../../lib/explorer-link') -const actions = require('../actions') -const addressSummary = require('../util').addressSummary - -const CopyButton = require('./copyButton') -const EthBalance = require('./eth-balance') -const Tooltip = require('./tooltip') - - -module.exports = connect(mapStateToProps)(ShiftListItem) - -function mapStateToProps (state) { - return { - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - } -} - -inherits(ShiftListItem, Component) - -function ShiftListItem () { - Component.call(this) -} - -ShiftListItem.prototype.render = function () { - return ( - h('.transaction-list-item.flex-row', { - style: { - paddingTop: '20px', - paddingBottom: '20px', - justifyContent: 'space-around', - alignItems: 'center', - }, - }, [ - h('div', { - style: { - width: '0px', - position: 'relative', - bottom: '19px', - }, - }, [ - h('img', { - src: 'https://info.shapeshift.io/sites/default/files/logo.png', - style: { - height: '35px', - width: '132px', - position: 'absolute', - clip: 'rect(0px,23px,34px,0px)', - }, - }), - ]), - - this.renderInfo(), - this.renderUtilComponents(), - ]) - ) -} - -function formatDate (date) { - return vreme.format(new Date(date), 'March 16 2014 14:30') -} - -ShiftListItem.prototype.renderUtilComponents = function () { - var props = this.props - const { conversionRate, currentCurrency } = props - - switch (props.response.status) { - case 'no_deposits': - return h('.flex-row', [ - h(CopyButton, { - value: this.props.depositAddress, - }), - h(Tooltip, { - title: 'QR Code', - }, [ - h('i.fa.fa-qrcode.pointer.pop-hover', { - onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType)), - style: { - margin: '5px', - marginLeft: '23px', - marginRight: '12px', - fontSize: '20px', - color: '#F7861C', - }, - }), - ]), - ]) - case 'received': - return h('.flex-row') - - case 'complete': - return h('.flex-row', [ - h(CopyButton, { - value: this.props.response.transaction, - }), - h(EthBalance, { - value: `${props.response.outgoingCoin}`, - conversionRate, - currentCurrency, - width: '55px', - shorten: true, - needsParse: false, - incoming: true, - style: { - fontSize: '15px', - color: '#01888C', - }, - }), - ]) - - case 'failed': - return '' - default: - return '' - } -} - -ShiftListItem.prototype.renderInfo = function () { - var props = this.props - switch (props.response.status) { - case 'no_deposits': - return h('.flex-column', { - style: { - width: '200px', - overflow: 'hidden', - }, - }, [ - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, `${props.depositType} to ETH via ShapeShift`), - h('div', 'No deposits received'), - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, formatDate(props.time)), - ]) - case 'received': - return h('.flex-column', { - style: { - width: '200px', - overflow: 'hidden', - }, - }, [ - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, `${props.depositType} to ETH via ShapeShift`), - h('div', 'Conversion in progress'), - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, formatDate(props.time)), - ]) - case 'complete': - var url = explorerLink(props.response.transaction, parseInt('1')) - - return h('.flex-column.pointer', { - style: { - width: '200px', - overflow: 'hidden', - }, - onClick: () => global.platform.openWindow({ url }), - }, [ - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, 'From ShapeShift'), - h('div', formatDate(props.time)), - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, addressSummary(props.response.transaction)), - ]) - - case 'failed': - return h('span.error', '(Failed)') - default: - return '' - } -} diff --git a/ui/app/components/tab-bar.js b/ui/app/components/tab-bar.js deleted file mode 100644 index 6295e7dd9..000000000 --- a/ui/app/components/tab-bar.js +++ /dev/null @@ -1,36 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = TabBar - -inherits(TabBar, Component) -function TabBar () { - Component.call(this) -} - -TabBar.prototype.render = function () { - const props = this.props - const state = this.state || {} - const { tabs = [], defaultTab, tabSelected } = props - const { subview = defaultTab } = state - - return ( - h('.flex-row.space-around.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - paddingTop: '4px', - }, - }, tabs.map((tab) => { - const { key, content } = tab - return h(subview === key ? '.activeForm' : '.inactiveForm.pointer', { - onClick: () => { - this.setState({ subview: key }) - tabSelected(key) - }, - }, content) - })) - ) -} - diff --git a/ui/app/components/template.js b/ui/app/components/template.js deleted file mode 100644 index b6ed8eaa0..000000000 --- a/ui/app/components/template.js +++ /dev/null @@ -1,18 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = NewComponent - -inherits(NewComponent, Component) -function NewComponent () { - Component.call(this) -} - -NewComponent.prototype.render = function () { - const props = this.props - - return ( - h('span', props.message) - ) -} diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js deleted file mode 100644 index 19d7139bb..000000000 --- a/ui/app/components/token-cell.js +++ /dev/null @@ -1,72 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const Identicon = require('./identicon') -const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') - -module.exports = TokenCell - -inherits(TokenCell, Component) -function TokenCell () { - Component.call(this) -} - -TokenCell.prototype.render = function () { - const props = this.props - const { address, symbol, string, network, userAddress } = props - - return ( - h('li.token-cell', { - style: { cursor: network === '1' ? 'pointer' : 'default' }, - onClick: this.view.bind(this, address, userAddress, network), - }, [ - - h(Identicon, { - diameter: 50, - address, - network, - }), - - h('h3', `${string || 0} ${symbol}`), - - h('span', { style: { flex: '1 0 auto' } }), - - /* - h('button', { - onClick: this.send.bind(this, address), - }, 'SEND'), - */ - - ]) - ) -} - -TokenCell.prototype.send = function (address, event) { - event.preventDefault() - event.stopPropagation() - const url = tokenFactoryFor(address) - if (url) { - navigateTo(url) - } -} - -TokenCell.prototype.view = function (address, userAddress, network, event) { - const url = etherscanLinkFor(address, userAddress, network) - if (url) { - navigateTo(url) - } -} - -function navigateTo (url) { - global.platform.openWindow({ url }) -} - -function etherscanLinkFor (tokenAddress, address, network) { - const prefix = prefixForNetwork(network) - return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}` -} - -function tokenFactoryFor (tokenAddress) { - return `https://tokenfactory.surge.sh/#/token/${tokenAddress}` -} - diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js deleted file mode 100644 index fed7e9f7a..000000000 --- a/ui/app/components/token-list.js +++ /dev/null @@ -1,194 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const TokenTracker = require('eth-token-tracker') -const TokenCell = require('./token-cell.js') -const normalizeAddress = require('eth-sig-util').normalize - -const defaultTokens = [] -/* -const contracts = require('eth-contract-metadata') -for (const address in contracts) { - const contract = contracts[address] - if (contract.erc20) { - contract.address = address - defaultTokens.push(contract) - } -} -*/ - -module.exports = TokenList - -inherits(TokenList, Component) -function TokenList () { - this.state = { - tokens: [], - isLoading: true, - network: null, - } - Component.call(this) -} - -TokenList.prototype.render = function () { - const state = this.state - const { tokens, isLoading, error } = state - const { userAddress, network } = this.props - - if (isLoading) { - return this.message('Loading') - } - - if (error) { - log.error(error) - return this.message('There was a problem loading your token balances.') - } - - const tokenViews = tokens.map((tokenData) => { - tokenData.network = network - tokenData.userAddress = userAddress - return h(TokenCell, tokenData) - }) - - return h('div', [ - h('ol', { - style: { - height: '260px', - overflowY: 'auto', - display: 'flex', - flexDirection: 'column', - }, - }, [ - h('style', ` - - li.token-cell { - display: flex; - flex-direction: row; - align-items: center; - padding: 10px; - } - - li.token-cell > h3 { - margin-left: 12px; - } - - li.token-cell:hover { - background: white; - cursor: pointer; - } - - `), - ...tokenViews, - tokenViews.length ? null : this.message('No Tokens Found.'), - ]), - this.addTokenButtonElement(), - ]) -} - -TokenList.prototype.addTokenButtonElement = function () { - return h('div', [ - h('div.footer.hover-white.pointer', { - key: 'reveal-account-bar', - onClick: () => { - this.props.addToken() - }, - style: { - display: 'flex', - height: '40px', - padding: '10px', - justifyContent: 'center', - alignItems: 'center', - }, - }, [ - h('i.fa.fa-plus.fa-lg'), - ]), - ]) -} - -TokenList.prototype.message = function (body) { - return h('div', { - style: { - display: 'flex', - height: '250px', - alignItems: 'center', - justifyContent: 'center', - padding: '30px', - }, - }, body) -} - -TokenList.prototype.componentDidMount = function () { - this.createFreshTokenTracker() -} - -TokenList.prototype.createFreshTokenTracker = function () { - if (this.tracker) { - // Clean up old trackers when refreshing: - this.tracker.stop() - this.tracker.removeListener('update', this.balanceUpdater) - this.tracker.removeListener('error', this.showError) - } - - if (!global.ethereumProvider) return - const { userAddress } = this.props - this.tracker = new TokenTracker({ - userAddress, - provider: global.ethereumProvider, - tokens: uniqueMergeTokens(defaultTokens, this.props.tokens), - pollingInterval: 8000, - }) - - - // Set up listener instances for cleaning up - this.balanceUpdater = this.updateBalances.bind(this) - this.showError = (error) => { - this.setState({ error, isLoading: false }) - } - this.tracker.on('update', this.balanceUpdater) - this.tracker.on('error', this.showError) - - this.tracker.updateBalances() - .then(() => { - this.updateBalances(this.tracker.serialize()) - }) - .catch((reason) => { - log.error(`Problem updating balances`, reason) - this.setState({ isLoading: false }) - }) -} - -TokenList.prototype.componentWillUpdate = function (nextProps) { - if (nextProps.network === 'loading') return - const oldNet = this.props.network - const newNet = nextProps.network - - if (oldNet && newNet && newNet !== oldNet) { - this.setState({ isLoading: true }) - this.createFreshTokenTracker() - } -} - -TokenList.prototype.updateBalances = function (tokens) { - const heldTokens = tokens.filter(token => { - return token.balance !== '0' && token.string !== '0.000' - }) - this.setState({ tokens: heldTokens, isLoading: false }) -} - -TokenList.prototype.componentWillUnmount = function () { - if (!this.tracker) return - this.tracker.stop() -} - -function uniqueMergeTokens (tokensA, tokensB) { - const uniqueAddresses = [] - const result = [] - tokensA.concat(tokensB).forEach((token) => { - const normal = normalizeAddress(token.address) - if (!uniqueAddresses.includes(normal)) { - uniqueAddresses.push(normal) - result.push(token) - } - }) - return result -} - diff --git a/ui/app/components/tooltip.js b/ui/app/components/tooltip.js deleted file mode 100644 index edbc074bb..000000000 --- a/ui/app/components/tooltip.js +++ /dev/null @@ -1,22 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ReactTooltip = require('react-tooltip-component') - -module.exports = Tooltip - -inherits(Tooltip, Component) -function Tooltip () { - Component.call(this) -} - -Tooltip.prototype.render = function () { - const props = this.props - const { position, title, children } = props - - return h(ReactTooltip, { - position: position || 'left', - title, - fixed: false, - }, children) -} diff --git a/ui/app/components/transaction-list-item-icon.js b/ui/app/components/transaction-list-item-icon.js deleted file mode 100644 index 431054340..000000000 --- a/ui/app/components/transaction-list-item-icon.js +++ /dev/null @@ -1,68 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const Tooltip = require('./tooltip') - -const Identicon = require('./identicon') - -module.exports = TransactionIcon - -inherits(TransactionIcon, Component) -function TransactionIcon () { - Component.call(this) -} - -TransactionIcon.prototype.render = function () { - const { transaction, txParams, isMsg } = this.props - switch (transaction.status) { - case 'unapproved': - return h(!isMsg ? '.unapproved-tx-icon' : 'i.fa.fa-certificate.fa-lg') - - case 'rejected': - return h('i.fa.fa-exclamation-triangle.fa-lg.warning', { - style: { - width: '24px', - }, - }) - - case 'failed': - return h('i.fa.fa-exclamation-triangle.fa-lg.error', { - style: { - width: '24px', - }, - }) - - case 'submitted': - return h(Tooltip, { - title: 'Pending', - position: 'bottom', - }, [ - h('i.fa.fa-ellipsis-h', { - style: { - fontSize: '27px', - }, - }), - ]) - } - - if (isMsg) { - return h('i.fa.fa-certificate.fa-lg', { - style: { - width: '24px', - }, - }) - } - - if (txParams.to) { - return h(Identicon, { - diameter: 24, - address: txParams.to || transaction.hash, - }) - } else { - return h('i.fa.fa-file-text-o.fa-lg', { - style: { - width: '24px', - }, - }) - } -} diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js deleted file mode 100644 index dbda66a31..000000000 --- a/ui/app/components/transaction-list-item.js +++ /dev/null @@ -1,165 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const EthBalance = require('./eth-balance') -const addressSummary = require('../util').addressSummary -const explorerLink = require('../../lib/explorer-link') -const CopyButton = require('./copyButton') -const vreme = new (require('vreme')) -const Tooltip = require('./tooltip') -const numberToBN = require('number-to-bn') - -const TransactionIcon = require('./transaction-list-item-icon') -const ShiftListItem = require('./shift-list-item') -module.exports = TransactionListItem - -inherits(TransactionListItem, Component) -function TransactionListItem () { - Component.call(this) -} - -TransactionListItem.prototype.render = function () { - const { transaction, network, conversionRate, currentCurrency } = this.props - if (transaction.key === 'shapeshift') { - if (network === '1') return h(ShiftListItem, transaction) - } - var date = formatDate(transaction.time) - - let isLinkable = false - const numericNet = parseInt(network) - isLinkable = numericNet === 1 || numericNet === 3 || numericNet === 4 || numericNet === 42 - - var isMsg = ('msgParams' in transaction) - var isTx = ('txParams' in transaction) - var isPending = transaction.status === 'unapproved' - let txParams - if (isTx) { - txParams = transaction.txParams - } else if (isMsg) { - txParams = transaction.msgParams - } - - const nonce = txParams.nonce ? numberToBN(txParams.nonce).toString(10) : '' - - const isClickable = ('hash' in transaction && isLinkable) || isPending - return ( - h(`.transaction-list-item.flex-row.flex-space-between${isClickable ? '.pointer' : ''}`, { - onClick: (event) => { - if (isPending) { - this.props.showTx(transaction.id) - } - event.stopPropagation() - if (!transaction.hash || !isLinkable) return - var url = explorerLink(transaction.hash, parseInt(network)) - global.platform.openWindow({ url }) - }, - style: { - padding: '20px 0', - }, - }, [ - - h('.identicon-wrapper.flex-column.flex-center.select-none', [ - h('.pop-hover', { - onClick: (event) => { - event.stopPropagation() - if (!isTx || isPending) return - var url = `https://metamask.github.io/eth-tx-viz/?tx=${transaction.hash}` - global.platform.openWindow({ url }) - }, - }, [ - h(TransactionIcon, { txParams, transaction, isTx, isMsg }), - ]), - ]), - - h(Tooltip, { - title: 'Transaction Number', - position: 'bottom', - }, [ - h('span', { - style: { - display: 'flex', - cursor: 'normal', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - padding: '10px', - }, - }, nonce), - ]), - - h('.flex-column', {style: {width: '200px', overflow: 'hidden'}}, [ - domainField(txParams), - h('div', date), - recipientField(txParams, transaction, isTx, isMsg), - ]), - - // Places a copy button if tx is successful, else places a placeholder empty div. - transaction.hash ? h(CopyButton, { value: transaction.hash }) : h('div', {style: { display: 'flex', alignItems: 'center', width: '26px' }}), - - isTx ? h(EthBalance, { - value: txParams.value, - conversionRate, - currentCurrency, - width: '55px', - shorten: true, - showFiat: false, - style: {fontSize: '15px'}, - }) : h('.flex-column'), - ]) - ) -} - -function domainField (txParams) { - return h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - overflow: 'hidden', - textOverflow: 'ellipsis', - width: '100%', - }, - }, [ - txParams.origin, - ]) -} - -function recipientField (txParams, transaction, isTx, isMsg) { - let message - - if (isMsg) { - message = 'Signature Requested' - } else if (txParams.to) { - message = addressSummary(txParams.to) - } else { - message = 'Contract Published' - } - - return h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - }, - }, [ - message, - failIfFailed(transaction), - ]) -} - -function formatDate (date) { - return vreme.format(new Date(date), 'March 16 2014 14:30') -} - -function failIfFailed (transaction) { - if (transaction.status === 'rejected') { - return h('span.error', ' (Rejected)') - } - if (transaction.err) { - return h(Tooltip, { - title: transaction.err.message, - position: 'bottom', - }, [ - h('span.error', ' (Failed)'), - ]) - } -} diff --git a/ui/app/components/transaction-list.js b/ui/app/components/transaction-list.js deleted file mode 100644 index 3b4ba741e..000000000 --- a/ui/app/components/transaction-list.js +++ /dev/null @@ -1,79 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const TransactionListItem = require('./transaction-list-item') - -module.exports = TransactionList - - -inherits(TransactionList, Component) -function TransactionList () { - Component.call(this) -} - -TransactionList.prototype.render = function () { - const { transactions, network, unapprovedMsgs, conversionRate } = this.props - - var shapeShiftTxList - if (network === '1') { - shapeShiftTxList = this.props.shapeShiftTxList - } - const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList) - .sort((a, b) => b.time - a.time) - - return ( - - h('section.transaction-list', [ - - h('style', ` - .transaction-list .transaction-list-item:not(:last-of-type) { - border-bottom: 1px solid #D4D4D4; - } - .transaction-list .transaction-list-item .ether-balance-label { - display: block !important; - font-size: small; - } - `), - - h('.tx-list', { - style: { - overflowY: 'auto', - height: '300px', - padding: '0 20px', - textAlign: 'center', - }, - }, [ - - txsToRender.length - ? txsToRender.map((transaction, i) => { - let key - switch (transaction.key) { - case 'shapeshift': - const { depositAddress, time } = transaction - key = `shift-tx-${depositAddress}-${time}-${i}` - break - default: - key = `tx-${transaction.id}-${i}` - } - return h(TransactionListItem, { - transaction, i, network, key, - conversionRate, - showTx: (txId) => { - this.props.viewPendingTx(txId) - }, - }) - }) - : h('.flex-center', { - style: { - flexDirection: 'column', - height: '100%', - }, - }, [ - 'No transaction history.', - ]), - ]), - ]) - ) -} - diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js deleted file mode 100644 index 747d3ce2b..000000000 --- a/ui/app/conf-tx.js +++ /dev/null @@ -1,213 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') -const NetworkIndicator = require('./components/network') -const txHelper = require('../lib/tx-helper') -const isPopupOrNotification = require('../../app/scripts/lib/is-popup-or-notification') - -const PendingTx = require('./components/pending-tx') -const PendingMsg = require('./components/pending-msg') -const PendingPersonalMsg = require('./components/pending-personal-msg') -const Loading = require('./components/loading') - -module.exports = connect(mapStateToProps)(ConfirmTxScreen) - -function mapStateToProps (state) { - return { - identities: state.metamask.identities, - accounts: state.metamask.accounts, - selectedAddress: state.metamask.selectedAddress, - unapprovedTxs: state.metamask.unapprovedTxs, - unapprovedMsgs: state.metamask.unapprovedMsgs, - unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs, - index: state.appState.currentView.context, - warning: state.appState.warning, - network: state.metamask.network, - provider: state.metamask.provider, - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - blockGasLimit: state.metamask.currentBlockGasLimit, - } -} - -inherits(ConfirmTxScreen, Component) -function ConfirmTxScreen () { - Component.call(this) -} - -ConfirmTxScreen.prototype.render = function () { - const props = this.props - const { network, provider, unapprovedTxs, currentCurrency, - unapprovedMsgs, unapprovedPersonalMsgs, conversionRate, blockGasLimit } = props - - var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) - - var txData = unconfTxList[props.index] || {} - var txParams = txData.params || {} - var isNotification = isPopupOrNotification() === 'notification' - - - log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`) - if (unconfTxList.length === 0) return h(Loading, { isLoading: true }) - - return ( - - h('.flex-column.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - !isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: this.goHome.bind(this), - }) : null, - h('h2.page-subtitle', 'Confirm Transaction'), - isNotification ? h(NetworkIndicator, { - network: network, - provider: provider, - }) : null, - ]), - - h('h3', { - style: { - alignSelf: 'center', - display: unconfTxList.length > 1 ? 'block' : 'none', - }, - }, [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - style: { - display: props.index === 0 ? 'none' : 'inline-block', - }, - onClick: () => props.dispatch(actions.previousTx()), - }), - ` ${props.index + 1} of ${unconfTxList.length} `, - h('i.fa.fa-arrow-right.fa-lg.cursor-pointer', { - style: { - display: props.index + 1 === unconfTxList.length ? 'none' : 'inline-block', - }, - onClick: () => props.dispatch(actions.nextTx()), - }), - ]), - - warningIfExists(props.warning), - - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - - currentTxView({ - // Properties - txData: txData, - key: txData.id, - selectedAddress: props.selectedAddress, - accounts: props.accounts, - identities: props.identities, - conversionRate, - currentCurrency, - blockGasLimit, - // Actions - buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), - sendTransaction: this.sendTransaction.bind(this), - cancelTransaction: this.cancelTransaction.bind(this, txData), - signMessage: this.signMessage.bind(this, txData), - signPersonalMessage: this.signPersonalMessage.bind(this, txData), - cancelMessage: this.cancelMessage.bind(this, txData), - cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), - }), - - ]), - ]) - ) -} - -function currentTxView (opts) { - log.info('rendering current tx view') - const { txData } = opts - const { txParams, msgParams, type } = txData - - if (txParams) { - log.debug('txParams detected, rendering pending tx') - return h(PendingTx, opts) - } else if (msgParams) { - log.debug('msgParams detected, rendering pending msg') - - if (type === 'eth_sign') { - log.debug('rendering eth_sign message') - return h(PendingMsg, opts) - } else if (type === 'personal_sign') { - log.debug('rendering personal_sign message') - return h(PendingPersonalMsg, opts) - } - } -} - -ConfirmTxScreen.prototype.buyEth = function (address, event) { - event.preventDefault() - this.props.dispatch(actions.buyEthView(address)) -} - -ConfirmTxScreen.prototype.sendTransaction = function (txData, event) { - this.stopPropagation(event) - this.props.dispatch(actions.updateAndApproveTx(txData)) -} - -ConfirmTxScreen.prototype.cancelTransaction = function (txData, event) { - this.stopPropagation(event) - event.preventDefault() - this.props.dispatch(actions.cancelTx(txData)) -} - -ConfirmTxScreen.prototype.signMessage = function (msgData, event) { - log.info('conf-tx.js: signing message') - var params = msgData.msgParams - params.metamaskId = msgData.id - this.stopPropagation(event) - this.props.dispatch(actions.signMsg(params)) -} - -ConfirmTxScreen.prototype.stopPropagation = function (event) { - if (event.stopPropagation) { - event.stopPropagation() - } -} - -ConfirmTxScreen.prototype.signPersonalMessage = function (msgData, event) { - log.info('conf-tx.js: signing personal message') - var params = msgData.msgParams - params.metamaskId = msgData.id - this.stopPropagation(event) - this.props.dispatch(actions.signPersonalMsg(params)) -} - -ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) { - log.info('canceling message') - this.stopPropagation(event) - this.props.dispatch(actions.cancelMsg(msgData)) -} - -ConfirmTxScreen.prototype.cancelPersonalMessage = function (msgData, event) { - log.info('canceling personal message') - this.stopPropagation(event) - this.props.dispatch(actions.cancelPersonalMsg(msgData)) -} - -ConfirmTxScreen.prototype.goHome = function (event) { - this.stopPropagation(event) - this.props.dispatch(actions.goHome()) -} - -function warningIfExists (warning) { - if (warning && - // Do not display user rejections on this screen: - warning.indexOf('User denied transaction signature') === -1) { - return h('.error', { - style: { - margin: 'auto', - }, - }, warning) - } -} diff --git a/ui/app/config.js b/ui/app/config.js deleted file mode 100644 index 62785c49b..000000000 --- a/ui/app/config.js +++ /dev/null @@ -1,211 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') -const currencies = require('./conversion.json').rows -const validUrl = require('valid-url') -const copyToClipboard = require('copy-to-clipboard') - -module.exports = connect(mapStateToProps)(ConfigScreen) - -function mapStateToProps (state) { - return { - metamask: state.metamask, - warning: state.appState.warning, - } -} - -inherits(ConfigScreen, Component) -function ConfigScreen () { - Component.call(this) -} - -ConfigScreen.prototype.render = function () { - var state = this.props - var metamaskState = state.metamask - var warning = state.warning - - return ( - h('.flex-column.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: (event) => { - state.dispatch(actions.goHome()) - }, - }), - h('h2.page-subtitle', 'Settings'), - ]), - - h('.error', { - style: { - display: warning ? 'block' : 'none', - padding: '0 20px', - textAlign: 'center', - }, - }, warning), - - // conf view - h('.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-space-around', { - style: { - padding: '20px', - }, - }, [ - - currentProviderDisplay(metamaskState), - - h('div', { style: {display: 'flex'} }, [ - h('input#new_rpc', { - placeholder: 'New RPC URL', - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - onKeyPress (event) { - if (event.key === 'Enter') { - var element = event.target - var newRpc = element.value - rpcValidation(newRpc, state) - } - }, - }), - h('button', { - style: { - alignSelf: 'center', - }, - onClick (event) { - event.preventDefault() - var element = document.querySelector('input#new_rpc') - var newRpc = element.value - rpcValidation(newRpc, state) - }, - }, 'Save'), - ]), - - h('hr.horizontal-line'), - - currentConversionInformation(metamaskState, state), - - h('hr.horizontal-line'), - - h('div', { - style: { - marginTop: '20px', - }, - }, [ - h('p', { - style: { - fontFamily: 'Montserrat Light', - fontSize: '13px', - }, - }, `State logs contain your public account addresses and sent transactions.`), - h('br'), - h('button', { - style: { - alignSelf: 'center', - }, - onClick (event) { - copyToClipboard(window.logState()) - }, - }, 'Copy State Logs'), - ]), - - h('hr.horizontal-line'), - - h('div', { - style: { - marginTop: '20px', - }, - }, [ - h('button', { - style: { - alignSelf: 'center', - }, - onClick (event) { - event.preventDefault() - state.dispatch(actions.revealSeedConfirmation()) - }, - }, 'Reveal Seed Words'), - ]), - - ]), - ]), - ]) - ) -} - -function rpcValidation (newRpc, state) { - if (validUrl.isWebUri(newRpc)) { - state.dispatch(actions.setRpcTarget(newRpc)) - } else { - var appendedRpc = `http://${newRpc}` - if (validUrl.isWebUri(appendedRpc)) { - state.dispatch(actions.displayWarning('URIs require the appropriate HTTP/HTTPS prefix.')) - } else { - state.dispatch(actions.displayWarning('Invalid RPC URI')) - } - } -} - -function currentConversionInformation (metamaskState, state) { - var currentCurrency = metamaskState.currentCurrency - var conversionDate = metamaskState.conversionDate - return h('div', [ - h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, 'Current Conversion'), - h('span', {style: { fontWeight: 'bold', paddingRight: '10px', fontSize: '13px'}}, `Updated ${Date(conversionDate)}`), - h('select#currentCurrency', { - onChange (event) { - event.preventDefault() - var element = document.getElementById('currentCurrency') - var newCurrency = element.value - state.dispatch(actions.setCurrentCurrency(newCurrency)) - }, - defaultValue: currentCurrency, - }, currencies.map((currency) => { - return h('option', {key: currency.code, value: currency.code}, `${currency.code} - ${currency.name}`) - }) - ), - ]) -} - -function currentProviderDisplay (metamaskState) { - var provider = metamaskState.provider - var title, value - - switch (provider.type) { - - case 'mainnet': - title = 'Current Network' - value = 'Main Ethereum Network' - break - - case 'ropsten': - title = 'Current Network' - value = 'Ropsten Test Network' - break - - case 'kovan': - title = 'Current Network' - value = 'Kovan Test Network' - break - - case 'rinkeby': - title = 'Current Network' - value = 'Rinkeby Test Network' - break - - default: - title = 'Current RPC' - value = metamaskState.provider.rpcTarget - } - - return h('div', [ - h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, title), - h('span', value), - ]) -} diff --git a/ui/app/conversion.json b/ui/app/conversion.json deleted file mode 100644 index 155ffc4fc..000000000 --- a/ui/app/conversion.json +++ /dev/null @@ -1,207 +0,0 @@ -{ - "rows": [ - { - "code": "REP", - "name": "Augur", - "statuses": [ - "primary" - ] - }, - { - "code": "BCN", - "name": "Bytecoin", - "statuses": [ - "primary" - ] - }, - { - "code": "BTC", - "name": "Bitcoin", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "BTS", - "name": "BitShares", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "BLK", - "name": "Blackcoin", - "statuses": [ - "primary" - ] - }, - { - "code": "GBP", - "name": "British Pound Sterling", - "statuses": [ - "secondary" - ] - }, - { - "code": "CAD", - "name": "Canadian Dollar", - "statuses": [ - "secondary" - ] - }, - { - "code": "CNY", - "name": "Chinese Yuan", - "statuses": [ - "secondary" - ] - }, - { - "code": "DSH", - "name": "Dashcoin", - "statuses": [ - "primary" - ] - }, - { - "code": "DOGE", - "name": "Dogecoin", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "ETC", - "name": "Ethereum Classic", - "statuses": [ - "primary" - ] - }, - { - "code": "EUR", - "name": "Euro", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "GNO", - "name": "GNO", - "statuses": [ - "primary" - ] - }, - { - "code": "GNT", - "name": "GNT", - "statuses": [ - "primary" - ] - }, - { - "code": "JPY", - "name": "Japanese Yen", - "statuses": [ - "secondary" - ] - }, - { - "code": "LTC", - "name": "Litecoin", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "MAID", - "name": "MaidSafeCoin", - "statuses": [ - "primary" - ] - }, - { - "code": "XEM", - "name": "NEM", - "statuses": [ - "primary" - ] - }, - { - "code": "XLM", - "name": "Stellar", - "statuses": [ - "primary" - ] - }, - { - "code": "XMR", - "name": "Monero", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "XRP", - "name": "Ripple", - "statuses": [ - "primary" - ] - }, - { - "code": "RUR", - "name": "Ruble", - "statuses": [ - "secondary" - ] - }, - { - "code": "STEEM", - "name": "Steem", - "statuses": [ - "primary" - ] - }, - { - "code": "STRAT", - "name": "STRAT", - "statuses": [ - "primary" - ] - }, - { - "code": "UAH", - "name": "Ukrainian Hryvnia", - "statuses": [ - "secondary" - ] - }, - { - "code": "USD", - "name": "US Dollar", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "WAVES", - "name": "WAVES", - "statuses": [ - "primary" - ] - }, - { - "code": "ZEC", - "name": "Zcash", - "statuses": [ - "primary" - ] - } - ] -} diff --git a/ui/app/css/debug.css b/ui/app/css/debug.css deleted file mode 100644 index 3e125bcd4..000000000 --- a/ui/app/css/debug.css +++ /dev/null @@ -1,21 +0,0 @@ -/* -debug / dev -*/ - -#app-content { - border: 2px solid green; -} - -#design-container { - position: absolute; - left: 360px; - top: -42px; - width: calc(100vw - 360px); - height: 100vh; - overflow: scroll; -} - -#design-container img { - width: 2000px; - margin-right: 600px; -} \ No newline at end of file diff --git a/ui/app/css/fonts.css b/ui/app/css/fonts.css deleted file mode 100644 index 3b9f581b9..000000000 --- a/ui/app/css/fonts.css +++ /dev/null @@ -1,36 +0,0 @@ -@import url(https://fonts.googleapis.com/css?family=Roboto:300,500); -@import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css); - -@font-face { - font-family: 'Montserrat Regular'; - src: url('/fonts/Montserrat/Montserrat-Regular.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-Regular.ttf') format('truetype'); - font-weight: normal; - font-style: normal; - font-size: 'small'; - -} - -@font-face { - font-family: 'Montserrat Bold'; - src: url('/fonts/Montserrat/Montserrat-Bold.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-Bold.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'Montserrat Light'; - src: url('/fonts/Montserrat/Montserrat-Light.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-Light.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'Montserrat UltraLight'; - src: url('/fonts/Montserrat/Montserrat-UltraLight.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-UltraLight.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} diff --git a/ui/app/css/index.css b/ui/app/css/index.css deleted file mode 100644 index 808aafb4c..000000000 --- a/ui/app/css/index.css +++ /dev/null @@ -1,667 +0,0 @@ -/* -faint orange (textfield shades) #FAF6F0 -light orange (button shades): #F5C26D -dark orange (text): #F5A623 -borders/font/any gray: #4A4A4A -*/ - -/* -application specific styles -*/ - -* { - box-sizing: border-box; -} - -html, body { - font-family: 'Montserrat Regular', Arial; - color: #4D4D4D; - font-weight: 300; - line-height: 1.4em; - background: #F7F7F7; -} - -input:focus, textarea:focus { - outline: none; -} - -#app-content { - overflow-x: hidden; - min-width: 357px; - width: 360px; - height: 500px; -} - -button, input[type="submit"] { - font-family: 'Montserrat Bold'; - outline: none; - cursor: pointer; - padding: 8px 12px; - border: none; - color: white; - transform-origin: center center; - transition: transform 50ms ease-in; - /* default orange */ - background: rgba(247, 134, 28, 1); - box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); -} - -.btn-green, input[type="submit"].btn-green { - background: rgba(106, 195, 96, 1); - box-shadow: 0px 3px 6px rgba(106, 195, 96, 0.36); -} - -.btn-red { - background: rgba(254, 35, 17, 1); - box-shadow: 0px 3px 6px rgba(254, 35, 17, 0.36); -} - -button[disabled], input[type="submit"][disabled] { - cursor: not-allowed; - background: rgba(197, 197, 197, 1); - box-shadow: 0px 3px 6px rgba(197, 197, 197, 0.36); -} - -button.spaced { - margin: 2px; -} - -button:not([disabled]):hover, input[type="submit"]:not([disabled]):hover { - transform: scale(1.1); -} -button:not([disabled]):active, input[type="submit"]:not([disabled]):active { - transform: scale(0.95); -} - -a { - text-decoration: none; - color: inherit; -} - -a:hover{ - color: #df6b0e; -} - -/* -app -*/ - -.active { - color: #909090; -} - -button.primary { - padding: 8px 12px; - background: #F7861C; - box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); - color: white; - font-size: 1.1em; - font-family: 'Montserrat Regular'; - text-transform: uppercase; -} - -button.btn-thin { - border: 1px solid; - border-color: #4D4D4D; - color: #4D4D4D; - background: rgb(255, 174, 41); - border-radius: 4px; - min-width: 200px; - margin: 12px 0; - padding: 6px; - font-size: 13px; -} - -.app-header { - padding: 6px 8px; -} - -.app-header h1 { - font-family: 'Montserrat Regular'; - text-transform: uppercase; - color: #AEAEAE; -} - -h2.page-subtitle { - font-family: 'Montserrat Regular'; - text-transform: uppercase; - color: #AEAEAE; - font-size: 1em; - margin: 12px; -} - -.app-primary { - -} - -.app-footer { - padding-bottom: 10px; - align-items: center; -} - -.identicon { - height: 46px; - width: 46px; - background-size: cover; - border-radius: 100%; - border: 3px solid gray; -} - -textarea.twelve-word-phrase { - padding: 12px; - width: 300px; - height: 140px; - font-size: 16px; - background: white; - resize: none; -} - -.network-indicator { - display: flex; - align-items: center; - font-size: 0.6em; - -} - -.network-name { - width: 5.2em; - line-height: 9px; - text-rendering: geometricPrecision; -} - -.check { - margin-left: 7px; - color: #F7861C; - flex: 1 0 auto; - display: flex; - justify-content: flex-end; -} -/* -app sections -*/ - -/* initialize */ - -.initialize-screen hr { - width: 60px; - margin: 12px; - border-color: #F7861C; - border-style: solid; -} - -.initialize-screen label { - margin-top: 20px; -} - -.initialize-screen button.create-vault { - margin-top: 40px; -} - -.initialize-screen .warning { - font-size: 14px; - margin: 0 16px; -} - -/* unlock */ -.error { - color: #E20202; -} - -.warning { - color: #FFAE00; -} - -.lock { - width: 50px; - height: 50px; -} - -.lock.locked { - transform: scale(1.5); - opacity: 0.0; - transition: opacity 400ms ease-in, transform 400ms ease-in; -} -.lock.unlocked { - transform: scale(1); - opacity: 1; - transition: opacity 500ms ease-out, transform 500ms ease-out, background 200ms ease-in; -} - -.lock.locked .lock-top { - transform: scaleX(1) translateX(0); - transition: transform 250ms ease-in; -} -.lock.unlocked .lock-top { - transform: scaleX(-1) translateX(-12px); - transition: transform 250ms ease-in; -} -.lock.unlocked:hover { - border-radius: 4px; - background: #e5e5e5; - border: 1px solid #b1b1b1; -} -.lock.unlocked:active { - background: #c3c3c3; -} - -.section-title .fa-arrow-left { - margin: -2px 8px 0px -8px; -} - -.unlock-screen #metamask-mascot-container { - margin-top: 24px; -} - -.unlock-screen h1 { - margin-top: -28px; - margin-bottom: 42px; -} - -.unlock-screen input[type=password] { - width: 260px; - /*height: 36px; - margin-bottom: 24px; - padding: 8px;*/ -} - -.sizing-input{ - font-size: 14px; - height: 30px; - padding-left: 5px; -} -.editable-label{ - display: flex; -} -/* Webkit */ -.unlock-screen input::-webkit-input-placeholder { - text-align: center; - font-size: 1.2em; -} -/* Firefox 18- */ -.unlock-screen input:-moz-placeholder { - text-align: center; - font-size: 1.2em; -} -/* Firefox 19+ */ -.unlock-screen input::-moz-placeholder { - text-align: center; - font-size: 1.2em; -} -/* IE */ -.unlock-screen input:-ms-input-placeholder { - text-align: center; - font-size: 1.2em; -} - -input.large-input, textarea.large-input { - /*margin-bottom: 24px;*/ - padding: 8px; -} - -input.large-input { - height: 36px; -} - -.letter-spacey { - letter-spacing: 0.1em; -} - - - -/* accounts */ - -.accounts-section { - margin: 0 0px; -} - -.accounts-section .horizontal-line { - margin: 0px 18px; -} - -.accounts-list-option { - height: 120px; -} - -.accounts-list-option .identicon-wrapper { - width: 100px; -} - -.unconftx-link { - margin-top: 24px; - cursor: pointer; -} - -.unconftx-link .fa-arrow-right { - margin: 0px -8px 0px 8px; -} - -/* identity panel */ - -.identity-panel { - font-weight: 500; -} - -.identity-panel .identicon-wrapper { - margin: 4px; - margin-top: 8px; - display: flex; - align-items: center; -} - -.identity-panel .identicon-wrapper span { - margin: 0 auto; -} - -.identity-panel .identity-data { - margin: 8px 8px 8px 18px; -} - -.identity-panel i { - margin-top: 32px; - margin-right: 6px; - color: #B9B9B9; -} - -.identity-panel .arrow-right { - padding-left: 18px; - width: 42px; - min-width: 18px; - height: 100%; -} - -.identity-copy.flex-column { - flex: 0.25 0 auto; - justify-content: center; -} - -/* accounts screen */ - -.identity-section { - -} - -.identity-section .identity-panel { - background: #E9E9E9; - border-bottom: 1px solid #B1B1B1; - cursor: pointer; -} - -.identity-section .identity-panel.selected { - background: white; - color: #F3C83E; -} - -.identity-section .identity-panel.selected .identicon { - border-color: orange; -} - -.identity-section .accounts-list-option:hover, -.identity-section .accounts-list-option.selected { - background:white; -} - -/* account detail screen */ - -.account-detail-section { - -} -.name-label{ - -} - -.unapproved-tx-icon { - height: 16px; - width: 16px; - background: rgb(47, 174, 244); - border-color: #AEAEAE; - border-radius: 13px; -} - -.edit-text { - height: 100%; - visibility: hidden; -} -.editing-label { - display: flex; - justify-content: flex-start; - margin-left: 50px; - margin-bottom: 2px; - font-size: 11px; - text-rendering: geometricPrecision; - color: #F7861C; -} -.name-label:hover .edit-text { - visibility: visible; -} -/* tx confirm */ - -.unconftx-section input[type=password] { - height: 22px; - padding: 2px; - margin: 12px; - margin-bottom: 24px; - border-radius: 4px; - border: 2px solid #F3C83E; - background: #FAF6F0; -} - -/* Send Screen */ - -.send-screen { - -} - -.send-screen section { - margin: 8px 16px; -} - -.send-screen input { - width: 100%; - font-size: 12px; -} - -/* Ether Balance Widget */ - -.ether-balance-amount { - color: #F7861C; -} - -.ether-balance-label { - color: #ABA9AA; -} - -/* Info screen */ -.info-gray{ - font-family: 'Montserrat Regular'; - text-transform: uppercase; - color: #AEAEAE; -} - -.icon-size{ - width: 20px; -} - -.info{ - font-family: 'Montserrat Regular', Arial; - padding-bottom: 10px; - display: inline-block; - padding-left: 5px; -} - -/* buy eth warning screen */ -.custom-radios { - justify-content: space-around; - align-items: center; -} - - -.custom-radio-selected { - width: 17px; - height: 17px; - border: solid; - border-style: double; - border-radius: 15px; - border-width: 5px; - background: rgba(247, 134, 28, 1); - border-color: #F7F7F7; -} - -.custom-radio-inactive { - width: 14px; - height: 14px; - border: solid; - border-width: 1px; - border-radius: 24px; - border-color: #AEAEAE; -} - -.radio-titles { - color: rgba(247, 134, 28, 1); -} - -.radio-titles-subtext { - -} - -.selected-exchange { - -} - -.buy-radio { - -} - -.eth-warning{ - transition: opacity 400ms ease-in, transform 400ms ease-in; -} - -.buy-subview{ - transition: opacity 400ms ease-in, transform 400ms ease-in; -} - -.input-container:hover .edit-text{ - visibility: visible; -} - -.buy-inputs{ - font-family: 'Montserrat Light'; - font-size: 13px; - height: 20px; - background: transparent; - box-sizing: border-box; - border: solid; - border-color: transparent; - border-width: 0.5px; - border-radius: 2px; - -} -.input-container:hover .buy-inputs{ - box-sizing: inherit; - border: solid; - border-color: #F7861C; - border-width: 0.5px; - border-radius: 2px; -} - -.buy-inputs:focus{ - border: solid; - border-color: #F7861C; - border-width: 0.5px; - border-radius: 2px; -} - -.activeForm { - background: #F7F7F7; - border: none; - border-radius: 8px 8px 0px 0px; - width: 50%; - text-align: center; - padding-bottom: 4px; - -} - -.inactiveForm { - border: none; - border-radius: 8px 8px 0px 0px; - width: 50%; - text-align: center; - padding-bottom: 4px; -} - -.ex-coins { - font-family: 'Montserrat Regular'; - text-transform: uppercase; - text-align: center; - font-size: 33px; - width: 118px; - height: 42px; - padding: 1px; - color: #4D4D4D; -} - -.marketinfo{ - font-family: 'Montserrat light'; - color: #AEAEAE; - font-size: 15px; - line-height: 17px; -} - -#fromCoin::-webkit-calendar-picker-indicator { - display: none; -} - -#coinList { - width: 400px; - height: 500px; - overflow: scroll; -} - -.icon-control .fa-refresh{ - visibility: hidden; -} - -.icon-control:hover .fa-refresh{ - visibility: visible; -} - -.icon-control:hover .fa-chevron-right{ - visibility: hidden; -} - -.inactive { - color: #AEAEAE; -} - -.inactive button{ - background: #AEAEAE; - color: white; -} - -.ellip-address { - overflow: hidden; - text-overflow: ellipsis; - width: 5em; - font-size: 14px; - font-family: "Montserrat Light"; - margin-left: 5px; -} - -.qr-header { - font-size: 25px; - margin-top: 40px; -} - -.qr-message { - font-size: 12px; - color: #F7861C; -} - -div.message-container > div:first-child { - margin-top: 18px; - font-size: 15px; - color: #4D4D4D; -} - -.pop-hover:hover { - transform: scale(1.1); -} diff --git a/ui/app/css/lib.css b/ui/app/css/lib.css deleted file mode 100644 index 910a24ee2..000000000 --- a/ui/app/css/lib.css +++ /dev/null @@ -1,268 +0,0 @@ -/* color */ - -.color-orange { - color: #F7861C; -} - -.color-forest { - color: #0A5448; -} - -/* lib */ - -.full-width { - width: 100%; -} - -.full-height { - height: 100%; -} - -.flex-column { - display: flex; - flex-direction: column; -} - -.space-between { - justify-content: space-between; -} - -.space-around { - justify-content: space-around; -} - -.flex-column-bottom { - display: flex; - flex-direction: column-reverse; -} - -.flex-row { - display: flex; - flex-direction: row; -} - -.flex-space-between { - justify-content: space-between; -} - -.flex-space-around { - justify-content: space-around; -} - -.flex-right { - display: flex; - flex-direction: row; - justify-content: flex-end; -} - -.flex-left { - display: flex; - flex-direction: row; - justify-content: flex-start; -} - -.flex-fixed { - flex: none; -} - -.flex-basis-auto { - flex-basis: auto; -} - -.flex-grow { - flex: 1 1 auto; -} - -.flex-wrap { - flex-wrap: wrap; -} - -.flex-center { - display: flex; - justify-content: center; - align-items: center; -} - -.flex-justify-center { - justify-content: center; -} - -.flex-align-center { - align-items: center; -} - -.flex-self-end { - align-self: flex-end; -} - -.flex-self-stretch { - align-self: stretch; -} - -.flex-vertical { - flex-direction: column; -} - -.z-bump { - z-index: 1; -} - -.select-none { - cursor: inherit; - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; -} - -.pointer { - cursor: pointer; -} -.cursor-pointer { - cursor: pointer; - transform-origin: center center; - transition: transform 50ms ease-in-out; -} -.cursor-pointer:hover { - transform: scale(1.1); -} -.cursor-pointer:active { - transform: scale(0.95); -} - -.cursor-disabled { - cursor: not-allowed; -} - -.margin-bottom-sml { - margin-bottom: 20px; -} - -.margin-bottom-med { - margin-bottom: 40px; -} - -.margin-right-left { - margin: 0 20px; -} - -.bold { - font-weight: bold; -} - -.text-transform-uppercase { - text-transform: uppercase; -} - -.font-small { - font-size: 12px; -} - -.font-medium { - font-size: 1.2em; -} - -hr.horizontal-line { - display: block; - height: 1px; - border: 0; - border-top: 1px solid #ccc; - margin: 1em 0; - padding: 0; -} - -.hover-white:hover { - background: white; -} - -.red-dot { - background: #E91550; - color: white; - border-radius: 10px; -} - -.diamond { - transform: rotate(45deg); - background: #038789; -} - -.hollow-diamond { - transform: rotate(45deg); - border: 3px solid #690496; -} - -.golden-square { - background: #EBB33F; -} - -.pending-dot { - background: red; - left: 14px; - top: 14px; - color: white; - border-radius: 10px; - height: 20px; - min-width: 20px; - position: relative; - display: flex; - align-items: center; - justify-content: center; - padding: 4px; - z-index: 1; -} - -.keyring-label { - z-index: 1; - font-size: 11px; - background: rgba(255,0,0,0.8); - bottom: -47px; - color: white; - border-radius: 10px; - height: 20px; - min-width: 20px; - position: relative; - display: flex; - align-items: center; - justify-content: center; - padding: 4px; -} - -.ether-balance { - display: flex; - align-items: center; -} - -.menu-icon { - display: inline-block; - height: 9px; - min-width: 9px; - margin: 13px; -} -.ether-icon { - background: rgb(0, 163, 68); - border-radius: 20px; -} -.testnet-icon { - background: #2465E1; -} - -.drop-menu-item { - display: flex; - align-items: center; -} - -.invisible { - visibility: hidden; -} - -.one-line-concat { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.critical-error { - text-align: center; - margin-top: 20px; - color: red; -} diff --git a/ui/app/css/reset.css b/ui/app/css/reset.css deleted file mode 100644 index 9ce89e8bc..000000000 --- a/ui/app/css/reset.css +++ /dev/null @@ -1,48 +0,0 @@ -/* http://meyerweb.com/eric/tools/css/reset/ - v2.0 | 20110126 - License: none (public domain) -*/ - -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; -} \ No newline at end of file diff --git a/ui/app/css/transitions.css b/ui/app/css/transitions.css deleted file mode 100644 index 393a944f9..000000000 --- a/ui/app/css/transitions.css +++ /dev/null @@ -1,42 +0,0 @@ -/* universal */ -.app-primary .main-enter { - position: absolute; - width: 100%; -} - -/* center position */ -.app-primary.from-right .main-enter-active, -.app-primary.from-left .main-enter-active { - overflow-x: hidden; - transform: translateX(0px); - transition: transform 300ms ease-in; -} - -/* exited positions */ -.app-primary.from-left .main-leave-active { - transform: translateX(360px); - transition: transform 300ms ease-in; -} -.app-primary.from-right .main-leave-active { - transform: translateX(-360px); - transition: transform 300ms ease-in; -} - -/* loader transitions */ -.loader-enter, .loader-leave-active { - opacity: 0.0; - transition: opacity 150 ease-in; -} -.loader-enter-active, .loader-leave { - opacity: 1.0; - transition: opacity 150 ease-in; -} - -/* entering positions */ -.app-primary.from-right .main-enter:not(.main-enter-active) { - transform: translateX(360px); -} -.app-primary.from-left .main-enter:not(.main-enter-active) { - transform: translateX(-360px); -} - diff --git a/ui/app/first-time/init-menu.js b/ui/app/first-time/init-menu.js deleted file mode 100644 index cc7c51bd3..000000000 --- a/ui/app/first-time/init-menu.js +++ /dev/null @@ -1,179 +0,0 @@ -const inherits = require('util').inherits -const EventEmitter = require('events').EventEmitter -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const Mascot = require('../components/mascot') -const actions = require('../actions') -const Tooltip = require('../components/tooltip') -const getCaretCoordinates = require('textarea-caret') - -module.exports = connect(mapStateToProps)(InitializeMenuScreen) - -inherits(InitializeMenuScreen, Component) -function InitializeMenuScreen () { - Component.call(this) - this.animationEventEmitter = new EventEmitter() -} - -function mapStateToProps (state) { - return { - // state from plugin - currentView: state.appState.currentView, - warning: state.appState.warning, - } -} - -InitializeMenuScreen.prototype.render = function () { - var state = this.props - - switch (state.currentView.name) { - - default: - return this.renderMenu(state) - - } -} - -// InitializeMenuScreen.prototype.componentDidMount = function(){ -// document.getElementById('password-box').focus() -// } - -InitializeMenuScreen.prototype.renderMenu = function (state) { - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - h(Mascot, { - animationEventEmitter: this.animationEventEmitter, - }), - - h('h1', { - style: { - fontSize: '1.3em', - textTransform: 'uppercase', - color: '#7F8082', - marginBottom: 10, - }, - }, 'MetaMask'), - - - h('div', [ - h('h3', { - style: { - fontSize: '0.8em', - color: '#7F8082', - display: 'inline', - }, - }, 'Encrypt your new DEN'), - - h(Tooltip, { - title: 'Your DEN is your password-encrypted storage within MetaMask.', - }, [ - h('i.fa.fa-question-circle.pointer', { - style: { - fontSize: '18px', - position: 'relative', - color: 'rgb(247, 134, 28)', - top: '2px', - marginLeft: '4px', - }, - }), - ]), - ]), - - h('span.in-progress-notification', state.warning), - - // password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box', - placeholder: 'New Password (min 8 chars)', - onInput: this.inputChanged.bind(this), - style: { - width: 260, - marginTop: 12, - }, - }), - - // confirm password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box-confirm', - placeholder: 'Confirm Password', - onKeyPress: this.createVaultOnEnter.bind(this), - onInput: this.inputChanged.bind(this), - style: { - width: 260, - marginTop: 16, - }, - }), - - - h('button.primary', { - onClick: this.createNewVaultAndKeychain.bind(this), - style: { - margin: 12, - }, - }, 'Create'), - - h('.flex-row.flex-center.flex-grow', [ - h('p.pointer', { - onClick: this.showRestoreVault.bind(this), - style: { - fontSize: '0.8em', - color: 'rgb(247, 134, 28)', - textDecoration: 'underline', - }, - }, 'Import Existing DEN'), - ]), - - ]) - ) -} - -InitializeMenuScreen.prototype.createVaultOnEnter = function (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.createNewVaultAndKeychain() - } -} - -InitializeMenuScreen.prototype.componentDidMount = function () { - document.getElementById('password-box').focus() -} - -InitializeMenuScreen.prototype.showRestoreVault = function () { - this.props.dispatch(actions.showRestoreVault()) -} - -InitializeMenuScreen.prototype.createNewVaultAndKeychain = function () { - var passwordBox = document.getElementById('password-box') - var password = passwordBox.value - var passwordConfirmBox = document.getElementById('password-box-confirm') - var passwordConfirm = passwordConfirmBox.value - - if (password.length < 8) { - this.warning = 'password not long enough' - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - if (password !== passwordConfirm) { - this.warning = 'passwords don\'t match' - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - - this.props.dispatch(actions.createNewVaultAndKeychain(password)) -} - -InitializeMenuScreen.prototype.inputChanged = function (event) { - // tell mascot to look at page action - var element = event.target - var boundingRect = element.getBoundingClientRect() - var coordinates = getCaretCoordinates(element, element.selectionEnd) - this.animationEventEmitter.emit('point', { - x: boundingRect.left + coordinates.left - element.scrollLeft, - y: boundingRect.top + coordinates.top - element.scrollTop, - }) -} diff --git a/ui/app/img/identicon-tardigrade.png b/ui/app/img/identicon-tardigrade.png deleted file mode 100644 index 1742a32b8..000000000 Binary files a/ui/app/img/identicon-tardigrade.png and /dev/null differ diff --git a/ui/app/img/identicon-walrus.png b/ui/app/img/identicon-walrus.png deleted file mode 100644 index d58fae912..000000000 Binary files a/ui/app/img/identicon-walrus.png and /dev/null differ diff --git a/ui/app/info.js b/ui/app/info.js deleted file mode 100644 index e8470de97..000000000 --- a/ui/app/info.js +++ /dev/null @@ -1,154 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') - -module.exports = connect(mapStateToProps)(InfoScreen) - -function mapStateToProps (state) { - return {} -} - -inherits(InfoScreen, Component) -function InfoScreen () { - Component.call(this) -} - -InfoScreen.prototype.render = function () { - const state = this.props - const version = global.platform.getVersion() - - return ( - h('.flex-column.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: (event) => { - state.dispatch(actions.goHome()) - }, - }), - h('h2.page-subtitle', 'Info'), - ]), - - // main view - h('.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-space-around', { - style: { - padding: '20px', - }, - }, [ - // current version number - - h('.info.info-gray', [ - h('div', 'Metamask'), - h('div', { - style: { - marginBottom: '10px', - }, - }, `Version: ${version}`), - ]), - - h('div', { - style: { - marginBottom: '5px', - }}, - [ - h('div', [ - h('a', { - href: 'https://metamask.io/privacy.html', - target: '_blank', - onClick (event) { this.navigateTo(event.target.href) }, - }, [ - h('div.info', 'Privacy Policy'), - ]), - ]), - h('div', [ - h('a', { - href: 'https://metamask.io/terms.html', - target: '_blank', - onClick (event) { this.navigateTo(event.target.href) }, - }, [ - h('div.info', 'Terms of Use'), - ]), - ]), - h('div', [ - h('a', { - href: 'https://metamask.io/attributions.html', - target: '_blank', - onClick (event) { this.navigateTo(event.target.href) }, - }, [ - h('div.info', 'Attributions'), - ]), - ]), - ] - ), - - h('hr', { - style: { - margin: '10px 0 ', - width: '7em', - }, - }), - - h('div', { - style: { - paddingLeft: '30px', - }}, - [ - h('div.fa.fa-github', [ - h('a.info', { - href: 'https://github.com/MetaMask/faq', - target: '_blank', - }, 'Need Help? Read our FAQ!'), - ]), - h('div', [ - h('a', { - href: 'https://metamask.io/', - target: '_blank', - }, [ - h('img.icon-size', { - src: 'images/icon-128.png', - style: { - // IE6-9 - filter: 'grayscale(100%)', - // Microsoft Edge and Firefox 35+ - WebkitFilter: 'grayscale(100%)', - }, - }), - h('div.info', 'Visit our web site'), - ]), - ]), - h('div.fa.fa-slack', [ - h('a.info', { - href: 'http://slack.metamask.io', - target: '_blank', - }, 'Join the conversation on Slack'), - ]), - - h('div.fa.fa-twitter', [ - h('a.info', { - href: 'https://twitter.com/metamask_io', - target: '_blank', - }, 'Follow us on Twitter'), - ]), - - h('div.fa.fa-envelope', [ - h('a.info', { - target: '_blank', - style: { width: '85vw' }, - href: 'mailto:help@metamask.io?subject=Feedback', - }, 'Email us!'), - ]), - ]), - ]), - ]), - ]) - ) -} - -InfoScreen.prototype.navigateTo = function (url) { - global.platform.openWindow({ url }) -} - diff --git a/ui/app/keychains/hd/create-vault-complete.js b/ui/app/keychains/hd/create-vault-complete.js deleted file mode 100644 index a318a9b50..000000000 --- a/ui/app/keychains/hd/create-vault-complete.js +++ /dev/null @@ -1,78 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('../../actions') - -module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen) - -inherits(CreateVaultCompleteScreen, Component) -function CreateVaultCompleteScreen () { - Component.call(this) -} - -function mapStateToProps (state) { - return { - seed: state.appState.currentView.seedWords, - cachedSeed: state.metamask.seedWords, - } -} - -CreateVaultCompleteScreen.prototype.render = function () { - var state = this.props - var seed = state.seed || state.cachedSeed || '' - - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - // // subtitle and nav - // h('.section-title.flex-row.flex-center', [ - // h('h2.page-subtitle', 'Vault Created'), - // ]), - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginTop: 36, - marginBottom: 8, - width: '100%', - fontSize: '20px', - padding: 6, - }, - }, [ - 'Vault Created', - ]), - - h('div', { - style: { - width: '360px', - height: '78px', - fontSize: '1em', - marginTop: '10px', - textAlign: 'center', - }, - }, [ - h('span.error', 'These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret.'), - ]), - - h('textarea.twelve-word-phrase', { - readOnly: true, - value: seed, - }), - - h('button.primary', { - onClick: () => this.confirmSeedWords(), - style: { - margin: '24px', - fontSize: '0.9em', - }, - }, 'I\'ve copied it somewhere safe'), - ]) - ) -} - -CreateVaultCompleteScreen.prototype.confirmSeedWords = function () { - this.props.dispatch(actions.confirmSeedWords()) -} diff --git a/ui/app/keychains/hd/recover-seed/confirmation.js b/ui/app/keychains/hd/recover-seed/confirmation.js deleted file mode 100644 index 4ccbec9fc..000000000 --- a/ui/app/keychains/hd/recover-seed/confirmation.js +++ /dev/null @@ -1,118 +0,0 @@ -const inherits = require('util').inherits - -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('../../../actions') - -module.exports = connect(mapStateToProps)(RevealSeedConfirmation) - -inherits(RevealSeedConfirmation, Component) -function RevealSeedConfirmation () { - Component.call(this) -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -RevealSeedConfirmation.prototype.render = function () { - const props = this.props - - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginBottom: 24, - width: '100%', - fontSize: '20px', - padding: 6, - }, - }, [ - 'Reveal Seed Words', - ]), - - h('.div', { - style: { - display: 'flex', - flexDirection: 'column', - padding: '20px', - justifyContent: 'center', - }, - }, [ - - h('h4', 'Do not recover your seed words in a public place! These words can be used to steal all your accounts.'), - - // confirmation - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box', - placeholder: 'Enter your password to confirm', - onKeyPress: this.checkConfirmation.bind(this), - style: { - width: 260, - marginTop: '12px', - }, - }), - - h('.flex-row.flex-space-between', { - style: { - marginTop: 30, - width: '50%', - }, - }, [ - // cancel - h('button.primary', { - onClick: this.goHome.bind(this), - }, 'CANCEL'), - - // submit - h('button.primary', { - onClick: this.revealSeedWords.bind(this), - }, 'OK'), - - ]), - - (props.warning) && ( - h('span.error', { - style: { - margin: '20px', - }, - }, props.warning.split('-')) - ), - - props.inProgress && ( - h('span.in-progress-notification', 'Generating Seed...') - ), - ]), - ]) - ) -} - -RevealSeedConfirmation.prototype.componentDidMount = function () { - document.getElementById('password-box').focus() -} - -RevealSeedConfirmation.prototype.goHome = function () { - this.props.dispatch(actions.showConfigPage(false)) -} - -// create vault - -RevealSeedConfirmation.prototype.checkConfirmation = function (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.revealSeedWords() - } -} - -RevealSeedConfirmation.prototype.revealSeedWords = function () { - var password = document.getElementById('password-box').value - this.props.dispatch(actions.requestRevealSeed(password)) -} diff --git a/ui/app/keychains/hd/restore-vault.js b/ui/app/keychains/hd/restore-vault.js deleted file mode 100644 index 06e51d9b3..000000000 --- a/ui/app/keychains/hd/restore-vault.js +++ /dev/null @@ -1,152 +0,0 @@ -const inherits = require('util').inherits -const PersistentForm = require('../../../lib/persistent-form') -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('../../actions') - -module.exports = connect(mapStateToProps)(RestoreVaultScreen) - -inherits(RestoreVaultScreen, PersistentForm) -function RestoreVaultScreen () { - PersistentForm.call(this) -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - forgottenPassword: state.appState.forgottenPassword, - } -} - -RestoreVaultScreen.prototype.render = function () { - var state = this.props - this.persistentFormParentId = 'restore-vault-form' - - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginBottom: 24, - width: '100%', - fontSize: '20px', - padding: 6, - }, - }, [ - 'Restore Vault', - ]), - - // wallet seed entry - h('h3', 'Wallet Seed'), - h('textarea.twelve-word-phrase.letter-spacey', { - dataset: { - persistentFormId: 'wallet-seed', - }, - placeholder: 'Enter your secret twelve word phrase here to restore your vault.', - }), - - // password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box', - placeholder: 'New Password (min 8 chars)', - dataset: { - persistentFormId: 'password', - }, - style: { - width: 260, - marginTop: 12, - }, - }), - - // confirm password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box-confirm', - placeholder: 'Confirm Password', - onKeyPress: this.createOnEnter.bind(this), - dataset: { - persistentFormId: 'password-confirmation', - }, - style: { - width: 260, - marginTop: 16, - }, - }), - - (state.warning) && ( - h('span.error.in-progress-notification', state.warning) - ), - - // submit - - h('.flex-row.flex-space-between', { - style: { - marginTop: 30, - width: '50%', - }, - }, [ - - // cancel - h('button.primary', { - onClick: this.showInitializeMenu.bind(this), - }, 'CANCEL'), - - // submit - h('button.primary', { - onClick: this.createNewVaultAndRestore.bind(this), - }, 'OK'), - - ]), - ]) - - ) -} - -RestoreVaultScreen.prototype.showInitializeMenu = function () { - if (this.props.forgottenPassword) { - this.props.dispatch(actions.backToUnlockView()) - } else { - this.props.dispatch(actions.showInitializeMenu()) - } -} - -RestoreVaultScreen.prototype.createOnEnter = function (event) { - if (event.key === 'Enter') { - this.createNewVaultAndRestore() - } -} - -RestoreVaultScreen.prototype.createNewVaultAndRestore = function () { - // check password - var passwordBox = document.getElementById('password-box') - var password = passwordBox.value - var passwordConfirmBox = document.getElementById('password-box-confirm') - var passwordConfirm = passwordConfirmBox.value - if (password.length < 8) { - this.warning = 'Password not long enough' - - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - if (password !== passwordConfirm) { - this.warning = 'Passwords don\'t match' - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - // check seed - var seedBox = document.querySelector('textarea.twelve-word-phrase') - var seed = seedBox.value.trim() - if (seed.split(' ').length !== 12) { - this.warning = 'seed phrases are 12 words long' - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - // submit - this.warning = null - this.props.dispatch(actions.displayWarning(this.warning)) - this.props.dispatch(actions.createNewVaultAndRestore(password, seed)) -} diff --git a/ui/app/new-keychain.js b/ui/app/new-keychain.js deleted file mode 100644 index cc9633166..000000000 --- a/ui/app/new-keychain.js +++ /dev/null @@ -1,29 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect - -module.exports = connect(mapStateToProps)(NewKeychain) - -function mapStateToProps (state) { - return {} -} - -inherits(NewKeychain, Component) -function NewKeychain () { - Component.call(this) -} - -NewKeychain.prototype.render = function () { - // const props = this.props - - return ( - h('div', { - style: { - background: 'blue', - }, - }, [ - h('h1', `Here's a list!!!!`), - ]) - ) -} diff --git a/ui/app/reducers.js b/ui/app/reducers.js deleted file mode 100644 index 11efca529..000000000 --- a/ui/app/reducers.js +++ /dev/null @@ -1,52 +0,0 @@ -const extend = require('xtend') - -// -// Sub-Reducers take in the complete state and return their sub-state -// -const reduceIdentities = require('./reducers/identities') -const reduceMetamask = require('./reducers/metamask') -const reduceApp = require('./reducers/app') - -window.METAMASK_CACHED_LOG_STATE = null - -module.exports = rootReducer - -function rootReducer (state, action) { - // clone - state = extend(state) - - if (action.type === 'GLOBAL_FORCE_UPDATE') { - return action.value - } - - // - // Identities - // - - state.identities = reduceIdentities(state, action) - - // - // MetaMask - // - - state.metamask = reduceMetamask(state, action) - - // - // AppState - // - - state.appState = reduceApp(state, action) - - window.METAMASK_CACHED_LOG_STATE = state - return state -} - -window.logState = function () { - var stateString = JSON.stringify(window.METAMASK_CACHED_LOG_STATE, removeSeedWords, 2) - console.log(stateString) - return stateString -} - -function removeSeedWords (key, value) { - return key === 'seedWords' ? undefined : value -} diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js deleted file mode 100644 index 2fcc9bfe0..000000000 --- a/ui/app/reducers/app.js +++ /dev/null @@ -1,585 +0,0 @@ -const extend = require('xtend') -const actions = require('../actions') -const txHelper = require('../../lib/tx-helper') - -module.exports = reduceApp - - -function reduceApp (state, action) { - log.debug('App Reducer got ' + action.type) - // clone and defaults - const selectedAddress = state.metamask.selectedAddress - const hasUnconfActions = checkUnconfActions(state) - let name = 'accounts' - if (selectedAddress) { - name = 'accountDetail' - } - if (hasUnconfActions) { - log.debug('pending txs detected, defaulting to conf-tx view.') - name = 'confTx' - } - - var defaultView = { - name, - detailView: null, - context: selectedAddress, - } - - // confirm seed words - var seedWords = state.metamask.seedWords - var seedConfView = { - name: 'createVaultComplete', - seedWords, - } - - // default state - var appState = extend({ - shouldClose: false, - menuOpen: false, - currentView: seedWords ? seedConfView : defaultView, - accountDetail: { - subview: 'transactions', - }, - transForward: true, // Used to render transition direction - isLoading: false, // Used to display loading indicator - warning: null, // Used to display error text - }, state.appState) - - switch (action.type) { - - // transition methods - - case actions.TRANSITION_FORWARD: - return extend(appState, { - transForward: true, - }) - - case actions.TRANSITION_BACKWARD: - return extend(appState, { - transForward: false, - }) - - // intialize - - case actions.SHOW_CREATE_VAULT: - return extend(appState, { - currentView: { - name: 'createVault', - }, - transForward: true, - warning: null, - }) - - case actions.SHOW_RESTORE_VAULT: - return extend(appState, { - currentView: { - name: 'restoreVault', - }, - transForward: true, - forgottenPassword: true, - }) - - case actions.FORGOT_PASSWORD: - return extend(appState, { - currentView: { - name: 'restoreVault', - }, - transForward: false, - forgottenPassword: true, - }) - - case actions.SHOW_INIT_MENU: - return extend(appState, { - currentView: defaultView, - transForward: false, - }) - - case actions.SHOW_CONFIG_PAGE: - return extend(appState, { - currentView: { - name: 'config', - context: appState.currentView.context, - }, - transForward: action.value, - }) - - case actions.SHOW_ADD_TOKEN_PAGE: - return extend(appState, { - currentView: { - name: 'add-token', - context: appState.currentView.context, - }, - transForward: action.value, - }) - - case actions.SHOW_IMPORT_PAGE: - - return extend(appState, { - currentView: { - name: 'import-menu', - }, - transForward: true, - }) - - case actions.SHOW_INFO_PAGE: - return extend(appState, { - currentView: { - name: 'info', - context: appState.currentView.context, - }, - transForward: true, - }) - - case actions.CREATE_NEW_VAULT_IN_PROGRESS: - return extend(appState, { - currentView: { - name: 'createVault', - inProgress: true, - }, - transForward: true, - isLoading: true, - }) - - case actions.SHOW_NEW_VAULT_SEED: - return extend(appState, { - currentView: { - name: 'createVaultComplete', - seedWords: action.value, - }, - transForward: true, - isLoading: false, - }) - - case actions.NEW_ACCOUNT_SCREEN: - return extend(appState, { - currentView: { - name: 'new-account', - context: appState.currentView.context, - }, - transForward: true, - }) - - case actions.SHOW_SEND_PAGE: - return extend(appState, { - currentView: { - name: 'sendTransaction', - context: appState.currentView.context, - }, - transForward: true, - warning: null, - }) - - case actions.SHOW_NEW_KEYCHAIN: - return extend(appState, { - currentView: { - name: 'newKeychain', - context: appState.currentView.context, - }, - transForward: true, - }) - - // unlock - - case actions.UNLOCK_METAMASK: - return extend(appState, { - forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null, - detailView: {}, - transForward: true, - isLoading: false, - warning: null, - }) - - case actions.LOCK_METAMASK: - return extend(appState, { - currentView: defaultView, - transForward: false, - warning: null, - }) - - case actions.BACK_TO_INIT_MENU: - return extend(appState, { - warning: null, - transForward: false, - forgottenPassword: true, - currentView: { - name: 'InitMenu', - }, - }) - - case actions.BACK_TO_UNLOCK_VIEW: - return extend(appState, { - warning: null, - transForward: true, - forgottenPassword: false, - currentView: { - name: 'UnlockScreen', - }, - }) - // reveal seed words - - case actions.REVEAL_SEED_CONFIRMATION: - return extend(appState, { - currentView: { - name: 'reveal-seed-conf', - }, - transForward: true, - warning: null, - }) - - // accounts - - case actions.SET_SELECTED_ACCOUNT: - return extend(appState, { - activeAddress: action.value, - }) - - case actions.GO_HOME: - return extend(appState, { - currentView: extend(appState.currentView, { - name: 'accountDetail', - }), - accountDetail: { - subview: 'transactions', - accountExport: 'none', - privateKey: '', - }, - transForward: false, - warning: null, - }) - - case actions.SHOW_ACCOUNT_DETAIL: - return extend(appState, { - forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null, - currentView: { - name: 'accountDetail', - context: action.value, - }, - accountDetail: { - subview: 'transactions', - accountExport: 'none', - privateKey: '', - }, - transForward: false, - }) - - case actions.BACK_TO_ACCOUNT_DETAIL: - return extend(appState, { - currentView: { - name: 'accountDetail', - context: action.value, - }, - accountDetail: { - subview: 'transactions', - accountExport: 'none', - privateKey: '', - }, - transForward: false, - }) - - case actions.SHOW_ACCOUNTS_PAGE: - return extend(appState, { - currentView: { - name: seedWords ? 'createVaultComplete' : 'accounts', - seedWords, - }, - transForward: true, - isLoading: false, - warning: null, - scrollToBottom: false, - forgottenPassword: false, - }) - - case actions.SHOW_NOTICE: - return extend(appState, { - transForward: true, - isLoading: false, - }) - - case actions.REVEAL_ACCOUNT: - return extend(appState, { - scrollToBottom: true, - }) - - case actions.SHOW_CONF_TX_PAGE: - return extend(appState, { - currentView: { - name: 'confTx', - context: 0, - }, - transForward: action.transForward, - warning: null, - isLoading: false, - }) - - case actions.SHOW_CONF_MSG_PAGE: - return extend(appState, { - currentView: { - name: hasUnconfActions ? 'confTx' : 'account-detail', - context: 0, - }, - transForward: true, - warning: null, - isLoading: false, - }) - - case actions.COMPLETED_TX: - log.debug('reducing COMPLETED_TX for tx ' + action.value) - const otherUnconfActions = getUnconfActionList(state) - .filter(tx => tx.id !== action.value) - const hasOtherUnconfActions = otherUnconfActions.length > 0 - - if (hasOtherUnconfActions) { - log.debug('reducer detected txs - rendering confTx view') - return extend(appState, { - transForward: false, - currentView: { - name: 'confTx', - context: 0, - }, - warning: null, - }) - } else { - log.debug('attempting to close popup') - return extend(appState, { - // indicate notification should close - shouldClose: true, - transForward: false, - warning: null, - currentView: { - name: 'accountDetail', - context: state.metamask.selectedAddress, - }, - accountDetail: { - subview: 'transactions', - }, - }) - } - - case actions.NEXT_TX: - return extend(appState, { - transForward: true, - currentView: { - name: 'confTx', - context: ++appState.currentView.context, - warning: null, - }, - }) - - case actions.VIEW_PENDING_TX: - const context = indexForPending(state, action.value) - return extend(appState, { - transForward: true, - currentView: { - name: 'confTx', - context, - warning: null, - }, - }) - - case actions.PREVIOUS_TX: - return extend(appState, { - transForward: false, - currentView: { - name: 'confTx', - context: --appState.currentView.context, - warning: null, - }, - }) - - case actions.TRANSACTION_ERROR: - return extend(appState, { - currentView: { - name: 'confTx', - errorMessage: 'There was a problem submitting this transaction.', - }, - }) - - case actions.UNLOCK_FAILED: - return extend(appState, { - warning: action.value || 'Incorrect password. Try again.', - }) - - case actions.SHOW_LOADING: - return extend(appState, { - isLoading: true, - loadingMessage: action.value, - }) - - case actions.HIDE_LOADING: - return extend(appState, { - isLoading: false, - }) - - case actions.SHOW_SUB_LOADING_INDICATION: - return extend(appState, { - isSubLoading: true, - }) - - case actions.HIDE_SUB_LOADING_INDICATION: - return extend(appState, { - isSubLoading: false, - }) - case actions.CLEAR_SEED_WORD_CACHE: - return extend(appState, { - transForward: true, - currentView: {}, - isLoading: false, - accountDetail: { - subview: 'transactions', - accountExport: 'none', - privateKey: '', - }, - }) - - case actions.DISPLAY_WARNING: - return extend(appState, { - warning: action.value, - isLoading: false, - }) - - case actions.HIDE_WARNING: - return extend(appState, { - warning: undefined, - }) - - case actions.REQUEST_ACCOUNT_EXPORT: - return extend(appState, { - transForward: true, - currentView: { - name: 'accountDetail', - context: appState.currentView.context, - }, - accountDetail: { - subview: 'export', - accountExport: 'requested', - }, - }) - - case actions.EXPORT_ACCOUNT: - return extend(appState, { - accountDetail: { - subview: 'export', - accountExport: 'completed', - }, - }) - - case actions.SHOW_PRIVATE_KEY: - return extend(appState, { - accountDetail: { - subview: 'export', - accountExport: 'completed', - privateKey: action.value, - }, - }) - - case actions.BUY_ETH_VIEW: - return extend(appState, { - transForward: true, - currentView: { - name: 'buyEth', - context: appState.currentView.name, - }, - identity: state.metamask.identities[action.value], - buyView: { - subview: 'Coinbase', - amount: '15.00', - buyAddress: action.value, - formView: { - coinbase: true, - shapeshift: false, - }, - }, - }) - - case actions.COINBASE_SUBVIEW: - return extend(appState, { - buyView: { - subview: 'Coinbase', - formView: { - coinbase: true, - shapeshift: false, - }, - buyAddress: appState.buyView.buyAddress, - amount: appState.buyView.amount, - }, - }) - - case actions.SHAPESHIFT_SUBVIEW: - return extend(appState, { - buyView: { - subview: 'ShapeShift', - formView: { - coinbase: false, - shapeshift: true, - marketinfo: action.value.marketinfo, - coinOptions: action.value.coinOptions, - }, - buyAddress: appState.buyView.buyAddress, - amount: appState.buyView.amount, - }, - }) - - case actions.PAIR_UPDATE: - return extend(appState, { - buyView: { - subview: 'ShapeShift', - formView: { - coinbase: false, - shapeshift: true, - marketinfo: action.value.marketinfo, - coinOptions: appState.buyView.formView.coinOptions, - }, - buyAddress: appState.buyView.buyAddress, - amount: appState.buyView.amount, - warning: null, - }, - }) - - case actions.SHOW_QR: - return extend(appState, { - qrRequested: true, - transForward: true, - - Qr: { - message: action.value.message, - data: action.value.data, - }, - }) - - case actions.SHOW_QR_VIEW: - return extend(appState, { - currentView: { - name: 'qr', - context: appState.currentView.context, - }, - transForward: true, - Qr: { - message: action.value.message, - data: action.value.data, - }, - }) - default: - return appState - } -} - -function checkUnconfActions (state) { - const unconfActionList = getUnconfActionList(state) - const hasUnconfActions = unconfActionList.length > 0 - return hasUnconfActions -} - -function getUnconfActionList (state) { - const { unapprovedTxs, unapprovedMsgs, - unapprovedPersonalMsgs, network } = state.metamask - - const unconfActionList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) - return unconfActionList -} - -function indexForPending (state, txId) { - const unconfTxList = getUnconfActionList(state) - const match = unconfTxList.find((tx) => tx.id === txId) - const index = unconfTxList.indexOf(match) - return index -} diff --git a/ui/app/reducers/identities.js b/ui/app/reducers/identities.js deleted file mode 100644 index 341a404e7..000000000 --- a/ui/app/reducers/identities.js +++ /dev/null @@ -1,15 +0,0 @@ -const extend = require('xtend') - -module.exports = reduceIdentities - -function reduceIdentities (state, action) { - // clone + defaults - var idState = extend({ - - }, state.identities) - - switch (action.type) { - default: - return idState - } -} diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js deleted file mode 100644 index e0c416c2d..000000000 --- a/ui/app/reducers/metamask.js +++ /dev/null @@ -1,137 +0,0 @@ -const extend = require('xtend') -const actions = require('../actions') - -module.exports = reduceMetamask - -function reduceMetamask (state, action) { - let newState - - // clone + defaults - var metamaskState = extend({ - isInitialized: false, - isUnlocked: false, - rpcTarget: 'https://rawtestrpc.metamask.io/', - identities: {}, - unapprovedTxs: {}, - noActiveNotices: true, - lastUnreadNotice: undefined, - frequentRpcList: [], - addressBook: [], - }, state.metamask) - - switch (action.type) { - - case actions.SHOW_ACCOUNTS_PAGE: - newState = extend(metamaskState) - delete newState.seedWords - return newState - - case actions.SHOW_NOTICE: - return extend(metamaskState, { - noActiveNotices: false, - lastUnreadNotice: action.value, - }) - - case actions.CLEAR_NOTICES: - return extend(metamaskState, { - noActiveNotices: true, - }) - - case actions.UPDATE_METAMASK_STATE: - return extend(metamaskState, action.value) - - case actions.UNLOCK_METAMASK: - return extend(metamaskState, { - isUnlocked: true, - isInitialized: true, - selectedAddress: action.value, - }) - - case actions.LOCK_METAMASK: - return extend(metamaskState, { - isUnlocked: false, - }) - - case actions.SET_RPC_LIST: - return extend(metamaskState, { - frequentRpcList: action.value, - }) - - case actions.SET_RPC_TARGET: - return extend(metamaskState, { - provider: { - type: 'rpc', - rpcTarget: action.value, - }, - }) - - case actions.SET_PROVIDER_TYPE: - return extend(metamaskState, { - provider: { - type: action.value, - }, - }) - - case actions.COMPLETED_TX: - var stringId = String(action.id) - newState = extend(metamaskState, { - unapprovedTxs: {}, - unapprovedMsgs: {}, - }) - for (const id in metamaskState.unapprovedTxs) { - if (id !== stringId) { - newState.unapprovedTxs[id] = metamaskState.unapprovedTxs[id] - } - } - for (const id in metamaskState.unapprovedMsgs) { - if (id !== stringId) { - newState.unapprovedMsgs[id] = metamaskState.unapprovedMsgs[id] - } - } - return newState - - case actions.SHOW_NEW_VAULT_SEED: - return extend(metamaskState, { - isUnlocked: true, - isInitialized: false, - seedWords: action.value, - }) - - case actions.CLEAR_SEED_WORD_CACHE: - newState = extend(metamaskState, { - isUnlocked: true, - isInitialized: true, - selectedAddress: action.value, - }) - delete newState.seedWords - return newState - - case actions.SHOW_ACCOUNT_DETAIL: - newState = extend(metamaskState, { - isUnlocked: true, - isInitialized: true, - selectedAddress: action.value, - }) - delete newState.seedWords - return newState - - case actions.SAVE_ACCOUNT_LABEL: - const account = action.value.account - const name = action.value.label - var id = {} - id[account] = extend(metamaskState.identities[account], { name }) - var identities = extend(metamaskState.identities, id) - return extend(metamaskState, { identities }) - - case actions.SET_CURRENT_FIAT: - return extend(metamaskState, { - currentCurrency: action.value.currentCurrency, - conversionRate: action.value.conversionRate, - conversionDate: action.value.conversionDate, - }) - - default: - return metamaskState - - } -} diff --git a/ui/app/root.js b/ui/app/root.js deleted file mode 100644 index 9e7314b20..000000000 --- a/ui/app/root.js +++ /dev/null @@ -1,22 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const Provider = require('react-redux').Provider -const h = require('react-hyperscript') -const App = require('./app') - -module.exports = Root - -inherits(Root, Component) -function Root () { Component.call(this) } - -Root.prototype.render = function () { - return ( - - h(Provider, { - store: this.props.store, - }, [ - h(App), - ]) - - ) -} diff --git a/ui/app/send.js b/ui/app/send.js deleted file mode 100644 index a21a219eb..000000000 --- a/ui/app/send.js +++ /dev/null @@ -1,288 +0,0 @@ -const inherits = require('util').inherits -const PersistentForm = require('../lib/persistent-form') -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const Identicon = require('./components/identicon') -const actions = require('./actions') -const util = require('./util') -const numericBalance = require('./util').numericBalance -const addressSummary = require('./util').addressSummary -const isHex = require('./util').isHex -const EthBalance = require('./components/eth-balance') -const EnsInput = require('./components/ens-input') -const ethUtil = require('ethereumjs-util') -module.exports = connect(mapStateToProps)(SendTransactionScreen) - -function mapStateToProps (state) { - var result = { - address: state.metamask.selectedAddress, - accounts: state.metamask.accounts, - identities: state.metamask.identities, - warning: state.appState.warning, - network: state.metamask.network, - addressBook: state.metamask.addressBook, - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - } - - result.error = result.warning && result.warning.split('.')[0] - - result.account = result.accounts[result.address] - result.identity = result.identities[result.address] - result.balance = result.account ? numericBalance(result.account.balance) : null - - return result -} - -inherits(SendTransactionScreen, PersistentForm) -function SendTransactionScreen () { - PersistentForm.call(this) -} - -SendTransactionScreen.prototype.render = function () { - this.persistentFormParentId = 'send-tx-form' - - const props = this.props - const { - address, - account, - identity, - network, - identities, - addressBook, - conversionRate, - currentCurrency, - } = props - - return ( - - h('.send-screen.flex-column.flex-grow', [ - - // - // Sender Profile - // - - h('.account-data-subsection.flex-row.flex-grow', { - style: { - margin: '0 20px', - }, - }, [ - - // header - identicon + nav - h('.flex-row.flex-space-between', { - style: { - marginTop: '15px', - }, - }, [ - // back button - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { - onClick: this.back.bind(this), - }), - - // large identicon - h('.identicon-wrapper.flex-column.flex-center.select-none', [ - h(Identicon, { - diameter: 62, - address: address, - }), - ]), - - // invisible place holder - h('i.fa.fa-users.fa-lg.invisible', { - style: { - marginTop: '28px', - }, - }), - - ]), - - // account label - - h('.flex-column', { - style: { - marginTop: '10px', - alignItems: 'flex-start', - }, - }, [ - h('h2.font-medium.color-forest.flex-center', { - style: { - paddingTop: '8px', - marginBottom: '8px', - }, - }, identity && identity.name), - - // address and getter actions - h('.flex-row.flex-center', { - style: { - marginBottom: '8px', - }, - }, [ - - h('div', { - style: { - lineHeight: '16px', - }, - }, addressSummary(address)), - - ]), - - // balance - h('.flex-row.flex-center', [ - - h(EthBalance, { - value: account && account.balance, - conversionRate, - currentCurrency, - }), - - ]), - ]), - ]), - - // - // Required Fields - // - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginTop: '15px', - marginBottom: '16px', - }, - }, [ - 'Send Transaction', - ]), - - // error message - props.error && h('span.error.flex-center', props.error), - - // 'to' field - h('section.flex-row.flex-center', [ - h(EnsInput, { - name: 'address', - placeholder: 'Recipient Address', - onChange: this.recipientDidChange.bind(this), - network, - identities, - addressBook, - }), - ]), - - // 'amount' and send button - h('section.flex-row.flex-center', [ - - h('input.large-input', { - name: 'amount', - placeholder: 'Amount', - type: 'number', - style: { - marginRight: '6px', - }, - dataset: { - persistentFormId: 'tx-amount', - }, - }), - - h('button.primary', { - onClick: this.onSubmit.bind(this), - style: { - textTransform: 'uppercase', - }, - }, 'Next'), - - ]), - - // - // Optional Fields - // - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginTop: '16px', - marginBottom: '16px', - }, - }, [ - 'Transaction Data (optional)', - ]), - - // 'data' field - h('section.flex-column.flex-center', [ - h('input.large-input', { - name: 'txData', - placeholder: '0x01234', - style: { - width: '100%', - resize: 'none', - }, - dataset: { - persistentFormId: 'tx-data', - }, - }), - ]), - ]) - ) -} - -SendTransactionScreen.prototype.navigateToAccounts = function (event) { - event.stopPropagation() - this.props.dispatch(actions.showAccountsPage()) -} - -SendTransactionScreen.prototype.back = function () { - var address = this.props.address - this.props.dispatch(actions.backToAccountDetail(address)) -} - -SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickname) { - this.setState({ - recipient: recipient, - nickname: nickname, - }) -} - -SendTransactionScreen.prototype.onSubmit = function () { - const state = this.state || {} - const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '') - const nickname = state.nickname || ' ' - const input = document.querySelector('input[name="amount"]').value - const value = util.normalizeEthStringToWei(input) - const txData = document.querySelector('input[name="txData"]').value - const balance = this.props.balance - let message - - if (value.gt(balance)) { - message = 'Insufficient funds.' - return this.props.dispatch(actions.displayWarning(message)) - } - - if (input < 0) { - message = 'Can not send negative amounts of ETH.' - return this.props.dispatch(actions.displayWarning(message)) - } - - if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) { - message = 'Recipient address is invalid.' - return this.props.dispatch(actions.displayWarning(message)) - } - - if (!isHex(ethUtil.stripHexPrefix(txData)) && txData) { - message = 'Transaction data must be hex string.' - return this.props.dispatch(actions.displayWarning(message)) - } - - this.props.dispatch(actions.hideWarning()) - - this.props.dispatch(actions.addToAddressBook(recipient, nickname)) - - var txParams = { - from: this.props.address, - value: '0x' + value.toString(16), - } - - if (recipient) txParams.to = ethUtil.addHexPrefix(recipient) - if (txData) txParams.data = txData - - this.props.dispatch(actions.signTx(txParams)) -} diff --git a/ui/app/settings.js b/ui/app/settings.js deleted file mode 100644 index 454cc95e0..000000000 --- a/ui/app/settings.js +++ /dev/null @@ -1,59 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') - -module.exports = connect(mapStateToProps)(AppSettingsPage) - -function mapStateToProps (state) { - return {} -} - -inherits(AppSettingsPage, Component) -function AppSettingsPage () { - Component.call(this) -} - -AppSettingsPage.prototype.render = function () { - return ( - - h('.account-detail-section.flex-column.flex-grow', [ - - // subtitle and nav - h('.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: this.navigateToAccounts.bind(this), - }), - h('h2.page-subtitle', 'Settings'), - ]), - - h('label', { - htmlFor: 'settings-rpc-endpoint', - }, 'RPC Endpoint:'), - h('input', { - type: 'url', - id: 'settings-rpc-endpoint', - onKeyPress: this.onKeyPress.bind(this), - }), - - ]) - - ) -} - -AppSettingsPage.prototype.componentDidMount = function () { - document.querySelector('input').focus() -} - -AppSettingsPage.prototype.onKeyPress = function (event) { - // get submit event - if (event.key === 'Enter') { - // this.submitPassword(event) - } -} - -AppSettingsPage.prototype.navigateToAccounts = function (event) { - event.stopPropagation() - this.props.dispatch(actions.showAccountsPage()) -} diff --git a/ui/app/store.js b/ui/app/store.js deleted file mode 100644 index ba9e58b49..000000000 --- a/ui/app/store.js +++ /dev/null @@ -1,21 +0,0 @@ -const createStore = require('redux').createStore -const applyMiddleware = require('redux').applyMiddleware -const thunkMiddleware = require('redux-thunk') -const rootReducer = require('./reducers') -const createLogger = require('redux-logger') - -global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' - -module.exports = configureStore - -const loggerMiddleware = createLogger({ - predicate: () => global.METAMASK_DEBUG, -}) - -const middlewares = [thunkMiddleware, loggerMiddleware] - -const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore) - -function configureStore (initialState) { - return createStoreWithMiddleware(rootReducer, initialState) -} diff --git a/ui/app/template.js b/ui/app/template.js deleted file mode 100644 index d15b30fd2..000000000 --- a/ui/app/template.js +++ /dev/null @@ -1,30 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect - -module.exports = connect(mapStateToProps)(COMPONENTNAME) - -function mapStateToProps (state) { - return {} -} - -inherits(COMPONENTNAME, Component) -function COMPONENTNAME () { - Component.call(this) -} - -COMPONENTNAME.prototype.render = function () { - const props = this.props - - return ( - h('div', { - style: { - background: 'blue', - }, - }, [ - `Hello, ${props.sender}`, - ]) - ) -} - diff --git a/ui/app/unlock.js b/ui/app/unlock.js deleted file mode 100644 index 1aee3c5d0..000000000 --- a/ui/app/unlock.js +++ /dev/null @@ -1,118 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') -const getCaretCoordinates = require('textarea-caret') -const EventEmitter = require('events').EventEmitter - -const Mascot = require('./components/mascot') - -module.exports = connect(mapStateToProps)(UnlockScreen) - -inherits(UnlockScreen, Component) -function UnlockScreen () { - Component.call(this) - this.animationEventEmitter = new EventEmitter() -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -UnlockScreen.prototype.render = function () { - const state = this.props - const warning = state.warning - return ( - h('.flex-column', [ - h('.unlock-screen.flex-column.flex-center.flex-grow', [ - - h(Mascot, { - animationEventEmitter: this.animationEventEmitter, - }), - - h('h1', { - style: { - fontSize: '1.4em', - textTransform: 'uppercase', - color: '#7F8082', - }, - }, 'MetaMask'), - - h('input.large-input', { - type: 'password', - id: 'password-box', - placeholder: 'enter password', - style: { - - }, - onKeyPress: this.onKeyPress.bind(this), - onInput: this.inputChanged.bind(this), - }), - - h('.error', { - style: { - display: warning ? 'block' : 'none', - padding: '0 20px', - textAlign: 'center', - }, - }, warning), - - h('button.primary.cursor-pointer', { - onClick: this.onSubmit.bind(this), - style: { - margin: 10, - }, - }, 'Unlock'), - ]), - - h('.flex-row.flex-center.flex-grow', [ - h('p.pointer', { - onClick: () => this.props.dispatch(actions.forgotPassword()), - style: { - fontSize: '0.8em', - color: 'rgb(247, 134, 28)', - textDecoration: 'underline', - }, - }, 'I forgot my password.'), - ]), - ]) - ) -} - -UnlockScreen.prototype.componentDidMount = function () { - document.getElementById('password-box').focus() -} - -UnlockScreen.prototype.onSubmit = function (event) { - const input = document.getElementById('password-box') - const password = input.value - this.props.dispatch(actions.tryUnlockMetamask(password)) -} - -UnlockScreen.prototype.onKeyPress = function (event) { - if (event.key === 'Enter') { - this.submitPassword(event) - } -} - -UnlockScreen.prototype.submitPassword = function (event) { - var element = event.target - var password = element.value - // reset input - element.value = '' - this.props.dispatch(actions.tryUnlockMetamask(password)) -} - -UnlockScreen.prototype.inputChanged = function (event) { - // tell mascot to look at page action - var element = event.target - var boundingRect = element.getBoundingClientRect() - var coordinates = getCaretCoordinates(element, element.selectionEnd) - this.animationEventEmitter.emit('point', { - x: boundingRect.left + coordinates.left - element.scrollLeft, - y: boundingRect.top + coordinates.top - element.scrollTop, - }) -} diff --git a/ui/app/util.js b/ui/app/util.js deleted file mode 100644 index ac3f42c6b..000000000 --- a/ui/app/util.js +++ /dev/null @@ -1,217 +0,0 @@ -const ethUtil = require('ethereumjs-util') - -var valueTable = { - wei: '1000000000000000000', - kwei: '1000000000000000', - mwei: '1000000000000', - gwei: '1000000000', - szabo: '1000000', - finney: '1000', - ether: '1', - kether: '0.001', - mether: '0.000001', - gether: '0.000000001', - tether: '0.000000000001', -} -var bnTable = {} -for (var currency in valueTable) { - bnTable[currency] = new ethUtil.BN(valueTable[currency], 10) -} - -module.exports = { - valuesFor: valuesFor, - addressSummary: addressSummary, - miniAddressSummary: miniAddressSummary, - isAllOneCase: isAllOneCase, - isValidAddress: isValidAddress, - numericBalance: numericBalance, - parseBalance: parseBalance, - formatBalance: formatBalance, - generateBalanceObject: generateBalanceObject, - dataSize: dataSize, - readableDate: readableDate, - normalizeToWei: normalizeToWei, - normalizeEthStringToWei: normalizeEthStringToWei, - normalizeNumberToWei: normalizeNumberToWei, - valueTable: valueTable, - bnTable: bnTable, - isHex: isHex, -} - -function valuesFor (obj) { - if (!obj) return [] - return Object.keys(obj) - .map(function (key) { return obj[key] }) -} - -function addressSummary (address, firstSegLength = 10, lastSegLength = 4, includeHex = true) { - if (!address) return '' - let checked = ethUtil.toChecksumAddress(address) - if (!includeHex) { - checked = ethUtil.stripHexPrefix(checked) - } - return checked ? checked.slice(0, firstSegLength) + '...' + checked.slice(checked.length - lastSegLength) : '...' -} - -function miniAddressSummary (address) { - if (!address) return '' - var checked = ethUtil.toChecksumAddress(address) - return checked ? checked.slice(0, 4) + '...' + checked.slice(-4) : '...' -} - -function isValidAddress (address) { - var prefixed = ethUtil.addHexPrefix(address) - if (address === '0x0000000000000000000000000000000000000000') return false - return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed) -} - -function isAllOneCase (address) { - if (!address) return true - var lower = address.toLowerCase() - var upper = address.toUpperCase() - return address === lower || address === upper -} - -// Takes wei Hex, returns wei BN, even if input is null -function numericBalance (balance) { - if (!balance) return new ethUtil.BN(0, 16) - var stripped = ethUtil.stripHexPrefix(balance) - return new ethUtil.BN(stripped, 16) -} - -// Takes hex, returns [beforeDecimal, afterDecimal] -function parseBalance (balance) { - var beforeDecimal, afterDecimal - const wei = numericBalance(balance) - var weiString = wei.toString() - const trailingZeros = /0+$/ - - beforeDecimal = weiString.length > 18 ? weiString.slice(0, weiString.length - 18) : '0' - afterDecimal = ('000000000000000000' + wei).slice(-18).replace(trailingZeros, '') - if (afterDecimal === '') { afterDecimal = '0' } - return [beforeDecimal, afterDecimal] -} - -// Takes wei hex, returns an object with three properties. -// Its "formatted" property is what we generally use to render values. -function formatBalance (balance, decimalsToKeep, needsParse = true) { - var parsed = needsParse ? parseBalance(balance) : balance.split('.') - var beforeDecimal = parsed[0] - var afterDecimal = parsed[1] - var formatted = 'None' - if (decimalsToKeep === undefined) { - if (beforeDecimal === '0') { - if (afterDecimal !== '0') { - var sigFigs = afterDecimal.match(/^0*(.{2})/) // default: grabs 2 most significant digits - if (sigFigs) { afterDecimal = sigFigs[0] } - formatted = '0.' + afterDecimal + ' ETH' - } - } else { - formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ' ETH' - } - } else { - afterDecimal += Array(decimalsToKeep).join('0') - formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ' ETH' - } - return formatted -} - - -function generateBalanceObject (formattedBalance, decimalsToKeep = 1) { - var balance = formattedBalance.split(' ')[0] - var label = formattedBalance.split(' ')[1] - var beforeDecimal = balance.split('.')[0] - var afterDecimal = balance.split('.')[1] - var shortBalance = shortenBalance(balance, decimalsToKeep) - - if (beforeDecimal === '0' && afterDecimal.substr(0, 5) === '00000') { - // eslint-disable-next-line eqeqeq - if (afterDecimal == 0) { - balance = '0' - } else { - balance = '<1.0e-5' - } - } else if (beforeDecimal !== '0') { - balance = `${beforeDecimal}.${afterDecimal.slice(0, decimalsToKeep)}` - } - - return { balance, label, shortBalance } -} - -function shortenBalance (balance, decimalsToKeep = 1) { - var truncatedValue - var convertedBalance = parseFloat(balance) - if (convertedBalance > 1000000) { - truncatedValue = (balance / 1000000).toFixed(decimalsToKeep) - return `${truncatedValue}m` - } else if (convertedBalance > 1000) { - truncatedValue = (balance / 1000).toFixed(decimalsToKeep) - return `${truncatedValue}k` - } else if (convertedBalance === 0) { - return '0' - } else if (convertedBalance < 0.001) { - return '<0.001' - } else if (convertedBalance < 1) { - var stringBalance = convertedBalance.toString() - if (stringBalance.split('.')[1].length > 3) { - return convertedBalance.toFixed(3) - } else { - return stringBalance - } - } else { - return convertedBalance.toFixed(decimalsToKeep) - } -} - -function dataSize (data) { - var size = data ? ethUtil.stripHexPrefix(data).length : 0 - return size + ' bytes' -} - -// Takes a BN and an ethereum currency name, -// returns a BN in wei -function normalizeToWei (amount, currency) { - try { - return amount.mul(bnTable.wei).div(bnTable[currency]) - } catch (e) {} - return amount -} - -function normalizeEthStringToWei (str) { - const parts = str.split('.') - let eth = new ethUtil.BN(parts[0], 10).mul(bnTable.wei) - if (parts[1]) { - var decimal = parts[1] - while (decimal.length < 18) { - decimal += '0' - } - const decimalBN = new ethUtil.BN(decimal, 10) - eth = eth.add(decimalBN) - } - return eth -} - -var multiple = new ethUtil.BN('10000', 10) -function normalizeNumberToWei (n, currency) { - var enlarged = n * 10000 - var amount = new ethUtil.BN(String(enlarged), 10) - return normalizeToWei(amount, currency).div(multiple) -} - -function readableDate (ms) { - var date = new Date(ms) - var month = date.getMonth() - var day = date.getDate() - var year = date.getFullYear() - var hours = date.getHours() - var minutes = '0' + date.getMinutes() - var seconds = '0' + date.getSeconds() - - var dateStr = `${month}/${day}/${year}` - var time = `${hours}:${minutes.substr(-2)}:${seconds.substr(-2)}` - return `${dateStr} ${time}` -} - -function isHex (str) { - return Boolean(str.match(/^(0x)?[0-9a-fA-F]+$/)) -} diff --git a/ui/classic/.gitignore b/ui/classic/.gitignore new file mode 100644 index 000000000..c6b1254b5 --- /dev/null +++ b/ui/classic/.gitignore @@ -0,0 +1,66 @@ + +# Created by https://www.gitignore.io/api/osx,node + +### OSX ### +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### Node ### +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git +node_modules + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + diff --git a/ui/classic/app/account-detail.js b/ui/classic/app/account-detail.js new file mode 100644 index 000000000..bed05a7fb --- /dev/null +++ b/ui/classic/app/account-detail.js @@ -0,0 +1,311 @@ +const inherits = require('util').inherits +const extend = require('xtend') +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const CopyButton = require('./components/copyButton') +const AccountInfoLink = require('./components/account-info-link') +const actions = require('./actions') +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +const valuesFor = require('./util').valuesFor + +const Identicon = require('./components/identicon') +const EthBalance = require('./components/eth-balance') +const TransactionList = require('./components/transaction-list') +const ExportAccountView = require('./components/account-export') +const ethUtil = require('ethereumjs-util') +const EditableLabel = require('./components/editable-label') +const Tooltip = require('./components/tooltip') +const TabBar = require('./components/tab-bar') +const TokenList = require('./components/token-list') + +module.exports = connect(mapStateToProps)(AccountDetailScreen) + +function mapStateToProps (state) { + return { + metamask: state.metamask, + identities: state.metamask.identities, + accounts: state.metamask.accounts, + address: state.metamask.selectedAddress, + accountDetail: state.appState.accountDetail, + network: state.metamask.network, + unapprovedMsgs: valuesFor(state.metamask.unapprovedMsgs), + shapeShiftTxList: state.metamask.shapeShiftTxList, + transactions: state.metamask.selectedAddressTxList || [], + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + currentAccountTab: state.metamask.currentAccountTab, + tokens: state.metamask.tokens, + } +} + +inherits(AccountDetailScreen, Component) +function AccountDetailScreen () { + Component.call(this) +} + +AccountDetailScreen.prototype.render = function () { + var props = this.props + var selected = props.address || Object.keys(props.accounts)[0] + var checksumAddress = selected && ethUtil.toChecksumAddress(selected) + var identity = props.identities[selected] + var account = props.accounts[selected] + const { network, conversionRate, currentCurrency } = props + + return ( + + h('.account-detail-section', [ + + // identicon, label, balance, etc + h('.account-data-subsection', { + style: { + margin: '0 20px', + }, + }, [ + + // header - identicon + nav + h('div', { + style: { + paddingTop: '20px', + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'flex-start', + }, + }, [ + + // large identicon and addresses + h('.identicon-wrapper.select-none', [ + h(Identicon, { + diameter: 62, + address: selected, + }), + ]), + h('flex-column', { + style: { + lineHeight: '10px', + marginLeft: '15px', + }, + }, [ + h(EditableLabel, { + textValue: identity ? identity.name : '', + state: { + isEditingLabel: false, + }, + saveText: (text) => { + props.dispatch(actions.saveAccountLabel(selected, text)) + }, + }, [ + + // What is shown when not editing + edit text: + h('label.editing-label', [h('.edit-text', 'edit')]), + h('h2.font-medium.color-forest', {name: 'edit'}, identity && identity.name), + ]), + h('.flex-row', { + style: { + width: '15em', + justifyContent: 'space-between', + alignItems: 'baseline', + }, + }, [ + + // address + + h('div', { + style: { + overflow: 'hidden', + textOverflow: 'ellipsis', + paddingTop: '3px', + width: '5em', + fontSize: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + marginTop: '10px', + marginBottom: '15px', + color: '#AEAEAE', + }, + }, checksumAddress), + + // copy and export + + h('.flex-row', { + style: { + justifyContent: 'flex-end', + }, + }, [ + + h(AccountInfoLink, { selected, network }), + + h(CopyButton, { + value: checksumAddress, + }), + + h(Tooltip, { + title: 'QR Code', + }, [ + h('i.fa.fa-qrcode.pointer.pop-hover', { + onClick: () => props.dispatch(actions.showQrView(selected, identity ? identity.name : '')), + style: { + fontSize: '18px', + position: 'relative', + color: 'rgb(247, 134, 28)', + top: '5px', + marginLeft: '3px', + marginRight: '3px', + }, + }), + ]), + + h(Tooltip, { + title: 'Export Private Key', + }, [ + h('div', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + h('img.cursor-pointer.color-orange', { + src: 'images/key-32.png', + onClick: () => this.requestAccountExport(selected), + style: { + height: '19px', + }, + }), + ]), + ]), + ]), + ]), + + // account ballence + + ]), + ]), + h('.flex-row', { + style: { + justifyContent: 'space-between', + alignItems: 'flex-start', + }, + }, [ + + h(EthBalance, { + value: account && account.balance, + conversionRate, + currentCurrency, + style: { + lineHeight: '7px', + marginTop: '10px', + }, + }), + + h('button', { + onClick: () => props.dispatch(actions.buyEthView(selected)), + style: { + marginBottom: '20px', + marginRight: '8px', + position: 'absolute', + left: '219px', + }, + }, 'BUY'), + + h('button', { + onClick: () => props.dispatch(actions.showSendPage()), + style: { + marginBottom: '20px', + marginRight: '8px', + }, + }, 'SEND'), + + ]), + ]), + + // subview (tx history, pk export confirm, buy eth warning) + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + this.subview(), + ]), + + ]) + ) +} + +AccountDetailScreen.prototype.subview = function () { + var subview + try { + subview = this.props.accountDetail.subview + } catch (e) { + subview = null + } + + switch (subview) { + case 'transactions': + return this.tabSections() + case 'export': + var state = extend({key: 'export'}, this.props) + return h(ExportAccountView, state) + default: + return this.tabSections() + } +} + +AccountDetailScreen.prototype.tabSections = function () { + const { currentAccountTab } = this.props + + return h('section.tabSection', [ + + h(TabBar, { + tabs: [ + { content: 'Sent', key: 'history' }, + { content: 'Tokens', key: 'tokens' }, + ], + defaultTab: currentAccountTab || 'history', + tabSelected: (key) => { + this.props.dispatch(actions.setCurrentAccountTab(key)) + }, + }), + + this.tabSwitchView(), + ]) +} + +AccountDetailScreen.prototype.tabSwitchView = function () { + const props = this.props + const { address, network } = props + const { currentAccountTab, tokens } = this.props + + switch (currentAccountTab) { + case 'tokens': + return h(TokenList, { + userAddress: address, + network, + tokens, + addToken: () => this.props.dispatch(actions.showAddTokenPage()), + }) + default: + return this.transactionList() + } +} + +AccountDetailScreen.prototype.transactionList = function () { + const {transactions, unapprovedMsgs, address, + network, shapeShiftTxList, conversionRate } = this.props + + return h(TransactionList, { + transactions: transactions.sort((a, b) => b.time - a.time), + network, + unapprovedMsgs, + conversionRate, + address, + shapeShiftTxList, + viewPendingTx: (txId) => { + this.props.dispatch(actions.viewPendingTx(txId)) + }, + }) +} + +AccountDetailScreen.prototype.requestAccountExport = function () { + this.props.dispatch(actions.requestExportAccount()) +} diff --git a/ui/classic/app/accounts/account-list-item.js b/ui/classic/app/accounts/account-list-item.js new file mode 100644 index 000000000..10a0b6cc7 --- /dev/null +++ b/ui/classic/app/accounts/account-list-item.js @@ -0,0 +1,91 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') + +const EthBalance = require('../components/eth-balance') +const CopyButton = require('../components/copyButton') +const Identicon = require('../components/identicon') + +module.exports = AccountListItem + +inherits(AccountListItem, Component) +function AccountListItem () { + Component.call(this) +} + +AccountListItem.prototype.render = function () { + const { identity, selectedAddress, accounts, onShowDetail, + conversionRate, currentCurrency } = this.props + + const checksumAddress = identity && identity.address && ethUtil.toChecksumAddress(identity.address) + const isSelected = selectedAddress === identity.address + const account = accounts[identity.address] + const selectedClass = isSelected ? '.selected' : '' + + return ( + h(`.accounts-list-option.flex-row.flex-space-between.pointer.hover-white${selectedClass}`, { + key: `account-panel-${identity.address}`, + onClick: (event) => onShowDetail(identity.address, event), + }, [ + + h('.identicon-wrapper.flex-column.flex-center.select-none', [ + this.pendingOrNot(), + this.indicateIfLoose(), + h(Identicon, { + address: identity.address, + imageify: true, + }), + ]), + + // account address, balance + h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', { + style: { + width: '200px', + }, + }, [ + h('span', identity.name), + h('span.font-small', { + style: { + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + }, checksumAddress), + h(EthBalance, { + value: account && account.balance, + currentCurrency, + conversionRate, + style: { + lineHeight: '7px', + marginTop: '10px', + }, + }), + ]), + + // copy button + h('.identity-copy.flex-column', { + style: { + margin: '0 20px', + }, + }, [ + h(CopyButton, { + value: checksumAddress, + }), + ]), + ]) + ) +} + +AccountListItem.prototype.indicateIfLoose = function () { + try { // Sometimes keyrings aren't loaded yet: + const type = this.props.keyring.type + const isLoose = type !== 'HD Key Tree' + return isLoose ? h('.keyring-label', 'LOOSE') : null + } catch (e) { return } +} + +AccountListItem.prototype.pendingOrNot = function () { + const pending = this.props.pending + if (pending.length === 0) return null + return h('.pending-dot', pending.length) +} diff --git a/ui/classic/app/accounts/import/index.js b/ui/classic/app/accounts/import/index.js new file mode 100644 index 000000000..97b387229 --- /dev/null +++ b/ui/classic/app/accounts/import/index.js @@ -0,0 +1,100 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../../actions') +import Select from 'react-select' + +// Subviews +const JsonImportView = require('./json.js') +const PrivateKeyImportView = require('./private-key.js') + +const menuItems = [ + 'Private Key', + 'JSON File', +] + +module.exports = connect(mapStateToProps)(AccountImportSubview) + +function mapStateToProps (state) { + return { + menuItems, + } +} + +inherits(AccountImportSubview, Component) +function AccountImportSubview () { + Component.call(this) +} + +AccountImportSubview.prototype.render = function () { + const props = this.props + const state = this.state || {} + const { menuItems } = props + const { type } = state + + return ( + h('div', { + style: { + }, + }, [ + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + props.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Import Accounts'), + ]), + h('div', { + style: { + padding: '10px', + color: 'rgb(174, 174, 174)', + }, + }, [ + + h('h3', { style: { padding: '3px' } }, 'SELECT TYPE'), + + h('style', ` + .has-value.Select--single > .Select-control .Select-value .Select-value-label, .Select-value-label { + color: rgb(174,174,174); + } + `), + + h(Select, { + name: 'import-type-select', + clearable: false, + value: type || menuItems[0], + options: menuItems.map((type) => { + return { + value: type, + label: type, + } + }), + onChange: (opt) => { + this.setState({ type: opt.value }) + }, + }), + ]), + + this.renderImportView(), + ]) + ) +} + +AccountImportSubview.prototype.renderImportView = function () { + const props = this.props + const state = this.state || {} + const { type } = state + const { menuItems } = props + const current = type || menuItems[0] + + switch (current) { + case 'Private Key': + return h(PrivateKeyImportView) + case 'JSON File': + return h(JsonImportView) + default: + return h(JsonImportView) + } +} diff --git a/ui/classic/app/accounts/import/json.js b/ui/classic/app/accounts/import/json.js new file mode 100644 index 000000000..158a3c923 --- /dev/null +++ b/ui/classic/app/accounts/import/json.js @@ -0,0 +1,100 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../../actions') +const FileInput = require('react-simple-file-input').default + +const HELP_LINK = 'https://github.com/MetaMask/faq/blob/master/README.md#q-i-cant-use-the-import-feature-for-uploading-a-json-file-the-window-keeps-closing-when-i-try-to-select-a-file' + +module.exports = connect(mapStateToProps)(JsonImportSubview) + +function mapStateToProps (state) { + return { + error: state.appState.warning, + } +} + +inherits(JsonImportSubview, Component) +function JsonImportSubview () { + Component.call(this) +} + +JsonImportSubview.prototype.render = function () { + const { error } = this.props + + return ( + h('div', { + style: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: '5px 15px 0px 15px', + }, + }, [ + + h('p', 'Used by a variety of different clients'), + h('a.warning', { href: HELP_LINK, target: '_blank' }, 'File import not working? Click here!'), + + h(FileInput, { + readAs: 'text', + onLoad: this.onLoad.bind(this), + style: { + margin: '20px 0px 12px 20px', + fontSize: '15px', + }, + }), + + h('input.large-input.letter-spacey', { + type: 'password', + placeholder: 'Enter password', + id: 'json-password-box', + onKeyPress: this.createKeyringOnEnter.bind(this), + style: { + width: 260, + marginTop: 12, + }, + }), + + h('button.primary', { + onClick: this.createNewKeychain.bind(this), + style: { + margin: 12, + }, + }, 'Import'), + + error ? h('span.error', error) : null, + ]) + ) +} + +JsonImportSubview.prototype.onLoad = function (event, file) { + this.setState({file: file, fileContents: event.target.result}) +} + +JsonImportSubview.prototype.createKeyringOnEnter = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.createNewKeychain() + } +} + +JsonImportSubview.prototype.createNewKeychain = function () { + const state = this.state + const { fileContents } = state + + if (!fileContents) { + const message = 'You must select a file to import.' + return this.props.dispatch(actions.displayWarning(message)) + } + + const passwordInput = document.getElementById('json-password-box') + const password = passwordInput.value + + if (!password) { + const message = 'You must enter a password for the selected file.' + return this.props.dispatch(actions.displayWarning(message)) + } + + this.props.dispatch(actions.importNewAccount('JSON File', [ fileContents, password ])) +} diff --git a/ui/classic/app/accounts/import/private-key.js b/ui/classic/app/accounts/import/private-key.js new file mode 100644 index 000000000..68ccee58e --- /dev/null +++ b/ui/classic/app/accounts/import/private-key.js @@ -0,0 +1,67 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../../actions') + +module.exports = connect(mapStateToProps)(PrivateKeyImportView) + +function mapStateToProps (state) { + return { + error: state.appState.warning, + } +} + +inherits(PrivateKeyImportView, Component) +function PrivateKeyImportView () { + Component.call(this) +} + +PrivateKeyImportView.prototype.render = function () { + const { error } = this.props + + return ( + h('div', { + style: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: '5px 15px 0px 15px', + }, + }, [ + h('span', 'Paste your private key string here'), + + h('input.large-input.letter-spacey', { + type: 'password', + id: 'private-key-box', + onKeyPress: this.createKeyringOnEnter.bind(this), + style: { + width: 260, + marginTop: 12, + }, + }), + + h('button.primary', { + onClick: this.createNewKeychain.bind(this), + style: { + margin: 12, + }, + }, 'Import'), + + error ? h('span.error', error) : null, + ]) + ) +} + +PrivateKeyImportView.prototype.createKeyringOnEnter = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.createNewKeychain() + } +} + +PrivateKeyImportView.prototype.createNewKeychain = function () { + const input = document.getElementById('private-key-box') + const privateKey = input.value + this.props.dispatch(actions.importNewAccount('Private Key', [ privateKey ])) +} diff --git a/ui/classic/app/accounts/import/seed.js b/ui/classic/app/accounts/import/seed.js new file mode 100644 index 000000000..b4a7c0afa --- /dev/null +++ b/ui/classic/app/accounts/import/seed.js @@ -0,0 +1,30 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect + +module.exports = connect(mapStateToProps)(SeedImportSubview) + +function mapStateToProps (state) { + return {} +} + +inherits(SeedImportSubview, Component) +function SeedImportSubview () { + Component.call(this) +} + +SeedImportSubview.prototype.render = function () { + return ( + h('div', { + style: { + }, + }, [ + `Paste your seed phrase here!`, + h('textarea'), + h('br'), + h('button', 'Submit'), + ]) + ) +} + diff --git a/ui/classic/app/accounts/index.js b/ui/classic/app/accounts/index.js new file mode 100644 index 000000000..ac2615cd7 --- /dev/null +++ b/ui/classic/app/accounts/index.js @@ -0,0 +1,164 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../actions') +const valuesFor = require('../util').valuesFor +const findDOMNode = require('react-dom').findDOMNode +const AccountListItem = require('./account-list-item') + +module.exports = connect(mapStateToProps)(AccountsScreen) + +function mapStateToProps (state) { + const pendingTxs = valuesFor(state.metamask.unapprovedTxs) + .filter(txMeta => txMeta.metamaskNetworkId === state.metamask.network) + const pendingMsgs = valuesFor(state.metamask.unapprovedMsgs) + const pending = pendingTxs.concat(pendingMsgs) + + return { + accounts: state.metamask.accounts, + identities: state.metamask.identities, + unapprovedTxs: state.metamask.unapprovedTxs, + selectedAddress: state.metamask.selectedAddress, + scrollToBottom: state.appState.scrollToBottom, + pending, + keyrings: state.metamask.keyrings, + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + } +} + +inherits(AccountsScreen, Component) +function AccountsScreen () { + Component.call(this) +} + +AccountsScreen.prototype.render = function () { + const props = this.props + const { keyrings, conversionRate, currentCurrency } = props + const identityList = valuesFor(props.identities) + const unapprovedTxList = valuesFor(props.unapprovedTxs) + + return ( + + h('.accounts-section.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: this.goHome.bind(this), + }), + h('h2.page-subtitle', 'Select Account'), + ]), + + h('hr.horizontal-line'), + + // identity selection + h('section.identity-section', { + style: { + height: '418px', + overflowY: 'auto', + overflowX: 'hidden', + }, + }, + [ + identityList.map((identity) => { + const pending = this.props.pending.filter((txOrMsg) => { + if ('txParams' in txOrMsg) { + return txOrMsg.txParams.from === identity.address + } else if ('msgParams' in txOrMsg) { + return txOrMsg.msgParams.from === identity.address + } else { + return false + } + }) + + const simpleAddress = identity.address.substring(2).toLowerCase() + const keyring = keyrings.find((kr) => { + return kr.accounts.includes(simpleAddress) || + kr.accounts.includes(identity.address) + }) + + return h(AccountListItem, { + key: `acct-panel-${identity.address}`, + identity, + selectedAddress: this.props.selectedAddress, + conversionRate, + currentCurrency, + accounts: this.props.accounts, + onShowDetail: this.onShowDetail.bind(this), + pending, + keyring, + }) + }), + + h('hr.horizontal-line'), + h('div.footer.hover-white.pointer', { + key: 'reveal-account-bar', + onClick: () => { + this.addNewAccount() + }, + style: { + display: 'flex', + height: '40px', + padding: '10px', + justifyContent: 'center', + alignItems: 'center', + }, + }, [ + h('i.fa.fa-plus.fa-lg', {key: ''}), + ]), + h('hr.horizontal-line'), + ]), + + unapprovedTxList.length ? ( + + h('.unconftx-link.flex-row.flex-center', { + onClick: this.navigateToConfTx.bind(this), + }, [ + h('span', 'Unconfirmed Txs'), + h('i.fa.fa-arrow-right.fa-lg'), + ]) + + ) : ( + null + ), + ]) + ) +} + +// If a new account was revealed, scroll to the bottom +AccountsScreen.prototype.componentDidUpdate = function () { + const scrollToBottom = this.props.scrollToBottom + + if (scrollToBottom) { + var container = findDOMNode(this) + var scrollable = container.querySelector('.identity-section') + scrollable.scrollTop = scrollable.scrollHeight + } +} + +AccountsScreen.prototype.navigateToConfTx = function () { + event.stopPropagation() + this.props.dispatch(actions.showConfTxPage()) +} + +AccountsScreen.prototype.onShowDetail = function (address, event) { + event.stopPropagation() + this.props.dispatch(actions.showAccountDetail(address)) +} + +AccountsScreen.prototype.addNewAccount = function () { + this.props.dispatch(actions.addNewAccount(0)) +} + +/* An optional view proposed in this design: + * https://consensys.quip.com/zZVrAysM5znY +AccountsScreen.prototype.addNewAccount = function () { + this.props.dispatch(actions.navigateToNewAccountScreen()) +} +*/ + +AccountsScreen.prototype.goHome = function () { + this.props.dispatch(actions.goHome()) +} diff --git a/ui/classic/app/actions.js b/ui/classic/app/actions.js new file mode 100644 index 000000000..d99291e46 --- /dev/null +++ b/ui/classic/app/actions.js @@ -0,0 +1,1031 @@ +const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url') + +var actions = { + _setBackgroundConnection: _setBackgroundConnection, + + GO_HOME: 'GO_HOME', + goHome: goHome, + // menu state + getNetworkStatus: 'getNetworkStatus', + // transition state + TRANSITION_FORWARD: 'TRANSITION_FORWARD', + TRANSITION_BACKWARD: 'TRANSITION_BACKWARD', + transitionForward, + transitionBackward, + // remote state + UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE', + updateMetamaskState: updateMetamaskState, + // notices + MARK_NOTICE_READ: 'MARK_NOTICE_READ', + markNoticeRead: markNoticeRead, + SHOW_NOTICE: 'SHOW_NOTICE', + showNotice: showNotice, + CLEAR_NOTICES: 'CLEAR_NOTICES', + clearNotices: clearNotices, + markAccountsFound, + // intialize screen + CREATE_NEW_VAULT_IN_PROGRESS: 'CREATE_NEW_VAULT_IN_PROGRESS', + SHOW_CREATE_VAULT: 'SHOW_CREATE_VAULT', + SHOW_RESTORE_VAULT: 'SHOW_RESTORE_VAULT', + FORGOT_PASSWORD: 'FORGOT_PASSWORD', + forgotPassword: forgotPassword, + SHOW_INIT_MENU: 'SHOW_INIT_MENU', + SHOW_NEW_VAULT_SEED: 'SHOW_NEW_VAULT_SEED', + SHOW_INFO_PAGE: 'SHOW_INFO_PAGE', + SHOW_IMPORT_PAGE: 'SHOW_IMPORT_PAGE', + unlockMetamask: unlockMetamask, + unlockFailed: unlockFailed, + showCreateVault: showCreateVault, + showRestoreVault: showRestoreVault, + showInitializeMenu: showInitializeMenu, + showImportPage, + createNewVaultAndKeychain: createNewVaultAndKeychain, + createNewVaultAndRestore: createNewVaultAndRestore, + createNewVaultInProgress: createNewVaultInProgress, + addNewKeyring, + importNewAccount, + addNewAccount, + NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN', + navigateToNewAccountScreen, + showNewVaultSeed: showNewVaultSeed, + showInfoPage: showInfoPage, + // seed recovery actions + REVEAL_SEED_CONFIRMATION: 'REVEAL_SEED_CONFIRMATION', + revealSeedConfirmation: revealSeedConfirmation, + requestRevealSeed: requestRevealSeed, + // unlock screen + UNLOCK_IN_PROGRESS: 'UNLOCK_IN_PROGRESS', + UNLOCK_FAILED: 'UNLOCK_FAILED', + UNLOCK_METAMASK: 'UNLOCK_METAMASK', + LOCK_METAMASK: 'LOCK_METAMASK', + tryUnlockMetamask: tryUnlockMetamask, + lockMetamask: lockMetamask, + unlockInProgress: unlockInProgress, + // error handling + displayWarning: displayWarning, + DISPLAY_WARNING: 'DISPLAY_WARNING', + HIDE_WARNING: 'HIDE_WARNING', + hideWarning: hideWarning, + // accounts screen + SET_SELECTED_ACCOUNT: 'SET_SELECTED_ACCOUNT', + SHOW_ACCOUNT_DETAIL: 'SHOW_ACCOUNT_DETAIL', + SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE', + SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE', + SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE', + SET_CURRENT_FIAT: 'SET_CURRENT_FIAT', + setCurrentCurrency: setCurrentCurrency, + setCurrentAccountTab, + // account detail screen + SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', + showSendPage: showSendPage, + ADD_TO_ADDRESS_BOOK: 'ADD_TO_ADDRESS_BOOK', + addToAddressBook: addToAddressBook, + REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT', + requestExportAccount: requestExportAccount, + EXPORT_ACCOUNT: 'EXPORT_ACCOUNT', + exportAccount: exportAccount, + SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY', + showPrivateKey: showPrivateKey, + SAVE_ACCOUNT_LABEL: 'SAVE_ACCOUNT_LABEL', + saveAccountLabel: saveAccountLabel, + // tx conf screen + COMPLETED_TX: 'COMPLETED_TX', + TRANSACTION_ERROR: 'TRANSACTION_ERROR', + NEXT_TX: 'NEXT_TX', + PREVIOUS_TX: 'PREV_TX', + signMsg: signMsg, + cancelMsg: cancelMsg, + signPersonalMsg, + cancelPersonalMsg, + sendTx: sendTx, + signTx: signTx, + updateAndApproveTx, + cancelTx: cancelTx, + completedTx: completedTx, + txError: txError, + nextTx: nextTx, + previousTx: previousTx, + viewPendingTx: viewPendingTx, + VIEW_PENDING_TX: 'VIEW_PENDING_TX', + // app messages + confirmSeedWords: confirmSeedWords, + showAccountDetail: showAccountDetail, + BACK_TO_ACCOUNT_DETAIL: 'BACK_TO_ACCOUNT_DETAIL', + backToAccountDetail: backToAccountDetail, + showAccountsPage: showAccountsPage, + showConfTxPage: showConfTxPage, + // config screen + SHOW_CONFIG_PAGE: 'SHOW_CONFIG_PAGE', + SET_RPC_TARGET: 'SET_RPC_TARGET', + SET_DEFAULT_RPC_TARGET: 'SET_DEFAULT_RPC_TARGET', + SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE', + USE_ETHERSCAN_PROVIDER: 'USE_ETHERSCAN_PROVIDER', + useEtherscanProvider: useEtherscanProvider, + showConfigPage, + SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', + showAddTokenPage, + addToken, + setRpcTarget: setRpcTarget, + setDefaultRpcTarget: setDefaultRpcTarget, + setProviderType: setProviderType, + // loading overlay + SHOW_LOADING: 'SHOW_LOADING_INDICATION', + HIDE_LOADING: 'HIDE_LOADING_INDICATION', + showLoadingIndication: showLoadingIndication, + hideLoadingIndication: hideLoadingIndication, + // buy Eth with coinbase + BUY_ETH: 'BUY_ETH', + buyEth: buyEth, + buyEthView: buyEthView, + BUY_ETH_VIEW: 'BUY_ETH_VIEW', + COINBASE_SUBVIEW: 'COINBASE_SUBVIEW', + coinBaseSubview: coinBaseSubview, + SHAPESHIFT_SUBVIEW: 'SHAPESHIFT_SUBVIEW', + shapeShiftSubview: shapeShiftSubview, + PAIR_UPDATE: 'PAIR_UPDATE', + pairUpdate: pairUpdate, + coinShiftRquest: coinShiftRquest, + SHOW_SUB_LOADING_INDICATION: 'SHOW_SUB_LOADING_INDICATION', + showSubLoadingIndication: showSubLoadingIndication, + HIDE_SUB_LOADING_INDICATION: 'HIDE_SUB_LOADING_INDICATION', + hideSubLoadingIndication: hideSubLoadingIndication, +// QR STUFF: + SHOW_QR: 'SHOW_QR', + showQrView: showQrView, + reshowQrCode: reshowQrCode, + SHOW_QR_VIEW: 'SHOW_QR_VIEW', +// FORGOT PASSWORD: + BACK_TO_INIT_MENU: 'BACK_TO_INIT_MENU', + goBackToInitView: goBackToInitView, + RECOVERY_IN_PROGRESS: 'RECOVERY_IN_PROGRESS', + BACK_TO_UNLOCK_VIEW: 'BACK_TO_UNLOCK_VIEW', + backToUnlockView: backToUnlockView, + // SHOWING KEYCHAIN + SHOW_NEW_KEYCHAIN: 'SHOW_NEW_KEYCHAIN', + showNewKeychain: showNewKeychain, + + callBackgroundThenUpdate, + forceUpdateMetamaskState, +} + +module.exports = actions + +var background = null +function _setBackgroundConnection (backgroundConnection) { + background = backgroundConnection +} + +function goHome () { + return { + type: actions.GO_HOME, + } +} + +// async actions + +function tryUnlockMetamask (password) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + dispatch(actions.unlockInProgress()) + log.debug(`background.submitPassword`) + background.submitPassword(password, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + dispatch(actions.unlockFailed(err.message)) + } else { + dispatch(actions.transitionForward()) + forceUpdateMetamaskState(dispatch) + } + }) + } +} + +function transitionForward () { + return { + type: this.TRANSITION_FORWARD, + } +} + +function transitionBackward () { + return { + type: this.TRANSITION_BACKWARD, + } +} + +function confirmSeedWords () { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.clearSeedWordCache`) + background.clearSeedWordCache((err, account) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + + log.info('Seed word cache cleared. ' + account) + dispatch(actions.showAccountDetail(account)) + }) + } +} + +function createNewVaultAndRestore (password, seed) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.createNewVaultAndRestore`) + background.createNewVaultAndRestore(password, seed, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.showAccountsPage()) + }) + } +} + +function createNewVaultAndKeychain (password) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.createNewVaultAndKeychain`) + background.createNewVaultAndKeychain(password, (err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + log.debug(`background.placeSeedWords`) + background.placeSeedWords((err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch(actions.hideLoadingIndication()) + forceUpdateMetamaskState(dispatch) + }) + }) + } +} + +function revealSeedConfirmation () { + return { + type: this.REVEAL_SEED_CONFIRMATION, + } +} + +function requestRevealSeed (password) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.submitPassword`) + background.submitPassword(password, (err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + log.debug(`background.placeSeedWords`) + background.placeSeedWords((err, result) => { + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.hideLoadingIndication()) + dispatch(actions.showNewVaultSeed(result)) + }) + }) + } +} + +function addNewKeyring (type, opts) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.addNewKeyring`) + background.addNewKeyring(type, opts, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.showAccountsPage()) + }) + } +} + +function importNewAccount (strategy, args) { + return (dispatch) => { + dispatch(actions.showLoadingIndication('This may take a while, be patient.')) + log.debug(`background.importAccountWithStrategy`) + background.importAccountWithStrategy(strategy, args, (err) => { + if (err) return dispatch(actions.displayWarning(err.message)) + log.debug(`background.getState`) + background.getState((err, newState) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch(actions.updateMetamaskState(newState)) + dispatch({ + type: actions.SHOW_ACCOUNT_DETAIL, + value: newState.selectedAddress, + }) + }) + }) + } +} + +function navigateToNewAccountScreen () { + return { + type: this.NEW_ACCOUNT_SCREEN, + } +} + +function addNewAccount () { + log.debug(`background.addNewAccount`) + return callBackgroundThenUpdate(background.addNewAccount) +} + +function showInfoPage () { + return { + type: actions.SHOW_INFO_PAGE, + } +} + +function setCurrentCurrency (currencyCode) { + return (dispatch) => { + dispatch(this.showLoadingIndication()) + log.debug(`background.setCurrentCurrency`) + background.setCurrentCurrency(currencyCode, (err, data) => { + dispatch(this.hideLoadingIndication()) + if (err) { + log.error(err.stack) + return dispatch(actions.displayWarning(err.message)) + } + dispatch({ + type: this.SET_CURRENT_FIAT, + value: { + currentCurrency: data.currentCurrency, + conversionRate: data.conversionRate, + conversionDate: data.conversionDate, + }, + }) + }) + } +} + +function signMsg (msgData) { + log.debug('action - signMsg') + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + + log.debug(`actions calling background.signMessage`) + background.signMessage(msgData, (err, newState) => { + log.debug('signMessage called back') + dispatch(actions.updateMetamaskState(newState)) + dispatch(actions.hideLoadingIndication()) + + if (err) log.error(err) + if (err) return dispatch(actions.displayWarning(err.message)) + + dispatch(actions.completedTx(msgData.metamaskId)) + }) + } +} + +function signPersonalMsg (msgData) { + log.debug('action - signPersonalMsg') + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + + log.debug(`actions calling background.signPersonalMessage`) + background.signPersonalMessage(msgData, (err, newState) => { + log.debug('signPersonalMessage called back') + dispatch(actions.updateMetamaskState(newState)) + dispatch(actions.hideLoadingIndication()) + + if (err) log.error(err) + if (err) return dispatch(actions.displayWarning(err.message)) + + dispatch(actions.completedTx(msgData.metamaskId)) + }) + } +} + +function signTx (txData) { + return (dispatch) => { + global.ethQuery.sendTransaction(txData, (err, data) => { + dispatch(actions.hideLoadingIndication()) + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.hideWarning()) + }) + dispatch(this.showConfTxPage()) + } +} + +function sendTx (txData) { + log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`) + return (dispatch) => { + log.debug(`actions calling background.approveTransaction`) + background.approveTransaction(txData.id, (err) => { + if (err) { + dispatch(actions.txError(err)) + return log.error(err.message) + } + dispatch(actions.completedTx(txData.id)) + }) + } +} + +function updateAndApproveTx (txData) { + log.info('actions: updateAndApproveTx: ' + JSON.stringify(txData)) + return (dispatch) => { + log.debug(`actions calling background.updateAndApproveTx`) + background.updateAndApproveTransaction(txData, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + dispatch(actions.txError(err)) + return log.error(err.message) + } + dispatch(actions.completedTx(txData.id)) + }) + } +} + +function completedTx (id) { + return { + type: actions.COMPLETED_TX, + value: id, + } +} + +function txError (err) { + return { + type: actions.TRANSACTION_ERROR, + message: err.message, + } +} + +function cancelMsg (msgData) { + log.debug(`background.cancelMessage`) + background.cancelMessage(msgData.id) + return actions.completedTx(msgData.id) +} + +function cancelPersonalMsg (msgData) { + const id = msgData.id + background.cancelPersonalMessage(id) + return actions.completedTx(id) +} + +function cancelTx (txData) { + log.debug(`background.cancelTransaction`) + background.cancelTransaction(txData.id) + return actions.completedTx(txData.id) +} + +// +// initialize screen +// + +function showCreateVault () { + return { + type: actions.SHOW_CREATE_VAULT, + } +} + +function showRestoreVault () { + return { + type: actions.SHOW_RESTORE_VAULT, + } +} + +function forgotPassword () { + return { + type: actions.FORGOT_PASSWORD, + } +} + +function showInitializeMenu () { + return { + type: actions.SHOW_INIT_MENU, + } +} + +function showImportPage () { + return { + type: actions.SHOW_IMPORT_PAGE, + } +} + +function createNewVaultInProgress () { + return { + type: actions.CREATE_NEW_VAULT_IN_PROGRESS, + } +} + +function showNewVaultSeed (seed) { + return { + type: actions.SHOW_NEW_VAULT_SEED, + value: seed, + } +} + +function backToUnlockView () { + return { + type: actions.BACK_TO_UNLOCK_VIEW, + } +} + +function showNewKeychain () { + return { + type: actions.SHOW_NEW_KEYCHAIN, + } +} + +// +// unlock screen +// + +function unlockInProgress () { + return { + type: actions.UNLOCK_IN_PROGRESS, + } +} + +function unlockFailed (message) { + return { + type: actions.UNLOCK_FAILED, + value: message, + } +} + +function unlockMetamask (account) { + return { + type: actions.UNLOCK_METAMASK, + value: account, + } +} + +function updateMetamaskState (newState) { + return { + type: actions.UPDATE_METAMASK_STATE, + value: newState, + } +} + +function lockMetamask () { + log.debug(`background.setLocked`) + return callBackgroundThenUpdate(background.setLocked) +} + +function setCurrentAccountTab (newTabName) { + log.debug(`background.setCurrentAccountTab: ${newTabName}`) + return callBackgroundThenUpdateNoSpinner(background.setCurrentAccountTab, newTabName) +} + +function showAccountDetail (address) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.setSelectedAddress`) + background.setSelectedAddress(address, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch({ + type: actions.SHOW_ACCOUNT_DETAIL, + value: address, + }) + }) + } +} + +function backToAccountDetail (address) { + return { + type: actions.BACK_TO_ACCOUNT_DETAIL, + value: address, + } +} + +function showAccountsPage () { + return { + type: actions.SHOW_ACCOUNTS_PAGE, + } +} + +function showConfTxPage (transForward = true) { + return { + type: actions.SHOW_CONF_TX_PAGE, + transForward: transForward, + } +} + +function nextTx () { + return { + type: actions.NEXT_TX, + } +} + +function viewPendingTx (txId) { + return { + type: actions.VIEW_PENDING_TX, + value: txId, + } +} + +function previousTx () { + return { + type: actions.PREVIOUS_TX, + } +} + +function showConfigPage (transitionForward = true) { + return { + type: actions.SHOW_CONFIG_PAGE, + value: transitionForward, + } +} + +function showAddTokenPage (transitionForward = true) { + return { + type: actions.SHOW_ADD_TOKEN_PAGE, + value: transitionForward, + } +} + +function addToken (address, symbol, decimals) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + background.addToken(address, symbol, decimals, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + setTimeout(() => { + dispatch(actions.goHome()) + }, 250) + }) + } +} + +function goBackToInitView () { + return { + type: actions.BACK_TO_INIT_MENU, + } +} + +// +// notice +// + +function markNoticeRead (notice) { + return (dispatch) => { + dispatch(this.showLoadingIndication()) + log.debug(`background.markNoticeRead`) + background.markNoticeRead(notice, (err, notice) => { + dispatch(this.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err)) + } + if (notice) { + return dispatch(actions.showNotice(notice)) + } else { + dispatch(this.clearNotices()) + return { + type: actions.SHOW_ACCOUNTS_PAGE, + } + } + }) + } +} + +function showNotice (notice) { + return { + type: actions.SHOW_NOTICE, + value: notice, + } +} + +function clearNotices () { + return { + type: actions.CLEAR_NOTICES, + } +} + +function markAccountsFound () { + log.debug(`background.markAccountsFound`) + return callBackgroundThenUpdate(background.markAccountsFound) +} + +// +// config +// + +// default rpc target refers to localhost:8545 in this instance. +function setDefaultRpcTarget (rpcList) { + log.debug(`background.setDefaultRpcTarget`) + return (dispatch) => { + background.setDefaultRpc((err, result) => { + if (err) { + log.error(err) + return dispatch(self.displayWarning('Had a problem changing networks.')) + } + }) + } +} + +function setRpcTarget (newRpc) { + log.debug(`background.setRpcTarget`) + return (dispatch) => { + background.setCustomRpc(newRpc, (err, result) => { + if (err) { + log.error(err) + return dispatch(self.displayWarning('Had a problem changing networks!')) + } + }) + } +} + +// Calls the addressBookController to add a new address. +function addToAddressBook (recipient, nickname) { + log.debug(`background.addToAddressBook`) + return (dispatch) => { + background.setAddressBook(recipient, nickname, (err, result) => { + if (err) { + log.error(err) + return dispatch(self.displayWarning('Address book failed to update')) + } + }) + } +} + +function setProviderType (type) { + log.debug(`background.setProviderType`) + background.setProviderType(type) + return { + type: actions.SET_PROVIDER_TYPE, + value: type, + } +} + +function useEtherscanProvider () { + log.debug(`background.useEtherscanProvider`) + background.useEtherscanProvider() + return { + type: actions.USE_ETHERSCAN_PROVIDER, + } +} + +function showLoadingIndication (message) { + return { + type: actions.SHOW_LOADING, + value: message, + } +} + +function hideLoadingIndication () { + return { + type: actions.HIDE_LOADING, + } +} + +function showSubLoadingIndication () { + return { + type: actions.SHOW_SUB_LOADING_INDICATION, + } +} + +function hideSubLoadingIndication () { + return { + type: actions.HIDE_SUB_LOADING_INDICATION, + } +} + +function displayWarning (text) { + return { + type: actions.DISPLAY_WARNING, + value: text, + } +} + +function hideWarning () { + return { + type: actions.HIDE_WARNING, + } +} + +function requestExportAccount () { + return { + type: actions.REQUEST_ACCOUNT_EXPORT, + } +} + +function exportAccount (password, address) { + var self = this + + return function (dispatch) { + dispatch(self.showLoadingIndication()) + + log.debug(`background.submitPassword`) + background.submitPassword(password, function (err) { + if (err) { + log.error('Error in submiting password.') + dispatch(self.hideLoadingIndication()) + return dispatch(self.displayWarning('Incorrect Password.')) + } + log.debug(`background.exportAccount`) + background.exportAccount(address, function (err, result) { + dispatch(self.hideLoadingIndication()) + + if (err) { + log.error(err) + return dispatch(self.displayWarning('Had a problem exporting the account.')) + } + + dispatch(self.showPrivateKey(result)) + }) + }) + } +} + +function showPrivateKey (key) { + return { + type: actions.SHOW_PRIVATE_KEY, + value: key, + } +} + +function saveAccountLabel (account, label) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.saveAccountLabel`) + background.saveAccountLabel(account, label, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch({ + type: actions.SAVE_ACCOUNT_LABEL, + value: { account, label }, + }) + }) + } +} + +function showSendPage () { + return { + type: actions.SHOW_SEND_PAGE, + } +} + +function buyEth (opts) { + return (dispatch) => { + const url = getBuyEthUrl(opts) + global.platform.openWindow({ url }) + dispatch({ + type: actions.BUY_ETH, + }) + } +} + +function buyEthView (address) { + return { + type: actions.BUY_ETH_VIEW, + value: address, + } +} + +function coinBaseSubview () { + return { + type: actions.COINBASE_SUBVIEW, + } +} + +function pairUpdate (coin) { + return (dispatch) => { + dispatch(actions.showSubLoadingIndication()) + dispatch(actions.hideWarning()) + shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { + dispatch(actions.hideSubLoadingIndication()) + dispatch({ + type: actions.PAIR_UPDATE, + value: { + marketinfo: mktResponse, + }, + }) + }) + } +} + +function shapeShiftSubview (network) { + var pair = 'btc_eth' + + return (dispatch) => { + dispatch(actions.showSubLoadingIndication()) + shapeShiftRequest('marketinfo', {pair}, (mktResponse) => { + shapeShiftRequest('getcoins', {}, (response) => { + dispatch(actions.hideSubLoadingIndication()) + if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) + dispatch({ + type: actions.SHAPESHIFT_SUBVIEW, + value: { + marketinfo: mktResponse, + coinOptions: response, + }, + }) + }) + }) + } +} + +function coinShiftRquest (data, marketData) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + shapeShiftRequest('shift', { method: 'POST', data}, (response) => { + dispatch(actions.hideLoadingIndication()) + if (response.error) return dispatch(actions.displayWarning(response.error)) + var message = ` + Deposit your ${response.depositType} to the address bellow:` + log.debug(`background.createShapeShiftTx`) + background.createShapeShiftTx(response.deposit, response.depositType) + dispatch(actions.showQrView(response.deposit, [message].concat(marketData))) + }) + } +} + +function showQrView (data, message) { + return { + type: actions.SHOW_QR_VIEW, + value: { + message: message, + data: data, + }, + } +} +function reshowQrCode (data, coin) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { + if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) + + var message = [ + `Deposit your ${coin} to the address bellow:`, + `Deposit Limit: ${mktResponse.limit}`, + `Deposit Minimum:${mktResponse.minimum}`, + ] + + dispatch(actions.hideLoadingIndication()) + return dispatch(actions.showQrView(data, message)) + }) + } +} + +function shapeShiftRequest (query, options, cb) { + var queryResponse, method + !options ? options = {} : null + options.method ? method = options.method : method = 'GET' + + var requestListner = function (request) { + queryResponse = JSON.parse(this.responseText) + cb ? cb(queryResponse) : null + return queryResponse + } + + var shapShiftReq = new XMLHttpRequest() + shapShiftReq.addEventListener('load', requestListner) + shapShiftReq.open(method, `https://shapeshift.io/${query}/${options.pair ? options.pair : ''}`, true) + + if (options.method === 'POST') { + var jsonObj = JSON.stringify(options.data) + shapShiftReq.setRequestHeader('Content-Type', 'application/json') + return shapShiftReq.send(jsonObj) + } else { + return shapShiftReq.send() + } +} + +// Call Background Then Update +// +// A function generator for a common pattern wherein: +// We show loading indication. +// We call a background method. +// We hide loading indication. +// If it errored, we show a warning. +// If it didn't, we update the state. +function callBackgroundThenUpdateNoSpinner (method, ...args) { + return (dispatch) => { + method.call(background, ...args, (err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + forceUpdateMetamaskState(dispatch) + }) + } +} + +function callBackgroundThenUpdate (method, ...args) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + method.call(background, ...args, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + forceUpdateMetamaskState(dispatch) + }) + } +} + +function forceUpdateMetamaskState (dispatch) { + log.debug(`background.getState`) + background.getState((err, newState) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch(actions.updateMetamaskState(newState)) + }) +} diff --git a/ui/classic/app/add-token.js b/ui/classic/app/add-token.js new file mode 100644 index 000000000..b303b5c0d --- /dev/null +++ b/ui/classic/app/add-token.js @@ -0,0 +1,219 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') + +const ethUtil = require('ethereumjs-util') +const abi = require('human-standard-token-abi') +const Eth = require('ethjs-query') +const EthContract = require('ethjs-contract') + +const emptyAddr = '0x0000000000000000000000000000000000000000' + +module.exports = connect(mapStateToProps)(AddTokenScreen) + +function mapStateToProps (state) { + return { + } +} + +inherits(AddTokenScreen, Component) +function AddTokenScreen () { + this.state = { + warning: null, + address: null, + symbol: 'TOKEN', + decimals: 18, + } + Component.call(this) +} + +AddTokenScreen.prototype.render = function () { + const state = this.state + const props = this.props + const { warning, symbol, decimals } = state + + return ( + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + props.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Add Token'), + ]), + + h('.error', { + style: { + display: warning ? 'block' : 'none', + padding: '0 20px', + textAlign: 'center', + }, + }, warning), + + // conf view + h('.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-space-around', { + style: { + padding: '20px', + }, + }, [ + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Token Address'), + ]), + + h('section.flex-row.flex-center', [ + h('input#token-address', { + name: 'address', + placeholder: 'Token Address', + onChange: this.tokenAddressDidChange.bind(this), + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + }), + ]), + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Token Sybmol'), + ]), + + h('div', { style: {display: 'flex'} }, [ + h('input#token_symbol', { + placeholder: `Like "ETH"`, + value: symbol, + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onChange: (event) => { + var element = event.target + var symbol = element.value + this.setState({ symbol }) + }, + }), + ]), + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Decimals of Precision'), + ]), + + h('div', { style: {display: 'flex'} }, [ + h('input#token_decimals', { + value: decimals, + type: 'number', + min: 0, + max: 36, + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onChange: (event) => { + var element = event.target + var decimals = element.value.trim() + this.setState({ decimals }) + }, + }), + ]), + + h('button', { + style: { + alignSelf: 'center', + }, + onClick: (event) => { + const valid = this.validateInputs() + if (!valid) return + + const { address, symbol, decimals } = this.state + this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) + }, + }, 'Add'), + ]), + ]), + ]) + ) +} + +AddTokenScreen.prototype.componentWillMount = function () { + if (typeof global.ethereumProvider === 'undefined') return + + this.eth = new Eth(global.ethereumProvider) + this.contract = new EthContract(this.eth) + this.TokenContract = this.contract(abi) +} + +AddTokenScreen.prototype.tokenAddressDidChange = function (event) { + const el = event.target + const address = el.value.trim() + if (ethUtil.isValidAddress(address) && address !== emptyAddr) { + this.setState({ address }) + this.attemptToAutoFillTokenParams(address) + } +} + +AddTokenScreen.prototype.validateInputs = function () { + let msg = '' + const state = this.state + const { address, symbol, decimals } = state + + const validAddress = ethUtil.isValidAddress(address) + if (!validAddress) { + msg += 'Address is invalid. ' + } + + const validDecimals = decimals >= 0 && decimals < 36 + if (!validDecimals) { + msg += 'Decimals must be at least 0, and not over 36. ' + } + + const symbolLen = symbol.trim().length + const validSymbol = symbolLen > 0 && symbolLen < 10 + if (!validSymbol) { + msg += 'Symbol must be between 0 and 10 characters.' + } + + const isValid = validAddress && validDecimals + + if (!isValid) { + this.setState({ + warning: msg, + }) + } else { + this.setState({ warning: null }) + } + + return isValid +} + +AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) { + const contract = this.TokenContract.at(address) + + const results = await Promise.all([ + contract.symbol(), + contract.decimals(), + ]) + + const [ symbol, decimals ] = results + if (symbol && decimals) { + console.log('SETTING SYMBOL AND DECIMALS', { symbol, decimals }) + this.setState({ symbol: symbol[0], decimals: decimals[0].toString() }) + } +} + diff --git a/ui/classic/app/app.js b/ui/classic/app/app.js new file mode 100644 index 000000000..1a63002e1 --- /dev/null +++ b/ui/classic/app/app.js @@ -0,0 +1,591 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('./actions') +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +// init +const InitializeMenuScreen = require('./first-time/init-menu') +const NewKeyChainScreen = require('./new-keychain') +// unlock +const UnlockScreen = require('./unlock') +// accounts +const AccountsScreen = require('./accounts') +const AccountDetailScreen = require('./account-detail') +const SendTransactionScreen = require('./send') +const ConfirmTxScreen = require('./conf-tx') +// notice +const NoticeScreen = require('./components/notice') +const generateLostAccountsNotice = require('../lib/lost-accounts-notice') +// other views +const ConfigScreen = require('./config') +const AddTokenScreen = require('./add-token') +const Import = require('./accounts/import') +const InfoScreen = require('./info') +const Loading = require('./components/loading') +const SandwichExpando = require('sandwich-expando') +const MenuDroppo = require('menu-droppo') +const DropMenuItem = require('./components/drop-menu-item') +const NetworkIndicator = require('./components/network') +const Tooltip = require('./components/tooltip') +const BuyView = require('./components/buy-button-subview') +const QrView = require('./components/qr-code') +const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete') +const HDRestoreVaultScreen = require('./keychains/hd/restore-vault') +const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation') + +module.exports = connect(mapStateToProps)(App) + +inherits(App, Component) +function App () { Component.call(this) } + +function mapStateToProps (state) { + return { + // state from plugin + isLoading: state.appState.isLoading, + loadingMessage: state.appState.loadingMessage, + noActiveNotices: state.metamask.noActiveNotices, + isInitialized: state.metamask.isInitialized, + isUnlocked: state.metamask.isUnlocked, + currentView: state.appState.currentView, + activeAddress: state.appState.activeAddress, + transForward: state.appState.transForward, + seedWords: state.metamask.seedWords, + unapprovedTxs: state.metamask.unapprovedTxs, + unapprovedMsgs: state.metamask.unapprovedMsgs, + menuOpen: state.appState.menuOpen, + network: state.metamask.network, + provider: state.metamask.provider, + forgottenPassword: state.appState.forgottenPassword, + lastUnreadNotice: state.metamask.lastUnreadNotice, + lostAccounts: state.metamask.lostAccounts, + frequentRpcList: state.metamask.frequentRpcList || [], + } +} + +App.prototype.render = function () { + var props = this.props + const { isLoading, loadingMessage, transForward, network } = props + const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config' + const loadMessage = loadingMessage || isLoadingNetwork ? + `Connecting to ${this.getNetworkName()}` : null + + log.debug('Main ui render function') + + return ( + + h('.flex-column.flex-grow.full-height', { + style: { + // Windows was showing a vertical scroll bar: + overflow: 'hidden', + position: 'relative', + }, + }, [ + + // app bar + this.renderAppBar(), + this.renderNetworkDropdown(), + this.renderDropdown(), + + h(Loading, { + isLoading: isLoading || isLoadingNetwork, + loadingMessage: loadMessage, + }), + + // panel content + h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), { + style: { + height: '380px', + width: '360px', + }, + }, [ + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + this.renderPrimary(), + ]), + ]), + ]) + ) +} + +App.prototype.renderAppBar = function () { + if (window.METAMASK_UI_TYPE === 'notification') { + return null + } + + const props = this.props + const state = this.state || {} + const isNetworkMenuOpen = state.isNetworkMenuOpen || false + + return ( + + h('div', [ + + h('.app-header.flex-row.flex-space-between', { + style: { + alignItems: 'center', + visibility: props.isUnlocked ? 'visible' : 'none', + background: props.isUnlocked ? 'white' : 'none', + height: '36px', + position: 'relative', + zIndex: 12, + }, + }, [ + + h('div.left-menu-section', { + style: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + }, + }, [ + + // mini logo + h('img', { + height: 24, + width: 24, + src: '/images/icon-128.png', + }), + + h(NetworkIndicator, { + network: this.props.network, + provider: this.props.provider, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen }) + }, + }), + ]), + + // metamask name + props.isUnlocked && h('h1', { + style: { + position: 'relative', + left: '9px', + }, + }, 'MetaMask'), + + props.isUnlocked && h('div', { + style: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + }, + }, [ + + // small accounts nav + props.isUnlocked && h(Tooltip, { title: 'Switch Accounts' }, [ + h('img.cursor-pointer.color-orange', { + src: 'images/switch_acc.svg', + style: { + width: '23.5px', + marginRight: '8px', + }, + onClick: (event) => { + event.stopPropagation() + this.props.dispatch(actions.showAccountsPage()) + }, + }), + ]), + + // hamburger + props.isUnlocked && h(SandwichExpando, { + width: 16, + barHeight: 2, + padding: 0, + isOpen: state.isMainMenuOpen, + color: 'rgb(247,146,30)', + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + this.setState({ isMainMenuOpen: !state.isMainMenuOpen }) + }, + }), + ]), + ]), + ]) + ) +} + +App.prototype.renderNetworkDropdown = function () { + const props = this.props + const rpcList = props.frequentRpcList + const state = this.state || {} + const isOpen = state.isNetworkMenuOpen + + return h(MenuDroppo, { + isOpen, + onClickOutside: (event) => { + this.setState({ isNetworkMenuOpen: !isOpen }) + }, + zIndex: 11, + style: { + position: 'absolute', + left: 0, + top: '36px', + }, + innerStyle: { + background: 'white', + boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', + }, + }, [ // DROP MENU ITEMS + h('style', ` + .drop-menu-item:hover { background:rgb(235, 235, 235); } + .drop-menu-item i { margin: 11px; } + `), + + h(DropMenuItem, { + label: 'Main Ethereum Network', + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setProviderType('mainnet')), + icon: h('.menu-icon.diamond'), + activeNetworkRender: props.network, + provider: props.provider, + }), + + h(DropMenuItem, { + label: 'Ropsten Test Network', + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setProviderType('ropsten')), + icon: h('.menu-icon.red-dot'), + activeNetworkRender: props.network, + provider: props.provider, + }), + + h(DropMenuItem, { + label: 'Kovan Test Network', + closeMenu: () => this.setState({ isNetworkMenuOpen: false}), + action: () => props.dispatch(actions.setProviderType('kovan')), + icon: h('.menu-icon.hollow-diamond'), + activeNetworkRender: props.network, + provider: props.provider, + }), + + h(DropMenuItem, { + label: 'Rinkeby Test Network', + closeMenu: () => this.setState({ isNetworkMenuOpen: false}), + action: () => props.dispatch(actions.setProviderType('rinkeby')), + icon: h('.menu-icon.golden-square'), + activeNetworkRender: props.network, + provider: props.provider, + }), + + h(DropMenuItem, { + label: 'Localhost 8545', + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setDefaultRpcTarget(rpcList)), + icon: h('i.fa.fa-question-circle.fa-lg'), + activeNetworkRender: props.provider.rpcTarget, + }), + + this.renderCustomOption(props.provider), + this.renderCommonRpc(rpcList, props.provider), + + h(DropMenuItem, { + label: 'Custom RPC', + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => this.props.dispatch(actions.showConfigPage()), + icon: h('i.fa.fa-question-circle.fa-lg'), + }), + + ]) +} + +App.prototype.renderDropdown = function () { + const state = this.state || {} + const isOpen = state.isMainMenuOpen + + return h(MenuDroppo, { + isOpen: isOpen, + zIndex: 11, + onClickOutside: (event) => { + this.setState({ isMainMenuOpen: !isOpen }) + }, + style: { + position: 'absolute', + right: 0, + top: '36px', + }, + innerStyle: { + background: 'white', + boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', + }, + }, [ // DROP MENU ITEMS + h('style', ` + .drop-menu-item:hover { background:rgb(235, 235, 235); } + .drop-menu-item i { margin: 11px; } + `), + + h(DropMenuItem, { + label: 'Settings', + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + action: () => this.props.dispatch(actions.showConfigPage()), + icon: h('i.fa.fa-gear.fa-lg'), + }), + + h(DropMenuItem, { + label: 'Import Account', + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + action: () => this.props.dispatch(actions.showImportPage()), + icon: h('i.fa.fa-arrow-circle-o-up.fa-lg'), + }), + + h(DropMenuItem, { + label: 'Lock', + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + action: () => this.props.dispatch(actions.lockMetamask()), + icon: h('i.fa.fa-lock.fa-lg'), + }), + + h(DropMenuItem, { + label: 'Info/Help', + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + action: () => this.props.dispatch(actions.showInfoPage()), + icon: h('i.fa.fa-question.fa-lg'), + }), + ]) +} + +App.prototype.renderBackButton = function (style, justArrow = false) { + var props = this.props + return ( + h('.flex-row', { + key: 'leftArrow', + style: style, + onClick: () => props.dispatch(actions.goBackToInitView()), + }, [ + h('i.fa.fa-arrow-left.cursor-pointer'), + justArrow ? null : h('div.cursor-pointer', { + style: { + marginLeft: '3px', + }, + onClick: () => props.dispatch(actions.goBackToInitView()), + }, 'BACK'), + ]) + ) +} + +App.prototype.renderPrimary = function () { + log.debug('rendering primary') + var props = this.props + + // notices + if (!props.noActiveNotices) { + log.debug('rendering notice screen for unread notices.') + return h(NoticeScreen, { + notice: props.lastUnreadNotice, + key: 'NoticeScreen', + onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)), + }) + } else if (props.lostAccounts && props.lostAccounts.length > 0) { + log.debug('rendering notice screen for lost accounts view.') + return h(NoticeScreen, { + notice: generateLostAccountsNotice(props.lostAccounts), + key: 'LostAccountsNotice', + onConfirm: () => props.dispatch(actions.markAccountsFound()), + }) + } + + if (props.seedWords) { + log.debug('rendering seed words') + return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'}) + } + + // show initialize screen + if (!props.isInitialized || props.forgottenPassword) { + // show current view + log.debug('rendering an initialize screen') + switch (props.currentView.name) { + + case 'restoreVault': + log.debug('rendering restore vault screen') + return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) + + default: + log.debug('rendering menu screen') + return h(InitializeMenuScreen, {key: 'menuScreenInit'}) + } + } + + // show unlock screen + if (!props.isUnlocked) { + switch (props.currentView.name) { + + case 'restoreVault': + log.debug('rendering restore vault screen') + return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) + + case 'config': + log.debug('rendering config screen from unlock screen.') + return h(ConfigScreen, {key: 'config'}) + + default: + log.debug('rendering locked screen') + return h(UnlockScreen, {key: 'locked'}) + } + } + + // show current view + switch (props.currentView.name) { + + case 'accounts': + log.debug('rendering accounts screen') + return h(AccountsScreen, {key: 'accounts'}) + + case 'accountDetail': + log.debug('rendering account detail screen') + return h(AccountDetailScreen, {key: 'account-detail'}) + + case 'sendTransaction': + log.debug('rendering send tx screen') + return h(SendTransactionScreen, {key: 'send-transaction'}) + + case 'newKeychain': + log.debug('rendering new keychain screen') + return h(NewKeyChainScreen, {key: 'new-keychain'}) + + case 'confTx': + log.debug('rendering confirm tx screen') + return h(ConfirmTxScreen, {key: 'confirm-tx'}) + + case 'add-token': + log.debug('rendering add-token screen from unlock screen.') + return h(AddTokenScreen, {key: 'add-token'}) + + case 'config': + log.debug('rendering config screen') + return h(ConfigScreen, {key: 'config'}) + + case 'import-menu': + log.debug('rendering import screen') + return h(Import, {key: 'import-menu'}) + + case 'reveal-seed-conf': + log.debug('rendering reveal seed confirmation screen') + return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'}) + + case 'info': + log.debug('rendering info screen') + return h(InfoScreen, {key: 'info'}) + + case 'buyEth': + log.debug('rendering buy ether screen') + return h(BuyView, {key: 'buyEthView'}) + + case 'qr': + log.debug('rendering show qr screen') + return h('div', { + style: { + position: 'absolute', + height: '100%', + top: '0px', + left: '0px', + }, + }, [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { + onClick: () => props.dispatch(actions.backToAccountDetail(props.activeAddress)), + style: { + marginLeft: '10px', + marginTop: '50px', + }, + }), + h('div', { + style: { + position: 'absolute', + left: '44px', + width: '285px', + }, + }, [ + h(QrView, {key: 'qr'}), + ]), + ]) + + default: + log.debug('rendering default, account detail screen') + return h(AccountDetailScreen, {key: 'account-detail'}) + } +} + +App.prototype.toggleMetamaskActive = function () { + if (!this.props.isUnlocked) { + // currently inactive: redirect to password box + var passwordBox = document.querySelector('input[type=password]') + if (!passwordBox) return + passwordBox.focus() + } else { + // currently active: deactivate + this.props.dispatch(actions.lockMetamask(false)) + } +} + +App.prototype.renderCustomOption = function (provider) { + const { rpcTarget, type } = provider + if (type !== 'rpc') return null + + // Concatenate long URLs + let label = rpcTarget + if (rpcTarget.length > 31) { + label = label.substr(0, 34) + '...' + } + + switch (rpcTarget) { + + case 'http://localhost:8545': + return null + + default: + return h(DropMenuItem, { + label, + key: rpcTarget, + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + icon: h('i.fa.fa-question-circle.fa-lg'), + activeNetworkRender: 'custom', + }) + } +} + +App.prototype.getNetworkName = function () { + const { provider } = this.props + const providerName = provider.type + + let name + + if (providerName === 'mainnet') { + name = 'Main Ethereum Network' + } else if (providerName === 'ropsten') { + name = 'Ropsten Test Network' + } else if (providerName === 'kovan') { + name = 'Kovan Test Network' + } else if (providerName === 'rinkeby') { + name = 'Rinkeby Test Network' + } else { + name = 'Unknown Private Network' + } + + return name +} + +App.prototype.renderCommonRpc = function (rpcList, provider) { + const { rpcTarget } = provider + const props = this.props + + return rpcList.map((rpc) => { + if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { + return null + } else { + return h(DropMenuItem, { + label: rpc, + key: rpc, + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setRpcTarget(rpc)), + icon: h('i.fa.fa-question-circle.fa-lg'), + activeNetworkRender: rpc, + }) + } + }) +} diff --git a/ui/classic/app/components/account-export.js b/ui/classic/app/components/account-export.js new file mode 100644 index 000000000..394d878f7 --- /dev/null +++ b/ui/classic/app/components/account-export.js @@ -0,0 +1,122 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const copyToClipboard = require('copy-to-clipboard') +const actions = require('../actions') +const ethUtil = require('ethereumjs-util') +const connect = require('react-redux').connect + +module.exports = connect(mapStateToProps)(ExportAccountView) + +inherits(ExportAccountView, Component) +function ExportAccountView () { + Component.call(this) +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +ExportAccountView.prototype.render = function () { + var state = this.props + var accountDetail = state.accountDetail + + if (!accountDetail) return h('div') + var accountExport = accountDetail.accountExport + + var notExporting = accountExport === 'none' + var exportRequested = accountExport === 'requested' + var accountExported = accountExport === 'completed' + + if (notExporting) return h('div') + + if (exportRequested) { + var warning = `Export private keys at your own risk.` + return ( + h('div', { + style: { + display: 'inline-block', + textAlign: 'center', + }, + }, + [ + h('div', { + key: 'exporting', + style: { + margin: '0 20px', + }, + }, [ + h('p.error', warning), + h('input#exportAccount.sizing-input', { + type: 'password', + placeholder: 'confirm password', + onKeyPress: this.onExportKeyPress.bind(this), + style: { + position: 'relative', + top: '1.5px', + marginBottom: '7px', + }, + }), + ]), + h('div', { + key: 'buttons', + style: { + margin: '0 20px', + }, + }, + [ + h('button', { + onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }), + style: { + marginRight: '10px', + }, + }, 'Submit'), + h('button', { + onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), + }, 'Cancel'), + ]), + (this.props.warning) && ( + h('span.error', { + style: { + margin: '20px', + }, + }, this.props.warning.split('-')) + ), + ]) + ) + } + + if (accountExported) { + return h('div.privateKey', { + style: { + margin: '0 20px', + }, + }, [ + h('label', 'Your private key (click to copy):'), + h('p.error.cursor-pointer', { + style: { + textOverflow: 'ellipsis', + overflow: 'hidden', + webkitUserSelect: 'text', + width: '100%', + }, + onClick: function (event) { + copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey)) + }, + }, ethUtil.stripHexPrefix(accountDetail.privateKey)), + h('button', { + onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), + }, 'Done'), + ]) + } +} + +ExportAccountView.prototype.onExportKeyPress = function (event) { + if (event.key !== 'Enter') return + event.preventDefault() + + var input = document.getElementById('exportAccount').value + this.props.dispatch(actions.exportAccount(input, this.props.address)) +} diff --git a/ui/classic/app/components/account-info-link.js b/ui/classic/app/components/account-info-link.js new file mode 100644 index 000000000..6526ab502 --- /dev/null +++ b/ui/classic/app/components/account-info-link.js @@ -0,0 +1,41 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Tooltip = require('./tooltip') +const genAccountLink = require('../../lib/account-link') + +module.exports = AccountInfoLink + +inherits(AccountInfoLink, Component) +function AccountInfoLink () { + Component.call(this) +} + +AccountInfoLink.prototype.render = function () { + const { selected, network } = this.props + const title = 'View account on Etherscan' + const url = genAccountLink(selected, network) + + if (!url) { + return null + } + + return h('.account-info-link', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + + h(Tooltip, { + title, + }, [ + h('i.fa.fa-info-circle.cursor-pointer.color-orange', { + style: { + margin: '5px', + }, + onClick () { global.platform.openWindow({ url }) }, + }), + ]), + ]) +} diff --git a/ui/classic/app/components/account-panel.js b/ui/classic/app/components/account-panel.js new file mode 100644 index 000000000..abaaf8163 --- /dev/null +++ b/ui/classic/app/components/account-panel.js @@ -0,0 +1,86 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const Identicon = require('./identicon') +const formatBalance = require('../util').formatBalance +const addressSummary = require('../util').addressSummary + +module.exports = AccountPanel + + +inherits(AccountPanel, Component) +function AccountPanel () { + Component.call(this) +} + +AccountPanel.prototype.render = function () { + var state = this.props + var identity = state.identity || {} + var account = state.account || {} + var isFauceting = state.isFauceting + + var panelState = { + key: `accountPanel${identity.address}`, + identiconKey: identity.address, + identiconLabel: identity.name || '', + attributes: [ + { + key: 'ADDRESS', + value: addressSummary(identity.address), + }, + balanceOrFaucetingIndication(account, isFauceting), + ], + } + + return ( + + h('.identity-panel.flex-row.flex-space-between', { + style: { + flex: '1 0 auto', + cursor: panelState.onClick ? 'pointer' : undefined, + }, + onClick: panelState.onClick, + }, [ + + // account identicon + h('.identicon-wrapper.flex-column.select-none', [ + h(Identicon, { + address: panelState.identiconKey, + imageify: state.imageifyIdenticons, + }), + h('span.font-small', panelState.identiconLabel.substring(0, 7) + '...'), + ]), + + // account address, balance + h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [ + + panelState.attributes.map((attr) => { + return h('.flex-row.flex-space-between', { + key: '' + Math.round(Math.random() * 1000000), + }, [ + h('label.font-small.no-select', attr.key), + h('span.font-small', attr.value), + ]) + }), + ]), + + ]) + + ) +} + +function balanceOrFaucetingIndication (account, isFauceting) { + // Temporarily deactivating isFauceting indication + // because it shows fauceting for empty restored accounts. + if (/* isFauceting*/ false) { + return { + key: 'Account is auto-funding.', + value: 'Please wait.', + } + } else { + return { + key: 'BALANCE', + value: formatBalance(account.balance), + } + } +} diff --git a/ui/classic/app/components/balance.js b/ui/classic/app/components/balance.js new file mode 100644 index 000000000..57ca84564 --- /dev/null +++ b/ui/classic/app/components/balance.js @@ -0,0 +1,89 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const formatBalance = require('../util').formatBalance +const generateBalanceObject = require('../util').generateBalanceObject +const Tooltip = require('./tooltip.js') +const FiatValue = require('./fiat-value.js') + +module.exports = EthBalanceComponent + +inherits(EthBalanceComponent, Component) +function EthBalanceComponent () { + Component.call(this) +} + +EthBalanceComponent.prototype.render = function () { + var props = this.props + let { value } = props + var style = props.style + var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true + value = value ? formatBalance(value, 6, needsParse) : '...' + var width = props.width + + return ( + + h('.ether-balance.ether-balance-amount', { + style: style, + }, [ + h('div', { + style: { + display: 'inline', + width: width, + }, + }, this.renderBalance(value)), + ]) + + ) +} +EthBalanceComponent.prototype.renderBalance = function (value) { + var props = this.props + if (value === 'None') return value + if (value === '...') return value + var balanceObj = generateBalanceObject(value, props.shorten ? 1 : 3) + var balance + var splitBalance = value.split(' ') + var ethNumber = splitBalance[0] + var ethSuffix = splitBalance[1] + const showFiat = 'showFiat' in props ? props.showFiat : true + + if (props.shorten) { + balance = balanceObj.shortBalance + } else { + balance = balanceObj.balance + } + + var label = balanceObj.label + + return ( + h(Tooltip, { + position: 'bottom', + title: `${ethNumber} ${ethSuffix}`, + }, h('div.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('div', { + style: { + width: '100%', + textAlign: 'right', + }, + }, this.props.incoming ? `+${balance}` : balance), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + }, + }, label), + ]), + + showFiat ? h(FiatValue, { value: props.value }) : null, + ])) + ) +} diff --git a/ui/classic/app/components/binary-renderer.js b/ui/classic/app/components/binary-renderer.js new file mode 100644 index 000000000..0b6a1f5c2 --- /dev/null +++ b/ui/classic/app/components/binary-renderer.js @@ -0,0 +1,46 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const extend = require('xtend') + +module.exports = BinaryRenderer + +inherits(BinaryRenderer, Component) +function BinaryRenderer () { + Component.call(this) +} + +BinaryRenderer.prototype.render = function () { + const props = this.props + const { value, style } = props + const text = this.hexToText(value) + + const defaultStyle = extend({ + width: '315px', + maxHeight: '210px', + resize: 'none', + border: 'none', + background: 'white', + padding: '3px', + }, style) + + return ( + h('textarea.font-small', { + readOnly: true, + style: defaultStyle, + defaultValue: text, + }) + ) +} + +BinaryRenderer.prototype.hexToText = function (hex) { + try { + const stripped = ethUtil.stripHexPrefix(hex) + const buff = Buffer.from(stripped, 'hex') + return buff.toString('utf8') + } catch (e) { + return hex + } +} + diff --git a/ui/classic/app/components/bn-as-decimal-input.js b/ui/classic/app/components/bn-as-decimal-input.js new file mode 100644 index 000000000..f3ace4720 --- /dev/null +++ b/ui/classic/app/components/bn-as-decimal-input.js @@ -0,0 +1,174 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const extend = require('xtend') + +module.exports = BnAsDecimalInput + +inherits(BnAsDecimalInput, Component) +function BnAsDecimalInput () { + this.state = { invalid: null } + Component.call(this) +} + +/* Bn as Decimal Input + * + * A component for allowing easy, decimal editing + * of a passed in bn string value. + * + * On change, calls back its `onChange` function parameter + * and passes it an updated bn string. + */ + +BnAsDecimalInput.prototype.render = function () { + const props = this.props + const state = this.state + + const { value, scale, precision, onChange, min, max } = props + + const suffix = props.suffix + const style = props.style + const valueString = value.toString(10) + const newValue = this.downsize(valueString, scale, precision) + + return ( + h('.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('input.hex-input', { + type: 'number', + step: 'any', + required: true, + min, + max, + style: extend({ + display: 'block', + textAlign: 'right', + backgroundColor: 'transparent', + border: '1px solid #bdbdbd', + + }, style), + value: newValue, + onBlur: (event) => { + this.updateValidity(event) + }, + onChange: (event) => { + this.updateValidity(event) + const value = (event.target.value === '') ? '' : event.target.value + + + const scaledNumber = this.upsize(value, scale, precision) + const precisionBN = new BN(scaledNumber, 10) + onChange(precisionBN, event.target.checkValidity()) + }, + onInvalid: (event) => { + const msg = this.constructWarning() + if (msg === state.invalid) { + return + } + this.setState({ invalid: msg }) + event.preventDefault() + return false + }, + }), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + marginRight: '6px', + width: '20px', + }, + }, suffix), + ]), + + state.invalid ? h('span.error', { + style: { + position: 'absolute', + right: '0px', + textAlign: 'right', + transform: 'translateY(26px)', + padding: '3px', + background: 'rgba(255,255,255,0.85)', + zIndex: '1', + textTransform: 'capitalize', + border: '2px solid #E20202', + }, + }, state.invalid) : null, + ]) + ) +} + +BnAsDecimalInput.prototype.setValid = function (message) { + this.setState({ invalid: null }) +} + +BnAsDecimalInput.prototype.updateValidity = function (event) { + const target = event.target + const value = this.props.value + const newValue = target.value + + if (value === newValue) { + return + } + + const valid = target.checkValidity() + + if (valid) { + this.setState({ invalid: null }) + } +} + +BnAsDecimalInput.prototype.constructWarning = function () { + const { name, min, max } = this.props + let message = name ? name + ' ' : '' + + if (min && max) { + message += `must be greater than or equal to ${min} and less than or equal to ${max}.` + } else if (min) { + message += `must be greater than or equal to ${min}.` + } else if (max) { + message += `must be less than or equal to ${max}.` + } else { + message += 'Invalid input.' + } + + return message +} + + +BnAsDecimalInput.prototype.downsize = function (number, scale, precision) { + // if there is no scaling, simply return the number + if (scale === 0) { + return Number(number) + } else { + // if the scale is the same as the precision, account for this edge case. + var decimals = (scale === precision) ? -1 : scale - precision + return Number(number.slice(0, -scale) + '.' + number.slice(-scale, decimals)) + } +} + +BnAsDecimalInput.prototype.upsize = function (number, scale, precision) { + var stringArray = number.toString().split('.') + var decimalLength = stringArray[1] ? stringArray[1].length : 0 + var newString = stringArray[0] + + // If there is scaling and decimal parts exist, integrate them in. + if ((scale !== 0) && (decimalLength !== 0)) { + newString += stringArray[1].slice(0, precision) + } + + // Add 0s to account for the upscaling. + for (var i = decimalLength; i < scale; i++) { + newString += '0' + } + return newString +} diff --git a/ui/classic/app/components/buy-button-subview.js b/ui/classic/app/components/buy-button-subview.js new file mode 100644 index 000000000..87084f92d --- /dev/null +++ b/ui/classic/app/components/buy-button-subview.js @@ -0,0 +1,197 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const actions = require('../actions') +const CoinbaseForm = require('./coinbase-form') +const ShapeshiftForm = require('./shapeshift-form') +const Loading = require('./loading') +const AccountPanel = require('./account-panel') +const RadioList = require('./custom-radio-list') + +module.exports = connect(mapStateToProps)(BuyButtonSubview) + +function mapStateToProps (state) { + return { + identity: state.appState.identity, + account: state.metamask.accounts[state.appState.buyView.buyAddress], + warning: state.appState.warning, + buyView: state.appState.buyView, + network: state.metamask.network, + provider: state.metamask.provider, + context: state.appState.currentView.context, + isSubLoading: state.appState.isSubLoading, + } +} + +inherits(BuyButtonSubview, Component) +function BuyButtonSubview () { + Component.call(this) +} + +BuyButtonSubview.prototype.render = function () { + const props = this.props + const isLoading = props.isSubLoading + + return ( + h('.buy-eth-section.flex-column', { + style: { + alignItems: 'center', + }, + }, [ + // back button + h('.flex-row', { + style: { + alignItems: 'center', + justifyContent: 'center', + }, + }, [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { + onClick: this.backButtonContext.bind(this), + style: { + position: 'absolute', + left: '10px', + }, + }), + h('h2.text-transform-uppercase.flex-center', { + style: { + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', + }, + }, 'Buy Eth'), + ]), + h('div', { + style: { + position: 'absolute', + top: '57vh', + left: '49vw', + }, + }, [ + h(Loading, {isLoading}), + ]), + h('div', { + style: { + width: '80%', + }, + }, [ + h(AccountPanel, { + showFullAddress: true, + identity: props.identity, + account: props.account, + }), + ]), + h('h3.text-transform-uppercase', { + style: { + paddingLeft: '15px', + fontFamily: 'Montserrat Light', + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', + }, + }, 'Select Service'), + h('.flex-row.selected-exchange', { + style: { + position: 'relative', + right: '35px', + marginTop: '20px', + marginBottom: '20px', + }, + }, [ + h(RadioList, { + defaultFocus: props.buyView.subview, + labels: [ + 'Coinbase', + 'ShapeShift', + ], + subtext: { + 'Coinbase': 'Crypto/FIAT (USA only)', + 'ShapeShift': 'Crypto', + }, + onClick: this.radioHandler.bind(this), + }), + ]), + h('h3.text-transform-uppercase', { + style: { + paddingLeft: '15px', + fontFamily: 'Montserrat Light', + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', + }, + }, props.buyView.subview), + this.formVersionSubview(), + ]) + ) +} + +BuyButtonSubview.prototype.formVersionSubview = function () { + const network = this.props.network + if (network === '1') { + if (this.props.buyView.formView.coinbase) { + return h(CoinbaseForm, this.props) + } else if (this.props.buyView.formView.shapeshift) { + return h(ShapeshiftForm, this.props) + } + } else { + return h('div.flex-column', { + style: { + alignItems: 'center', + margin: '50px', + }, + }, [ + h('h3.text-transform-uppercase', { + style: { + width: '225px', + marginBottom: '15px', + }, + }, 'In order to access this feature, please switch to the Main Network'), + ((network === '3') || (network === '4') || (network === '42')) ? h('h3.text-transform-uppercase', 'or go to the') : null, + (network === '3') ? h('button.text-transform-uppercase', { + onClick: () => this.props.dispatch(actions.buyEth({ network })), + style: { + marginTop: '15px', + }, + }, 'Ropsten Test Faucet') : null, + (network === '4') ? h('button.text-transform-uppercase', { + onClick: () => this.props.dispatch(actions.buyEth({ network })), + style: { + marginTop: '15px', + }, + }, 'Rinkeby Test Faucet') : null, + (network === '42') ? h('button.text-transform-uppercase', { + onClick: () => this.props.dispatch(actions.buyEth({ network })), + style: { + marginTop: '15px', + }, + }, 'Kovan Test Faucet') : null, + ]) + } +} + +BuyButtonSubview.prototype.navigateTo = function (url) { + global.platform.openWindow({ url }) +} + +BuyButtonSubview.prototype.backButtonContext = function () { + if (this.props.context === 'confTx') { + this.props.dispatch(actions.showConfTxPage(false)) + } else { + this.props.dispatch(actions.goHome()) + } +} + +BuyButtonSubview.prototype.radioHandler = function (event) { + switch (event.target.title) { + case 'Coinbase': + return this.props.dispatch(actions.coinBaseSubview()) + case 'ShapeShift': + return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type)) + } +} diff --git a/ui/classic/app/components/coinbase-form.js b/ui/classic/app/components/coinbase-form.js new file mode 100644 index 000000000..f44d86045 --- /dev/null +++ b/ui/classic/app/components/coinbase-form.js @@ -0,0 +1,63 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const actions = require('../actions') + +module.exports = connect(mapStateToProps)(CoinbaseForm) + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +inherits(CoinbaseForm, Component) + +function CoinbaseForm () { + Component.call(this) +} + +CoinbaseForm.prototype.render = function () { + var props = this.props + + return h('.flex-column', { + style: { + marginTop: '35px', + padding: '25px', + width: '100%', + }, + }, [ + h('.flex-row', { + style: { + justifyContent: 'space-around', + margin: '33px', + marginTop: '0px', + }, + }, [ + h('button.btn-green', { + onClick: this.toCoinbase.bind(this), + }, 'Continue to Coinbase'), + + h('button.btn-red', { + onClick: () => props.dispatch(actions.backTobuyView(props.accounts.address)), + }, 'Cancel'), + ]), + ]) +} + +CoinbaseForm.prototype.toCoinbase = function () { + const props = this.props + const address = props.buyView.buyAddress + props.dispatch(actions.buyEth({ network: '1', address, amount: 0 })) +} + +CoinbaseForm.prototype.renderLoading = function () { + return h('img', { + style: { + width: '27px', + marginRight: '-27px', + }, + src: 'images/loading.svg', + }) +} diff --git a/ui/classic/app/components/copyButton.js b/ui/classic/app/components/copyButton.js new file mode 100644 index 000000000..a25d0719c --- /dev/null +++ b/ui/classic/app/components/copyButton.js @@ -0,0 +1,59 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const copyToClipboard = require('copy-to-clipboard') + +const Tooltip = require('./tooltip') + +module.exports = CopyButton + +inherits(CopyButton, Component) +function CopyButton () { + Component.call(this) +} + +// As parameters, accepts: +// "value", which is the value to copy (mandatory) +// "title", which is the text to show on hover (optional, defaults to 'Copy') +CopyButton.prototype.render = function () { + const props = this.props + const state = this.state || {} + + const value = props.value + const copied = state.copied + + const message = copied ? 'Copied' : props.title || ' Copy ' + + return h('.copy-button', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + + h(Tooltip, { + title: message, + }, [ + h('i.fa.fa-clipboard.cursor-pointer.color-orange', { + style: { + margin: '5px', + }, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(value) + this.debounceRestore() + }, + }), + ]), + + ]) +} + +CopyButton.prototype.debounceRestore = function () { + this.setState({ copied: true }) + clearTimeout(this.timeout) + this.timeout = setTimeout(() => { + this.setState({ copied: false }) + }, 850) +} diff --git a/ui/classic/app/components/copyable.js b/ui/classic/app/components/copyable.js new file mode 100644 index 000000000..a4f6f4bc6 --- /dev/null +++ b/ui/classic/app/components/copyable.js @@ -0,0 +1,46 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const Tooltip = require('./tooltip') +const copyToClipboard = require('copy-to-clipboard') + +module.exports = Copyable + +inherits(Copyable, Component) +function Copyable () { + Component.call(this) + this.state = { + copied: false, + } +} + +Copyable.prototype.render = function () { + const props = this.props + const state = this.state + const { value, children } = props + const { copied } = state + + return h(Tooltip, { + title: copied ? 'Copied!' : 'Copy', + position: 'bottom', + }, h('span', { + style: { + cursor: 'pointer', + }, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(value) + this.debounceRestore() + }, + }, children)) +} + +Copyable.prototype.debounceRestore = function () { + this.setState({ copied: true }) + clearTimeout(this.timeout) + this.timeout = setTimeout(() => { + this.setState({ copied: false }) + }, 850) +} diff --git a/ui/classic/app/components/custom-radio-list.js b/ui/classic/app/components/custom-radio-list.js new file mode 100644 index 000000000..a4c525396 --- /dev/null +++ b/ui/classic/app/components/custom-radio-list.js @@ -0,0 +1,60 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = RadioList + +inherits(RadioList, Component) +function RadioList () { + Component.call(this) +} + +RadioList.prototype.render = function () { + const props = this.props + const activeClass = '.custom-radio-selected' + const inactiveClass = '.custom-radio-inactive' + const { + labels, + defaultFocus, + } = props + + + return ( + h('.flex-row', { + style: { + fontSize: '12px', + }, + }, [ + h('.flex-column.custom-radios', { + style: { + marginRight: '5px', + }, + }, + labels.map((lable, i) => { + let isSelcted = (this.state !== null) + isSelcted = isSelcted ? (this.state.selected === lable) : (defaultFocus === lable) + return h(isSelcted ? activeClass : inactiveClass, { + title: lable, + onClick: (event) => { + this.setState({selected: event.target.title}) + props.onClick(event) + }, + }) + }) + ), + h('.text', {}, + labels.map((lable) => { + if (props.subtext) { + return h('.flex-row', {}, [ + h('.radio-titles', lable), + h('.radio-titles-subtext', `- ${props.subtext[lable]}`), + ]) + } else { + return h('.radio-titles', lable) + } + }) + ), + ]) + ) +} + diff --git a/ui/classic/app/components/drop-menu-item.js b/ui/classic/app/components/drop-menu-item.js new file mode 100644 index 000000000..e42948209 --- /dev/null +++ b/ui/classic/app/components/drop-menu-item.js @@ -0,0 +1,59 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = DropMenuItem + +inherits(DropMenuItem, Component) +function DropMenuItem () { + Component.call(this) +} + +DropMenuItem.prototype.render = function () { + return h('li.drop-menu-item', { + onClick: () => { + this.props.closeMenu() + this.props.action() + }, + style: { + listStyle: 'none', + padding: '6px 16px 6px 5px', + fontFamily: 'Montserrat Regular', + color: 'rgb(125, 128, 130)', + cursor: 'pointer', + display: 'flex', + justifyContent: 'flex-start', + }, + }, [ + this.props.icon, + this.props.label, + this.activeNetworkRender(), + ]) +} + +DropMenuItem.prototype.activeNetworkRender = function () { + const activeNetwork = this.props.activeNetworkRender + const { provider } = this.props + const providerType = provider ? provider.type : null + if (activeNetwork === undefined) return + + switch (this.props.label) { + case 'Main Ethereum Network': + if (providerType === 'mainnet') return h('.check', '✓') + break + case 'Ropsten Test Network': + if (providerType === 'ropsten') return h('.check', '✓') + break + case 'Kovan Test Network': + if (providerType === 'kovan') return h('.check', '✓') + break + case 'Rinkeby Test Network': + if (providerType === 'rinkeby') return h('.check', '✓') + break + case 'Localhost 8545': + if (activeNetwork === 'http://localhost:8545') return h('.check', '✓') + break + default: + if (activeNetwork === 'custom') return h('.check', '✓') + } +} diff --git a/ui/classic/app/components/editable-label.js b/ui/classic/app/components/editable-label.js new file mode 100644 index 000000000..41936f5e0 --- /dev/null +++ b/ui/classic/app/components/editable-label.js @@ -0,0 +1,51 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const findDOMNode = require('react-dom').findDOMNode + +module.exports = EditableLabel + +inherits(EditableLabel, Component) +function EditableLabel () { + Component.call(this) +} + +EditableLabel.prototype.render = function () { + const props = this.props + const state = this.state + + if (state && state.isEditingLabel) { + return h('div.editable-label', [ + h('input.sizing-input', { + defaultValue: props.textValue, + maxLength: '20', + onKeyPress: (event) => { + this.saveIfEnter(event) + }, + }), + h('button.editable-button', { + onClick: () => this.saveText(), + }, 'Save'), + ]) + } else { + return h('div.name-label', { + onClick: (event) => { + this.setState({ isEditingLabel: true }) + }, + }, this.props.children) + } +} + +EditableLabel.prototype.saveIfEnter = function (event) { + if (event.key === 'Enter') { + this.saveText() + } +} + +EditableLabel.prototype.saveText = function () { + var container = findDOMNode(this) + var text = container.querySelector('.editable-label input').value + var truncatedText = text.substring(0, 20) + this.props.saveText(truncatedText) + this.setState({ isEditingLabel: false, textLabel: truncatedText }) +} diff --git a/ui/classic/app/components/ens-input.js b/ui/classic/app/components/ens-input.js new file mode 100644 index 000000000..3a33ebf74 --- /dev/null +++ b/ui/classic/app/components/ens-input.js @@ -0,0 +1,170 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const extend = require('xtend') +const debounce = require('debounce') +const copyToClipboard = require('copy-to-clipboard') +const ENS = require('ethjs-ens') +const networkMap = require('ethjs-ens/lib/network-map.json') +const ensRE = /.+\.eth$/ +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + + +module.exports = EnsInput + +inherits(EnsInput, Component) +function EnsInput () { + Component.call(this) +} + +EnsInput.prototype.render = function () { + const props = this.props + const opts = extend(props, { + list: 'addresses', + onChange: () => { + const network = this.props.network + const networkHasEnsSupport = getNetworkEnsSupport(network) + if (!networkHasEnsSupport) return + + const recipient = document.querySelector('input[name="address"]').value + if (recipient.match(ensRE) === null) { + return this.setState({ + loadingEns: false, + ensResolution: null, + ensFailure: null, + }) + } + + this.setState({ + loadingEns: true, + }) + this.checkName() + }, + }) + return h('div', { + style: { width: '100%' }, + }, [ + h('input.large-input', opts), + // The address book functionality. + h('datalist#addresses', + [ + // Corresponds to the addresses owned. + Object.keys(props.identities).map((key) => { + const identity = props.identities[key] + return h('option', { + value: identity.address, + label: identity.name, + key: identity.address, + }) + }), + // Corresponds to previously sent-to addresses. + props.addressBook.map((identity) => { + return h('option', { + value: identity.address, + label: identity.name, + key: identity.address, + }) + }), + ]), + this.ensIcon(), + ]) +} + +EnsInput.prototype.componentDidMount = function () { + const network = this.props.network + const networkHasEnsSupport = getNetworkEnsSupport(network) + this.setState({ ensResolution: ZERO_ADDRESS }) + + if (networkHasEnsSupport) { + const provider = global.ethereumProvider + this.ens = new ENS({ provider, network }) + this.checkName = debounce(this.lookupEnsName.bind(this), 200) + } +} + +EnsInput.prototype.lookupEnsName = function () { + const recipient = document.querySelector('input[name="address"]').value + const { ensResolution } = this.state + + log.info(`ENS attempting to resolve name: ${recipient}`) + this.ens.lookup(recipient.trim()) + .then((address) => { + if (address === ZERO_ADDRESS) throw new Error('No address has been set for this name.') + if (address !== ensResolution) { + this.setState({ + loadingEns: false, + ensResolution: address, + nickname: recipient.trim(), + hoverText: address + '\nClick to Copy', + ensFailure: false, + }) + } + }) + .catch((reason) => { + log.error(reason) + return this.setState({ + loadingEns: false, + ensResolution: ZERO_ADDRESS, + ensFailure: true, + hoverText: reason.message, + }) + }) +} + +EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) { + const state = this.state || {} + const ensResolution = state.ensResolution + // If an address is sent without a nickname, meaning not from ENS or from + // the user's own accounts, a default of a one-space string is used. + const nickname = state.nickname || ' ' + if (prevState && ensResolution && this.props.onChange && + ensResolution !== prevState.ensResolution) { + this.props.onChange(ensResolution, nickname) + } +} + +EnsInput.prototype.ensIcon = function (recipient) { + const { hoverText } = this.state || {} + return h('span', { + title: hoverText, + style: { + position: 'absolute', + padding: '9px', + transform: 'translatex(-40px)', + }, + }, this.ensIconContents(recipient)) +} + +EnsInput.prototype.ensIconContents = function (recipient) { + const { loadingEns, ensFailure, ensResolution } = this.state || { ensResolution: ZERO_ADDRESS} + + if (loadingEns) { + return h('img', { + src: 'images/loading.svg', + style: { + width: '30px', + height: '30px', + transform: 'translateY(-6px)', + }, + }) + } + + if (ensFailure) { + return h('i.fa.fa-warning.fa-lg.warning') + } + + if (ensResolution && (ensResolution !== ZERO_ADDRESS)) { + return h('i.fa.fa-check-circle.fa-lg.cursor-pointer', { + style: { color: 'green' }, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(ensResolution) + }, + }) + } +} + +function getNetworkEnsSupport (network) { + return Boolean(networkMap[network]) +} diff --git a/ui/classic/app/components/eth-balance.js b/ui/classic/app/components/eth-balance.js new file mode 100644 index 000000000..4f538fd31 --- /dev/null +++ b/ui/classic/app/components/eth-balance.js @@ -0,0 +1,89 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const formatBalance = require('../util').formatBalance +const generateBalanceObject = require('../util').generateBalanceObject +const Tooltip = require('./tooltip.js') +const FiatValue = require('./fiat-value.js') + +module.exports = EthBalanceComponent + +inherits(EthBalanceComponent, Component) +function EthBalanceComponent () { + Component.call(this) +} + +EthBalanceComponent.prototype.render = function () { + var props = this.props + let { value } = props + const { style, width } = props + var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true + value = value ? formatBalance(value, 6, needsParse) : '...' + + return ( + + h('.ether-balance.ether-balance-amount', { + style, + }, [ + h('div', { + style: { + display: 'inline', + width, + }, + }, this.renderBalance(value)), + ]) + + ) +} +EthBalanceComponent.prototype.renderBalance = function (value) { + var props = this.props + const { conversionRate, shorten, incoming, currentCurrency } = props + if (value === 'None') return value + if (value === '...') return value + var balanceObj = generateBalanceObject(value, shorten ? 1 : 3) + var balance + var splitBalance = value.split(' ') + var ethNumber = splitBalance[0] + var ethSuffix = splitBalance[1] + const showFiat = 'showFiat' in props ? props.showFiat : true + + if (shorten) { + balance = balanceObj.shortBalance + } else { + balance = balanceObj.balance + } + + var label = balanceObj.label + + return ( + h(Tooltip, { + position: 'bottom', + title: `${ethNumber} ${ethSuffix}`, + }, h('div.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('div', { + style: { + width: '100%', + textAlign: 'right', + }, + }, incoming ? `+${balance}` : balance), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + }, + }, label), + ]), + + showFiat ? h(FiatValue, { value: props.value, conversionRate, currentCurrency }) : null, + ])) + ) +} diff --git a/ui/classic/app/components/fiat-value.js b/ui/classic/app/components/fiat-value.js new file mode 100644 index 000000000..8a64a1cfc --- /dev/null +++ b/ui/classic/app/components/fiat-value.js @@ -0,0 +1,63 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const formatBalance = require('../util').formatBalance + +module.exports = FiatValue + +inherits(FiatValue, Component) +function FiatValue () { + Component.call(this) +} + +FiatValue.prototype.render = function () { + const props = this.props + const { conversionRate, currentCurrency } = props + + const value = formatBalance(props.value, 6) + + if (value === 'None') return value + var fiatDisplayNumber, fiatTooltipNumber + var splitBalance = value.split(' ') + + if (conversionRate !== 0) { + fiatTooltipNumber = Number(splitBalance[0]) * conversionRate + fiatDisplayNumber = fiatTooltipNumber.toFixed(2) + } else { + fiatDisplayNumber = 'N/A' + fiatTooltipNumber = 'Unknown' + } + + return fiatDisplay(fiatDisplayNumber, currentCurrency) +} + +function fiatDisplay (fiatDisplayNumber, fiatSuffix) { + if (fiatDisplayNumber !== 'N/A') { + return h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('div', { + style: { + width: '100%', + textAlign: 'right', + fontSize: '12px', + color: '#333333', + }, + }, fiatDisplayNumber), + h('div', { + style: { + color: '#AEAEAE', + marginLeft: '5px', + fontSize: '12px', + }, + }, fiatSuffix), + ]) + } else { + return h('div') + } +} diff --git a/ui/classic/app/components/hex-as-decimal-input.js b/ui/classic/app/components/hex-as-decimal-input.js new file mode 100644 index 000000000..4a71e9585 --- /dev/null +++ b/ui/classic/app/components/hex-as-decimal-input.js @@ -0,0 +1,154 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const extend = require('xtend') + +module.exports = HexAsDecimalInput + +inherits(HexAsDecimalInput, Component) +function HexAsDecimalInput () { + this.state = { invalid: null } + Component.call(this) +} + +/* Hex as Decimal Input + * + * A component for allowing easy, decimal editing + * of a passed in hex string value. + * + * On change, calls back its `onChange` function parameter + * and passes it an updated hex string. + */ + +HexAsDecimalInput.prototype.render = function () { + const props = this.props + const state = this.state + + const { value, onChange, min, max } = props + + const toEth = props.toEth + const suffix = props.suffix + const decimalValue = decimalize(value, toEth) + const style = props.style + + return ( + h('.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('input.hex-input', { + type: 'number', + required: true, + min: min, + max: max, + style: extend({ + display: 'block', + textAlign: 'right', + backgroundColor: 'transparent', + border: '1px solid #bdbdbd', + + }, style), + value: parseInt(decimalValue), + onBlur: (event) => { + this.updateValidity(event) + }, + onChange: (event) => { + this.updateValidity(event) + const hexString = (event.target.value === '') ? '' : hexify(event.target.value) + onChange(hexString) + }, + onInvalid: (event) => { + const msg = this.constructWarning() + if (msg === state.invalid) { + return + } + this.setState({ invalid: msg }) + event.preventDefault() + return false + }, + }), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + marginRight: '6px', + width: '20px', + }, + }, suffix), + ]), + + state.invalid ? h('span.error', { + style: { + position: 'absolute', + right: '0px', + textAlign: 'right', + transform: 'translateY(26px)', + padding: '3px', + background: 'rgba(255,255,255,0.85)', + zIndex: '1', + textTransform: 'capitalize', + border: '2px solid #E20202', + }, + }, state.invalid) : null, + ]) + ) +} + +HexAsDecimalInput.prototype.setValid = function (message) { + this.setState({ invalid: null }) +} + +HexAsDecimalInput.prototype.updateValidity = function (event) { + const target = event.target + const value = this.props.value + const newValue = target.value + + if (value === newValue) { + return + } + + const valid = target.checkValidity() + if (valid) { + this.setState({ invalid: null }) + } +} + +HexAsDecimalInput.prototype.constructWarning = function () { + const { name, min, max } = this.props + let message = name ? name + ' ' : '' + + if (min && max) { + message += `must be greater than or equal to ${min} and less than or equal to ${max}.` + } else if (min) { + message += `must be greater than or equal to ${min}.` + } else if (max) { + message += `must be less than or equal to ${max}.` + } else { + message += 'Invalid input.' + } + + return message +} + +function hexify (decimalString) { + const hexBN = new BN(parseInt(decimalString), 10) + return '0x' + hexBN.toString('hex') +} + +function decimalize (input, toEth) { + if (input === '') { + return '' + } else { + const strippedInput = ethUtil.stripHexPrefix(input) + const inputBN = new BN(strippedInput, 'hex') + return inputBN.toString(10) + } +} diff --git a/ui/classic/app/components/identicon.js b/ui/classic/app/components/identicon.js new file mode 100644 index 000000000..c754bc6ba --- /dev/null +++ b/ui/classic/app/components/identicon.js @@ -0,0 +1,72 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const isNode = require('detect-node') +const findDOMNode = require('react-dom').findDOMNode +const jazzicon = require('jazzicon') +const iconFactoryGen = require('../../lib/icon-factory') +const iconFactory = iconFactoryGen(jazzicon) + +module.exports = IdenticonComponent + +inherits(IdenticonComponent, Component) +function IdenticonComponent () { + Component.call(this) + + this.defaultDiameter = 46 +} + +IdenticonComponent.prototype.render = function () { + var props = this.props + var diameter = props.diameter || this.defaultDiameter + return ( + h('div', { + key: 'identicon-' + this.props.address, + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: diameter, + width: diameter, + borderRadius: diameter / 2, + overflow: 'hidden', + }, + }) + ) +} + +IdenticonComponent.prototype.componentDidMount = function () { + var props = this.props + const { address } = props + + if (!address) return + + var container = findDOMNode(this) + + var diameter = props.diameter || this.defaultDiameter + if (!isNode) { + var img = iconFactory.iconForAddress(address, diameter) + container.appendChild(img) + } +} + +IdenticonComponent.prototype.componentDidUpdate = function () { + var props = this.props + const { address } = props + + if (!address) return + + var container = findDOMNode(this) + + var children = container.children + for (var i = 0; i < children.length; i++) { + container.removeChild(children[i]) + } + + var diameter = props.diameter || this.defaultDiameter + if (!isNode) { + var img = iconFactory.iconForAddress(address, diameter) + container.appendChild(img) + } +} + diff --git a/ui/classic/app/components/loading.js b/ui/classic/app/components/loading.js new file mode 100644 index 000000000..87d6f5d20 --- /dev/null +++ b/ui/classic/app/components/loading.js @@ -0,0 +1,53 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') + + +inherits(LoadingIndicator, Component) +module.exports = LoadingIndicator + +function LoadingIndicator () { + Component.call(this) +} + +LoadingIndicator.prototype.render = function () { + const { isLoading, loadingMessage } = this.props + + return ( + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'loader', + transitionEnterTimeout: 150, + transitionLeaveTimeout: 150, + }, [ + + isLoading ? h('div', { + style: { + zIndex: 10, + position: 'absolute', + flexDirection: 'column', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '100%', + width: '100%', + background: 'rgba(255, 255, 255, 0.8)', + }, + }, [ + h('img', { + src: 'images/loading.svg', + }), + + h('br'), + + showMessageIfAny(loadingMessage), + ]) : null, + ]) + ) +} + +function showMessageIfAny (loadingMessage) { + if (!loadingMessage) return null + return h('span', loadingMessage) +} diff --git a/ui/classic/app/components/mascot.js b/ui/classic/app/components/mascot.js new file mode 100644 index 000000000..973ec2cad --- /dev/null +++ b/ui/classic/app/components/mascot.js @@ -0,0 +1,59 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const metamaskLogo = require('metamask-logo') +const debounce = require('debounce') + +module.exports = Mascot + +inherits(Mascot, Component) +function Mascot () { + Component.call(this) + this.logo = metamaskLogo({ + followMouse: true, + pxNotRatio: true, + width: 200, + height: 200, + }) + + this.refollowMouse = debounce(this.logo.setFollowMouse.bind(this.logo, true), 1000) + this.unfollowMouse = this.logo.setFollowMouse.bind(this.logo, false) +} + +Mascot.prototype.render = function () { + // this is a bit hacky + // the event emitter is on `this.props` + // and we dont get that until render + this.handleAnimationEvents() + + return h('#metamask-mascot-container', { + style: { zIndex: 0 }, + }) +} + +Mascot.prototype.componentDidMount = function () { + var targetDivId = 'metamask-mascot-container' + var container = document.getElementById(targetDivId) + container.appendChild(this.logo.container) +} + +Mascot.prototype.componentWillUnmount = function () { + this.animations = this.props.animationEventEmitter + this.animations.removeAllListeners() + this.logo.container.remove() + this.logo.stopAnimation() +} + +Mascot.prototype.handleAnimationEvents = function () { + // only setup listeners once + if (this.animations) return + this.animations = this.props.animationEventEmitter + this.animations.on('point', this.lookAt.bind(this)) + this.animations.on('setFollowMouse', this.logo.setFollowMouse.bind(this.logo)) +} + +Mascot.prototype.lookAt = function (target) { + this.unfollowMouse() + this.logo.lookAt(target) + this.refollowMouse() +} diff --git a/ui/classic/app/components/mini-account-panel.js b/ui/classic/app/components/mini-account-panel.js new file mode 100644 index 000000000..c09cf5b7a --- /dev/null +++ b/ui/classic/app/components/mini-account-panel.js @@ -0,0 +1,74 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const Identicon = require('./identicon') + +module.exports = AccountPanel + + +inherits(AccountPanel, Component) +function AccountPanel () { + Component.call(this) +} + +AccountPanel.prototype.render = function () { + var props = this.props + var picOrder = props.picOrder || 'left' + const { imageSeed } = props + + return ( + + h('.identity-panel.flex-row.flex-left', { + style: { + cursor: props.onClick ? 'pointer' : undefined, + }, + onClick: props.onClick, + }, [ + + this.genIcon(imageSeed, picOrder), + + h('div.flex-column.flex-justify-center', { + style: { + lineHeight: '15px', + order: 2, + display: 'flex', + alignItems: picOrder === 'left' ? 'flex-begin' : 'flex-end', + }, + }, this.props.children), + ]) + ) +} + +AccountPanel.prototype.genIcon = function (seed, picOrder) { + const props = this.props + + // When there is no seed value, this is a contract creation. + // We then show the contract icon. + if (!seed) { + return h('.identicon-wrapper.flex-column.select-none', { + style: { + order: picOrder === 'left' ? 1 : 3, + }, + }, [ + h('i.fa.fa-file-text-o.fa-lg', { + style: { + fontSize: '42px', + transform: 'translate(0px, -16px)', + }, + }), + ]) + } + + // If there was a seed, we return an identicon for that address. + return h('.identicon-wrapper.flex-column.select-none', { + style: { + order: picOrder === 'left' ? 1 : 3, + }, + }, [ + h(Identicon, { + address: seed, + imageify: props.imageifyIdenticons, + }), + ]) +} + diff --git a/ui/classic/app/components/network.js b/ui/classic/app/components/network.js new file mode 100644 index 000000000..d5d3e18cd --- /dev/null +++ b/ui/classic/app/components/network.js @@ -0,0 +1,125 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = Network + +inherits(Network, Component) + +function Network () { + Component.call(this) +} + +Network.prototype.render = function () { + const props = this.props + const networkNumber = props.network + let providerName + try { + providerName = props.provider.type + } catch (e) { + providerName = null + } + let iconName, hoverText + + if (networkNumber === 'loading') { + return h('span', { + style: { + display: 'flex', + alignItems: 'center', + flexDirection: 'row', + }, + onClick: (event) => this.props.onClick(event), + }, [ + h('img', { + title: 'Attempting to connect to blockchain.', + style: { + width: '27px', + }, + src: 'images/loading.svg', + }), + h('i.fa.fa-sort-desc'), + ]) + + } else if (providerName === 'mainnet') { + hoverText = 'Main Ethereum Network' + iconName = 'ethereum-network' + } else if (providerName === 'ropsten') { + hoverText = 'Ropsten Test Network' + iconName = 'ropsten-test-network' + } else if (parseInt(networkNumber) === 3) { + hoverText = 'Ropsten Test Network' + iconName = 'ropsten-test-network' + } else if (providerName === 'kovan') { + hoverText = 'Kovan Test Network' + iconName = 'kovan-test-network' + } else if (providerName === 'rinkeby') { + hoverText = 'Rinkeby Test Network' + iconName = 'rinkeby-test-network' + } else { + hoverText = 'Unknown Private Network' + iconName = 'unknown-private-network' + } + + return ( + h('#network_component.pointer', { + title: hoverText, + onClick: (event) => this.props.onClick(event), + }, [ + (function () { + switch (iconName) { + case 'ethereum-network': + return h('.network-indicator', [ + h('.menu-icon.diamond'), + h('.network-name', { + style: { + color: '#039396', + }}, + 'Ethereum Main Net'), + ]) + case 'ropsten-test-network': + return h('.network-indicator', [ + h('.menu-icon.red-dot'), + h('.network-name', { + style: { + color: '#ff6666', + }}, + 'Ropsten Test Net'), + ]) + case 'kovan-test-network': + return h('.network-indicator', [ + h('.menu-icon.hollow-diamond'), + h('.network-name', { + style: { + color: '#690496', + }}, + 'Kovan Test Net'), + ]) + case 'rinkeby-test-network': + return h('.network-indicator', [ + h('.menu-icon.golden-square'), + h('.network-name', { + style: { + color: '#e7a218', + }}, + 'Rinkeby Test Net'), + ]) + default: + return h('.network-indicator', [ + h('i.fa.fa-question-circle.fa-lg', { + style: { + margin: '10px', + color: 'rgb(125, 128, 130)', + }, + }), + + h('.network-name', { + style: { + color: '#AEAEAE', + }}, + 'Private Network'), + ]) + } + })(), + ]) + ) +} diff --git a/ui/classic/app/components/notice.js b/ui/classic/app/components/notice.js new file mode 100644 index 000000000..d9f0067cd --- /dev/null +++ b/ui/classic/app/components/notice.js @@ -0,0 +1,126 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const ReactMarkdown = require('react-markdown') +const linker = require('extension-link-enabler') +const findDOMNode = require('react-dom').findDOMNode + +module.exports = Notice + +inherits(Notice, Component) +function Notice () { + Component.call(this) +} + +Notice.prototype.render = function () { + const { notice, onConfirm } = this.props + const { title, date, body } = notice + const state = this.state || { disclaimerDisabled: true } + const disabled = state.disclaimerDisabled + + return ( + h('.flex-column.flex-center.flex-grow', [ + h('h3.flex-center.text-transform-uppercase.terms-header', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + width: '100%', + fontSize: '20px', + textAlign: 'center', + padding: 6, + }, + }, [ + title, + ]), + + h('h5.flex-center.text-transform-uppercase.terms-header', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginBottom: 24, + width: '100%', + fontSize: '20px', + textAlign: 'center', + padding: 6, + }, + }, [ + date, + ]), + + h('style', ` + + .markdown { + overflow-x: hidden; + } + + .markdown h1, .markdown h2, .markdown h3 { + margin: 10px 0; + font-weight: bold; + } + + .markdown strong { + font-weight: bold; + } + .markdown em { + font-style: italic; + } + + .markdown p { + margin: 10px 0; + } + + .markdown a { + color: #df6b0e; + } + + `), + + h('div.markdown', { + onScroll: (e) => { + var object = e.currentTarget + if (object.offsetHeight + object.scrollTop + 100 >= object.scrollHeight) { + this.setState({disclaimerDisabled: false}) + } + }, + style: { + background: 'rgb(235, 235, 235)', + height: '310px', + padding: '6px', + width: '90%', + overflowY: 'scroll', + scroll: 'auto', + }, + }, [ + h(ReactMarkdown, { + className: 'notice-box', + source: body, + skipHtml: true, + }), + ]), + + h('button', { + disabled, + onClick: () => { + this.setState({disclaimerDisabled: true}) + onConfirm() + }, + style: { + marginTop: '18px', + }, + }, 'Accept'), + ]) + ) +} + +Notice.prototype.componentDidMount = function () { + var node = findDOMNode(this) + linker.setupListener(node) + if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) { + this.setState({disclaimerDisabled: false}) + } +} + +Notice.prototype.componentWillUnmount = function () { + var node = findDOMNode(this) + linker.teardownListener(node) +} diff --git a/ui/classic/app/components/pending-msg-details.js b/ui/classic/app/components/pending-msg-details.js new file mode 100644 index 000000000..16308d121 --- /dev/null +++ b/ui/classic/app/components/pending-msg-details.js @@ -0,0 +1,50 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const AccountPanel = require('./account-panel') + +module.exports = PendingMsgDetails + +inherits(PendingMsgDetails, Component) +function PendingMsgDetails () { + Component.call(this) +} + +PendingMsgDetails.prototype.render = function () { + var state = this.props + var msgData = state.txData + + var msgParams = msgData.msgParams || {} + var address = msgParams.from || state.selectedAddress + var identity = state.identities[address] || { address: address } + var account = state.accounts[address] || { address: address } + + return ( + h('div', { + key: msgData.id, + style: { + margin: '10px 20px', + }, + }, [ + + // account that will sign + h(AccountPanel, { + showFullAddress: true, + identity: identity, + account: account, + imageifyIdenticons: state.imageifyIdenticons, + }), + + // message data + h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-row.flex-space-between', [ + h('label.font-small', 'MESSAGE'), + h('span.font-small', msgParams.data), + ]), + ]), + + ]) + ) +} + diff --git a/ui/classic/app/components/pending-msg.js b/ui/classic/app/components/pending-msg.js new file mode 100644 index 000000000..b2cac164a --- /dev/null +++ b/ui/classic/app/components/pending-msg.js @@ -0,0 +1,56 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const PendingTxDetails = require('./pending-msg-details') + +module.exports = PendingMsg + +inherits(PendingMsg, Component) +function PendingMsg () { + Component.call(this) +} + +PendingMsg.prototype.render = function () { + var state = this.props + var msgData = state.txData + + return ( + + h('div', { + key: msgData.id, + }, [ + + // header + h('h3', { + style: { + fontWeight: 'bold', + textAlign: 'center', + }, + }, 'Sign Message'), + + h('.error', { + style: { + margin: '10px', + }, + }, `Signing this message can have + dangerous side effects. Only sign messages from + sites you fully trust with your entire account. + This will be fixed in a future version.`), + + // message details + h(PendingTxDetails, state), + + // sign + cancel + h('.flex-row.flex-space-around', [ + h('button', { + onClick: state.cancelMessage, + }, 'Cancel'), + h('button', { + onClick: state.signMessage, + }, 'Sign'), + ]), + ]) + + ) +} + diff --git a/ui/classic/app/components/pending-personal-msg-details.js b/ui/classic/app/components/pending-personal-msg-details.js new file mode 100644 index 000000000..1050513f2 --- /dev/null +++ b/ui/classic/app/components/pending-personal-msg-details.js @@ -0,0 +1,60 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const AccountPanel = require('./account-panel') +const BinaryRenderer = require('./binary-renderer') + +module.exports = PendingMsgDetails + +inherits(PendingMsgDetails, Component) +function PendingMsgDetails () { + Component.call(this) +} + +PendingMsgDetails.prototype.render = function () { + var state = this.props + var msgData = state.txData + + var msgParams = msgData.msgParams || {} + var address = msgParams.from || state.selectedAddress + var identity = state.identities[address] || { address: address } + var account = state.accounts[address] || { address: address } + + var { data } = msgParams + + return ( + h('div', { + key: msgData.id, + style: { + margin: '10px 20px', + }, + }, [ + + // account that will sign + h(AccountPanel, { + showFullAddress: true, + identity: identity, + account: account, + imageifyIdenticons: state.imageifyIdenticons, + }), + + // message data + h('div', { + style: { + height: '260px', + }, + }, [ + h('label.font-small', { style: { display: 'block' } }, 'MESSAGE'), + h(BinaryRenderer, { + value: data, + style: { + height: '215px', + }, + }), + ]), + + ]) + ) +} + diff --git a/ui/classic/app/components/pending-personal-msg.js b/ui/classic/app/components/pending-personal-msg.js new file mode 100644 index 000000000..4542adb28 --- /dev/null +++ b/ui/classic/app/components/pending-personal-msg.js @@ -0,0 +1,47 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const PendingTxDetails = require('./pending-personal-msg-details') + +module.exports = PendingMsg + +inherits(PendingMsg, Component) +function PendingMsg () { + Component.call(this) +} + +PendingMsg.prototype.render = function () { + var state = this.props + var msgData = state.txData + + return ( + + h('div', { + key: msgData.id, + }, [ + + // header + h('h3', { + style: { + fontWeight: 'bold', + textAlign: 'center', + }, + }, 'Sign Message'), + + // message details + h(PendingTxDetails, state), + + // sign + cancel + h('.flex-row.flex-space-around', [ + h('button', { + onClick: state.cancelPersonalMessage, + }, 'Cancel'), + h('button', { + onClick: state.signPersonalMessage, + }, 'Sign'), + ]), + ]) + + ) +} + diff --git a/ui/classic/app/components/pending-tx.js b/ui/classic/app/components/pending-tx.js new file mode 100644 index 000000000..d7d602f31 --- /dev/null +++ b/ui/classic/app/components/pending-tx.js @@ -0,0 +1,480 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const actions = require('../actions') +const clone = require('clone') + +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const hexToBn = require('../../../app/scripts/lib/hex-to-bn') +const util = require('../util') +const MiniAccountPanel = require('./mini-account-panel') +const Copyable = require('./copyable') +const EthBalance = require('./eth-balance') +const addressSummary = util.addressSummary +const nameForAddress = require('../../lib/contract-namer') +const BNInput = require('./bn-as-decimal-input') + +const MIN_GAS_PRICE_GWEI_BN = new BN(2) +const GWEI_FACTOR = new BN(1e9) +const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) +const MIN_GAS_LIMIT_BN = new BN(21000) + +module.exports = PendingTx +inherits(PendingTx, Component) +function PendingTx () { + Component.call(this) + this.state = { + valid: true, + txData: null, + submitting: false, + } +} + +PendingTx.prototype.render = function () { + const props = this.props + const { currentCurrency, blockGasLimit } = props + + const conversionRate = props.conversionRate + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} + + // Account Details + const address = txParams.from || props.selectedAddress + const identity = props.identities[address] || { address: address } + const account = props.accounts[address] + const balance = account ? account.balance : '0x0' + + // recipient check + const isValidAddress = !txParams.to || util.isValidAddress(txParams.to) + + // Gas + const gas = txParams.gas + const gasBn = hexToBn(gas) + const gasLimit = new BN(parseInt(blockGasLimit)) + const safeGasLimit = this.bnMultiplyByFraction(gasLimit, 19, 20).toString(10) + + // Gas Price + const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) + const gasPriceBn = hexToBn(gasPrice) + + const txFeeBn = gasBn.mul(gasPriceBn) + const valueBn = hexToBn(txParams.value) + const maxCost = txFeeBn.add(valueBn) + + const dataLength = txParams.data ? (txParams.data.length - 2) / 2 : 0 + + const balanceBn = hexToBn(balance) + const insufficientBalance = balanceBn.lt(maxCost) + + this.inputs = [] + + return ( + + h('div', { + key: txMeta.id, + }, [ + + h('form#pending-tx-form', { + onSubmit: this.onSubmit.bind(this), + + }, [ + + // tx info + h('div', [ + + h('.flex-row.flex-center', { + style: { + maxWidth: '100%', + }, + }, [ + + h(MiniAccountPanel, { + imageSeed: address, + picOrder: 'right', + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, identity.name), + + h(Copyable, { + value: ethUtil.toChecksumAddress(address), + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, addressSummary(address, 6, 4, false)), + ]), + + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, [ + h(EthBalance, { + value: balance, + conversionRate, + currentCurrency, + inline: true, + labelColor: '#F7861C', + }), + ]), + ]), + + forwardCarrat(), + + this.miniAccountPanelForRecipient(), + ]), + + h('style', ` + .table-box { + margin: 7px 0px 0px 0px; + width: 100%; + } + .table-box .row { + margin: 0px; + background: rgb(236,236,236); + display: flex; + justify-content: space-between; + font-family: Montserrat Light, sans-serif; + font-size: 13px; + padding: 5px 25px; + } + .table-box .row .value { + font-family: Montserrat Regular; + } + `), + + h('.table-box', [ + + // Ether Value + // Currently not customizable, but easily modified + // in the way that gas and gasLimit currently are. + h('.row', [ + h('.cell.label', 'Amount'), + h(EthBalance, { value: txParams.value, currentCurrency, conversionRate }), + ]), + + // Gas Limit (customizable) + h('.cell.row', [ + h('.cell.label', 'Gas Limit'), + h('.cell.value', { + }, [ + h(BNInput, { + name: 'Gas Limit', + value: gasBn, + precision: 0, + scale: 0, + // The hard lower limit for gas. + min: MIN_GAS_LIMIT_BN.toString(10), + max: safeGasLimit, + suffix: 'UNITS', + style: { + position: 'relative', + top: '5px', + }, + onChange: this.gasLimitChanged.bind(this), + + ref: (hexInput) => { this.inputs.push(hexInput) }, + }), + ]), + ]), + + // Gas Price (customizable) + h('.cell.row', [ + h('.cell.label', 'Gas Price'), + h('.cell.value', { + }, [ + h(BNInput, { + name: 'Gas Price', + value: gasPriceBn, + precision: 9, + scale: 9, + suffix: 'GWEI', + min: MIN_GAS_PRICE_GWEI_BN.toString(10), + style: { + position: 'relative', + top: '5px', + }, + onChange: this.gasPriceChanged.bind(this), + ref: (hexInput) => { this.inputs.push(hexInput) }, + }), + ]), + ]), + + // Max Transaction Fee (calculated) + h('.cell.row', [ + h('.cell.label', 'Max Transaction Fee'), + h(EthBalance, { value: txFeeBn.toString(16), currentCurrency, conversionRate }), + ]), + + h('.cell.row', { + style: { + fontFamily: 'Montserrat Regular', + background: 'white', + padding: '10px 25px', + }, + }, [ + h('.cell.label', 'Max Total'), + h('.cell.value', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + h(EthBalance, { + value: maxCost.toString(16), + currentCurrency, + conversionRate, + inline: true, + labelColor: 'black', + fontSize: '16px', + }), + ]), + ]), + + // Data size row: + h('.cell.row', { + style: { + background: '#f7f7f7', + paddingBottom: '0px', + }, + }, [ + h('.cell.label'), + h('.cell.value', { + style: { + fontFamily: 'Montserrat Light', + fontSize: '11px', + }, + }, `Data included: ${dataLength} bytes`), + ]), + ]), // End of Table + + ]), + + h('style', ` + .conf-buttons button { + margin-left: 10px; + text-transform: uppercase; + } + `), + + txMeta.simulationFails ? + h('.error', { + style: { + marginLeft: 50, + fontSize: '0.9em', + }, + }, 'Transaction Error. Exception thrown in contract code.') + : null, + + !isValidAddress ? + h('.error', { + style: { + marginLeft: 50, + fontSize: '0.9em', + }, + }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.') + : null, + + insufficientBalance ? + h('span.error', { + style: { + marginLeft: 50, + fontSize: '0.9em', + }, + }, 'Insufficient balance for transaction') + : null, + + // send + cancel + h('.flex-row.flex-space-around.conf-buttons', { + style: { + display: 'flex', + justifyContent: 'flex-end', + margin: '14px 25px', + }, + }, [ + + + insufficientBalance ? + h('button.btn-green', { + onClick: props.buyEth, + }, 'Buy Ether') + : null, + + h('button', { + onClick: (event) => { + this.resetGasFields() + event.preventDefault() + }, + }, 'Reset'), + + // Accept Button + h('input.confirm.btn-green', { + type: 'submit', + value: 'SUBMIT', + style: { marginLeft: '10px' }, + disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, + }), + + h('button.cancel.btn-red', { + onClick: props.cancelTransaction, + }, 'Reject'), + ]), + ]), + ]) + ) +} + +PendingTx.prototype.miniAccountPanelForRecipient = function () { + const props = this.props + const txData = props.txData + const txParams = txData.txParams || {} + const isContractDeploy = !('to' in txParams) + + // If it's not a contract deploy, send to the account + if (!isContractDeploy) { + return h(MiniAccountPanel, { + imageSeed: txParams.to, + picOrder: 'left', + }, [ + + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, nameForAddress(txParams.to, props.identities)), + + h(Copyable, { + value: ethUtil.toChecksumAddress(txParams.to), + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, addressSummary(txParams.to, 6, 4, false)), + ]), + + ]) + } else { + return h(MiniAccountPanel, { + picOrder: 'left', + }, [ + + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, 'New Contract'), + + ]) + } +} + +PendingTx.prototype.gasPriceChanged = function (newBN, valid) { + log.info(`Gas price changed to: ${newBN.toString(10)}`) + const txMeta = this.gatherTxMeta() + txMeta.txParams.gasPrice = '0x' + newBN.toString('hex') + this.setState({ + txData: clone(txMeta), + valid, + }) +} + +PendingTx.prototype.gasLimitChanged = function (newBN, valid) { + log.info(`Gas limit changed to ${newBN.toString(10)}`) + const txMeta = this.gatherTxMeta() + txMeta.txParams.gas = '0x' + newBN.toString('hex') + this.setState({ + txData: clone(txMeta), + valid, + }) +} + +PendingTx.prototype.resetGasFields = function () { + log.debug(`pending-tx resetGasFields`) + + this.inputs.forEach((hexInput) => { + if (hexInput) { + hexInput.setValid() + } + }) + + this.setState({ + txData: null, + valid: true, + }) +} + +PendingTx.prototype.onSubmit = function (event) { + event.preventDefault() + const txMeta = this.gatherTxMeta() + const valid = this.checkValidity() + this.setState({ valid, submitting: true }) + if (valid && this.verifyGasParams()) { + this.props.sendTransaction(txMeta, event) + } else { + this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) + this.setState({ submitting: false }) + } +} + +PendingTx.prototype.checkValidity = function () { + const form = this.getFormEl() + const valid = form.checkValidity() + return valid +} + +PendingTx.prototype.getFormEl = function () { + const form = document.querySelector('form#pending-tx-form') + // Stub out form for unit tests: + if (!form) { + return { checkValidity () { return true } } + } + return form +} + +// After a customizable state value has been updated, +PendingTx.prototype.gatherTxMeta = function () { + log.debug(`pending-tx gatherTxMeta`) + const props = this.props + const state = this.state + const txData = clone(state.txData) || clone(props.txData) + + log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) + return txData +} + +PendingTx.prototype.verifyGasParams = function () { + // We call this in case the gas has not been modified at all + if (!this.state) { return true } + return ( + this._notZeroOrEmptyString(this.state.gas) && + this._notZeroOrEmptyString(this.state.gasPrice) + ) +} + +PendingTx.prototype._notZeroOrEmptyString = function (obj) { + return obj !== '' && obj !== '0x0' +} + +PendingTx.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) { + const numBN = new BN(numerator) + const denomBN = new BN(denominator) + return targetBN.mul(numBN).div(denomBN) +} + +function forwardCarrat () { + return ( + h('img', { + src: 'images/forward-carrat.svg', + style: { + padding: '5px 6px 0px 10px', + height: '37px', + }, + }) + ) +} diff --git a/ui/classic/app/components/qr-code.js b/ui/classic/app/components/qr-code.js new file mode 100644 index 000000000..06b9aed9b --- /dev/null +++ b/ui/classic/app/components/qr-code.js @@ -0,0 +1,79 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const qrCode = require('qrcode-npm').qrcode +const inherits = require('util').inherits +const connect = require('react-redux').connect +const isHexPrefixed = require('ethereumjs-util').isHexPrefixed +const CopyButton = require('./copyButton') + +module.exports = connect(mapStateToProps)(QrCodeView) + +function mapStateToProps (state) { + return { + Qr: state.appState.Qr, + buyView: state.appState.buyView, + warning: state.appState.warning, + } +} + +inherits(QrCodeView, Component) + +function QrCodeView () { + Component.call(this) +} + +QrCodeView.prototype.render = function () { + const props = this.props + const Qr = props.Qr + const address = `${isHexPrefixed(Qr.data) ? 'ethereum:' : ''}${Qr.data}` + const qrImage = qrCode(4, 'M') + qrImage.addData(address) + qrImage.make() + return h('.main-container.flex-column', { + key: 'qr', + style: { + justifyContent: 'center', + paddingBottom: '45px', + paddingLeft: '45px', + paddingRight: '45px', + alignItems: 'center', + }, + }, [ + Array.isArray(Qr.message) ? h('.message-container', this.renderMultiMessage()) : h('.qr-header', Qr.message), + + this.props.warning ? this.props.warning && h('span.error.flex-center', { + style: { + textAlign: 'center', + width: '229px', + height: '82px', + }, + }, + this.props.warning) : null, + + h('#qr-container.flex-column', { + style: { + marginTop: '25px', + marginBottom: '15px', + }, + dangerouslySetInnerHTML: { + __html: qrImage.createTableTag(4), + }, + }), + h('.flex-row', [ + h('h3.ellip-address', { + style: { + width: '247px', + }, + }, Qr.data), + h(CopyButton, { + value: Qr.data, + }), + ]), + ]) +} + +QrCodeView.prototype.renderMultiMessage = function () { + var Qr = this.props.Qr + var multiMessage = Qr.message.map((message) => h('.qr-message', message)) + return multiMessage +} diff --git a/ui/classic/app/components/range-slider.js b/ui/classic/app/components/range-slider.js new file mode 100644 index 000000000..823f5eb01 --- /dev/null +++ b/ui/classic/app/components/range-slider.js @@ -0,0 +1,58 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = RangeSlider + +inherits(RangeSlider, Component) +function RangeSlider () { + Component.call(this) +} + +RangeSlider.prototype.render = function () { + const state = this.state || {} + const props = this.props + const onInput = props.onInput || function () {} + const name = props.name + const { + min = 0, + max = 100, + increment = 1, + defaultValue = 50, + mirrorInput = false, + } = this.props.options + const {container, input, range} = props.style + + return ( + h('.flex-row', { + style: container, + }, [ + h('input', { + type: 'range', + name: name, + min: min, + max: max, + step: increment, + style: range, + value: state.value || defaultValue, + onChange: mirrorInput ? this.mirrorInputs.bind(this, event) : onInput, + }), + + // Mirrored input for range + mirrorInput ? h('input.large-input', { + type: 'number', + name: `${name}Mirror`, + min: min, + max: max, + value: state.value || defaultValue, + step: increment, + style: input, + onChange: this.mirrorInputs.bind(this, event), + }) : null, + ]) + ) +} + +RangeSlider.prototype.mirrorInputs = function (event) { + this.setState({value: event.target.value}) +} diff --git a/ui/classic/app/components/shapeshift-form.js b/ui/classic/app/components/shapeshift-form.js new file mode 100644 index 000000000..e0a720426 --- /dev/null +++ b/ui/classic/app/components/shapeshift-form.js @@ -0,0 +1,306 @@ +const PersistentForm = require('../../lib/persistent-form') +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +const actions = require('../actions') +const Qr = require('./qr-code') +const isValidAddress = require('../util').isValidAddress +module.exports = connect(mapStateToProps)(ShapeshiftForm) + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + isSubLoading: state.appState.isSubLoading, + qrRequested: state.appState.qrRequested, + } +} + +inherits(ShapeshiftForm, PersistentForm) + +function ShapeshiftForm () { + PersistentForm.call(this) + this.persistentFormParentId = 'shapeshift-buy-form' +} + +ShapeshiftForm.prototype.render = function () { + return h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + this.props.qrRequested ? h(Qr, {key: 'qr'}) : this.renderMain(), + ]) +} + +ShapeshiftForm.prototype.renderMain = function () { + const marketinfo = this.props.buyView.formView.marketinfo + const coinOptions = this.props.buyView.formView.coinOptions + var coin = marketinfo.pair.split('_')[0].toUpperCase() + + return h('.flex-column', { + style: { + // marginTop: '10px', + padding: '25px', + paddingTop: '5px', + width: '100%', + minHeight: '215px', + alignItems: 'center', + overflowY: 'auto', + }, + }, [ + h('.flex-row', { + style: { + justifyContent: 'center', + alignItems: 'baseline', + height: '42px', + }, + }, [ + h('img', { + src: coinOptions[coin].image, + width: '25px', + height: '25px', + style: { + marginRight: '5px', + }, + }), + + h('.input-container', [ + h('input#fromCoin.buy-inputs.ex-coins', { + type: 'text', + list: 'coinList', + autoFocus: true, + dataset: { + persistentFormId: 'input-coin', + }, + style: { + boxSizing: 'border-box', + }, + onChange: this.handleLiveInput.bind(this), + defaultValue: 'BTC', + }), + + this.renderCoinList(), + + h('i.fa.fa-pencil-square-o.edit-text', { + style: { + fontSize: '12px', + color: '#F7861C', + position: 'relative', + bottom: '48px', + left: '106px', + }, + }), + ]), + + h('.icon-control', [ + h('i.fa.fa-refresh.fa-4.orange', { + style: { + bottom: '5px', + left: '5px', + color: '#F7861C', + }, + onClick: this.updateCoin.bind(this), + }), + h('i.fa.fa-chevron-right.fa-4.orange', { + style: { + position: 'relative', + bottom: '26px', + left: '10px', + color: '#F7861C', + }, + onClick: this.updateCoin.bind(this), + }), + ]), + + h('#toCoin.ex-coins', marketinfo.pair.split('_')[1].toUpperCase()), + + h('img', { + src: coinOptions[marketinfo.pair.split('_')[1].toUpperCase()].image, + width: '25px', + height: '25px', + style: { + marginLeft: '5px', + }, + }), + ]), + h('.flex-column', { + style: { + alignItems: 'flex-start', + }, + }, [ + this.props.warning ? this.props.warning && h('span.error.flex-center', { + style: { + textAlign: 'center', + width: '229px', + height: '82px', + }, + }, + this.props.warning) : this.renderInfo(), + ]), + + h(this.activeToggle('.input-container'), { + style: { + padding: '10px', + paddingTop: '0px', + width: '100%', + }, + }, [ + + h('div', `${coin} Address:`), + + h('input#fromCoinAddress.buy-inputs', { + type: 'text', + placeholder: `Your ${coin} Refund Address`, + dataset: { + persistentFormId: 'refund-address', + }, + style: { + boxSizing: 'border-box', + width: '227px', + height: '30px', + padding: ' 5px ', + }, + }), + + h('i.fa.fa-pencil-square-o.edit-text', { + style: { + fontSize: '12px', + color: '#F7861C', + position: 'relative', + bottom: '10px', + right: '11px', + }, + }), + h('.flex-row', { + style: { + justifyContent: 'flex-end', + }, + }, [ + h('button', { + onClick: this.shift.bind(this), + style: { + marginTop: '10px', + position: 'relative', + bottom: '40px', + }, + }, + 'Submit'), + ]), + ]), + ]) +} + +ShapeshiftForm.prototype.shift = function () { + var props = this.props + var withdrawal = this.props.buyView.buyAddress + var returnAddress = document.getElementById('fromCoinAddress').value + var pair = this.props.buyView.formView.marketinfo.pair + var data = { + 'withdrawal': withdrawal, + 'pair': pair, + 'returnAddress': returnAddress, + // Public api key + 'apiKey': '803d1f5df2ed1b1476e4b9e6bcd089e34d8874595dda6a23b67d93c56ea9cc2445e98a6748b219b2b6ad654d9f075f1f1db139abfa93158c04e825db122c14b6', + } + var message = [ + `Deposit Limit: ${props.buyView.formView.marketinfo.limit}`, + `Deposit Minimum:${props.buyView.formView.marketinfo.minimum}`, + ] + if (isValidAddress(withdrawal)) { + this.props.dispatch(actions.coinShiftRquest(data, message)) + } +} + +ShapeshiftForm.prototype.renderCoinList = function () { + var list = Object.keys(this.props.buyView.formView.coinOptions).map((item) => { + return h('option', { + value: item, + }, item) + }) + + return h('datalist#coinList', { + onClick: (event) => { + event.preventDefault() + }, + }, list) +} + +ShapeshiftForm.prototype.updateCoin = function (event) { + event.preventDefault() + const props = this.props + var coinOptions = this.props.buyView.formView.coinOptions + var coin = document.getElementById('fromCoin').value + + if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { + var message = 'Not a valid coin' + return props.dispatch(actions.displayWarning(message)) + } else { + return props.dispatch(actions.pairUpdate(coin)) + } +} + +ShapeshiftForm.prototype.handleLiveInput = function () { + const props = this.props + var coinOptions = this.props.buyView.formView.coinOptions + var coin = document.getElementById('fromCoin').value + + if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { + return null + } else { + return props.dispatch(actions.pairUpdate(coin)) + } +} + +ShapeshiftForm.prototype.renderInfo = function () { + const marketinfo = this.props.buyView.formView.marketinfo + const coinOptions = this.props.buyView.formView.coinOptions + var coin = marketinfo.pair.split('_')[0].toUpperCase() + + return h('span', { + style: { + }, + }, [ + h('h3.flex-row.text-transform-uppercase', { + style: { + color: '#868686', + paddingTop: '4px', + justifyContent: 'space-around', + textAlign: 'center', + fontSize: '17px', + }, + }, `Market Info for ${marketinfo.pair.replace('_', ' to ').toUpperCase()}:`), + h('.marketinfo', ['Status : ', `${coinOptions[coin].status}`]), + h('.marketinfo', ['Exchange Rate: ', `${marketinfo.rate}`]), + h('.marketinfo', ['Limit: ', `${marketinfo.limit}`]), + h('.marketinfo', ['Minimum : ', `${marketinfo.minimum}`]), + ]) +} + +ShapeshiftForm.prototype.activeToggle = function (elementType) { + if (!this.props.buyView.formView.response || this.props.warning) return elementType + return `${elementType}.inactive` +} + +ShapeshiftForm.prototype.renderLoading = function () { + return h('span', { + style: { + position: 'absolute', + left: '70px', + bottom: '194px', + background: 'transparent', + width: '229px', + height: '82px', + display: 'flex', + justifyContent: 'center', + }, + }, [ + h('img', { + style: { + width: '60px', + }, + src: 'images/loading.svg', + }), + ]) +} diff --git a/ui/classic/app/components/shift-list-item.js b/ui/classic/app/components/shift-list-item.js new file mode 100644 index 000000000..32bfbeda4 --- /dev/null +++ b/ui/classic/app/components/shift-list-item.js @@ -0,0 +1,204 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const vreme = new (require('vreme')) +const explorerLink = require('../../lib/explorer-link') +const actions = require('../actions') +const addressSummary = require('../util').addressSummary + +const CopyButton = require('./copyButton') +const EthBalance = require('./eth-balance') +const Tooltip = require('./tooltip') + + +module.exports = connect(mapStateToProps)(ShiftListItem) + +function mapStateToProps (state) { + return { + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + } +} + +inherits(ShiftListItem, Component) + +function ShiftListItem () { + Component.call(this) +} + +ShiftListItem.prototype.render = function () { + return ( + h('.transaction-list-item.flex-row', { + style: { + paddingTop: '20px', + paddingBottom: '20px', + justifyContent: 'space-around', + alignItems: 'center', + }, + }, [ + h('div', { + style: { + width: '0px', + position: 'relative', + bottom: '19px', + }, + }, [ + h('img', { + src: 'https://info.shapeshift.io/sites/default/files/logo.png', + style: { + height: '35px', + width: '132px', + position: 'absolute', + clip: 'rect(0px,23px,34px,0px)', + }, + }), + ]), + + this.renderInfo(), + this.renderUtilComponents(), + ]) + ) +} + +function formatDate (date) { + return vreme.format(new Date(date), 'March 16 2014 14:30') +} + +ShiftListItem.prototype.renderUtilComponents = function () { + var props = this.props + const { conversionRate, currentCurrency } = props + + switch (props.response.status) { + case 'no_deposits': + return h('.flex-row', [ + h(CopyButton, { + value: this.props.depositAddress, + }), + h(Tooltip, { + title: 'QR Code', + }, [ + h('i.fa.fa-qrcode.pointer.pop-hover', { + onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType)), + style: { + margin: '5px', + marginLeft: '23px', + marginRight: '12px', + fontSize: '20px', + color: '#F7861C', + }, + }), + ]), + ]) + case 'received': + return h('.flex-row') + + case 'complete': + return h('.flex-row', [ + h(CopyButton, { + value: this.props.response.transaction, + }), + h(EthBalance, { + value: `${props.response.outgoingCoin}`, + conversionRate, + currentCurrency, + width: '55px', + shorten: true, + needsParse: false, + incoming: true, + style: { + fontSize: '15px', + color: '#01888C', + }, + }), + ]) + + case 'failed': + return '' + default: + return '' + } +} + +ShiftListItem.prototype.renderInfo = function () { + var props = this.props + switch (props.response.status) { + case 'no_deposits': + return h('.flex-column', { + style: { + width: '200px', + overflow: 'hidden', + }, + }, [ + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, `${props.depositType} to ETH via ShapeShift`), + h('div', 'No deposits received'), + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, formatDate(props.time)), + ]) + case 'received': + return h('.flex-column', { + style: { + width: '200px', + overflow: 'hidden', + }, + }, [ + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, `${props.depositType} to ETH via ShapeShift`), + h('div', 'Conversion in progress'), + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, formatDate(props.time)), + ]) + case 'complete': + var url = explorerLink(props.response.transaction, parseInt('1')) + + return h('.flex-column.pointer', { + style: { + width: '200px', + overflow: 'hidden', + }, + onClick: () => global.platform.openWindow({ url }), + }, [ + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, 'From ShapeShift'), + h('div', formatDate(props.time)), + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, addressSummary(props.response.transaction)), + ]) + + case 'failed': + return h('span.error', '(Failed)') + default: + return '' + } +} diff --git a/ui/classic/app/components/tab-bar.js b/ui/classic/app/components/tab-bar.js new file mode 100644 index 000000000..6295e7dd9 --- /dev/null +++ b/ui/classic/app/components/tab-bar.js @@ -0,0 +1,36 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = TabBar + +inherits(TabBar, Component) +function TabBar () { + Component.call(this) +} + +TabBar.prototype.render = function () { + const props = this.props + const state = this.state || {} + const { tabs = [], defaultTab, tabSelected } = props + const { subview = defaultTab } = state + + return ( + h('.flex-row.space-around.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + paddingTop: '4px', + }, + }, tabs.map((tab) => { + const { key, content } = tab + return h(subview === key ? '.activeForm' : '.inactiveForm.pointer', { + onClick: () => { + this.setState({ subview: key }) + tabSelected(key) + }, + }, content) + })) + ) +} + diff --git a/ui/classic/app/components/template.js b/ui/classic/app/components/template.js new file mode 100644 index 000000000..b6ed8eaa0 --- /dev/null +++ b/ui/classic/app/components/template.js @@ -0,0 +1,18 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = NewComponent + +inherits(NewComponent, Component) +function NewComponent () { + Component.call(this) +} + +NewComponent.prototype.render = function () { + const props = this.props + + return ( + h('span', props.message) + ) +} diff --git a/ui/classic/app/components/token-cell.js b/ui/classic/app/components/token-cell.js new file mode 100644 index 000000000..19d7139bb --- /dev/null +++ b/ui/classic/app/components/token-cell.js @@ -0,0 +1,72 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Identicon = require('./identicon') +const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') + +module.exports = TokenCell + +inherits(TokenCell, Component) +function TokenCell () { + Component.call(this) +} + +TokenCell.prototype.render = function () { + const props = this.props + const { address, symbol, string, network, userAddress } = props + + return ( + h('li.token-cell', { + style: { cursor: network === '1' ? 'pointer' : 'default' }, + onClick: this.view.bind(this, address, userAddress, network), + }, [ + + h(Identicon, { + diameter: 50, + address, + network, + }), + + h('h3', `${string || 0} ${symbol}`), + + h('span', { style: { flex: '1 0 auto' } }), + + /* + h('button', { + onClick: this.send.bind(this, address), + }, 'SEND'), + */ + + ]) + ) +} + +TokenCell.prototype.send = function (address, event) { + event.preventDefault() + event.stopPropagation() + const url = tokenFactoryFor(address) + if (url) { + navigateTo(url) + } +} + +TokenCell.prototype.view = function (address, userAddress, network, event) { + const url = etherscanLinkFor(address, userAddress, network) + if (url) { + navigateTo(url) + } +} + +function navigateTo (url) { + global.platform.openWindow({ url }) +} + +function etherscanLinkFor (tokenAddress, address, network) { + const prefix = prefixForNetwork(network) + return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}` +} + +function tokenFactoryFor (tokenAddress) { + return `https://tokenfactory.surge.sh/#/token/${tokenAddress}` +} + diff --git a/ui/classic/app/components/token-list.js b/ui/classic/app/components/token-list.js new file mode 100644 index 000000000..fed7e9f7a --- /dev/null +++ b/ui/classic/app/components/token-list.js @@ -0,0 +1,194 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const TokenTracker = require('eth-token-tracker') +const TokenCell = require('./token-cell.js') +const normalizeAddress = require('eth-sig-util').normalize + +const defaultTokens = [] +/* +const contracts = require('eth-contract-metadata') +for (const address in contracts) { + const contract = contracts[address] + if (contract.erc20) { + contract.address = address + defaultTokens.push(contract) + } +} +*/ + +module.exports = TokenList + +inherits(TokenList, Component) +function TokenList () { + this.state = { + tokens: [], + isLoading: true, + network: null, + } + Component.call(this) +} + +TokenList.prototype.render = function () { + const state = this.state + const { tokens, isLoading, error } = state + const { userAddress, network } = this.props + + if (isLoading) { + return this.message('Loading') + } + + if (error) { + log.error(error) + return this.message('There was a problem loading your token balances.') + } + + const tokenViews = tokens.map((tokenData) => { + tokenData.network = network + tokenData.userAddress = userAddress + return h(TokenCell, tokenData) + }) + + return h('div', [ + h('ol', { + style: { + height: '260px', + overflowY: 'auto', + display: 'flex', + flexDirection: 'column', + }, + }, [ + h('style', ` + + li.token-cell { + display: flex; + flex-direction: row; + align-items: center; + padding: 10px; + } + + li.token-cell > h3 { + margin-left: 12px; + } + + li.token-cell:hover { + background: white; + cursor: pointer; + } + + `), + ...tokenViews, + tokenViews.length ? null : this.message('No Tokens Found.'), + ]), + this.addTokenButtonElement(), + ]) +} + +TokenList.prototype.addTokenButtonElement = function () { + return h('div', [ + h('div.footer.hover-white.pointer', { + key: 'reveal-account-bar', + onClick: () => { + this.props.addToken() + }, + style: { + display: 'flex', + height: '40px', + padding: '10px', + justifyContent: 'center', + alignItems: 'center', + }, + }, [ + h('i.fa.fa-plus.fa-lg'), + ]), + ]) +} + +TokenList.prototype.message = function (body) { + return h('div', { + style: { + display: 'flex', + height: '250px', + alignItems: 'center', + justifyContent: 'center', + padding: '30px', + }, + }, body) +} + +TokenList.prototype.componentDidMount = function () { + this.createFreshTokenTracker() +} + +TokenList.prototype.createFreshTokenTracker = function () { + if (this.tracker) { + // Clean up old trackers when refreshing: + this.tracker.stop() + this.tracker.removeListener('update', this.balanceUpdater) + this.tracker.removeListener('error', this.showError) + } + + if (!global.ethereumProvider) return + const { userAddress } = this.props + this.tracker = new TokenTracker({ + userAddress, + provider: global.ethereumProvider, + tokens: uniqueMergeTokens(defaultTokens, this.props.tokens), + pollingInterval: 8000, + }) + + + // Set up listener instances for cleaning up + this.balanceUpdater = this.updateBalances.bind(this) + this.showError = (error) => { + this.setState({ error, isLoading: false }) + } + this.tracker.on('update', this.balanceUpdater) + this.tracker.on('error', this.showError) + + this.tracker.updateBalances() + .then(() => { + this.updateBalances(this.tracker.serialize()) + }) + .catch((reason) => { + log.error(`Problem updating balances`, reason) + this.setState({ isLoading: false }) + }) +} + +TokenList.prototype.componentWillUpdate = function (nextProps) { + if (nextProps.network === 'loading') return + const oldNet = this.props.network + const newNet = nextProps.network + + if (oldNet && newNet && newNet !== oldNet) { + this.setState({ isLoading: true }) + this.createFreshTokenTracker() + } +} + +TokenList.prototype.updateBalances = function (tokens) { + const heldTokens = tokens.filter(token => { + return token.balance !== '0' && token.string !== '0.000' + }) + this.setState({ tokens: heldTokens, isLoading: false }) +} + +TokenList.prototype.componentWillUnmount = function () { + if (!this.tracker) return + this.tracker.stop() +} + +function uniqueMergeTokens (tokensA, tokensB) { + const uniqueAddresses = [] + const result = [] + tokensA.concat(tokensB).forEach((token) => { + const normal = normalizeAddress(token.address) + if (!uniqueAddresses.includes(normal)) { + uniqueAddresses.push(normal) + result.push(token) + } + }) + return result +} + diff --git a/ui/classic/app/components/tooltip.js b/ui/classic/app/components/tooltip.js new file mode 100644 index 000000000..edbc074bb --- /dev/null +++ b/ui/classic/app/components/tooltip.js @@ -0,0 +1,22 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ReactTooltip = require('react-tooltip-component') + +module.exports = Tooltip + +inherits(Tooltip, Component) +function Tooltip () { + Component.call(this) +} + +Tooltip.prototype.render = function () { + const props = this.props + const { position, title, children } = props + + return h(ReactTooltip, { + position: position || 'left', + title, + fixed: false, + }, children) +} diff --git a/ui/classic/app/components/transaction-list-item-icon.js b/ui/classic/app/components/transaction-list-item-icon.js new file mode 100644 index 000000000..431054340 --- /dev/null +++ b/ui/classic/app/components/transaction-list-item-icon.js @@ -0,0 +1,68 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Tooltip = require('./tooltip') + +const Identicon = require('./identicon') + +module.exports = TransactionIcon + +inherits(TransactionIcon, Component) +function TransactionIcon () { + Component.call(this) +} + +TransactionIcon.prototype.render = function () { + const { transaction, txParams, isMsg } = this.props + switch (transaction.status) { + case 'unapproved': + return h(!isMsg ? '.unapproved-tx-icon' : 'i.fa.fa-certificate.fa-lg') + + case 'rejected': + return h('i.fa.fa-exclamation-triangle.fa-lg.warning', { + style: { + width: '24px', + }, + }) + + case 'failed': + return h('i.fa.fa-exclamation-triangle.fa-lg.error', { + style: { + width: '24px', + }, + }) + + case 'submitted': + return h(Tooltip, { + title: 'Pending', + position: 'bottom', + }, [ + h('i.fa.fa-ellipsis-h', { + style: { + fontSize: '27px', + }, + }), + ]) + } + + if (isMsg) { + return h('i.fa.fa-certificate.fa-lg', { + style: { + width: '24px', + }, + }) + } + + if (txParams.to) { + return h(Identicon, { + diameter: 24, + address: txParams.to || transaction.hash, + }) + } else { + return h('i.fa.fa-file-text-o.fa-lg', { + style: { + width: '24px', + }, + }) + } +} diff --git a/ui/classic/app/components/transaction-list-item.js b/ui/classic/app/components/transaction-list-item.js new file mode 100644 index 000000000..dbda66a31 --- /dev/null +++ b/ui/classic/app/components/transaction-list-item.js @@ -0,0 +1,165 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const EthBalance = require('./eth-balance') +const addressSummary = require('../util').addressSummary +const explorerLink = require('../../lib/explorer-link') +const CopyButton = require('./copyButton') +const vreme = new (require('vreme')) +const Tooltip = require('./tooltip') +const numberToBN = require('number-to-bn') + +const TransactionIcon = require('./transaction-list-item-icon') +const ShiftListItem = require('./shift-list-item') +module.exports = TransactionListItem + +inherits(TransactionListItem, Component) +function TransactionListItem () { + Component.call(this) +} + +TransactionListItem.prototype.render = function () { + const { transaction, network, conversionRate, currentCurrency } = this.props + if (transaction.key === 'shapeshift') { + if (network === '1') return h(ShiftListItem, transaction) + } + var date = formatDate(transaction.time) + + let isLinkable = false + const numericNet = parseInt(network) + isLinkable = numericNet === 1 || numericNet === 3 || numericNet === 4 || numericNet === 42 + + var isMsg = ('msgParams' in transaction) + var isTx = ('txParams' in transaction) + var isPending = transaction.status === 'unapproved' + let txParams + if (isTx) { + txParams = transaction.txParams + } else if (isMsg) { + txParams = transaction.msgParams + } + + const nonce = txParams.nonce ? numberToBN(txParams.nonce).toString(10) : '' + + const isClickable = ('hash' in transaction && isLinkable) || isPending + return ( + h(`.transaction-list-item.flex-row.flex-space-between${isClickable ? '.pointer' : ''}`, { + onClick: (event) => { + if (isPending) { + this.props.showTx(transaction.id) + } + event.stopPropagation() + if (!transaction.hash || !isLinkable) return + var url = explorerLink(transaction.hash, parseInt(network)) + global.platform.openWindow({ url }) + }, + style: { + padding: '20px 0', + }, + }, [ + + h('.identicon-wrapper.flex-column.flex-center.select-none', [ + h('.pop-hover', { + onClick: (event) => { + event.stopPropagation() + if (!isTx || isPending) return + var url = `https://metamask.github.io/eth-tx-viz/?tx=${transaction.hash}` + global.platform.openWindow({ url }) + }, + }, [ + h(TransactionIcon, { txParams, transaction, isTx, isMsg }), + ]), + ]), + + h(Tooltip, { + title: 'Transaction Number', + position: 'bottom', + }, [ + h('span', { + style: { + display: 'flex', + cursor: 'normal', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + padding: '10px', + }, + }, nonce), + ]), + + h('.flex-column', {style: {width: '200px', overflow: 'hidden'}}, [ + domainField(txParams), + h('div', date), + recipientField(txParams, transaction, isTx, isMsg), + ]), + + // Places a copy button if tx is successful, else places a placeholder empty div. + transaction.hash ? h(CopyButton, { value: transaction.hash }) : h('div', {style: { display: 'flex', alignItems: 'center', width: '26px' }}), + + isTx ? h(EthBalance, { + value: txParams.value, + conversionRate, + currentCurrency, + width: '55px', + shorten: true, + showFiat: false, + style: {fontSize: '15px'}, + }) : h('.flex-column'), + ]) + ) +} + +function domainField (txParams) { + return h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + overflow: 'hidden', + textOverflow: 'ellipsis', + width: '100%', + }, + }, [ + txParams.origin, + ]) +} + +function recipientField (txParams, transaction, isTx, isMsg) { + let message + + if (isMsg) { + message = 'Signature Requested' + } else if (txParams.to) { + message = addressSummary(txParams.to) + } else { + message = 'Contract Published' + } + + return h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + }, + }, [ + message, + failIfFailed(transaction), + ]) +} + +function formatDate (date) { + return vreme.format(new Date(date), 'March 16 2014 14:30') +} + +function failIfFailed (transaction) { + if (transaction.status === 'rejected') { + return h('span.error', ' (Rejected)') + } + if (transaction.err) { + return h(Tooltip, { + title: transaction.err.message, + position: 'bottom', + }, [ + h('span.error', ' (Failed)'), + ]) + } +} diff --git a/ui/classic/app/components/transaction-list.js b/ui/classic/app/components/transaction-list.js new file mode 100644 index 000000000..3b4ba741e --- /dev/null +++ b/ui/classic/app/components/transaction-list.js @@ -0,0 +1,79 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const TransactionListItem = require('./transaction-list-item') + +module.exports = TransactionList + + +inherits(TransactionList, Component) +function TransactionList () { + Component.call(this) +} + +TransactionList.prototype.render = function () { + const { transactions, network, unapprovedMsgs, conversionRate } = this.props + + var shapeShiftTxList + if (network === '1') { + shapeShiftTxList = this.props.shapeShiftTxList + } + const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList) + .sort((a, b) => b.time - a.time) + + return ( + + h('section.transaction-list', [ + + h('style', ` + .transaction-list .transaction-list-item:not(:last-of-type) { + border-bottom: 1px solid #D4D4D4; + } + .transaction-list .transaction-list-item .ether-balance-label { + display: block !important; + font-size: small; + } + `), + + h('.tx-list', { + style: { + overflowY: 'auto', + height: '300px', + padding: '0 20px', + textAlign: 'center', + }, + }, [ + + txsToRender.length + ? txsToRender.map((transaction, i) => { + let key + switch (transaction.key) { + case 'shapeshift': + const { depositAddress, time } = transaction + key = `shift-tx-${depositAddress}-${time}-${i}` + break + default: + key = `tx-${transaction.id}-${i}` + } + return h(TransactionListItem, { + transaction, i, network, key, + conversionRate, + showTx: (txId) => { + this.props.viewPendingTx(txId) + }, + }) + }) + : h('.flex-center', { + style: { + flexDirection: 'column', + height: '100%', + }, + }, [ + 'No transaction history.', + ]), + ]), + ]) + ) +} + diff --git a/ui/classic/app/conf-tx.js b/ui/classic/app/conf-tx.js new file mode 100644 index 000000000..747d3ce2b --- /dev/null +++ b/ui/classic/app/conf-tx.js @@ -0,0 +1,213 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') +const NetworkIndicator = require('./components/network') +const txHelper = require('../lib/tx-helper') +const isPopupOrNotification = require('../../app/scripts/lib/is-popup-or-notification') + +const PendingTx = require('./components/pending-tx') +const PendingMsg = require('./components/pending-msg') +const PendingPersonalMsg = require('./components/pending-personal-msg') +const Loading = require('./components/loading') + +module.exports = connect(mapStateToProps)(ConfirmTxScreen) + +function mapStateToProps (state) { + return { + identities: state.metamask.identities, + accounts: state.metamask.accounts, + selectedAddress: state.metamask.selectedAddress, + unapprovedTxs: state.metamask.unapprovedTxs, + unapprovedMsgs: state.metamask.unapprovedMsgs, + unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs, + index: state.appState.currentView.context, + warning: state.appState.warning, + network: state.metamask.network, + provider: state.metamask.provider, + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + blockGasLimit: state.metamask.currentBlockGasLimit, + } +} + +inherits(ConfirmTxScreen, Component) +function ConfirmTxScreen () { + Component.call(this) +} + +ConfirmTxScreen.prototype.render = function () { + const props = this.props + const { network, provider, unapprovedTxs, currentCurrency, + unapprovedMsgs, unapprovedPersonalMsgs, conversionRate, blockGasLimit } = props + + var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) + + var txData = unconfTxList[props.index] || {} + var txParams = txData.params || {} + var isNotification = isPopupOrNotification() === 'notification' + + + log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`) + if (unconfTxList.length === 0) return h(Loading, { isLoading: true }) + + return ( + + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + !isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: this.goHome.bind(this), + }) : null, + h('h2.page-subtitle', 'Confirm Transaction'), + isNotification ? h(NetworkIndicator, { + network: network, + provider: provider, + }) : null, + ]), + + h('h3', { + style: { + alignSelf: 'center', + display: unconfTxList.length > 1 ? 'block' : 'none', + }, + }, [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + style: { + display: props.index === 0 ? 'none' : 'inline-block', + }, + onClick: () => props.dispatch(actions.previousTx()), + }), + ` ${props.index + 1} of ${unconfTxList.length} `, + h('i.fa.fa-arrow-right.fa-lg.cursor-pointer', { + style: { + display: props.index + 1 === unconfTxList.length ? 'none' : 'inline-block', + }, + onClick: () => props.dispatch(actions.nextTx()), + }), + ]), + + warningIfExists(props.warning), + + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + + currentTxView({ + // Properties + txData: txData, + key: txData.id, + selectedAddress: props.selectedAddress, + accounts: props.accounts, + identities: props.identities, + conversionRate, + currentCurrency, + blockGasLimit, + // Actions + buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), + sendTransaction: this.sendTransaction.bind(this), + cancelTransaction: this.cancelTransaction.bind(this, txData), + signMessage: this.signMessage.bind(this, txData), + signPersonalMessage: this.signPersonalMessage.bind(this, txData), + cancelMessage: this.cancelMessage.bind(this, txData), + cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), + }), + + ]), + ]) + ) +} + +function currentTxView (opts) { + log.info('rendering current tx view') + const { txData } = opts + const { txParams, msgParams, type } = txData + + if (txParams) { + log.debug('txParams detected, rendering pending tx') + return h(PendingTx, opts) + } else if (msgParams) { + log.debug('msgParams detected, rendering pending msg') + + if (type === 'eth_sign') { + log.debug('rendering eth_sign message') + return h(PendingMsg, opts) + } else if (type === 'personal_sign') { + log.debug('rendering personal_sign message') + return h(PendingPersonalMsg, opts) + } + } +} + +ConfirmTxScreen.prototype.buyEth = function (address, event) { + event.preventDefault() + this.props.dispatch(actions.buyEthView(address)) +} + +ConfirmTxScreen.prototype.sendTransaction = function (txData, event) { + this.stopPropagation(event) + this.props.dispatch(actions.updateAndApproveTx(txData)) +} + +ConfirmTxScreen.prototype.cancelTransaction = function (txData, event) { + this.stopPropagation(event) + event.preventDefault() + this.props.dispatch(actions.cancelTx(txData)) +} + +ConfirmTxScreen.prototype.signMessage = function (msgData, event) { + log.info('conf-tx.js: signing message') + var params = msgData.msgParams + params.metamaskId = msgData.id + this.stopPropagation(event) + this.props.dispatch(actions.signMsg(params)) +} + +ConfirmTxScreen.prototype.stopPropagation = function (event) { + if (event.stopPropagation) { + event.stopPropagation() + } +} + +ConfirmTxScreen.prototype.signPersonalMessage = function (msgData, event) { + log.info('conf-tx.js: signing personal message') + var params = msgData.msgParams + params.metamaskId = msgData.id + this.stopPropagation(event) + this.props.dispatch(actions.signPersonalMsg(params)) +} + +ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) { + log.info('canceling message') + this.stopPropagation(event) + this.props.dispatch(actions.cancelMsg(msgData)) +} + +ConfirmTxScreen.prototype.cancelPersonalMessage = function (msgData, event) { + log.info('canceling personal message') + this.stopPropagation(event) + this.props.dispatch(actions.cancelPersonalMsg(msgData)) +} + +ConfirmTxScreen.prototype.goHome = function (event) { + this.stopPropagation(event) + this.props.dispatch(actions.goHome()) +} + +function warningIfExists (warning) { + if (warning && + // Do not display user rejections on this screen: + warning.indexOf('User denied transaction signature') === -1) { + return h('.error', { + style: { + margin: 'auto', + }, + }, warning) + } +} diff --git a/ui/classic/app/config.js b/ui/classic/app/config.js new file mode 100644 index 000000000..62785c49b --- /dev/null +++ b/ui/classic/app/config.js @@ -0,0 +1,211 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') +const currencies = require('./conversion.json').rows +const validUrl = require('valid-url') +const copyToClipboard = require('copy-to-clipboard') + +module.exports = connect(mapStateToProps)(ConfigScreen) + +function mapStateToProps (state) { + return { + metamask: state.metamask, + warning: state.appState.warning, + } +} + +inherits(ConfigScreen, Component) +function ConfigScreen () { + Component.call(this) +} + +ConfigScreen.prototype.render = function () { + var state = this.props + var metamaskState = state.metamask + var warning = state.warning + + return ( + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + state.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Settings'), + ]), + + h('.error', { + style: { + display: warning ? 'block' : 'none', + padding: '0 20px', + textAlign: 'center', + }, + }, warning), + + // conf view + h('.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-space-around', { + style: { + padding: '20px', + }, + }, [ + + currentProviderDisplay(metamaskState), + + h('div', { style: {display: 'flex'} }, [ + h('input#new_rpc', { + placeholder: 'New RPC URL', + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onKeyPress (event) { + if (event.key === 'Enter') { + var element = event.target + var newRpc = element.value + rpcValidation(newRpc, state) + } + }, + }), + h('button', { + style: { + alignSelf: 'center', + }, + onClick (event) { + event.preventDefault() + var element = document.querySelector('input#new_rpc') + var newRpc = element.value + rpcValidation(newRpc, state) + }, + }, 'Save'), + ]), + + h('hr.horizontal-line'), + + currentConversionInformation(metamaskState, state), + + h('hr.horizontal-line'), + + h('div', { + style: { + marginTop: '20px', + }, + }, [ + h('p', { + style: { + fontFamily: 'Montserrat Light', + fontSize: '13px', + }, + }, `State logs contain your public account addresses and sent transactions.`), + h('br'), + h('button', { + style: { + alignSelf: 'center', + }, + onClick (event) { + copyToClipboard(window.logState()) + }, + }, 'Copy State Logs'), + ]), + + h('hr.horizontal-line'), + + h('div', { + style: { + marginTop: '20px', + }, + }, [ + h('button', { + style: { + alignSelf: 'center', + }, + onClick (event) { + event.preventDefault() + state.dispatch(actions.revealSeedConfirmation()) + }, + }, 'Reveal Seed Words'), + ]), + + ]), + ]), + ]) + ) +} + +function rpcValidation (newRpc, state) { + if (validUrl.isWebUri(newRpc)) { + state.dispatch(actions.setRpcTarget(newRpc)) + } else { + var appendedRpc = `http://${newRpc}` + if (validUrl.isWebUri(appendedRpc)) { + state.dispatch(actions.displayWarning('URIs require the appropriate HTTP/HTTPS prefix.')) + } else { + state.dispatch(actions.displayWarning('Invalid RPC URI')) + } + } +} + +function currentConversionInformation (metamaskState, state) { + var currentCurrency = metamaskState.currentCurrency + var conversionDate = metamaskState.conversionDate + return h('div', [ + h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, 'Current Conversion'), + h('span', {style: { fontWeight: 'bold', paddingRight: '10px', fontSize: '13px'}}, `Updated ${Date(conversionDate)}`), + h('select#currentCurrency', { + onChange (event) { + event.preventDefault() + var element = document.getElementById('currentCurrency') + var newCurrency = element.value + state.dispatch(actions.setCurrentCurrency(newCurrency)) + }, + defaultValue: currentCurrency, + }, currencies.map((currency) => { + return h('option', {key: currency.code, value: currency.code}, `${currency.code} - ${currency.name}`) + }) + ), + ]) +} + +function currentProviderDisplay (metamaskState) { + var provider = metamaskState.provider + var title, value + + switch (provider.type) { + + case 'mainnet': + title = 'Current Network' + value = 'Main Ethereum Network' + break + + case 'ropsten': + title = 'Current Network' + value = 'Ropsten Test Network' + break + + case 'kovan': + title = 'Current Network' + value = 'Kovan Test Network' + break + + case 'rinkeby': + title = 'Current Network' + value = 'Rinkeby Test Network' + break + + default: + title = 'Current RPC' + value = metamaskState.provider.rpcTarget + } + + return h('div', [ + h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, title), + h('span', value), + ]) +} diff --git a/ui/classic/app/conversion.json b/ui/classic/app/conversion.json new file mode 100644 index 000000000..155ffc4fc --- /dev/null +++ b/ui/classic/app/conversion.json @@ -0,0 +1,207 @@ +{ + "rows": [ + { + "code": "REP", + "name": "Augur", + "statuses": [ + "primary" + ] + }, + { + "code": "BCN", + "name": "Bytecoin", + "statuses": [ + "primary" + ] + }, + { + "code": "BTC", + "name": "Bitcoin", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "BTS", + "name": "BitShares", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "BLK", + "name": "Blackcoin", + "statuses": [ + "primary" + ] + }, + { + "code": "GBP", + "name": "British Pound Sterling", + "statuses": [ + "secondary" + ] + }, + { + "code": "CAD", + "name": "Canadian Dollar", + "statuses": [ + "secondary" + ] + }, + { + "code": "CNY", + "name": "Chinese Yuan", + "statuses": [ + "secondary" + ] + }, + { + "code": "DSH", + "name": "Dashcoin", + "statuses": [ + "primary" + ] + }, + { + "code": "DOGE", + "name": "Dogecoin", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "ETC", + "name": "Ethereum Classic", + "statuses": [ + "primary" + ] + }, + { + "code": "EUR", + "name": "Euro", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "GNO", + "name": "GNO", + "statuses": [ + "primary" + ] + }, + { + "code": "GNT", + "name": "GNT", + "statuses": [ + "primary" + ] + }, + { + "code": "JPY", + "name": "Japanese Yen", + "statuses": [ + "secondary" + ] + }, + { + "code": "LTC", + "name": "Litecoin", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "MAID", + "name": "MaidSafeCoin", + "statuses": [ + "primary" + ] + }, + { + "code": "XEM", + "name": "NEM", + "statuses": [ + "primary" + ] + }, + { + "code": "XLM", + "name": "Stellar", + "statuses": [ + "primary" + ] + }, + { + "code": "XMR", + "name": "Monero", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "XRP", + "name": "Ripple", + "statuses": [ + "primary" + ] + }, + { + "code": "RUR", + "name": "Ruble", + "statuses": [ + "secondary" + ] + }, + { + "code": "STEEM", + "name": "Steem", + "statuses": [ + "primary" + ] + }, + { + "code": "STRAT", + "name": "STRAT", + "statuses": [ + "primary" + ] + }, + { + "code": "UAH", + "name": "Ukrainian Hryvnia", + "statuses": [ + "secondary" + ] + }, + { + "code": "USD", + "name": "US Dollar", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "WAVES", + "name": "WAVES", + "statuses": [ + "primary" + ] + }, + { + "code": "ZEC", + "name": "Zcash", + "statuses": [ + "primary" + ] + } + ] +} diff --git a/ui/classic/app/css/debug.css b/ui/classic/app/css/debug.css new file mode 100644 index 000000000..3e125bcd4 --- /dev/null +++ b/ui/classic/app/css/debug.css @@ -0,0 +1,21 @@ +/* +debug / dev +*/ + +#app-content { + border: 2px solid green; +} + +#design-container { + position: absolute; + left: 360px; + top: -42px; + width: calc(100vw - 360px); + height: 100vh; + overflow: scroll; +} + +#design-container img { + width: 2000px; + margin-right: 600px; +} \ No newline at end of file diff --git a/ui/classic/app/css/fonts.css b/ui/classic/app/css/fonts.css new file mode 100644 index 000000000..3b9f581b9 --- /dev/null +++ b/ui/classic/app/css/fonts.css @@ -0,0 +1,36 @@ +@import url(https://fonts.googleapis.com/css?family=Roboto:300,500); +@import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css); + +@font-face { + font-family: 'Montserrat Regular'; + src: url('/fonts/Montserrat/Montserrat-Regular.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; + font-size: 'small'; + +} + +@font-face { + font-family: 'Montserrat Bold'; + src: url('/fonts/Montserrat/Montserrat-Bold.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-Bold.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Montserrat Light'; + src: url('/fonts/Montserrat/Montserrat-Light.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-Light.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Montserrat UltraLight'; + src: url('/fonts/Montserrat/Montserrat-UltraLight.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-UltraLight.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} diff --git a/ui/classic/app/css/index.css b/ui/classic/app/css/index.css new file mode 100644 index 000000000..808aafb4c --- /dev/null +++ b/ui/classic/app/css/index.css @@ -0,0 +1,667 @@ +/* +faint orange (textfield shades) #FAF6F0 +light orange (button shades): #F5C26D +dark orange (text): #F5A623 +borders/font/any gray: #4A4A4A +*/ + +/* +application specific styles +*/ + +* { + box-sizing: border-box; +} + +html, body { + font-family: 'Montserrat Regular', Arial; + color: #4D4D4D; + font-weight: 300; + line-height: 1.4em; + background: #F7F7F7; +} + +input:focus, textarea:focus { + outline: none; +} + +#app-content { + overflow-x: hidden; + min-width: 357px; + width: 360px; + height: 500px; +} + +button, input[type="submit"] { + font-family: 'Montserrat Bold'; + outline: none; + cursor: pointer; + padding: 8px 12px; + border: none; + color: white; + transform-origin: center center; + transition: transform 50ms ease-in; + /* default orange */ + background: rgba(247, 134, 28, 1); + box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); +} + +.btn-green, input[type="submit"].btn-green { + background: rgba(106, 195, 96, 1); + box-shadow: 0px 3px 6px rgba(106, 195, 96, 0.36); +} + +.btn-red { + background: rgba(254, 35, 17, 1); + box-shadow: 0px 3px 6px rgba(254, 35, 17, 0.36); +} + +button[disabled], input[type="submit"][disabled] { + cursor: not-allowed; + background: rgba(197, 197, 197, 1); + box-shadow: 0px 3px 6px rgba(197, 197, 197, 0.36); +} + +button.spaced { + margin: 2px; +} + +button:not([disabled]):hover, input[type="submit"]:not([disabled]):hover { + transform: scale(1.1); +} +button:not([disabled]):active, input[type="submit"]:not([disabled]):active { + transform: scale(0.95); +} + +a { + text-decoration: none; + color: inherit; +} + +a:hover{ + color: #df6b0e; +} + +/* +app +*/ + +.active { + color: #909090; +} + +button.primary { + padding: 8px 12px; + background: #F7861C; + box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); + color: white; + font-size: 1.1em; + font-family: 'Montserrat Regular'; + text-transform: uppercase; +} + +button.btn-thin { + border: 1px solid; + border-color: #4D4D4D; + color: #4D4D4D; + background: rgb(255, 174, 41); + border-radius: 4px; + min-width: 200px; + margin: 12px 0; + padding: 6px; + font-size: 13px; +} + +.app-header { + padding: 6px 8px; +} + +.app-header h1 { + font-family: 'Montserrat Regular'; + text-transform: uppercase; + color: #AEAEAE; +} + +h2.page-subtitle { + font-family: 'Montserrat Regular'; + text-transform: uppercase; + color: #AEAEAE; + font-size: 1em; + margin: 12px; +} + +.app-primary { + +} + +.app-footer { + padding-bottom: 10px; + align-items: center; +} + +.identicon { + height: 46px; + width: 46px; + background-size: cover; + border-radius: 100%; + border: 3px solid gray; +} + +textarea.twelve-word-phrase { + padding: 12px; + width: 300px; + height: 140px; + font-size: 16px; + background: white; + resize: none; +} + +.network-indicator { + display: flex; + align-items: center; + font-size: 0.6em; + +} + +.network-name { + width: 5.2em; + line-height: 9px; + text-rendering: geometricPrecision; +} + +.check { + margin-left: 7px; + color: #F7861C; + flex: 1 0 auto; + display: flex; + justify-content: flex-end; +} +/* +app sections +*/ + +/* initialize */ + +.initialize-screen hr { + width: 60px; + margin: 12px; + border-color: #F7861C; + border-style: solid; +} + +.initialize-screen label { + margin-top: 20px; +} + +.initialize-screen button.create-vault { + margin-top: 40px; +} + +.initialize-screen .warning { + font-size: 14px; + margin: 0 16px; +} + +/* unlock */ +.error { + color: #E20202; +} + +.warning { + color: #FFAE00; +} + +.lock { + width: 50px; + height: 50px; +} + +.lock.locked { + transform: scale(1.5); + opacity: 0.0; + transition: opacity 400ms ease-in, transform 400ms ease-in; +} +.lock.unlocked { + transform: scale(1); + opacity: 1; + transition: opacity 500ms ease-out, transform 500ms ease-out, background 200ms ease-in; +} + +.lock.locked .lock-top { + transform: scaleX(1) translateX(0); + transition: transform 250ms ease-in; +} +.lock.unlocked .lock-top { + transform: scaleX(-1) translateX(-12px); + transition: transform 250ms ease-in; +} +.lock.unlocked:hover { + border-radius: 4px; + background: #e5e5e5; + border: 1px solid #b1b1b1; +} +.lock.unlocked:active { + background: #c3c3c3; +} + +.section-title .fa-arrow-left { + margin: -2px 8px 0px -8px; +} + +.unlock-screen #metamask-mascot-container { + margin-top: 24px; +} + +.unlock-screen h1 { + margin-top: -28px; + margin-bottom: 42px; +} + +.unlock-screen input[type=password] { + width: 260px; + /*height: 36px; + margin-bottom: 24px; + padding: 8px;*/ +} + +.sizing-input{ + font-size: 14px; + height: 30px; + padding-left: 5px; +} +.editable-label{ + display: flex; +} +/* Webkit */ +.unlock-screen input::-webkit-input-placeholder { + text-align: center; + font-size: 1.2em; +} +/* Firefox 18- */ +.unlock-screen input:-moz-placeholder { + text-align: center; + font-size: 1.2em; +} +/* Firefox 19+ */ +.unlock-screen input::-moz-placeholder { + text-align: center; + font-size: 1.2em; +} +/* IE */ +.unlock-screen input:-ms-input-placeholder { + text-align: center; + font-size: 1.2em; +} + +input.large-input, textarea.large-input { + /*margin-bottom: 24px;*/ + padding: 8px; +} + +input.large-input { + height: 36px; +} + +.letter-spacey { + letter-spacing: 0.1em; +} + + + +/* accounts */ + +.accounts-section { + margin: 0 0px; +} + +.accounts-section .horizontal-line { + margin: 0px 18px; +} + +.accounts-list-option { + height: 120px; +} + +.accounts-list-option .identicon-wrapper { + width: 100px; +} + +.unconftx-link { + margin-top: 24px; + cursor: pointer; +} + +.unconftx-link .fa-arrow-right { + margin: 0px -8px 0px 8px; +} + +/* identity panel */ + +.identity-panel { + font-weight: 500; +} + +.identity-panel .identicon-wrapper { + margin: 4px; + margin-top: 8px; + display: flex; + align-items: center; +} + +.identity-panel .identicon-wrapper span { + margin: 0 auto; +} + +.identity-panel .identity-data { + margin: 8px 8px 8px 18px; +} + +.identity-panel i { + margin-top: 32px; + margin-right: 6px; + color: #B9B9B9; +} + +.identity-panel .arrow-right { + padding-left: 18px; + width: 42px; + min-width: 18px; + height: 100%; +} + +.identity-copy.flex-column { + flex: 0.25 0 auto; + justify-content: center; +} + +/* accounts screen */ + +.identity-section { + +} + +.identity-section .identity-panel { + background: #E9E9E9; + border-bottom: 1px solid #B1B1B1; + cursor: pointer; +} + +.identity-section .identity-panel.selected { + background: white; + color: #F3C83E; +} + +.identity-section .identity-panel.selected .identicon { + border-color: orange; +} + +.identity-section .accounts-list-option:hover, +.identity-section .accounts-list-option.selected { + background:white; +} + +/* account detail screen */ + +.account-detail-section { + +} +.name-label{ + +} + +.unapproved-tx-icon { + height: 16px; + width: 16px; + background: rgb(47, 174, 244); + border-color: #AEAEAE; + border-radius: 13px; +} + +.edit-text { + height: 100%; + visibility: hidden; +} +.editing-label { + display: flex; + justify-content: flex-start; + margin-left: 50px; + margin-bottom: 2px; + font-size: 11px; + text-rendering: geometricPrecision; + color: #F7861C; +} +.name-label:hover .edit-text { + visibility: visible; +} +/* tx confirm */ + +.unconftx-section input[type=password] { + height: 22px; + padding: 2px; + margin: 12px; + margin-bottom: 24px; + border-radius: 4px; + border: 2px solid #F3C83E; + background: #FAF6F0; +} + +/* Send Screen */ + +.send-screen { + +} + +.send-screen section { + margin: 8px 16px; +} + +.send-screen input { + width: 100%; + font-size: 12px; +} + +/* Ether Balance Widget */ + +.ether-balance-amount { + color: #F7861C; +} + +.ether-balance-label { + color: #ABA9AA; +} + +/* Info screen */ +.info-gray{ + font-family: 'Montserrat Regular'; + text-transform: uppercase; + color: #AEAEAE; +} + +.icon-size{ + width: 20px; +} + +.info{ + font-family: 'Montserrat Regular', Arial; + padding-bottom: 10px; + display: inline-block; + padding-left: 5px; +} + +/* buy eth warning screen */ +.custom-radios { + justify-content: space-around; + align-items: center; +} + + +.custom-radio-selected { + width: 17px; + height: 17px; + border: solid; + border-style: double; + border-radius: 15px; + border-width: 5px; + background: rgba(247, 134, 28, 1); + border-color: #F7F7F7; +} + +.custom-radio-inactive { + width: 14px; + height: 14px; + border: solid; + border-width: 1px; + border-radius: 24px; + border-color: #AEAEAE; +} + +.radio-titles { + color: rgba(247, 134, 28, 1); +} + +.radio-titles-subtext { + +} + +.selected-exchange { + +} + +.buy-radio { + +} + +.eth-warning{ + transition: opacity 400ms ease-in, transform 400ms ease-in; +} + +.buy-subview{ + transition: opacity 400ms ease-in, transform 400ms ease-in; +} + +.input-container:hover .edit-text{ + visibility: visible; +} + +.buy-inputs{ + font-family: 'Montserrat Light'; + font-size: 13px; + height: 20px; + background: transparent; + box-sizing: border-box; + border: solid; + border-color: transparent; + border-width: 0.5px; + border-radius: 2px; + +} +.input-container:hover .buy-inputs{ + box-sizing: inherit; + border: solid; + border-color: #F7861C; + border-width: 0.5px; + border-radius: 2px; +} + +.buy-inputs:focus{ + border: solid; + border-color: #F7861C; + border-width: 0.5px; + border-radius: 2px; +} + +.activeForm { + background: #F7F7F7; + border: none; + border-radius: 8px 8px 0px 0px; + width: 50%; + text-align: center; + padding-bottom: 4px; + +} + +.inactiveForm { + border: none; + border-radius: 8px 8px 0px 0px; + width: 50%; + text-align: center; + padding-bottom: 4px; +} + +.ex-coins { + font-family: 'Montserrat Regular'; + text-transform: uppercase; + text-align: center; + font-size: 33px; + width: 118px; + height: 42px; + padding: 1px; + color: #4D4D4D; +} + +.marketinfo{ + font-family: 'Montserrat light'; + color: #AEAEAE; + font-size: 15px; + line-height: 17px; +} + +#fromCoin::-webkit-calendar-picker-indicator { + display: none; +} + +#coinList { + width: 400px; + height: 500px; + overflow: scroll; +} + +.icon-control .fa-refresh{ + visibility: hidden; +} + +.icon-control:hover .fa-refresh{ + visibility: visible; +} + +.icon-control:hover .fa-chevron-right{ + visibility: hidden; +} + +.inactive { + color: #AEAEAE; +} + +.inactive button{ + background: #AEAEAE; + color: white; +} + +.ellip-address { + overflow: hidden; + text-overflow: ellipsis; + width: 5em; + font-size: 14px; + font-family: "Montserrat Light"; + margin-left: 5px; +} + +.qr-header { + font-size: 25px; + margin-top: 40px; +} + +.qr-message { + font-size: 12px; + color: #F7861C; +} + +div.message-container > div:first-child { + margin-top: 18px; + font-size: 15px; + color: #4D4D4D; +} + +.pop-hover:hover { + transform: scale(1.1); +} diff --git a/ui/classic/app/css/lib.css b/ui/classic/app/css/lib.css new file mode 100644 index 000000000..910a24ee2 --- /dev/null +++ b/ui/classic/app/css/lib.css @@ -0,0 +1,268 @@ +/* color */ + +.color-orange { + color: #F7861C; +} + +.color-forest { + color: #0A5448; +} + +/* lib */ + +.full-width { + width: 100%; +} + +.full-height { + height: 100%; +} + +.flex-column { + display: flex; + flex-direction: column; +} + +.space-between { + justify-content: space-between; +} + +.space-around { + justify-content: space-around; +} + +.flex-column-bottom { + display: flex; + flex-direction: column-reverse; +} + +.flex-row { + display: flex; + flex-direction: row; +} + +.flex-space-between { + justify-content: space-between; +} + +.flex-space-around { + justify-content: space-around; +} + +.flex-right { + display: flex; + flex-direction: row; + justify-content: flex-end; +} + +.flex-left { + display: flex; + flex-direction: row; + justify-content: flex-start; +} + +.flex-fixed { + flex: none; +} + +.flex-basis-auto { + flex-basis: auto; +} + +.flex-grow { + flex: 1 1 auto; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.flex-center { + display: flex; + justify-content: center; + align-items: center; +} + +.flex-justify-center { + justify-content: center; +} + +.flex-align-center { + align-items: center; +} + +.flex-self-end { + align-self: flex-end; +} + +.flex-self-stretch { + align-self: stretch; +} + +.flex-vertical { + flex-direction: column; +} + +.z-bump { + z-index: 1; +} + +.select-none { + cursor: inherit; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.pointer { + cursor: pointer; +} +.cursor-pointer { + cursor: pointer; + transform-origin: center center; + transition: transform 50ms ease-in-out; +} +.cursor-pointer:hover { + transform: scale(1.1); +} +.cursor-pointer:active { + transform: scale(0.95); +} + +.cursor-disabled { + cursor: not-allowed; +} + +.margin-bottom-sml { + margin-bottom: 20px; +} + +.margin-bottom-med { + margin-bottom: 40px; +} + +.margin-right-left { + margin: 0 20px; +} + +.bold { + font-weight: bold; +} + +.text-transform-uppercase { + text-transform: uppercase; +} + +.font-small { + font-size: 12px; +} + +.font-medium { + font-size: 1.2em; +} + +hr.horizontal-line { + display: block; + height: 1px; + border: 0; + border-top: 1px solid #ccc; + margin: 1em 0; + padding: 0; +} + +.hover-white:hover { + background: white; +} + +.red-dot { + background: #E91550; + color: white; + border-radius: 10px; +} + +.diamond { + transform: rotate(45deg); + background: #038789; +} + +.hollow-diamond { + transform: rotate(45deg); + border: 3px solid #690496; +} + +.golden-square { + background: #EBB33F; +} + +.pending-dot { + background: red; + left: 14px; + top: 14px; + color: white; + border-radius: 10px; + height: 20px; + min-width: 20px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + z-index: 1; +} + +.keyring-label { + z-index: 1; + font-size: 11px; + background: rgba(255,0,0,0.8); + bottom: -47px; + color: white; + border-radius: 10px; + height: 20px; + min-width: 20px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding: 4px; +} + +.ether-balance { + display: flex; + align-items: center; +} + +.menu-icon { + display: inline-block; + height: 9px; + min-width: 9px; + margin: 13px; +} +.ether-icon { + background: rgb(0, 163, 68); + border-radius: 20px; +} +.testnet-icon { + background: #2465E1; +} + +.drop-menu-item { + display: flex; + align-items: center; +} + +.invisible { + visibility: hidden; +} + +.one-line-concat { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.critical-error { + text-align: center; + margin-top: 20px; + color: red; +} diff --git a/ui/classic/app/css/reset.css b/ui/classic/app/css/reset.css new file mode 100644 index 000000000..9ce89e8bc --- /dev/null +++ b/ui/classic/app/css/reset.css @@ -0,0 +1,48 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +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; +} \ No newline at end of file diff --git a/ui/classic/app/css/transitions.css b/ui/classic/app/css/transitions.css new file mode 100644 index 000000000..393a944f9 --- /dev/null +++ b/ui/classic/app/css/transitions.css @@ -0,0 +1,42 @@ +/* universal */ +.app-primary .main-enter { + position: absolute; + width: 100%; +} + +/* center position */ +.app-primary.from-right .main-enter-active, +.app-primary.from-left .main-enter-active { + overflow-x: hidden; + transform: translateX(0px); + transition: transform 300ms ease-in; +} + +/* exited positions */ +.app-primary.from-left .main-leave-active { + transform: translateX(360px); + transition: transform 300ms ease-in; +} +.app-primary.from-right .main-leave-active { + transform: translateX(-360px); + transition: transform 300ms ease-in; +} + +/* loader transitions */ +.loader-enter, .loader-leave-active { + opacity: 0.0; + transition: opacity 150 ease-in; +} +.loader-enter-active, .loader-leave { + opacity: 1.0; + transition: opacity 150 ease-in; +} + +/* entering positions */ +.app-primary.from-right .main-enter:not(.main-enter-active) { + transform: translateX(360px); +} +.app-primary.from-left .main-enter:not(.main-enter-active) { + transform: translateX(-360px); +} + diff --git a/ui/classic/app/first-time/init-menu.js b/ui/classic/app/first-time/init-menu.js new file mode 100644 index 000000000..cc7c51bd3 --- /dev/null +++ b/ui/classic/app/first-time/init-menu.js @@ -0,0 +1,179 @@ +const inherits = require('util').inherits +const EventEmitter = require('events').EventEmitter +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const Mascot = require('../components/mascot') +const actions = require('../actions') +const Tooltip = require('../components/tooltip') +const getCaretCoordinates = require('textarea-caret') + +module.exports = connect(mapStateToProps)(InitializeMenuScreen) + +inherits(InitializeMenuScreen, Component) +function InitializeMenuScreen () { + Component.call(this) + this.animationEventEmitter = new EventEmitter() +} + +function mapStateToProps (state) { + return { + // state from plugin + currentView: state.appState.currentView, + warning: state.appState.warning, + } +} + +InitializeMenuScreen.prototype.render = function () { + var state = this.props + + switch (state.currentView.name) { + + default: + return this.renderMenu(state) + + } +} + +// InitializeMenuScreen.prototype.componentDidMount = function(){ +// document.getElementById('password-box').focus() +// } + +InitializeMenuScreen.prototype.renderMenu = function (state) { + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + h(Mascot, { + animationEventEmitter: this.animationEventEmitter, + }), + + h('h1', { + style: { + fontSize: '1.3em', + textTransform: 'uppercase', + color: '#7F8082', + marginBottom: 10, + }, + }, 'MetaMask'), + + + h('div', [ + h('h3', { + style: { + fontSize: '0.8em', + color: '#7F8082', + display: 'inline', + }, + }, 'Encrypt your new DEN'), + + h(Tooltip, { + title: 'Your DEN is your password-encrypted storage within MetaMask.', + }, [ + h('i.fa.fa-question-circle.pointer', { + style: { + fontSize: '18px', + position: 'relative', + color: 'rgb(247, 134, 28)', + top: '2px', + marginLeft: '4px', + }, + }), + ]), + ]), + + h('span.in-progress-notification', state.warning), + + // password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: 'New Password (min 8 chars)', + onInput: this.inputChanged.bind(this), + style: { + width: 260, + marginTop: 12, + }, + }), + + // confirm password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box-confirm', + placeholder: 'Confirm Password', + onKeyPress: this.createVaultOnEnter.bind(this), + onInput: this.inputChanged.bind(this), + style: { + width: 260, + marginTop: 16, + }, + }), + + + h('button.primary', { + onClick: this.createNewVaultAndKeychain.bind(this), + style: { + margin: 12, + }, + }, 'Create'), + + h('.flex-row.flex-center.flex-grow', [ + h('p.pointer', { + onClick: this.showRestoreVault.bind(this), + style: { + fontSize: '0.8em', + color: 'rgb(247, 134, 28)', + textDecoration: 'underline', + }, + }, 'Import Existing DEN'), + ]), + + ]) + ) +} + +InitializeMenuScreen.prototype.createVaultOnEnter = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.createNewVaultAndKeychain() + } +} + +InitializeMenuScreen.prototype.componentDidMount = function () { + document.getElementById('password-box').focus() +} + +InitializeMenuScreen.prototype.showRestoreVault = function () { + this.props.dispatch(actions.showRestoreVault()) +} + +InitializeMenuScreen.prototype.createNewVaultAndKeychain = function () { + var passwordBox = document.getElementById('password-box') + var password = passwordBox.value + var passwordConfirmBox = document.getElementById('password-box-confirm') + var passwordConfirm = passwordConfirmBox.value + + if (password.length < 8) { + this.warning = 'password not long enough' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + if (password !== passwordConfirm) { + this.warning = 'passwords don\'t match' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + + this.props.dispatch(actions.createNewVaultAndKeychain(password)) +} + +InitializeMenuScreen.prototype.inputChanged = function (event) { + // tell mascot to look at page action + var element = event.target + var boundingRect = element.getBoundingClientRect() + var coordinates = getCaretCoordinates(element, element.selectionEnd) + this.animationEventEmitter.emit('point', { + x: boundingRect.left + coordinates.left - element.scrollLeft, + y: boundingRect.top + coordinates.top - element.scrollTop, + }) +} diff --git a/ui/classic/app/img/identicon-tardigrade.png b/ui/classic/app/img/identicon-tardigrade.png new file mode 100644 index 000000000..1742a32b8 Binary files /dev/null and b/ui/classic/app/img/identicon-tardigrade.png differ diff --git a/ui/classic/app/img/identicon-walrus.png b/ui/classic/app/img/identicon-walrus.png new file mode 100644 index 000000000..d58fae912 Binary files /dev/null and b/ui/classic/app/img/identicon-walrus.png differ diff --git a/ui/classic/app/info.js b/ui/classic/app/info.js new file mode 100644 index 000000000..e8470de97 --- /dev/null +++ b/ui/classic/app/info.js @@ -0,0 +1,154 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') + +module.exports = connect(mapStateToProps)(InfoScreen) + +function mapStateToProps (state) { + return {} +} + +inherits(InfoScreen, Component) +function InfoScreen () { + Component.call(this) +} + +InfoScreen.prototype.render = function () { + const state = this.props + const version = global.platform.getVersion() + + return ( + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + state.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Info'), + ]), + + // main view + h('.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-space-around', { + style: { + padding: '20px', + }, + }, [ + // current version number + + h('.info.info-gray', [ + h('div', 'Metamask'), + h('div', { + style: { + marginBottom: '10px', + }, + }, `Version: ${version}`), + ]), + + h('div', { + style: { + marginBottom: '5px', + }}, + [ + h('div', [ + h('a', { + href: 'https://metamask.io/privacy.html', + target: '_blank', + onClick (event) { this.navigateTo(event.target.href) }, + }, [ + h('div.info', 'Privacy Policy'), + ]), + ]), + h('div', [ + h('a', { + href: 'https://metamask.io/terms.html', + target: '_blank', + onClick (event) { this.navigateTo(event.target.href) }, + }, [ + h('div.info', 'Terms of Use'), + ]), + ]), + h('div', [ + h('a', { + href: 'https://metamask.io/attributions.html', + target: '_blank', + onClick (event) { this.navigateTo(event.target.href) }, + }, [ + h('div.info', 'Attributions'), + ]), + ]), + ] + ), + + h('hr', { + style: { + margin: '10px 0 ', + width: '7em', + }, + }), + + h('div', { + style: { + paddingLeft: '30px', + }}, + [ + h('div.fa.fa-github', [ + h('a.info', { + href: 'https://github.com/MetaMask/faq', + target: '_blank', + }, 'Need Help? Read our FAQ!'), + ]), + h('div', [ + h('a', { + href: 'https://metamask.io/', + target: '_blank', + }, [ + h('img.icon-size', { + src: 'images/icon-128.png', + style: { + // IE6-9 + filter: 'grayscale(100%)', + // Microsoft Edge and Firefox 35+ + WebkitFilter: 'grayscale(100%)', + }, + }), + h('div.info', 'Visit our web site'), + ]), + ]), + h('div.fa.fa-slack', [ + h('a.info', { + href: 'http://slack.metamask.io', + target: '_blank', + }, 'Join the conversation on Slack'), + ]), + + h('div.fa.fa-twitter', [ + h('a.info', { + href: 'https://twitter.com/metamask_io', + target: '_blank', + }, 'Follow us on Twitter'), + ]), + + h('div.fa.fa-envelope', [ + h('a.info', { + target: '_blank', + style: { width: '85vw' }, + href: 'mailto:help@metamask.io?subject=Feedback', + }, 'Email us!'), + ]), + ]), + ]), + ]), + ]) + ) +} + +InfoScreen.prototype.navigateTo = function (url) { + global.platform.openWindow({ url }) +} + diff --git a/ui/classic/app/keychains/hd/create-vault-complete.js b/ui/classic/app/keychains/hd/create-vault-complete.js new file mode 100644 index 000000000..a318a9b50 --- /dev/null +++ b/ui/classic/app/keychains/hd/create-vault-complete.js @@ -0,0 +1,78 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('../../actions') + +module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen) + +inherits(CreateVaultCompleteScreen, Component) +function CreateVaultCompleteScreen () { + Component.call(this) +} + +function mapStateToProps (state) { + return { + seed: state.appState.currentView.seedWords, + cachedSeed: state.metamask.seedWords, + } +} + +CreateVaultCompleteScreen.prototype.render = function () { + var state = this.props + var seed = state.seed || state.cachedSeed || '' + + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + // // subtitle and nav + // h('.section-title.flex-row.flex-center', [ + // h('h2.page-subtitle', 'Vault Created'), + // ]), + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginTop: 36, + marginBottom: 8, + width: '100%', + fontSize: '20px', + padding: 6, + }, + }, [ + 'Vault Created', + ]), + + h('div', { + style: { + width: '360px', + height: '78px', + fontSize: '1em', + marginTop: '10px', + textAlign: 'center', + }, + }, [ + h('span.error', 'These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret.'), + ]), + + h('textarea.twelve-word-phrase', { + readOnly: true, + value: seed, + }), + + h('button.primary', { + onClick: () => this.confirmSeedWords(), + style: { + margin: '24px', + fontSize: '0.9em', + }, + }, 'I\'ve copied it somewhere safe'), + ]) + ) +} + +CreateVaultCompleteScreen.prototype.confirmSeedWords = function () { + this.props.dispatch(actions.confirmSeedWords()) +} diff --git a/ui/classic/app/keychains/hd/recover-seed/confirmation.js b/ui/classic/app/keychains/hd/recover-seed/confirmation.js new file mode 100644 index 000000000..4ccbec9fc --- /dev/null +++ b/ui/classic/app/keychains/hd/recover-seed/confirmation.js @@ -0,0 +1,118 @@ +const inherits = require('util').inherits + +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('../../../actions') + +module.exports = connect(mapStateToProps)(RevealSeedConfirmation) + +inherits(RevealSeedConfirmation, Component) +function RevealSeedConfirmation () { + Component.call(this) +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +RevealSeedConfirmation.prototype.render = function () { + const props = this.props + + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginBottom: 24, + width: '100%', + fontSize: '20px', + padding: 6, + }, + }, [ + 'Reveal Seed Words', + ]), + + h('.div', { + style: { + display: 'flex', + flexDirection: 'column', + padding: '20px', + justifyContent: 'center', + }, + }, [ + + h('h4', 'Do not recover your seed words in a public place! These words can be used to steal all your accounts.'), + + // confirmation + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: 'Enter your password to confirm', + onKeyPress: this.checkConfirmation.bind(this), + style: { + width: 260, + marginTop: '12px', + }, + }), + + h('.flex-row.flex-space-between', { + style: { + marginTop: 30, + width: '50%', + }, + }, [ + // cancel + h('button.primary', { + onClick: this.goHome.bind(this), + }, 'CANCEL'), + + // submit + h('button.primary', { + onClick: this.revealSeedWords.bind(this), + }, 'OK'), + + ]), + + (props.warning) && ( + h('span.error', { + style: { + margin: '20px', + }, + }, props.warning.split('-')) + ), + + props.inProgress && ( + h('span.in-progress-notification', 'Generating Seed...') + ), + ]), + ]) + ) +} + +RevealSeedConfirmation.prototype.componentDidMount = function () { + document.getElementById('password-box').focus() +} + +RevealSeedConfirmation.prototype.goHome = function () { + this.props.dispatch(actions.showConfigPage(false)) +} + +// create vault + +RevealSeedConfirmation.prototype.checkConfirmation = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.revealSeedWords() + } +} + +RevealSeedConfirmation.prototype.revealSeedWords = function () { + var password = document.getElementById('password-box').value + this.props.dispatch(actions.requestRevealSeed(password)) +} diff --git a/ui/classic/app/keychains/hd/restore-vault.js b/ui/classic/app/keychains/hd/restore-vault.js new file mode 100644 index 000000000..06e51d9b3 --- /dev/null +++ b/ui/classic/app/keychains/hd/restore-vault.js @@ -0,0 +1,152 @@ +const inherits = require('util').inherits +const PersistentForm = require('../../../lib/persistent-form') +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('../../actions') + +module.exports = connect(mapStateToProps)(RestoreVaultScreen) + +inherits(RestoreVaultScreen, PersistentForm) +function RestoreVaultScreen () { + PersistentForm.call(this) +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + forgottenPassword: state.appState.forgottenPassword, + } +} + +RestoreVaultScreen.prototype.render = function () { + var state = this.props + this.persistentFormParentId = 'restore-vault-form' + + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginBottom: 24, + width: '100%', + fontSize: '20px', + padding: 6, + }, + }, [ + 'Restore Vault', + ]), + + // wallet seed entry + h('h3', 'Wallet Seed'), + h('textarea.twelve-word-phrase.letter-spacey', { + dataset: { + persistentFormId: 'wallet-seed', + }, + placeholder: 'Enter your secret twelve word phrase here to restore your vault.', + }), + + // password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: 'New Password (min 8 chars)', + dataset: { + persistentFormId: 'password', + }, + style: { + width: 260, + marginTop: 12, + }, + }), + + // confirm password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box-confirm', + placeholder: 'Confirm Password', + onKeyPress: this.createOnEnter.bind(this), + dataset: { + persistentFormId: 'password-confirmation', + }, + style: { + width: 260, + marginTop: 16, + }, + }), + + (state.warning) && ( + h('span.error.in-progress-notification', state.warning) + ), + + // submit + + h('.flex-row.flex-space-between', { + style: { + marginTop: 30, + width: '50%', + }, + }, [ + + // cancel + h('button.primary', { + onClick: this.showInitializeMenu.bind(this), + }, 'CANCEL'), + + // submit + h('button.primary', { + onClick: this.createNewVaultAndRestore.bind(this), + }, 'OK'), + + ]), + ]) + + ) +} + +RestoreVaultScreen.prototype.showInitializeMenu = function () { + if (this.props.forgottenPassword) { + this.props.dispatch(actions.backToUnlockView()) + } else { + this.props.dispatch(actions.showInitializeMenu()) + } +} + +RestoreVaultScreen.prototype.createOnEnter = function (event) { + if (event.key === 'Enter') { + this.createNewVaultAndRestore() + } +} + +RestoreVaultScreen.prototype.createNewVaultAndRestore = function () { + // check password + var passwordBox = document.getElementById('password-box') + var password = passwordBox.value + var passwordConfirmBox = document.getElementById('password-box-confirm') + var passwordConfirm = passwordConfirmBox.value + if (password.length < 8) { + this.warning = 'Password not long enough' + + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + if (password !== passwordConfirm) { + this.warning = 'Passwords don\'t match' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + // check seed + var seedBox = document.querySelector('textarea.twelve-word-phrase') + var seed = seedBox.value.trim() + if (seed.split(' ').length !== 12) { + this.warning = 'seed phrases are 12 words long' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + // submit + this.warning = null + this.props.dispatch(actions.displayWarning(this.warning)) + this.props.dispatch(actions.createNewVaultAndRestore(password, seed)) +} diff --git a/ui/classic/app/new-keychain.js b/ui/classic/app/new-keychain.js new file mode 100644 index 000000000..cc9633166 --- /dev/null +++ b/ui/classic/app/new-keychain.js @@ -0,0 +1,29 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect + +module.exports = connect(mapStateToProps)(NewKeychain) + +function mapStateToProps (state) { + return {} +} + +inherits(NewKeychain, Component) +function NewKeychain () { + Component.call(this) +} + +NewKeychain.prototype.render = function () { + // const props = this.props + + return ( + h('div', { + style: { + background: 'blue', + }, + }, [ + h('h1', `Here's a list!!!!`), + ]) + ) +} diff --git a/ui/classic/app/reducers.js b/ui/classic/app/reducers.js new file mode 100644 index 000000000..11efca529 --- /dev/null +++ b/ui/classic/app/reducers.js @@ -0,0 +1,52 @@ +const extend = require('xtend') + +// +// Sub-Reducers take in the complete state and return their sub-state +// +const reduceIdentities = require('./reducers/identities') +const reduceMetamask = require('./reducers/metamask') +const reduceApp = require('./reducers/app') + +window.METAMASK_CACHED_LOG_STATE = null + +module.exports = rootReducer + +function rootReducer (state, action) { + // clone + state = extend(state) + + if (action.type === 'GLOBAL_FORCE_UPDATE') { + return action.value + } + + // + // Identities + // + + state.identities = reduceIdentities(state, action) + + // + // MetaMask + // + + state.metamask = reduceMetamask(state, action) + + // + // AppState + // + + state.appState = reduceApp(state, action) + + window.METAMASK_CACHED_LOG_STATE = state + return state +} + +window.logState = function () { + var stateString = JSON.stringify(window.METAMASK_CACHED_LOG_STATE, removeSeedWords, 2) + console.log(stateString) + return stateString +} + +function removeSeedWords (key, value) { + return key === 'seedWords' ? undefined : value +} diff --git a/ui/classic/app/reducers/app.js b/ui/classic/app/reducers/app.js new file mode 100644 index 000000000..2fcc9bfe0 --- /dev/null +++ b/ui/classic/app/reducers/app.js @@ -0,0 +1,585 @@ +const extend = require('xtend') +const actions = require('../actions') +const txHelper = require('../../lib/tx-helper') + +module.exports = reduceApp + + +function reduceApp (state, action) { + log.debug('App Reducer got ' + action.type) + // clone and defaults + const selectedAddress = state.metamask.selectedAddress + const hasUnconfActions = checkUnconfActions(state) + let name = 'accounts' + if (selectedAddress) { + name = 'accountDetail' + } + if (hasUnconfActions) { + log.debug('pending txs detected, defaulting to conf-tx view.') + name = 'confTx' + } + + var defaultView = { + name, + detailView: null, + context: selectedAddress, + } + + // confirm seed words + var seedWords = state.metamask.seedWords + var seedConfView = { + name: 'createVaultComplete', + seedWords, + } + + // default state + var appState = extend({ + shouldClose: false, + menuOpen: false, + currentView: seedWords ? seedConfView : defaultView, + accountDetail: { + subview: 'transactions', + }, + transForward: true, // Used to render transition direction + isLoading: false, // Used to display loading indicator + warning: null, // Used to display error text + }, state.appState) + + switch (action.type) { + + // transition methods + + case actions.TRANSITION_FORWARD: + return extend(appState, { + transForward: true, + }) + + case actions.TRANSITION_BACKWARD: + return extend(appState, { + transForward: false, + }) + + // intialize + + case actions.SHOW_CREATE_VAULT: + return extend(appState, { + currentView: { + name: 'createVault', + }, + transForward: true, + warning: null, + }) + + case actions.SHOW_RESTORE_VAULT: + return extend(appState, { + currentView: { + name: 'restoreVault', + }, + transForward: true, + forgottenPassword: true, + }) + + case actions.FORGOT_PASSWORD: + return extend(appState, { + currentView: { + name: 'restoreVault', + }, + transForward: false, + forgottenPassword: true, + }) + + case actions.SHOW_INIT_MENU: + return extend(appState, { + currentView: defaultView, + transForward: false, + }) + + case actions.SHOW_CONFIG_PAGE: + return extend(appState, { + currentView: { + name: 'config', + context: appState.currentView.context, + }, + transForward: action.value, + }) + + case actions.SHOW_ADD_TOKEN_PAGE: + return extend(appState, { + currentView: { + name: 'add-token', + context: appState.currentView.context, + }, + transForward: action.value, + }) + + case actions.SHOW_IMPORT_PAGE: + + return extend(appState, { + currentView: { + name: 'import-menu', + }, + transForward: true, + }) + + case actions.SHOW_INFO_PAGE: + return extend(appState, { + currentView: { + name: 'info', + context: appState.currentView.context, + }, + transForward: true, + }) + + case actions.CREATE_NEW_VAULT_IN_PROGRESS: + return extend(appState, { + currentView: { + name: 'createVault', + inProgress: true, + }, + transForward: true, + isLoading: true, + }) + + case actions.SHOW_NEW_VAULT_SEED: + return extend(appState, { + currentView: { + name: 'createVaultComplete', + seedWords: action.value, + }, + transForward: true, + isLoading: false, + }) + + case actions.NEW_ACCOUNT_SCREEN: + return extend(appState, { + currentView: { + name: 'new-account', + context: appState.currentView.context, + }, + transForward: true, + }) + + case actions.SHOW_SEND_PAGE: + return extend(appState, { + currentView: { + name: 'sendTransaction', + context: appState.currentView.context, + }, + transForward: true, + warning: null, + }) + + case actions.SHOW_NEW_KEYCHAIN: + return extend(appState, { + currentView: { + name: 'newKeychain', + context: appState.currentView.context, + }, + transForward: true, + }) + + // unlock + + case actions.UNLOCK_METAMASK: + return extend(appState, { + forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null, + detailView: {}, + transForward: true, + isLoading: false, + warning: null, + }) + + case actions.LOCK_METAMASK: + return extend(appState, { + currentView: defaultView, + transForward: false, + warning: null, + }) + + case actions.BACK_TO_INIT_MENU: + return extend(appState, { + warning: null, + transForward: false, + forgottenPassword: true, + currentView: { + name: 'InitMenu', + }, + }) + + case actions.BACK_TO_UNLOCK_VIEW: + return extend(appState, { + warning: null, + transForward: true, + forgottenPassword: false, + currentView: { + name: 'UnlockScreen', + }, + }) + // reveal seed words + + case actions.REVEAL_SEED_CONFIRMATION: + return extend(appState, { + currentView: { + name: 'reveal-seed-conf', + }, + transForward: true, + warning: null, + }) + + // accounts + + case actions.SET_SELECTED_ACCOUNT: + return extend(appState, { + activeAddress: action.value, + }) + + case actions.GO_HOME: + return extend(appState, { + currentView: extend(appState.currentView, { + name: 'accountDetail', + }), + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + transForward: false, + warning: null, + }) + + case actions.SHOW_ACCOUNT_DETAIL: + return extend(appState, { + forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null, + currentView: { + name: 'accountDetail', + context: action.value, + }, + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + transForward: false, + }) + + case actions.BACK_TO_ACCOUNT_DETAIL: + return extend(appState, { + currentView: { + name: 'accountDetail', + context: action.value, + }, + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + transForward: false, + }) + + case actions.SHOW_ACCOUNTS_PAGE: + return extend(appState, { + currentView: { + name: seedWords ? 'createVaultComplete' : 'accounts', + seedWords, + }, + transForward: true, + isLoading: false, + warning: null, + scrollToBottom: false, + forgottenPassword: false, + }) + + case actions.SHOW_NOTICE: + return extend(appState, { + transForward: true, + isLoading: false, + }) + + case actions.REVEAL_ACCOUNT: + return extend(appState, { + scrollToBottom: true, + }) + + case actions.SHOW_CONF_TX_PAGE: + return extend(appState, { + currentView: { + name: 'confTx', + context: 0, + }, + transForward: action.transForward, + warning: null, + isLoading: false, + }) + + case actions.SHOW_CONF_MSG_PAGE: + return extend(appState, { + currentView: { + name: hasUnconfActions ? 'confTx' : 'account-detail', + context: 0, + }, + transForward: true, + warning: null, + isLoading: false, + }) + + case actions.COMPLETED_TX: + log.debug('reducing COMPLETED_TX for tx ' + action.value) + const otherUnconfActions = getUnconfActionList(state) + .filter(tx => tx.id !== action.value) + const hasOtherUnconfActions = otherUnconfActions.length > 0 + + if (hasOtherUnconfActions) { + log.debug('reducer detected txs - rendering confTx view') + return extend(appState, { + transForward: false, + currentView: { + name: 'confTx', + context: 0, + }, + warning: null, + }) + } else { + log.debug('attempting to close popup') + return extend(appState, { + // indicate notification should close + shouldClose: true, + transForward: false, + warning: null, + currentView: { + name: 'accountDetail', + context: state.metamask.selectedAddress, + }, + accountDetail: { + subview: 'transactions', + }, + }) + } + + case actions.NEXT_TX: + return extend(appState, { + transForward: true, + currentView: { + name: 'confTx', + context: ++appState.currentView.context, + warning: null, + }, + }) + + case actions.VIEW_PENDING_TX: + const context = indexForPending(state, action.value) + return extend(appState, { + transForward: true, + currentView: { + name: 'confTx', + context, + warning: null, + }, + }) + + case actions.PREVIOUS_TX: + return extend(appState, { + transForward: false, + currentView: { + name: 'confTx', + context: --appState.currentView.context, + warning: null, + }, + }) + + case actions.TRANSACTION_ERROR: + return extend(appState, { + currentView: { + name: 'confTx', + errorMessage: 'There was a problem submitting this transaction.', + }, + }) + + case actions.UNLOCK_FAILED: + return extend(appState, { + warning: action.value || 'Incorrect password. Try again.', + }) + + case actions.SHOW_LOADING: + return extend(appState, { + isLoading: true, + loadingMessage: action.value, + }) + + case actions.HIDE_LOADING: + return extend(appState, { + isLoading: false, + }) + + case actions.SHOW_SUB_LOADING_INDICATION: + return extend(appState, { + isSubLoading: true, + }) + + case actions.HIDE_SUB_LOADING_INDICATION: + return extend(appState, { + isSubLoading: false, + }) + case actions.CLEAR_SEED_WORD_CACHE: + return extend(appState, { + transForward: true, + currentView: {}, + isLoading: false, + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + }) + + case actions.DISPLAY_WARNING: + return extend(appState, { + warning: action.value, + isLoading: false, + }) + + case actions.HIDE_WARNING: + return extend(appState, { + warning: undefined, + }) + + case actions.REQUEST_ACCOUNT_EXPORT: + return extend(appState, { + transForward: true, + currentView: { + name: 'accountDetail', + context: appState.currentView.context, + }, + accountDetail: { + subview: 'export', + accountExport: 'requested', + }, + }) + + case actions.EXPORT_ACCOUNT: + return extend(appState, { + accountDetail: { + subview: 'export', + accountExport: 'completed', + }, + }) + + case actions.SHOW_PRIVATE_KEY: + return extend(appState, { + accountDetail: { + subview: 'export', + accountExport: 'completed', + privateKey: action.value, + }, + }) + + case actions.BUY_ETH_VIEW: + return extend(appState, { + transForward: true, + currentView: { + name: 'buyEth', + context: appState.currentView.name, + }, + identity: state.metamask.identities[action.value], + buyView: { + subview: 'Coinbase', + amount: '15.00', + buyAddress: action.value, + formView: { + coinbase: true, + shapeshift: false, + }, + }, + }) + + case actions.COINBASE_SUBVIEW: + return extend(appState, { + buyView: { + subview: 'Coinbase', + formView: { + coinbase: true, + shapeshift: false, + }, + buyAddress: appState.buyView.buyAddress, + amount: appState.buyView.amount, + }, + }) + + case actions.SHAPESHIFT_SUBVIEW: + return extend(appState, { + buyView: { + subview: 'ShapeShift', + formView: { + coinbase: false, + shapeshift: true, + marketinfo: action.value.marketinfo, + coinOptions: action.value.coinOptions, + }, + buyAddress: appState.buyView.buyAddress, + amount: appState.buyView.amount, + }, + }) + + case actions.PAIR_UPDATE: + return extend(appState, { + buyView: { + subview: 'ShapeShift', + formView: { + coinbase: false, + shapeshift: true, + marketinfo: action.value.marketinfo, + coinOptions: appState.buyView.formView.coinOptions, + }, + buyAddress: appState.buyView.buyAddress, + amount: appState.buyView.amount, + warning: null, + }, + }) + + case actions.SHOW_QR: + return extend(appState, { + qrRequested: true, + transForward: true, + + Qr: { + message: action.value.message, + data: action.value.data, + }, + }) + + case actions.SHOW_QR_VIEW: + return extend(appState, { + currentView: { + name: 'qr', + context: appState.currentView.context, + }, + transForward: true, + Qr: { + message: action.value.message, + data: action.value.data, + }, + }) + default: + return appState + } +} + +function checkUnconfActions (state) { + const unconfActionList = getUnconfActionList(state) + const hasUnconfActions = unconfActionList.length > 0 + return hasUnconfActions +} + +function getUnconfActionList (state) { + const { unapprovedTxs, unapprovedMsgs, + unapprovedPersonalMsgs, network } = state.metamask + + const unconfActionList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) + return unconfActionList +} + +function indexForPending (state, txId) { + const unconfTxList = getUnconfActionList(state) + const match = unconfTxList.find((tx) => tx.id === txId) + const index = unconfTxList.indexOf(match) + return index +} diff --git a/ui/classic/app/reducers/identities.js b/ui/classic/app/reducers/identities.js new file mode 100644 index 000000000..341a404e7 --- /dev/null +++ b/ui/classic/app/reducers/identities.js @@ -0,0 +1,15 @@ +const extend = require('xtend') + +module.exports = reduceIdentities + +function reduceIdentities (state, action) { + // clone + defaults + var idState = extend({ + + }, state.identities) + + switch (action.type) { + default: + return idState + } +} diff --git a/ui/classic/app/reducers/metamask.js b/ui/classic/app/reducers/metamask.js new file mode 100644 index 000000000..e0c416c2d --- /dev/null +++ b/ui/classic/app/reducers/metamask.js @@ -0,0 +1,137 @@ +const extend = require('xtend') +const actions = require('../actions') + +module.exports = reduceMetamask + +function reduceMetamask (state, action) { + let newState + + // clone + defaults + var metamaskState = extend({ + isInitialized: false, + isUnlocked: false, + rpcTarget: 'https://rawtestrpc.metamask.io/', + identities: {}, + unapprovedTxs: {}, + noActiveNotices: true, + lastUnreadNotice: undefined, + frequentRpcList: [], + addressBook: [], + }, state.metamask) + + switch (action.type) { + + case actions.SHOW_ACCOUNTS_PAGE: + newState = extend(metamaskState) + delete newState.seedWords + return newState + + case actions.SHOW_NOTICE: + return extend(metamaskState, { + noActiveNotices: false, + lastUnreadNotice: action.value, + }) + + case actions.CLEAR_NOTICES: + return extend(metamaskState, { + noActiveNotices: true, + }) + + case actions.UPDATE_METAMASK_STATE: + return extend(metamaskState, action.value) + + case actions.UNLOCK_METAMASK: + return extend(metamaskState, { + isUnlocked: true, + isInitialized: true, + selectedAddress: action.value, + }) + + case actions.LOCK_METAMASK: + return extend(metamaskState, { + isUnlocked: false, + }) + + case actions.SET_RPC_LIST: + return extend(metamaskState, { + frequentRpcList: action.value, + }) + + case actions.SET_RPC_TARGET: + return extend(metamaskState, { + provider: { + type: 'rpc', + rpcTarget: action.value, + }, + }) + + case actions.SET_PROVIDER_TYPE: + return extend(metamaskState, { + provider: { + type: action.value, + }, + }) + + case actions.COMPLETED_TX: + var stringId = String(action.id) + newState = extend(metamaskState, { + unapprovedTxs: {}, + unapprovedMsgs: {}, + }) + for (const id in metamaskState.unapprovedTxs) { + if (id !== stringId) { + newState.unapprovedTxs[id] = metamaskState.unapprovedTxs[id] + } + } + for (const id in metamaskState.unapprovedMsgs) { + if (id !== stringId) { + newState.unapprovedMsgs[id] = metamaskState.unapprovedMsgs[id] + } + } + return newState + + case actions.SHOW_NEW_VAULT_SEED: + return extend(metamaskState, { + isUnlocked: true, + isInitialized: false, + seedWords: action.value, + }) + + case actions.CLEAR_SEED_WORD_CACHE: + newState = extend(metamaskState, { + isUnlocked: true, + isInitialized: true, + selectedAddress: action.value, + }) + delete newState.seedWords + return newState + + case actions.SHOW_ACCOUNT_DETAIL: + newState = extend(metamaskState, { + isUnlocked: true, + isInitialized: true, + selectedAddress: action.value, + }) + delete newState.seedWords + return newState + + case actions.SAVE_ACCOUNT_LABEL: + const account = action.value.account + const name = action.value.label + var id = {} + id[account] = extend(metamaskState.identities[account], { name }) + var identities = extend(metamaskState.identities, id) + return extend(metamaskState, { identities }) + + case actions.SET_CURRENT_FIAT: + return extend(metamaskState, { + currentCurrency: action.value.currentCurrency, + conversionRate: action.value.conversionRate, + conversionDate: action.value.conversionDate, + }) + + default: + return metamaskState + + } +} diff --git a/ui/classic/app/root.js b/ui/classic/app/root.js new file mode 100644 index 000000000..9e7314b20 --- /dev/null +++ b/ui/classic/app/root.js @@ -0,0 +1,22 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const Provider = require('react-redux').Provider +const h = require('react-hyperscript') +const App = require('./app') + +module.exports = Root + +inherits(Root, Component) +function Root () { Component.call(this) } + +Root.prototype.render = function () { + return ( + + h(Provider, { + store: this.props.store, + }, [ + h(App), + ]) + + ) +} diff --git a/ui/classic/app/send.js b/ui/classic/app/send.js new file mode 100644 index 000000000..a21a219eb --- /dev/null +++ b/ui/classic/app/send.js @@ -0,0 +1,288 @@ +const inherits = require('util').inherits +const PersistentForm = require('../lib/persistent-form') +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const Identicon = require('./components/identicon') +const actions = require('./actions') +const util = require('./util') +const numericBalance = require('./util').numericBalance +const addressSummary = require('./util').addressSummary +const isHex = require('./util').isHex +const EthBalance = require('./components/eth-balance') +const EnsInput = require('./components/ens-input') +const ethUtil = require('ethereumjs-util') +module.exports = connect(mapStateToProps)(SendTransactionScreen) + +function mapStateToProps (state) { + var result = { + address: state.metamask.selectedAddress, + accounts: state.metamask.accounts, + identities: state.metamask.identities, + warning: state.appState.warning, + network: state.metamask.network, + addressBook: state.metamask.addressBook, + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + } + + result.error = result.warning && result.warning.split('.')[0] + + result.account = result.accounts[result.address] + result.identity = result.identities[result.address] + result.balance = result.account ? numericBalance(result.account.balance) : null + + return result +} + +inherits(SendTransactionScreen, PersistentForm) +function SendTransactionScreen () { + PersistentForm.call(this) +} + +SendTransactionScreen.prototype.render = function () { + this.persistentFormParentId = 'send-tx-form' + + const props = this.props + const { + address, + account, + identity, + network, + identities, + addressBook, + conversionRate, + currentCurrency, + } = props + + return ( + + h('.send-screen.flex-column.flex-grow', [ + + // + // Sender Profile + // + + h('.account-data-subsection.flex-row.flex-grow', { + style: { + margin: '0 20px', + }, + }, [ + + // header - identicon + nav + h('.flex-row.flex-space-between', { + style: { + marginTop: '15px', + }, + }, [ + // back button + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { + onClick: this.back.bind(this), + }), + + // large identicon + h('.identicon-wrapper.flex-column.flex-center.select-none', [ + h(Identicon, { + diameter: 62, + address: address, + }), + ]), + + // invisible place holder + h('i.fa.fa-users.fa-lg.invisible', { + style: { + marginTop: '28px', + }, + }), + + ]), + + // account label + + h('.flex-column', { + style: { + marginTop: '10px', + alignItems: 'flex-start', + }, + }, [ + h('h2.font-medium.color-forest.flex-center', { + style: { + paddingTop: '8px', + marginBottom: '8px', + }, + }, identity && identity.name), + + // address and getter actions + h('.flex-row.flex-center', { + style: { + marginBottom: '8px', + }, + }, [ + + h('div', { + style: { + lineHeight: '16px', + }, + }, addressSummary(address)), + + ]), + + // balance + h('.flex-row.flex-center', [ + + h(EthBalance, { + value: account && account.balance, + conversionRate, + currentCurrency, + }), + + ]), + ]), + ]), + + // + // Required Fields + // + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginTop: '15px', + marginBottom: '16px', + }, + }, [ + 'Send Transaction', + ]), + + // error message + props.error && h('span.error.flex-center', props.error), + + // 'to' field + h('section.flex-row.flex-center', [ + h(EnsInput, { + name: 'address', + placeholder: 'Recipient Address', + onChange: this.recipientDidChange.bind(this), + network, + identities, + addressBook, + }), + ]), + + // 'amount' and send button + h('section.flex-row.flex-center', [ + + h('input.large-input', { + name: 'amount', + placeholder: 'Amount', + type: 'number', + style: { + marginRight: '6px', + }, + dataset: { + persistentFormId: 'tx-amount', + }, + }), + + h('button.primary', { + onClick: this.onSubmit.bind(this), + style: { + textTransform: 'uppercase', + }, + }, 'Next'), + + ]), + + // + // Optional Fields + // + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginTop: '16px', + marginBottom: '16px', + }, + }, [ + 'Transaction Data (optional)', + ]), + + // 'data' field + h('section.flex-column.flex-center', [ + h('input.large-input', { + name: 'txData', + placeholder: '0x01234', + style: { + width: '100%', + resize: 'none', + }, + dataset: { + persistentFormId: 'tx-data', + }, + }), + ]), + ]) + ) +} + +SendTransactionScreen.prototype.navigateToAccounts = function (event) { + event.stopPropagation() + this.props.dispatch(actions.showAccountsPage()) +} + +SendTransactionScreen.prototype.back = function () { + var address = this.props.address + this.props.dispatch(actions.backToAccountDetail(address)) +} + +SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickname) { + this.setState({ + recipient: recipient, + nickname: nickname, + }) +} + +SendTransactionScreen.prototype.onSubmit = function () { + const state = this.state || {} + const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '') + const nickname = state.nickname || ' ' + const input = document.querySelector('input[name="amount"]').value + const value = util.normalizeEthStringToWei(input) + const txData = document.querySelector('input[name="txData"]').value + const balance = this.props.balance + let message + + if (value.gt(balance)) { + message = 'Insufficient funds.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if (input < 0) { + message = 'Can not send negative amounts of ETH.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) { + message = 'Recipient address is invalid.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if (!isHex(ethUtil.stripHexPrefix(txData)) && txData) { + message = 'Transaction data must be hex string.' + return this.props.dispatch(actions.displayWarning(message)) + } + + this.props.dispatch(actions.hideWarning()) + + this.props.dispatch(actions.addToAddressBook(recipient, nickname)) + + var txParams = { + from: this.props.address, + value: '0x' + value.toString(16), + } + + if (recipient) txParams.to = ethUtil.addHexPrefix(recipient) + if (txData) txParams.data = txData + + this.props.dispatch(actions.signTx(txParams)) +} diff --git a/ui/classic/app/settings.js b/ui/classic/app/settings.js new file mode 100644 index 000000000..454cc95e0 --- /dev/null +++ b/ui/classic/app/settings.js @@ -0,0 +1,59 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') + +module.exports = connect(mapStateToProps)(AppSettingsPage) + +function mapStateToProps (state) { + return {} +} + +inherits(AppSettingsPage, Component) +function AppSettingsPage () { + Component.call(this) +} + +AppSettingsPage.prototype.render = function () { + return ( + + h('.account-detail-section.flex-column.flex-grow', [ + + // subtitle and nav + h('.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: this.navigateToAccounts.bind(this), + }), + h('h2.page-subtitle', 'Settings'), + ]), + + h('label', { + htmlFor: 'settings-rpc-endpoint', + }, 'RPC Endpoint:'), + h('input', { + type: 'url', + id: 'settings-rpc-endpoint', + onKeyPress: this.onKeyPress.bind(this), + }), + + ]) + + ) +} + +AppSettingsPage.prototype.componentDidMount = function () { + document.querySelector('input').focus() +} + +AppSettingsPage.prototype.onKeyPress = function (event) { + // get submit event + if (event.key === 'Enter') { + // this.submitPassword(event) + } +} + +AppSettingsPage.prototype.navigateToAccounts = function (event) { + event.stopPropagation() + this.props.dispatch(actions.showAccountsPage()) +} diff --git a/ui/classic/app/store.js b/ui/classic/app/store.js new file mode 100644 index 000000000..ba9e58b49 --- /dev/null +++ b/ui/classic/app/store.js @@ -0,0 +1,21 @@ +const createStore = require('redux').createStore +const applyMiddleware = require('redux').applyMiddleware +const thunkMiddleware = require('redux-thunk') +const rootReducer = require('./reducers') +const createLogger = require('redux-logger') + +global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' + +module.exports = configureStore + +const loggerMiddleware = createLogger({ + predicate: () => global.METAMASK_DEBUG, +}) + +const middlewares = [thunkMiddleware, loggerMiddleware] + +const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore) + +function configureStore (initialState) { + return createStoreWithMiddleware(rootReducer, initialState) +} diff --git a/ui/classic/app/template.js b/ui/classic/app/template.js new file mode 100644 index 000000000..d15b30fd2 --- /dev/null +++ b/ui/classic/app/template.js @@ -0,0 +1,30 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect + +module.exports = connect(mapStateToProps)(COMPONENTNAME) + +function mapStateToProps (state) { + return {} +} + +inherits(COMPONENTNAME, Component) +function COMPONENTNAME () { + Component.call(this) +} + +COMPONENTNAME.prototype.render = function () { + const props = this.props + + return ( + h('div', { + style: { + background: 'blue', + }, + }, [ + `Hello, ${props.sender}`, + ]) + ) +} + diff --git a/ui/classic/app/unlock.js b/ui/classic/app/unlock.js new file mode 100644 index 000000000..1aee3c5d0 --- /dev/null +++ b/ui/classic/app/unlock.js @@ -0,0 +1,118 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') +const getCaretCoordinates = require('textarea-caret') +const EventEmitter = require('events').EventEmitter + +const Mascot = require('./components/mascot') + +module.exports = connect(mapStateToProps)(UnlockScreen) + +inherits(UnlockScreen, Component) +function UnlockScreen () { + Component.call(this) + this.animationEventEmitter = new EventEmitter() +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +UnlockScreen.prototype.render = function () { + const state = this.props + const warning = state.warning + return ( + h('.flex-column', [ + h('.unlock-screen.flex-column.flex-center.flex-grow', [ + + h(Mascot, { + animationEventEmitter: this.animationEventEmitter, + }), + + h('h1', { + style: { + fontSize: '1.4em', + textTransform: 'uppercase', + color: '#7F8082', + }, + }, 'MetaMask'), + + h('input.large-input', { + type: 'password', + id: 'password-box', + placeholder: 'enter password', + style: { + + }, + onKeyPress: this.onKeyPress.bind(this), + onInput: this.inputChanged.bind(this), + }), + + h('.error', { + style: { + display: warning ? 'block' : 'none', + padding: '0 20px', + textAlign: 'center', + }, + }, warning), + + h('button.primary.cursor-pointer', { + onClick: this.onSubmit.bind(this), + style: { + margin: 10, + }, + }, 'Unlock'), + ]), + + h('.flex-row.flex-center.flex-grow', [ + h('p.pointer', { + onClick: () => this.props.dispatch(actions.forgotPassword()), + style: { + fontSize: '0.8em', + color: 'rgb(247, 134, 28)', + textDecoration: 'underline', + }, + }, 'I forgot my password.'), + ]), + ]) + ) +} + +UnlockScreen.prototype.componentDidMount = function () { + document.getElementById('password-box').focus() +} + +UnlockScreen.prototype.onSubmit = function (event) { + const input = document.getElementById('password-box') + const password = input.value + this.props.dispatch(actions.tryUnlockMetamask(password)) +} + +UnlockScreen.prototype.onKeyPress = function (event) { + if (event.key === 'Enter') { + this.submitPassword(event) + } +} + +UnlockScreen.prototype.submitPassword = function (event) { + var element = event.target + var password = element.value + // reset input + element.value = '' + this.props.dispatch(actions.tryUnlockMetamask(password)) +} + +UnlockScreen.prototype.inputChanged = function (event) { + // tell mascot to look at page action + var element = event.target + var boundingRect = element.getBoundingClientRect() + var coordinates = getCaretCoordinates(element, element.selectionEnd) + this.animationEventEmitter.emit('point', { + x: boundingRect.left + coordinates.left - element.scrollLeft, + y: boundingRect.top + coordinates.top - element.scrollTop, + }) +} diff --git a/ui/classic/app/util.js b/ui/classic/app/util.js new file mode 100644 index 000000000..ac3f42c6b --- /dev/null +++ b/ui/classic/app/util.js @@ -0,0 +1,217 @@ +const ethUtil = require('ethereumjs-util') + +var valueTable = { + wei: '1000000000000000000', + kwei: '1000000000000000', + mwei: '1000000000000', + gwei: '1000000000', + szabo: '1000000', + finney: '1000', + ether: '1', + kether: '0.001', + mether: '0.000001', + gether: '0.000000001', + tether: '0.000000000001', +} +var bnTable = {} +for (var currency in valueTable) { + bnTable[currency] = new ethUtil.BN(valueTable[currency], 10) +} + +module.exports = { + valuesFor: valuesFor, + addressSummary: addressSummary, + miniAddressSummary: miniAddressSummary, + isAllOneCase: isAllOneCase, + isValidAddress: isValidAddress, + numericBalance: numericBalance, + parseBalance: parseBalance, + formatBalance: formatBalance, + generateBalanceObject: generateBalanceObject, + dataSize: dataSize, + readableDate: readableDate, + normalizeToWei: normalizeToWei, + normalizeEthStringToWei: normalizeEthStringToWei, + normalizeNumberToWei: normalizeNumberToWei, + valueTable: valueTable, + bnTable: bnTable, + isHex: isHex, +} + +function valuesFor (obj) { + if (!obj) return [] + return Object.keys(obj) + .map(function (key) { return obj[key] }) +} + +function addressSummary (address, firstSegLength = 10, lastSegLength = 4, includeHex = true) { + if (!address) return '' + let checked = ethUtil.toChecksumAddress(address) + if (!includeHex) { + checked = ethUtil.stripHexPrefix(checked) + } + return checked ? checked.slice(0, firstSegLength) + '...' + checked.slice(checked.length - lastSegLength) : '...' +} + +function miniAddressSummary (address) { + if (!address) return '' + var checked = ethUtil.toChecksumAddress(address) + return checked ? checked.slice(0, 4) + '...' + checked.slice(-4) : '...' +} + +function isValidAddress (address) { + var prefixed = ethUtil.addHexPrefix(address) + if (address === '0x0000000000000000000000000000000000000000') return false + return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed) +} + +function isAllOneCase (address) { + if (!address) return true + var lower = address.toLowerCase() + var upper = address.toUpperCase() + return address === lower || address === upper +} + +// Takes wei Hex, returns wei BN, even if input is null +function numericBalance (balance) { + if (!balance) return new ethUtil.BN(0, 16) + var stripped = ethUtil.stripHexPrefix(balance) + return new ethUtil.BN(stripped, 16) +} + +// Takes hex, returns [beforeDecimal, afterDecimal] +function parseBalance (balance) { + var beforeDecimal, afterDecimal + const wei = numericBalance(balance) + var weiString = wei.toString() + const trailingZeros = /0+$/ + + beforeDecimal = weiString.length > 18 ? weiString.slice(0, weiString.length - 18) : '0' + afterDecimal = ('000000000000000000' + wei).slice(-18).replace(trailingZeros, '') + if (afterDecimal === '') { afterDecimal = '0' } + return [beforeDecimal, afterDecimal] +} + +// Takes wei hex, returns an object with three properties. +// Its "formatted" property is what we generally use to render values. +function formatBalance (balance, decimalsToKeep, needsParse = true) { + var parsed = needsParse ? parseBalance(balance) : balance.split('.') + var beforeDecimal = parsed[0] + var afterDecimal = parsed[1] + var formatted = 'None' + if (decimalsToKeep === undefined) { + if (beforeDecimal === '0') { + if (afterDecimal !== '0') { + var sigFigs = afterDecimal.match(/^0*(.{2})/) // default: grabs 2 most significant digits + if (sigFigs) { afterDecimal = sigFigs[0] } + formatted = '0.' + afterDecimal + ' ETH' + } + } else { + formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ' ETH' + } + } else { + afterDecimal += Array(decimalsToKeep).join('0') + formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ' ETH' + } + return formatted +} + + +function generateBalanceObject (formattedBalance, decimalsToKeep = 1) { + var balance = formattedBalance.split(' ')[0] + var label = formattedBalance.split(' ')[1] + var beforeDecimal = balance.split('.')[0] + var afterDecimal = balance.split('.')[1] + var shortBalance = shortenBalance(balance, decimalsToKeep) + + if (beforeDecimal === '0' && afterDecimal.substr(0, 5) === '00000') { + // eslint-disable-next-line eqeqeq + if (afterDecimal == 0) { + balance = '0' + } else { + balance = '<1.0e-5' + } + } else if (beforeDecimal !== '0') { + balance = `${beforeDecimal}.${afterDecimal.slice(0, decimalsToKeep)}` + } + + return { balance, label, shortBalance } +} + +function shortenBalance (balance, decimalsToKeep = 1) { + var truncatedValue + var convertedBalance = parseFloat(balance) + if (convertedBalance > 1000000) { + truncatedValue = (balance / 1000000).toFixed(decimalsToKeep) + return `${truncatedValue}m` + } else if (convertedBalance > 1000) { + truncatedValue = (balance / 1000).toFixed(decimalsToKeep) + return `${truncatedValue}k` + } else if (convertedBalance === 0) { + return '0' + } else if (convertedBalance < 0.001) { + return '<0.001' + } else if (convertedBalance < 1) { + var stringBalance = convertedBalance.toString() + if (stringBalance.split('.')[1].length > 3) { + return convertedBalance.toFixed(3) + } else { + return stringBalance + } + } else { + return convertedBalance.toFixed(decimalsToKeep) + } +} + +function dataSize (data) { + var size = data ? ethUtil.stripHexPrefix(data).length : 0 + return size + ' bytes' +} + +// Takes a BN and an ethereum currency name, +// returns a BN in wei +function normalizeToWei (amount, currency) { + try { + return amount.mul(bnTable.wei).div(bnTable[currency]) + } catch (e) {} + return amount +} + +function normalizeEthStringToWei (str) { + const parts = str.split('.') + let eth = new ethUtil.BN(parts[0], 10).mul(bnTable.wei) + if (parts[1]) { + var decimal = parts[1] + while (decimal.length < 18) { + decimal += '0' + } + const decimalBN = new ethUtil.BN(decimal, 10) + eth = eth.add(decimalBN) + } + return eth +} + +var multiple = new ethUtil.BN('10000', 10) +function normalizeNumberToWei (n, currency) { + var enlarged = n * 10000 + var amount = new ethUtil.BN(String(enlarged), 10) + return normalizeToWei(amount, currency).div(multiple) +} + +function readableDate (ms) { + var date = new Date(ms) + var month = date.getMonth() + var day = date.getDate() + var year = date.getFullYear() + var hours = date.getHours() + var minutes = '0' + date.getMinutes() + var seconds = '0' + date.getSeconds() + + var dateStr = `${month}/${day}/${year}` + var time = `${hours}:${minutes.substr(-2)}:${seconds.substr(-2)}` + return `${dateStr} ${time}` +} + +function isHex (str) { + return Boolean(str.match(/^(0x)?[0-9a-fA-F]+$/)) +} diff --git a/ui/classic/css.js b/ui/classic/css.js new file mode 100644 index 000000000..043363cd7 --- /dev/null +++ b/ui/classic/css.js @@ -0,0 +1,29 @@ +const fs = require('fs') +const path = require('path') + +module.exports = bundleCss + +var cssFiles = { + 'fonts.css': fs.readFileSync(path.join(__dirname, '/app/css/fonts.css'), 'utf8'), + 'reset.css': fs.readFileSync(path.join(__dirname, '/app/css/reset.css'), 'utf8'), + 'lib.css': fs.readFileSync(path.join(__dirname, '/app/css/lib.css'), 'utf8'), + 'index.css': fs.readFileSync(path.join(__dirname, '/app/css/index.css'), 'utf8'), + 'transitions.css': fs.readFileSync(path.join(__dirname, '/app/css/transitions.css'), 'utf8'), + 'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'), + 'react-css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'), +} + +function bundleCss () { + var cssBundle = Object.keys(cssFiles).reduce(function (bundle, fileName) { + var fileContent = cssFiles[fileName] + var output = String() + + output += '/*========== ' + fileName + ' ==========*/\n\n' + output += fileContent + output += '\n\n' + + return bundle + output + }, String()) + + return cssBundle +} diff --git a/ui/classic/design/00-metamask-SignIn.jpg b/ui/classic/design/00-metamask-SignIn.jpg new file mode 100644 index 000000000..2becdb032 Binary files /dev/null and b/ui/classic/design/00-metamask-SignIn.jpg differ diff --git a/ui/classic/design/01-metamask-SelectAcc.jpg b/ui/classic/design/01-metamask-SelectAcc.jpg new file mode 100644 index 000000000..239091a98 Binary files /dev/null and b/ui/classic/design/01-metamask-SelectAcc.jpg differ diff --git a/ui/classic/design/02-metamask-AccDetails.jpg b/ui/classic/design/02-metamask-AccDetails.jpg new file mode 100644 index 000000000..d7d408ffc Binary files /dev/null and b/ui/classic/design/02-metamask-AccDetails.jpg differ diff --git a/ui/classic/design/02a-metamask-AccDetails-OverToken.jpg b/ui/classic/design/02a-metamask-AccDetails-OverToken.jpg new file mode 100644 index 000000000..f26ff31e8 Binary files /dev/null and b/ui/classic/design/02a-metamask-AccDetails-OverToken.jpg differ diff --git a/ui/classic/design/02a-metamask-AccDetails-OverTransaction.jpg b/ui/classic/design/02a-metamask-AccDetails-OverTransaction.jpg new file mode 100644 index 000000000..8a06be6b9 Binary files /dev/null and b/ui/classic/design/02a-metamask-AccDetails-OverTransaction.jpg differ diff --git a/ui/classic/design/02a-metamask-AccDetails.jpg b/ui/classic/design/02a-metamask-AccDetails.jpg new file mode 100644 index 000000000..c37e0f539 Binary files /dev/null and b/ui/classic/design/02a-metamask-AccDetails.jpg differ diff --git a/ui/classic/design/02b-metamask-AccDetails-Send.jpg b/ui/classic/design/02b-metamask-AccDetails-Send.jpg new file mode 100644 index 000000000..10f2d27fd Binary files /dev/null and b/ui/classic/design/02b-metamask-AccDetails-Send.jpg differ diff --git a/ui/classic/design/03-metamask-Qr.jpg b/ui/classic/design/03-metamask-Qr.jpg new file mode 100644 index 000000000..9c09de42f Binary files /dev/null and b/ui/classic/design/03-metamask-Qr.jpg differ diff --git a/ui/classic/design/05-metamask-Menu.jpg b/ui/classic/design/05-metamask-Menu.jpg new file mode 100644 index 000000000..0a43d7b2a Binary files /dev/null and b/ui/classic/design/05-metamask-Menu.jpg differ diff --git a/ui/classic/design/chromeStorePics/final_screen_dao_accounts.png b/ui/classic/design/chromeStorePics/final_screen_dao_accounts.png new file mode 100644 index 000000000..805cc96b6 Binary files /dev/null and b/ui/classic/design/chromeStorePics/final_screen_dao_accounts.png differ diff --git a/ui/classic/design/chromeStorePics/final_screen_dao_locked.png b/ui/classic/design/chromeStorePics/final_screen_dao_locked.png new file mode 100644 index 000000000..9d9e33930 Binary files /dev/null and b/ui/classic/design/chromeStorePics/final_screen_dao_locked.png differ diff --git a/ui/classic/design/chromeStorePics/final_screen_dao_notification.png b/ui/classic/design/chromeStorePics/final_screen_dao_notification.png new file mode 100644 index 000000000..d56a5ce62 Binary files /dev/null and b/ui/classic/design/chromeStorePics/final_screen_dao_notification.png differ diff --git a/ui/classic/design/chromeStorePics/final_screen_wei_account.png b/ui/classic/design/chromeStorePics/final_screen_wei_account.png new file mode 100644 index 000000000..d503ff301 Binary files /dev/null and b/ui/classic/design/chromeStorePics/final_screen_wei_account.png differ diff --git a/ui/classic/design/chromeStorePics/final_screen_wei_notification.png b/ui/classic/design/chromeStorePics/final_screen_wei_notification.png new file mode 100644 index 000000000..3560c51ff Binary files /dev/null and b/ui/classic/design/chromeStorePics/final_screen_wei_notification.png differ diff --git a/ui/classic/design/chromeStorePics/icon-128.png b/ui/classic/design/chromeStorePics/icon-128.png new file mode 100644 index 000000000..ae687147d Binary files /dev/null and b/ui/classic/design/chromeStorePics/icon-128.png differ diff --git a/ui/classic/design/chromeStorePics/icon-64.png b/ui/classic/design/chromeStorePics/icon-64.png new file mode 100644 index 000000000..7062cf4f1 Binary files /dev/null and b/ui/classic/design/chromeStorePics/icon-64.png differ diff --git a/ui/classic/design/chromeStorePics/metamask_icon.ai b/ui/classic/design/chromeStorePics/metamask_icon.ai new file mode 100644 index 000000000..27400c5a4 --- /dev/null +++ b/ui/classic/design/chromeStorePics/metamask_icon.ai @@ -0,0 +1,2383 @@ +%PDF-1.5 % +1 0 obj <>/OCGs[5 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + metamask_icon + + + Adobe Illustrator CC 2015 (Macintosh) + 2016-06-15T14:23:12-04:00 + 2016-06-15T14:23:12-04:00 + 2016-06-15T14:23:12-04:00 + + + + 240 + 256 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAADwAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXnP5r/mvB5Tg/RmnAT6/cJyUMKx28bVAkf+ZjT4U+k7UDYuo1HBsObl6bTce5+l5X+Wf5t6jonm KZtfu5rzTNVcG+lkZpHilACLOAamgUBWC/sgUrxAzEwakxl6uRczUaYSj6eYfS9vcQXEEdxA6ywT KskUimqsjCqsCOoIObUG3UkUvxQ7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXln5rfnHb+X1l0bQnWfXCCs0+zR2tfEGoaTwXoO/hmJqNTw7Dm5mm0plvL6fvfO U889xPJcXEjTTzM0ksshLO7saszMdySTUk5qybdsBSzAl7R+Rv5ni0dPKutTqto5ppNw/KqyOwH1 c0BHFuVVJpTpvUUz9Jnr0n4Ov1mnv1D4ve82LrHYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FUn1Pzl5W0yF5r3VLeNI9no4kYfNU5N+GY89XijsZC/mfkHIhpMstxE18h8ynCmqg7iorQ7HMhx 3Yq7FXYq7FXYq7FXjf5v/nG2nPL5e8tXAN9Ro9Rv039A7D04WBp6nUM1Ph7fFXjg6nU16Y83P0ul v1S5PAWZmYsxJYmpJ3JJzWu0axV2KuxV9Ifkr+Zq67py6FrF1y121+G3eUjlcwKtQeRPxyIAeXci jbnkc2mlz8Qo83U6vT8J4h9L1PMxwnYq7FXYq7FXYq7FXYq7FXYq7FXYqlN95s8t2I/0jUYQQaFE b1HHzWPk34Zi5Nbhh9Uh9/3OVi0Waf0xP3fex7UfzX0WEOtjBNdSCoR2AjjPgak8/wDhcwcvbWIf SDL7B+v7HOxdi5T9REftP6vtYre/mf5ouD+5eK0UdoowxPzMnqfhmsydsZpcqj7h+u3aY+x8Eedy 95/VTFdc803n1dptVv5powSVjeRmqx7IhNMxPEy5jRJPx2cwY8WEWAB8N0l8gRXnm/z/AKXazpXT 7WX65NCo5II4PjHqVrXk3FDX+btXNtodLETDqddqiYH7H1LnQPOuxV2KuxV2KuZlVSzEBQKknYAD FXhX5t/nOsyzaB5XuCEDBbzVoXZSSrA8Ld0I2qKM/foNt81+o1X8Mfm7LTaT+KXyeI5r3YuxV2Ku xV2KqtpdXNpdQ3VrI0NzA6yQyoaMroaqwPiCMINboIsUX1j+XH5g6f5w0WOcNHDq0I439irbqwoD IiklvTaux7dK1GbnBmEx5ukz4DjPky3Lmh2KuxV2KuxV2KuxVKb/AM2eW7CoudQhDA0KI3qMD7rH yYZjZdbhh9Uh9/3OVi0Waf0xP3fex6//ADY0OHktnbzXTKaKxpFGw8QTyb/hcwMnbWIfSDL7B+Pg 5+PsXKa4iI/afx8WO3/5ra9OJEtYIbVG2RqNJIo/1iQv/C5gZe2sp+kCP2n8fBz8XYuIVxEy+wfj 4sVvdY1a+FLy8muFBLBZJGZQT4KTQZrMmfJP6pE/F2ePBjh9MQPghMqbXYqler+YLPTgUJ9W57Qq en+se2X4dPKfuaMueMPewW+vrm9uGnuH5Ox2G/FR/KoPQZtYQERQdZOZkbL3L/nG7y8Y7PVPMEqj lOy2VqxBDBEpJKQSPsszINu6nNpoYbGTqdfPcRe1ZnuvdirsVdirsVeF/n1+Y96l1N5O04+lCERt Vn35uXAkWBfBOJVmI+1XjsAeWv1ec3wD4uy0eAVxn4PEM17sXYq7FXYq7FXYq7FU18seZNU8uaxD qumymOeKqsBQh0YUZCGDDceI2O+ESlH6TRYmEZbSFh7bbfmL5mubeO4iv6xSqHQ+lD0YV/kzVy7U 1MTRl9kf1Owj2XpiLEftP61T/Hvmv/lt/wCSUP8AzRkf5W1P877I/qZfyTp/5v2n9bv8e+a/+W3/ AJJQ/wDNGP8AK2p/nfZH9S/yTp/5v2n9bv8AHvmv/lt/5JQ/80Y/ytqf532R/Uv8k6f+b9p/W7/H vmv/AJbf+SUP/NGP8ran+d9kf1L/ACTp/wCb9p/W7/Hvmv8A5bf+SUP/ADRj/K2p/nfZH9S/yTp/ 5v2n9bHtW1/WtUeuoXTz8eiGioCNtkUKv4ZDNqsmX6zbdh0uPF9ApL8ob3Yq7FXYq7FWO695pW0d rWyo9wtRJKd1Q+A8WH3ZmYNLxby5OJn1PDtHmw6SR5JGkclnclmY9STuTmzArZ1xN7uhhlmmSGJS 8sjBI0HUsxoAPpwhiS+zvLGhxaF5e0/SIiGFlAkTOoIDuB8b0JNOb1bN5jhwxAdBknxSJ70zybB2 KuxV2KpX5o12HQfL2oaxNxK2UDyKjHiHkpSOOu9ObkL9OQyT4Yks8cOKQHe+Nr6+u7+8mvLyVp7q 4cyTTOaszNuSc0ZJJsu/AAFBRwJdirsVdirsVdirsVdirKvI2tmC6OmzN+5uDWEmlFkp03/mp9/z zX67BY4hzDm6PNR4T1Z5mpdo7FXYq7FXYqg7laSn33y2PJgVLJIdirsVWu6IjO7BUUEsxNAAOpJO IFqTTD9c81yT1gsGaKIH4px8LtT+Xuo/HNlg0gG8ubrs2qJ2ixzM1w3Yqzz8k/L66z5/s2kAMGmK 2oSAkgkwkCKlO4ldDTwBzJ0sOKfucbVz4YHz2fU+bd0rsVdirsVdirxT/nIzzWi2ll5ZtZ1Mkj/W dRjQnkqoB6KPTajli9D/ACqcwNbk2EQ7DQ49zIvB81zs3Yq7FXYq7FXYq7FXYq7FW0d0dXRirqQV YGhBG4IIxItQXp3ljWjqunc5Cv1qI8J1Xb/Van+UPxrmh1WDw5bcnc6fNxx35pvmO5DsVdirsVQ1 2v2W+gnJwYlD5YxdiqheXttZwGe4cRxjap6k+AHc5KEDI0GM5iIssE1fzBeakeB/dWw6Qqevux7n Nth08Ye91eXPKfuSzL2h2KuxV9If84/eVk03ys+tyEm51lqhSKcIYHdEArv8Zq3uKZtdHjqN97qN bkuVdz1PMtw3Yq7FXYqp3V1b2ltNdXMgit4EaWaVjRVRByZifAAYCaSBez418169Nr/mPUdYl5Vv Z2kjVyCyRVpEhIp9iMKv0Zo8k+KRLv8AHDhiB3JVkGbsVdirsVdirsVdirsVdirsVTPy7q50vU45 2J9B/guFHdD36H7J3/DKNTh8SFdejdgy8Er6PUYpY5okljblHIoZGHQqwqDmhIINF3QNiwuwJdir sVUrlaxH23yUeaCg8tYJfq2t2Wmx/vW5TleUcC/abtv/ACj3OXYsEp8uTVlzRhz5sD1DULm+uGnu GLE/ZX9lR/KozbY8YgKDqsmQyNlDZNg7FXYqiNN0+51HUbXT7UBrm8mjggUmgLysEWp7bnDEWaRK VCy+0tM0+307TbXT7YEW9nDHbwgmp4RKEWp+QzfRFCnnpSs2UThQ7FXYq7FXmX59ebIdL8ovpEMw Goauyx+mrFXW2U8pH2/Zbj6dD15HwOYmryVGupczR4uKd9A+ac1Tt3Yq7FXYq7FXYq7FXYq7FXYq 7FXYqzXyJrSem2lztRgS9sSQAQT8SD3ruPpzV6/Bvxj4ux0Wb+EsxzWuwdirsVadeSlfEEYhDE9b 8zwWLNb24E10pKuK/AhHjTqa9s2ODSme52Dh5tSI7DcsJmmlmkaWVy8jmrOxqTm0AAFB1hJJsrMK HYq7FXYq9S/5x+8qvqXmp9bkIFtoq1CEV5zTo6IBXb4RVq9jTMzR47lfc4WtyVHh730jm0dS7FXY q7FXYq+X/wA+dQmuvzGu4JPsWMFvBF/qtGJ/+JTHNTq5Xk9zudHGsY83nmYrlOxV2KuxV2KuxV2K uxV2KuxV2KtqrMwVQSxNABuSTirN/Lfl9LBVurhQ1626g7iMeA/yvE/R89VqdRx7D6XZ6fT8O55s tUggEdDuM1zmt4pdirsVYZ5s8s+pLJfWS0lNXmhH7fcsv+V4jv8APrs9JqaHDJ1+p01+qLDM2brn Yq7FXYq7FX0n/wA48to48kyR2cwfUPrLyanEdmjZvhiA2rwMaAj35ZtdHXBtzdRrr49+XR6hmW4b sVdirsVdir5I/Ne/+vfmJrs1QeFx6G3/AC7osP8AzLzTag3Mu800axhieUN7sVdirsVdirsVdirs VdirsVdirMPLHl8wAXt4hE5/uYmFCg/mI8f1ZrdVqL9MeTsdNgr1HmyXMJzEXatWOndf1ZVMbswr ZFLsVdiqGu13VvoOTgWJYV5o8vFS9/aL8O7XEQ7eLj28c2ml1H8MnXanT/xBi+Z7guxV2KuxVP8A yLf+abLzPZP5ZDyarI4SO3XdZVO7JKKgenQVYkjiPiqKVFuKUhIcPNqzRiYni5PsKIymJDKFWXiP UCElQ1N6EgEivtm7dCuxV2KuxV4P+Zn5i/m7o189tNbRaNYszi2urVBOsqEkD/SJAw5bV2VG8QNs 12fNlie4Oy0+DFId5eL3FxPczyXFxI01xMzSTSuSzu7GrMzHckk1JzBJt2AFLMCXYq7FXYq7FXYq 7FXYq7FXYqyvyv5fZWF9ex0IobaNvv5kfq/2s1+q1H8Mfi5+mwfxS+DKswHOdiqrbuVkA7Nsf4ZG Q2SEZlTN2KuxVTnTlEfbcfRhid0FBZcwYd5k8uNAz3tkg+rdZYl6oe7KKfZ/V8umy02pv0y5uu1G nr1R5MbzNcN2KuxVnH5Z/mdP5MuXjayhudPu5Fa9cJS6CAEUjkqoIFa8W2/1ak5kYM/B02cbUafx Ou76X8s+ZdL8x6RDqumGU2s32TLG8R5DZl+IUbi1VJQlajrm1hMSFh1GTGYGimmTYOxV2KqN9HZS Wk0d8sT2boVuEnCmIodiHDfDT54DVbpF3s+b/wAyfI/kCy9fU/LvmWzJZix0f1BOQSWJWF4OZXsq q6/N81efFAbxkPc7bBmmdpRPveZZiOY7FXYq7FXYq7FXYq7FXYqybyz5cMhjv7xaRD4oYSPteDN/ k+Hj8uuDqdTXpi5um09+osvzXOwdirsVdiqYIwZA3iMoIZt4pdirsVS9l4sV8DTLg1tEAih6YVYT 5k8vGzY3dsC1qxJdf99knpt+z4ZtNNqOLY83W6jT8O45JBmW4jsVZP8Alp5c07zH5z0/SNQd1tZz I7rH1f0o2l4V/ZDcNzl2CAlMAtOomYQJD65gggt4I7e3jWGCFRHFFGAqIiiiqqjYADYAZugKdGTa /FDsVdirHfNvkDyv5rjUava87iNGSC7iYxzRhvBhs1DuA4I9sqyYYz5tuLNKHJ4v5q/5x58xWBlu NAuE1S1G6270iuQCTtv+7fitN+QJ7LmDk0Uh9O7sMeuifq2eUzQzQTPDMjRTRMUkjcFWVlNCrA7g g5hkU5oNrMCXYq7FXYq7FXYq7FWR+XfLJuAl5eDjBUGKEjdx4nwX9fy64Wo1NemPNzNPpr9UuTMs 1rsXYq7FXYq7FUVavVSh6jcfLK5hkFfIMnYq7FUHdLSWviK/wyyHJgVLJoWuiOjI4DIwKsp3BB2I OINKRbB/MPl19Pb6xb1ezY713MZPY+3gfo+e10+o49j9Tq9Rp+DcckkzKcZmP5P3EsH5k6G8cTTM ZZIyqgkhZIXRm27IrFj7DL9Mf3gcfVC8ZfWWbl0jsVdirsVdirwr87POX5jaTqj6dFIdP0O4VTaX dorK8o6lXuDusgZTVUI+HrUHfX6rLOJrkHZaTFjkL5l4jmvdi7FXYq7FXYq7FXYqn/lzy6t8purr kLZTREG3Mg77/wAvbbMTU6jg2HNy9Pp+Lc8maqqooVQFVRRVGwAHYZqyXZAN4q7FXYq7FXYqvhfj Ip7dD9ORkNkhHZUzdirsVULpKoG/l/jkoFiULlrF2KrJYYpozHKiyRt9pGAIP0HCCQbCCAdiwTzD obabcB4qm0lJ9M7nif5Sf1ZttPn4xvzdXqMPAduT6E/Jz8tofLejx6pqMStrt+iyNzSj20bLtCOY DK9G/edN/h7VO+02DhFnmXn9Vn4zQ+kPSMynEdirsVdirsVS7zBoGl6/pM+l6nCJrScUI6MrD7Lo ezKehyM4CQos4TMTYfKfn3yJq3lDWHs7pWkspCTYX1KJMgp4Vo61oy9vlQ5p82EwNF3WHMMgsMYq MpbnVGKuxVvFXYqnnlvQDfSfWbhSLRDsP9+MOw9h3zF1Oo4BQ5uVp8HEbPJm6IiIqIoVFACqBQAD YADNUTbswKXYq7FXYq7FXYq7FXYqjoX5xg9+h+eUyFFmF+BLsVWyLyRl8RtiChAZewdirsVTjyfZ JeeZ9NidOarOkpUio/dH1Afo45mdnx4s8R5/du4faE+HBI+X37Pds7N4t2KuxV2KuxV2KuxV5Z+Y esC91gWcZBhsKpUb1kahf7qcfozke2dTx5eEcoff1/U9b2PpuDFxHnP7un62K5p3buxV2KuxV1Bi qFukowcdDsfnlkCxKhk2LsVdirsVdirsVdirsVRFo/xFfHcZCYZBE5WydirsVQEq8ZGHv+vLgdmB W4UOxVmH5WQCTzOzn/dFvI4+kqn/ABvm27GiDm90T+h1PbUiMI85D9L17OpeVdirsVdirsVdiqG1 K/i0+wuL2X7ECFyK0qR0Ue7HbKs+UY4GZ6Btw4jkmIjqXh000k0zzSsWkkYu7HqWY1Jzz+UjIknm XvYxEQAOQWYGTsVdirsVdiq2ROaFfHp88INIKAIIJB6jrlrB2FXYq7FXYq7FXYq7FV0bcXDeBwEJ CPylm7FXYqhbtTyDdiKfTlkCxKhk2LsVZ7+UduW1S+uO0cCx/TI4P/MvN32HC5yl3Cvn/Y6PtydQ jHvN/L+16jnSPNuxV2KuxV2KuxVhP5l60IrOPSY6+rccZZj29NSeI+l1r9GaHtzUgQGMc5bn3f2/ c73sTTEzOQ8o7D3/ANn3vOM5d6d2KuxV2KuxV2KuxVCXiFT6iqWB+1Sn8csgejEhBfW4/Bvw/rlv Chr64n8px4Va+uD+T8ceBWvrv+R+P9mHgVv67/kfj/ZjwK19cP8AL+OPArX1yT+UY8KtfXJfBfx/ rjwhU2s5Ge3Ut9rv/DMeYosgr5FLsVUL3l9WZlFWX4hX26/hkoc0FKDdSnwHyGZPCGKhZ6oLy3We CTlGxIBoBupKnt4jLMuA45cMhu1Yc0ckeKPJ6F+UmpSxapdQMapPGrGvjG1BT/kYc2vYs6nKPeL+ X9rqO3IeiMu418/7HsA3GdE807FXYq7FXYq7FXi/mfVP0nrl1dA1i5cIaGo9NPhUj/Wpy+nOF7Qz +LmlLpyHuH4t7jQYPCwxj15n3n8UlWYbmOxV2KuxV2KuxV2KuIB2PTFUDdWaV5caqe/cZbCbAhAy WjCpQ8h4d8tElUCCDQ9ckrWKpZrHmHTtKUeuxeY9II6F6eJBIoMztJoMmf6dh3nk4Os7Rxaceo2e 4c2Far5x1a+5JE31W3bb04z8RHu/X7qZ0ul7IxYtz6pef6v7XltX2zmy7A8EfL9f9id+R9d9WP8A Rc5q8YLW7kjdR1Tfeo6j2+WaztrRcJ8WPI8/1/jr73adh6/iHgy5jl7u78dPcy5RyYL4mmc+9GnN o1HK9iP1ZjzCQisrZOxVp1DoynowIPyOIKscl/dc+ewSvL2p1zNiL5NZNCy888na79QvDa3DgWlw d2Y0CPTZvDfofo8M67tfQ+LDiiPXH7Q8b2Nr/CnwSPol9h7/ANb2PytcvZ6rYSqwX96odu3FzRq/ Qc5fRZTDPEjvr57PT6/GJ4ZA91/J79A3KJT7Z2bxS/FXYq7FXYqk/m7VRpug3UyvwnkX0oN6Hm+1 V91FWzC7Rz+FhkevIe8/i3N7PweLmiOnM/D8U8azhnt3Yq7FXYq7FXYq7FXYq7FXEAih6Yqg54Ch 5L9j9WWRlbAhQeNHFGFcmChJ9b0DXL6ALpN8lt2kVwysfcSLyI+hfpzP0WqwY5XliZfju/b8HB12 DPkjWKQj+O/9nxYTe/l55tilc+gt11Zpo5VPInc7OUcn6M6XD23pSBvw+RH6rDzGXsXUgnbi8wf1 0Uiu9K1SzAa7s5rdTsGljZAfkWAzZYtTjyfTKMvcQ67Jp8kPqiY+8KNrczWtxHcQNwliYMje4yeX HGcTGXIscWWWOQlHmHrei39tqUMVzAQyMKuvdGAqVb3GcDqsEsMjGX9vm+g6bUxzQE4/2eSco3Fw 3gcwyHITAEEAjodxlLJ2KXYqx/X7J5UubeJgj3MThHaoVWcFakgHau+Z2kyiMoyPKJH2OPqMZnjl EcyCEB5f/LjSLBEm1AC+ux1Dbwqd+iftf7KvyGZ2t7dy5DUPRH7fn+p1ej7DxYxc/XL7Pl+tkDoI 3KqAoX7IGwA7UzUg3u7iq2fQWlzGfTbadl4mWJHK+HJQaZ30JcUQe8PAzjwyI7iiskxdirsVdirz L8y9S9fV4rFSeFmnxj/iySjH/heOcp25n4sogP4R9p/ZT1PYmDhxmZ/iP2D9tsPzSO7dirsVdirs VdirsVdirsVdiriARQ9MVQc8BT4hun6ssjK2BDdq1JCP5h+IxmNkhF5WydiqAu9A0O8Ltc2FvLJJ 9uQxrzP+zpy/HMnHrc0KEZyAHnt8nGyaPDO+KEST5b/NRsPLOlaYH/R0RgEn205u6kjvRy1D8snn 12TNXiG68h+hjp9Hjw2MYoHzP6VVlZTRhQ5SC5CJt5l4hCaEdK98hKKQVfIMnYqo3dstxEV2DjdG 8DkoSooKhp8jrW2mqJE3UH+XJZB1ChddLSWviMYcmJfQsESwwRxL9mNQo+QFM9BAfPyV+FDsVdiq jeXdvZ2st1cOEhhUs7HwGQyZIwiZS2AZ48cpyEY7kvD7+7kvL2e7kFHuJGkYDoORrQfLOAzZDOZk ept73FjEICI6ClDK2x2KuxV2KuxV2KuxV2KuxV2KuxVxAIoeh64qhZIjE4dd0B+7LAbY1SKBBAI6 HplbJ2KuxV2KrJI1kWh69jhBpBCElhaM77jscsErYkK8NwGor/a7HxyEopBV8iydiqhcW5kKyRkL Mn2GPT3ByUZVz5IbVDcyQLTizuI2U9mYgZZijcuEdWGSXDEnufQeegPn7sVdirsVYb+ZmqGDS4dP Q/Fdvyk6f3cRBp47tT7s0fbmfhxiA/iP2D9tO67EwcWQzP8ACPtP7LeaZyr1TsVdirsVdirsVdir sVdirsVdirsVWvJGgq7BR2qaYgEoQc2qW4BVVMn4A/x/DLRiKLVIbscAGQjbpWtPbtgMFtEJIj/Z NfbvlZFJtdil2KuxVogEUIqMUIWa3K1ZN18O4yyMkEKkFxyoj9ex8cEoqCr5Bk7FUTpEdv8Apmxe YqkQuYWmZjReIcVJJ2+z3zI0kgMsCeXEPvcfVRJxSA58J+57pnfPBuxV2KuxV5B531M3/mK4INYr b/R46eEZPL/hy2cV2rn8TOe6O3y/bb2fZeHw8A75b/P9lJDmudi7FXYq7FXYq7FXYq7FXYqoSXtq nWQE+A3/AFZIQJRaEk1c7iOP5Fj/AAH9csGHvRaFkv7t+shA8F2/VvlgxgLagSSanqckhVtY+UnI 9F3+nBIqjcrQ7FVRZ5V/aqPA75ExCbVUuwdnFPcZEwTauro32SDkCEt4pdiqhNbhqsmzeHY5KMmJ DoJzXhJs3YnDKPUKCr5Bk7FWd+RvOPp+npOoyfu9ltJ2/Z7CNj4fy+HTpnQ9k9pVWKZ/qn9H6vk8 92r2dd5YD3j9P6/m9CzpXnHYq7FXhnnLS30vzHeW4BWF3M1vQED05PiAX2U1X6M4vX4PDzSHTmPj +Ke00GfxMMT15H4fi0l5N4nMOnMdybxONK7kfHFXVPjirVT44q6p8cVdU+OKuqcKrZEEiFT9B8Di DSoB0ZGKt1GWgpW4q7FWwCTQdT0xVHxR+mgXv1Jysm0L8CuxV2KuxV2KqiXEq96jwORMQm1dbpD9 oFfxGQMCm1VWVhVSD8sjSVssSyDfY9jhBpSFkbsh4S9f2W7HCRfJVbIpdir0fyR5zW4WPStRci5A 421wxr6ngjH+bw8fn16jsvtTjrHk+roe/wAvf9/v58x2n2Zw3kx/T1Hd5+77vdym2b50TsVef/m1 pJktbTVY1FYCYJyAa8X3Qk+CtUf7LNH21guImOmx/H45u97Ez1IwPXcfj8cnmOc49G7FXYq7FXYq 7FXYq7FXYqpTw+otR9odMINKgiCDQ9csS1iqItI6vzPRenzyMiqLyCHYq7FXYq7FXYq7FXYq4Eg1 HXFVVLmRdj8Q9+uRMQm1UXETijinz3GR4SE2vRgopXkg6N1p88iUoTWNf0bRoBNqd3Hao1eAc1Zq UrxQVZqV3oMtw6fJlNQFtWbUQxC5mmMXn5w+TbYqYJbi8J7wRFeP/I4xfhmwx9i6g86j7z+q3Ayd sYByuXuH66ehfl//AM5Q+UtVuk0rzAH0mT4Y7XUp6GGToo9dgW9JjWpY/B1qV79Tp4zEAJkGQeX1 BgZkwFRe3wzRTRJNC6yRSANHIhDKyncEEbEHL2hC6xpcGq6ZcafOSIp1oWHUEEMp+hgDlWfCMkDA 8i24MxxTExzDwG5t5ra5ltpl4zQO0ci9aMh4kfeM4ecDEkHmHuYTEoiQ5FTyLJ2KuxV2KuxV2Kux V2KuxVDXMNayL/shkolKGAJIA6nYZNUwRAihR2yolC7FXYq7FXYq7FW1VmPFQWJ6AbnEC+Skgc0V DpGpzbpbPTxYcR/w1MyIaTLLlEuPPV4o85BEDy3rJNDBT3Lp/A5aOzs3837Q1HtHD/O+wq48pamR UvCPYs38Fy4dk5e+P4+DSe1sXdL7P1q8Xk+cj97cqp/yVLfrK5ZHsiXWX4+xql2vHpH8farR+Tog f3l0zDwVAv6y2Wx7IHWX2Ncu1z0j9quvlLTlIPqzVH+Uv/NOWfyTi75fZ+pq/lbL3R+39b5x/OyS VfzBvrLmWt7JII7ZDT4VeFJW6UrV5Cc2uk00MUKiHV6rUTyyuRYJmS4zsVfU/wDziJp/mFdA1bUJ 72QaC8/oWOnHiUNwqq00+68h8JRBxeh+LkKgHCgvoLFDx/8AM3SBZeYfrMakQ36erWlF9RfhkA8e zH55yna+Dgy8Q5S+/r+v4vV9kZ+PFwnnHb4dP1fBiOat2rsVdirsVdirsVdirsVdiqvaWF9euUtL eW4cdViRnI+fEHJ48Up/SCfcwyZYw+oge9PY/wArfMqQrdskahhX0ORaVK+KqD+GbQdkZjGzQdbL tnCDQstDybIrFJboJKv2k4EkfOrKckOxz1l9jWe2B0j9v7FaLyfbj++uHf8A1AF/XyyyPZEesj+P m1y7Xl0iPv8A1Ky+U9MBqXlYeBZf4KMsHZWLvl+Pg1HtXKekfx8VceW9G/5Z6+/N/wDmrLv5Ow/z ftP62r+Uc3877B+pXTSNLQUFrER/lKGP3tXLY6TEP4R8mqWryn+I/NWis7SI1igjjPiqgfqGWRww jyAHwapZpy5kn4q2WNbsVdirsVdirsVdir5U/O7/AMmdrP8A0bf9QsWZEOTRPmwbJMU28p+XL3zL 5l03QbIH6xqNwkAcKX9NWPxysq78Y0q7ewOKv0B8teXNJ8t6FZ6HpEPoafYpwgjJLHclmZierMzF ifE4WKZYqxn8wtFj1Hy7PMIw11YqZoHrSiggyj6UB28QM13amAZMJPWO/wCv7HY9l6g48wH8Mtv1 fa8XzkXr3Yq7FXYq7FXYqmOm+Xdc1Ir9SspZkbYS8eMe3/FjUT8cvxaXLk+mJP3fNx82qxY/qkB9 /wAubK9I/KjUpZEfVJ0t4OrRRHnL8q04D51ObTB2LMm8hoeXP9X3usz9tQArGLPny/X9zL9N/L3y tYgH6r9akH+7Lk+pX5rtH/wubTF2Zgh0s+e/7PsdVm7Tzz60PLb9v2sghhhhiWKFFjiQUSNAFUD2 A2zPjEAUOTgSkSbPNfhQo3NnaXScLmFJV3oHANK+HhjSQUovPKNhIpNo720gHwgMXSvuG5fgcrOM MxkKQ3fl7zBa1IjW6Qb8o9zT/V+E/cMgcZbBkCXNdCNzHNG8Ug2ZWFCCPHvkKZ2vW4gbo4+nb9eB VTFLsUOxV2KuxV2KuxVpnVBViAPE4q8U89flBq3mfzvf6yt/b2un3Xo8Kh3mAjhSNqpRF6pt8eWx nQa5Qsr7b/nH3yssai51C+llH2mjaKNT/sTHIR/wWPiFfDD178qfyf8AKnli6/Ttrp3p35jMVrcT SPI4jcDm4ViVUsNgwANKjocnC+rXOuj1DJsHYq0yqylWAZWFGU7gg9sVeBa/pbaXrN3YN0gkIQ+K H4kP0qQc4fVYfDySj3H+x7nS5vExxl3j+1L8ob21VmYKoJYmgA3JOICkp5p3kjzRfjlFYPHHt8c9 IhuKggPQn6Bmbi7OzT5Rr37OFl7RwQ5yv3bsp0z8o3qj6nfLQN8cNupNV9pG40/4DNli7E6zl8B+ v9jrM3bnSEfif1ftZlp3lLy5p9Da2EQdSGWRx6jgjuGfkR9GbbFosOP6Yj7/AL3U5dbmyfVI/d9y bZlOK7FXYq7FXYq7FXYq7FVKe1tbheM8KSgdA6huvz+WNKCkWoeSdNnFbVjav4CrqfoJr+OQOMNg yFILvylrlpVolE6AVLQtv16cTRvuyswLYMgSx5b23k9OZWRx1SRSp/GhyBDMFUXUF/aQj5Gv9MaV VW7ganxUJ7EYFVVZWFVII8RviriQBUmgHUnFUNNfINo/iPiemGlQTu7mrmpxVrFWReVfLr3cyXt0 g+poaorb+ow26fyg9a/LxyyEba5zrZneXNDsVdirsVYV538iXeuajBe2Uscb8PSnEpIFFJKsOIap 3pmo7Q7OlmmJRIG1G3cdndpRwwMZAnexShp35S6ZEQ1/eS3J2PCICJfcGvMn6KZDF2JAfVIy+xnl 7bmfpiI/b+plmk+X9H0lWXT7VYOf22BLMfYsxZqfTmzwabHi+gU6vPqcmX6zaYZe0OxV2KuxV2Ku xV2KuxV2KuxV2KuxV2KuxVRu7K1vITDcxiSM9j/AjcYCLSDSQ3vkbTpSWtZHtif2f7xfuJDf8NkD jDMZCkV55O1m3HJEW4UVJMR3FP8AJbifurkDAsxkCXjRtX/5Yrjb/ip/6YOEsuIK40PX5lANpKQO nIcev+tTHgK8YVB5S8wEf7y/8lI/+asPAUcYVU8ma4w3RE9mcfwrj4ZR4gRlr5EvfXT61NGIK/vP TLF6eAqoGSGNByBmccaRRrHGOKIAqqOwAoBlrSuxV2Kv/9k= + + + + proof:pdf + uuid:65E6390686CF11DBA6E2D887CEACB407 + xmp.did:d4d07395-aa96-47c2-a9e5-d0351947bb0c + uuid:c63c1031-e157-9748-9c58-86481308e954 + + uuid:1abccb90-0c26-4942-b156-fd2eb962e3e1 + xmp.did:58fdc1b8-1448-3a44-9e20-282d8ec1cf95 + uuid:65E6390686CF11DBA6E2D887CEACB407 + proof:pdf + + + + + saved + xmp.iid:d4d07395-aa96-47c2-a9e5-d0351947bb0c + 2016-06-15T14:23:10-04:00 + Adobe Illustrator CC 2015 (Macintosh) + / + + + + Web + Document + 1 + True + False + + 128.000000 + 128.000000 + Pixels + + + + Cyan + Magenta + Yellow + Black + + + + + + Default Swatch Group + 0 + + + + White + RGB + PROCESS + 255 + 255 + 255 + + + Black + RGB + PROCESS + 0 + 0 + 0 + + + RGB Red + RGB + PROCESS + 255 + 0 + 0 + + + RGB Yellow + RGB + PROCESS + 255 + 255 + 0 + + + RGB Green + RGB + PROCESS + 0 + 255 + 0 + + + RGB Cyan + RGB + PROCESS + 0 + 255 + 255 + + + RGB Blue + RGB + PROCESS + 0 + 0 + 255 + + + RGB Magenta + RGB + PROCESS + 255 + 0 + 255 + + + R=193 G=39 B=45 + RGB + PROCESS + 193 + 39 + 45 + + + R=237 G=28 B=36 + RGB + PROCESS + 237 + 28 + 36 + + + R=241 G=90 B=36 + RGB + PROCESS + 241 + 90 + 36 + + + R=247 G=147 B=30 + RGB + PROCESS + 247 + 147 + 30 + + + R=251 G=176 B=59 + RGB + PROCESS + 251 + 176 + 59 + + + R=252 G=238 B=33 + RGB + PROCESS + 252 + 238 + 33 + + + R=217 G=224 B=33 + RGB + PROCESS + 217 + 224 + 33 + + + R=140 G=198 B=63 + RGB + PROCESS + 140 + 198 + 63 + + + R=57 G=181 B=74 + RGB + PROCESS + 57 + 181 + 74 + + + R=0 G=146 B=69 + RGB + PROCESS + 0 + 146 + 69 + + + R=0 G=104 B=55 + RGB + PROCESS + 0 + 104 + 55 + + + R=34 G=181 B=115 + RGB + PROCESS + 34 + 181 + 115 + + + R=0 G=169 B=157 + RGB + PROCESS + 0 + 169 + 157 + + + R=41 G=171 B=226 + RGB + PROCESS + 41 + 171 + 226 + + + R=0 G=113 B=188 + RGB + PROCESS + 0 + 113 + 188 + + + R=46 G=49 B=146 + RGB + PROCESS + 46 + 49 + 146 + + + R=27 G=20 B=100 + RGB + PROCESS + 27 + 20 + 100 + + + R=102 G=45 B=145 + RGB + PROCESS + 102 + 45 + 145 + + + R=147 G=39 B=143 + RGB + PROCESS + 147 + 39 + 143 + + + R=158 G=0 B=93 + RGB + PROCESS + 158 + 0 + 93 + + + R=212 G=20 B=90 + RGB + PROCESS + 212 + 20 + 90 + + + R=237 G=30 B=121 + RGB + PROCESS + 237 + 30 + 121 + + + R=199 G=178 B=153 + RGB + PROCESS + 199 + 178 + 153 + + + R=153 G=134 B=117 + RGB + PROCESS + 153 + 134 + 117 + + + R=115 G=99 B=87 + RGB + PROCESS + 115 + 99 + 87 + + + R=83 G=71 B=65 + RGB + PROCESS + 83 + 71 + 65 + + + R=198 G=156 B=109 + RGB + PROCESS + 198 + 156 + 109 + + + R=166 G=124 B=82 + RGB + PROCESS + 166 + 124 + 82 + + + R=140 G=98 B=57 + RGB + PROCESS + 140 + 98 + 57 + + + R=117 G=76 B=36 + RGB + PROCESS + 117 + 76 + 36 + + + R=96 G=56 B=19 + RGB + PROCESS + 96 + 56 + 19 + + + R=66 G=33 B=11 + RGB + PROCESS + 66 + 33 + 11 + + + + + + Grays + 1 + + + + R=0 G=0 B=0 + RGB + PROCESS + 0 + 0 + 0 + + + R=26 G=26 B=26 + RGB + PROCESS + 26 + 26 + 26 + + + R=51 G=51 B=51 + RGB + PROCESS + 51 + 51 + 51 + + + R=77 G=77 B=77 + RGB + PROCESS + 77 + 77 + 77 + + + R=102 G=102 B=102 + RGB + PROCESS + 102 + 102 + 102 + + + R=128 G=128 B=128 + RGB + PROCESS + 128 + 128 + 128 + + + R=153 G=153 B=153 + RGB + PROCESS + 153 + 153 + 153 + + + R=179 G=179 B=179 + RGB + PROCESS + 179 + 179 + 179 + + + R=204 G=204 B=204 + RGB + PROCESS + 204 + 204 + 204 + + + R=230 G=230 B=230 + RGB + PROCESS + 230 + 230 + 230 + + + R=242 G=242 B=242 + RGB + PROCESS + 242 + 242 + 242 + + + + + + Web Color Group + 1 + + + + R=63 G=169 B=245 + RGB + PROCESS + 63 + 169 + 245 + + + R=122 G=201 B=67 + RGB + PROCESS + 122 + 201 + 67 + + + R=255 G=147 B=30 + RGB + PROCESS + 255 + 147 + 30 + + + R=255 G=29 B=37 + RGB + PROCESS + 255 + 29 + 37 + + + R=255 G=123 B=172 + RGB + PROCESS + 255 + 123 + 172 + + + R=189 G=204 B=212 + RGB + PROCESS + 189 + 204 + 212 + + + + + + + Adobe PDF library 15.00 + + + + + + + + + + + + + + + + + + + + + + + + + endstream endobj 3 0 obj <> endobj 7 0 obj <>/Resources<>/ExtGState<>/ProcSet[/PDF/ImageC]/Properties<>/XObject<>>>/Thumb 14 0 R/TrimBox[0.0 0.0 128.0 128.0]/Type/Page>> endobj 8 0 obj <>stream +HwVu6PprqV*234R04S32P4ճT(J +W*w6PH/H+X)Hwr.gK>W /@.ӊ endstream endobj 9 0 obj <> endobj 14 0 obj <>stream +8;W:dYmnJk$j=`^PKX*GV"-/6MPPhMW4o*jFegTA5n:ROqi. +8M?-(/t#IN>re.=TbIMqYWQK1D%b&pOLGa]H?hKs'8Gqa4A/k;[i&\e-=4:h!/H6BW;~> endstream endobj 16 0 obj [/Indexed/DeviceRGB 255 17 0 R] endobj 17 0 obj <>stream +8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 +b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` +E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn +6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( +l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 13 0 obj <>/Filter/FlateDecode/Height 947/Intent/RelativeColorimetric/Length 90241/Name/X/SMask 18 0 R/Subtype/Image/Type/XObject/Width 880>>stream +Hoi@Hy&8_nyA'?6G3+ZHҥYakOj6gיoU GHII_AgK/EcF6LrchI 2$҆ԘU4w$5_7BQUm"Ť>&k2W$%Nib;Iߓuavտ,HJ \u.&1ٌ^₞@Ǥl_Lrs:#ј,32] IJ7d+65i1$Lb#d]G>&Y=g답*_/*:p.uʙcRIf") ˬ#q4Ό=sL&=(P{ HJ+b~n+cSFsm0'&&cܼXI=3zER,D#0)2=r +I Ә}즟 (9?l?ݳ;݃Q~twoo `41)"g476WxzGMݞx7hpqh{ƃn\ w Zᶂ37{M23>)25Eܩo|+>8q/8m y3=??~wL#I\dΕ/doޱ=Hh|d]ү$*wOc} yz<*\@~R/}}FRHxw G]as &lu9x")`m;=-Ƀ -O8Cmȑ{mG.&?){3];,V01o`it4)'ѭU, ]?b<ݳN=;.]Lں*_w6}hL[I$np+bdjlb46[ܩ0k`I{-ɯG_>]zt8rI_K}4јغOinӭng`HUN4ݛ|yRr #+x/>骞SyXAQȃękfUXDvE#&sBe< HQ%\Ωdg'sǟá:>xQ +!K +W<* '%Y%Vmao!ǩkv>w u{=Q<\ȃ*fƸmqY%ŏRpV{ ueW&)!)sE2Jݓ?ҋӆgohԎeɝiFGb}/g. +,%m.7'FX!TՀITV $y9Iפ?_ȼ0;Wi;h9:FQ]itB)`5yIe[j*lraպS"u 3$hۯNT´A}ٷO.ϡqˤ3!"Iڻkha˳J)@) iq1J٦oչrEIWt+>]hlrW9,-nr_}i#eR=椔 5 ](?"aIK9;z>g9d68 =FGY/Հ@@ 9ۋFJT2[̟~>:ekG<Q2B&M)}YƢ}\ekfeJ#.-3 +iHF'>hd,I#_ыTj~Q5cR`n:s e8 P/di]Ҩm +!g֝V=@nI${%3Tj[ԣ`Į;m$XOT4==Aŵ͆ޭ|hˑ"6XWvZY,{&y` wɵs/NٮMDz3z2 F^ęA r۝gB7hu ȲhI})CF +WWzٶ:lgl7ɃHiJ&/ Ӻg.}C'dD|V֪'9TL*4I]6 x74MVK%X T8ENZ!f8ah@&͚-AsׄOQ"2sO-ʃ#db-tgHIFHVj.Y!х@Cdҕ@2ǯeqyJΎC43nw0"D2ȥXiQϖJ'&:?Ed!ªGIKSيb$utõǭ2'}~dbNct`d K񕨈\29juC ڣy 5,ҧ9.~&g(r\&$zkddjd_&n,dk딝]|ڷт$lw)-H](H&, tHU4ѐxIE$\+Kl֓ȁI*?^^O/N*)iit<~O&=۠SZ0LK578hsZ?5ĬeV"k K +>#gV-?}= TjOK<>Nh auOBnY#qkB)fiQ@ 7@Olo-n 9 =)~erŗzC9z9Ilr&JO-NbW3Ӳh adR<+}D?NJiPJG%:?5pn2TI2v֋rBk &l f'<[wm}2I4KtwH r"!hͣ .:17f͝$Wq`Z Z 'R\%n~2/f|i|a*J&r>-fj@eRc}'yGBvr*'r +>|.WB~0ɱ{H ܌232ɤMRe7r!ic/_Ȗm͇OJ!dfH)OtE9#jԝj'C]aպWf+>KctȁL&rK%SͧH&1'ejuQA2p!Ϟ* UKW?-02hZ!nKO.? ZdzѤ٣wrLI*΍Sі2+TI,5N$6ぺ G7 whQXI4:?5ƫhq-Ǿ(v,vHz&.aKiݵdTOph2E ɤ0J>-zBb8,A6Mgd͝$K I,E[ 8ƶ0yTS rlS]|ѩ+&_9Ezb(Jr2h+ɓ)⇻A!_:۪.%ٱ4E3w~ sOq9F$~EH(M"0>"C|)4 {edUŭ߿/|}e.tD _(^u)TJ 8([E;ZgbDR!TY;g$÷=W__h8pü8SqO㠸*5:Mb)r2`Ny@:iXg*-X=zb-osW̟Y[V$J|!h|$p6V2LRwsU""YA(\A[u0#0j>k6NZ *P$Idv`;K?29G3@/)hqGaLH&)#f2ݥ:"@ +c1BuUU!hB +m?IXqBf=O-uS]*pb Lp=a d0 '%}QJ1kv-E&Y%͇ѓ!L6y֯-ZNſw@ME<V ++Qf1XGbu.AL}{;j:1XM;`m)ݒr2??bӥ^"T.4{7V:7cO n]&IIʴ׭]׳L&ټ~e?618qW 6$уS-J+j &HR#Y(u3C"vaVO qˤg/{Nd* w4~8`ਡODT +( ƃ(rVu6z0F|iU6_Up_ |7//y e26kaE9JTh<'| e\xy(7QQ1Z)7#5eoӈ0g+ۨwCxc XSg")-nkEJN[* FAKLLR7R.LBME#@* +~U݊tEEVOt7U콊ؾxԜ'isjf=O[ZO ((A>&]r"т|f0`A|0/2}+58{:!ELTǝuB1HzGQ0g[|Q_[VSor^Gy?lD$g=?ȩՕLN9 +K*RYģpu0%K'*- lpD ID2MnݾbL)OYׯR3OF"R j"iO8%{Pqs ?Hee}-XJNm\-H/}GϩZ&L/u>7&I wQ) /+P.bP"$N55B]={2`[Hnk?-s\粓y*dqP9I,1k[`^A/n3ՕcVna-_s%YSM{b FǠoZnkE%yt$mAs%Ev]JW^xA Xk0v_KӉ i$Fߪ/u( jLIO%k/Sr7B>Y,770ݙs)]ubQ9OΈL12$jn*2*쾊O&iV"sr+ 2LjȞCxҜJ 9:Jʌ H(hxA&xk i&Iu$6ԕj:.E Q\-^*%V@V;RM]dLW<}*w&Kߊ{-7@-&;. +C66 @9TBUfI[#v1r`,f/5n TLֹޤosIwT&\ߍ#UBXJ&uo6TE-EHLu[FUf +x謖Xz{FEr6qiVd>սl +\Uv^dKCR&p6kڄo@)ɛzxZZfBv5nFC `r{Lŷy7g2H&;x@kASYQC,29Wp +c!{)r*Rj!&#8ˁScM}Zi*H&Mf$\P +Ŵ\Id eDҐIЍF1|CeH ldԬ6i.2K8t׎&t(Q+ZfB*R&~?g4|W~ !$1[NIkqS(T['iͧ4*m~@?>KT)ΕJ3t +dEÀ."!|g_F>";,o)%OQ~Z2FBt-Ŵ=ݗJ)Rbd/}0i +3%f_,%.u;}oZ_`>19)ۂ֙Ĥ b- 2z[&;BEz1i6Cӯ!G9htj9'I#8ˁB!=*t-T:zTG\2z;F3}ZgLhHӍָ!fiVL:,N0EwHVsR I !-U֐L1#4zvB`V|u 4i$\"&^Xjbޟ mR q6H JER/-N&w',͌ƹӿ8!fI|1TBS?D%}35fW̊CȊ\!/5n:VC1@#&R&2rÚ!"H OS&c1[pUyuN'v96c9)Ӿ/I W%f<}*Gz{-/0D֧)T!LSXva?:n[ʜUX% *R&~o㹤w-dr>و'r*+*1=9)zKy$Sp$"ӔIWcQ@&~!AZۑ[W *'W|쌰95n}ăF.i(xyB [(58|i+&Yjhz>˜2m^?fA)\('N,1^{,gI&}<Ǿ dTVԀaI$7XUkt sU dl:OOٯ<2w)6E =Lq<{ hK|7%FE=ikA(#cia78Hw:;i'}h%Z^N^7VҺuQIHNcMft+ +0 '0$:HJ\ ;``pPL=NM.H1Eb~ԓvsK`TO=hwѓ7|GOYZaȎL7e Ջ[녤&Ɍ;b1lx"Iw!}9pXfv`7xgV~π\\zπD-/؁_~ɬebJz!QyrTS蕴.}?]$i;o"):F)(&ۂS^/Ǧ1IG )!bZ1 p8ĕT-@Kp +m crE?m}F!e_JRPF +7b1T}<x4zV,&읲yeTJ=q#cz>)Rv:5[QГO"5o) c^mXyTعT=%o-oK2U~c͠B>(h1*h|:6Ll' ޑ4 =ui7eGo{s͈KjDE1!3e00R,I %y)1]5ά>#Vgr|%vVV>&I5lS<v Z ;RLj/1'{uO +ؐsz( o?;I&mT@L>`|wdF[pY!=;Yk AR;o^2Lh ~_ٛ|GdO q/zRqcw_}9~eiq$i[?~$N%Y72zىSEx1,HDlEg3JJy}F}7)?'|(GoŤ*isFGO=`r3R8)p/MM$j٪u=9(&˜|m5(t75zM'O \]]ITٱ3u0Ǜe/$iF1Da*b1Tzz_W8/wzg'=srV~@?];(&0H1ʿ[P=kW˻zBf`d.d*XJZC^mt.'h V QLqr9wTҺ辣QEx=D19-d!}?d!}3Z#)nmDzly_|1^Nd`Q0l9'0NnbX9T0ZdWf˂8d=pJl#&BsԨA7FDqڇJZ*レT=eSH'YTo4>doE|~ $#&1¾W` ڑ1)د6:'P}O࿠ne*Fرs|q +(iC4P+ $ +cT6^b-4je˷O|zS~?_qCjRr̖E˓>jEk.C-n#E<IO{vE5ӄ&EN& ݆)-:< I'%}8HwяV4d=Yv 1|Dh*; <Okr(Ny%*~*Yy&WA4!x2zsY-L"=\Le5ƔRFI%IF\3_87 0>hq x2`+IǙBh#R8-w*Ó1YeVO[x!mh?[ <$|`2s2l嚽O EҌX)#Z!Sр4Yoy?ªf8jO1O_9O"%z dy.nNY2u5UV\Q~ɲ|kxrd'?apK tE7s!m`jqZv[>hZ-%6}az,ڜdKɷGM)،xd"6T\l,&c<'Uf; +w&B gw)#„S]\QVQ>$I_jJX,\^YSd|'zD%{o1!䅇qx';qڈ>khYӷ@mwGyxbr ~=ͱ{9hsۈ!x2< !f!mf" e!ONYW,B-%'>,;} ^&CE"OtzK68]dGRfɈt}B)ILN-^3d[ɿ/3GlV&77ug#K1P)^I\D}&/o>߭SHh+<1Iy_uA^ +sMzC*d\'\z1zADd& +9$Y"?LtzK_14*Y|!ԯ)7$URyuf۲/ɖ$yhs:ڏa<#<(){;qSdLt&}ZHy$yyLܘ: ew}\yYj|aIQ%#?xCE"Oq1Nb5˵I~t֌ZcEb-%Շ8}@i?qqN~ Ndl'\z¤.o !جlݜ(B],YoSO&w"0fr +L&\Pby?ޓur!m FZKGbrΓͲc)eE+WqytT?w ]]}["֫K5Ez~J+T3YnO26hSEH'㷢MO$ta0?j9VuK'/K~CcζI-OV/KѸmkҡuΦ)"&㸔]LyĂX)cML=yn\Ω۬ crї'ma5r(E=pu7< + [rd{d7.`w(d;wr(M=zRy +7]=!Ij9Cidy!NmSiǯƆX9r R:wP<+y^{᬴$eYn;ﷂ2^%)uũ Kw Eߊ. UxlidW)I5Ip΍y%gMGƔd Z9"~t]utڵɲ2E#{Xtp7 #i4i>f-2x jL?bGœ{y9k +AQש'=FE4b2&al6>` +hB");Is*QY9c"1鲒z2klvy0 7>`%JN dXn; ŘWO8@g,צ)_cvH$q\ѾM_@%Ƈ؛XBRcΜJcRΆ1xZU]è-9NEw'c뜠I=]c fi~>?!NI&ļfb2 Z8,W䥌e|a(!me?MQH'cMsY*+!\VuSLQ5Kp#}֓mj:SHú5\bØC)I0> jYn>_+c t57*pT̛6=nW%4EQ4z2~+ɜEO1$|T9c^Jt,Ύ9AŵK2Ɏ'+{,uCL/ڻAZ" +d֕MW.oBgDtʿv(uX4Kp}Gߓ-8,]o^ѓ[J^c(NY]$eo h9[ƣ:1vt᥌e| q'Kp4 b>HAB t2n{'ͩ(*rGOӡz>(-ɘ-d6=г.k؉NO&37{UGɓ*Ysgzҋ>XA[ &f.oqC󢔱gs.$e/;?n>.0&cT51}<;(*FOkE;a\.S+S=˖&EǗo3rLdA˿}yb/IE\E"*;pIыeCbuZ ){ٻ #=I_cN|k)rd@Q?h.3Ed^ts*g5 xQX욮&VO䩪(Idt1>ðh[[AEX̕ϺܓyK{./T˱^>xL,Mo'svy[/*|OJgC{::EQ1SDLk%>>Ѕ<I t -eo$7-gHIΆ(&-^^rs *BadVؓ-W*j͉IvHʞNLO:W%_31dӨXܸvH^|Tݓ+9/Hrdz_wq\e?@MQ̙ݙ"NuhEA(iK̙}v(AHQ"@D(WDUlMĎ-'Eyf&٦Q|~GV ?|mzR3|}pӬ3 QJoJpd\nc\7n,Aײmɏzs ޡ22JywoK2/X'& ځNJ* pbR3|w) A7ɤbrc )#f dp[M_&g][ه?@O*v'd5Ä-`˨[ GOFntLλ=95fPL +&!&;=@OK1Ŏ=5:2J.778&$k4RJGL*sͽ֨+IN,Iij&}`K闍sEvDRϿd}OIb_93Da`(r\|@O@Moߎ$gi)R~cb]_+$H!n~F]1GL*vZFi2T'{ z\ARFMbz~wb1Qe5+zRb25em-3_~Ȯ) ֧I/_Ox^JIQOyT=F9CW鉣)fʴRڴ1[Ig + &NA@Ot\ᏻNoV0[%dRїbۤARJwu oX7h4b0$m~a'U:냰6G8XГOГWuNVL1҅ Qp00bIe΍{֠ 6 =q(B:w6Ñrޯ[?^ORLRIv+/gRJ:A{MAOzIN0n/{Nac5H$,+ԓr~ ~OWllfC+0+ГwځZWBA9%gѓ-y唋w-GF)Uk(5?C%;=GLj/bIIzYw~<HיմFi1GL*t\۵ŤH5$gP!||5Y,_;$8hdẂHh C~aГʜ` 2$R 2-D=yq{IeMИJ)1 wT2#&:=FI'@L&ƥfZDRʲ2n%%AL)~(_"H1IW0J W'91S;nZk PJk3=) {=^)&/75fPOG3-#&7@.d{LO\ ~OywtALL%h//8IeN@7sbHbۼ1W2\jn} bRn@z4M6 +'?Ztw +٫0T?^Г" [od% I'{}q{LII6WeVgROS8 K@D\>&;sp6"l\Z= SԞ89c-|~ۘlr\iƌU?D'3&Haaőom“ߓ?wP9Ô"BH$&;m5wI'6WvyCi~tw g#̵͎.p9 FЧziۈ$)&$#})r%R+ [=ړ~t; ՛!Qײ^Gzr}\10Ouc+#C͂&(_HP(D +d!թXKtkcZ*yͅ (  w¯<\*Db,$=OVp'1K.:|/lӥ+i&o练Ɏ[UlL=t _wHY{D'C%A)Cyt)8tDzPˠ|!B!DDEg ̓~WF/Vs'A\U/! +.a{0Ç)zfnڛ>< +.ĕ#_uMLzb)ZOVfc+UA)" +4D')58=26L">^&Ư~nc#{Uҭ' T Z; $U:ri +_͒K 쳷x#LJ4K\4^mΔX][XVBf@)5:'7}OV2L=Piϲgc0Yh-8iҧVk6\o'Nq|$T($)y6Aߓg O"Hb1flsarEtku*F?L$ |)> R ѸoB(L7H>IwUhc}[3;/)go2qCJ= RHOY$BkѧJ6)b Fl{h-8թrbVyZgcF;HҢt@ʰߓŤA#v齌3BILODxRzI;e5 Rkw'w9OD)Ý$)Cy|O[X)7PG$E,̿6D\GLJ_VO,Zhֻ\/of>!Ç6A0ߓN(+MC d.ia1^j&VmXuw}q:ZLa\RM24Iaw ! +yš|%0KeX\vIِ)H{fZ;qRC{ /Dny]&OkꉥUS=l'凯Gw%)H3ct:BxO 3$0.e#PɶO +|XZE_\(ZODhғŔA-]%f.>nEѫpE/zT(ЄOJs-M*_*PEJ}{ Pm3\)W>[w*]d:@,wZ$IQ@97,aNEd$KeR,}jV]RDP($)]ߎccC$BhGlkFbRz@>ZVN|H[l$R=y:QE>HiϯFxbJjVV5ܮyR^ +rk'eG!% :W!G{DNhJ\9\wACl +wϱR>"j'3J)_PKwG&) wZtݠVwgc)ßHaO&nr#󬦲l'3yxY}fdKJdARH9E}%TTҌS4<zbtXG٧WgB'WVD1T}gLbi[wǚS$B Lٹ#VLXC+;ݪd #+oIA{0]ceR(d֙F3$Bxt@V IғVm̴dE!)yJ>D*:!Zy1Mz/PBIf0 Ҏɸ; Gځ*z͹6HOI`)\NW~㠧£aITVߓMӬ$B'ctL&ݪ\Ylik>Wccyh)GՋ`Ji\1W݅JHrmL*w@ʡxѲHzCx /+΅cf%&B_$Gc&/ ɴ.>)=yV`pB_5B#:ʹv't,;fs8KUeD 0p1>$Bhg91jILJup6ꭕ7k6$?:L2zקb`};=R=fyq($&jkeMkoxs9["L +UB.t/MO0tx!Dn}~yLҿV]=2f^CQ_uyp%(I͹䈬!I-yBs95kIAQnԩ{Sѯp篧Gm2=d7R&Ӻ4M 54;=NGc2ncRt`AJxw&ӷ4p#BΣ)Óe6y0)%L^_׫rhe{-G^O9&%4j2Q/ +LAHiL"CɇvMå)30PfۿՂXa0)QqVgsIV^UB~l׋ Gސp3 L4RI9&M?V !$)n+Ye6\V0YO'j Sm,u^)dw>R CҠ/eO 5`2 j'#=%JXHPe9zTw?pf}iTnQntwR)OX"pO%tT&Ϗ]3)$7ePf[pr޸||l5pndғ+ĽH9zK6]mŤ zͿR8!>u=oD!h`EE}^:zO)vNVB%Ϩtg13nՅvkj,2Y'@]>';qQ)dzٳbT O? :wu\hvߟ#zٜl]CV rneu&w{LJw'R-FE?!R/DJrI$d<12,REV%U<7g:fg^d`bcꩶ[:+7>VΫq ҴP+}coVD0+ [uBKZ~ RyUs.++3UgD0:=/mCԁ*#iU}$]9e88 ?N!"::-_:kúXQG9zչ'Iy\8R*KƸ/!~Tk4NFIʞ.9])3#ByoL>{cRnn[n7V>)WKRQY#UzO?'s=Рg]duC. R> +'}nMty!׸/0y([v7t%OZ`bsI=P'L'ol3{%RJ }&|DQ5M,$)KBcuq\B銞SV'Ofw57'H#RΘs9'R/)Ȗ1BrTk,pY +}+;B(Ř ɯGE'ts+ mF\wO;KlTȒ_SOcf@=-M//:GnW?qPgE9oO%`^ xm{5Hܞ^Ĕ™M:'Z!2Wƶ4ro+nopEfDjtKГAbk&V=Bj%ܡHIc8gRchJ\#YYZi\\h<]R]Q_^vЮPv7>>i <]NlskT?'P5mIF@OU)xUaT}#F;B@ =~_ `rm,Z="]H ?jjR*~yT TI*{zԢ,HF +W!DdUb;<޵s[#Vۦ-Q|_緍Thwr+PKR*B@E` kR.Itiai^ArȥkP_ڃbDJl2ˣfgIrlX?gw>X<}"W +*e Oh)5ЋPŅ]lxh7&\B{ԭxhvRzE,Y0C>yF UJA)_~D7DAA$;)Q&%AGt)K^y4ν* o{MO8pr\r@x"Bqبrۑ]JƾИk7lJ){'@oJMNJ"\ Fc}IJӴq%M d* ,l(:KU+͌'ᏏGJ)23,I#kLZ 9:F-G'\Aθnqޒ`w.kY=ʆ7 ?>:ϕJ1+4V(*il"K!ʓ2G9.]*ӱU@)[r7]>cےuLDMbV [FQ )zeK W2|2& %n0I]4ukǢYNfh;Afbke2$op푮^J2\2޴ )HR>}yRE*oyd } iAbI_ :KUli +d4<@{d΍b}rJ4E7l;|@*w&$!ϧUke2t-:] +,!߼l]}~ =oz{֤X5Q$>eL)Lҹא/] +Og6*beC>7WH+#bX&8)rXu$|RGmkÛVlR޼lURՙ+ڑB#0UIEKأ9~,bԲ surX`,Pvx#Er TUUnMt)NOsT,pa]~C@0 蓉-#$Z|f—)E\%.rolϛ͂ / ߍbE2yL{L& "t{~@C"a(R +tRuf:NReؚ3CQJXkl cfS,hICc=u0_Wfk>knL1ז^O> ~Q'tz`'#W xV +t`O=?7F{Nvfowvv*QJ*0 +D?ޙa B J_$<z;i{wF#e={\&C[r!7&'kn¼~Ѻ{]2 @ *n{Q^Qw+eǔwT>~',U)+DBGbe!z/E"-|tʌWXbvF<6NHP&?pdrA[_Wm_ +5?&PF1J'3p|R]]9M]9LL2 Q +LrHP<ɤv4ΒV^ZYv?`vFRB(M(  +H4JoէX)Ϣ G)<Ʈ@C*p&̟\q7H&5UQ^Z^u-R)E7?A|^u60H%LϐORКr{$$A@$n|^v$zn₰WSo_Z[sSrdRޛ>||R +% +X3J*%0|,ϙ"g,39!+\JdR"NtgQ^ҊRlr?R)i'a,P * Jycq?DVI1? IM<(.-i[-gb\~{ ֟!ɥOZ:,Ø9{ٵJ:36pVݕII- o޾Ѳcݷ85kk,K(;9g' 8r[a/#<4+, +:VInI(o d^r@ԛ/{w?p_&4(eDRcёD>]+Xkdqj22y{6pdRw S^yK RE)10Kҟ(. E6L, bT!ЕLnTνe%U-V* [}iIX+ٯUBT&C86OʳDP1[]\/&ְUڪKKjn#2LvɩO%JzQΎ~.' τ9+RTDL.tdR>"V+[Wo__w7B/W3 O.9+Sl>t]ӉO"oOB|r +VNͪ^32 X)mP ?sbֺVf{D0/#o`7ΒVm_Z_~ BpSETTzaLtZv2Z)8Jq%S&I?IHd:A7.ɲG=Pkɳ)?(_;YDlgO_!;FJ**e' )2H& zӏz,T=Y]=+eQ/0g"cb|蛲IkF $!YrڪKK4.»5Jj;>i:':nA){;,nXepx}P<4MXyd@=K?IFmr[ŬUT=Nr I!ԡ +b(9qarJans=Iu f'-'Lu,QJ RtRJHEx<'*\aOpw@ӣe nhzuFa·-T_~M2I<L3+vm n a`Vx|€q*&L:t+/,C&MXeUH8mL^UI2IV9{5)uR +ƏA)(#nao$<2cIGG&/Zw$Q ھފ}!iD_~ϾzdÈ40ɴb)+2 dtd}wCxkW 1  >U ~lUH&6(^q10Po=&L_v|ș$+Aer@B6.䉳:/ Jy'Qʹ sx7o}'oLO sSao^<I) -2 +$lWS/`_wt7U6?J)p0g%z*u#"#eDy ڔן8ȈggnW4c[4R~zŰ:,,G L}ʳAHLBoHxVۗ~,9ʛ;`:A)10C|E"Edxvۭ +tbX:OZ` RFyxQh$TIcz78'a0y&'2Yd̟L/b]U/`_w[Q|$wYKRwEWp =NdcTWd@gBDHvrPXx^`aL?$I&Q`OnfY)*͎LL cz$GD<Rѕ۳dIⅉvkLn{yL2U+»=J$FwB! LouM_9[ƵR`#JA }IlVk^ݍ[[$<9Z; z>I)E߆Eܘ&LiÐ/Q/|e0A EߊH&~ȑq?, TUt<d`_3MG*&􏩧w +H\A=xraOjVDEI`NI&Ό8隧ϗ7E69t}y^&+tz“/޽%vp\ԧ">AnB.WC(yS&rt+YHJ W"U|C~KJǬwRŔ!3c[1 FdI2qǐxCo)Bi)LN0&%6$5KL#xG;n䟴T%m8<9%S4o6 I@Kq=ow;Gnۊhх|!`Jd}<v,Yl6I&ozIki[іp뺤u13O*QL#dI'LH;, o+R1 ;ϝ DDX| R&K!R]?y;i'|WKCr0X,>{;P7`Hdt~ìsVBF *`V7'98QN;&Uqk¤Th:0l.0Ϲm>h7JmTEHy R}ӿ_J9pIcT~B{Lh"axov% L"%˛:0Nܮ7Jm7XۊdJY>;)E{d ;L*;[0&|X$Hl٘LD'qYTjd]#mcw%R/oSa~/؉0+ ^&? +\CD}#IgJE>Bm'Rӆ"ixؐcχn' =B3]LItf-"`*E'GI)\R&'vؒNK)¤V3,L*> E}o|| +Dң[+lvu,ޒfZ˭+G_2T$fvS3DJUzDJF+7$; S2[+K}t]aBz1e m л~> R"eΙFc!έ|F W"%FI O)KHrڰ8:3Rƿ$ %R$Z㼩{W"=q t)pmyۊd@}zY:3JJ[F,qB&$Haos\7h=$%L&8ͤj߹4n1ȡ3)틤 % +n^_G,hZm|R0e"<9XiI$O.>j<3!R i^L LrД/15D6ĖmEl6uA]W6<=H"Hk!.I4yIܬEKLj6#k[F|r1 [C YI2ܟ!M]t-u:~lDAVtĥ3LMU߬JI瑒:vT +Q$EmHfDSɼ߽4Z&Q0"v%Ls|'瑒PJV4 h0yd…F e3LV[җZ.)Hm^sH=10 H^;ly,pg/B~\:Ôi| ٘F,& z BF +&H㑒#RʆBl, m+ +L`ڪlѠ6~TK''W"y0i%#gL襔AOI,g~et)AAII(kWu%2?X[E"\NHZ[Ѯ&QPs/Ƞ)_bɆ+Z%\yѿ^% cG_ I҆)щ7dDo·=D >V`%^n_&|l!#Of!!e +D'a?t1<|)zXZgz9x;$"%v)i)юec^$RӔ/7hJd]kUҳg}qOb&3IYJD~Fە30k1.zmE[_=.FZA<#VNɁ.g*|Rܨ=ާ&Ir}dEGwL~F4ƤD&sQ> &nl.]bj` Džؠvuf|c0~̈ Dh_ne6v/r+.& -BcO"N*HsVpIzQ.h% P@ Oq-ko&zcǛwy4;k5$wxPѰ@cl.7x[:qΙPJ0${p!YKVb1`= 5Z-0~),ȸgG)OEI)[K@#$|=+qPODo[kVf'g1sg-gf"p]N@w 3߈%-Y,p|Zyǂiy1ո*DIOu=tNZací( (8s '%$!@6/cAѲ*e}Y>Qc33gC)CB2de 2 ?eGneel LY(Q4:]rD *l= aϼ/_-t쯥,vAh,XM}ow#$D筫3y(% t0b3F+d/O8 &d3cAjBtl/hA;];ICJQJJ d©o-Tz*iʉXF:72'zڬvaf@eJ;}3R=L3/NFL>tZyZb*UVf/9!l{JL@). d]h +V$*#%RJsci$—0R.dL@&@@R+Q,8+δqh_=DXq(%8wr⧟L ڼj,zIQ6ǃj\kNA[fk$^rθ%rď?;s +2 h"V <44^WGúZU6v=JIF. +ẅ́c=M~_ghf]Sɷϩ`6SVOVd-6秋1}ᓈ/U# ??I}G> ;9G'#~,CڹI9=3 z+{ak?qz8gd%$}Ye喱CBN=sXlQOO"~ɫ#旗Y[>}OM ʫߌON 6L_=>~|'ޙOFLYO9ϙ`hg&W$m=4óI@A3+YLYyrAg;5MWځL"~6r+WԻEI0 KVs[ dE-irB ʿy?oGxzt/DhG╯O]Ͼ0&ѽsg#>|Ȩɿ?+V / cAeò1?7|M<\ݷ.I[GK3Sԭ7.J|7ux纇>?<{_}d)*n?&LC+YСLmp$x>}QҘe1LWe^9RgΈ7QdDGp#oU]t;w%ǂF 09IJO"~Jh(^_^Tfm34{ɞ~{xyiIR'k$4; $hY4J D|suIng=UW'#4&+qDO8YuH&UY.q|2ho,J,4҂ک]zTe{lڼOM5ZI'1G+a#5!aa@ʉ͔*ڢGhs'io K0Hr$>,ffQֲm[lE:>"KVF={jٕm (hql!!&$ +g8& Dỷ^/Z,iUJ|β-d@[I$ٿ̀Uո*bw'C7|B4<});B/R^`|2&V溳CI7Rr}Ew `]iiJ @_hF65'7leDőh|[)v9|a6'E̊X @hF I>l'(kYӝeO"=~͌h_VDK#-JZ $zf@lO&F[uΨ \|]T-V~ +$HOkidm'W'sY37%{HVЀT)R04+q`8AW'v_+owQ87+PUOKi 4k ybk:jO"1ƥh_FiEIwsgh@jh)+ T㪨UEKc rI`KЀTOkpʊSK-*|aJȜ7xLO}iz,iU.O|ƂmXmSuF>Er$SMQ\M&> +<}!jqj!!GuW'g!$”P'/\FϩeI+T=FZH3'H~6aF50t +J)H ®A]T*Cp&NlLnfUYT*V%6a50C00D?Փ+os9ؿAtjJ|LGq'l/-~zG7 0OhAW\m5-2V*Z<!Q=dF-V"`R!ZZͿ |;?E*80K2HGܲ,K̡x9Ulu,hOҰUEĵ.d쑭J (h5iI5˾:b-#0o#%E?+IFxl'Qw`4smH*3Ͽ9b#|9.Ī:Id .2}'lY; {oCEM+>#XJ=5k vi-:ӿl-ŗ^ao}^ty`$u-ž+fg+jZб>mHȢ+[wQ>j`"!jU.ZF'GeXM)p_0D[߉+vhh5ֳҵPZyhRUhs|_Wm(ًz%QOC)MMrЫcK3;>;|z2 vA' F;Ͽҳ*G@ΩV,|oakh9> nI4E##ɋD\[4s"%6 *Ap'6Mrg> ^L* uKg>9ڜOя>5,)^I^4sKj[EYӸJSՔ$2;A9+[:hZyt>#kIw.=5-3(zv||Wasrx8qYOIM$Uwъ -k)>%`tsg^. +{0y=k=+78\ɲE*'k_k>1+m;QOD= `7twKKj"T,)'t_S>)yvtp2 v*(lKj"Y+ldTmNwv*'q$me -znh)2ҵ8'VfE">|ڐI0&`@6,{IMd(X\_IO"`ƾa߉'D# `7) ^*R[US$qrأsm`?opW.I]D6rh*s'dJ\^LEgoĬQ솖bsB!΂& +=Sb#VS2H'?]/},6P. +w0iO6si"=[Դ-=ұ7'#_Gp[rHsē%^ lJR +$EFj-3>YM#D?Vx

`t ~9:X%I$i(%@A-#kY>?v|:H$'GG߉ eZ$@QӪ[_O٢+X(z uȅ333dC%H#{0C&iǽ& F,N>y=?})'~fso l?3tb]L$kI;:֙OfVTN|I"qn蓉D>g^mboz%HKnX9`yi[EY2WԔȨc~xLtrId?Βw;}lUנV3'kiJ4'g~/W.$.p}蓉DJ[A`Y/>Cz%QOP +C#-%4 l0VE>љxH$D#=>D += $AYH\4:襑SO|#܊⟞={G.\?}|ٿS+Kڸ'Kz%QOC)JJ6sµ,LT&)Ъ?n8dU%璤䓉D[EJb_yv.`ti- {7͂^z6eBC4G?㙙SHO>u|tr/}A`~ +s}tHOFBx7q!D5z[Z_ϊGG77?EI#^x:{i:<.! |2뭧 oc4Ό lf`̈'苪RJmݒ؀9Ćv~JժR/zҶ `!Y`se睱6Cד< 3 e+vPa>z^dPϦ5%JiB(.vnBn=4f]WyFd~Usd/0_OUOJu"aǰm$%[/MJ(1M*%H Z {X1Ԯ(e4}K1%.ghjR^6'Kj'!f+-ubY?GOb< +8TSsm֕$+F".P(. +Źڬ6:TQߵO"4TJ͕Wr'x(9$ IO= XN=? ++38B0 gS[=%;ˋ/qUb'D}$C*,=\C8/C1ԮԊ@;mx""5r tHoN ekk+k1r#@`>vt[#™ƨ'KfM d'S|%3YBWR/YCDk'(κh +@S8e1[ gY4UrgI(9+ʝ'%ItuKkK8ӤDtT0|vƐ8{bRI6eGX(Z9-A:E1/'|Ŵɲnw4e驒/tDyC=nzu ^<CO / QL#8K&<'A˰ßɋ$;)rOt:D姻e3}lߛDObh{@Rbxi"/žI)(*~]xAp=q S͸CfT>|{1~05$Ia6e?*/W5;glkJ,h,v(uP}J>0:x)[KUW:Rc}?)% + JZ$O|v؟ _ +P 3>o tC, U͂d7; V %gI${r5Tpi`ԓNߛDObZjwW,[\{S󥐏D|~H՗(/)K$ 0"'?nLv$Nv;U 5K$tpvx~Oe=)N#|=!%0#\ vF +sS0Hb<)V:o(Ic\&zb2|1$m$;āko`\}|0O%_߁RȧEr |'Puqn9dԜ;x@߇uZH?Jm K]T{I.;aCk(9 +ji4.;Nꌒi2:dm\xLd>v`n+̿>.ҟTQy$K!߼>^U+qGp)gQ9ݔw6' $惩ڝ ^f{w>Ki8` _~\j07Yf;0,u8l'u:kn 7)0k ;]cwՁEiUQS7b`ޒ*0{򹌽v.ԮOUK)6tۉ-XnF+6bTj&ٓC5S6{;/$_"U2lh/qsH}_  +-vY`+Iѩ"[pi4agi.uR1Nɬ[x_zBRamOv KjbgHvPJQImHoT'iWB?ZF|2.u/S(rc*'}JJvfT"xL_?;Hɂ6eEk nU[_NdQaUJZkшvw1qR +5mYlQlDne6$@ڡO?wd[:(Ԛyo5bxpmZ >ū +VkkWX 2.$<y =VCyY_)*=)$OwJRozj?D?@h|8և77_!xK}rBv6!f'up-0mA J~̀|%G||RWqTmήtkC%n'OJɕXB"ÉMRd|Or i/@#5<֊@/wy |rayxU6E)|/Od^msN̸RvIٙ^pN}I-נ nvTSST>rOZq ,|2J}WB)mVJ`ٞ)ia K c=>r 6qvHBgzf;&_%\ai/^3# {a5U{-铚d; $څu&(ڻ(\'u'Q5ݪ7j}($TPR -)w.G#ʛT&){xf LZeG>ӁVͱBY*kR 3]+U73k[)g]'{0v,6~ ASyZɺAF̞{ cmcS°/f@g~R*ӖZ]I]|ׂO*q|rQd*I7ʞr3\mV""2)vY3Jqq Vs~k}b9EM +dKz9A|X|/㬶#/ÀR*b}LԦVӄd.-uךxge#V϶# &O +.KwfZiS痧2.&>)=bxIǫv|'QMMJ)vZRp_cVn-" aLJ pZe&9 +B/Gne^;͓SufuG%A}C<*xKVߜ('5froo? b,*^uZ vš\>k'_27ɼ<ņ$xt{]Y)V“>ʜ D 8Ҏ<'gy'G&zʃp}0c7ӳDo]BGr "$\x7533> +olMze[nw hyɞI>j[IJ)J"`>enX +EZU%RܨCRe]`&Q0,Oo2L~r ?L8vVS>'"+=r!cTVPv D)/n_) +YʙJ* Vلfتy&R'=KWnH'EUvHD7YIpB0/ZpmU-)BǙг[I_xQAvX Sٵ&($U%cں8$Ϲcشݾ`M% &Iv/ɦj*R2MUjkތ1yH3̐tUsJB˵WGWߗs~x < "<{`8LVѢ)QV)U<}BSTG{XØ~!7J~pOsW֗dy%#Qqdd=a딈('lHɟpL/8t y7>S{&JMa$ )38qf R$~,]r@#,O>LM%~[Mp ~a'N +,gGCO֗$Ħ223؍{UQ0!"z^"eT*'TмM9%Tkժ $e:;__r'8j)Iԫ.]k#8O +ϓpC`:Tjϓu4-ZCIOKÅV7~fyuJoyR]eJsDx2O-vGض^DDY? pUΞ*b6IYq oe Ӳ|x9 7}tp΅ƻDJҴ%-4BDe<7JZsOټጭҵ8q $DAiB<8DϜ9lJ.fO'AP `h);ă\A%vy^;ʀ'J RUa2yr ^njtH_@JÃ8؏'{$~rH\3NH?)4ij,2 Y7Os%Ӌ A1d]-k|p"hQ6ɣTaQYVeUjNo2&I <]HIx2dZ$"]E9H fN-OişbJ|NW~1ӷbS.J_rBP.Def>]0ICkަ1td'h4Xzl)<OR#dy xD}V^v[ZE?MP""arO:%[*/bIr΅~Js}},(:A1@7}| ؃3nhҩ"jeU +cA + 4"E(@cC㝣2H!:ovj'+j' *'nb`rZb$"24RrqpUwL%@`_FJYAZG̺>- Iy *Waq;zGh9 @^ ;[qPAC`5OjZU6EU3]i&IW +PJPpL>L:_HIWi͊ +5U +{2-nt IHR2{r,҉B܀1`u s% L^IJwM./?O¦x6yp8"SgQw%aTB6P!J.ԘsFbJ'\ :b҅E&VR]z94x I(3 <)1 )[*nE u$Eg`CTȠMz1+b;jAeF]/~\0B^j-ڃqK)Td9; KյIFCaH*J?!*@v<·=˄ǮjsFӍڬ Y8|vG=EO@b/FѵL~"a2?h.+ ؕt~ }A)4N=05@vI x2[[M<&^9ӳ^:$u두l$,) \ijāa&F=64Մ>?A_xc$L)vK2bs7u x́'SQ?qoLՅEqD;p +4OҋcHJ{2cr2l'5)Ry<8ϡ"dAqx-,On!LPP` ɦTO\RFr!~F 'cA￙c. ݆cʇ_{;:1kڸ9G(j2fV-9iL7j. ޓو[[E '-D ePv|&oo8>3}=y/<2!/#ړgme_ +./g MA~5xR/Ynne@=c$5!“?iHfUφ8Ucl3]\cZ$<%cat3jؙx2b^HHH"dPejHkX5!\;hGЅ(jO45qd=sQ"y$~zfp3kVyD7%s3/iȜLn +B~ri~?5c2 $HCDaAř$""WW$uwjfvvڭڣfv-P rprk췻!NBw߫OP <}- O[|'O#e#g`RJ13C_^SlFI?}I8nf`N$|@ e,ydMUn$9K1|N=@~{ 太7$ŻI_2FB"l3~n@♨z2|Rا:Dx|r])O03[<+$xa0.uN3zގZk`QS}m>@,5:,kQ0P~"@#!񰊿 ^)\3g%݋N0O|?Z}1I +DPÁ2$BGFťs>7gO` 伍r_`bc RJX䨙m^ CWۘZ=u[͂\ mRJݦֶ̗́{CG>0P KqQ>UD .ҐstӢSV6 &a!0ZtЄ~wQ>$bUI`5`SJ`qmN(`فX{VF)y}U|RWT"< b?<ɾRꑙ-O,_2"ƼZYk>sa8 o +r+9g[9mj6FO&@FZ{->9_b uR +'TYXSpmx5t1۪Od%N?`jb9nyƎDwOe$o>9lBރT1S G%įNL&6'$;ۘXMY L+`" |2;[2}r 3ye -/1 1JY+v"X꫟vC0d-1K$0(\.Uiiڗt ĒeBߎ(i&©Y) UjL6E+Ep%L \!@}co1q쳢VLѥϸy-e>La 9;\  :fYJYC=i[IqK=&\CkZn%a|Ju>~W-m(SJTӼ غdÄDl.탄<' ί".2rA Quj2&jBWb̌}2d.p! vGZb0#~6z`^[<3-;iP0Gne䝒_D*(ֻ)2Rh-܆Of ۳ådWዄ7<r7x9KrPTY'~a\ުPY\zllfLt'v"<:Rn|BrKbƊ%3˔[_Dr*#B}ĩR_!/ +]bfi"p~}SL<'(%Dp)"`G~) MĬ5lkz9o'hHpoW|>"WyxbjZꁣڍ&X?B{O¬V'u~` zb3Ta0AC.B@.9ȱjIdªH bAJ' +|;d!I쓻b0[K家т>Uʑjۀ͚Khw+VN-=ƨ_SgM 23Le0/!׽ѫx$#}k.b+9@BRJ.O-cv{e ߛI3㓐-—>UJ`J +Z\z服`/~κMTQ)=p HoJ b!Orw?tTŇC"b3L-E!r[FCWI_g4d`}]yyjIIddV&m?Ttwb`vTJ،96!=i,Ҟ;ƨqi T/8L\KJ`s:=ީ'z PG>9PF +tC9O皇WI=f~"Xu>:63;;n3>Њ<"*%,Z-.;гv`Vrʈ__:\?HO9S[sKf^p)UDxɡ +ꑙ&5Ԩ]U.D*K&NWl3|M7呜OTTO-Fx?֢hG bm f;M)a%5̮$y-5{.@&A)W8üܝj=P+?vu܉pHʷ)Sdœ t ԿE{RV \ܧw(xXEX5 ~OBHO鑚/دۧ;Й1sZiW*AY+,i)jʃ" +< ‹ng:w7}v:ӝo;l _');-T[L)AGuD5KO   wQ@L0Rj$vϜ$ 긣dV_𹒚- #l5VOJܗhr*-:*LW\VA_x#?(fouʊglkԖVRp0 [1Hv}#eQF5 òGRf!C1 HvY CMƜoG-4ƿR9үx'MwL?! +veGT +^txZ`vIr@ 1P^A]t3snZ9zO*O>q*eɍOB,0LfZmq'SVuǖ֡&Rj= =aٳәqG}'O>w`&mI6d`nāRid!`KaS^x{x/c瀵_]=ٲŨpqÙ_pN:XG`z·uIwEr?0OadmjV2D炝CnE:r\IMO"&udV01wJfrc"<Ò6ZDT ĔW1eqN8{մW~~'U'ږ-/xA*hxKrӓ6UGR;@!dFg0 CK-w@5%&ГlJբ>aՏD f7&'Hi NF-iWI77]>RYiyEEqYM +s_Iǘ'JO⾑[q#%O#V\/HRDa)dfhjoeN[CBy9zRM)`w>/ %I LwzÖ ⪟9p_x;ieeHuJni]tEJ Яxő&4'E {BvhZWyڌWM.ozil2rw#> ;YpQuϪ+>&ܿۗM[1gt,1湐Tjג"ela`F-xJI$-d2'1OuHd(0LNeEnrow"jS<$e:K ? (1hNpxI2i)̥]oU+Mdoa 񻸄16>)a?R%]E8)€TI=XVd) %EJVXp.idڌЛL&^ɇ9Fx 2)Fv_|d#΀xcrs̵sXd`6ҟ)fMÊWl!g۴{RۤQ (H=߶:(m/ 6)-DS9H`OJ SĤ +)AdH LXZwyøEKЛL'jjE/ &)6*sę|,$CJ`v1Rk݄'%$zRK='1L2)+wO"JSVs$'IO҆I65Gd 2cnx'udV/8<4_ &5RZCDOrJ8kcY)tFlEA hT9mr^9MO6MM{O +'?K6H2$li0gmN:Bk"%& +X8rKfãÒ2-wsh9Ȓ U6!KR<<^B>aBIk  >Ʀ%W*aKkջ)h7'k'G x>2Â{f*v@ReK쮨L@43Lzj Jyd/nj[C-=/$~$+1N>ۯ+u>?1Է: Z)g,'S(XJYO7!JI_s6R:L\,I9n?'CVOMN)iVK` d3ZA@)gY7QʷtۓIԢa仇>Pܟ&\f4+ѝtj>iyIJrcH>' vvƨb|#~z"!)7O(5SycPJjteO9C5n@)CɢH䓞j5-8 +oH\6_?৖ +AEdR r+TOnגRTY%rwͅJI_O,^cId(ʕɞNM[0r3 v;wŁJI⌎<*D +-QO^g*J= \gyhTԕh$y(VC)C;V7wͅJIΰEg|䓥m$|2 eS$`LW)w~RC!c)eJO)[i_)I554<|2䧂!zIݭ3UY3`CR̒$igC(𔲙/oi}rkQ{~C'FOpj|OR4SsՆGk}ROSIVƝT?N+ʱ"Td/ojd畒<]}u(k _m@>YI@&CPnF:zL= nJ9 ؉~82Rc\ʏ5:fe _Nz߶bKK>ڐ}^ʻ=`LE) +ոͩ.;'sRrO^)s2t"CwUuŲ^cN譛g^p9H*XxhyILa55GO ڻZwE6УS(a Ԝ瓿jT 'Hrf= Pŕ&KwR1rعW)ê"ƽr%>'7h$4)*DjDEd@v,7L *%MLո} ,8'AT)ͅo|-fR)],E|2 u'|Hꁻd?F_P)Տ|lwɲw6UJ}Э&mK'i*>83.āe(0 ČWJFm3;ǝ#~}G\J)m-:Yt%8'O_ pLW1Aabx +%RqY(%m~ apink_)%II_)9N2?'Kl[* |2%i/ytTw)2R)eatV?ͻ$tPJ1֓Mm\Њt!܋f˚+ ^\өX2e +LyY&SJ27.H+*1; ܏7JIAѷ~3lGy'=O;kd $JĻ쓝 %f +K@) IcUR#O|\);i(d= pm\ ?5Sy[@_R铕:AbR +۩D֢vV)rmjhg%dKJ *ھ{ @3RTThyYi(/H wg}Ma餔9ZcF^槕R$]?~RM~d2}23RD{S+q.4, _k#e;l˚HX~?+'Tr}0}\`JIP%ftW3"$LD $jlN9~O"2{U7!TQ.AsS%ftŝ +% opV 􁧔RnǙ3T6(ja?!%QO\m&@*(m;Uaq(e |qC?VEuN?,Fc.>rs3LcaH*tԥa ^{2mέ L͕+/ɀՆLSftq_o'ϻ+J ekO>ד>^~oGy2wg |2H%dVy[kzhr)~_f;wՇ,:J +X\H)iN/wp'*>'0+O6d݂5sP"H$jIcR.fC3˱Lh`z֢݇TJV)AWed~/\qHRepU?}m F+`y C_ycc.#1r[2iN}5ՌeszِŃLTr8Fk?#d kn'@R[<04.o)|rS2FOvqs[[ .ᓣ器+5rRJ$ApRH(uВ_MYro_bK;z'G&:DGz`F)iNPm?!R\KJ0}8ߙQJ\d%sSҕ*I>92k`͔8p^|{\!y"?jR1JYgsy5TRS"繝zi<\H|rtBբw].;7E,'I-?^?d AYJ)3إ;Hm[O(#B)vn-(eHbi@t9o (e<^/ﰭ)xGE.߅]/Q U[>>֛ +9qD`dIyPJR%)?cY> ePЅh5n 2$򞬯*`j?(9_dXcyf=7jO@*d7HOv!kRd9n-ҷG^/Fxs:91@~6mwQ$03 =-)AJy%kY~ӺOŘ^6$* N lsUU8p /#_ 'Q~PJK'9v:>үuNj#<2rǷH#-Y~65 ϺF,!?<A$~R۹t#̆\.hǔOӌ +Z֛HRzbۙa[]{Tх$5myS:~ I)3 jRg<$'IK4@Cxg9մ9 |=Y9~?$[PbXh IuŨkZbJKj-5S$OyaadƦPT+j_ȏߋ^4w*q^<}yy]=ܾ/pDΈḙRRkA){)&3rϛN?O'$HFW#ZRfG?-XĒps(eƯ&[ZjBe{#~FF/aX?d;3J igQ~h$8ZR܆z Nn{RC3?>i'~A׆~-o Hy}ٜt6Ccwg=730nyOEd).{7?*:20>쟕'JFZ[SH3I+q~w؛{4r@"= zb-<r{ojڪ(,E)P nդřIJFJkH(-Yj!h"~0qDT|ӞkJ=U +lÆHFO=Ac7RNk%7F֕FWE%Ώy!EL)Cwa)K>˓&e= Q 2@(!$xPCgۏ)v؄4& ~!vپtR3XY0vэQ'gv4 Ő<%{kJ|H; Α{F03ΈԾEiM +hT`HN90/,9cka َqvЅEqH#uۀ,X;: R>R嬢p?|,c ד02󖢨T ?/: IF{rgkazX,E^-)Ja"ŜHF}):^W{)zZ\i|wmL +ifɍIp q!,iT*X6},I[G"c59G6@BOvV E#)qeȿb;$[/p'g"]8WvJ:HJ;5)z֧b3C"Ͽb[S +ӭ?2g+XiT\$$lR_0(LʠRu_Nt)Hȓw8Qw0uL ӾEu=x43ȋ8Dq #9䘱3˵_ʷl ,qe}>l)+K  +JEFYxGðZڼp6//g}C՘(Y0vf8'ILp<B*ZQYK×{A "HILeOw8?^:'Ӿ|EuPG;'Iq;6'QdvTɝ9~dmֿ,,&.I#:YvRRiJQ@1)1ɹYIJ GQ}SC6`ȋ8//ۚ2Gɍ # l0BUq2,qܐ?u&<N";^RHyDR&MTs"巆aΚ}({u12< ߟh1Oz98L + ՘ X>egQQLeNVH Po$dzHa#f#YdEB- ߛCC*ƈi/ ,S;3KuiQFqo(u/ '%h԰՗ۛ()F+eȿb;3 9tl`ܚo*KERXv~KRB^((Cօ]ey R1^lfj_RШ 0/,9ckȵ^|~@,S,DXF@A ʒ7,MH2©VL59^{-iiǮbEB%۞fhHb{r؞DVx,1e79L,]g%_' bO2O"<,}u}<8_"ߗiE޿VO:ug몴S2ރ(+/+=m(x&P=K쳔| TqtDܹZ)Uw3ĤLIkAQbd-!*a^\0ٯ64Πg眝G[1vplK*ef]#!گ +F|Oo6riwȐhsv{ɓN-߳e9,1 Kp5$Lh@֤l ?ehZ)_$񗃺Z'ъ3T`0醴*,޿6툢,1rj]dQnpW?R g=r`0E$+HĞ0og NߵACFؙc}4sȏ;{l[D] +7DH;~аLf +Sf 6D~^#eAqnuMx!ruA<(`W8[eWha dڂƤ=uN2,.ۙ@JH3s@_n#HŐ8*kZd:B0("(.3Fp@*))߃eނe5I K0A)^)fgk|1LC0R3Kl[A9,1^_e4YZi^I7QL)'e+c4"Lwƥe*YT$(77׏ "%z{ 끚KPAI%!&H9j R*wzR*E};S΢Hy97D/oN8Xx˒MOlkx76)O ָ/wҦr.8=)ʅ))=_ j(DD-I N+5qT{{xRֈ@M~e/҃*)OJX_raJ,ϼA>0e@9|ϖ%# `paXa6`N=x?3z6|IHiH +}!ORԤ{6XrK H~P.A^ +㨨%Dx`U@4nrEʙrh߳஻ Re0; F +sr KU+m)7{biƬw"X,wrI 3 ak')jB= D;`)T (?et@T +Hhe/ 斗5~,(YΡoOrkW8:<}g7GQ_ކuC4,AtI0RH)úlgŅ\DWXAIIQL%A8>2IXbe)^0"3iC4f +<&)j#H9`za>(jrٰ,*QjL6t4.~XDʷYѓf(*RJ6N(Er>pCC_k٪AX⼙l0lzUh"0}\@vZhz@|?M&oyH9`V_?2WȆig HiQt ]5^#Ð$=ا 3~ͧ)RCx;< >rw ^K}~F;S-ϰĆFљ`oLJ²)j){^(̐ RRԤ{̴ `>aVWUكqT,[Uo 8/(9)ZykUjzOI֢sJFQn "ay#_? "t94_s]0R4R"BnqD%H=NJ%;dʩ|@yUМGr~#R23D_G\*ᠨD9"j 3i>ib8 qRͲ&kaҡ8m3ug=r`vu&r_}X<o8 pΠHY [-SE][5 +Rv! 6&uW,3t9Fw*ʃ{ٰuK8C0p%<'[i>vw& +s.}93e(;=aÇ.4s@_5 ``V +Y\e0I:T'%ybH͌HٽTۄ<BHD{(jJTPR2ϓ†eF7TKp8I9?ɌO3LL9Г9zi#8οvwIxΜːV6j+5UWizjWEj6UwەY !!`\CUO"c}Z. !m`n1$ߙ, ig`~W3g,j.,Xh#&HE׽^,eD8٤>fugy5sR宺_y(iMF2lnL^UKa+;*[.2cڙ%j>TyYI SKcJg)exJ_7HJ +sZG~58cL2a~ɁeRZXa PҖՄ _"!1aRDgT.c 5!`f#Mt'$>0r`9-E* 9 M;6НH')ZL>oVs* +MȺ6v1zDR>Փ.1|(aKvX.(Xfj"C>1L@d"'Fփ(W+ +J|=jR ? ~cU>H[09Dڄ°fX·82ӫ蒋1vJw[I{-NKnp6D`6KNsKv9g{,Ťu +N8W5@/32WP-;E/jRF- ,RFŃ/Zmҙ _lox `cGvȹ!#M4.cg)p31R'c&SA_ Z>&)Oü<<^HJǓ/3+a^<@߲Q |MwuR߹@7`X˅n(i0")K=S'Zs떚po!)1LWtxC 0V-T-vseGPq)]Z@z&3_x3Rj3yYO‰߽H7`+Ii +Mn-MHx .b[k`&]Oz5^B뿓1̳Tu1̻Ik*i!%)/a3Sw@w!f6rݗy.(JlI-e1$O7LpEsߣ?8I^3 g`A)ڭ•T#ud3"0LKWvӓcL50m fHwq`H)[?f l&}x?gr97 Ai3giۏSwOAWUFu2dmb$xj55LS 3R XoT .1v9&H#l1 {*mD:Wn[3Pk.#eI^{?/w%kI[[f(@r8\ td`%}sO:*+++ŹI~ڞJ <<\pZas``]j/OBlQ+jgc~mc?Go;cֿI5F2Ɨ+VRDm=7M +^jRV͉ao6pj#MNb(ޟ ,sT;T\K=('HJ!!8JngDoi rǘ_u* > r;U:;Pg^\Kô'V>ܨ~{{-Lu0lm JDr!pOw +{DJУj1 o + 娟C_+gO'Z%;0$l$SㄘQJ6)N5@A˹ߐwi^`r9._d 24-g"/=pn6 c:) {7lC+?WYz7@W*CUFUu <@#K@>#sDYR}t*xB;0fzH9@[o'#FM|*7ѩ z.njWMJX:|cKjg̓DJ8;|h9JoSBb)X^K=0j{6p;{mo%Ga kki!5q! ;>KI)s$#iM^# ?Z1̳ſt_X,W"b&)T=ej뱺0j ̀Nj'%R[ŏKJ}aVHZ㗙vJ;=aVe)2}.Ul +΅s#%a-ƲJdeJY>UJYIɁIiVaӽK 0;oU0m)Uc6VcΟ8W4վZ`(LiKmɴIai0iPsؒm|M(%@6`1i2+r8@6͸ϻ+ɒ6vw֫]iw0aDJ*W3e09StM+}E[%djj+bilLY=<&>!Ϭ*cF%>X7ɃaXrj]YORu6fat +`鰼!o@U-@ʁLFV{[De%:BgO<><9P>=aҦ)"0lZw6{`U ++ԗ֒oxػ*b R*ŰJId>a0 Q00`RYç=0l'jY"o߃*j2Mhm`)Z !)؍d~q3$ɟ#ؘqtO6  þ3$Aw]`q"6AQ7Soq%jo~<˚e%ɇHiTr|n=@K(aX4ݻ6iI Y'vaؔ*Fp.c+mnG֊j%˂zWߛi"o338n='/;@JI̜0R e-?iI#0,M 4GheEfh$9Z|%|Z#zihǛ= z/ptDLq9iY!Z9s-誴ZZ!/ů Ib(\fOq=m~0 ıJB7E3YW4-V޺,ݿ z?pd=jo^O&wݭjQ]ptysYГ< ΢EٓqR{d OQ zY2 e>8wI2m$G#i lZ| +bFoUpvhRJ_ß<ǕmNyjȸ+M5*+rgC<=bb?&&ޔ,/ iķQ]3d̓kxa0J1+Ŋ:A]՟&jK@]+ |mm>9s3^,_(yYHĨt5}ؐD ++e]iEOyXfA0Ko0,}jݳnҐ2 9ُ ace(Սbt#h)EZ|tQ-_zh|w۰zsrIjZ|]GcW(;Gq(>f13ƄdΓpa5IJ$*/ &ia'mI aXGK1h^b,]~ּYx8(6T.f\ m:X-=FPbZ4>ѓI({ndaبIb0$- Ű za 4ԍNc̈ۦS˗Kõ? }ЮsQ14z4]rO%pZ ĸlPbИ)7'$Vkf=94tb=x-/#n ȟ#YaNMJ4n#݊q\Kj 7C{9'n6\:Kwe'cυu1&)#z=i/ZN)t?|0 Linw )Z^u 8`ؔ*FG8 ( ]ֈqu%ڌgi Zx1\jxC{|7IDb^ϹC#JI J|蘄FA\/%L`cRIEZ8P>;Ma.;nI g"&1,mb:4:.11hLF(9juW. Zط*pd};]9_t|\<\:`9]&a46q=ԞV{ɡ":HJ Ib0\&p.e} ıs H4R74mk_׹|_|9w~\Y.Уբlp$iFaS\%2mg;wתk8wkQaDQgT +>BxFgydI=i{?~>IEؠud~iZ#L˄>IR@y-= rVrzl/fa1+xr[/|/PHݗEo8x45OD??чD(<<|4&cEGNk(j|٦8)$(ta}i\TŨQb"auTIFA) x;T xl_q_[yN꾄bo[N(`^a!q8ч-nwkQ?ߑFF򤮘LKI<$,/rV|Ơ(j86gA'!ٮ0W% ܞhKŕxr|f\AO*s]oDZrPVԸ莡KɍyraO2L wkI%~bUYg͟c~?}5$ ?+e?@%*Eaq41sCC*-\V|kW? fwJ|mFZv(`(Gt#p("1&0 `R=cSb{¡(jxȴ|,F[֯uUٴ] p:?en\ʄ!W _soR&uWb-qY2"Flo.;XPBԋG &W:GE'%pU8_euјF8,c$oô݀hgbqrts[/)p9;Ǥ*V+ +#*T4{U1^\F@Ejݨ?xU<6|Ѩ'gЏ8+=JulY+IP]* Y4Ba]l FbُC U/}G9RK}Yې1btrЈ9]MPDT1Ӫ>rziE-@Rt:%ȐS}l2GňhdЍU=ǔe[B,t5{uo}w.J=WŁ&a DbD+@c܍oIb'YI +Orx_GȓR, %.4>"Jc,mZ +Ew~=|ts||f|~7>򵞖ګˢ[lFF0Уq0w87OjEL <^*)a2FuⰊ_ua(Ƹ3-* r1?%b7Lb2&;ʑ \Prտ~Xk)/z0$ Fc!DD^6w9t$(RyRK`I6Or}"OR+T`E`zЪ@X\7XG##uEqf(%o O$|Q1^KsFi[끐Wցح~xA4)"O03Ƀ<X<^8,>bU=R86gy>Lj 暧_؅dRܘnۀhteOn}sOY[_{uI)?SWsfHa̭rѭޣ4%&H%gIH`I*ODIqZIS.EQΟc>&ŃfЪ,Ŕs)gShZcV[0ä.!W +^iFrLj.ub0 +2FC1=tGHȓ'SSg 33GL0>v9RZ)9H̳̚zhPhԔXS[`/gSk4N:S1T,uTKݗEOD̍{~0!vƚgE'!3 3|3 }R|?]S…f@>)2HBn.Y%"V"yЍX.}3\%\5T#x+{ B:p0%n8=XO@ąHh^ȓXbR}qxVaS-9VDZgä:.U tAt1b7#aܤݬ+tP{r.5{u -eRG'ychcg\R8E%C'- +&Lr{1Sb$E8oS\>N)BBU~PJ4h[WuhҤp~iRPB;qqbqǹlv9>vHy$&  y?z~>me)+qe~j$RZFJѤSkL=x@)v*bhbbLCI@W+6"~رN6;\ +\8ꁋ3!OSk?'険QI}VBa&5{R<)mM{F-E)[JO + D!H _E*߮h$l4.4/Y*ɥpEKXQX%QLȎ'/~7"m(. +V4 +^~q|zh=ԊtT|bzR'7RJk}>z&[`߾]ѽZ9aExS*)&|/?SQi]=µI45 O$E@cfg`{G=7fJf.VFai%@B͔O(1f]7NŮňnİ5=0)}OJl3 MIaf+1){rf{y nV B1/ױ㿛=ٿP1X"G#]B} 1<VLdP\YM VM̓k)hߪ6ʓa#pn2L +oiГL&chbP\bf»ӈ?b|eoϴTDZFaP0h'ꑹ$Iؓ)bİM_s۶mRxR"0 Up0PYՉ&C© R~wh\JJfa&YV,ͣ14%'ErX9qx[英 {glHu:{64|mN>*F>hhYa*JIكIvGRiaC(7oieR=~̭F32icInTIFYӉڭTGvĎvkцweÑΏC&aN;0(n>ab~ wht24.w#T +=j&Zy 7*ѓ.Vޣf3.W$dɏR~,׈"*&=?BD?o gދtԅ]gB?σ YE[ҟdJK hD7bJjF2L.)~bL6e=pl Ё'C{򓥚T&Yn(Z65?Mz.~wΞ=1:=BsK Ђ!(.\Tbf(F ,zR$ I)ѓh'1\$yzTݎ'j_"j"pt! ~;U(qb2@/Qh%Q=TIYI=3F"a>`$Rf*ɛU[F;sf]z봽n*x[c=d8}؀0'=]Qa:S' +!%Ub#$FOI P0E)yٚ0O +wՀǕ/}e_t8C7#F Vj휜@O$`$ݪ6ГEC^ݩ5-SrcRZ7dSI4`x]}Hi#oV)#1II1^mXX[vRF>C8vXp3AmeO;j vx%0l}Oj +uI >hߪ6'1%rbQuwkV("͑Lpɦ 09nt:),G:ݶqɾ+^;RCðuٺQ~~IZ|_ 1j[H!F^P6.u֬lRKֵ>12m{Ntuϱ%qr#ݝ=T'%X-Ap|{>"nyIꪢP(IQ&u:K`5ׄy)goX#!, n4O3FJaF؀1*|L`lq7v !GolN/[Xƈ[v#c}-) +eYJ'P2)v~MMsc5 sI,b>De?(lH\v8oq42֡.pW$VJޚ˼[K].pˉN.-Jp:.}Q(T}ZT +%쓂Ї5c*%R FF3I"LjC悈@2%%zZS-Y$+㕜az[Eǔ6v'"ީ-CBY{J'5*P2IYϚ[jRw7.]$g+#Y?1 LҀۦ_ݹd+F.;EVI&fE.c}|*`JR$࿌ty&"ɘ:)GPֆRI_a}RC{A\#PhRۜBg +_33̭f"&l_H&MwBhJo{^+HOPؕ>N\Q薶cu}? PP6'Eut Q*UBYP("lă|R1n41')wobC*UmȎ-CrS +)8[\ب+&|l  M!hO'qK]jH*P(I:g:FFTj~mpHR%z掯}h/̚쓒;du|R;kgz+eikwL,p/s d2pcn6klx=J-Lcc3MP@t2$yR({d=!5*B^UUQYRR1mٽ~G^<{̟|mgY,^AffQ5*yS(_FEtn%(&Lv!l2EgUq`j(\Iۢkbٞ.d- l:hr'"ީe%A'Eu $qHryEeٖe堔ۿ\g=z虧BˉglhKӖco*` /F=nW]9szv) *z 7A#L2۠Q_sL&+Kqo[_ّўNH:R/zt>vt%B5Bi,|PGRV(UUe[*|`cowO + r ADPoK8 I(φRedЭ̤yR rŗ^.F&k6|nq#R&ȨF~Q*|LA XX8o]Uet-S~|pkC2lsMǥ/P +(:F4BU] ƀF* ޯ?xgק;p} +8ǃ@B=d9a®36&w֬>Iٸd5Ks̭fuMm!X4>!pprl"&C6l|!:>?[tKNuOT6:랈xh$&b7)BG٣F#Q]UrR Vco{/'^=fwIm( b!S[ωWsn]*JhOf*Rr7oi/p3!!܀B +$$.VH+jsݴ$!^ ,4cO$>Ӻ)Hۍ$mzb+ϙuv$c;Gq{k_w9{*ֹsm6.e۲F6{1N&CxLjƬ1R+Dp GD _fS0%A,O ᓐɎ';N8ucǎ=zȑc>j~ ׿~zgu/5HHbih{hRY#caÝJ@&=頺(;\e(eil3DLUC#7|YgS>Ҋҵ>m![5UUu 녈 +,d"0 0&y9`-Ǽ!ˆ&Rp{GYNԯ3|䓟z)|{?zo]u:Oކ.!샓q1-Fuo|:q>ol3t 7\Ds$%+]UX%*̾zs'8NFWR=np\ZJ +PsmA?!3Ҙ~3C!۵$$ŸG'vr{_k (%& {v9 H0r?aL1ƬQ"65OmE8!En?0H&wK赑Fw?{FP?G"'3thq4RH@%< %mZp<aUPQVַ}1'fsLq\Qq0sܻ 44R\ر4Yu8d쳩Р\aFwSl*]#$ggq4< ?7 +uwcݽ\wқxZ|J^:/A0I8+0IfPC H#}1̵k\,#}9F*&TI[iwQ^?6@W[ɤ"U8_jhCoE$.ؓ=ǾʇAͦBŨWBaRPՉzxp>Z<Ơ"ki2YKQ"|Jٶ|9\2|Uτ›Wᒱ<2i߹`oh>FR=Qīy9 YW +pp*`z<&9Lj-}dr0\C d&)d"lc݄k^^sJ.YX#J l%DXיcu}mf9$~VTU52i\+FP;eF^9raKXe[hbO7.KG1ʂfzzDp4cA3 +M Zx2%d?3<]vK r>ak3Xrf%êG?$%r!!/J VEN\6c+PQHչ?H^@-yL%`2/i "ZDWJfS\r 96ܑ3Í`!0(qت2nRCRc%?ﻥnʹfضT^g3i~#l"RJnS'Tpa$ ,cۈb.WuxRi5!~5/M- +(,⠨8 dk{Wp^Uk}eiH6H,G:g"Ckg[9n9<\Ӆ^B,M$$Y}Э&l'DmP-XgR6ǰǪ0IC#_k?rvw؋;ZGmFRs1USG]XΦP1.gu%JCsh!nIr1vI biẆ=1z%ç{;WMdwE{O&!ﺤn & Q{ ᐤT 'E[o؍aV+UZR"" i6 NHJJp`sN>b'Ƙd}0vubh;{#UbgVV$ò?닳[9gK2ެRIҰG}d%^awXgڌT.yNG!b',*!㉓AR ,eY_dl5:*Yq&:c4/][J5[[H6Bp%^2c,I]uIM_B q񋠎6R~p' 8M/:xEnߝ<H2\RW2aFanfj lC|bR^4]^-hi^} ^W0$ |M#ߘ +s' w a/f8 +?|"tABeQF#m4^D8oMӋ 8A #1 ab/3.,rj}>Q6Y6qx fgan`˽2%w`лc}q<#I[[HښTqhN%y-'(v s0)EXVP<o9H"3`.'yoS)-lkT5+QL;(޸OڲFX6j&Xk,k`K IX3Q'd,_H M:ܽ=y|M÷'˪hH +"Щ$KyǒP=ԖeSլ{j{R2L|}qRO~V +XF6UMNJ$Iӭ-‹Yθo=^?=$^z›OnHAO',yr{Ԑw#2lE3R2dw'YC6JcL VNLD[ޠbQI.kL c$IqSn0B_ѓKK(Ey@vsLxf$uH/k)zw|f6sZ𲚎7)ɳg}u>QL#tu^F% wV[y;턩!<dsW !#1kA V( pT2Źp'] F͙., jMjng;- +/`n3vYZTu+jgwya6WSbF fHJ Z|_2 FE8`nT-e1> +S%̻Յd͊3*eϖeSclj'-U]VcEscqpZx<<%]9y1CM'$ez0OOmmà?L#cwπ*!Z2.*aUұFVf,y W\nVbcin{Eظ+[PW;%{ʥ*2tM491bB$v Ꭾ+5d!"0EXh(kfCrZD8#K`F&t%6E}6d:R2;UƴR}[U닳em'/{)+SI6aWgIhR fqy7'-]_$;aݴ ;2C<-rA ,RѐlDZOM# G3[fk lj{E- '<0 <7] ,?fziWSbCP_H$ rJC^^{9uw)IR8NF8֬DJT3ļZ(jT;5ڲWXVIv  c.IZ- +H1WI+=7 +"dGoO8($y4`0c?Y^&jdђFgeT6jV܅e2l Nfbk ɭ`P%kg]>/qWIضh6) t MRR^'#$u``>lGpf1)h2 |r3*2#C([N^-`˽-p@9b$L)KZ^BsWz着ՔؕS EXM <{𭳷xc0 +0"ꌭSIںsgtЊ Ll,5liA$oZ&<ZIYxIV@tI=A||"8򝺥T !#1DgU*` ېZLF*pOK J,|ǚ_[HnfS;ypxWSkV%qVGs%ɾV/Hy[Dl'AO;i=:ACb0al#KUrkmasCjϖ8/r%78_U7ڶ3hՋJ[cje怱f7؎RloY{osƬc_(@8o}m \;3 0h]34_ +뻉9'+||Ig5ȓr6I`Ħd* ճ뽳_wO^w@`0 w?[H$H#`J&R2%xYVB^X%,z +&\eb׀&`ͿX_Ƙo#b\CގJN'T 8Rҹjq0RnE\.$۳ZV.x%גߠ+ڶ젫To.[~Tg I+|kKpBL^(wѮ:A%TϏ{zM.L´Ur4d f`B)1pH 0e [0͵B*^DWfkIzt&]od^ʬQoL0YA h +X%_z7HJC }H{_|raf8! f0#~-5ogj˰2X9˹R6*%%d+z>ԼTtR Q53 9ҜAL[J_wݧr UvUUv鯺g$O;`0gv.,=Z Zv`Y2t$"–p&[DWf ϟR +.-mSc9sNRD nx[<@Fx^@=֒.k0RN'7?%nҋhzG0 fC)mpH6euA'-LZ>R0[H9ğ-$7z*vlsW1[(Z wZPa/y@OGoH8#OʷF#dCb˧.`0 YӼZ*wgCHm.߲e%lObz}ް^HçL|yMJMR*y3I0Rs۔#~b~!S+]Wj"#1-Hk A j6VIGZjQe!ؒlf7uaK`Y/$KJ&|K.%#X_`2d}0W}i}`&]W8rard?{~} `0:ҬXNGVf@S!!m[CJ& +n \r^¾SxdVZh^ _}H<)*$89C?## 'gwϛx.d$\/⇔۲k$$2{}9@˹R6M%SKaK fd5ՐUԥՈp5_N +#mq<  p`-$[7vL0`07XgAga U'HQA*JGG$RǼo@tǁ>y%F?$i;/Q!}B}fH [(MR=jAy}u.)T,,i-Us9_uፐێQ~FUݖs:@mudW#F^Ģ\ l+W`Qg I y (!ܔeZ 2 _(OA}G~mqܧC?z8#sdϫ|Ey~2! 3@R_,#0XWfr:Pm}[⸞O"3bCHKnƵ鼃-< <[tx~O!ECb0P4Eq:\FLAvu. Bey>kx={ɧ^ lc}yGMz\H PJ,=RƸtfPnidb0Tm쀴6({P.:HqNzl8lyh8@l۠#t§ |u!^1B6,7 6*t`%TUsqqsZ#;*9P$IzNp㡏N(L(ޡ gGoEFb0̇3PYf>[e"fQp}d"t`>q=#?Fnp㡟'py`]vOX koVqI( &&u ^K0@l b^۹uhc;׶4J+lC$Mz5Z'- Ǒ EQW56T@R]v*9lDʏd~VpK%U|nRlrg^2uL#)'(BNrv04gƱl %JL[[,)+n,E;B(/pc긿cl}mlss,1j|Iu)^75iN2$EQu~qΣTr۳`rtKg)1~ab%*.j`>Rkթ0Xr lWb=9Bt}l㞎 ȐBE8FÕD&&V.׵S;#>4_ EQԅ9|$= @聭 -"rͼs1dE3bD v*n5!̳?:5 ݻ$v{i,=F2’+d6N >nzw+v=WqhJo꣎i[H(Ҏ7H wI ASj.S1N7`M7TsbJ_ƕ6# +!Ü.Vimt«~P"Sg]C(]*yg?'Ckr晷,eB%e_ԩii@UʴY &X.n,/l0PP0Ca+.;zgu-,:b߄({gE؞((Tv&tMߑ̓~d;8A#D:GRUbpn5ەv긜RapI0&|hs}G-*"zƝ= JqnL^An5ԏ((TҷgW.&\[,/߄btb1>y@g-65ki8<DT =%; b h\1떹R+bkYJO)=*:REQEQ,Ym=z ms&< /.\2”۱>mJʱ:򆣇;:\ns3>4nһVL%cS3 +EQEQuFLXBʊ0&;|UnL2|TOH*1RbiQEQEQ0GOvcu)((9C7y(JREQEQE` endstream endobj 11 0 obj [/ICCBased 19 0 R] endobj 18 0 obj <>/Filter/FlateDecode/Height 947/Intent/RelativeColorimetric/Length 20505/Name/X/Subtype/Image/Type/XObject/Width 880>>stream +H pT/!&_aBA+ʄbJZDhjhb"SlSF[qvBP +P)-7Bd I&;=&_{{|!;Lsw9W=!ግPڇ#=^v^fV$Ax 6G<=\ k˰n3,- gGX+)G`{[-6t 8W{17# 8nz GJyH&AwB tTV \D06ns'Ab G.⬊FI8 dw AZ-hV7%\( "~,z2YlKA bYqM V&u8minO1"`y;A^[7`:58vԆw^Z"VPQ $A.c'nnJm4RD,8>F75RI8; pћ"AрMV7Q#(Ag^PD ,=A$ۂXtSADXtSZM#%A} '&3>A$ u]NgO Z,;ȘuS+\$Aǥ|Z⚠nƩnc; 6,n>DACƔaw 0CKDSDr * FVFw w? GM!Unٕ#A݊a2, CŮ~ :vW74g58hr׹Z܉&j,v&pu$A\ t_7%܆4R8g ?#n#N{1L9# KM?)Txԯm4Q eS/1uC&n>v3>'$x i} RBsNOuSnk GH1>pRfDݦJuS­LOEne9< +]J{X/-0GOKK;ֺW̷Baפ =R2XG8ciLaԾG\},Rԗ)2NZʄs$\Ed\1f9t]RE?%H7%$\QO>KUsj Á]Sb}X & q kR"D-JS2sO}Uj9(%INtSsR"=Hla|9.X@S0 ~}}?X;8bcG[ʇ +|f|k|njMV Ϙ0"# F‘~$K֦F`__C KsM]f[:pfMeVtmdkP!(2K g\^t4饝T7HAھHL%}eu]EȨ;\߸`@7s%\X:]{x~'a,Z=lcx⼑\†n Nd{pnqY8ҕ2rr(Km{i]ذ xx;?GuGƼ#Mo' _qScm5k?bަTpt57mbtSLg ("dw2{J0ۅ}.6DŽ'<20äFFD@pa=x]aO) 've_ pPY+EЦI7H8cdo x".@״lᛇUrB7mDy=D yrEcU'tf<'_21uMq!~pz7Gqc:{loP%ZH:kծJp_ky6\ME}ߍrAp3j Nk˃iqV(8dݔp$\Bk9~{6& %dngZ8A(S7WSdZg AZ=$7`&&X护M5*~Z${S:ƎVBLJ`Y+TH$\gR|%1닶43hXz4J w @(_R `_ g,]ƜrlY.8.lg;jȾX8w޸`oO:ݔpZDE-#*5D^A;LI56(VX 2"sAVãTl1qVx`L2% 4MTદpuwVUn2"`Cpٯ+ sAGETdTQXRP#m5)hՖg}Ԉ $!)f,Q*>ZEB ַG0ujEq@{usIg0ufs_7ZY_z_W9.Or?KSy/?X?'[Ư +q'+l魌~_$ۘ\zFܠ=F_.LkQG+Y I1ӠMå;o]syk.Z5׽FĖ .?Ǐ}Q$^k0p2E~L4Z ^b7k'7-Xx]#]Z\)߽+k"IJj~ނU7:>"p3{+P֬&Y}4cI,ҝ~eݣV_y{&!DqK;]F>pqO׳UQ+fRn r[mEeZv7a񺷛p^k͖wpR{YP^:y-[W!=AvZk^w7nquEٍ+y*+E/lͼn=XN|qڏۏ}kO*lXZu^{Nj7hk k!9n>JTO2A: 8A ߆xm8 Wgqys޾;[Y5kmx] ~G7K~ t"\ZjU\^y[sΜ ~lm}r~̜~=oT*ZKc2 J +에T{aku)\`f+Qg>q,^yײjv}5}_CC9?׍5py¸<{{n9ZFp699%D@7˚qDuX7MA +0PGDrz-oD]][,Sghۃ,o)ZOXSK[j_Md,o) =doٽ]w:I߭Vym: NQ)#5Pya:1Mda +0D3ܩIF) rv7u2Vysr5NZ_kWG,7.W1n*I'cT/#^ "DA7Yy#H^GN(JPysRn@ͽZWm$Bd8bnPy!X,o*%`b ͵vaDm9*k`U74\U䷌*G`R 7{~oỲIFAJYAj'c*ay#Pvs$^e#'$;#%#7Btu /]PY(n6 }j2%5JoN3o*(SQZ@{`%(n~̼*8_$75vQ89m}3e>F7 ȼ2v3NzeVoNƔ7?NFbW6]l' ׍H)Jt!N7i_o}2M\FtIXV:}w7Q{ER*2(D8d,#YV0 'n u vqﰼ8&2M7ew"ER"IuSQ{9'Jt շ틉@;7•#%,o,Hm7as^N~FNzp┷H~2(k'ÔH5 N;Ɉ&ˊD,o,0[$'o$-#4i'c`ck9-#4O N27BVֹ0qs!NokHv2Q{p.$Zb*7'?`y#`y +Nyʼ0 Nz$ 7k'{7uv!D P´vYsv.Jy3wh9#5P%(-+y#d^oɡ7B"Cd\¼2k'qIYiT!J}q#d!7'a/:$nVަ4A7'FٯX2>c:$Ok +a "TZKX3@-DL~sCBؚJRa#jdSaSB=Pj~>?7}uݼH:ӕ8Ps%!kQ}լ'0@5s7y1zޚ: ym184hR3 ̷݃]0@5O u |d TsZB tS%dN=ycՌyh :z˨oy:I9jJym TFM~V /ɡ@N#bnױMJo St>Ղ9G|hFqK>IEou_yvX' ]0@7[Trߦc44 Is(C= T79-fo1rq},*Z Я ଔIۤ t|b- A|i{o畤79ϑ(rq@3;z(e7M>)Y'QynAy: kN`rb$z + 6 ؿɨEqWMz -AYtQNÎz:wEny gI/#Mɯ7Hq|qAJn$ʅ "o47e !en&3_$Cr[Zd3 RQVtJş y8no ~IئɋݧA"<UnƘ8MsљGm gSD;[߳8s GUd'.z$H>#+?G&/j|_KbyBg*y3]p8r T6mܭϝ8st9 skw?"cb4G1k[[[i-rgClPN^P}ŝ A-)Kt[yS2Dl;/#7\QW{!cA2fSߠ*'<A)QB^AOʓj{_l*9u3Z0694zKɈ=kԦkgX5œC/Duj+du#j{\mT W5,\BBfTuնr5Uˮ&,ZcjnEqD5򑩕\F>>qPS,*@n[1⠆+Uyk8*;傥's$d#RS.[T 9Oᅡ 56܈iOEqin5n:9 :b .lq6rX*! 75F0h3X*!/eu89~2*#6}ن~agoqԼ 8LޛUF;2ʼX>Y0R\£*C# ~}1@)kȄARl)U8t8ڏ(oEi`9sH ay|AS#3'yuwѼRR8^5xo.}?Y9GH }C/>?f69^ȣ5HWPNZ,2χP7 Uk]R'*_U|>{r Ns7RUVT J1Em(-'P6Κ#7%6FO%ɏrsλre֐e}^ݿŵQ,rm[%Hogqn 9U$Mw X~LI&M{.@;*BR"J _-e Ax!mX렯{.z?Xo˛t>g\q WIge9frBnI  Drpx+p77A,w41ތ Kc-6{3 dr+nY1@bUJ?ފYf9M2Vj_}*oR[vz[ `9N}n2}ox,7Ie;K{nR=θJBd^`rΫ"8<v{u.0me`BkX!8}x H3Cn  wzop e#8K!7*K%\X~@[AH}xe 8scs nyv6Pu fJ}WZ;I R@?G?| WJPѡ?z`A,oEoEps8zWhPyEFTj#wŕ!|o){n9@?O%ߙiͷr +pNUo1cpMCp?!;24 J9o[o 8)ur> 8ЩʏG7$8\)An^F=3u|lRbz=,p%s[ p}h{3t~7P$翛h{Ki1rM,oMbqEkoM/gi\ko e+1@[fha7Dݑ&5IPE8sְ x91JhiXGQ;AM.Wo ˹x7[J2byCs%Tz?X^]_417CcN9>$%C3W)B9fbo xD՛+N`R9`LυRd7P4w: J AIz&8CN7PJzMo =@+٥xz3t&PuuB0@-'-gi$.1@-'F˥X\JS7)JU54xoTWʢ-11@/ mn*G2 r斆L^pk鳄%s-2Ny(%-PR;: 7tHr]Ku'羷=Io*Z[L.;0@19Ul[B\xèЁK7P󰼷}DeP&-gh1fsK h e)L_o1@7#d5ԕw4}`~Yq?3;33n(Mj6J1zAŠ5P\`$1i0FBQ"4 mѴTUD:ҨW +5|k҆@1J NPlgƶfҟMTo8NV>P'z  y(ɘos|l]k^.dRgsНӟl Ut*P9bf p\&Q߰c3K"^ʉZK%ԛGk^T "zo>8nT2}Ǡ<7,$پ|Ypɸ͠RƛppgH}{%Uo9y/YkZ9T^gXaYqdQ+5e>(HOnorT,8CYR^LHꅽkzϦP: I0Mȏ\552\)uIheCkW5ToL2.fp=@3 '&r!M)4fQE655[\-G\Ћ{ZѪW +0N-8$_])yts\YOlӉX4bBoޅ tPp ˛…\sZ#N˚5[6iHom^=vh\gSr#7Г\`|tPߗQ߄VMc=-4$aykh؟uP څu4%ݔ|`Yͷě=J=>KzwR1j5L҄~[V\rg ;pmU +tR͡Ƴjlor7|HR̦z 7#ۍ@1J"9!wc,w{{j6PjޛGkNcQ[SԪloOu'70jb.7}{s~L4NZ)͡1IB>5[˺(Ǒ@sB.|m{si٫X'_yoDwb+H>Ų-9 +2/jyi$!C|.M{{ , }{si 7-"zt+XC|>mo.5:h.E<ձ7օj!'3x9܃ Grxu8{r}Ft#7G닩t<u!%ߑi,eAT.¥=_Kn$Hհ7J9#l>״7>2CIǣ_!q \>d\ͣx|ojV QuG1]{NU:3u]oy"7K7=i`bxSmIzhٛCu ` ]y$E򹛂}-I#B٭ш_o.M$XFoڝo݌|I06O5ͣ![$źiNϳ $X'Ro`FXGt!' #ycoN|HvK_~4w^62[o>8>ަ]oj˝^?NQȣ`Gp٩6kכKzIv^\* l$X4m^֬7Do`!ϔHͥI`%: V^9 @qi+X'Nt>7ݤUo.Eo`)՛GeMJx[ ެF>_1[uǿ A5lMeH:f` C.50ӦJij>d+ְrRqfP@&E~߷﹵\}=:sޯk'ΖZޑͦRR +X2q etӴ"ݓ +H9{I5+H7*Po|s -=z +N욾!yۙ7G~2o(1Ņ-ۅ U]Ϸ_tPZ;2"˻dPf^OZޡ%#UC3AW{2ZrC}آqN̼nY"9so۰A$B-Y(5Oe)D1o(7)FoLZMo(7w.Fo!詺ϷHF.aPr;[Ho(9?,Dov^>o) +qOƲJ~/(ȾUzC~R_9PvNɿXXμBkG ׬r +My9 +䝛W +꿖9-gPAwo7Tg["W*k3r-v&_HZ4&"Z{+Dʼ".38{;m`˲E*#aT~E2'pN:*+8;'傼}8'Q)^9CS|K7Tӭ%HF?d;P)N%rk*[fqtJd'Tw[,]XIo } .C?7 n>\PQ($ rNlkI߂d>$~g7TWP +ˇ6ͩߟB=$1(#ѴnҜtO*i=.71o'upL{y4]'|βhzĞG .Y=:$&KaLn٭0YpL9 pB,+kUpW](y"D?-qet@x<({aހt^erӳn2zށ{+[3А +(x@-Sz506{xgF?PP9"Q].Lpe۵g +ƣ .3ug[,< ӧ -V08]55刭4O镅Hj+ h x],ݥg0OXl\6 ˔AzK& Ɉ8(lf\"s8(Ƭs3 .3p@c^w? Pp|.<M8|DE88+'\"'=>}`E24tۧn{M9}dB}|? +PxqK~ h.][ '_Z` 8n$2yzZ`\u\$О5pe} ;]p|;I92'\,=qaT"YJo@>;b9q?prrEj^p@R7\8UNCL{ހV8XFhEpc߀<]w\"fY.D UNIȜW(681_⎂d}Z;[7\"(?&.u7Ʀk~o46X2Ercɸk_\,cW2p@E҂79[8]m U\"S_7}ܶ+RkkF,ڔNܺSǶ5q5舷py[Ge,$83VܫWgE-w%ЩwIZmfTzT1ӂki_Z6 +#Q˙AC?3 +"{QiL- s@7V&7;WǞq̓;Np@wgܚkM'.1孢k\\$=KlT\"6qP uFWc}KnL{^pjYV܋osd#XKȰ v@մ@ .8 +6qs-aJR_E!([>)ɰwȆAy9d 39p96J ^_ޤ7{}ݮNi еjz{ZyNM-[8K^׾jj&WAyIz{ӵSZ-)9(_QUw?啘 +$AQ#+X +>x4 "2h;NA* +% a)Pa HA) ;gY{w?;ݻ;{o4Hz%"ADo)E$8P"HH $9Lo8T98I"/!S9R{K[# #xVVX)Aȏ| NUl@p Po&vPfKo|& ]`M~.48qmX͡BQreI(6Lli-c%ob)IJ)70qDPQvm{uJ:7\* 6b 1X_5؁ L4X vb3{#$@D9Z}hp9L +8'%& {PnE$"4)m778&Lwnoepxo%?L,pn\㨷1Ln+5qtA7X]6%xPrɋ `(9gwre';ZvQ.Yt^T$ m7 +O!_k#5ݯ4cH $JIz j{.i&-y1%L.Vbp=ymAlzov>BQrEP̌"Eme^M?#K\ `4`z8YtЪ2:ʑn/qHw7&yU]T]5)ly=5HN\o'`9rsGn&=/l+洄nYMjJc!YC{쒀,J(`b__4߀vMdd`U,{7aZ~ҏ1r~En$7Ħ7ʯ<൦!x\oou~lQTlaboқ|La(L(pݳ2EFӑ-z]\ш`b;$B^4yV}["l{ e+Oã1 *-{#$@@Szj.l 6a䬮TΔa}ܫ)rQ i"eͿ?Ckĭ4z\*ʡۻgo`pSEo>gz>iJbӄzJ|1.ڛGS:qdT.D64b 6lκpy $Ey&ZI,OMUXjL6rw&ܖͻY{tL..]IyJ +sN/ߗs) K.|"T= Kz3H=7t}wjjC,"[_)ZzD%E_/bL!8Ӂ)R&x-wޏ/0rރn ʜT$2fRUWY.{j0y *674[*%gP[qlo.y &}XIFqcҡ53u tFZYE?W{>,Ɇ&q h!7!6Mh1/ԋE%&*z?M&yI]^ TUoeAP6g.$HlΉEy:i NF6{jwb[B?+fA4D{7bk}RLlN?TEgkaad \` Fr+b"ٶ!k=UŦ euɶ/@6UqM8`} ?Mcp̟@Qr3:-f:^$gvcVL.5`,;q(%Dr)g `yٔPZ fTY.3mqYH\[c)7-w'8W&?,Me"{7c Uqb*7(BT_|fUo)d"SVJ.0q!c~^6h D62t!Z4L $z3HE&Tp +Gmd aҲrJVʂ>l̘ n/Jj{Sư5B҇d[$g$;|gp\-AW-<>nT="8 ydt|C}aLÀkA܄3Q1.r?)xұ[V +h3 d t"=T͖ '[wFeK!) R6V +49{Yx} -m?zΛPQ;Fvw(97uUK6S7M^uJ.*cGׄ .$SF\ˌ_kD0;$c .gPBTs1h3NrMACD2<'jp!eOd(AA7cM(/Va*g_i)Wc֜c(k'ە(KԮzy$ 򏭢3C@$0?!vi! +%QSE@EXݒ?lVC]A Eإ +*hE`/ymiiݖ7ܙs?sWO.X}[2t6BmE`6NBnn l@) '?X>Y e ^#S?YB\ܑnƒskl_m^FPB-/+?faP!E2 .dup}R-Ԩg + Y1T.k'Ql o܅φoKll}aW%rɳ&$Rxؾb11ԁ'lO\)d9ҭ)>Mp]7Z‚G':u΀(q) +u$dlM +'wk S-| O;y] +1ԍ]7T5ھGOݸ$kTF`OV|PčM]tMcY }v,n \fcj{.)ٚUŠrLV$B@u*B7F6y2|rr;+\[ιrptrCx!r](P +g=c(1 fB8P +G]d i9++m'AUdQn3%ilrO8Ӫo+9ETOiSWhrFdw3:{ϣlW .6lY{ex!8t_xW>mΈȀ5{J=WFsLhPv$}_RMפ} +˴{Bzr5x.UI&${C[#Eqx(կa9EP\%wEOwS8q'^ӘD٢#AJSTY+v0ZGM0٪]lQm>Pvy$+^D & H}Bp8o/wo$_u.7Zհ 7D-z VwԼuYa0N@Ye囍 'cmt-ƗYkC#01*SuS,٩p܅[C_qbhjF Y.&L0W7O.(Uf?UʍlE0%ݹ@"qy*`@vyIy%YqpF$Xu{9"yFmNp*{BeQ-f^}szs>^D lZu4']G67TN@]6dA3$6(OY99Q#a9q 9\=Mx$U /> o$Co0:')8\٣!=zЎˋ5~Q6#]1rZ1O/h_ ~]e7yasrvlRB$@Ifs3FJ1!]Cmhykr-!إ?ns෣Ξr ONۯ0BBʆt/6ds;t>u>-.3oJM:h$c۶2kEȫ`KE9 +~&enc*wnjIcw5kΗ@,'k;}Y.Z.\\)+;=V~M+\`I)^*-O0 ! AB\u߃7%˵ .}w[<Ċsdr#Go?"Z-6K1 +$d/:0\}]7> +vTUC:ˉA€e>Ś>stream +HyTSwoɞc [5laQIBHADED2mtFOE.c}08׎8GNg9w߽'0 ֠Jb  + 2y.-;!KZ ^i"L0- @8(r;q7Ly&Qq4j|9 +V)gB0iW8#8wթ8_٥ʨQQj@&A)/g>'Kt;\ ӥ$պFZUn(4T%)뫔0C&Zi8bxEB;Pӓ̹A om?W= +x-[0}y)7ta>jT7@tܛ`q2ʀ&6ZLĄ?_yxg)˔zçLU*uSkSeO4?׸c. R ߁-25 S>ӣVd`rn~Y&+`;A4 A9=-tl`;~p Gp| [`L`< "A YA+Cb(R,*T2B- +ꇆnQt}MA0alSx k&^>0|>_',G!"F$H:R!zFQd?r 9\A&G rQ hE]a4zBgE#H *B=0HIpp0MxJ$D1D, VĭKĻYdE"EI2EBGt4MzNr!YK ?%_&#(0J:EAiQ(()ӔWT6U@P+!~mD eԴ!hӦh/']B/ҏӿ?a0nhF!X8܌kc&5S6lIa2cKMA!E#ƒdV(kel }}Cq9 +N')].uJr + wG xR^[oƜchg`>b$*~ :Eb~,m,-ݖ,Y¬*6X[ݱF=3뭷Y~dó ti zf6~`{v.Ng#{}}jc1X6fm;'_9 r:8q:˜O:ϸ8uJqnv=MmR 4 +n3ܣkGݯz=[==<=GTB(/S,]6*-W:#7*e^YDY}UjAyT`#D="b{ų+ʯ:!kJ4Gmt}uC%K7YVfFY .=b?SƕƩȺy چ k5%4m7lqlioZlG+Zz͹mzy]?uuw|"űNwW&e֥ﺱ*|j5kyݭǯg^ykEklD_p߶7Dmo꿻1ml{Mś nLl<9O[$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! +zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)Km endstream endobj 5 0 obj <> endobj 20 0 obj [/View/Design] endobj 21 0 obj <>>> endobj 12 0 obj <> endobj 10 0 obj <> endobj 22 0 obj <> endobj 23 0 obj <>stream +%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 17.0 %%AI8_CreatorVersion: 19.2.1 %%For: (Zachary Mitton) () %%Title: (metamask_icon) %%CreationDate: 6/15/16 2:23 PM %%Canvassize: 16383 %%BoundingBox: 98 -140 188 -44 %%HiResBoundingBox: 98.7919746568114 -140 188 -44 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 13.0 %AI12_BuildNumber: 147 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBProcessColor: 0 0 0 ([Registration]) %AI3_Cropmarks: 79 -156 207 -28 %AI3_TemplateBox: 180.5 -120.5 180.5 -120.5 %AI3_TileBox: -163 -488 449 304 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 6 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 1 %AI17_Begin_Content_if_version_gt:17 1 %AI9_OpenToView: -39.6666666666679 23.666666666667 3 1419 866 18 0 0 -5 38 0 0 0 1 1 0 1 1 0 1 %AI17_Alternate_Content %AI9_OpenToView: -39.6666666666679 23.666666666667 3 1419 866 18 0 0 -5 38 0 0 0 1 1 0 1 1 0 1 %AI17_End_Versioned_Content %AI5_OpenViewLayers: 7 %%PageOrigin:-220 -420 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 24 0 obj <>stream +%%BoundingBox: 98 -140 188 -44 %%HiResBoundingBox: 98.7919746568114 -140 188 -44 %AI7_Thumbnail: 120 128 8 %%BeginData: 22554 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C45FD24FFA776FD75FFA04A4AA1FD73FFA04A754A75A8FD71FF7C4475 %4A6F4A6FA8FD6FFFA04A754B754B754A75FD6EFF764A6F4A754A6F4A754A %76FD6CFF764A754B754A754B754A754AA1FD69FFA8754A6F4A754A6F4A75 %4A6F4A6F4AA1FD44FFA7C9A075A8FD1EFFA8754A754B754B754B754B754B %754B754ACAFD3FFFCFC9C299C1997476FD1EFFA76F4A754A6F4A754A6F4A %754A6F4A754A4B4AFD3BFFA7C99FC198BB98C198754AA8FD1DFFA8754A75 %4A754B754A754B754A754B754A754B6F76FD37FFC9C99FC198C199C199C1 %99754A75FD1DFFA74B4A754A6F4A754A6F4A754A6F4A754A6F4A754A4A76 %FD31FFA8C9A0C1999998C1999F99C199C1746F4A4A76FD1CFFA1754A754B %754B754B754B754B754B754B754B754B754B6F7CFD2CFFCAC9C89FC198C1 %99C199C199C199C1C1C175754B754ACAFD1BFF7D4A4A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A6FA8FD27FFCAC9A1C2989998C199C199 %C199C199C199C199C16E4B4A754A75A8FD1AFF7C6F4A754B754A754B754A %754B754A754B754A754B754A754B754A75FD24FFC9C99FC199C199C199C1 %99C199C199C199C199C1C1994A754B754A6F76FD1AFF764A4A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A76A8FD1DFFA7C9A0C1 %99BB989998C1999F99C1999F99C1999F99C199C199994A4B4A754A6F4AA8 %FD19FF756F4B754B754B754B754B754B754B754B754B754B754B754B754B %754B754A76FD1AFFCAC299C198C199C199C199C199C199C199C199C199C1 %99C199C199994B754B754B754A7CFD19FF764A4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A99FD04C199C1C1C199C1 %C1C199C1C1C199C1C1C199C1C1C199C198C199C199C199C199C199C199C1 %99C199C199C199C199C199754A754A6F4A754A4A7DFD18FF7C6E4B754A75 %4B754A754B754A754B754A754B754A754B754A754B754A754B754A754BFD %05C1BBC1C1C1BBC1C1C1BBC1C1C1BBC1C1C1BBC1C1C199C199C199C199C1 %99C199C199C199C199C199C199C199C199754B754A754B754A754BFD19FF %754A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A4B74C199C199C199C199C199C199C199C199C199C199C199C199 %9F99C1999F99C1999F99C1999F99C1999F99C1999F99C1994B4A754A6F4A %754A6F4A76FD18FFA14A754B754B754B754B754B754B754B754B754B754B %754B754B754B754B754B754B754B7599C2FD16C199C199C199C199C199C1 %99C199C199C199C199C199C175754B754B754B754B754B75A1FD18FF4A4B %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1 %99C199C199C199C199C199C199C199C199C199C16E4B4A6F4A754A6F4A75 %4A6F4AFD18FFA16F4B754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B75FD16C199C199C199C199C199C199 %C199C199C199C1999F6F754A754B754A754B754A754A7CFD18FF764A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A9999C199C199C199C199C199C199C199C199C199C199C199 %9F99C1999F99C1999F99C1999F99C199994A6F4A6F4A754A6F4A754A6F4A %4A7DFD17FFCA4A754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B7575FD17C199C199C199C199C199C1 %99C199C1C1994B754B754B754B754B754B754B754BFD18FF764A4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A4B74C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1 %99C199C199C199C199C199C199754A6F4A754A6F4A754A6F4A754A6F4A7C %FD18FF754B754A754B754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B7599FD13C1BBC199C199C199C199C1 %99C199C199754A754B754A754B754A754B754A754B75A8FD17FFA04A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A7599C199C199C199C199C199C199C199C199C199 %C199C1999F99C1999F99C199C174754A6F4A754A6F4A754A6F4A754A6F4A %6F75FD18FF4A754B754B754B754B754B754B754B754B754B754B754B754B %754B754B754B754B754B754B754B754B754B754A9FFD14C199C199C199C1 %99C199C175754B754B754B754B754B754B754B754B754AA7FD17FF7D4A4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A4B4AC1C1C199C1BBC199C1BBC199C1BBC199 %C1BBC199C199C199C199C199C16F4B4A754A6F4A754A6F4A754A6F4A754A %6F4A75A8FD17FF764A754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B7575FD13C199C1 %99C199C1BBC16F754B754A754B754A754B754A754B754A754B6F75FD17FF %A84A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A4B74C199C199C199C199C199 %C199C199C199C199C199C199C199994A4B4A754A6F4A754A6F4A754A6F4A %754A6F4A754AA1FD17FF76754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %9FFD11C199C199C199994B754B754B754B754B754B754B754B754B754B75 %4B75A8FD16FFA86F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A99C1C1 %99C1BBC199C1BBC199C1BBC199C1BBC199C199994A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A6F4A6F75FD17FFA74A754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A754B754A754B754A754B754A %754B754A754B754AFD13C199754B754A754B754A754B754A754B754A754B %754A754B754ACAFD16FFCA4A6F4A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A4B4AC1BBC199C199C199C199C199C199C199C1996F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A75FD17FF7D6F4B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B7599FD10C19F4A754B754B754B754B754B %754B754B754B754B754B754B6F7CFD17FF764A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A7599C1BBC199C1BBC199C1BBC199C1BBC199C1C199 %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754AA8FD17FF75754A %754B754A754B754A754B754A754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A7599C199FD12C1994A754B75 %4A754B754A754B754A754B754A754B754A75FD17FFA8754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A7599C1999999C199C199C199C199C199C199 %C199C199C199994A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A7CFD %17FF75754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B7599C199C199FD13C1 %99994B754B754B754B754B754B754B754B754B754B754A7CFD15FFA8754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A7599C199C199C199C1BBC199C1BB %C199C1BBC199C1BBC199C199C199994A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A76A8FD13FFA84A754B754A754B754A754B754A754B754A75 %4B754A754B754A754B754A754B754A754B754A754B754A754B754A754B75 %99C199C199C199C199FD0FC1BBC199C199994B754A754B754A754B754A75 %4B754A754B754A754AFD14FFA14A4A754A6F4A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %75C199C1999F99C199C199C199C199C199C199C199C199C199C199C199C1 %99754A6F4A754A6F4A754A6F4A754A6F4A754A4B4AA8FD14FF7C4A754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B7599C199C199C199C199C199FD11C199C199C1 %C1754A754B754B754B754B754B754B754B7576FD12FFA8A151754A6F4A75 %4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A4B74C199C199C199C199C199C199C1BBC199C1 %BBC199C1BBC199C1BBC199C199C199C199754A754A6F4A754A6F4A754A6F %4A754A757DFD11FFA14A4A4A754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A754B754A754B754A7575C199 %C199C199C199C199C199C1BBFD0FC199C199C199C199754A754B754A754B %754A754B754A754A4A75CFFD10FFA8754A4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %4B6EC1999F99C1999F99C1999F99C199C199C199C199C199C199C199C199 %C199C199C1999F99C199754A754A6F4A754A6F4A754A6F4A754A4A4ACAFD %11FFA8754A754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B7575C199C199C199C199C199C199C1 %99C199FD11C199C199C199C199754B754B754B754B754B754B754B754ACA %FD13FFA87C4A4B4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A756EC199C199C199C199C199C199C199 %C199C199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C199754A %6F4A754A6F4A754A6F4A754AA7FD17FF75754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B756FC199C199C1 %99C199C199C199C199C199C199FD11C199C199C199C199C199754B754A75 %4B754A754B754A76FD13FFA8CA7DA176754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A4B6EC199C1999F99 %C1999F99C1999F99C1999F99C199C199C199C199C199C199C199C199C199 %9F99C1999F99C199C198754A6F4A754A6F4A754A4B4AFD12FFA87C4A4A4A %754B754B754B754B754B754B754B754B754B754B754B754B754B754B754B %754B754B754B7575C199C199C199C199C199C199C199C199C199C199FD11 %C199C199C199C199C199C199754B754B754B754B754A75FD14FFA8754A4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A4B4A9F99C199C199C199C199C199C199C199C199C199C199C199 %C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C199C1994B4A754A %6F4A754A6F4AFD16FFA8A14B754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A7575C199C199C199C199C199C199C199 %C199C199C199C199C199FD11C199C199C199C199C199C199754A754B754A %754B6FA7FD18FF516F4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A4B4AC1999F99C1999F99C1999F99C1999F99C1999F99 %C1999F99C199C199C199C199C199C199C199C199C1999F99C1999F99C199 %9F99C1754B4A754A6F4A6F4AA8FD17FFA1754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754BC1C1C199C199C199C199C1 %99C199C199C199C199C199C199C199FD11C199C199C199C199C199C199C1 %75754B754B754AA7FD15FFA8A14B4A4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A756E9999C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C1BBC199C1BBC199C1BBC199C199 %C199C199C199C199C199C199C174754A6F4AA1FD17FF4B6F4B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B754AC1C1C199C1 %99C199C199C199C199C199C199C199C199C199C199C199FD11C199C199C1 %99C199C199C199C199C175754A76FD18FFCA4B4B4A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A6F4A9999C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C199C199C199C199C199C1 %99C199C199C1999F99C1999F99C1999F99C199C16E4BA8FD1AFF756F4B75 %4B754B754B754B754B754B754B754B754B754B754B754B756F9F99C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199FD0FC1 %99C199C199C199C199C199C199C199C1A1FD1CFF4B4B4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A4B4A9F99C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C1BBC199C1BBC1 %99C1BBC199C199C199C199C199C199C199C199C198CAFD1CFFCF4A754A75 %4B754A754B754A754B754A754B754A754B754A754B9999C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199FD0FC199C1 %99C199C199C199C199C199C199C1CAFD1DFFA84A6F4A754A6F4A754A6F4A %754A754A754A754A754A6F4A99999F99C1999F99C1999F99C1999F99C199 %9F99C1999F99C1999F99C1999F99C1999F99C199C199C199C199C199C199 %C199C199C1999F99C1999F99C1999F99C199FD1FFFC299C1C1C19FFD0FC1 %99C1C1C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199FD11C199C199C199C199C199C199C199C2FD1EFFCABBC1 %99C1C1C199C1C1C199C1C1C199C1C1C199C1C1C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1BBC199C1BBC199C1BBC199C199C199C199C199C199C199C1A0FD1EFF %FD18C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199FD0FC199C199C199C199C199C199C198C9FD1DFF %C9C199C199C199C199C199C199C199C199C199C199C199C199C199C1999F %99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1 %999F99C199C199C199C199C199C199C199C199C1999F99C1999F99C19999 %A1FD1DFFC9BBFD19C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199FD0FC199C199C199C199C199C199C198 %C9FD1DFFC1C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C1 %99C199BBA7FD1CFFC9FD1BC199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199FD0FC199C199C199C199C1 %99C199CFFD1CFFC298C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C1999F99C1999F99C1999F99C1999F99C1999F99C1999F %99C1999F99C1999F99C1999F99C199C199C199C199C199C199C1999F99C1 %999F99C1999F98C1A8FD1CFFFD1EC199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199FD0FC199C199C199C199 %C199C199FD1CFFC9C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C199C199C199C199C199C199C199C199C199C199C199 %C199C1999999C1999999C1999999C1BBC199C1BBC199C1BBC199C1999999 %C19999989999999899A8FD1BFFC2FD1EC1BBC199C199C199C199C199C199 %C1999999C1999999C1999999C199BB99C1999999C1999999FD0DC1999999 %C1999999C1999998C9FD1AFFC998C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199999899999998999999989999 %99989999999899999998BB999998999999989999C199C199C199C199C199 %C199C199C199999899279998999999A0FD1AFFC2FD23C199C199C199C199 %C199C199C199C199C199C199C1999F515299C199C199C199C199FD0DC199 %9999C1992E4BC199C199C2A8FD18FFCAC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C19999989999 %99989999999899999998C175510528057598999999989999C199C1BBC199 %C1BBC199C1BBC199C19999989927286FBB99C198C9FD18FFC9BBFD25C199 %C199C199C1999999C1999999C1BB994B2E0628272E279999C1999999C199 %FD0DC1BBC199BB992E282E99C199C1A0FD18FF9FC199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C1999F99 %C1999998999999989999BB98752706052827280528279998FD0499C199C1 %99C199C199C199C199C199C1989998990528054B98C198A0A9FD16FFCAFD %29C199C199C199C199C1999F7552282E272E272E272E272875C199C199C1 %99FD0FC199C1752E062E51C1BBC199FD17FFC998C1BBC199C1BBC199C1BB %C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C199C199C199C199992706052805280528272805280528989999C199C199 %C199C1BBC199C1BBC199C1BBC199999951057699C199C1999FA8FD16FFFD %2AC199C199C199C199C199A07576272E2728052E2728272E277599C199C1 %99C199FD0DC199C1759FFD04C199C199CAFD15FFA7C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C1999F99C199C1BBC1C1C1999F7575272827280528057598C1 %999F99C199C199C199C199C199C199C199C198BBC1C199C1999F98BBA7FD %15FFC2FD2EC199C199C199FD0BC17576512E27C19FC199C199FD0FC199FD %05C199C199CFFD14FFCA98C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1999F99C1 %999F99C1BBC199C1BBC199FD05C1999F99C199C199C199C199C1BBC199C1 %BBC199C1BBC1999999C199C1BBC198C1CAFD14FFC2FD31C199C199FD11C1 %99C199C199C199FD0DC199C199FD05C199FD14FFA8C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C1999F99C199C199C199C199C199C199C199C199C1 %99C199C1999F99C199C199C199C199C199C199C199C1999999C199C199C1 %CAFD13FFCFFD32C199C199FD15C199C199C199FD0FC1BBC1C1C199FD14FF %A0C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1 %BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C199C199C1BBC199C1BBC199C1 %BBC199C1999999C199C1CAFD13FFC1BBFD33C199C199FD15C199C199C199 %FD0DC199C1C1C1C2FD13FFC998C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1999F99C199C199C199C199C199C199C199C198C2FD13FFFD52C199C1 %99FD0DC199C1A1FD12FFA7C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1 %BBC199C1BBC199C199C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C199C199C199C199C1BBC199C1BBC199C1BBC198C9FD12FFC2BBFD35C1 %99C199C199C199FD17C199C199FD0DC1C9FD12FF99C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199999899999998C1999999C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %98C2FD11FFC9FD31C199C199BB99C199BB99C199C199C199C199FD15C199 %C199FD0BC1BAC9FD10FFC2BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1FD0499 %98999999989999C199C199C199C199C199C199C199C1BBC199C1BBC199C1 %BBC199C1BBC199C1999F99C1BBC199C1BBC199C1BBC198C9FD0EFFCFBBFD %2BC199C1999999C1999999C1999999C199C199C199C199C199C199C199C1 %99FD11C199C199FD0BC1BAC9FD0DFFA0C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C19999989999 %99989999999899999998FD0499C1999F99C1999F99C1999F99C1999F99C1 %99C199C199C199C199C199C199C199C1999F99C199C199C199C199C199C1 %98C9FD0CFFC299C19FC199C19FC199C19FC199C199C199C199C199C199C1 %99C199C199C199C199C199C199C1999999C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C19FFD0FC199FD %0CC1CFFD0BFFA79999C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C19999989999999899999998999999 %989999C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %BBC199C1BBC199C1BBC199C199C199C1BBC199C1BBC199C199CFFD0BFF99 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C1999999C1999999C1999999C1999999C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C19FC1BBFD09C199 %FD0CC1CFFD0AFFC2989F99C1999F99C1999F99C1999F99C1999F99C1999F %99C1999F99C1999F99C1999F99C199C1FD0499989999999899999998FD04 %99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1 %999F99C199C199C199C199C199C199C199C199C199C199C199CFFD09FFCA %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %FD07C199FD0CC1FD0AFF99C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199999899999998999999 %989999C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C1C1C199C1BBC199C1BBC199FD04C1 %FD09FFC999C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C1999999C1999999C1999999C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C1C1C199FD07C19975754B27A8FD07FFA8C1999F99 %C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C199 %9F99C1999F99C199999899999998FD0499C1999F99C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F %99C199C199C199C14A27F827F805F8F8F852A8FD06FF9FC199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1BBC175270027F82727272027F82752FD05FFCA98C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C1999998999999989999C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C198BB98C198C199C199C199C2A0A0A0 %C9A127F827F827F827F827F827F8F87DFD05FFC299C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C1999999C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C299C199C2A0C3A0C9A1CAA7CAA7CAA8CAA8CAA8A8 %2727F8272727F8272727F8274BFD06FFA0BB999F99C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C19999 %98FD0499C1999F99C1999F99C1999F98C1999998C198BB98C1999F99C199 %A09FA1A1A7A1A8A1A8A1A8A8A8A7A8A7A8A1A8A7A8A1A8A7A8A127F827F8 %27F827F827F827F8A8FD06FFCF99C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C29FC199C2A0C9A0C9A0C9A7CAA7CAA7CAA8 %CAA8CAA8CAA8CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA8CA27272027272720 %272727F852FD08FFC299C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C1989998BB99C199C199 %C2A0A0A0C3A0A7A1A8A7A8A1FD07A8A7A8A7A8A1A8A7A8A1A8A7A8A1A8A7 %A8A1A8A7A8A1A8A7A8A1A8A7A8A127F827F827F827F827F827A8FD08FFA1 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C198C2A1C9A0C9A1CAA7CAA7CAA8CAA8CAA8CAA7 %CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8 %CAA7CAA8CAA7CAA8A8FD0427F8272727F82752FD09FFCF98C1999F99C199 %9F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999998 %BB98C1A0C9CAFFAFFFA8A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7 %A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1 %CAA127F827F827F827F827F8A8FD0AFFC299C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C2C9CFFD0AFFA8A8 %A7A8A7CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CA %A8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8A8FD0427F827F827F87DFD0BFF %A8C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %989999C9A7CFFD10FFA8A87DA7A1A8A7A8A7CAA7A8A1A8A7A8A1A8A7A8A1 %A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1CAA127F82727 %5252767CA1A8FD0CFF9FC199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C2C9CFFD16FFA8A8A1A8A7A8A7CAA8CAA7CAA8CAA7CA %A8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7A8527D %7DA8A7CAA7A8A8FD0DFFC998C1999F99C1999F99C1999F99C1999F99C199 %9F98BB989999C9A7FD1CFFCFA7A87DA17DA8A1A8A1A8A7A8A1A8A7A8A1A8 %A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A1A77DA8A1A77DA7A1A1 %A7FD0EFFCAC199C199C199C199C199C199C199C199C199C199C2A0C9CAFD %23FFA8CAA1A8A1A8A7A8A7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CA %A7CAA8CAA7CAA7A8A1A8A1A8A1A8A1A8A8FD10FFA0C199C199C199C199C1 %99C199C199BB98C199C9CAFD29FFA8A77DA7A1A77DA8A1A8A1A8A7A8A7CA %A7A8A1A8A7A8A1A8A7A8A1CAA7A87DA8A1A77DA8A1A77DA7A1FD11FFCA98 %C199C199C199C199C199C198C2A0C9CAFD2FFFA8A8A1A7A1A8A1A8A1A8A7 %A8A7CAA8CAA7CAA8CAA7CAA8CAA7A8A1A8A1A8A1A8A1A8A1A8A1FD12FFCA %C198C1999F99C1999998C2A0CAA8FD33FFA8FFA8A87DA77DA77DA77DA77D %A8A1A8A1A8A7A8A1A8A1A77DA7A1A77DA7A1A77DA7A1FD14FFA0C199C199 %C199C1A0FD3DFFA8A8A1A8A1A8A1A8A1A8A1A8A7A8A7A8A1A8A1A8A1A8A1 %A8A1A8A1A8A1FD15FFCA98BB99C2A0CFFD41FFCFA7A8A1A17DA8A1A77DA8 %A1A77DA8A1A77DA8A1A77DA77DA17DCAFD16FFC9A7FD49FFA8A8A1A8A1A7 %A1A8A1A8A1A8A7A8A1FD04A8FFA8FD66FFA8CAA8A8A8FFA8FFA8FFFFFFA8 %FD12FFFF %%EndData endstream endobj 25 0 obj <>stream +%AI12_CompressedDatax$&?d& C;\;fJ-n[[YUZ(xd&,f #+3q98OxOq< ?wo ~7_{ˏ~/&G4M~Vۯ߼LG{4?oqwo^_^ޡݻ篞g/PWnC} 4`e߱=&7Ô?L/ޣvu&3%Co^|O߾yq7/߼W?>y~^|0;qv~c7/o^a|t/_/tqzWw0R<3ٯOaC)?l/Jo|ۿi[Lݘج{K̬̂1??J[x?~W~NguG|˻FѤS7_ܽD/ H1oɦ >Kz3 3y}vgӭw2IM~?WyOtҺ2!Lj}.6(^{_G<Ѽb |aoSl߿DŽkѽ_bٚO1';=I~R4!o;H]cձW?y=5w7_j|ӷR}To?~_^bqQ>mŃߔ?뿏 Bӛ{U>}|CsL!޽zn +!}Ho_wg߾܎Ͽzz_~˧ߩO n_|=w.̙/|ܿ8~_xNuTOX[˷Zߥfi>$_>ksP3|q-y=?F?!]϶j~xx7/~tS/>\?߿cwwI|+r;鳶|0e> loq\߼3L/pba,H=;0?)vU\) ߀%%Ӧ\ \܌x[f`*nU-LDIo^iSi.繜5Jz7ѵ][*FAiUU*'-VmӯVuY[a^^Zd]fU͛V+ebe7g]k;la]/WkdYԬU)۵Z)*և:YeXtMe@CY#չk)7ܲԓŗYUeLIɭ̍zʵؔF2 ssλɝP-Vx>'O[L .C +S +p7v v)esUsSʧ|o-&7 LNyni̕W*^rdNONtemwp|:[l6#ut}>_\ތXwoM7 usnnnn#n1aoz^m7r*U9զL _^*qSªUq 8R$l!z7M9kӪ\ʴ*ySҪU M*vU̪KS>_֣_WENf]Z%. bXv 2ʌd)v64o򬡙R&)$%JR\)vWL%Ϫ?')WLRr)8K R|)%Ѓֵ;zeY eeگed&G/fdndbN2yw|Q^Z^Jd^Fq``2Ϡ[W_t,yP5 j>H73_J F1aoטt@H tr@x#HuN D;x;p{{@}w & +D5CpqnX. K׌ucq7g\DW);2UnC|Ey x;Rٙ-خv8Pz? gx'H˗v؅0ܮH *`Cld!2r.y 9&*w"6`ټ.b/+>P,"eXalX~PdS }bٛ2<duXE3Ϩr;.E9J\d('}(bs=U-l4W=9"}ZӬYAL6%Ts끼VJ{OX 2=ye[3UCwD翇d?Sԇos<;XԵ?ʏOBF7mLE߰s8҆+ H$" (" tAy\( !DʢJBFǭH|! ,DiȪ4$uN"e(rE"R,RQш‘Q ,e$JI OeSB%'ЈjFĥkK(2Qhؔ|J5t[듖|97nI?FյX4̲P,~)uuxIx" ?4 Qs E6< KCvD1l4맕aýE|r.VOoGEq3- -U +ͪLTTJэEUZ*mXM]JY\9UDi%%2r5=Ҵъ %s(It8i4fCT +a);12y?Ӌ+[ @c&*PV5m\86 ŸV+-7bU[#w)Zf{% 0<@jXp1frb=]9It=G9 [>EXy+3KoJO+ï~QQ0:1L<98Ilj> -Ѿ8Jl+u!3)]Hh% \hB#ڸLo{[Z&qk"fÊmIWCv8x}i؎u'^{߇qmCIJϞ=c@09Bk۱iccwt6sM&;XݸCI1TQoZ@,qx)G7?}HH|5FPH!H58RZ$n҇U}|OV#pDnyu:q/#7I0c (0 ++tǵl⟿Z +R>qV#6Cxji7~zQfd @,zv4./_bS;;c4`&# 4؜6%0ySIRм m{=jP]||s|<:@> lm/zr._S  +_rUXBa3|!<4S<0< Sy+De&W@AAr-^uUjKTˈXōuXᒗe?#uFnfVŜzQ27sPf'z3 mdq<9+r>3_;ug{YʪHNEa십]ԕd1U w]<[ )8c(bb<;]=),IȥId,v+POD QuVoʬ*rk$V_ba_U[X.}7IʲC#rOL,lEkY, En%ʦ"ƞD1V] ֯$XJ:vl`{6VY=+2ҡYAnVط6{;,Y ugeV^am|O;>ͩ3/Յ8/+w J.zꓨW|٘X \bU鵧_o(r = wxw<}e|m>rz|/GTy/Ҡ `+0{ط>WG|/\,O_{ُF-I}34Yݵuw}\&vřTi: }S͍2.",W(^QFl~34mٷ!,ԅl#AiEqڐ9,%#s֡%bn#'g=<r9?3:Iq?:?'3{T|ʞ:/yE X0=QnEk Zi:EBm9Ɍ \:H͕}g02x˩yj4>/=?S=w טiąYpo8&joEI'Oo>sQcb J"cTՎb&)xEUܔ&`dgFzv%ś9rJAKqj6Myt a\~ا~+7^ݽJhgyus]j/iR:SnBL%4#Kq _ KX*8^Kz\e>l/ư7TVrǩ0 7U/ޅA[lH8$&qOTqʅZ%.B5!% KJ)@(|GY:>LBaK\NKyUoItfm6ŭݐ\¦?)NUWe6>%BE6Q%J>q唚mWCyZy"obݮuc*qj+}ZX9>p,JͫoD Dޤ$;;mXGƕl_f#2-dpeQSEN[cnm4;h\; Vymff g,Ju}o14Q$:i\-.ns8ič+9m6VkϚ|['hZzvvAuaG`}t`}YhG_:m8GN84hu*, _ CkA|,|aGWuw#my{"Vފݹ(|مavWJů9jZ~,wr Ez_dxZ_KY˻JR EV 襠qwU)y@R޸0fwnxQ4;9LAǦ6 +wz·2_}q|t0>\v,нe| +(Cq7o]ͷ`['sثj[d˧1rQNت8 ETԑ<}>S|efk Hʾ_>Ctu;,0D9,'%CS9 .N[ZcqaO΃q5Ovi7bE@Om>nzw)6>FaCmq$8I?I7 ,BԽMٻ +M^A*V*#9Փi]nLʼnigLìQ&[,_?5@'Q +oDT7 F\'uY6@  ,[eh>O*r.V+÷h#exZ:i0$3QN +ߑ6V<ϒ^XEH92kV'jX\+KdP8SGYр3ɡeNqV1 e'%:U9J1џP:vlu{L[5*F6Ԯ/+B ƪ-; eфaǓJږ߶<' Oo_KM ]"\EkF0EDϤE Ȕθϔog|VUu^v)H! {'xT:UjCITҒآ(ZJ(Z4KY薮lZJa\"6Bi(D9P{i{::6KOyíO0i5*alX!X%RK努Q>E(c4,Zۙ;^o;r%( />1]3HJW.=Cq Q; JliJB%۵C^ײҫjL[ExDZ䤵 nI˴~ԻzMj,v8'2<9> Oo_êD Oس&F$(0SNeb + +0jhMMop-~v*fPj0<м4A͗ƪ]\ /0)zdp[_bݫjZL]kmrkX6ָ՚.XƬuɨ1i=d.LYO^IS)exZ 2< NO' +O' +xm7?a>Ykpi>d8$\09 f‡B'zgFxqO/e⎭cT1;ɸC0tmojj{ (qp͂C@z Nim)x?NvU!qnܺ//rvEi]ŧMi|T3ٷO'ؤ\L+u6u&٦|t,JtѲK]j=?Q`%֜k,pHyTak5u1'@IQAl P8MJ&7nJiQ<YOdF0DG3yvhCx/ew9ˣ/h-UPhnu&rSwOfc[x^$؝lxqq}csnٖIİX#!uDmw2<@7[I%~d<>WMeuv]a v2K%ѳ.ڟsq\\40Ž[Ѭ 7A 7_,~bN|v#n`^E.'[dՊ>R: ^G=,>($&K}id5VZڨpk ,ٴ 0 JJTgb60rOd`WIj>Q)3G^<-g@GpAYj,D9&# n-ƄA2 m2v'}2(W]c~TQ9oXVͨt?{<% rv 3nl=ثcvL !crU8+}Nߓ%@q+%ǭ$"~(2^ +Ma\Qݙ>Y+|Z[1z[gz[1Ԙ۴mJUr5Y|Vnodw`xNRKisl\7P΍||TGYugnKE$%V`t +6>+j::T\Phel銻PnC%oS5 +YSh +fWX1V *:I2SOBI44!eVk?xܓ'cOLj4-oKYuUh% JLfd-ytu<+qeYmˇ_CUVN`7(<ˌpaN ƍ]崂v + 6<žr[D\,-9n^$ D8aw2\0[ԡz*--ZLTFh7@K`nQ@밆#^MnRD_yrwQvpѹჲpb?McCa7Mt;HsM8)4y%qi$!wb-jqo1Pm+?:W]:HC'tJ!v\Y"fzw)n.(RD +K gѢ_ \߹D!˔] bjQ"#ΐSm2a+|;rudȼ]A>Q?ulR0Ԝn`uEss9 +Q37fQ;NLthp) n)n6WZnE*:]yu}; ӕdqYJZ,=*SZ< JӐH~[͎csߥ ÅR$Azae+̬䜽)Ò)SA$ZܬKV׬q:k)=As<2mS/LtMo@] ?}S>wihx9{JgU|>i?)D=:Kb%5ީ xyN$ȫik. ~s>4P}h/=vY@M~ ƽ#j&btan"k9hj/$s.!OE8]O$opLs*~$neqb:<7BB.oN4;rN/+"V}ZC^2TX5wb!xS`*ffs9 : i.!JUfQ?H*F!uV[6Nա$ͲWa⇰0IRnJ:'hp!bBY +`E;p8O +n2 ;aN(FXg `ᡭXbmZbw k7ax-(ÅS[/|um*][Wm!n;2._=11i8cT]#w-ՇI~ݔ÷Q~^/CQJ Rz$-Hi[Ȥ"k?hR{W5qn UVi+~ + +whpC=C~ӷݿOVrbXݽ} ?9DaSt~;X43a{GOl6O2+o:O?1'?O}t+T>:oZv{{~ywo^?ïD{ӛ7/1y)wo>;=WL?ܿ{݋w8w|ȯZ>@ȒgEYkpZtSacBM~˵)\Y$gq81A`EtY5$fL!Lzv#`!1<5ق6$CkG9`g9A&0=޲;z|ŏM?!0}<&|;t> C0/6R[¾3>8bx 7鈹3]Gg #.6ÄJ*1*ɁDݡ ؚ#{6+#Xu<*ln[T8lǀ`!k_&.GN΄bia&P,. G_;3GxbcY16TCG)D[^6Wc,d:ɓGd1IN'LH0%@;K'Y(G(MO#6%] @PbsO' d$S߆d-J4Rwg4udo+qo5!|~lb>Y Vg= !9P`W^éȝGV` jTZYBؓuZuvd1elYSg#n +}[Ϲ0qufۿdf7_/ ʃs7_ٛ? ojõL{آ!TfEbѝ(xL(2f㴸˸$n +,L"C"zJЄ \7T[ʤd + 6vBoeW]!DW1oHwm%H2kdRq'CՎXZ[i;Fx#B޵] yoeNl3_cD9D/*m% +dބSy]- u•HJbcx[ w?* rYrBʟ:3̑nsS.eUdSik3G>fT9i\h?qsiC;+x#@E:PlXJŽKun|ƛy$ 8C(A]Z0Oׄm x{\F0TOEȓHfwaaJ@Cr`%@|R%RU#>Lo;yp"MfP"0=Xpn |9APr- +23A(LOř\'"Dӂ3 +|=g>/ݡR҉Ѩ#3'7=.{&a1[f^qɷb!qVxT,@m gS+ + ~ +gZh+6,QYޚYռ9>sǹ %ґ?l mm]㽃)Lp轑ChM + SI8\Յ<%44.i+഻2>=ρo?Rݵ41?U58?!Yި&2qňLeLf9m#p\ Kdve~e K¶#}0UOe$|xA*4>:Q[# + LTU$ ֎&&4yr($bx(5M0Ɉprwݗ#-F6<_fn.(`wy|nME&ڑFȄ/;b)q&L{30D>fL4Z$.H=%7}+.Xl Vnp4s. .N:іDkP'_C:QӍŪNT)/#*tNN."PtHݼBsEn>nlG؊ TV7SBH`dp,d kEڴqƑ-@#v∣ؔfAR !D^Kh׊309Pd.WjZQJ`t-vߩkNM7nv[y=Anu =U^d*NIq<4Q) +4֣VԻs9)߲zNWQFm;-:ϱ?3RONYϹ4wJ?Y1F7lֵ, F8J\oY}68j @)7TەTEਟ +ic c&N-pdd?ѭLc̍w0SkY~x;(&68`d]@i U}q@3ŭOʨ4|nނ, xʀ1MO#iڸ&H-nqdq]$v0qTɣR;{#OR?WjJ'$q+M[1MFU7$#C tm!NLa\h{:ldneMq @G׾QKr>2k;AMx_}puB^C?1*!jT-tC"GӰ׏-8`Ǹ;EN/ik0C?* RFnvn~Uh:}|KCpބTmOM4{ޭ8Ix!@cnY`LXV@--4%"Epj|E$GΈdɄx%hkQQomh=mCQoOwN $. +4"Fj0Z̝eHӐKaDcJj{ eY[El]lB8y@A;^|Dڋ(@\▃@:usFtΐ"hνIB/>RzH#wm=Y sΈr ̎\DK墿8 W&:nХb%uV7K$3)ގI^c_SroT#xCAa#4[So+8ո{|*&D +l0;ut0ۙ\L2(D/qpoTWF\ n6!XH/@]/"#RBեx9D +1 fEh5X1FHXPIX X|5^ɓIr=t]F3}7Q0Yuoe%D>9tɆ/Ge#* BBu Vѫ!-W'0rǺ~,!^iTvtItu:+! H{D>GYƕn\/ iǩ'+z&;vʈ!5p O| ߩw{$${b[BPO;y,Ͼ YKfa5;-gLP>]yl?w"CuQ*v>ͫ֋БGqE{'{: +豛ɗWi+vwf7V'˒Qv]j.gT쒑hL~^eY݄6:oCت(^@Úd7^_y“-]- T?Q{jԿYgOcV/ɂ3xo]:y_~lukjȣ^}V%/VQ92RඈPūV Xԓ;Nev=ϻJbOӪwCi֠cû3P.v^z:p}:5I /P'U`fxX;Eu} e?HS߼N"7m&E&<0A4۴-fRny|WF$C2KaR`/v&ӋKD?:\ + DLsL^:~"r|ws5mn%n!#\ +얍y09ġxJxI&0!Sl <ཽAz#௑(k$2JS` L%NO;/< &*0yf0 +XOV:GKoe'o/^wDFFWfn +8ݱ ɉ)Z\ɹxf#~2`gWuSq!nH "74w`>`ő<`z*Po1գguΐ!?穋H#o1)g 5k78'*%PbNHj2pWFpJO^6GA0SXb;VF h=Yt՘‡Ob4Q4d1VTGKN,"6ΜF %3a-W3e^\ zr] Ki +/ƺ7EN)(ꦑ6~,VE VSӢRfn~:}t0%GQ Dj[1"FQQb3Q]?h+&@zLYB +,%Mw'Ia$ Q=uYsTB -ݓU g&TQLQLgyiP6Ǝ}]7iR^!FKBᦢ5Jd2Ɲ:qu#U +H)4v-@BN ifSeO>I"Ntl?Us*Oi4K;!ql%1- "%(ѥܹLoSSᕝm( +֌ܪ+sFFWod,QbdB(*dtI$  F`3fP"0 }̼aiටkp=YS‰7-b$'186h^H6 +Gbgy@h <):o^i&망n( +"deA53cK^3C"xfB+DuŅ*MfHV@ cX=dԥ bkug(]ꓚVI`4f !m :Ez8ӿR@LM7P[y=A + D T C׊Vc}jxɠj)BН+e <*%2.Aܖnb;_G韬蓺VɕVv,]ߠwjڵm?cDtp4Gb@ +(•<j"y/R;θ:Q4rS%f6tw"zSv[NC*FJ""9XvZ4OZYե洣ԏ8^/\NMe4c ݲ +X dp> .hۈ~$t$kUeGʀ<K^ԷxQ$d B(^먭ŞBע}*M694O +XΛ +u,_w-/ߢZ {[;=qUZ*Qu஺/[etl mѾQZ7nv`܆EU vY};ڏ %^VDoɻ^taecDX)pÉꮅm3׭8mP d\K 6oѼЋaNt43ҌGS!xzFж^pݴt3zT9BET"C ":] È@:~$@":$:Jz^.8DׁCt|8D8D5lסD0};-^_  $C@"*_ 1CAB~-D *.D*`Pwa*&`P;P P6Cp~` U-.C:Eoha;px( .N8ZH] P"ҷ~~۱tk(M+%X8ag$ ٿQBT'z7[e 'bh3PW$PfOVtE9JF;z␙q@2\!"y҆Dp-T>jhGV,J;#[4oajRAq膂i_-􍚉ick4>ْ`86:~rѴcu{BXX,VR0HP]0OθN*E >eat <~*6Gۑ"ok ԯLpǖǻ*L{U8HJtRFmKr }#B;Ӷ > +|\oa\=y)t3!-m߈7p@y7E:B޵Ҕ=׼{R?G|?Va$T1Oب*Ed\5!soD 4@.3b$nJ{Us8^}]U~9)-ר>M!:4iR#e6ݏiXeiig#[%.hb7Ϡ=[/abrQkG0^3Y'{: Ė: virwb+VUcն$4M?ʼӽP짠<-1$[&mI1_VbFVcF2i@TkʸLŰA[#4S~^Ʀ5u<4O>NJE(پv +s.MCoELIənܶx`;(VLG+qv^BdIkQdX佗ak4Y|X;;iӍ4h|^3\ĺ&qeߩiQfn~:\l(neGQj~ZߊU!n+*Z3Vbk%Iu3f퀴$݇ϢzYTW$Kn_ $\VbE Xnfa}\":*K5*Z z*[^ŕb:Qw3H`P=)I`M]aU +3ryDgQMtz8EECẠ~ +E{,6A2 xA @ɹkAv*«;- dCBDn}'dхt_"1chFZ@*̓dZި\yřLb +---8 "d/]T̴t>PI(v u 4O8oub){4W`Chr)8E h`hK}LZ*hE4i[4gcj#;DKeuk#i[Ni9sR ,i@`-~k9N.mm]+[Tɹ@tWiߣķϩ{@:Kg~LAǴB_y22O8:}UaFE#b)#N( + ,0+\10WsZpyt{˵ jD$}4E} o~߼}wwᗿynovw_^߿~ֿgx۷o^_:7_m]iFϻ/ٛw n޽}qR0Y߾y|YڛgWqn^QпOw_޿./GEQ ɺQ1FF>gg|p4smYn^۬Ù߉|@c5eD!'1ծ뀑V+Fky>٩* ~ y#ogclJ)+J&H4 +f`E +ZT:УRS9@3%O,#/hF1CvU@uyPk%dK0^;:#x#vbΜ%gǤ_YR'$MhAܖFJÒI_G5@T0{7+\Τ7710 0@,n&V 0RUBn>GRW9E&?Y#Њ +lJFIVE [VW}-^pG|L8Mo F&ЃP=c0a`oM }8&\&)x,l'PX&8gc`RIM$h8t8*sȘV<$XzUAtDTTK{K(f@Z`@Nb}Koiٗ.F|ȀGԈ4тk ɳ`KZ9M5XXI3m"3'!Dk:$$'5A:ы;I0ђ#Hs9虙x3$f +TىVl K+nKv b@LjHE# +&4240=#%sM9I $Q,*WNXYI*J GFO%]POH(ߢTseѲWT<0ƬF&v +FB&?iRa}4J[1Ez.GLLĞ=ܻͻqt[ C5>N]d`Q8nvr-[2o5ȓo";fQlk=V1s!&imI*VpdJ11GA[.T¨heg4UȔ(1qK2g ] D`~K+U։SSZcIӚ\ܬu>B|59F$x8X3=,'(SOHc\|(6sZ3ʕqpǒ:Uh!v%,z8)Js [1I9(zdŅvj>i"eې4NRcy ;j@6WIF})Gbc-I * ̜!+(+oe#BC]Ѧ K*`R/}Wq 9W|.<(8"d譙PɕLU+Y/|d^+¬[I,$ےB L +W aҏe + +/AxC!A]U8VwC !oOsᓗCj%\B[.|&HpxQ3t+d`X)dVǩ +4+GNIeΥ3I bDI `xV4HE=sJeg %h>g8H\;nZne3ٙYL4tR9%\UصءIT|>T~>T*YIxYq;gn0+)["|=8:W4nDL_BQI>lC$;w$tд;#/%~ԥbœoG_0;hAr^9\o'“ +QWT &?H8 5`RoF9Hl Iev#Y B\j'ADUÒCTG|z!VXV."=X>w 8Fi$RJ[JW|_poe-v8GO| !v6*,W`-eIMu_HBRg_T]$:߆6P8|!= +IEmla˴Rvg *t-#dD| n1w81ge/$R`i qE8+e^2e[5ע>)~-~(i"^Yш*W#CJ '~L#?ϸKOՀhJu38IL/EQYɭ@E'R]3D"8Y!.=cVL7I/n[s .O99s}?B8AMQ2z݋RYajQ2u?Oύ}$)%M*CE[Br3:[!Q䗩rd(0-.J9*&rcDC +R1cţߑt0w7E%IKMj7؄^hqIj!HBT (M)j@%$q)dFF! TSF&7DaSGT'S= k +!#.ZUzZi)Vy ~TD܄tYx w &:GX~i+;$2d9&Ee'i! VAY)VQAEṑHTjŭQaW]2Ĭ HdffT7G2ydVgVZ*=IHyt 뢹rq=*7 ~&\jYP0ⓖE +j/P݉2L8zt?PsiFOIqK&W'5!4ĥj(YQ( +XվUPdlV@Dw<%ߜbF=EQ30YUJY9A}7 +jO*ijI[9p>;2U%Dc&ƴ0'NˎcyB *¥$ @k[ _K@(w˚fIP},PǼ(gn$:PIc㘊O0gT@t;)-",$U 1=ȗN/m3x6i^dMf., &b HdIЊQHğ5\j"Gs`c~\|P$=Dz8N4jsM$PJq^GY׈Ҡ4ptkO +} +%EEWg8չƩA]xnY!gŐIYW)¨{D*$q F'wc2xL=h+)WXQ/ Gf[4%XCJ損Pa^04yB +3ٙĊL$-8hRdwrzjsKlWMxI)v3d>J3CnYvܼhv 9z|`1m +`N_7y:x5uQO`̚($C#Q&fMΆuEXRQ`L{Y2gI@a!cհѧK-Kdr08OOQ[ptNL]6,J`I#ĕu=ADQL/@^I|ſGc!qʦL{>ggf0.JR]rOԸR]w 5{i, X]ź G+)%k\i"5(&)TJ' P^f/E Qԍ:5Kk5gPﲑFL55[!_DhXW] ă'$^υyt!D8 +YOlOEa)J2jBޘgA^($p$轕dpeՊłd ec9d_ +PHEw21hH#u;(DMv,ٓ-  ;IdgzW{8C@_al=0` eC<+ܟkR<0pg3"L2bgCh49r0GGu'x,Z0y2jLղ=;(fnzӸWl88@-aF`"Z̵I6$py30ܚ8oDXr82Ռ~Zq&nMDt 0ng=ܞq) l  {Fn^ +4Zc[,kl\bI+(5Sgw!9~qpT40c/[FbʞaRE7kg~D8ࡉ1`V aKb%J*c9dм(=L N7"Lbu9%6W`_ +2Ar+Pj#؂\͍i&YS~IY6e mCۨjk!atZ=x9[m5{z`fyv>C',|du2M JOjߑfI b಴^˥ǐ%0~VjA`Xl_nZCu$ (f_Ŝr}A'V e쳄mgB+ |%v1_#NjJم10tfW +'-L#!<؍IMMΪn0ǟ` cu + n-K#!!?FT 4ISiAKXZ^!TztAwJ6:7~:*GCb!1; 8[]>mS*|)겍@ .(W <:; :զ3 +h8qML(=\2)@xYȫ3{!n ؿ? +mD=ߞ+#QZ)N,czA-\7t6lN B{7qes P)|ïMCcK-8>4 34Lh=^Q HW,7)ӣ3?P8 +!FRd% Hi&v * r*Y6zELS "XG}v `ͿMA7R-̫{f4-?BEf/3~bpGhvcPS|]rߘd 2J9|J6 +m!Dr< K9o)ζ !~H㓃6-$ж|PN *>q<QRpQU?9/amK,?hca&ť6htKwO- G +U!|j l_p$7G:j'Rdq! U~~־ Em;np A*&<8'cQiTr[LmG@^J).bf_yXz|+ǞaV'%Sf'\<]1)fv=%LVe%wb$T*돐Cʉ뱉|^@r|,9Ff g*;7;v-n9,Vl(Z5420 2( νD͗8Q)XE4}<ZM7Jh]5y曲71RddYl=yEpw_L!;!N?P Gk6H~)H$(Ңa~=ЋorUO _7t~o }\vS)uIMaSw }҆߇ORޞ @1Bʰ'p PےZ(<A[lg ym <FumI ! ~mm.?pٓ~4袽H 22w΍kDSRꢺP)a戀~>$4B;>P_]{|WG(tϐf(r>Rǜgˎڵko +nSVfNMԴG<qҘ<35\Ҏ]5ۖt0ɰB;/1'YV4%AN$ErrR:u`+R_.5luG&Bv.̯iUsh_jә!f?PzE2U|\WFU:wX#= +ψ5vW=JEאd;3]助Q2ztN+_`}{"}4*T*QГB~_d)봳4Fړ{f) $yضi9ؿ`W@hMZ,[P B0@Z,a$|3Y4st(?~N!$s0 ͵ +ku{aR9'tv5e +K'S>!1=@tPWfl;Mu8Z]oTXó.?@i d30P^6@(AG`Se'.`+V]ˠ^B|,r:ҢcI]^_T6 tzU{9S5L-H AFce5GiH x7)G~9ї{>&0 +?:2˴elX,&6IGt&s۠qJr6:Z$Q6D0N{+X POM#; +g +2㧢Ѓ^Nω*0sʤ>G VrljIfmP%YU, yB h;+bPi,hvC%5x,=V_Kdt5첷f0L6OL:l[Wk"qƼre̋899C!)Bqpu»!^ҵ HAbB?Ww7 lgHH BW3|[(@"UZK!餍m=V̥Wx`p}{]$B~EjQf9!jBt<ފI \=)GW(fKQŲg.+sC?#Du>zoh.0C0i˔ē&鈀¤|3nJ `.GVk>nX9l +@0*'7v ?b՘ᒏZa6`P"̎垔]Ur'2Q ΨWDH B%}C[7c"))C_Fgl9B s^op"T6 XEoIdtĄ N"'L|[R=8;by!MRZLk&}9EoB1ws44&䩡4"t}n U (as!Sf6CeU3<  3ޖ,8aEv~@3ڑ*BDzL%gƟS mA92@E h1tzE-h 6= |^qD*bT`[BDˋM9W._ Cgz#_iYn!T{Z^ =xYahT<RQ{%"'M86P,vKnr-XJTi2sh2Gaɳj͇Gy6N +]l뫜%[U>C 8L޷8d8(s7QF (a6Ķ)2Li(X +G8x^+g+)}ǯxeQ@!= + X{3Y=aYLRIN+v\)3a +i, +MH^ƒd_w)sC~IPLzfW$dm&*n뫜ThN*m6RT)RŠc U>1n=PKG{ah̼r #moaN#Yl粄~ 术I_3gX Xus]|fAJ̗վ +8bʋң9rK֚FHU[O]Ad xހ?7L|' 8e%1`KQ,S +JytwFWr-ԼgT9ǁ77 MVDFO%a=WV8s{9DUpfB)R|N9E.6݊'5&%5*G*عJ pm}jHg/!I̼޸rqޕ9TVr(zD+="F$qG9mFpU,T·T3s1He>w#,fY\5Whߘ ˆYȽ ^(T=z sUU(K5ڟ 0F9RՆ ă蜉p{nakS-V'q˂Iɧ] +o}un`׵\YZHiQסostqRj{6aΟȡ1K mFvʫRBP%D-Ά9 xg + &\[SYg?Q] {I>xu/e|ڱOBɪXq*3o-D:ET]p?BtuG41E,4 Ż}d()#գKv66o +OX@(X8bZgw@C!'AQ{`w+9qVr6%}L +u I)#Eq&GUӒJCxzC>s4fYHx{DdǒԱ p\lwMgn0.PQV<S72=rj륯pTխQd:35k3;wPgRlx _lBGR׼:TbOqZno6A&F?FyP+Ytg3dk5^l4xSy`áͤWbw}5m:OX:If\i,o%6H2_57b;s7Z~C 8 Or;UMg,{2rfc:Sm?VXY0;ܾOX@u2M,6ª3e3xc 9YxAEIA`YZh`aQ+I?exBMZ0d 8ܾ !I{D>2@T:sYvpDvF>"'oz3ι0\2;zc@fC!!.(zUsF)WcќpCG`GRs^9(-J\s +7\%`7Ľ?#O MMV>żyH|+D3Sry,$GyЀ@@ceJ% =5tn+C'P|oUa$4{,g0 t _}8PHG PbΛS$@Ǣ*_A%$I)rmD1׎ʶFe^;^F‘|ȫ|7B<쉀.-- +AQe|(UXjno*I!CuԐEVAd\d[V%$M-V;C <36لdi$s̳8ȅ(ߧ5y- dnѽW "^m1$d,_zDD9Ƕ>4#XhD;uܦ$Ks9MGjToRn_Ds??O?ӿOO?OOO޾ͭ0'Ϸ`<១6`V1g ܪn17Zr7ŭa~|f{ 1S=V!osT^!<!# +I#*6(g}|щ|BȺb+[)>$;fqZvBIUsB0W0HQLslT Rg/?/zKԾ{#'A =fy_ݧo(?[.&ʧ}! 틐09 +a__$Z_ )Y=LٞG}okzJWGz)o#z>HBd|垦2oC?W-O˷<#Iʀ~6Ĵxm]<4O.)s6Wl[KG'xoξH}'Wݻp+Avos.~tqs6=| s~wB~Xryw6gj˓rmŎ"0ۂ} xf;'BOu>=hKPE:t{o/u1#D.;q2]N jendYG!,aq[m L1}QRP.C̲|>-tyahJG8402~~'woy| DQ?% 1QSnduh5OhZRQ9 ++t/ksG55x/FQ6J7lq((Π4~8/moct: .? /d`!_>+/Bnz1uNi[s!˰!Afy[ _~V۶ܱ[Y9nQ2L^ї!#W~G/ݮ5(}O%0"(߲oyX3Đ|C7!Dn4\m&ᔿ NJa !X?YEŒf]"j7Fp;,=/u[{7.OG<[|0.Sy۶jpaEnZOnS +mҝZ<'Y yF~FIDpFżvp<})?m-)e 4fZjOmIm]SlScⴎne$FV厍 +m 4uΧ6~gq!?k,ݚ=Mˈ}?\]T8o/ND۰H|7QA1n{j+5[+53 یfӃKmip7bt-P>Z~rf /jk8Lp҄\vYExI4$WmhNsy fDa _3J.oh B}l2{噿A=͝9E d;9smsFn,ݤDK:fIJ+@mD͉2y!кn2xOX[S2k\<#c[ݕ]Se6JGy)'Pd+S^~ɇ% e 6G.nrx.ܞȔK{Y!_a 2 +(=NZK*#69ON/scu/scdiS^V遼>H,a Q{HlpEJ|0#Ƕ1R.^8|)YnIak4OgA*&-yY v'v>T 7{Aέ.,̂r=*f *Qxdw'`V@ Gƿ棞Km{V.d9+`b|:g2=%Ǜ/BmVC.L{w>zYQ0wŲlg^\ko>"ۇVx:?Qo +c؋1.qZ{Ɓ6C&im\#f>o2/r/l6kqiO{[rp҇R}brE +1mu0~[2Xtч-TbW~ǖ;meq3d-yɟ?R.TIb%ݦgY+ Du}ڻ=5S$Vk&zʴ=]=htoGX,2_޳Oωg*QYJT=oWWGjCUw.iWb Neۻf4>we8&]8MH,W?e9?e5iQ쀄˛+ TtU~1{`_y1h>q žËm^="㵔7Wb/UCl4,U39\UiT 9\}w%Ai:yȵtܭy{k4Frկ*`r0nW,ίle6'=25R(1\ί=`˄bRsˣA|<`amg_֯4E=C/XzHgH hl^HG4ڗW|r􂅜-kg/X@8Yx?` B4Zxsei+?&@L`9윆)96G?{Bd1@x.;}ZVlWe9>+=1}8R@=9u 7Lvί/|< k畈A,l?6zw  Fv]t;AWͨн_ikwY +v| p5!)ެ_(cթm8iƑOW!)Zcێ"Ηe+ۈʲInåi6V$JyWQ\aTBv|WL2'(k#_{ۆӒNEMaU!<`zZÈtz}:ӻ{2N?RC&u^;: #Ӎz ~{֙s>3fX@t:TIo۾B:)"Mڛ i3=oR^ádlz2}4=m:m)sQt;.(^^x~ۈ9ú9)ܛkԏnP0IZGn'6Ed6NқRnflV|->Ufa$.kOe\@ -G/϶-۾/qa։/`<:NBd,#'xrv+R$SwMf[l{lX ;ŶyMeU32l->L2 @1I)ļTn'L#-@n'LP`敍y8acNB&pGSj+Lo|`N8 O4 %ˉd:e9r%I W٥W{s1. wTbP4^xذe9 +G78oKBt^'EmzIF&KXe!II"M4֥"Drm/Me3,ߍ7_3 +=U YD[".s^Z)& 4rS0(50x&6T0)o6JlL('F 0%׷ ﺝ0mcu\ }ZRUn(Pb! ezү oڑN+Ey(4+΀2yxb~E2I3.ٺfSs.3SuVggOOLɟ!cqeC\R)paĔVM o +$ϮgH( ?ζD:oLF@ΗñIb+d64M /ME+P2 RAV49_\2P6ΧexT7CgDɥsiѦ +z>&jkҷϥY}^A +lWKl3K)᩼yXAA a[Wvݎ]fTɱS2:*;-%+]ێvқdDZmsZN$I= &;e2e0˪ƒm7~HS6-&"֛"wɷsmE=MD+ vƞ&މtܖ\GҍJ.ȔN\-"ZULe9%އnC9j=!QZBf%_ml!,Ů o1zಷ}!oٮG+jl]^KxZba9mHx?OBJgߙ ڮ-6< {yeslvO`6"<59K63+.!I͏O#B/%4o#B#FfWq[! B0A)4 $$ +tֈ*H̨k 6/] v E(YUm@{θ/ ׷Dc/6HugSj&-|y ^aDRf_NO +6S/R¿cT ?0hpj4 M͂//>3ı,Wr[y42i!:޼ec/h%ؠoAb7\~}Y2Pg-N:IG!U*@ܯUFg N9nS@!n h?[(voK@Rbv{J6Vy4N/G@j,#@@L:6}s8ZFrT0EX`Cɂ2{8;# Pn'@`բ\ޜgo'@ I/Hhu9N`u(*< r: ]oF|Vumu'o6J5g/@Q9yWmHr rU9&4QQx31&01ڪ Г) +9<t4)}gJ A+y6q2^~@E6䜁J})"ڰzO?(Z[h05WEWRX|xsӇĐv@vP@;|y\S9( +v3}hw!\y;y:RpiwG-FQ$>AiO; f|8@iy +6QdDZ$]w']ZsIߑ{Q j + ݟ&xv~ q(/jESSyQrlx}7.?."R7Z}EsD}iI҆@`?w7ට׋93t{GMmO?qJ=nj㯚E7Zy_(۝gYj۪f+}T $J,cۏoޛ MpWX) oqs]p| +TR͎%^=M]oӷjo U.7e樟ooTŀYl_pܺ6o&n@nﶞBr[O6аsCZicWm6~UDF6[=BhZ=՚^vTXH-g٦ZG.-u3 ,Y=TDht7|V%73sdKY[|7+Wnqq +-j~0>f{ːYe=O2U5[ɲu͒l˹n'XdU1a2C/Tebʝ0@N|l+14(=djzui%,`}v{ ߬EEeU']:#ܼBb4꛻WE( k#"#Miu)vBC/.F/O׹eP()#pcqAW͸X.@)%C!&.,4@X@֗-BZs`xi/-c%gemLlY2-i\Klr/*y^f(zx]lvbwU,Р8C+xcj∐J۪|UQe"}}2@x.8D3m? :{[yOd!0"b{MEvh[L)W7g)f!5Mּ}G4 +uh&5ET!KT,~_O\.m51ށAhcmtiH/z;;QKcۆN^}@Q/x7 vX˵)̄GT&Md/6-O'}l0n+jmT5(l>=mԗrA z8 Qu58IuP]hI5_&H k28tFw[ roH*1;Lo~Gcin#SFdRK{352)_2Ůuys.b۱h@2*%џhE R yTߨ>i:@n!͆d 7!Tr+ng! /C!1~&okj>]Igz8 Yѐ,H0bϗO?Yv?Âm|4Rz=}ˋGOKZ%6={d,_~(K Ӟ/ӕCǻʷ$H/?x4r4inx켎N[r_%,O|,Lto wNQN 4,s/xѯq((۝S6))#oޗ Zownb@6WgDyXp=/7e[S`ӭ8c!LjJ +7MA'K( ۉ3_1 h_2) :Y%,iC:Oq7D/G]_Nb=@ $k5sכ"D"ߌ\EK /&46v'[xfc䑊chsi2JLI:5e99K)`9ˁS vOj3yӰ1Cx\DB( <ޗ`zb^<ʄc*fZGѹ@P0!Ӛ+5+ v:{_P;>whK90%ud`]թO.ѩa"ۊJ + LHxNb0,sjnWEaV֜9)DtM(6gQ{*hK4{U6^DI-1JصME˯sN +V4C}H~s#W0h.sZD&?B:?UXiZ m0!NBwcPv@ +TbOd[3FՊ+=DRי+u3Ϝ[)w@3x8JhL-hZG(uA) x%f }ZQKѹa7 Cjb.uv4v[XVb:R X| 8Eu˅WS͜7Mm:B(:7RfYYWG:gxsc¢Y@vM}*0^Fo +# ܄zMъYV!:{ac=ؗ5}y9eS,v"88@flf"jQ̕ED,Cp0dZђmM} K\qs:A"LeY5̶ׁs8ey+¥oq$Ґl;AdMwU~j+l.фeuVQ1ΐ–*1Ѓ,: 5$M̼ayhSZA)ex$@V9PGCc.huzE^8P0L8Or7A|ʯ͸ (/z7=m+J8U Ĥ@:|=20UNXD;e)+SkPg/ΡUE³Nyuy(dqI#z 7Efb'ZKN}K\j*h@>7,-k +.E[Afx^nFVQIh86|ƟcyϲR6icTPqtaijI#zkVRsa=?Cʤ%8L\{47T6^qj}h"JM`V%qmN=$@F!DFK%hT?$x!P1W^Sy/t b +BZB6 36tB(qj_JϑG/B'e~5~+եL%NglfHtng[̙6h>8ה)3a'ё0#z!'Luѷ)';f[4 uTʪL +&E"1@Ys'8˹B䮴jk_]%W_6J IRZ,u B&9 V5Wp9K[[[ZFVs ۩]M{mP?4|yI"lCN~|Jo}Q|S}hb(?y,1h]aŋ M!wH:PJq'!He1vumel~MO!=!?ΑqKGK +3 zOPBpc'Oj%`E6!h2&Q&ufv"b;ח]*\HhEi6v;z=[z-9#v-" +%MGZ`v;)T <k"AZT)}R5%gD (T!!=c_=fdpWBFD-݅4R0;J3J``\IV3km{e?Xu6WĄi TLQ-?SH_ˢDmEW2&,ނA.clӌӹ]p=Ѱ1Bz&AΑ ]!2yg];\Hy> s/ ø%46©hbj2.޾9l5-tZ7g:(q*m:VBYw}.C,'3D u6kSaT<(Y~2RC&ZI!s* 3G%Vi dԔC_5=z?ap*:=b_ PH& c:NmɽT`$ǖ *3maWBѰ &f)W̯F nyh8$Ա!k)la iFMkG#$H4VpTL{׼RRlPA ӎe8ڭ;ؼĚ'5ch5WL^{̢3|XD: X {bY-[|ܱm9.3 }cFԂ5$[bO8J 2챾 6DZsrQFNa)KfeN>SLLV6^9Q4"9Kf$-ܑkxaWk,uv e%Ѳm#9sY229dd< 5va͹#d@;זfe4{ uWIk_!}E)oV3!ORfFx \S) S/"yORRcjY;0*fDB (&6:%!G~/۹ 2g~%LIۿ ,1 Y^.Y:A+{/$HvX>ƕ9^AR,(Yβ! =5*݈vrɫ5i1nmeц̝Cns  ➿|ϯVZ`2~ o^LjQl"  Z+PG;6{F{ߔy=n,ߔs(_,uك,% 2as+P54$1Odkc#iIwntLىTsnCr!+>-,]r@!)\ +^C19+lIoy +4FDbe =;~[pp5WaBqrd (0]e/~1vm^젅@'U G8fSpud< tC- xm~l\E_#@ 8tSFaD/3vqaȸBp%36 J(m瘘q̰G6%|o60RkFJX,wfڢ /x N#;ٗ>qz<=J Kݒ9ߏoiA8CMuW.^{Q/YMsޅl~Y/˒# $Jit65Sև M(F:-1z9 EQ(cH&4Cl~\; +bP~lhґ Z#;[ ͊pm/,:%f'4h5H͋G,NC:l؇(5ۙFh +Bj hP3N +dM#/P\p7DHq F +4| gJyk52=c +{n=qXF$_q[V(Ʀ@e}CUz =PitSAfɴ ]MzT4=ۻvMM|)i`XXIc9!r;Iٱ\RfWt*_Q#-`m` 6ъ]W?.HNF:='K,M{=1b|FiLzhRSئxm`uyΦ~#d8֍yhi)Z2(.kTˏ/IcXU]pz:uiP/\b8WerP\bs:L!;ju?֒Qb2~),h|K:-IoX6Vu @)YhjXX, e8 8;Nbߊj"^pCH4\案,Z%:/dXu몐C.:G2J/BlQ GhXbAE@ OD\?`FsK_` ̀<"v=pzQKᕗ/BI4Vt6qL5?P`[SwҔ~PͰ-]%xLQ.-]UBҘq*SE I& +q IqԍzCPt+eBSABOz!yI/D"]R)pQ;Y AFRW  @mHGcc5RH|hv }9KIN0nA-دVH K },x8y)\&I]3\.g& wDD`;aiL; +mR!gotF:U DZؼ:;˾#[_sgb@.L?DCbK43o/]jU&ӒHzﲂ>?=Y+#dK/rx+ ,^k=_,JP4g%hS_%HMxU ഗgfIPIЃJ8\x̀?mq?̺۫x[ul7:2߯T +Bn/\qgXSpҁ ܄[ 4{|?M0"q^F 抌8{^%f'}QiXom=0缊ױ,o=b%zr̀ܶC-S "=ܨ5yhaJ/ҕEbd>A5{tC.rudp 뛨:Q.]ak]ݤU^Wh/Iy`^#(8yr-X06񎖾[`YK"LW*X7k/-%A(` +3@ pa3a.@${.0N {W6Q:4{B8Gyd&.Z!hEdE "<-5)D%jAL/rU ^b9" ߏb`T)o,/r:uDƽvp`0-9 elO%=Q)@DQ9o]2ywz)1q'9I`CX#C45JFklm]N$:'^W ]G#&p6zJl*#??Jwp! wHA ¹sZ'B8x}=HkrDԋpo) '6'Q*J@'r2X + -g{BHkKi&-Ez\ %@O9GӤ}5>>}C=qu0ˁ_wwr6EqTjD;=:7nmai_uVi9Ic|ǜH0QF~T&BqW丹KCCȏ1ѵ6<%;>6Sb#B pf:IΜp}A⇸"#X=9/9Rt D19M=Q\+,w(M@mr՜דșRS$\;I-zkpTqiVZ?|+{ ڱv"@`ZPߤ"[yAt8#H¤HU\{}G\R&aaVYRBNN_jо5KKUo?k) 3a;֩6{"ͽZեo9X}N+)$gϺ:(>ǽWJ odQDe-!ʽ(g&8mW@2zIǸkQemG* {=E8k\a9L J 4JZCf_xxHbפpQl8Kqe@}eO&lc= +fsii1v)\ 'ÿ^KSD'g,pD%CP A:"DRo`g{QuJzj*9]05ԃ i.2 "|\ 6+s=<sT3cM4rHc+Ňnؕ;jdRZnn B[a$ʪTwJToo~cZ;|Qfp998+Cru/F|"P 4lt+ 5Ab|Mj7RRhg!/7|]dr\AQSjo<?mޚXGdUd~N~]rpQb?¬M5ucIw)7f^1yga\oq {Ѵi{c _"[Kw=PC6* +x=1F_CvcRhd-gT'jm$+9/SH_W9ToĂVB +2 /`0@xTJxgTH/ϠLC4}V0 $$kX0\,ZXWk %I!@LwEHP\η>LK٫uv5x|B05Nޏ[4`BDIL9^0t +?dd4d|n:DtN߱q-GZN)HTk) W04JU* fZ 﫻Mֱ8sVu5O5-Dzᣌ(vP9)T6Tx.'/-/w$`ok՝Pv0(0^!uƂi +zWi+&## mQ2  S8=b8 m؅3@r^ŝ]=䒣$:Q׾@^014~]ԃDi l]%'U`0I,#;uG=r%w|0[t@'G*w63OuZpʁhQCW +> ԡ3˭l7I|m +JT ) )F^dEIRU=b9_EJ#C1#EzөqB(Hs9ԢMΔ(%7nzF+qbr6b؂Mλ0 !܊@ɍT:/M +ra5qQ9!J)7ԫHwSe{\g* gIᠺK`f(MH~ KT* e~ ı FӔ@ !Bg^& +ux5x EZ xY#4!A,# |"u5n#7 JAU}r)^[xbU=13e +OJCDA BG8Z;0J(N5䝖9b-'n;Jj׫# ^Fw"ʾ^%SsX)HW=lTS[D9Pt2 D2Ca"јiK&Fg`])qLKVa!%u l){$9.`1b sߥPx!(=ٗ#*`זej1&0Շ"PA:ɘOjgTϱ%BSEےװc#PΌSdlb|?eV&NƣC̽S#(F7YyQ3be={(0JmQ9+_\,UkZptw2l#NNXPcՄ72.Nw;[;)?+aQKHAWO=.fJ Xcy]2)/e !$2֯$gX4P/28 +\Ӎ{U0]n8Sݏ *IT'bS,C#ߢ =lm5t=RO5 [=8xK n\F[Z`(o%o+=|q,Ӏ7~1n:Y 3ڵ;@(݋~U0Fc.6@1UXfa9a(@>p w^oD&bp2ɋ.qc&ˢkZ)INV#MuU7(XÅ#NтH~D{Rr2֠ghjկd(qP 1@N1:m6U[2=닿c)s!ș(rw +4yeMrj5@qhcP/Pj(!C.SI*\d 4oe.PسW,݆ɘ%V=epnWg"2+p 9j\eD!i5ӞVP#z JT -5vE4Lft{V,cnwE* 6l9#SSh9$VXC\b) ۪`Y[/Yu 9YA\šnb'jI h/i/6D@iJC@%Z=VĉæK?{CWC` W\=(G%B[JI;9UzٰH҈[bInh`Mw;ȬyUK͓ ۸p^-.+D|JINw !жSʝuZ\1@yc!M2 +xطh^wCe [= +ɏW)YMDRyD$Ujsn$y.f߱Y/f&`xq-dӫ|sM`\ u +P{''"@l'D m +L"ќ mاEm=NFI +w(!Mj챙}ǜc-~SX@ܯrN9f;ЃPF/[vHlzmj܉`v<´B'Tdz s{b/K~'qwsT!]OL~2#eȓ v"Kb^LOF(wF H-Tԋ<$F؂ĽĴ\Pn)e}S jX2|()F'`2B{ /&O*k\6cͻ-Rpa+6K*lIQ\"2BSV^bi(aϳ% +M\V)!d!B'h|ԍ(B +,MQִ*R4!M,K x !rH<$@ H Li"S4zc9);{ړ]\0?])%{}ZIa9w@j 잤"v* Xl[B}!֦^uGT77hœޙ%ǜ2n QW7)7Ȏ,5mpa\~EOxzOM1gfFL]b$d`ثN[|[3LA5Pkcdh SDʂ6L]PGp rZu"9@^M A!Wuu1EPI7" Hc`Tv2X6&c¸ė-5Gf{%S=AUrKҰ5X-vlrRu*zH +e_iZ0{ +;kAje_) 'E\3P3q7BzJo4K[k)hk3$':oIQ(!r\!:!L[1^x\1@ +M!"(cT|˱H {#K=?Cz~I%Yy„f+K<lyC< Z쁎M+oLjFSWU~!+].TrZaՇJ79+A-Rryf4?!R)S:c'"fllxmBb!بÉe4("rRt.D1rG}$?_uYgKH:sSIpfBV҄5[e!d^L1`6^J̛,n->.=X=e10&Rg>b--`|;`>40VIA(KHݣ+1iڥWWy ++Ib˩;B}Jh;O/ i~eRBdrR8cHA=X$4Y\L>("D7y+.mT>,Hނ<$#ju{"eɷp`a%׌ƜAOD7=X&޹n$O.qb{؃D iRoRl6Cկ @YU.`Qd6`{e""R>AiWsXAm2^D!9jrZ@&QHx|`7%_Jx(z)~,ȭ>vw5{Vv|s8`Q4h[lO[A( dO_ijFA^΃+2݋Mu5]D endstream endobj 26 0 obj <>stream +hFux(cŻ,ыqyh +.GQSC +ѫ!dPPfD2ӍWUЈw[H8K{hzH#0'V%s_DnQ|?$D *#U..yr(mȺּqmvU~WMfd)~u[%ggKe$pKN!p1Wﲩu͎>?ţD^T_ߟߨ>_#Mn1أ#ܤ#0sJ\ +Tct9\\"$H"=lRW| 9 iŃS] lB*.=R|>U=h \<pᤗLxt!\>GF$ H/$b51h3Ov~?`Co9K7-,K"j5w])r#[IC ~DS[Esx#U1tuEg:ԁsEzv` +d] n*T_:Je)CI[uŅeZ5KZ"@R]ZVw{NhA:tW?(r9$ӂ7y^MӖKboMh +v9ٛ+?M)dD_uO瑒90{q8v_m#DdӤf"qdTPv\ud4|*b/K=O769X7+!m)M0%)$Ԭ~xuٮPOz ~c"8h; Y%l&f;G6lI{~hb iyCuKۖ"`{u c:6|*PK!tʧ !2O&tUwa7Cߣ n &QUa*3w$W߯XlmA +i;=&GaށRµ%܊%ރfGjǯ5} Xa@vuS +ᐰ1PWy-_C}7t`TdҘn6#-#*2^Z=qlȿi3砺L!OfA +͍M-8ŐKƍ)I;JΑ}\.=]տE~+4PKUWmpqKAJD'Cu86 ^J'Vq}bH0RVXGJy +{L-,;B/;@=W3^RMKy1Mצ* ۏgGoT/9lFNR E]RWWЯ-S@}Џ(;b!g)YéԜ)t&5?o7ﳐ̘jL%I-,űw,>ʗF+@s-I3iLŎ܂Xmܾpąo<(ry>v>gqwv2P:z,)v\Cn%n&܏A@Ra;6qSu Bo6 _ʤp5}NnpGaYInvA@Φ9_hh7!Hmz($*PP|) MޏHua]o/NS@BmdvK/E) +yu^඗覙 .p0ir0dUzQ`(lY; 1x#p63Y4]0ʤnId̃+23ҟ)*e~j6꯳k)?s3Ik2ih{5~'Z;F#Ě:/ɭ2ЇkAɟ!g'wG[/d[^ 23[-%} ȗHKݰ_ +~)>Ct<8 jΰ(H*ֶv͎vP$G.d~UWKc +|? +oHDiQ`)Y_awxupKTff>">B5 8Ԝ.ݗFcQ>`EjR; Bg1|RmCe!Vodz=$.a)lm#(RRA7s^vۄi/9X +)׋ͬ`MA5}8=  [/4`C* )_K)ч4^a@7Wvܱg\J䙺@a=[m 6Z|pE0X,k2YԞ&Gi\?.;|vX[x= +E1_ :Pv:k;W/$M;~ZF"E K嶄5;vaCTn5WXA] m$hv 95B@\"pm{ldEA{0-%C6GwĞՕ؏:\-Q!P~`x6<QRaD6AηgZ$x0Ym]>p :X2X+y5*Uռb9IcR B&'QtbTj{PQi0 CH]I+Ps= ˫!VYby˓{\7Ce )!9P?ڟ&ܾv4&i.&'aY5*,o2cBC,ޛ_ғ<Òk\хԀّV H6+Lb{*S}G-I"SjztSM2yR=Xa?cbg#/l$ow\crb!I5޽%xL^e(DBJsUGoF2ILn]譒6%'݃\}q$UCυ(RYdKMU"!s|PfBC+UQJi#_7T 'ЎIV :Rm^\ujiKCV+|q$e4aaAY#;pipR=Hh3z-;=YZ+I YK(uڊHd¼١o"&aQ ڑk!@ҟMCT "qpgG.7G2Zz`o ;OK9Apj?19-iP={ 0+|HQwP9o3y`Ҏ wz *xWS;]w(ˠr.zڦ ^+&} OP%q9@IPE,Nרw4硦I1S@v`BbRn%Z@0'эHCaf`XƢ_D*?x{(j~27 121X$3 O*zQBHrPL(Ӑm p |Zo2i(-qgd`=r$&E,ɉQS{%P[(#N7l F!գ bbJ!F携ZW* VŒ#k80\H!Q%8L4ນ\ 47K+헓:P%_ZPLg)YVݸ}o#YN@O5U"w(9=RAdO5zF1n>I-,T!3B76cp<@!5Y{/H4/z}I&b鹯jI-Q2*ôH8KJ}B8= k, Yl"ZCa2Lw[r~"!2nfj)-5HcJg]m{u&恹;Q,fڹ`-~gb+/C&L]r~‘ʕP M܄eSU=CkC{oT\J%U(d!aIn;W/9(W{Kh;x!# #\W(C0Kþ#sYSp@hc"/P۪8.;)&W\CE(l%9 +*sEKV3Q).I/i +|ĥdlVFf6\n=|Gy'45%tX;wnb$(.&:n *r1U"K%!H.^*ݳac?]1N#29NҼ‰"/!ds4unxr A T:@mAv"{.tkuMUP(2~oq؂ 5J:jVgH4xDlSĞQ۸Uسt>%(SIJÍ6O\(&BZaE @3RzĮ")CbJS6ݸ?`*jJJ#䀷 +̻t}7݁ᑯ-xɺsUlw  HX a?=7z-#jnFC,0 +8A`b0G`K/R1)w\Sy>K +bwd~k[ Pq VyAt1$DHR}Pcn⒟j@[3w,)S3-۪ʑ!?qNh@=zP̶ |OgVKa/G-D^ 9~8?ؕO x*L+(&q5X'wU_R:(G(5NJ9Yt@?~il?ǵj`I2XZ>YLoaKPpo&EK{\$鶷Q;<4!^OEF?_)0ՒK"XU~HsEgnpv! Gq nlWµK`n ܠ|Yg[J+0JkP 5{lDM_%]7<صʡ8R߃b #+h\Sbc}ΕN7،Y%n({s.Bݷ2L@}v0 `J%$1fACfP1 Qo$ofk9KR ̠ab'i>eiq>l04k7GȎ~pwh/_.,{sʛ4Y 鰽~vAq~9'`wn%U E%$#mw!q>V вO8WLE (\,?!KdƁ/Y+A OZͨ~([ +XͣJF ePlIHC()PV>}ciuT +ʉ.1&t 'Lɴ-Ety/$Y@`k?R=T~( ,TmѪʯF{Ԭk&5T~+*$upqX+|G-CY"G3w/_d!/PɄr9m'2G +B)1aj^($ !@*%(OGWFL[RM-;=J TD2zh,|y +/P+Hx!v`^cAЀ%P[#Ģg 36:S9Fpa7Xo ' 0ako%m<\C=;ƃ6krƬ 񊬪0C301N<-}|<(RR+~!Ȇ렰"o:5K<$K:0FEBAA}^xÍW|@L`3RÓBG8_v-> ^ H4=3(S$D롪fHI6Nľ_^OOWDvA2ܑlх9!A'*UGqF^{C!b{ϩnRlCI9$րfy3Bl!E)M;@iiD$`S0vr-I@ek2lBs4^%xUBRd,VjP·\$N.K-2Oe-VB (XHBH9_ag8_[5M՞ȤP6ܞe-!xjI m^uMN52Q΋%NB9$>㼬+*jCN/K^IW g( + lO͌(e \l<}K.&e' wqx{`uJh|5ǶTK?6/0]KMrv$1` 7idx +Ri/}R fA噀.sDX0(&uґwMhb,F̻yF s4A:ɥ 1GJC6{ 2ʜ!Ez6Kꏑ:v -͚d*fk/p#0m *GdjF*%({Q4 8Ƶ[k,v* t٦â̧ol^` CBTc|Wā:`ԉ^s0fߗFj(0j!дD420PR&YxOC@jIz4@"i;Iդg-7R(HrI逮 N QOSULlj<%!t_@N$@('7d_;=f&T*%Ca%E%SXE匨LSvn_ImkC?.m. ~X?`Z\O +^t|v%ugK*k8#s tt] +Rl䰊 _CV8Cᤴ:h%qu?__0CQZ$MwV?љւE{їn@ޥyb݈.rDPc3mwܢfT>A)dk0/G{29Wܷ&n= +ZhT"ہLOszqe_Y/  *ɰ-GPQwݾ;9HĪ7 X9DjՔ훹2̓¨I\sl<.Y,w"{ΗS +ؿ\/r] XROjd:4*pxu}TQϢ@א\-G^ bCXBô8L) ;(,\5вw!`o^i,u(eB +ZԂ'"Ԙd n g$̂ȄDP;17-RJYz$1D,~%Fi⑭A]3ܡ?7T/_1$"98*⮴_r{_WK88D@J`+x?gWs\߯YPbCYv~l'mˢ$D]JCR\RN +xmsG"IC%v7-Edဧd$ ʟO|eH%(g9";A}$f;dogDo"{ߐ$ +T Lq?DG६׶(#%}î_UF=jo?K#kELlL@>T@# +1{Ű{I|ҫ1͆Uң J0jΣF!Tk|(Š$RLg͈7 zS=*$u*@ %էiu䡁]ڋ I6(SY)b;w<|3pHW cU0&9~H2FLTLFTZ)c'M{HHm=+u!? $J$"f&G 6>[w? !Q\QE')TX^7pHz;@w[$4Jr{ŷ6wrMk-HE'- )Z+)nRf^Zj5ɠ%bFr:L :if=-Xޛ$NQ`?uu?j֎ $Z,42t`M@zz}U]BeNG1vmVU=ˣoI,4?F"uW?5@ ],='8zcbN'}}YM!u]8SVqF\Z21DQ a^IP; WfX9SʋI1ō`ygǐ=U|{x;gx9铧- +)A&Uv4;+I2\a9N|q I@de]y]VnWI ' ^쾉gtfDZ)Vċ!Lwzln +[gѐT QAf@I(E_+,9=q3K΀87zU_ C1א%9i ʁX+/2뼔'cO]xŘM,*)tqV%&0ij 2qd4?3Ե kPޯL}~34+S`v +ȝYRl4w4% ySG%uz+߁҆W'1 q>n\Qaر'E8◙,G2=x5C"`_qeD~IK"fm)'5}n'5kl#e^tˏJ0ebŧG<.F8(]!3u<yF Ug.FN܆]16RV +@V빃.+H({T;f$[Pi/]&$U׮eU-#c J'v@T2XPviQ4z &=B} ʋ Q﷟M-Il?$A޹.C{T>85^F//Vvl&Tgs&E6 +!]Ԁ!Kn1%>?^.O4ChcƙtOaYÁɦ6et,S.%Bd剅ap~$,ݻdf"aGIP"w-< +Vk/MKBN@"s:T}ܕLS-faG~IGl8д$yi~~r +ݓVN6c{om~اzXf?dT&/R.b_%~5EZ`FSUbp;'q(uɲ,~TTo2MA-$DrX{,\Ҝ8-9}%lCfڙ%}z8:OboG$2]ΐd'A>Ȅjp`aT8b[6|`َMA;> +ET^W?юs]g'P~0qoaGꦀlRA3`IV|Fp,N<J!HshNDjͰRWj:cJEyHt$Ԋ"?ïMe ~3Lb PM{ +E]<5R幈ZSQՖwJXsИe|[cI(X tJ5'CAfPP [l, 3a,C Q| A9'%6-C0lM!YQ}QM7vU׳xcX}u9IbKNtոTW+A}áA2TA* ,Pbf`w/r֪̒\/B++ꥃځ$,[rR#xlIk)gmӞ |6 O#~ 8CGf&X*i-Hb4{fLM3H|zij3ّ1CaT-'DnǛ|c"][+?2N`HOt ץ3ќʆk4%MB*7֛} Jpy'jyn$sf*JQ a!jz1n r@3 &H q@$@3ULiS3ڗ/um_K-ݟXi{ +]+nah2A8t$[en2 BK%.kg(PIӁn*_xEұ W )?uɶCZdJu|^(8fQwGO%bR 2Qe'4)Chv,ě/C( C mbp @"~J%EMHctKTxhxN`dC +Hpٽ QW`Kع~¥ƾj!=fZDKOLY88ntqZ9m-knj^%L_vH) Rv$~_ʗ ׀ޖT@KL_ho/<%XE> f埭[M)b]LnC@ 7"AdžbqXjv|['6i1"qvQK+2˦ +BV +40[ J# 0 .t:ܫ)s Jt1&}ZuIfD*K)ME4>b&/*'☣lX
s.5pu;Y1R[y\{dV\0- 1:MPma~{Mo Y_`V$mɇp ++f]uײc ,"tPYM]Yʬn@y_:ph{]1J`cC!ֽ+J5ʒ{62@+`F;x@ۙ+6"!U@eԲdw +.;m^K:VJev|8{iA=_̣jxD,j)[KeIG67L-L3nޑBr`&SRW>`bnb'ThZq:cJ])(-PtIqn_Ot:DX)pMLF83iVj(oj}"qBL"6(+)V.aDW(h^TIB JډO~bcng#ܔrhT~]j|A({ڭ #Nb*.w2ETͨy<ɳnNbĊNmOVj=w֞9KUu+hjʅ`J⬆yUR@E$V<|nV/•Ϋ\Xu2[O8=@uiɆ}Vjp'RCRFXkﰠ"m%IQ-W("Qa +=G}:bEWa"n#2+5x!Kxfs?r9#:ň*Bʴ~0Ywlazx d<f[k9YV!F 8nq:@BogwT/ovsAK ʣ*mH& T>/FR|0bH{:Wߵry9S"s%/:()uK*dtg-W&=W`}̭r ܦҥ| 8J( ̏$R +$u,u^]yB}jB(b!Tn[} xBz5zX.ϸ* +CNőudY~"A܎[]iGدs:Dwأ &[eͶ@|=>h<;8^W0]ቼ9h(Qm()9 BEut&OCrDp_kF&L/I6S4eDz-+nK;o[)N3)o?Oo7o?7?~W~?wo_ =~'w?t}O_?n7ww?[j~|lS~ x͇WW귿Sο?ovʻ̑#㷿m_\oRq+/u{ko_W_6Ibq"~{!yYb 퇓L9o_$pw rIXZEYu%d;D!KG Zį Qû{^_}! ;=SE#*j|э7q|ظ||~su`HsXq*dyr#d ~ +wGJ?uBqʛ~pRqNtsx`3wD9~FWϗ|X%H )8,*eZ=/'` ^ |WC+C4ƙ 58A8-?=eh pv{jJ4x~'􍞍D<_/b9وN4ӈ[ st:<&3>Ύ+ GZ< +2ʖ?O+h\IiΩ|D4q ĝz En. Fˢq8X7y_ Qx^xb+U9+=#-uij^ +NPO5Ϗw'xyibYqiTUNSW [6^> k|ncw^A=;~f<8Wk'1{`jqĸrN %Kgh1C[ _ @U>0٫zvdU/55daL373Zk;ss;I?}ICco|>b 9<+9 {QU+LDi1֕8o~ξ1()w^A`ިE^k\"ƙOA I:o=!'1 5rN< +HٯUO*P\A1&12\3H=X8i+x)ΉFE[#*M& '8sŁ3h( _7`+3L76s>s\?)zXqa ~W]^}bm6_m{%q8CWϛw(;_w!T4kFعz7tsyi U||^_1c8|e79yL%zs#'qS4"2v(_ +ԝ<'ڮ◰;F8"xHD#hl2cѲohDcobfSkzij'#P_~- j)Y{Uc_?.׻95]9pz/aM4fј>l'YZNWl n9TpǷ &kjmB9mnbcl7;UF9|PhuA#)ϙJ?gs=ƹvD}'%WnV$[h9.7K73iO+4W+rNGNHŧ%z\ֽ\g >}DJ!oaBܾeEh^ y)϶ʟKcȠdg}|%ϔZ(4휠?DdPN^s;>2N$˒["ª +p=k}+ɸ|\@zyظgA)9a;)ۻ:2=zyDL]2,vO(R;A:Cɷ6X4^H_2|1;J j>ypc.o$lONPy邲3crx3{Oc ly/WS (@dڟ/R&V5VFDU+bIgX=x_9Zjفb~LNPW_$lFd/ +˵w<Ӳxv}ˍٜ#yVmą P"n#{yEL5q~yYk[Wl'Z։FLJAԒnrc;  LJ}/ o :^FMgcN4QŤu Z8YryLEWR6"쨈O{u裝v{sWzYcjGXep4SF;\Nl (l|V;.kn,R\h#$(|HgW}G'oyZ)g:kW{[FI)_"N ]9 u-f|6.,ˏ##kq.9/,CS̬S`؞}0jMAK)Ol<)೾ )O}`F㨙.( Y#f>1`?_؅Iگ^Y9аcOQߥl_!+O سc|2Tc\]40Yj%'w;6~4`AP':nZ7's4 oF{k kE.@Qys^9zoyhfSoQŬ}YKɾbFEۈ)NrSvO3hgnH0/|x9@try9g/4ڍ̜j PU8H3 +"w*iiVk.0x^iyȭ-?+<IW ֿ udew]q[/йfc;1o}v6%>69dN(<V{'n7{?gsԅGE˷<$fGg3}Ŵg@9끷wqs20%{w ެZb09cV-.(?tVghgKT!l}i9`PxR,w*;:+#y fF+y#X NNsͧp+Ϲ9|Z<\$UN4A +E!jYES̩2} |FvL4=b :Hrn<]½+u/ZS0YFZDQ$i8MMjq_Q}QO3S O ߍQB30\R n(23 W9\!ٕ-͑''Fz]}9^տSA6'"#|2EĉM Oj'gE*S/AK$ s-u]gA3O\IchHzWc@X,, u?_1\jL +I뙪{}q%ڕtu2 d}ؕ29ۙ E=\{բmmfX=AC_jG^W~r'/F=MOLyb1Ȭ+skM*CaO姜p9}(g5WR͚+Y65nq"QQ x61Ո|JN=*U3{]~K&֥ḽTc04#y۽1+* f>r\×8՜F>RǞC^{ԨOw{yaRȑ?@9hZjvqW't[w#ժ'rC7.~k;f6g:A ΓBVoD=d:6sF98"%D.p?D\ +]Ɖ#e[3.C|I:Ʈ+{*h泍ƽQ8th;״6E|PYON䏽ڕC~y+ZY]m4WΗyϼvzwkƔ됕ƉhȀI 2xGh\'i9UvrOj^4]zw8uZyH}hHa_sU6aq'+\'x?^l_'ONWlɵ5#XrB9ih~h;Nq1HcɞP^+dv6$m}ngD~W"l/0'Oczʅ|$5?4+! :)I,#",s=n\+aW,ܫHcgOeZ>=PD5j)ܠOԺѳא~=TCKz +}y·8A + P܋EΠo=_ש-@ +ٶg˙ IS,9U;(OkYN)2w,lA9ܘ;1NPj\s'fAcs̼HuhM:9oL)A=._g}sIm3K$lLSBi<2=|&~>|6TI_˾9U3D3xwBA8T!c 8HrYgx'ȒCwhl9fkJn+?8T!dt G'xmLJ߄un5+$I.! JN|sإ;'י<",d*?nwוdW䥮@IY=SW*~V3U*V ^~™2E. '*]#V+N0F6gV&g Ȱd]\tN%#2ΚJ~I +/bH렽ƻhH o 'ȓH}}MۜąF` rRdJ4bCWT8 n|YpvE/p, GCu͚[Yя9Uצy]OQPߣ,_29I9=iNk Npbe r"^'Z/vئ Q, +\g'H(t'yo +/z_ +A{LXQ'xr^{E?6f}*nqZJ^؍xa  |r'YVߣ3G}#>gBIh*}۩(KK!U5qGWő4o{.FrqEF].~nwg=@ٞ߯@RZjU,|UBL^:xU[qH__xc9id"dMBF)F}=*A@?(Sc- "*S}U5jQI25ā*&f"kRn,lӇb6P0fj`>@(5 * +~Wf*Oz@fߧ +O IH _7pZpZh) G_JW7NЫfd~:8;X;L4d2+ +LK^„_VPL;XU$Fը)bս 4M=8D|XK*v fP( J&0cUQdkZDb*fPPGG |jQTi5UL#`HhBBx*2x_}ʄ #U23o /w"/f&%sՍYCxS58F(Xg*KU6 (lõTTW . n6|@M2vBTLQ4zv +TW9a&bh( +3&S/㭎Û75$DfVi FJeB 蘚è5jd-Jn͸QFFRME]Z +ex U9 J +h'PUɍLj,rtu5Hkh F 5h5D苾T%-S3S5k`5ve*mIlR9Iwv3hоf q/՗]ƒT`WyMՁpLP_k*bGa]8AZ6iz&Qo&̔ %UFPGIQo ʽp/!/S5LT(' e* 2T2S~Ѓ$Tc\Bi +EhJ ! +,[+z.*k[Ruؾ-̭>T:a+YpH d + F}K-?=q]jR`5)tF33C͔n ʣ(XQjl,\+SF_ƚ1|Ш+o'eve`"RTsvezFaߪۨ:0cpMf+L]E>*؎u[eb(rYF!,E3]δj/Q|#Fd⪌j8b)^UVt1& +jjbҗo~0R\Hc.M|^hNdF8\}3HbY$j P4ZC3bQ1M57>AmJE$K4ePUK#-S5 +)@ß;Дǀfqqje :T[̠Q8}@]P[MkMRoi nXP*Ə؁NDk w"a,kjG*pPz}}B.<+B,RS+ XLjNJdh;1D(Ǘd*K' Ȍ 4Lq1*:h" w6C#Z31L_(R(3!V4GakM,gXtԆ7Im F]4&}MT oZ*dh,V."JPLT¬oTfFMe7 獪53AOV6*qҾB؀f 2Uh43KUs5jeB'hD.Dt|h:MdgUzP^ #ԵXEoYZHKM?TqbJkuڨSO#@su)]h4ƌBH 8!.ŝWxUeGv&}Pc!j U_j"W׫˪kM]KbyOo(ȝ-7Ԝڢbth>(T!IOJbj;)5]"WL 5S#"p@?fX .;\bR"4DEjMJMl c.13c?(ri&⻙諪Iy$lH•BBJ +܁N-y{+5ybۯn4EɮrPTO`/^+s0 J=Vb5Չ:xMZ ◉F7c1rPW[6S: oX_fj`*l+>0եLUUDui,V3PRj7i XD#7r,W>| Raȵ,TIdLF#S#qbwQ_R3d* D:|o)DC0cB'QK)˚bmj3[%VxR>e8S h6GHŚ3&j+5*1U:OۧvPTb,͚R6ҭZ柺$n,VѓKY}Xoa VF+fm45F3 /lV'^.>mЁz}^A]3FA=/uiH]D@1QCՈC+5\Z_VZȋثQ,m R4QR?UVjd"tWMl&aϫ:Eoz.nvt.`7XdY曑H5JHSoÍFu<ʸQ@¾zu+3ygbZB9f1TepU+F?dMs\eo&d )ԁ!k5447)LS/>O* S;5vS_7WhYlgNJ/U 妿cx۟NqfuNJFO EDXEu^'`pLR'j֔YA]*0B G,, +<";U.'UE6U vڤ9鏵܄󐄍,Z5xZLфXPXRG j*U,Uo&$jND|CaCDn \PPal"f5RO}Q2SUVH':kՇbu }\cm]5 +%!j%mi ͌LjPa:UQ)̇E<̇e=Cn!/Nf="1Ssfl #>&w\C#١Cў9ĺ|JpoyIJa>4k,=$ W8 +G_]Uu&u/$)$3SIL`p9\\d>~;L#2#A oOIΆ"NŇN3,ds.C3^1C( \B4.n2KmÅgRG2ICA6lJtS7TtLD6(g48ԆGsrFπ9a<'S^a||t:(m$2 "y#XǚՖ3h.uc͸kmHl$Vr~r61p +AkޡB)fnZ:cC.:H X*Y.~{ :x 0Ȇ׆{ t@#~=Ϗ{  o!OhSaY!rWۆCzGuO…Gs^QC8A|h8exx!?ޟLGg2e3Q"yC3Cr){6d҉LK|b4>,{5RLlh^?L`t(< Cxx&8k4%.yL6"&g,^MjB}M<sSo*b&n*?dEr6 bz,:R>}6U?U4k%%?2à6HmzXX+7{g S}*}j+l 8 ]P~֤}? Q6P{@-l s曁8@GuH~}S1ѥQ4W)TNoNg˦dž#uiKa\oPxɇ5;t I e.y6K3@+v9F1ly „qS<#܃׆,D:*b?7`;C>x"p\Xx.,c < 6dtHf.:Pt$0lXh>aX +K&$k4 tUJ .f<'-Q6䐏>I3l<:s<:TytT$:at&\ǦM7MerF.YHɒ3\ڮ9l$h,|O#4*@IkDMlzM2d68g4M8L=t@eLZcHn"dz>IHH`l(7Ⱦh33\zC]؆~ Yy݌*8,$r.`Dz>tTo«FWp klxx6bR6éȢq Z\ropЩ; Lm& 4-n9Dt% +,۾Y#[nV5_r}lꯋ)؏ +2WLyt@2yX:c{&ml*B:}]9.@EŠR_Dd;ZRzjɃ1_0kXk`R!'S"10; , +X_dc0yc{V`>D4{_)j{& +N,b맂|bܨ:p5!'٭ Gxv`~ |LȾ0ȾƘ2<]48,p CCg<oa{]DdCXSl]7l-35&rM9ÄeϠa;gC::q{)hgH!{G9"WIe~ڂ dž}J#K!΁lցSXc=$t@H'g6(kq®³=iCܑMG\` 6X_`P$YhS|H{(}u~:dP;Zrh- ^n!l ;iac |ֶ qjӮhm{Oݢ2ap6q$4GRw&k{ ^_b'*:ޙwcM33* ]_qr'+Gw!K4 Aa|`b|8|(KW nQ6U0oP't0O."} &8i; +k#ԟDؔʇ l3E1g#^<Zd}=Ekn]pt)5A=裢sFT'Vǫ)7!Q!y#51鉮] ~!kCGb\]x`&03pt $w`Xk XǢ߇.lXQ. j{!UKN;kqAoHCκjä[=Il΄Gki=t@ +qK[O#NAQHv"#yn#c,Z atC:;a\`hFށl4hXW%OK蒳23"KlQ}Q|ˈ)x "\){ߑQi#?a +ߨǃ[=)jMb>Cx yH@-:Xe,멣illyLk!7G .p_%!Y ̧V`3[璥eĶ'+_  Μx[KE PE `'^%+ЅʉIȁlv=Y&yS?Sac ftL}* +4F]*=)Ej>K_0I0>F6B:슱| 7U|)wO`s"2v$)L ~-? :0 ||#AF"=9L^1OF>it%ljLN7(K!1 +THH KttRt̟wcK6=& 0_`' Ɇ˩Tb4Ld$举2yc:u *8o4l^9)z;+ooҋ&>sK£K ii$Z-/a$ox53 |v&wuTX805ȒLjt. *sɂSK# s;Q1֔2 +|z`l |X/$H߂L8<%bĄmj55Wt%ws d81[2(=O~=HIYGN"w@;$qNG!?9Ħ8$\ph> +|b y> {4x%N~p} A5_ | hL2wys ?tM陜#@'UWLR9AF?P >'KD%cEM3*\7w84]ވ{';$\Z,)x%ٔ'M Et:W}w%[`vh>` #r5kT1]?SemD3Ll|-Ͼ:`%S3b=К A)<&q I( 5Im_%T ܣ 1oC\>;{2'>"_ -a.tXT|K,ɣp| #ȉm\\_G82;|3Y;1iۿ~&oga[2T fd9sGŝa} _Hڄg|0_Ll0:'!y`r/fvʧaΤψ:O0Uxl)Y~Έ,8|\ Rc+'CLIBeźԽhY0b{`bgI x 1)&6;>OR l~:{c,8z߈!v ~5OLL؁^ c5+U~g3Ph٦3y^;VՈc>Wgl9omVwF^uۘLc{2NM ؆-8O_6o/~ :'tq)Dٔz=L\C+#,{B؇>PgzOP+*ܴ@r z0`AG <(u!@;!ǻsgC HwqH^<]trF-z.zlR46񈬝HbKd L`XqQV[BܓFv֓cp{^O؇W5N=>@8b; I ̡7LNA,֥;F@\ o8X3*=aHY^CσG*=pż-4u4Z2j ۏx%'2W )g Cy) ݔcI稁ce Nɻ#E|Im3!V\@V^7%ko|M]FU}rf[0!oyRS_c ^(>o37 ;s#~gZm  x).J8?1 wćN6א"Sn9O`.!=m-r~re6CF|ŏ9&%̰ΛJ!Gy(rH9 &'s8ँr +JAl)? O~9'=Jyk@)|o9> p!UBe~s >|=릵~)YFoK6n$kqC<,riH3IGDv{C쳏ΟI5zG=L5[+ 1e鍳;G6gdˋdRtX't91"nQ5 J>ZT\TRC;)53 kHPhIm}FP.h.vn뮅.Ą'n~MW\1>@`Q6K7j|ڜhl/dۋDӣ"| H_yE〗:_>?qΡytCI>,.cOQx!CF#A>97uk,$+VlXg%q<p'poQxhh8aָ|"!|Aiĥݹƛ 26{W1x[z۳ duTE 3 +fy \|蚩\%L\`(٠D@ š|tdr7_P[=uqK@Er,U XuV5cϛ(`>B)\jLC+OS{,a~){/ko.o[3 wVgA7PswArB [3DWl :tZ,1aI#s !k/=g4. `lSgStʈڷ`va+0uwVэ7W,`6~ +wDE}*2"ͧ +PY @ +]yr@Α2r<9r:Ta*(8}G|02G5=xG|+|uڷ*Ύww_n >{/j*=|tx(:o|jWfLM:6I,9 ekolg?o +:j^G^1fZ3}U: 0q<)T!.Dpn#B +y㍯ĶD@H2t,yz`:|^\RtS1U~l  Oe +醻+54v UxZJ?(>42O,cVT\1_-֤l*Ϡn: y p#Kت٦udErGbq$vo'ax7+`-gPaa5/8p@H*jϛMW#%c(>:'yfGT]2!PrGW0av^IiCLRfr.B'"R1 l>Z`' `b l6>_Ŝ6uU{}Www-[w5>_trfgZ?"aP V(k GHn[rw!.:i@\@r1tT,0 A.@7Zzٻ2uS3.0dD8 ]1ȖbumdVߓqт|X} 4s7DϤ=%+pΑAx C3'z2+jҩ\֮K2˜du[9<[sB!`c͝g$aW8>Y$S{@J\V8Upt1ؤzm3K)jn{fe ')KW^2񟄚i%nFdb@D9ς|#0هQ53w/9}v&^ʹ̤kو3)zp@kQ Y1>8H?1! 1?%}c3t mi1Z>D`0Kfm Flaq*bts%\ܽN;0 M gF8[k6-p,~Wx @vAo@TxwU;šg7GS|/BmA~Y1x*RۧI +|=[fL9FkocgĊ w&jnن/XĤ_8/4 ,hD 4csGn8W|ΤFMfGs%Wl.Fp?bS s;cۏGw!{ +u~4*G|K6>3Vx VW@.OP:q*UxސekAax ұ!7pE叧c&A^ +]p@5egK&DnƴfC'>/[Qi3Z{YpÌi}l}{ cKȊD2= qTďͦlyh!]tl)6=YEzdIh yw!ܽuCh~hz*!q9Te)Z6HlzmLu| 8< q)פxJ\CqwdۗdyG81Gn=:iL*|E9/`񊠢QDtD2atEMbw[lvF~[~%J}O;>oT}^M~XC{eÝvxMQ-ѕWM~ ƌޏd}NӍ];٭ͷM7٪+@ʘZ<+{߀?# 7HM0D؞n{jj~jx&!ko\s2ySwʛ/M'l g9ٶ[@θ̽ /z ~ҩgE'׍-W+l{hԳR[xF+j~rrM4=[#7k#:\a;y͓Ne</NPP^A:mrv7{ˎ>" +oWLZF}I:=;;d '++SW̸;Ozk9Uu+6jxR^zYlYf{讣{Nd/kɂKlʑyl%pL+E`)?A1PCo'C%zCsHƷM%wLWiYPˉsK'/,[mn:O~b^?O!/S޾aμu&O|`S9CO^9([:5|Tcj{y% +N.,Xm@Oײ'WKGO Hm:)Y`3$oA#})MyЧmeO>pa>ufw=#k_[^.w?>(4,~BdE`ExF_?hwZ{Zyo^@ sT ck'eڟlnQ${0TKZ{9>eP4?_h}fw=6+Dɂ>4qOƕ=r+でs;ֳ>#_6ˏEq'>E~g~aOw~lw7ஒѳj{.>oxknzF`kvtU[t9!j{Rm_k[[%˷?!Iǜm{n}mNg}(OgK^3 y59|zM+5ͬuY^UXwzW(~.eg0ݧ$ݯ ko?S}xǾ~|0RקgkwS8r䏿leοv=P{x+w0C|zinP=VpW^n{J뗒߈tzY*߮F";Go{aBc.jTyuk:o>_>e>?Ϣ|~E..p3I{OOsy[i/Ͻwy~R\Yw6yEO_ǏNV*?,v];G:w=b~b= otu\Hcrs/_n>DxNjpÏx<[W_dm]ihX"sa>LUU|Q`dpr'#mn͍SY~g7(LsGcYR1FhcO?r=w;Xy'#[}RqM{^xÝ}͝x~Aq͊c)dsO;ww3_])q~ɮ:חޝd?Ieov??{۽\lӽ=A몃Ww5Ty_F`Noo l~n]CGBU&t@WK6?̨(~XWSNl z߲Y9׳rd>X M2yu='/^w}wXl>ӿLY߳gGWŽ,O7wMq-.Zn>ۻ֢rثOVl9Mu7un\K%&pzяYnOW;>;Qp]PwȮҪ{eKގ/?›]!W:".މ,Vp^XMwËn܈*>z3fBY*G͕(`?tf;:Qx_~6'zkCՆ:edI5J27_o󘗏Sϓ Ϸ oe)({{z2?m+ K;rf4mw|y{%JW]^\RYp/`GtEVx < SQ%GкkPVByԻ9-n2E7]rGor.ݸdJk%.?yUwn9w+l)9w6?RʜOs&?NeĠF'{^A|ݻk%/ _)._Uy?q[zAۋ5IݹǗ.ρ%ߣy:w+쵸Sb܈-\lgq;Uy#ԋq?(}/ +F4$Y*nVwV_K̹]G|ocuW+Oy2kVl\ W}}ᝌzwlćq/nw7?۫kSAG{y;}mq~y~t;}+0~a+|ݍ,s/dQ%W5=py}u^7we[oqvOtTͨTz%,nYÆ2=7^*tϻr'Y=9C2&Z*=STY?©_;\^+wxwYUGH}p>ލ[~m>1}Kxh[ n%޾x+^b󫽥V vӛ^n^~^}/c/V HHQsǽܦ{]뉰ב`]̽Ʋu"ws]g샻 GZ:J"VT~r\IjOW).vx}ՑYMuO*J#_a^w!yJ=UURd2{W?E/~J>m*1 zWX͛n^kXz3īdNȎ]/bݞIWwR|%G.%ᅵPzr\鶫N/}wk֝쪓WcK/\.jG:!y +u+qoA./o[y;ģ#"Sn岿{hRo<ݻ|Ktuګ$+%F_}'16[)14^!7VxDBLoMԉQk{k,*v.|Ϗeʫg7_L.-SOW[M;ŮOT!\TSx3rjն;nR8ϦGkFwz-}ݣ[Fn=8~cU=?o8?2c_W>M2Kod/%zgw'&"*$>#п&HJIfѓ,5|gW<5]gso_e3]שg9nVuV\N-k\RR)ҋeSʿ[ː>.=})Njq7J,/Zʷ |X埽/V%8[.W$sI-'Yc淋!exֱk"gOד Jt轴$Z렖i㶡iK YD wWJV.+YZq!jZYmSʶ_H.;x>̅ңK!ZŗWvfTqݜO3Ny\QCOB ++*ֿ򭭻dޤyHGgJ ''ZmJaF%LHD(Y1{crxⅸIeWː)>{%(N^)A6_-9p5ZJӶ29?b;ĕXkuSn a ~ a"~m_?v}߻n0oZ ,z2n͞[>/ޤ*sx|Ϊ{EmޓD%"( Q9CUb2DI3JT$&0ۆVYQn[y}κsLsyyS#59b1l ⦳ȸ/=q1W]įJX/B 1 }9~E߯وh4# ,;E+sV4<_|b~ -'|bA峅.4nVP0T۲yƦCӰo0sI㜰ؽss꣈ wmN\ b?aK?AZP_s)F4A^u%ν _4n9[sڎa6BV;bď?߃gM.B(2{=\u3\'?MpeÄ=/nq)!GW=\F޶r]otRAӶMUJ4̭/O?:t~Smo$9Qhtᔊ*:V"#y3 [\,vba//}!v}#< +jq|ohSAw=kiL~.g6ѰB8`dcDd5Ҝ 5ll4 \Wr(^yKo6+ +p&cOa_ٍw`zzvóip͙!£i|hel+?SGNEc qd 2̌!Syt\d7>=3g4eA8rڭ6 ؊^ ёOTQ %p{qŒ_]+y{xǷ +~n;s;r-,? O2ײFFQx!n}D4FLt쐩4vds?sPd#BYEcWvJ4}IVL?sEd [ c \!ye眩D(0aY,m2['I.9hj@%t'vJ}Y5(g^OKԵr]]8ܦ8 ^iz9vNˡ ׻_Y/ =D*S sd= 8l8bL}<2ױ\7푥J4q2lƠi+45M\7 VRmW_ZbIa轰}9w [8%]ϗ]5[럴^u£[YdR2T r,$8ziXg} 2לDlcmf&z6Do1z&23Yۅ 9hX4eEP&T!V&>ptI½͵.n:W yunf}7evݺy>7)v d8)C 0՘G%cenmv8aMb, q|2&:9XX屁c`jHX<$8-6Iu˚l9sill~vۇ]?<|s'P%5x?D6Kq\p?YOpFy8!WBfc}&gxX,ǭDVNhdΠ {#FsWS_Pxkg' {rIXZ~xxigjn_(z)&//#K(ՋWBMaƫ&m:$;/K,'Q%SGhC:C&O\4y qKpN)Bf3h<9朄T9L+_ƂWK>9V~-|"Eow%Qo{9#qOvy~Skm!^oPsֆ-2jۯ7z~ks&c@bhdNGA_jYd!36aAg";ϵȖ@#4ʯh/ȹi'5ZTzKwQ3E\8!a}S33a {) +zr88wλEĈ = ާ<}GU|]1>cDwdn+F]֢9T)MZ0fC}ժ3x|\K-zh8z\y%W5-ۣ"pes疟ƻ/B׏)܏W|WM+Ƨ-Wmÿo_>[ʏ!?V:q/Zqyw +*:)4L5!0ӌGN¹4Z& +F6hG^ќh=vGhb-Ԗ$tU]qWpy(BɍjVʤ?.L믳}^+ +bM !'9&w_U1>KnJٝ_p߼(.chpÖ⡚fΏ&[/R6{yDo +\. D <UhYlҺkF. 3=~$b:eOd!iz߅5"/'!uk!ثx߸zmK0qޞB }~˽mQ&?`G1Zh[E;XF06Mƾ@(& d~#Ċn \iKN\|J^:ݷtK!o֒m?x1-7SgSg3ʏ]S(o] +yt?Mэr`/e=eCRU-t_1i ':z4@56_&$:+*9mף>vNjOdn$D梄՚uEy%~Ml +>'%ܓC%r)Ԟ_e5L-7rŹqܙag?ٝʕ;P'u&M_I?8f̞+CK +53N $B +1??,þ{C'Ox|x䭗ɵw?m +{ChH}v~7Ƿ炓j4z_|R 7b" J !JAt@?ۊisTեd ی'I$FktR qmB4AԜތs>-v}$u"o*B>{̃w#sSW?+<)R>}PP:|}RJY%)n6WVa}в =爐{dmc i\]WKTGR/$Ԯoܨ֛&&76SMvl 4a+훈'0[jX p~^|_?G-o`*E`Q1a+v~0irbc8
 s0^qQd;?':&G K-Y;ڃzfU)}^~ionsv?З>F0ަ=Un/XzջcFx^pkOv'm۟-~\":"20hr'4zZ`id^(4~n䆜1eO,̕ъlHG#dٕTi'"GasVFo+ іhɢHע]xJJn̓T*8t;J qoy7Ln (aR`UQ{QҚ33̞tJ 9cdWxz#++)!2U:Sf)m4_zqtE{mQ;ߺG^r_d_IlASyޚ竂n^S^^;UPYz>Exl:c2Tc2 +1F&#q=Py"wiS[~y*93'Kn6r[ĒDz4ʿ*OHhYh̅(+4Sr#sMBVe֌;//}GԱ$ݯWzf+쫕Cr;zI:ͩ,&~2f?jshd7a-`ץ~{m^(< g':#m>eퟐ퉖Μ\(lR>hhG"dMlv%-]})̩4g +1˨+L^d!þPyvlӥo[#S'+.}k2gKҽuƲ/mŗ ȋO =O'L=:#ya;! Ia̼|?>'wtrY 3u=/b禎lmXBГ.yJ6^4vC}40d۟9GUJ'o22635GgBqpOD/nEɏ=Q*> va_u~{>$KCґrrYң?^sЀWB7B͏jZ%Bb% +PiHRG +WDNi=\6̢=<r"Ua!I=SBg@opxq_Z~kWYN]c;'YTj4J_7'g졷 uٰM{'SelŇ~u Ӫy4(e +(;e&Ӂ/ZLd5KT[bb;z@KOZnZTkog#=f|i +Sk%aZ4M1FϿhG蔊Ѳ]8ü @3tDMUJ担|3$뽻 4 T*o\LnXo"zA}퓝/w `l:=9X.'u?]}h:]kJ嵎dw(zm(}*M݃WUdju?q,XyU<u\ uru\}EDX$ V1vhyD/RCR&kguOyl(Lqz~,ӌj2_]r`2ĊXu +۪PšJzp s^+:c q` +hR=Oq qdI>+iVYB/rcӨ`TW*]Aa>"%S5t#Gf:}]L2jgj3WOa;dzi[L#zCy$;^L{b|Ͼ .|E{/Ճ>^.W*Dϟ QՀ-zSݾR1ÜywBOim>2?G~,DoЖ٠VHӖk:\HxlTr!' TX'*Rʌ 3*:C KRu@+,V|`}Hӫ}=AאI2⳷3}]'E]=U}Ӄ̎Ѓ4w``zg15g8AS"$ZH.-//du-ù펽oi+$UjeTe^de6nD5.bd>{!eν +a/OfpLtv[IctDt߃A61 >,2 /k;>:4dOOI;AԲJ +I;IO%d :L]]@7:8*lJ3SD БsʨtUAdmph=6bNL>^twbw?>Wj- ҽO{KO%o%~pOc䗞O?]}-~neՏjϔMzOQ uFTj1~*񚽬T:BZvd,99̱G+p<%8HUDd'dbSGf*K7ٺCv̮<|jh.1S/nOeC7t7VKfC)Wr` +6̭lf9I6łhl`ŋvLds[,dF:!SN(#z;ے)[5ρ>|ШԆ>i*૾C'Rj Lq0_߹Jvqv}pu?[m<0 A{ +k=D kXS<]/CeJt{gf@w↬B;j޲  XW7Ug<[9~o)􏃞?hGJxiPڀ +B*h' #.b4o2hSrE}4MKcGJkJ5A94U3/}މ=Jq]Z(}GG|cpl֋>Mw L땪W>™tc9\&i,޷~ЋKWoI,Rƅ7Q=dGR!pfk}Y{1FdV& f)\%AS/]&4 +t4h{Hwum3/hu2C&˒)q,LVٔj4q}وIb̬5 %4n*Jw^:W5jpzb./@cM).̾OÏܾR@3NP^[s @{ 4GOie0g@ÌLR>Wny [ @l`AHA]&U[;.qr_yDhE&81z}B&@!6Z _ +MW:$IdXӆhǍ|YD+t|S㧓C߈{= +<ܑңD˛C^^^9*QB_>d _̑D톃/K֔tu峖 o\kGLP&k° ClX6hO jFb@+ +%ϦךkRF@V~v[zF <ХM%B sGxǏL!аgpNY<6$P[,xMm4@j}hi|.zluŞ/ϯ8Ezо!ц?樬=rg Ч5<2!ؗ`Rp|$U*: j?Yr?tIӴD o\zq.sHy4oe!j Ӡ_ +tXRCi5LAnH?M '{^'^ѧdGtq͆&js@ Ult6ԱWf00W_q4𺁯Aw\wd{Bnl :媓wbh qw]Al}Y-Gsms{9cLb!꾸 UVvIOO!]1H18YzǁIPu& Y\b^hcȵ_Z8o|.5`lեlUԘ\r0(Z*&@Yyh6hm*vߓлd֕Bc5 tdYdwaXԙh1F1<+U/:Vp``- ؝_{)7lv86ے5_=w_z*)1]W Q^mL-Cу oYLbk7(D/'SzdSsTlي3'#NNQ\-ѕ==|i.X`}: +w`{_yc*poNXhD&hA\$ܬUFJw0ҁyHW8MYߗkY7cHW lY!ccA[Pl є M戦m>T޴k +H- n̨cK h甠1To|4ĵr#/714.?l<x$zvgq&, g(udpR%CG\iUBXmkl +†'}@lv̍16N]m X6Ԍ|Z^=Edt'|m`gU; wqwv['y \:+(T eVms k18FDKGQk4D" -P6xuAYm K8+{"ȑ^sb=>zXچ,0AwA;ЬhAgx80>T1yR$d }a}Kv +E3I.0C4YcEJ`gEdTewMמWLSxFtnWf8P̬02 +YqG=?? +4lieFM}ӂ:}B uSy*?ux! ME4mX0[_c 1YTVGh sqGoЖ2ja;$uţ=HbLtx& D(yes.!?w/Βr +5Ov$X#( +Pw /#Q:ty|dH&|q+lB{»זk>OVh8 |N^&aMdv7V +Z\J1_f\]gIa PՃ}#U; [7׊YiIXN8f; { R`gAbgV YK?Y,c5!Rj)0wA_r – gB; z~>񊒮ɠM}k4_<Tg< tLXLcvw}+Y`<NX`Zo1m Dz x$#r$"ՠv>0rI> P A*JCYi*6 8P)ĝv@Xe=)7Wwv!恝{% u`g16`+W(R +GŦGOf8~ do +0F΁k>kz3U^50*[}ȩ@ 7uLf!FB;96η]T9 1yzAYeu[s_.\Lvr"=ӨŶt$쬜V`gi\T$sy;@7&,%𓁝U8R;+eATr^`m}oo@N,0ejV uFՀK9"`jdXx<='^? Bv൯2RQH~$-G#wB զ7x]X4f 6 zlt*[-mKJy'ŵ ^Kߊԙ̏@#=6]GOvV:*7G^{^\6Z* QmW̬3;{JPr![z ,|}:2g$*M.; ֗9@J*) +X^fm;K^l`WsaqX7QwZ 9'ܘvk]rqUtp"wb9':pbuH\R!BJ!a=KAStL 15م5O\4X/am}ChK`mVwJۈPT+ǵ*0p`}s)\m<̡1ԭ'ya ׄ}' `F!ly|㥥+}`8?sm[&Ģi|-'OܯS u.]5_W6 +Ø0 ٺp_r>r}1Nb7=\\nxвfȚXϪY֋ /Y|K FV󕹭V$~-3ͧZ|eݓI5_k"µ{.ɥF\\ïߖt݃+mjp>8>GbDi5fb="ԉjpEj#k+K'$!Vhr!iڰX[`TEL2”s[LX2j q=( 6D0uc4KW:]PyVKvNToÂeB.6O0€$:0 g{|F}䥇aMD^;Y-Q l2U^90@oqh E%,6n>Ol3 ͇gÚ#Аk$YO;?m!u] +,2C[s|6 XiYx";s8S- c]Qfu{&=Z8UsֳƖrgHB֬gk6\){  SxոﹻJ.Mڗt=vp]Qy|77Orc'\T +dnz3"ENK|o +{ݸܑHzQQg&UQgpNM}bhu,R$1c8/"X)T#,O{mo"`S9Z֢بX-`m|>lCfst(ZM(=ty鄏B}"p`mz3pϊ0a LEVwYpnIѮiXz +&5{9\RÆ&iS}=ޗDl25>:kRqt4-HZu`/ oxj!+ze߅ͤ1XĤ}3C.%O/6"yJv8eQ5';zvxb.\C=/ǩ0Up\!>L' 8o?=[ϖ9[B>!y(57=cw> n/g-a^OžG =Y[ g%>% q֮ >ܛЀb,>{2`-8El:-$h4*pNpo\sZ9a{iMyϑWkprFyR!ab 97eityŰ>#/ߟ .n `rtį+}I9`r0?X[l u6d˫=hVd Q/',.zjUl|vw +ɝ]St-rplgyߪ|TEA%Wpl>y}%p3a"nDzTx}r&C^%1Ϛ޲ϖH^ZL5_^T66Kl\|{H +vOWHo2VX8媶c$M3ue*dLσk|iJ)y4&Pu< rf bL^9Y +'A<-aϖnifP;˨73 ~Ŝ6V@[XPnBXM>p^a0ǹ-LJO#+`:;cT| +"A\-d꿜9!b>O~(aʌȽRM?1i8߆m,:+tP'66EW] B/ڂRuL qas6uM&vZӆ|r]u|Vo +97~alE%j'\Jg_p_s53Qq`L(3$csOǧt|:>Oǧt|:>Oǧt|:>Oǧt|:>Oǧt|:>SBC X/ދגuIS:'%E%G'ć$[;·X?/$=")h^} eֶ'ΰuޜs%!ֶ/jl-JߔF 20-,~jqH2~J7]"sӷmpk]kmE3 Y; +D[#td Vyr A?T!CkmMw7?DRe4-(Tk4@K'@]WcVǟG."!||(.yW/cԁJƿ +Zj}4A)2QLVSחʩ(Ϭ52|=Ӷ*Y*AAZe +zD+쇋MաSk)񊆳K'9;.>MzIa8h6B6@L0 4(uZg2tPEt&cru٤}Te7a>PC4-KVmf2iOGkk@CBm#zfɫQ6q9zlB.9z37[Ht/.[C_$3=-ԗ*X`{ ؞w2h3>\|.](6VwL֤֗$)GǍEjqY]\\Pp>I<ߔ)>QCL֤C¯QQ/;Qk T<Ĕ +~+?esF@?W~:b*\-R#K3 +t$ s=|=Й z{AIlLHb="QC~4(AS@Oe2XIׁVUF7em&gx|t kI4U$C/#KC)a,_}b.NBo7uM {DYV=3Db4ep0gB scd>vej’~fU)>zHR#bo/+S*MeF<sZDg "'A %c-zOd<~>ItXLD, YAGiB/Hpu&b6:AlNӂa[sE +{%SL@tz@CC\m :nRĪˡ'*_ +^ zDW^+}!EL.h.ȓFoRvM8*ʺl'm2G=-gPUsl :'*J +4**ߤظ|2u1hkZ -u#Ub|]^;tEh#@i ,Ķ}Чђ#m=dԙF%sWMzx..G)gN6+xm#Vv-sAS@YY{2 +2=S|b!ѓz]WbHbzz9&_ G|*A$[Mtב た}p=@O]ʪԀF㸰&GjdM.4Ct@c~RDCNa z + +ob>n˦A/5s*"M/-+nӡöDG.,Yþ9yh:o6~x 誱DI6xq‰@lqzwGXs¼ĞAk 3ȅ@f S7v0D%qfDi}hEJ S¶>$l3g>Pv=2cea$Ea5H<_a^n%¼=9чh5C#-l=lA4؊ö Z"gMmﱂUҏm (@4\5Ұ/ۗ ~ДekoWնOs 4fD100PE7 +bآHw#9g;p{{~~b˜s}c>umh- -]'8΢0k0k&Ff(.<} ;>4(hs)S^*]=Kc)Et[@n Z:&:U?y{"k7yÆ̞ h"XcZsW/~^rA9lEӄo,a l!f& c>qJ 4 @' +h3g.O`R|PIQ6{DC8@ /A8mt Ԭ11&%w6?d5C|9p5hB5nM-P`h@YT_@Ƙ2 +{`dV *жx&fpFqAӁު瓷~ե6Ocq|)EБf_:6hnxtIQ/+1{:, +_%>j +Z1Tоחc?O0p, ŶA +!FY/{-`/~S$b|.#n[ը_dlˍ(f7t阒TJ^ +]Is!p%#+&ԩjԄ 媠2K\X 40.{bej e@\:+S~Ih"?({9^ ʟf#ӁFhV[ -&C^=aЁ]xgccg}&9HsWNbC O" w}2`=rmH/FM\SCg@-/ ߀^aMx(O`|S.sqOzH>W~DQˀvz}VKAxu5޸ +TCE<97Z=fND~e;G AA Z#rg +WAW[Ș?~uõ'M X 럻NDUng2~(D@&EF|!Zq< Qqx/#c`MHۏ~ \ze.=BnGF7Ħx?=&ȧ R*G4t`͝FӦNx.x*-}4(?)1C:͐?R+^/So:? b]!>8քt'Z +̛ iZM=|koN/ Y 9-ƄZvL!u =>a\e어~|j{hPӠw-:ksi%QM]4OD539T}:5y@+kTטpTb^oev Ʒ0f+ޖ+2wH=*,М?i;r|+ }^䞤$vMx"j +_>G/71W"DcކhX-I/:OΆ_ Is0_4Gcb蹘">hlq+s1yBk˱^DaJ(Pxdvj =?99"KyJ|V~HKq`n9guT`%п~P<X!va:h9b=D < fC:)1WCsu^k\}o.52b 뒥Do19ȯb4%GIנۏZf- ~*G endstream endobj 27 0 obj <>stream +A<(wQnPaE~)m[1ᝤO ?.?`ln/ G ➠ +ixq[;\w>hA_qܝyWɔ.:C{&QW:|u"E#=Iw zc >75,wcZ௨fB|(x< p^пuB3ZМ1a,A{LhӄEGGKkܟAxu2<5t!!>ycr&iDz(N:ZkAzX| +Ό|"F1J". .N)d Qmýwxr8l\Mo!9!fzLX8?Z=&n~y)[D5%1s$ +pK#%spŐX\xd,\Fo(xB( F zFJ/xU\W,* +:mt%~+5<&1QWѥ-l55+*ŰCu%8j=<>/ :q2SV:wT˄!7W Þm^q0T;a脢 +56TN)S3^nDyk)P ++\\YJ=[sa]_ +csX W1لJ-J|z^Y9)_|)DejMx?فoϵ^U2` xxLmqzH@r9m$ g4p@l6qTCoEW.)3QwVJҗ@UuhOH6({sFOIxBh)d TkI۱'$xpGm}оW0|1g UuQ2qS7(J*xL96h}"}u.x2x3Wb iG\]=/{|111!= +>Xa)J TQg+UuORTa|' +?|'/S!-r q5rQj&Z^XK4#nPO^<:Q@eAK&%|.p2Ή@NmQxFxP{.+P}Gu^|axZX.9b}eX;eE!h3宒d,5! (a}%g'^Q1"ģ/<{7ǹx!!vY?W4 '^1hre_k􄄱%3K6)xck:ڄv2JNnPf# +|yf.o=:/x]#q>ĕl:Xq(F2w4wO@\%_P'`?^5t-P%YK_ŜR&ὲ8RCTNOO^vɫS<؇P,b+nd4kחaOH N9π^ 셡mf[53!倣D:BD +~X}9Gdg{@?bjhh5Ox +Ç|O̎xޣ {rր rX=XU'+}kp=pԥiT`s!p^rЫF]_ { ^ ~veGxc{Tp1b'L@v0.XA>^{瘹xOehZ]]k&Z2j/_{")o3"rU-~o?gb^n!eyp2%7V=}.񾽄J/ڧ'| ?.eYKg0Xr0h.nm `]qJ랰uƘ>qi4<hPKY@<'g)yaiL%apfB6v%'1;e7N!֗%qK U Vo "50f k؋~ B }~ q_LQk )85C<\\'Ea6'_y&6֤ ASӎ>BiBkAb )OɔJekrF~(~_lƾAVVgawCg'3[Ԡx\Ϻ+>+ e;/74_6ƹ7Z1{Oxr6<EпZ}X煞ǡ +7YZĚS(cx$ar}b1zEh,@a)l:FM{m[c{Q?2@M`||hO[1yq,yf$ 2 HGm~fQ\ {s+WKaC6 ݃YPS9kmE<ʭװO=y楀GqbO*XS\Y [b5lDVYل+{ + zK//lh&K.Q,#lk(pҗ #=ScRy[i/ +iLX&h[`DzTӸ֡蓉jӂoYkՎFe?$55):p2jj>}ذ}CJ%B5k+aXGEx3EC# {@z!P{G@ށ'LZ>*[ +#aen/EMgg^ѩmDMz෎\pX< +JuUqP>a/!=ɐrxcJͅL@ +R.`VX*l +4v͞OFn":@_.OfLhwLl!̥Lr>A feV3j!oփZP״|zmFdÊB 8DV]d Һ\񧙴K<|3wa\|)]\o^iHWa N+j/z'"YXiI{fҌ$J]TwIiڏr.W*j&UE|J%Jj?] \-a!x9Wa8?/(A}dn07؜6i浙STXKѩh!yoUш;`m{]|QHtY,ɭ9dW|N҄Io&ӻTѡ1DŽ^q 1F_fjz0bS_zެ25Tj +>6 忤&eX >(W{]z]G=Gq}֋@7K(a_vFxK[N rO>l#77!_| +K[ĥ&]0yn֬dhຂQ"閰,Imj'̬%>/6JŷEVJr5k2٫ڬU[FGož[<-"Jo}ТSY_T;x~(H^g!󒸿6PC|9M5f>}\%zGo# uvtEmKUz(_=(>..l8+"ީ11-;ye3R2 uz'_[sKR;=caf>C+ەoVKoi,BF&}'i25C|E@? +ߴwgJG}y[vt~)>üd~>FqzX*dʔuYJJ}$}Ee5E&!3>هbqn){eZuȫjV飊#ҧe;- }ˈޡ'F4 +R7jw?걷H*O>ꢨ>)0ے-8Ǽ:T|8Tv^ .jWD  tA!Je>\<$h,y59Sa˼=LeҢP 4q.Gxakx&Hޔ_4wQ_?ZͿ]MG3C>Y +bz%jI3[H{UǥolK\n_b'~QsBrI$j7XJ&496_ a­b+q5OŃ%O_|د}@x4xkV4Wy`\x嘣d.⾦#ĚtJڛڎ~Z2ٗi*FK2'W=GK,~hmR$.mn>)*n=')T9T\o7e_vӯ;Q;2F4P+nXӯLgjz-n%X6Y']mLh͒0=_Oi"mJ66] S>ne + 1kˏ4-?,PǫhaU軩N|!H=mcDN7s:^NNjLO?0Ѹ{]k4|?髊s&CD[V 2h $ބ<1Uu9*̲9%ZpWNԱ[1`qsΗ2aRY5]'K+_<9/yq~2Z^DZoϼy'ƽU–7qgwQΪ㤠1gSj%vs19D&.7M za?9^O\M-wIp Ip N<ߜh F"L5H-@5Pès)[e}?C@ӊ*6h`W&[- W6m6b/n+ueIR7IiI }ud5ȧg GK;JEjK#.4&&z&y&&WzJZ麶"]BS}ܢV9Gf8߬p@]' xP#T}aAfJ5l(~pL7D dsv+ɖגN6JtTş-x%j~#+~-gium1q]녃=ŲCy2Ǟ)<ŔA8ɐ+zP#+?kX۝CO=HjJKkrZy!":bpQmXa]XIuHeuH)&><1jkTPoeCZq@YדH3'z>sy+=8(ѐI.I3Ҏ ަ agC_*TJ ?IKLz+#N愴齷*Ĥm[Mpr@O+:$EwVMKDLH^CdQMXauF7k~Mh~M*4R+]eer7We@SAA)'4 +YƩ +Ĺ%oϚ4G6d }j&Qaɛ BT_~1zT:WGE +[-k }`MnV)'Zo%\mW!Zr;Gf$KLuuOlLa;\u܍wo +LuYJh^G;>+v-q-:ޞ% loVW]Z7X˫anD_Fq/+v/u +M* (O;{\^s|^#w(OAeE^E2(_II>IѹQzo9=לO6I +ʶlaޙ6 +λ׆^qe]jLjr{5E a GPvpnjqH~l$ر*"6'&=jG}eT䨳)Ǜ"=oͺEJzC$%ͭ~Gn]GfMQg2SO7g%p7`C=Ϣ}Ȟln7ѷ#ef}"9 #j#c]#_6xg~ۢdMTǤMQbCcd]"~urƺ=XqB~̌rg~pV*O{'勓^ '֍jZfӤ8$.n5=r/ѱ:*%;j[mcl2@Xֵۉ*]% g2"m+cnuC9y<ܬ0ڼ+'BsR_KMx≶tGpܘmYqcccbT7;^-g_ܡ;S'8{V1~JkewLZ[${7"J"N6eDw܈`<[էèa;ߍJ%{7;o]sd;ÝC}})!, 嬒JAQrGZ-;guJSLQӥ' v9 vQM^ɢePMDSwyR~y=|1}6y^b}ƭ{5[U[ eq)[QiP_ڝJ"_zn +/ +="C /#p13VkU~n,E񡥾 ob߻ɲn.o +Kߞ5Zinh:rX5WX8\O(silt(WMطODh4n;w)s9,[ +dJK iks7+V([ -}>3vUqBAV[gKwYo=b +:-v /( {1"-:+ֻ2 ѭ2$!Wm4jSt)b0C>7@k8ޘm1Y鞂N;%/rE!S;n +Xip"BIa1b1CE$"1PA,DfD5 W|j<&N w+gc"D/lt5u{}s*0.WTZG7(/F]-=.v|!J[Ǒ8G.t/|Z&knuY~iI\p9o-j ۰SXTXf9nzFN󚂶lW"&(b1@%>r?^z,'yx˄"b/JW?*727*BoYzgԕ{nQ/ +\"s \"rbEQoK}*ٯ䯟\/DǼjF5bhts.Nqb(sJ_8t#rj7L F N$FMD? fNYMrU@ucsB9갸9zlVGTasշQYܢP +r 5yYSwΑŞQmQ_[>X7;GΝ/{=w]Bi t DgO@4ET|?\_{h.{Pk~[?e;zAf-#d.Zb^BX$aoc+Bg';QrkGh9-CVGGqx!5&-}Kt"7nMj_uF){R*K( r bƄixnwyuwpMc~H9_$jĂebEb8q؞<Ϝ[I/|e<^ž-pM(+pK)-vNz^[RX[EPѕnhr:m__P6,/c:{cѻa3I8MC9ii#/&&/"YMT8@Tv!v}=B[8!Ԅ(hぼM(D=E25ɯZXt)&2aKwiK/cC\ ?U/`N@Y IqbM;YӶ3 +S''ZGL +ߏ@(GJ,xf%?[n3oxJ(`{/<Pk}\5Ǖ2׌JJRD]ʹǿ \/JĔIh)7ČˉiVƬ!H̜Xa? gb#}I{Fq+ש导 9b,~~?1?QqBb696s6%,Q'-2$f-sԉ bN(˱e[n AQz\,#hw +~AX{Ǩ7Qo5F~[i'<9g84b42!75v91k:b̽Ē5bS sR̥$1s1CQ=~o/T;A6kwٲ<50Ts n Ǿ.rP3P_c;PXST'Y5fp0F=<{CM@N!F"[BP”5h fMM(NML_&Ebbẓ +m_b{;ps[?rG] <#5SuCT[S<̹ZǴ*z䷥NKr{_еxMELG ͫihM^OLDc8 F-"f_m#0k+OU&ļĂ}}bO fOv6C@_X&2{܇}|yYd֬uv-`DTZ6N E(G949]N]bEĴq PnD䕄tiJkL Ă '{bmm^5p*<]*0 ȊWztm%OK">Tل~oK.vHL*q0}a:fC?~+ a1hơ6,|LP<5:llgPN',&g 4&h-o i7KۨZm3 k>Ee(wƼBXhtscBgCRoC:J甎 +G%Ejp[&/q(LDׂ/%-t*Ĭj(W( +3Q L4\;k71g^b +1w1oM,xXJmt+!y/~Wm zy ,L/*t~M/~9mV7F WŻh℅u ն1BQT4N +VsP=^XaM,fF,Y#CCr5耚;{;”Ă4`XzXHcGo 7[SV z9 H󭰓uч:Dl̊M}'TӵӞ(ڈԲ2ې +b}Axm#Ly28ᯀA _N1ah>*SGDL@Xs[xR1lwf읶=k;.=ǯ|j.b-j;k;-Re(lc|$95w-t=QZb!j9SA4C|2V5;e-pF !r};r1<ގ^wuy#x<~o-Foӣb¾ܚ}n"flnf-S t8n'r.VDfBٹ2,ƀ6$!$r 9+g1xR ӍjZnR?Sea$zݔ}T+>>&.h9 8#qtA?f H#2!m;A,հ!<{6SIJBb+0lǣvݗUvg˙_; }~w훵;=j:7zA G*8SAiցPzYYKc{];`{I?U!!AjI *m7RXh6n11gVb&#bIbA]'7E R3ω>=u!3s?mx6Yh~x:9WC39X0WdM󠑺6;vC39#ŢFܑ;Ӓ-$!L؋ zy+6LE*q0O>&qEqt \obW#Oܙ)~3GΉ5q;U˸MݜQڈ2 +HK7hxm|m>Pi4q.`O{ o턶:Q>F2QAd1W|*l9w0wIZT1z!1g ؔG8 aIˉ[f$Am AI_9KOa j9R#usz`3Nq adC R7+8L?rּ6B7ϭ9J-ԵG,?db5Wc$}'ٛtImLuLnc2@]hP1vQuEyq\byԉ B,XCӳ!5|wݷZ% '/=<_(8:V偢+Ni*p#^:X 4 zm 9(EGi9QWHv5z-^^-eDALxvNUI6͌-j>!~_z(k`7u2CQ]A*AsY,ۋy~LE5c:7i%}s(_ Qݥ|s ܠe{mvMb Fz9iLnM>ԛ+>TpBS1xvl4 яԳvg$y1f 'Nﻤ̣v1׿K_7^\ gd7Ulzg6-~b z)n&/m)[8)1)-򔾨=%i [ydf~2d BQam w0 +WT#VX-o[ш_vqBDvvkBPF<%MZ؍7sMY*.<\#-M\< NJلƾmY8GlOϳth 1HВ&w S4tjfTuYΒWOZÿU*%SwI~4kY#T-Gi\z8IɴY_Ѓnǹ:,jS6ZoeNڏFޭBSMl3$:3q!˳91{_mG~M]`*zu)u|cftOu˥mLHمcgG^ L1ίH`xy nO[풫O,^EJ,Ey*eb)`0E^|t<@y?gi^Q0 +( uai  RgRb6I3ihsSi:f7tG5Iv;-6-e?tt1n*5x~olxqP=;Z9vKٓU V3ݶ²o|Z!ϔh>[N?Y͗ۗ"DS%Lf.8M5?4RTқ]Wބmٟ5yY]{ً[Vp/m 6uy($E.b?a}c@%Ulᅽ^aֽ00C߷^6kp;59}_9 ]t_r:Fk?U;> (+?"7O\SE|Kg]lyfB-a62W{waJfû?ԌU6+]o1U9qF}.+ e 3r)M1 3C +Sٺضd9gVB;hx $};bݥ%_#>wiF>էE?\Camg)shxoga&75ɇy׾lQa~ &VIm· lgagSחsUܧЦ_Jli.ٍ\nHSݩ:BdAf*}xhfn1srBtqгM´Tj j/r4 $n5r=Hz{oUV!ww'xTUq!n4hhwq~ߨ1ҡTR3#}N<8y/bŻ#p׿uݯ7}/w|y|F|W>ᘣp6ac4cYݴqf]7k{׌3Y'dnų +1[\9&cf W_:q/<7./5ܜc\zᕁn\GeB^T>fXrr6kz{^>s3g[꣊p_R4g>/:/wsEgbTw(ڝan^Irz-_o[_nn6HO ^=Zlp/wb^g>E"&ՌᓪFN,]x_/Žg ݟ} Ӟ=_(; yX`~P?)u٣{<<+?WY<~ftyf~Q IօZ{XrI:c5[ܺv#ظr!:NdX!G%ڪ ŎČ +I,Dh%'y矩…)|rF{k($U3j Own|ܐG}oߋ݋4)L:ۙ\: ']{eiG*5޹ 7i6ݩqٸOs@`9KmxK4l4hW>930g8rmpOoV/kDc%iW8Jɫ,Jx&K/׵<\wWOpز.u7rmO7qO7}?^u~P7;~oxpVx{?ׁ? ,&nm<0{͞O܇ ckFe B4׏f0MgU.܊p7w<.<ˆ-!tx~ )&Yue&<w?{~aNCb_c,| je%ڞn G2l􃿹pL\}˵=?n:t͎{=4-,qY7{f]vi\{hvcnޒC';B{3Z֟sZ{@sb\tP>4[ڑbYgh%/6`?ՇgQO~7}Jd__&b~Mx[2;Gzmį;0ܡhcTI*mWI޾ɖ;6k6/ޠٵiKCkt>Ֆѹ#4[mp%h'78I7CqjppE +07's{f-6A |fxth&ld8 sRũ{(/]g|QW|y>if՚=[kKcYIp4:aL%d~SDv/soX0JN 757nۤBjTxn6X|Zvl<⤘44uRY=rbVDҒLiGQvS|P3 Ny<sШJƜ4 [K\G{"줬X/ݩ;^no]/H%B:B7;!ɤa͝~4x^ J "Ts?r;7WVrW'W0r ȑCڑ:\j=l?Yw+:hfFI%3M)Ngrk GZ>V4 [+OҸQfB*]l=n]_!6oI{f8Nrp0>ҍo}Gӌ>+7~q>,]} +&Z8~g]ybfm}Dr'K'wD.g>ېnd\1f c*F!q4Ux`taEΟM7 ͸pEfʘb!s,/D+qRJ~t꣄BB_;Z, nq@Hs1 \.\7ۡ!uN#VQs?]x!1`$4\3=WՋ_3_P#@3*?=_,tJhπ ³:?+w/{uݯ6a@L&6{:y .,pwY- t`&l\d`w}h>,k8{r[ۥ<556 :"s>]l J.i!fZL4]yXKhɈ=0T\w\q~i"}UO_0$8J\Ik Ah7_}A-fU{,b.<}㭥zV>_iy;]4׸N7c +Feu0ӟMl |Z>EiavЊbr0 qpäˡKM%U…ߴQ(0 B)o-jxfĢ3gQWFZB۟;\b@.j.2<;12Gz)V4I&'p{%␉q2)1oZ:x_B[.==zu,4gB"ҌJt!>=_o_X6]K;BbXfR`xS0oĠa!bfD>tO1ut< 5'Qfr3}@_9ZNHHV~fx6۷A7QjuTmiU_Zf'}'xVϙF҆lxo֨Ո/ʟœjx +~̗Z,>vNb+GWŧ5C_o}~͆y5hth WVriC*!A/?g*'<@6G?[c8l8R+/-&‚}!y=|9w'KkՀ;z0i{]E┱<@Le1=lN>0īdM(OYʱ +Fl-eLEJ%rh`UU]YV[].0Đ(kԉo+,o5B-^7|gŞ;A3'CS9L1x{*-cTh +)NBD> + )1K/lM,.gp֗'AW`1zWN"L~phեc1-BRbtLU'=)9.{Qx)7W.e=ssb.\mRF NN`۟m3xm$jB;.4[4炒p\D+WO<;5lXVͼ-:GșBBhCK8‰jF-}3P4}">yAqg7{_SѩbHe˱eR%|rݽuЊG 4h%)7bF$1k2U.^mkzty+XJr=Zy 9S)(P_FҊ>0 + +:bW8s\ ~c#/8P3]9JT-4;pg^{;'h2z*,8B|(褋%7]z)('cv1T`Aq34z zۂ7+ҟ,91G9&Bs~eT5 ZHs/Z0 bڰ{ Q}F.W^bE|B=4Ÿ|YʲCnރ g 'g=A`챘1 +||O.' 9:&v]ӝ·Q󂙅 +g _֛rz+5#t~M脢'B׹,VC׮BwwBZ9^?luZseE|3XP:[>fTwq ӳ{U|i|;/ooJEP,ҷ;x~z_RLjE@j@p=CfA$1b}:;[+ˍsJt(%ydhXj5fؿxZzu1_h `^Pw9z[k]-:CBs/JIn1t9ӉSw`w=w{Mw7Z塾foxs<!( +qY1JA!6Xwj\HAhBYu?ګx81`gʷ'X--b9 Q;0NHS`D9#HSl`@$cWX!qtq f$TrSE93R:0;z)]~-fvyiL wyÆ{W>˧knhbgMI`g R?ם屟\*$Vm 6c .jV"$U9A\xyYھag{5hX%iY|`^K梁-E#Pk#XH\H.$|&hz;kH-U< +:IK.;1+u'rde ,g,CLZjytf|5^w%Ьy4Fژ)0e;JJ3꧊CO2j<3IN CR#lggM;J߰Ӈ3:)WNUY<#\[vL"bZ8Ʃ73_oG]{g-HsL +jطl5xH\{n~m4u_nw̓5Ӄj\zrZ$?Tkk*f'a=\0X쉅즩Z kŅB]cy2{5w#vNR!&zz5,#>Sl !6J`09(e8|4Xħj4MRlwt:0~H>yr}7)(gehޠW +-n)Q`|3 rRl[5<FU3bfL⏌,b&7G;KyBUk +'/L,#}-_ Uck]h+,q1f";dabICo_:^- mb!h|>ŧ* 3`K# Ycƣ؉|krk V"b'k<@:E%vD?CjĖk?X\R*/h{h51V>X6t6E)>;b {)@ʝU +yY. "j;׬V-*I `T]_F}T&>Bb$#m#=Yuo1q!,7aO:덧(+/-;r-;Klb#2βpZT̥&6!bgO;+2+;+ ;+abTYގ충9 X`b>&@gu̞jfR!g _qe}l]K%"ێd!Vқ&G?\%&9` /j5"~]g5y`0˩N#"~Ei.?'JW6UA-$2\7Q-/,n'%ZFN%[q}/\d쬤Y6FbgY\^!~ZujOAl<[S=vt鱷|x,KCZaZrŖO7˝v n[ 

jNg/JhܣZzy1j114͖kS6#X-kKYlwWNdˆe'Yq!y߰_sJ%]sʳKܚs[ӋwWJ/,RbXr:ţV&nLQ3~CrÑ լ +0-pĎF{` 28Au"sx.ri,_]}ᚊEbXN 4#vR MJ'=[{LO6D;5L>Y>H cPp|)3DYxy=ȥ (FF]{{b|;K>oe1ߐ,Zeﻸk\=XsԆk'-\FX}_R=n+'RCJD!(J:= cy6z_lZ?\K|'VcQQvyVG'yj41RKFGuU K5$9I_E,qxVy{z]l[0h0B<5(#i!y +]쎩`nȅ31cB}D+QrLaĀaT[ bGڋI#S.=g&߈"q` Ả%w| +4RTՐ\> Xf5} /*rٵ<>t/.FOCj(v?!DZ굲WĨɮD/pސE䏠~¿Ć;nm"^[|\hw}PbR8ҔYcxU1l# k61Y= @ 8CڋK똍mPB= ; W=bkTd|mG<;wFÞZ~m)L)}Ih>2<z` ^3|>Kص/陃nRpp\nTR錡˩a 6Cz4:Q# C~%%X=9L̈R3 |+_"&Qō׀99Z(18 +-\OebE#F!6!e+Ezjzۘ%kƞ1Ä]_[!oƻܒ\C%{7Ǖ2 kK{9$F<k$֍Ź7s_+JמbMAWoZ?ߪ #9,磜/PrOK0yo[%;YOׂXj5㐳ag/{¹{]o>-P" QV8^88Ea+DL;6L' A\YNh.ǜT=3gR~zz؃DΨ9hHxmkџQק_\ %׏ř1"VIǒ~I +XQv_A`m v|lGd 2j/A>Bj0 f;/'Og.sC9|Nj|ۣMyJ9%UB~=ҁzb}%:q|]~EzJIb֘o2яM9r4#?I.'[ -PK[L81ř2?e*6Nƫ+Ď϶J>a)qX,+sZ>^O5'cI|͙\ǧr?XYT1<|L1"F)C|8!)k&V6G>C.Mk_RSJb9)c5yFK{k5:N< ]\wIoJ)6d`LkuLY92VEܢGZ:XMYAWLX5.\3#O#X賳5$k'ȋ`Z%49Z>'~(OT$o,mnGNxeاK+sBv6]ˋ\bvDžDE&;oė9]KYbM]\ٿ3ط8/f?|˴~Mՙ}36$8$}QhM~ɺ V,]bWZzuKW-__/%//9d/G{opY;ǘŞd ~p~ٮ$wg^q^Lo?^:W-_vyڕoO`?IZ:̡W[ykً\4]lAsOg>wYgVhCf,k92Aڀ+/5|f}=ƃ30Ƙ{].ޚ}{4@22"rm)VZσ>,ôI>ݼ w4ZlQ߿ߗ~[ +1ヒ!ebH "cv4cԕdzs0L-Gc8zqLC~t`?c`<'i˪129"Ö$(g%4FDIxI4 ~#HAX!I"Dgؑ4B`#żьS!Gi! +#c!$ n!V8 oǒ $tKBBU5DㇱbWWKO.0dwO(b&[AZ{l߃P\#I(}70Cl$9qUcZ'!8{!Rt8>dMh&>> L8>O!fKG2CF2̞Af^Z?3w/U%Fps<_`^Qaggה?0wگx)d]B[.V0 +ݞ/荡ti0>-cv@9Vdk9rW=Z(́h6Ys i^_3Hu@H`)-$k545B/Y Վqs={5+I5NjFdCN3 Ng$9R'؏hÇ$Z-@"jCfDlGLHIӘIe:l'n1&dgH*sdI2udw +򃐁}B,H+ ˲c3G`Ҙql +|<%(Æ$NȕT$g +[CFqb`$$f E12o(=HԐxti'Fe19"И^r8sT6cQ4IE1y#tv/=qḡ"mi$ѐ>]/%&QFxAVRzd)"K4߳ݍ=֛!R+G[0v/,9Cxwф1FZdL`"[f^QU>ȵ0~#D~`-2{YH6:Cd)0AO(FLՐH Kh Ok&Ռ5Ė;vjX FQ)$:XcUJQ1Hf`T[Bc@ y +18 +n]Ըj -55z=$?Jo2!فaf_agwO[ 0S,oC^6&Yak #R4^sH/`P1rCՁWY?]{5n`l&8ǖ󍱄<ɠK2Ga)HPbG7BjT MHZ9 c-9'44<ӆ|Xb  Ҩ4ojڱcZ;S!)Q/,oRKNχ/2$8B +K -[!a4UM-.-'׎ ɐA0cgs1hcXCo$^ !~$𞰖0_\L#wWa csB>CR9YmS!S1#,fwW-͆4hh!c*'94ÅӓcGbT|*m;Duq?s0&sh4bA aq!4Ot0J_s/],d 9 |Pcd]4d2'1̱\ L"G@zJ:raRtq>ɾYI̗h([> ?>ZȠ$-cM .cFÓ(T#hLw8#X,d9avgHð&6C.IQ)' pIB%qɵH)`P:䏘mGQe6@XiSŎj^3wiWKLbl+ +9N$S3;.4ј"ls! *|bCc I#E鞊1SdHCr$i|9̾CVͷ'$FщY`8$r =9:df*I2_˳B3 A#l1L ʷvx >K5mdW2xaI6P*o.jFZo{mK75pՅ4U LPlyEtQ੅PR)UȵWa4]A CF`e<,~d _#KŐyJa>B5%ȃho$&hc&l}l +˅l;|Wl(\K>Ƌc,~f %foZht_JQgA#//,62UkWGbV>H QHy@SH#_LH'fB^ebR:KUWcXe/8s8^dm0 +AՂYRt|*d7h<׶~j-lAV2[dE 3-*j[0ײK$f BHxkɅ@FY~L##VbF`ĬI3:&Yst bL=Zc9rRYMfd;(urH(q5cX@|TYA~YvrjI9tt;$ņb"\㍐kn,;|@A3|jH!b54F>', ߢ 9,,Sl%uh-XARnK\N +Mz f\P@֜<7dX;X;(Ll⭰HJMXnQdVC[&|b֣t>qG+$!dkc``Q@G:ctKj $xmpsPsC}gg!&\F) RlH'efE[b,).Ʌ$[C2ZVd-ћ"xxK/J`z '$t)$}8$ V{< gi}eVO:mqӟun&Sc-% 7R+JG. +=R` 1#7άCǧ@FИrjT-O5d  S o5EYd 9NYn )q*ǒe1  `ggK5W$\0~'|"YPv.dd@מy|'5-p Ŕ8RQ x 2ڧ4q B>?j H5pc$ւzY\p}&[~Vִ~^ +2²m%SȐqDql- 4p(c8joVx|4޲JYC::9%|R-i1%|',)Q/2}Ȩ;X|(!#H|fd5.$Sqgc?_j`5Qyꌄ1oB;Ÿ⑈E|ېHA +{rzJe'cvtߐ +f05[AiɇBT9BPS:Uk<=C5j!$Q;咞Y|->J,~&?5PJ {_AxOK5wWBP+X > X"OIқ& ?B҆Cv\hb|+I1Yh7G- =.w<)w\Iŕ)KIK@:S~>+Kţukˀ]pכufWXS`u?Iȇ&Gdhm2G)Afu dѷCd?5$ A$Sn-GW_F0daqc*G}ͥL9(% s8lC Wfvߙ^7A-=g@rQH$.o$OZ?Yǝ/_ ca^j9FkEXOP~)GȩOIE_R2z}襰|!Bob*cLrC,e~j}Ce@1ɓ$QL5B>bj#_ GA6egL䦫-rON횄\ qB*Hnxz1o⢜:Qlxg96໤bB#b^a`cwW_:TjuHא2EBFΗXNbBB zv1ʿ !DysV4_H"{%ᴚ@B(ѩvX;_3!!'.rNjxG ߏ/w$W[X]yz$!w\fn] "'5+8=טlwm"!c;o[>D2xlxgD8gj:7pdgaF:rnIU^" 9Q#?ʅÿ=θIk!)XaoGݕﬠ׆CÙRo_$5fCOg%'B/Fa}`.m9|H0`!p@֮<Ѝ'( '@M5}Q,[lVo,%fB.\t=/ OCҋ{] g ߀+O{BEh'%7s&Ր<0 =,=7@*ug GA6#L.Cq\NkNN[c@D1evi*rbk%BP>tB΄z {TD[l!#7N籫4 Bo A2r,{ !pEB䛐,ҁٍ1@Xk/tNGHlRR?ow[A|N{?aV1,a8 +ȃ=:$MQ =<547CèrEC46kvl٧BV]P^ME {X=ȁIZ+[GQ $7PBׅd=^ԟŕ5@b +9p3|I#HPMw5ҡF7䡤ߴiEppJQnQ}f9Jݵ3!{ + $7Z=Q[JWWH_A~{(dKf+=o=p\BB_l $qGDL P.l +!!rMH_oւoS=a{lXI=GL=:^-<7Ov JީYrFda=Q kK8>[0EԍE]juԿ@x"CM Jn* d>!/+4|FEAƝԪ!{w{j\vոxJS| r/GK?cMDյ8[K{W?\FJ'Ylȶ8sHߺb "w$Dp!!.W25[O<Ξ\Ӑ4ـPצ `X3q梠o6|O Θ;)jQLHn,O_-E6}`tZq$ѻ 3-;uoٽABjg7e'?^vxp6xC􇒭|7HfeS.5Nb\sf\wkZvnX젳=^ ml씺܎޳PJ([˵}hX|ePuk1K NENG9!?0[jYj{5zlף\MnW:q7\|g8R*o.R \l tіޢ7S "z &^XOG ?Y i|HU~R,G@߄`5[j8rM6eyz$0bAtDTp(w"r QR[Pg!]΄_cu*I +K]'>z=`{ݔ?v=?ƹ`sk߇4zJBG+p`;'RQxj${QwhO䂵 TY*&w=jMK|rR_턽|m~‹jYW#3쌈#@7Vйb%~ +!cgpEReWrf`O/aذGv/qV >yjLHʴQX}jRsc$2կWH+Q_}c3&&A/ +}|jTǑG[n./}?SxT&±B`iov)t>&5|V9raR88OU`O{YJBӁ/G=obr|D-^lldXx~pЄoci$՜["1P84ߛ*մsŶϷ(w _`N߱vO]9LZc1[$yS ?ADӳpF@)q<孥rFdj3^P gguYƊ+KAo>D+Sa{CQଂLʁoPa,J#D 32,!rԌh zƩj٩8+ሜ S}߷Bd1;>؈ $ɷ&*>=84*WuSn RVu‘3scq&„#bO; Q;A؝7B*Ȅ,8pCtqvk]/| YJ氚a0.$ + }TԔӎc r] p&c{~bp*}c!| 1@wNb etΠ\jrFQyo<,2NeD>{nE/<7*,"tI.΁gXE} +[YccbBE?`&g|/壇^%$u'HEf P^B =O;cP=(\UN͝.~ҹ?ԛ/ =Q)*WjtOqcDtv]3 :'WZ.u=i<u*͟l#=n6K|.<Gr`pw6:r}11;SQJuE <KpK=(\V?Mr +y'sWgz<]uvooOO߂u_xtJW#}K87=Z6t<'v|&WbvoS^fR~s)jE߼ꉾ};C&`q3bh%KgSу^ mƂ.~,?lv|,XX=LPE {+;ω+k]^:=-=!)yd΀>:7*h;l\.P:̨.-9E$A 5}I^tߔ.ou$$ D9Zx.]Ev2E;P"ydQv(>ٯ%!LvCm1ÍpeI9ї-- ';u3A<&,i7뭒z"i(&|˚/H_tHwTY*r2TvJ|^|v +P1<~ZCktN!jvz)7nm +•]N_\f&zxTNX苐x:,uģ!~mI:J}& &+O϶xoW_h9Yg/骼,.V?B"wTK:]t^A=SZ(2֣Etrj#>:F }&cN!jN?2o~5 +>S0[=CR5UoOU!N5Β=#bAbbP  {4͊l8GGG[„=[>;=7x'}" +P٨.Ci(>*~Tkх=ɻjG/%ɍ:d[u >ؓB0ap ^Q43[wTٴۢYQEq6(ۄU:qև9N¤]ogOPhE(CK_ %:tT̕lϊ MpV>x-f2@G-hqiO^Z skbY#꒗fUG8O,.ou2( WoDeg$%Β:wE$EEW=6ě^ fh5vŵ35ҷW|?a+?Od谨9@W!^/[C. *0*~|%>|tG[%"m&?5NA.~3z۸&zbcA^ZXc-)h1ޯ4-3'u'س>za#zrRIq%V{8'a^uڈkj\E^Ҷ}%1G?u^O={?zUh>PQ lp"^Q坧z/1V/uJoJ:לИw5=XgnHQ9:'l8hG|&REymA1 ߆b"^ǝd]E2`gV=6aws}FC|)e) y%&K˞qv]εfgvǻr%U#k:6J]Ck +ػqh}]$õWqdˈ3c~#$~3{pV!,|#?n7RYddM9a÷#d_'qJW_uWrFpP1ߏu\K̬IHM +iz=#զ~KMsg+D3Īa֌䣝qC?Ev8?K>\GElC+o{w`ך.* _+~igZS# s~Ɣmm7aOG1IcR=OT>4V875y'4fJ:.M]g͇Ke-/G_6G߬퍽TRRs9|sBj`>I>1g[6HtAJV6kydu'ɪIѮڲ]Zen1a1תdwb"+[K.D׸DU48E;ETŸk**\T\ LoJ0~!i{"Ú8%xw ȇ"ѣ :#-%{A2FvP$ÍOՑȀ;H:\e6X+#;z`>` ǥm$ufe> !͗3϶\}', p p߭x玘zunF׭NcQsdQs:טB82k㲫ҫ}jCކs+UFhFD{}w*|ZJBͻbD[O sG+CZ6Q]W_,% !5L]y y0dPDkM8y+l¢x¸ mq)^ 9۶ ٷ-4ů981Kv-n18r,L|VWP!K=ڝ+BwH.oN'qu+Юオ}#"BS/7=SXU`rbI@|v_[S,ȚiG#cXvw2#BC;o]-Z/Ug}#qBǫZ1^1w\cJZ2[${[3OfObCj/'$W^+u?՜{9#h{L:.ܲL2!*i b!k˶m9'sS(H|k5,}ՙ} ;s;r̤9f>zPr^wsLiSEkƐm'"s2=m'mI"|Xo^bdobROM-ɯY^h3!6fHQ+ssVO}Ǿ|0`Fmx]+qYwSc3b/^IК) [nQSd%t˕4Ϻ )7}qjN>yt:ڲ8ަ0=-3)(ӫ!:xWN[Eǝo:ڦA]WnC{bbBOb@Sp +=|s72cxotQdle /aj|d0n*Cbڮ܈!G O0Z{--sʅ>9b1# 1'. (ǢWп=kuy)r8l,UY`>Ps>_;n.4w}Ga~>sofisl``Ftvfkf`1s{Ɗ~ެT/{2跕.QOKcҪ}+/ՆFH͎,bBtx2РkkTBCp8C>- W/z7 L3;0tAakдRo /^Z0L$0LT*+UMLA('i$.icty__g`uW p+b]hRج~|e/^+{\!{S}'cw7ŞљU>q5IL}._΄Ɉ;١FYn^-jb~jm~J^?oڶV,Z )` "{WRw9 ϊIo?)M׀[E`,* %> ſ /X|).?[ػE>/zy;;e^%+]/׆$S_{́/;^p笆v XxVl+L:S97ɳAz`2+7>Ncf׃;Y7F裬C!:'76'%Q~5aO"“m>zSP)K,=֕KBT\xtg 7Pܛœf6c OJi񋀊 +0Uq9 L̞֪yG(ie6Qo=`"ńXxӑaۮȠ} +}e #נ*en17agrV|%xdPTǭlsgsTvՙ,``M@yFOfM7K,jv*gVs{[ۡvuW#Qnvl +}|_.,:P}e+{#-#]Ω +o_~)hT銪pQS~%L@ez2~1i+=X~:`+.Δ? cIH|{Ť"Ībw)Şy%-} R8z\LXIn_~ gqipUplgmsVhx`2TiKU$` wLVlx",[]}\%Y\B /0+/wm|#Qfs,\~ٶ0Nۅ83(spL3g9V97y$``vx֊8qLUm0w&~<8Yb-[^ My[/oJ[ߔ>Tx$Ը% 7$46Tx#Y3tSmϘ&wYcyW9U7oo3Tm _&K7ف5`]=} ՚eNGᦼ@>X;S}[lWG2sݍY]u]ͮ+=`-XN߶͟mx,?(+ + +8f(,-SNe`m[9;}j@䀹[`;X kdO[,uZ%,;X.::]hL9̽`{=1===-g̾9S|CE +@uئg'È={± `ެ`R ,v <xZ^`3X},v1`=S՟0kǙPߢg/=Ooz]TjsɉŰNhre!E_]Tì3gӿ\ 0z)X6q1ˀRAڦ +ϐ۷̞ L\}#<f@,`oKx l-+qRFv'#_~'y VQ-CsFvzԴ +DN1x8Z\p{PXTnbJuAC0­p3 } +[nTj%:@uX9PXŽ{9j5VfmZ.G'}gkKCosOmpKlrK_S֒d wҸ)?cxXXCY}5ٻτ εsys`R.XKڂ`0l:@~Oո->f1W٪p]L&..)'NP݂10TwIcSTT:yڮm<"/9ͲP q5u=-6`XXK (m9%,YO%E`i +6`g +[#{&~fԴ>0QƆ;ߊkʉcךRbڛˉ'Rbc2.D 8G& 1Skưi@ٜ"r/g(-9|8U:S69K 25zx rh"gjO=fVOۜA}xUMsA50k52&ӼOIKIУӭ2Tϡ嚏QZ=5-v3# Y|m3#Dy``:CAX;U6Q` l *M(nr#jŽI;'p/өh^ɚ ~2YZ*FFװu1"62bP΂ڕ sgڨ.ƚ4$7hw{(lՁ.1C~}*̇gYjXy +lz"$ێ]W0e@%Zff-}!Mw(lX=vוxUN9ءuV˜:P_J| Z/bQlj^Dj<#?8iP0=nN=lNJmo;N8%8Dazi\+X3v+u?Xwl<` m`j~' vb!r>Bͬ/թc AæfsEoN?\ ;J/} o0ۍ+!5e$F}|o*N;hq3a폣Too]DjMx%KRUm0 Ser0oNb تkva~`OU]:]V1gDzaָ?ڜ83Gu3\ ;#1X3з bڄ1*f U'<gX׍=w϶f뾓b8'yYvK|>NnS-TVn<eZiwP*zWcmk1{5mY~gLb̫>,F4@y}l׫btm99%U>CFczL^cn` >Jt#ժND"r2_|"j:`!`KZ"pQ5 ~+̛m*< XSW;h4M~X +>łs>1ֆ-42c^3c)a\Lz*`x <킚߭/GƉ3c'/p1?qYx4J *`/c/wwLDJŒW!^s*m] }|F +Wr+r>l*-S:zXl2vRuz[_U9BU.s}U׌VrÍdprW84xSpAܓGg΍zO1{̨1Z7ł|X8) v>lEnM_Q/,JGܑߴ-Gm 苅 FЙ܉KVd)Ø~졂4_N=T˨~d&FÌ)bo>Q_#/M߭ v8gLZK=Oud:%[ >?xskǍo?bԹ'g+/Jޣzn7d">映"0=`h"IB}Dr>7MQ1OxD$eӱKlgc.]*i +0V9/ZA_W._ 1?jÏhr^&&KFO[}%ԃ1v!m!߻ɕyOK.?4&mruCaBGl?}8(k%X4p6,h{K!l$} ?XΚ,\ڃǬ`X_Akt# eмY+=-0 +382;c%_q +yc{ѱf.ĢMG= lT!#c K1%Kh}:d˭DffEƮvzAp^ٓSEM؋Wx7kt''uh׃YbORR s"JN#AU٠9[g񽥢{6v,%&! x1ss +^\2g *sl|Dd=-} x\^(5'| nݎ'<&Ly6E4{]%w7 iry$090Z/c I)h tv5m@ؽB,;'wSl+z}7Xb,j"`y*QveNKD{0[M"a?tG-I~7!U'c rt4m+ 4y/ ~0_N0~![|4u3NnYO_z'(o=VcOV$`umĖ +V6g=T,NyNCҏͰ|,.?b,8qO&L w#~|B*vu<3Sٲ1bjvެf=`txwC{n}uf[f012͌Q)cy1%chtI1K{ +`E@s.`q ysbh"/·89}^Y@VJ܃Uɓ>HŒ}Xz"b3i7G}eq^❥er+ImRZZB27碕!W>6EO:nOkNu,s6w}f#s]ǯe,9^9 M1l6ڣ4B{.óԅ<'RqHOMF{ȫM7>\ň!Ҧr}~9\+\H2[8hʕ<:,z*6YP׆ ´d֨6STx7~V^,ZauoF&`Ƶ0f,8=97c6F>Jjj\˓!vإ $@W]tEC@_\ +]d.7[I|=T^yAbAg-{,BaGul "82IxuIQY``\HQE7b>)sqeZ&׿$Un^SskR7IH_S]0$vn&/:FNr;]+Vw#8p[qh/bIU~ kio +!},+y=rϘG&C<Ca-m]Ig>7-AG5AMAt:  By]OmEJ.>Lzy/.͉Zo@Uc SiL +]Aӕ)y"?*WοM@ #C4aN7T13W:=Fgᙕ¬=Āޮ ٤_" 폧._Y<^hK>z-͢GtmC<_O\~:A<7?bxh*-r86xg7t隊vBPxVrB8e)jt + M ־C@k~`le9\&9✺4Md}\ mE/SwKr{ +}m3LIXtH7.i0-auRǠtY :$OڨMO9B&h|6IK~7\g +OLDbx"C[>X  +0a\ xR^2]qψ :ho\ڪk8)W|| +~>l:~A6C | ~auaOJ<ͨ؃a鰈WM1e.3;oṟ%OVW/%1n庂`rn"8o{a1&1cs weذS{ω|sy+蓎Y([IEqY6ʯLҗ"nB{$nk䯡N*4 Y!8?M=}xPQ j +>SwpՎHG84.QO7b)M}A=vYM\A4!u +{ɷ>Ľoq\tԹ8^p칈xwDOGۍh +7bHŽ{NM"2a<Y짏 +\U#25=\<_mh0m0~:jYt7|X*2z~?>tTIDUœ &fb!wVbVeY{y7 u9`۪`Ì`M`СC@߈c +pz<< tVFA_mZD?`~\Tf!<]11{DprzGrFl YFy&EL,FPIBv[\,6E Ó>#W RJM]V[C_Ve2+gģOx1)cl6-\ jFo +҈Dlx%i:Hw q/.X=L9h?AWV}0,$eoaZ>!Q|JIevO{\6y +b_lƣn$  +8DA?E twey"v,p mz3g%CG8=}Xo܉ 1a^82?8wؑ߭e”=G{JL%jeIM`DP_h ڛo-_5iӄ>9yÚ(߁)X`çКWl%rZ+3wYsM̜5C]GR^h kK{IӊHeX9*N13'q]ѳćƙv/%KI 1pU<0w)rhP's; >W*jijZ5D7ĝ6 wP&xh;B +r[K9HϘXݵ[bah*p9(cCjxlvaGKT448`@@:HK 0,$;ET.$ 69ݺXVoe2KX(=Lj . aq0 1Ckm%4BXtH8S^ga}AB_AfQ3Y§Ĺ I)8H醰:S ǃI_j:'tCxHi3!71\FVn(@5bF!|c5y1}1b=[{iNGQ]Af>4gÌB3='Q> Pނ]?DZtwM`-j2D^[l149| .<8FxcmuU%> LhY:(Ѿ) Gq^' lt )`LXC銛VD,g_[ƲT㥼t` +::tgr )rGW|ዳw˭}GǚF%]̻"^9*߮'Ѿ| =(50| k0*QqZ<nktb)IFHXrRgqVѐk % - UӤC,;DĊB%O:E:Eq[rZjqx3!zQ0'~~K,C {"6*&bk978j E\عigL$s Ro9q𰣒1B + N2 XG `q4P>S *ˈڅtP +` Ⱥnˌr8!j>X-Xjʻ8৽' l0?ucJaJn1~Wd'oBBHXˑ6cQ !SyvʎyfbvTld.@1( pSJH)hϨ9H sÊ۹Gd<<V.csӉ.c1SD!V{*xu97ҨGdTS̻{f'_oZ<s3'ӛ5R z7Q,[%'>=T#+af}Q1Щh +wIj#~#gnf V{}Xj`. sH-!&7O#~)bgay6 +@mcvHn6Ғzo=.K^_1wOL0:+ظ\gcG*΢}He> ӫK8Ehq\9HY* +[tɜA"oАm,)r6`ycJSO6-]tИ4&"}e5O!R|=F*CҕdI2H Q:\Xg@|c+{s=XKƖ> $Y\ZJ!'¹Q䓴P9WU(ӤS+mbs2WX41dΰ!7h-:)AظKW#ĥC2f2;>H/RgмGkN-Df8gSOj=H(Xݫֈ{ <'m, pg~|1o˷J ZM񈢍#a&ZրHwKCDLH"hw|򰼑#m/n_]I .&rMC63Ȩ., WĻ>=iQjga0/?lbj By.>eIQ*u_x(ֲ0".>0~KҠc\g JTgOLΕnD_g6Doq06Wj6g=&#m +Vf/Dgd{igX;sO m!y1j6"E蜨)kױH_^pAaJoAၹ+a9먘EiuڈKeu"#ڪc˝ǢgH +--_y5q[kuCwm̮+'^@k|suLüuIV9 +圬^1Eby؊X6Sc.WΎA96EڍY,طig#,{M{GX {jg'al|HpJBSBeR +m(eV1vMlT"gBLo{rF:[0NiH5rrj:h7XAyZ=,L'&Ҷ?YܬPR^34w؝ YGc{.j|HyNBQ⓮jgE/ s 刴Ez&cN 酡8$;?OH;e:NꖾPb'3{4Pt t,%^/qw"kߑ/wx8~~-sbV#&OB[=qrJӴJ:[7ew;߹͚;/}44n)E^ًϏAk£R”QGYB etTn.sQC~Յ1Lh'tgcO64/]KdBDh}Q-tιB`jg (%hD.G +8|_lQ|X;v/u/>7q|4f=b抽 It=w i|sA(o\bٓ]DֲmIu:VXܔC2&7R4PNE=&FXm,%hZ@<jN:$ +p'7,YTy-=\N󠝥e(jƃjSVS,%֣g,?va--%ہy\8&rO4c@={+G-bt>L%=<Ǖ%r/ET4E1[  e%?@Pne +@FϾ +k-E\Arrۀ>xPm|F t ' +hsn1e 6簇1R|4hR\IC|.e4V¾-T; ,E;˳jg#]\$b!CokJRY-wQ}ke|SKCW@3G {8_!ԯgȻ94)uKSK*k2ԗ[8R9'8>f0Or},jg5$TtͰp  cn-wvKJ1RґRhXCC3ŌˋqgARt b㨛(C z9./1Qzx-b=fHAcEV^߂=wq)WdktZ!iGKFP gPu +-&aErY{EqN~,8[M6MN4pǘol|{+]!U |2rlTĈ cb{@_|#zb\ejU{~HBS}Kc$kTg5Oa=%U_R⩹"UfpҰ*CS/,pDw-`z, :N}zu{"U]pgRۄ?ESG`|MXʛ OҙA?N@GLvFƚ>Ƅu u)6|Enhg)gť>z"T6\,9_NzVrrMzޣ%d[1[IW卧X}h9 i亣K"˾ZUR|﩮-\vE U~qŵ҆佒Oτt>$şڹh ɇg!z.mP}'c\#,!OBI&֌Bo5=ttP%'=#k-Lo.Cf$_f 4P7 QSqB'Y9=&{E5<8j[zxmxL:>˹51|"49-|ŰqЬ}.S DNI5 +$n xEqz>:~tSNBMC:,)׫5^2sը j|A 8%| 'vyu!B igˊ!yB+MI>2'jgEm#s* +XMy߲oa4ubT>l.Rbc̫КUx E|mН"sK"|YJHBq䅡v9a]J8;,yla}ݙ n6Gک*ؽukH> _$'cIr}uQGМ9r'CBLM5CAw#q2%a|؜;Q d Rjyc'A +&Q99M9@rYԢ8{gmhm~rI'g9+{AGkbѩTB`tPF/vu֬!j3СVa փ-@  ,.Bž򼻋f1Ƥ3灥Dd} /ңkBĿ+-gu&+ ++EaT&2﹤8I޲xœ'P޿{ϸMTn]XS__ĕ^Op3˥XD}.0+ޗ2{ ' q+EVçҵik[-佈xU{2P NgSܲG/{44UpMXc{l&Vd.2G1tP (z X$F#ܔQW>#F#'E{gĞ% K_ Nвw qy14~ 3y6]G$ 2 y]romrr Vs@K%qF=J S~j})T=k~חb m;jȷXQ'ϿOk=#)RO_.ϻDsszQ9g~\1T{HB' +$GAP6|bGf&I5ko/Л|p;{ aGu>3|M 3 9.;p[yb~Զ1MV-;K@O lI{'aŢW.rgoH?9WF8WSxL]h:S=aW ̧tnQN>7꼢fBp(8zA8sX{F'EA-d3/8=uR=1BuɽҵR&V<5BE6F{` +^G Ov6)f&c+tA_#{  .ƥ㳩&8f>d"ӸI 3gs^#aKOTcM:;|aaWu*OeF;}80GhFM_z|=_z|=_z|=_z|=_z|=?wl[gZۤzbK]::sW{Z`ur<mo^\oko[y,7m,NכE^:o΂Ezsm򤅍mK K-~yK}?o9,Z^p΢K9xz1}{7qYgCLg޴z#cֻyr-4.zi%u&n$_ͣ8oƑH((,&M ĀLM3%4"2Fj$ck!h(l=%?s- gAX:D;Dm) YMQ  ùClܵ fF޽I])C92Uf]Z6鰕Hp [݋UbzK.AEϨ!i _.6sRGHc!z iPIIJ7;p[߱"7sIkĴ Icë%(JX%![ȟIrQzx Pzŏ%:dNm+FaЦNA9hUdC+lhЏgE34BpncɈؾ~G/]dQCA;^ 60[S8b#Ж)4(W|QKa M݆&2ƌۦf |ؘKM̘[h [as>׈ Ra<]I@XyFF.9kh/rJJb/% >Br#|0|#HT9G~hK&!%8$Irǂp¼__J'6/"+&p2n_\P9chs#vCJ@pw.%"NmGAl CYchs9)JŠИL4|ż&1o@,#RoqAG2Cri&lͷqGZ\i  ,>62l<hic(5bd2=dmė{Ak8|?m9p*e l'qڀ݉($#w 2^9F1%kN;_BP/H|AկCŹtn7;`!62.\Jxfmtcȧ_Ļ$ 21v}u㳅sN]Dw A%q d\=I"QFo2|g] N/EB8ئZ tMdhs-o3h8qO0 ;/4BHRD$-n|*>6cIr-HY~Ͱۣ _X!O3j$3NÜr|% +7]4I%zM(ß 1gft6A&=S чs3A䎆`v8\P7x(@ M y@FϹG@\ LJ + !qWpj$V >J$ߠ;#(IEqj;1r(R/=R=j[[>Ch,`~Hd@G,//s[+麴I$$opO)a HMf/9,D~GEULIG"~\ qh6Om3JA>m$gtBȑx` H\VnNs=\O'3 +0;$։[ +!w&(3Me$WQXi\sc-;k$qqh L|b2ĸ30An{īNvnd{x3n7|AhNB^M;(<{"_wh4!}Yil's֣/i@UXħ\{& oF {@MBrq HT!S|b0 nǜɵ F j+l #T]dApqze,Pl'>hj2zf@I#Sk:()%q |M rw@|{b{k.Nkn `+Qrۇ`a h<%&$0GuWC鏹ۤ> >y!qYyk#`n٭ ex;M)?6O| + 3 %sH}$6#9%c&hPL<|خ_ob"_\ v>1eV;{9F|,$E:DyQ_k)K3gFܱ#lQdIQ\#u?LJlW9U]l*\w{>5 +NQk!#w%U6R>"?w4!le֟(Y,rE=! .v_tPܴV([ "2FWB!/D D\K^=C?=O8˺XƠ02Į0)9㞾"rN?ԍMb Hu{[퍜dU W=3(/& Bz$!ul4y~Gq[`_&+C;#i$Ԩzh)hQ @v֣,S∢o}2Ph,|O 34Q8p%<'~s`њ>jHzd}Se@"-Z +$_jJOS}G)J$͡b@hxd*)yuנ"+Mkll"(A rf7uM=CQ_D9?1B4v9|Kx!N gf%/VqY sa". +~cs+!LG۝ELFrx޷ք1{PBIF޳Hax_.C[S-*E՛ S삷 Z br:ag0-wF!?ڔD쐒s03;RQ +pB )qRA]=r,% /Ty:͛k 5|5 g OςOT[ʥ^%æÇ(HmC )#ʿ>w +ȹ9\܉[ 4Ԯ$=|@q$ Bnl ++I)Lrׂ5bӵAƀH&q +.&%17o 'ٔƼsş:0ST4Éy [!<1XO +-@I <@p)B.,>Bm$W #Dآ?|}12E|9oB 8s'_| }F endstream endobj 28 0 obj <>stream +vbw3$_[ZwLe<+`%nB]T '69Wr2*v@6ݴV+9@:0GJ".3PO" $-.fiLui" yC;lNIs5Ki@Bqs"PQTy]LyJk*[*v!}ߚIl"gXk/bQ:'~ v!ܡB*+z.ĂBqZ\C9; @r/U(It\)9^)w @ k ,UX-|l؆/f]׭@ 6q Wu"k?{lM,4 ֳyjB[`A.KV.ifvۤy5>Q@<ĺ k.=.etJ:@yw_꟢+_bJרA~ؚ|кG +~ +B-$֣Lݽɷ*3?.J׊7~viQA/i$=.0R?]]G*ݨIdۗM !%;RA2P7djwqG +/}-Zͦ_OQ^<=Ct+Q?1 +9=<ȩ!2/+ kE.خibjo1eHޯ<ȇ)/BuFYܐY&ⰨZ8@,WĨa  LIsz!j!H + 37Vty̩؅f[ja %\]Co輸״>ݫ/ƛkЦ_ܴqDg ?@a!-2/󮯠~Ne\\e_[ H)|}~Ajh^uTJ_lm/),oRVXH:M؊ '|ܡ)T{]- -cUO@D +~G|*v#v!؅۟bY+-I.քIgjs- 5CGh5J<:+\{m(ui1$!p,[յBr47%#*1I#!N +dW3b\Xq.`1o%jR>%͍Mk7271&#]dK.QòP@Xɽ7'./(ݳ7Ori\TZ˸X"}F?!mG~of*vzzlF /b*ő9 v Em͂' ^(F{>˕ չ'u$%ǞUW@suqs*T;D1S k^j7b|!E}m~JeuPRϾ~jri.~<7œWb3E}XQl#[O uD2)%)Q2!,!")LH{{XVEkڵlU_Ԟٌ[ v#B;yaSKJ89b\)tcxBᕓ|a* "5AWJ,kȦݞۡkB Z*rtI$iRX_<׸՘ l9w(#DDׁw& 8h8YLH@l=bh$Mȉ1M{&Aػ;Ufh1B~ǥ#/oI^p){X==BKg*DuuE +HQ +B}&tEiXPG fA\whӇ}ػsT_:yj*_a0ಘj}3̇@ &y_:*rJ~9{ օOPX{b͏qc:{{[KabDuYh#Oxܑ`*=!fpP>f0t֢8lCJtOKz/䭨J3(K(^+g@$؇|fpz6kXcbBE?r" a͔^ +z(Z3l4<*z:ֹ"&Q5 S'@~a /o[ qJaw05pΙ!h+ NACr. j +O +QWL )~2,ɧf#g0UkQ "Maj=G˄ki] CMqniX+7"=f 1~Y/`5 an/b_o5-v% U8н)b]96M/KEg|3GVD>] +H}#t+}&M?~w +;Fݣ{QPGY:쩷qڒj>!>tQI4/ ɉ33S7b_R|%dOLP&oa/|ՅKbN) 46W&uGԌ#K'PD♢Olt5jDܰ/z~u!byPaOXGb`lهSL  !Nce&YEJ.;CYq +I#4.*;כE-+ݧEQ$f-:*ɏ-F #ŝɧKk>b([VN"U9Ц4/(^.}V.B+kW4:8-{Fh3;7 9*KнA̒өBy|iZ +:qkyܺ\̻ +/{P{RQx3bʕؗ(~F4$؞z~cȕ[ϕ<_u< D\yJbem?ski]O9?SQٹ+n_KΤ#3h-=>~ _ClŹl%kmkʶMbuPٲq a'kEY*a1C;BnQ'Y7p^[^tHS8y*7Yd6wV+KNp"c~e$?a'#űO&*[^+73/G54[{9j~5X~g^v߬W~gRm.ߡ<ޠqUo6 h\!HqR8Zɗ_`_|V-3^Z;ުunז*H~Pofv1_ڀ??b]|쉩f H͸E!J_ +7_;J5;k-XG.[>W.|W^VlpLv["2P3:2U{,>`ky +&[޹-TtmduK۬īOW[\dE~{J]ʑVŞ{ ;m[m;ݹoݸ?;(nVq7Kٵ_ܣWn統qb# k[q0],SδY'[xkPDuI8G_cYTp鹵㟌#ӝsT_mG];\.ŕwؤVT|!{'N 쭶m\s- cKͼNn˗Vqf6P˰I}9W?m~ +;u{_ݣ,xK97G3]_ZTe,ߖ%7:'UwX N͇~ҕ&g6Q4XeۍX7LD7^ߴTvSyo6۟/?^ [_pEHjSU]˳3En:][SyV7IζIr?ݸkmeUXfuQ|p={ꣂJz[u$KvN@[vO'n'nVZ{Vj3z孇w,*^-H8o״ؿ7m^|z(U1U|+k^&^Yvk/Q}u:\}{⶗ײ>g((+zqa +MVv)/t9+e<9˿qgv]w^(|ۘ!tI/c|٘tn87S٥ +3gܫ!m//dkNeIㅦUJ݆mst+ -Ol- oJ)^ZdTtgo>فlw3,}鱿b"~FBٙ|꽅\kCsOv(C[V>)K?)r/QQŲ6LY[$\xc-Yf\yʝs?ʕOj÷Thr6I|e6swkQQ䓤'IE%yQOʔ51绬W4dT63e͊N|X՜L͛{Wkg7{K-v?n&~j2ƥ@G.GJ]ٚRwyÿ965ڽb}&{\a;RE)wTJ_ar#m*l+ת΋zМYY>$ =)}o?lNmxZK~kI?Lm`femhVrm\[Ky⇦Wnq\q-cYpgߊ⹮O\/YoP۵[-Yxſ||$M\~x&z0mx`qK/&gUۃTwaM)%ͱeګm{y9[^˹_Ϸu$ǧ#37qD'>[Ӯz!_o>I'Ri2B> +. ˎjHU}bR-?n{p(yUytaMU˽$ΚLnZyŻRuL:Vyutk(D&'?VY?n>sEQR_6pފB[kgkI#Vo_x3]Mksק}۲ +> /r*Qw= u:^ޖTќ$D}1  8]{䯾Zw&( u^mDc,SGz-z"=|->hcnk]'dץ4D&%/߄0ovNS򣸂{Q9{Ed<"U\V[HMcn#F.R$=N>H9|KPhWJ2?[y?^wzYK`㹊?^v0{.w9̼Ć܂Y"rܛKLG7x2avҽe7ݒnn}7gnw{5F7F%Ȩ[ڒ6TFݿoj}79v( +|UUST(,Ϸ5L!Qhn vOBo[Rw78ӻ9yu6oؽ:,i|YHSF\ٮ@u+uv<ݞ4Px7?'6 O~\%-oaʁqkW}GCrL}BnyM={aLLC|_g[֮ N Q_{yNhoCޏȱ}Ib{ yoW'c۞Oq } +mn{#Zry]hSnJi*$ސxh_v^^e2xv7uV40,YYd53oя%yKL@voPTZLYneߎPJ̉&*Ox&U}.*H+,AXr",Z!׈~wڱN -F>/6VYk(231{2]f3<3~FYFd6KUaZW̔㘱1.3Dft/=f\)9qW2Mݘ5#7][OmݟݬF޾q&0V_ua99Qy1 Ea wr1 SߏxE/Pܦ6=)d.̠dӁ?l +<4bQ ]äz~-68۽ Udѓ=F2:f ӏOOHfp=fR3'tU^.8~C[F_wcJU1\l{%Ltxq$71)nTV;j*oGf Rx&,c;3n?({U޾;[/}x3ѹ4Nt/^qckSY157G5WK3e0L/O\h~yf UOZ߷:әy DEc3wWyԷ9rGgݎ{;2k߭7ò +Qyzhq27ՄO κ[ېX''s螎$?ۓU񏖎̴1ӈ#sCJ^fyL58zhҫ+qt>CALoA䧑衳91u?;([]{SYogT ϪE|L{yo.E>Jɇmy~:&2yUGG"֔)&vY}c0zGkKϿ79C|Cia;|߮5?]~#\3*gwRu6$t\t8Ք$;ueMb dcO_Ⱦv;|[!Y-5A9]2O5 tuﺝ-~{rc;f(Q_n}fDcf/_uoR|~8Z|9od_{^p9WoܹsfhV8uh}ZnJm\Nr]Lve=ɻՇk3k#p{#VjΝ9`?z4עvz,7}:z%ÌfjOfƎZLYb]{痁֯%\Q|ح, y%^YqoSVj<AHfM=X;Ihͻj/^i}Əz{?_z{;ӟܡ`:{{3df$fhYhM̌!7W˿DpĄSm凫mjc._YJ| 0>[oۼ2ӻ=3n /`dFk Jg3zB˙1ÿgF\[̌Йnj36`3 $]L^YjY@4n֣I.ҀN\*$ٝ"+:ͣЊ !{67!ZS׽d`Fi1C{"? &OXK&0#{Mc 71p3z:imf%2_5Fx}#(Z|Ko=Ww'aPyw䝸uk|Hݫg]?&|D|/#|?(7d_Č]Č3V=3~)3f3f)3jzfwqS9faC`ΪsW2$gQr'65fٹ5j3s{E bf=u^PFߺ.sJќp9C ~/ =>ghd03Rs434fй?1w0ّ3vȌ`FO3'0#Ggf\fE/7붵i=VWTx)!5 +Iϻq/PaPM~y5= *&5{rb՘S6 zy{=!Ìό85lݡ[ƌ5|3bOMbMs]q̢m't55K?u? zjD[dd= *|X1y Kĥ}kV010-!y5̹ZsAd >gk[YBs5IcV%nwk}f:_LW3m.jHȕٷ5^{nFȈ{ϰu{R"vQ{/(vAXkU ґARl ņ]cXc&{[v.9g=?Hu͙k˘?i^wȹpin\/F_4tӢw[=>3,g=n7{D*{A Q䜆{'!۶ߌ3,&Hj:̰K9iv,3nI3s53ֽ4qaFx2Ťq=cxcX!p5?:;\*|x j 74]!uÛytRÅJ#CLc)͵#eJb#N Ϗ$9f@r~VNLQ7GI@{|ǩ(f8f"fL`#3ʷuإՒÆ $}uMF0Lc5ؠ3Y:f @f_fP7f(OfЌf2jYGMl|d0d-,[ϿE=^[uu}C'[vF>S{mA^wڿUM> +'yָ\Ha=qƌr! N|Rf`?IkÇ,b ]8 reV0ffjfL_qxV3Z.8mˮfy4(8UQ~aM_^.zhK%[7~ S|l :N+I_=/ a뚫ٌ_ϫ'Y^ҒfYC~d #x(fg3%;QŌq rT@̛ : +f5CÌsKa0ӄ&yG&꾱aR  ˣgkķm길ڦ'C~c͛9_ʮk^9=29́ |ؙNec87\aɥ ! 6?o~oZ<{/4L\~C/D?ܐe95OvRk7kxy混}^$߽mZaȌ 7=ُL೙Q̔S>3seytzy97-j=kf]Ͳӆ-=hpr}:}a}C b w5"gy%sm +A,cx"'~be.3/_ҘA$  d/Kfqcf7qg<ۆ9n-~on~fo.zm×6"_ !ޥK?]- {Wƫc+/K| +lI+HsP쏡W:1O`SUC{XL=*f%LU3SӘic<3~3a'3e媍楞5?ykaW5?^TKo~ Mrs RAP2&JK_]꣭//lUpmsJew6޺VVwF;7U=<#j 3l\f fW<•1|fa\k~jA4[borU?f~J_ + *_ : _^4x|2/+W}yʢէ A7oc_hHS_i T0bDXnvC^v.M'tO>6Pzh:w ^Sf)a6cZۦ ɳHg3f8/}ot)5+jH +! \M!@ξ5}kS4H M/*ˎ:(:sVGb, C*N8ߟw-|NRob&Wwn +z~p^5ռЇTg -n5NO>C\}wFq \?˒>&Ù;QÌLf\zѡ~۰'bs;,h|O6o_^1(6R$0UJ3Wo9+|\.=~<:GaòLc 76֖ 5̓}^|/.]}FF޷Kxp{qw!:S9U{F_B=H̘2eK +Y~A!!k]_J_ޕo3wL&0),+okEtd=i U]rસh f8凿y}%rb݇_b2>΄r/nwZ-8C^B6qUPVfE)̨!4Ft?ҌkBx6rPs) K|fг}Tuo7ޟYpu/ſkūn<-Nr9mQ-2j<_^ʖkUlnR_[ +B3]B 1͵_>Nb@2j(`0f2\鲳⃕o$Of^=ѱBx9XoR&pLUyc'3K&f1!pBaE}Ob|aw+G/ޗKNn>'7 +eb6 ?U[osq yBnuǣܾ[sT&*:mCx`~k9'}Rf k!({仞EJAO}b z]|^>G00؝Obqkm vBI?u< +DBh:݉a7BˋlWwv?Uu g& NV?\mS}e J-avcK̖.U0^J4Qoa3~:Շqx-^ޣE#'{z2n1>Z3}IhJmHIǘв ҕG?c_k?[eF]?2W'V[~w]~*}%^̭=0+>_>:l<{5ߘvn`O\At?tAWC?]CE[+w2?jz>[?}QO)GΝϸ,1')^6tUbM|pO#/KG +jakTe0@*h&2[5P;7!O%r~TRuO˄S q.cS +i\և.¹w*c=]jy"#GS +OZ +Ɓm}N5gjUflH_i̿R{uü X K2էWkk?-w;nw;y?|G;_Q(:X,^/Kkw:G-ɏw]\:PxDn[77"^:{i*mHE}ƞ\6D+#-vUunKѽ3H4np9`ৠbbbM.N򚾚83+_ nRVם=L_ 6}6S+_~x48cت0g:~_{cq;1`r)N#A~]%G:ZV?)5 zN#W^cG0_a5&Bqp+Q'<{'z[ ]ݸ=O/" 373k9$}n;/hW%w^*DU4wZu7OߖqVR2$[}tg o_'S!PQ/u43efHs8/2sdS.2\O3u],!sTK]o+$YB+ KmzՌɶM GJGZA\Ep>N`Bfu?9 끻̵gW!܍Ta/I%G 5'|B푉$OUvջ*;S{zy!h; PERWat{^b()@w?;`!`7G&kvޘ#z$| + ?yɟ~)+렎+P^`S6󻆂GR1w$[!G'з"t +-2k#$2P#kOLn.ڟ.|'Z΃ >N> "Tվ7B-=o?y!?UއM̷yz L8ꃱk3T-Pu:+0=~3~42f:p,f匏 ܛ6Iox{*o_% O4aA6X`|1"4q[/͕:,;%[ ? U{`btgZ +nޟ7ߝk(¯~Y8*H>"B}^ ؚS\:^/PW7Sƾl2,ӔKhZg> 3}8feR7 4"׮?AvS讁C^3A +zE Cn880B(4H(mu`۞,}4Ui$xbF+hJOgRf g FUǨw};}p;%'wn1'2TXmVYAA&9 +L=uH8{?+V v NLޟ #>|r-_d;R,;n4 }8|V;y;B>H?+Q` vE NDo"ׄK_.hNm˖t WUfb{Zb5 6˜rZnVw ;d.[,2YЛإ TZnF2i5kN1%eDdY3SU0P;8:B#E뢳,;G76_m_w4h~U}R 4#OT꽯T\Ɵ~-h)㺸s$أMbluEd>.)6}2oPyW}_-uv~6P:U|n>,# |Pe0W.3K\2lzkeo};U'ư[퐋z.fk0 FVG#{}CΦ`CW zxuעCa}DU~ ,:hqLw JH%H|i^<+&Q~BF;!Pb'$C!TT`,fۣzupC*1jՒ: Š]^h31$E8|V{N:x_hP`=.{ <}o}}b΀a&A&Z*iq@nX~ AAM jh(\BEJ` ;2Ü-ͯ7mWiͱpc$3\x\}8 VPHQGyILcDC`)`ɈۉTsM8k\}d2e;2|~&}.ObNK =yȇ(XKǟ+t/Xrz*+/z 즏a~1[xnJfy tq3zĺDceX"pF 2{*op唝[jVtKu|;yTzt`*qMg Gߪc?~Z u^&9eG3AH#zbA'YSUklj +,㉯Xg'740  1p'XaKġ12J9UeyA,nxH sad t(3*C>·K/@`xa6.3:|jk6$Ƅ mcJvybV{&8H$Q4{!MC>x!gA!+)g19JRfd3:e\T?Xa,NcI`F i5^j)r$ IUƐ})BRéyჩvv=6Ki> +>xKOِgcM叟W%O5<8BH(e+Mw Rj`65?nȟ?~4QEduI&>vuV\䐚dCRT35ṁ2jo>GwTk>Bمe-#ɧg*1xkqdMfJYCNkbNļ: -GƙI*} Pˌk*K/+T< z`i6J$Ò(tmrAJI4Nll\sp2=ڔ5} } ;r30Cnva`j{\2 g'O"#Ė[q+-4 \T9ka棐Hθ̟:p3Hgg kk*ΨA'>؏~,fRٶ (ܫP_4\ˋےaGj ] 'X[@AF=t4qZF-MщyRRfFJ-WZ/.7~8v-D&n:_l/v ͵]Ƿ: aڲ[t +X|ڽwUoUlX)9>Z{2t=|W~4w=q,cĘ +K1В#OE\iT" XqQØa- l"q^T1uX>O?s_xڟ ;\-DMVT1,Cp|cˁNN7_+}\|뼫麭~"4%#tEuC5'fחɭ7܉mМ6BŮQ$M<*[hbm{IݯnuR𠅊4?fXF k;.چ[mC/g-\P m-w]׈ԵJfШ֟whivTs\ m-qg-CRlzgvwj-ɟGP^ߑ ++^Gw!w= +Jztऋkov"RTYp6G}`|xJ!F1"n7x|dOzit}tY`|PjwKSzqt1sO)ŭ56d\B5]}_] t=X.7̧ڀ/B?B+X{Jw:xΘPo91};wܛOl6;n/@ ,u=``O-_."7t5#츹PP\Ih}D㡏rzwp95UfXsn<1SsG箠i7>R`IeU5]_+:^,*ǕNoBH)u}5֨uH uD)mS?HB3؝J6VS1U/U/5NJ[O&izoX6w BF|zj[hIhgq[I^vR{Aev(^x) LA8_]A]f 0P[E$ނO+ h niJ+l֝ThldZPltY=,nqcD}`q2|nA] )5e b[.V$ڋI^W9@aq|:kuxhJE5.+Gf#6kRK kз>V"P#qWj qY?A9uzCPJ~ڪ3Q=$1N1Cʹ}(s9{>zG\g;S]a`*j/qݱܮG37ϝKםBu;_wnt1Xh>zGb=e<_UI$ 1SoNTb+D%!.Rݬ~J;0ɳӦkHc\nEW/M=|➯}v@;kE:<ڲgŠqlu1V8Lٰ s2m)Sy12ЀȨ nw +6NWI(fm?Yܚ^עC_,?`3˕LNӶ,uS=lO}=DvTg(i(X]vwԇ\G;jPmBhSPmxʥ3lO,m˩9Yb[Кkjȩ|ZS>I."zA_^|o|4^g#~ 4ëĚ}ƳΚ;JϽxZ/%1r;?o=u'_P@!H P{$^oGkNL\pUd)X2usBI MDmZAs=^l]$W=TsMEЎ&Yhg58{a}PʀvQ*g :]0!m|YEyjgE;/7NK\mIurR uбorK_G|!WMLsz`VþDbtk ~'Mwɝןދ4hhjwd3ͱ&%!C7+ 3R!F,tJ;7!ϕsۇ"Aۏ1W !F*!vbJuXWZEtu=zmkM:/.$ t~,MԒֈ5 +kD@="%I[kɲD{GCS6os- .|uv[$'~`v͆S5Mm#]w<k`qKvyOIwܐb PSd+¾U9 +-TAH[ۇ O4ZN$fv֎;λXKS; V?Yέ^쿵Jz3E|ѧ#1w׮C+6l8rxv/E^a ^u|h0frM.Wɫ3]==+v,Ifq=)Jq+i0J)#ֳZb%J e(/0*FGrԗmtTqP-ʃcǞԬ܁} 7@암 +#@;8!G~ץM/eSjF9"\H4$ .媮1@];Y˭%Rwd6u8BBn<9SM ׶o!PJwo.es.G$ߺL_i.*$qHp홠%OLɃ|ۅjgQOvU,?EmTnMv/A?Y%=YJS;+c‰Ԓ9/㋶; $eT8 +CVp|t3PdKz8ǻhɵYQ}^modBr!&_Γj5Z'ħCM}4'5yd~/<'P1j}ScuDox_a@w O!RWmn?OabLCN/+IܖOKj9ja󉛼?#=.BO?,T;4MYgmje_5jm+6 \>&8[zv1-x.bz\5'f-8XׇŘB :RY+=wh ߻i6Ξj8ǭZխrU_]?pMŊG16B-Fƈ4?SdTaFbx..w`h'KLvdܫX>_JE,t&Ju{~HA.8+hAc$kTgkn4q6[5xzttٛS}fĝfǧkH + oh +P<3ߐPMh\&@  hzCy yq_EΏXj:MhX4Nn +:_z~_۲D'tO2:e;Fʫ*>DH̷})sH`w>wvD|H{GKT; Z!XZO쳲9_hghgɛ?#6]A[I69\}h uyW;FQ=cgH[/ΣZO_HhUuOn=jOLt>]wj.{!*L=8kwyEtm+\Dj,Zk;>EnDre$*Rh9|ɫmͲev|j D.z(HIuضcauc`]`T4RW4]6hnGDŽ3t;W_&XEè %MɸA5Vۈ4G8:^zmD4坣-m!\Wԣ/ljRe ʮ3r=ۤFCOul;z2GJjg9XזryߎZ BzxT Ry5x WA|r۠;EXCj;K5 ܵOM&ȇCO]]uLRϢo ]_.#3|Ձ%9rt^7)8Z37]Xn֯4HNM5Ww9BwK$gc &Q 1KKzQ-OY5Nз%IT}f9s ,֢83hmzjj?.|0kbLK62k*(^/4ZFBg k%;<&$mСpiӅܒΣ=t? +c&q4ڵyϗiRJk!i2`_xECfCtk!՝>1Cl|횽㩦94oIGs>@Su4gg󻞸sM]oqeV4&?8 ~&S7Օ:{BĿMpOO:y&TS*Ze< y2ŚDe6 DC^{&~ @ۿ\$n8sK\$x {= +b%g6DΊ>%^B h֫nth ^Xh=X NL +D2*8'VVȫ]s=zNӬ?0kQRbK5HF嶺i<73 +bi+M`O_`ɂ%yh'[aRnx4~ u0lz/4ȠWwd*[ NFw_NxZ/v^C=ԪnAo{~g!|f=%XT㱛v5tbpj}Wѽ'Xk +BrǝlMf{:$vv7nFb={6ɑG`͝=c,3Vts@NH̦A2?e췪;Vz0zləb%Ҧ.O/+F +v:r͟|bE0Ǥ.G~ \۵\ӕE,)gYhKᰇdJCW[i VZ$wzh2l\ָ)MRVQ_.$>u8]#57p/S1AjA.;$oJ Զ1Mm yZs@O li˻w +5[vFIu=GЂ`O={%>EnMg'T[:XͩOp&yŚ C1ǥzO:WЊ:;c rJyl@Əgb +ϕk{zb0yNYُ+%94;N$6Vv8hQ2ܧ]g: u:?l8>^w` B7/EsrmtE#h\>:jsMjdD1=7{i|؊N2N%_J_H{ AGn,tzHH#+mtx?x?x?x?x?ر CSCmDon>sUR#SlmOwKI]9 'fEVB>?mh$BR"DfM8M1HD +f&DǰbJ.4DJ4|L9B1oIy=U͆0#-yU&:F 9Hc$@ϽrX:lWAG(g~^T)=J*h*U؊u:{!w.V)К2;)&(:]L9Ow@>5chcA`VE)UF~J-ڒ] R%9k(x :G՚xN);(t*Ĕ[r1lX +K _ioA紸_Pxo,R0l 2jHe֬h u }y;!F{FN7tIBTve.ZV767R[7;Yr;k27k;b˨a?R\aiJ0 [k7];+HvL tP*II+4g'R2tA+4UmABW< c"'Q"dlR`NJ}SKl5QTy5r\b9\ >`*F&$^򺪃55Ǧ26 |K9:@"BjH;JaڢxyrӍE` +z(BrZGƂ:N%Ѡ(P%b"3Ȝ7RrzU|?Zň^+A( !ڈ9[YJo/EPRL RX[l"iJGblVo2+Vd?hYJq.e6>Je.T}v@)]p6.12%Oǒbqd*;tR^z:Ny5]Bd +U쟠pjnZJq9Nn"k%>LthG*+167:a3~Z*)9]=357ukS+iRl^1ϭ]{kc@D;:Fj [_ۉ~ɅrQptNt#qS7A+12.59n&muo]WAwd,a!> 2o.7!`W +_ŀĊy&_ϰ2y=I|x2ā^f6t og?BdSh->dyi._-tGюq~e֚ څ&D?^ 3֠c&ؒ I6PI #ٖ#hRJ72*̳@7r:`[r,O2p0yI+E䰌O_{&2'/hCЄkjߤ]s`|.c(XbT"R9hV'g @1@<ۄqZԕ[BG,: +7JX#04':R5Mݶ f cpB>C)9 TLF_:`kvYWw.a.IN*9vܖM0Wrs,ѥG}* 6pIdVP"0|tz|FPWcH\XY` j*eO"1FE1@`m7c@BPu#ƐSIU7ҘjӫS (S͠#>s(} TVd(mI#:wi# Lc %9tGE?SO1)qݱ bDϤݔB,p@)$jᠭ̈/'ׂW~_ŔRQ +:![ImYHm lz%w%6B8}բ~5c.{Km@bWµ\~/H|ՀiICtn7om >/nh: +WD;J9̓N,9K5 +t&~TttkWs:AC(OƓ2k_d>js՗_#0K 5smi%GA7lt IY\2?(1BrB:O)> +RRGI㕶GR1y?:Ca3"Dώ||ڵLrHUv@% ǧMUj6R_If? + ;2*̑RfK/eݣA?]\T24%m#ړSy,.1]PT[IJOA͋pmI݂-Y*a_X!O)H2RXВVxcXm!ف$Pyig뚣Xb 1r偱b͇A hĬ{pP(kwA^ r+R!qy"(Qy-Z28KL 09-Su-4R0HcIs;?~O*$ s +Y.oEIUw9 + 5#~>s eGaQLR3ǙfI㡨zC傓iGd +$J1 1I% 2˜'ZOiUu_F:k]ɒZ1ZB|7!u{J&uJcC aBL&$/f_u}(~Bh)dF$xe-*RL6!$Z!iJaz dz#Œ:] UԸdU 5": +6 vY$y`RR?l0qb$i&l,m#y5T (5Plۤ>-}$qӏ!Yf(3͠4+EKE)X 8 e K@VH]Dj: Í(QO\Al72H렼sP@㣲1ِUtuKNRG| u#Q_ +E9pjFRゾ  y՟o E cq +*B7aÇS>E|lQ:%kOEA &z/m2@ZA4ԇ-+@*U'A[}$Xjsj{uChӉor1U=X뙔P4JS{9|sw~˨cӵy @3c+kn"Ua5g)o`BLnoDžm_/4Uk [{r&[n̗?pۿtVHJqpԥZ%OJi~Lu'&Kg de.؞zU u?ǯ|COѹIC͕\>΁i)"]\ +"ȍK/ +&lФ!_85wg9(_?0N ]SsɸiD`R#LNŴMt +Kק@~>&s6^W1ǟ`Ҵu'IX 㝳FVn]R~(M<@$ԫr㑼7_[VEд&v~,u4u7A +7,L$='s9Fs!^i{˩D٫DfҖG9E d6Cs\ Sl@"La 92!p^ćm@MHz-O~ek\!eGb.GQ0lSy? H ԭvޣ nR`,FSX<8gWa"sMIrӫ`2O0} _1d?M[c^[n_ ^7)jZR̝Wri\8۫Ӌ|׀O$#>]'?GGqݒ>a_e /TErDi9s1S4%qL ftFc^c6LMJ*:(HI*c4@%0P +ܹqƱ+ +MM( +0>~J:`9U̟MVh5NY0U0(Lb5Hj3d ("30W)D|}+s'_gu#ȿN +hyqGSEy_7|!ryg%_(򺱂ly&N "c(BSQk,Diط;85XERrNqh)؇ل1 $l"k^VJ9u +C><|(àHe[<%0.:LӃ\s<Ԏ/$cw߬1ɐdpN q!5PCgS2!0W2?m@ B|fNi1'Ĥ!5qt(I!Җ߿=&+,BFa˹rg.#bv}A%Z&sUq ؆NvWJGMuːǛN 2N__^o +{OˡF\r-㔡LvoQWs2#$kPǮi- )tݾ7r 1ǾK};En?NF0(xZN Z? @ wh2?/5sQ&F2A&q~4N wc<!}w&L5ǡ17L}&f3R#`^.؆ʏ}=2~/ss +gMgȩo B\ jRdl,AMA=8L8T j6_jWFM|EvkcrA5}⓹ +TȨ]tPzP+߅njףUDD6Pt7U\p7P3%uLrS@܊(ALNcnMx&vO5 蕀<p~ \>|Xxr&9OP&ڎEK3OQ ǀ?jĘ-~S.E4e;IM{Am#m]N B2]@'[%-/rmr&a8 +rXb@ɳab*W+?9^MYM0chg ԁ )>ۥD. V0@z;s} N-gPH'URJAe9< { qW5!585l72+́یw9;n/D#J$tQ T\$2s?S?p~(XS>~q46PdR7"(ya? x#zOʡ:ڿSۣ@=r1{!o5,;_q̄|!(q ^ƀc&`ryaR%ngJN.>S[A  x0 t\AOsjz2'shn`'\ 5,N>ZJ|P=}ey"8ld8n/\'s?DR aO7PaO6 MQ]Mf Ȥ~PCYa_~-a8BG -"r; ykǩAP%;85̟BO+"(BSv^*+_ qa$evTS.Ԅq"Kw1E;;׭G8i$qNhϤv둉ꐗ\,Kwhya5%ym2a4x9䏀NCX^-=}mUA&R!2`;y>`2'_rTA[5 1Ǹz(N*Z&ra HP:;~6A(uHsiX$T*˽[N[=Lr.l՟jGK95;`)[DB ƚRRx(ȵ ).0B=n)KWr~&m9%(~BT#H<4ca f(IT8_U*(+3a*:= K&zejvAМD^QJ8Gfkj%P!؏Isݸ[y))(7Y'u wD׈ox]`8vp=^Q1EELBP g*8nĠl~gS *@_^F=_(+Am 0͡x\Ӗ0J5X] +ypȭH4{8;6qo#y)9eI=reo^V=-=^ĒҔ#QjSoly +LŇ)\=0Fl63.JD`nqVI(nI ɟ'J:ba;Q>i+ %q^A A7-c95SiL|:X&ub(ڑݼ*%ujBߏ.C_0=\MبMlՄ@Uj!s$U=p(uUL |;Ŏa:آ~NY/y)q2#z(s>\_5x7 jG[=?8.`WjSB ؠF +>(IκNrGLj/`}s8fہZ>ܔf]ڋ˫CDS"I >:Mz{K7=*9d_hMoBPO 6q| r;`(RImI72!*Y+95]+A>6+Wr]'Q%@Q<:8quyͩʻ`/5Џpw-r+l{R~W9N뙶y*UkhGhd_A03#ldxߛ+9'ԇ8$[G+r%}܌Nifo.n-h*t:i<r- Cل0jf"GE+BV~X}Z#sdƐdcx Cj\T?_>xx +`v3]e^ j\rSA}jC^k2W P0O.MNxP>8#\vQsA rRg|UKz#`f*=P>r= +\C!py芝3oN \}೾Z8F*(JdKКAIU{mK)1D攚rpAvJ05۠dīDXF.oQO<20p (K=ozz\L/J3?\׀rkK1g +.ܤ|W೸ w6 +xm\\O7uA!f.=G>1uh>~hA8l&uP1ҕE[wq/=S$8r~ W.;[!zrofTKuq `l{g59cB y~״E\^jr|aqsΏ:ԚFm u.Ŝ2[A7'F-x:Ԥ +ꍢ~S5c_E.N +l IOJUȫpkrį )`=\82 ҲHoр\(햺qMV>+1e;D6ryi|6_6s}nZ1nmxEX#v뗅y!8Th*[W[Ctw +iq땄"ԮἋ[!;Se "|ru 9h:J.s#zcu (G꩷8=rKӊ3&Nُ%[qP(aӁymp@y z7Sz})L@ơ{:b*:J^ Tc9>}+5􉚺/x)PX|=g5K@ K╳ )P0+0`%pqSO`/L`fP}]ʠj$.FqJ!(sFx 8<C%Uױܳ*NI@Г >/8rKq^I0uLB.n圹n&n~ҡn n~f0"_3%>r'}"~2m@%TN'őE22$cwC~Fr3eqջA3 }:Oou@sIͪ&6%"J4d=O2]| ׬ +v&񼳊˥rY+GR*z* +aԴJkg4Di HxDLNk2] zG=zf>D?޽֝Dw-yLaIVZt_wY5̽&^Z qv&K66 UNF44zt@ z 谢"^90o'@>x7߬jiV!*Pp^/qk1u\M5|!" +.|_dIun]AW,i>rpYdU)|CH&% +3Y +퀠Z\uF|*z2}'K]$51_"yYBj YJ _])ӡc߳ xkU6֑6-u\bIg15m(jǹR(xB#}G7a&E>'2y/ ysDy@|1$0Z$N> +O?SL¿/D$W^h)iVlHkc@, +GdG([$k1YlĵiT40'j_h%{Q~F|T+iK3ZIzJԬ'N6bҺ'A͊~9%/VD~_+;Α/$tN |+&|"* ]rAa>X2'lfkXKEÆ~{Kj> x49_q)Wo]C!_|{I]E?^#]M_3M< #zUvue-c"k" ]Jl%/kNH6&VIF8$bL":_UM1Q͂(W2X+Sws k͐nN +( +.qN9𛓤f咞_i{7Uq 3AK!a}DҀ4YAI1A~%o2k)6_i>).nYZTi--7ʿ*~|AmI^9U%$cIi~YIAYLMRv +.Gl&kB{Ⱥd}U뢾Fo悒?̈_,_R>^ 6xJ?UZv=|j*>^u[<"*z2VQ'~`i,,4T4ʜ~&a~ϻU8$iQv "!hO˼jTT\V;ɚ*w{f·Q^E=%-+u,\d6`on;1g.F] +;y5#t /qrwCH~&vL[@&|Pn 1z v ^,0NB 1k`і'7-><`pQU軹CV]bHC=SDQ +b %uNݍИ|Qi}'5n3)h"fuy^?37w[gFm"b޻yx j:$T`H~NdFSag¥nuem!7|F7 XsX2㻶0ktEYyӎD !U/Ѳ*/ˎQGڟFVZ\$t vV'LO~7>&w' šoG:'ؓOեWLTo6NaQoɖr +(15b-FHTI>>~scB0'GA“d8?-| ( ؕb7[Ӵ:4MKS!o|Ԗj=iZQ"-p=1OR/\˗_O<$G><9"ZQ+Pa:Xr16ֽ7έ769ң7QR|k;oNpzxpKxZ<>vC^or0^>.GyQ% 'H /I7e7S̭fF/ k|v93\sjq'K;% +k~%*~56exI][S?kx8`wns.R=.^ J9i'xxל5& +0+wx9=`0ioGw n v _e'/*h +|hX?YZ>]bsXrsX|KGOS?`EĒ]sf%Ůf E!ⷍgDJ$I(yvHZxQRc/ҚZw +Djyyk\z ~-iXr1>D8" C$_vg4:EFԺƞiI`:꜏vdE6Ƹ{GF:oWÕUiӫ+baY%Rc홡->&YuUQ~III} + yUla^^偑NUqn>ޱTgVodɯ3:#<-̮.W\\`coYCP,퓍|3}2ϴIgmDe]k`zUir>$)~͕Y/=Qg/?9? +]Vz24l}|crYOEs1@W%zAE/~9dTz)=tsFYz_y.~՚v;q7̢ܤ=nOzOOΗZ#^7z)01\5cz Tᵍ¯J[8|+ ~oF ?ND#&fYS֠u[U38n G5oZ54K(1EAn~=O6:dsGoKUpc7761@g5^H + xMӱMşj0nuan,{3Y\Rjy]e4a:zHϵɛЪcwGhf +YC-U&^tCbhMK:EN1M.Mcj_u +9,#LnTqg ۷߽#hn;S˅h%Y;lO;3kyεawss߸Dw)wI(-v{QSP./ ˥/wyx`RVMu yCE5+˞gcO)fѤa3>M>i. дKѤKDhʘ5hLJ wEĄPpRӁ\M)rcBa_طɛ.ƜӪ9$;{H+ò]zR4Kqeo_`MJY79wӶ#̩[ QqT|F4sp $4Ca=v=e=;k/ZVo?mq4w]DY4kPDsp?[Gj'h~`C)OyZ-kLqK oJ"*"kl#jbkJb` ^$M=<8{C?:9 +)M]}J4sZ|vSw#ii{[a-EOzh;e+*FÇnx}YQ.!\{}R[CB[KxiԲ{_ZϦ*FS1Cj>s-@N״Qьk6U}c4Bs6*g}h7ZDdhLaw3TKٍToKn^!1k{! qam]mvy竣C4߲fOo_ =DJHiB9O{!1+?{7n=[ܙ{тEZ8Z +-rFK5Uk4_iVCԞ+yQWy!A]z_=9/) 8gˮpyHWWKg^op)c߿) _l38ۜߧ+[f[[}#&B3cX)5G,'В=hQZZUQ;n]vFqIiKI}CtWvk+s ~Vb:Smtm|Y]l\kɏkV=_d 5xgstşҤ^)6`;i>]#ZHiZ9_xր ;oU+M6lqݳ;8:j16\eYc[gJ\wcBG=-,ok?x-}_RG u:W L|Y7{7;g/-)hQB&Gr6Z`ܩk1o37g;Ɣ

9٘Zdܛ둧nĘ:Ն$UXFFFrH8?1ϣ:.e4}Z4{>Zٗs]]xR |wfN5eSU>E=]U7Ğ3Ğ})0{1Z|89`'] ^)-.t1CN4g,d4j.7eZZ>VheZc3|ۑޏvOգlV[=o7Z*oN kղUogjd˵kYmzVhƊ X`|:"0 +cgʠet !!$"ynw稊. [|` +ܞ_,54s&RZa;h qo*n󫙸;_5O%u_%.)*G׋Y'oT:qg4?a7a| k1xR͊t|J60(ZAXT}^F俩30?nT;}n*L݇gǘV7ת؊R +pU Tֆ>gx-As窠M&hvvh׉;MQ-dWi`OϜ_+lMg?~gi}gyڟYwVj=Hr3bt| m7Ҡ2nxskBu;ou^Ƥb }:FIf=I?'}ڪu!vܒ8߽M6lkh#{Oܙ*aۮ% ~e`}g%؝efnVwum<1$ET"|9=l{Zmڏ-:e5wg\|c|fU32=y;D%1IjTb۔ySASD9E9dgmBm5 KK.?lS,&@K8~m̝ٓƿg>/e 20x׳֤=+cm[|^bA'G֊7C3[SYz<"J6/*q;y}i97FshW>2_^)KoE$yhhąGoE Whۉ;÷]+Ҩdwyqs2qC%׬Tn{íh`?=*1*`y YS%8_] oе1e6Rٱ9?aٖD9ag^+KN|Ǭ5kdر82?t4Dx7]R49E}!"d'9I0)a>ݰH񒬠 apq&VQS ΅mx-ъ\>}؟v9MVqmf|m S 랣]Q |BVe>?ōDvx.<%mt6;29&n>ݹA;?thG[Ͽ5Fm"2}aDVC,='W2&tiz%+,6+-r=%n2[Dz~"d"6}.wo3'D?@kϠg]R_t"vk`t4ȄoLq D%{Gk h9h璹h7\+ }Eo${O3 c( 0KVߡ)JҢr7Q]M ]uA\tHm?zW=`U{Ԫ`ULq8YqỏdP?9JIOd|]K @/cuP CV8kqɍ{M.EMٽ[ W6͝:c-mg:3EzvTRQa'x _p=_5> i{5Zcժh9r hVV W;_OYzgбFΩ3Mn,4Iorw /v FN_J6qy1bc795:=AGCiދz8^Dj +P.Qs1[ȈǛ1oi6|.*G/7ϪV8[1-)vU_Ԅ*+!ysMu=w^ 54c26CWl!uǂɚM~&|t!uai[Ct#^^t ztKpx)hnCFHDfǹd,6 +%  k<"t+ᗳZN_\ܬg^%eKw2Wg3ϝdR>*f7l04TȚʼMn]]0W]x6H6{ 3?XkENvn6]rd5-920a m#*D=χOٯ{F + +=m_]SGf ŃZwvd,2pԛ9|"6?kj|;s1u}[ߴmQnH)lCfvQ -~\V~ЛnV>([W1ngwjFzAP50xbMY?S6<8⌢ɁӊzhǪh߶mHgȃF M}_vs |oIϔ<ϤeR{o%x*|0v߲HUfSNaJsO(yA[$ J?\"yrPZY}wl{AB"BTϋ,bH[3c/eO:1^! |J)})T3kD;TѾf(LnRuk,!x&OxN5],uMT”&U8sͥ k_*xD`0OgRܳ.61J<3q"%f0O虵8nq _UMJ7RTn|al3ԤowSgæ|A&DL^ +b,:4LMeڶtڳAsGu ϲ,=%i0~?֊fJtQ@eRNs}\J4ѳk okfšģ|o􏪂_Qz((}^  ymG1Lgq.3zj1tQgtQg.M6fϖI*-lG.7&)To=Rx5Yީƈ133`n1}pEEI:db0j/Ң3 dwn j2>vK^z0Ǣݩg繈hk/Wԓ ugm"ېZNN'#gf{[ zg6MWvYA85L_b#/*&6sDuU#h44SB#>C #9]&AⵂD%=c@MWfa1KF蒒z͘DN_̿>8C|YEx~+}Vw̴}=+51k}D[pQZVOX?CZ4)8f3pمj4^?؈3 ++D/\:ӌ:j=8=\t `, :MyTArN*KR}: wEϜV=6{YzJ׆g(y6Jj2jwo% +7Êכ؆N;1],89EC-n S,'2zHAjQyM2:"nWNκ߽ԵDxVN U{K0[O(w'\ +ZR!̹?4<0KsHKQ<(ȟ`[rŗ_:xn1O{f-'N8!ιq<`LZ>#Wqe}ԭ}ѭ۾eD=s„kjpY^){!oOZOCy_.LP'RL2R~RN[U"j7mK֣]6#]#_6LgD!-3FVSa xFf-¬?P 87*Lb~2YfvgQKMF;b]3}19fbn +9uRQv#?j_02zCsl;u nYAߴV}K'n3I)cY=a.'L2rmhOЮ0^r +i;-qD^]9t0/!8=L?}y,Hq+QqMۃDG-2Z&J̋/Lɳᛐ\AD duGBAj^"Lj6&Rh/y|9EuDof=89tD?q3f#`$8Ҟ+Z/qO9IFe:P 6ŦIrXFG#idi4-cq1w !JrJ!ޔ۾~㾵֩3#Ygʌ8֜}Z{U^{-:֞O: cG6rEӟtyw+Jų ۶ț56t'[x5Nóx~-nM_3wwUج=0XâIQkHqZvT9tm_uSfͼe}B_kؐ۲]+O_}<栤/m&vM?u6-{آ89nظ{]L1f`}vc'7x5ũO0pK*_F3?,?9tߑ+Bkw5 <:^-{,]gƆ)/oL {o/oX9 dP6熚2㉹|>kJLԲ1f*KG^CYpS#8{C < +;}19+1F`ɡdhSL\^ZKrŮo+O=lT^YENXXc*'9iim=6ۄ2g7}^1NA1 +<Ű#_.dž_6ǰTwFÓWk^csu lyP?1?Ǝ`tspmПnz0&Ptk;alI1cR,m=C_qFR{?y[~ 3|^;M|Ow>" ~;ػHwn, Ƽ7:'vN#~%AsvBbbn&1Z |k"o}˟FѸvB1xijh~z +<.g07H >L3,gמ^ŨY\ywo[m28F1s 'LĘ w,;aɚp qºֲ7--Ro]:)rӗ`q\w*-%'-.C5>cɄ(~sWP,U-Dv-J~RNgenOd[?e"|y:?cO/r$!<l)m?'UW֕]R6lDr `>ĺk[oZNSmrk΢iK=cEa19pQn7~7M-ηK1僯{}{n| +a:#gBKws[,1uЦ@9vx﹯c{<׹xSCm=;&W>aѲI  ladʱ Xڣc r?#rזs0.#yP1wdKWc:aZg/XwbF->bC P.X̧<]7H紆ۓoOiy12ʆ?TB˞\x[xc01!ԾD!ջ,rF]su0m1l?x1QE-wNE`Y|̉:Vdu%AGж`V /8Eao<{%7a3<ÏR̻U9;1w_OZ~Mr׻<ц?PG~EKL9=1k8; .{'EuהW+eoZPV߶|y`VR4$߮kOb@kf/hbgR^y m +<F:]K/_6o{;l3j0,pV0w^}ESz@hkNܜDxI*a|aLZ_,زH7 u\ZBG^AqAtMxU9= +˅9B .gjdb|C-b1F`3B~[ k`F_,GwD_]( +aMZbT\`ǦSz'aG(&1u ;N\Bx=Nw7e;XG|3>QNW| #0V39rwql+S)sȡĞ.9GḌ?3Gy|Jh*r7tzd2i` ߾/}G/yBlۼYs`>ʂK6cɷE;'N&ͫ S]&_%RJ2.:.l_2tY'cRe[ΎݽskF0gT_e +c1o}A~x 9zt@?V7ܹ eR[=8NQ'o|˛bDWZ~?;b? ?{߭<8I,оo¸_K֓)04yal0@_{Sc\8ͧ7<盞|s]Ƶ;.| x*j2d7={ȺiOs3BG1?|o pI1tpKIw)>\% Xm+N 'T07B c~3%][W* $:&aɔwٔ9"vÚ?S"kȇ3N{kSk[|x2dMsv&I0w"ƥwNj^EMky>~1`,(k>1x[ wN}'3û߿bQ_A o9p}W ?v䓺nZr崁~Bÿ +i]\Xzo9e=sw(Tq!|37pYC4uq& keOTC"+]Haog͋9p]6 |O`/BOF}Y$뇹f}SOo8&B+{qyƸMwmfg{\s=&Fvv`/UFƺ ox +{[Ӣ2?rugkn ozm +o>zŷ{ڦ7}?>F3]m`b 19HbwEknQc̭ 4m|pgE]OnΟ b!|x=9z,[kjȝzT pMķqK&@mZe-yq)ފMgE{9O}tkt7S.;7޴ _̱dʃ jbq>:-ʧuw?L64Wykšsmg?Y?m lhc=Ywt+)"7qá+Qco\zu${0It E׽xը+`"#z!-_s"{M{?Pm'&OWrpdD21.'`^^ӟϏmyNjqq ?9) }k>Bq1ߑ0(/._36W1TO m}cnn|pi>X\Tv|须ŜsOgŸ0r=7ֲևA}ok$7mga\U'㼋e֝rcb\nʝGsG?|#1;kZe©BK&b<N'Se ai4|+#s@,t&<3M?]9V(qצ)o]ټt7n\s:TJ#wtsW~9_,wWḙʴ= sGҫNrg=rq2̝TVWLyp u,?Eϝu΢xFĪIt/\yFq۵rCqɘDzqݣc,U _{OGyxV~Vcjߡ4jU>5G n:w>`ĝq_qFߋ/(0f/;|iâ'Dw}l7keBm Gznd Pan@]'O89l8r/xVl7.ۡk9}> -yk7̾rg± A3weF2vaS _'|jL8qʓ}'G'FW<sqS~5.{b䟡QS m˘n==}Պ\C964GsJ1wxgNoQ%ꤸM=%Zs*Pn .𷦏\N 3IFT9-bBy}ϑ&c2̝6rgrg5{x/MKFm 8w1g٨!m\%Go#ҺDʽ囗=t.xh6cnS>5ڸ'͛_&j=S.^*DbX̍GAv@p^]>s%b\=HYm$1q/ZVn#g/0'vxFhOY%#j֗׸s6<}AGrT\;\_1ѧmVK~ǺN[rgQ'ʝ;kn̝u|g/Xݔ)wV#OYYYi#w!-c΋>}1`.ea~s+Py;"wMT3P,kDsܕ1ec,{"zcć^ ?/:m5:zO +[-MD|fa21rɸ700﴿ 8?[` +=NCy eoˀ3wr=wVM;eotXf󙾆l;ƽƕ{79[=(Ϡ| 0/ Ų||y+Qgw%OkaC~~ƒGؖo_yIc~0e oc)`]~Q -rg)w'7Fѣ_܆>hP.uWXh$rIF,\_œ_Sb_$ ?ҵG~ 4ny:]Z}uu<?(o̦<,ʎ(&xk<|>(oI0}xоOo5s\v}Xfd᜺r7ݳr +ܿHЦВ'B mQv1Wl<N ֻNh|:ڷ+>s O?ڵC.8ȸW燻7ߘ>2}~@.rqKHF\< vz2\]o3qcQ~5{.FӼiضoO9pp}S)l-PSw#`7(' +]wq| u}<"`x̋y~B;ߚNcsˬ"YtU_ߌ3|_oءOǏ6yj`zK#==}`Sp_*K;ς |y3 +=4<5/XAZs4ʝBp=N/κW˝ybhO +2qI9[P>=!|ƃQGv#xWtV0ߖS>n< u=浘|`ַ+RWf#P/zXǽz-wVh׏x}Qd[, {?R~oh_qB{\iz.N`_6rgmw6 +zc=vWhقzWwQcƘ#YhjiXއ6;s4l}@?`{쁷7ٷ+oڛT=TmF^[bKhߡ)^W"ygDWxGУ^^/zR̳kx,GYQC\qkb^yMk%ͣ#W5> +׸ބ׮#>7ܽ<\yr{Rto_O?-l|賿 p&5;&O}_M91-|n{u"{B́z{|?TwL]o Lzkϣ.p)w?vA{='=Ǣ#}0:hD_A9U@۴}XA7$#`އ<59-<aO:ۍg羸sp\z]ʯWV @zΊDK~$p +?DJ{qh$pSgYˉ0 +{c;_p}~y"zE畱?aܫ\_KM'ֈ>@[^z-7{OwG74x,فc昖xυ;5y%G3ҝ"\os +u Z0șG3V[ iﰡ}~]郭wPc1Ȳ x{~&c_s7{/9xx}E V1C}'G{f +C{htG= [0fÝ+N9f.A_HSb(w-DW+lG?q.ȿQ[&# &|S+v9)0+C9c`[\k9xwYk&?Ə0)8sbqXtn=3OP/16uHaP?x.mt0+}['pn?8UkLu-7N1Kό:*;tog*)F4uL*3:ƣ1}>nnδ/̴,B T`*39E5_Zf"?zxObNwDgSj1v xjEgg rOw7\;@4=F6IhpcVA"d lŮೣ>ʒNqIw%tc/ݚZluYNtGW')`D;Htڄ힙 X| (&k/ X&cRz&uҴۻ@tǵ]ŵ]еƝ XF%dV%&RqXLƭZ1S6QOmVbi2 *޳uZRHU{{zΪAq,FksL2us6 RL-ޓ >3Jw7{*v[7SqUȝB_ښNg,[<-jW +4Qǰ+Z;1{ Лw^ޕLt:-cY(ΞL|HZKRs >N1UV)J[l!2v"udgYL[ӎ>CC{ vJǚqū-qіCs, 8dGD(jD;;"ls◙dfqG"|cd܏I.tE([Jrx,Ht/J %KO%*t.vGãQI8UөDbm:?"_.`V[d*|Lmu-1"%u;ޖuNENww-Nҋ1[/ws~Vȹ۸i#i.OCu8vڤX'9R[L/U#rp,@"8F1Z[ &"%se$S)Gm& +;U2VIOwhiq7.+>)EJ;(Ջ㝝/JȖ_pԑw5T$ 8J[̂xW$cɻd.v+]97:G}vġ*[%sq\Bq&^o8DZTq>|8?3[^2wE w)Uc;ܧA:f?ڳ"|Y{z=R}qwdJg"pCk<պ AVxwr2iGz Lw:Јvgdg"[w{赵%3ɥ gBۓT!>ש1LɎQ)wxqiufs%$یc73+WǙq8S-v:|rLgZǍ8bb8:ΰLjqq\Bu)F:ng +E>UƵWj|`JR,`xBQ=J/Fcۆq2r^, +\ڟ#%t 4._;_q#&o%Umv7[IrnrZys[)q7Wwscf6j.qa19˾gII$'cIN"u#:vmЮ/Vb5%'Rux_saGi,mt8DK> 'n70 :;c< =~7.YRXlK';ͱl]xfVVbsٔd DGz~-*RH:"?%0I_ck~`4vn`4 +bEZ]Zk{ K/8^`[2?=9O[2,AvE }gG--%g;Yܑ8JIEꔒ$ASǐ=8vsυ,%4wv&Y;3 Q*{7K9<{sv-s-ξg)ݦd{{oO: wWnvLº/Ш#חH˜bJ.Z+Z1xcs: /yP"ԍpUl"tD]ѱJRndq٪nuZ cۆqժr^oq-;z},sPڇms!AXE>W{݉uqZke# sDZP,3:/WF+0SeɶGwD!>Y㣎ej!=ʨV\= L[,}[ +QOi/(e9"G Xȥw.֙xNOY];؝i_&idn Tgʌc7ގL|iًqxObNwDgs "Ԩ#gh"hij-lҡ*0LuO `~&ޝpãx +&5^/DqLڹN*n4%7Ҁȹє{hJ#қd80%pDp:]=ոB+B$F %9Tí$nJtU +Cxػ;>stream +TU喻Snn;2h#iæ՗W0^ަ* z:+X˪{Y<U^"D,{*T*.`,FԔcQYT(x0 @/x/HP=+{y K*i6+ +'qJVD p) 멀j*^xlI +k%hUYI)W0:YU"C0%Ydh-v1/Z h&{Ut40hO!~*̪pmOAXVÊQeC +r}LD@?V!2+(B֒,,9emMig\S f]SļPה~,Q\&|66yH 2pf +;lH49c_Qe9 zJe_Yx_ne$mx9c6Ҭyױ8C@gDQK8.ׇ<$ 1G/,J؍l=< 5p@aXu;{$l Bo%` 0T />5$zh69m p`׿c?LqBeu+#CyLA p86nUËmhJ54DmhЙu 9AC9ă=0dVqUTT\xRFp`[f*c  V]Lpm.<:}CTZ>UÊ:&8XMpv :O\u1c) +>4&8XMp)_E"8 I]T6E(F@p E!Ʊ N&-(dK<ʪxU9 S5HNeC"#`".fPqя mi]\ ۠DaINC҂#y4^-Ia?1[765T6 +QnSIADGnH9i_c/)rЀօmQtqel3xB?TNhHRA$ FNY=1GKs]c~91nj;U!z*hDN7WdkoLM9Aup xUcal +s,#^ +Ί飡yMtM0CJ7jP?-ztƢk|q?V-ngdc־]o0eǻU9reN#{kSx +JW .UÄC0`[(8cenxfkxX鮶c:Z[Y ΘY Dt0QGƄRp@nL'.fE ӵdѪۂHV]j@fUTMNKT9b`oԚK2wOEEKy$MaWUUvԧ3fA(l/陾0OۀicKAI<9*# ysu6О0bӠAۖnI4WSL_*l6t6\8%S̲Fb;mF xgT+&" M ˵Ba8T$Yڨq8VX;-˕c@ $J ,%s(8ƙ[1F駫*kۖn9!Z2QmF.sW( + +I"f{Cw0lm+0mxU_j.'\p"065mk]~2?fmM0 G.(cb2\cvn~a8[qAMdʐ<)me\$NN Y!Zs`٬mS<\8'usvbtYbt 2:N>Z ɪWoX(V5g1%_/F5A);G]%)<#s:.Ж+" +s 9˲Xr$lapV*X0h?s~ cgN%B~#G^"k~ +!9TDZM~ cؽey[B]NF>tTcYԘY  v0@( J^CbZ>Ä~gxeE`x=:#ʁ*P +'Jt^-̳LRb%S¼;F䄀8ɲIsY^EIJ4b`xJv#fbFYW +)xRADgNez'`#YTAY@S@,kkmEEx0H ?b=Qz`B{Е5si;hYN%g%{#On`Nif|bqC>Ca䚡m dK,MR>Oh!hG),U=xG7 Ab>֝9S- "\wJWh{Y1c>jd S@vvf2RzXAjӒ4#6uе֝QjK :*lΩJcfrԡH@\E*'NwR*TK!(O¸WyZ hS۩<]t+-Pe,kJ3\UZͣw?FE 5|tMiCӄh s ˈ{ Py΂LeEr +V,-gbc,|:򨄤J٤=j`0pWTlȤHf"$ˣLY""$;d/˼b2]J^MѪ)fLAYj`f,8 KZN<ڐe 6AZ[,Jx`qY7v&Mf6N1 dRBz wۦBd&f1O^۽N&fhI6YP̊ d&Ȣ fTlbo>/&'51 ͉ỉCvfь3)afh3cT,2+Z`/bsZ2p0 E>v+.vu̢12kCӱheC ɀQqoɄdAN_Y42n@Ll:m:ra׿,drcHW,*1T(J" 1Id:FK- +(X &z{B԰+\ 3Ne, + +E"C'2:Y~oȄ$AemN_㭶̢vQu(9q0lJX4bHQ|As_е_uXߪ2Xq~܏g ǡ~f+zyjz Omw1-߇7ei' }5gKDaSD+--`cKmhes?i)aƎ\o$| +m!nPɼ+wC@vh[+Pa{ndoRy,6K;~Ɂ`:M* uCmt(1Qo`JI tMI7j(֐jdfMv fy۰AT ߲jltsCby[:@:M޶ Z l-dowRO6Ⓑ·1ml!(5D7XP6aED7ֱdZ XMlp![H6Z:F`kUȦl%Cc6,`ɦfv(M66Cl,eéj9u;(E·,u6Cؒ)YdʶT?rf3!lX,eʦfQv(W66 az )[MuH[[6}N!moY*oqipN7,N7, N7,N73jd[dj'ddJj'd[ejce\Aӱ4K94K 5K+SM| LɁrӾ%ݑ8Kd"I ^jJ 9`Y̐zr8\ e//iA"FXM9^xg"\J$쵃 Rv3j ŮBҞ7(Y:o%04e[CaU{CvU$ONXx:02Nu7A.eDQB_0byé^*dЖNqHlAjLnΆ .K`~"@*6/I6粄QEmEdm7uhiRێܦ <|*k[;Nr^U; `(;Q'-'hɸ1o D-5m#;lU]noQE3x:aCjL'k;5ŴӿcBݨnۺcQgM[o3k+:X Κ/͹Wd"u^!cA}_{ +" +M, +'[]F7^@xȽXsjZ=L{pGPpMY +_;o>_>#en1 +0Gi^=$).<%"y\(I*KNAu d=>f%xQk A;WD`cZͶ5+Ũu5y<3iTA3NEsgt*:a^f'(ZNLVX p}5FB€p^TH8aL +2-@ 2NQ/8Z H B;bqK +*I[ASEܚWQ x@VD{P0'\4`ڀC?>>i-BcU F5ګ{myTCC0߮pyLN +F`i$$4x}{M*i}f00[^ZnYX9YèZ Ψr<1|FS^Lu<3,jHșL/1kB&ƾ\rmYQY[I =#-^LK)rvYZx`*PXɏY5C0$ VPm,soVy5鬿Bg'F</ff,Jh.x^~PW&#cs<``wzF#s)(,6Ġ3fw +[ƽ$dn#ĵh +qkm6 + nwp@Ud"Y2. 2,b\b!R[sXWi+~`G_iy:) gB]v1mV!-:S{ԨHZU-LOs@*bU4Urә;[cg~KjFt5Bcߦxsf m!:B8jq<Ѯ!9 @rYUǼYNwղ8. +JԶVɑKEW!G x#W5i>`˱灑ㅖ5;OieN8m>&h ԬY?y4A@]ʢ<ys\53Y[d>l] +ڿ0[5ZUIF96xՏOCCHAe%UBS0'YPǪxT + .a%Eai` ~jf7·X%ڵw0JuԊwJ^p8 %F +}fJ55vCby;onOks=0YND:'Ak]t1v8+inʬ'TٿXg# jZuSTw7u/z% +*e2: d7]~Xtu%<*0OaF֍axFm^$W4hFEErC.E~Kږ;r-ggA3(nCyT\dd6{2^O}mz<S 1Kd?,z +(0' |CuqTEb4 NYEǻ[SlM)"@@2#^ދӌ^+z4M@Gsў90  Lt_/)>3n@OLěy/ tz%K#ӫ]_s)(?Cӣ=A5kmuO${ |xj$[ŻKsC[/5ڕ @=OAVc:}Dk/~yL؋?~6&tvYޓL^`^*Kcc&0őNYzj df( {jmo,AĬdOW*ާNAn 5Sk#fTjsfװޱ(īJ6 `DN'bţr+Z\W h8+NH&y$%SR=DwpIJMIp̾`,:-gao]I闱(ŭ򺳱g#D /32/q"uxNdJ,:;A#]PV5%GZLD=;9SQN\u% +FLLuJJ(~qL*՛,L3ĸȊ,'rxJ2'<+`MV׏gluH9, |=%1ϓlk,'2"fP;)YxPv䘮H eו&!X ֖PSA5Ɏ'zJA&p*.ELdI6܍y;x]l@à|"1*z#qzCT<栣QH>L3\Z~dPyOYQ00yeEa)ωfplM_xLj w^yXI Β,q"O Gxm N^I|&gHd3MM3{xn g̵U]]]QY#IhWV0i7dН!:bOSA:(n! UU  KrDr+@8m`$Hja _O $KRV_g?5ݮ&W),K7hIl|;87&Qz~UW\zÏZtWP&>Z4ׂNj4O3n/jdNЙ7# g)(,CBX$tbz.%<\ZB&[ +{)-ޮ7ioU)\aoKp`ED3KcܮMoxs_?𻙻b+a75a+tKs-OGuxDwpbo3@&Gvtmw+j\dN)5$ϨAgӠH_-t/!MD#j@H*H|l E>'ڡ8(aH2;@1?Yz@'fvx=jZ)_lj38g`j/kؿDkc,N;d<6@r S Y >Y-F1Y_'J0~S\ڞ#o/M`PmG'8Ym}40!ht7/8t~7z>lGVh$Jl(?dsBn;~MCA޼1H)nH6~@K#G)A&}Lnϔ?*)e+գJo2Z{\`Y$flxKxIdkbcSx|M2>D0{/djDR]1Am. +$>qO{QZ- G $ 9ހ$Z?  :1*\P"p'SVIobE-rQ*Xy'2e*Ea +0B/Tx}~pP>剡"GHHQE } pV0.za% N!|=b(g.'h>@т>@!D~tm!+~%̓BR Ä (]r_hIP"`c^ ; +Q,H~PBXԠ4"RP^@lU/':CGY& –&٪%sAm8F fs;ESh<Zl%:I oFD"b!C@;oF`KzX0Gz lȢp 0w?&3~?~:lC˒`XypYJˏFJQUK>V⠱SQ>T@#&*>$Rf]籑^4@]A IDgVBܱk @sAw0u5"PHa%(.E |>vjILSGQ5 `!u "$89v((_u h/Ddc@瓬nQfo,<`;!2 E剬#̇YP , z5I@gM$aI$5$]){a Pҗ9I`rXyQ&~v~%t9 x>DXbu +"Pa]hJ'G'`!nF p2xdG`s kL֥BD|$^ + !Ų>^䌁KPddRay bfȣM4P`ASH惩Ьc.V!-0C/'{8N˺D#x`"+F恁0 k0)-A"Q 9RJR +nǷ/XieNz}X3'Ë5Ff8h:ou!itGz +!}.6 +.k_VB Xh%NX1ȋ5ʢ]ހL"H\l_+S +k +bO/%&,, +''] >0hO.X^A\{)I첸ע~Jh}D=.Hc CN;kЕ0`w/:HcФt{H"rJHy0TVؚ PG*U ҋ + ݠcGOXHLGHKZ Cl|[lܞ, /&A`]c4whe ڏ bn&xy!V@[990E~q)hPO`p{qi!G +p8 tX.^bZJ6\ud*À!P^Bν0x,QlXA]PJ!OqLF+-r޲%^ K*7KGcꫨv0 U_q=)zX.!\%l3.rpk~mQg[+t׈%)kUZsޟ\_Ld lO6%I!4$"G]gm3`̐<$!X/~ ĬZ@'FGaXI\d-?D3w x2#X:ebpxcY0N +g͂, v[4Z  e&1' B$5$9iOlet0 ?ߔG<*0} 9 0ؘi J|*gسNăUÉZQR0 (г`Pb_MC ЀwX~ N<|_`B[ŀ〢ulBZ| /~Kp1>_84֤ž sT`|->$ϕAe*"Z"|;@=kYAP/hM6A +QSdr/F_ p%Y!GoSz{ۮ2 V >""(e)rCiD^K((Fe9f.\Jq.#6ș h;0QvȸgB&@ha{eZCx[|0<` B +h9D^'! esHځoEȥ89R2 $ *Aps0%O{-k.+v{02oڟĩDG|w$ ʨ`t׫fm]^ekmyBz=XFl*׮eg0;[{8 4ƨ*"v4oS-&1]o6#hmzx6Ϳig_]|~߄7&{*p$0|krǮ~=-؟.~^+phŶ9f?eVjGo L7vٶL;T60 Rg^4fGC-9tVC@06`h>>~_RTZ_uVzK59 ~{%ٿCJpC:Vv? t76n1@M CII1 Xn Q+35{EqPu<wꀎ-LZ 7IOM lO-#)^n58hng?j;F~a;%Qϯnv|vٓ(|gbc[,NHOSQ6#\eΆu@yCJ]DAڏuPhZ?)9`+͂q:S5Jčxfӳݾct*!o` ֺxNj78.`G\͸cd?1B[D~S?dgJ۞9CA),W   +XDSeHd`Kd75Z,Rr }_h(y4*ϋQ2oM2b4ȵzvs;. A6D ~X@X|n;a _b{-yytyc<"QFLq4t}h,Xvwjୋ +h~~a.R[ NHv@jbۉD^aF;| S?xB!O)vQMzoZ$(H}`_ sIo`49_ +Mg5PgtG7p5 o7 x7[3o{G]1oN!v"Q uKfM'*5@ÒM,|5Ih^pKbw1.1?!/b0Uk#ҖX=|}|[p6ŎҟLTf-YQyPmٛvtV51}M3hXۮQ +Fi$fbAS%(%!9;ux /X3` +gՑ]w :1u?LMyd_6EP.*}DAC:?b6QPr?Ba +L|vaj8Ww.V_C.:(h<>n&L%)jZytZLL +mJ)a3iRmъOD,5p&\[pE.!Xwg>=WnBor^,Nz?Fr@v~a5^ߞ7WU}*mi=zSt{Q\/C*Aq{Z xҌfsCGm8)M.Uex3AViB6r݋X[zr{#ۖͲ(2fx6S)@@D3F$cZv8:5r +o{(ˏh7x#JI`U eȬxg q9ֽoNIWJ"%@dc|0QCz.cÉA}o 6d ) ĭ|\?.,Nqʩ81OyVa V q=]Ah7t)ྙ, +% )I]jw6 O/pyѬ*pԴ߻ %5A(8h +?=Bt!X+&uX̛TpgFqPumЙ}zQ#}=g{IvMu>H4mGՕ}Yч Դ_X_gS_3fMB |g&=Z3e3I;eKzlɛҥI1a;ݔJ*m| -_qlB7]g*s%=eqM㘛<ԸΎw# ?aR~ώm!4;wxRe0$w뽛%;D}xgNf<|u<1+Wgҹ*7yX'l$[tiJ͢#S)Ȕxg^,(* g|M|ӣygS;||aS vjO8R0t$]taXܘ›\M}vP Z"@.nHfq.An]xO\n}zzJL<\y/Ftu:0ӥ>mJ+znl7vvx^bo!k]klzB3淐L댔31tѪx"r:Eg4:{)9Im\OcClH|yP{}QcY oЫѤ4|RaUOM ;sbbWF351:>~|}2J1&-Ƽ72VmEmw(g|j^bWJScw0n9I>DoL&}8X^M}oe )roĸaMJ)j35wL4Ѧi3,ͺaġfy1}/,a1stdKZj;K{fXi2;˲\x;XTguOV:HޚɆaaqS.m'Kz%9<)GƔ1]w;[=ڞc+f&@}-~)尻/;}s{k랽uE>6ysP#bwviw&g%vu䴾N*[wF\gf9yyoƹ.9wL1 WUZ׮xmW\sw}:L #RqnԲݭrEqވU=I]O5z]0U 5!9qef Zt?x +|c"{6lS#*㚡>Z|Ef ZTlYPTC=9 +L^*,^[>ۇ7x֘d4ƽIoWϳO9|c;nFηw~ܾیޟKLGO&igh=yGȏ RI_9JϠԨT &ͭM!3?FFX3duB^2.ꌦЬMߘbrp:mمrx.ZюV?O}"^mGuIEc-zߛ-c))1gE}^?W`c|rw]uSv =՜'`xjŇ&,v&=N|ZzӤ%2LdiCldΐlS}oT9jԸkOaŪ۝~kOGcL2`e_u̾d-]6c_r-\vProA%mDʧkyiŤlbYx| +ܶh;P(6ӷ5՗])mSSSaIgFZ,4gT3;>Ag^~2򚮺+p5%~e5jC}?Ƈ@vAVOy?z+๫/ yFj4uWG3м4k!k9ُA|)m3?ڠxu,?7Bt4/V:w"y,:r?-cx\vlqe-|] yyq6u" -#Wg_*|Ė} ׹_Bbž4u}x}ʷna ΢$NAAMO%PcLD>s6:NKd㦗ihM1͔=Mݾ=Bj.֜vvtn1v&>VojTo {ܹ?ׇ^Zni˨gn. 9ovF] F3ջ]De0 8_)BJs7,[gj.hv+sqvQ &rfv[֪GTw%!ɋn*[@,7-ْOe# {9j<볧]y^O#8vajR|^Q8=>_<ʗ\~ɫDѪBU8K'SAyE!U3{TE;C{Te튩$TFReXF U&Esճ\)J`l +X :{׾iG-ޓ5W]6&+]1sZ-YϽ3ҏ ms.T~TÀfJD|إ?'an{<1eبONx7jf v[&cYSyӟoI߳˙X˻D:bMҾ1D/ٙ krۅy-ڹtZs +1=~k3hK)4NN)L +aNWΨU}"XI&Qv4~wLI?%'h5o~]2I6}39H4ӟSzc?>kD5ǍW-41%};Kgzy')Qͦ?' I65@ԇ3@kYJTR6mX >~~pܵz_bt7D?\MDYjʩ74>"wi"b|Diӑ\R?lj`9n;`*Xt*s"9&؄?qɝ "gfFXD(k5ol)]gew̩:k$a!8,f&̎  j砖_}s|TJݘ&L'˝%]Ìad(*emE8?SXuc: F5g3i?]8n2rbiƞŪN4OF8u8Tb^hgEPE"[7l$GZyni'IX_=N 7j\<|8…$OKQLqOx$rCH9I>Cw~ dfg>=]5b㉕w^ΘA<"sr}%F鴦Noܘ-k~e=ooIDĕetzdPnoL' +'"~B>vz# ljp&;`SW0 w!&e1P_rzF0Τ1z,WÉ Lh`%1ߘ4pPw}a`: ţ:ŝ $pK%>%,өG+ C-PiTgV$~|I(;Vr䈞njW*Z'FYivq'&u(n|^uj̵9DBbONƒeR[I/䘊gǤ ii^dž씜+-1u:@ڪK~4 <,4f-&Xc0I9Q鄲:W]F\x8Iͺ`efUϜk p;С'.k +׳x[!޲X.%G*]7>\B(;{]zpB[$i'd?IٶBR<^U7"5Emp]Y좃'Z{* ,%?qN4&Rs5ᬾXYvg%po<}TLb3LsW< Hk-O>_HT0t"ctɝ.|23@ȟX֢LTK:Y<#"TѴr=yl_":ssC5ɹm` -8) +ar"'_~W⏺ 6# ܧSsCj spݰwZ !ts*;?81;.GH}vlfuUIYN% C-ڢ=tLF O/G㵙`~(o܋ܮ:ڊ sR!ߘN,oG_Ҿ]yw)bLx}W7|wHqK7 \Mߗ{Xj@;ճwnFGU0 _9O:_0tqk`yct~i[0c 0j{:*^"h+f +:Q|D=jy>@sW̗7 Y[O:䐝h~.#YT*#Zs6z֎CSI.u:mqcb!?2] ?ſ]IcMO|R69L<0Xb`ߝ XJLiu7ےx3L$ؖ8ׄ|kGr܉{m7YL7&9'[*']x)ۑQM@:GRfc#n0wcWr0:E͊ǖ4ccc*J8\mޓx˸ߟ a':z_ցMKȢ# +/Ims<,0霉lK M1/JZv~G]B,\? dϾ{gOӹi&Ffcm8k- ȹ|E2)=tpp.npgJ&f_G3> =<*;3{e~W + +nfNg˫.MXӨda::1JCʗr]Xz"& iiZgEOwbJ] :[:|;ȠYFu qYhȣh䂉+mdhs\4R~b!rH,z#+a@5D.h)'C1gz <6 bc+_/cG9<^g[Hθd"8Rj2,r-tݎGYw֐_wh 3Ps,AZ!EP=3C(FI/$v{ZV|\£y cc_KlE"|7 !o&<C/ޚXi0fE`o7qhN/*>m +HX m_` w4 BZ^Vo>ҭǷVKSӗyXxMJiXcٞ9=<85)<;$ [ {OI.FJwkL1AT4=:c_ xAl$;$N(89㜊34DRLc51v]fh84CT^K.\d3y'<}~Flb߃W&њX'lإXr,ݶx)cDii?0e[DThbXIKqu$Џ՗ON”}gm:3mdf1ƒyhJl|θ:Ix?:'fzgJ(Vp"ީ{K?4fN`tR Cj҅E-RjZKKٹ_OaOϒQ!X#$ASA> +c.LәXjkg}wÅjN0Q38iQ$&0`+?VO oRwGªs8a 5wA,]=7cG(.&((+U\1T`~s\æϡl3.S/Ql[I^GvY7&7[{prxPMa5-v{ 5C{*~YSf/ OgV#G|O&;>_$ +1k=Qq9{/D"!~5Ir!&vzJ9uzyQcߘtvgϾVfLf6HVrL [bʻ6kg0Θ}fbh&1@߁#әn.Ÿ5;i![AhC[hx>(=+L@#:vG@kOOg73؝YqO=K'# g{uDHٮdx,Rc%R\(m0 NȎg86{Sh.O5eK=ǚ(b?#6p!VF`mjx_ T&К"/7ggNfoߦ xfFRh.4c2zWFto;tTž˲PGx¼Y~Ju:D6HKg'R@oLD-+3V`*)9="hI t0{qYEE9f^YQf\$j1њ\e6b-v倦-"n@aWhiR@ƻM/5֝ݿ<۬@[Dhc@oLc}&" 5=f㑨4MQ/d2z2~RZh%y>I} (/cYmpI,WEk%K୩<ٲ( 4po.c6%^4wD\1P#-Pu: +V t6G9;hދ tnw1@KTpB4 ,Dйݳk J(5n +%D@h}ߢNt q.W}4o8a{HuHi\Np~z!A2o#^#FMnXjz +Z!*4@OD <}Zv(8|~*mہ9yܱOǖGhI}bbc%ri|+m{tO_ӡb?L](s_e[Tby6֕{Pze)_Ib(˽m֙ v٧D?}Z<:h 2l>͆3PlzEEiW>X,Pl(E07Y@O(eޘ?kDVZ6(ZȦh +fu<ǟK-势eZ҆n橪?IkKúDԩ4a s#.ZhyXMYG2csъOE,{IedY4qcYYs |p-\t6E@%=QtS[\̬zjFPr~︘XȠ=؏avNFP3 xDo2?!B/I +y B[qR;G1AZ%5?3/1>Nv|7<_C>I +>k̟gX +gV-~$VugJYʽ~]OyIqq)O%EeK(zlQcka' eͬ葦]U+ ,3dp#WҴtb[nUx:bxp޻VF\&H"vFbQjn37b4PZ$%aw{ |a3rOiirnȞђ8qoӵ#z'㠎tgΤt/]/u):Е=Aq. t?/&[dfJR O(zD_$/ypB>'Y, 2N +rJ_q9%ÜUb`35UK7k7hzZÜPNK>+^wEY]Ysh1%y8u7&m3^af fpeR4,\mytXi UkLPuQvb$ߪ1޷H1D0l/}lMX䥜A9VRASɧNE lUڪL>}s؋̣-6:Yq-ԉNjY5 mEBArOSl8) m=,{"QQ< +]\ᓳ$SnymT@<LP,A #)>dHA1]@(-ђ{ۛղVP8 ,H~[A=!ϱkSdm;KA.#OZ۱R"%.`/ u^cp."pKz%ZR,(ɊQ +Ɋ"$ˢЂqC04Bf0I%T7N^A>pli+Nܘt"(Ȣx yrSiLAlJ9FT.Mkhc2>Z ޻G"/v",,ﭗЃMyh|^:+~F4zS=ݘ8xG#M9Fw Hٲ@SC|[ Oա* ? +~ )}]rO )m'ռ [g!oE]%X47oRYSVy7:a퉳ts/G|M?뽓/љ`:%*`iZ)+; )|zcR_ r_'cD\N&RЗ@%nnhxOD0JdzD;zX%ڍ$%iUZ_h0kR\/.bl??h~vIiscJV[6Y[sGt_Ɩ/y12K:3NBg-UB$+f \-K <(0k&9 ޏ6^~~{qE;75%vpgfu!ρ 6_]?yLw?ZY؅6l_e+`Qg?_tZ !K-} p?T/'UPYb cm(Ѕ}b ~t$$$8])H:a[)ҩa'jQ:%s(=$"\5sгQ\iH$8GuySPK)G_A1Qɧlz|暌QHaqwm ݜ=Z31\*F(\gb |wk,b-7&4r42t,毬$= npnsuV7s%]T7cOny _]VЉ*]C\Ae/tՂW)WRC\A'~PC\A'A rBU5ttZjq?X +g: +:l *j-/_ $Jvрd7mV/NM_HKZ:_Zm:v֊tUK1sR :S6>S<>Qrh'zd*U"WJ(I̡\U4IdD ܞ +Wc ׇdǫ:.n4 3! bN9iĘ-v۶zIjnOZfAU3*u&L"/wlԗZ6^U))WڷƪCu%}.Cgjy`# +Iرɚ]U`ȳDń_*q zJ.mE:-tR^NԅLsP#KtAuICqu58{'Oٛ5;{rsх(0ϧS5}k ur4i*qS2(QUwJ5r7*e<񀔏S>Iա>U&,w*R{w E+R{J߯T~NE*7*RQ+RQ/Qv % Dԫl.nPT +'-~+fF)z)B)W?(A%pQA)t|LQ2 ~RT6WUˉB{,Vq&z"Ȩ3a.vsWѸt:/r)w^,{=GQ p^8iljF-}eM=l5:˴!zbBPbRncŎEGT3G=CU5KJ}1ԒS{.:>4]Yj??Nzɍ/"Mwzr;tߋ\[M'j:N*&(^/?n|5T,^:'tpkVUIurB7龩ڧ9_SM'UK1j:X߫]j:)㆟;;tRt2ARn5qzcj:Ȇa5+;UM'g[n5vNԕxOEk~N:(\M'["ʁj:) ^Neg䗪oTIlV5Z%TIsuv]ut-^TX}u(.oW'o]haNg* 2!QMa +2UrHP* +4.'ܘJbU.+$H!+apDZLݑŝ#͕#s۲.5ws4߹NvZ%Uri+Ӕ |gsl2t͝jDq6Ew?掭}SNѦ \yII^gQMlriO]tAj2:<+F5ihQ0O\_PH"Cԑ 9Y [`CSe,u6~Ofa  +J%\s6t?9 +:Ӗѭ،e߯T>|+(p87tT/̮o@E%dz-;LSa결SQgr11V0.YR6Hz߫RrKU]fP+zr9ԣW*SN'_oI\vU> &Ey?^uQxۋRV)l??N8.WQC!U;62li(d wJ; %+{ou7)U>`WnS'vSON7|*p'KR{Ew]ÝSQ k_f:S7sn:t+W>?Brι|Cn^z +SGVTtv.v"&(΋eL7eLZ,Ѯi1-eLAN]E)dT趟VeȪeUj)bDWb~UELrDDM{aT~a(qXS7j\SnSŐrtW]I)ou~h}׎T0U=ܔf+o}04T=׸Jj\2# h|NFƍ)}h4 r5\ݗ}z)KLfb'A]TPwcZ?T%-z𶇏)ɢ2<.WGL&W* Ƣnc%rGYB=vz:x@i; c>#U9ڬw/ )7&D`s2OR&6|s V\4g R@oR t`%4y +2=w>qE{#}v!ێ__I|C =:B}&arڪZhU͕%Eumɴ-0}|dl!],)>+*>_.}$>ْ3{+9V(':5qN)"hMDIZBœؔB%I>Au>}my%M Z!KG՗1]Mݡ-n:>fV))Yy^Rlyɧѹ٣MRCլτ\~v65ƶ\J̥ɚ|XȔ,n]ߠ5^_ngoeqBVhՂ% +-V6] ,-𸾝T`֏)$Qs~'ִqWVe\v=h 7򇬪BZ]"ķV`Z*uY>ɰgyڋ>lHe"쨴 +== ^nb7;"J{vj]G~]6T-B|ד#( ?Z^[zY]&/kN >co,5@XNG-%HSPeC,ÎecNK'$N>)׺S'>$ylk$,tGT'=V%F|q%? -Oml`y`]qq< SZ8 I7S{a8XV/Fc,ð0stҫ001ٜ-m"3A6qԇA6Db#-lw}rvj 3&JB{Yss\X'L?[mZ?}c>1 $dz^g[☏ͭc>Y?99W J 6Wab߿dwi 5ޥ ۸8%b9Rh汐5&7- +dXn?ow](]zfq.(]}HV[VQ|1SMc+.[Giء=C>fѶi WyuX6-?=:WqRXWƆJDjy]I|${F{Yr_9>VI[| +;bW%2S/-Vy2/=o&>m0 Od5lUkv]'*{ 5Pj[Re=@ tRf´LCkYŕy*YV<[]}z]7S+y)2=˧k\9,=tW$VEN[tl2_*IY J5V*V3HwJ2"->yZfl[ -iGfmUJ&ӗ $f¸R㽤l}܃ftζijk.'|$0~V'Nݛ)+7)0lםm2N’q>esoMn붷+c yg~;Pݮl h u.}C7:f0XqKY|38u3\xW񒹌ij!,z&-qt11S–⩤KR>%5H͖T67|0IW}i q?1V(Iؔ8T^218?1VT0Ky5K ;.`d c,M(5olKyKJjWiFY_X7AUԧ=_:в 9;˦ه ť'rhEkQcM=$pTaDr/؇ q☴zٜFıeG#FжrՕLQ&}]{lEƆ#J͖U,[V' Ok[#0|--rCnۉ&pvYYcR-4-Rqz_M%1*T0g0ÜU7g}^qLS͑O,*JuZOiOʕJmEw=E%MM-g9ti:UCmM}³A#z.4&?*>ROџ +T>u|Kg.9zgˮ]7#kx9mQ~p仧ɇ*qՓOgq; 9wb$~>=2yhjzUqwU␔P5CLDC1RXQ[hhX RZ} I L)&G \O2I^_˃t'*K!*&M,7' += 0p1>XM2%iZ)cUb&XpMU=Ĥ))ɯiU{p6eJV7I̘DU'7SMAnk)I͐䍾-HJ'!//cUmIa#C'DG~:@~Z5Ր5̔1֍1q8TܜK!)rf""]5{ˤURk 0aW4W![I{x] X.&ӥ$DXss( )rfXa!VdBUoMa'rzf~$@Jws_ .Rʝ㻗[1" [&kRgfݴ#Y +.&;kEfa$ד(ʊS  `RiWQ3f2TފS2VżaZA DxQL!1 +B SLFǙ,dGҶ?W(+2baeô, T] Q4쎲t7%^]vV_nO|W qBpڋ'Ԑ+s$۱U?.%Â3sKR+\q&dU䑐zO1I$I#Ҩ&xe@$DۥU7I {0 D䐊s#;.V <`yJWכGUנ*lnu\4IA"&oTO_O#(I ݲ*&F/9S3$)Tu3!{FcÝD #T@<MxK_IRLJy-,haNفD/ŪdU%uXpItoCO* 7oǨxHI8hdI{<A!yqSU2y7h̝nx!2yFa _ NZft|?pC.lV@rpJ iqaU;qƜ2frv{ӽ+qHJL__kڟ* ikfuPwox.i(5#dbL㾾Wg0'r +JKfX&m^9I͡n1wG L+v957=0BJCZ$/"GidR1I_Qd{X) caDR4w=I^{5E19,̜ eƉR?P!kS+) fLI219?{Z=]J"f# Hɰ[Q:?,'MyDS3q=A-׺LFRFH* FHe5`_>S0i}^ذ]2|n4涃47Z;68$%,DN9HHiDͲo&HN#;_uȟ: #&1F`{ Ӡɪjr2dzZv'7@*7CRy}H"vSGM" 5yȪd?l>\'Wn,0'LW,[ נb矫z +aHKުf4V@],υC):[jHFo6IW T|fh0s/F/La؈Q!0I!rHujn*^?ԻtIiOjqfh},zC/*^b2htYH 4itlkO-UZei)nwN7ug22br21Iv?kNVar2hd psg&wwGG-PLpk_o.RT@y&܌GyN) 6EQLLFOzq{,jx""襘ԥ~Xa !otϵHy۟i)iz(&o_߿C4y)_CZ`3oB"wsI#@nY9q~ַ#!ުFٗ4diȄdߞj\4`rVOjq X$MFif2B 9ު$pK]uz8D^f7zw7>d&)/2ÀI냀*O80G`4De$sgcznz0B"G۲.~UcV:jԋ}+F"oU3brD}KTʕ'%kuBG2,mH`L$)$%n-Vf|D Gr9FIMA.70g'9|*29Ƭ`C&jFY?씩;Bo  i6V3E,`*m&!Y*7dR +ZNJÌ(#]rHF1[UMׇ͏YS3#19Tq/Ywշc +uv.0]S1?|TE{ I5 +cJ\L~ea"42a:u<;{Π'AլHIC 9^cn}O wE-C)8و4&){=3`rLCG/n`EBGbՊ#(џ yyȱ}~= _g!E7"PͨD 57d]e5߻.tY#hgpAogoDNiJO QpiɋGi7&% V54r\u+틯u_WÚgЌFoߎO19ɿh>}۩r 9o"by!&#!p@pZU8%y;1w-u͘U8ftMUe %L@zhb^ΜܜKQ kyyyPjn$Qc.%iL\yжQy4K䕈hX{C!IoňHXjxŷ{QŦ5ƍǻ=mr8̀ɟ>%Zr哑5q$Qisal&tRQsj&xŸ6:\WO D:ތ!zH$MNvaʕOAHrTa.gV{j+ɓ4ӽ7RäբtS桏DFD@Z{yfY LtWNմvnh0HȤ"1` ɑgp| oT,1"YzO;[1OQc$ϾyK~0 Twjfj1.&;՝IT]9C^CR8>5/U[%>^`Z$`j7z>$ȎOLɏtL&&gP;t$B/vp0hzUT7 aȺ<8J<[oFw7y7kW.V7E5=[1V3hHnɬ?0,`&pEpBcnp +RiqDf$q\<0-y:4շb4&GvNAẌ09tQr$5fDcnţ{TN#ra=BY8TVU]8'V578m9̺&|v0םsm}ceG f/hhbE^!E3jOd^R,QPߗ'p !@f^uN*0N3T#Om2~Ivt(a2i[U͘ljH"FwÚ]АA&&G4W-2,`J# W $:@!U砣ssx +3rvU֪HSwotŨ'hۭp#ϾMz*&#\/XVFE`fy[Z#zI!$R]y#R"~Vf\$ˬ&J%)}K=`2"i1v1fI+*^+$X# 5p1W" f$Vn-X߀tnL9|I-#$mkoxH8t<̀_wȟoI,&ͷS2Mel)c81Vvz tQ1)t"m,wL7 Ez@S$4;~_>(S֖А`j4&$%`r,]O70 ,bz Q^Qec8~= ;AX .C'$b{}/Sͫ!'yjݩ,VQ~Ƙuߺ,6ſ۔]-?zUvoD*9!ѪirV9āZOeߘ{,*nX&.&h/NCB譸?sU9?~9Lwf;='+eo$OqYzg춆[= 4`2"j?>!>5ʕ'Hkz0$G$w&xb9RjYzL{}|k !4&_>Ue}$'K`AOc$iAY(<&Ɗ?C@Rx2SvK0iL]!/YveyriGEELj`JKG=o_x@n8d1E%8{r壚'wgqQWxIAKUA0Uo]E'ouKГGΰ&#SfX+[CF@G  +'#O`V/`탞gC09rZv嘘TdI'L!8:xI'AL? |F"CIpv_<( +4=ؚZQ + .r eTg@3OI'@4+tJArf /6Z}{=wwP˛Г`J4˕Y|L3YncideaB>JA.vT$rv 0sxW= Nk,`jwIUqEѳ0AŚt8 :A孞_G$T8M=z?q)阗O&9<5Z} 8e DC.}l=JAF.&!)A''䟷˟!J2ʕ''U#j sלQlQ(ՌgQT5Ѭ06Cr\yRFT0thI } A +ϳ&}V \n +%8Iߕ4 QnA++ `7t1Y*.ުA͖8y 1`3DLjh8&ӣzvk8I!Ch'A0A^/!1Y RbW9I+YE=qX8}2qSj}sWBF)9uKIROolxn԰e{L!8|M)>m $3^'& jn)&$]&8$f{XLZ4X$Xƾ] 8h+dߌ#twW(c s>so@|&&*V5-JFoDqP8zcH rG"#yp| ߻"$B(G\>V=)B-8݀ 8i9JW"ϡ¹Ih zYfTNIɶ0YQL7Һ.sa(L&M+ܟkܿ,pEUs\!?_=v;3`21JA0 +=v` +na(4-|U5c)N5VaNG}]4IK S6n4IQv;0Q:|W5[J=`2*S'INjI(-o4dIH}|`c'rN%B(s|@w/J/r;8r UPD8o= /Asis;=j<`*8PqxdbSTWghqe'#?^܃2A 8sjaj0:w$ +u_>#3wL6x0N:q֫n+^oB(s]&]/QȾRcժ#!oI4Nn) 8;M]ހ LBB3rXUpLi%uu\0szS@p+ܟ}c@~ルnPi$l.o0ȁZZPГ' $&;(G<=yb2HͰ=\Oةf3qzt`9DR^t}퓰R:d1AOf+SҋYr&xu8"'>UN%ha~&g'y$zA|v^gTd6(!zv#bJpGtzք^ovviΔ,`oNy{ TS8G& QVJ8bQ~UX){ћ|êZFoD !K='쎅\y +^M(ΆV-X'w> vĜ?-PQBOf5e!\bTf6]O2!n' uB~0dGP^(=wg:i꿡_;qрI犬 ?K4Br=a;Ѱ"P<׻6a&$3x_]/SL)1o,*L=Wd OSUtg7*9>]&k¸ܿ,!1!|2-v&p"y͈&S'yD&փn n6Xja E(G $WDL~UP!Iev񓀳9.aPCFPBOfyaU9ߕBG攙Oe,rR d123M1@FE7 P$/z<%~z2&뗏{`U͇U$0. &:qzRГ$Mכٲl?|ҰʅUDLI#{X$o1enz`l^Ü&tz_G$R6GcГ (&٩&_#`2S/CTD : Q=" &+ݟ:y2 ==~7Tʯո:J'{Mpv;=`DqGBo5}(sc4^19ɯv~ħrLiw氠Qv`zR -V 5 +mG}z*߼,@*I#`gO˟!Ji' =Aq:xQBpti$$Q|$;DL~iXWv'n1 ň⎅&(G |ɡ4~V\!bS Q7XuB.sl>>A4IjPg[2$e>Wg=GQL'yRIc搰}d 8<޷;x@BJN:0KҧNM)8LgMָ@03٥I޹GrAB.1%19o?\'7onASDnB7NnTcAp2\J' W )s4z2b:Ld,yI՛=T葓AVkG&19UIܦB$Ĥca^z5EEx+ބ99W7 &$_K=(4>brv0.vf>|"&F^na7Qݶ0 zsIgC zwG&0^݂99Er)ov#%gbaU(Wp!vGhIIByDm H `T>"镮AOPLɿ9FB:Ae'Q)ȱqLFQyc.E )o=yU/ڮвDLkKeb=\T DZ^Sh&A:r?xsГ9<&i}'t(WԌh8z,:.  &?#CbN\ј=9$QߚwA<xC-I"Oo؍ &@Of?vPN)&!C~Nq%avTn" I^ja'SgP2I>5[ $q˳4D"4ŭiEX{^\醊&G427f\T ǙdND  W&-9Г$yh?&mٚ9z,'s k> NEQ^&A9,+j>VLaUY3[nVܦvw1 `Zs?} X)w{!&bz#_Ej{ۥ `2#mJ'9L]U\1'A0s>=#$)9<`rD&q&3ŒQaﱈnn\$_:}eݺ5oƜ'k7ې}qea Y=DvH9 = GބOyQ2y~]gR=I_w M"z$2NתVQLN-LkȚ0락|cI0ʯtH;Io(p{sw ʘ;Z^Ü&\fGP3޷;;e5ۮHI2j򏞔>sHz4VbҙЍ[ ΠNg띕$Wm1!0Gww `2s zMZ5NL뮅B0N`>ˬ%$eN=˕P寞Iop2`/8NyyZ'm .#,7M=Ĝ#Ys^O +v~ׅ &Dzs 8 znŭ[A (!Q7$e6'vKRLLF<%}VDd&S32qZ/0N:EZ U +g7:<#޷;H4Q^>Sij)II A8叨dDo&Y[ҵӓ<`Y_-&2J!1TpgwTYl .5V;Cun"$eUno +D$Q +੔1{%Vv2 +=ul!ġm-T6Ua -$j^o$dBJ3`{ 4L\Ǫh8ʘ;d$עL:G}*<`rXIzfʳTL*KqIGj8 '9YW;&b_>鋱rqg5VƼ3L#'L /GEtFE٭CO:FEI 7%R pej2+emLIђ;[ɍZ8쓎n )Ă hKqo!;K8m!4`2" k/n+2i&z,:6VaH30QfodL|Sf73Iu)82q 'C.vCO heGU`|pD۵4J(WjaogMr;O#a^tU`qԌ~<sed?)}|]S/z=qn += DDGn^'@ZFPrK^߁!aA@O:9 =iΠ1='#ov*g>_2SOq xqҡQDN 8Rc}AM!vXt"ISa>DQf8|FfPY!`2IC 89UhD'ox. ^ Lqy8#9FBg7ʘ; z r H*]9b9W'I^} 饦4=qT +rt=Yn'AL (3v19y(Wde̅(āqiH0x?qS$OyNa(FzCRf&_ +%/WNOpwS LG8LO* L%KGo=CMaLތ٣'idDV9/^ &_Bi8٭,6 +F? A.F'}>Ĝ#{l'}0-W4`Q3Pg7N_A<]dL#:TPvrI^arD!Y7I%x>Vf},,rV&2l f);9(OD޿,XJ0I䏟>4z,Y`TW˿ո~.r3# R\mߎr 슄8ĤsB'A 4^>2ޙ R՜lJ+rnEﱈL$bg6a-0fyt~G,!,c{UIDO9i!z?J^iHχ 5x\chdv4<ɋvn֍7a\M0k1UG5-v"`҉CO: zŰ1FsV-zgovwC^ Xha솧i`WAyv)yUp/NOɟ˟9! &0!#'{ȊRq8 (?_7 e&PQM?ͲoP4"쓎^v#rbY)Ḿ$LPoN[r>@%_FEA  @!"65Mv>"'y7=U4`f5v_71'vx 턥a7 H]\a"]V9o]!wR*A&_? `stǢ` @"Ham?`VM<`?)}ZOg;NcDnj 41;iFT8DϏD'Ŀ'b;,`e3=QQhULOzuݲʱ$fwr$$Y_o5]094I$-9viQsu"awo5 F6 )^o{$k,`˕CLS-Dq'~$'AGSt8MqZΔ$q C.&_ɓ҈6IwzE h:x8| *+;GE'Z,mAd !jdTUhDE[x@J8EuII.((N_YF/0=Y;Oh’ɁZ.J7)Y&z(m[cC(\^V!)HV%fV{qIɖ(> ’I=$PLr2mou ju|s.)mcBF2t; zk;4 DuAc&'-ODyǏ,~1v.)%{6y缧M_[ ےX\%JdbDZ-ﲭ}Ip_=mI:ik7i$y['qӦf'M#.qQINw`\`}p\o;s?{YB m {QƼ˰ \&ɗ<-e4d/VR\ cl LJR۳R +m#8Sx7WU~& I܄= NҗVfRS{(x<qRt 47*b'qZ`Ad=O6IF~`0\&J,yq18M{4R(+-wԔt"o JibX s`DOFGnvX{z>{GTQ2tR>WR~Jivm{^(e᠖Z|1 07b"I[ݒx*$u*nH`{Ձ[kmQݷFbkd1s߼hZhbv\LGŔR(Yu"o=c 89Z.Ko KY1^kMQ +B\K8L[ +;EKf[ҚWrQʿyȎnG6/sfX0+=XL-bm&Tu? ߦJYX|Xe_w 2G7ZvN1B&f4 #ʫu6d)M'bͻE=P}T&O2(3-ybgK#)OV^ MWJK#PJ > 0.?Eyp#sF[VqMQδĔtSєsX}C4J5}9U_rRʯ+m7(є ߦ yo(-ѳ3iIF}2{Qbo20kttwoj|2B^ .we؞;&66[ҼXCL)tsADs.܁zNd?2[F!,3!Rǯ 쒚LC{)M'⓸sC>h/2A{/Vn?h./·AҜI]0׻bJ~2܄S9fYN' W2(R0zwiq{-XnL/lOʢe&eӎ@) L-ma N勵N#Ey a@ rcV27J龕HJ]ڂb L1bJ>9V;[V@~ᦌ.}/I(eUJ%^Kyz]B$]-p蒯^}?ȁbE v̷cI/Wò eLUX~1 Z2b;_{0)kw1\hTy90wTRmQKWg.A#L}JM$D|)ݪAӼ+=,Rb;Cu伶Jd߷Kjɤ$M + +g'+]a>۠|V_v+eJo}2vVc0WeE0.XǾe)OVXkkRY<5RKmls9+W{1 xqrSJuԽEPvVI(fH7:.9ܜ:32F1A/:} qHU;$?Z_bRzH2->z-h Hl@&ދ(DEh ?}j(b<"ƘN 9 IL Kˤ"WZ=;YE_/X67!_qgUK`; RK-jqv<̉&8{PXB!l2NJ˽rkYJ3œ5&2Y늠wGd-jۑQYxr"'+9'ypP_ՠ|1*]j./Fdp ҒA`;Cum82e))~~!ڞ;e;aCμLJb(9Pc1d^oZQ0343Yb9^z;K{1ƋFzLo!\&UddIł3KLFtj8%c;O8>&%zr9ɼ v^}b),ZP,0b 8Ė'xE"wC XT)2 =L 2G/֡ bLEe4y|g&Z&O^ b;{u%iCwے9IrYR>"$ojLBRv d2"ZC&OBfons r=wD Q*puR &g>9)ZI%N:I&5ދЪA+eGJ0(DL8<ӈw[xJ,=`;ߨR&uɬ@RSR\)OVXkR Ks="fWE/~BG6 tR6Ljbָښ-dQ/`v5b{&o`;C*'Ah(ICe^l/ZX|1=:R<|a s *^k2i.vVHDe{&_\Ldr2re75TtWj|tf)[4`d>GQQQhJP5VIHf&=XGEˤz/f-/WJ2dKo`d~HrZצZX Q{k^d# bָ.i/k͕4A)#[ ' Fes9d-n%}xi|SD ?<UJw+6b&ATҌ(6J̢~5no$40#%ů?l'sk {r{q(fGx]SJwѶ\l_׎OHhd,I*6[8 /k0d :IHQDD~gᗛnr`JGc@#\f`; *4S L:ZDVd`bvr,7K>y[]YP}Y)Ld轘:sh e JGw XUmN{-XƢ"XeznOXFY2+Y{&+!HͳP8%WRr$cg%Лb%Лm܈hM!,TZɟ*scI/}pșv΂ދYJ=QlA)s~6UFpT'RKC <$Ho=@@,0MJ/R.5W3(gZ,u.}RK?+eD)ܷ<; +ƀ=a.uW.wZp1* UJ?2#ViWپJd)mSe['CX=yy +zcB&3s rQUFQxFɾؽÕLP܁+hn'3ZLjyn.7 +ɀmAd.Wb`&XՠɾXr8F/M7UrOffT%DDgi)_t@v2IG|Lf$~e8S9FI}rU }g ?}M^g/"D'jBIB;S_5OBs +xs_.|%o!ƞUeaR)ELlnK^QTTFml]n)9O2iE2%OtK[o(O ݳ/H[0I%T턐4,E]-w:JJLrKF2EK3q+r'3.s kL &X6:4cW7XouiU`2<֯&;Ȥ@—qP_iɧGʼnB&s),!ڨ?G<-d2_r;y +-D-!EiѪA3iwUy[X6in d2"nSeS-֖>ZBn#%Y9d&b O1Ĕ-t/%V v endstream endobj 30 0 obj <>stream +dI"![xi n/9Őנbvi΃# Wj+R?Gs{A.w-up;i{h[adv[e,yl#uVA)uCI۳Њ7@3èIq΀ދFC~B?WUo!gb(e/sC&+&ov6Jrr?6xſO- ~}\)@PyGǔRC#IXLcT|#%AX%B2Yb!2 p _a@&b[(RQ{1u2YaIȤ HT.moAL2en@>.3)[ՠܧ?|B)SxsAȤB;HE^z}yM d-uVY-\j/]f9_AB>xR&ctRuk2iig zi5VwYl.+.i(6f[& Й~cJ)a˨Zm4g?/BX +dO;..]&f]^$_(LbR_5/Warj,QJL@&s ^RtN&`@A'|;|u /IuPѷzjB}!d2gKA^4nrK'r1mPs6rS <9I}g N!E(~SeEe]uZ$Vx8+d2W(W/S2%_khfOk ʹkۡSwA /Bq Rkkі k| d$|B zh$5SL:[}e6w' .Ox=C!t ]Lu3|j2Q +Np<>R?$vM%,U`5* ?QʘL 4dŠ&SAe^4w}3SJtc C-Ehy+z "{Q*7jwȇBLdd2nދF2!¯Æe)_҅ovKկBIGNgS*yZU%> @jd5jbH ?WʷHU*2X-jzf< #27yh9H _hhLPv@KE_eCFNRz*Al %Jq#oDl=k+VGU +ypO@p<נ`;mM{;L*ee+Ƀq/xI]4EKoo`iU}N^flk 4LZEeV:^bڨÔ_qVʶ +[%r>W߼ֺG5Pz+rp8dS5&߆C *d2({Y; +zk20BWJ0l52 CEE3'.4yj,RnmHׯi@/ukg*L+t[EEjZ싥R6F9ϒ֊hIJ:x=UrB+~{:iJ)*:~砪|qU-̙IV,ɜ Zg {!~62k.H0)LQ`aKNaX5!ěa{ۭ +Le&ODPRhD̢zMI|VM2mC r6@&MGyMBE#`UL! + _CcJa^rP + MTFΐK2 8X_by~݃ty 2iz{8얮I/qJ!{5f%((A)*J龕mh @eGBfd/c8c JyABRJ.*ͬL%v %h[&7Iw3=m!z/PPj *Rz +e>ɖ->RjUF+7*3AȤI!pYw.z^%XJ٨|>[,|OXJ!&視7:EJ}eM)i2 4%%NyTKpPGW؎98D \ V\#VVpR]>qp x2Y\Œ1Q0{ՠx!tL!(e7< ̥ z*̛y HLzL~cUL^4ᦂ+³*OynͿG|c#drPLphx2%W[_b!<@~_鮶^)Y_qynGVڞ5ѦA$*Е<d +{QAas#6~=X*nWN[aGs>nWz?*:;&dɤ[/OILt; l b [ꪲ6xL{ Rl'$JTW *!΀)b[[EaNK/z/ɕQ!UϝRs9=g™G/ +½2ǀiGnmSYgFowK9 VH.2YH@@PVCE?t׻#Is;/. +Zj z!` +%o?PX-LA,yƋ/ƔRPGhC'oFfm]QC_iU1p(%8$ +"UAN|Zj^?(%0\&LS< +Qxa7^eGӱ y_8?Y'eˬ2 +@&p쨲t $/D9- w0A9WʷaEYy5zZJf`/%ker(DLb"IދqM~qs^<#ѓ7oE-d'Z=b9ݢ[Ƞ%I H~*g-y㬷uh8Ӑ#ydUR&)3J7" n TLTXɓy2 ҂ ^4^mȱQGYo=#ן G7$V )+o27C46ǀ +CGNlC'=t29c{)J=V ;4s74 2I^Ӹ2 2f)^X<’lPH㎆K@su*M^[' 8{K/a_"Z.| ݀8  Q +0qYO>r?ͭFOqN)e[7]Ɓ ޿[C36Q LwLz/ +031-]i+\)ŔRc6K:ˬ*kB E|wE̦zCW"0 VjU76g/K9mt=j{JpdC轨3WZ_ټJYc%JiS/>k2iкoV~C>'S لom`fbҡ(eH}RWJ-]dMچX?-DwSTؐ&Fe?A&A!3,x @8d T ґVASzm2e)CNڤ2Y Q+8*J LXc=BAEpSg0&@PyS'ԶMJ-fDB놣 C)PYf˽҃yuOfBT)UVz}k‹IޢB)I9]nRki蒯+dʷgf ]TY`t$QPQ`%(yv>@DeJdr(V ' Q ԋNyU}47:R(ޤ[듂(Ў簝PJ@<39$h"^L1KMXgdD)OR|YFL";eR|>IPB&uA'K\*Fb "7f C)A^Q6/Z9v Z5ug)OrTaj5zg^? ">M2 d dFE}J}砪J+ Y A$MJ HLA&A‹Hc[/&XELDK,3 ]uC܈"60Km h[`L! @Eݹ&/n6xqur8A(cuk gZKA$E!JJє锒akZe%M">Bj렔 G2)Vo=ދB/8Zs="JI>J)Z$ '7>o= /A轨7\l^a%2٢rDJiXzL$Shf,y4^4lZ;Nº#XFd|/ +B) L~>zuM +Q@'mxMzp~;17j!+e?ǻX]J(%2LB&A%oi ~? _ +; x+xhA}jd "_ L AE}$`~9,S؂$BnXAep^\|7@ qa;cْl4!I>}"^)F5cd$0轨 ]vV{}AHI~ށB%>/³DCGsA(bSD ø'V~[zfJYl)J;dP5z$f +`2Ȑ:{QKޗmd^mvI$Bf)+XV B=]UOc8{E2ug_Qa@Db]2Đ=’>uǁ16W_AIe)&?L^.c`0'm^DwX(Z:fh]J(%ȘD `h!O-I JAD){O"RzD)y!f.27B/y.\o>熞."O'_n*TYJ"4~gC&P8\kIFoCXPͻ >B +ɣhi S^2 +^T Z|2 .<#ҩK י 1TPT.\mѓX3AkYR"K wR&* +@v!uaCFE=ˣ{@d'x+RB) `{&c p!>9Ʃ ʍp7QPR"K d[)U!.$XΜ Z5HFG- Îy$x`@dhJ(Z "~oyIg$[Pk;]Lz/v&g_9nDD(N)$bNKW?'?)7 DMtpCJdLs=m҄h{:RfW_y>x ᓈM)*uUwovW[*kZaM|tt:ˁ!Ň7L H qf7o'tь1QG/C&rI?h ~z ]F1sD[-=!_>({?cO;{OS⟷ٿl_ǵ?K6Y[+mCL T9Ĝ9 vfZy*čmZg;۠@&rw{d(3*]rlj`kvu:#/9ۣ +yA(E?SãZP0HK|E;.T9N4PN2'n;S_pxsJGlyо$|O/Y_8Ĩۻ!UKf(VK2މ:p`̱؍ȝ`295F t9g,wE)A7uQ2B#q L>ڜ:?!1+m>U[n $Q!^4^Vy l +O"r&OxVW#%!8ΤTdՙ;gxL .ϳa4܄g;s{/b;sy +fN '}|6&f<%?\AA)^{Qc$m/h^JUJ$"ѣ= +&HQ (D c2fJIl{f&.v {/cfT J-fR,DHС8ޭD^r +(@(3dU 'mF>mDB6r< OQ +NBXNӍvAf^◯4Xn$"u:'ȋ9 `ne!FՠtIZ>ȍ6H]2v[ǯ 7aA'NtEF۔ ҕᐳ?Z|2DX"%sB轘ZؔaBnI'SA/\"HfQH!b#AȑD :>|~SeE?L@n%sB{/6[mR_LA2Aּ>'sRz/fX'P%ּ{wU$BPwXHL22NGDڴ6bPfMN'I%z/<9<'J7O"DEIWe.q.ALK!~ͮ>?-"{.8KQ/D9-a'=:Q5(9Xkͮޠ`c<>2-1r`v %Ʉf,y'bsQd|h[$H`vY!JL-]eU05[|+xh@d=q&:RyẌދ~ +] |2c"B+VSrFT =~'_z7"_qˁ$Ћ-x0=|fGg3g[:9"F-[vD cd"6_R[ ⓈX!@O.`%CߤyX'{]}srtᓈlA 2AW^L^ÒwZw. k-y_9>Z6EM@_& Yֆ#9i>['? '_oe3<| G$`&}ދI}2ڊKE5oA>?܍݈l/-!&z,#@ ŠB޹ߧXIQ,y*HLePu ~ֶ>R͓KI`8hDi0ƃ&C|a~QQ|d1?~-pF 7`a +C9>yaAQQ'擣I%&`:z\kp(2 U`Wh1wvQ Iث] Yc 2Ҍ"iщ$z/$2I9`qVCYa({QE!4nO:6zɺ"(%¸.z(ވAVA-JZW$/~zǯ']:ƻt$6ZLfSJ ++f>++=R)b!:& =$/(; jQ0~*g /3Ƌ?BF'IWÂI<pb9Ή''KNvDP)( +/D)/AxPhs|ȂE jkkc)J,y# tqD; +(Hf UC! n|'$B`#[F" 0-#NNKMX򦇻ɏOOnuvQ"E5^g=q7:nI +.(7v]'Kag%OmJ,E DDHN] '轘ە.OktwBw1E b)J$B`>GUsr +/D LZ5(,_D|aM%{֮-^)JҍnǺA${/TNLtK&A'S.}V9ةmhA (:,"_> r ZM"ÇC.]u{ӝwX^(n9*@rH/yTNݒ:%VչHw9奄keQJphAwh$hrԢ {q(S\C}Cvw-PG|(yys(?- cQ-@E @vދs#=JOtA>D>X:wX9틉F޹(.Jh!AqQ;^e'AN%a(`;Ui0O},șzѣQHTD8qjn;ݎwQe{?߳՛W >(.JJ`m\n'w7OnS1i:f LQNp/WIp|+= DNA'AnB^vP4=xѠ+2.rWis,Z_}{+֊D."ʘ(A;K2`}6pU՛]+/Dɱ.Gd;Y@-J7hԿ*d_D?١]IyM:oR 2ɍ0vot~O/YSu7/[U)Z'{sh-2|Rwrsv0VB읏!5/!=?`},OFR妍pg8C1C3N;G/tw++dx{[rIo}ѺMwx*$&Z_ v'D$l?m1A^v9iw(5lrszϯve_^i|.OB~o-am7v!:E#&W7^2P$B2܄S)@~JC\8I_J^3~R΁E nwњWoVܴv(|Uv& .rԢ4:/Z'V? =v9w(qhe_cqğVHaMO"%aޮ`)YؒǩY$ g[/VFaŵ.w˽/yrn>%"S<9_z:$݄,_E>^9yr<=UWVܹr*VB{wϧ$RقXJ7k&zy:KIcZeSr#:@>B.|ON^5(,_ﴝqC<82v`t۳uKrO.x|)JLv?oR"/#9 -Ǜfm7vMs#َ>A%aϤ$רXN^ww8~#KY9WhsspbjZ +Q.^UA|_F1m|^gw  .51UYop1 8C@/T!3ใ,3sߙDyRJ#ڴn-N$S9IltD~G!#t'*z8"z;0ɁkKvr[Jv׬>'z&. hxL!yuK%kK W%o$UZ%2$U"J9,#X 9k/p]<^'v$`g Gί?yb<[O]w(*~*$/D DLlHu9>~ThJIiapIi6 XN3rO.())%>YU3ݒJELIƵ׏.]f-cO]W΢uItC':e' JiTU6) ~"Azkm +Do}v:zK;%l<3{?a܂;q0qd1v'o+nj^\H|rŲ'){' Jߊ:$s.yRDL~ o{f:I 薆CRvŃ䒵sٽUغg)J2T߼/br eyؖuU5| E]kK'q*1m|Э$0{)=C*Ҡ;'s婭6)2BI>4Xk҉l<1tHS\SM _K=?yFYJzʓ= 4>-ˏ֎F9VE ݵ2z*'>vNrD^O33U)\%-.wՠp19ΎH8G#NfH:}qmj% %}O^xcgVm[donv\iEh>y1WntYjuެ֖y;o(%"GOr|G 0;+e,sDrr˻lڅ}47.޾i?=0%ӆ#%IX~Ľ/%ygqͫk.V֥dME.9;9`䊻7\^vὼ,ha@d=qɉoE)9VYhj{vK}bͫnsX2v' >yQŒwwF’ ;'w]z^'/U=#2"4F E,_G}r{vʛ|YXd,E ,`e'?`gp>IP0̡1ˬ2Z.=A${Kuh˪{Ui/>~m?3$%ow_Pg{/>ӧڲ;B/*uawjnqƋk\M߄7Y7.^$KQA) <ި'KIPr4Ѝn^*/=r֝KjM$Nt;yr%~[X"q7\pǽUM{!}޺ ?^|<6IIO/!>x[( OhA vg}߿dR YVװC:/km,L&T/yNnz}?=Wo_[GnϼcRskͮ3 Ƕ_pOBɫ/$_y %NyjvSDsPȤ1ay9i@IbP }2wO&;o(KX@Lw r*E=@T+HQ&,yg~]lE7 %Cgi k-c ?[n涒 +{|ɛɹy[( -xO9MN3d+UjdR>Q} L#ח6>OrU̖O'$fRtQ\1ٳC^gʋJ%|[(O78_z'qVҺ.%o' ŒJ/yʺ/b e!+;9 >fZُ:ـ(}V'}&)dڲw[v4$1'z2h|jK'^j W>=ms邞JKn1% W|]]]kI( ){nxy'JׁUxa-?q6sJs՛}\蘓dpE?і{u鲵U?$߹i ;Ҫ;<9WArCފ7.]_}r٪: W%ɁOVNZLG'MW'|{ڍ_~hY.IZ [(vխ]pOէח#' 'y  +dMNrOM RriD{LmnNi-^_ɞ9irǦ5KI1d +s]FW[뺅Owg|;GWJv՛w^'ɗ+O^[v]\zw _'(ĉ]H^P&d0B)h+p)^ŅRiIIfU:#e)wmZsr=Ut;ؠ ;c9ejx'o]ᓅp  <3W:U-юo_'XQY jtҘz@17).~]T4r_5|rݢLɥk Iz˔r8w˖Shix' & Mt>~^A1`Ԭgyw~7вTaM2>EIhw0V2M34_e9_VB}Z|τ_)Y͓wK!G葜-T,\U]rOz}?e'$cRjO*̃kܯ=|ÕL=t#$Rn(u0>Sd>]Rzy'D7Byjnh.ge4">w׬zc}ɹw̽ Wo]M|8$?ûxO瓨Biэ!'gGS +; =7]T[;sD;6!yj[tr46Oj0%v6<9߽,!8v;α}>4{G:l+xn\n$vNI(^mƻw%yG2]PϬrW%%)rY}k ¦N9 }mtBˇo/e;E˖a-ϫHQ&`Uu[#|㛏.k]E&H/,6ٙzᏎ]&>ɻpL 4l6&|x> @dC)cЦ*N38m{V[͛L_)YrsYϞUNز$n{OC݄2܄I!)O^ +Gd~8!-[dR.amߴG7O֧?H.79Yhuk=MHO#޼7|@F=vN>RA0{U.G~ۜm+O8} ݸ_^G/hvCګϒtwx'L'ݱd?}V8OHq'Mך]g]eeOuUuZ~2"|NNm{(:eVy9"j:~]zsjks#|B_OΟeQߍIw{R7'gɛX_GcdLG,+h+h'ᐳ|'^#>95B ,=HN#Vh!t嬉B@ qp5>΋d܄N?^{zxRgSy2')o7T0eAMkheay? 6E.9dp-wT\r٪ū*z7| %'[l7:e$z޸*i2 IwD#K(_w[L Ө'}m= s覸]lZD jPJǣN>_y |dysC |r5'yI[&mN3Ar0ՅI Х{:N햾,wkǷc{ZWۥ솮z8z yKz܌g)wоE+0BF3 ޵]}džr^2tg˫$R +snt$z,`20Y],s$u{n(z9UR29Ds`Fn-38%$UB<ޒOoup]ЊQˉO.>O8ƻ\UpvCjT=ϞQ:z6.ٕrOwU8-Z;ZҾvO~ yGOMw^Q6]J|>Z_}R+i4iKuKF x8<$b_zpyew2nZo_[.F(:yeisq;L1;=êAp;bqܘqի{|i/X|ɚ;hz>YA/5yc&Q>^~GPM''v|.-0?Yk't6זm Rht^oRu@@:lQV\jcc++ޅ#agO d@vnp[K|rC4ҵf:#Z\EƑsJ[7g:Y0IV׷.}rmO.Zᓱ/]k@hjLo@iqQ83m)pTiˠ(oXGyrcww`ѥf3VBpœ&>rH]E|I? ~ R&Xncs{i9lkB@f]ݞ >P>R+x. +3$=dR\ow֭D,}v35M?}io*>L)4emWmeO<?1D wD9[Bdr㧝|%ҺG/(“w4/&mj϶P_Kܛ{DIđJɋ ;,#}<}c-;˽^MIGLن_mx]|^AYgU(/7K's{7-JXϪWZy6 wGCu:&&ֻ9C ^w-gI]ADnđssfTss{+K=|3eI̔~ܧqc՛}>x+HQ}wt eK'{K;\tly'y& +$?x{Uq"⓷Ts\h'ᓦ +vny$id$יU,yIIb֥|%Z B;t0po(6=R0a_͓aY]LtKGY>~#x,+mD##/(29\?ڡ:T)>`Jzgu•uIF$j;RrUNBOИw>ZSY^mIΦs&R9qsLj8>IAŒV4Z 8_yEk$ndU9'LMh\JrA3#!zyO5]vW{ʻ?|ӻ@Hb,$BIYa=K:-rCȞ| F)pRZ $l e{;a{XdK:H|ر-{ށZkA}@&N`#")z* +K+}r!y1~jvT<\R3Nrʇ7(e\ab\ ]<+y=~E59N'#$ǢHiΎؕ-[K~yuLF͢~rE?bzr!_Гqxg+682udshcR_<ԾF@<~{׍W)Ma?X2ӓ9֟5zux{RÚ;/UOuEOLI~Q#9F8{H]vZz]{;'~A v%JƏv@ιAX}61rzjM' =V3>C oь} $One<ڷi ?LħCvˑ渕&fᚳ\u'g]~Г~Г^|mW$a&IӨ,ŷ,Z %8Dž%Xӵ{Oގ28䏝y֟,6G %R2pM4cɏoť擌fd$hLES>M]<7PmHazr›]xً|AO:~;x,E3YRͤnROɘ{~-m$/vRԀ!7kiݝW,o&/b-?Sl (+pfޯOsm]rAG aqR!I9%yUi9?cE?1=9g$-R%54$%{>].0nR3֘s_Z6/Xwwr9;XMo|۳W$qbs̰ҔN%, ֑5v63ThɉД]k1=k d\-i9}N\㕋;JN[HX2+g᜝w t8bR^&0?<(ۘC84o+֟26p +՜}`zr߽go[y'RS%rHAyg3=y_O + SOZ-&er"3qpΒj/Wbrә)g}v K.Ȼ_q4[M$mi-?W2*5rd[mA{INl CAr_| of$h_{:EGf(lю?ŋ) =?Kd=} +:qp $e$܆'3՞,4e?/䦴f더cxsSޓ_~ۢkqz.ϪKQEy͢VĜɵcyr'> s *[*tܭ];nLv8D:ӗR:οzhx7qamI:fp8F2C #)=\a:}=".l_q׌7Ef(L9!N1>H؟ k' } T=IC_*_n4NX΢4nV[ozœ]bM/_RIDեRCgam0<()zEc\xc"QE_jY!x.oQl,[h~ GTr,MO)KK{M_|0 +ԓk"I/>Y_&bsIFCc6'kˮB6's^]>?xW^uݍL}΅#k/Ĝ_|?t̾ $%+U#SN|ahs[<[c"iJNd-8Uz|YLO۠'lJUd%p4]Q-OFr3u- 3yݷ_~pͣi[-k^ +)w:+v[mXjv׬uM!]5k\tdb'od\3'S]xm&=I'/6l9"vU`s_`(,UޤK=a">7龋ux 7𽠾ˆ۲Rd#*(Ԍȝ~"Kb!Rލa'(e;vQ v6EڢkL6gi31 =vܩTvz#'l+))坩ƶ.Rs]=w/k.1Q{iP+jx!J&<ʖP"UcQG:=ec:#ВVĸ9\&&'EX]xRbi: p\~oW,xbzc8iqN-ƥ eÜiWb]6gL,ޭ}'E{…(t2ڴbuəl$%{% +Ye&$0-. ՙR(5=񇋖n~^reSՖ/ݿSbXʘ;JG~Rnn!*Wz +>ݲ-Wtk[y… 'x[F tUM7 տ-UH=3z\;/榠dz7_jY+Kv j[G-TjʓG=>LQOx4v0rf]~ Г^\O >- HR2i4/ iKH CuAks}`~=Ϋn麋Fi.Bk^J#D)'-l Mj,8>ޛURXR alϕd`GB_~J]zE'b8N\|mƎoyp2^/ l") hv|;D<ֿ!%Y9={.͝W_cLc6w*&rW}-l#1#zW /MRTGOsT!6'tSL;HJ P_I[>&I=1 3~qOnYtᕷ*/#i/6zf'.)saitG1Zapr:J[V2{._N]ӌNafY-nCR +!mYHyϸ:(g_i䤾~`O.+̳y=97۳Vʟ/qU-OvrxHyR9>A,adla's DfJT'.|/V 8 @b)oԵ-R귛T*w/ +/9;^P[|m7]pQxn_KerZf@I_Xцf]9>pQs3Qv=8Ɉt:-IbΓ\]J?SVaD*{Ba>L >x_Oޯ/Wzu|^Jv22p7gT$<;!sssewJI&eHO't1[$"$6K<7S0/yk=|)L/>u>d~/H9b/=Qb#;0yWs'ceZ?bz =%R?I|BX))[8? >Fy) Y嵲v:Cl8;!- !¸!Zvnn#=g^"$'žx`ҧk4[PB){ ,U;"0s9$Y8Mz Nc6>1HjGSHU^6m M&)Wb ax2;:ՆIN\OX-\zҩ'Oj` +CRVT?גPUtR&,r6M2]i +A4$¯&jޖM]~bv/|0`b/) >cr^Hㅲ<4SJTȍdZrJGN +{3q'xdAj?D4`Tb,)Rޱ@iΚKt8+BDD)C9bf2 aI]kϡd.eJzҹX:Uڤx@kIV"ȉX } g !F)2xcN^C' n5[Itkb0!rŠX?đ:% ] +ER!]y%|!ԓ-YLOj zҁ`= IJ3>!|IӤ*+)AȰ;{cz~UxGtќ$') =ŋ'7Hy⒘Izڞ k-6 +(|%lɌd21>ȦZa=Xⶓr$!A`|LQ}6(D2=XD1"9_74!뭹| 5'ܪhÁtM.1:.Pq|yr"zsY3q;C;ٹnʏ7j,'vﬓ%ࣰL$XN>{e6wIٝ/W`tz!S=I&&i2%gBL:j8'K>n;i1p(іJفs}8eht j)^%Dq{0$w#et⨘"Q;⃆̗-#pH!"f"VCԓM*;kKLOX =d'-lS&ڒ)oS0zMas|x94qK ag')V?zanQѓ"i&"m$$`*YWnGRV!?۸Փ5IG8FpH%IJڋ9{`6X>FrrȐƑG9L;HX⇾+p*==ԗl6!!)ocw7$ex?5M._HSf;%cdVN1U +-O` D×)oBctrEr0X~<!uŁTG*zKA2g@Tx4C&4}iX >^HS0yK<(%-[rG(KuVhK$bn@F3O<8Y0_nn$eXSJ&)VCIad?'})Fdz K8|`4 w9ֿ)0LεdV} II>箱|Ιl5DҎA ?s/,N˃['BDTR5R޶rsάZ%MsDڔ'2IONt +E@ !I iQVr; z +f )ÅI,y,S/]F xvyR,NI/ 6uLQRv3Jk߃vŔbcW^lӹeI',ccMRSOL+rߦDeO\vSJuaQvx&AbآOMJmMPW~ b{N%ct؟c:#SJ&|x˜ew&9'֧`3LT)N%]d?՜1ĶRlK:=RŊ(%'әbcՓ V?KI.5f-%ӖQQ]OdX =i;Ji?v8B1b&Jy< 8 6, IBRvJ~sn͚כQc[Idda8-o~#۹[Pp$F)w pg:b8F2Dy(S;;`d %ӓ n)绡'MPHE8B:-aҥ+_nY2Ҝ1)6f=w]M]C;ctIiJ_8qTKm\OR!NhŃ%W<  Y !)'snaMcadiľ(HmqP-Ƒ'LPiΒc&)C|Η!|vcDт}v ՗AfA0΅M KR"址xё*$x|z5E)k6o ;ٙIiy|NI,A_28xcIS`+$6ct )'DRv+wʗ}>iwl}1Q(8Q%e[mF|21:dCRb ._bz)S|qRRn@2RRzy)< eGd\$w ;&SI&D|.˟.Fg7 ER./: IV\]=LR SʕRO} b\p"3TavZxX:L7 1"-A<ir˟?jHK&͍ٖQe:Ob8n8X|G n3Q}(-)ezCbE+ =)ГvXv2N8C`;{ 4Q#$ec`׮VI?cp5׵TSI1@!$e[eH )4tKonʄћSTӇy{xحtyR T2XV.16hA|ԐvTEs$_׮;Dֻs:X ۣ5dh|N|I@"bZr\atѳÔ2WfђV:sS|:=lғaHP=dnM",J,I& Dy$ĻDȕZlw0=ٜ5ꪡg']\O +?L'EoݪҤR1/v@k%"JsMd Bz23ГV.cK"8F@\n@B䟝?fH+V/XBSjHɄ%og~*̛cG7$eH;/B+K$7Q:qGۺi@pcz4䱇2uSZ[F}[.O##~ +(с >~:w4ۭ4VHv!#ȕu~TB:WC0yY6ƍޯ- a),EQ}A\{u3X,cO3:qP&p.ECcDF[8׵fQUdWre3<!M׫빑 0­?(2s(rTvh"Pz31œ]t^lBg7yP:?V'ХڵrJ^94d2W}LB- 9ĄhšI9Qjя=iڱ?6A`4h4װ(P.W2d19,8fXy>J *,-P 6Fg$;z< =i3q +/v >Nȓ\FxGjTK+\cUE"&˖ӧbr˅b>l7$ _;D=i:/86 lM#ʖ5dk(ן +'X2 =tcHʘU)/t5Г,G3Q'/?#ȺuRJi*r +208 Zvcq;P{ܖhHNH|Q.P{$5ۡ'X|,wQd(U4|yU!RN grztGo!̾>FQQ6{Lmh/B$zLyԨʓ[dzkȣ<8ŌXR2 Wws"!HQa)xlR6zrI+0`-ovm*f Y6DVCASXrq"KmмBdķonZ!&!)chSc& fuMNtBKU%o4+#?$\`Xf;:S &)D%GmNgߝ  oHX,c#\yGPUxn9/MV 9$& N/,uUD,Otx_)4 _4®6B虐^O=2mo15~"bTEfu+RΚ$e䨪ɽDv$娜*%UN@%'%d~HUTٞý"26UIbҬ3.D5΅ё#7jQJHh-ݤX| !#|*rζ\C'WOf²1:plPrTx-9!{ARFaqk $a9/mYU֥' NNq%r;zw]crߐ#9ɢR>͉zmG5|\cjHϮ|UE'8#89X2aM,с7$e(sMt @' te$"WKe\Bgblq1%„eGNc?BRF-Uq*?V8ZȈ@U9Sn ڦؚ굲M<"*"Yr{.E1K)tHIʶ\ߑM LR҉$%1Ffg = ")UiTYΫ"m_L/q]TzNң}gAeԟȊdҤ9Yj )xd4 @4̓OR:rlD~㎕Ԭ#kG˅_tU?nt<JH@|9ߔCX]7=- Hd$2@iUK%pr42 +Nkaޜ wtӑG{*"JH ڨ=7]!~Q|E4ל2 NԒ){S] vkaX +w|@D)ྔ EXa/n_ww+ %7EsMOʽ"k\b|v\jH{k+m7]Ba)(b;1 rJhtH NH^~g"t@gtͺ"P`L,s(Z"/"2FrEʗ*{HBJ9r@1)I&r=-T:r%Yulir2rp"4]q c ,}Ƅe[&tEbj)WߘڱX$UtǘFUdWܚ%{7"H]CǗJui Z90%31:"J~71I)$PH6Al >6d~DsMwܖ#7ڵrٲēCzrj5K/,ѹcB2Uߖk©֖RP){ nVSL ”2g=کMp`W*2Ө,[FUS+1@aNˈIGtې5>CD5$$xp4@|77UGk֐*>?oY0wt3op*ct%%8)jϹ_J%⺺7 !#-#QU4㐉7q(8zM";z4љD[ph;@R%Q>tXFg7fW]۲"@:;zb=.M]O )H;Q SCҏS,$崄 z +]Œ,h3*4g*uʕM?|22zrr3nXAXF|yrct )3zKVRd@4$%PFtvOґ+5ilMR)D#c)&/Ӡ'h 2D1:}CR{OBy0M)SVݬbF'QadNԒ%{7H+TEZ'+WCL:!wtP/74vo$f蜄I񊐑=T٘]+/JgCCZ#&J>LOt*kpGGd̛H!HR ~D*1Ka;DXٕ'7TY "mk &Ē \%}d'I)TÞ?<VRHhGQ;#9B@@m"`9Pa6hcK!#mXb;G)B|(bup%JRDkR,:;+{ +c"P@mH:!'|dol44Q_pŹll$%fb|&LWBpCEB(BcNRe)(r4{R-??DoR +6XHb +7>psߐZ_[w7pot F.+$%3a)Z‡س?Ii&aϦz&JGN +RDtl9-ǗJui B W;zڞ'rbN(}ߦe[XT+>%)'>Ctv $zr5+1 ,(jpGOΝn!쀤,t)le^3VV7(V d2LWS +oɦb=2eGȹo!;=ֿf|D܂sڱ8$%G|['ћc Uk0]DCXf(MjKڑ&j +q$AE)icl.KR[93v:(09=N(a a5=A1:!)}|7]z`E)uMГD Ǟuwǘ)/,*TbCVB%"J)jxY<5t=)AM4ހ;'xKX&;zoHJX*,$ֹ&QUt &1Ise„ew<ݜ-CRžݤBxX\{u٧k9Dvy+%'a٨]=ct>ޖᏐ'y(|ƬJ Lc@zZ X;6ğ;zW>hHPzF"'EʻQwJ (*09&"8i^ 3΄esXct 5GIxo4/K]$CRFvhQmMA䱥u64-CjaYFbaܷ[*,蜾 '9ww&]uu*W7'zmU,b%ctIDep6Pk$AR[hY?)I2§{ L#,&J>TON(ѳ@8 IY,QHJRnw7d'2%$%QEטyWjIHM,l^Cz3%hN63sBt!ʦtڹD]]ؚz01ɔ9 sGRpG\L sߐZʮM3I9YR 5 ]9Ĉ5͉^ 1 +#[=JI痔Es˧+m<ٍ7ǛTT,[O9OLaIK*ZEB(os4%evoaD٦Emf +BO +N/k<8V;H;-box;cM9\R,{Cs*)rȈgbEL'b' @PKþ~"Kj)rD>U8JNi|ϭt)095ټôL 8AXW4pVBC竍Z?a9XROWr TyPpr+D z KX\GSQk&)^ҁҘ+9@LyԮ~`~~{G,;ƴ +RZ:@ʗ2i3'Rܢl'1.My(MTRQD6,,{ + J&>(ٝ<7 LΗ|"&m@7cᱯ;:s.k)~@Jg:s)iϲO7Ę5(q*WPq;zG5o[ĉvZk6>sJ*dǗIuiN6fKx3X-̄nhct )$>/Z%L#J=% AX@} +djx0yM,^C +Z51zlxMYjO2I0 nP5[/nsT$qXEzLXR}(f&@w,Y1:2Ιvx2̚Fj8ft尽֛j @"S&tE'$,O bNPF)F`b Kl%QEB !J,ibdz'"NٱsGSsq/)ꑬzy $o7)lvSS4[*y e9Ah>\49I7>`T<8u-9SE6=0"-G\@yp|D(sL~.g.]{~ig$ϓ$CSR_OWZՇ.K♜GRk7>`\M,~sKjEҖ}&A~^9* H_r,M`}Ҧ\$]7/y$'%f%͘4+INNv%%%''=Bқ-}J~wKi7'}i%Iuz3<4̙$# 'q CLUY9kR`D =H%Sp2 f*Dږtˑ5ctH*m&)}TUF&#WuJ^)6oHsA%s$9=$ ]$=&' IWvPq6WVًRų<LZ$g=0f-ct!)n>sA{d|4#?uE M>?eJ:팤{=$q&ӣPRh"r:6Jݜ1I #J@}\,{stE6 D*"ЅBX&# Fhd#S9t{]uys2i3$x3gq +[@*m,!!)k2)ɮty`=V^9z==$qׯ-s5DSS3Uo4k)@ЯL$e[pR׼ԤY3I1HdV@"9CPʑE]XUũ9{Se] pb2pGL19'!) B O-k\7Od&iIO!]"ldh( ȱ5*T:Oygna9O`*Wa!ч @ pGRle`IJQ#%&KR9#)ifpr1tU)%uAc! tzmSpr$@M,CQ\ +:XYy<(aTΚYUTC>]aamoL0l=WΗC,R:]uɹNb" XjkmT8 +x;З׌<^g +3-%'+bI Ocz7/z s" 8 +eCeܙOCBRX7'(47+AARR4)D QN\+"&˖>ωlݣv]|J|SC2b2PUx TFt:mg큱96&UEp)4BdC^LԐ.yӒ>Ywf&g']$OOp%PevGLQFoZ1{u+eTN)Co1ŘcrCZRÆ{¾[Lff]XWf˭UO>;.N3P$tlF$%Rؒtl}{sj֡`0*#"_4%s8÷no\4* x=]X_xY}Gn}⮚g~~mՎ]*?Yew?7>sf9]_U&%ERLӞvE/^:\!rDј[?a=ӍO2H&= .+]Tϫ꿯 jqŁ';?|^;ށOSߛ;g3'Bqƙ7o^_R&Tm54G&t тg,eiC4jR7<12Ÿ&٥ϯz1}8tez7roEү`8?㓯3=yG8HwJ3f`cJ*WRR|ECZ E?E)[\PWr%SV?jWK*wod?B/{?yS72XA>I(o<90*_Г#LΣ"&Ry?S/\\m38B7* 3)udC1F*eϳҋ5}j[⻯EO䦙n2E&&?@r 8P  D4B^] + 9{JOkQtp[{otS9QKѥ%Wmnc|a1}~I?.R(h|ӈ1qM T⨢qzr*zRmQdg>It`wt&KP9 @ %1-_ܒ7k.1/tm?}'cU[D4oDޠL4Qc,3A!''-&h'ڱvk06XkrDQ㊢?Z" mћsn}_+ן-?nR}Tx$| ]#|QՓwrct͗+c9MW\O LF:1Ec`7gN\&J|cw/;2I.xgЎw^);"Ȥch*bX# [rE]Lg* 7/r?i%&FpbQþ;/hHF3ȾxV<?/n"J?uOWToPR[ɾݝ+^EI1"Q8vE#X"DI|S +I ѵnrKIIt$A2}f.1~ϐ}?V?jkJybU-7Js:#Ì*'''&]Ib$& J}.;krSPE е굱.^6Hwwk~iH)i&)%}CT<@Yzy8?o>WK*1gRF뵜GOF|%R޾ )S5nT*tEpF䣇:_Fݦ}7͞[_ti]E_T?lxŁH1!Ҽ=^Z|+hӞ;=9m4ʢդ>ܚ2(e:/1n;Ⱦ{:=/z*y9ϛ;>o~}ɕ^<@+_ίܵz^|#|!n&ΗC"a-uFFhz2fzR@LzrB +uyZ]ZM//_!!8 ĈӢcJ'u֞EF/L.\L~r)?[+Tݤb}B/:F3;n 'CII̙VI!S9ҨHy &Qw׮mD!nehiӾ{ 1fCڇoOQ6wŕ{-凶3h +F}7׊12`r'Gד<8iԍw(ADڊUֿAG+qnҐ^=>bɾ٥U3^ʭܽr#\i=E'btГɤ$mtkd˧k)V_pa>|֛bKypm <z3"ơQVTzaߝ5duQ+Tqi*e|kk;/IQ}y(Q@OXV9.,v^+>p79gr 'XÌÊO/<%=빠>K>Ú'Ỹ4%beR/]R{^)fϗkݣ8x#%@Oɘ{Nx}$#J1 T"hrNՒDHކ+cwB݅mی_YʗWzurh>nz #;_.@O(.Zў= y$%'Ip)b:FKz۴烈\@jXK(+؝5o˫vdSˁ'i[[~J&$ڥH#OIΗ"FEU'O{zROZs$΃LΗM[LWOi}ƾ1nEsL^./*C1>gڮWO#Y4MӘ1qԎiU@OaL+9s+9)>(p~Iɚ21aV@#zJG~[Ӫ^̭hzWihӾh)f ldX2U&&aC9MW\.{rcLOa5["w‹#$_)ؽbxh;|ڐ}wn@O +=I[gi)R8[X9w:qp 3)ΚC 6./O)F`̮o{yJE0c"eGFaD#W'[K[wGZ)>r t9,s +Np6fǴ᫓L gOd41 ܌uȏbƚ':̗ݛ+Њd߽@Q2}T"ӍΛ?"t#D#H4' IxWNWK+p"V^9441i/BE3D#9xg(+[tQݖjq?ɾ{cc|{XRF|žc_0Ip=$'IlgK%(&hrOx50ׂ:_w0WafzsΣΗҫ6PiT[jq?Vx6(dC+ /ef Id=ެ%mŤX\Rzr_~ +Zrp4d_mY84%Ⱦ1?NuWm?}W4e&+woaruK!ďߡ"aݦMtYO*\O +fk=]Q|fdΤ>}s)(b |O+%{7|g~}Eu>zgyWF釩Kŷ_,{û@Q1q}7G^h $A5hPϮdtnr|4]qTfJimDڟ)ica\wƜeǸڇoyU/fS$TAn}hӥEq/! 'x}_o;Nǯch~ؕѓ⭩MvS 9D Ilyy5${I +N +2S9(!@FNMe9+{/?տ޵탽{˴e$Ax_ftܱXʙݐACHt'2S:rR ;/Yϫkzj~NmWT{ٽ=;{>:=hO_{oʾ{ %Dѓq4Ez3x%$Q+pW!OdRF{zz +:CH_sT%[dzwogb_W>O=>ܷ/~. .{=Qՠy +>β9K>]>]񺵆tͷn٨tv55mww]L[ncb?:^`û?{}q{=ɐ,8~ %vwc9on{nƔ$:%6rH=(^ +tas۶mzJ*xo^O$/?9"mvfƙ,L|xj$w,t0y(`<Sl*=i7mR5Q[6+|p]t|o5|=*L^nLv]K=LJkKדX%% 1 H4-WDGgYuY>tCA% JI9s`CA%I pi&#'/1~ZzEwbWm| }iûۻ?}KCaf%#-Qu @"zm8A]2޼T 5bvS_Í3J|i|_.VPΠvCGk/nP*|P8Ӗo*BG+U$')_rGM_OC:FWH`/6jsfsa_>| ?_5 {^`J|^h@XRd?ط/$ՓB:2t͚ۖ%;WqM]QA2 +#@'EbFFt pAVnjAzxꞭb0Qy@OO"m}駟~}/*s5_rǍW7XM>A|˟^d ~E:{k/=c5ײԳ{ks&8,/owk,{gN6w } g+] _EU3Qkmg4]i}Cθ_5 =sGv=oW{ {ÃlRݢ8ό KOU8g<(>>'[w[Հ @8 PB +%@Bc܀GI@dl5wclq{l|3wSI73=!}m#:eN}129!1wY\tW]=>k/Lzfx{KGl!\GL 7sǥkbny~D˨OoDr7D-V+_~gRD 0-$nXsH= ,zY,7.6)'^3'ŬƊynn, XX[5IY.+G:՛usG?᧿O?r޿Lsy\W!_̇\VUz9顱&%DBVaUOQ𧼇Uxѕ% +cT FJbSi }E3s F+?(\4T2i!nxKXXm$ E|rMƭxt<0μvʼnOg̷x*Z0}1#5v+1g2hNfeх;<5s%;zQ'qkJ.i՞ +#Uրtuy9{gQΛrLo ]Q틼id4_`^{tB¸ λ庫ͯS{Rud?Y4].2ﱞZǚcxN&S1K2g&W0>9,)Fv^?=ܿrVΛY7p}ˀnqwyḎo%tyok~uO=O:&s0.xНtֱMJ9oP'i`pPaƚ[;XQxLx yʋ$kzYFjNK򘿗.La\>>ǒ?ǦnA⹛0D +,i9n?&X^Ͷl1q~fu$Z{0EcOw]%?uN 6]!//^QHKsSk]`.a3%fn:?C QwC-6,7#pU: Q^ŜܬH} ʊ8+gSObJ!]]Iޤ<  ABJzk8vL7pF˛wv/Ʈ^ȇC̴Df b{Ug`ʒ{|l[*'G|=C{@XZ`7m^N27r[J _{>0s~@-DZeq2fsֳ)9qZ9oEsIDu QŠT϶`Ğ=P߲?D}Ƈ[F̌G(Y6 +V')'sj'ɶx7툅ҕϋrMޤ) 'fߌHL^ov^]#•yEtnƲ}3V/n1O-4^n {f+I$hMVzrqf& dʼnl+ݾܖHoo<9us>j QG%*+cÒܕ~LqO8 +4u}oOٞJOb5fC$ēݏ >aމ"hC!wV,q?y6Z%!,[/+c:Z+|n*S* w%Hp7,7D|mHycճB}Ia3śXG;hH@풧r_*(߲e"v*&>=AKŞO=nj/1Y?j4 +azy SXa_{ɉ܍ ww=3'K@2EuEiAb)*k Hok^-.n.u,l/'ֱ-RqVs]9XQx+MЇ sȸޡLJ粃P^.U›w2jH +QeUGO[ +޾4'Y*2JgNA림w2\(^]}Ըm>9,,@uXM OfGfOod%8 -+--%%K.?3YX]^M rҍN˝ &Nn&B;/LG&| ?J}"E_M=sO_7hF̠=๙U҃>^D}ˠggeCԱZi},na} g_校ۼ^nWAX /wOjմkDCVoQ 5@sΖ8+s#yad}ț 2-~䧕*W͟(g;Nz#^RT d=[|Sq!X/VfUnҗU(~YKx'#[&H~̺7#sR|5$Hd uByXg,\?j2&wtI RMqWQBk`[jv/F|z;x{+!5E9/NYܱ %Oz5pm,_p/U*Y&YUJJ?a> lTr$ybV]*{~_xU99y'77z97f ,E'.7,9D}Ni6ݬ[ď>ډ['$TLUz14Mc&ri\Dt1zVi`:jt,gl謺tOY8gEt7\tҸ<Ւ r*lOࠓ"kb$܉۽RvLNvIFM]җ|<|L/+'kZ]'_Նgq]F|Km2z$kfGD.egQBY͍ qLDm}vsLJ5[Cniy6YIȝx8c'B*'GTT K#6|˕f0O{✏n,w t Zpvit]6 $"JmOIZsEԑEa$#Dq~Mo^orIFeZ/WI+#|Lv5V!:&U]'ݰ M7MY+Q^lR ʝ^䳻Jyk"dn()ĿLsQ v+r33K}~\ H_Ƭu^u wz$'ugƃǎg&.ǙMX%04/cq&zpAlZ{ߖY9Fz&8&ś{ }orPǛwrN`@zv*kC2Wf.?%m>nLaM7X̙ddQ޻ΤZxalMIM+kFq)Q _x|G7yY452(0kQ_&ŃC? {\?͗.ZdZ8{ +o,g}Z>"ҟv #+Jo|uk#4ڝc^X1Kŀ(B!̽tLt IkżQ8[EDy!eN-f3rd^{Rb mU/3xs y{jvՀH,X)[t!Aig/p#+į'ę?{^W{ +#1 }Už̲ _cq? m+2}/I 7&}d!}y(Oeb7~kJ +#XZJ_ZYȀeXQ5 t&={19]ܖFwrN&=p=p#Lw2Lsˑz.ʫ_|A_1XYV&?kՂYe܏}iU{wwĎTQ )If#@#oY?E4@cNVR_ƀXuUޫ-|K%./fk|>kt-akÒM3o^^D>nKM:0%i]y{7nkJ?1qNmbe%inpՀt<*V%keynqY~˘X8-]s\Q8{ka_*wK[K/qw璼ikf+qpp˄EI$V-}BL/~ /ɗkΫr볬 ]g!5]49+`lן q ځ,a_3/NJE\1/gXViN3W_[ Bo,7@R{@ziwj HgN=!b+VC("w]xy03s_Y%yl3GXNZ/8' p 2]9?y}EEEMqW\kOefd +(}/L.^_7@[QY+-e2n)j )u+!-y<32޺![6Ӫ,a>g>//xEnKSQi20/1d<p2P +pH=e".e{ )vn,9ŀM^WvR.f_ծod88]ynj,?ԗYV7pFQrrZrQbU k5S@ACfBLb[ɞGJkQZ#8>u_J >ܨ@f.m16] aw +?aQP2=`ܸ঵+ +NaSǩ_OGb<݋r¡tᢞ~qϮtf[t(03%`SVxQ ߯ Mm +n_(0=GjyOfeQ}ZƮ5[U'6d,1[8߽~SZGbߏ{rf@pz/7y U6@USM=, fW 1{^)?j1#On޸ܜM\Ra ]KlV ~!_\6=s8]H;_/jC9L=0cPIc6bVw.Nm:r٤Y+ubӯyȀ9\VG/kM*}3ImYUEvd͝v]qCx탯ʹjM|(Ο&SԶ5\ubBo߯NC{a֣F6yVAXW_&Iwp +a=`.o:{k-;S[LփE?N ؘqMs*g1rHVvD0P +Iد%Oŝjn1EWƮۍhex.Tql/#SدWM1~*[>ycrS"hm2w7Bc-vwpՉM +ɗ9{{jn1,w-,u,'!$3>vS}-ši<$uW6,VB_c7n1Z7_kĦoN5=ӬͩlTZaB~S*#MRnha?οuk5o}$EdljA#/[7 [ޯ7EԁM[ZGOBØnĭװh0ɹk5#P{մo/ +~"DC}M=>:ȓm-}goM2L~~׿WhVYCJ!vq[+݈4{7<$?rf^:5c=o,!.{kĢ34+Wgwq)n5$?WW תP>n9[YXtqoXV0‘~cn[!2bFjeL*rpoy 0l_{#C*Hfo+#7Qؕ4CH]^U\Lf7C+2IP~{N7 k|ߗ~΃|&6ݠSCFPn?W t0cU1gs H_^H=db)5 +`.NZ:!Ups60YMt׷&5U٧򙜯+ɰBmZ.j{6^hC!B^o3w[4dc P؃r.0q:;C:vcvqh.{u&M^}h đ.q<1g(KxHcpdGT3>Fe}2 Eп0G=Ƽ2{XMgŢ {¥l`cv+֋DпXt(- ^J}^BO N0kA,-6D )8𼇲P6Ʀޠ!]_U]nn [=0mZW8l]ƽ/0:Z+?AZuث? <4CP:T#< c{e:_U(ׇcXH88w?# +GۯA{H|%uD <DϾ?C+56}TkxūeY ]T>HK~rWkbApѳOv{mX7MҘIUcٿ}d"ݵhf'{?٪rQItC4v2b[- !mycNGLb(r/7$k 'R^іaONS~wY7*>_&Jߘ&lw;hfTJ{&1On"Hq2n7~:v?E=ĘBu(T[CI/^^xj'¢)!!^TnD5x:JuܯUWvZbHL=jĦSp잣耸4H]yHWMJ&Ʊ4b&3oS[wǦk:({C=$nr$ݏkovggXZd=||mM%7ZwhKۨh{>ু/jԃj_j |(!8/1?d h3iO=ۥ4%7 o7 [8zaCCDI-ހv'7Bo@3nKLF:/f A}/K 7 B0 r.:s6 6)3߳z1ն6= .G٤)KQh h{D$p) W[[{. n$ǢtLpT!L[ϳZrJ]ý}(Sf;S@_Aƹy( +rGqMYxnkVm\l1Y=JթJ?}߳Ak d++on3L]o[سh*irg.  D33VOI*;C&B$J{vE^Ξchao~Bv Ozyt9< ٽƁ?_OMl2W܅Yb2F"wۨcí+a@b}?ks6gjQo :,zӛ.a`D%m78r>,zaҦr#㻑цEo;뭣Ɓ¢o̔o^[ +J!4|d\ǿDnD LnGJn~%ǁ@Ta3JX|)`\rZSI7@nĭt>J1tF4i.}!7y/%" ĢF,Px%RɭTr#6@q@7,F*G(#܈[|JCJ*7"&{>b/Lf:VnS9p*8t~?FsT ph^nub-q$#P-i.%o6=|Ï?;vq@ezs-j &@e*3*?W SI_;:>I?NaဢX4Mo a5Ŀb"a/1v9ŦY Zh (Cw/e5_nx%P~{tz3%>X箄րX6܃M~\Klh Ebƕ9^-mR`g"? 1)caJԀF'۩wTo˂Ħ"Pvs/_Xawma՛Aztj, $MpB nȶ 59?3 }#hvUobi89Lo8\qw+W }azqI>CߎAo\qe:i+AT@[@7$~Fq0I|xM Ǧ{K7wϫ:b#(q_HaSYoxzK(@3j#1cc8 +h{S#-:W>ǡoJhi N>tzsC'1?ytVo=Q{<<@bSYohM.W&"~Ƴ J66=pz+.iq{dW{I>HaOxFW7P(E恞*P*(%Ej4fxBەőn4~8H!126\JwZrqڀzb*MMn4ǦX&ܶu*^J æz(M2uqx(MJM}%@Ml/Tn"};EC:Il޸=(b]0oRy;0 E׍RBn<(91(MR[{7S%01bHyj-c%-y^urb.kW XtyˍY m:6#ܘھ7$Pf/* " 4n)@},ˍKw"RTǦ^+D2({ܛ7&!84M:`ވܕ@p@a, eZL mAp@YlzMJz/Ԅm^7ټXZrt9 +m@p@IZyj֍twp$P y#\p(E7&yџ*j8P ?)0~LbBp@+ 2!@Z6DaoR`3!8!JXIqg iqSԷo~cOG}>Z n.@nl:zcA+,ƢQ;V$@f,\EP/}@&FEZ&2P_p9q7?Jo$}sz鍽킅Rbӕ4I1ɥ.꧛ވ9hv{mo I%}M;&NkV ~nH?ita.M' 8 Ro|O;ȅC] 8 6XxaOF7~w@,fv Lp ! -/]E  JZ;I ȃMؼsz{ HC{cb2~wz$T_H1 3Q^$¦i7 HECR!7 6-3t՛I84[jCE2{_Uoy d]fSwü`ꍽN`zʍeNp}8¦p=1I 6>HOfa_#: ¦7y 6-誥 ·3dX-rA7 6y7 6n Mrfx-62vl7 \N 5V!MA HM7'i3H p'tt@ fހtXtfW$7 }ӣLr0o@>*; $W1i#8\s zdSA-ʻ۵Go&9K$eǘ81q7 -6=rn& =ia endstream endobj 31 0 obj <>stream +:yǑ bm:wlGyqhq.]ڷBo2jڥ& wHCӵ[ؘ "&0rΗIDL"-c 0H ۣ}{wڱCG&/@b~}wԡ)8I 1LoԿw.c+!w!d= اgfbTכAfc  ԯgNb)7 56zAwazS$ ȍMf<%оcM2xZG  \G^iѴ\3q'tCYm ¼ٱ釣F Ч3p1 + 2I!$ش#&ewpO-cӣōE(S#c֑t)1(M_:Mg8HH6瞒8t@޽DD yȥG mLo{S{2Ґjz)#Л%&&I # Eo?u!Lo;+7܀ % v?84clRܐ@R< )NC?<7ݣkXLԖIi9Էww=wH'}6zSF r+(6pI )Lo'5&~`7QA)=v+IyvRFߗ7^%Lr0o@VN|to짆wM_Orh ?ӛ !@ ѿO̾:$c2ճs8I6 hE?WN cF8\opCSz(L{.q@,?Л7/a߳8TB1wr.7'zVo܅=zt$ԛAb8t|nBԖpP"-:0ɿpH?vۮ) o o@#l:Ror'o@'l몮۷pf#cN;ws!y:a;0gޘ}{7]Po& +-AV]th/\bO:.b79}p'^8b< w{+ I KLrN"-Z?[TsCH7$whM_^Ɠ0o@32&UtæΏPor:qvtP 'O IJƃqL2d ~Oo܁-1+4plSF7CeLU Ja$SlAE ӐMo184h {No̻}c0]+܈!Lo7H7<Zc OV]tĦvh . b7%6Џȵ3I0BbVo&qeIzBo :qlp@ +C1x ZaІ3ygmXJi[Nٚ]eA; fmh+1w2 D56m.ReX6 wD7][x 辋 +C~N6mu0o[[p2vI8M L}%qhvN4ep'p]'8ij3HNajx +MLo`SgvEHE+ok!yr%N:6{-1[Ap&ycM_*i0oĢZN tC+:z,Z7iC2:pl?܂{9 +Iq:s7#o +Ya{^݂I#̂%Fj.æUp?<sW8K)c砞 zΟOpGDLib&& ;8Ǣ-IE`5ceLr`g2 tNX8{E8tnX8$8LpSq\JN/ }ax)e疗R%YRzHb@#`:2 "4ҔVvy'`h =~ogah=qG!^dh=pYp&9k8Ew\ҢA7EiL㈘H,:3H Xlv(i1XA=Xl%Y7_Ji_xyzl<$38OJ) "Rp4Zw7Wprax4A:-i4yH$,ǤY& q_b@Gfv1#B @`)9& p(hL2kc8t_ @qh|sSM< MƱ(-II1(Z0}QI-:}J7q_߽I&.܆ f&9& ̈́)qMT84JJ?}&d7 ͅ;ye-@2͆'ynT#J< %@ʏ~סQo +Wp^cL`hLp6qem_4hLu)JZT}!`'WAh9LF{o 7!cw8n-JxB {>!7&{'=_ 'Jo?$Ep#?$g킁 \0wC10pJ$ vDL6=p*L+8ƒE8!:4o7M\lApNk8tσC @顳:C~ao6xݧ7X)ܹK$YK3{9s9νz7 2O!@|JOpp;Q tEH98'ɷz@DrpOP{ 8'l P37Y{C |'_ҽs9&PVS㭹QJŗh@@x N0Ǜ$ߤ#@x e4 %'Wȧ.ڽ+-wr08P""wzxdXW!ހ +}MbFF|7vn%P<ܨ]98sp0߆Ș܍1y2ǧ$ಾES\_7ްzw7$#龻7@;c7 =w@_p3N dpGS}N$t+P{ Ԇ~Ñ/Tpaf4 g;wQ=yYP.X!c`@y鳨=tdgn]9ҙ~ΘAAn#9vp޼37S](JR _~>#P,tg%'i{K="8/SNͥdU Fp_{%̏yȏ lⴓC.(YPQ,|9w`b@m16t=9J|ppΐ_"I93i)?2Iڷ/b9@ëXto ciHGpq d+:.5 l PwU|y;r"Vsz1@gK57YGpv=ɡ%pnl4L|q$J|L'Jг78${g&z '!~x7N~f=]bsLO4(үΔ›Q)\ddo0?rf + ct,+@pf$yʀ/_9bGf|X +_l}8rr'_j-AUoif~tn=R],%_q"p)v՛nYN+y`#3L)CMئB&I;#”  r) 3 ZGސ44 GHANOprFYɠtp%m>hʄ”~'0DoN927KCMR `bt_V\D鷱=d+lҲ{6,Øu9/|/Ks<o1btft2 ȣɜPv}Ҳ{n]JIs奙 #T S})f{c#g_ B6LD鑙ES'@Q)[p]j-S_6C #C^65d89\*s յqnnUr@ɬ;[- :c0ӚcuDpΠn w=sp_`+ du{ 01rlB7VڟBC:dX +?gOBP涋mL=C) +~КVQGKf ytETmTdE8Y?^MՓ$8qJ\02&VQG_<26OGݴoX &9&Lz<9p(dl^K/r3"]0^2܋|% ?~ЛN-"ŖΟc#Ǣ#KS~69>QtG` oca+C o޾5olah/pnix %*O A^+cenGziYJS/ܨ _;8w}=M$>+gWTQ,7ҀHn d]޷ye< Rnp7S +G&ǀx47L:0{[ixgyrlKs4?yx9r4.|)6;08y it ei7 ba(s;SOms{sCE{]-.irms8ITx#L>7/ lHR졥±[y77`p+y3뎟릜%]!⽽^kahfտ>FYNp`koqɽ}'p(O\xMM2H;8fף/<XBB>L~ (fǬ=(x dl /"+D- s(Jf_3IbLl[i;8d馽P CdZ<}l7q}DX1R[i=pZeE\>5g&H0ZNB*AfcJ /#A S +WE>!pr@͍˿r׮1H^ҋ/P6dd2T\ 6#ܥ*#FƏI]ç/঍?/0iml3R +Mڐr#rM7AԱc}m߸᧫V2(&C@S +_Zv׿Xnt8<]>xG~~݆9_u!ԍd~Z@.]Zaz1M †8/xs3ݵ 1qK]17|s|@X +G#_o/-m5YQgM ңKf}]\{Ւ>X" Lƈ0y0psDB9z|(1/ bJS_JCqZ3V6 /+ՆgF/-)6*t5fMsRs"Kc(N4S_>U$P&)+mE GceQk\~']6Tgmm6Լ^Wc5g*cbE Ćx:)>n%ӶZפmЬ7g袲YzGsW`.#-ɘ7+Fkmj}|֛#W?k¦FCAQ]rCF˺ڼx}A`lN6}[Mb[fLQe5E&;Őܬ.]ї9یMٵm%re|v`-Ԣj-2R樾 +C2l27kʲB]_zuEkFV\L6ڠQw>j0#ݽ˴5EZr%}n\bj{RuD93rx G^r0&ϤꯏfΡ@ݗѮh rfENMݘST4 +mIT:VQ +}Vowö^5ݍ{cs"fjZL}wQc02c;ӝ>Rl[)qW6z]P"~`s+[EbZBDQnSMC(ΑXߝ:؝[d2csbm֖ ߥ,mj0QoӒX3@ggT*+ kkʛu9jg8^ng2R' )UUҟѕnHoo.p4$GMΠFh2s\^m1e^#<tNZ`TimYPG>t ~TP:-Jԓ+\e6ZU*SSuԷv)6Z,EI}v^ұ/МߝC40w#d*]Ѣn+ ڢWeO16UeFVk]Uݥ+Փf!r؃&ܙs͚5GPF[SRBY.ĤkY-Ime)U&92ͨn.6W-3[Zcu ZmkIF[\e'yP'+RB兆h&+ٸ^Zҥ5e)v}JSTȭv9QNhlNXƩ]ָ3Ra6%SJ][i[M9efBJwŕJ +"'9h,dRLQZJӔjcIAǪ|٘֘ڣcIi)-2IUV:*q[< = +p*EwmlعU{O쫀mش*0L1YU_{̻˳/T Vɭ +xTs4> +LL*\q3Q5 J,(I endstream endobj 15 0 obj [/ICCBased 19 0 R] endobj 6 0 obj [5 0 R] endobj 32 0 obj <> endobj xref 0 33 0000000000 65535 f +0000000016 00000 n +0000000144 00000 n +0000047649 00000 n +0000000000 00000 f +0000163121 00000 n +0000593503 00000 n +0000047700 00000 n +0000048109 00000 n +0000048283 00000 n +0000163420 00000 n +0000139682 00000 n +0000163307 00000 n +0000049181 00000 n +0000048344 00000 n +0000593468 00000 n +0000048620 00000 n +0000048668 00000 n +0000139717 00000 n +0000160473 00000 n +0000163191 00000 n +0000163222 00000 n +0000163494 00000 n +0000163800 00000 n +0000165099 00000 n +0000187851 00000 n +0000253439 00000 n +0000319027 00000 n +0000384615 00000 n +0000450203 00000 n +0000515791 00000 n +0000581379 00000 n +0000593526 00000 n +trailer <]>> startxref 593722 %%EOF \ No newline at end of file diff --git a/ui/classic/design/chromeStorePics/promo1400560.png b/ui/classic/design/chromeStorePics/promo1400560.png new file mode 100644 index 000000000..d3637ecc8 Binary files /dev/null and b/ui/classic/design/chromeStorePics/promo1400560.png differ diff --git a/ui/classic/design/chromeStorePics/promo440280.png b/ui/classic/design/chromeStorePics/promo440280.png new file mode 100644 index 000000000..c1f92b1c0 Binary files /dev/null and b/ui/classic/design/chromeStorePics/promo440280.png differ diff --git a/ui/classic/design/chromeStorePics/promo920680.png b/ui/classic/design/chromeStorePics/promo920680.png new file mode 100644 index 000000000..726bd810a Binary files /dev/null and b/ui/classic/design/chromeStorePics/promo920680.png differ diff --git a/ui/classic/design/chromeStorePics/screen_dao_accounts.png b/ui/classic/design/chromeStorePics/screen_dao_accounts.png new file mode 100644 index 000000000..1a2e8052c Binary files /dev/null and b/ui/classic/design/chromeStorePics/screen_dao_accounts.png differ diff --git a/ui/classic/design/chromeStorePics/screen_dao_locked.png b/ui/classic/design/chromeStorePics/screen_dao_locked.png new file mode 100644 index 000000000..6592c17e4 Binary files /dev/null and b/ui/classic/design/chromeStorePics/screen_dao_locked.png differ diff --git a/ui/classic/design/chromeStorePics/screen_dao_notification.png b/ui/classic/design/chromeStorePics/screen_dao_notification.png new file mode 100644 index 000000000..baeb2ec39 Binary files /dev/null and b/ui/classic/design/chromeStorePics/screen_dao_notification.png differ diff --git a/ui/classic/design/chromeStorePics/screen_wei_account.png b/ui/classic/design/chromeStorePics/screen_wei_account.png new file mode 100644 index 000000000..23301e4bf Binary files /dev/null and b/ui/classic/design/chromeStorePics/screen_wei_account.png differ diff --git a/ui/classic/design/chromeStorePics/screen_wei_notification.png b/ui/classic/design/chromeStorePics/screen_wei_notification.png new file mode 100644 index 000000000..7a763e5df Binary files /dev/null and b/ui/classic/design/chromeStorePics/screen_wei_notification.png differ diff --git a/ui/classic/design/metamask-logo-eyes.png b/ui/classic/design/metamask-logo-eyes.png new file mode 100644 index 000000000..c29331b28 Binary files /dev/null and b/ui/classic/design/metamask-logo-eyes.png differ diff --git a/ui/classic/design/wireframes/1st_time_use.png b/ui/classic/design/wireframes/1st_time_use.png new file mode 100644 index 000000000..c18ced5e2 Binary files /dev/null and b/ui/classic/design/wireframes/1st_time_use.png differ diff --git a/ui/classic/design/wireframes/metamask_wfs_jan_13.pdf b/ui/classic/design/wireframes/metamask_wfs_jan_13.pdf new file mode 100644 index 000000000..c77c9274a Binary files /dev/null and b/ui/classic/design/wireframes/metamask_wfs_jan_13.pdf differ diff --git a/ui/classic/design/wireframes/metamask_wfs_jan_13.png b/ui/classic/design/wireframes/metamask_wfs_jan_13.png new file mode 100644 index 000000000..d71d7bdb4 Binary files /dev/null and b/ui/classic/design/wireframes/metamask_wfs_jan_13.png differ diff --git a/ui/classic/design/wireframes/metamask_wfs_jan_18.pdf b/ui/classic/design/wireframes/metamask_wfs_jan_18.pdf new file mode 100644 index 000000000..592ba8532 Binary files /dev/null and b/ui/classic/design/wireframes/metamask_wfs_jan_18.pdf differ diff --git a/ui/classic/example.js b/ui/classic/example.js new file mode 100644 index 000000000..4627c0e9c --- /dev/null +++ b/ui/classic/example.js @@ -0,0 +1,123 @@ +const injectCss = require('inject-css') +const MetaMaskUi = require('./index.js') +const MetaMaskUiCss = require('./css.js') +const EventEmitter = require('events').EventEmitter + +// account management + +var identities = { + '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111': { + name: 'Walrus', + img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', + address: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', + balance: 220, + txCount: 4, + }, + '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222': { + name: 'Tardus', + img: 'QmQYaRdrf2EhRhJWaHnts8Meu1mZiXrNib5W1P6cYmXWRL', + address: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222', + balance: 10.005, + txCount: 16, + }, + '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333': { + name: 'Gambler', + img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', + address: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333', + balance: 0.000001, + txCount: 1, + }, +} + +var unapprovedTxs = {} +addUnconfTx({ + from: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222', + to: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', + value: '0x123', +}) +addUnconfTx({ + from: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', + to: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333', + value: '0x0000', + data: '0x000462427bcc9133bb46e88bcbe39cd7ef0e7000', +}) + +function addUnconfTx (txParams) { + var time = (new Date()).getTime() + var id = createRandomId() + unapprovedTxs[id] = { + id: id, + txParams: txParams, + time: time, + } +} + +var isUnlocked = false +var selectedAccount = null + +function getState () { + return { + isUnlocked: isUnlocked, + identities: isUnlocked ? identities : {}, + unapprovedTxs: isUnlocked ? unapprovedTxs : {}, + selectedAccount: selectedAccount, + } +} + +var accountManager = new EventEmitter() + +accountManager.getState = function (cb) { + cb(null, getState()) +} + +accountManager.setLocked = function () { + isUnlocked = false + this._didUpdate() +} + +accountManager.submitPassword = function (password, cb) { + if (password === 'test') { + isUnlocked = true + cb(null, getState()) + this._didUpdate() + } else { + cb(new Error('Bad password -- try "test"')) + } +} + +accountManager.setSelectedAccount = function (address, cb) { + selectedAccount = address + cb(null, getState()) + this._didUpdate() +} + +accountManager.signTransaction = function (txParams, cb) { + alert('signing tx....') +} + +accountManager._didUpdate = function () { + this.emit('update', getState()) +} + +// start app + +var container = document.getElementById('app-content') + +var css = MetaMaskUiCss() +injectCss(css) + +MetaMaskUi({ + container: container, + accountManager: accountManager, +}) + +// util + +function createRandomId () { + // 13 time digits + var datePart = new Date().getTime() * Math.pow(10, 3) + // 3 random digits + var extraPart = Math.floor(Math.random() * Math.pow(10, 3)) + // 16 digits + return datePart + extraPart +} diff --git a/ui/classic/index.html b/ui/classic/index.html new file mode 100644 index 000000000..9dfaefbb3 --- /dev/null +++ b/ui/classic/index.html @@ -0,0 +1,20 @@ + + + + + MetaMask + + + + +

+ + + + +
+ +
+ + + diff --git a/ui/classic/index.js b/ui/classic/index.js new file mode 100644 index 000000000..a729138d3 --- /dev/null +++ b/ui/classic/index.js @@ -0,0 +1,58 @@ +const render = require('react-dom').render +const h = require('react-hyperscript') +const Root = require('./app/root') +const actions = require('./app/actions') +const configureStore = require('./app/store') +const txHelper = require('./lib/tx-helper') +global.log = require('loglevel') + +module.exports = launchMetamaskUi + + +log.setLevel(global.METAMASK_DEBUG ? 'debug' : 'warn') + +function launchMetamaskUi (opts, cb) { + var accountManager = opts.accountManager + actions._setBackgroundConnection(accountManager) + // check if we are unlocked first + accountManager.getState(function (err, metamaskState) { + if (err) return cb(err) + const store = startApp(metamaskState, accountManager, opts) + cb(null, store) + }) +} + +function startApp (metamaskState, accountManager, opts) { + // parse opts + const store = configureStore({ + + // metamaskState represents the cross-tab state + metamask: metamaskState, + + // appState represents the current tab's popup state + appState: {}, + + // Which blockchain we are using: + networkVersion: opts.networkVersion, + }) + + // if unconfirmed txs, start on txConf page + const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.network) + if (unapprovedTxsAll.length > 0) { + store.dispatch(actions.showConfTxPage()) + } + + accountManager.on('update', function (metamaskState) { + store.dispatch(actions.updateMetamaskState(metamaskState)) + }) + + // start app + render( + h(Root, { + // inject initial state + store: store, + } + ), opts.container) + + return store +} diff --git a/ui/classic/lib/account-link.js b/ui/classic/lib/account-link.js new file mode 100644 index 000000000..d061d0ad1 --- /dev/null +++ b/ui/classic/lib/account-link.js @@ -0,0 +1,26 @@ +module.exports = function (address, network) { + const net = parseInt(network) + let link + switch (net) { + case 1: // main net + link = `http://etherscan.io/address/${address}` + break + case 2: // morden test net + link = `http://morden.etherscan.io/address/${address}` + break + case 3: // ropsten test net + link = `http://ropsten.etherscan.io/address/${address}` + break + case 4: // rinkeby test net + link = `http://rinkeby.etherscan.io/address/${address}` + break + case 42: // kovan test net + link = `http://kovan.etherscan.io/address/${address}` + break + default: + link = '' + break + } + + return link +} diff --git a/ui/classic/lib/contract-namer.js b/ui/classic/lib/contract-namer.js new file mode 100644 index 000000000..f05e770cc --- /dev/null +++ b/ui/classic/lib/contract-namer.js @@ -0,0 +1,33 @@ +/* CONTRACT NAMER + * + * Takes an address, + * Returns a nicname if we have one stored, + * otherwise returns null. + */ + +const contractMap = require('eth-contract-metadata') +const ethUtil = require('ethereumjs-util') + +module.exports = function (addr, identities = {}) { + const checksummed = ethUtil.toChecksumAddress(addr) + if (contractMap[checksummed] && contractMap[checksummed].name) { + return contractMap[checksummed].name + } + + const address = addr.toLowerCase() + const ids = hashFromIdentities(identities) + return addrFromHash(address, ids) +} + +function hashFromIdentities (identities) { + const result = {} + for (const key in identities) { + result[key] = identities[key].name + } + return result +} + +function addrFromHash (addr, hash) { + const address = addr.toLowerCase() + return hash[address] || null +} diff --git a/ui/classic/lib/etherscan-prefix-for-network.js b/ui/classic/lib/etherscan-prefix-for-network.js new file mode 100644 index 000000000..2c1904f1c --- /dev/null +++ b/ui/classic/lib/etherscan-prefix-for-network.js @@ -0,0 +1,21 @@ +module.exports = function (network) { + const net = parseInt(network) + let prefix + switch (net) { + case 1: // main net + prefix = '' + break + case 3: // ropsten test net + prefix = 'ropsten.' + break + case 4: // rinkeby test net + prefix = 'rinkeby.' + break + case 42: // kovan test net + prefix = 'kovan.' + break + default: + prefix = '' + } + return prefix +} diff --git a/ui/classic/lib/explorer-link.js b/ui/classic/lib/explorer-link.js new file mode 100644 index 000000000..3b82ecd5f --- /dev/null +++ b/ui/classic/lib/explorer-link.js @@ -0,0 +1,6 @@ +const prefixForNetwork = require('./etherscan-prefix-for-network') + +module.exports = function (hash, network) { + const prefix = prefixForNetwork(network) + return `http://${prefix}etherscan.io/tx/${hash}` +} diff --git a/ui/classic/lib/icon-factory.js b/ui/classic/lib/icon-factory.js new file mode 100644 index 000000000..27a74de66 --- /dev/null +++ b/ui/classic/lib/icon-factory.js @@ -0,0 +1,65 @@ +var iconFactory +const isValidAddress = require('ethereumjs-util').isValidAddress +const toChecksumAddress = require('ethereumjs-util').toChecksumAddress +const contractMap = require('eth-contract-metadata') + +module.exports = function (jazzicon) { + if (!iconFactory) { + iconFactory = new IconFactory(jazzicon) + } + return iconFactory +} + +function IconFactory (jazzicon) { + this.jazzicon = jazzicon + this.cache = {} +} + +IconFactory.prototype.iconForAddress = function (address, diameter) { + const addr = toChecksumAddress(address) + if (iconExistsFor(addr)) { + return imageElFor(addr) + } + + return this.generateIdenticonSvg(address, diameter) +} + +// returns svg dom element +IconFactory.prototype.generateIdenticonSvg = function (address, diameter) { + var cacheId = `${address}:${diameter}` + // check cache, lazily generate and populate cache + var identicon = this.cache[cacheId] || (this.cache[cacheId] = this.generateNewIdenticon(address, diameter)) + // create a clean copy so you can modify it + var cleanCopy = identicon.cloneNode(true) + return cleanCopy +} + +// creates a new identicon +IconFactory.prototype.generateNewIdenticon = function (address, diameter) { + var numericRepresentation = jsNumberForAddress(address) + var identicon = this.jazzicon(diameter, numericRepresentation) + return identicon +} + +// util + +function iconExistsFor (address) { + return contractMap[address] && isValidAddress(address) && contractMap[address].logo +} + +function imageElFor (address) { + const contract = contractMap[address] + const fileName = contract.logo + const path = `images/contract/${fileName}` + const img = document.createElement('img') + img.src = path + img.style.width = '75%' + return img +} + +function jsNumberForAddress (address) { + var addr = address.slice(2, 10) + var seed = parseInt(addr, 16) + return seed +} + diff --git a/ui/classic/lib/lost-accounts-notice.js b/ui/classic/lib/lost-accounts-notice.js new file mode 100644 index 000000000..948b13db6 --- /dev/null +++ b/ui/classic/lib/lost-accounts-notice.js @@ -0,0 +1,23 @@ +const summary = require('../app/util').addressSummary + +module.exports = function (lostAccounts) { + return { + date: new Date().toDateString(), + title: 'Account Problem Caught', + body: `MetaMask has fixed a bug where some accounts were previously mis-generated. This was a rare issue, but you were affected! + +We have successfully imported the accounts that were mis-generated, but they will no longer be recovered with your normal seed phrase. + +We have marked the affected accounts as "Loose", and recommend you transfer ether and tokens away from those accounts, or export & back them up elsewhere. + +Your affected accounts are: +${lostAccounts.map(acct => ` - ${summary(acct)}`).join('\n')} + +These accounts have been marked as "Loose" so they will be easy to recognize in the account list. + +For more information, please read [our blog post.][1] + +[1]: https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd#.7d8ktj4h3 + `, + } +} diff --git a/ui/classic/lib/persistent-form.js b/ui/classic/lib/persistent-form.js new file mode 100644 index 000000000..d4dc20b03 --- /dev/null +++ b/ui/classic/lib/persistent-form.js @@ -0,0 +1,61 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const defaultKey = 'persistent-form-default' +const eventName = 'keyup' + +module.exports = PersistentForm + +function PersistentForm () { + Component.call(this) +} + +inherits(PersistentForm, Component) + +PersistentForm.prototype.componentDidMount = function () { + const fields = document.querySelectorAll('[data-persistent-formid]') + const store = this.getPersistentStore() + + for (var i = 0; i < fields.length; i++) { + const field = fields[i] + const key = field.getAttribute('data-persistent-formid') + const cached = store[key] + if (cached !== undefined) { + field.value = cached + } + + field.addEventListener(eventName, this.persistentFieldDidUpdate.bind(this)) + } +} + +PersistentForm.prototype.getPersistentStore = function () { + let store = window.localStorage[this.persistentFormParentId || defaultKey] + if (store && store !== 'null') { + store = JSON.parse(store) + } else { + store = {} + } + return store +} + +PersistentForm.prototype.setPersistentStore = function (newStore) { + window.localStorage[this.persistentFormParentId || defaultKey] = JSON.stringify(newStore) +} + +PersistentForm.prototype.persistentFieldDidUpdate = function (event) { + const field = event.target + const store = this.getPersistentStore() + const key = field.getAttribute('data-persistent-formid') + const val = field.value + store[key] = val + this.setPersistentStore(store) +} + +PersistentForm.prototype.componentWillUnmount = function () { + const fields = document.querySelectorAll('[data-persistent-formid]') + for (var i = 0; i < fields.length; i++) { + const field = fields[i] + field.removeEventListener(eventName, this.persistentFieldDidUpdate.bind(this)) + } + this.setPersistentStore({}) +} + diff --git a/ui/classic/lib/tx-helper.js b/ui/classic/lib/tx-helper.js new file mode 100644 index 000000000..ec19daf64 --- /dev/null +++ b/ui/classic/lib/tx-helper.js @@ -0,0 +1,17 @@ +const valuesFor = require('../app/util').valuesFor + +module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, network) { + log.debug('tx-helper called with params:') + log.debug({ unapprovedTxs, unapprovedMsgs, personalMsgs, network }) + + const txValues = network ? valuesFor(unapprovedTxs).filter(txMeta => txMeta.metamaskNetworkId === network) : valuesFor(unapprovedTxs) + log.debug(`tx helper found ${txValues.length} unapproved txs`) + const msgValues = valuesFor(unapprovedMsgs) + log.debug(`tx helper found ${msgValues.length} unsigned messages`) + let allValues = txValues.concat(msgValues) + const personalValues = valuesFor(personalMsgs) + log.debug(`tx helper found ${personalValues.length} unsigned personal messages`) + allValues = allValues.concat(personalValues) + + return allValues.sort(txMeta => txMeta.time) +} diff --git a/ui/css.js b/ui/css.js deleted file mode 100644 index 043363cd7..000000000 --- a/ui/css.js +++ /dev/null @@ -1,29 +0,0 @@ -const fs = require('fs') -const path = require('path') - -module.exports = bundleCss - -var cssFiles = { - 'fonts.css': fs.readFileSync(path.join(__dirname, '/app/css/fonts.css'), 'utf8'), - 'reset.css': fs.readFileSync(path.join(__dirname, '/app/css/reset.css'), 'utf8'), - 'lib.css': fs.readFileSync(path.join(__dirname, '/app/css/lib.css'), 'utf8'), - 'index.css': fs.readFileSync(path.join(__dirname, '/app/css/index.css'), 'utf8'), - 'transitions.css': fs.readFileSync(path.join(__dirname, '/app/css/transitions.css'), 'utf8'), - 'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'), - 'react-css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'), -} - -function bundleCss () { - var cssBundle = Object.keys(cssFiles).reduce(function (bundle, fileName) { - var fileContent = cssFiles[fileName] - var output = String() - - output += '/*========== ' + fileName + ' ==========*/\n\n' - output += fileContent - output += '\n\n' - - return bundle + output - }, String()) - - return cssBundle -} diff --git a/ui/design/00-metamask-SignIn.jpg b/ui/design/00-metamask-SignIn.jpg deleted file mode 100644 index 2becdb032..000000000 Binary files a/ui/design/00-metamask-SignIn.jpg and /dev/null differ diff --git a/ui/design/01-metamask-SelectAcc.jpg b/ui/design/01-metamask-SelectAcc.jpg deleted file mode 100644 index 239091a98..000000000 Binary files a/ui/design/01-metamask-SelectAcc.jpg and /dev/null differ diff --git a/ui/design/02-metamask-AccDetails.jpg b/ui/design/02-metamask-AccDetails.jpg deleted file mode 100644 index d7d408ffc..000000000 Binary files a/ui/design/02-metamask-AccDetails.jpg and /dev/null differ diff --git a/ui/design/02a-metamask-AccDetails-OverToken.jpg b/ui/design/02a-metamask-AccDetails-OverToken.jpg deleted file mode 100644 index f26ff31e8..000000000 Binary files a/ui/design/02a-metamask-AccDetails-OverToken.jpg and /dev/null differ diff --git a/ui/design/02a-metamask-AccDetails-OverTransaction.jpg b/ui/design/02a-metamask-AccDetails-OverTransaction.jpg deleted file mode 100644 index 8a06be6b9..000000000 Binary files a/ui/design/02a-metamask-AccDetails-OverTransaction.jpg and /dev/null differ diff --git a/ui/design/02a-metamask-AccDetails.jpg b/ui/design/02a-metamask-AccDetails.jpg deleted file mode 100644 index c37e0f539..000000000 Binary files a/ui/design/02a-metamask-AccDetails.jpg and /dev/null differ diff --git a/ui/design/02b-metamask-AccDetails-Send.jpg b/ui/design/02b-metamask-AccDetails-Send.jpg deleted file mode 100644 index 10f2d27fd..000000000 Binary files a/ui/design/02b-metamask-AccDetails-Send.jpg and /dev/null differ diff --git a/ui/design/03-metamask-Qr.jpg b/ui/design/03-metamask-Qr.jpg deleted file mode 100644 index 9c09de42f..000000000 Binary files a/ui/design/03-metamask-Qr.jpg and /dev/null differ diff --git a/ui/design/05-metamask-Menu.jpg b/ui/design/05-metamask-Menu.jpg deleted file mode 100644 index 0a43d7b2a..000000000 Binary files a/ui/design/05-metamask-Menu.jpg and /dev/null differ diff --git a/ui/design/chromeStorePics/final_screen_dao_accounts.png b/ui/design/chromeStorePics/final_screen_dao_accounts.png deleted file mode 100644 index 805cc96b6..000000000 Binary files a/ui/design/chromeStorePics/final_screen_dao_accounts.png and /dev/null differ diff --git a/ui/design/chromeStorePics/final_screen_dao_locked.png b/ui/design/chromeStorePics/final_screen_dao_locked.png deleted file mode 100644 index 9d9e33930..000000000 Binary files a/ui/design/chromeStorePics/final_screen_dao_locked.png and /dev/null differ diff --git a/ui/design/chromeStorePics/final_screen_dao_notification.png b/ui/design/chromeStorePics/final_screen_dao_notification.png deleted file mode 100644 index d56a5ce62..000000000 Binary files a/ui/design/chromeStorePics/final_screen_dao_notification.png and /dev/null differ diff --git a/ui/design/chromeStorePics/final_screen_wei_account.png b/ui/design/chromeStorePics/final_screen_wei_account.png deleted file mode 100644 index d503ff301..000000000 Binary files a/ui/design/chromeStorePics/final_screen_wei_account.png and /dev/null differ diff --git a/ui/design/chromeStorePics/final_screen_wei_notification.png b/ui/design/chromeStorePics/final_screen_wei_notification.png deleted file mode 100644 index 3560c51ff..000000000 Binary files a/ui/design/chromeStorePics/final_screen_wei_notification.png and /dev/null differ diff --git a/ui/design/chromeStorePics/icon-128.png b/ui/design/chromeStorePics/icon-128.png deleted file mode 100644 index ae687147d..000000000 Binary files a/ui/design/chromeStorePics/icon-128.png and /dev/null differ diff --git a/ui/design/chromeStorePics/icon-64.png b/ui/design/chromeStorePics/icon-64.png deleted file mode 100644 index 7062cf4f1..000000000 Binary files a/ui/design/chromeStorePics/icon-64.png and /dev/null differ diff --git a/ui/design/chromeStorePics/metamask_icon.ai b/ui/design/chromeStorePics/metamask_icon.ai deleted file mode 100644 index 27400c5a4..000000000 --- a/ui/design/chromeStorePics/metamask_icon.ai +++ /dev/null @@ -1,2383 +0,0 @@ -%PDF-1.5 % -1 0 obj <>/OCGs[5 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream - - - - - application/pdf - - - metamask_icon - - - Adobe Illustrator CC 2015 (Macintosh) - 2016-06-15T14:23:12-04:00 - 2016-06-15T14:23:12-04:00 - 2016-06-15T14:23:12-04:00 - - - - 240 - 256 - JPEG - /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAADwAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXnP5r/mvB5Tg/RmnAT6/cJyUMKx28bVAkf+ZjT4U+k7UDYuo1HBsObl6bTce5+l5X+Wf5t6jonm KZtfu5rzTNVcG+lkZpHilACLOAamgUBWC/sgUrxAzEwakxl6uRczUaYSj6eYfS9vcQXEEdxA6ywT KskUimqsjCqsCOoIObUG3UkUvxQ7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXln5rfnHb+X1l0bQnWfXCCs0+zR2tfEGoaTwXoO/hmJqNTw7Dm5mm0plvL6fvfO U889xPJcXEjTTzM0ksshLO7saszMdySTUk5qybdsBSzAl7R+Rv5ni0dPKutTqto5ppNw/KqyOwH1 c0BHFuVVJpTpvUUz9Jnr0n4Ov1mnv1D4ve82LrHYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FUn1Pzl5W0yF5r3VLeNI9no4kYfNU5N+GY89XijsZC/mfkHIhpMstxE18h8ynCmqg7iorQ7HMhx 3Yq7FXYq7FXYq7FXjf5v/nG2nPL5e8tXAN9Ro9Rv039A7D04WBp6nUM1Ph7fFXjg6nU16Y83P0ul v1S5PAWZmYsxJYmpJ3JJzWu0axV2KuxV9Ifkr+Zq67py6FrF1y121+G3eUjlcwKtQeRPxyIAeXci jbnkc2mlz8Qo83U6vT8J4h9L1PMxwnYq7FXYq7FXYq7FXYq7FXYq7FXYqlN95s8t2I/0jUYQQaFE b1HHzWPk34Zi5Nbhh9Uh9/3OVi0Waf0xP3fex7UfzX0WEOtjBNdSCoR2AjjPgak8/wDhcwcvbWIf SDL7B+v7HOxdi5T9REftP6vtYre/mf5ouD+5eK0UdoowxPzMnqfhmsydsZpcqj7h+u3aY+x8Eedy 95/VTFdc803n1dptVv5powSVjeRmqx7IhNMxPEy5jRJPx2cwY8WEWAB8N0l8gRXnm/z/AKXazpXT 7WX65NCo5II4PjHqVrXk3FDX+btXNtodLETDqddqiYH7H1LnQPOuxV2KuxV2KuZlVSzEBQKknYAD FXhX5t/nOsyzaB5XuCEDBbzVoXZSSrA8Ld0I2qKM/foNt81+o1X8Mfm7LTaT+KXyeI5r3YuxV2Ku xV2KqtpdXNpdQ3VrI0NzA6yQyoaMroaqwPiCMINboIsUX1j+XH5g6f5w0WOcNHDq0I439irbqwoD IiklvTaux7dK1GbnBmEx5ukz4DjPky3Lmh2KuxV2KuxV2KuxVKb/AM2eW7CoudQhDA0KI3qMD7rH yYZjZdbhh9Uh9/3OVi0Waf0xP3fex6//ADY0OHktnbzXTKaKxpFGw8QTyb/hcwMnbWIfSDL7B+Pg 5+PsXKa4iI/afx8WO3/5ra9OJEtYIbVG2RqNJIo/1iQv/C5gZe2sp+kCP2n8fBz8XYuIVxEy+wfj 4sVvdY1a+FLy8muFBLBZJGZQT4KTQZrMmfJP6pE/F2ePBjh9MQPghMqbXYqler+YLPTgUJ9W57Qq en+se2X4dPKfuaMueMPewW+vrm9uGnuH5Ox2G/FR/KoPQZtYQERQdZOZkbL3L/nG7y8Y7PVPMEqj lOy2VqxBDBEpJKQSPsszINu6nNpoYbGTqdfPcRe1ZnuvdirsVdirsVeF/n1+Y96l1N5O04+lCERt Vn35uXAkWBfBOJVmI+1XjsAeWv1ec3wD4uy0eAVxn4PEM17sXYq7FXYq7FXYq7FU18seZNU8uaxD qumymOeKqsBQh0YUZCGDDceI2O+ESlH6TRYmEZbSFh7bbfmL5mubeO4iv6xSqHQ+lD0YV/kzVy7U 1MTRl9kf1Owj2XpiLEftP61T/Hvmv/lt/wCSUP8AzRkf5W1P877I/qZfyTp/5v2n9bv8e+a/+W3/ AJJQ/wDNGP8AK2p/nfZH9S/yTp/5v2n9bv8AHvmv/lt/5JQ/80Y/ytqf532R/Uv8k6f+b9p/W7/H vmv/AJbf+SUP/NGP8ran+d9kf1L/ACTp/wCb9p/W7/Hvmv8A5bf+SUP/ADRj/K2p/nfZH9S/yTp/ 5v2n9bHtW1/WtUeuoXTz8eiGioCNtkUKv4ZDNqsmX6zbdh0uPF9ApL8ob3Yq7FXYq7FWO695pW0d rWyo9wtRJKd1Q+A8WH3ZmYNLxby5OJn1PDtHmw6SR5JGkclnclmY9STuTmzArZ1xN7uhhlmmSGJS 8sjBI0HUsxoAPpwhiS+zvLGhxaF5e0/SIiGFlAkTOoIDuB8b0JNOb1bN5jhwxAdBknxSJ70zybB2 KuxV2KpX5o12HQfL2oaxNxK2UDyKjHiHkpSOOu9ObkL9OQyT4Yks8cOKQHe+Nr6+u7+8mvLyVp7q 4cyTTOaszNuSc0ZJJsu/AAFBRwJdirsVdirsVdirsVdirKvI2tmC6OmzN+5uDWEmlFkp03/mp9/z zX67BY4hzDm6PNR4T1Z5mpdo7FXYq7FXYqg7laSn33y2PJgVLJIdirsVWu6IjO7BUUEsxNAAOpJO IFqTTD9c81yT1gsGaKIH4px8LtT+Xuo/HNlg0gG8ubrs2qJ2ixzM1w3Yqzz8k/L66z5/s2kAMGmK 2oSAkgkwkCKlO4ldDTwBzJ0sOKfucbVz4YHz2fU+bd0rsVdirsVdirxT/nIzzWi2ll5ZtZ1Mkj/W dRjQnkqoB6KPTajli9D/ACqcwNbk2EQ7DQ49zIvB81zs3Yq7FXYq7FXYq7FXYq7FW0d0dXRirqQV YGhBG4IIxItQXp3ljWjqunc5Cv1qI8J1Xb/Van+UPxrmh1WDw5bcnc6fNxx35pvmO5DsVdirsVQ1 2v2W+gnJwYlD5YxdiqheXttZwGe4cRxjap6k+AHc5KEDI0GM5iIssE1fzBeakeB/dWw6Qqevux7n Nth08Ye91eXPKfuSzL2h2KuxV9If84/eVk03ys+tyEm51lqhSKcIYHdEArv8Zq3uKZtdHjqN97qN bkuVdz1PMtw3Yq7FXYqp3V1b2ltNdXMgit4EaWaVjRVRByZifAAYCaSBez418169Nr/mPUdYl5Vv Z2kjVyCyRVpEhIp9iMKv0Zo8k+KRLv8AHDhiB3JVkGbsVdirsVdirsVdirsVdirsVTPy7q50vU45 2J9B/guFHdD36H7J3/DKNTh8SFdejdgy8Er6PUYpY5okljblHIoZGHQqwqDmhIINF3QNiwuwJdir sVUrlaxH23yUeaCg8tYJfq2t2Wmx/vW5TleUcC/abtv/ACj3OXYsEp8uTVlzRhz5sD1DULm+uGnu GLE/ZX9lR/KozbY8YgKDqsmQyNlDZNg7FXYqiNN0+51HUbXT7UBrm8mjggUmgLysEWp7bnDEWaRK VCy+0tM0+307TbXT7YEW9nDHbwgmp4RKEWp+QzfRFCnnpSs2UThQ7FXYq7FXmX59ebIdL8ovpEMw Goauyx+mrFXW2U8pH2/Zbj6dD15HwOYmryVGupczR4uKd9A+ac1Tt3Yq7FXYq7FXYq7FXYq7FXYq 7FXYqzXyJrSem2lztRgS9sSQAQT8SD3ruPpzV6/Bvxj4ux0Wb+EsxzWuwdirsVadeSlfEEYhDE9b 8zwWLNb24E10pKuK/AhHjTqa9s2ODSme52Dh5tSI7DcsJmmlmkaWVy8jmrOxqTm0AAFB1hJJsrMK HYq7FXYq9S/5x+8qvqXmp9bkIFtoq1CEV5zTo6IBXb4RVq9jTMzR47lfc4WtyVHh730jm0dS7FXY q7FXYq+X/wA+dQmuvzGu4JPsWMFvBF/qtGJ/+JTHNTq5Xk9zudHGsY83nmYrlOxV2KuxV2KuxV2K uxV2KuxV2KtqrMwVQSxNABuSTirN/Lfl9LBVurhQ1626g7iMeA/yvE/R89VqdRx7D6XZ6fT8O55s tUggEdDuM1zmt4pdirsVYZ5s8s+pLJfWS0lNXmhH7fcsv+V4jv8APrs9JqaHDJ1+p01+qLDM2brn Yq7FXYq7FX0n/wA48to48kyR2cwfUPrLyanEdmjZvhiA2rwMaAj35ZtdHXBtzdRrr49+XR6hmW4b sVdirsVdir5I/Ne/+vfmJrs1QeFx6G3/AC7osP8AzLzTag3Mu800axhieUN7sVdirsVdirsVdirs VdirsVdirMPLHl8wAXt4hE5/uYmFCg/mI8f1ZrdVqL9MeTsdNgr1HmyXMJzEXatWOndf1ZVMbswr ZFLsVdiqGu13VvoOTgWJYV5o8vFS9/aL8O7XEQ7eLj28c2ml1H8MnXanT/xBi+Z7guxV2KuxVP8A yLf+abLzPZP5ZDyarI4SO3XdZVO7JKKgenQVYkjiPiqKVFuKUhIcPNqzRiYni5PsKIymJDKFWXiP UCElQ1N6EgEivtm7dCuxV2KuxV4P+Zn5i/m7o189tNbRaNYszi2urVBOsqEkD/SJAw5bV2VG8QNs 12fNlie4Oy0+DFId5eL3FxPczyXFxI01xMzSTSuSzu7GrMzHckk1JzBJt2AFLMCXYq7FXYq7FXYq 7FXYq7FXYqyvyv5fZWF9ex0IobaNvv5kfq/2s1+q1H8Mfi5+mwfxS+DKswHOdiqrbuVkA7Nsf4ZG Q2SEZlTN2KuxVTnTlEfbcfRhid0FBZcwYd5k8uNAz3tkg+rdZYl6oe7KKfZ/V8umy02pv0y5uu1G nr1R5MbzNcN2KuxVnH5Z/mdP5MuXjayhudPu5Fa9cJS6CAEUjkqoIFa8W2/1ak5kYM/B02cbUafx Ou76X8s+ZdL8x6RDqumGU2s32TLG8R5DZl+IUbi1VJQlajrm1hMSFh1GTGYGimmTYOxV2KqN9HZS Wk0d8sT2boVuEnCmIodiHDfDT54DVbpF3s+b/wAyfI/kCy9fU/LvmWzJZix0f1BOQSWJWF4OZXsq q6/N81efFAbxkPc7bBmmdpRPveZZiOY7FXYq7FXYq7FXYq7FXYqybyz5cMhjv7xaRD4oYSPteDN/ k+Hj8uuDqdTXpi5um09+osvzXOwdirsVdiqYIwZA3iMoIZt4pdirsVS9l4sV8DTLg1tEAih6YVYT 5k8vGzY3dsC1qxJdf99knpt+z4ZtNNqOLY83W6jT8O45JBmW4jsVZP8Alp5c07zH5z0/SNQd1tZz I7rH1f0o2l4V/ZDcNzl2CAlMAtOomYQJD65gggt4I7e3jWGCFRHFFGAqIiiiqqjYADYAZugKdGTa /FDsVdirHfNvkDyv5rjUava87iNGSC7iYxzRhvBhs1DuA4I9sqyYYz5tuLNKHJ4v5q/5x58xWBlu NAuE1S1G6270iuQCTtv+7fitN+QJ7LmDk0Uh9O7sMeuifq2eUzQzQTPDMjRTRMUkjcFWVlNCrA7g g5hkU5oNrMCXYq7FXYq7FXYq7FWR+XfLJuAl5eDjBUGKEjdx4nwX9fy64Wo1NemPNzNPpr9UuTMs 1rsXYq7FXYq7FUVavVSh6jcfLK5hkFfIMnYq7FUHdLSWviK/wyyHJgVLJoWuiOjI4DIwKsp3BB2I OINKRbB/MPl19Pb6xb1ezY713MZPY+3gfo+e10+o49j9Tq9Rp+DcckkzKcZmP5P3EsH5k6G8cTTM ZZIyqgkhZIXRm27IrFj7DL9Mf3gcfVC8ZfWWbl0jsVdirsVdirwr87POX5jaTqj6dFIdP0O4VTaX dorK8o6lXuDusgZTVUI+HrUHfX6rLOJrkHZaTFjkL5l4jmvdi7FXYq7FXYq7FXYqn/lzy6t8purr kLZTREG3Mg77/wAvbbMTU6jg2HNy9Pp+Lc8maqqooVQFVRRVGwAHYZqyXZAN4q7FXYq7FXYqvhfj Ip7dD9ORkNkhHZUzdirsVULpKoG/l/jkoFiULlrF2KrJYYpozHKiyRt9pGAIP0HCCQbCCAdiwTzD obabcB4qm0lJ9M7nif5Sf1ZttPn4xvzdXqMPAduT6E/Jz8tofLejx6pqMStrt+iyNzSj20bLtCOY DK9G/edN/h7VO+02DhFnmXn9Vn4zQ+kPSMynEdirsVdirsVS7zBoGl6/pM+l6nCJrScUI6MrD7Lo ezKehyM4CQos4TMTYfKfn3yJq3lDWHs7pWkspCTYX1KJMgp4Vo61oy9vlQ5p82EwNF3WHMMgsMYq MpbnVGKuxVvFXYqnnlvQDfSfWbhSLRDsP9+MOw9h3zF1Oo4BQ5uVp8HEbPJm6IiIqIoVFACqBQAD YADNUTbswKXYq7FXYq7FXYq7FXYqjoX5xg9+h+eUyFFmF+BLsVWyLyRl8RtiChAZewdirsVTjyfZ JeeZ9NidOarOkpUio/dH1Afo45mdnx4s8R5/du4faE+HBI+X37Pds7N4t2KuxV2KuxV2KuxV5Z+Y esC91gWcZBhsKpUb1kahf7qcfozke2dTx5eEcoff1/U9b2PpuDFxHnP7un62K5p3buxV2KuxV1Bi qFukowcdDsfnlkCxKhk2LsVdirsVdirsVdirsVRFo/xFfHcZCYZBE5WydirsVQEq8ZGHv+vLgdmB W4UOxVmH5WQCTzOzn/dFvI4+kqn/ABvm27GiDm90T+h1PbUiMI85D9L17OpeVdirsVdirsVdiqG1 K/i0+wuL2X7ECFyK0qR0Ue7HbKs+UY4GZ6Btw4jkmIjqXh000k0zzSsWkkYu7HqWY1Jzz+UjIknm XvYxEQAOQWYGTsVdirsVdiq2ROaFfHp88INIKAIIJB6jrlrB2FXYq7FXYq7FXYq7FV0bcXDeBwEJ CPylm7FXYqhbtTyDdiKfTlkCxKhk2LsVZ7+UduW1S+uO0cCx/TI4P/MvN32HC5yl3Cvn/Y6PtydQ jHvN/L+16jnSPNuxV2KuxV2KuxVhP5l60IrOPSY6+rccZZj29NSeI+l1r9GaHtzUgQGMc5bn3f2/ c73sTTEzOQ8o7D3/ANn3vOM5d6d2KuxV2KuxV2KuxVCXiFT6iqWB+1Sn8csgejEhBfW4/Bvw/rlv Chr64n8px4Va+uD+T8ceBWvrv+R+P9mHgVv67/kfj/ZjwK19cP8AL+OPArX1yT+UY8KtfXJfBfx/ rjwhU2s5Ge3Ut9rv/DMeYosgr5FLsVUL3l9WZlFWX4hX26/hkoc0FKDdSnwHyGZPCGKhZ6oLy3We CTlGxIBoBupKnt4jLMuA45cMhu1Yc0ckeKPJ6F+UmpSxapdQMapPGrGvjG1BT/kYc2vYs6nKPeL+ X9rqO3IeiMu418/7HsA3GdE807FXYq7FXYq7FXi/mfVP0nrl1dA1i5cIaGo9NPhUj/Wpy+nOF7Qz +LmlLpyHuH4t7jQYPCwxj15n3n8UlWYbmOxV2KuxV2KuxV2KuIB2PTFUDdWaV5caqe/cZbCbAhAy WjCpQ8h4d8tElUCCDQ9ckrWKpZrHmHTtKUeuxeY9II6F6eJBIoMztJoMmf6dh3nk4Os7Rxaceo2e 4c2Far5x1a+5JE31W3bb04z8RHu/X7qZ0ul7IxYtz6pef6v7XltX2zmy7A8EfL9f9id+R9d9WP8A Rc5q8YLW7kjdR1Tfeo6j2+WaztrRcJ8WPI8/1/jr73adh6/iHgy5jl7u78dPcy5RyYL4mmc+9GnN o1HK9iP1ZjzCQisrZOxVp1DoynowIPyOIKscl/dc+ewSvL2p1zNiL5NZNCy888na79QvDa3DgWlw d2Y0CPTZvDfofo8M67tfQ+LDiiPXH7Q8b2Nr/CnwSPol9h7/ANb2PytcvZ6rYSqwX96odu3FzRq/ Qc5fRZTDPEjvr57PT6/GJ4ZA91/J79A3KJT7Z2bxS/FXYq7FXYqk/m7VRpug3UyvwnkX0oN6Hm+1 V91FWzC7Rz+FhkevIe8/i3N7PweLmiOnM/D8U8azhnt3Yq7FXYq7FXYq7FXYq7FXEAih6Yqg54Ch 5L9j9WWRlbAhQeNHFGFcmChJ9b0DXL6ALpN8lt2kVwysfcSLyI+hfpzP0WqwY5XliZfju/b8HB12 DPkjWKQj+O/9nxYTe/l55tilc+gt11Zpo5VPInc7OUcn6M6XD23pSBvw+RH6rDzGXsXUgnbi8wf1 0Uiu9K1SzAa7s5rdTsGljZAfkWAzZYtTjyfTKMvcQ67Jp8kPqiY+8KNrczWtxHcQNwliYMje4yeX HGcTGXIscWWWOQlHmHrei39tqUMVzAQyMKuvdGAqVb3GcDqsEsMjGX9vm+g6bUxzQE4/2eSco3Fw 3gcwyHITAEEAjodxlLJ2KXYqx/X7J5UubeJgj3MThHaoVWcFakgHau+Z2kyiMoyPKJH2OPqMZnjl EcyCEB5f/LjSLBEm1AC+ux1Dbwqd+iftf7KvyGZ2t7dy5DUPRH7fn+p1ej7DxYxc/XL7Pl+tkDoI 3KqAoX7IGwA7UzUg3u7iq2fQWlzGfTbadl4mWJHK+HJQaZ30JcUQe8PAzjwyI7iiskxdirsVdirz L8y9S9fV4rFSeFmnxj/iySjH/heOcp25n4sogP4R9p/ZT1PYmDhxmZ/iP2D9tsPzSO7dirsVdirs VdirsVdirsVdiriARQ9MVQc8BT4hun6ssjK2BDdq1JCP5h+IxmNkhF5WydiqAu9A0O8Ltc2FvLJJ 9uQxrzP+zpy/HMnHrc0KEZyAHnt8nGyaPDO+KEST5b/NRsPLOlaYH/R0RgEn205u6kjvRy1D8snn 12TNXiG68h+hjp9Hjw2MYoHzP6VVlZTRhQ5SC5CJt5l4hCaEdK98hKKQVfIMnYqo3dstxEV2DjdG 8DkoSooKhp8jrW2mqJE3UH+XJZB1ChddLSWviMYcmJfQsESwwRxL9mNQo+QFM9BAfPyV+FDsVdiq jeXdvZ2st1cOEhhUs7HwGQyZIwiZS2AZ48cpyEY7kvD7+7kvL2e7kFHuJGkYDoORrQfLOAzZDOZk ept73FjEICI6ClDK2x2KuxV2KuxV2KuxV2KuxV2KuxVxAIoeh64qhZIjE4dd0B+7LAbY1SKBBAI6 HplbJ2KuxV2KrJI1kWh69jhBpBCElhaM77jscsErYkK8NwGor/a7HxyEopBV8iydiqhcW5kKyRkL Mn2GPT3ByUZVz5IbVDcyQLTizuI2U9mYgZZijcuEdWGSXDEnufQeegPn7sVdirsVYb+ZmqGDS4dP Q/Fdvyk6f3cRBp47tT7s0fbmfhxiA/iP2D9tO67EwcWQzP8ACPtP7LeaZyr1TsVdirsVdirsVdir sVdirsVdirsVWvJGgq7BR2qaYgEoQc2qW4BVVMn4A/x/DLRiKLVIbscAGQjbpWtPbtgMFtEJIj/Z NfbvlZFJtdil2KuxVogEUIqMUIWa3K1ZN18O4yyMkEKkFxyoj9ex8cEoqCr5Bk7FUTpEdv8Apmxe YqkQuYWmZjReIcVJJ2+z3zI0kgMsCeXEPvcfVRJxSA58J+57pnfPBuxV2KuxV5B531M3/mK4INYr b/R46eEZPL/hy2cV2rn8TOe6O3y/bb2fZeHw8A75b/P9lJDmudi7FXYq7FXYq7FXYq7FXYqoSXtq nWQE+A3/AFZIQJRaEk1c7iOP5Fj/AAH9csGHvRaFkv7t+shA8F2/VvlgxgLagSSanqckhVtY+UnI 9F3+nBIqjcrQ7FVRZ5V/aqPA75ExCbVUuwdnFPcZEwTauro32SDkCEt4pdiqhNbhqsmzeHY5KMmJ DoJzXhJs3YnDKPUKCr5Bk7FWd+RvOPp+npOoyfu9ltJ2/Z7CNj4fy+HTpnQ9k9pVWKZ/qn9H6vk8 92r2dd5YD3j9P6/m9CzpXnHYq7FXhnnLS30vzHeW4BWF3M1vQED05PiAX2U1X6M4vX4PDzSHTmPj +Ke00GfxMMT15H4fi0l5N4nMOnMdybxONK7kfHFXVPjirVT44q6p8cVdU+OKuqcKrZEEiFT9B8Di DSoB0ZGKt1GWgpW4q7FWwCTQdT0xVHxR+mgXv1Jysm0L8CuxV2KuxV2KqiXEq96jwORMQm1dbpD9 oFfxGQMCm1VWVhVSD8sjSVssSyDfY9jhBpSFkbsh4S9f2W7HCRfJVbIpdir0fyR5zW4WPStRci5A 421wxr6ngjH+bw8fn16jsvtTjrHk+roe/wAvf9/v58x2n2Zw3kx/T1Hd5+77vdym2b50TsVef/m1 pJktbTVY1FYCYJyAa8X3Qk+CtUf7LNH21guImOmx/H45u97Ez1IwPXcfj8cnmOc49G7FXYq7FXYq 7FXYq7FXYqpTw+otR9odMINKgiCDQ9csS1iqItI6vzPRenzyMiqLyCHYq7FXYq7FXYq7FXYq4Eg1 HXFVVLmRdj8Q9+uRMQm1UXETijinz3GR4SE2vRgopXkg6N1p88iUoTWNf0bRoBNqd3Hao1eAc1Zq UrxQVZqV3oMtw6fJlNQFtWbUQxC5mmMXn5w+TbYqYJbi8J7wRFeP/I4xfhmwx9i6g86j7z+q3Ayd sYByuXuH66ehfl//AM5Q+UtVuk0rzAH0mT4Y7XUp6GGToo9dgW9JjWpY/B1qV79Tp4zEAJkGQeX1 BgZkwFRe3wzRTRJNC6yRSANHIhDKyncEEbEHL2hC6xpcGq6ZcafOSIp1oWHUEEMp+hgDlWfCMkDA 8i24MxxTExzDwG5t5ra5ltpl4zQO0ci9aMh4kfeM4ecDEkHmHuYTEoiQ5FTyLJ2KuxV2KuxV2Kux V2KuxVDXMNayL/shkolKGAJIA6nYZNUwRAihR2yolC7FXYq7FXYq7FW1VmPFQWJ6AbnEC+Skgc0V DpGpzbpbPTxYcR/w1MyIaTLLlEuPPV4o85BEDy3rJNDBT3Lp/A5aOzs3837Q1HtHD/O+wq48pamR UvCPYs38Fy4dk5e+P4+DSe1sXdL7P1q8Xk+cj97cqp/yVLfrK5ZHsiXWX4+xql2vHpH8farR+Tog f3l0zDwVAv6y2Wx7IHWX2Ncu1z0j9quvlLTlIPqzVH+Uv/NOWfyTi75fZ+pq/lbL3R+39b5x/OyS VfzBvrLmWt7JII7ZDT4VeFJW6UrV5Cc2uk00MUKiHV6rUTyyuRYJmS4zsVfU/wDziJp/mFdA1bUJ 72QaC8/oWOnHiUNwqq00+68h8JRBxeh+LkKgHCgvoLFDx/8AM3SBZeYfrMakQ36erWlF9RfhkA8e zH55yna+Dgy8Q5S+/r+v4vV9kZ+PFwnnHb4dP1fBiOat2rsVdirsVdirsVdirsVdiqvaWF9euUtL eW4cdViRnI+fEHJ48Up/SCfcwyZYw+oge9PY/wArfMqQrdskahhX0ORaVK+KqD+GbQdkZjGzQdbL tnCDQstDybIrFJboJKv2k4EkfOrKckOxz1l9jWe2B0j9v7FaLyfbj++uHf8A1AF/XyyyPZEesj+P m1y7Xl0iPv8A1Ky+U9MBqXlYeBZf4KMsHZWLvl+Pg1HtXKekfx8VceW9G/5Z6+/N/wDmrLv5Ow/z ftP62r+Uc3877B+pXTSNLQUFrER/lKGP3tXLY6TEP4R8mqWryn+I/NWis7SI1igjjPiqgfqGWRww jyAHwapZpy5kn4q2WNbsVdirsVdirsVdir5U/O7/AMmdrP8A0bf9QsWZEOTRPmwbJMU28p+XL3zL 5l03QbIH6xqNwkAcKX9NWPxysq78Y0q7ewOKv0B8teXNJ8t6FZ6HpEPoafYpwgjJLHclmZierMzF ifE4WKZYqxn8wtFj1Hy7PMIw11YqZoHrSiggyj6UB28QM13amAZMJPWO/wCv7HY9l6g48wH8Mtv1 fa8XzkXr3Yq7FXYq7FXYqmOm+Xdc1Ir9SspZkbYS8eMe3/FjUT8cvxaXLk+mJP3fNx82qxY/qkB9 /wAubK9I/KjUpZEfVJ0t4OrRRHnL8q04D51ObTB2LMm8hoeXP9X3usz9tQArGLPny/X9zL9N/L3y tYgH6r9akH+7Lk+pX5rtH/wubTF2Zgh0s+e/7PsdVm7Tzz60PLb9v2sghhhhiWKFFjiQUSNAFUD2 A2zPjEAUOTgSkSbPNfhQo3NnaXScLmFJV3oHANK+HhjSQUovPKNhIpNo720gHwgMXSvuG5fgcrOM MxkKQ3fl7zBa1IjW6Qb8o9zT/V+E/cMgcZbBkCXNdCNzHNG8Ug2ZWFCCPHvkKZ2vW4gbo4+nb9eB VTFLsUOxV2KuxV2KuxVpnVBViAPE4q8U89flBq3mfzvf6yt/b2un3Xo8Kh3mAjhSNqpRF6pt8eWx nQa5Qsr7b/nH3yssai51C+llH2mjaKNT/sTHIR/wWPiFfDD178qfyf8AKnli6/Ttrp3p35jMVrcT SPI4jcDm4ViVUsNgwANKjocnC+rXOuj1DJsHYq0yqylWAZWFGU7gg9sVeBa/pbaXrN3YN0gkIQ+K H4kP0qQc4fVYfDySj3H+x7nS5vExxl3j+1L8ob21VmYKoJYmgA3JOICkp5p3kjzRfjlFYPHHt8c9 IhuKggPQn6Bmbi7OzT5Rr37OFl7RwQ5yv3bsp0z8o3qj6nfLQN8cNupNV9pG40/4DNli7E6zl8B+ v9jrM3bnSEfif1ftZlp3lLy5p9Da2EQdSGWRx6jgjuGfkR9GbbFosOP6Yj7/AL3U5dbmyfVI/d9y bZlOK7FXYq7FXYq7FXYq7FVKe1tbheM8KSgdA6huvz+WNKCkWoeSdNnFbVjav4CrqfoJr+OQOMNg yFILvylrlpVolE6AVLQtv16cTRvuyswLYMgSx5b23k9OZWRx1SRSp/GhyBDMFUXUF/aQj5Gv9MaV VW7ganxUJ7EYFVVZWFVII8RviriQBUmgHUnFUNNfINo/iPiemGlQTu7mrmpxVrFWReVfLr3cyXt0 g+poaorb+ow26fyg9a/LxyyEba5zrZneXNDsVdirsVYV538iXeuajBe2Uscb8PSnEpIFFJKsOIap 3pmo7Q7OlmmJRIG1G3cdndpRwwMZAnexShp35S6ZEQ1/eS3J2PCICJfcGvMn6KZDF2JAfVIy+xnl 7bmfpiI/b+plmk+X9H0lWXT7VYOf22BLMfYsxZqfTmzwabHi+gU6vPqcmX6zaYZe0OxV2KuxV2Ku xV2KuxV2KuxV2KuxV2KuxVRu7K1vITDcxiSM9j/AjcYCLSDSQ3vkbTpSWtZHtif2f7xfuJDf8NkD jDMZCkV55O1m3HJEW4UVJMR3FP8AJbifurkDAsxkCXjRtX/5Yrjb/ip/6YOEsuIK40PX5lANpKQO nIcev+tTHgK8YVB5S8wEf7y/8lI/+asPAUcYVU8ma4w3RE9mcfwrj4ZR4gRlr5EvfXT61NGIK/vP TLF6eAqoGSGNByBmccaRRrHGOKIAqqOwAoBlrSuxV2Kv/9k= - - - - proof:pdf - uuid:65E6390686CF11DBA6E2D887CEACB407 - xmp.did:d4d07395-aa96-47c2-a9e5-d0351947bb0c - uuid:c63c1031-e157-9748-9c58-86481308e954 - - uuid:1abccb90-0c26-4942-b156-fd2eb962e3e1 - xmp.did:58fdc1b8-1448-3a44-9e20-282d8ec1cf95 - uuid:65E6390686CF11DBA6E2D887CEACB407 - proof:pdf - - - - - saved - xmp.iid:d4d07395-aa96-47c2-a9e5-d0351947bb0c - 2016-06-15T14:23:10-04:00 - Adobe Illustrator CC 2015 (Macintosh) - / - - - - Web - Document - 1 - True - False - - 128.000000 - 128.000000 - Pixels - - - - Cyan - Magenta - Yellow - Black - - - - - - Default Swatch Group - 0 - - - - White - RGB - PROCESS - 255 - 255 - 255 - - - Black - RGB - PROCESS - 0 - 0 - 0 - - - RGB Red - RGB - PROCESS - 255 - 0 - 0 - - - RGB Yellow - RGB - PROCESS - 255 - 255 - 0 - - - RGB Green - RGB - PROCESS - 0 - 255 - 0 - - - RGB Cyan - RGB - PROCESS - 0 - 255 - 255 - - - RGB Blue - RGB - PROCESS - 0 - 0 - 255 - - - RGB Magenta - RGB - PROCESS - 255 - 0 - 255 - - - R=193 G=39 B=45 - RGB - PROCESS - 193 - 39 - 45 - - - R=237 G=28 B=36 - RGB - PROCESS - 237 - 28 - 36 - - - R=241 G=90 B=36 - RGB - PROCESS - 241 - 90 - 36 - - - R=247 G=147 B=30 - RGB - PROCESS - 247 - 147 - 30 - - - R=251 G=176 B=59 - RGB - PROCESS - 251 - 176 - 59 - - - R=252 G=238 B=33 - RGB - PROCESS - 252 - 238 - 33 - - - R=217 G=224 B=33 - RGB - PROCESS - 217 - 224 - 33 - - - R=140 G=198 B=63 - RGB - PROCESS - 140 - 198 - 63 - - - R=57 G=181 B=74 - RGB - PROCESS - 57 - 181 - 74 - - - R=0 G=146 B=69 - RGB - PROCESS - 0 - 146 - 69 - - - R=0 G=104 B=55 - RGB - PROCESS - 0 - 104 - 55 - - - R=34 G=181 B=115 - RGB - PROCESS - 34 - 181 - 115 - - - R=0 G=169 B=157 - RGB - PROCESS - 0 - 169 - 157 - - - R=41 G=171 B=226 - RGB - PROCESS - 41 - 171 - 226 - - - R=0 G=113 B=188 - RGB - PROCESS - 0 - 113 - 188 - - - R=46 G=49 B=146 - RGB - PROCESS - 46 - 49 - 146 - - - R=27 G=20 B=100 - RGB - PROCESS - 27 - 20 - 100 - - - R=102 G=45 B=145 - RGB - PROCESS - 102 - 45 - 145 - - - R=147 G=39 B=143 - RGB - PROCESS - 147 - 39 - 143 - - - R=158 G=0 B=93 - RGB - PROCESS - 158 - 0 - 93 - - - R=212 G=20 B=90 - RGB - PROCESS - 212 - 20 - 90 - - - R=237 G=30 B=121 - RGB - PROCESS - 237 - 30 - 121 - - - R=199 G=178 B=153 - RGB - PROCESS - 199 - 178 - 153 - - - R=153 G=134 B=117 - RGB - PROCESS - 153 - 134 - 117 - - - R=115 G=99 B=87 - RGB - PROCESS - 115 - 99 - 87 - - - R=83 G=71 B=65 - RGB - PROCESS - 83 - 71 - 65 - - - R=198 G=156 B=109 - RGB - PROCESS - 198 - 156 - 109 - - - R=166 G=124 B=82 - RGB - PROCESS - 166 - 124 - 82 - - - R=140 G=98 B=57 - RGB - PROCESS - 140 - 98 - 57 - - - R=117 G=76 B=36 - RGB - PROCESS - 117 - 76 - 36 - - - R=96 G=56 B=19 - RGB - PROCESS - 96 - 56 - 19 - - - R=66 G=33 B=11 - RGB - PROCESS - 66 - 33 - 11 - - - - - - Grays - 1 - - - - R=0 G=0 B=0 - RGB - PROCESS - 0 - 0 - 0 - - - R=26 G=26 B=26 - RGB - PROCESS - 26 - 26 - 26 - - - R=51 G=51 B=51 - RGB - PROCESS - 51 - 51 - 51 - - - R=77 G=77 B=77 - RGB - PROCESS - 77 - 77 - 77 - - - R=102 G=102 B=102 - RGB - PROCESS - 102 - 102 - 102 - - - R=128 G=128 B=128 - RGB - PROCESS - 128 - 128 - 128 - - - R=153 G=153 B=153 - RGB - PROCESS - 153 - 153 - 153 - - - R=179 G=179 B=179 - RGB - PROCESS - 179 - 179 - 179 - - - R=204 G=204 B=204 - RGB - PROCESS - 204 - 204 - 204 - - - R=230 G=230 B=230 - RGB - PROCESS - 230 - 230 - 230 - - - R=242 G=242 B=242 - RGB - PROCESS - 242 - 242 - 242 - - - - - - Web Color Group - 1 - - - - R=63 G=169 B=245 - RGB - PROCESS - 63 - 169 - 245 - - - R=122 G=201 B=67 - RGB - PROCESS - 122 - 201 - 67 - - - R=255 G=147 B=30 - RGB - PROCESS - 255 - 147 - 30 - - - R=255 G=29 B=37 - RGB - PROCESS - 255 - 29 - 37 - - - R=255 G=123 B=172 - RGB - PROCESS - 255 - 123 - 172 - - - R=189 G=204 B=212 - RGB - PROCESS - 189 - 204 - 212 - - - - - - - Adobe PDF library 15.00 - - - - - - - - - - - - - - - - - - - - - - - - - endstream endobj 3 0 obj <> endobj 7 0 obj <>/Resources<>/ExtGState<>/ProcSet[/PDF/ImageC]/Properties<>/XObject<>>>/Thumb 14 0 R/TrimBox[0.0 0.0 128.0 128.0]/Type/Page>> endobj 8 0 obj <>stream -HwVu6PprqV*234R04S32P4ճT(J -W*w6PH/H+X)Hwr.gK>W /@.ӊ endstream endobj 9 0 obj <> endobj 14 0 obj <>stream -8;W:dYmnJk$j=`^PKX*GV"-/6MPPhMW4o*jFegTA5n:ROqi. -8M?-(/t#IN>re.=TbIMqYWQK1D%b&pOLGa]H?hKs'8Gqa4A/k;[i&\e-=4:h!/H6BW;~> endstream endobj 16 0 obj [/Indexed/DeviceRGB 255 17 0 R] endobj 17 0 obj <>stream -8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 -b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` -E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn -6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( -l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 13 0 obj <>/Filter/FlateDecode/Height 947/Intent/RelativeColorimetric/Length 90241/Name/X/SMask 18 0 R/Subtype/Image/Type/XObject/Width 880>>stream -Hoi@Hy&8_nyA'?6G3+ZHҥYakOj6gיoU GHII_AgK/EcF6LrchI 2$҆ԘU4w$5_7BQUm"Ť>&k2W$%Nib;Iߓuavտ,HJ \u.&1ٌ^₞@Ǥl_Lrs:#ј,32] IJ7d+65i1$Lb#d]G>&Y=g답*_/*:p.uʙcRIf") ˬ#q4Ό=sL&=(P{ HJ+b~n+cSFsm0'&&cܼXI=3zER,D#0)2=r -I Ә}즟 (9?l?ݳ;݃Q~twoo `41)"g476WxzGMݞx7hpqh{ƃn\ w Zᶂ37{M23>)25Eܩo|+>8q/8m y3=??~wL#I\dΕ/doޱ=Hh|d]ү$*wOc} yz<*\@~R/}}FRHxw G]as &lu9x")`m;=-Ƀ -O8Cmȑ{mG.&?){3];,V01o`it4)'ѭU, ]?b<ݳN=;.]Lں*_w6}hL[I$np+bdjlb46[ܩ0k`I{-ɯG_>]zt8rI_K}4јغOinӭng`HUN4ݛ|yRr #+x/>骞SyXAQȃękfUXDvE#&sBe< HQ%\Ωdg'sǟá:>xQ -!K -W<* '%Y%Vmao!ǩkv>w u{=Q<\ȃ*fƸmqY%ŏRpV{ ueW&)!)sE2Jݓ?ҋӆgohԎeɝiFGb}/g. -,%m.7'FX!TՀITV $y9Iפ?_ȼ0;Wi;h9:FQ]itB)`5yIe[j*lraպS"u 3$hۯNT´A}ٷO.ϡqˤ3!"Iڻkha˳J)@) iq1J٦oչrEIWt+>]hlrW9,-nr_}i#eR=椔 5 ](?"aIK9;z>g9d68 =FGY/Հ@@ 9ۋFJT2[̟~>:ekG<Q2B&M)}YƢ}\ekfeJ#.-3 -iHF'>hd,I#_ыTj~Q5cR`n:s e8 P/di]Ҩm +!g֝V=@nI${%3Tj[ԣ`Į;m$XOT4==Aŵ͆ޭ|hˑ"6XWvZY,{&y` wɵs/NٮMDz3z2 F^ęA r۝gB7hu ȲhI})CF -WWzٶ:lgl7ɃHiJ&/ Ӻg.}C'dD|V֪'9TL*4I]6 x74MVK%X T8ENZ!f8ah@&͚-AsׄOQ"2sO-ʃ#db-tgHIFHVj.Y!х@Cdҕ@2ǯeqyJΎC43nw0"D2ȥXiQϖJ'&:?Ed!ªGIKSيb$utõǭ2'}~dbNct`d K񕨈\29juC ڣy 5,ҧ9.~&g(r\&$zkddjd_&n,dk딝]|ڷт$lw)-H](H&, tHU4ѐxIE$\+Kl֓ȁI*?^^O/N*)iit<~O&=۠SZ0LK578hsZ?5ĬeV"k K ->#gV-?}= TjOK<>Nh auOBnY#qkB)fiQ@ 7@Olo-n 9 =)~erŗzC9z9Ilr&JO-NbW3Ӳh adR<+}D?NJiPJG%:?5pn2TI2v֋rBk &l f'<[wm}2I4KtwH r"!hͣ .:17f͝$Wq`Z Z 'R\%n~2/f|i|a*J&r>-fj@eRc}'yGBvr*'r ->|.WB~0ɱ{H ܌232ɤMRe7r!ic/_Ȗm͇OJ!dfH)OtE9#jԝj'C]aպWf+>KctȁL&rK%SͧH&1'ejuQA2p!Ϟ* UKW?-02hZ!nKO.? ZdzѤ٣wrLI*΍Sі2+TI,5N$6ぺ G7 whQXI4:?5ƫhq-Ǿ(v,vHz&.aKiݵdTOph2E ɤ0J>-zBb8,A6Mgd͝$K I,E[ 8ƶ0yTS rlS]|ѩ+&_9Ezb(Jr2h+ɓ)⇻A!_:۪.%ٱ4E3w~ sOq9F$~EH(M"0>"C|)4 {edUŭ߿/|}e.tD _(^u)TJ 8([E;ZgbDR!TY;g$÷=W__h8pü8SqO㠸*5:Mb)r2`Ny@:iXg*-X=zb-osW̟Y[V$J|!h|$p6V2LRwsU""YA(\A[u0#0j>k6NZ *P$Idv`;K?29G3@/)hqGaLH&)#f2ݥ:"@ -c1BuUU!hB -m?IXqBf=O-uS]*pb Lp=a d0 '%}QJ1kv-E&Y%͇ѓ!L6y֯-ZNſw@ME<V -+Qf1XGbu.AL}{;j:1XM;`m)ݒr2??bӥ^"T.4{7V:7cO n]&IIʴ׭]׳L&ټ~e?618qW 6$уS-J+j &HR#Y(u3C"vaVO qˤg/{Nd* w4~8`ਡODT -( ƃ(rVu6z0F|iU6_Up_ |7//y e26kaE9JTh<'| e\xy(7QQ1Z)7#5eoӈ0g+ۨwCxc XSg")-nkEJN[* FAKLLR7R.LBME#@* -~U݊tEEVOt7U콊ؾxԜ'isjf=O[ZO ((A>&]r"т|f0`A|0/2}+58{:!ELTǝuB1HzGQ0g[|Q_[VSor^Gy?lD$g=?ȩՕLN9 -K*RYģpu0%K'*- lpD ID2MnݾbL)OYׯR3OF"R j"iO8%{Pqs ?Hee}-XJNm\-H/}GϩZ&L/u>7&I wQ) /+P.bP"$N55B]={2`[Hnk?-s\粓y*dqP9I,1k[`^A/n3ՕcVna-_s%YSM{b FǠoZnkE%yt$mAs%Ev]JW^xA Xk0v_KӉ i$Fߪ/u( jLIO%k/Sr7B>Y,770ݙs)]ubQ9OΈL12$jn*2*쾊O&iV"sr+ 2LjȞCxҜJ 9:Jʌ H(hxA&xk i&Iu$6ԕj:.E Q\-^*%V@V;RM]dLW<}*w&Kߊ{-7@-&;. -C66 @9TBUfI[#v1r`,f/5n TLֹޤosIwT&\ߍ#UBXJ&uo6TE-EHLu[FUf -x謖Xz{FEr6qiVd>սl -\Uv^dKCR&p6kڄo@)ɛzxZZfBv5nFC `r{Lŷy7g2H&;x@kASYQC,29Wp -c!{)r*Rj!&#8ˁScM}Zi*H&Mf$\P -Ŵ\Id eDҐIЍF1|CeH ldԬ6i.2K8t׎&t(Q+ZfB*R&~?g4|W~ !$1[NIkqS(T['iͧ4*m~@?>KT)ΕJ3t -dEÀ."!|g_F>";,o)%OQ~Z2FBt-Ŵ=ݗJ)Rbd/}0i -3%f_,%.u;}oZ_`>19)ۂ֙Ĥ b- 2z[&;BEz1i6Cӯ!G9htj9'I#8ˁB!=*t-T:zTG\2z;F3}ZgLhHӍָ!fiVL:,N0EwHVsR I !-U֐L1#4zvB`V|u 4i$\"&^Xjbޟ mR q6H JER/-N&w',͌ƹӿ8!fI|1TBS?D%}35fW̊CȊ\!/5n:VC1@#&R&2rÚ!"H OS&c1[pUyuN'v96c9)Ӿ/I W%f<}*Gz{-/0D֧)T!LSXva?:n[ʜUX% *R&~o㹤w-dr>و'r*+*1=9)zKy$Sp$"ӔIWcQ@&~!AZۑ[W *'W|쌰95n}ăF.i(xyB [(58|i+&Yjhz>˜2m^?fA)\('N,1^{,gI&}<Ǿ dTVԀaI$7XUkt sU dl:OOٯ<2w)6E =Lq<{ hK|7%FE=ikA(#cia78Hw:;i'}h%Z^N^7VҺuQIHNcMft+ -0 '0$:HJ\ ;``pPL=NM.H1Eb~ԓvsK`TO=hwѓ7|GOYZaȎL7e Ջ[녤&Ɍ;b1lx"Iw!}9pXfv`7xgV~π\\zπD-/؁_~ɬebJz!QyrTS蕴.}?]$i;o"):F)(&ۂS^/Ǧ1IG )!bZ1 p8ĕT-@Kp -m crE?m}F!e_JRPF -7b1T}<x4zV,&읲yeTJ=q#cz>)Rv:5[QГO"5o) c^mXyTعT=%o-oK2U~c͠B>(h1*h|:6Ll' ޑ4 =ui7eGo{s͈KjDE1!3e00R,I %y)1]5ά>#Vgr|%vVV>&I5lS<v Z ;RLj/1'{uO -ؐsz( o?;I&mT@L>`|wdF[pY!=;Yk AR;o^2Lh ~_ٛ|GdO q/zRqcw_}9~eiq$i[?~$N%Y72zىSEx1,HDlEg3JJy}F}7)?'|(GoŤ*isFGO=`r3R8)p/MM$j٪u=9(&˜|m5(t75zM'O \]]ITٱ3u0Ǜe/$iF1Da*b1Tzz_W8/wzg'=srV~@?];(&0H1ʿ[P=kW˻zBf`d.d*XJZC^mt.'h V QLqr9wTҺ辣QEx=D19-d!}?d!}3Z#)nmDzly_|1^Nd`Q0l9'0NnbX9T0ZdWf˂8d=pJl#&BsԨA7FDqڇJZ*レT=eSH'YTo4>doE|~ $#&1¾W` ڑ1)د6:'P}O࿠ne*Fرs|q -(iC4P+ $ -cT6^b-4je˷O|zS~?_qCjRr̖E˓>jEk.C-n#E<IO{vE5ӄ&EN& ݆)-:< I'%}8HwяV4d=Yv 1|Dh*; <Okr(Ny%*~*Yy&WA4!x2zsY-L"=\Le5ƔRFI%IF\3_87 0>hq x2`+IǙBh#R8-w*Ó1YeVO[x!mh?[ <$|`2s2l嚽O EҌX)#Z!Sр4Yoy?ªf8jO1O_9O"%z dy.nNY2u5UV\Q~ɲ|kxrd'?apK tE7s!m`jqZv[>hZ-%6}az,ڜdKɷGM)،xd"6T\l,&c<'Uf; -w&B gw)#„S]\QVQ>$I_jJX,\^YSd|'zD%{o1!䅇qx';qڈ>
khYӷ@mwGyxbr ~=ͱ{9hsۈ!x2< !f!mf" e!ONYW,B-%'>,;} ^&CE"OtzK68]dGRfɈt}B)ILN-^3d[ɿ/3GlV&77ug#K1P)^I\D}&/o>߭SHh+<1Iy_uA^ -sMzC*d\'\z1zADd& -9$Y"?LtzK_14*Y|!ԯ)7$URyuf۲/ɖ$yhs:ڏa<#<(){;qSdLt&}ZHy$yyLܘ: ew}\yYj|aIQ%#?xCE"Oq1Nb5˵I~t֌ZcEb-%Շ8}@i?qqN~ Ndl'\z¤.o !جlݜ(B],YoSO&w"0fr -L&\Pby?ޓur!m FZKGbrΓͲc)eE+WqytT?w ]]}["֫K5Ez~J+T3YnO26hSEH'㷢MO$ta0?j9VuK'/K~CcζI-OV/KѸmkҡuΦ)"&㸔]LyĂX)cML=yn\Ω۬ crї'ma5r(E=pu7< - [rd{d7.`w(d;wr(M=zRy -7]=!Ij9Cidy!NmSiǯƆX9r R:wP<+y^{᬴$eYn;ﷂ2^%)uũ Kw Eߊ. UxlidW)I5Ip΍y%gMGƔd Z9"~t]utڵɲ2E#{Xtp7 #i4i>f-2x jL?bGœ{y9k -AQש'=FE4b2&al6>` -hB");Is*QY9c"1鲒z2klvy0 7>`%JN dXn; ŘWO8@g,צ)_cvH$q\ѾM_@%Ƈ؛XBRcΜJcRΆ1xZU]è-9NEw'c뜠I=]c fi~>?!NI&ļfb2 Z8,W䥌e|a(!me?MQH'cMsY*+!\VuSLQ5Kp#}֓mj:SHú5\bØC)I0> jYn>_+c t57*pT̛6=nW%4EQ4z2~+ɜEO1$|T9c^Jt,Ύ9AŵK2Ɏ'+{,uCL/ڻAZ" -d֕MW.oBgDtʿv(uX4Kp}Gߓ-8,]o^ѓ[J^c(NY]$eo h9[ƣ:1vt᥌e| q'Kp4 b>HAB t2n{'ͩ(*rGOӡz>(-ɘ-d6=г.k؉NO&37{UGɓ*Ysgzҋ>XA[ &f.oqC󢔱gs.$e/;?n>.0&cT51}<;(*FOkE;a\.S+S=˖&EǗo3rLdA˿}yb/IE\E"*;pIыeCbuZ ){ٻ #=I_cN|k)rd@Q?h.3Ed^ts*g5 xQX욮&VO䩪(Idt1>ðh[[AEX̕ϺܓyK{./T˱^>xL,Mo'svy[/*|OJgC{::EQ1SDLk%>>Ѕ<I t -eo$7-gHIΆ(&-^^rs *BadVؓ-W*j͉IvHʞNLO:W%_31dӨXܸvH^|Tݓ+9/Hrdz_wq\e?@MQ̙ݙ"NuhEA(iK̙}v(AHQ"@D(WDUlMĎ-'Eyf&٦Q|~GV ?|mzR3|}pӬ3 QJoJpd\nc\7n,Aײmɏzs ޡ22JywoK2/X'& ځNJ* pbR3|w) A7ɤbrc )#f dp[M_&g][ه?@O*v'd5Ä-`˨[ GOFntLλ=95fPL -&!&;=@OK1Ŏ=5:2J.778&$k4RJGL*sͽ֨+IN,Iij&}`K闍sEvDRϿd}OIb_93Da`(r\|@O@Moߎ$gi)R~cb]_+$H!n~F]1GL*vZFi2T'{ z\ARFMbz~wb1Qe5+zRb25em-3_~Ȯ) ֧I/_Ox^JIQOyT=F9CW鉣)fʴRڴ1[Ig - &NA@Ot\ᏻNoV0[%dRїbۤARJwu oX7h4b0$m~a'U:냰6G8XГOГWuNVL1҅ Qp00bIe΍{֠ 6 =q(B:w6Ñrޯ[?^ORLRIv+/gRJ:A{MAOzIN0n/{Nac5H$,+ԓr~ ~OWllfC+0+ГwځZWBA9%gѓ-y唋w-GF)Uk(5?C%;=GLj/bIIzYw~<HיմFi1GL*t\۵ŤH5$gP!||5Y,_;$8hdẂHh C~aГʜ` 2$R 2-D=yq{IeMИJ)1 wT2#&:=FI'@L&ƥfZDRʲ2n%%AL)~(_"H1IW0J W'91S;nZk PJk3=) {=^)&/75fPOG3-#&7@.d{LO\ ~OywtALL%h//8IeN@7sbHbۼ1W2\jn} bRn@z4M6 -'?Ztw -٫0T?^Г" [od% I'{}q{LII6WeVgROS8 K@D\>&;sp6"l\Z= SԞ89c-|~ۘlr\iƌU?D'3&Haaőom“ߓ?wP9Ô"BH$&;m5wI'6WvyCi~tw g#̵͎.p9 FЧziۈ$)&$#})r%R+ [=ړ~t; ՛!Qײ^Gzr}\10Ouc+#C͂&(_HP(D -d!թXKtkcZ*yͅ (  w¯<\*Db,$=OVp'1K.:|/lӥ+i&o练Ɏ[UlL=t _wHY{D'C%A)Cyt)8tDzPˠ|!B!DDEg ̓~WF/Vs'A\U/! -.a{0Ç)zfnڛ>< -.ĕ#_uMLzb)ZOVfc+UA)" -4D')58=26L">^&Ư~nc#{Uҭ' T Z; $U:ri -_͒K 쳷x#LJ4K\4^mΔX][XVBf@)5:'7}OV2L=Piϲgc0Yh-8iҧVk6\o'Nq|$T($)y6Aߓg O"Hb1flsarEtku*F?L$ |)> R ѸoB(L7H>IwUhc}[3;/)go2qCJ= RHOY$BkѧJ6)b Fl{h-8թrbVyZgcF;HҢt@ʰߓŤA#v齌3BILODxRzI;e5 Rkw'w9OD)Ý$)Cy|O[X)7PG$E,̿6D\GLJ_VO,Zhֻ\/of>!Ç6A0ߓN(+MC d.ia1^j&VmXuw}q:ZLa\RM24Iaw ! -yš|%0KeX\vIِ)H{fZ;qRC{ /Dny]&OkꉥUS=l'凯Gw%)H3ct:BxO 3$0.e#PɶO -|XZE_\(ZODhғŔA-]%f.>nEѫpE/zT(ЄOJs-M*_*PEJ}{ Pm3\)W>[w*]d:@,wZ$IQ@97,aNEd$KeR,}jV]RDP($)]ߎccC$BhGlkFbRz@>ZVN|H[l$R=y:QE>HiϯFxbJjVV5ܮyR^ -rk'eG!% :W!G{DNhJ\9\wACl -wϱR>"j'3J)_PKwG&) wZtݠVwgc)ßHaO&nr#󬦲l'3yxY}fdKJdARH9E}%TTҌS4<zbtXG٧WgB'WVD1T}gLbi[wǚS$B Lٹ#VLXC+;ݪd #+oIA{0]ceR(d֙F3$Bxt@V IғVm̴dE!)yJ>D*:!Zy1Mz/PBIf0 Ҏɸ; Gځ*z͹6HOI`)\NW~㠧£aITVߓMӬ$B'ctL&ݪ\Ylik>Wccyh)GՋ`Ji\1W݅JHrmL*w@ʡxѲHzCx /+΅cf%&B_$Gc&/ ɴ.>)=yV`pB_5B#:ʹv't,;fs8KUeD 0p1>$Bhg91jILJup6ꭕ7k6$?:L2zקb`};=R=fyq($&jkeMkoxs9["L -UB.t/MO0tx!Dn}~yLҿV]=2f^CQ_uyp%(I͹䈬!I-yBs95kIAQnԩ{Sѯp篧Gm2=d7R&Ӻ4M 54;=NGc2ncRt`AJxw&ӷ4p#BΣ)Óe6y0)%L^_׫rhe{-G^O9&%4j2Q/ -LAHiL"CɇvMå)30PfۿՂXa0)QqVgsIV^UB~l׋ Gސp3 L4RI9&M?V !$)n+Ye6\V0YO'j Sm,u^)dw>R CҠ/eO 5`2 j'#=%JXHPe9zTw?pf}iTnQntwR)OX"pO%tT&Ϗ]3)$7ePf[pr޸||l5pndғ+ĽH9zK6]mŤ zͿR8!>u=oD!h`EE}^:zO)vNVB%Ϩtg13nՅvkj,2Y'@]>';qQ)dzٳbT O? :wu\hvߟ#zٜl]CV rneu&w{LJw'R-FE?!R/DJrI$d<12,REV%U<7g:fg^d`bcꩶ[:+7>VΫq ҴP+}coVD0+ [uBKZ~ RyUs.++3UgD0:=/mCԁ*#iU}$]9e88 ?N!"::-_:kúXQG9zչ'Iy\8R*KƸ/!~Tk4NFIʞ.9])3#ByoL>{cRnn[n7V>)WKRQY#UzO?'s=Рg]duC. R> -'}nMty!׸/0y([v7t%OZ`bsI=P'L'ol3{%RJ }&|DQ5M,$)KBcuq\B銞SV'Ofw57'H#RΘs9'R/)Ȗ1BrTk,pY -}+;B(Ř ɯGE'ts+ mF\wO;KlTȒ_SOcf@=-M//:GnW?qPgE9oO%`^ xm{5Hܞ^Ĕ™M:'Z!2Wƶ4ro+nopEfDjtKГAbk&V=Bj%ܡHIc8gRchJ\#YYZi\\h<]R]Q_^vЮPv7>>i <]NlskT?'P5mIF@OU)xUaT}#F;B@ =~_ `rm,Z="]H ?jjR*~yT TI*{zԢ,HF -W!DdUb;<޵s[#Vۦ-Q|_緍Thwr+PKR*B@E` kR.Itiai^ArȥkP_ڃbDJl2ˣfgIrlX?gw>X<}"W -*e Oh)5ЋPŅ]lxh7&\B{ԭxhvRzE,Y0C>yF UJA)_~D7DAA$;)Q&%AGt)K^y4ν* o{MO8pr\r@x"Bqبrۑ]JƾИk7lJ){'@oJMNJ"\ Fc}IJӴq%M d* ,l(:KU+͌'ᏏGJ)23,I#kLZ 9:F-G'\Aθnqޒ`w.kY=ʆ7 ?>:ϕJ1+4V(*il"K!ʓ2G9.]*ӱU@)[r7]>cےuLDMbV [FQ )zeK W2|2& %n0I]4ukǢYNfh;Afbke2$op푮^J2\2޴ )HR>}yRE*oyd } iAbI_ :KUli -d4<@{d΍b}rJ4E7l;|@*w&$!ϧUke2t-:] -,!߼l]}~ =oz{֤X5Q$>eL)Lҹא/] -Og6*beC>7WH+#bX&8)rXu$|RGmkÛVlR޼lURՙ+ڑB#0UIEKأ9~,bԲ surX`,Pvx#Er TUUnMt)NOsT,pa]~C@0 蓉-#$Z|f—)E\%.rolϛ͂ / ߍbE2yL{L& "t{~@C"a(R -tRuf:NReؚ3CQJXkl cfS,hICc=u0_Wfk>knL1ז^O> ~Q'tz`'#W xV -t`O=?7F{Nvfowvv*QJ*0 -D?ޙa B J_$<z;i{wF#e={\&C[r!7&'kn¼~Ѻ{]2 @ *n{Q^Qw+eǔwT>~',U)+DBGbe!z/E"-|tʌWXbvF<6NHP&?pdrA[_Wm_ -5?&PF1J'3p|R]]9M]9LL2 Q -LrHP<ɤv4ΒV^ZYv?`vFRB(M(  -H4JoէX)Ϣ G)<Ʈ@C*p&̟\q7H&5UQ^Z^u-R)E7?A|^u60H%LϐORКr{$$A@$n|^v$zn₰WSo_Z[sSrdRޛ>||R -% -X3J*%0|,ϙ"g,39!+\JdR"NtgQ^ҊRlr?R)i'a,P * Jycq?DVI1? IM<(.-i[-gb\~{ ֟!ɥOZ:,Ø9{ٵJ:36pVݕII- o޾Ѳcݷ85kk,K(;9g' 8r[a/#<4+, -:VInI(o d^r@ԛ/{w?p_&4(eDRcёD>]+Xkdqj22y{6pdRw S^yK RE)10Kҟ(. E6L, bT!ЕLnTνe%U-V* [}iIX+ٯUBT&C86OʳDP1[]\/&ְUڪKKjn#2LvɩO%JzQΎ~.' τ9+RTDL.tdR>"V+[Wo__w7B/W3 O.9+Sl>t]ӉO"oOB|r -VNͪ^32 X)mP ?sbֺVf{D0/#o`7ΒVm_Z_~ BpSETTzaLtZv2Z)8Jq%S&I?IHd:A7.ɲG=Pkɳ)?(_;YDlgO_!;FJ**e' )2H& zӏz,T=Y]=+eQ/0g"cb|蛲IkF $!YrڪKK4.»5Jj;>i:':nA){;,nXepx}P<4MXyd@=K?IFmr[ŬUT=Nr I!ԡ +b(9qarJans=Iu f'-'Lu,QJ RtRJHEx<'*\aOpw@ӣe nhzuFa·-T_~M2I<L3+vm n a`Vx|€q*&L:t+/,C&MXeUH8mL^UI2IV9{5)uR -ƏA)(#nao$<2cIGG&/Zw$Q ھފ}!iD_~ϾzdÈ40ɴb)+2 dtd}wCxkW 1  >U ~lUH&6(^q10Po=&L_v|ș$+Aer@B6.䉳:/ Jy'Qʹ sx7o}'oLO sSao^<I) -2 -$lWS/`_wt7U6?J)p0g%z*u#"#eDy ڔן8ȈggnW4c[4R~zŰ:,,G L}ʳAHLBoHxVۗ~,9ʛ;`:A)10C|E"Edxvۭ -tbX:OZ` RFyxQh$TIcz78'a0y&'2Yd̟L/b]U/`_w[Q|$wYKRwEWp =NdcTWd@gBDHvrPXx^`aL?$I&Q`OnfY)*͎LL cz$GD<Rѕ۳dIⅉvkLn{yL2U+»=J$FwB! LouM_9[ƵR`#JA }IlVk^ݍ[[$<9Z; z>I)E߆Eܘ&LiÐ/Q/|e0A EߊH&~ȑq?, TUt<d`_3MG*&􏩧w -H\A=xraOjVDEI`NI&Ό8隧ϗ7E69t}y^&+tz“/޽%vp\ԧ">AnB.WC(yS&rt+YHJ W"U|C~KJǬwRŔ!3c[1 FdI2qǐxCo)Bi)LN0&%6$5KL#xG;n䟴T%m8<9%S4o6 I@Kq=ow;Gnۊhх|!`Jd}<v,Yl6I&ozIki[іp뺤u13O*QL#dI'LH;, o+R1 ;ϝ DDX| R&K!R]?y;i'|WKCr0X,>{;P7`Hdt~ìsVBF *`V7'98QN;&Uqk¤Th:0l.0Ϲm>h7JmTEHy R}ӿ_J9pIcT~B{Lh"axov% L"%˛:0Nܮ7Jm7XۊdJY>;)E{d ;L*;[0&|X$Hl٘LD'qYTjd]#mcw%R/oSa~/؉0+ ^&? -\CD}#IgJE>Bm'Rӆ"ixؐcχn' =B3]LItf-"`*E'GI)\R&'vؒNK)¤V3,L*> E}o|| -Dң[+lvu,ޒfZ˭+G_2T$fvS3DJUzDJF+7$; S2[+K}t]aBz1e m л~> R"eΙFc!έ|F W"%FI O)KHrڰ8:3Rƿ$ %R$Z㼩{W"=q t)pmyۊd@}zY:3JJ[F,qB&$Haos\7h=$%L&8ͤj߹4n1ȡ3)틤 % -n^_G,hZm|R0e"<9XiI$O.>j<3!R i^L LrД/15D6ĖmEl6uA]W6<=H"Hk!.I4yIܬEKLj6#k[F|r1 [C YI2ܟ!M]t-u:~lDAVtĥ3LMU߬JI瑒:vT -Q$EmHfDSɼ߽4Z&Q0"v%Ls|'瑒PJV4 h0yd…F e3LV[җZ.)Hm^sH=10 H^;ly,pg/B~\:Ôi| ٘F,& z BF -&H㑒#RʆBl, m+ -L`ڪlѠ6~TK''W"y0i%#gL襔AOI,g~et)AAII(kWu%2?X[E"\NHZ[Ѯ&QPs/Ƞ)_bɆ+Z%\yѿ^% cG_ I҆)щ7dDo·=D >V`%^n_&|l!#Of!!e -D'a?t1<|)zXZgz9x;$"%v)i)юec^$RӔ/7hJd]kUҳg}qOb&3IYJD~Fە30k1.zmE[_=.FZA<#VNɁ.g*|Rܨ=ާ&Ir}dEGwL~F4ƤD&sQ> &nl.]bj` Džؠvuf|c0~̈ Dh_ne6v/r+.& -BcO"N*HsVpIzQ.h% P@ Oq-ko&zcǛwy4;k5$wxPѰ@cl.7x[:qΙPJ0${p!YKVb1`= 5Z-0~),ȸgG)OEI)[K@#$|=+qPODo[kVf'g1sg-gf"p]N@w 3߈%-Y,p|Zyǂiy1ո*DIOu=tNZací( (8s '%$!@6/cAѲ*e}Y>Qc33gC)CB2de 2 ?eGneel LY(Q4:]rD *l= aϼ/_-t쯥,vAh,XM}ow#$D筫3y(% t0b3F+d/O8 &d3cAjBtl/hA;];ICJQJJ d©o-Tz*iʉXF:72'zڬvaf@eJ;}3R=L3/NFL>tZyZb*UVf/9!l{JL@). d]h -V$*#%RJsci$—0R.dL@&@@R+Q,8+δqh_=DXq(%8wr⧟L ڼj,zIQ6ǃj\kNA[fk$^rθ%rď?;s -2 h"V <44^WGúZU6v=JIF. -ẅ́c=M~_ghf]Sɷϩ`6SVOVd-6秋1}ᓈ/U# ??I}G> ;9G'#~,CڹI9=3 z+{ak?qz8gd%$}Ye喱CBN=sXlQOO"~ɫ#旗Y[>}OM ʫߌON 6L_=>~|'ޙOFLYO9ϙ`hg&W$m=4óI@A3+YLYyrAg;5MWځL"~6r+WԻEI0 KVs[ dE-irB ʿy?oGxzt/DhG╯O]Ͼ0&ѽsg#>|Ȩɿ?+V / cAeò1?7|M<\ݷ.I[GK3Sԭ7.J|7ux纇>?<{_}d)*n?&LC+YСLmp$x>}QҘe1LWe^9RgΈ7QdDGp#oU]t;w%ǂF 09IJO"~Jh(^_^Tfm34{ɞ~{xyiIR'k$4; $hY4J D|suIng=UW'#4&+qDO8YuH&UY.q|2ho,J,4҂ک]zTe{lڼOM5ZI'1G+a#5!aa@ʉ͔*ڢGhs'io K0Hr$>,ffQֲm[lE:>"KVF={jٕm (hql!!&$ -g8& Dỷ^/Z,iUJ|β-d@[I$ٿ̀Uո*bw'C7|B4<});B/R^`|2&V溳CI7Rr}Ew `]iiJ @_hF65'7leDőh|[)v9|a6'E̊X @hF I>l'(kYӝeO"=~͌h_VDK#-JZ $zf@lO&F[uΨ \|]T-V~ -$HOkidm'W'sY37%{HVЀT)R04+q`8AW'v_+owQ87+PUOKi 4k ybk:jO"1ƥh_FiEIwsgh@jh)+ T㪨UEKc rI`KЀTOkpʊSK-*|aJȜ7xLO}iz,iU.O|ƂmXmSuF>Er$SMQ\M&> -<}!jqj!!GuW'g!$”P'/\FϩeI+T=FZH3'H~6aF50t -J)H ®A]T*Cp&NlLnfUYT*V%6a50C00D?Փ+os9ؿAtjJ|LGq'l/-~zG7 0OhAW\m5-2V*Z<!Q=dF-V"`R!ZZͿ |;?E*80K2HGܲ,K̡x9Ulu,hOҰUEĵ.d쑭J (h5iI5˾:b-#0o#%E?+IFxl'Qw`4smH*3Ͽ9b#|9.Ī:Id .2}'lY; {oCEM+>#XJ=5k vi-:ӿl-ŗ^ao}^ty`$u-ž+fg+jZб>mHȢ+[wQ>j`"!jU.ZF'GeXM)p_0D[߉+vhh5ֳҵPZyhRUhs|_Wm(ًz%QOC)MMrЫcK3;>;|z2 vA' F;Ͽҳ*G@ΩV,|oakh9> nI4E##ɋD\[4s"%6 *Ap'6Mrg> ^L* uKg>9ڜOя>5,)^I^4sKj[EYӸJSՔ$2;A9+[:hZyt>#kIw.=5-3(zv||Wasrx8qYOIM$Uwъ -k)>%`tsg^. -{0y=k=+78\ɲE*'k_k>1+m;QOD= `7twKKj"T,)'t_S>)yvtp2 v*(lKj"Y+ldTmNwv*'q$me -znh)2ҵ8'VfE">|ڐI0&`@6,{IMd(X\_IO"`ƾa߉'D# `7) ^*R[US$qrأsm`?opW.I]D6rh*s'dJ\^LEgoĬQ솖bsB!΂& -=Sb#VS2H'?]/},6P. -w0iO6si"=[Դ-=ұ7'#_Gp[rHsē%^ lJR -$EFj-3>YM#D?Vx

`t ~9:X%I$i(%@A-#kY>?v|:H$'GG߉ eZ$@QӪ[_O٢+X(z uȅ333dC%H#{0C&iǽ& F,N>y=?})'~fso l?3tb]L$kI;:֙OfVTN|I"qn蓉D>g^mboz%HKnX9`yi[EY2WԔȨc~xLtrId?Βw;}lUנV3'kiJ4'g~/W.$.p}蓉DJ[A`Y/>Cz%QOP -C#-%4 l0VE>љxH$D#=>D += $AYH\4:襑SO|#܊⟞={G.\?}|ٿS+Kڸ'Kz%QOC)JJ6sµ,LT&)Ъ?n8dU%璤䓉D[EJb_yv.`ti- {7͂^z6eBC4G?㙙SHO>u|tr/}A`~ -s}tHOFBx7q!D5z[Z_ϊGG77?EI#^x:{i:<.! |2뭧 oc4Ό lf`̈'苪RJmݒ؀9Ćv~JժR/zҶ `!Y`se睱6Cד< 3 e+vPa>z^dPϦ5%JiB(.vnBn=4f]WyFd~Usd/0_OUOJu"aǰm$%[/MJ(1M*%H Z {X1Ԯ(e4}K1%.ghjR^6'Kj'!f+-ubY?GOb< -8TSsm֕$+F".P(. -Źڬ6:TQߵO"4TJ͕Wr'x(9$ IO= XN=? -+38B0 gS[=%;ˋ/qUb'D}$C*,=\C8/C1ԮԊ@;mx""5r tHoN ekk+k1r#@`>vt[#™ƨ'KfM d'S|%3YBWR/YCDk'(κh -@S8e1[ gY4UrgI(9+ʝ'%ItuKkK8ӤDtT0|vƐ8{bRI6eGX(Z9-A:E1/'|Ŵɲnw4e驒/tDyC=nzu ^<CO / QL#8K&<'A˰ßɋ$;)rOt:D姻e3}lߛDObh{@Rbxi"/žI)(*~]xAp=q S͸CfT>|{1~05$Ia6e?*/W5;glkJ,h,v(uP}J>0:x)[KUW:Rc}?)% - JZ$O|v؟ _ -P 3>o tC, U͂d7; V %gI${r5Tpi`ԓNߛDObZjwW,[\{S󥐏D|~H՗(/)K$ 0"'?nLv$Nv;U 5K$tpvx~Oe=)N#|=!%0#\ vF -sS0Hb<)V:o(Ic\&zb2|1$m$;āko`\}|0O%_߁RȧEr |'Puqn9dԜ;x@߇uZH?Jm K]T{I.;aCk(9 -ji4.;Nꌒi2:dm\xLd>v`n+̿>.ҟTQy$K!߼>^U+qGp)gQ9ݔw6' $惩ڝ ^f{w>Ki8` _~\j07Yf;0,u8l'u:kn 7)0k ;]cwՁEiUQS7b`ޒ*0{򹌽v.ԮOUK)6tۉ-XnF+6bTj&ٓC5S6{;/$_"U2lh/qsH}_  --vY`+Iѩ"[pi4agi.uR1Nɬ[x_zBRamOv KjbgHvPJQImHoT'iWB?ZF|2.u/S(rc*'}JJvfT"xL_?;Hɂ6eEk nU[_NdQaUJZkшvw1qR -5mYlQlDne6$@ڡO?wd[:(Ԛyo5bxpmZ >ū -VkkWX 2.$<y =VCyY_)*=)$OwJRozj?D?@h|8և77_!xK}rBv6!f'up-0mA J~̀|%G||RWqTmήtkC%n'OJɕXB"ÉMRd|Or i/@#5<֊@/wy |rayxU6E)|/Od^msN̸RvIٙ^pN}I-נ nvTSST>rOZq ,|2J}WB)mVJ`ٞ)ia K c=>r 6qvHBgzf;&_%\ai/^3# {a5U{-铚d; $څu&(ڻ(\'u'Q5ݪ7j}($TPR -)w.G#ʛT&){xf LZeG>ӁVͱBY*kR 3]+U73k[)g]'{0v,6~ ASyZɺAF̞{ cmcS°/f@g~R*ӖZ]I]|ׂO*q|rQd*I7ʞr3\mV""2)vY3Jqq Vs~k}b9EM -dKz9A|X|/㬶#/ÀR*b}LԦVӄd.-uךxge#V϶# &O -.KwfZiS痧2.&>)=bxIǫv|'QMMJ)vZRp_cVn-" aLJ pZe&9 -B/Gne^;͓SufuG%A}C<*xKVߜ('5froo? b,*^uZ vš\>k'_27ɼ<ņ$xt{]Y)V“>ʜ D 8Ҏ<'gy'G&zʃp}0c7ӳDo]BGr "$\x7533> -olMze[nw hyɞI>j[IJ)J"`>enX -EZU%RܨCRe]`&Q0,Oo2L~r ?L8vVS>'"+=r!cTVPv D)/n_) -YʙJ* Vلfتy&R'=KWnH'EUvHD7YIpB0/ZpmU-)BǙг[I_xQAvX Sٵ&($U%cں8$Ϲcشݾ`M% &Iv/ɦj*R2MUjkތ1yH3̐tUsJB˵WGWߗs~x < "<{`8LVѢ)QV)U<}BSTG{XØ~!7J~pOsW֗dy%#Qqdd=a딈('lHɟpL/8t y7>S{&JMa$ )38qf R$~,]r@#,O>LM%~[Mp ~a'N -,gGCO֗$Ħ223؍{UQ0!"z^"eT*'TмM9%Tkժ $e:;__r'8j)Iԫ.]k#8O -ϓpC`:Tjϓu4-ZCIOKÅV7~fyuJoyR]eJsDx2O-vGض^DDY? pUΞ*b6IYq oe Ӳ|x9 7}tp΅ƻDJҴ%-4BDe<7JZsOټጭҵ8q $DAiB<8DϜ9lJ.fO'AP `h);ă\A%vy^;ʀ'J RUa2yr ^njtH_@JÃ8؏'{$~rH\3NH?)4ij,2 Y7Os%Ӌ A1d]-k|p"hQ6ɣTaQYVeUjNo2&I <]HIx2dZ$"]E9H fN-OişbJ|NW~1ӷbS.J_rBP.Def>]0ICkަ1td'h4Xzl)<OR#dy xD}V^v[ZE?MP""arO:%[*/bIr΅~Js}},(:A1@7}| ؃3nhҩ"jeU -cA - 4"E(@cC㝣2H!:ovj'+j' *'nb`rZb$"24RrqpUwL%@`_FJYAZG̺>- Iy *Waq;zGh9 @^ ;[qPAC`5OjZU6EU3]i&IW -PJPpL>L:_HIWi͊ -5U -{2-nt IHR2{r,҉B܀1`u s% L^IJwM./?O¦x6yp8"SgQw%aTB6P!J.ԘsFbJ'\ :b҅E&VR]z94x I(3 <)1 )[*nE u$Eg`CTȠMz1+b;jAeF]/~\0B^j-ڃqK)Td9; KյIFCaH*J?!*@v<·=˄ǮjsFӍڬ Y8|vG=EO@b/FѵL~"a2?h.+ ؕt~ }A)4N=05@vI x2[[M<&^9ӳ^:$u두l$,) \ijāa&F=64Մ>?A_xc$L)vK2bs7u x́'SQ?qoLՅEqD;p -4OҋcHJ{2cr2l'5)Ry<8ϡ"dAqx-,On!LPP` ɦTO\RFr!~F 'cA￙c. ݆cʇ_{;:1kڸ9G(j2fV-9iL7j. ޓو[[E '-D ePv|&oo8>3}=y/<2!/#ړgme_ -./g MA~5xR/Ynne@=c$5!“?iHfUφ8Ucl3]\cZ$<%cat3jؙx2b^HHH"dPejHkX5!\;hGЅ(jO45qd=sQ"y$~zfp3kVyD7%s3/iȜLn -B~ri~?5c2 $HCDaAř$""WW$uwjfvvڭڣfv-P rprk췻!NBw߫OP <}- O[|'O#e#g`RJ13C_^SlFI?}I8nf`N$|@ e,ydMUn$9K1|N=@~{ 太7$ŻI_2FB"l3~n@♨z2|Rا:Dx|r])O03[<+$xa0.uN3zގZk`QS}m>@,5:,kQ0P~"@#!񰊿 ^)\3g%݋N0O|?Z}1I -DPÁ2$BGFťs>7gO` 伍r_`bc RJX䨙m^ CWۘZ=u[͂\ mRJݦֶ̗́{CG>0P KqQ>UD .ҐstӢSV6 &a!0ZtЄ~wQ>$bUI`5`SJ`qmN(`فX{VF)y}U|RWT"< b?<ɾRꑙ-O,_2"ƼZYk>sa8 o -r+9g[9mj6FO&@FZ{->9_b uR -'TYXSpmx5t1۪Od%N?`jb9nyƎDwOe$o>9lBރT1S G%įNL&6'$;ۘXMY L+`" |2;[2}r 3ye -/1 1JY+v"X꫟vC0d-1K$0(\.Uiiڗt ĒeBߎ(i&©Y) UjL6E+Ep%L \!@}co1q쳢VLѥϸy-e>La 9;\  :fYJYC=i[IqK=&\CkZn%a|Ju>~W-m(SJTӼ غdÄDl.탄<' ί".2rA Quj2&jBWb̌}2d.p! vGZb0#~6z`^[<3-;iP0Gne䝒_D*(ֻ)2Rh-܆Of ۳ådWዄ7<r7x9KrPTY'~a\ުPY\zllfLt'v"<:Rn|BrKbƊ%3˔[_Dr*#B}ĩR_!/ -]bfi"p~}SL<'(%Dp)"`G~) MĬ5lkz9o'hHpoW|>"WyxbjZꁣڍ&X?B{O¬V'u~` zb3Ta0AC.B@.9ȱjIdªH bAJ' -|;d!I쓻b0[K家т>Uʑjۀ͚Khw+VN-=ƨ_SgM 23Le0/!׽ѫx$#}k.b+9@BRJ.O-cv{e ߛI3㓐-—>UJ`J -Z\z服`/~κMTQ)=p HoJ b!Orw?tTŇC"b3L-E!r[FCWI_g4d`}]yyjIIddV&m?Ttwb`vTJ،96!=i,Ҟ;ƨqi T/8L\KJ`s:=ީ'z PG>9PF -tC9O皇WI=f~"Xu>:63;;n3>Њ<"*%,Z-.;гv`Vrʈ__:\?HO9S[sKf^p)UDxɡ -ꑙ&5Ԩ]U.D*K&NWl3|M7呜OTTO-Fx?֢hG bm f;M)a%5̮$y-5{.@&A)W8üܝj=P+?vu܉pHʷ)Sdœ t ԿE{RV \ܧw(xXEX5 ~OBHO鑚/دۧ;Й1sZiW*AY+,i)jʃ" -< ‹ng:w7}v:ӝo;l _');-T[L)AGuD5KO   wQ@L0Rj$vϜ$ 긣dV_𹒚- #l5VOJܗhr*-:*LW\VA_x#?(fouʊglkԖVRp0 [1Hv}#eQF5 òGRf!C1 HvY CMƜoG-4ƿR9үx'MwL?! -veGT -^txZ`vIr@ 1P^A]t3snZ9zO*O>q*eɍOB,0LfZmq'SVuǖ֡&Rj= =aٳәqG}'O>w`&mI6d`nāRid!`KaS^x{x/c瀵_]=ٲŨpqÙ_pN:XG`z·uIwEr?0OadmjV2D炝CnE:r\IMO"&udV01wJfrc"<Ò6ZDT ĔW1eqN8{մW~~'U'ږ-/xA*hxKrӓ6UGR;@!dFg0 CK-w@5%&ГlJբ>aՏD f7&'Hi NF-iWI77]>RYiyEEqYM -s_Iǘ'JO⾑[q#%O#V\/HRDa)dfhjoeN[CBy9zRM)`w>/ %I LwzÖ ⪟9p_x;ieeHuJni]tEJ Яxő&4'E {BvhZWyڌWM.ozil2rw#> ;YpQuϪ+>&ܿۗM[1gt,1湐Tjג"ela`F-xJI$-d2'1OuHd(0LNeEnrow"jS<$e:K ? (1hNpxI2i)̥]oU+Mdoa 񻸄16>)a?R%]E8)€TI=XVd) %EJVXp.idڌЛL&^ɇ9Fx 2)Fv_|d#΀xcrs̵sXd`6ҟ)fMÊWl!g۴{RۤQ (H=߶:(m/ 6)-DS9H`OJ SĤ -)AdH LXZwyøEKЛL'jjE/ &)6*sę|,$CJ`v1Rk݄'%$zRK='1L2)+wO"JSVs$'IO҆I65Gd 2cnx'udV/8<4_ &5RZCDOrJ8kcY)tFlEA hT9mr^9MO6MM{O -'?K6H2$li0gmN:Bk"%& -X8rKfãÒ2-wsh9Ȓ U6!KR<<^B>aBIk  >Ʀ%W*aKkջ)h7'k'G x>2Â{f*v@ReK쮨L@43Lzj Jyd/nj[C-=/$~$+1N>ۯ+u>?1Է: Z)g,'S(XJYO7!JI_s6R:L\,I9n?'CVOMN)iVK` d3ZA@)gY7QʷtۓIԢa仇>Pܟ&\f4+ѝtj>iyIJrcH>' vvƨb|#~z"!)7O(5SycPJjteO9C5n@)CɢH䓞j5-8 -oH\6_?৖ -AEdR r+TOnגRTY%rwͅJI_O,^cId(ʕɞNM[0r3 v;wŁJI⌎<*D --QO^g*J= \gyhTԕh$y(VC)C;V7wͅJIΰEg|䓥m$|2 eS$`LW)w~RC!c)eJO)[i_)I554<|2䧂!zIݭ3UY3`CR̒$igC(𔲙/oi}rkQ{~C'FOpj|OR4SsՆGk}ROSIVƝT?N+ʱ"Td/ojd畒<]}u(k _m@>YI@&CPnF:zL= nJ9 ؉~82Rc\ʏ5:fe _Nz߶bKK>ڐ}^ʻ=`LE) -ոͩ.;'sRrO^)s2t"CwUuŲ^cN譛g^p9H*XxhyILa55GO ڻZwE6УS(a Ԝ瓿jT 'Hrf= Pŕ&KwR1rعW)ê"ƽr%>'7h$4)*DjDEd@v,7L *%MLո} ,8'AT)ͅo|-fR)],E|2 u'|Hꁻd?F_P)Տ|lwɲw6UJ}Э&mK'i*>83.āe(0 ČWJFm3;ǝ#~}G\J)m-:Yt%8'O_ pLW1Aabx -%RqY(%m~ apink_)%II_)9N2?'Kl[* |2%i/ytTw)2R)eatV?ͻ$tPJ1֓Mm\Њt!܋f˚+ ^\өX2e -LyY&SJ27.H+*1; ܏7JIAѷ~3lGy'=O;kd $JĻ쓝 %f -K@) IcUR#O|\);i(d= pm\ ?5Sy[@_R铕:AbR -۩D֢vV)rmjhg%dKJ *ھ{ @3RTThyYi(/H wg}Ma餔9ZcF^槕R$]?~RM~d2}23RD{S+q.4, _k#e;l˚HX~?+'Tr}0}\`JIP%ftW3"$LD $jlN9~O"2{U7!TQ.AsS%ftŝ -% opV 􁧔RnǙ3T6(ja?!%QO\m&@*(m;Uaq(e |qC?VEuN?,Fc.>rs3LcaH*tԥa ^{2mέ L͕+/ɀՆLSftq_o'ϻ+J ekO>ד>^~oGy2wg |2H%dVy[kzhr)~_f;wՇ,:J -X\H)iN/wp'*>'0+O6d݂5sP"H$jIcR.fC3˱Lh`z֢݇TJV)AWed~/\qHRepU?}m F+`y C_ycc.#1r[2iN}5ՌeszِŃLTr8Fk?#d kn'@R[<04.o)|rS2FOvqs[[ .ᓣ器+5rRJ$ApRH(uВ_MYro_bK;z'G&:DGz`F)iNPm?!R\KJ0}8ߙQJ\d%sSҕ*I>92k`͔8p^|{\!y"?jR1JYgsy5TRS"繝zi<\H|rtBբw].;7E,'I-?^?d AYJ)3إ;Hm[O(#B)vn-(eHbi@t9o (e<^/ﰭ)xGE.߅]/Q U[>>֛ -9qD`dIyPJR%)?cY> ePЅh5n 2$򞬯*`j?(9_dXcyf=7jO@*d7HOv!kRd9n-ҷG^/Fxs:91@~6mwQ$03 =-)AJy%kY~ӺOŘ^6$* N lsUU8p /#_ 'Q~PJK'9v:>үuNj#<2rǷH#-Y~65 ϺF,!?<A$~R۹t#̆\.hǔOӌ -Z֛HRzbۙa[]{Tх$5myS:~ I)3 jRg<$'IK4@Cxg9մ9 |=Y9~?$[PbXh IuŨkZbJKj-5S$OyaadƦPT+j_ȏߋ^4w*q^<}yy]=ܾ/pDΈḙRRkA){)&3rϛN?O'$HFW#ZRfG?-XĒps(eƯ&[ZjBe{#~FF/aX?d;3J igQ~h$8ZR܆z Nn{RC3?>i'~A׆~-o Hy}ٜt6Ccwg=730nyOEd).{7?*:20>쟕'JFZ[SH3I+q~w؛{4r@"= zb-<r{ojڪ(,E)P nդřIJFJkH(-Yj!h"~0qDT|ӞkJ=U -lÆHFO=Ac7RNk%7F֕FWE%Ώy!EL)Cwa)K>˓&e= Q 2@(!$xPCgۏ)v؄4& ~!vپtR3XY0vэQ'gv4 Ő<%{kJ|H; Α{F03ΈԾEiM -hT`HN90/,9cka َqvЅEqH#uۀ,X;: R>R嬢p?|,c ד02󖢨T ?/: IF{rgkazX,E^-)Ja"ŜHF}):^W{)zZ\i|wmL -ifɍIp q!,iT*X6},I[G"c59G6@BOvV E#)qeȿb;$[/p'g"]8WvJ:HJ;5)z֧b3C"Ͽb[S -ӭ?2g+XiT\$$lR_0(LʠRu_Nt)Hȓw8Qw0uL ӾEu=x43ȋ8Dq #9䘱3˵_ʷl ,qe}>l)+K  -JEFYxGðZڼp6//g}C՘(Y0vf8'ILp<B*ZQYK×{A "HILeOw8?^:'Ӿ|EuPG;'Iq;6'QdvTɝ9~dmֿ,,&.I#:YvRRiJQ@1)1ɹYIJ GQ}SC6`ȋ8//ۚ2Gɍ # l0BUq2,qܐ?u&<N";^RHyDR&MTs"巆aΚ}({u12< ߟh1Oz98L - ՘ X>egQQLeNVH Po$dzHa#f#YdEB- ߛCC*ƈi/ ,S;3KuiQFqo(u/ '%h԰՗ۛ()F+eȿb;3 9tl`ܚo*KERXv~KRB^((Cօ]ey R1^lfj_RШ 0/,9ckȵ^|~@,S,DXF@A ʒ7,MH2©VL59^{-iiǮbEB%۞fhHb{r؞DVx,1e79L,]g%_' bO2O"<,}u}<8_"ߗiE޿VO:ug몴S2ރ(+/+=m(x&P=K쳔| TqtDܹZ)Uw3ĤLIkAQbd-!*a^\0ٯ64Πg眝G[1vplK*ef]#!گ -F|Oo6riwȐhsv{ɓN-߳e9,1 Kp5$Lh@֤l ?ehZ)_$񗃺Z'ъ3T`0醴*,޿6툢,1rj]dQnpW?R g=r`0E$+HĞ0og NߵACFؙc}4sȏ;{l[D] -7DH;~аLf -Sf 6D~^#eAqnuMx!ruA<(`W8[eWha dڂƤ=uN2,.ۙ@JH3s@_n#HŐ8*kZd:B0("(.3Fp@*))߃eނe5I K0A)^)fgk|1LC0R3Kl[A9,1^_e4YZi^I7QL)'e+c4"Lwƥe*YT$(77׏ "%z{ 끚KPAI%!&H9j R*wzR*E};S΢Hy97D/oN8Xx˒MOlkx76)O ָ/wҦr.8=)ʅ))=_ j(DD-I N+5qT{{xRֈ@M~e/҃*)OJX_raJ,ϼA>0e@9|ϖ%# `paXa6`N=x?3z6|IHiH -}!ORԤ{6XrK H~P.A^ -㨨%Dx`U@4nrEʙrh߳஻ Re0; F -sr KU+m)7{biƬw"X,wrI 3 ak')jB= D;`)T (?et@T +Hhe/ 斗5~,(YΡoOrkW8:<}g7GQ_ކuC4,AtI0RH)úlgŅ\DWXAIIQL%A8>2IXbe)^0"3iC4f -<&)j#H9`za>(jrٰ,*QjL6t4.~XDʷYѓf(*RJ6N(Er>pCC_k٪AX⼙l0lzUh"0}\@vZhz@|?M&oyH9`V_?2WȆig HiQt ]5^#Ð$=ا 3~ͧ)RCx;< >rw ^K}~F;S-ϰĆFљ`oLJ²)j){^(̐ RRԤ{̴ `>aVWUكqT,[Uo 8/(9)ZykUjzOI֢sJFQn "ay#_? "t94_s]0R4R"BnqD%H=NJ%;dʩ|@yUМGr~#R23D_G\*ᠨD9"j 3i>ib8 qRͲ&kaҡ8m3ug=r`vu&r_}X<o8 pΠHY [-SE][5 -Rv! 6&uW,3t9Fw*ʃ{ٰuK8C0p%<'[i>vw& -s.}93e(;=aÇ.4s@_5 ``V -Y\e0I:T'%ybH͌HٽTۄ<BHD{(jJTPR2ϓ†eF7TKp8I9?ɌO3LL9Г9zi#8οvwIxΜːV6j+5UWizjWEj6UwەY !!`\CUO"c}Z. !m`n1$ߙ, ig`~W3g,j.,Xh#&HE׽^,eD8٤>fugy5sR宺_y(iMF2lnL^UKa+;*[.2cڙ%j>TyYI SKcJg)exJ_7HJ +sZG~58cL2a~ɁeRZXa PҖՄ _"!1aRDgT.c 5!`f#Mt'$>0r`9-E* 9 M;6НH')ZL>oVs* -MȺ6v1zDR>Փ.1|(aKvX.(Xfj"C>1L@d"'Fփ(W+ -J|=jR ? ~cU>H[09Dڄ°fX·82ӫ蒋1vJw[I{-NKnp6D`6KNsKv9g{,Ťu -N8W5@/32WP-;E/jRF- ,RFŃ/Zmҙ _lox `cGvȹ!#M4.cg)p31R'c&SA_ Z>&)Oü<<^HJǓ/3+a^<@߲Q |MwuR߹@7`X˅n(i0")K=S'Zs떚po!)1LWtxC 0V-T-vseGPq)]Z@z&3_x3Rj3yYO‰߽H7`+Ii -Mn-MHx .b[k`&]Oz5^B뿓1̳Tu1̻Ik*i!%)/a3Sw@w!f6rݗy.(JlI-e1$O7LpEsߣ?8I^3 g`A)ڭ•T#ud3"0LKWvӓcL50m fHwq`H)[?f l&}x?gr97 Ai3giۏSwOAWUFu2dmb$xj55LS 3R XoT .1v9&H#l1 {*mD:Wn[3Pk.#eI^{?/w%kI[[f(@r8\ td`%}sO:*+++ŹI~ڞJ <<\pZas``]j/OBlQ+jgc~mc?Go;cֿI5F2Ɨ+VRDm=7M -^jRV͉ao6pj#MNb(ޟ ,sT;T\K=('HJ!!8JngDoi rǘ_u* > r;U:;Pg^\Kô'V>ܨ~{{-Lu0lm JDr!pOw -{DJУj1 o - 娟C_+gO'Z%;0$l$SㄘQJ6)N5@A˹ߐwi^`r9._d 24-g"/=pn6 c:) {7lC+?WYz7@W*CUFUu <@#K@>#sDYR}t*xB;0fzH9@[o'#FM|*7ѩ z.njWMJX:|cKjg̓DJ8;|h9JoSBb)X^K=0j{6p;{mo%Ga kki!5q! ;>KI)s$#iM^# ?Z1̳ſt_X,W"b&)T=ej뱺0j ̀Nj'%R[ŏKJ}aVHZ㗙vJ;=aVe)2}.Ul -΅s#%a-ƲJdeJY>UJYIɁIiVaӽK 0;oU0m)Uc6VcΟ8W4վZ`(LiKmɴIai0iPsؒm|M(%@6`1i2+r8@6͸ϻ+ɒ6vw֫]iw0aDJ*W3e09StM+}E[%djj+bilLY=<&>!Ϭ*cF%>X7ɃaXrj]YORu6fat -`鰼!o@U-@ʁLFV{[De%:BgO<><9P>=aҦ)"0lZw6{`U -+ԗ֒oxػ*b R*ŰJId>a0 Q00`RYç=0l'jY"o߃*j2Mhm`)Z !)؍d~q3$ɟ#ؘqtO6  þ3$Aw]`q"6AQ7Soq%jo~<˚e%ɇHiTr|n=@K(aX4ݻ6iI Y'vaؔ*Fp.c+mnG֊j%˂zWߛi"o338n='/;@JI̜0R e-?iI#0,M 4GheEfh$9Z|%|Z#zihǛ= z/ptDLq9iY!Z9s-誴ZZ!/ů Ib(\fOq=m~0 ıJB7E3YW4-V޺,ݿ z?pd=jo^O&wݭjQ]ptysYГ< ΢EٓqR{d OQ zY2 e>8wI2m$G#i lZ| -bFoUpvhRJ_ß<ǕmNyjȸ+M5*+rgC<=bb?&&ޔ,/ iķQ]3d̓kxa0J1+Ŋ:A]՟&jK@]+ |mm>9s3^,_(yYHĨt5}ؐD -+e]iEOyXfA0Ko0,}jݳnҐ2 9ُ ace(Սbt#h)EZ|tQ-_zh|w۰zsrIjZ|]GcW(;Gq(>f13ƄdΓpa5IJ$*/ &ia'mI aXGK1h^b,]~ּYx8(6T.f\ m:X-=FPbZ4>ѓI({ndaبIb0$- Ű za 4ԍNc̈ۦS˗Kõ? }ЮsQ14z4]rO%pZ ĸlPbИ)7'$Vkf=94tb=x-/#n ȟ#YaNMJ4n#݊q\Kj 7C{9'n6\:Kwe'cυu1&)#z=i/ZN)t?|0 Linw )Z^u 8`ؔ*FG8 ( ]ֈqu%ڌgi Zx1\jxC{|7IDb^ϹC#JI J|蘄FA\/%L`cRIEZ8P>;Ma.;nI g"&1,mb:4:.11hLF(9juW. Zط*pd};]9_t|\<\:`9]&a46q=ԞV{ɡ":HJ Ib0\&p.e} ıs H4R74mk_׹|_|9w~\Y.Уբlp$iFaS\%2mg;wתk8wkQaDQgT ->BxFgydI=i{?~>IEؠud~iZ#L˄>IR@y-= rVrzl/fa1+xr[/|/PHݗEo8x45OD??чD(<<|4&cEGNk(j|٦8)$(ta}i\TŨQb"auTIFA) x;T xl_q_[yN꾄bo[N(`^a!q8ч-nwkQ?ߑFF򤮘LKI<$,/rV|Ơ(j86gA'!ٮ0W% ܞhKŕxr|f\AO*s]oDZrPVԸ莡KɍyraO2L wkI%~bUYg͟c~?}5$ ?+e?@%*Eaq41sCC*-\V|kW? fwJ|mFZv(`(Gt#p("1&0 `R=cSb{¡(jxȴ|,F[֯uUٴ] p:?en\ʄ!W _soR&uWb-qY2"Flo.;XPBԋG &W:GE'%pU8_euјF8,c$oô݀hgbqrts[/)p9;Ǥ*V+ -#*T4{U1^\F@Ejݨ?xU<6|Ѩ'gЏ8+=JulY+IP]* Y4Ba]l FbُC U/}G9RK}Yې1btrЈ9]MPDT1Ӫ>rziE-@Rt:%ȐS}l2GňhdЍU=ǔe[B,t5{uo}w.J=WŁ&a DbD+@c܍oIb'YI -Orx_GȓR, %.4>"Jc,mZ -Ew~=|ts||f|~7>򵞖ګˢ[lFF0Уq0w87OjEL <^*)a2FuⰊ_ua(Ƹ3-* r1?%b7Lb2&;ʑ \Prտ~Xk)/z0$ Fc!DD^6w9t$(RyRK`I6Or}"OR+T`E`zЪ@X\7XG##uEqf(%o O$|Q1^KsFi[끐Wցح~xA4)"O03Ƀ<X<^8,>bU=R86gy>Lj 暧_؅dRܘnۀhteOn}sOY[_{uI)?SWsfHa̭rѭޣ4%&H%gIH`I*ODIqZIS.EQΟc>&ŃfЪ,Ŕs)gShZcV[0ä.!W -^iFrLj.ub0 -2FC1=tGHȓ'SSg 33GL0>v9RZ)9H̳̚zhPhԔXS[`/gSk4N:S1T,uTKݗEOD̍{~0!vƚgE'!3 3|3 }R|?]S…f@>)2HBn.Y%"V"yЍX.}3\%\5T#x+{ B:p0%n8=XO@ąHh^ȓXbR}qxVaS-9VDZgä:.U tAt1b7#aܤݬ+tP{r.5{u -eRG'ychcg\R8E%C'- +&Lr{1Sb$E8oS\>N)BBU~PJ4h[WuhҤp~iRPB;qqbqǹlv9>vHy$&  y?z~>me)+qe~j$RZFJѤSkL=x@)v*bhbbLCI@W+6"~رN6;\ -\8ꁋ3!OSk?'険QI}VBa&5{R<)mM{F-E)[JO - D!H _E*߮h$l4.4/Y*ɥpEKXQX%QLȎ'/~7"m(. -V4 -^~q|zh=ԊtT|bzR'7RJk}>z&[`߾]ѽZ9aExS*)&|/?SQi]=µI45 O$E@cfg`{G=7fJf.VFai%@B͔O(1f]7NŮňnİ5=0)}OJl3 MIaf+1){rf{y nV B1/ױ㿛=ٿP1X"G#]B} 1<VLdP\YM VM̓k)hߪ6ʓa#pn2L -oiГL&chbP\bf»ӈ?b|eoϴTDZFaP0h'ꑹ$Iؓ)bİM_s۶mRxR"0 Up0PYՉ&C© R~wh\JJfa&YV,ͣ14%'ErX9qx[英 {glHu:{64|mN>*F>hhYa*JIكIvGRiaC(7oieR=~̭F32icInTIFYӉڭTGvĎvkцweÑΏC&aN;0(n>ab~ wht24.w#T -=j&Zy 7*ѓ.Vޣf3.W$dɏR~,׈"*&=?BD?o gދtԅ]gB?σ YE[ҟdJK hD7bJjF2L.)~bL6e=pl Ё'C{򓥚T&Yn(Z65?Mz.~wΞ=1:=BsK Ђ!(.\Tbf(F ,zR$ I)ѓh'1\$yzTݎ'j_"j"pt! ~;U(qb2@/Qh%Q=TIYI=3F"a>`$Rf*ɛU[F;sf]z봽n*x[c=d8}؀0'=]Qa:S' -!%Ub#$FOI P0E)yٚ0O -wՀǕ/}e_t8C7#F Vj휜@O$`$ݪ6ГEC^ݩ5-SrcRZ7dSI4`x]}Hi#oV)#1II1^mXX[vRF>C8vXp3AmeO;j vx%0l}Oj -uI >hߪ6'1%rbQuwkV("͑Lpɦ 09nt:),G:ݶqɾ+^;RCðuٺQ~~IZ|_ 1j[H!F^P6.u֬lRKֵ>12m{Ntuϱ%qr#ݝ=T'%X-Ap|{>"nyIꪢP(IQ&u:K`5ׄy)goX#!, n4O3FJaF؀1*|L`lq7v !GolN/[Xƈ[v#c}-) -eYJ'P2)v~MMsc5 sI,b>De?(lH\v8oq42֡.pW$VJޚ˼[K].pˉN.-Jp:.}Q(T}ZT -%쓂Ї5c*%R FF3I"LjC悈@2%%zZS-Y$+㕜az[Eǔ6v'"ީ-CBY{J'5*P2IYϚ[jRw7.]$g+#Y?1 LҀۦ_ݹd+F.;EVI&fE.c}|*`JR$࿌ty&"ɘ:)GPֆRI_a}RC{A\#PhRۜBg -_33̭f"&l_H&MwBhJo{^+HOPؕ>N\Q薶cu}? PP6'Eut Q*UBYP("lă|R1n41')wobC*UmȎ-CrS -)8[\ب+&|l  M!hO'qK]jH*P(I:g:FFTj~mpHR%z掯}h/̚쓒;du|R;kgz+eikwL,p/s d2pcn6klx=J-Lcc3MP@t2$yR({d=!5*B^UUQYRR1mٽ~G^<{̟|mgY,^AffQ5*yS(_FEtn%(&Lv!l2EgUq`j(\Iۢkbٞ.d- l:hr'"ީe%A'Eu $qHryEeٖe堔ۿ\g=z虧BˉglhKӖco*` /F=nW]9szv) *z 7A#L2۠Q_sL&+Kqo[_ّўNH:R/zt>vt%B5Bi,|PGRV(UUe[*|`cowO - r ADPoK8 I(φRedЭ̤yR rŗ^.F&k6|nq#R&ȨF~Q*|LA XX8o]Uet-S~|pkC2lsMǥ/P -(:F4BU] ƀF* ޯ?xgק;p} -8ǃ@B=d9a®36&w֬>Iٸd5Ks̭fuMm!X4>!pprl"&C6l|!:>?[tKNuOT6:랈xh$&b7)BG٣F#Q]UrR Vco{/'^=fwIm( b!S[ωWsn]*JhOf*Rr7oi/p3!!܀B -$$.VH+jsݴ$!^ ,4cO$>Ӻ)Hۍ$mzb+ϙuv$c;Gq{k_w9{*ֹsm6.e۲F6{1N&CxLjƬ1R+Dp GD _fS0%A,O ᓐɎ';N8ucǎ=zȑc>j~ ׿~zgu/5HHbih{hRY#caÝJ@&=頺(;\e(eil3DLUC#7|YgS>Ҋҵ>m![5UUu 녈 -,d"0 0&y9`-Ǽ!ˆ&Rp{GYNԯ3|䓟z)|{?zo]u:Oކ.!샓q1-Fuo|:q>ol3t 7\Ds$%+]UX%*̾zs'8NFWR=np\ZJ -PsmA?!3Ҙ~3C!۵$$ŸG'vr{_k (%& {v9 H0r?aL1ƬQ"65OmE8!En?0H&wK赑Fw?{FP?G"'3thq4RH@%< %mZp<aUPQVַ}1'fsLq\Qq0sܻ 44R\ر4Yu8d쳩Р\aFwSl*]#$ggq4< ?7 -uwcݽ\wқxZ|J^:/A0I8+0IfPC H#}1̵k\,#}9F*&TI[iwQ^?6@W[ɤ"U8_jhCoE$.ؓ=ǾʇAͦBŨWBaRPՉzxp>Z<Ơ"ki2YKQ"|Jٶ|9\2|Uτ›Wᒱ<2i߹`oh>FR=Qīy9 YW -pp*`z<&9Lj-}dr0\C d&)d"lc݄k^^sJ.YX#J l%DXיcu}mf9$~VTU52i\+FP;eF^9raKXe[hbO7.KG1ʂfzzDp4cA3 -M Zx2%d?3<]vK r>ak3Xrf%êG?$%r!!/J VEN\6c+PQHչ?H^@-yL%`2/i "ZDWJfS\r 96ܑ3Í`!0(qت2nRCRc%?ﻥnʹfضT^g3i~#l"RJnS'Tpa$ ,cۈb.WuxRi5!~5/M- -(,⠨8 dk{Wp^Uk}eiH6H,G:g"Ckg[9n9<\Ӆ^B,M$$Y}Э&l'DmP-XgR6ǰǪ0IC#_k?rvw؋;ZGmFRs1USG]XΦP1.gu%JCsh!nIr1vI biẆ=1z%ç{;WMdwE{O&!ﺤn & Q{ ᐤT 'E[o؍aV+UZR"" i6 NHJJp`sN>b'Ƙd}0vubh;{#UbgVV$ò?닳[9gK2ެRIҰG}d%^awXgڌT.yNG!b',*!㉓AR ,eY_dl5:*Yq&:c4/][J5[[H6Bp%^2c,I]uIM_B q񋠎6R~p' 8M/:xEnߝ<H2\RW2aFanfj lC|bR^4]^-hi^} ^W0$ |M#ߘ -s' w a/f8 -?|"tABeQF#m4^D8oMӋ 8A #1 ab/3.,rj}>Q6Y6qx fgan`˽2%w`лc}q<#I[[HښTqhN%y-'(v s0)EXVP<o9H"3`.'yoS)-lkT5+QL;(޸OڲFX6j&Xk,k`K IX3Q'd,_H M:ܽ=y|M÷'˪hH -"Щ$KyǒP=ԖeSլ{j{R2L|}qRO~V -XF6UMNJ$Iӭ-‹Yθo=^?=$^z›OnHAO',yr{Ԑw#2lE3R2dw'YC6JcL VNLD[ޠbQI.kL c$IqSn0B_ѓKK(Ey@vsLxf$uH/k)zw|f6sZ𲚎7)ɳg}u>QL#tu^F% wV[y;턩!<dsW !#1kA V( pT2Źp'] F͙., jMjng;- -/`n3vYZTu+jgwya6WSbF fHJ Z|_2 FE8`nT-e1> -S%̻Յd͊3*eϖeSclj'-U]VcEscqpZx<<%]9y1CM'$ez0OOmmà?L#cwπ*!Z2.*aUұFVf,y W\nVbcin{Eظ+[PW;%{ʥ*2tM491bB$v Ꭾ+5d!"0EXh(kfCrZD8#K`F&t%6E}6d:R2;UƴR}[U닳em'/{)+SI6aWgIhR fqy7'-]_$;aݴ ;2C<-rA ,RѐlDZOM# G3[fk lj{E- '<0 <7] ,?fziWSbCP_H$ rJC^^{9uw)IR8NF8֬DJT3ļZ(jT;5ڲWXVIv  c.IZ- -H1WI+=7 +"dGoO8($y4`0c?Y^&jdђFgeT6jV܅e2l Nfbk ɭ`P%kg]>/qWIضh6) t MRR^'#$u``>lGpf1)h2 |r3*2#C([N^-`˽-p@9b$L)KZ^BsWz着ՔؕS EXM <{𭳷xc0 +0"ꌭSIںsgtЊ Ll,5liA$oZ&<ZIYxIV@tI=A||"8򝺥T !#1DgU*` ېZLF*pOK J,|ǚ_[HnfS;ypxWSkV%qVGs%ɾV/Hy[Dl'AO;i=:ACb0al#KUrkmasCjϖ8/r%78_U7ڶ3hՋJ[cje怱f7؎RloY{osƬc_(@8o}m \;3 0h]34_ +뻉9'+||Ig5ȓr6I`Ħd* ճ뽳_wO^w@`0 w?[H$H#`J&R2%xYVB^X%,z -&\eb׀&`ͿX_Ƙo#b\CގJN'T 8Rҹjq0RnE\.$۳ZV.x%גߠ+ڶ젫To.[~Tg I+|kKpBL^(wѮ:A%TϏ{zM.L´Ur4d f`B)1pH 0e [0͵B*^DWfkIzt&]od^ʬQoL0YA h -X%_z7HJC }H{_|raf8! f0#~-5ogj˰2X9˹R6*%%d+z>ԼTtR Q53 9ҜAL[J_wݧr UvUUv鯺g$O;`0gv.,=Z Zv`Y2t$"–p&[DWf ϟR -.-mSc9sNRD nx[<@Fx^@=֒.k0RN'7?%nҋhzG0 fC)mpH6euA'-LZ>R0[H9ğ-$7z*vlsW1[(Z wZPa/y@OGoH8#OʷF#dCb˧.`0 YӼZ*wgCHm.߲e%lObz}ް^HçL|yMJMR*y3I0Rs۔#~b~!S+]Wj"#1-Hk A j6VIGZjQe!ؒlf7uaK`Y/$KJ&|K.%#X_`2d}0W}i}`&]W8rard?{~} `0:ҬXNGVf@S!!m[CJ& -n \r^¾SxdVZh^ _}H<)*$89C?## 'gwϛx.d$\/⇔۲k$$2{}9@˹R6M%SKaK fd5ՐUԥՈp5_N -#mq<  p`-$[7vL0`07XgAga U'HQA*JGG$RǼo@tǁ>y%F?$i;/Q!}B}fH [(MR=jAy}u.)T,,i-Us9_uፐێQ~FUݖs:@mudW#F^Ģ\ l+W`Qg I y (!ܔeZ 2 _(OA}G~mqܧC?z8#sdϫ|Ey~2! 3@R_,#0XWfr:Pm}[⸞O"3bCHKnƵ鼃-< <[tx~O!ECb0P4Eq:\FLAvu. Bey>kx={ɧ^ lc}yGMz\H PJ,=RƸtfPnidb0Tm쀴6({P.:HqNzl8lyh8@l۠#t§ |u!^1B6,7 6*t`%TUsqqsZ#;*9P$IzNp㡏N(L(ޡ gGoEFb0̇3PYf>[e"fQp}d"t`>q=#?Fnp㡟'py`]vOX koVqI( &&u ^K0@l b^۹uhc;׶4J+lC$Mz5Z'- Ǒ EQW56T@R]v*9lDʏd~VpK%U|nRlrg^2uL#)'(BNrv04gƱl %JL[[,)+n,E;B(/pc긿cl}mlss,1j|Iu)^75iN2$EQu~qΣTr۳`rtKg)1~ab%*.j`>Rkթ0Xr lWb=9Bt}l㞎 ȐBE8FÕD&&V.׵S;#>4_ EQԅ9|$= @聭 -"rͼs1dE3bD v*n5!̳?:5 ݻ$v{i,=F2’+d6N >nzw+v=WqhJo꣎i[H(Ҏ7H wI ASj.S1N7`M7TsbJ_ƕ6# -!Ü.Vimt«~P"Sg]C(]*yg?'Ckr晷,eB%e_ԩii@UʴY &X.n,/l0PP0Ca+.;zgu-,:b߄({gE؞((Tv&tMߑ̓~d;8A#D:GRUbpn5ەv긜RapI0&|hs}G-*"zƝ= JqnL^An5ԏ((TҷgW.&\[,/߄btb1>y@g-65ki8<DT =%; b h\1떹R+bkYJO)=*:REQEQ,Ym=z ms&< /.\2”۱>mJʱ:򆣇;:\ns3>4nһVL%cS3 -EQEQuFLXBʊ0&;|UnL2|TOH*1RbiQEQEQ0GOvcu)((9C7y(JREQEQE` endstream endobj 11 0 obj [/ICCBased 19 0 R] endobj 18 0 obj <>/Filter/FlateDecode/Height 947/Intent/RelativeColorimetric/Length 20505/Name/X/Subtype/Image/Type/XObject/Width 880>>stream -H pT/!&_aBA+ʄbJZDhjhb"SlSF[qvBP -P)-7Bd I&;=&_{{|!;Lsw9W=!ግPڇ#=^v^fV$Ax 6G<=\ k˰n3,- gGX+)G`{[-6t 8W{17# 8nz GJyH&AwB tTV \D06ns'Ab G.⬊FI8 dw AZ-hV7%\( "~,z2YlKA bYqM V&u8minO1"`y;A^[7`:58vԆw^Z"VPQ $A.c'nnJm4RD,8>F75RI8; pћ"AрMV7Q#(Ag^PD ,=A$ۂXtSADXtSZM#%A} '&3>A$ u]NgO Z,;ȘuS+\$Aǥ|Z⚠nƩnc; 6,n>DACƔaw 0CKDSDr * FVFw w? GM!Unٕ#A݊a2, CŮ~ :vW74g58hr׹Z܉&j,v&pu$A\ t_7%܆4R8g ?#n#N{1L9# KM?)Txԯm4Q eS/1uC&n>v3>'$x i} RBsNOuSnk GH1>pRfDݦJuS­LOEne9< -]J{X/-0GOKK;ֺW̷Baפ =R2XG8ciLaԾG\},Rԗ)2NZʄs$\Ed\1f9t]RE?%H7%$\QO>KUsj Á]Sb}X & q kR"D-JS2sO}Uj9(%INtSsR"=Hla|9.X@S0 ~}}?X;8bcG[ʇ +|f|k|njMV Ϙ0"# F‘~$K֦F`__C KsM]f[:pfMeVtmdkP!(2K g\^t4饝T7HAھHL%}eu]EȨ;\߸`@7s%\X:]{x~'a,Z=lcx⼑\†n Nd{pnqY8ҕ2rr(Km{i]ذ xx;?GuGƼ#Mo' _qScm5k?bަTpt57mbtSLg ("dw2{J0ۅ}.6DŽ'<20äFFD@pa=x]aO) 've_ pPY+EЦI7H8cdo x".@״lᛇUrB7mDy=D yrEcU'tf<'_21uMq!~pz7Gqc:{loP%ZH:kծJp_ky6\ME}ߍrAp3j Nk˃iqV(8dݔp$\Bk9~{6& %dngZ8A(S7WSdZg AZ=$7`&&X护M5*~Z${S:ƎVBLJ`Y+TH$\gR|%1닶43hXz4J w @(_R `_ g,]ƜrlY.8.lg;jȾX8w޸`oO:ݔpZDE-#*5D^A;LI56(VX 2"sAVãTl1qVx`L2% 4MTદpuwVUn2"`Cpٯ+ sAGETdTQXRP#m5)hՖg}Ԉ $!)f,Q*>ZEB ַG0ujEq@{usIg0ufs_7ZY_z_W9.Or?KSy/?X?'[Ư -q'+l魌~_$ۘ\zFܠ=F_.LkQG+Y I1ӠMå;o]syk.Z5׽FĖ .?Ǐ}Q$^k0p2E~L4Z ^b7k'7-Xx]#]Z\)߽+k"IJj~ނU7:>"p3{+P֬&Y}4cI,ҝ~eݣV_y{&!DqK;]F>pqO׳UQ+fRn r[mEeZv7a񺷛p^k͖wpR{YP^:y-[W!=AvZk^w7nquEٍ+y*+E/lͼn=XN|qڏۏ}kO*lXZu^{Nj7hk k!9n>JTO2A: 8A ߆xm8 Wgqys޾;[Y5kmx] ~G7K~ t"\ZjU\^y[sΜ ~lm}r~̜~=oT*ZKc2 J -에T{aku)\`f+Qg>q,^yײjv}5}_CC9?׍5py¸<{{n9ZFp699%D@7˚qDuX7MA -0PGDrz-oD]][,Sghۃ,o)ZOXSK[j_Md,o) =doٽ]w:I߭Vym: NQ)#5Pya:1Mda -0D3ܩIF) rv7u2Vysr5NZ_kWG,7.W1n*I'cT/#^ "DA7Yy#H^GN(JPysRn@ͽZWm$Bd8bnPy!X,o*%`b ͵vaDm9*k`U74\U䷌*G`R 7{~oỲIFAJYAj'c*ay#Pvs$^e#'$;#%#7Btu /]PY(n6 }j2%5JoN3o*(SQZ@{`%(n~̼*8_$75vQ89m}3e>F7 ȼ2v3NzeVoNƔ7?NFbW6]l' ׍H)Jt!N7i_o}2M\FtIXV:}w7Q{ER*2(D8d,#YV0 'n u vqﰼ8&2M7ew"ER"IuSQ{9'Jt շ틉@;7•#%,o,Hm7as^N~FNzp┷H~2(k'ÔH5 N;Ɉ&ˊD,o,0[$'o$-#4i'c`ck9-#4O N27BVֹ0qs!NokHv2Q{p.$Zb*7'?`y#`y -Nyʼ0 Nz$ 7k'{7uv!D P´vYsv.Jy3wh9#5P%(-+y#d^oɡ7B"Cd\¼2k'qIYiT!J}q#d!7'a/:$nVަ4A7'FٯX2>c:$Ok -a "TZKX3@-DL~sCBؚJRa#jdSaSB=Pj~>?7}uݼH:ӕ8Ps%!kQ}լ'0@5s7y1zޚ: ym184hR3 ̷݃]0@5O u |d TsZB tS%dN=ycՌyh :z˨oy:I9jJym TFM~V /ɡ@N#bnױMJo St>Ղ9G|hFqK>IEou_yvX' ]0@7[Trߦc44 Is(C= T79-fo1rq},*Z Я ଔIۤ t|b- A|i{o畤79ϑ(rq@3;z(e7M>)Y'QynAy: kN`rb$z - 6 ؿɨEqWMz -AYtQNÎz:wEny gI/#Mɯ7Hq|qAJn$ʅ "o47e !en&3_$Cr[Zd3 RQVtJş y8no ~IئɋݧA"<UnƘ8MsљGm gSD;[߳8s GUd'.z$H>#+?G&/j|_KbyBg*y3]p8r T6mܭϝ8st9 skw?"cb4G1k[[[i-rgClPN^P}ŝ A-)Kt[yS2Dl;/#7\QW{!cA2fSߠ*'<A)QB^AOʓj{_l*9u3Z0694zKɈ=kԦkgX5œC/Duj+du#j{\mT W5,\BBfTuնr5Uˮ&,ZcjnEqD5򑩕\F>>qPS,*@n[1⠆+Uyk8*;傥's$d#RS.[T 9Oᅡ 56܈iOEqin5n:9 :b .lq6rX*! 75F0h3X*!/eu89~2*#6}ن~agoqԼ 8LޛUF;2ʼX>Y0R\£*C# ~}1@)kȄARl)U8t8ڏ(oEi`9sH ay|AS#3'yuwѼRR8^5xo.}?Y9GH }C/>?f69^ȣ5HWPNZ,2χP7 Uk]R'*_U|>{r Ns7RUVT J1Em(-'P6Κ#7%6FO%ɏrsλre֐e}^ݿŵQ,rm[%Hogqn 9U$Mw X~LI&M{.@;*BR"J _-e Ax!mX렯{.z?Xo˛t>g\q WIge9frBnI  Drpx+p77A,w41ތ Kc-6{3 dr+nY1@bUJ?ފYf9M2Vj_}*oR[vz[ `9N}n2}ox,7Ie;K{nR=θJBd^`rΫ"8<v{u.0me`BkX!8}x H3Cn  wzop e#8K!7*K%\X~@[AH}xe 8scs nyv6Pu fJ}WZ;I R@?G?| WJPѡ?z`A,oEoEps8zWhPyEFTj#wŕ!|o){n9@?O%ߙiͷr -pNUo1cpMCp?!;24 J9o[o 8)ur> 8ЩʏG7$8\)An^F=3u|lRbz=,p%s[ p}h{3t~7P$翛h{Ki1rM,oMbqEkoM/gi\ko e+1@[fha7Dݑ&5IPE8sְ x91JhiXGQ;AM.Wo ˹x7[J2byCs%Tz?X^]_417CcN9>$%C3W)B9fbo xD՛+N`R9`LυRd7P4w: J AIz&8CN7PJzMo =@+٥xz3t&PuuB0@-'-gi$.1@-'F˥X\JS7)JU54xoTWʢ-11@/ mn*G2 r斆L^pk鳄%s-2Ny(%-PR;: 7tHr]Ku'羷=Io*Z[L.;0@19Ul[B\xèЁK7P󰼷}DeP&-gh1fsK h e)L_o1@7#d5ԕw4}`~Yq?3;33n(Mj6J1zAŠ5P\`$1i0FBQ"4 mѴTUD:ҨW -5|k҆@1J NPlgƶfҟMTo8NV>P'z  y(ɘos|l]k^.dRgsНӟl Ut*P9bf p\&Q߰c3K"^ʉZK%ԛGk^T "zo>8nT2}Ǡ<7,$پ|Ypɸ͠RƛppgH}{%Uo9y/YkZ9T^gXaYqdQ+5e>(HOnorT,8CYR^LHꅽkzϦP: I0Mȏ\552\)uIheCkW5ToL2.fp=@3 '&r!M)4fQE655[\-G\Ћ{ZѪW -0N-8$_])yts\YOlӉX4bBoޅ tPp ˛…\sZ#N˚5[6iHom^=vh\gSr#7Г\`|tPߗQ߄VMc=-4$aykh؟uP څu4%ݔ|`Yͷě=J=>KzwR1j5L҄~[V\rg ;pmU -tR͡Ƴjlor7|HR̦z 7#ۍ@1J"9!wc,w{{j6PjޛGkNcQ[SԪloOu'70jb.7}{s~L4NZ)͡1IB>5[˺(Ǒ@sB.|m{si٫X'_yoDwb+H>Ų-9 -2/jyi$!C|.M{{ , }{si 7-"zt+XC|>mo.5:h.E<ձ7օj!'3x9܃ Grxu8{r}Ft#7G닩t<u!%ߑi,eAT.¥=_Kn$Hհ7J9#l>״7>2CIǣ_!q \>d\ͣx|ojV QuG1]{NU:3u]oy"7K7=i`bxSmIzhٛCu ` ]y$E򹛂}-I#B٭ш_o.M$XFoڝo݌|I06O5ͣ![$źiNϳ $X'Ro`FXGt!' #ycoN|HvK_~4w^62[o>8>ަ]oj˝^?NQȣ`Gp٩6kכKzIv^\* l$X4m^֬7Do`!ϔHͥI`%: V^9 @qi+X'Nt>7ݤUo.Eo`)՛GeMJx[ ެF>_1[uǿ A5lMeH:f` C.50ӦJij>d+ְrRqfP@&E~߷﹵\}=:sޯk'ΖZޑͦRR -X2q etӴ"ݓ -H9{I5+H7*Po|s -=z +N욾!yۙ7G~2o(1Ņ-ۅ U]Ϸ_tPZ;2"˻dPf^OZޡ%#UC3AW{2ZrC}آqN̼nY"9so۰A$B-Y(5Oe)D1o(7)FoLZMo(7w.Fo!詺ϷHF.aPr;[Ho(9?,Dov^>o) -qOƲJ~/(ȾUzC~R_9PvNɿXXμBkG ׬r -My9 -䝛W -꿖9-gPAwo7Tg["W*k3r-v&_HZ4&"Z{+Dʼ".38{;m`˲E*#aT~E2'pN:*+8;'傼}8'Q)^9CS|K7Tӭ%HF?d;P)N%rk*[fqtJd'Tw[,]XIo } .C?7 n>\PQ($ rNlkI߂d>$~g7TWP -ˇ6ͩߟB=$1(#ѴnҜtO*i=.71o'upL{y4]'|βhzĞG .Y=:$&KaLn٭0YpL9 pB,+kUpW](y"D?-qet@x<({aހt^erӳn2zށ{+[3А -(x@-Sz506{xgF?PP9"Q].Lpe۵g -ƣ .3ug[,< ӧ -V08]55刭4O镅Hj+ h x],ݥg0OXl\6 ˔AzK& Ɉ8(lf\"s8(Ƭs3 .3p@c^w? Pp|.<M8|DE88+'\"'=>}`E24tۧn{M9}dB}|? -PxqK~ h.][ '_Z` 8n$2yzZ`\u\$О5pe} ;]p|;I92'\,=qaT"YJo@>;b9q?prrEj^p@R7\8UNCL{ހV8XFhEpc߀<]w\"fY.D UNIȜW(681_⎂d}Z;[7\"(?&.u7Ʀk~o46X2Ercɸk_\,cW2p@E҂79[8]m U\"S_7}ܶ+RkkF,ڔNܺSǶ5q5舷py[Ge,$83VܫWgE-w%ЩwIZmfTzT1ӂki_Z6 -#Q˙AC?3 -"{QiL- s@7V&7;WǞq̓;Np@wgܚkM'.1孢k\\$=KlT\"6qP uFWc}KnL{^pjYV܋osd#XKȰ v@մ@ .8 +6qs-aJR_E!([>)ɰwȆAy9d 39p96J ^_ޤ7{}ݮNi еjz{ZyNM-[8K^׾jj&WAyIz{ӵSZ-)9(_QUw?啘 -$AQ#+X ->x4 "2h;NA* -% a)Pa HA) ;gY{w?;ݻ;{o4Hz%"ADo)E$8P"HH $9Lo8T98I"/!S9R{K[# #xVVX)Aȏ| NUl@p Po&vPfKo|& ]`M~.48qmX͡BQreI(6Lli-c%ob)IJ)70qDPQvm{uJ:7\* 6b 1X_5؁ L4X vb3{#$@D9Z}hp9L -8'%& {PnE$"4)m778&Lwnoepxo%?L,pn\㨷1Ln+5qtA7X]6%xPrɋ `(9gwre';ZvQ.Yt^T$ m7 -O!_k#5ݯ4cH $JIz j{.i&-y1%L.Vbp=ymAlzov>BQrEP̌"Eme^M?#K\ `4`z8YtЪ2:ʑn/qHw7&yU]T]5)ly=5HN\o'`9rsGn&=/l+洄nYMjJc!YC{쒀,J(`b__4߀vMdd`U,{7aZ~ҏ1r~En$7Ħ7ʯ<൦!x\oou~lQTlaboқ|La(L(pݳ2EFӑ-z]\ш`b;$B^4yV}["l{ e+Oã1 *-{#$@@Szj.l 6a䬮TΔa}ܫ)rQ i"eͿ?Ckĭ4z\*ʡۻgo`pSEo>gz>iJbӄzJ|1.ڛGS:qdT.D64b 6lκpy $Ey&ZI,OMUXjL6rw&ܖͻY{tL..]IyJ -sN/ߗs) K.|"T= Kz3H=7t}wjjC,"[_)ZzD%E_/bL!8Ӂ)R&x-wޏ/0rރn ʜT$2fRUWY.{j0y *674[*%gP[qlo.y &}XIFqcҡ53u tFZYE?W{>,Ɇ&q h!7!6Mh1/ԋE%&*z?M&yI]^ TUoeAP6g.$HlΉEy:i NF6{jwb[B?+fA4D{7bk}RLlN?TEgkaad \` Fr+b"ٶ!k=UŦ euɶ/@6UqM8`} ?Mcp̟@Qr3:-f:^$gvcVL.5`,;q(%Dr)g `yٔPZ fTY.3mqYH\[c)7-w'8W&?,Me"{7c Uqb*7(BT_|fUo)d"SVJ.0q!c~^6h D62t!Z4L $z3HE&Tp -Gmd aҲrJVʂ>l̘ n/Jj{Sư5B҇d[$g$;|gp\-AW-<>nT="8 ydt|C}aLÀkA܄3Q1.r?)xұ[V -h3 d t"=T͖ '[wFeK!) R6V -49{Yx} -m?zΛPQ;Fvw(97uUK6S7M^uJ.*cGׄ .$SF\ˌ_kD0;$c .gPBTs1h3NrMACD2<'jp!eOd(AA7cM(/Va*g_i)Wc֜c(k'ە(KԮzy$ 򏭢3C@$0?!vi! -%QSE@EXݒ?lVC]A Eإ -*hE`/ymiiݖ7ܙs?sWO.X}[2t6BmE`6NBnn l@) '?X>Y e ^#S?YB\ܑnƒskl_m^FPB-/+?faP!E2 .dup}R-Ԩg - Y1T.k'Ql o܅φoKll}aW%rɳ&$Rxؾb11ԁ'lO\)d9ҭ)>Mp]7Z‚G':u΀(q) -u$dlM -'wk S-| O;y] -1ԍ]7T5ھGOݸ$kTF`OV|PčM]tMcY }v,n \fcj{.)ٚUŠrLV$B@u*B7F6y2|rr;+\[ιrptrCx!r](P -g=c(1 fB8P -G]d i9++m'AUdQn3%ilrO8Ӫo+9ETOiSWhrFdw3:{ϣlW .6lY{ex!8t_xW>mΈȀ5{J=WFsLhPv$}_RMפ} -˴{Bzr5x.UI&${C[#Eqx(կa9EP\%wEOwS8q'^ӘD٢#AJSTY+v0ZGM0٪]lQm>Pvy$+^D & H}Bp8o/wo$_u.7Zհ 7D-z VwԼuYa0N@Ye囍 'cmt-ƗYkC#01*SuS,٩p܅[C_qbhjF Y.&L0W7O.(Uf?UʍlE0%ݹ@"qy*`@vyIy%YqpF$Xu{9"yFmNp*{BeQ-f^}szs>^D lZu4']G67TN@]6dA3$6(OY99Q#a9q 9\=Mx$U /> o$Co0:')8\٣!=zЎˋ5~Q6#]1rZ1O/h_ ~]e7yasrvlRB$@Ifs3FJ1!]Cmhykr-!إ?ns෣Ξr ONۯ0BBʆt/6ds;t>u>-.3oJM:h$c۶2kEȫ`KE9 -~&enc*wnjIcw5kΗ@,'k;}Y.Z.\\)+;=V~M+\`I)^*-O0 ! AB\u߃7%˵ .}w[<Ċsdr#Go?"Z-6K1 -$d/:0\}]7> -vTUC:ˉA€e>Ś>stream -HyTSwoɞc [5laQIBHADED2mtFOE.c}08׎8GNg9w߽'0 ֠Jb  - 2y.-;!KZ ^i"L0- @8(r;q7Ly&Qq4j|9 -V)gB0iW8#8wթ8_٥ʨQQj@&A)/g>'Kt;\ ӥ$պFZUn(4T%)뫔0C&Zi8bxEB;Pӓ̹A om?W= -x-[0}y)7ta>jT7@tܛ`q2ʀ&6ZLĄ?_yxg)˔zçLU*uSkSeO4?׸c. R ߁-25 S>ӣVd`rn~Y&+`;A4 A9=-tl`;~p Gp| [`L`< "A YA+Cb(R,*T2B- -ꇆnQt}MA0alSx k&^>0|>_',G!"F$H:R!zFQd?r 9\A&G rQ hE]a4zBgE#H *B=0HIpp0MxJ$D1D, VĭKĻYdE"EI2EBGt4MzNr!YK ?%_&#(0J:EAiQ(()ӔWT6U@P+!~mD eԴ!hӦh/']B/ҏӿ?a0nhF!X8܌kc&5S6lIa2cKMA!E#ƒdV(kel }}Cq9 -N')].uJr - wG xR^[oƜchg`>b$*~ :Eb~,m,-ݖ,Y¬*6X[ݱF=3뭷Y~dó ti zf6~`{v.Ng#{}}jc1X6fm;'_9 r:8q:˜O:ϸ8uJqnv=MmR 4 -n3ܣkGݯz=[==<=GTB(/S,]6*-W:#7*e^YDY}UjAyT`#D="b{ų+ʯ:!kJ4Gmt}uC%K7YVfFY .=b?SƕƩȺy چ k5%4m7lqlioZlG+Zz͹mzy]?uuw|"űNwW&e֥ﺱ*|j5kyݭǯg^ykEklD_p߶7Dmo꿻1ml{Mś nLl<9O[$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! -zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)Km endstream endobj 5 0 obj <> endobj 20 0 obj [/View/Design] endobj 21 0 obj <>>> endobj 12 0 obj <> endobj 10 0 obj <> endobj 22 0 obj <> endobj 23 0 obj <>stream -%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 17.0 %%AI8_CreatorVersion: 19.2.1 %%For: (Zachary Mitton) () %%Title: (metamask_icon) %%CreationDate: 6/15/16 2:23 PM %%Canvassize: 16383 %%BoundingBox: 98 -140 188 -44 %%HiResBoundingBox: 98.7919746568114 -140 188 -44 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 13.0 %AI12_BuildNumber: 147 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBProcessColor: 0 0 0 ([Registration]) %AI3_Cropmarks: 79 -156 207 -28 %AI3_TemplateBox: 180.5 -120.5 180.5 -120.5 %AI3_TileBox: -163 -488 449 304 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 6 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 1 %AI17_Begin_Content_if_version_gt:17 1 %AI9_OpenToView: -39.6666666666679 23.666666666667 3 1419 866 18 0 0 -5 38 0 0 0 1 1 0 1 1 0 1 %AI17_Alternate_Content %AI9_OpenToView: -39.6666666666679 23.666666666667 3 1419 866 18 0 0 -5 38 0 0 0 1 1 0 1 1 0 1 %AI17_End_Versioned_Content %AI5_OpenViewLayers: 7 %%PageOrigin:-220 -420 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 24 0 obj <>stream -%%BoundingBox: 98 -140 188 -44 %%HiResBoundingBox: 98.7919746568114 -140 188 -44 %AI7_Thumbnail: 120 128 8 %%BeginData: 22554 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C45FD24FFA776FD75FFA04A4AA1FD73FFA04A754A75A8FD71FF7C4475 %4A6F4A6FA8FD6FFFA04A754B754B754A75FD6EFF764A6F4A754A6F4A754A %76FD6CFF764A754B754A754B754A754AA1FD69FFA8754A6F4A754A6F4A75 %4A6F4A6F4AA1FD44FFA7C9A075A8FD1EFFA8754A754B754B754B754B754B %754B754ACAFD3FFFCFC9C299C1997476FD1EFFA76F4A754A6F4A754A6F4A %754A6F4A754A4B4AFD3BFFA7C99FC198BB98C198754AA8FD1DFFA8754A75 %4A754B754A754B754A754B754A754B6F76FD37FFC9C99FC198C199C199C1 %99754A75FD1DFFA74B4A754A6F4A754A6F4A754A6F4A754A6F4A754A4A76 %FD31FFA8C9A0C1999998C1999F99C199C1746F4A4A76FD1CFFA1754A754B %754B754B754B754B754B754B754B754B754B6F7CFD2CFFCAC9C89FC198C1 %99C199C199C199C1C1C175754B754ACAFD1BFF7D4A4A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A6FA8FD27FFCAC9A1C2989998C199C199 %C199C199C199C199C16E4B4A754A75A8FD1AFF7C6F4A754B754A754B754A %754B754A754B754A754B754A754B754A75FD24FFC9C99FC199C199C199C1 %99C199C199C199C199C1C1994A754B754A6F76FD1AFF764A4A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A76A8FD1DFFA7C9A0C1 %99BB989998C1999F99C1999F99C1999F99C199C199994A4B4A754A6F4AA8 %FD19FF756F4B754B754B754B754B754B754B754B754B754B754B754B754B %754B754A76FD1AFFCAC299C198C199C199C199C199C199C199C199C199C1 %99C199C199994B754B754B754A7CFD19FF764A4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A99FD04C199C1C1C199C1 %C1C199C1C1C199C1C1C199C1C1C199C198C199C199C199C199C199C199C1 %99C199C199C199C199C199754A754A6F4A754A4A7DFD18FF7C6E4B754A75 %4B754A754B754A754B754A754B754A754B754A754B754A754B754A754BFD %05C1BBC1C1C1BBC1C1C1BBC1C1C1BBC1C1C1BBC1C1C199C199C199C199C1 %99C199C199C199C199C199C199C199C199754B754A754B754A754BFD19FF %754A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A4B74C199C199C199C199C199C199C199C199C199C199C199C199 %9F99C1999F99C1999F99C1999F99C1999F99C1999F99C1994B4A754A6F4A %754A6F4A76FD18FFA14A754B754B754B754B754B754B754B754B754B754B %754B754B754B754B754B754B754B7599C2FD16C199C199C199C199C199C1 %99C199C199C199C199C199C175754B754B754B754B754B75A1FD18FF4A4B %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1 %99C199C199C199C199C199C199C199C199C199C16E4B4A6F4A754A6F4A75 %4A6F4AFD18FFA16F4B754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B75FD16C199C199C199C199C199C199 %C199C199C199C1999F6F754A754B754A754B754A754A7CFD18FF764A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A9999C199C199C199C199C199C199C199C199C199C199C199 %9F99C1999F99C1999F99C1999F99C199994A6F4A6F4A754A6F4A754A6F4A %4A7DFD17FFCA4A754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B7575FD17C199C199C199C199C199C1 %99C199C1C1994B754B754B754B754B754B754B754BFD18FF764A4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A4B74C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1 %99C199C199C199C199C199C199754A6F4A754A6F4A754A6F4A754A6F4A7C %FD18FF754B754A754B754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B7599FD13C1BBC199C199C199C199C1 %99C199C199754A754B754A754B754A754B754A754B75A8FD17FFA04A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A7599C199C199C199C199C199C199C199C199C199 %C199C1999F99C1999F99C199C174754A6F4A754A6F4A754A6F4A754A6F4A %6F75FD18FF4A754B754B754B754B754B754B754B754B754B754B754B754B %754B754B754B754B754B754B754B754B754B754A9FFD14C199C199C199C1 %99C199C175754B754B754B754B754B754B754B754B754AA7FD17FF7D4A4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A4B4AC1C1C199C1BBC199C1BBC199C1BBC199 %C1BBC199C199C199C199C199C16F4B4A754A6F4A754A6F4A754A6F4A754A %6F4A75A8FD17FF764A754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B7575FD13C199C1 %99C199C1BBC16F754B754A754B754A754B754A754B754A754B6F75FD17FF %A84A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A4B74C199C199C199C199C199 %C199C199C199C199C199C199C199994A4B4A754A6F4A754A6F4A754A6F4A %754A6F4A754AA1FD17FF76754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %9FFD11C199C199C199994B754B754B754B754B754B754B754B754B754B75 %4B75A8FD16FFA86F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A99C1C1 %99C1BBC199C1BBC199C1BBC199C1BBC199C199994A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A6F4A6F75FD17FFA74A754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A754B754A754B754A754B754A %754B754A754B754AFD13C199754B754A754B754A754B754A754B754A754B %754A754B754ACAFD16FFCA4A6F4A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A4B4AC1BBC199C199C199C199C199C199C199C1996F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A75FD17FF7D6F4B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B7599FD10C19F4A754B754B754B754B754B %754B754B754B754B754B754B6F7CFD17FF764A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A7599C1BBC199C1BBC199C1BBC199C1BBC199C1C199 %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754AA8FD17FF75754A %754B754A754B754A754B754A754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A7599C199FD12C1994A754B75 %4A754B754A754B754A754B754A754B754A75FD17FFA8754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A7599C1999999C199C199C199C199C199C199 %C199C199C199994A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A7CFD %17FF75754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B7599C199C199FD13C1 %99994B754B754B754B754B754B754B754B754B754B754A7CFD15FFA8754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A7599C199C199C199C1BBC199C1BB %C199C1BBC199C1BBC199C199C199994A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A76A8FD13FFA84A754B754A754B754A754B754A754B754A75 %4B754A754B754A754B754A754B754A754B754A754B754A754B754A754B75 %99C199C199C199C199FD0FC1BBC199C199994B754A754B754A754B754A75 %4B754A754B754A754AFD14FFA14A4A754A6F4A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %75C199C1999F99C199C199C199C199C199C199C199C199C199C199C199C1 %99754A6F4A754A6F4A754A6F4A754A6F4A754A4B4AA8FD14FF7C4A754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B7599C199C199C199C199C199FD11C199C199C1 %C1754A754B754B754B754B754B754B754B7576FD12FFA8A151754A6F4A75 %4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A4B74C199C199C199C199C199C199C1BBC199C1 %BBC199C1BBC199C1BBC199C199C199C199754A754A6F4A754A6F4A754A6F %4A754A757DFD11FFA14A4A4A754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A754B754A754B754A7575C199 %C199C199C199C199C199C1BBFD0FC199C199C199C199754A754B754A754B %754A754B754A754A4A75CFFD10FFA8754A4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %4B6EC1999F99C1999F99C1999F99C199C199C199C199C199C199C199C199 %C199C199C1999F99C199754A754A6F4A754A6F4A754A6F4A754A4A4ACAFD %11FFA8754A754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B7575C199C199C199C199C199C199C1 %99C199FD11C199C199C199C199754B754B754B754B754B754B754B754ACA %FD13FFA87C4A4B4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A756EC199C199C199C199C199C199C199 %C199C199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C199754A %6F4A754A6F4A754A6F4A754AA7FD17FF75754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B756FC199C199C1 %99C199C199C199C199C199C199FD11C199C199C199C199C199754B754A75 %4B754A754B754A76FD13FFA8CA7DA176754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A4B6EC199C1999F99 %C1999F99C1999F99C1999F99C199C199C199C199C199C199C199C199C199 %9F99C1999F99C199C198754A6F4A754A6F4A754A4B4AFD12FFA87C4A4A4A %754B754B754B754B754B754B754B754B754B754B754B754B754B754B754B %754B754B754B7575C199C199C199C199C199C199C199C199C199C199FD11 %C199C199C199C199C199C199754B754B754B754B754A75FD14FFA8754A4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A4B4A9F99C199C199C199C199C199C199C199C199C199C199C199 %C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C199C1994B4A754A %6F4A754A6F4AFD16FFA8A14B754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A7575C199C199C199C199C199C199C199 %C199C199C199C199C199FD11C199C199C199C199C199C199754A754B754A %754B6FA7FD18FF516F4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A4B4AC1999F99C1999F99C1999F99C1999F99C1999F99 %C1999F99C199C199C199C199C199C199C199C199C1999F99C1999F99C199 %9F99C1754B4A754A6F4A6F4AA8FD17FFA1754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754BC1C1C199C199C199C199C1 %99C199C199C199C199C199C199C199FD11C199C199C199C199C199C199C1 %75754B754B754AA7FD15FFA8A14B4A4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A756E9999C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C1BBC199C1BBC199C1BBC199C199 %C199C199C199C199C199C199C174754A6F4AA1FD17FF4B6F4B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B754AC1C1C199C1 %99C199C199C199C199C199C199C199C199C199C199C199FD11C199C199C1 %99C199C199C199C199C175754A76FD18FFCA4B4B4A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A6F4A9999C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C199C199C199C199C199C1 %99C199C199C1999F99C1999F99C1999F99C199C16E4BA8FD1AFF756F4B75 %4B754B754B754B754B754B754B754B754B754B754B754B756F9F99C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199FD0FC1 %99C199C199C199C199C199C199C199C1A1FD1CFF4B4B4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A4B4A9F99C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C1BBC199C1BBC1 %99C1BBC199C199C199C199C199C199C199C199C198CAFD1CFFCF4A754A75 %4B754A754B754A754B754A754B754A754B754A754B9999C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199FD0FC199C1 %99C199C199C199C199C199C199C1CAFD1DFFA84A6F4A754A6F4A754A6F4A %754A754A754A754A754A6F4A99999F99C1999F99C1999F99C1999F99C199 %9F99C1999F99C1999F99C1999F99C1999F99C199C199C199C199C199C199 %C199C199C1999F99C1999F99C1999F99C199FD1FFFC299C1C1C19FFD0FC1 %99C1C1C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199FD11C199C199C199C199C199C199C199C2FD1EFFCABBC1 %99C1C1C199C1C1C199C1C1C199C1C1C199C1C1C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1BBC199C1BBC199C1BBC199C199C199C199C199C199C199C1A0FD1EFF %FD18C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199FD0FC199C199C199C199C199C199C198C9FD1DFF %C9C199C199C199C199C199C199C199C199C199C199C199C199C199C1999F %99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1 %999F99C199C199C199C199C199C199C199C199C1999F99C1999F99C19999 %A1FD1DFFC9BBFD19C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199FD0FC199C199C199C199C199C199C198 %C9FD1DFFC1C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C1 %99C199BBA7FD1CFFC9FD1BC199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199FD0FC199C199C199C199C1 %99C199CFFD1CFFC298C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C1999F99C1999F99C1999F99C1999F99C1999F99C1999F %99C1999F99C1999F99C1999F99C199C199C199C199C199C199C1999F99C1 %999F99C1999F98C1A8FD1CFFFD1EC199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199FD0FC199C199C199C199 %C199C199FD1CFFC9C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C199C199C199C199C199C199C199C199C199C199C199 %C199C1999999C1999999C1999999C1BBC199C1BBC199C1BBC199C1999999 %C19999989999999899A8FD1BFFC2FD1EC1BBC199C199C199C199C199C199 %C1999999C1999999C1999999C199BB99C1999999C1999999FD0DC1999999 %C1999999C1999998C9FD1AFFC998C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199999899999998999999989999 %99989999999899999998BB999998999999989999C199C199C199C199C199 %C199C199C199999899279998999999A0FD1AFFC2FD23C199C199C199C199 %C199C199C199C199C199C199C1999F515299C199C199C199C199FD0DC199 %9999C1992E4BC199C199C2A8FD18FFCAC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C19999989999 %99989999999899999998C175510528057598999999989999C199C1BBC199 %C1BBC199C1BBC199C19999989927286FBB99C198C9FD18FFC9BBFD25C199 %C199C199C1999999C1999999C1BB994B2E0628272E279999C1999999C199 %FD0DC1BBC199BB992E282E99C199C1A0FD18FF9FC199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C1999F99 %C1999998999999989999BB98752706052827280528279998FD0499C199C1 %99C199C199C199C199C199C1989998990528054B98C198A0A9FD16FFCAFD %29C199C199C199C199C1999F7552282E272E272E272E272875C199C199C1 %99FD0FC199C1752E062E51C1BBC199FD17FFC998C1BBC199C1BBC199C1BB %C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C199C199C199C199992706052805280528272805280528989999C199C199 %C199C1BBC199C1BBC199C1BBC199999951057699C199C1999FA8FD16FFFD %2AC199C199C199C199C199A07576272E2728052E2728272E277599C199C1 %99C199FD0DC199C1759FFD04C199C199CAFD15FFA7C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C1999F99C199C1BBC1C1C1999F7575272827280528057598C1 %999F99C199C199C199C199C199C199C199C198BBC1C199C1999F98BBA7FD %15FFC2FD2EC199C199C199FD0BC17576512E27C19FC199C199FD0FC199FD %05C199C199CFFD14FFCA98C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1999F99C1 %999F99C1BBC199C1BBC199FD05C1999F99C199C199C199C199C1BBC199C1 %BBC199C1BBC1999999C199C1BBC198C1CAFD14FFC2FD31C199C199FD11C1 %99C199C199C199FD0DC199C199FD05C199FD14FFA8C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C1999F99C199C199C199C199C199C199C199C199C1 %99C199C1999F99C199C199C199C199C199C199C199C1999999C199C199C1 %CAFD13FFCFFD32C199C199FD15C199C199C199FD0FC1BBC1C1C199FD14FF %A0C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1 %BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C199C199C1BBC199C1BBC199C1 %BBC199C1999999C199C1CAFD13FFC1BBFD33C199C199FD15C199C199C199 %FD0DC199C1C1C1C2FD13FFC998C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1999F99C199C199C199C199C199C199C199C198C2FD13FFFD52C199C1 %99FD0DC199C1A1FD12FFA7C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1 %BBC199C1BBC199C199C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C199C199C199C199C1BBC199C1BBC199C1BBC198C9FD12FFC2BBFD35C1 %99C199C199C199FD17C199C199FD0DC1C9FD12FF99C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199999899999998C1999999C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %98C2FD11FFC9FD31C199C199BB99C199BB99C199C199C199C199FD15C199 %C199FD0BC1BAC9FD10FFC2BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1FD0499 %98999999989999C199C199C199C199C199C199C199C1BBC199C1BBC199C1 %BBC199C1BBC199C1999F99C1BBC199C1BBC199C1BBC198C9FD0EFFCFBBFD %2BC199C1999999C1999999C1999999C199C199C199C199C199C199C199C1 %99FD11C199C199FD0BC1BAC9FD0DFFA0C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C19999989999 %99989999999899999998FD0499C1999F99C1999F99C1999F99C1999F99C1 %99C199C199C199C199C199C199C199C1999F99C199C199C199C199C199C1 %98C9FD0CFFC299C19FC199C19FC199C19FC199C199C199C199C199C199C1 %99C199C199C199C199C199C199C1999999C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C19FFD0FC199FD %0CC1CFFD0BFFA79999C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C19999989999999899999998999999 %989999C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %BBC199C1BBC199C1BBC199C199C199C1BBC199C1BBC199C199CFFD0BFF99 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C1999999C1999999C1999999C1999999C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C19FC1BBFD09C199 %FD0CC1CFFD0AFFC2989F99C1999F99C1999F99C1999F99C1999F99C1999F %99C1999F99C1999F99C1999F99C199C1FD0499989999999899999998FD04 %99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1 %999F99C199C199C199C199C199C199C199C199C199C199C199CFFD09FFCA %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %FD07C199FD0CC1FD0AFF99C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199999899999998999999 %989999C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C1C1C199C1BBC199C1BBC199FD04C1 %FD09FFC999C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C1999999C1999999C1999999C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C1C1C199FD07C19975754B27A8FD07FFA8C1999F99 %C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C199 %9F99C1999F99C199999899999998FD0499C1999F99C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F %99C199C199C199C14A27F827F805F8F8F852A8FD06FF9FC199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1BBC175270027F82727272027F82752FD05FFCA98C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C1999998999999989999C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C198BB98C198C199C199C199C2A0A0A0 %C9A127F827F827F827F827F827F8F87DFD05FFC299C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C1999999C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C299C199C2A0C3A0C9A1CAA7CAA7CAA8CAA8CAA8A8 %2727F8272727F8272727F8274BFD06FFA0BB999F99C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C19999 %98FD0499C1999F99C1999F99C1999F98C1999998C198BB98C1999F99C199 %A09FA1A1A7A1A8A1A8A1A8A8A8A7A8A7A8A1A8A7A8A1A8A7A8A127F827F8 %27F827F827F827F8A8FD06FFCF99C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C29FC199C2A0C9A0C9A0C9A7CAA7CAA7CAA8 %CAA8CAA8CAA8CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA8CA27272027272720 %272727F852FD08FFC299C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C1989998BB99C199C199 %C2A0A0A0C3A0A7A1A8A7A8A1FD07A8A7A8A7A8A1A8A7A8A1A8A7A8A1A8A7 %A8A1A8A7A8A1A8A7A8A1A8A7A8A127F827F827F827F827F827A8FD08FFA1 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C198C2A1C9A0C9A1CAA7CAA7CAA8CAA8CAA8CAA7 %CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8 %CAA7CAA8CAA7CAA8A8FD0427F8272727F82752FD09FFCF98C1999F99C199 %9F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999998 %BB98C1A0C9CAFFAFFFA8A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7 %A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1 %CAA127F827F827F827F827F8A8FD0AFFC299C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C2C9CFFD0AFFA8A8 %A7A8A7CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CA %A8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8A8FD0427F827F827F87DFD0BFF %A8C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %989999C9A7CFFD10FFA8A87DA7A1A8A7A8A7CAA7A8A1A8A7A8A1A8A7A8A1 %A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1CAA127F82727 %5252767CA1A8FD0CFF9FC199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C2C9CFFD16FFA8A8A1A8A7A8A7CAA8CAA7CAA8CAA7CA %A8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7A8527D %7DA8A7CAA7A8A8FD0DFFC998C1999F99C1999F99C1999F99C1999F99C199 %9F98BB989999C9A7FD1CFFCFA7A87DA17DA8A1A8A1A8A7A8A1A8A7A8A1A8 %A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A1A77DA8A1A77DA7A1A1 %A7FD0EFFCAC199C199C199C199C199C199C199C199C199C199C2A0C9CAFD %23FFA8CAA1A8A1A8A7A8A7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CA %A7CAA8CAA7CAA7A8A1A8A1A8A1A8A1A8A8FD10FFA0C199C199C199C199C1 %99C199C199BB98C199C9CAFD29FFA8A77DA7A1A77DA8A1A8A1A8A7A8A7CA %A7A8A1A8A7A8A1A8A7A8A1CAA7A87DA8A1A77DA8A1A77DA7A1FD11FFCA98 %C199C199C199C199C199C198C2A0C9CAFD2FFFA8A8A1A7A1A8A1A8A1A8A7 %A8A7CAA8CAA7CAA8CAA7CAA8CAA7A8A1A8A1A8A1A8A1A8A1A8A1FD12FFCA %C198C1999F99C1999998C2A0CAA8FD33FFA8FFA8A87DA77DA77DA77DA77D %A8A1A8A1A8A7A8A1A8A1A77DA7A1A77DA7A1A77DA7A1FD14FFA0C199C199 %C199C1A0FD3DFFA8A8A1A8A1A8A1A8A1A8A1A8A7A8A7A8A1A8A1A8A1A8A1 %A8A1A8A1A8A1FD15FFCA98BB99C2A0CFFD41FFCFA7A8A1A17DA8A1A77DA8 %A1A77DA8A1A77DA8A1A77DA77DA17DCAFD16FFC9A7FD49FFA8A8A1A8A1A7 %A1A8A1A8A1A8A7A8A1FD04A8FFA8FD66FFA8CAA8A8A8FFA8FFA8FFFFFFA8 %FD12FFFF %%EndData endstream endobj 25 0 obj <>stream -%AI12_CompressedDatax$&?d& C;\;fJ-n[[YUZ(xd&,f #+3q98OxOq< ?wo ~7_{ˏ~/&G4M~Vۯ߼LG{4?oqwo^_^ޡݻ篞g/PWnC} 4`e߱=&7Ô?L/ޣvu&3%Co^|O߾yq7/߼W?>y~^|0;qv~c7/o^a|t/_/tqzWw0R<3ٯOaC)?l/Jo|ۿi[Lݘج{K̬̂1??J[x?~W~NguG|˻FѤS7_ܽD/ H1oɦ >Kz3 3y}vgӭw2IM~?WyOtҺ2!Lj}.6(^{_G<Ѽb |aoSl߿DŽkѽ_bٚO1';=I~R4!o;H]cձW?y=5w7_j|ӷR}To?~_^bqQ>mŃߔ?뿏 Bӛ{U>}|CsL!޽zn -!}Ho_wg߾܎Ͽzz_~˧ߩO n_|=w.̙/|ܿ8~_xNuTOX[˷Zߥfi>$_>ksP3|q-y=?F?!]϶j~xx7/~tS/>\?߿cwwI|+r;鳶|0e> loq\߼3L/pba,H=;0?)vU\) ߀%%Ӧ\ \܌x[f`*nU-LDIo^iSi.繜5Jz7ѵ][*FAiUU*'-VmӯVuY[a^^Zd]fU͛V+ebe7g]k;la]/WkdYԬU)۵Z)*և:YeXtMe@CY#չk)7ܲԓŗYUeLIɭ̍zʵؔF2 ssλɝP-Vx>'O[L .C -S -p7v v)esUsSʧ|o-&7 LNyni̕W*^rdNONtemwp|:[l6#ut}>_\ތXwoM7 usnnnn#n1aoz^m7r*U9զL _^*qSªUq 8R$l!z7M9kӪ\ʴ*ySҪU M*vU̪KS>_֣_WENf]Z%. bXv 2ʌd)v64o򬡙R&)$%JR\)vWL%Ϫ?')WLRr)8K R|)%Ѓֵ;zeY eeگed&G/fdndbN2yw|Q^Z^Jd^Fq``2Ϡ[W_t,yP5 j>H73_J F1aoטt@H tr@x#HuN D;x;p{{@}w & -D5CpqnX. K׌ucq7g\DW);2UnC|Ey x;Rٙ-خv8Pz? gx'H˗v؅0ܮH *`Cld!2r.y 9&*w"6`ټ.b/+>P,"eXalX~PdS }bٛ2<duXE3Ϩr;.E9J\d('}(bs=U-l4W=9"}ZӬYAL6%Ts끼VJ{OX 2=ye[3UCwD翇d?Sԇos<;XԵ?ʏOBF7mLE߰s8҆+ H$" (" tAy\( !DʢJBFǭH|! ,DiȪ4$uN"e(rE"R,RQш‘Q ,e$JI OeSB%'ЈjFĥkK(2Qhؔ|J5t[듖|97nI?FյX4̲P,~)uuxIx" ?4 Qs E6< KCvD1l4맕aýE|r.VOoGEq3- -U -ͪLTTJэEUZ*mXM]JY\9UDi%%2r5=Ҵъ %s(It8i4fCT -a);12y?Ӌ+[ @c&*PV5m\86 ŸV+-7bU[#w)Zf{% 0<@jXp1frb=]9It=G9 [>EXy+3KoJO+ï~QQ0:1L<98Ilj> -Ѿ8Jl+u!3)]Hh% \hB#ڸLo{[Z&qk"fÊmIWCv8x}i؎u'^{߇qmCIJϞ=c@09Bk۱iccwt6sM&;XݸCI1TQoZ@,qx)G7?}HH|5FPH!H58RZ$n҇U}|OV#pDnyu:q/#7I0c (0 -+tǵl⟿Z +R>qV#6Cxji7~zQfd @,zv4./_bS;;c4`&# 4؜6%0ySIRм m{=jP]||s|<:@> lm/zr._S  -_rUXBa3|!<4S<0< Sy+De&W@AAr-^uUjKTˈXōuXᒗe?#uFnfVŜzQ27sPf'z3 mdq<9+r>3_;ug{YʪHNEa십]ԕd1U w]<[ )8c(bb<;]=),IȥId,v+POD QuVoʬ*rk$V_ba_U[X.}7IʲC#rOL,lEkY, En%ʦ"ƞD1V] ֯$XJ:vl`{6VY=+2ҡYAnVط6{;,Y ugeV^am|O;>ͩ3/Յ8/+w J.zꓨW|٘X \bU鵧_o(r = wxw<}e|m>rz|/GTy/Ҡ `+0{ط>WG|/\,O_{ُF-I}34Yݵuw}\&vřTi: }S͍2.",W(^QFl~34mٷ!,ԅl#AiEqڐ9,%#s֡%bn#'g=<r9?3:Iq?:?'3{T|ʞ:/yE X0=QnEk Zi:EBm9Ɍ \:H͕}g02x˩yj4>/=?S=w טiąYpo8&joEI'Oo>sQcb J"cTՎb&)xEUܔ&`dgFzv%ś9rJAKqj6Myt a\~ا~+7^ݽJhgyus]j/iR:SnBL%4#Kq _ KX*8^Kz\e>l/ư7TVrǩ0 7U/ޅA[lH8$&qOTqʅZ%.B5!% KJ)@(|GY:>LBaK\NKyUoItfm6ŭݐ\¦?)NUWe6>%BE6Q%J>q唚mWCyZy"obݮuc*qj+}ZX9>p,JͫoD Dޤ$;;mXGƕl_f#2-dpeQSEN[cnm4;h\; Vymff g,Ju}o14Q$:i\-.ns8ič+9m6VkϚ|['hZzvvAuaG`}t`}YhG_:m8GN84hu*, _ CkA|,|aGWuw#my{"Vފݹ(|مavWJů9jZ~,wr Ez_dxZ_KY˻JR EV 襠qwU)y@R޸0fwnxQ4;9LAǦ6 -wz·2_}q|t0>\v,нe| -(Cq7o]ͷ`['sثj[d˧1rQNت8 ETԑ<}>S|efk Hʾ_>Ctu;,0D9,'%CS9 .N[ZcqaO΃q5Ovi7bE@Om>nzw)6>FaCmq$8I?I7 ,BԽMٻ -M^A*V*#9Փi]nLʼnigLìQ&[,_?5@'Q -oDT7 F\'uY6@  ,[eh>O*r.V+÷h#exZ:i0$3QN -ߑ6V<ϒ^XEH92kV'jX\+KdP8SGYр3ɡeNqV1 e'%:U9J1џP:vlu{L[5*F6Ԯ/+B ƪ-; eфaǓJږ߶<' Oo_KM ]"\EkF0EDϤE Ȕθϔog|VUu^v)H! {'xT:UjCITҒآ(ZJ(Z4KY薮lZJa\"6Bi(D9P{i{::6KOyíO0i5*alX!X%RK努Q>E(c4,Zۙ;^o;r%( />1]3HJW.=Cq Q; JliJB%۵C^ײҫjL[ExDZ䤵 nI˴~ԻzMj,v8'2<9> Oo_êD Oس&F$(0SNeb - -0jhMMop-~v*fPj0<м4A͗ƪ]\ /0)zdp[_bݫjZL]kmrkX6ָ՚.XƬuɨ1i=d.LYO^IS)exZ 2< NO' -O' -xm7?a>Ykpi>d8$\09 f‡B'zgFxqO/e⎭cT1;ɸC0tmojj{ (qp͂C@z Nim)x?NvU!qnܺ//rvEi]ŧMi|T3ٷO'ؤ\L+u6u&٦|t,JtѲK]j=?Q`%֜k,pHyTak5u1'@IQAl P8MJ&7nJiQ<YOdF0DG3yvhCx/ew9ˣ/h-UPhnu&rSwOfc[x^$؝lxqq}csnٖIİX#!uDmw2<@7[I%~d<>WMeuv]a v2K%ѳ.ڟsq\\40Ž[Ѭ 7A 7_,~bN|v#n`^E.'[dՊ>R: ^G=,>($&K}id5VZڨpk ,ٴ 0 JJTgb60rOd`WIj>Q)3G^<-g@GpAYj,D9&# n-ƄA2 m2v'}2(W]c~TQ9oXVͨt?{<% rv 3nl=ثcvL !crU8+}Nߓ%@q+%ǭ$"~(2^ -Ma\Qݙ>Y+|Z[1z[gz[1Ԙ۴mJUr5Y|Vnodw`xNRKisl\7P΍||TGYugnKE$%V`t -6>+j::T\Phel銻PnC%oS5 -YSh -fWX1V *:I2SOBI44!eVk?xܓ'cOLj4-oKYuUh% JLfd-ytu<+qeYmˇ_CUVN`7(<ˌpaN ƍ]崂v - 6<žr[D\,-9n^$ D8aw2\0[ԡz*--ZLTFh7@K`nQ@밆#^MnRD_yrwQvpѹჲpb?McCa7Mt;HsM8)4y%qi$!wb-jqo1Pm+?:W]:HC'tJ!v\Y"fzw)n.(RD -K gѢ_ \߹D!˔] bjQ"#ΐSm2a+|;rudȼ]A>Q?ulR0Ԝn`uEss9 +Q37fQ;NLthp) n)n6WZnE*:]yu}; ӕdqYJZ,=*SZ< JӐH~[͎csߥ ÅR$Azae+̬䜽)Ò)SA$ZܬKV׬q:k)=As<2mS/LtMo@] ?}S>wihx9{JgU|>i?)D=:Kb%5ީ xyN$ȫik. ~s>4P}h/=vY@M~ ƽ#j&btan"k9hj/$s.!OE8]O$opLs*~$neqb:<7BB.oN4;rN/+"V}ZC^2TX5wb!xS`*ffs9 : i.!JUfQ?H*F!uV[6Nա$ͲWa⇰0IRnJ:'hp!bBY -`E;p8O -n2 ;aN(FXg `ᡭXbmZbw k7ax-(ÅS[/|um*][Wm!n;2._=11i8cT]#w-ՇI~ݔ÷Q~^/CQJ Rz$-Hi[Ȥ"k?hR{W5qn UVi+~ - -whpC=C~ӷݿOVrbXݽ} ?9DaSt~;X43a{GOl6O2+o:O?1'?O}t+T>:oZv{{~ywo^?ïD{ӛ7/1y)wo>;=WL?ܿ{݋w8w|ȯZ>@ȒgEYkpZtSacBM~˵)\Y$gq81A`EtY5$fL!Lzv#`!1<5ق6$CkG9`g9A&0=޲;z|ŏM?!0}<&|;t> C0/6R[¾3>8bx 7鈹3]Gg #.6ÄJ*1*ɁDݡ ؚ#{6+#Xu<*ln[T8lǀ`!k_&.GN΄bia&P,. G_;3GxbcY16TCG)D[^6Wc,d:ɓGd1IN'LH0%@;K'Y(G(MO#6%] @PbsO' d$S߆d-J4Rwg4udo+qo5!|~lb>Y Vg= !9P`W^éȝGV` jTZYBؓuZuvd1elYSg#n -}[Ϲ0qufۿdf7_/ ʃs7_ٛ? ojõL{آ!TfEbѝ(xL(2f㴸˸$n -,L"C"zJЄ \7T[ʤd + 6vBoeW]!DW1oHwm%H2kdRq'CՎXZ[i;Fx#B޵] yoeNl3_cD9D/*m% -dބSy]- u•HJbcx[ w?* rYrBʟ:3̑nsS.eUdSik3G>fT9i\h?qsiC;+x#@E:PlXJŽKun|ƛy$ 8C(A]Z0Oׄm x{\F0TOEȓHfwaaJ@Cr`%@|R%RU#>Lo;yp"MfP"0=Xpn |9APr- -23A(LOř\'"Dӂ3 -|=g>/ݡR҉Ѩ#3'7=.{&a1[f^qɷb!qVxT,@m gS+ + ~ -gZh+6,QYޚYռ9>sǹ %ґ?l mm]㽃)Lp轑ChM - SI8\Յ<%44.i+഻2>=ρo?Rݵ41?U58?!Yި&2qňLeLf9m#p\ Kdve~e K¶#}0UOe$|xA*4>:Q[# - LTU$ ֎&&4yr($bx(5M0Ɉprwݗ#-F6<_fn.(`wy|nME&ڑFȄ/;b)q&L{30D>fL4Z$.H=%7}+.Xl Vnp4s. .N:іDkP'_C:QӍŪNT)/#*tNN."PtHݼBsEn>nlG؊ TV7SBH`dp,d kEڴqƑ-@#v∣ؔfAR !D^Kh׊309Pd.WjZQJ`t-vߩkNM7nv[y=Anu =U^d*NIq<4Q) -4֣VԻs9)߲zNWQFm;-:ϱ?3RONYϹ4wJ?Y1F7lֵ, F8J\oY}68j @)7TەTEਟ -ic c&N-pdd?ѭLc̍w0SkY~x;(&68`d]@i U}q@3ŭOʨ4|nނ, xʀ1MO#iڸ&H-nqdq]$v0qTɣR;{#OR?WjJ'$q+M[1MFU7$#C tm!NLa\h{:ldneMq @G׾QKr>2k;AMx_}puB^C?1*!jT-tC"GӰ׏-8`Ǹ;EN/ik0C?* RFnvn~Uh:}|KCpބTmOM4{ޭ8Ix!@cnY`LXV@--4%"Epj|E$GΈdɄx%hkQQomh=mCQoOwN $. -4"Fj0Z̝eHӐKaDcJj{ eY[El]lB8y@A;^|Dڋ(@\▃@:usFtΐ"hνIB/>RzH#wm=Y sΈr ̎\DK墿8 W&:nХb%uV7K$3)ގI^c_SroT#xCAa#4[So+8ո{|*&D -l0;ut0ۙ\L2(D/qpoTWF\ n6!XH/@]/"#RBեx9D -1 fEh5X1FHXPIX X|5^ɓIr=t]F3}7Q0Yuoe%D>9tɆ/Ge#* BBu Vѫ!-W'0rǺ~,!^iTvtItu:+! H{D>GYƕn\/ iǩ'+z&;vʈ!5p O| ߩw{$${b[BPO;y,Ͼ YKfa5;-gLP>]yl?w"CuQ*v>ͫ֋БGqE{'{: -豛ɗWi+vwf7V'˒Qv]j.gT쒑hL~^eY݄6:oCت(^@Úd7^_y“-]- T?Q{jԿYgOcV/ɂ3xo]:y_~lukjȣ^}V%/VQ92RඈPūV Xԓ;Nev=ϻJbOӪwCi֠cû3P.v^z:p}:5I /P'U`fxX;Eu} e?HS߼N"7m&E&<0A4۴-fRny|WF$C2KaR`/v&ӋKD?:\ - DLsL^:~"r|ws5mn%n!#\ -얍y09ġxJxI&0!Sl <ཽAz#௑(k$2JS` L%NO;/< &*0yf0 -XOV:GKoe'o/^wDFFWfn -8ݱ ɉ)Z\ɹxf#~2`gWuSq!nH "74w`>`ő<`z*Po1գguΐ!?穋H#o1)g 5k78'*%PbNHj2pWFpJO^6GA0SXb;VF h=Yt՘‡Ob4Q4d1VTGKN,"6ΜF %3a-W3e^\ zr] Ki -/ƺ7EN)(ꦑ6~,VE VSӢRfn~:}t0%GQ Dj[1"FQQb3Q]?h+&@zLYB -,%Mw'Ia$ Q=uYsTB -ݓU g&TQLQLgyiP6Ǝ}]7iR^!FKBᦢ5Jd2Ɲ:qu#U -H)4v-@BN ifSeO>I"Ntl?Us*Oi4K;!ql%1- "%(ѥܹLoSSᕝm( +֌ܪ+sFFWod,QbdB(*dtI$  F`3fP"0 }̼aiටkp=YS‰7-b$'186h^H6 -Gbgy@h <):o^i&망n( -"deA53cK^3C"xfB+DuŅ*MfHV@ cX=dԥ bkug(]ꓚVI`4f !m :Ez8ӿR@LM7P[y=A - D T C׊Vc}jxɠj)BН+e <*%2.Aܖnb;_G韬蓺VɕVv,]ߠwjڵm?cDtp4Gb@ +(•<j"y/R;θ:Q4rS%f6tw"zSv[NC*FJ""9XvZ4OZYե洣ԏ8^/\NMe4c ݲ -X dp> .hۈ~$t$kUeGʀ<K^ԷxQ$d B(^먭ŞBע}*M694O -XΛ -u,_w-/ߢZ {[;=qUZ*Qu஺/[etl mѾQZ7nv`܆EU vY};ڏ %^VDoɻ^taecDX)pÉꮅm3׭8mP d\K 6oѼЋaNt43ҌGS!xzFж^pݴt3zT9BET"C ":] È@:~$@":$:Jz^.8DׁCt|8D8D5lסD0};-^_  $C@"*_ 1CAB~-D *.D*`Pwa*&`P;P P6Cp~` U-.C:Eoha;px( .N8ZH] P"ҷ~~۱tk(M+%X8ag$ ٿQBT'z7[e 'bh3PW$PfOVtE9JF;z␙q@2\!"y҆Dp-T>jhGV,J;#[4oajRAq膂i_-􍚉ick4>ْ`86:~rѴcu{BXX,VR0HP]0OθN*E >eat <~*6Gۑ"ok ԯLpǖǻ*L{U8HJtRFmKr }#B;Ӷ > -|\oa\=y)t3!-m߈7p@y7E:B޵Ҕ=׼{R?G|?Va$T1Oب*Ed\5!soD 4@.3b$nJ{Us8^}]U~9)-ר>M!:4iR#e6ݏiXeiig#[%.hb7Ϡ=[/abrQkG0^3Y'{: Ė: virwb+VUcն$4M?ʼӽP짠<-1$[&mI1_VbFVcF2i@TkʸLŰA[#4S~^Ʀ5u<4O>NJE(پv -s.MCoELIənܶx`;(VLG+qv^BdIkQdX佗ak4Y|X;;iӍ4h|^3\ĺ&qeߩiQfn~:\l(neGQj~ZߊU!n+*Z3Vbk%Iu3f퀴$݇ϢzYTW$Kn_ $\VbE Xnfa}\":*K5*Z z*[^ŕb:Qw3H`P=)I`M]aU +3ryDgQMtz8EECẠ~ -E{,6A2 xA @ɹkAv*«;- dCBDn}'dхt_"1chFZ@*̓dZި\yřLb ----8 "d/]T̴t>PI(v u 4O8oub){4W`Chr)8E h`hK}LZ*hE4i[4gcj#;DKeuk#i[Ni9sR ,i@`-~k9N.mm]+[Tɹ@tWiߣķϩ{@:Kg~LAǴB_y22O8:}UaFE#b)#N( - ,0+\10WsZpyt{˵ jD$}4E} o~߼}wwᗿynovw_^߿~ֿgx۷o^_:7_m]iFϻ/ٛw n޽}qR0Y߾y|YڛgWqn^QпOw_޿./GEQ ɺQ1FF>gg|p4smYn^۬Ù߉|@c5eD!'1ծ뀑V+Fky>٩* ~ y#ogclJ)+J&H4 -f`E -ZT:УRS9@3%O,#/hF1CvU@uyPk%dK0^;:#x#vbΜ%gǤ_YR'$MhAܖFJÒI_G5@T0{7+\Τ7710 0@,n&V 0RUBn>GRW9E&?Y#Њ -lJFIVE [VW}-^pG|L8Mo F&ЃP=c0a`oM }8&\&)x,l'PX&8gc`RIM$h8t8*sȘV<$XzUAtDTTK{K(f@Z`@Nb}Koiٗ.F|ȀGԈ4тk ɳ`KZ9M5XXI3m"3'!Dk:$$'5A:ы;I0ђ#Hs9虙x3$f -TىVl K+nKv b@LjHE# -&4240=#%sM9I $Q,*WNXYI*J GFO%]POH(ߢTseѲWT<0ƬF&v -FB&?iRa}4J[1Ez.GLLĞ=ܻͻqt[ C5>N]d`Q8nvr-[2o5ȓo";fQlk=V1s!&imI*VpdJ11GA[.T¨heg4UȔ(1qK2g ] D`~K+U։SSZcIӚ\ܬu>B|59F$x8X3=,'(SOHc\|(6sZ3ʕqpǒ:Uh!v%,z8)Js [1I9(zdŅvj>i"eې4NRcy ;j@6WIF})Gbc-I * ̜!+(+oe#BC]Ѧ K*`R/}Wq 9W|.<(8"d譙PɕLU+Y/|d^+¬[I,$ےB L -W aҏe - -/AxC!A]U8VwC !oOsᓗCj%\B[.|&HpxQ3t+d`X)dVǩ -4+GNIeΥ3I bDI `xV4HE=sJeg %h>g8H\;nZne3ٙYL4tR9%\UصءIT|>T~>T*YIxYq;gn0+)["|=8:W4nDL_BQI>lC$;w$tд;#/%~ԥbœoG_0;hAr^9\o'“ -QWT &?H8 5`RoF9Hl Iev#Y B\j'ADUÒCTG|z!VXV."=X>w 8Fi$RJ[JW|_poe-v8GO| !v6*,W`-eIMu_HBRg_T]$:߆6P8|!= -IEmla˴Rvg *t-#dD| n1w81ge/$R`i qE8+e^2e[5ע>)~-~(i"^Yш*W#CJ '~L#?ϸKOՀhJu38IL/EQYɭ@E'R]3D"8Y!.=cVL7I/n[s .O99s}?B8AMQ2z݋RYajQ2u?Oύ}$)%M*CE[Br3:[!Q䗩rd(0-.J9*&rcDC +R1cţߑt0w7E%IKMj7؄^hqIj!HBT (M)j@%$q)dFF! TSF&7DaSGT'S= k -!#.ZUzZi)Vy ~TD܄tYx w &:GX~i+;$2d9&Ee'i! VAY)VQAEṑHTjŭQaW]2Ĭ HdffT7G2ydVgVZ*=IHyt 뢹rq=*7 ~&\jYP0ⓖE -j/P݉2L8zt?PsiFOIqK&W'5!4ĥj(YQ( -XվUPdlV@Dw<%ߜbF=EQ30YUJY9A}7 -jO*ijI[9p>;2U%Dc&ƴ0'NˎcyB *¥$ @k[ _K@(w˚fIP},PǼ(gn$:PIc㘊O0gT@t;)-",$U 1=ȗN/m3x6i^dMf., &b HdIЊQHğ5\j"Gs`c~\|P$=Dz8N4jsM$PJq^GY׈Ҡ4ptkO -} -%EEWg8չƩA]xnY!gŐIYW)¨{D*$q F'wc2xL=h+)WXQ/ Gf[4%XCJ損Pa^04yB -3ٙĊL$-8hRdwrzjsKlWMxI)v3d>J3CnYvܼhv 9z|`1m -`N_7y:x5uQO`̚($C#Q&fMΆuEXRQ`L{Y2gI@a!cհѧK-Kdr08OOQ[ptNL]6,J`I#ĕu=ADQL/@^I|ſGc!qʦL{>ggf0.JR]rOԸR]w 5{i, X]ź G+)%k\i"5(&)TJ' P^f/E Qԍ:5Kk5gPﲑFL55[!_DhXW] ă'$^υyt!D8 -YOlOEa)J2jBޘgA^($p$轕dpeՊłd ec9d_ -PHEw21hH#u;(DMv,ٓ-  ;IdgzW{8C@_al=0` eC<+ܟkR<0pg3"L2bgCh49r0GGu'x,Z0y2jLղ=;(fnzӸWl88@-aF`"Z̵I6$py30ܚ8oDXr82Ռ~Zq&nMDt 0ng=ܞq) l  {Fn^ -4Zc[,kl\bI+(5Sgw!9~qpT40c/[FbʞaRE7kg~D8ࡉ1`V aKb%J*c9dм(=L N7"Lbu9%6W`_ -2Ar+Pj#؂\͍i&YS~IY6e mCۨjk!atZ=x9[m5{z`fyv>C',|du2M JOjߑfI b಴^˥ǐ%0~VjA`Xl_nZCu$ (f_Ŝr}A'V e쳄mgB+ |%v1_#NjJم10tfW -'-L#!<؍IMMΪn0ǟ` cu - n-K#!!?FT 4ISiAKXZ^!TztAwJ6:7~:*GCb!1; 8[]>mS*|)겍@ .(W <:; :զ3 -h8qML(=\2)@xYȫ3{!n ؿ? -mD=ߞ+#QZ)N,czA-\7t6lN B{7qes P)|ïMCcK-8>4 34Lh=^Q HW,7)ӣ3?P8 -!FRd% Hi&v * r*Y6zELS "XG}v `ͿMA7R-̫{f4-?BEf/3~bpGhvcPS|]rߘd 2J9|J6 -m!Dr< K9o)ζ !~H㓃6-$ж|PN *>q<QRpQU?9/amK,?hca&ť6htKwO- G -U!|j l_p$7G:j'Rdq! U~~־ Em;np A*&<8'cQiTr[LmG@^J).bf_yXz|+ǞaV'%Sf'\<]1)fv=%LVe%wb$T*돐Cʉ뱉|^@r|,9Ff g*;7;v-n9,Vl(Z5420 2( νD͗8Q)XE4}<ZM7Jh]5y曲71RddYl=yEpw_L!;!N?P Gk6H~)H$(Ңa~=ЋorUO _7t~o }\vS)uIMaSw }҆߇ORޞ @1Bʰ'p PےZ(<A[lg ym <FumI ! ~mm.?pٓ~4袽H 22w΍kDSRꢺP)a戀~>$4B;>P_]{|WG(tϐf(r>Rǜgˎڵko -nSVfNMԴG<qҘ<35\Ҏ]5ۖt0ɰB;/1'YV4%AN$ErrR:u`+R_.5luG&Bv.̯iUsh_jә!f?PzE2U|\WFU:wX#= -ψ5vW=JEאd;3]助Q2ztN+_`}{"}4*T*QГB~_d)봳4Fړ{f) $yضi9ؿ`W@hMZ,[P B0@Z,a$|3Y4st(?~N!$s0 ͵ -ku{aR9'tv5e -K'S>!1=@tPWfl;Mu8Z]oTXó.?@i d30P^6@(AG`Se'.`+V]ˠ^B|,r:ҢcI]^_T6 tzU{9S5L-H AFce5GiH x7)G~9ї{>&0 -?:2˴elX,&6IGt&s۠qJr6:Z$Q6D0N{+X POM#; -g +2㧢Ѓ^Nω*0sʤ>G VrljIfmP%YU, yB h;+bPi,hvC%5x,=V_Kdt5첷f0L6OL:l[Wk"qƼre̋899C!)Bqpu»!^ҵ HAbB?Ww7 lgHH BW3|[(@"UZK!餍m=V̥Wx`p}{]$B~EjQf9!jBt<ފI \=)GW(fKQŲg.+sC?#Du>zoh.0C0i˔ē&鈀¤|3nJ `.GVk>nX9l -@0*'7v ?b՘ᒏZa6`P"̎垔]Ur'2Q ΨWDH B%}C[7c"))C_Fgl9B s^op"T6 XEoIdtĄ N"'L|[R=8;by!MRZLk&}9EoB1ws44&䩡4"t}n U (as!Sf6CeU3<  3ޖ,8aEv~@3ڑ*BDzL%gƟS mA92@E h1tzE-h 6= |^qD*bT`[BDˋM9W._ Cgz#_iYn!T{Z^ =xYahT<RQ{%"'M86P,vKnr-XJTi2sh2Gaɳj͇Gy6N -]l뫜%[U>C 8L޷8d8(s7QF (a6Ķ)2Li(X -G8x^+g+)}ǯxeQ@!= - X{3Y=aYLRIN+v\)3a +i, -MH^ƒd_w)sC~IPLzfW$dm&*n뫜ThN*m6RT)RŠc U>1n=PKG{ah̼r #moaN#Yl粄~ 术I_3gX Xus]|fAJ̗վ -8bʋң9rK֚FHU[O]Ad xހ?7L|' 8e%1`KQ,S -JytwFWr-ԼgT9ǁ77 MVDFO%a=WV8s{9DUpfB)R|N9E.6݊'5&%5*G*عJ pm}jHg/!I̼޸rqޕ9TVr(zD+="F$qG9mFpU,T·T3s1He>w#,fY\5Whߘ ˆYȽ ^(T=z sUU(K5ڟ 0F9RՆ ă蜉p{nakS-V'q˂Iɧ] -o}un`׵\YZHiQסostqRj{6aΟȡ1K mFvʫRBP%D-Ά9 xg - &\[SYg?Q] {I>xu/e|ڱOBɪXq*3o-D:ET]p?BtuG41E,4 Ż}d()#գKv66o -OX@(X8bZgw@C!'AQ{`w+9qVr6%}L -u I)#Eq&GUӒJCxzC>s4fYHx{DdǒԱ p\lwMgn0.PQV<S72=rj륯pTխQd:35k3;wPgRlx _lBGR׼:TbOqZno6A&F?FyP+Ytg3dk5^l4xSy`áͤWbw}5m:OX:If\i,o%6H2_57b;s7Z~C 8 Or;UMg,{2rfc:Sm?VXY0;ܾOX@u2M,6ª3e3xc 9YxAEIA`YZh`aQ+I?exBMZ0d 8ܾ !I{D>2@T:sYvpDvF>"'oz3ι0\2;zc@fC!!.(zUsF)WcќpCG`GRs^9(-J\s -7\%`7Ľ?#O MMV>żyH|+D3Sry,$GyЀ@@ceJ% =5tn+C'P|oUa$4{,g0 t _}8PHG PbΛS$@Ǣ*_A%$I)rmD1׎ʶFe^;^F‘|ȫ|7B<쉀.-- -AQe|(UXjno*I!CuԐEVAd\d[V%$M-V;C <36لdi$s̳8ȅ(ߧ5y- dnѽW "^m1$d,_zDD9Ƕ>4#XhD;uܦ$Ks9MGjToRn_Ds??O?ӿOO?OOO޾ͭ0'Ϸ`<១6`V1g ܪn17Zr7ŭa~|f{ 1S=V!osT^!<!# +I#*6(g}|щ|BȺb+[)>$;fqZvBIUsB0W0HQLslT Rg/?/zKԾ{#'A =fy_ݧo(?[.&ʧ}! 틐09 -a__$Z_ )Y=LٞG}okzJWGz)o#z>HBd|垦2oC?W-O˷<#Iʀ~6Ĵxm]<4O.)s6Wl[KG'xoξH}'Wݻp+Avos.~tqs6=| s~wB~Xryw6gj˓rmŎ"0ۂ} xf;'BOu>=hKPE:t{o/u1#D.;q2]N jendYG!,aq[m L1}QRP.C̲|>-tyahJG8402~~'woy| DQ?% 1QSnduh5OhZRQ9 -+t/ksG55x/FQ6J7lq((Π4~8/moct: .? /d`!_>+/Bnz1uNi[s!˰!Afy[ _~V۶ܱ[Y9nQ2L^ї!#W~G/ݮ5(}O%0"(߲oyX3Đ|C7!Dn4\m&ᔿ NJa !X?YEŒf]"j7Fp;,=/u[{7.OG<[|0.Sy۶jpaEnZOnS -mҝZ<'Y yF~FIDpFżvp<})?m-)e 4fZjOmIm]SlScⴎne$FV厍 +m 4uΧ6~gq!?k,ݚ=Mˈ}?\]T8o/ND۰H|7QA1n{j+5[+53 یfӃKmip7bt-P>Z~rf /jk8Lp҄\vYExI4$WmhNsy fDa _3J.oh B}l2{噿A=͝9E d;9smsFn,ݤDK:fIJ+@mD͉2y!кn2xOX[S2k\<#c[ݕ]Se6JGy)'Pd+S^~ɇ% e 6G.nrx.ܞȔK{Y!_a 2 -(=NZK*#69ON/scu/scdiS^V遼>H,a Q{HlpEJ|0#Ƕ1R.^8|)YnIak4OgA*&-yY v'v>T 7{Aέ.,̂r=*f *Qxdw'`V@ Gƿ棞Km{V.d9+`b|:g2=%Ǜ/BmVC.L{w>zYQ0wŲlg^\ko>"ۇVx:?Qo -c؋1.qZ{Ɓ6C&im\#f>o2/r/l6kqiO{[rp҇R}brE -1mu0~[2Xtч-TbW~ǖ;meq3d-yɟ?R.TIb%ݦgY+ Du}ڻ=5S$Vk&zʴ=]=htoGX,2_޳Oωg*QYJT=oWWGjCUw.iWb Neۻf4>we8&]8MH,W?e9?e5iQ쀄˛+ TtU~1{`_y1h>q žËm^="㵔7Wb/UCl4,U39\UiT 9\}w%Ai:yȵtܭy{k4Frկ*`r0nW,ίle6'=25R(1\ί=`˄bRsˣA|<`amg_֯4E=C/XzHgH hl^HG4ڗW|r􂅜-kg/X@8Yx?` B4Zxsei+?&@L`9윆)96G?{Bd1@x.;}ZVlWe9>+=1}8R@=9u 7Lvί/|< k畈A,l?6zw  Fv]t;AWͨн_ikwY -v| p5!)ެ_(cթm8iƑOW!)Zcێ"Ηe+ۈʲInåi6V$JyWQ\aTBv|WL2'(k#_{ۆӒNEMaU!<`zZÈtz}:ӻ{2N?RC&u^;: #Ӎz ~{֙s>3fX@t:TIo۾B:)"Mڛ i3=oR^ádlz2}4=m:m)sQt;.(^^x~ۈ9ú9)ܛkԏnP0IZGn'6Ed6NқRnflV|->Ufa$.kOe\@ -G/϶-۾/qa։/`<:NBd,#'xrv+R$SwMf[l{lX ;ŶyMeU32l->L2 @1I)ļTn'L#-@n'LP`敍y8acNB&pGSj+Lo|`N8 O4 %ˉd:e9r%I W٥W{s1. wTbP4^xذe9 -G78oKBt^'EmzIF&KXe!II"M4֥"Drm/Me3,ߍ7_3 -=U YD[".s^Z)& 4rS0(50x&6T0)o6JlL('F 0%׷ ﺝ0mcu\ }ZRUn(Pb! ezү oڑN+Ey(4+΀2yxb~E2I3.ٺfSs.3SuVggOOLɟ!cqeC\R)paĔVM o -$ϮgH( ?ζD:oLF@ΗñIb+d64M /ME+P2 RAV49_\2P6ΧexT7CgDɥsiѦ -z>&jkҷϥY}^A -lWKl3K)᩼yXAA a[Wvݎ]fTɱS2:*;-%+]ێvқdDZmsZN$I= &;e2e0˪ƒm7~HS6-&"֛"wɷsmE=MD+ vƞ&މtܖ\GҍJ.ȔN\-"ZULe9%އnC9j=!QZBf%_ml!,Ů o1zಷ}!oٮG+jl]^KxZba9mHx?OBJgߙ ڮ-6< {yeslvO`6"<59K63+.!I͏O#B/%4o#B#FfWq[! B0A)4 $$ +tֈ*H̨k 6/] v E(YUm@{θ/ ׷Dc/6HugSj&-|y ^aDRf_NO -6S/R¿cT ?0hpj4 M͂//>3ı,Wr[y42i!:޼ec/h%ؠoAb7\~}Y2Pg-N:IG!U*@ܯUFg N9nS@!n h?[(voK@Rbv{J6Vy4N/G@j,#@@L:6}s8ZFrT0EX`Cɂ2{8;# Pn'@`բ\ޜgo'@ I/Hhu9N`u(*< r: ]oF|Vumu'o6J5g/@Q9yWmHr rU9&4QQx31&01ڪ Г) -9<t4)}gJ A+y6q2^~@E6䜁J})"ڰzO?(Z[h05WEWRX|xsӇĐv@vP@;|y\S9( -v3}hw!\y;y:RpiwG-FQ$>AiO; f|8@iy -6QdDZ$]w']ZsIߑ{Q j - ݟ&xv~ q(/jESSyQrlx}7.?."R7Z}EsD}iI҆@`?w7ට׋93t{GMmO?qJ=nj㯚E7Zy_(۝gYj۪f+}T $J,cۏoޛ MpWX) oqs]p| -TR͎%^=M]oӷjo U.7e樟ooTŀYl_pܺ6o&n@nﶞBr[O6аsCZicWm6~UDF6[=BhZ=՚^vTXH-g٦ZG.-u3 ,Y=TDht7|V%73sdKY[|7+Wnqq --j~0>f{ːYe=O2U5[ɲu͒l˹n'XdU1a2C/Tebʝ0@N|l+14(=djzui%,`}v{ ߬EEeU']:#ܼBb4꛻WE( k#"#Miu)vBC/.F/O׹eP()#pcqAW͸X.@)%C!&.,4@X@֗-BZs`xi/-c%gemLlY2-i\Klr/*y^f(zx]lvbwU,Р8C+xcj∐J۪|UQe"}}2@x.8D3m? :{[yOd!0"b{MEvh[L)W7g)f!5Mּ}G4 -uh&5ET!KT,~_O\.m51ށAhcmtiH/z;;QKcۆN^}@Q/x7 vX˵)̄GT&Md/6-O'}l0n+jmT5(l>=mԗrA z8 Qu58IuP]hI5_&H k28tFw[ roH*1;Lo~Gcin#SFdRK{352)_2Ůuys.b۱h@2*%џhE R yTߨ>i:@n!͆d 7!Tr+ng! /C!1~&okj>]Igz8 Yѐ,H0bϗO?Yv?Âm|4Rz=}ˋGOKZ%6={d,_~(K Ӟ/ӕCǻʷ$H/?x4r4inx켎N[r_%,O|,Lto wNQN 4,s/xѯq((۝S6))#oޗ Zownb@6WgDyXp=/7e[S`ӭ8c!LjJ -7MA'K( ۉ3_1 h_2) :Y%,iC:Oq7D/G]_Nb=@ $k5sכ"D"ߌ\EK /&46v'[xfc䑊chsi2JLI:5e99K)`9ˁS vOj3yӰ1Cx\DB( <ޗ`zb^<ʄc*fZGѹ@P0!Ӛ+5+ v:{_P;>whK90%ud`]թO.ѩa"ۊJ - LHxNb0,sjnWEaV֜9)DtM(6gQ{*hK4{U6^DI-1JصME˯sN -V4C}H~s#W0h.sZD&?B:?UXiZ m0!NBwcPv@ -TbOd[3FՊ+=DRי+u3Ϝ[)w@3x8JhL-hZG(uA) x%f }ZQKѹa7 Cjb.uv4v[XVb:R X| 8Eu˅WS͜7Mm:B(:7RfYYWG:gxsc¢Y@vM}*0^Fo -# ܄zMъYV!:{ac=ؗ5}y9eS,v"88@flf"jQ̕ED,Cp0dZђmM} K\qs:A"LeY5̶ׁs8ey+¥oq$Ґl;AdMwU~j+l.фeuVQ1ΐ–*1Ѓ,: 5$M̼ayhSZA)ex$@V9PGCc.huzE^8P0L8Or7A|ʯ͸ (/z7=m+J8U Ĥ@:|=20UNXD;e)+SkPg/ΡUE³Nyuy(dqI#z 7Efb'ZKN}K\j*h@>7,-k -.E[Afx^nFVQIh86|ƟcyϲR6icTPqtaijI#zkVRsa=?Cʤ%8L\{47T6^qj}h"JM`V%qmN=$@F!DFK%hT?$x!P1W^Sy/t b -BZB6 36tB(qj_JϑG/B'e~5~+եL%NglfHtng[̙6h>8ה)3a'ё0#z!'Luѷ)';f[4 uTʪL -&E"1@Ys'8˹B䮴jk_]%W_6J IRZ,u B&9 V5Wp9K[[[ZFVs ۩]M{mP?4|yI"lCN~|Jo}Q|S}hb(?y,1h]aŋ M!wH:PJq'!He1vumel~MO!=!?ΑqKGK -3 zOPBpc'Oj%`E6!h2&Q&ufv"b;ח]*\HhEi6v;z=[z-9#v-" -%MGZ`v;)T <k"AZT)}R5%gD (T!!=c_=fdpWBFD-݅4R0;J3J``\IV3km{e?Xu6WĄi TLQ-?SH_ˢDmEW2&,ނA.clӌӹ]p=Ѱ1Bz&AΑ ]!2yg];\Hy> s/ ø%46©hbj2.޾9l5-tZ7g:(q*m:VBYw}.C,'3D u6kSaT<(Y~2RC&ZI!s* 3G%Vi dԔC_5=z?ap*:=b_ PH& c:NmɽT`$ǖ *3maWBѰ &f)W̯F nyh8$Ա!k)la iFMkG#$H4VpTL{׼RRlPA ӎe8ڭ;ؼĚ'5ch5WL^{̢3|XD: X {bY-[|ܱm9.3 }cFԂ5$[bO8J 2챾 6DZsrQFNa)KfeN>SLLV6^9Q4"9Kf$-ܑkxaWk,uv e%Ѳm#9sY229dd< 5va͹#d@;זfe4{ uWIk_!}E)oV3!ORfFx \S) S/"yORRcjY;0*fDB (&6:%!G~/۹ 2g~%LIۿ ,1 Y^.Y:A+{/$HvX>ƕ9^AR,(Yβ! =5*݈vrɫ5i1nmeц̝Cns  ➿|ϯVZ`2~ o^LjQl"  Z+PG;6{F{ߔy=n,ߔs(_,uك,% 2as+P54$1Odkc#iIwntLىTsnCr!+>-,]r@!)\ -^C19+lIoy -4FDbe =;~[pp5WaBqrd (0]e/~1vm^젅@'U G8fSpud< tC- xm~l\E_#@ 8tSFaD/3vqaȸBp%36 J(m瘘q̰G6%|o60RkFJX,wfڢ /x N#;ٗ>qz<=J Kݒ9ߏoiA8CMuW.^{Q/YMsޅl~Y/˒# $Jit65Sև M(F:-1z9 EQ(cH&4Cl~\; -bP~lhґ Z#;[ ͊pm/,:%f'4h5H͋G,NC:l؇(5ۙFh -Bj hP3N -dM#/P\p7DHq F +4| gJyk52=c -{n=qXF$_q[V(Ʀ@e}CUz =PitSAfɴ ]MzT4=ۻvMM|)i`XXIc9!r;Iٱ\RfWt*_Q#-`m` 6ъ]W?.HNF:='K,M{=1b|FiLzhRSئxm`uyΦ~#d8֍yhi)Z2(.kTˏ/IcXU]pz:uiP/\b8WerP\bs:L!;ju?֒Qb2~),h|K:-IoX6Vu @)YhjXX, e8 8;Nbߊj"^pCH4\案,Z%:/dXu몐C.:G2J/BlQ GhXbAE@ OD\?`FsK_` ̀<"v=pzQKᕗ/BI4Vt6qL5?P`[SwҔ~PͰ-]%xLQ.-]UBҘq*SE I& -q IqԍzCPt+eBSABOz!yI/D"]R)pQ;Y AFRW  @mHGcc5RH|hv }9KIN0nA-دVH K },x8y)\&I]3\.g& wDD`;aiL; -mR!gotF:U DZؼ:;˾#[_sgb@.L?DCbK43o/]jU&ӒHzﲂ>?=Y+#dK/rx+ ,^k=_,JP4g%hS_%HMxU ഗgfIPIЃJ8\x̀?mq?̺۫x[ul7:2߯T -Bn/\qgXSpҁ ܄[ 4{|?M0"q^F 抌8{^%f'}QiXom=0缊ױ,o=b%zr̀ܶC-S "=ܨ5yhaJ/ҕEbd>A5{tC.rudp 뛨:Q.]ak]ݤU^Wh/Iy`^#(8yr-X06񎖾[`YK"LW*X7k/-%A(` -3@ pa3a.@${.0N {W6Q:4{B8Gyd&.Z!hEdE "<-5)D%jAL/rU ^b9" ߏb`T)o,/r:uDƽvp`0-9 elO%=Q)@DQ9o]2ywz)1q'9I`CX#C45JFklm]N$:'^W ]G#&p6zJl*#??Jwp! wHA ¹sZ'B8x}=HkrDԋpo) '6'Q*J@'r2X - -g{BHkKi&-Ez\ %@O9GӤ}5>>}C=qu0ˁ_wwr6EqTjD;=:7nmai_uVi9Ic|ǜH0QF~T&BqW丹KCCȏ1ѵ6<%;>6Sb#B pf:IΜp}A⇸"#X=9/9Rt D19M=Q\+,w(M@mr՜דșRS$\;I-zkpTqiVZ?|+{ ڱv"@`ZPߤ"[yAt8#H¤HU\{}G\R&aaVYRBNN_jо5KKUo?k) 3a;֩6{"ͽZեo9X}N+)$gϺ:(>ǽWJ odQDe-!ʽ(g&8mW@2zIǸkQemG* {=E8k\a9L J 4JZCf_xxHbפpQl8Kqe@}eO&lc= -fsii1v)\ 'ÿ^KSD'g,pD%CP A:"DRo`g{QuJzj*9]05ԃ i.2 "|\ 6+s=<sT3cM4rHc+Ňnؕ;jdRZnn B[a$ʪTwJToo~cZ;|Qfp998+Cru/F|"P 4lt+ 5Ab|Mj7RRhg!/7|]dr\AQSjo<?mޚXGdUd~N~]rpQb?¬M5ucIw)7f^1yga\oq {Ѵi{c _"[Kw=PC6* -x=1F_CvcRhd-gT'jm$+9/SH_W9ToĂVB -2 /`0@xTJxgTH/ϠLC4}V0 $$kX0\,ZXWk %I!@LwEHP\η>LK٫uv5x|B05Nޏ[4`BDIL9^0t -?dd4d|n:DtN߱q-GZN)HTk) W04JU* fZ 﫻Mֱ8sVu5O5-Dzᣌ(vP9)T6Tx.'/-/w$`ok՝Pv0(0^!uƂi -zWi+&## mQ2  S8=b8 m؅3@r^ŝ]=䒣$:Q׾@^014~]ԃDi l]%'U`0I,#;uG=r%w|0[t@'G*w63OuZpʁhQCW -> ԡ3˭l7I|m -JT ) )F^dEIRU=b9_EJ#C1#EzөqB(Hs9ԢMΔ(%7nzF+qbr6b؂Mλ0 !܊@ɍT:/M -ra5qQ9!J)7ԫHwSe{\g* gIᠺK`f(MH~ KT* e~ ı FӔ@ !Bg^& -ux5x EZ xY#4!A,# |"u5n#7 JAU}r)^[xbU=13e -OJCDA BG8Z;0J(N5䝖9b-'n;Jj׫# ^Fw"ʾ^%SsX)HW=lTS[D9Pt2 D2Ca"јiK&Fg`])qLKVa!%u l){$9.`1b sߥPx!(=ٗ#*`זej1&0Շ"PA:ɘOjgTϱ%BSEےװc#PΌSdlb|?eV&NƣC̽S#(F7YyQ3be={(0JmQ9+_\,UkZptw2l#NNXPcՄ72.Nw;[;)?+aQKHAWO=.fJ Xcy]2)/e !$2֯$gX4P/28 +\Ӎ{U0]n8Sݏ *IT'bS,C#ߢ =lm5t=RO5 [=8xK n\F[Z`(o%o+=|q,Ӏ7~1n:Y 3ڵ;@(݋~U0Fc.6@1UXfa9a(@>p w^oD&bp2ɋ.qc&ˢkZ)INV#MuU7(XÅ#NтH~D{Rr2֠ghjկd(qP 1@N1:m6U[2=닿c)s!ș(rw -4yeMrj5@qhcP/Pj(!C.SI*\d 4oe.PسW,݆ɘ%V=epnWg"2+p 9j\eD!i5ӞVP#z JT -5vE4Lft{V,cnwE* 6l9#SSh9$VXC\b) ۪`Y[/Yu 9YA\šnb'jI h/i/6D@iJC@%Z=VĉæK?{CWC` W\=(G%B[JI;9UzٰH҈[bInh`Mw;ȬyUK͓ ۸p^-.+D|JINw !жSʝuZ\1@yc!M2 -xطh^wCe [= -ɏW)YMDRyD$Ujsn$y.f߱Y/f&`xq-dӫ|sM`\ u +P{''"@l'D m -L"ќ mاEm=NFI -w(!Mj챙}ǜc-~SX@ܯrN9f;ЃPF/[vHlzmj܉`v<´B'Tdz s{b/K~'qwsT!]OL~2#eȓ v"Kb^LOF(wF H-Tԋ<$F؂ĽĴ\Pn)e}S jX2|()F'`2B{ /&O*k\6cͻ-Rpa+6K*lIQ\"2BSV^bi(aϳ% -M\V)!d!B'h|ԍ(B -,MQִ*R4!M,K x !rH<$@ H Li"S4zc9);{ړ]\0?])%{}ZIa9w@j 잤"v* Xl[B}!֦^uGT77hœޙ%ǜ2n QW7)7Ȏ,5mpa\~EOxzOM1gfFL]b$d`ثN[|[3LA5Pkcdh SDʂ6L]PGp rZu"9@^M A!Wuu1EPI7" Hc`Tv2X6&c¸ė-5Gf{%S=AUrKҰ5X-vlrRu*zH -e_iZ0{ -;kAje_) 'E\3P3q7BzJo4K[k)hk3$':oIQ(!r\!:!L[1^x\1@ -M!"(cT|˱H {#K=?Cz~I%Yy„f+K<lyC< Z쁎M+oLjFSWU~!+].TrZaՇJ79+A-Rryf4?!R)S:c'"fllxmBb!بÉe4("rRt.D1rG}$?_uYgKH:sSIpfBV҄5[e!d^L1`6^J̛,n->.=X=e10&Rg>b--`|;`>40VIA(KHݣ+1iڥWWy -+Ib˩;B}Jh;O/ i~eRBdrR8cHA=X$4Y\L>("D7y+.mT>,Hނ<$#ju{"eɷp`a%׌ƜAOD7=X&޹n$O.qb{؃D iRoRl6Cկ @YU.`Qd6`{e""R>AiWsXAm2^D!9jrZ@&QHx|`7%_Jx(z)~,ȭ>vw5{Vv|s8`Q4h[lO[A( dO_ijFA^΃+2݋Mu5]D endstream endobj 26 0 obj <>stream -hFux(cŻ,ыqyh -.GQSC -ѫ!dPPfD2ӍWUЈw[H8K{hzH#0'V%s_DnQ|?$D *#U..yr(mȺּqmvU~WMfd)~u[%ggKe$pKN!p1Wﲩu͎>?ţD^T_ߟߨ>_#Mn1أ#ܤ#0sJ\ -Tct9\\"$H"=lRW| 9 iŃS] lB*.=R|>U=h \<pᤗLxt!\>GF$ H/$b51h3Ov~?`Co9K7-,K"j5w])r#[IC ~DS[Esx#U1tuEg:ԁsEzv` -d] n*T_:Je)CI[uŅeZ5KZ"@R]ZVw{NhA:tW?(r9$ӂ7y^MӖKboMh -v9ٛ+?M)dD_uO瑒90{q8v_m#DdӤf"qdTPv\ud4|*b/K=O769X7+!m)M0%)$Ԭ~xuٮPOz ~c"8h; Y%l&f;G6lI{~hb iyCuKۖ"`{u c:6|*PK!tʧ !2O&tUwa7Cߣ n &QUa*3w$W߯XlmA -i;=&GaށRµ%܊%ރfGjǯ5} Xa@vuS +ᐰ1PWy-_C}7t`TdҘn6#-#*2^Z=qlȿi3砺L!OfA -͍M-8ŐKƍ)I;JΑ}\.=]տE~+4PKUWmpqKAJD'Cu86 ^J'Vq}bH0RVXGJy -{L-,;B/;@=W3^RMKy1Mצ* ۏgGoT/9lFNR E]RWWЯ-S@}Џ(;b!g)YéԜ)t&5?o7ﳐ̘jL%I-,űw,>ʗF+@s-I3iLŎ܂Xmܾpąo<(ry>v>gqwv2P:z,)v\Cn%n&܏A@Ra;6qSu Bo6 _ʤp5}NnpGaYInvA@Φ9_hh7!Hmz($*PP|) MޏHua]o/NS@BmdvK/E) -yu^඗覙 .p0ir0dUzQ`(lY; 1x#p63Y4]0ʤnId̃+23ҟ)*e~j6꯳k)?s3Ik2ih{5~'Z;F#Ě:/ɭ2ЇkAɟ!g'wG[/d[^ 23[-%} ȗHKݰ_ -~)>Ct<8 jΰ(H*ֶv͎vP$G.d~UWKc -|? -oHDiQ`)Y_awxupKTff>">B5 8Ԝ.ݗFcQ>`EjR; Bg1|RmCe!Vodz=$.a)lm#(RRA7s^vۄi/9X -)׋ͬ`MA5}8=  [/4`C* )_K)ч4^a@7Wvܱg\J䙺@a=[m 6Z|pE0X,k2YԞ&Gi\?.;|vX[x= -E1_ :Pv:k;W/$M;~ZF"E K嶄5;vaCTn5WXA] m$hv 95B@\"pm{ldEA{0-%C6GwĞՕ؏:\-Q!P~`x6<QRaD6AηgZ$x0Ym]>p :X2X+y5*Uռb9IcR B&'QtbTj{PQi0 CH]I+Ps= ˫!VYby˓{\7Ce )!9P?ڟ&ܾv4&i.&'aY5*,o2cBC,ޛ_ғ<Òk\хԀّV H6+Lb{*S}G-I"SjztSM2yR=Xa?cbg#/l$ow\crb!I5޽%xL^e(DBJsUGoF2ILn]譒6%'݃\}q$UCυ(RYdKMU"!s|PfBC+UQJi#_7T 'ЎIV :Rm^\ujiKCV+|q$e4aaAY#;pipR=Hh3z-;=YZ+I YK(uڊHd¼١o"&aQ ڑk!@ҟMCT "qpgG.7G2Zz`o ;OK9Apj?19-iP={ 0+|HQwP9o3y`Ҏ wz *xWS;]w(ˠr.zڦ ^+&} OP%q9@IPE,Nרw4硦I1S@v`BbRn%Z@0'эHCaf`XƢ_D*?x{(j~27 121X$3 O*zQBHrPL(Ӑm p |Zo2i(-qgd`=r$&E,ɉQS{%P[(#N7l F!գ bbJ!F携ZW* VŒ#k80\H!Q%8L4ນ\ 47K+헓:P%_ZPLg)YVݸ}o#YN@O5U"w(9=RAdO5zF1n>I-,T!3B76cp<@!5Y{/H4/z}I&b鹯jI-Q2*ôH8KJ}B8= k, Yl"ZCa2Lw[r~"!2nfj)-5HcJg]m{u&恹;Q,fڹ`-~gb+/C&L]r~‘ʕP M܄eSU=CkC{oT\J%U(d!aIn;W/9(W{Kh;x!# #\W(C0Kþ#sYSp@hc"/P۪8.;)&W\CE(l%9 -*sEKV3Q).I/i -|ĥdlVFf6\n=|Gy'45%tX;wnb$(.&:n *r1U"K%!H.^*ݳac?]1N#29NҼ‰"/!ds4unxr A T:@mAv"{.tkuMUP(2~oq؂ 5J:jVgH4xDlSĞQ۸Uسt>%(SIJÍ6O\(&BZaE @3RzĮ")CbJS6ݸ?`*jJJ#䀷 -̻t}7݁ᑯ-xɺsUlw  HX a?=7z-#jnFC,0 -8A`b0G`K/R1)w\Sy>K -bwd~k[ Pq VyAt1$DHR}Pcn⒟j@[3w,)S3-۪ʑ!?qNh@=zP̶ |OgVKa/G-D^ 9~8?ؕO x*L+(&q5X'wU_R:(G(5NJ9Yt@?~il?ǵj`I2XZ>YLoaKPpo&EK{\$鶷Q;<4!^OEF?_)0ՒK"XU~HsEgnpv! Gq nlWµK`n ܠ|Yg[J+0JkP 5{lDM_%]7<صʡ8R߃b #+h\Sbc}ΕN7،Y%n({s.Bݷ2L@}v0 `J%$1fACfP1 Qo$ofk9KR ̠ab'i>eiq>l04k7GȎ~pwh/_.,{sʛ4Y 鰽~vAq~9'`wn%U E%$#mw!q>V вO8WLE (\,?!KdƁ/Y+A OZͨ~([ -XͣJF ePlIHC()PV>}ciuT -ʉ.1&t 'Lɴ-Ety/$Y@`k?R=T~( ,TmѪʯF{Ԭk&5T~+*$upqX+|G-CY"G3w/_d!/PɄr9m'2G -B)1aj^($ !@*%(OGWFL[RM-;=J TD2zh,|y -/P+Hx!v`^cAЀ%P[#Ģg 36:S9Fpa7Xo ' 0ako%m<\C=;ƃ6krƬ 񊬪0C301N<-}|<(RR+~!Ȇ렰"o:5K<$K:0FEBAA}^xÍW|@L`3RÓBG8_v-> ^ H4=3(S$D롪fHI6Nľ_^OOWDvA2ܑlх9!A'*UGqF^{C!b{ϩnRlCI9$րfy3Bl!E)M;@iiD$`S0vr-I@ek2lBs4^%xUBRd,VjP·\$N.K-2Oe-VB (XHBH9_ag8_[5M՞ȤP6ܞe-!xjI m^uMN52Q΋%NB9$>㼬+*jCN/K^IW g( - lO͌(e \l<}K.&e' wqx{`uJh|5ǶTK?6/0]KMrv$1` 7idx -Ri/}R fA噀.sDX0(&uґwMhb,F̻yF s4A:ɥ 1GJC6{ 2ʜ!Ez6Kꏑ:v -͚d*fk/p#0m *GdjF*%({Q4 8Ƶ[k,v* t٦â̧ol^` CBTc|Wā:`ԉ^s0fߗFj(0j!дD420PR&YxOC@jIz4@"i;Iդg-7R(HrI逮 N QOSULlj<%!t_@N$@('7d_;=f&T*%Ca%E%SXE匨LSvn_ImkC?.m. ~X?`Z\O -^t|v%ugK*k8#s tt] -Rl䰊 _CV8Cᤴ:h%qu?__0CQZ$MwV?љւE{їn@ޥyb݈.rDPc3mwܢfT>A)dk0/G{29Wܷ&n= -ZhT"ہLOszqe_Y/  *ɰ-GPQwݾ;9HĪ7 X9DjՔ훹2̓¨I\sl<.Y,w"{ΗS -ؿ\/r] XROjd:4*pxu}TQϢ@א\-G^ bCXBô8L) ;(,\5вw!`o^i,u(eB +ZԂ'"Ԙd n g$̂ȄDP;17-RJYz$1D,~%Fi⑭A]3ܡ?7T/_1$"98*⮴_r{_WK88D@J`+x?gWs\߯YPbCYv~l'mˢ$D]JCR\RN -xmsG"IC%v7-Edဧd$ ʟO|eH%(g9";A}$f;dogDo"{ߐ$ -T Lq?DG६׶(#%}î_UF=jo?K#kELlL@>T@# -1{Ű{I|ҫ1͆Uң J0jΣF!Tk|(Š$RLg͈7 zS=*$u*@ %էiu䡁]ڋ I6(SY)b;w<|3pHW cU0&9~H2FLTLFTZ)c'M{HHm=+u!? $J$"f&G 6>[w? !Q\QE')TX^7pHz;@w[$4Jr{ŷ6wrMk-HE'- )Z+)nRf^Zj5ɠ%bFr:L :if=-Xޛ$NQ`?uu?j֎ $Z,42t`M@zz}U]BeNG1vmVU=ˣoI,4?F"uW?5@ ],='8zcbN'}}YM!u]8SVqF\Z21DQ a^IP; WfX9SʋI1ō`ygǐ=U|{x;gx9铧- -)A&Uv4;+I2\a9N|q I@de]y]VnWI ' ^쾉gtfDZ)Vċ!Lwzln -[gѐT QAf@I(E_+,9=q3K΀87zU_ C1א%9i ʁX+/2뼔'cO]xŘM,*)tqV%&0ij 2qd4?3Ե kPޯL}~34+S`v -ȝYRl4w4% ySG%uz+߁҆W'1 q>n\Qaر'E8◙,G2=x5C"`_qeD~IK"fm)'5}n'5kl#e^tˏJ0ebŧG<.F8(]!3u<yF Ug.FN܆]16RV -@V빃.+H({T;f$[Pi/]&$U׮eU-#c J'v@T2XPviQ4z &=B} ʋ Q﷟M-Il?$A޹.C{T>85^F//Vvl&Tgs&E6 -!]Ԁ!Kn1%>?^.O4ChcƙtOaYÁɦ6et,S.%Bd剅ap~$,ݻdf"aGIP"w-< -Vk/MKBN@"s:T}ܕLS-faG~IGl8д$yi~~r -ݓVN6c{om~اzXf?dT&/R.b_%~5EZ`FSUbp;'q(uɲ,~TTo2MA-$DrX{,\Ҝ8-9}%lCfڙ%}z8:OboG$2]ΐd'A>Ȅjp`aT8b[6|`َMA;> -ET^W?юs]g'P~0qoaGꦀlRA3`IV|Fp,N<J!HshNDjͰRWj:cJEyHt$Ԋ"?ïMe ~3Lb PM{ -E]<5R幈ZSQՖwJXsИe|[cI(X tJ5'CAfPP [l, 3a,C Q| A9'%6-C0lM!YQ}QM7vU׳xcX}u9IbKNtոTW+A}áA2TA* ,Pbf`w/r֪̒\/B++ꥃځ$,[rR#xlIk)gmӞ |6 O#~ 8CGf&X*i-Hb4{fLM3H|zij3ّ1CaT-'DnǛ|c"][+?2N`HOt ץ3ќʆk4%MB*7֛} Jpy'jyn$sf*JQ a!jz1n r@3 &H q@$@3ULiS3ڗ/um_K-ݟXi{ -]+nah2A8t$[en2 BK%.kg(PIӁn*_xEұ W )?uɶCZdJu|^(8fQwGO%bR 2Qe'4)Chv,ě/C( C mbp @"~J%EMHctKTxhxN`dC -Hpٽ QW`Kع~¥ƾj!=fZDKOLY88ntqZ9m-knj^%L_vH) Rv$~_ʗ ׀ޖT@KL_ho/<%XE> f埭[M)b]LnC@ 7"AdžbqXjv|['6i1"qvQK+2˦ -BV -40[ J# 0 .t:ܫ)s Jt1&}ZuIfD*K)ME4>b&/*'☣lX
s.5pu;Y1R[y\{dV\0- 1:MPma~{Mo Y_`V$mɇp -+f]uײc ,"tPYM]Yʬn@y_:ph{]1J`cC!ֽ+J5ʒ{62@+`F;x@ۙ+6"!U@eԲdw -.;m^K:VJev|8{iA=_̣jxD,j)[KeIG67L-L3nޑBr`&SRW>`bnb'ThZq:cJ])(-PtIqn_Ot:DX)pMLF83iVj(oj}"qBL"6(+)V.aDW(h^TIB JډO~bcng#ܔrhT~]j|A({ڭ #Nb*.w2ETͨy<ɳnNbĊNmOVj=w֞9KUu+hjʅ`J⬆yUR@E$V<|nV/•Ϋ\Xu2[O8=@uiɆ}Vjp'RCRFXkﰠ"m%IQ-W("Qa -=G}:bEWa"n#2+5x!Kxfs?r9#:ň*Bʴ~0Ywlazx d<f[k9YV!F 8nq:@BogwT/ovsAK ʣ*mH& T>/FR|0bH{:Wߵry9S"s%/:()uK*dtg-W&=W`}̭r ܦҥ| 8J( ̏$R -$u,u^]yB}jB(b!Tn[} xBz5zX.ϸ* -CNőudY~"A܎[]iGدs:Dwأ &[eͶ@|=>h<;8^W0]ቼ9h(Qm()9 BEut&OCrDp_kF&L/I6S4eDz-+nK;o[)N3)o?Oo7o?7?~W~?wo_ =~'w?t}O_?n7ww?[j~|lS~ x͇WW귿Sο?ovʻ̑#㷿m_\oRq+/u{ko_W_6Ibq"~{!yYb 퇓L9o_$pw rIXZEYu%d;D!KG Zį Qû{^_}! ;=SE#*j|э7q|ظ||~su`HsXq*dyr#d ~ -wGJ?uBqʛ~pRqNtsx`3wD9~FWϗ|X%H )8,*eZ=/'` ^ |WC+C4ƙ 58A8-?=eh pv{jJ4x~'􍞍D<_/b9وN4ӈ[ st:<&3>Ύ+ GZ< -2ʖ?O+h\IiΩ|D4q ĝz En. Fˢq8X7y_ Qx^xb+U9+=#-uij^ -NPO5Ϗw'xyibYqiTUNSW [6^> k|ncw^A=;~f<8Wk'1{`jqĸrN %Kgh1C[ _ @U>0٫zvdU/55daL373Zk;ss;I?}ICco|>b 9<+9 {QU+LDi1֕8o~ξ1()w^A`ިE^k\"ƙOA I:o=!'1 5rN< -HٯUO*P\A1&12\3H=X8i+x)ΉFE[#*M& '8sŁ3h( _7`+3L76s>s\?)zXqa ~W]^}bm6_m{%q8CWϛw(;_w!T4kFعz7tsyi U||^_1c8|e79yL%zs#'qS4"2v(_ -ԝ<'ڮ◰;F8"xHD#hl2cѲohDcobfSkzij'#P_~- j)Y{Uc_?.׻95]9pz/aM4fј>l'YZNWl n9TpǷ &kjmB9mnbcl7;UF9|PhuA#)ϙJ?gs=ƹvD}'%WnV$[h9.7K73iO+4W+rNGNHŧ%z\ֽ\g >}DJ!oaBܾeEh^ y)϶ʟKcȠdg}|%ϔZ(4휠?DdPN^s;>2N$˒["ª -p=k}+ɸ|\@zyظgA)9a;)ۻ:2=zyDL]2,vO(R;A:Cɷ6X4^H_2|1;J j>ypc.o$lONPy邲3crx3{Oc ly/WS (@dڟ/R&V5VFDU+bIgX=x_9Zjفb~LNPW_$lFd/ +˵w<Ӳxv}ˍٜ#yVmą P"n#{yEL5q~yYk[Wl'Z։FLJAԒnrc;  LJ}/ o :^FMgcN4QŤu Z8YryLEWR6"쨈O{u裝v{sWzYcjGXep4SF;\Nl (l|V;.kn,R\h#$(|HgW}G'oyZ)g:kW{[FI)_"N ]9 u-f|6.,ˏ##kq.9/,CS̬S`؞}0jMAK)Ol<)೾ )O}`F㨙.( Y#f>1`?_؅Iگ^Y9аcOQߥl_!+O سc|2Tc\]40Yj%'w;6~4`AP':nZ7's4 oF{k kE.@Qys^9zoyhfSoQŬ}YKɾbFEۈ)NrSvO3hgnH0/|x9@try9g/4ڍ̜j PU8H3 -"w*iiVk.0x^iyȭ-?+<IW ֿ udew]q[/йfc;1o}v6%>69dN(<V{'n7{?gsԅGE˷<$fGg3}Ŵg@9끷wqs20%{w ެZb09cV-.(?tVghgKT!l}i9`PxR,w*;:+#y fF+y#X NNsͧp+Ϲ9|Z<\$UN4A -E!jYES̩2} |FvL4=b :Hrn<]½+u/ZS0YFZDQ$i8MMjq_Q}QO3S O ߍQB30\R n(23 W9\!ٕ-͑''Fz]}9^տSA6'"#|2EĉM Oj'gE*S/AK$ s-u]gA3O\IchHzWc@X,, u?_1\jL +I뙪{}q%ڕtu2 d}ؕ29ۙ E=\{բmmfX=AC_jG^W~r'/F=MOLyb1Ȭ+skM*CaO姜p9}(g5WR͚+Y65nq"QQ x61Ո|JN=*U3{]~K&֥ḽTc04#y۽1+* f>r\×8՜F>RǞC^{ԨOw{yaRȑ?@9hZjvqW't[w#ժ'rC7.~k;f6g:A ΓBVoD=d:6sF98"%D.p?D\ +]Ɖ#e[3.C|I:Ʈ+{*h泍ƽQ8th;״6E|PYON䏽ڕC~y+ZY]m4WΗyϼvzwkƔ됕ƉhȀI 2xGh\'i9UvrOj^4]zw8uZyH}hHa_sU6aq'+\'x?^l_'ONWlɵ5#XrB9ih~h;Nq1HcɞP^+dv6$m}ngD~W"l/0'Oczʅ|$5?4+! :)I,#",s=n\+aW,ܫHcgOeZ>=PD5j)ܠOԺѳא~=TCKz -}y·8A + P܋EΠo=_ש-@ -ٶg˙ IS,9U;(OkYN)2w,lA9ܘ;1NPj\s'fAcs̼HuhM:9oL)A=._g}sIm3K$lLSBi<2=|&~>|6TI_˾9U3D3xwBA8T!c 8HrYgx'ȒCwhl9fkJn+?8T!dt G'xmLJ߄un5+$I.! JN|sإ;'י<",d*?nwוdW䥮@IY=SW*~V3U*V ^~™2E. '*]#V+N0F6gV&g Ȱd]\tN%#2ΚJ~I -/bH렽ƻhH o 'ȓH}}MۜąF` rRdJ4bCWT8 n|YpvE/p, GCu͚[Yя9Uצy]OQPߣ,_29I9=iNk Npbe r"^'Z/vئ Q, -\g'H(t'yo -/z_ -A{LXQ'xr^{E?6f}*nqZJ^؍xa  |r'YVߣ3G}#>gBIh*}۩(KK!U5qGWő4o{.FrqEF].~nwg=@ٞ߯@RZjU,|UBL^:xU[qH__xc9id"dMBF)F}=*A@?(Sc- "*S}U5jQI25ā*&f"kRn,lӇb6P0fj`>@(5 * -~Wf*Oz@fߧ -O IH _7pZpZh) G_JW7NЫfd~:8;X;L4d2+ +LK^„_VPL;XU$Fը)bս 4M=8D|XK*v fP( J&0cUQdkZDb*fPPGG |jQTi5UL#`HhBBx*2x_}ʄ #U23o /w"/f&%sՍYCxS58F(Xg*KU6 (lõTTW . n6|@M2vBTLQ4zv -TW9a&bh( -3&S/㭎Û75$DfVi FJeB 蘚è5jd-Jn͸QFFRME]Z -ex U9 J -h'PUɍLj,rtu5Hkh F 5h5D苾T%-S3S5k`5ve*mIlR9Iwv3hоf q/՗]ƒT`WyMՁpLP_k*bGa]8AZ6iz&Qo&̔ %UFPGIQo ʽp/!/S5LT(' e* 2T2S~Ѓ$Tc\Bi -EhJ ! -,[+z.*k[Ruؾ-̭>T:a+YpH d - F}K-?=q]jR`5)tF33C͔n ʣ(XQjl,\+SF_ƚ1|Ш+o'eve`"RTsvezFaߪۨ:0cpMf+L]E>*؎u[eb(rYF!,E3]δj/Q|#Fd⪌j8b)^UVt1& -jjbҗo~0R\Hc.M|^hNdF8\}3HbY$j P4ZC3bQ1M57>AmJE$K4ePUK#-S5 -)@ß;Дǀfqqje :T[̠Q8}@]P[MkMRoi nXP*Ə؁NDk w"a,kjG*pPz}}B.<+B,RS+ XLjNJdh;1D(Ǘd*K' Ȍ 4Lq1*:h" w6C#Z31L_(R(3!V4GakM,gXtԆ7Im F]4&}MT oZ*dh,V."JPLT¬oTfFMe7 獪53AOV6*qҾB؀f 2Uh43KUs5jeB'hD.Dt|h:MdgUzP^ #ԵXEoYZHKM?TqbJkuڨSO#@su)]h4ƌBH 8!.ŝWxUeGv&}Pc!j U_j"W׫˪kM]KbyOo(ȝ-7Ԝڢbth>(T!IOJbj;)5]"WL 5S#"p@?fX .;\bR"4DEjMJMl c.13c?(ri&⻙諪Iy$lH•BBJ -܁N-y{+5ybۯn4EɮrPTO`/^+s0 J=Vb5Չ:xMZ ◉F7c1rPW[6S: oX_fj`*l+>0եLUUDui,V3PRj7i XD#7r,W>| Raȵ,TIdLF#S#qbwQ_R3d* D:|o)DC0cB'QK)˚bmj3[%VxR>e8S h6GHŚ3&j+5*1U:OۧvPTb,͚R6ҭZ柺$n,VѓKY}Xoa VF+fm45F3 /lV'^.>mЁz}^A]3FA=/uiH]D@1QCՈC+5\Z_VZȋثQ,m R4QR?UVjd"tWMl&aϫ:Eoz.nvt.`7XdY曑H5JHSoÍFu<ʸQ@¾zu+3ygbZB9f1TepU+F?dMs\eo&d )ԁ!k5447)LS/>O* S;5vS_7WhYlgNJ/U 妿cx۟NqfuNJFO EDXEu^'`pLR'j֔YA]*0B G,, -<";U.'UE6U vڤ9鏵܄󐄍,Z5xZLфXPXRG j*U,Uo&$jND|CaCDn \PPal"f5RO}Q2SUVH':kՇbu }\cm]5 -%!j%mi ͌LjPa:UQ)̇E<̇e=Cn!/Nf="1Ssfl #>&w\C#١Cў9ĺ|JpoyIJa>4k,=$ W8 -G_]Uu&u/$)$3SIL`p9\\d>~;L#2#A oOIΆ"NŇN3,ds.C3^1C( \B4.n2KmÅgRG2ICA6lJtS7TtLD6(g48ԆGsrFπ9a<'S^a||t:(m$2 "y#XǚՖ3h.uc͸kmHl$Vr~r61p -AkޡB)fnZ:cC.:H X*Y.~{ :x 0Ȇ׆{ t@#~=Ϗ{  o!OhSaY!rWۆCzGuO…Gs^QC8A|h8exx!?ޟLGg2e3Q"yC3Cr){6d҉LK|b4>,{5RLlh^?L`t(< Cxx&8k4%.yL6"&g,^MjB}M<sSo*b&n*?dEr6 bz,:R>}6U?U4k%%?2à6HmzXX+7{g S}*}j+l 8 ]P~֤}? Q6P{@-l s曁8@GuH~}S1ѥQ4W)TNoNg˦dž#uiKa\oPxɇ5;t I e.y6K3@+v9F1ly „qS<#܃׆,D:*b?7`;C>x"p\Xx.,c < 6dtHf.:Pt$0lXh>aX +K&$k4 tUJ .f<'-Q6䐏>I3l<:s<:TytT$:at&\ǦM7MerF.YHɒ3\ڮ9l$h,|O#4*@IkDMlzM2d68g4M8L=t@eLZcHn"dz>IHH`l(7Ⱦh33\zC]؆~ Yy݌*8,$r.`Dz>tTo«FWp klxx6bR6éȢq Z\ropЩ; Lm& 4-n9Dt% -,۾Y#[nV5_r}lꯋ)؏ +2WLyt@2yX:c{&ml*B:}]9.@EŠR_Dd;ZRzjɃ1_0kXk`R!'S"10; , -X_dc0yc{V`>D4{_)j{& -N,b맂|bܨ:p5!'٭ Gxv`~ |LȾ0ȾƘ2<]48,p CCg<oa{]DdCXSl]7l-35&rM9ÄeϠa;gC::q{)hgH!{G9"WIe~ڂ dž}J#K!΁lցSXc=$t@H'g6(kq®³=iCܑMG\` 6X_`P$YhS|H{(}u~:dP;Zrh- ^n!l ;iac |ֶ qjӮhm{Oݢ2ap6q$4GRw&k{ ^_b'*:ޙwcM33* ]_qr'+Gw!K4 Aa|`b|8|(KW nQ6U0oP't0O."} &8i; -k#ԟDؔʇ l3E1g#^<Zd}=Ekn]pt)5A=裢sFT'Vǫ)7!Q!y#51鉮] ~!kCGb\]x`&03pt $w`Xk XǢ߇.lXQ. j{!UKN;kqAoHCκjä[=Il΄Gki=t@ -qK[O#NAQHv"#yn#c,Z atC:;a\`hFށl4hXW%OK蒳23"KlQ}Q|ˈ)x "\){ߑQi#?a -ߨǃ[=)jMb>Cx yH@-:Xe,멣illyLk!7G .p_%!Y ̧V`3[璥eĶ'+_  Μx[KE PE `'^%+ЅʉIȁlv=Y&yS?Sac ftL}* -4F]*=)Ej>K_0I0>F6B:슱| 7U|)wO`s"2v$)L ~-? :0 ||#AF"=9L^1OF>it%ljLN7(K!1 -THH KttRt̟wcK6=& 0_`' Ɇ˩Tb4Ld$举2yc:u *8o4l^9)z;+ooҋ&>sK£K ii$Z-/a$ox53 |v&wuTX805ȒLjt. *sɂSK# s;Q1֔2 -|z`l |X/$H߂L8<%bĄmj55Wt%ws d81[2(=O~=HIYGN"w@;$qNG!?9Ħ8$\ph> +|b y> {4x%N~p} A5_ | hL2wys ?tM陜#@'UWLR9AF?P >'KD%cEM3*\7w84]ވ{';$\Z,)x%ٔ'M Et:W}w%[`vh>` #r5kT1]?SemD3Ll|-Ͼ:`%S3b=К A)<&q I( 5Im_%T ܣ 1oC\>;{2'>"_ -a.tXT|K,ɣp| #ȉm\\_G82;|3Y;1iۿ~&oga[2T fd9sGŝa} _Hڄg|0_Ll0:'!y`r/fvʧaΤψ:O0Uxl)Y~Έ,8|\ Rc+'CLIBeźԽhY0b{`bgI x 1)&6;>OR l~:{c,8z߈!v ~5OLL؁^ c5+U~g3Ph٦3y^;VՈc>Wgl9omVwF^uۘLc{2NM ؆-8O_6o/~ :'tq)Dٔz=L\C+#,{B؇>PgzOP+*ܴ@r z0`AG <(u!@;!ǻsgC HwqH^<]trF-z.zlR46񈬝HbKd L`XqQV[BܓFv֓cp{^O؇W5N=>@8b; I ̡7LNA,֥;F@\ o8X3*=aHY^CσG*=pż-4u4Z2j ۏx%'2W )g Cy) ݔcI稁ce Nɻ#E|Im3!V\@V^7%ko|M]FU}rf[0!oyRS_c ^(>o37 ;s#~gZm  x).J8?1 wćN6א"Sn9O`.!=m-r~re6CF|ŏ9&%̰ΛJ!Gy(rH9 &'s8ँr -JAl)? O~9'=Jyk@)|o9> p!UBe~s >|=릵~)YFoK6n$kqC<,riH3IGDv{C쳏ΟI5zG=L5[+ 1e鍳;G6gdˋdRtX't91"nQ5 J>ZT\TRC;)53 kHPhIm}FP.h.vn뮅.Ą'n~MW\1>@`Q6K7j|ڜhl/dۋDӣ"| H_yE〗:_>?qΡytCI>,.cOQx!CF#A>97uk,$+VlXg%q<p'poQxhh8aָ|"!|Aiĥݹƛ 26{W1x[z۳ duTE 3 -fy \|蚩\%L\`(٠D@ š|tdr7_P[=uqK@Er,U XuV5cϛ(`>B)\jLC+OS{,a~){/ko.o[3 wVgA7PswArB [3DWl :tZ,1aI#s !k/=g4. `lSgStʈڷ`va+0uwVэ7W,`6~ -wDE}*2"ͧ -PY @ -]yr@Α2r<9r:Ta*(8}G|02G5=xG|+|uڷ*Ύww_n >{/j*=|tx(:o|jWfLM:6I,9 ekolg?o -:j^G^1fZ3}U: 0q<)T!.Dpn#B -y㍯ĶD@H2t,yz`:|^\RtS1U~l  Oe -醻+54v UxZJ?(>42O,cVT\1_-֤l*Ϡn: y p#Kت٦udErGbq$vo'ax7+`-gPaa5/8p@H*jϛMW#%c(>:'yfGT]2!PrGW0av^IiCLRfr.B'"R1 l>Z`' `b l6>_Ŝ6uU{}Www-[w5>_trfgZ?"aP V(k GHn[rw!.:i@\@r1tT,0 A.@7Zzٻ2uS3.0dD8 ]1ȖbumdVߓqт|X} 4s7DϤ=%+pΑAx C3'z2+jҩ\֮K2˜du[9<[sB!`c͝g$aW8>Y$S{@J\V8Upt1ؤzm3K)jn{fe ')KW^2񟄚i%nFdb@D9ς|#0هQ53w/9}v&^ʹ̤kو3)zp@kQ Y1>8H?1! 1?%}c3t mi1Z>D`0Kfm Flaq*bts%\ܽN;0 M gF8[k6-p,~Wx @vAo@TxwU;šg7GS|/BmA~Y1x*RۧI -|=[fL9FkocgĊ w&jnن/XĤ_8/4 ,hD 4csGn8W|ΤFMfGs%Wl.Fp?bS s;cۏGw!{ -u~4*G|K6>3Vx VW@.OP:q*UxސekAax ұ!7pE叧c&A^ -]p@5egK&DnƴfC'>/[Qi3Z{YpÌi}l}{ cKȊD2= qTďͦlyh!]tl)6=YEzdIh yw!ܽuCh~hz*!q9Te)Z6HlzmLu| 8< q)פxJ\CqwdۗdyG81Gn=:iL*|E9/`񊠢QDtD2atEMbw[lvF~[~%J}O;>oT}^M~XC{eÝvxMQ-ѕWM~ ƌޏd}NӍ];٭ͷM7٪+@ʘZ<+{߀?# 7HM0D؞n{jj~jx&!ko\s2ySwʛ/M'l g9ٶ[@θ̽ /z ~ҩgE'׍-W+l{hԳR[xF+j~rrM4=[#7k#:\a;y͓Ne</NPP^A:mrv7{ˎ>" -oWLZF}I:=;;d '++SW̸;Ozk9Uu+6jxR^zYlYf{讣{Nd/kɂKlʑyl%pL+E`)?A1PCo'C%zCsHƷM%wLWiYPˉsK'/,[mn:O~b^?O!/S޾aμu&O|`S9CO^9([:5|Tcj{y% -N.,Xm@Oײ'WKGO Hm:)Y`3$oA#})MyЧmeO>pa>ufw=#k_[^.w?>(4,~BdE`ExF_?hwZ{Zyo^@ sT ck'eڟlnQ${0TKZ{9>eP4?_h}fw=6+Dɂ>4qOƕ=r+でs;ֳ>#_6ˏEq'>E~g~aOw~lw7ஒѳj{.>oxknzF`kvtU[t9!j{Rm_k[[%˷?!Iǜm{n}mNg}(OgK^3 y59|zM+5ͬuY^UXwzW(~.eg0ݧ$ݯ ko?S}xǾ~|0RקgkwS8r䏿leοv=P{x+w0C|zinP=VpW^n{J뗒߈tzY*߮F";Go{aBc.jTyuk:o>_>e>?Ϣ|~E..p3I{OOsy[i/Ͻwy~R\Yw6yEO_ǏNV*?,v];G:w=b~b= otu\Hcrs/_n>DxNjpÏx<[W_dm]ihX"sa>LUU|Q`dpr'#mn͍SY~g7(LsGcYR1FhcO?r=w;Xy'#[}RqM{^xÝ}͝x~Aq͊c)dsO;ww3_])q~ɮ:חޝd?Ieov??{۽\lӽ=A몃Ww5Ty_F`Noo l~n]CGBU&t@WK6?̨(~XWSNl z߲Y9׳rd>X M2yu='/^w}wXl>ӿLY߳gGWŽ,O7wMq-.Zn>ۻ֢rثOVl9Mu7un\K%&pzяYnOW;>;Qp]PwȮҪ{eKގ/?›]!W:".މ,Vp^XMwËn܈*>z3fBY*G͕(`?tf;:Qx_~6'zkCՆ:edI5J27_o󘗏Sϓ Ϸ oe)({{z2?m+ K;rf4mw|y{%JW]^\RYp/`GtEVx < SQ%GкkPVByԻ9-n2E7]rGor.ݸdJk%.?yUwn9w+l)9w6?RʜOs&?NeĠF'{^A|ݻk%/ _)._Uy?q[zAۋ5IݹǗ.ρ%ߣy:w+쵸Sb܈-\lgq;Uy#ԋq?(}/ -F4$Y*nVwV_K̹]G|ocuW+Oy2kVl\ W}}ᝌzwlćq/nw7?۫kSAG{y;}mq~y~t;}+0~a+|ݍ,s/dQ%W5=py}u^7we[oqvOtTͨTz%,nYÆ2=7^*tϻr'Y=9C2&Z*=STY?©_;\^+wxwYUGH}p>ލ[~m>1}Kxh[ n%޾x+^b󫽥V vӛ^n^~^}/c/V HHQsǽܦ{]뉰ב`]̽Ʋu"ws]g샻 GZ:J"VT~r\IjOW).vx}ՑYMuO*J#_a^w!yJ=UURd2{W?E/~J>m*1 zWX͛n^kXz3īdNȎ]/bݞIWwR|%G.%ᅵPzr\鶫N/}wk֝쪓WcK/\.jG:!y -u+qoA./o[y;ģ#"Sn岿{hRo<ݻ|Ktuګ$+%F_}'16[)14^!7VxDBLoMԉQk{k,*v.|Ϗeʫg7_L.-SOW[M;ŮOT!\TSx3rjն;nR8ϦGkFwz-}ݣ[Fn=8~cU=?o8?2c_W>M2Kod/%zgw'&"*$>#п&HJIfѓ,5|gW<5]gso_e3]שg9nVuV\N-k\RR)ҋeSʿ[ː>.=})Njq7J,/Zʷ |X埽/V%8[.W$sI-'Yc淋!exֱk"gOד Jt轴$Z렖i㶡iK YD wWJV.+YZq!jZYmSʶ_H.;x>̅ңK!ZŗWvfTqݜO3Ny\QCOB -+*ֿ򭭻dޤyHGgJ ''ZmJaF%LHD(Y1{crxⅸIeWː)>{%(N^)A6_-9p5ZJӶ29?b;ĕXkuSn a ~ a"~m_?v}߻n0oZ ,z2n͞[>/ޤ*sx|Ϊ{EmޓD%"( Q9CUb2DI3JT$&0ۆVYQn[y}κsLsyyS#59b1l ⦳ȸ/=q1W]įJX/B 1 }9~E߯وh4# ,;E+sV4<_|b~ -'|bA峅.4nVP0T۲yƦCӰo0sI㜰ؽss꣈ wmN\ b?aK?AZP_s)F4A^u%ν _4n9[sڎa6BV;bď?߃gM.B(2{=\u3\'?MpeÄ=/nq)!GW=\F޶r]otRAӶMUJ4̭/O?:t~Smo$9Qhtᔊ*:V"#y3 [\,vba//}!v}#< -jq|ohSAw=kiL~.g6ѰB8`dcDd5Ҝ 5ll4 \Wr(^yKo6+ -p&cOa_ٍw`zzvóip͙!£i|hel+?SGNEc qd 2̌!Syt\d7>=3g4eA8rڭ6 ؊^ ёOTQ %p{qŒ_]+y{xǷ -~n;s;r-,? O2ײFFQx!n}D4FLt쐩4vds?sPd#BYEcWvJ4}IVL?sEd [ c \!ye眩D(0aY,m2['I.9hj@%t'vJ}Y5(g^OKԵr]]8ܦ8 ^iz9vNˡ ׻_Y/ =D*S sd= 8l8bL}<2ױ\7푥J4q2lƠi+45M\7 VRmW_ZbIa轰}9w [8%]ϗ]5[럴^u£[YdR2T r,$8ziXg} 2לDlcmf&z6Do1z&23Yۅ 9hX4eEP&T!V&>ptI½͵.n:W yunf}7evݺy>7)v d8)C 0՘G%cenmv8aMb, q|2&:9XX屁c`jHX<$8-6Iu˚l9sill~vۇ]?<|s'P%5x?D6Kq\p?YOpFy8!WBfc}&gxX,ǭDVNhdΠ {#FsWS_Pxkg' {rIXZ~xxigjn_(z)&//#K(ՋWBMaƫ&m:$;/K,'Q%SGhC:C&O\4y qKpN)Bf3h<9朄T9L+_ƂWK>9V~-|"Eow%Qo{9#qOvy~Skm!^oPsֆ-2jۯ7z~ks&c@bhdNGA_jYd!36aAg";ϵȖ@#4ʯh/ȹi'5ZTzKwQ3E\8!a}S33a {) -zr88wλEĈ = ާ<}GU|]1>cDwdn+F]֢9T)MZ0fC}ժ3x|\K-zh8z\y%W5-ۣ"pes疟ƻ/B׏)܏W|WM+Ƨ-Wmÿo_>[ʏ!?V:q/Zqyw -*:)4L5!0ӌGN¹4Z& -F6hG^ќh=vGhb-Ԗ$tU]qWpy(BɍjVʤ?.L믳}^+ -bM !'9&w_U1>KnJٝ_p߼(.chpÖ⡚fΏ&[/R6{yDo -\. D <UhYlҺkF. 3=~$b:eOd!iz߅5"/'!uk!ثx߸zmK0qޞB }~˽mQ&?`G1Zh[E;XF06Mƾ@(& d~#Ċn \iKN\|J^:ݷtK!o֒m?x1-7SgSg3ʏ]S(o] -yt?Mэr`/e=eCRU-t_1i ':z4@56_&$:+*9mף>vNjOdn$D梄՚uEy%~Ml ->'%ܓC%r)Ԟ_e5L-7rŹqܙag?ٝʕ;P'u&M_I?8f̞+CK -53N $B -1??,þ{C'Ox|x䭗ɵw?m -{ChH}v~7Ƿ炓j4z_|R 7b" J !JAt@?ۊisTեd ی'I$FktR qmB4AԜތs>-v}$u"o*B>{̃w#sSW?+<)R>}PP:|}RJY%)n6WVa}в =爐{dmc i\]WKTGR/$Ԯoܨ֛&&76SMvl 4a+훈'0[jX p~^|_?G-o`*E`Q1a+v~0irbc8
 s0^qQd;?':&G K-Y;ڃzfU)}^~ionsv?З>F0ަ=Un/XzջcFx^pkOv'm۟-~\":"20hr'4zZ`id^(4~n䆜1eO,̕ъlHG#dٕTi'"GasVFo+ іhɢHע]xJJn̓T*8t;J qoy7Ln (aR`UQ{QҚ33̞tJ 9cdWxz#++)!2U:Sf)m4_zqtE{mQ;ߺG^r_d_IlASyޚ竂n^S^^;UPYz>Exl:c2Tc2 -1F&#q=Py"wiS[~y*93'Kn6r[ĒDz4ʿ*OHhYh̅(+4Sr#sMBVe֌;//}GԱ$ݯWzf+쫕Cr;zI:ͩ,&~2f?jshd7a-`ץ~{m^(< g':#m>eퟐ퉖Μ\(lR>hhG"dMlv%-]})̩4g -1˨+L^d!þPyvlӥo[#S'+.}k2gKҽuƲ/mŗ ȋO =O'L=:#ya;! Ia̼|?>'wtrY 3u=/b禎lmXBГ.yJ6^4vC}40d۟9GUJ'o22635GgBqpOD/nEɏ=Q*> va_u~{>$KCґrrYң?^sЀWB7B͏jZ%Bb% -PiHRG -WDNi=\6̢=<r"Ua!I=SBg@opxq_Z~kWYN]c;'YTj4J_7'g졷 uٰM{'SelŇ~u Ӫy4(e -(;e&Ӂ/ZLd5KT[bb;z@KOZnZTkog#=f|i -Sk%aZ4M1FϿhG蔊Ѳ]8ü @3tDMUJ担|3$뽻 4 T*o\LnXo"zA}퓝/w `l:=9X.'u?]}h:]kJ嵎dw(zm(}*M݃WUdju?q,XyU<u\ uru\}EDX$ V1vhyD/RCR&kguOyl(Lqz~,ӌj2_]r`2ĊXu -۪PšJzp s^+:c q` -hR=Oq qdI>+iVYB/rcӨ`TW*]Aa>"%S5t#Gf:}]L2jgj3WOa;dzi[L#zCy$;^L{b|Ͼ .|E{/Ճ>^.W*Dϟ QՀ-zSݾR1ÜywBOim>2?G~,DoЖ٠VHӖk:\HxlTr!' TX'*Rʌ 3*:C KRu@+,V|`}Hӫ}=AאI2⳷3}]'E]=U}Ӄ̎Ѓ4w``zg15g8AS"$ZH.-//du-ù펽oi+$UjeTe^de6nD5.bd>{!eν -a/OfpLtv[IctDt߃A61 >,2 /k;>:4dOOI;AԲJ -I;IO%d :L]]@7:8*lJ3SD БsʨtUAdmph=6bNL>^twbw?>Wj- ҽO{KO%o%~pOc䗞O?]}-~neՏjϔMzOQ uFTj1~*񚽬T:BZvd,99̱G+p<%8HUDd'dbSGf*K7ٺCv̮<|jh.1S/nOeC7t7VKfC)Wr` -6̭lf9I6łhl`ŋvLds[,dF:!SN(#z;ے)[5ρ>|ШԆ>i*૾C'Rj Lq0_߹Jvqv}pu?[m<0 A{ -k=D kXS<]/CeJt{gf@w↬B;j޲  XW7Ug<[9~o)􏃞?hGJxiPڀ -B*h' #.b4o2hSrE}4MKcGJkJ5A94U3/}މ=Jq]Z(}GG|cpl֋>Mw L땪W>™tc9\&i,޷~ЋKWoI,Rƅ7Q=dGR!pfk}Y{1FdV& f)\%AS/]&4 -t4h{Hwum3/hu2C&˒)q,LVٔj4q}وIb̬5 %4n*Jw^:W5jpzb./@cM).̾OÏܾR@3NP^[s @{ 4GOie0g@ÌLR>Wny [ @l`AHA]&U[;.qr_yDhE&81z}B&@!6Z _ +MW:$IdXӆhǍ|YD+t|S㧓C߈{= -<ܑңD˛C^^^9*QB_>d _̑D톃/K֔tu峖 o\kGLP&k° ClX6hO jFb@+ -%ϦךkRF@V~v[zF <ХM%B sGxǏL!аgpNY<6$P[,xMm4@j}hi|.zluŞ/ϯ8Ezо!ц?樬=rg Ч5<2!ؗ`Rp|$U*: j?Yr?tIӴD o\zq.sHy4oe!j Ӡ_ -tXRCi5LAnH?M '{^'^ѧdGtq͆&js@ Ult6ԱWf00W_q4𺁯Aw\wd{Bnl :媓wbh qw]Al}Y-Gsms{9cLb!꾸 UVvIOO!]1H18YzǁIPu& Y\b^hcȵ_Z8o|.5`lեlUԘ\r0(Z*&@Yyh6hm*vߓлd֕Bc5 tdYdwaXԙh1F1<+U/:Vp``- ؝_{)7lv86ے5_=w_z*)1]W Q^mL-Cу oYLbk7(D/'SzdSsTlي3'#NNQ\-ѕ==|i.X`}: -w`{_yc*poNXhD&hA\$ܬUFJw0ҁyHW8MYߗkY7cHW lY!ccA[Pl є M戦m>T޴k -H- n̨cK h甠1To|4ĵr#/714.?l<x$zvgq&, g(udpR%CG\iUBXmkl -†'}@lv̍16N]m X6Ԍ|Z^=Edt'|m`gU; wqwv['y \:+(T eVms k18FDKGQk4D" -P6xuAYm K8+{"ȑ^sb=>zXچ,0AwA;ЬhAgx80>T1yR$d }a}Kv -E3I.0C4YcEJ`gEdTewMמWLSxFtnWf8P̬02 -YqG=?? -4lieFM}ӂ:}B uSy*?ux! ME4mX0[_c 1YTVGh sqGoЖ2ja;$uţ=HbLtx& D(yes.!?w/Βr -5Ov$X#( -Pw /#Q:ty|dH&|q+lB{»זk>OVh8 |N^&aMdv7V -Z\J1_f\]gIa PՃ}#U; [7׊YiIXN8f; { R`gAbgV YK?Y,c5!Rj)0wA_r – gB; z~>񊒮ɠM}k4_<Tg< tLXLcvw}+Y`<NX`Zo1m Dz x$#r$"ՠv>0rI> P A*JCYi*6 8P)ĝv@Xe=)7Wwv!恝{% u`g16`+W(R -GŦGOf8~ do -0F΁k>kz3U^50*[}ȩ@ 7uLf!FB;96η]T9 1yzAYeu[s_.\Lvr"=ӨŶt$쬜V`gi\T$sy;@7&,%𓁝U8R;+eATr^`m}oo@N,0ejV uFՀK9"`jdXx<='^? Bv൯2RQH~$-G#wB զ7x]X4f 6 zlt*[-mKJy'ŵ ^Kߊԙ̏@#=6]GOvV:*7G^{^\6Z* QmW̬3;{JPr![z ,|}:2g$*M.; ֗9@J*) -X^fm;K^l`WsaqX7QwZ 9'ܘvk]rqUtp"wb9':pbuH\R!BJ!a=KAStL 15م5O\4X/am}ChK`mVwJۈPT+ǵ*0p`}s)\m<̡1ԭ'ya ׄ}' `F!ly|㥥+}`8?sm[&Ģi|-'OܯS u.]5_W6 -Ø0 ٺp_r>r}1Nb7=\\nxвfȚXϪY֋ /Y|K FV󕹭V$~-3ͧZ|eݓI5_k"µ{.ɥF\\ïߖt݃+mjp>8>GbDi5fb="ԉjpEj#k+K'$!Vhr!iڰX[`TEL2”s[LX2j q=( 6D0uc4KW:]PyVKvNToÂeB.6O0€$:0 g{|F}䥇aMD^;Y-Q l2U^90@oqh E%,6n>Ol3 ͇gÚ#Аk$YO;?m!u] -,2C[s|6 XiYx";s8S- c]Qfu{&=Z8UsֳƖrgHB֬gk6\){  SxոﹻJ.Mڗt=vp]Qy|77Orc'\T -dnz3"ENK|o -{ݸܑHzQQg&UQgpNM}bhu,R$1c8/"X)T#,O{mo"`S9Z֢بX-`m|>lCfst(ZM(=ty鄏B}"p`mz3pϊ0a LEVwYpnIѮiXz -&5{9\RÆ&iS}=ޗDl25>:kRqt4-HZu`/ oxj!+ze߅ͤ1XĤ}3C.%O/6"yJv8eQ5';zvxb.\C=/ǩ0Up\!>L' 8o?=[ϖ9[B>!y(57=cw> n/g-a^OžG =Y[ g%>% q֮ >ܛЀb,>{2`-8El:-$h4*pNpo\sZ9a{iMyϑWkprFyR!ab 97eityŰ>#/ߟ .n `rtį+}I9`r0?X[l u6d˫=hVd Q/',.zjUl|vw -ɝ]St-rplgyߪ|TEA%Wpl>y}%p3a"nDzTx}r&C^%1Ϛ޲ϖH^ZL5_^T66Kl\|{H -vOWHo2VX8媶c$M3ue*dLσk|iJ)y4&Pu< rf bL^9Y -'A<-aϖnifP;˨73 ~Ŝ6V@[XPnBXM>p^a0ǹ-LJO#+`:;cT| -"A\-d꿜9!b>O~(aʌȽRM?1i8߆m,:+tP'66EW] B/ڂRuL qas6uM&vZӆ|r]u|Vo -97~alE%j'\Jg_p_s53Qq`L(3$csOǧt|:>Oǧt|:>Oǧt|:>Oǧt|:>Oǧt|:>SBC X/ދגuIS:'%E%G'ć$[;·X?/$=")h^} eֶ'ΰuޜs%!ֶ/jl-JߔF 20-,~jqH2~J7]"sӷmpk]kmE3 Y; -D[#td Vyr A?T!CkmMw7?DRe4-(Tk4@K'@]WcVǟG."!||(.yW/cԁJƿ -Zj}4A)2QLVSחʩ(Ϭ52|=Ӷ*Y*AAZe -zD+쇋MաSk)񊆳K'9;.>MzIa8h6B6@L0 4(uZg2tPEt&cru٤}Te7a>PC4-KVmf2iOGkk@CBm#zfɫQ6q9zlB.9z37[Ht/.[C_$3=-ԗ*X`{ ؞w2h3>\|.](6VwL֤֗$)GǍEjqY]\\Pp>I<ߔ)>QCL֤C¯QQ/;Qk T<Ĕ -~+?esF@?W~:b*\-R#K3 -t$ s=|=Й z{AIlLHb="QC~4(AS@Oe2XIׁVUF7em&gx|t kI4U$C/#KC)a,_}b.NBo7uM {DYV=3Db4ep0gB scd>vej’~fU)>zHR#bo/+S*MeF<sZDg "'A %c-zOd<~>ItXLD, YAGiB/Hpu&b6:AlNӂa[sE -{%SL@tz@CC\m :nRĪˡ'*_ -^ zDW^+}!EL.h.ȓFoRvM8*ʺl'm2G=-gPUsl :'*J -4**ߤظ|2u1hkZ -u#Ub|]^;tEh#@i ,Ķ}Чђ#m=dԙF%sWMzx..G)gN6+xm#Vv-sAS@YY{2 -2=S|b!ѓz]WbHbzz9&_ G|*A$[Mtב た}p=@O]ʪԀF㸰&GjdM.4Ct@c~RDCNa z - -ob>n˦A/5s*"M/-+nӡöDG.,Yþ9yh:o6~x 誱DI6xq‰@lqzwGXs¼ĞAk 3ȅ@f S7v0D%qfDi}hEJ S¶>$l3g>Pv=2cea$Ea5H<_a^n%¼=9чh5C#-l=lA4؊ö Z"gMmﱂUҏm (@4\5Ұ/ۗ ~ДekoWնOs 4fD100PE7 -bآHw#9g;p{{~~b˜s}c>umh- -]'8΢0k0k&Ff(.<} ;>4(hs)S^*]=Kc)Et[@n Z:&:U?y{"k7yÆ̞ h"XcZsW/~^rA9lEӄo,a l!f& c>qJ 4 @' -h3g.O`R|PIQ6{DC8@ /A8mt Ԭ11&%w6?d5C|9p5hB5nM-P`h@YT_@Ƙ2 -{`dV *жx&fpFqAӁު瓷~ե6Ocq|)EБf_:6hnxtIQ/+1{:, -_%>j -Z1Tоחc?O0p, ŶA -!FY/{-`/~S$b|.#n[ը_dlˍ(f7t阒TJ^ -]Is!p%#+&ԩjԄ 媠2K\X 40.{bej e@\:+S~Ih"?({9^ ʟf#ӁFhV[ -&C^=aЁ]xgccg}&9HsWNbC O" w}2`=rmH/FM\SCg@-/ ߀^aMx(O`|S.sqOzH>W~DQˀvz}VKAxu5޸ -TCE<97Z=fND~e;G AA Z#rg -WAW[Ș?~uõ'M X 럻NDUng2~(D@&EF|!Zq< Qqx/#c`MHۏ~ \ze.=BnGF7Ħx?=&ȧ R*G4t`͝FӦNx.x*-}4(?)1C:͐?R+^/So:? b]!>8քt'Z -̛ iZM=|koN/ Y 9-ƄZvL!u =>a\e어~|j{hPӠw-:ksi%QM]4OD539T}:5y@+kTטpTb^oev Ʒ0f+ޖ+2wH=*,М?i;r|+ }^䞤$vMx"j -_>G/71W"DcކhX-I/:OΆ_ Is0_4Gcb蹘">hlq+s1yBk˱^DaJ(Pxdvj =?99"KyJ|V~HKq`n9guT`%п~P<X!va:h9b=D < fC:)1WCsu^k\}o.52b 뒥Do19ȯb4%GIנۏZf- ~*G endstream endobj 27 0 obj <>stream -A<(wQnPaE~)m[1ᝤO ?.?`ln/ G ➠ -ixq[;\w>hA_qܝyWɔ.:C{&QW:|u"E#=Iw zc >75,wcZ௨fB|(x< p^пuB3ZМ1a,A{LhӄEGGKkܟAxu2<5t!!>ycr&iDz(N:ZkAzX| -Ό|"F1J". .N)d Qmýwxr8l\Mo!9!fzLX8?Z=&n~y)[D5%1s$ -pK#%spŐX\xd,\Fo(xB( F zFJ/xU\W,* +:mt%~+5<&1QWѥ-l55+*ŰCu%8j=<>/ :q2SV:wT˄!7W Þm^q0T;a脢 -56TN)S3^nDyk)P -+\\YJ=[sa]_ -csX W1لJ-J|z^Y9)_|)DejMx?فoϵ^U2` xxLmqzH@r9m$ g4p@l6qTCoEW.)3QwVJҗ@UuhOH6({sFOIxBh)d TkI۱'$xpGm}оW0|1g UuQ2qS7(J*xL96h}"}u.x2x3Wb iG\]=/{|111!= ->Xa)J TQg+UuORTa|' -?|'/S!-r q5rQj&Z^XK4#nPO^<:Q@eAK&%|.p2Ή@NmQxFxP{.+P}Gu^|axZX.9b}eX;eE!h3宒d,5! (a}%g'^Q1"ģ/<{7ǹx!!vY?W4 '^1hre_k􄄱%3K6)xck:ڄv2JNnPf# -|yf.o=:/x]#q>ĕl:Xq(F2w4wO@\%_P'`?^5t-P%YK_ŜR&ὲ8RCTNOO^vɫS<؇P,b+nd4kחaOH N9π^ 셡mf[53!倣D:BD -~X}9Gdg{@?bjhh5Ox -Ç|O̎xޣ {rր rX=XU'+}kp=pԥiT`s!p^rЫF]_ { ^ ~veGxc{Tp1b'L@v0.XA>^{瘹xOehZ]]k&Z2j/_{")o3"rU-~o?gb^n!eyp2%7V=}.񾽄J/ڧ'| ?.eYKg0Xr0h.nm `]qJ랰uƘ>qi4<hPKY@<'g)yaiL%apfB6v%'1;e7N!֗%qK U Vo "50f k؋~ B }~ q_LQk )85C<\\'Ea6'_y&6֤ ASӎ>BiBkAb )OɔJekrF~(~_lƾAVVgawCg'3[Ԡx\Ϻ+>+ e;/74_6ƹ7Z1{Oxr6<EпZ}X煞ǡ -7YZĚS(cx$ar}b1zEh,@a)l:FM{m[c{Q?2@M`||hO[1yq,yf$ 2 HGm~fQ\ {s+WKaC6 ݃YPS9kmE<ʭװO=y楀GqbO*XS\Y [b5lDVYل+{ - zK//lh&K.Q,#lk(pҗ #=ScRy[i/ -iLX&h[`DzTӸ֡蓉jӂoYkՎFe?$55):p2jj>}ذ}CJ%B5k+aXGEx3EC# {@z!P{G@ށ'LZ>*[ +#aen/EMgg^ѩmDMz෎\pX< +JuUqP>a/!=ɐrxcJͅL@ -R.`VX*l -4v͞OFn":@_.OfLhwLl!̥Lr>A feV3j!oփZP״|zmFdÊB 8DV]d Һ\񧙴K<|3wa\|)]\o^iHWa N+j/z'"YXiI{fҌ$J]TwIiڏr.W*j&UE|J%Jj?] \-a!x9Wa8?/(A}dn07؜6i浙STXKѩh!yoUш;`m{]|QHtY,ɭ9dW|N҄Io&ӻTѡ1DŽ^q 1F_fjz0bS_zެ25Tj ->6 忤&eX >(W{]z]G=Gq}֋@7K(a_vFxK[N rO>l#77!_| -K[ĥ&]0yn֬dhຂQ"閰,Imj'̬%>/6JŷEVJr5k2٫ڬU[FGož[<-"Jo}ТSY_T;x~(H^g!󒸿6PC|9M5f>}\%zGo# uvtEmKUz(_=(>..l8+"ީ11-;ye3R2 uz'_[sKR;=caf>C+ەoVKoi,BF&}'i25C|E@? -ߴwgJG}y[vt~)>üd~>FqzX*dʔuYJJ}$}Ee5E&!3>هbqn){eZuȫjV飊#ҧe;- }ˈޡ'F4 -R7jw?걷H*O>ꢨ>)0ے-8Ǽ:T|8Tv^ .jWD  tA!Je>\<$h,y59Sa˼=LeҢP 4q.Gxakx&Hޔ_4wQ_?ZͿ]MG3C>Y -bz%jI3[H{UǥolK\n_b'~QsBrI$j7XJ&496_ a­b+q5OŃ%O_|د}@x4xkV4Wy`\x嘣d.⾦#ĚtJڛڎ~Z2ٗi*FK2'W=GK,~hmR$.mn>)*n=')T9T\o7e_vӯ;Q;2F4P+nXӯLgjz-n%X6Y']mLh͒0=_Oi"mJ66] S>ne - 1kˏ4-?,PǫhaU軩N|!H=mcDN7s:^NNjLO?0Ѹ{]k4|?髊s&CD[V 2h $ބ<1Uu9*̲9%ZpWNԱ[1`qsΗ2aRY5]'K+_<9/yq~2Z^DZoϼy'ƽU–7qgwQΪ㤠1gSj%vs19D&.7M za?9^O\M-wIp Ip N<ߜh F"L5H-@5Pès)[e}?C@ӊ*6h`W&[- W6m6b/n+ueIR7IiI }ud5ȧg GK;JEjK#.4&&z&y&&WzJZ麶"]BS}ܢV9Gf8߬p@]' xP#T}aAfJ5l(~pL7D dsv+ɖגN6JtTş-x%j~#+~-gium1q]녃=ŲCy2Ǟ)<ŔA8ɐ+zP#+?kX۝CO=HjJKkrZy!":bpQmXa]XIuHeuH)&><1jkTPoeCZq@YדH3'z>sy+=8(ѐI.I3Ҏ ަ agC_*TJ ?IKLz+#N愴齷*Ĥm[Mpr@O+:$EwVMKDLH^CdQMXauF7k~Mh~M*4R+]eer7We@SAA)'4 +YƩ -Ĺ%oϚ4G6d }j&Qaɛ BT_~1zT:WGE -[-k }`MnV)'Zo%\mW!Zr;Gf$KLuuOlLa;\u܍wo -LuYJh^G;>+v-q-:ޞ% loVW]Z7X˫anD_Fq/+v/u -M* (O;{\^s|^#w(OAeE^E2(_II>IѹQzo9=לO6I -ʶlaޙ6 -λ׆^qe]jLjr{5E a GPvpnjqH~l$ر*"6'&=jG}eT䨳)Ǜ"=oͺEJzC$%ͭ~Gn]GfMQg2SO7g%p7`C=Ϣ}Ȟln7ѷ#ef}"9 #j#c]#_6xg~ۢdMTǤMQbCcd]"~urƺ=XqB~̌rg~pV*O{'勓^ '֍jZfӤ8$.n5=r/ѱ:*%;j[mcl2@Xֵۉ*]% g2"m+cnuC9y<ܬ0ڼ+'BsR_KMx≶tGpܘmYqcccbT7;^-g_ܡ;S'8{V1~JkewLZ[${7"J"N6eDw܈`<[էèa;ߍJ%{7;o]sd;ÝC}})!, 嬒JAQrGZ-;guJSLQӥ' v9 vQM^ɢePMDSwyR~y=|1}6y^b}ƭ{5[U[ eq)[QiP_ڝJ"_zn -/ -="C /#p13VkU~n,E񡥾 ob߻ɲn.o -Kߞ5Zinh:rX5WX8\O(silt(WMطODh4n;w)s9,[ -dJK iks7+V([ -}>3vUqBAV[gKwYo=b -:-v /( {1"-:+ֻ2 ѭ2$!Wm4jSt)b0C>7@k8ޘm1Y鞂N;%/rE!S;n -Xip"BIa1b1CE$"1PA,DfD5 W|j<&N w+gc"D/lt5u{}s*0.WTZG7(/F]-=.v|!J[Ǒ8G.t/|Z&knuY~iI\p9o-j ۰SXTXf9nzFN󚂶lW"&(b1@%>r?^z,'yx˄"b/JW?*727*BoYzgԕ{nQ/ -\"s \"rbEQoK}*ٯ䯟\/DǼjF5bhts.Nqb(sJ_8t#rj7L F N$FMD? fNYMrU@ucsB9갸9zlVGTasշQYܢP +r 5yYSwΑŞQmQ_[>X7;GΝ/{=w]Bi t DgO@4ET|?\_{h.{Pk~[?e;zAf-#d.Zb^BX$aoc+Bg';QrkGh9-CVGGqx!5&-}Kt"7nMj_uF){R*K( r bƄixnwyuwpMc~H9_$jĂebEb8q؞<Ϝ[I/|e<^ž-pM(+pK)-vNz^[RX[EPѕnhr:m__P6,/c:{cѻa3I8MC9ii#/&&/"YMT8@Tv!v}=B[8!Ԅ(hぼM(D=E25ɯZXt)&2aKwiK/cC\ ?U/`N@Y IqbM;YӶ3 -S''ZGL -ߏ@(GJ,xf%?[n3oxJ(`{/<Pk}\5Ǖ2׌JJRD]ʹǿ \/JĔIh)7ČˉiVƬ!H̜Xa? gb#}I{Fq+ש导 9b,~~?1?QqBb696s6%,Q'-2$f-sԉ bN(˱e[n AQz\,#hw -~AX{Ǩ7Qo5F~[i'<9g84b42!75v91k:b̽Ē5bS sR̥$1s1CQ=~o/T;A6kwٲ<50Ts n Ǿ.rP3P_c;PXST'Y5fp0F=<{CM@N!F"[BP”5h fMM(NML_&Ebbẓ -m_b{;ps[?rG] <#5SuCT[S<̹ZǴ*z䷥NKr{_еxMELG ͫihM^OLDc8 F-"f_m#0k+OU&ļĂ}}bO fOv6C@_X&2{܇}|yYd֬uv-`DTZ6N E(G949]N]bEĴq PnD䕄tiJkL Ă '{bmm^5p*<]*0 ȊWztm%OK">Tل~oK.vHL*q0}a:fC?~+ a1hơ6,|LP<5:llgPN',&g 4&h-o i7KۨZm3 k>Ee(wƼBXhtscBgCRoC:J甎 -G%Ejp[&/q(LDׂ/%-t*Ĭj(W( -3Q L4\;k71g^b -1w1oM,xXJmt+!y/~Wm zy ,L/*t~M/~9mV7F WŻh℅u ն1BQT4N -VsP=^XaM,fF,Y#CCr5耚;{;”Ă4`XzXHcGo 7[SV z9 H󭰓uч:Dl̊M}'TӵӞ(ڈԲ2ې -b}Axm#Ly28ᯀA _N1ah>*SGDL@Xs[xR1lwf읶=k;.=ǯ|j.b-j;k;-Re(lc|$95w-t=QZb!j9SA4C|2V5;e-pF !r};r1<ގ^wuy#x<~o-Foӣb¾ܚ}n"flnf-S t8n'r.VDfBٹ2,ƀ6$!$r 9+g1xR ӍjZnR?Sea$zݔ}T+>>&.h9 8#qtA?f H#2!m;A,հ!<{6SIJBb+0lǣvݗUvg˙_; }~w훵;=j:7zA G*8SAiցPzYYKc{];`{I?U!!AjI *m7RXh6n11gVb&#bIbA]'7E R3ω>=u!3s?mx6Yh~x:9WC39X0WdM󠑺6;vC39#ŢFܑ;Ӓ-$!L؋ zy+6LE*q0O>&qEqt \obW#Oܙ)~3GΉ5q;U˸MݜQڈ2 -HK7hxm|m>Pi4q.`O{ o턶:Q>F2QAd1W|*l9w0wIZT1z!1g ؔG8 aIˉ[f$Am AI_9KOa j9R#usz`3Nq adC R7+8L?rּ6B7ϭ9J-ԵG,?db5Wc$}'ٛtImLuLnc2@]hP1vQuEyq\byԉ B,XCӳ!5|wݷZ% '/=<_(8:V偢+Ni*p#^:X 4 zm 9(EGi9QWHv5z-^^-eDALxvNUI6͌-j>!~_z(k`7u2CQ]A*AsY,ۋy~LE5c:7i%}s(_ Qݥ|s ܠe{mvMb Fz9iLnM>ԛ+>TpBS1xvl4 яԳvg$y1f 'Nﻤ̣v1׿K_7^\ gd7Ulzg6-~b z)n&/m)[8)1)-򔾨=%i [ydf~2d BQam w0 -WT#VX-o[ш_vqBDvvkBPF<%MZ؍7sMY*.<\#-M\< NJلƾmY8GlOϳth 1HВ&w S4tjfTuYΒWOZÿU*%SwI~4kY#T-Gi\z8IɴY_Ѓnǹ:,jS6ZoeNڏFޭBSMl3$:3q!˳91{_mG~M]`*zu)u|cftOu˥mLHمcgG^ L1ίH`xy nO[풫O,^EJ,Ey*eb)`0E^|t<@y?gi^Q0 -( uai  RgRb6I3ihsSi:f7tG5Iv;-6-e?tt1n*5x~olxqP=;Z9vKٓU V3ݶ²o|Z!ϔh>[N?Y͗ۗ"DS%Lf.8M5?4RTқ]Wބmٟ5yY]{ً[Vp/m 6uy($E.b?a}c@%Ulᅽ^aֽ00C߷^6kp;59}_9 ]t_r:Fk?U;> (+?"7O\SE|Kg]lyfB-a62W{waJfû?ԌU6+]o1U9qF}.+ e 3r)M1 3C -Sٺضd9gVB;hx $};bݥ%_#>wiF>էE?\Camg)shxoga&75ɇy׾lQa~ &VIm· lgagSחsUܧЦ_Jli.ٍ\nHSݩ:BdAf*}xhfn1srBtqгM´Tj j/r4 $n5r=Hz{oUV!ww'xTUq!n4hhwq~ߨ1ҡTR3#}N<8y/bŻ#p׿uݯ7}/w|y|F|W>ᘣp6ac4cYݴqf]7k{׌3Y'dnų -1[\9&cf W_:q/<7./5ܜc\zᕁn\GeB^T>fXrr6kz{^>s3g[꣊p_R4g>/:/wsEgbTw(ڝan^Irz-_o[_nn6HO ^=Zlp/wb^g>E"&ՌᓪFN,]x_/Žg ݟ} Ӟ=_(; yX`~P?)u٣{<<+?WY<~ftyf~Q IօZ{XrI:c5[ܺv#ظr!:NdX!G%ڪ ŎČ -I,Dh%'y矩…)|rF{k($U3j Own|ܐG}oߋ݋4)L:ۙ\: ']{eiG*5޹ 7i6ݩqٸOs@`9KmxK4l4hW>930g8rmpOoV/kDc%iW8Jɫ,Jx&K/׵<\wWOpز.u7rmO7qO7}?^u~P7;~oxpVx{?ׁ? ,&nm<0{͞O܇ ckFe B4׏f0MgU.܊p7w<.<ˆ-!tx~ )&Yue&<w?{~aNCb_c,| je%ڞn G2l􃿹pL\}˵=?n:t͎{=4-,qY7{f]vi\{hvcnޒC';B{3Z֟sZ{@sb\tP>4[ڑbYgh%/6`?ՇgQO~7}Jd__&b~Mx[2;Gzmį;0ܡhcTI*mWI޾ɖ;6k6/ޠٵiKCkt>Ֆѹ#4[mp%h'78I7CqjppE -07's{f-6A |fxth&ld8 sRũ{(/]g|QW|y>if՚=[kKcYIp4:aL%d~SDv/soX0JN 757nۤBjTxn6X|Zvl<⤘44uRY=rbVDҒLiGQvS|P3 Ny<sШJƜ4 [K\G{"줬X/ݩ;^no]/H%B:B7;!ɤa͝~4x^ J "Ts?r;7WVrW'W0r ȑCڑ:\j=l?Yw+:hfFI%3M)Ngrk GZ>V4 [+OҸQfB*]l=n]_!6oI{f8Nrp0>ҍo}Gӌ>+7~q>,]} -&Z8~g]ybfm}Dr'K'wD.g>ېnd\1f c*F!q4Ux`taEΟM7 ͸pEfʘb!s,/D+qRJ~t꣄BB_;Z, nq@Hs1 \.\7ۡ!uN#VQs?]x!1`$4\3=WՋ_3_P#@3*?=_,tJhπ ³:?+w/{uݯ6a@L&6{:y .,pwY- t`&l\d`w}h>,k8{r[ۥ<556 :"s>]l J.i!fZL4]yXKhɈ=0T\w\q~i"}UO_0$8J\Ik Ah7_}A-fU{,b.<}㭥zV>_iy;]4׸N7c -Feu0ӟMl |Z>EiavЊbr0 qpäˡKM%U…ߴQ(0 B)o-jxfĢ3gQWFZB۟;\b@.j.2<;12Gz)V4I&'p{%␉q2)1oZ:x_B[.==zu,4gB"ҌJt!>=_o_X6]K;BbXfR`xS0oĠa!bfD>tO1ut< 5'Qfr3}@_9ZNHHV~fx6۷A7QjuTmiU_Zf'}'xVϙF҆lxo֨Ո/ʟœjx -~̗Z,>vNb+GWŧ5C_o}~͆y5hth WVriC*!A/?g*'<@6G?[c8l8R+/-&‚}!y=|9w'KkՀ;z0i{]E┱<@Le1=lN>0īdM(OYʱ -Fl-eLEJ%rh`UU]YV[].0Đ(kԉo+,o5B-^7|gŞ;A3'CS9L1x{*-cTh -)NBD> - )1K/lM,.gp֗'AW`1zWN"L~phեc1-BRbtLU'=)9.{Qx)7W.e=ssb.\mRF NN`۟m3xm$jB;.4[4炒p\D+WO<;5lXVͼ-:GșBBhCK8‰jF-}3P4}">yAqg7{_SѩbHe˱eR%|rݽuЊG 4h%)7bF$1k2U.^mkzty+XJr=Zy 9S)(P_FҊ>0 - -:bW8s\ ~c#/8P3]9JT-4;pg^{;'h2z*,8B|(褋%7]z)('cv1T`Aq34z zۂ7+ҟ,91G9&Bs~eT5 ZHs/Z0 bڰ{ Q}F.W^bE|B=4Ÿ|YʲCnރ g 'g=A`챘1 -||O.' 9:&v]ӝ·Q󂙅 -g _֛rz+5#t~M脢'B׹,VC׮BwwBZ9^?luZseE|3XP:[>fTwq ӳ{U|i|;/ooJEP,ҷ;x~z_RLjE@j@p=CfA$1b}:;[+ˍsJt(%ydhXj5fؿxZzu1_h `^Pw9z[k]-:CBs/JIn1t9ӉSw`w=w{Mw7Z塾foxs<!( -qY1JA!6Xwj\HAhBYu?ګx81`gʷ'X--b9 Q;0NHS`D9#HSl`@$cWX!qtq f$TrSE93R:0;z)]~-fvyiL wyÆ{W>˧knhbgMI`g R?ם屟\*$Vm 6c .jV"$U9A\xyYھag{5hX%iY|`^K梁-E#Pk#XH\H.$|&hz;kH-U< -:IK.;1+u'rde ,g,CLZjytf|5^w%Ьy4Fژ)0e;JJ3꧊CO2j<3IN CR#lggM;J߰Ӈ3:)WNUY<#\[vL"bZ8Ʃ73_oG]{g-HsL -jطl5xH\{n~m4u_nw̓5Ӄj\zrZ$?Tkk*f'a=\0X쉅즩Z kŅB]cy2{5w#vNR!&zz5,#>Sl !6J`09(e8|4Xħj4MRlwt:0~H>yr}7)(gehޠW --n)Q`|3 rRl[5<FU3bfL⏌,b&7G;KyBUk -'/L,#}-_ Uck]h+,q1f";dabICo_:^- mb!h|>ŧ* 3`K# Ycƣ؉|krk V"b'k<@:E%vD?CjĖk?X\R*/h{h51V>X6t6E)>;b {)@ʝU -yY. "j;׬V-*I `T]_F}T&>Bb$#m#=Yuo1q!,7aO:덧(+/-;r-;Klb#2βpZT̥&6!bgO;+2+;+ ;+abTYގ충9 X`b>&@gu̞jfR!g _qe}l]K%"ێd!Vқ&G?\%&9` /j5"~]g5y`0˩N#"~Ei.?'JW6UA-$2\7Q-/,n'%ZFN%[q}/\d쬤Y6FbgY\^!~ZujOAl<[S=vt鱷|x,KCZaZrŖO7˝v n[ 

jNg/JhܣZzy1j114͖kS6#X-kKYlwWNdˆe'Yq!y߰_sJ%]sʳKܚs[ӋwWJ/,RbXr:ţV&nLQ3~CrÑ լ -0-pĎF{` 28Au"sx.ri,_]}ᚊEbXN 4#vR MJ'=[{LO6D;5L>Y>H cPp|)3DYxy=ȥ (FF]{{b|;K>oe1ߐ,Zeﻸk\=XsԆk'-\FX}_R=n+'RCJD!(J:= cy6z_lZ?\K|'VcQQvyVG'yj41RKFGuU K5$9I_E,qxVy{z]l[0h0B<5(#i!y -]쎩`nȅ31cB}D+QrLaĀaT[ bGڋI#S.=g&߈"q` Ả%w| -4RTՐ\> Xf5} /*rٵ<>t/.FOCj(v?!DZ굲WĨɮD/pސE䏠~¿Ć;nm"^[|\hw}PbR8ҔYcxU1l# k61Y= @ 8CڋK똍mPB= ; W=bkTd|mG<;wFÞZ~m)L)}Ih>2<z` ^3|>Kص/陃nRpp\nTR錡˩a 6Cz4:Q# C~%%X=9L̈R3 |+_"&Qō׀99Z(18 --\OebE#F!6!e+Ezjzۘ%kƞ1Ä]_[!oƻܒ\C%{7Ǖ2 kK{9$F<k$֍Ź7s_+JמbMAWoZ?ߪ #9,磜/PrOK0yo[%;YOׂXj5㐳ag/{¹{]o>-P" QV8^88Ea+DL;6L' A\YNh.ǜT=3gR~zz؃DΨ9hHxmkџQק_\ %׏ř1"VIǒ~I -XQv_A`m v|lGd 2j/A>Bj0 f;/'Og.sC9|Nj|ۣMyJ9%UB~=ҁzb}%:q|]~EzJIb֘o2яM9r4#?I.'[ -PK[L81ř2?e*6Nƫ+Ď϶J>a)qX,+sZ>^O5'cI|͙\ǧr?XYT1<|L1"F)C|8!)k&V6G>C.Mk_RSJb9)c5yFK{k5:N< ]\wIoJ)6d`LkuLY92VEܢGZ:XMYAWLX5.\3#O#X賳5$k'ȋ`Z%49Z>'~(OT$o,mnGNxeاK+sBv6]ˋ\bvDžDE&;oė9]KYbM]\ٿ3ط8/f?|˴~Mՙ}36$8$}QhM~ɺ V,]bWZzuKW-__/%//9d/G{opY;ǘŞd ~p~ٮ$wg^q^Lo?^:W-_vyڕoO`?IZ:̡W[ykً\4]lAsOg>wYgVhCf,k92Aڀ+/5|f}=ƃ30Ƙ{].ޚ}{4@22"rm)VZσ>,ôI>ݼ w4ZlQ߿ߗ~[ -1ヒ!ebH "cv4cԕdzs0L-Gc8zqLC~t`?c`<'i˪129"Ö$(g%4FDIxI4 ~#HAX!I"Dgؑ4B`#żьS!Gi! -#c!$ n!V8 oǒ $tKBBU5DㇱbWWKO.0dwO(b&[AZ{l߃P\#I(}70Cl$9qUcZ'!8{!Rt8>dMh&>> L8>O!fKG2CF2̞Af^Z?3w/U%Fps<_`^Qaggה?0wگx)d]B[.V0 -ݞ/荡ti0>-cv@9Vdk9rW=Z(́h6Ys i^_3Hu@H`)-$k545B/Y Վqs={5+I5NjFdCN3 Ng$9R'؏hÇ$Z-@"jCfDlGLHIӘIe:l'n1&dgH*sdI2udw -򃐁}B,H+ ˲c3G`Ҙql -|<%(Æ$NȕT$g -[CFqb`$$f E12o(=HԐxti'Fe19"И^r8sT6cQ4IE1y#tv/=qḡ"mi$ѐ>]/%&QFxAVRzd)"K4߳ݍ=֛!R+G[0v/,9Cxwф1FZdL`"[f^QU>ȵ0~#D~`-2{YH6:Cd)0AO(FLՐH Kh Ok&Ռ5Ė;vjX FQ)$:XcUJQ1Hf`T[Bc@ y -18 -n]Ըj -55z=$?Jo2!فaf_agwO[ 0S,oC^6&Yak #R4^sH/`P1rCՁWY?]{5n`l&8ǖ󍱄<ɠK2Ga)HPbG7BjT MHZ9 c-9'44<ӆ|Xb  Ҩ4ojڱcZ;S!)Q/,oRKNχ/2$8B -K -[!a4UM-.-'׎ ɐA0cgs1hcXCo$^ !~$𞰖0_\L#wWa csB>CR9YmS!S1#,fwW-͆4hh!c*'94ÅӓcGbT|*m;Duq?s0&sh4bA aq!4Ot0J_s/],d 9 |Pcd]4d2'1̱\ L"G@zJ:raRtq>ɾYI̗h([> ?>ZȠ$-cM .cFÓ(T#hLw8#X,d9avgHð&6C.IQ)' pIB%qɵH)`P:䏘mGQe6@XiSŎj^3wiWKLbl+ -9N$S3;.4ј"ls! *|bCc I#E鞊1SdHCr$i|9̾CVͷ'$FщY`8$r =9:df*I2_˳B3 A#l1L ʷvx >K5mdW2xaI6P*o.jFZo{mK75pՅ4U LPlyEtQ੅PR)UȵWa4]A CF`e<,~d _#KŐyJa>B5%ȃho$&hc&l}l -˅l;|Wl(\K>Ƌc,~f %foZht_JQgA#//,62UkWGbV>H QHy@SH#_LH'fB^ebR:KUWcXe/8s8^dm0 -AՂYRt|*d7h<׶~j-lAV2[dE 3-*j[0ײK$f BHxkɅ@FY~L##VbF`ĬI3:&Yst bL=Zc9rRYMfd;(urH(q5cX@|TYA~YvrjI9tt;$ņb"\㍐kn,;|@A3|jH!b54F>', ߢ 9,,Sl%uh-XARnK\N -Mz f\P@֜<7dX;X;(Ll⭰HJMXnQdVC[&|b֣t>qG+$!dkc``Q@G:ctKj $xmpsPsC}gg!&\F) RlH'efE[b,).Ʌ$[C2ZVd-ћ"xxK/J`z '$t)$}8$ V{< gi}eVO:mqӟun&Sc-% 7R+JG. -=R` 1#7άCǧ@FИrjT-O5d  S o5EYd 9NYn )q*ǒe1  `ggK5W$\0~'|"YPv.dd@מy|'5-p Ŕ8RQ x 2ڧ4q B>?j H5pc$ւzY\p}&[~Vִ~^ -2²m%SȐqDql- 4p(c8joVx|4޲JYC::9%|R-i1%|',)Q/2}Ȩ;X|(!#H|fd5.$Sqgc?_j`5Qyꌄ1oB;Ÿ⑈E|ېHA -{rzJe'cvtߐ -f05[AiɇBT9BPS:Uk<=C5j!$Q;咞Y|->J,~&?5PJ {_AxOK5wWBP+X > X"OIқ& ?B҆Cv\hb|+I1Yh7G- =.w<)w\Iŕ)KIK@:S~>+Kţukˀ]pכufWXS`u?Iȇ&Gdhm2G)Afu dѷCd?5$ A$Sn-GW_F0daqc*G}ͥL9(% s8lC Wfvߙ^7A-=g@rQH$.o$OZ?Yǝ/_ ca^j9FkEXOP~)GȩOIE_R2z}襰|!Bob*cLrC,e~j}Ce@1ɓ$QL5B>bj#_ GA6egL䦫-rON횄\ qB*Hnxz1o⢜:Qlxg96໤bB#b^a`cwW_:TjuHא2EBFΗXNbBB zv1ʿ !DysV4_H"{%ᴚ@B(ѩvX;_3!!'.rNjxG ߏ/w$W[X]yz$!w\fn] "'5+8=טlwm"!c;o[>D2xlxgD8gj:7pdgaF:rnIU^" 9Q#?ʅÿ=θIk!)XaoGݕﬠ׆CÙRo_$5fCOg%'B/Fa}`.m9|H0`!p@֮<Ѝ'( '@M5}Q,[lVo,%fB.\t=/ OCҋ{] g ߀+O{BEh'%7s&Ր<0 =,=7@*ug GA6#L.Cq\NkNN[c@D1evi*rbk%BP>tB΄z {TD[l!#7N籫4 Bo A2r,{ !pEB䛐,ҁٍ1@Xk/tNGHlRR?ow[A|N{?aV1,a8 +ȃ=:$MQ =<547CèrEC46kvl٧BV]P^ME {X=ȁIZ+[GQ $7PBׅd=^ԟŕ5@b -9p3|I#HPMw5ҡF7䡤ߴiEppJQnQ}f9Jݵ3!{ - $7Z=Q[JWWH_A~{(dKf+=o=p\BB_l $qGDL P.l -!!rMH_oւoS=a{lXI=GL=:^-<7Ov JީYrFda=Q kK8>[0EԍE]juԿ@x"CM Jn* d>!/+4|FEAƝԪ!{w{j\vոxJS| r/GK?cMDյ8[K{W?\FJ'Ylȶ8sHߺb "w$Dp!!.W25[O<Ξ\Ӑ4ـPצ `X3q梠o6|O Θ;)jQLHn,O_-E6}`tZq$ѻ 3-;uoٽABjg7e'?^vxp6xC􇒭|7HfeS.5Nb\sf\wkZvnX젳=^ ml씺܎޳PJ([˵}hX|ePuk1K NENG9!?0[jYj{5zlף\MnW:q7\|g8R*o.R \l tіޢ7S "z &^XOG ?Y i|HU~R,G@߄`5[j8rM6eyz$0bAtDTp(w"r QR[Pg!]΄_cu*I -K]'>z=`{ݔ?v=?ƹ`sk߇4zJBG+p`;'RQxj${QwhO䂵 TY*&w=jMK|rR_턽|m~‹jYW#3쌈#@7Vйb%~ -!cgpEReWrf`O/aذGv/qV >yjLHʴQX}jRsc$2կWH+Q_}c3&&A/ -}|jTǑG[n./}?SxT&±B`iov)t>&5|V9raR88OU`O{YJBӁ/G=obr|D-^lldXx~pЄoci$՜["1P84ߛ*մsŶϷ(w _`N߱vO]9LZc1[$yS ?ADӳpF@)q<孥rFdj3^P gguYƊ+KAo>D+Sa{CQଂLʁoPa,J#D 32,!rԌh zƩj٩8+ሜ S}߷Bd1;>؈ $ɷ&*>=84*WuSn RVu‘3scq&„#bO; Q;A؝7B*Ȅ,8pCtqvk]/| YJ氚a0.$ - }TԔӎc r] p&c{~bp*}c!| 1@wNb etΠ\jrFQyo<,2NeD>{nE/<7*,"tI.΁gXE} -[YccbBE?`&g|/壇^%$u'HEf P^B =O;cP=(\UN͝.~ҹ?ԛ/ =Q)*WjtOqcDtv]3 :'WZ.u=i<u*͟l#=n6K|.<Gr`pw6:r}11;SQJuE <KpK=(\V?Mr -y'sWgz<]uvooOO߂u_xtJW#}K87=Z6t<'v|&WbvoS^fR~s)jE߼ꉾ};C&`q3bh%KgSу^ mƂ.~,?lv|,XX=LPE {+;ω+k]^:=-=!)yd΀>:7*h;l\.P:̨.-9E$A 5}I^tߔ.ou$$ D9Zx.]Ev2E;P"ydQv(>ٯ%!LvCm1ÍpeI9ї-- ';u3A<&,i7뭒z"i(&|˚/H_tHwTY*r2TvJ|^|v -P1<~ZCktN!jvz)7nm -•]N_\f&zxTNX苐x:,uģ!~mI:J}& &+O϶xoW_h9Yg/骼,.V?B"wTK:]t^A=SZ(2֣Etrj#>:F }&cN!jN?2o~5 ->S0[=CR5UoOU!N5Β=#bAbbP  {4͊l8GGG[„=[>;=7x'}" -P٨.Ci(>*~Tkх=ɻjG/%ɍ:d[u >ؓB0ap ^Q43[wTٴۢYQEq6(ۄU:qև9N¤]ogOPhE(CK_ %:tT̕lϊ MpV>x-f2@G-hqiO^Z skbY#꒗fUG8O,.ou2( WoDeg$%Β:wE$EEW=6ě^ fh5vŵ35ҷW|?a+?Od谨9@W!^/[C. *0*~|%>|tG[%"m&?5NA.~3z۸&zbcA^ZXc-)h1ޯ4-3'u'س>za#zrRIq%V{8'a^uڈkj\E^Ҷ}%1G?u^O={?zUh>PQ lp"^Q坧z/1V/uJoJ:לИw5=XgnHQ9:'l8hG|&REymA1 ߆b"^ǝd]E2`gV=6aws}FC|)e) y%&K˞qv]εfgvǻr%U#k:6J]Ck -ػqh}]$õWqdˈ3c~#$~3{pV!,|#?n7RYddM9a÷#d_'qJW_uWrFpP1ߏu\K̬IHM -iz=#զ~KMsg+D3Īa֌䣝qC?Ev8?K>\GElC+o{w`ך.* _+~igZS# s~Ɣmm7aOG1IcR=OT>4V875y'4fJ:.M]g͇Ke-/G_6G߬퍽TRRs9|sBj`>I>1g[6HtAJV6kydu'ɪIѮڲ]Zen1a1תdwb"+[K.D׸DU48E;ETŸk**\T\ LoJ0~!i{"Ú8%xw ȇ"ѣ :#-%{A2FvP$ÍOՑȀ;H:\e6X+#;z`>` ǥm$ufe> !͗3϶\}', p p߭x玘zunF׭NcQsdQs:טB82k㲫ҫ}jCކs+UFhFD{}w*|ZJBͻbD[O sG+CZ6Q]W_,% !5L]y y0dPDkM8y+l¢x¸ mq)^ 9۶ ٷ-4ů981Kv-n18r,L|VWP!K=ڝ+BwH.oN'qu+Юオ}#"BS/7=SXU`rbI@|v_[S,ȚiG#cXvw2#BC;o]-Z/Ug}#qBǫZ1^1w\cJZ2[${[3OfObCj/'$W^+u?՜{9#h{L:.ܲL2!*i b!k˶m9'sS(H|k5,}ՙ} ;s;r̤9f>zPr^wsLiSEkƐm'"s2=m'mI"|Xo^bdobROM-ɯY^h3!6fHQ+ssVO}Ǿ|0`Fmx]+qYwSc3b/^IК) [nQSd%t˕4Ϻ )7}qjN>yt:ڲ8ަ0=-3)(ӫ!:xWN[Eǝo:ڦA]WnC{bbBOb@Sp -=|s72cxotQdle /aj|d0n*Cbڮ܈!G O0Z{--sʅ>9b1# 1'. (ǢWп=kuy)r8l,UY`>Ps>_;n.4w}Ga~>sofisl``Ftvfkf`1s{Ɗ~ެT/{2跕.QOKcҪ}+/ՆFH͎,bBtx2РkkTBCp8C>- W/z7 L3;0tAakдRo /^Z0L$0LT*+UMLA('i$.icty__g`uW p+b]hRج~|e/^+{\!{S}'cw7ŞљU>q5IL}._΄Ɉ;١FYn^-jb~jm~J^?oڶV,Z )` "{WRw9 ϊIo?)M׀[E`,* %> ſ /X|).?[ػE>/zy;;e^%+]/׆$S_{́/;^p笆v XxVl+L:S97ɳAz`2+7>Ncf׃;Y7F裬C!:'76'%Q~5aO"“m>zSP)K,=֕KBT\xtg 7Pܛœf6c OJi񋀊 -0Uq9 L̞֪yG(ie6Qo=`"ńXxӑaۮȠ} -}e #נ*en17agrV|%xdPTǭlsgsTvՙ,``M@yFOfM7K,jv*gVs{[ۡvuW#Qnvl -}|_.,:P}e+{#-#]Ω -o_~)hT銪pQS~%L@ez2~1i+=X~:`+.Δ? cIH|{Ť"Ībw)Şy%-} R8z\LXIn_~ gqipUplgmsVhx`2TiKU$` wLVlx",[]}\%Y\B /0+/wm|#Qfs,\~ٶ0Nۅ83(spL3g9V97y$``vx֊8qLUm0w&~<8Yb-[^ My[/oJ[ߔ>Tx$Ը% 7$46Tx#Y3tSmϘ&wYcyW9U7oo3Tm _&K7ف5`]=} ՚eNGᦼ@>X;S}[lWG2sݍY]u]ͮ+=`-XN߶͟mx,?(+ - +8f(,-SNe`m[9;}j@䀹[`;X kdO[,uZ%,;X.::]hL9̽`{=1===-g̾9S|CE -@uئg'È={± `ެ`R ,v <xZ^`3X},v1`=S՟0kǙPߢg/=Ooz]TjsɉŰNhre!E_]Tì3gӿ\ 0z)X6q1ˀRAڦ -ϐ۷̞ L\}#<f@,`oKx l-+qRFv'#_~'y VQ-CsFvzԴ -DN1x8Z\p{PXTnbJuAC0­p3 } -[nTj%:@uX9PXŽ{9j5VfmZ.G'}gkKCosOmpKlrK_S֒d wҸ)?cxXXCY}5ٻτ εsys`R.XKڂ`0l:@~Oո->f1W٪p]L&..)'NP݂10TwIcSTT:yڮm<"/9ͲP q5u=-6`XXK (m9%,YO%E`i -6`g -[#{&~fԴ>0QƆ;ߊkʉcךRbڛˉ'Rbc2.D 8G& 1Skưi@ٜ"r/g(-9|8U:S69K 25zx rh"gjO=fVOۜA}xUMsA50k52&ӼOIKIУӭ2Tϡ嚏QZ=5-v3# Y|m3#Dy``:CAX;U6Q` l *M(nr#jŽI;'p/өh^ɚ ~2YZ*FFװu1"62bP΂ڕ sgڨ.ƚ4$7hw{(lՁ.1C~}*̇gYjXy -lz"$ێ]W0e@%Zff-}!Mw(lX=vוxUN9ءuV˜:P_J| Z/bQlj^Dj<#?8iP0=nN=lNJmo;N8%8Dazi\+X3v+u?Xwl<` m`j~' vb!r>Bͬ/թc AæfsEoN?\ ;J/} o0ۍ+!5e$F}|o*N;hq3a폣Too]DjMx%KRUm0 Ser0oNb تkva~`OU]:]V1gDzaָ?ڜ83Gu3\ ;#1X3з bڄ1*f U'<gX׍=w϶f뾓b8'yYvK|>NnS-TVn<eZiwP*zWcmk1{5mY~gLb̫>,F4@y}l׫btm99%U>CFczL^cn` >Jt#ժND"r2_|"j:`!`KZ"pQ5 ~+̛m*< XSW;h4M~X ->łs>1ֆ-42c^3c)a\Lz*`x <킚߭/GƉ3c'/p1?qYx4J *`/c/wwLDJŒW!^s*m] }|F -Wr+r>l*-S:zXl2vRuz[_U9BU.s}U׌VrÍdprW84xSpAܓGg΍zO1{̨1Z7ł|X8) v>lEnM_Q/,JGܑߴ-Gm 苅 FЙ܉KVd)Ø~졂4_N=T˨~d&FÌ)bo>Q_#/M߭ v8gLZK=Oud:%[ >?xskǍo?bԹ'g+/Jޣzn7d">映"0=`h"IB}Dr>7MQ1OxD$eӱKlgc.]*i -0V9/ZA_W._ 1?jÏhr^&&KFO[}%ԃ1v!m!߻ɕyOK.?4&mruCaBGl?}8(k%X4p6,h{K!l$} ?XΚ,\ڃǬ`X_Akt# eмY+=-0 -382;c%_q -yc{ѱf.ĢMG= lT!#c K1%Kh}:d˭DffEƮvzAp^ٓSEM؋Wx7kt''uh׃YbORR s"JN#AU٠9[g񽥢{6v,%&! x1ss -^\2g *sl|Dd=-} x\^(5'| nݎ'<&Ly6E4{]%w7 iry$090Z/c I)h tv5m@ؽB,;'wSl+z}7Xb,j"`y*QveNKD{0[M"a?tG-I~7!U'c rt4m+ 4y/ ~0_N0~![|4u3NnYO_z'(o=VcOV$`umĖ -V6g=T,NyNCҏͰ|,.?b,8qO&L w#~|B*vu<3Sٲ1bjvެf=`txwC{n}uf[f012͌Q)cy1%chtI1K{ -`E@s.`q ysbh"/·89}^Y@VJ܃Uɓ>HŒ}Xz"b3i7G}eq^❥er+ImRZZB27碕!W>6EO:nOkNu,s6w}f#s]ǯe,9^9 M1l6ڣ4B{.óԅ<'RqHOMF{ȫM7>\ň!Ҧr}~9\+\H2[8hʕ<:,z*6YP׆ ´d֨6STx7~V^,ZauoF&`Ƶ0f,8=97c6F>Jjj\˓!vإ $@W]tEC@_\ -]d.7[I|=T^yAbAg-{,BaGul "82IxuIQY``\HQE7b>)sqeZ&׿$Un^SskR7IH_S]0$vn&/:FNr;]+Vw#8p[qh/bIU~ kio -!},+y=rϘG&C<Ca-m]Ig>7-AG5AMAt:  By]OmEJ.>Lzy/.͉Zo@Uc SiL -]Aӕ)y"?*WοM@ #C4aN7T13W:=Fgᙕ¬=Āޮ ٤_" 폧._Y<^hK>z-͢GtmC<_O\~:A<7?bxh*-r86xg7t隊vBPxVrB8e)jt - M ־C@k~`le9\&9✺4Md}\ mE/SwKr{ -}m3LIXtH7.i0-auRǠtY :$OڨMO9B&h|6IK~7\g -OLDbx"C[>X  +0a\ xR^2]qψ :ho\ڪk8)W|| -~>l:~A6C | ~auaOJ<ͨ؃a鰈WM1e.3;oṟ%OVW/%1n庂`rn"8o{a1&1cs weذS{ω|sy+蓎Y([IEqY6ʯLҗ"nB{$nk䯡N*4 Y!8?M=}xPQ j ->SwpՎHG84.QO7b)M}A=vYM\A4!u -{ɷ>Ľoq\tԹ8^p칈xwDOGۍh -7bHŽ{NM"2a<Y짏 +\U#25=\<_mh0m0~:jYt7|X*2z~?>tTIDUœ &fb!wVbVeY{y7 u9`۪`Ì`M`СC@߈c -pz<< tVFA_mZD?`~\Tf!<]11{DprzGrFl YFy&EL,FPIBv[\,6E Ó>#W RJM]V[C_Ve2+gģOx1)cl6-\ jFo -҈Dlx%i:Hw q/.X=L9h?AWV}0,$eoaZ>!Q|JIevO{\6y -b_lƣn$  -8DA?E twey"v,p mz3g%CG8=}Xo܉ 1a^82?8wؑ߭e”=G{JL%jeIM`DP_h ڛo-_5iӄ>9yÚ(߁)X`çКWl%rZ+3wYsM̜5C]GR^h kK{IӊHeX9*N13'q]ѳćƙv/%KI 1pU<0w)rhP's; >W*jijZ5D7ĝ6 wP&xh;B -r[K9HϘXݵ[bah*p9(cCjxlvaGKT448`@@:HK 0,$;ET.$ 69ݺXVoe2KX(=Lj . aq0 1Ckm%4BXtH8S^ga}AB_AfQ3Y§Ĺ I)8H醰:S ǃI_j:'tCxHi3!71\FVn(@5bF!|c5y1}1b=[{iNGQ]Af>4gÌB3='Q> Pނ]?DZtwM`-j2D^[l149| .<8FxcmuU%> LhY:(Ѿ) Gq^' lt )`LXC銛VD,g_[ƲT㥼t` -::tgr )rGW|ዳw˭}GǚF%]̻"^9*߮'Ѿ| =(50| k0*QqZ<nktb)IFHXrRgqVѐk % - UӤC,;DĊB%O:E:Eq[rZjqx3!zQ0'~~K,C {"6*&bk978j E\عigL$s Ro9q𰣒1B - N2 XG `q4P>S *ˈڅtP -` Ⱥnˌr8!j>X-Xjʻ8৽' l0?ucJaJn1~Wd'oBBHXˑ6cQ !SyvʎyfbvTld.@1( pSJH)hϨ9H sÊ۹Gd<<V.csӉ.c1SD!V{*xu97ҨGdTS̻{f'_oZ<s3'ӛ5R z7Q,[%'>=T#+af}Q1Щh -wIj#~#gnf V{}Xj`. sH-!&7O#~)bgay6 -@mcvHn6Ғzo=.K^_1wOL0:+ظ\gcG*΢}He> ӫK8Ehq\9HY* -[tɜA"oАm,)r6`ycJSO6-]tИ4&"}e5O!R|=F*CҕdI2H Q:\Xg@|c+{s=XKƖ> $Y\ZJ!'¹Q䓴P9WU(ӤS+mbs2WX41dΰ!7h-:)AظKW#ĥC2f2;>H/RgмGkN-Df8gSOj=H(Xݫֈ{ <'m, pg~|1o˷J ZM񈢍#a&ZրHwKCDLH"hw|򰼑#m/n_]I .&rMC63Ȩ., WĻ>=iQjga0/?lbj By.>eIQ*u_x(ֲ0".>0~KҠc\g JTgOLΕnD_g6Doq06Wj6g=&#m -Vf/Dgd{igX;sO m!y1j6"E蜨)kױH_^pAaJoAၹ+a9먘EiuڈKeu"#ڪc˝ǢgH ---_y5q[kuCwm̮+'^@k|suLüuIV9 -圬^1Eby؊X6Sc.WΎA96EڍY,طig#,{M{GX {jg'al|HpJBSBeR -m(eV1vMlT"gBLo{rF:[0NiH5rrj:h7XAyZ=,L'&Ҷ?YܬPR^34w؝ YGc{.j|HyNBQ⓮jgE/ s 刴Ez&cN 酡8$;?OH;e:NꖾPb'3{4Pt t,%^/qw"kߑ/wx8~~-sbV#&OB[=qrJӴJ:[7ew;߹͚;/}44n)E^ًϏAk£R”QGYB etTn.sQC~Յ1Lh'tgcO64/]KdBDh}Q-tιB`jg (%hD.G -8|_lQ|X;v/u/>7q|4f=b抽 It=w i|sA(o\bٓ]DֲmIu:VXܔC2&7R4PNE=&FXm,%hZ@<jN:$ -p'7,YTy-=\N󠝥e(jƃjSVS,%֣g,?va--%ہy\8&rO4c@={+G-bt>L%=<Ǖ%r/ET4E1[  e%?@Pne -@FϾ -k-E\Arrۀ>xPm|F t ' -hsn1e 6簇1R|4hR\IC|.e4V¾-T; ,E;˳jg#]\$b!CokJRY-wQ}ke|SKCW@3G {8_!ԯgȻ94)uKSK*k2ԗ[8R9'8>f0Or},jg5$TtͰp  cn-wvKJ1RґRhXCC3ŌˋqgARt b㨛(C z9./1Qzx-b=fHAcEV^߂=wq)WdktZ!iGKFP gPu --&aErY{EqN~,8[M6MN4pǘol|{+]!U |2rlTĈ cb{@_|#zb\ejU{~HBS}Kc$kTg5Oa=%U_R⩹"UfpҰ*CS/,pDw-`z, :N}zu{"U]pgRۄ?ESG`|MXʛ OҙA?N@GLvFƚ>Ƅu u)6|Enhg)gť>z"T6\,9_NzVrrMzޣ%d[1[IW卧X}h9 i亣K"˾ZUR|﩮-\vE U~qŵ҆佒Oτt>$şڹh ɇg!z.mP}'c\#,!OBI&֌Bo5=ttP%'=#k-Lo.Cf$_f 4P7 QSqB'Y9=&{E5<8j[zxmxL:>˹51|"49-|ŰqЬ}.S DNI5 -$n xEqz>:~tSNBMC:,)׫5^2sը j|A 8%| 'vyu!B igˊ!yB+MI>2'jgEm#s* -XMy߲oa4ubT>l.Rbc̫КUx E|mН"sK"|YJHBq䅡v9a]J8;,yla}ݙ n6Gک*ؽukH> _$'cIr}uQGМ9r'CBLM5CAw#q2%a|؜;Q d Rjyc'A -&Q99M9@rYԢ8{gmhm~rI'g9+{AGkbѩTB`tPF/vu֬!j3СVa փ-@  ,.Bž򼻋f1Ƥ3灥Dd} /ңkBĿ+-gu&+ -+EaT&2﹤8I޲xœ'P޿{ϸMTn]XS__ĕ^Op3˥XD}.0+ޗ2{ ' q+EVçҵik[-佈xU{2P NgSܲG/{44UpMXc{l&Vd.2G1tP (z X$F#ܔQW>#F#'E{gĞ% K_ Nвw qy14~ 3y6]G$ 2 y]romrr Vs@K%qF=J S~j})T=k~חb m;jȷXQ'ϿOk=#)RO_.ϻDsszQ9g~\1T{HB' -$GAP6|bGf&I5ko/Л|p;{ aGu>3|M 3 9.;p[yb~Զ1MV-;K@O lI{'aŢW.rgoH?9WF8WSxL]h:S=aW ̧tnQN>7꼢fBp(8zA8sX{F'EA-d3/8=uR=1BuɽҵR&V<5BE6F{` -^G Ov6)f&c+tA_#{  .ƥ㳩&8f>d"ӸI 3gs^#aKOTcM:;|aaWu*OeF;}80GhFM_z|=_z|=_z|=_z|=_z|=?wl[gZۤzbK]::sW{Z`ur<mo^\oko[y,7m,NכE^:o΂Ezsm򤅍mK K-~yK}?o9,Z^p΢K9xz1}{7qYgCLg޴z#cֻyr-4.zi%u&n$_ͣ8oƑH((,&M ĀLM3%4"2Fj$ck!h(l=%?s- gAX:D;Dm) YMQ  ùClܵ fF޽I])C92Uf]Z6鰕Hp [݋UbzK.AEϨ!i _.6sRGHc!z iPIIJ7;p[߱"7sIkĴ Icë%(JX%![ȟIrQzx Pzŏ%:dNm+FaЦNA9hUdC+lhЏgE34BpncɈؾ~G/]dQCA;^ 60[S8b#Ж)4(W|QKa M݆&2ƌۦf |ؘKM̘[h [as>׈ Ra<]I@XyFF.9kh/rJJb/% >Br#|0|#HT9G~hK&!%8$Irǂp¼__J'6/"+&p2n_\P9chs#vCJ@pw.%"NmGAl CYchs9)JŠИL4|ż&1o@,#RoqAG2Cri&lͷqGZ\i  ,>62l<hic(5bd2=dmė{Ak8|?m9p*e l'qڀ݉($#w 2^9F1%kN;_BP/H|AկCŹtn7;`!62.\Jxfmtcȧ_Ļ$ 21v}u㳅sN]Dw A%q d\=I"QFo2|g] N/EB8ئZ tMdhs-o3h8qO0 ;/4BHRD$-n|*>6cIr-HY~Ͱۣ _X!O3j$3NÜr|% -7]4I%zM(ß 1gft6A&=S чs3A䎆`v8\P7x(@ M y@FϹG@\ LJ - !qWpj$V >J$ߠ;#(IEqj;1r(R/=R=j[[>Ch,`~Hd@G,//s[+麴I$$opO)a HMf/9,D~GEULIG"~\ qh6Om3JA>m$gtBȑx` H\VnNs=\O'3 -0;$։[ -!w&(3Me$WQXi\sc-;k$qqh L|b2ĸ30An{īNvnd{x3n7|AhNB^M;(<{"_wh4!}Yil's֣/i@UXħ\{& oF {@MBrq HT!S|b0 nǜɵ F j+l #T]dApqze,Pl'>hj2zf@I#Sk:()%q |M rw@|{b{k.Nkn `+Qrۇ`a h<%&$0GuWC鏹ۤ> >y!qYyk#`n٭ ex;M)?6O| - 3 %sH}$6#9%c&hPL<|خ_ob"_\ v>1eV;{9F|,$E:DyQ_k)K3gFܱ#lQdIQ\#u?LJlW9U]l*\w{>5 -NQk!#w%U6R>"?w4!le֟(Y,rE=! .v_tPܴV([ "2FWB!/D D\K^=C?=O8˺XƠ02Į0)9㞾"rN?ԍMb Hu{[퍜dU W=3(/& Bz$!ul4y~Gq[`_&+C;#i$Ԩzh)hQ @v֣,S∢o}2Ph,|O 34Q8p%<'~s`њ>jHzd}Se@"-Z -$_jJOS}G)J$͡b@hxd*)yuנ"+Mkll"(A rf7uM=CQ_D9?1B4v9|Kx!N gf%/VqY sa". -~cs+!LG۝ELFrx޷ք1{PBIF޳Hax_.C[S-*E՛ S삷 Z br:ag0-wF!?ڔD쐒s03;RQ -pB )qRA]=r,% /Ty:͛k 5|5 g OςOT[ʥ^%æÇ(HmC )#ʿ>w +ȹ9\܉[ 4Ԯ$=|@q$ Bnl -+I)Lrׂ5bӵAƀH&q +.&%17o 'ٔƼsş:0ST4Éy [!<1XO --@I <@p)B.,>Bm$W #Dآ?|}12E|9oB 8s'_| }F endstream endobj 28 0 obj <>stream -vbw3$_[ZwLe<+`%nB]T '69Wr2*v@6ݴV+9@:0GJ".3PO" $-.fiLui" yC;lNIs5Ki@Bqs"PQTy]LyJk*[*v!}ߚIl"gXk/bQ:'~ v!ܡB*+z.ĂBqZ\C9; @r/U(It\)9^)w @ k ,UX-|l؆/f]׭@ 6q Wu"k?{lM,4 ֳyjB[`A.KV.ifvۤy5>Q@<ĺ k.=.etJ:@yw_꟢+_bJרA~ؚ|кG -~ -B-$֣Lݽɷ*3?.J׊7~viQA/i$=.0R?]]G*ݨIdۗM !%;RA2P7djwqG +/}-Zͦ_OQ^<=Ct+Q?1 -9=<ȩ!2/+ kE.خibjo1eHޯ<ȇ)/BuFYܐY&ⰨZ8@,WĨa  LIsz!j!H - 37Vty̩؅f[ja %\]Co輸״>ݫ/ƛkЦ_ܴqDg ?@a!-2/󮯠~Ne\\e_[ H)|}~Ajh^uTJ_lm/),oRVXH:M؊ '|ܡ)T{]- -cUO@D -~G|*v#v!؅۟bY+-I.քIgjs- 5CGh5J<:+\{m(ui1$!p,[յBr47%#*1I#!N -dW3b\Xq.`1o%jR>%͍Mk7271&#]dK.QòP@Xɽ7'./(ݳ7Ori\TZ˸X"}F?!mG~of*vzzlF /b*ő9 v Em͂' ^(F{>˕ չ'u$%ǞUW@suqs*T;D1S k^j7b|!E}m~JeuPRϾ~jri.~<7œWb3E}XQl#[O uD2)%)Q2!,!")LH{{XVEkڵlU_Ԟٌ[ v#B;yaSKJ89b\)tcxBᕓ|a* "5AWJ,kȦݞۡkB Z*rtI$iRX_<׸՘ l9w(#DDׁw& 8h8YLH@l=bh$Mȉ1M{&Aػ;Ufh1B~ǥ#/oI^p){X==BKg*DuuE -HQ -B}&tEiXPG fA\whӇ}ػsT_:yj*_a0ಘj}3̇@ &y_:*rJ~9{ օOPX{b͏qc:{{[KabDuYh#Oxܑ`*=!fpP>f0t֢8lCJtOKz/䭨J3(K(^+g@$؇|fpz6kXcbBE?r" a͔^ +z(Z3l4<*z:ֹ"&Q5 S'@~a /o[ qJaw05pΙ!h+ NACr. j -O +QWL )~2,ɧf#g0UkQ "Maj=G˄ki] CMqniX+7"=f 1~Y/`5 an/b_o5-v% U8н)b]96M/KEg|3GVD>] -H}#t+}&M?~w -;Fݣ{QPGY:쩷qڒj>!>tQI4/ ɉ33S7b_R|%dOLP&oa/|ՅKbN) 46W&uGԌ#K'PD♢Olt5jDܰ/z~u!byPaOXGb`lهSL  !Nce&YEJ.;CYq -I#4.*;כE-+ݧEQ$f-:*ɏ-F #ŝɧKk>b([VN"U9Ц4/(^.}V.B+kW4:8-{Fh3;7 9*KнA̒өBy|iZ -:qkyܺ\̻ -/{P{RQx3bʕؗ(~F4$؞z~cȕ[ϕ<_u< D\yJbem?ski]O9?SQٹ+n_KΤ#3h-=>~ _ClŹl%kmkʶMbuPٲq a'kEY*a1C;BnQ'Y7p^[^tHS8y*7Yd6wV+KNp"c~e$?a'#űO&*[^+73/G54[{9j~5X~g^v߬W~gRm.ߡ<ޠqUo6 h\!HqR8Zɗ_`_|V-3^Z;ުunז*H~Pofv1_ڀ??b]|쉩f H͸E!J_ -7_;J5;k-XG.[>W.|W^VlpLv["2P3:2U{,>`ky -&[޹-TtmduK۬īOW[\dE~{J]ʑVŞ{ ;m[m;ݹoݸ?;(nVq7Kٵ_ܣWn統qb# k[q0],SδY'[xkPDuI8G_cYTp鹵㟌#ӝsT_mG];\.ŕwؤVT|!{'N 쭶m\s- cKͼNn˗Vqf6P˰I}9W?m~ -;u{_ݣ,xK97G3]_ZTe,ߖ%7:'UwX N͇~ҕ&g6Q4XeۍX7LD7^ߴTvSyo6۟/?^ [_pEHjSU]˳3En:][SyV7IζIr?ݸkmeUXfuQ|p={ꣂJz[u$KvN@[vO'n'nVZ{Vj3z孇w,*^-H8o״ؿ7m^|z(U1U|+k^&^Yvk/Q}u:\}{⶗ײ>g((+zqa -MVv)/t9+e<9˿qgv]w^(|ۘ!tI/c|٘tn87S٥ -3gܫ!m//dkNeIㅦUJ݆mst+ -Ol- oJ)^ZdTtgo>فlw3,}鱿b"~FBٙ|꽅\kCsOv(C[V>)K?)r/QQŲ6LY[$\xc-Yf\yʝs?ʕOj÷Thr6I|e6swkQQ䓤'IE%yQOʔ51绬W4dT63e͊N|X՜L͛{Wkg7{K-v?n&~j2ƥ@G.GJ]ٚRwyÿ965ڽb}&{\a;RE)wTJ_ar#m*l+ת΋zМYY>$ =)}o?lNmxZK~kI?Lm`femhVrm\[Ky⇦Wnq\q-cYpgߊ⹮O\/YoP۵[-Yxſ||$M\~x&z0mx`qK/&gUۃTwaM)%ͱeګm{y9[^˹_Ϸu$ǧ#37qD'>[Ӯz!_o>I'Ri2B> -. ˎjHU}bR-?n{p(yUytaMU˽$ΚLnZyŻRuL:Vyutk(D&'?VY?n>sEQR_6pފB[kgkI#Vo_x3]Mksק}۲ -> /r*Qw= u:^ޖTќ$D}1  8]{䯾Zw&( u^mDc,SGz-z"=|->hcnk]'dץ4D&%/߄0ovNS򣸂{Q9{Ed<"U\V[HMcn#F.R$=N>H9|KPhWJ2?[y?^wzYK`㹊?^v0{.w9̼Ć܂Y"rܛKLG7x2avҽe7ݒnn}7gnw{5F7F%Ȩ[ڒ6TFݿoj}79v( -|UUST(,Ϸ5L!Qhn vOBo[Rw78ӻ9yu6oؽ:,i|YHSF\ٮ@u+uv<ݞ4Px7?'6 O~\%-oaʁqkW}GCrL}BnyM={aLLC|_g[֮ N Q_{yNhoCޏȱ}Ib{ yoW'c۞Oq } -mn{#Zry]hSnJi*$ސxh_v^^e2xv7uV40,YYd53oя%yKL@voPTZLYneߎPJ̉&*Ox&U}.*H+,AXr",Z!׈~wڱN -F>/6VYk(231{2]f3<3~FYFd6KUaZW̔㘱1.3Dft/=f\)9qW2Mݘ5#7][OmݟݬF޾q&0V_ua99Qy1 Ea wr1 SߏxE/Pܦ6=)d.̠dӁ?l -<4bQ ]äz~-68۽ Udѓ=F2:f ӏOOHfp=fR3'tU^.8~C[F_wcJU1\l{%Ltxq$71)nTV;j*oGf Rx&,c;3n?({U޾;[/}x3ѹ4Nt/^qckSY157G5WK3e0L/O\h~yf UOZ߷:әy DEc3wWyԷ9rGgݎ{;2k߭7ò -Qyzhq27ՄO κ[ېX''s螎$?ۓU񏖎̴1ӈ#sCJ^fyL58zhҫ+qt>CALoA䧑衳91u?;([]{SYogT ϪE|L{yo.E>Jɇmy~:&2yUGG"֔)&vY}c0zGkKϿ79C|Cia;|߮5?]~#\3*gwRu6$t\t8Ք$;ueMb dcO_Ⱦv;|[!Y-5A9]2O5 tuﺝ-~{rc;f(Q_n}fDcf/_uoR|~8Z|9od_{^p9WoܹsfhV8uh}ZnJm\Nr]Lve=ɻՇk3k#p{#VjΝ9`?z4עvz,7}:z%ÌfjOfƎZLYb]{痁֯%\Q|ح, y%^YqoSVj<AHfM=X;Ihͻj/^i}Əz{?_z{;ӟܡ`:{{3df$fhYhM̌!7W˿DpĄSm凫mjc._YJ| 0>[oۼ2ӻ=3n /`dFk Jg3zB˙1ÿgF\[̌Йnj36`3 $]L^YjY@4n֣I.ҀN\*$ٝ"+:ͣЊ !{67!ZS׽d`Fi1C{"? &OXK&0#{Mc 71p3z:imf%2_5Fx}#(Z|Ko=Ww'aPyw䝸uk|Hݫg]?&|D|/#|?(7d_Č]Č3V=3~)3f3f)3jzfwqS9faC`ΪsW2$gQr'65fٹ5j3s{E bf=u^PFߺ.sJќp9C ~/ =>ghd03Rs434fй?1w0ّ3vȌ`FO3'0#Ggf\fE/7붵i=VWTx)!5 -Iϻq/PaPM~y5= *&5{rb՘S6 zy{=!Ìό85lݡ[ƌ5|3bOMbMs]q̢m't55K?u? zjD[dd= *|X1y Kĥ}kV010-!y5̹ZsAd >gk[YBs5IcV%nwk}f:_LW3m.jHȕٷ5^{nFȈ{ϰu{R"vQ{/(vAXkU ґARl ņ]cXc&{[v.9g=?Hu͙k˘?i^wȹpin\/F_4tӢw[=>3,g=n7{D*{A Q䜆{'!۶ߌ3,&Hj:̰K9iv,3nI3s53ֽ4qaFx2Ťq=cxcX!p5?:;\*|x j 74]!uÛytRÅJ#CLc)͵#eJb#N Ϗ$9f@r~VNLQ7GI@{|ǩ(f8f"fL`#3ʷuإՒÆ $}uMF0Lc5ؠ3Y:f @f_fP7f(OfЌf2jYGMl|d0d-,[ϿE=^[uu}C'[vF>S{mA^wڿUM> -'yָ\Ha=qƌr! N|Rf`?IkÇ,b ]8 reV0ffjfL_qxV3Z.8mˮfy4(8UQ~aM_^.zhK%[7~ S|l :N+I_=/ a뚫ٌ_ϫ'Y^ҒfYC~d #x(fg3%;QŌq rT@̛ : -f5CÌsKa0ӄ&yG&꾱aR  ˣgkķm길ڦ'C~c͛9_ʮk^9=29́ |ؙNec87\aɥ ! 6?o~oZ<{/4L\~C/D?ܐe95OvRk7kxy混}^$߽mZaȌ 7=ُL೙Q̔S>3seytzy97-j=kf]Ͳӆ-=hpr}:}a}C b w5"gy%sm -A,cx"'~be.3/_ҘA$  d/Kfqcf7qg<ۆ9n-~on~fo.zm×6"_ !ޥK?]- {Wƫc+/K| -lI+HsP쏡W:1O`SUC{XL=*f%LU3SӘic<3~3a'3e媍楞5?ykaW5?^TKo~ Mrs RAP2&JK_]꣭//lUpmsJew6޺VVwF;7U=<#j 3l\f fW<•1|fa\k~jA4[borU?f~J_ - *_ : _^4x|2/+W}yʢէ A7oc_hHS_i T0bDXnvC^v.M'tO>6Pzh:w ^Sf)a6cZۦ ɳHg3f8/}ot)5+jH -! \M!@ξ5}kS4H M/*ˎ:(:sVGb, C*N8ߟw-|NRob&Wwn -z~p^5ռЇTg -n5NO>C\}wFq \?˒>&Ù;QÌLf\zѡ~۰'bs;,h|O6o_^1(6R$0UJ3Wo9+|\.=~<:GaòLc 76֖ 5̓}^|/.]}FF޷Kxp{qw!:S9U{F_B=H̘2eK -Y~A!!k]_J_ޕo3wL&0),+okEtd=i U]rસh f8凿y}%rb݇_b2>΄r/nwZ-8C^B6qUPVfE)̨!4Ft?ҌkBx6rPs) K|fг}Tuo7ޟYpu/ſkūn<-Nr9mQ-2j<_^ʖkUlnR_[ +B3]B 1͵_>Nb@2j(`0f2\鲳⃕o$Of^=ѱBx9XoR&pLUyc'3K&f1!pBaE}Ob|aw+G/ޗKNn>'7 -eb6 ?U[osq yBnuǣܾ[sT&*:mCx`~k9'}Rf k!({仞EJAO}b z]|^>G00؝Obqkm vBI?u< -DBh:݉a7BˋlWwv?Uu g& NV?\mS}e J-avcK̖.U0^J4Qoa3~:Շqx-^ޣE#'{z2n1>Z3}IhJmHIǘв ҕG?c_k?[eF]?2W'V[~w]~*}%^̭=0+>_>:l<{5ߘvn`O\At?tAWC?]CE[+w2?jz>[?}QO)GΝϸ,1')^6tUbM|pO#/KG -jakTe0@*h&2[5P;7!O%r~TRuO˄S q.cS -i\և.¹w*c=]jy"#GS -OZ -Ɓm}N5gjUflH_i̿R{uü X K2էWkk?-w;nw;y?|G;_Q(:X,^/Kkw:G-ɏw]\:PxDn[77"^:{i*mHE}ƞ\6D+#-vUunKѽ3H4np9`ৠbbbM.N򚾚83+_ nRVם=L_ 6}6S+_~x48cت0g:~_{cq;1`r)N#A~]%G:ZV?)5 zN#W^cG0_a5&Bqp+Q'<{'z[ ]ݸ=O/" 373k9$}n;/hW%w^*DU4wZu7OߖqVR2$[}tg o_'S!PQ/u43efHs8/2sdS.2\O3u],!sTK]o+$YB+ KmzՌɶM GJGZA\Ep>N`Bfu?9 끻̵gW!܍Ta/I%G 5'|B푉$OUvջ*;S{zy!h; PERWat{^b()@w?;`!`7G&kvޘ#z$| - ?yɟ~)+렎+P^`S6󻆂GR1w$[!G'з"t --2k#$2P#kOLn.ڟ.|'Z΃ >N> "Tվ7B-=o?y!?UއM̷yz L8ꃱk3T-Pu:+0=~3~42f:p,f匏 ܛ6Iox{*o_% O4aA6X`|1"4q[/͕:,;%[ ? U{`btgZ -nޟ7ߝk(¯~Y8*H>"B}^ ؚS\:^/PW7Sƾl2,ӔKhZg> 3}8feR7 4"׮?AvS讁C^3A -zE Cn880B(4H(mu`۞,}4Ui$xbF+hJOgRf g FUǨw};}p;%'wn1'2TXmVYAA&9 -L=uH8{?+V v NLޟ #>|r-_d;R,;n4 }8|V;y;B>H?+Q` vE NDo"ׄK_.hNm˖t WUfb{Zb5 6˜rZnVw ;d.[,2YЛإ TZnF2i5kN1%eDdY3SU0P;8:B#E뢳,;G76_m_w4h~U}R 4#OT꽯T\Ɵ~-h)㺸s$أMbluEd>.)6}2oPyW}_-uv~6P:U|n>,# |Pe0W.3K\2lzkeo};U'ư[퐋z.fk0 FVG#{}CΦ`CW zxuעCa}DU~ ,:hqLw JH%H|i^<+&Q~BF;!Pb'$C!TT`,fۣzupC*1jՒ: Š]^h31$E8|V{N:x_hP`=.{ <}o}}b΀a&A&Z*iq@nX~ AAM jh(\BEJ` ;2Ü-ͯ7mWiͱpc$3\x\}8 VPHQGyILcDC`)`ɈۉTsM8k\}d2e;2|~&}.ObNK =yȇ(XKǟ+t/Xrz*+/z 즏a~1[xnJfy tq3zĺDceX"pF 2{*op唝[jVtKu|;yTzt`*qMg Gߪc?~Z u^&9eG3AH#zbA'YSUklj -,㉯Xg'740  1p'XaKġ12J9UeyA,nxH sad t(3*C>·K/@`xa6.3:|jk6$Ƅ mcJvybV{&8H$Q4{!MC>x!gA!+)g19JRfd3:e\T?Xa,NcI`F i5^j)r$ IUƐ})BRéyჩvv=6Ki> ->xKOِgcM叟W%O5<8BH(e+Mw Rj`65?nȟ?~4QEduI&>vuV\䐚dCRT35ṁ2jo>GwTk>Bمe-#ɧg*1xkqdMfJYCNkbNļ: -GƙI*} Pˌk*K/+T< z`i6J$Ò(tmrAJI4Nll\sp2=ڔ5} } ;r30Cnva`j{\2 g'O"#Ė[q+-4 \T9ka棐Hθ̟:p3Hgg kk*ΨA'>؏~,fRٶ (ܫP_4\ˋےaGj ] 'X[@AF=t4qZF-MщyRRfFJ-WZ/.7~8v-D&n:_l/v ͵]Ƿ: aڲ[t -X|ڽwUoUlX)9>Z{2t=|W~4w=q,cĘ -K1В#OE\iT" XqQØa- l"q^T1uX>O?s_xڟ ;\-DMVT1,Cp|cˁNN7_+}\|뼫麭~"4%#tEuC5'fחɭ7܉mМ6BŮQ$M<*[hbm{IݯnuR𠅊4?fXF k;.چ[mC/g-\P m-w]׈ԵJfШ֟whivTs\ m-qg-CRlzgvwj-ɟGP^ߑ -+^Gw!w= -Jztऋkov"RTYp6G}`|xJ!F1"n7x|dOzit}tY`|PjwKSzqt1sO)ŭ56d\B5]}_] t=X.7̧ڀ/B?B+X{Jw:xΘPo91};wܛOl6;n/@ ,u=``O-_."7t5#츹PP\Ih}D㡏rzwp95UfXsn<1SsG箠i7>R`IeU5]_+:^,*ǕNoBH)u}5֨uH uD)mS?HB3؝J6VS1U/U/5NJ[O&izoX6w BF|zj[hIhgq[I^vR{Aev(^x) LA8_]A]f 0P[E$ނO+ h niJ+l֝ThldZPltY=,nqcD}`q2|nA] )5e b[.V$ڋI^W9@aq|:kuxhJE5.+Gf#6kRK kз>V"P#qWj qY?A9uzCPJ~ڪ3Q=$1N1Cʹ}(s9{>zG\g;S]a`*j/qݱܮG37ϝKםBu;_wnt1Xh>zGb=e<_UI$ 1SoNTb+D%!.Rݬ~J;0ɳӦkHc\nEW/M=|➯}v@;kE:<ڲgŠqlu1V8Lٰ s2m)Sy12ЀȨ nw -6NWI(fm?Yܚ^עC_,?`3˕LNӶ,uS=lO}=DvTg(i(X]vwԇ\G;jPmBhSPmxʥ3lO,m˩9Yb[Кkjȩ|ZS>I."zA_^|o|4^g#~ 4ëĚ}ƳΚ;JϽxZ/%1r;?o=u'_P@!H P{$^oGkNL\pUd)X2usBI MDmZAs=^l]$W=TsMEЎ&Yhg58{a}PʀvQ*g :]0!m|YEyjgE;/7NK\mIurR uбorK_G|!WMLsz`VþDbtk ~'Mwɝןދ4hhjwd3ͱ&%!C7+ 3R!F,tJ;7!ϕsۇ"Aۏ1W !F*!vbJuXWZEtu=zmkM:/.$ t~,MԒֈ5 -kD@="%I[kɲD{GCS6os- .|uv[$'~`v͆S5Mm#]w<k`qKvyOIwܐb PSd+¾U9 --TAH[ۇ O4ZN$fv֎;λXKS; V?Yέ^쿵Jz3E|ѧ#1w׮C+6l8rxv/E^a ^u|h0frM.Wɫ3]==+v,Ifq=)Jq+i0J)#ֳZb%J e(/0*FGrԗmtTqP-ʃcǞԬ܁} 7@암 -#@;8!G~ץM/eSjF9"\H4$ .媮1@];Y˭%Rwd6u8BBn<9SM ׶o!PJwo.es.G$ߺL_i.*$qHp홠%OLɃ|ۅjgQOvU,?EmTnMv/A?Y%=YJS;+c‰Ԓ9/㋶; $eT8 -CVp|t3PdKz8ǻhɵYQ}^modBr!&_Γj5Z'ħCM}4'5yd~/<'P1j}ScuDox_a@w O!RWmn?OabLCN/+IܖOKj9ja󉛼?#=.BO?,T;4MYgmje_5jm+6 \>&8[zv1-x.bz\5'f-8XׇŘB :RY+=wh ߻i6Ξj8ǭZխrU_]?pMŊG16B-Fƈ4?SdTaFbx..w`h'KLvdܫX>_JE,t&Ju{~HA.8+hAc$kTgkn4q6[5xzttٛS}fĝfǧkH - oh -P<3ߐPMh\&@  hzCy yq_EΏXj:MhX4Nn -:_z~_۲D'tO2:e;Fʫ*>DH̷})sH`w>wvD|H{GKT; Z!XZO쳲9_hghgɛ?#6]A[I69\}h uyW;FQ=cgH[/ΣZO_HhUuOn=jOLt>]wj.{!*L=8kwyEtm+\Dj,Zk;>EnDre$*Rh9|ɫmͲev|j D.z(HIuضcauc`]`T4RW4]6hnGDŽ3t;W_&XEè %MɸA5Vۈ4G8:^zmD4坣-m!\Wԣ/ljRe ʮ3r=ۤFCOul;z2GJjg9XזryߎZ BzxT Ry5x WA|r۠;EXCj;K5 ܵOM&ȇCO]]uLRϢo ]_.#3|Ձ%9rt^7)8Z37]Xn֯4HNM5Ww9BwK$gc &Q 1KKzQ-OY5Nз%IT}f9s ,֢83hmzjj?.|0kbLK62k*(^/4ZFBg k%;<&$mСpiӅܒΣ=t? -c&q4ڵyϗiRJk!i2`_xECfCtk!՝>1Cl|횽㩦94oIGs>@Su4gg󻞸sM]oqeV4&?8 ~&S7Օ:{BĿMpOO:y&TS*Ze< y2ŚDe6 DC^{&~ @ۿ\$n8sK\$x {= -b%g6DΊ>%^B h֫nth ^Xh=X NL -D2*8'VVȫ]s=zNӬ?0kQRbK5HF嶺i<73 -bi+M`O_`ɂ%yh'[aRnx4~ u0lz/4ȠWwd*[ NFw_NxZ/v^C=ԪnAo{~g!|f=%XT㱛v5tbpj}Wѽ'Xk -BrǝlMf{:$vv7nFb={6ɑG`͝=c,3Vts@NH̦A2?e췪;Vz0zləb%Ҧ.O/+F -v:r͟|bE0Ǥ.G~ \۵\ӕE,)gYhKᰇdJCW[i VZ$wzh2l\ָ)MRVQ_.$>u8]#57p/S1AjA.;$oJ Զ1Mm yZs@O li˻w -5[vFIu=GЂ`O={%>EnMg'T[:XͩOp&yŚ C1ǥzO:WЊ:;c rJyl@Əgb -ϕk{zb0yNYُ+%94;N$6Vv8hQ2ܧ]g: u:?l8>^w` B7/EsrmtE#h\>:jsMjdD1=7{i|؊N2N%_J_H{ AGn,tzHH#+mtx?x?x?x?x?ر CSCmDon>sUR#SlmOwKI]9 'fEVB>?mh$BR"DfM8M1HD -f&DǰbJ.4DJ4|L9B1oIy=U͆0#-yU&:F 9Hc$@ϽrX:lWAG(g~^T)=J*h*U؊u:{!w.V)К2;)&(:]L9Ow@>5chcA`VE)UF~J-ڒ] R%9k(x :G՚xN);(t*Ĕ[r1lX -K _ioA紸_Pxo,R0l 2jHe֬h u }y;!F{FN7tIBTve.ZV767R[7;Yr;k27k;b˨a?R\aiJ0 [k7];+HvL tP*II+4g'R2tA+4UmABW< c"'Q"dlR`NJ}SKl5QTy5r\b9\ >`*F&$^򺪃55Ǧ26 |K9:@"BjH;JaڢxyrӍE` -z(BrZGƂ:N%Ѡ(P%b"3Ȝ7RrzU|?Zň^+A( !ڈ9[YJo/EPRL RX[l"iJGblVo2+Vd?hYJq.e6>Je.T}v@)]p6.12%Oǒbqd*;tR^z:Ny5]Bd -U쟠pjnZJq9Nn"k%>LthG*+167:a3~Z*)9]=357ukS+iRl^1ϭ]{kc@D;:Fj [_ۉ~ɅrQptNt#qS7A+12.59n&muo]WAwd,a!> 2o.7!`W -_ŀĊy&_ϰ2y=I|x2ā^f6t og?BdSh->dyi._-tGюq~e֚ څ&D?^ 3֠c&ؒ I6PI #ٖ#hRJ72*̳@7r:`[r,O2p0yI+E䰌O_{&2'/hCЄkjߤ]s`|.c(XbT"R9hV'g @1@<ۄqZԕ[BG,: -7JX#04':R5Mݶ f cpB>C)9 TLF_:`kvYWw.a.IN*9vܖM0Wrs,ѥG}* 6pIdVP"0|tz|FPWcH\XY` j*eO"1FE1@`m7c@BPu#ƐSIU7ҘjӫS (S͠#>s(} TVd(mI#:wi# Lc %9tGE?SO1)qݱ bDϤݔB,p@)$jᠭ̈/'ׂW~_ŔRQ -:![ImYHm lz%w%6B8}բ~5c.{Km@bWµ\~/H|ՀiICtn7om >/nh: -WD;J9̓N,9K5 -t&~TttkWs:AC(OƓ2k_d>js՗_#0K 5smi%GA7lt IY\2?(1BrB:O)> -RRGI㕶GR1y?:Ca3"Dώ||ڵLrHUv@% ǧMUj6R_If? - ;2*̑RfK/eݣA?]\T24%m#ړSy,.1]PT[IJOA͋pmI݂-Y*a_X!O)H2RXВVxcXm!ف$Pyig뚣Xb 1r偱b͇A hĬ{pP(kwA^ r+R!qy"(Qy-Z28KL 09-Su-4R0HcIs;?~O*$ s -Y.oEIUw9 - 5#~>s eGaQLR3ǙfI㡨zC傓iGd -$J1 1I% 2˜'ZOiUu_F:k]ɒZ1ZB|7!u{J&uJcC aBL&$/f_u}(~Bh)dF$xe-*RL6!$Z!iJaz dz#Œ:] UԸdU 5": -6 vY$y`RR?l0qb$i&l,m#y5T (5Plۤ>-}$qӏ!Yf(3͠4+EKE)X 8 e K@VH]Dj: Í(QO\Al72H렼sP@㣲1ِUtuKNRG| u#Q_ -E9pjFRゾ  y՟o E cq -*B7aÇS>E|lQ:%kOEA &z/m2@ZA4ԇ-+@*U'A[}$Xjsj{uChӉor1U=X뙔P4JS{9|sw~˨cӵy @3c+kn"Ua5g)o`BLnoDžm_/4Uk [{r&[n̗?pۿtVHJqpԥZ%OJi~Lu'&Kg de.؞zU u?ǯ|COѹIC͕\>΁i)"]\ -"ȍK/ -&lФ!_85wg9(_?0N ]SsɸiD`R#LNŴMt -Kק@~>&s6^W1ǟ`Ҵu'IX 㝳FVn]R~(M<@$ԫr㑼7_[VEд&v~,u4u7A -7,L$='s9Fs!^i{˩D٫DfҖG9E d6Cs\ Sl@"La 92!p^ćm@MHz-O~ek\!eGb.GQ0lSy? H ԭvޣ nR`,FSX<8gWa"sMIrӫ`2O0} _1d?M[c^[n_ ^7)jZR̝Wri\8۫Ӌ|׀O$#>]'?GGqݒ>a_e /TErDi9s1S4%qL ftFc^c6LMJ*:(HI*c4@%0P -ܹqƱ+ -MM( -0>~J:`9U̟MVh5NY0U0(Lb5Hj3d ("30W)D|}+s'_gu#ȿN -hyqGSEy_7|!ryg%_(򺱂ly&N "c(BSQk,Diط;85XERrNqh)؇ل1 $l"k^VJ9u -C><|(àHe[<%0.:LӃ\s<Ԏ/$cw߬1ɐdpN q!5PCgS2!0W2?m@ B|fNi1'Ĥ!5qt(I!Җ߿=&+,BFa˹rg.#bv}A%Z&sUq ؆NvWJGMuːǛN 2N__^o -{OˡF\r-㔡LvoQWs2#$kPǮi- )tݾ7r 1ǾK};En?NF0(xZN Z? @ wh2?/5sQ&F2A&q~4N wc<!}w&L5ǡ17L}&f3R#`^.؆ʏ}=2~/ss -gMgȩo B\ jRdl,AMA=8L8T j6_jWFM|EvkcrA5}⓹ -TȨ]tPzP+߅njףUDD6Pt7U\p7P3%uLrS@܊(ALNcnMx&vO5 蕀<p~ \>|Xxr&9OP&ڎEK3OQ ǀ?jĘ-~S.E4e;IM{Am#m]N B2]@'[%-/rmr&a8 -rXb@ɳab*W+?9^MYM0chg ԁ )>ۥD. V0@z;s} N-gPH'URJAe9< { qW5!585l72+́یw9;n/D#J$tQ T\$2s?S?p~(XS>~q46PdR7"(ya? x#zOʡ:ڿSۣ@=r1{!o5,;_q̄|!(q ^ƀc&`ryaR%ngJN.>S[A  x0 t\AOsjz2'shn`'\ 5,N>ZJ|P=}ey"8ld8n/\'s?DR aO7PaO6 MQ]Mf Ȥ~PCYa_~-a8BG -"r; ykǩAP%;85̟BO+"(BSv^*+_ qa$evTS.Ԅq"Kw1E;;׭G8i$qNhϤv둉ꐗ\,Kwhya5%ym2a4x9䏀NCX^-=}mUA&R!2`;y>`2'_rTA[5 1Ǹz(N*Z&ra HP:;~6A(uHsiX$T*˽[N[=Lr.l՟jGK95;`)[DB ƚRRx(ȵ ).0B=n)KWr~&m9%(~BT#H<4ca f(IT8_U*(+3a*:= K&zejvAМD^QJ8Gfkj%P!؏Isݸ[y))(7Y'u wD׈ox]`8vp=^Q1EELBP g*8nĠl~gS *@_^F=_(+Am 0͡x\Ӗ0J5X] -ypȭH4{8;6qo#y)9eI=reo^V=-=^ĒҔ#QjSoly -LŇ)\=0Fl63.JD`nqVI(nI ɟ'J:ba;Q>i+ %q^A A7-c95SiL|:X&ub(ڑݼ*%ujBߏ.C_0=\MبMlՄ@Uj!s$U=p(uUL |;Ŏa:آ~NY/y)q2#z(s>\_5x7 jG[=?8.`WjSB ؠF +>(IκNrGLj/`}s8fہZ>ܔf]ڋ˫CDS"I >:Mz{K7=*9d_hMoBPO 6q| r;`(RImI72!*Y+95]+A>6+Wr]'Q%@Q<:8quyͩʻ`/5Џpw-r+l{R~W9N뙶y*UkhGhd_A03#ldxߛ+9'ԇ8$[G+r%}܌Nifo.n-h*t:i<r- Cل0jf"GE+BV~X}Z#sdƐdcx Cj\T?_>xx -`v3]e^ j\rSA}jC^k2W P0O.MNxP>8#\vQsA rRg|UKz#`f*=P>r= -\C!py芝3oN \}೾Z8F*(JdKКAIU{mK)1D攚rpAvJ05۠dīDXF.oQO<20p (K=ozz\L/J3?\׀rkK1g -.ܤ|W೸ w6 -xm\\O7uA!f.=G>1uh>~hA8l&uP1ҕE[wq/=S$8r~ W.;[!zrofTKuq `l{g59cB y~״E\^jr|aqsΏ:ԚFm u.Ŝ2[A7'F-x:Ԥ -ꍢ~S5c_E.N -l IOJUȫpkrį )`=\82 ҲHoр\(햺qMV>+1e;D6ryi|6_6s}nZ1nmxEX#v뗅y!8Th*[W[Ctw -iq땄"ԮἋ[!;Se "|ru 9h:J.s#zcu (G꩷8=rKӊ3&Nُ%[qP(aӁymp@y z7Sz})L@ơ{:b*:J^ Tc9>}+5􉚺/x)PX|=g5K@ K╳ )P0+0`%pqSO`/L`fP}]ʠj$.FqJ!(sFx 8<C%Uױܳ*NI@Г >/8rKq^I0uLB.n圹n&n~ҡn n~f0"_3%>r'}"~2m@%TN'őE22$cwC~Fr3eqջA3 }:Oou@sIͪ&6%"J4d=O2]| ׬ -v&񼳊˥rY+GR*z* -aԴJkg4Di HxDLNk2] zG=zf>D?޽֝Dw-yLaIVZt_wY5̽&^Z qv&K66 UNF44zt@ z 谢"^90o'@>x7߬jiV!*Pp^/qk1u\M5|!" +.|_dIun]AW,i>rpYdU)|CH&% -3Y -퀠Z\uF|*z2}'K]$51_"yYBj YJ _])ӡc߳ xkU6֑6-u\bIg15m(jǹR(xB#}G7a&E>'2y/ ysDy@|1$0Z$N> -O?SL¿/D$W^h)iVlHkc@, -GdG([$k1YlĵiT40'j_h%{Q~F|T+iK3ZIzJԬ'N6bҺ'A͊~9%/VD~_+;Α/$tN |+&|"* ]rAa>X2'lfkXKEÆ~{Kj> x49_q)Wo]C!_|{I]E?^#]M_3M< #zUvue-c"k" ]Jl%/kNH6&VIF8$bL":_UM1Q͂(W2X+Sws k͐nN -( -.qN9𛓤f咞_i{7Uq 3AK!a}DҀ4YAI1A~%o2k)6_i>).nYZTi--7ʿ*~|AmI^9U%$cIi~YIAYLMRv -.Gl&kB{Ⱥd}U뢾Fo悒?̈_,_R>^ 6xJ?UZv=|j*>^u[<"*z2VQ'~`i,,4T4ʜ~&a~ϻU8$iQv "!hO˼jTT\V;ɚ*w{f·Q^E=%-+u,\d6`on;1g.F] -;y5#t /qrwCH~&vL[@&|Pn 1z v ^,0NB 1k`і'7-><`pQU軹CV]bHC=SDQ -b %uNݍИ|Qi}'5n3)h"fuy^?37w[gFm"b޻yx j:$T`H~NdFSag¥nuem!7|F7 XsX2㻶0ktEYyӎD !U/Ѳ*/ˎQGڟFVZ\$t vV'LO~7>&w' šoG:'ؓOեWLTo6NaQoɖr -(15b-FHTI>>~scB0'GA“d8?-| ( ؕb7[Ӵ:4MKS!o|Ԗj=iZQ"-p=1OR/\˗_O<$G><9"ZQ+Pa:Xr16ֽ7έ769ң7QR|k;oNpzxpKxZ<>vC^or0^>.GyQ% 'H /I7e7S̭fF/ k|v93\sjq'K;% -k~%*~56exI][S?kx8`wns.R=.^ J9i'xxל5& -0+wx9=`0ioGw n v _e'/*h -|hX?YZ>]bsXrsX|KGOS?`EĒ]sf%Ůf E!ⷍgDJ$I(yvHZxQRc/ҚZw -Djyyk\z ~-iXr1>D8" C$_vg4:EFԺƞiI`:꜏vdE6Ƹ{GF:oWÕUiӫ+baY%Rc홡->&YuUQ~III} - yUla^^偑NUqn>ޱTgVodɯ3:#<-̮.W\\`coYCP,퓍|3}2ϴIgmDe]k`zUir>$)~͕Y/=Qg/?9? -]Vz24l}|crYOEs1@W%zAE/~9dTz)=tsFYz_y.~՚v;q7̢ܤ=nOzOOΗZ#^7z)01\5cz Tᵍ¯J[8|+ ~oF ?ND#&fYS֠u[U38n G5oZ54K(1EAn~=O6:dsGoKUpc7761@g5^H - xMӱMşj0nuan,{3Y\Rjy]e4a:zHϵɛЪcwGhf -YC-U&^tCbhMK:EN1M.Mcj_u -9,#LnTqg ۷߽#hn;S˅h%Y;lO;3kyεawss߸Dw)wI(-v{QSP./ ˥/wyx`RVMu yCE5+˞gcO)fѤa3>M>i. дKѤKDhʘ5hLJ wEĄPpRӁ\M)rcBa_طɛ.ƜӪ9$;{H+ò]zR4Kqeo_`MJY79wӶ#̩[ QqT|F4sp $4Ca=v=e=;k/ZVo?mq4w]DY4kPDsp?[Gj'h~`C)OyZ-kLqK oJ"*"kl#jbkJb` ^$M=<8{C?:9 -)M]}J4sZ|vSw#ii{[a-EOzh;e+*FÇnx}YQ.!\{}R[CB[KxiԲ{_ZϦ*FS1Cj>s-@N״Qьk6U}c4Bs6*g}h7ZDdhLaw3TKٍToKn^!1k{! qam]mvy竣C4߲fOo_ =DJHiB9O{!1+?{7n=[ܙ{тEZ8Z --rFK5Uk4_iVCԞ+yQWy!A]z_=9/) 8gˮpyHWWKg^op)c߿) _l38ۜߧ+[f[[}#&B3cX)5G,'В=hQZZUQ;n]vFqIiKI}CtWvk+s ~Vb:Smtm|Y]l\kɏkV=_d 5xgstşҤ^)6`;i>]#ZHiZ9_xր ;oU+M6lqݳ;8:j16\eYc[gJ\wcBG=-,ok?x-}_RG u:W L|Y7{7;g/-)hQB&Gr6Z`ܩk1o37g;Ɣ

9٘Zdܛ둧nĘ:Ն$UXFFFrH8?1ϣ:.e4}Z4{>Zٗs]]xR |wfN5eSU>E=]U7Ğ3Ğ})0{1Z|89`'] ^)-.t1CN4g,d4j.7eZZ>VheZc3|ۑޏvOգlV[=o7Z*oN kղUogjd˵kYmzVhƊ X`|:"0 +cgʠet !!$"ynw稊. [|` -ܞ_,54s&RZa;h qo*n󫙸;_5O%u_%.)*G׋Y'oT:qg4?a7a| k1xR͊t|J60(ZAXT}^F俩30?nT;}n*L݇gǘV7ת؊R -pU Tֆ>gx-As窠M&hvvh׉;MQ-dWi`OϜ_+lMg?~gi}gyڟYwVj=Hr3bt| m7Ҡ2nxskBu;ou^Ƥb }:FIf=I?'}ڪu!vܒ8߽M6lkh#{Oܙ*aۮ% ~e`}g%؝efnVwum<1$ET"|9=l{Zmڏ-:e5wg\|c|fU32=y;D%1IjTb۔ySASD9E9dgmBm5 KK.?lS,&@K8~m̝ٓƿg>/e 20x׳֤=+cm[|^bA'G֊7C3[SYz<"J6/*q;y}i97FshW>2_^)KoE$yhhąGoE Whۉ;÷]+Ҩdwyqs2qC%׬Tn{íh`?=*1*`y YS%8_] oе1e6Rٱ9?aٖD9ag^+KN|Ǭ5kdر82?t4Dx7]R49E}!"d'9I0)a>ݰH񒬠 apq&VQS ΅mx-ъ\>}؟v9MVqmf|m S 랣]Q |BVe>?ōDvx.<%mt6;29&n>ݹA;?thG[Ͽ5Fm"2}aDVC,='W2&tiz%+,6+-r=%n2[Dz~"d"6}.wo3'D?@kϠg]R_t"vk`t4ȄoLq D%{Gk h9h璹h7\+ }Eo${O3 c( 0KVߡ)JҢr7Q]M ]uA\tHm?zW=`U{Ԫ`ULq8YqỏdP?9JIOd|]K @/cuP CV8kqɍ{M.EMٽ[ W6͝:c-mg:3EzvTRQa'x _p=_5> i{5Zcժh9r hVV W;_OYzgбFΩ3Mn,4Iorw /v FN_J6qy1bc795:=AGCiދz8^Dj +P.Qs1[ȈǛ1oi6|.*G/7ϪV8[1-)vU_Ԅ*+!ysMu=w^ 54c26CWl!uǂɚM~&|t!uai[Ct#^^t ztKpx)hnCFHDfǹd,6 -%  k<"t+ᗳZN_\ܬg^%eKw2Wg3ϝdR>*f7l04TȚʼMn]]0W]x6H6{ 3?XkENvn6]rd5-920a m#*D=χOٯ{F - -=m_]SGf ŃZwvd,2pԛ9|"6?kj|;s1u}[ߴmQnH)lCfvQ -~\V~ЛnV>([W1ngwjFzAP50xbMY?S6<8⌢ɁӊzhǪh߶mHgȃF M}_vs |oIϔ<ϤeR{o%x*|0v߲HUfSNaJsO(yA[$ J?\"yrPZY}wl{AB"BTϋ,bH[3c/eO:1^! |J)})T3kD;TѾf(LnRuk,!x&OxN5],uMT”&U8sͥ k_*xD`0OgRܳ.61J<3q"%f0O虵8nq _UMJ7RTn|al3ԤowSgæ|A&DL^ -b,:4LMeڶtڳAsGu ϲ,=%i0~?֊fJtQ@eRNs}\J4ѳk okfšģ|o􏪂_Qz((}^  ymG1Lgq.3zj1tQgtQg.M6fϖI*-lG.7&)To=Rx5Yީƈ133`n1}pEEI:db0j/Ң3 dwn j2>vK^z0Ǣݩg繈hk/Wԓ ugm"ېZNN'#gf{[ zg6MWvYA85L_b#/*&6sDuU#h44SB#>C #9]&AⵂD%=c@MWfa1KF蒒z͘DN_̿>8C|YEx~+}Vw̴}=+51k}D[pQZVOX?CZ4)8f3pمj4^?؈3 -+D/\:ӌ:j=8=\t `, :MyTArN*KR}: wEϜV=6{YzJ׆g(y6Jj2jwo% +7Êכ؆N;1],89EC-n S,'2zHAjQyM2:"nWNκ߽ԵDxVN U{K0[O(w'\ -ZR!̹?4<0KsHKQ<(ȟ`[rŗ_:xn1O{f-'N8!ιq<`LZ>#Wqe}ԭ}ѭ۾eD=s„kjpY^){!oOZOCy_.LP'RL2R~RN[U"j7mK֣]6#]#_6LgD!-3FVSa xFf-¬?P 87*Lb~2YfvgQKMF;b]3}19fbn -9uRQv#?j_02zCsl;u nYAߴV}K'n3I)cY=a.'L2rmhOЮ0^r -i;-qD^]9t0/!8=L?}y,Hq+QqMۃDG-2Z&J̋/Lɳᛐ\AD duGBAj^"Lj6&Rh/y|9EuDof=89tD?q3f#`$8Ҟ+Z/qO9IFe:P 6ŦIrXFG#idi4-cq1w !JrJ!ޔ۾~㾵֩3#Ygʌ8֜}Z{U^{-:֞O: cG6rEӟtyw+Jų ۶ț56t'[x5Nóx~-nM_3wwUج=0XâIQkHqZvT9tm_uSfͼe}B_kؐ۲]+O_}<栤/m&vM?u6-{آ89nظ{]L1f`}vc'7x5ũO0pK*_F3?,?9tߑ+Bkw5 <:^-{,]gƆ)/oL {o/oX9 dP6熚2㉹|>kJLԲ1f*KG^CYpS#8{C < -;}19+1F`ɡdhSL\^ZKrŮo+O=lT^YENXXc*'9iim=6ۄ2g7}^1NA1 -<Ű#_.dž_6ǰTwFÓWk^csu lyP?1?Ǝ`tspmПnz0&Ptk;alI1cR,m=C_qFR{?y[~ 3|^;M|Ow>" ~;ػHwn, Ƽ7:'vN#~%AsvBbbn&1Z |k"o}˟FѸvB1xijh~z -<.g07H >L3,gמ^ŨY\ywo[m28F1s 'LĘ w,;aɚp qºֲ7--Ro]:)rӗ`q\w*-%'-.C5>cɄ(~sWP,U-Dv-J~RNgenOd[?e"|y:?cO/r$!<l)m?'UW֕]R6lDr `>ĺk[oZNSmrk΢iK=cEa19pQn7~7M-ηK1僯{}{n| -a:#gBKws[,1uЦ@9vx﹯c{<׹xSCm=;&W>aѲI  ladʱ Xڣc r?#rזs0.#yP1wdKWc:aZg/XwbF->bC P.X̧<]7H紆ۓoOiy12ʆ?TB˞\x[xc01!ԾD!ջ,rF]su0m1l?x1QE-wNE`Y|̉:Vdu%AGж`V /8Eao<{%7a3<ÏR̻U9;1w_OZ~Mr׻<ц?PG~EKL9=1k8; .{'EuהW+eoZPV߶|y`VR4$߮kOb@kf/hbgR^y m -<F:]K/_6o{;l3j0,pV0w^}ESz@hkNܜDxI*a|aLZ_,زH7 u\ZBG^AqAtMxU9= -˅9B .gjdb|C-b1F`3B~[ k`F_,GwD_]( -aMZbT\`ǦSz'aG(&1u ;N\Bx=Nw7e;XG|3>QNW| #0V39rwql+S)sȡĞ.9GḌ?3Gy|Jh*r7tzd2i` ߾/}G/yBlۼYs`>ʂK6cɷE;'N&ͫ S]&_%RJ2.:.l_2tY'cRe[ΎݽskF0gT_e -c1o}A~x 9zt@?V7ܹ eR[=8NQ'o|˛bDWZ~?;b? ?{߭<8I,оo¸_K֓)04yal0@_{Sc\8ͧ7<盞|s]Ƶ;.| x*j2d7={ȺiOs3BG1?|o pI1tpKIw)>\% Xm+N 'T07B c~3%][W* $:&aɔwٔ9"vÚ?S"kȇ3N{kSk[|x2dMsv&I0w"ƥwNj^EMky>~1`,(k>1x[ wN}'3û߿bQ_A o9p}W ?v䓺nZr崁~Bÿ -i]\Xzo9e=sw(Tq!|37pYC4uq& keOTC"+]Haog͋9p]6 |O`/BOF}Y$뇹f}SOo8&B+{qyƸMwmfg{\s=&Fvv`/UFƺ ox -{[Ӣ2?rugkn ozm -o>zŷ{ڦ7}?>F3]m`b 19HbwEknQc̭ 4m|pgE]OnΟ b!|x=9z,[kjȝzT pMķqK&@mZe-yq)ފMgE{9O}tkt7S.;7޴ _̱dʃ jbq>:-ʧuw?L64Wykšsmg?Y?m lhc=Ywt+)"7qá+Qco\zu${0It E׽xը+`"#z!-_s"{M{?Pm'&OWrpdD21.'`^^ӟϏmyNjqq ?9) }k>Bq1ߑ0(/._36W1TO m}cnn|pi>X\Tv|须ŜsOgŸ0r=7ֲևA}ok$7mga\U'㼋e֝rcb\nʝGsG?|#1;kZe©BK&b<N'Se ai4|+#s@,t&<3M?]9V(qצ)o]ټt7n\s:TJ#wtsW~9_,wWḙʴ= sGҫNrg=rq2̝TVWLyp u,?Eϝu΢xFĪIt/\yFq۵rCqɘDzqݣc,U _{OGyxV~Vcjߡ4jU>5G n:w>`ĝq_qFߋ/(0f/;|iâ'Dw}l7keBm Gznd Pan@]'O89l8r/xVl7.ۡk9}> -yk7̾rg± A3weF2vaS _'|jL8qʓ}'G'FW<sqS~5.{b䟡QS m˘n==}Պ\C964GsJ1wxgNoQ%ꤸM=%Zs*Pn .𷦏\N 3IFT9-bBy}ϑ&c2̝6rgrg5{x/MKFm 8w1g٨!m\%Go#ҺDʽ囗=t.xh6cnS>5ڸ'͛_&j=S.^*DbX̍GAv@p^]>s%b\=HYm$1q/ZVn#g/0'vxFhOY%#j֗׸s6<}AGrT\;\_1ѧmVK~ǺN[rgQ'ʝ;kn̝u|g/Xݔ)wV#OYYYi#w!-c΋>}1`.ea~s+Py;"wMT3P,kDsܕ1ec,{"zcć^ ?/:m5:zO -[-MD|fa21rɸ700﴿ 8?[` -=NCy eoˀ3wr=wVM;eotXf󙾆l;ƽƕ{79[=(Ϡ| 0/ Ų||y+Qgw%OkaC~~ƒGؖo_yIc~0e oc)`]~Q -rg)w'7Fѣ_܆>hP.uWXh$rIF,\_œ_Sb_$ ?ҵG~ 4ny:]Z}uu<?(o̦<,ʎ(&xk<|>(oI0}xоOo5s\v}Xfd᜺r7ݳr -ܿHЦВ'B mQv1Wl<N ֻNh|:ڷ+>s O?ڵC.8ȸW燻7ߘ>2}~@.rqKHF\< vz2\]o3qcQ~5{.FӼiضoO9pp}S)l-PSw#`7(' -]wq| u}<"`x̋y~B;ߚNcsˬ"YtU_ߌ3|_oءOǏ6yj`zK#==}`Sp_*K;ς |y3 -=4<5/XAZs4ʝBp=N/κW˝ybhO -2qI9[P>=!|ƃQGv#xWtV0ߖS>n< u=浘|`ַ+RWf#P/zXǽz-wVh׏x}Qd[, {?R~oh_qB{\iz.N`_6rgmw6 -zc=vWhقzWwQcƘ#YhjiXއ6;s4l}@?`{쁷7ٷ+oڛT=TmF^[bKhߡ)^W"ygDWxGУ^^/zR̳kx,GYQC\qkb^yMk%ͣ#W5> -׸ބ׮#>7ܽ<\yr{Rto_O?-l|賿 p&5;&O}_M91-|n{u"{B́z{|?TwL]o Lzkϣ.p)w?vA{='=Ǣ#}0:hD_A9U@۴}XA7$#`އ<59-<aO:ۍg羸sp\z]ʯWV @zΊDK~$p -?DJ{qh$pSgYˉ0 -{c;_p}~y"zE畱?aܫ\_KM'ֈ>@[^z-7{OwG74x,فc昖xυ;5y%G3ҝ"\os -u Z0șG3V[ iﰡ}~]郭wPc1Ȳ x{~&c_s7{/9xx}E V1C}'G{f -C{htG= [0fÝ+N9f.A_HSb(w-DW+lG?q.ȿQ[&# &|S+v9)0+C9c`[\k9xwYk&?Ə0)8sbqXtn=3OP/16uHaP?x.mt0+}['pn?8UkLu-7N1Kό:*;tog*)F4uL*3:ƣ1}>nnδ/̴,B T`*39E5_Zf"?zxObNwDgSj1v xjEgg rOw7\;@4=F6IhpcVA"d lŮೣ>ʒNqIw%tc/ݚZluYNtGW')`D;Htڄ힙 X| (&k/ X&cRz&uҴۻ@tǵ]ŵ]еƝ XF%dV%&RqXLƭZ1S6QOmVbi2 *޳uZRHU{{zΪAq,FksL2us6 RL-ޓ >3Jw7{*v[7SqUȝB_ښNg,[<-jW -4Qǰ+Z;1{ Лw^ޕLt:-cY(ΞL|HZKRs >N1UV)J[l!2v"udgYL[ӎ>CC{ vJǚqū-qіCs, 8dGD(jD;;"ls◙dfqG"|cd܏I.tE([Jrx,Ht/J %KO%*t.vGãQI8UөDbm:?"_.`V[d*|Lmu-1"%u;ޖuNENww-Nҋ1[/ws~Vȹ۸i#i.OCu8vڤX'9R[L/U#rp,@"8F1Z[ &"%se$S)Gm& -;U2VIOwhiq7.+>)EJ;(Ջ㝝/JȖ_pԑw5T$ 8J[̂xW$cɻd.v+]97:G}vġ*[%sq\Bq&^o8DZTq>|8?3[^2wE w)Uc;ܧA:f?ڳ"|Y{z=R}qwdJg"pCk<պ AVxwr2iGz Lw:Јvgdg"[w{赵%3ɥ gBۓT!>ש1LɎQ)wxqiufs%$یc73+WǙq8S-v:|rLgZǍ8bb8:ΰLjqq\Bu)F:ng -E>UƵWj|`JR,`xBQ=J/Fcۆq2r^, -\ڟ#%t 4._;_q#&o%Umv7[IrnrZys[)q7Wwscf6j.qa19˾gII$'cIN"u#:vmЮ/Vb5%'Rux_saGi,mt8DK> 'n70 :;c< =~7.YRXlK';ͱl]xfVVbsٔd DGz~-*RH:"?%0I_ck~`4vn`4 -bEZ]Zk{ K/8^`[2?=9O[2,AvE }gG--%g;Yܑ8JIEꔒ$ASǐ=8vsυ,%4wv&Y;3 Q*{7K9<{sv-s-ξg)ݦd{{oO: wWnvLº/Ш#חH˜bJ.Z+Z1xcs: /yP"ԍpUl"tD]ѱJRndq٪nuZ cۆqժr^oq-;z},sPڇms!AXE>W{݉uqZke# sDZP,3:/WF+0SeɶGwD!>Y㣎ej!=ʨV\= L[,}[ -QOi/(e9"G Xȥw.֙xNOY];؝i_&idn Tgʌc7ގL|iًqxObNwDgs "Ԩ#gh"hij-lҡ*0LuO `~&ޝpãx -&5^/DqLڹN*n4%7Ҁȹє{hJ#қd80%pDp:]=ոB+B$F %9Tí$nJtU -Cxػ;>stream -TU喻Snn;2h#iæ՗W0^ަ* z:+X˪{Y<U^"D,{*T*.`,FԔcQYT(x0 @/x/HP=+{y K*i6+ -'qJVD p) 멀j*^xlI -k%hUYI)W0:YU"C0%Ydh-v1/Z h&{Ut40hO!~*̪pmOAXVÊQeC -r}LD@?V!2+(B֒,,9emMig\S f]SļPה~,Q\&|66yH 2pf -;lH49c_Qe9 zJe_Yx_ne$mx9c6Ҭyױ8C@gDQK8.ׇ<$ 1G/,J؍l=< 5p@aXu;{$l Bo%` 0T />5$zh69m p`׿c?LqBeu+#CyLA p86nUËmhJ54DmhЙu 9AC9ă=0dVqUTT\xRFp`[f*c  V]Lpm.<:}CTZ>UÊ:&8XMpv :O\u1c) ->4&8XMp)_E"8 I]T6E(F@p E!Ʊ N&-(dK<ʪxU9 S5HNeC"#`".fPqя mi]\ ۠DaINC҂#y4^-Ia?1[765T6 -QnSIADGnH9i_c/)rЀօmQtqel3xB?TNhHRA$ FNY=1GKs]c~91nj;U!z*hDN7WdkoLM9Aup xUcal -s,#^ -Ί飡yMtM0CJ7jP?-ztƢk|q?V-ngdc־]o0eǻU9reN#{kSx -JW .UÄC0`[(8cenxfkxX鮶c:Z[Y ΘY Dt0QGƄRp@nL'.fE ӵdѪۂHV]j@fUTMNKT9b`oԚK2wOEEKy$MaWUUvԧ3fA(l/陾0OۀicKAI<9*# ysu6О0bӠAۖnI4WSL_*l6t6\8%S̲Fb;mF xgT+&" M ˵Ba8T$Yڨq8VX;-˕c@ $J ,%s(8ƙ[1F駫*kۖn9!Z2QmF.sW( - -I"f{Cw0lm+0mxU_j.'\p"065mk]~2?fmM0 G.(cb2\cvn~a8[qAMdʐ<)me\$NN Y!Zs`٬mS<\8'usvbtYbt 2:N>Z ɪWoX(V5g1%_/F5A);G]%)<#s:.Ж+" -s 9˲Xr$lapV*X0h?s~ cgN%B~#G^"k~ -!9TDZM~ cؽey[B]NF>tTcYԘY  v0@( J^CbZ>Ä~gxeE`x=:#ʁ*P +'Jt^-̳LRb%S¼;F䄀8ɲIsY^EIJ4b`xJv#fbFYW -)xRADgNez'`#YTAY@S@,kkmEEx0H ?b=Qz`B{Е5si;hYN%g%{#On`Nif|bqC>Ca䚡m dK,MR>Oh!hG),U=xG7 Ab>֝9S- "\wJWh{Y1c>jd S@vvf2RzXAjӒ4#6uе֝QjK :*lΩJcfrԡH@\E*'NwR*TK!(O¸WyZ hS۩<]t+-Pe,kJ3\UZͣw?FE 5|tMiCӄh s ˈ{ Py΂LeEr -V,-gbc,|:򨄤J٤=j`0pWTlȤHf"$ˣLY""$;d/˼b2]J^MѪ)fLAYj`f,8 KZN<ڐe 6AZ[,Jx`qY7v&Mf6N1 dRBz wۦBd&f1O^۽N&fhI6YP̊ d&Ȣ fTlbo>/&'51 ͉ỉCvfь3)afh3cT,2+Z`/bsZ2p0 E>v+.vu̢12kCӱheC ɀQqoɄdAN_Y42n@Ll:m:ra׿,drcHW,*1T(J" 1Id:FK- -(X &z{B԰+\ 3Ne, - -E"C'2:Y~oȄ$AemN_㭶̢vQu(9q0lJX4bHQ|As_е_uXߪ2Xq~܏g ǡ~f+zyjz Omw1-߇7ei' }5gKDaSD+--`cKmhes?i)aƎ\o$| -m!nPɼ+wC@vh[+Pa{ndoRy,6K;~Ɂ`:M* uCmt(1Qo`JI tMI7j(֐jdfMv fy۰AT ߲jltsCby[:@:M޶ Z l-dowRO6Ⓑ·1ml!(5D7XP6aED7ֱdZ XMlp![H6Z:F`kUȦl%Cc6,`ɦfv(M66Cl,eéj9u;(E·,u6Cؒ)YdʶT?rf3!lX,eʦfQv(W66 az )[MuH[[6}N!moY*oqipN7,N7, N7,N73jd[dj'ddJj'd[ejce\Aӱ4K94K 5K+SM| LɁrӾ%ݑ8Kd"I ^jJ 9`Y̐zr8\ e//iA"FXM9^xg"\J$쵃 Rv3j ŮBҞ7(Y:o%04e[CaU{CvU$ONXx:02Nu7A.eDQB_0byé^*dЖNqHlAjLnΆ .K`~"@*6/I6粄QEmEdm7uhiRێܦ <|*k[;Nr^U; `(;Q'-'hɸ1o D-5m#;lU]noQE3x:aCjL'k;5ŴӿcBݨnۺcQgM[o3k+:X Κ/͹Wd"u^!cA}_{ -" -M, -'[]F7^@xȽXsjZ=L{pGPpMY -_;o>_>#en1 -0Gi^=$).<%"y\(I*KNAu d=>f%xQk A;WD`cZͶ5+Ũu5y<3iTA3NEsgt*:a^f'(ZNLVX p}5FB€p^TH8aL -2-@ 2NQ/8Z H B;bqK -*I[ASEܚWQ x@VD{P0'\4`ڀC?>>i-BcU F5ګ{myTCC0߮pyLN -F`i$$4x}{M*i}f00[^ZnYX9YèZ Ψr<1|FS^Lu<3,jHșL/1kB&ƾ\rmYQY[I =#-^LK)rvYZx`*PXɏY5C0$ VPm,soVy5鬿Bg'F</ff,Jh.x^~PW&#cs<``wzF#s)(,6Ġ3fw -[ƽ$dn#ĵh -qkm6 - nwp@Ud"Y2. 2,b\b!R[sXWi+~`G_iy:) gB]v1mV!-:S{ԨHZU-LOs@*bU4Urә;[cg~KjFt5Bcߦxsf m!:B8jq<Ѯ!9 @rYUǼYNwղ8. +JԶVɑKEW!G x#W5i>`˱灑ㅖ5;OieN8m>&h ԬY?y4A@]ʢ<ys\53Y[d>l] -ڿ0[5ZUIF96xՏOCCHAe%UBS0'YPǪxT + .a%Eai` ~jf7·X%ڵw0JuԊwJ^p8 %F -}fJ55vCby;onOks=0YND:'Ak]t1v8+inʬ'TٿXg# jZuSTw7u/z% -*e2: d7]~Xtu%<*0OaF֍axFm^$W4hFEErC.E~Kږ;r-ggA3(nCyT\dd6{2^O}mz<S 1Kd?,z -(0' |CuqTEb4 NYEǻ[SlM)"@@2#^ދӌ^+z4M@Gsў90  Lt_/)>3n@OLěy/ tz%K#ӫ]_s)(?Cӣ=A5kmuO${ |xj$[ŻKsC[/5ڕ @=OAVc:}Dk/~yL؋?~6&tvYޓL^`^*Kcc&0őNYzj df( {jmo,AĬdOW*ާNAn 5Sk#fTjsfװޱ(īJ6 `DN'bţr+Z\W h8+NH&y$%SR=DwpIJMIp̾`,:-gao]I闱(ŭ򺳱g#D /32/q"uxNdJ,:;A#]PV5%GZLD=;9SQN\u% -FLLuJJ(~qL*՛,L3ĸȊ,'rxJ2'<+`MV׏gluH9, |=%1ϓlk,'2"fP;)YxPv䘮H eו&!X ֖PSA5Ɏ'zJA&p*.ELdI6܍y;x]l@à|"1*z#qzCT<栣QH>L3\Z~dPyOYQ00yeEa)ωfplM_xLj w^yXI Β,q"O Gxm N^I|&gHd3MM3{xn g̵U]]]QY#IhWV0i7dН!:bOSA:(n! UU  KrDr+@8m`$Hja _O $KRV_g?5ݮ&W),K7hIl|;87&Qz~UW\zÏZtWP&>Z4ׂNj4O3n/jdNЙ7# g)(,CBX$tbz.%<\ZB&[ -{)-ޮ7ioU)\aoKp`ED3KcܮMoxs_?𻙻b+a75a+tKs-OGuxDwpbo3@&Gvtmw+j\dN)5$ϨAgӠH_-t/!MD#j@H*H|l E>'ڡ8(aH2;@1?Yz@'fvx=jZ)_lj38g`j/kؿDkc,N;d<6@r S Y >Y-F1Y_'J0~S\ڞ#o/M`PmG'8Ym}40!ht7/8t~7z>lGVh$Jl(?dsBn;~MCA޼1H)nH6~@K#G)A&}Lnϔ?*)e+գJo2Z{\`Y$flxKxIdkbcSx|M2>D0{/djDR]1Am. -$>qO{QZ- G $ 9ހ$Z?  :1*\P"p'SVIobE-rQ*Xy'2e*Ea -0B/Tx}~pP>剡"GHHQE } pV0.za% N!|=b(g.'h>@т>@!D~tm!+~%̓BR Ä (]r_hIP"`c^ ; -Q,H~PBXԠ4"RP^@lU/':CGY& –&٪%sAm8F fs;ESh<Zl%:I oFD"b!C@;oF`KzX0Gz lȢp 0w?&3~?~:lC˒`XypYJˏFJQUK>V⠱SQ>T@#&*>$Rf]籑^4@]A IDgVBܱk @sAw0u5"PHa%(.E |>vjILSGQ5 `!u "$89v((_u h/Ddc@瓬nQfo,<`;!2 E剬#̇YP , z5I@gM$aI$5$]){a Pҗ9I`rXyQ&~v~%t9 x>DXbu -"Pa]hJ'G'`!nF p2xdG`s kL֥BD|$^ - !Ų>^䌁KPddRay bfȣM4P`ASH惩Ьc.V!-0C/'{8N˺D#x`"+F恁0 k0)-A"Q 9RJR -nǷ/XieNz}X3'Ë5Ff8h:ou!itGz -!}.6 -.k_VB Xh%NX1ȋ5ʢ]ހL"H\l_+S -k -bO/%&,, -''] >0hO.X^A\{)I첸ע~Jh}D=.Hc CN;kЕ0`w/:HcФt{H"rJHy0TVؚ PG*U ҋ - ݠcGOXHLGHKZ Cl|[lܞ, /&A`]c4whe ڏ bn&xy!V@[990E~q)hPO`p{qi!G -p8 tX.^bZJ6\ud*À!P^Bν0x,QlXA]PJ!OqLF+-r޲%^ K*7KGcꫨv0 U_q=)zX.!\%l3.rpk~mQg[+t׈%)kUZsޟ\_Ld lO6%I!4$"G]gm3`̐<$!X/~ ĬZ@'FGaXI\d-?D3w x2#X:ebpxcY0N -g͂, v[4Z  e&1' B$5$9iOlet0 ?ߔG<*0} 9 0ؘi J|*gسNăUÉZQR0 (г`Pb_MC ЀwX~ N<|_`B[ŀ〢ulBZ| /~Kp1>_84֤ž sT`|->$ϕAe*"Z"|;@=kYAP/hM6A -QSdr/F_ p%Y!GoSz{ۮ2 V >""(e)rCiD^K((Fe9f.\Jq.#6ș h;0QvȸgB&@ha{eZCx[|0<` B -h9D^'! esHځoEȥ89R2 $ *Aps0%O{-k.+v{02oڟĩDG|w$ ʨ`t׫fm]^ekmyBz=XFl*׮eg0;[{8 4ƨ*"v4oS-&1]o6#hmzx6Ϳig_]|~߄7&{*p$0|krǮ~=-؟.~^+phŶ9f?eVjGo L7vٶL;T60 Rg^4fGC-9tVC@06`h>>~_RTZ_uVzK59 ~{%ٿCJpC:Vv? t76n1@M CII1 Xn Q+35{EqPu<wꀎ-LZ 7IOM lO-#)^n58hng?j;F~a;%Qϯnv|vٓ(|gbc[,NHOSQ6#\eΆu@yCJ]DAڏuPhZ?)9`+͂q:S5Jčxfӳݾct*!o` ֺxNj78.`G\͸cd?1B[D~S?dgJ۞9CA),W   -XDSeHd`Kd75Z,Rr }_h(y4*ϋQ2oM2b4ȵzvs;. A6D ~X@X|n;a _b{-yytyc<"QFLq4t}h,Xvwjୋ -h~~a.R[ NHv@jbۉD^aF;| S?xB!O)vQMzoZ$(H}`_ sIo`49_ -Mg5PgtG7p5 o7 x7[3o{G]1oN!v"Q uKfM'*5@ÒM,|5Ih^pKbw1.1?!/b0Uk#ҖX=|}|[p6ŎҟLTf-YQyPmٛvtV51}M3hXۮQ -Fi$fbAS%(%!9;ux /X3` -gՑ]w :1u?LMyd_6EP.*}DAC:?b6QPr?Ba -L|vaj8Ww.V_C.:(h<>n&L%)jZytZLL -mJ)a3iRmъOD,5p&\[pE.!Xwg>=WnBor^,Nz?Fr@v~a5^ߞ7WU}*mi=zSt{Q\/C*Aq{Z xҌfsCGm8)M.Uex3AViB6r݋X[zr{#ۖͲ(2fx6S)@@D3F$cZv8:5r -o{(ˏh7x#JI`U eȬxg q9ֽoNIWJ"%@dc|0QCz.cÉA}o 6d ) ĭ|\?.,Nqʩ81OyVa V q=]Ah7t)ྙ, -% )I]jw6 O/pyѬ*pԴ߻ %5A(8h -?=Bt!X+&uX̛TpgFqPumЙ}zQ#}=g{IvMu>H4mGՕ}Yч Դ_X_gS_3fMB |g&=Z3e3I;eKzlɛҥI1a;ݔJ*m| -_qlB7]g*s%=eqM㘛<ԸΎw# ?aR~ώm!4;wxRe0$w뽛%;D}xgNf<|u<1+Wgҹ*7yX'l$[tiJ͢#S)Ȕxg^,(* g|M|ӣygS;||aS vjO8R0t$]taXܘ›\M}vP Z"@.nHfq.An]xO\n}zzJL<\y/Ftu:0ӥ>mJ+znl7vvx^bo!k]klzB3淐L댔31tѪx"r:Eg4:{)9Im\OcClH|yP{}QcY oЫѤ4|RaUOM ;sbbWF351:>~|}2J1&-Ƽ72VmEmw(g|j^bWJScw0n9I>DoL&}8X^M}oe )roĸaMJ)j35wL4Ѧi3,ͺaġfy1}/,a1stdKZj;K{fXi2;˲\x;XTguOV:HޚɆaaqS.m'Kz%9<)GƔ1]w;[=ڞc+f&@}-~)尻/;}s{k랽uE>6ysP#bwviw&g%vu䴾N*[wF\gf9yyoƹ.9wL1 WUZ׮xmW\sw}:L #RqnԲݭrEqވU=I]O5z]0U 5!9qef Zt?x -|c"{6lS#*㚡>Z|Ef ZTlYPTC=9 -L^*,^[>ۇ7x֘d4ƽIoWϳO9|c;nFηw~ܾیޟKLGO&igh=yGȏ RI_9JϠԨT &ͭM!3?FFX3duB^2.ꌦЬMߘbrp:mمrx.ZюV?O}"^mGuIEc-zߛ-c))1gE}^?W`c|rw]uSv =՜'`xjŇ&,v&=N|ZzӤ%2LdiCldΐlS}oT9jԸkOaŪ۝~kOGcL2`e_u̾d-]6c_r-\vProA%mDʧkyiŤlbYx| -ܶh;P(6ӷ5՗])mSSSaIgFZ,4gT3;>Ag^~2򚮺+p5%~e5jC}?Ƈ@vAVOy?z+๫/ yFj4uWG3м4k!k9ُA|)m3?ڠxu,?7Bt4/V:w"y,:r?-cx\vlqe-|] yyq6u" -#Wg_*|Ė} ׹_Bbž4u}x}ʷna ΢$NAAMO%PcLD>s6:NKd㦗ihM1͔=Mݾ=Bj.֜vvtn1v&>VojTo {ܹ?ׇ^Zni˨gn. 9ovF] F3ջ]De0 8_)BJs7,[gj.hv+sqvQ &rfv[֪GTw%!ɋn*[@,7-ْOe# {9j<볧]y^O#8vajR|^Q8=>_<ʗ\~ɫDѪBU8K'SAyE!U3{TE;C{Te튩$TFReXF U&Esճ\)J`l -X :{׾iG-ޓ5W]6&+]1sZ-YϽ3ҏ ms.T~TÀfJD|إ?'an{<1eبONx7jf v[&cYSyӟoI߳˙X˻D:bMҾ1D/ٙ krۅy-ڹtZs +1=~k3hK)4NN)L -aNWΨU}"XI&Qv4~wLI?%'h5o~]2I6}39H4ӟSzc?>kD5ǍW-41%};Kgzy')Qͦ?' I65@ԇ3@kYJTR6mX >~~pܵz_bt7D?\MDYjʩ74>"wi"b|Diӑ\R?lj`9n;`*Xt*s"9&؄?qɝ "gfFXD(k5ol)]gew̩:k$a!8,f&̎  j砖_}s|TJݘ&L'˝%]Ìad(*emE8?SXuc: F5g3i?]8n2rbiƞŪN4OF8u8Tb^hgEPE"[7l$GZyni'IX_=N 7j\<|8…$OKQLqOx$rCH9I>Cw~ dfg>=]5b㉕w^ΘA<"sr}%F鴦Noܘ-k~e=ooIDĕetzdPnoL' -'"~B>vz# ljp&;`SW0 w!&e1P_rzF0Τ1z,WÉ Lh`%1ߘ4pPw}a`: ţ:ŝ $pK%>%,өG+ C-PiTgV$~|I(;Vr䈞njW*Z'FYivq'&u(n|^uj̵9DBbONƒeR[I/䘊gǤ ii^dž씜+-1u:@ڪK~4 <,4f-&Xc0I9Q鄲:W]F\x8Iͺ`efUϜk p;С'.k -׳x[!޲X.%G*]7>\B(;{]zpB[$i'd?IٶBR<^U7"5Emp]Y좃'Z{* ,%?qN4&Rs5ᬾXYvg%po<}TLb3LsW< Hk-O>_HT0t"ctɝ.|23@ȟX֢LTK:Y<#"TѴr=yl_":ssC5ɹm` -8) +ar"'_~W⏺ 6# ܧSsCj spݰwZ !ts*;?81;.GH}vlfuUIYN% C-ڢ=tLF O/G㵙`~(o܋ܮ:ڊ sR!ߘN,oG_Ҿ]yw)bLx}W7|wHqK7 \Mߗ{Xj@;ճwnFGU0 _9O:_0tqk`yct~i[0c 0j{:*^"h+f -:Q|D=jy>@sW̗7 Y[O:䐝h~.#YT*#Zs6z֎CSI.u:mqcb!?2] ?ſ]IcMO|R69L<0Xb`ߝ XJLiu7ےx3L$ؖ8ׄ|kGr܉{m7YL7&9'[*']x)ۑQM@:GRfc#n0wcWr0:E͊ǖ4ccc*J8\mޓx˸ߟ a':z_ցMKȢ# -/Ims<,0霉lK M1/JZv~G]B,\? dϾ{gOӹi&Ffcm8k- ȹ|E2)=tpp.npgJ&f_G3> =<*;3{e~W - -nfNg˫.MXӨda::1JCʗr]Xz"& iiZgEOwbJ] :[:|;ȠYFu qYhȣh䂉+mdhs\4R~b!rH,z#+a@5D.h)'C1gz <6 bc+_/cG9<^g[Hθd"8Rj2,r-tݎGYw֐_wh 3Ps,AZ!EP=3C(FI/$v{ZV|\£y cc_KlE"|7 !o&<C/ޚXi0fE`o7qhN/*>m -HX m_` w4 BZ^Vo>ҭǷVKSӗyXxMJiXcٞ9=<85)<;$ [ {OI.FJwkL1AT4=:c_ xAl$;$N(89㜊34DRLc51v]fh84CT^K.\d3y'<}~Flb߃W&њX'lإXr,ݶx)cDii?0e[DThbXIKqu$Џ՗ON”}gm:3mdf1ƒyhJl|θ:Ix?:'fzgJ(Vp"ީ{K?4fN`tR Cj҅E-RjZKKٹ_OaOϒQ!X#$ASA> -c.LәXjkg}wÅjN0Q38iQ$&0`+?VO oRwGªs8a 5wA,]=7cG(.&((+U\1T`~s\æϡl3.S/Ql[I^GvY7&7[{prxPMa5-v{ 5C{*~YSf/ OgV#G|O&;>_$ -1k=Qq9{/D"!~5Ir!&vzJ9uzyQcߘtvgϾVfLf6HVrL [bʻ6kg0Θ}fbh&1@߁#әn.Ÿ5;i![AhC[hx>(=+L@#:vG@kOOg73؝YqO=K'# g{uDHٮdx,Rc%R\(m0 NȎg86{Sh.O5eK=ǚ(b?#6p!VF`mjx_ T&К"/7ggNfoߦ xfFRh.4c2zWFto;tTž˲PGx¼Y~Ju:D6HKg'R@oLD-+3V`*)9="hI t0{qYEE9f^YQf\$j1њ\e6b-v倦-"n@aWhiR@ƻM/5֝ݿ<۬@[Dhc@oLc}&" 5=f㑨4MQ/d2z2~RZh%y>I} (/cYmpI,WEk%K୩<ٲ( 4po.c6%^4wD\1P#-Pu: -V t6G9;hދ tnw1@KTpB4 ,Dйݳk J(5n -%D@h}ߢNt q.W}4o8a{HuHi\Np~z!A2o#^#FMnXjz -Z!*4@OD <}Zv(8|~*mہ9yܱOǖGhI}bbc%ri|+m{tO_ӡb?L](s_e[Tby6֕{Pze)_Ib(˽m֙ v٧D?}Z<:h 2l>͆3PlzEEiW>X,Pl(E07Y@O(eޘ?kDVZ6(ZȦh -fu<ǟK-势eZ҆n橪?IkKúDԩ4a s#.ZhyXMYG2csъOE,{IedY4qcYYs |p-\t6E@%=QtS[\̬zjFPr~︘XȠ=؏avNFP3 xDo2?!B/I -y B[qR;G1AZ%5?3/1>Nv|7<_C>I ->k̟gX -gV-~$VugJYʽ~]OyIqq)O%EeK(zlQcka' eͬ葦]U+ ,3dp#WҴtb[nUx:bxp޻VF\&H"vFbQjn37b4PZ$%aw{ |a3rOiirnȞђ8qoӵ#z'㠎tgΤt/]/u):Е=Aq. t?/&[dfJR O(zD_$/ypB>'Y, 2N +rJ_q9%ÜUb`35UK7k7hzZÜPNK>+^wEY]Ysh1%y8u7&m3^af fpeR4,\mytXi UkLPuQvb$ߪ1޷H1D0l/}lMX䥜A9VRASɧNE lUڪL>}s؋̣-6:Yq-ԉNjY5 mEBArOSl8) m=,{"QQ< -]\ᓳ$SnymT@<LP,A #)>dHA1]@(-ђ{ۛղVP8 ,H~[A=!ϱkSdm;KA.#OZ۱R"%.`/ u^cp."pKz%ZR,(ɊQ -Ɋ"$ˢЂqC04Bf0I%T7N^A>pli+Nܘt"(Ȣx yrSiLAlJ9FT.Mkhc2>Z ޻G"/v",,ﭗЃMyh|^:+~F4zS=ݘ8xG#M9Fw Hٲ@SC|[ Oա* ? -~ )}]rO )m'ռ [g!oE]%X47oRYSVy7:a퉳ts/G|M?뽓/љ`:%*`iZ)+; )|zcR_ r_'cD\N&RЗ@%nnhxOD0JdzD;zX%ڍ$%iUZ_h0kR\/.bl??h~vIiscJV[6Y[sGt_Ɩ/y12K:3NBg-UB$+f \-K <(0k&9 ޏ6^~~{qE;75%vpgfu!ρ 6_]?yLw?ZY؅6l_e+`Qg?_tZ !K-} p?T/'UPYb cm(Ѕ}b ~t$$$8])H:a[)ҩa'jQ:%s(=$"\5sгQ\iH$8GuySPK)G_A1Qɧlz|暌QHaqwm ݜ=Z31\*F(\gb |wk,b-7&4r42t,毬$= npnsuV7s%]T7cOny _]VЉ*]C\Ae/tՂW)WRC\A'~PC\A'A rBU5ttZjq?X -g: -:l *j-/_ $Jvрd7mV/NM_HKZ:_Zm:v֊tUK1sR :S6>S<>Qrh'zd*U"WJ(I̡\U4IdD ܞ -Wc ׇdǫ:.n4 3! bN9iĘ-v۶zIjnOZfAU3*u&L"/wlԗZ6^U))WڷƪCu%}.Cgjy`# -Iرɚ]U`ȳDń_*q zJ.mE:-tR^NԅLsP#KtAuICqu58{'Oٛ5;{rsх(0ϧS5}k ur4i*qS2(QUwJ5r7*e<񀔏S>Iա>U&,w*R{w E+R{J߯T~NE*7*RQ+RQ/Qv % Dԫl.nPT -'-~+fF)z)B)W?(A%pQA)t|LQ2 ~RT6WUˉB{,Vq&z"Ȩ3a.vsWѸt:/r)w^,{=GQ p^8iljF-}eM=l5:˴!zbBPbRncŎEGT3G=CU5KJ}1ԒS{.:>4]Yj??Nzɍ/"Mwzr;tߋ\[M'j:N*&(^/?n|5T,^:'tpkVUIurB7龩ڧ9_SM'UK1j:X߫]j:)㆟;;tRt2ARn5qzcj:Ȇa5+;UM'g[n5vNԕxOEk~N:(\M'["ʁj:) ^Neg䗪oTIlV5Z%TIsuv]ut-^TX}u(.oW'o]haNg* 2!QMa -2UrHP* -4.'ܘJbU.+$H!+apDZLݑŝ#͕#s۲.5ws4߹NvZ%Uri+Ӕ |gsl2t͝jDq6Ew?掭}SNѦ \yII^gQMlriO]tAj2:<+F5ihQ0O\_PH"Cԑ 9Y [`CSe,u6~Ofa  -J%\s6t?9 -:Ӗѭ،e߯T>|+(p87tT/̮o@E%dz-;LSa결SQgr11V0.YR6Hz߫RrKU]fP+zr9ԣW*SN'_oI\vU> &Ey?^uQxۋRV)l??N8.WQC!U;62li(d wJ; %+{ou7)U>`WnS'vSON7|*p'KR{Ew]ÝSQ k_f:S7sn:t+W>?Brι|Cn^z -SGVTtv.v"&(΋eL7eLZ,Ѯi1-eLAN]E)dT趟VeȪeUj)bDWb~UELrDDM{aT~a(qXS7j\SnSŐrtW]I)ou~h}׎T0U=ܔf+o}04T=׸Jj\2# h|NFƍ)}h4 r5\ݗ}z)KLfb'A]TPwcZ?T%-z𶇏)ɢ2<.WGL&W* Ƣnc%rGYB=vz:x@i; c>#U9ڬw/ )7&D`s2OR&6|s V\4g R@oR t`%4y -2=w>qE{#}v!ێ__I|C =:B}&arڪZhU͕%Eumɴ-0}|dl!],)>+*>_.}$>ْ3{+9V(':5qN)"hMDIZBœؔB%I>Au>}my%M Z!KG՗1]Mݡ-n:>fV))Yy^Rlyɧѹ٣MRCլτ\~v65ƶ\J̥ɚ|XȔ,n]ߠ5^_ngoeqBVhՂ% --V6] ,-𸾝T`֏)$Qs~'ִqWVe\v=h 7򇬪BZ]"ķV`Z*uY>ɰgyڋ>lHe"쨴 +== ^nb7;"J{vj]G~]6T-B|ד#( ?Z^[zY]&/kN >co,5@XNG-%HSPeC,ÎecNK'$N>)׺S'>$ylk$,tGT'=V%F|q%? -Oml`y`]qq< SZ8 I7S{a8XV/Fc,ð0stҫ001ٜ-m"3A6qԇA6Db#-lw}rvj 3&JB{Yss\X'L?[mZ?}c>1 $dz^g[☏ͭc>Y?99W J 6Wab߿dwi 5ޥ ۸8%b9Rh汐5&7- +dXn?ow](]zfq.(]}HV[VQ|1SMc+.[Giء=C>fѶi WyuX6-?=:WqRXWƆJDjy]I|${F{Yr_9>VI[| -;bW%2S/-Vy2/=o&>m0 Od5lUkv]'*{ 5Pj[Re=@ tRf´LCkYŕy*YV<[]}z]7S+y)2=˧k\9,=tW$VEN[tl2_*IY J5V*V3HwJ2"->yZfl[ -iGfmUJ&ӗ $f¸R㽤l}܃ftζijk.'|$0~V'Nݛ)+7)0lםm2N’q>esoMn붷+c yg~;Pݮl h u.}C7:f0XqKY|38u3\xW񒹌ij!,z&-qt11S–⩤KR>%5H͖T67|0IW}i q?1V(Iؔ8T^218?1VT0Ky5K ;.`d c,M(5olKyKJjWiFY_X7AUԧ=_:в 9;˦ه ť'rhEkQcM=$pTaDr/؇ q☴zٜFıeG#FжrՕLQ&}]{lEƆ#J͖U,[V' Ok[#0|--rCnۉ&pvYYcR-4-Rqz_M%1*T0g0ÜU7g}^qLS͑O,*JuZOiOʕJmEw=E%MM-g9ti:UCmM}³A#z.4&?*>ROџ -T>u|Kg.9zgˮ]7#kx9mQ~p仧ɇ*qՓOgq; 9wb$~>=2yhjzUqwU␔P5CLDC1RXQ[hhX RZ} I L)&G \O2I^_˃t'*K!*&M,7' -= 0p1>XM2%iZ)cUb&XpMU=Ĥ))ɯiU{p6eJV7I̘DU'7SMAnk)I͐䍾-HJ'!//cUmIa#C'DG~:@~Z5Ր5̔1֍1q8TܜK!)rf""]5{ˤURk 0aW4W![I{x] X.&ӥ$DXss( )rfXa!VdBUoMa'rzf~$@Jws_ .Rʝ㻗[1" [&kRgfݴ#Y -.&;kEfa$ד(ʊS  `RiWQ3f2TފS2VżaZA DxQL!1 -B SLFǙ,dGҶ?W(+2baeô, T] Q4쎲t7%^]vV_nO|W qBpڋ'Ԑ+s$۱U?.%Â3sKR+\q&dU䑐zO1I$I#Ҩ&xe@$DۥU7I {0 D䐊s#;.V <`yJWכGUנ*lnu\4IA"&oTO_O#(I ݲ*&F/9S3$)Tu3!{FcÝD #T@<MxK_IRLJy-,haNفD/ŪdU%uXpItoCO* 7oǨxHI8hdI{<A!yqSU2y7h̝nx!2yFa _ NZft|?pC.lV@rpJ iqaU;qƜ2frv{ӽ+qHJL__kڟ* ikfuPwox.i(5#dbL㾾Wg0'r -JKfX&m^9I͡n1wG L+v957=0BJCZ$/"GidR1I_Qd{X) caDR4w=I^{5E19,̜ eƉR?P!kS+) fLI219?{Z=]J"f# Hɰ[Q:?,'MyDS3q=A-׺LFRFH* FHe5`_>S0i}^ذ]2|n4涃47Z;68$%,DN9HHiDͲo&HN#;_uȟ: #&1F`{ Ӡɪjr2dzZv'7@*7CRy}H"vSGM" 5yȪd?l>\'Wn,0'LW,[ נb矫z -aHKުf4V@],υC):[jHFo6IW T|fh0s/F/La؈Q!0I!rHujn*^?ԻtIiOjqfh},zC/*^b2htYH 4itlkO-UZei)nwN7ug22br21Iv?kNVar2hd psg&wwGG-PLpk_o.RT@y&܌GyN) 6EQLLFOzq{,jx""襘ԥ~Xa !otϵHy۟i)iz(&o_߿C4y)_CZ`3oB"wsI#@nY9q~ַ#!ުFٗ4diȄdߞj\4`rVOjq X$MFif2B 9ު$pK]uz8D^f7zw7>d&)/2ÀI냀*O80G`4De$sgcznz0B"G۲.~UcV:jԋ}+F"oU3brD}KTʕ'%kuBG2,mH`L$)$%n-Vf|D Gr9FIMA.70g'9|*29Ƭ`C&jFY?씩;Bo  i6V3E,`*m&!Y*7dR -ZNJÌ(#]rHF1[UMׇ͏YS3#19Tq/Ywշc -uv.0]S1?|TE{ I5 -cJ\L~ea"42a:u<;{Π'AլHIC 9^cn}O wE-C)8و4&){=3`rLCG/n`EBGbՊ#(џ yyȱ}~= _g!E7"PͨD 57d]e5߻.tY#hgpAogoDNiJO QpiɋGi7&% V54r\u+틯u_WÚgЌFoߎO19ɿh>}۩r 9o"by!&#!p@pZU8%y;1w-u͘U8ftMUe %L@zhb^ΜܜKQ kyyyPjn$Qc.%iL\yжQy4K䕈hX{C!IoňHXjxŷ{QŦ5ƍǻ=mr8̀ɟ>%Zr哑5q$Qisal&tRQsj&xŸ6:\WO D:ތ!zH$MNvaʕOAHrTa.gV{j+ɓ4ӽ7RäբtS桏DFD@Z{yfY LtWNմvnh0HȤ"1` ɑgp| oT,1"YzO;[1OQc$ϾyK~0 Twjfj1.&;՝IT]9C^CR8>5/U[%>^`Z$`j7z>$ȎOLɏtL&&gP;t$B/vp0hzUT7 aȺ<8J<[oFw7y7kW.V7E5=[1V3hHnɬ?0,`&pEpBcnp -RiqDf$q\<0-y:4շb4&GvNAẌ09tQr$5fDcnţ{TN#ra=BY8TVU]8'V578m9̺&|v0םsm}ceG f/hhbE^!E3jOd^R,QPߗ'p !@f^uN*0N3T#Om2~Ivt(a2i[U͘ljH"FwÚ]АA&&G4W-2,`J# W $:@!U砣ssx -3rvU֪HSwotŨ'hۭp#ϾMz*&#\/XVFE`fy[Z#zI!$R]y#R"~Vf\$ˬ&J%)}K=`2"i1v1fI+*^+$X# 5p1W" f$Vn-X߀tnL9|I-#$mkoxH8t<̀_wȟoI,&ͷS2Mel)c81Vvz tQ1)t"m,wL7 Ez@S$4;~_>(S֖А`j4&$%`r,]O70 ,bz Q^Qec8~= ;AX .C'$b{}/Sͫ!'yjݩ,VQ~Ƙuߺ,6ſ۔]-?zUvoD*9!ѪirV9āZOeߘ{,*nX&.&h/NCB譸?sU9?~9Lwf;='+eo$OqYzg춆[= 4`2"j?>!>5ʕ'Hkz0$G$w&xb9RjYzL{}|k !4&_>Ue}$'K`AOc$iAY(<&Ɗ?C@Rx2SvK0iL]!/YveyriGEELj`JKG=o_x@n8d1E%8{r壚'wgqQWxIAKUA0Uo]E'ouKГGΰ&#SfX+[CF@G  -'#O`V/`탞gC09rZv嘘TdI'L!8:xI'AL? |F"CIpv_<( -4=ؚZQ - .r eTg@3OI'@4+tJArf /6Z}{=wwP˛Г`J4˕Y|L3YncideaB>JA.vT$rv 0sxW= Nk,`jwIUqEѳ0AŚt8 :A孞_G$T8M=z?q)阗O&9<5Z} 8e DC.}l=JAF.&!)A''䟷˟!J2ʕ''U#j sלQlQ(ՌgQT5Ѭ06Cr\yRFT0thI } A -ϳ&}V \n -%8Iߕ4 QnA++ `7t1Y*.ުA͖8y 1`3DLjh8&ӣzvk8I!Ch'A0A^/!1Y RbW9I+YE=qX8}2qSj}sWBF)9uKIROolxn԰e{L!8|M)>m $3^'& jn)&$]&8$f{XLZ4X$Xƾ] 8h+dߌ#twW(c s>so@|&&*V5-JFoDqP8zcH rG"#yp| ߻"$B(G\>V=)B-8݀ 8i9JW"ϡ¹Ih zYfTNIɶ0YQL7Һ.sa(L&M+ܟkܿ,pEUs\!?_=v;3`21JA0 -=v` -na(4-|U5c)N5VaNG}]4IK S6n4IQv;0Q:|W5[J=`2*S'INjI(-o4dIH}|`c'rN%B(s|@w/J/r;8r UPD8o= /Asis;=j<`*8PqxdbSTWghqe'#?^܃2A 8sjaj0:w$ -u_>#3wL6x0N:q֫n+^oB(s]&]/QȾRcժ#!oI4Nn) 8;M]ހ LBB3rXUpLi%uu\0szS@p+ܟ}c@~ルnPi$l.o0ȁZZPГ' $&;(G<=yb2HͰ=\Oةf3qzt`9DR^t}퓰R:d1AOf+SҋYr&xu8"'>UN%ha~&g'y$zA|v^gTd6(!zv#bJpGtzք^ovviΔ,`oNy{ TS8G& QVJ8bQ~UX){ћ|êZFoD !K='쎅\y -^M(ΆV-X'w> vĜ?-PQBOf5e!\bTf6]O2!n' uB~0dGP^(=wg:i꿡_;qрI犬 ?K4Br=a;Ѱ"P<׻6a&$3x_]/SL)1o,*L=Wd OSUtg7*9>]&k¸ܿ,!1!|2-v&p"y͈&S'yD&փn n6Xja E(G $WDL~UP!Iev񓀳9.aPCFPBOfyaU9ߕBG攙Oe,rR d123M1@FE7 P$/z<%~z2&뗏{`U͇U$0. &:qzRГ$Mכٲl?|ҰʅUDLI#{X$o1enz`l^Ü&tz_G$R6GcГ (&٩&_#`2S/CTD : Q=" &+ݟ:y2 ==~7Tʯո:J'{Mpv;=`DqGBo5}(sc4^19ɯv~ħrLiw氠Qv`zR -V 5 -mG}z*߼,@*I#`gO˟!Ji' =Aq:xQBpti$$Q|$;DL~iXWv'n1 ň⎅&(G |ɡ4~V\!bS Q7XuB.sl>>A4IjPg[2$e>Wg=GQL'yRIc搰}d 8<޷;x@BJN:0KҧNM)8LgMָ@03٥I޹GrAB.1%19o?\'7onASDnB7NnTcAp2\J' W )s4z2b:Ld,yI՛=T葓AVkG&19UIܦB$Ĥca^z5EEx+ބ99W7 &$_K=(4>brv0.vf>|"&F^na7Qݶ0 zsIgC zwG&0^݂99Er)ov#%gbaU(Wp!vGhIIByDm H `T>"镮AOPLɿ9FB:Ae'Q)ȱqLFQyc.E )o=yU/ڮвDLkKeb=\T DZ^Sh&A:r?xsГ9<&i}'t(WԌh8z,:.  &?#CbN\ј=9$QߚwA<xC-I"Oo؍ &@Of?vPN)&!C~Nq%avTn" I^ja'SgP2I>5[ $q˳4D"4ŭiEX{^\醊&G427f\T ǙdND  W&-9Г$yh?&mٚ9z,'s k> NEQ^&A9,+j>VLaUY3[nVܦvw1 `Zs?} X)w{!&bz#_Ej{ۥ `2#mJ'9L]U\1'A0s>=#$)9<`rD&q&3ŒQaﱈnn\$_:}eݺ5oƜ'k7ې}qea Y=DvH9 = GބOyQ2y~]gR=I_w M"z$2NתVQLN-LkȚ0락|cI0ʯtH;Io(p{sw ʘ;Z^Ü&\fGP3޷;;e5ۮHI2j򏞔>sHz4VbҙЍ[ ΠNg띕$Wm1!0Gww `2s zMZ5NL뮅B0N`>ˬ%$eN=˕P寞Iop2`/8NyyZ'm .#,7M=Ĝ#Ys^O -v~ׅ &Dzs 8 znŭ[A (!Q7$e6'vKRLLF<%}VDd&S32qZ/0N:EZ U -g7:<#޷;H4Q^>Sij)II A8叨dDo&Y[ҵӓ<`Y_-&2J!1TpgwTYl .5V;Cun"$eUno -D$Q -੔1{%Vv2 -=ul!ġm-T6Ua -$j^o$dBJ3`{ 4L\Ǫh8ʘ;d$עL:G}*<`rXIzfʳTL*KqIGj8 '9YW;&b_>鋱rqg5VƼ3L#'L /GEtFE٭CO:FEI 7%R pej2+emLIђ;[ɍZ8쓎n )Ă hKqo!;K8m!4`2" k/n+2i&z,:6VaH30QfodL|Sf73Iu)82q 'C.vCO heGU`|pD۵4J(WjaogMr;O#a^tU`qԌ~<sed?)}|]S/z=qn -= DDGn^'@ZFPrK^߁!aA@O:9 =iΠ1='#ov*g>_2SOq xqҡQDN 8Rc}AM!vXt"ISa>DQf8|FfPY!`2IC 89UhD'ox. ^ Lqy8#9FBg7ʘ; z r H*]9b9W'I^} 饦4=qT -rt=Yn'AL (3v19y(Wde̅(āqiH0x?qS$OyNa(FzCRf&_ -%/WNOpwS LG8LO* L%KGo=CMaLތ٣'idDV9/^ &_Bi8٭,6 -F? A.F'}>Ĝ#{l'}0-W4`Q3Pg7N_A<]dL#:TPvrI^arD!Y7I%x>Vf},,rV&2l f);9(OD޿,XJ0I䏟>4z,Y`TW˿ո~.r3# R\mߎr 슄8ĤsB'A 4^>2ޙ R՜lJ+rnEﱈL$bg6a-0fyt~G,!,c{UIDO9i!z?J^iHχ 5x\chdv4<ɋvn֍7a\M0k1UG5-v"`҉CO: zŰ1FsV-zgovwC^ Xha솧i`WAyv)yUp/NOɟ˟9! &0!#'{ȊRq8 (?_7 e&PQM?ͲoP4"쓎^v#rbY)Ḿ$LPoN[r>@%_FEA  @!"65Mv>"'y7=U4`f5v_71'vx 턥a7 H]\a"]V9o]!wR*A&_? `stǢ` @"Ham?`VM<`?)}ZOg;NcDnj 41;iFT8DϏD'Ŀ'b;,`e3=QQhULOzuݲʱ$fwr$$Y_o5]094I$-9viQsu"awo5 F6 )^o{$k,`˕CLS-Dq'~$'AGSt8MqZΔ$q C.&_ɓ҈6IwzE h:x8| *+;GE'Z,mAd !jdTUhDE[x@J8EuII.((N_YF/0=Y;Oh’ɁZ.J7)Y&z(m[cC(\^V!)HV%fV{qIɖ(> ’I=$PLr2mou ju|s.)mcBF2t; zk;4 DuAc&'-ODyǏ,~1v.)%{6y缧M_[ ےX\%JdbDZ-ﲭ}Ip_=mI:ik7i$y['qӦf'M#.qQINw`\`}p\o;s?{YB m {QƼ˰ \&ɗ<-e4d/VR\ cl LJR۳R -m#8Sx7WU~& I܄= NҗVfRS{(x<qRt 47*b'qZ`Ad=O6IF~`0\&J,yq18M{4R(+-wԔt"o JibX s`DOFGnvX{z>{GTQ2tR>WR~Jivm{^(e᠖Z|1 07b"I[ݒx*$u*nH`{Ձ[kmQݷFbkd1s߼hZhbv\LGŔR(Yu"o=c 89Z.Ko KY1^kMQ -B\K8L[ -;EKf[ҚWrQʿyȎnG6/sfX0+=XL-bm&Tu? ߦJYX|Xe_w 2G7ZvN1B&f4 #ʫu6d)M'bͻE=P}T&O2(3-ybgK#)OV^ MWJK#PJ > 0.?Eyp#sF[VqMQδĔtSєsX}C4J5}9U_rRʯ+m7(є ߦ yo(-ѳ3iIF}2{Qbo20kttwoj|2B^ .we؞;&66[ҼXCL)tsADs.܁zNd?2[F!,3!Rǯ 쒚LC{)M'⓸sC>h/2A{/Vn?h./·AҜI]0׻bJ~2܄S9fYN' W2(R0zwiq{-XnL/lOʢe&eӎ@) L-ma N勵N#Ey a@ rcV27J龕HJ]ڂb L1bJ>9V;[V@~ᦌ.}/I(eUJ%^Kyz]B$]-p蒯^}?ȁbE v̷cI/Wò eLUX~1 Z2b;_{0)kw1\hTy90wTRmQKWg.A#L}JM$D|)ݪAӼ+=,Rb;Cu伶Jd߷Kjɤ$M - -g'+]a>۠|V_v+eJo}2vVc0WeE0.XǾe)OVXkkRY<5RKmls9+W{1 xqrSJuԽEPvVI(fH7:.9ܜ:32F1A/:} qHU;$?Z_bRzH2->z-h Hl@&ދ(DEh ?}j(b<"ƘN 9 IL Kˤ"WZ=;YE_/X67!_qgUK`; RK-jqv<̉&8{PXB!l2NJ˽rkYJ3œ5&2Y늠wGd-jۑQYxr"'+9'ypP_ՠ|1*]j./Fdp ҒA`;Cum82e))~~!ڞ;e;aCμLJb(9Pc1d^oZQ0343Yb9^z;K{1ƋFzLo!\&UddIł3KLFtj8%c;O8>&%zr9ɼ v^}b),ZP,0b 8Ė'xE"wC XT)2 =L 2G/֡ bLEe4y|g&Z&O^ b;{u%iCwے9IrYR>"$ojLBRv d2"ZC&OBfons r=wD Q*puR &g>9)ZI%N:I&5ދЪA+eGJ0(DL8<ӈw[xJ,=`;ߨR&uɬ@RSR\)OVXkR Ks="fWE/~BG6 tR6Ljbָښ-dQ/`v5b{&o`;C*'Ah(ICe^l/ZX|1=:R<|a s *^k2i.vVHDe{&_\Ldr2re75TtWj|tf)[4`d>GQQQhJP5VIHf&=XGEˤz/f-/WJ2dKo`d~HrZצZX Q{k^d# bָ.i/k͕4A)#[ ' Fes9d-n%}xi|SD ?<UJw+6b&ATҌ(6J̢~5no$40#%ů?l'sk {r{q(fGx]SJwѶ\l_׎OHhd,I*6[8 /k0d :IHQDD~gᗛnr`JGc@#\f`; *4S L:ZDVd`bvr,7K>y[]YP}Y)Ld轘:sh e JGw XUmN{-XƢ"XeznOXFY2+Y{&+!HͳP8%WRr$cg%Лb%Лm܈hM!,TZɟ*scI/}pșv΂ދYJ=QlA)s~6UFpT'RKC <$Ho=@@,0MJ/R.5W3(gZ,u.}RK?+eD)ܷ<; -ƀ=a.uW.wZp1* UJ?2#ViWپJd)mSe['CX=yy -zcB&3s rQUFQxFɾؽÕLP܁+hn'3ZLjyn.7 -ɀmAd.Wb`&XՠɾXr8F/M7UrOffT%DDgi)_t@v2IG|Lf$~e8S9FI}rU }g ?}M^g/"D'jBIB;S_5OBs -xs_.|%o!ƞUeaR)ELlnK^QTTFml]n)9O2iE2%OtK[o(O ݳ/H[0I%T턐4,E]-w:JJLrKF2EK3q+r'3.s kL &X6:4cW7XouiU`2<֯&;Ȥ@—qP_iɧGʼnB&s),!ڨ?G<-d2_r;y --D-!EiѪA3iwUy[X6in d2"nSeS-֖>ZBn#%Y9d&b O1Ĕ-t/%V v endstream endobj 30 0 obj <>stream -dI"![xi n/9Őנbvi΃# Wj+R?Gs{A.w-up;i{h[adv[e,yl#uVA)uCI۳Њ7@3èIq΀ދFC~B?WUo!gb(e/sC&+&ov6Jrr?6xſO- ~}\)@PyGǔRC#IXLcT|#%AX%B2Yb!2 p _a@&b[(RQ{1u2YaIȤ HT.moAL2en@>.3)[ՠܧ?|B)SxsAȤB;HE^z}yM d-uVY-\j/]f9_AB>xR&ctRuk2iig zi5VwYl.+.i(6f[& Й~cJ)a˨Zm4g?/BX +dO;..]&f]^$_(LbR_5/Warj,QJL@&s ^RtN&`@A'|;|u /IuPѷzjB}!d2gKA^4nrK'r1mPs6rS <9I}g N!E(~SeEe]uZ$Vx8+d2W(W/S2%_khfOk ʹkۡSwA /Bq Rkkі k| d$|B zh$5SL:[}e6w' .Ox=C!t ]Lu3|j2Q -Np<>R?$vM%,U`5* ?QʘL 4dŠ&SAe^4w}3SJtc C-Ehy+z "{Q*7jwȇBLdd2nދF2!¯Æe)_҅ovKկBIGNgS*yZU%> @jd5jbH ?WʷHU*2X-jzf< #27yh9H _hhLPv@KE_eCFNRz*Al %Jq#oDl=k+VGU -ypO@p<נ`;mM{;L*ee+Ƀq/xI]4EKoo`iU}N^flk 4LZEeV:^bڨÔ_qVʶ -[%r>W߼ֺG5Pz+rp8dS5&߆C *d2({Y; -zk20BWJ0l52 CEE3'.4yj,RnmHׯi@/ukg*L+t[EEjZ싥R6F9ϒ֊hIJ:x=UrB+~{:iJ)*:~砪|qU-̙IV,ɜ Zg {!~62k.H0)LQ`aKNaX5!ěa{ۭ -Le&ODPRhD̢zMI|VM2mC r6@&MGyMBE#`UL! - _CcJa^rP - MTFΐK2 8X_by~݃ty 2iz{8얮I/qJ!{5f%((A)*J龕mh @eGBfd/c8c JyABRJ.*ͬL%v %h[&7Iw3=m!z/PPj *Rz -e>ɖ->RjUF+7*3AȤI!pYw.z^%XJ٨|>[,|OXJ!&視7:EJ}eM)i2 4%%NyTKpPGW؎98D \ V\#VVpR]>qp x2Y\Œ1Q0{ՠx!tL!(e7< ̥ z*̛y HLzL~cUL^4ᦂ+³*OynͿG|c#drPLphx2%W[_b!<@~_鮶^)Y_qynGVڞ5ѦA$*Е<d -{QAas#6~=X*nWN[aGs>nWz?*:;&dɤ[/OILt; l b [ꪲ6xL{ Rl'$JTW *!΀)b[[EaNK/z/ɕQ!UϝRs9=g™G/ -½2ǀiGnmSYgFowK9 VH.2YH@@PVCE?t׻#Is;/. -Zj z!` -%o?PX-LA,yƋ/ƔRPGhC'oFfm]QC_iU1p(%8$ -"UAN|Zj^?(%0\&LS< -Qxa7^eGӱ y_8?Y'eˬ2 -@&p쨲t $/D9- w0A9WʷaEYy5zZJf`/%ker(DLb"IދqM~qs^<#ѓ7oE-d'Z=b9ݢ[Ƞ%I H~*g-y㬷uh8Ӑ#ydUR&)3J7" n TLTXɓy2 ҂ ^4^mȱQGYo=#ן G7$V )+o27C46ǀ -CGNlC'=t29c{)J=V ;4s74 2I^Ӹ2 2f)^X<’lPH㎆K@su*M^[' 8{K/a_"Z.| ݀8  Q -0qYO>r?ͭFOqN)e[7]Ɓ ޿[C36Q LwLz/ -031-]i+\)ŔRc6K:ˬ*kB E|wE̦zCW"0 VjU76g/K9mt=j{JpdC轨3WZ_ټJYc%JiS/>k2iкoV~C>'S لom`fbҡ(eH}RWJ-]dMچX?-DwSTؐ&Fe?A&A!3,x @8d T ґVASzm2e)CNڤ2Y Q+8*J LXc=BAEpSg0&@PyS'ԶMJ-fDB놣 C)PYf˽҃yuOfBT)UVz}k‹IޢB)I9]nRki蒯+dʷgf ]TY`t$QPQ`%(yv>@DeJdr(V ' Q ԋNyU}47:R(ޤ[듂(Ў簝PJ@<39$h"^L1KMXgdD)OR|YFL";eR|>IPB&uA'K\*Fb "7f C)A^Q6/Z9v Z5ug)OrTaj5zg^? ">M2 d dFE}J}砪J+ Y A$MJ HLA&A‹Hc[/&XELDK,3 ]uC܈"60Km h[`L! @Eݹ&/n6xqur8A(cuk gZKA$E!JJє锒akZe%M">Bj렔 G2)Vo=ދB/8Zs="JI>J)Z$ '7>o= /A轨7\l^a%2٢rDJiXzL$Shf,y4^4lZ;Nº#XFd|/ -B) L~>zuM -Q@'mxMzp~;17j!+e?ǻX]J(%2LB&A%oi ~? _ -; x+xhA}jd "_ L AE}$`~9,S؂$BnXAep^\|7@ qa;cْl4!I>}"^)F5cd$0轨 ]vV{}AHI~ށB%>/³DCGsA(bSD ø'V~[zfJYl)J;dP5z$f -`2Ȑ:{QKޗmd^mvI$Bf)+XV B=]UOc8{E2ug_Qa@Db]2Đ=’>uǁ16W_AIe)&?L^.c`0'm^DwX(Z:fh]J(%ȘD `h!O-I JAD){O"RzD)y!f.27B/y.\o>熞."O'_n*TYJ"4~gC&P8\kIFoCXPͻ >B -ɣhi S^2 -^T Z|2 .<#ҩK י 1TPT.\mѓX3AkYR"K wR&* -@v!uaCFE=ˣ{@d'x+RB) `{&c p!>9Ʃ ʍp7QPR"K d[)U!.$XΜ Z5HFG- Îy$x`@dhJ(Z "~oyIg$[Pk;]Lz/v&g_9nDD(N)$bNKW?'?)7 DMtpCJdLs=m҄h{:RfW_y>x ᓈM)*uUwovW[*kZaM|tt:ˁ!Ň7L H qf7o'tь1QG/C&rI?h ~z ]F1sD[-=!_>({?cO;{OS⟷ٿl_ǵ?K6Y[+mCL T9Ĝ9 vfZy*čmZg;۠@&rw{d(3*]rlj`kvu:#/9ۣ -yA(E?SãZP0HK|E;.T9N4PN2'n;S_pxsJGlyо$|O/Y_8Ĩۻ!UKf(VK2މ:p`̱؍ȝ`295F t9g,wE)A7uQ2B#q L>ڜ:?!1+m>U[n $Q!^4^Vy l -O"r&OxVW#%!8ΤTdՙ;gxL .ϳa4܄g;s{/b;sy +fN '}|6&f<%?\AA)^{Qc$m/h^JUJ$"ѣ= -&HQ (D c2fJIl{f&.v {/cfT J-fR,DHС8ޭD^r -(@(3dU 'mF>mDB6r< OQ -NBXNӍvAf^◯4Xn$"u:'ȋ9 `ne!FՠtIZ>ȍ6H]2v[ǯ 7aA'NtEF۔ ҕᐳ?Z|2DX"%sB轘ZؔaBnI'SA/\"HfQH!b#AȑD :>|~SeE?L@n%sB{/6[mR_LA2Aּ>'sRz/fX'P%ּ{wU$BPwXHL22NGDڴ6bPfMN'I%z/<9<'J7O"DEIWe.q.ALK!~ͮ>?-"{.8KQ/D9-a'=:Q5(9Xkͮޠ`c<>2-1r`v %Ʉf,y'bsQd|h[$H`vY!JL-]eU05[|+xh@d=q&:RyẌދ~ -] |2c"B+VSrFT =~'_z7"_qˁ$Ћ-x0=|fGg3g[:9"F-[vD cd"6_R[ ⓈX!@O.`%CߤyX'{]}srtᓈlA 2AW^L^ÒwZw. k-y_9>Z6EM@_& Yֆ#9i>['? '_oe3<| G$`&}ދI}2ڊKE5oA>?܍݈l/-!&z,#@ ŠB޹ߧXIQ,y*HLePu ~ֶ>R͓KI`8hDi0ƃ&C|a~QQ|d1?~-pF 7`a -C9>yaAQQ'擣I%&`:z\kp(2 U`Wh1wvQ Iث] Yc 2Ҍ"iщ$z/$2I9`qVCYa({QE!4nO:6zɺ"(%¸.z(ވAVA-JZW$/~zǯ']:ƻt$6ZLfSJ -+f>++=R)b!:& =$/(; jQ0~*g /3Ƌ?BF'IWÂI<pb9Ή''KNvDP)( -/D)/AxPhs|ȂE jkkc)J,y# tqD; -(Hf UC! n|'$B`#[F" 0-#NNKMX򦇻ɏOOnuvQ"E5^g=q7:nI -.(7v]'Kag%OmJ,E DDHN] '轘ە.OktwBw1E b)J$B`>GUsr -/D LZ5(,_D|aM%{֮-^)JҍnǺA${/TNLtK&A'S.}V9ةmhA (:,"_> r ZM"ÇC.]u{ӝwX^(n9*@rH/yTNݒ:%VչHw9奄keQJphAwh$hrԢ {q(S\C}Cvw-PG|(yys(?- cQ-@E @vދs#=JOtA>D>X:wX9틉F޹(.Jh!AqQ;^e'AN%a(`;Ui0O},șzѣQHTD8qjn;ݎwQe{?߳՛W >(.JJ`m\n'w7OnS1i:f LQNp/WIp|+= DNA'AnB^vP4=xѠ+2.rWis,Z_}{+֊D."ʘ(A;K2`}6pU՛]+/Dɱ.Gd;Y@-J7hԿ*d_D?١]IyM:oR 2ɍ0vot~O/YSu7/[U)Z'{sh-2|Rwrsv0VB읏!5/!=?`},OFR妍pg8C1C3N;G/tw++dx{[rIo}ѺMwx*$&Z_ v'D$l?m1A^v9iw(5lrszϯve_^i|.OB~o-am7v!:E#&W7^2P$B2܄S)@~JC\8I_J^3~R΁E nwњWoVܴv(|Uv& .rԢ4:/Z'V? =v9w(qhe_cqğVHaMO"%aޮ`)YؒǩY$ g[/VFaŵ.w˽/yrn>%"S<9_z:$݄,_E>^9yr<=UWVܹr*VB{wϧ$RقXJ7k&zy:KIcZeSr#:@>B.|ON^5(,_ﴝqC<82v`t۳uKrO.x|)JLv?oR"/#9 -Ǜfm7vMs#َ>A%aϤ$רXN^ww8~#KY9WhsspbjZ -Q.^UA|_F1m|^gw  .51UYop1 8C@/T!3ใ,3sߙDyRJ#ڴn-N$S9IltD~G!#t'*z8"z;0ɁkKvr[Jv׬>'z&. hxL!yuK%kK W%o$UZ%2$U"J9,#X 9k/p]<^'v$`g Gί?yb<[O]w(*~*$/D DLlHu9>~ThJIiapIi6 XN3rO.())%>YU3ݒJELIƵ׏.]f-cO]W΢uItC':e' JiTU6) ~"Azkm -Do}v:zK;%l<3{?a܂;q0qd1v'o+nj^\H|rŲ'){' Jߊ:$s.yRDL~ o{f:I 薆CRvŃ䒵sٽUغg)J2T߼/br eyؖuU5| E]kK'q*1m|Э$0{)=C*Ҡ;'s婭6)2BI>4Xk҉l<1tHS\SM _K=?yFYJzʓ= 4>-ˏ֎F9VE ݵ2z*'>vNrD^O33U)\%-.wՠp19ΎH8G#NfH:}qmj% %}O^xcgVm[donv\iEh>y1WntYjuެ֖y;o(%"GOr|G 0;+e,sDrr˻lڅ}47.޾i?=0%ӆ#%IX~Ľ/%ygqͫk.V֥dME.9;9`䊻7\^vὼ,ha@d=qɉoE)9VYhj{vK}bͫnsX2v' >yQŒwwF’ ;'w]z^'/U=#2"4F E,_G}r{vʛ|YXd,E ,`e'?`gp>IP0̡1ˬ2Z.=A${Kuh˪{Ui/>~m?3$%ow_Pg{/>ӧڲ;B/*uawjnqƋk\M߄7Y7.^$KQA) <ި'KIPr4Ѝn^*/=r֝KjM$Nt;yr%~[X"q7\pǽUM{!}޺ ?^|<6IIO/!>x[( OhA vg}߿dR YVװC:/km,L&T/yNnz}?=Wo_[GnϼcRskͮ3 Ƕ_pOBɫ/$_y %NyjvSDsPȤ1ay9i@IbP }2wO&;o(KX@Lw r*E=@T+HQ&,yg~]lE7 %Cgi k-c ?[n涒 -{|ɛɹy[( -xO9MN3d+UjdR>Q} L#ח6>OrU̖O'$fRtQ\1ٳC^gʋJ%|[(O78_z'qVҺ.%o' ŒJ/yʺ/b e!+;9 >fZُ:ـ(}V'}&)dڲw[v4$1'z2h|jK'^j W>=ms邞JKn1% W|]]]kI( ){nxy'JׁUxa-?q6sJs՛}\蘓dpE?і{u鲵U?$߹i ;Ҫ;<9WArCފ7.]_}r٪: W%ɁOVNZLG'MW'|{ڍ_~hY.IZ [(vխ]pOէח#' 'y  -dMNrOM RriD{LmnNi-^_ɞ9irǦ5KI1d -s]FW[뺅Owg|;GWJv՛w^'ɗ+O^[v]\zw _'(ĉ]H^P&d0B)h+p)^ŅRiIIfU:#e)wmZsr=Ut;ؠ ;c9ejx'o]ᓅp  <3W:U-юo_'XQY jtҘz@17).~]T4r_5|rݢLɥk Iz˔r8w˖Shix' & Mt>~^A1`Ԭgyw~7вTaM2>EIhw0V2M34_e9_VB}Z|τ_)Y͓wK!G葜-T,\U]rOz}?e'$cRjO*̃kܯ=|ÕL=t#$Rn(u0>Sd>]Rzy'D7Byjnh.ge4">w׬zc}ɹw̽ Wo]M|8$?ûxO瓨Biэ!'gGS -; =7]T[;sD;6!yj[tr46Oj0%v6<9߽,!8v;α}>4{G:l+xn\n$vNI(^mƻw%yG2]PϬrW%%)rY}k ¦N9 }mtBˇo/e;E˖a-ϫHQ&`Uu[#|㛏.k]E&H/,6ٙzᏎ]&>ɻpL 4l6&|x> @dC)cЦ*N38m{V[͛L_)YrsYϞUNز$n{OC݄2܄I!)O^ -Gd~8!-[dR.amߴG7O֧?H.79Yhuk=MHO#޼7|@F=vN>RA0{U.G~ۜm+O8} ݸ_^G/hvCګϒtwx'L'ݱd?}V8OHq'Mך]g]eeOuUuZ~2"|NNm{(:eVy9"j:~]zsjks#|B_OΟeQߍIw{R7'gɛX_GcdLG,+h+h'ᐳ|'^#>95B ,=HN#Vh!t嬉B@ qp5>΋d܄N?^{zxRgSy2')o7T0eAMkheay? 6E.9dp-wT\r٪ū*z7| %'[l7:e$z޸*i2 IwD#K(_w[L Ө'}m= s覸]lZD jPJǣN>_y |dysC |r5'yI[&mN3Ar0ՅI Х{:N햾,wkǷc{ZWۥ솮z8z yKz܌g)wоE+0BF3 ޵]}džr^2tg˫$R -snt$z,`20Y],s$u{n(z9UR29Ds`Fn-38%$UB<ޒOoup]ЊQˉO.>O8ƻ\UpvCjT=ϞQ:z6.ٕrOwU8-Z;ZҾvO~ yGOMw^Q6]J|>Z_}R+i4iKuKF x8<$b_zpyew2nZo_[.F(:yeisq;L1;=êAp;bqܘqի{|i/X|ɚ;hz>YA/5yc&Q>^~GPM''v|.-0?Yk't6זm Rht^oRu@@:lQV\jcc++ޅ#agO d@vnp[K|rC4ҵf:#Z\EƑsJ[7g:Y0IV׷.}rmO.Zᓱ/]k@hjLo@iqQ83m)pTiˠ(oXGyrcww`ѥf3VBpœ&>rH]E|I? ~ R&Xncs{i9lkB@f]ݞ >P>R+x. -3$=dR\ow֭D,}v35M?}io*>L)4emWmeO<?1D wD9[Bdr㧝|%ҺG/(“w4/&mj϶P_Kܛ{DIđJɋ ;,#}<}c-;˽^MIGLن_mx]|^AYgU(/7K's{7-JXϪWZy6 wGCu:&&ֻ9C ^w-gI]ADnđssfTss{+K=|3eI̔~ܧqc՛}>x+HQ}wt eK'{K;\tly'y& +$?x{Uq"⓷Ts\h'ᓦ -vny$id$יU,yIIb֥|%Z B;t0po(6=R0a_͓aY]LtKGY>~#x,+mD##/(29\?ڡ:T)>`Jzgu•uIF$j;RrUNBOИw>ZSY^mIΦs&R9qsLj8>IAŒV4Z 8_yEk$ndU9'LMh\JrA3#!zyO5]vW{ʻ?|ӻ@Hb,$BIYa=K:-rCȞ| F)pRZ $l e{;a{XdK:H|ر-{ށZkA}@&N`#")z* -K+}r!y1~jvT<\R3Nrʇ7(e\ab\ ]<+y=~E59N'#$ǢHiΎؕ-[K~yuLF͢~rE?bzr!_Гqxg+682udshcR_<ԾF@<~{׍W)Ma?X2ӓ9֟5zux{RÚ;/UOuEOLI~Q#9F8{H]vZz]{;'~A v%JƏv@ιAX}61rzjM' =V3>C oь} $One<ڷi ?LħCvˑ渕&fᚳ\u'g]~Г~Г^|mW$a&IӨ,ŷ,Z %8Dž%Xӵ{Oގ28䏝y֟,6G %R2pM4cɏoť擌fd$hLES>M]<7PmHazr›]xً|AO:~;x,E3YRͤnROɘ{~-m$/vRԀ!7kiݝW,o&/b-?Sl (+pfޯOsm]rAG aqR!I9%yUi9?cE?1=9g$-R%54$%{>].0nR3֘s_Z6/Xwwr9;XMo|۳W$qbs̰ҔN%, ֑5v63ThɉД]k1=k d\-i9}N\㕋;JN[HX2+g᜝w t8bR^&0?<(ۘC84o+֟26p -՜}`zr߽go[y'RS%rHAyg3=y_O - SOZ-&er"3qpΒj/Wbrә)g}v K.Ȼ_q4[M$mi-?W2*5rd[mA{INl CAr_| of$h_{:EGf(lю?ŋ) =?Kd=} -:qp $e$܆'3՞,4e?/䦴f더cxsSޓ_~ۢkqz.ϪKQEy͢VĜɵcyr'> s *[*tܭ];nLv8D:ӗR:οzhx7qamI:fp8F2C #)=\a:}=".l_q׌7Ef(L9!N1>H؟ k' } T=IC_*_n4NX΢4nV[ozœ]bM/_RIDեRCgam0<()zEc\xc"QE_jY!x.oQl,[h~ GTr,MO)KK{M_|0 -ԓk"I/>Y_&bsIFCc6'kˮB6's^]>?xW^uݍL}΅#k/Ĝ_|?t̾ $%+U#SN|ahs[<[c"iJNd-8Uz|YLO۠'lJUd%p4]Q-OFr3u- 3yݷ_~pͣi[-k^ -)w:+v[mXjv׬uM!]5k\tdb'od\3'S]xm&=I'/6l9"vU`s_`(,UޤK=a">7龋ux 7𽠾ˆ۲Rd#*(Ԍȝ~"Kb!Rލa'(e;vQ v6EڢkL6gi31 =vܩTvz#'l+))坩ƶ.Rs]=w/k.1Q{iP+jx!J&<ʖP"UcQG:=ec:#ВVĸ9\&&'EX]xRbi: p\~oW,xbzc8iqN-ƥ eÜiWb]6gL,ޭ}'E{…(t2ڴbuəl$%{% +Ye&$0-. ՙR(5=񇋖n~^reSՖ/ݿSbXʘ;JG~Rnn!*Wz ->ݲ-Wtk[y… 'x[F tUM7 տ-UH=3z\;/榠dz7_jY+Kv j[G-TjʓG=>LQOx4v0rf]~ Г^\O >- HR2i4/ iKH CuAks}`~=Ϋn麋Fi.Bk^J#D)'-l Mj,8>ޛURXR alϕd`GB_~J]zE'b8N\|mƎoyp2^/ l") hv|;D<ֿ!%Y9={.͝W_cLc6w*&rW}-l#1#zW /MRTGOsT!6'tSL;HJ P_I[>&I=1 3~qOnYtᕷ*/#i/6zf'.)saitG1Zapr:J[V2{._N]ӌNafY-nCR -!mYHyϸ:(g_i䤾~`O.+̳y=97۳Vʟ/qU-OvrxHyR9>A,adla's DfJT'.|/V 8 @b)oԵ-R귛T*w/ -/9;^P[|m7]pQxn_KerZf@I_Xцf]9>pQs3Qv=8Ɉt:-IbΓ\]J?SVaD*{Ba>L >x_Oޯ/Wzu|^Jv22p7gT$<;!sssewJI&eHO't1[$"$6K<7S0/yk=|)L/>u>d~/H9b/=Qb#;0yWs'ceZ?bz =%R?I|BX))[8? >Fy) Y嵲v:Cl8;!- !¸!Zvnn#=g^"$'žx`ҧk4[PB){ ,U;"0s9$Y8Mz Nc6>1HjGSHU^6m M&)Wb ax2;:ՆIN\OX-\zҩ'Oj` -CRVT?גPUtR&,r6M2]i -A4$¯&jޖM]~bv/|0`b/) >cr^Hㅲ<4SJTȍdZrJGN -{3q'xdAj?D4`Tb,)Rޱ@iΚKt8+BDD)C9bf2 aI]kϡd.eJzҹX:Uڤx@kIV"ȉX } g !F)2xcN^C' n5[Itkb0!rŠX?đ:% ] -ER!]y%|!ԓ-YLOj zҁ`= IJ3>!|IӤ*+)AȰ;{cz~UxGtќ$') =ŋ'7Hy⒘Izڞ k-6 -(|%lɌd21>ȦZa=Xⶓr$!A`|LQ}6(D2=XD1"9_74!뭹| 5'ܪhÁtM.1:.Pq|yr"zsY3q;C;ٹnʏ7j,'vﬓ%ࣰL$XN>{e6wIٝ/W`tz!S=I&&i2%gBL:j8'K>n;i1p(іJفs}8eht j)^%Dq{0$w#et⨘"Q;⃆̗-#pH!"f"VCԓM*;kKLOX =d'-lS&ڒ)oS0zMas|x94qK ag')V?zanQѓ"i&"m$$`*YWnGRV!?۸Փ5IG8FpH%IJڋ9{`6X>FrrȐƑG9L;HX⇾+p*==ԗl6!!)ocw7$ex?5M._HSf;%cdVN1U --O` D×)oBctrEr0X~<!uŁTG*zKA2g@Tx4C&4}iX >^HS0yK<(%-[rG(KuVhK$bn@F3O<8Y0_nn$eXSJ&)VCIad?'})Fdz K8|`4 w9ֿ)0LεdV} II>箱|Ιl5DҎA ?s/,N˃['BDTR5R޶rsάZ%MsDڔ'2IONt -E@ !I iQVr; z -f )ÅI,y,S/]F xvyR,NI/ 6uLQRv3Jk߃vŔbcW^lӹeI',ccMRSOL+rߦDeO\vSJuaQvx&AbآOMJmMPW~ b{N%ct؟c:#SJ&|x˜ew&9'֧`3LT)N%]d?՜1ĶRlK:=RŊ(%'әbcՓ V?KI.5f-%ӖQQ]OdX =i;Ji?v8B1b&Jy< 8 6, IBRvJ~sn͚כQc[Idda8-o~#۹[Pp$F)w pg:b8F2Dy(S;;`d %ӓ n)绡'MPHE8B:-aҥ+_nY2Ҝ1)6f=w]M]C;ctIiJ_8qTKm\OR!NhŃ%W<  Y !)'snaMcadiľ(HmqP-Ƒ'LPiΒc&)C|Η!|vcDт}v ՗AfA0΅M KR"址xё*$x|z5E)k6o ;ٙIiy|NI,A_28xcIS`+$6ct )'DRv+wʗ}>iwl}1Q(8Q%e[mF|21:dCRb ._bz)S|qRRn@2RRzy)< eGd\$w ;&SI&D|.˟.Fg7 ER./: IV\]=LR SʕRO} b\p"3TavZxX:L7 1"-A<ir˟?jHK&͍ٖQe:Ob8n8X|G n3Q}(-)ezCbE+ =)ГvXv2N8C`;{ 4Q#$ec`׮VI?cp5׵TSI1@!$e[eH )4tKonʄћSTӇy{xحtyR T2XV.16hA|ԐvTEs$_׮;Dֻs:X ۣ5dh|N|I@"bZr\atѳÔ2WfђV:sS|:=lғaHP=dnM",J,I& Dy$ĻDȕZlw0=ٜ5ꪡg']\O -?L'EoݪҤR1/v@k%"JsMd Bz23ГV.cK"8F@\n@B䟝?fH+V/XBSjHɄ%og~*̛cG7$eH;/B+K$7Q:qGۺi@pcz4䱇2uSZ[F}[.O##~ -(с >~:w4ۭ4VHv!#ȕu~TB:WC0yY6ƍޯ- a),EQ}A\{u3X,cO3:qP&p.ECcDF[8׵fQUdWre3<!M׫빑 0­?(2s(rTvh"Pz31œ]t^lBg7yP:?V'ХڵrJ^94d2W}LB- 9ĄhšI9Qjя=iڱ?6A`4h4װ(P.W2d19,8fXy>J *,-P 6Fg$;z< =i3q -/v >Nȓ\FxGjTK+\cUE"&˖ӧbr˅b>l7$ _;D=i:/86 lM#ʖ5dk(ן -'X2 =tcHʘU)/t5Г,G3Q'/?#ȺuRJi*r -208 Zvcq;P{ܖhHNH|Q.P{$5ۡ'X|,wQd(U4|yU!RN grztGo!̾>FQQ6{Lmh/B$zLyԨʓ[dzkȣ<8ŌXR2 Wws"!HQa)xlR6zrI+0`-ovm*f Y6DVCASXrq"KmмBdķonZ!&!)chSc& fuMNtBKU%o4+#?$\`Xf;:S &)D%GmNgߝ  oHX,c#\yGPUxn9/MV 9$& N/,uUD,Otx_)4 _4®6B虐^O=2mo15~"bTEfu+RΚ$e䨪ɽDv$娜*%UN@%'%d~HUTٞý"26UIbҬ3.D5΅ё#7jQJHh-ݤX| !#|*rζ\C'WOf²1:plPrTx-9!{ARFaqk $a9/mYU֥' NNq%r;zw]crߐ#9ɢR>͉zmG5|\cjHϮ|UE'8#89X2aM,с7$e(sMt @' te$"WKe\Bgblq1%„eGNc?BRF-Uq*?V8ZȈ@U9Sn ڦؚ굲M<"*"Yr{.E1K)tHIʶ\ߑM LR҉$%1Ffg = ")UiTYΫ"m_L/q]TzNң}gAeԟȊdҤ9Yj )xd4 @4̓OR:rlD~㎕Ԭ#kG˅_tU?nt<JH@|9ߔCX]7=- Hd$2@iUK%pr42 -Nkaޜ wtӑG{*"JH ڨ=7]!~Q|E4ל2 NԒ){S] vkaX -w|@D)ྔ EXa/n_ww+ %7EsMOʽ"k\b|v\jH{k+m7]Ba)(b;1 rJhtH NH^~g"t@gtͺ"P`L,s(Z"/"2FrEʗ*{HBJ9r@1)I&r=-T:r%Yulir2rp"4]q c ,}Ƅe[&tEbj)WߘڱX$UtǘFUdWܚ%{7"H]CǗJui Z90%31:"J~71I)$PH6Al >6d~DsMwܖ#7ڵrٲēCzrj5K/,ѹcB2Uߖk©֖RP){ nVSL ”2g=کMp`W*2Ө,[FUS+1@aNˈIGtې5>CD5$$xp4@|77UGk֐*>?oY0wt3op*ct%%8)jϹ_J%⺺7 !#-#QU4㐉7q(8zM";z4љD[ph;@R%Q>tXFg7fW]۲"@:;zb=.M]O )H;Q SCҏS,$崄 z -]Œ,h3*4g*uʕM?|22zrr3nXAXF|yrct )3zKVRd@4$%PFtvOґ+5ilMR)D#c)&/Ӡ'h 2D1:}CR{OBy0M)SVݬbF'QadNԒ%{7H+TEZ'+WCL:!wtP/74vo$f蜄I񊐑=T٘]+/JgCCZ#&J>LOt*kpGGd̛H!HR ~D*1Ka;DXٕ'7TY "mk &Ē \%}d'I)TÞ?<VRHhGQ;#9B@@m"`9Pa6hcK!#mXb;G)B|(bup%JRDkR,:;+{ -c"P@mH:!'|dol44Q_pŹll$%fb|&LWBpCEB(BcNRe)(r4{R-??DoR -6XHb -7>psߐZ_[w7pot F.+$%3a)Z‡س?Ii&aϦz&JGN -RDtl9-ǗJui B W;zڞ'rbN(}ߦe[XT+>%)'>Ctv $zr5+1 ,(jpGOΝn!쀤,t)le^3VV7(V d2LWS -oɦb=2eGȹo!;=ֿf|D܂sڱ8$%G|['ћc Uk0]DCXf(MjKڑ&j -q$AE)icl.KR[93v:(09=N(a a5=A1:!)}|7]z`E)uMГD Ǟuwǘ)/,*TbCVB%"J)jxY<5t=)AM4ހ;'xKX&;zoHJX*,$ֹ&QUt &1Ise„ew<ݜ-CRžݤBxX\{u٧k9Dvy+%'a٨]=ct>ޖᏐ'y(|ƬJ Lc@zZ X;6ğ;zW>hHPzF"'EʻQwJ (*09&"8i^ 3΄esXct 5GIxo4/K]$CRFvhQmMA䱥u64-CjaYFbaܷ[*,蜾 '9ww&]uu*W7'zmU,b%ctIDep6Pk$AR[hY?)I2§{ L#,&J>TON(ѳ@8 IY,QHJRnw7d'2%$%QEטyWjIHM,l^Cz3%hN63sBt!ʦtڹD]]ؚz01ɔ9 sGRpG\L sߐZʮM3I9YR 5 ]9Ĉ5͉^ 1 -#[=JI痔Es˧+m<ٍ7ǛTT,[O9OLaIK*ZEB(os4%evoaD٦Emf -BO -N/k<8V;H;-box;cM9\R,{Cs*)rȈgbEL'b' @PKþ~"Kj)rD>U8JNi|ϭt)095ټôL 8AXW4pVBC竍Z?a9XROWr TyPpr+D z KX\GSQk&)^ҁҘ+9@LyԮ~`~~{G,;ƴ -RZ:@ʗ2i3'Rܢl'1.My(MTRQD6,,{ - J&>(ٝ<7 LΗ|"&m@7cᱯ;:s.k)~@Jg:s)iϲO7Ę5(q*WPq;zG5o[ĉvZk6>sJ*dǗIuiN6fKx3X-̄nhct )$>/Z%L#J=% AX@} -djx0yM,^C -Z51zlxMYjO2I0 nP5[/nsT$qXEzLXR}(f&@w,Y1:2Ιvx2̚Fj8ft尽֛j @"S&tE'$,O bNPF)F`b Kl%QEB !J,ibdz'"NٱsGSsq/)ꑬzy $o7)lvSS4[*y e9Ah>\49I7>`T<8u-9SE6=0"-G\@yp|D(sL~.g.]{~ig$ϓ$CSR_OWZՇ.K♜GRk7>`\M,~sKjEҖ}&A~^9* H_r,M`}Ҧ\$]7/y$'%f%͘4+INNv%%%''=Bқ-}J~wKi7'}i%Iuz3<4̙$# 'q CLUY9kR`D =H%Sp2 f*Dږtˑ5ctH*m&)}TUF&#WuJ^)6oHsA%s$9=$ ]$=&' IWvPq6WVًRų<LZ$g=0f-ct!)n>sA{d|4#?uE M>?eJ:팤{=$q&ӣPRh"r:6Jݜ1I #J@}\,{stE6 D*"ЅBX&# Fhd#S9t{]uys2i3$x3gq -[@*m,!!)k2)ɮty`=V^9z==$qׯ-s5DSS3Uo4k)@ЯL$e[pR׼ԤY3I1HdV@"9CPʑE]XUũ9{Se] pb2pGL19'!) B O-k\7Od&iIO!]"ldh( ȱ5*T:Oygna9O`*Wa!ч @ pGRle`IJQ#%&KR9#)ifpr1tU)%uAc! tzmSpr$@M,CQ\ -:XYy<(aTΚYUTC>]aamoL0l=WΗC,R:]uɹNb" XjkmT8 -x;З׌<^g -3-%'+bI Ocz7/z s" 8 -eCeܙOCBRX7'(47+AARR4)D QN\+"&˖>ωlݣv]|J|SC2b2PUx TFt:mg큱96&UEp)4BdC^LԐ.yӒ>Ywf&g']$OOp%PevGLQFoZ1{u+eTN)Co1ŘcrCZRÆ{¾[Lff]XWf˭UO>;.N3P$tlF$%Rؒtl}{sj֡`0*#"_4%s8÷no\4* x=]X_xY}Gn}⮚g~~mՎ]*?Yew?7>sf9]_U&%ERLӞvE/^:\!rDј[?a=ӍO2H&= .+]Tϫ꿯 jqŁ';?|^;ށOSߛ;g3'Bqƙ7o^_R&Tm54G&t тg,eiC4jR7<12Ÿ&٥ϯz1}8tez7roEү`8?㓯3=yG8HwJ3f`cJ*WRR|ECZ E?E)[\PWr%SV?jWK*wod?B/{?yS72XA>I(o<90*_Г#LΣ"&Ry?S/\\m38B7* 3)udC1F*eϳҋ5}j[⻯EO䦙n2E&&?@r 8P  D4B^] - 9{JOkQtp[{otS9QKѥ%Wmnc|a1}~I?.R(h|ӈ1qM T⨢qzr*zRmQdg>It`wt&KP9 @ %1-_ܒ7k.1/tm?}'cU[D4oDޠL4Qc,3A!''-&h'ڱvk06XkrDQ㊢?Z" mћsn}_+ן-?nR}Tx$| ]#|QՓwrct͗+c9MW\O LF:1Ec`7gN\&J|cw/;2I.xgЎw^);"Ȥch*bX# [rE]Lg* 7/r?i%&FpbQþ;/hHF3ȾxV<?/n"J?uOWToPR[ɾݝ+^EI1"Q8vE#X"DI|S -I ѵnrKIIt$A2}f.1~ϐ}?V?jkJybU-7Js:#Ì*'''&]Ib$& J}.;krSPE е굱.^6Hwwk~iH)i&)%}CT<@Yzy8?o>WK*1gRF뵜GOF|%R޾ )S5nT*tEpF䣇:_Fݦ}7͞[_ti]E_T?lxŁH1!Ҽ=^Z|+hӞ;=9m4ʢդ>ܚ2(e:/1n;Ⱦ{:=/z*y9ϛ;>o~}ɕ^<@+_ίܵz^|#|!n&ΗC"a-uFFhz2fzR@LzrB -uyZ]ZM//_!!8 ĈӢcJ'u֞EF/L.\L~r)?[+Tݤb}B/:F3;n 'CII̙VI!S9ҨHy &Qw׮mD!nehiӾ{ 1fCڇoOQ6wŕ{-凶3h -F}7׊12`r'Gד<8iԍw(ADڊUֿAG+qnҐ^=>bɾ٥U3^ʭܽr#\i=E'btГɤ$mtkd˧k)V_pa>|֛bKypm <z3"ơQVTzaߝ5duQ+Tqi*e|kk;/IQ}y(Q@OXV9.,v^+>p79gr 'XÌÊO/<%=빠>K>Ú'Ỹ4%beR/]R{^)fϗkݣ8x#%@Oɘ{Nx}$#J1 T"hrNՒDHކ+cwB݅mی_YʗWzurh>nz #;_.@O(.Zў= y$%'Ip)b:FKz۴烈\@jXK(+؝5o˫vdSˁ'i[[~J&$ڥH#OIΗ"FEU'O{zROZs$΃LΗM[LWOi}ƾ1nEsL^./*C1>gڮWO#Y4MӘ1qԎiU@OaL+9s+9)>(p~Iɚ21aV@#zJG~[Ӫ^̭hzWihӾh)f ldX2U&&aC9MW\.{rcLOa5["w‹#$_)ؽbxh;|ڐ}wn@O -=I[gi)R8[X9w:qp 3)ΚC 6./O)F`̮o{yJE0c"eGFaD#W'[K[wGZ)>r t9,s -Np6fǴ᫓L gOd41 ܌uȏbƚ':̗ݛ+Њd߽@Q2}T"ӍΛ?"t#D#H4' IxWNWK+p"V^9441i/BE3D#9xg(+[tQݖjq?ɾ{cc|{XRF|žc_0Ip=$'IlgK%(&hrOx50ׂ:_w0WafzsΣΗҫ6PiT[jq?Vx6(dC+ /ef Id=ެ%mŤX\Rzr_~ -Zrp4d_mY84%Ⱦ1?NuWm?}W4e&+woaruK!ďߡ"aݦMtYO*\O -fk=]Q|fdΤ>}s)(b |O+%{7|g~}Eu>zgyWF釩Kŷ_,{û@Q1q}7G^h $A5hPϮdtnr|4]qTfJimDڟ)ica\wƜeǸڇoyU/fS$TAn}hӥEq/! 'x}_o;Nǯch~ؕѓ⭩MvS 9D Ilyy5${I -N -2S9(!@FNMe9+{/?տ޵탽{˴e$Ax_ftܱXʙݐACHt'2S:rR ;/Yϫkzj~NmWT{ٽ=;{>:=hO_{oʾ{ %Dѓq4Ez3x%$Q+pW!OdRF{zz -:CH_sT%[dzwogb_W>O=>ܷ/~. .{=Qՠy ->β9K>]>]񺵆tͷn٨tv55mww]L[ncb?:^`û?{}q{=ɐ,8~ %vwc9on{nƔ$:%6rH=(^ -tas۶mzJ*xo^O$/?9"mvfƙ,L|xj$w,t0y(`<Sl*=i7mR5Q[6+|p]t|o5|=*L^nLv]K=LJkKדX%% 1 H4-WDGgYuY>tCA% JI9s`CA%I pi&#'/1~ZzEwbWm| }iûۻ?}KCaf%#-Qu @"zm8A]2޼T 5bvS_Í3J|i|_.VPΠvCGk/nP*|P8Ӗo*BG+U$')_rGM_OC:FWH`/6jsfsa_>| ?_5 {^`J|^h@XRd?ط/$ՓB:2t͚ۖ%;WqM]QA2 -#@'EbFFt pAVnjAzxꞭb0Qy@OO"m}駟~}/*s5_rǍW7XM>A|˟^d ~E:{k/=c5ײԳ{ks&8,/owk,{gN6w } g+] _EU3Qkmg4]i}Cθ_5 =sGv=oW{ {ÃlRݢ8ό KOU8g<(>>'[w[Հ @8 PB -%@Bc܀GI@dl5wclq{l|3wSI73=!}m#:eN}129!1wY\tW]=>k/Lzfx{KGl!\GL 7sǥkbny~D˨OoDr7D-V+_~gRD 0-$nXsH= ,zY,7.6)'^3'ŬƊynn, XX[5IY.+G:՛usG?᧿O?r޿Lsy\W!_̇\VUz9顱&%DBVaUOQ𧼇Uxѕ% -cT FJbSi }E3s F+?(\4T2i!nxKXXm$ E|rMƭxt<0μvʼnOg̷x*Z0}1#5v+1g2hNfeх;<5s%;zQ'qkJ.i՞ -#Uրtuy9{gQΛrLo ]Q틼id4_`^{tB¸ λ庫ͯS{Rud?Y4].2ﱞZǚcxN&S1K2g&W0>9,)Fv^?=ܿrVΛY7p}ˀnqwyḎo%tyok~uO=O:&s0.xНtֱMJ9oP'i`pPaƚ[;XQxLx yʋ$kzYFjNK򘿗.La\>>ǒ?ǦnA⹛0D +,i9n?&X^Ͷl1q~fu$Z{0EcOw]%?uN 6]!//^QHKsSk]`.a3%fn:?C QwC-6,7#pU: Q^ŜܬH} ʊ8+gSObJ!]]Iޤ<  ABJzk8vL7pF˛wv/Ʈ^ȇC̴Df b{Ug`ʒ{|l[*'G|=C{@XZ`7m^N27r[J _{>0s~@-DZeq2fsֳ)9qZ9oEsIDu QŠT϶`Ğ=P߲?D}Ƈ[F̌G(Y6 -V')'sj'ɶx7툅ҕϋrMޤ) 'fߌHL^ov^]#•yEtnƲ}3V/n1O-4^n {f+I$hMVzrqf& dʼnl+ݾܖHoo<9us>j QG%*+cÒܕ~LqO8 +4u}oOٞJOb5fC$ēݏ >aމ"hC!wV,q?y6Z%!,[/+c:Z+|n*S* w%Hp7,7D|mHycճB}Ia3śXG;hH@풧r_*(߲e"v*&>=AKŞO=nj/1Y?j4 -azy SXa_{ɉ܍ ww=3'K@2EuEiAb)*k Hok^-.n.u,l/'ֱ-RqVs]9XQx+MЇ sȸޡLJ粃P^.U›w2jH -QeUGO[ +޾4'Y*2JgNA림w2\(^]}Ըm>9,,@uXM OfGfOod%8 -+--%%K.?3YX]^M rҍN˝ &Nn&B;/LG&| ?J}"E_M=sO_7hF̠=๙U҃>^D}ˠggeCԱZi},na} g_校ۼ^nWAX /wOjմkDCVoQ 5@sΖ8+s#yad}ț 2-~䧕*W͟(g;Nz#^RT d=[|Sq!X/VfUnҗU(~YKx'#[&H~̺7#sR|5$Hd uByXg,\?j2&wtI RMqWQBk`[jv/F|z;x{+!5E9/NYܱ %Oz5pm,_p/U*Y&YUJJ?a> lTr$ybV]*{~_xU99y'77z97f ,E'.7,9D}Ni6ݬ[ď>ډ['$TLUz14Mc&ri\Dt1zVi`:jt,gl謺tOY8gEt7\tҸ<Ւ r*lOࠓ"kb$܉۽RvLNvIFM]җ|<|L/+'kZ]'_Նgq]F|Km2z$kfGD.egQBY͍ qLDm}vsLJ5[Cniy6YIȝx8c'B*'GTT K#6|˕f0O{✏n,w t Zpvit]6 $"JmOIZsEԑEa$#Dq~Mo^orIFeZ/WI+#|Lv5V!:&U]'ݰ M7MY+Q^lR ʝ^䳻Jyk"dn()ĿLsQ v+r33K}~\ H_Ƭu^u wz$'ugƃǎg&.ǙMX%04/cq&zpAlZ{ߖY9Fz&8&ś{ }orPǛwrN`@zv*kC2Wf.?%m>nLaM7X̙ddQ޻ΤZxalMIM+kFq)Q _x|G7yY452(0kQ_&ŃC? {\?͗.ZdZ8{ -o,g}Z>"ҟv #+Jo|uk#4ڝc^X1Kŀ(B!̽tLt IkżQ8[EDy!eN-f3rd^{Rb mU/3xs y{jvՀH,X)[t!Aig/p#+į'ę?{^W{ -#1 }Už̲ _cq? m+2}/I 7&}d!}y(Oeb7~kJ -#XZJ_ZYȀeXQ5 t&={19]ܖFwrN&=p=p#Lw2Lsˑz.ʫ_|A_1XYV&?kՂYe܏}iU{wwĎTQ )If#@#oY?E4@cNVR_ƀXuUޫ-|K%./fk|>kt-akÒM3o^^D>nKM:0%i]y{7nkJ?1qNmbe%inpՀt<*V%keynqY~˘X8-]s\Q8{ka_*wK[K/qw璼ikf+qpp˄EI$V-}BL/~ /ɗkΫr볬 ]g!5]49+`lן q ځ,a_3/NJE\1/gXViN3W_[ Bo,7@R{@ziwj HgN=!b+VC("w]xy03s_Y%yl3GXNZ/8' p 2]9?y}EEEMqW\kOefd -(}/L.^_7@[QY+-e2n)j )u+!-y<32޺![6Ӫ,a>g>//xEnKSQi20/1d<p2P -pH=e".e{ )vn,9ŀM^WvR.f_ծod88]ynj,?ԗYV7pFQrrZrQbU k5S@ACfBLb[ɞGJkQZ#8>u_J >ܨ@f.m16] aw -?aQP2=`ܸ঵+ -NaSǩ_OGb<݋r¡tᢞ~qϮtf[t(03%`SVxQ ߯ Mm -n_(0=GjyOfeQ}ZƮ5[U'6d,1[8߽~SZGbߏ{rf@pz/7y U6@USM=, fW 1{^)?j1#On޸ܜM\Ra ]KlV ~!_\6=s8]H;_/jC9L=0cPIc6bVw.Nm:r٤Y+ubӯyȀ9\VG/kM*}3ImYUEvd͝v]qCx탯ʹjM|(Ο&SԶ5\ubBo߯NC{a֣F6yVAXW_&Iwp -a=`.o:{k-;S[LփE?N ؘqMs*g1rHVvD0P -Iد%Oŝjn1EWƮۍhex.Tql/#SدWM1~*[>ycrS"hm2w7Bc-vwpՉM -ɗ9{{jn1,w-,u,'!$3>vS}-ši<$uW6,VB_c7n1Z7_kĦoN5=ӬͩlTZaB~S*#MRnha?οuk5o}$EdljA#/[7 [ޯ7EԁM[ZGOBØnĭװh0ɹk5#P{մo/ -~"DC}M=>:ȓm-}goM2L~~׿WhVYCJ!vq[+݈4{7<$?rf^:5c=o,!.{kĢ34+Wgwq)n5$?WW תP>n9[YXtqoXV0‘~cn[!2bFjeL*rpoy 0l_{#C*Hfo+#7Qؕ4CH]^U\Lf7C+2IP~{N7 k|ߗ~΃|&6ݠSCFPn?W t0cU1gs H_^H=db)5 -`.NZ:!Ups60YMt׷&5U٧򙜯+ɰBmZ.j{6^hC!B^o3w[4dc P؃r.0q:;C:vcvqh.{u&M^}h đ.q<1g(KxHcpdGT3>Fe}2 Eп0G=Ƽ2{XMgŢ {¥l`cv+֋DпXt(- ^J}^BO N0kA,-6D )8𼇲P6Ʀޠ!]_U]nn [=0mZW8l]ƽ/0:Z+?AZuث? <4CP:T#< c{e:_U(ׇcXH88w?# -GۯA{H|%uD <DϾ?C+56}TkxūeY ]T>HK~rWkbApѳOv{mX7MҘIUcٿ}d"ݵhf'{?٪rQItC4v2b[- !mycNGLb(r/7$k 'R^іaONS~wY7*>_&Jߘ&lw;hfTJ{&1On"Hq2n7~:v?E=ĘBu(T[CI/^^xj'¢)!!^TnD5x:JuܯUWvZbHL=jĦSp잣耸4H]yHWMJ&Ʊ4b&3oS[wǦk:({C=$nr$ݏkovggXZd=||mM%7ZwhKۨh{>ু/jԃj_j |(!8/1?d h3iO=ۥ4%7 o7 [8zaCCDI-ހv'7Bo@3nKLF:/f A}/K 7 B0 r.:s6 6)3߳z1ն6= .G٤)KQh h{D$p) W[[{. n$ǢtLpT!L[ϳZrJ]ý}(Sf;S@_Aƹy( -rGqMYxnkVm\l1Y=JթJ?}߳Ak d++on3L]o[سh*irg.  D33VOI*;C&B$J{vE^Ξchao~Bv Ozyt9< ٽƁ?_OMl2W܅Yb2F"wۨcí+a@b}?ks6gjQo :,zӛ.a`D%m78r>,zaҦr#㻑цEo;뭣Ɓ¢o̔o^[ -J!4|d\ǿDnD LnGJn~%ǁ@Ta3JX|)`\rZSI7@nĭt>J1tF4i.}!7y/%" ĢF,Px%RɭTr#6@q@7,F*G(#܈[|JCJ*7"&{>b/Lf:VnS9p*8t~?FsT ph^nub-q$#P-i.%o6=|Ï?;vq@ezs-j &@e*3*?W SI_;:>I?NaဢX4Mo a5Ŀb"a/1v9ŦY Zh (Cw/e5_nx%P~{tz3%>X箄րX6܃M~\Klh Ebƕ9^-mR`g"? 1)caJԀF'۩wTo˂Ħ"Pvs/_Xawma՛Aztj, $MpB nȶ 59?3 }#hvUobi89Lo8\qw+W }azqI>CߎAo\qe:i+AT@[@7$~Fq0I|xM Ǧ{K7wϫ:b#(q_HaSYoxzK(@3j#1cc8 -h{S#-:W>ǡoJhi N>tzsC'1?ytVo=Q{<<@bSYohM.W&"~Ƴ J66=pz+.iq{dW{I>HaOxFW7P(E恞*P*(%Ej4fxBەőn4~8H!126\JwZrqڀzb*MMn4ǦX&ܶu*^J æz(M2uqx(MJM}%@Ml/Tn"};EC:Il޸=(b]0oRy;0 E׍RBn<(91(MR[{7S%01bHyj-c%-y^urb.kW XtyˍY m:6#ܘھ7$Pf/* " 4n)@},ˍKw"RTǦ^+D2({ܛ7&!84M:`ވܕ@p@a, eZL mAp@YlzMJz/Ԅm^7ټXZrt9 -m@p@IZyj֍twp$P y#\p(E7&yџ*j8P ?)0~LbBp@+ 2!@Z6DaoR`3!8!JXIqg iqSԷo~cOG}>Z n.@nl:zcA+,ƢQ;V$@f,\EP/}@&FEZ&2P_p9q7?Jo$}sz鍽킅Rbӕ4I1ɥ.꧛ވ9hv{mo I%}M;&NkV ~nH?ita.M' 8 Ro|O;ȅC] 8 6XxaOF7~w@,fv Lp ! -/]E  JZ;I ȃMؼsz{ HC{cb2~wz$T_H1 3Q^$¦i7 HECR!7 6-3t՛I84[jCE2{_Uoy d]fSwü`ꍽN`zʍeNp}8¦p=1I 6>HOfa_#: ¦7y 6-誥 ·3dX-rA7 6y7 6n Mrfx-62vl7 \N 5V!MA HM7'i3H p'tt@ fހtXtfW$7 }ӣLr0o@>*; $W1i#8\s zdSA-ʻ۵Go&9K$eǘ81q7 -6=rn& =ia endstream endobj 31 0 obj <>stream -:yǑ bm:wlGyqhq.]ڷBo2jڥ& wHCӵ[ؘ "&0rΗIDL"-c 0H ۣ}{wڱCG&/@b~}wԡ)8I 1LoԿw.c+!w!d= اgfbTכAfc  ԯgNb)7 56zAwazS$ ȍMf<%оcM2xZG  \G^iѴ\3q'tCYm ¼ٱ釣F Ч3p1 - 2I!$ش#&ewpO-cӣōE(S#c֑t)1(M_:Mg8HH6瞒8t@޽DD yȥG mLo{S{2Ґjz)#Л%&&I # Eo?u!Lo;+7܀ % v?84clRܐ@R< )NC?<7ݣkXLԖIi9Էww=wH'}6zSF r+(6pI )Lo'5&~`7QA)=v+IyvRFߗ7^%Lr0o@VN|to짆wM_Orh ?ӛ !@ ѿO̾:$c2ճs8I6 hE?WN cF8\opCSz(L{.q@,?Л7/a߳8TB1wr.7'zVo܅=zt$ԛAb8t|nBԖpP"-:0ɿpH?vۮ) o o@#l:Ror'o@'l몮۷pf#cN;ws!y:a;0gޘ}{7]Po& --AV]th/\bO:.b79}p'^8b< w{+ I KLrN"-Z?[TsCH7$whM_^Ɠ0o@32&UtæΏPor:qvtP 'O IJƃqL2d ~Oo܁-1+4plSF7CeLU Ja$SlAE ӐMo184h {No̻}c0]+܈!Lo7H7<Zc OV]tĦvh . b7%6Џȵ3I0BbVo&qeIzBo :qlp@ -C1x ZaІ3ygmXJi[Nٚ]eA; fmh+1w2 D56m.ReX6 wD7][x 辋 +C~N6mu0o[[p2vI8M L}%qhvN4ep'p]'8ij3HNajx -MLo`SgvEHE+ok!yr%N:6{-1[Ap&ycM_*i0oĢZN tC+:z,Z7iC2:pl?܂{9 -Iq:s7#o -Ya{^݂I#̂%Fj.æUp?<sW8K)c砞 zΟOpGDLib&& ;8Ǣ-IE`5ceLr`g2 tNX8{E8tnX8$8LpSq\JN/ }ax)e疗R%YRzHb@#`:2 "4ҔVvy'`h =~ogah=qG!^dh=pYp&9k8Ew\ҢA7EiL㈘H,:3H Xlv(i1XA=Xl%Y7_Ji_xyzl<$38OJ) "Rp4Zw7Wprax4A:-i4yH$,ǤY& q_b@Gfv1#B @`)9& p(hL2kc8t_ @qh|sSM< MƱ(-II1(Z0}QI-:}J7q_߽I&.܆ f&9& ̈́)qMT84JJ?}&d7 ͅ;ye-@2͆'ynT#J< %@ʏ~סQo -Wp^cL`hLp6qem_4hLu)JZT}!`'WAh9LF{o 7!cw8n-JxB {>!7&{'=_ 'Jo?$Ep#?$g킁 \0wC10pJ$ vDL6=p*L+8ƒE8!:4o7M\lApNk8tσC @顳:C~ao6xݧ7X)ܹK$YK3{9s9νz7 2O!@|JOpp;Q tEH98'ɷz@DrpOP{ 8'l P37Y{C |'_ҽs9&PVS㭹QJŗh@@x N0Ǜ$ߤ#@x e4 %'Wȧ.ڽ+-wr08P""wzxdXW!ހ -}MbFF|7vn%P<ܨ]98sp0߆Ș܍1y2ǧ$ಾES\_7ްzw7$#龻7@;c7 =w@_p3N dpGS}N$t+P{ Ԇ~Ñ/Tpaf4 g;wQ=yYP.X!c`@y鳨=tdgn]9ҙ~ΘAAn#9vp޼37S](JR _~>#P,tg%'i{K="8/SNͥdU Fp_{%̏yȏ lⴓC.(YPQ,|9w`b@m16t=9J|ppΐ_"I93i)?2Iڷ/b9@ëXto ciHGpq d+:.5 l PwU|y;r"Vsz1@gK57YGpv=ɡ%pnl4L|q$J|L'Jг78${g&z '!~x7N~f=]bsLO4(үΔ›Q)\ddo0?rf - ct,+@pf$yʀ/_9bGf|X -_l}8rr'_j-AUoif~tn=R],%_q"p)v՛nYN+y`#3L)CMئB&I;#”  r) 3 ZGސ44 GHANOprFYɠtp%m>hʄ”~'0DoN927KCMR `bt_V\D鷱=d+lҲ{6,Øu9/|/Ks<o1btft2 ȣɜPv}Ҳ{n]JIs奙 #T S})f{c#g_ B6LD鑙ES'@Q)[p]j-S_6C #C^65d89\*s յqnnUr@ɬ;[- :c0ӚcuDpΠn w=sp_`+ du{ 01rlB7VڟBC:dX -?gOBP涋mL=C) -~КVQGKf ytETmTdE8Y?^MՓ$8qJ\02&VQG_<26OGݴoX &9&Lz<9p(dl^K/r3"]0^2܋|% ?~ЛN-"ŖΟc#Ǣ#KS~69>QtG` oca+C o޾5olah/pnix %*O A^+cenGziYJS/ܨ _;8w}=M$>+gWTQ,7ҀHn d]޷ye< Rnp7S -G&ǀx47L:0{[ixgyrlKs4?yx9r4.|)6;08y it ei7 ba(s;SOms{sCE{]-.irms8ITx#L>7/ lHR졥±[y77`p+y3뎟릜%]!⽽^kahfտ>FYNp`koqɽ}'p(O\xMM2H;8fף/<XBB>L~ (fǬ=(x dl /"+D- s(Jf_3IbLl[i;8d馽P CdZ<}l7q}DX1R[i=pZeE\>5g&H0ZNB*AfcJ /#A S -WE>!pr@͍˿r׮1H^ҋ/P6dd2T\ 6#ܥ*#FƏI]ç/঍?/0iml3R -Mڐr#rM7AԱc}m߸᧫V2(&C@S -_Zv׿Xnt8<]>xG~~݆9_u!ԍd~Z@.]Zaz1M †8/xs3ݵ 1qK]17|s|@X -G#_o/-m5YQgM ңKf}]\{Ւ>X" Lƈ0y0psDB9z|(1/ bJS_JCqZ3V6 /+ՆgF/-)6*t5fMsRs"Kc(N4S_>U$P&)+mE GceQk\~']6Tgmm6Լ^Wc5g*cbE Ćx:)>n%ӶZפmЬ7g袲YzGsW`.#-ɘ7+Fkmj}|֛#W?k¦FCAQ]rCF˺ڼx}A`lN6}[Mb[fLQe5E&;Őܬ.]ї9یMٵm%re|v`-Ԣj-2R樾 -C2l27kʲB]_zuEkFV\L6ڠQw>j0#ݽ˴5EZr%}n\bj{RuD93rx G^r0&ϤꯏfΡ@ݗѮh rfENMݘST4 -mIT:VQ -}Vowö^5ݍ{cs"fjZL}wQc02c;ӝ>Rl[)qW6z]P"~`s+[EbZBDQnSMC(ΑXߝ:؝[d2csbm֖ ߥ,mj0QoӒX3@ggT*+ kkʛu9jg8^ng2R' )UUҟѕnHoo.p4$GMΠFh2s\^m1e^#<tNZ`TimYPG>t ~TP:-Jԓ+\e6ZU*SSuԷv)6Z,EI}v^ұ/МߝC40w#d*]Ѣn+ ڢWeO16UeFVk]Uݥ+Փf!r؃&ܙs͚5GPF[SRBY.ĤkY-Ime)U&92ͨn.6W-3[Zcu ZmkIF[\e'yP'+RB兆h&+ٸ^Zҥ5e)v}JSTȭv9QNhlNXƩ]ָ3Ra6%SJ][i[M9efBJwŕJ -"'9h,dRLQZJӔjcIAǪ|٘֘ڣcIi)-2IUV:*q[< = -p*EwmlعU{O쫀mش*0L1YU_{̻˳/T Vɭ -xTs4> -LL*\q3Q5 J,(I endstream endobj 15 0 obj [/ICCBased 19 0 R] endobj 6 0 obj [5 0 R] endobj 32 0 obj <> endobj xref 0 33 0000000000 65535 f -0000000016 00000 n -0000000144 00000 n -0000047649 00000 n -0000000000 00000 f -0000163121 00000 n -0000593503 00000 n -0000047700 00000 n -0000048109 00000 n -0000048283 00000 n -0000163420 00000 n -0000139682 00000 n -0000163307 00000 n -0000049181 00000 n -0000048344 00000 n -0000593468 00000 n -0000048620 00000 n -0000048668 00000 n -0000139717 00000 n -0000160473 00000 n -0000163191 00000 n -0000163222 00000 n -0000163494 00000 n -0000163800 00000 n -0000165099 00000 n -0000187851 00000 n -0000253439 00000 n -0000319027 00000 n -0000384615 00000 n -0000450203 00000 n -0000515791 00000 n -0000581379 00000 n -0000593526 00000 n -trailer <]>> startxref 593722 %%EOF \ No newline at end of file diff --git a/ui/design/chromeStorePics/promo1400560.png b/ui/design/chromeStorePics/promo1400560.png deleted file mode 100644 index d3637ecc8..000000000 Binary files a/ui/design/chromeStorePics/promo1400560.png and /dev/null differ diff --git a/ui/design/chromeStorePics/promo440280.png b/ui/design/chromeStorePics/promo440280.png deleted file mode 100644 index c1f92b1c0..000000000 Binary files a/ui/design/chromeStorePics/promo440280.png and /dev/null differ diff --git a/ui/design/chromeStorePics/promo920680.png b/ui/design/chromeStorePics/promo920680.png deleted file mode 100644 index 726bd810a..000000000 Binary files a/ui/design/chromeStorePics/promo920680.png and /dev/null differ diff --git a/ui/design/chromeStorePics/screen_dao_accounts.png b/ui/design/chromeStorePics/screen_dao_accounts.png deleted file mode 100644 index 1a2e8052c..000000000 Binary files a/ui/design/chromeStorePics/screen_dao_accounts.png and /dev/null differ diff --git a/ui/design/chromeStorePics/screen_dao_locked.png b/ui/design/chromeStorePics/screen_dao_locked.png deleted file mode 100644 index 6592c17e4..000000000 Binary files a/ui/design/chromeStorePics/screen_dao_locked.png and /dev/null differ diff --git a/ui/design/chromeStorePics/screen_dao_notification.png b/ui/design/chromeStorePics/screen_dao_notification.png deleted file mode 100644 index baeb2ec39..000000000 Binary files a/ui/design/chromeStorePics/screen_dao_notification.png and /dev/null differ diff --git a/ui/design/chromeStorePics/screen_wei_account.png b/ui/design/chromeStorePics/screen_wei_account.png deleted file mode 100644 index 23301e4bf..000000000 Binary files a/ui/design/chromeStorePics/screen_wei_account.png and /dev/null differ diff --git a/ui/design/chromeStorePics/screen_wei_notification.png b/ui/design/chromeStorePics/screen_wei_notification.png deleted file mode 100644 index 7a763e5df..000000000 Binary files a/ui/design/chromeStorePics/screen_wei_notification.png and /dev/null differ diff --git a/ui/design/metamask-logo-eyes.png b/ui/design/metamask-logo-eyes.png deleted file mode 100644 index c29331b28..000000000 Binary files a/ui/design/metamask-logo-eyes.png and /dev/null differ diff --git a/ui/design/wireframes/1st_time_use.png b/ui/design/wireframes/1st_time_use.png deleted file mode 100644 index c18ced5e2..000000000 Binary files a/ui/design/wireframes/1st_time_use.png and /dev/null differ diff --git a/ui/design/wireframes/metamask_wfs_jan_13.pdf b/ui/design/wireframes/metamask_wfs_jan_13.pdf deleted file mode 100644 index c77c9274a..000000000 Binary files a/ui/design/wireframes/metamask_wfs_jan_13.pdf and /dev/null differ diff --git a/ui/design/wireframes/metamask_wfs_jan_13.png b/ui/design/wireframes/metamask_wfs_jan_13.png deleted file mode 100644 index d71d7bdb4..000000000 Binary files a/ui/design/wireframes/metamask_wfs_jan_13.png and /dev/null differ diff --git a/ui/design/wireframes/metamask_wfs_jan_18.pdf b/ui/design/wireframes/metamask_wfs_jan_18.pdf deleted file mode 100644 index 592ba8532..000000000 Binary files a/ui/design/wireframes/metamask_wfs_jan_18.pdf and /dev/null differ diff --git a/ui/example.js b/ui/example.js deleted file mode 100644 index 4627c0e9c..000000000 --- a/ui/example.js +++ /dev/null @@ -1,123 +0,0 @@ -const injectCss = require('inject-css') -const MetaMaskUi = require('./index.js') -const MetaMaskUiCss = require('./css.js') -const EventEmitter = require('events').EventEmitter - -// account management - -var identities = { - '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111': { - name: 'Walrus', - img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', - address: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', - balance: 220, - txCount: 4, - }, - '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222': { - name: 'Tardus', - img: 'QmQYaRdrf2EhRhJWaHnts8Meu1mZiXrNib5W1P6cYmXWRL', - address: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222', - balance: 10.005, - txCount: 16, - }, - '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333': { - name: 'Gambler', - img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', - address: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333', - balance: 0.000001, - txCount: 1, - }, -} - -var unapprovedTxs = {} -addUnconfTx({ - from: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222', - to: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', - value: '0x123', -}) -addUnconfTx({ - from: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', - to: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333', - value: '0x0000', - data: '0x000462427bcc9133bb46e88bcbe39cd7ef0e7000', -}) - -function addUnconfTx (txParams) { - var time = (new Date()).getTime() - var id = createRandomId() - unapprovedTxs[id] = { - id: id, - txParams: txParams, - time: time, - } -} - -var isUnlocked = false -var selectedAccount = null - -function getState () { - return { - isUnlocked: isUnlocked, - identities: isUnlocked ? identities : {}, - unapprovedTxs: isUnlocked ? unapprovedTxs : {}, - selectedAccount: selectedAccount, - } -} - -var accountManager = new EventEmitter() - -accountManager.getState = function (cb) { - cb(null, getState()) -} - -accountManager.setLocked = function () { - isUnlocked = false - this._didUpdate() -} - -accountManager.submitPassword = function (password, cb) { - if (password === 'test') { - isUnlocked = true - cb(null, getState()) - this._didUpdate() - } else { - cb(new Error('Bad password -- try "test"')) - } -} - -accountManager.setSelectedAccount = function (address, cb) { - selectedAccount = address - cb(null, getState()) - this._didUpdate() -} - -accountManager.signTransaction = function (txParams, cb) { - alert('signing tx....') -} - -accountManager._didUpdate = function () { - this.emit('update', getState()) -} - -// start app - -var container = document.getElementById('app-content') - -var css = MetaMaskUiCss() -injectCss(css) - -MetaMaskUi({ - container: container, - accountManager: accountManager, -}) - -// util - -function createRandomId () { - // 13 time digits - var datePart = new Date().getTime() * Math.pow(10, 3) - // 3 random digits - var extraPart = Math.floor(Math.random() * Math.pow(10, 3)) - // 16 digits - return datePart + extraPart -} diff --git a/ui/index.html b/ui/index.html deleted file mode 100644 index 9dfaefbb3..000000000 --- a/ui/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - MetaMask - - - - -

- - - - -
- -
- - - diff --git a/ui/index.js b/ui/index.js deleted file mode 100644 index a729138d3..000000000 --- a/ui/index.js +++ /dev/null @@ -1,58 +0,0 @@ -const render = require('react-dom').render -const h = require('react-hyperscript') -const Root = require('./app/root') -const actions = require('./app/actions') -const configureStore = require('./app/store') -const txHelper = require('./lib/tx-helper') -global.log = require('loglevel') - -module.exports = launchMetamaskUi - - -log.setLevel(global.METAMASK_DEBUG ? 'debug' : 'warn') - -function launchMetamaskUi (opts, cb) { - var accountManager = opts.accountManager - actions._setBackgroundConnection(accountManager) - // check if we are unlocked first - accountManager.getState(function (err, metamaskState) { - if (err) return cb(err) - const store = startApp(metamaskState, accountManager, opts) - cb(null, store) - }) -} - -function startApp (metamaskState, accountManager, opts) { - // parse opts - const store = configureStore({ - - // metamaskState represents the cross-tab state - metamask: metamaskState, - - // appState represents the current tab's popup state - appState: {}, - - // Which blockchain we are using: - networkVersion: opts.networkVersion, - }) - - // if unconfirmed txs, start on txConf page - const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.network) - if (unapprovedTxsAll.length > 0) { - store.dispatch(actions.showConfTxPage()) - } - - accountManager.on('update', function (metamaskState) { - store.dispatch(actions.updateMetamaskState(metamaskState)) - }) - - // start app - render( - h(Root, { - // inject initial state - store: store, - } - ), opts.container) - - return store -} diff --git a/ui/lib/account-link.js b/ui/lib/account-link.js deleted file mode 100644 index d061d0ad1..000000000 --- a/ui/lib/account-link.js +++ /dev/null @@ -1,26 +0,0 @@ -module.exports = function (address, network) { - const net = parseInt(network) - let link - switch (net) { - case 1: // main net - link = `http://etherscan.io/address/${address}` - break - case 2: // morden test net - link = `http://morden.etherscan.io/address/${address}` - break - case 3: // ropsten test net - link = `http://ropsten.etherscan.io/address/${address}` - break - case 4: // rinkeby test net - link = `http://rinkeby.etherscan.io/address/${address}` - break - case 42: // kovan test net - link = `http://kovan.etherscan.io/address/${address}` - break - default: - link = '' - break - } - - return link -} diff --git a/ui/lib/contract-namer.js b/ui/lib/contract-namer.js deleted file mode 100644 index f05e770cc..000000000 --- a/ui/lib/contract-namer.js +++ /dev/null @@ -1,33 +0,0 @@ -/* CONTRACT NAMER - * - * Takes an address, - * Returns a nicname if we have one stored, - * otherwise returns null. - */ - -const contractMap = require('eth-contract-metadata') -const ethUtil = require('ethereumjs-util') - -module.exports = function (addr, identities = {}) { - const checksummed = ethUtil.toChecksumAddress(addr) - if (contractMap[checksummed] && contractMap[checksummed].name) { - return contractMap[checksummed].name - } - - const address = addr.toLowerCase() - const ids = hashFromIdentities(identities) - return addrFromHash(address, ids) -} - -function hashFromIdentities (identities) { - const result = {} - for (const key in identities) { - result[key] = identities[key].name - } - return result -} - -function addrFromHash (addr, hash) { - const address = addr.toLowerCase() - return hash[address] || null -} diff --git a/ui/lib/etherscan-prefix-for-network.js b/ui/lib/etherscan-prefix-for-network.js deleted file mode 100644 index 2c1904f1c..000000000 --- a/ui/lib/etherscan-prefix-for-network.js +++ /dev/null @@ -1,21 +0,0 @@ -module.exports = function (network) { - const net = parseInt(network) - let prefix - switch (net) { - case 1: // main net - prefix = '' - break - case 3: // ropsten test net - prefix = 'ropsten.' - break - case 4: // rinkeby test net - prefix = 'rinkeby.' - break - case 42: // kovan test net - prefix = 'kovan.' - break - default: - prefix = '' - } - return prefix -} diff --git a/ui/lib/explorer-link.js b/ui/lib/explorer-link.js deleted file mode 100644 index 3b82ecd5f..000000000 --- a/ui/lib/explorer-link.js +++ /dev/null @@ -1,6 +0,0 @@ -const prefixForNetwork = require('./etherscan-prefix-for-network') - -module.exports = function (hash, network) { - const prefix = prefixForNetwork(network) - return `http://${prefix}etherscan.io/tx/${hash}` -} diff --git a/ui/lib/icon-factory.js b/ui/lib/icon-factory.js deleted file mode 100644 index 27a74de66..000000000 --- a/ui/lib/icon-factory.js +++ /dev/null @@ -1,65 +0,0 @@ -var iconFactory -const isValidAddress = require('ethereumjs-util').isValidAddress -const toChecksumAddress = require('ethereumjs-util').toChecksumAddress -const contractMap = require('eth-contract-metadata') - -module.exports = function (jazzicon) { - if (!iconFactory) { - iconFactory = new IconFactory(jazzicon) - } - return iconFactory -} - -function IconFactory (jazzicon) { - this.jazzicon = jazzicon - this.cache = {} -} - -IconFactory.prototype.iconForAddress = function (address, diameter) { - const addr = toChecksumAddress(address) - if (iconExistsFor(addr)) { - return imageElFor(addr) - } - - return this.generateIdenticonSvg(address, diameter) -} - -// returns svg dom element -IconFactory.prototype.generateIdenticonSvg = function (address, diameter) { - var cacheId = `${address}:${diameter}` - // check cache, lazily generate and populate cache - var identicon = this.cache[cacheId] || (this.cache[cacheId] = this.generateNewIdenticon(address, diameter)) - // create a clean copy so you can modify it - var cleanCopy = identicon.cloneNode(true) - return cleanCopy -} - -// creates a new identicon -IconFactory.prototype.generateNewIdenticon = function (address, diameter) { - var numericRepresentation = jsNumberForAddress(address) - var identicon = this.jazzicon(diameter, numericRepresentation) - return identicon -} - -// util - -function iconExistsFor (address) { - return contractMap[address] && isValidAddress(address) && contractMap[address].logo -} - -function imageElFor (address) { - const contract = contractMap[address] - const fileName = contract.logo - const path = `images/contract/${fileName}` - const img = document.createElement('img') - img.src = path - img.style.width = '75%' - return img -} - -function jsNumberForAddress (address) { - var addr = address.slice(2, 10) - var seed = parseInt(addr, 16) - return seed -} - diff --git a/ui/lib/lost-accounts-notice.js b/ui/lib/lost-accounts-notice.js deleted file mode 100644 index 948b13db6..000000000 --- a/ui/lib/lost-accounts-notice.js +++ /dev/null @@ -1,23 +0,0 @@ -const summary = require('../app/util').addressSummary - -module.exports = function (lostAccounts) { - return { - date: new Date().toDateString(), - title: 'Account Problem Caught', - body: `MetaMask has fixed a bug where some accounts were previously mis-generated. This was a rare issue, but you were affected! - -We have successfully imported the accounts that were mis-generated, but they will no longer be recovered with your normal seed phrase. - -We have marked the affected accounts as "Loose", and recommend you transfer ether and tokens away from those accounts, or export & back them up elsewhere. - -Your affected accounts are: -${lostAccounts.map(acct => ` - ${summary(acct)}`).join('\n')} - -These accounts have been marked as "Loose" so they will be easy to recognize in the account list. - -For more information, please read [our blog post.][1] - -[1]: https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd#.7d8ktj4h3 - `, - } -} diff --git a/ui/lib/persistent-form.js b/ui/lib/persistent-form.js deleted file mode 100644 index d4dc20b03..000000000 --- a/ui/lib/persistent-form.js +++ /dev/null @@ -1,61 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const defaultKey = 'persistent-form-default' -const eventName = 'keyup' - -module.exports = PersistentForm - -function PersistentForm () { - Component.call(this) -} - -inherits(PersistentForm, Component) - -PersistentForm.prototype.componentDidMount = function () { - const fields = document.querySelectorAll('[data-persistent-formid]') - const store = this.getPersistentStore() - - for (var i = 0; i < fields.length; i++) { - const field = fields[i] - const key = field.getAttribute('data-persistent-formid') - const cached = store[key] - if (cached !== undefined) { - field.value = cached - } - - field.addEventListener(eventName, this.persistentFieldDidUpdate.bind(this)) - } -} - -PersistentForm.prototype.getPersistentStore = function () { - let store = window.localStorage[this.persistentFormParentId || defaultKey] - if (store && store !== 'null') { - store = JSON.parse(store) - } else { - store = {} - } - return store -} - -PersistentForm.prototype.setPersistentStore = function (newStore) { - window.localStorage[this.persistentFormParentId || defaultKey] = JSON.stringify(newStore) -} - -PersistentForm.prototype.persistentFieldDidUpdate = function (event) { - const field = event.target - const store = this.getPersistentStore() - const key = field.getAttribute('data-persistent-formid') - const val = field.value - store[key] = val - this.setPersistentStore(store) -} - -PersistentForm.prototype.componentWillUnmount = function () { - const fields = document.querySelectorAll('[data-persistent-formid]') - for (var i = 0; i < fields.length; i++) { - const field = fields[i] - field.removeEventListener(eventName, this.persistentFieldDidUpdate.bind(this)) - } - this.setPersistentStore({}) -} - diff --git a/ui/lib/tx-helper.js b/ui/lib/tx-helper.js deleted file mode 100644 index ec19daf64..000000000 --- a/ui/lib/tx-helper.js +++ /dev/null @@ -1,17 +0,0 @@ -const valuesFor = require('../app/util').valuesFor - -module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, network) { - log.debug('tx-helper called with params:') - log.debug({ unapprovedTxs, unapprovedMsgs, personalMsgs, network }) - - const txValues = network ? valuesFor(unapprovedTxs).filter(txMeta => txMeta.metamaskNetworkId === network) : valuesFor(unapprovedTxs) - log.debug(`tx helper found ${txValues.length} unapproved txs`) - const msgValues = valuesFor(unapprovedMsgs) - log.debug(`tx helper found ${msgValues.length} unsigned messages`) - let allValues = txValues.concat(msgValues) - const personalValues = valuesFor(personalMsgs) - log.debug(`tx helper found ${personalValues.length} unsigned personal messages`) - allValues = allValues.concat(personalValues) - - return allValues.sort(txMeta => txMeta.time) -} -- cgit v1.2.3 From e285f2cae958437160f86171f1fccfec66799883 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 3 Jul 2017 16:09:17 -0700 Subject: Get duplicate UI template working --- app/scripts/popup.js | 2 +- app/scripts/responsive.js | 7 +- gulpfile.js | 1 + ui/classic/app/actions.js | 2 +- ui/classic/app/components/pending-tx.js | 2 +- ui/classic/app/conf-tx.js | 2 +- ui/classic/css.js | 4 +- ui/responsive/.gitignore | 66 + ui/responsive/app/account-detail.js | 311 +++ ui/responsive/app/accounts/account-list-item.js | 91 + ui/responsive/app/accounts/import/index.js | 100 + ui/responsive/app/accounts/import/json.js | 100 + ui/responsive/app/accounts/import/private-key.js | 67 + ui/responsive/app/accounts/import/seed.js | 30 + ui/responsive/app/accounts/index.js | 164 ++ ui/responsive/app/actions.js | 1031 +++++++++ ui/responsive/app/add-token.js | 219 ++ ui/responsive/app/app.js | 591 +++++ ui/responsive/app/components/account-export.js | 122 + ui/responsive/app/components/account-info-link.js | 41 + ui/responsive/app/components/account-panel.js | 86 + ui/responsive/app/components/balance.js | 89 + ui/responsive/app/components/binary-renderer.js | 46 + .../app/components/bn-as-decimal-input.js | 174 ++ ui/responsive/app/components/buy-button-subview.js | 197 ++ ui/responsive/app/components/coinbase-form.js | 63 + ui/responsive/app/components/copyButton.js | 59 + ui/responsive/app/components/copyable.js | 46 + ui/responsive/app/components/custom-radio-list.js | 60 + ui/responsive/app/components/drop-menu-item.js | 59 + ui/responsive/app/components/editable-label.js | 51 + ui/responsive/app/components/ens-input.js | 170 ++ ui/responsive/app/components/eth-balance.js | 89 + ui/responsive/app/components/fiat-value.js | 63 + .../app/components/hex-as-decimal-input.js | 154 ++ ui/responsive/app/components/identicon.js | 72 + ui/responsive/app/components/loading.js | 53 + ui/responsive/app/components/mascot.js | 59 + ui/responsive/app/components/mini-account-panel.js | 74 + ui/responsive/app/components/network.js | 125 + ui/responsive/app/components/notice.js | 126 ++ .../app/components/pending-msg-details.js | 50 + ui/responsive/app/components/pending-msg.js | 56 + .../app/components/pending-personal-msg-details.js | 60 + .../app/components/pending-personal-msg.js | 47 + ui/responsive/app/components/pending-tx.js | 480 ++++ ui/responsive/app/components/qr-code.js | 79 + ui/responsive/app/components/range-slider.js | 58 + ui/responsive/app/components/shapeshift-form.js | 306 +++ ui/responsive/app/components/shift-list-item.js | 204 ++ ui/responsive/app/components/tab-bar.js | 36 + ui/responsive/app/components/template.js | 18 + ui/responsive/app/components/token-cell.js | 72 + ui/responsive/app/components/token-list.js | 194 ++ ui/responsive/app/components/tooltip.js | 22 + .../app/components/transaction-list-item-icon.js | 68 + .../app/components/transaction-list-item.js | 165 ++ ui/responsive/app/components/transaction-list.js | 79 + ui/responsive/app/conf-tx.js | 213 ++ ui/responsive/app/config.js | 211 ++ ui/responsive/app/conversion.json | 207 ++ ui/responsive/app/css/debug.css | 21 + ui/responsive/app/css/fonts.css | 36 + ui/responsive/app/css/index.css | 667 ++++++ ui/responsive/app/css/lib.css | 268 +++ ui/responsive/app/css/reset.css | 48 + ui/responsive/app/css/transitions.css | 42 + ui/responsive/app/first-time/init-menu.js | 179 ++ ui/responsive/app/img/identicon-tardigrade.png | Bin 0 -> 141119 bytes ui/responsive/app/img/identicon-walrus.png | Bin 0 -> 388973 bytes ui/responsive/app/info.js | 154 ++ .../app/keychains/hd/create-vault-complete.js | 78 + .../app/keychains/hd/recover-seed/confirmation.js | 118 + ui/responsive/app/keychains/hd/restore-vault.js | 152 ++ ui/responsive/app/new-keychain.js | 29 + ui/responsive/app/reducers.js | 52 + ui/responsive/app/reducers/app.js | 585 +++++ ui/responsive/app/reducers/identities.js | 15 + ui/responsive/app/reducers/metamask.js | 137 ++ ui/responsive/app/root.js | 22 + ui/responsive/app/send.js | 288 +++ ui/responsive/app/settings.js | 59 + ui/responsive/app/store.js | 21 + ui/responsive/app/template.js | 30 + ui/responsive/app/unlock.js | 118 + ui/responsive/app/util.js | 217 ++ ui/responsive/css.js | 29 + ui/responsive/design/00-metamask-SignIn.jpg | Bin 0 -> 57848 bytes ui/responsive/design/01-metamask-SelectAcc.jpg | Bin 0 -> 76063 bytes ui/responsive/design/02-metamask-AccDetails.jpg | Bin 0 -> 75780 bytes .../design/02a-metamask-AccDetails-OverToken.jpg | Bin 0 -> 121847 bytes .../02a-metamask-AccDetails-OverTransaction.jpg | Bin 0 -> 122075 bytes ui/responsive/design/02a-metamask-AccDetails.jpg | Bin 0 -> 117570 bytes .../design/02b-metamask-AccDetails-Send.jpg | Bin 0 -> 110143 bytes ui/responsive/design/03-metamask-Qr.jpg | Bin 0 -> 66052 bytes ui/responsive/design/05-metamask-Menu.jpg | Bin 0 -> 130264 bytes .../chromeStorePics/final_screen_dao_accounts.png | Bin 0 -> 249708 bytes .../chromeStorePics/final_screen_dao_locked.png | Bin 0 -> 220295 bytes .../final_screen_dao_notification.png | Bin 0 -> 214405 bytes .../chromeStorePics/final_screen_wei_account.png | Bin 0 -> 253382 bytes .../final_screen_wei_notification.png | Bin 0 -> 193865 bytes ui/responsive/design/chromeStorePics/icon-128.png | Bin 0 -> 5770 bytes ui/responsive/design/chromeStorePics/icon-64.png | Bin 0 -> 3573 bytes .../design/chromeStorePics/metamask_icon.ai | 2383 ++++++++++++++++++++ .../design/chromeStorePics/promo1400560.png | Bin 0 -> 261644 bytes .../design/chromeStorePics/promo440280.png | Bin 0 -> 57471 bytes .../design/chromeStorePics/promo920680.png | Bin 0 -> 206713 bytes .../design/chromeStorePics/screen_dao_accounts.png | Bin 0 -> 517598 bytes .../design/chromeStorePics/screen_dao_locked.png | Bin 0 -> 287108 bytes .../chromeStorePics/screen_dao_notification.png | Bin 0 -> 296498 bytes .../design/chromeStorePics/screen_wei_account.png | Bin 0 -> 653633 bytes .../chromeStorePics/screen_wei_notification.png | Bin 0 -> 402486 bytes ui/responsive/design/metamask-logo-eyes.png | Bin 0 -> 146076 bytes ui/responsive/design/wireframes/1st_time_use.png | Bin 0 -> 937556 bytes .../design/wireframes/metamask_wfs_jan_13.pdf | Bin 0 -> 452413 bytes .../design/wireframes/metamask_wfs_jan_13.png | Bin 0 -> 419066 bytes .../design/wireframes/metamask_wfs_jan_18.pdf | Bin 0 -> 612778 bytes ui/responsive/example.js | 123 + ui/responsive/index.html | 20 + ui/responsive/index.js | 58 + ui/responsive/lib/account-link.js | 26 + ui/responsive/lib/contract-namer.js | 33 + ui/responsive/lib/etherscan-prefix-for-network.js | 21 + ui/responsive/lib/explorer-link.js | 6 + ui/responsive/lib/icon-factory.js | 65 + ui/responsive/lib/lost-accounts-notice.js | 23 + ui/responsive/lib/persistent-form.js | 61 + ui/responsive/lib/tx-helper.js | 17 + 128 files changed, 13683 insertions(+), 11 deletions(-) create mode 100644 ui/responsive/.gitignore create mode 100644 ui/responsive/app/account-detail.js create mode 100644 ui/responsive/app/accounts/account-list-item.js create mode 100644 ui/responsive/app/accounts/import/index.js create mode 100644 ui/responsive/app/accounts/import/json.js create mode 100644 ui/responsive/app/accounts/import/private-key.js create mode 100644 ui/responsive/app/accounts/import/seed.js create mode 100644 ui/responsive/app/accounts/index.js create mode 100644 ui/responsive/app/actions.js create mode 100644 ui/responsive/app/add-token.js create mode 100644 ui/responsive/app/app.js create mode 100644 ui/responsive/app/components/account-export.js create mode 100644 ui/responsive/app/components/account-info-link.js create mode 100644 ui/responsive/app/components/account-panel.js create mode 100644 ui/responsive/app/components/balance.js create mode 100644 ui/responsive/app/components/binary-renderer.js create mode 100644 ui/responsive/app/components/bn-as-decimal-input.js create mode 100644 ui/responsive/app/components/buy-button-subview.js create mode 100644 ui/responsive/app/components/coinbase-form.js create mode 100644 ui/responsive/app/components/copyButton.js create mode 100644 ui/responsive/app/components/copyable.js create mode 100644 ui/responsive/app/components/custom-radio-list.js create mode 100644 ui/responsive/app/components/drop-menu-item.js create mode 100644 ui/responsive/app/components/editable-label.js create mode 100644 ui/responsive/app/components/ens-input.js create mode 100644 ui/responsive/app/components/eth-balance.js create mode 100644 ui/responsive/app/components/fiat-value.js create mode 100644 ui/responsive/app/components/hex-as-decimal-input.js create mode 100644 ui/responsive/app/components/identicon.js create mode 100644 ui/responsive/app/components/loading.js create mode 100644 ui/responsive/app/components/mascot.js create mode 100644 ui/responsive/app/components/mini-account-panel.js create mode 100644 ui/responsive/app/components/network.js create mode 100644 ui/responsive/app/components/notice.js create mode 100644 ui/responsive/app/components/pending-msg-details.js create mode 100644 ui/responsive/app/components/pending-msg.js create mode 100644 ui/responsive/app/components/pending-personal-msg-details.js create mode 100644 ui/responsive/app/components/pending-personal-msg.js create mode 100644 ui/responsive/app/components/pending-tx.js create mode 100644 ui/responsive/app/components/qr-code.js create mode 100644 ui/responsive/app/components/range-slider.js create mode 100644 ui/responsive/app/components/shapeshift-form.js create mode 100644 ui/responsive/app/components/shift-list-item.js create mode 100644 ui/responsive/app/components/tab-bar.js create mode 100644 ui/responsive/app/components/template.js create mode 100644 ui/responsive/app/components/token-cell.js create mode 100644 ui/responsive/app/components/token-list.js create mode 100644 ui/responsive/app/components/tooltip.js create mode 100644 ui/responsive/app/components/transaction-list-item-icon.js create mode 100644 ui/responsive/app/components/transaction-list-item.js create mode 100644 ui/responsive/app/components/transaction-list.js create mode 100644 ui/responsive/app/conf-tx.js create mode 100644 ui/responsive/app/config.js create mode 100644 ui/responsive/app/conversion.json create mode 100644 ui/responsive/app/css/debug.css create mode 100644 ui/responsive/app/css/fonts.css create mode 100644 ui/responsive/app/css/index.css create mode 100644 ui/responsive/app/css/lib.css create mode 100644 ui/responsive/app/css/reset.css create mode 100644 ui/responsive/app/css/transitions.css create mode 100644 ui/responsive/app/first-time/init-menu.js create mode 100644 ui/responsive/app/img/identicon-tardigrade.png create mode 100644 ui/responsive/app/img/identicon-walrus.png create mode 100644 ui/responsive/app/info.js create mode 100644 ui/responsive/app/keychains/hd/create-vault-complete.js create mode 100644 ui/responsive/app/keychains/hd/recover-seed/confirmation.js create mode 100644 ui/responsive/app/keychains/hd/restore-vault.js create mode 100644 ui/responsive/app/new-keychain.js create mode 100644 ui/responsive/app/reducers.js create mode 100644 ui/responsive/app/reducers/app.js create mode 100644 ui/responsive/app/reducers/identities.js create mode 100644 ui/responsive/app/reducers/metamask.js create mode 100644 ui/responsive/app/root.js create mode 100644 ui/responsive/app/send.js create mode 100644 ui/responsive/app/settings.js create mode 100644 ui/responsive/app/store.js create mode 100644 ui/responsive/app/template.js create mode 100644 ui/responsive/app/unlock.js create mode 100644 ui/responsive/app/util.js create mode 100644 ui/responsive/css.js create mode 100644 ui/responsive/design/00-metamask-SignIn.jpg create mode 100644 ui/responsive/design/01-metamask-SelectAcc.jpg create mode 100644 ui/responsive/design/02-metamask-AccDetails.jpg create mode 100644 ui/responsive/design/02a-metamask-AccDetails-OverToken.jpg create mode 100644 ui/responsive/design/02a-metamask-AccDetails-OverTransaction.jpg create mode 100644 ui/responsive/design/02a-metamask-AccDetails.jpg create mode 100644 ui/responsive/design/02b-metamask-AccDetails-Send.jpg create mode 100644 ui/responsive/design/03-metamask-Qr.jpg create mode 100644 ui/responsive/design/05-metamask-Menu.jpg create mode 100644 ui/responsive/design/chromeStorePics/final_screen_dao_accounts.png create mode 100644 ui/responsive/design/chromeStorePics/final_screen_dao_locked.png create mode 100644 ui/responsive/design/chromeStorePics/final_screen_dao_notification.png create mode 100644 ui/responsive/design/chromeStorePics/final_screen_wei_account.png create mode 100644 ui/responsive/design/chromeStorePics/final_screen_wei_notification.png create mode 100644 ui/responsive/design/chromeStorePics/icon-128.png create mode 100644 ui/responsive/design/chromeStorePics/icon-64.png create mode 100644 ui/responsive/design/chromeStorePics/metamask_icon.ai create mode 100644 ui/responsive/design/chromeStorePics/promo1400560.png create mode 100644 ui/responsive/design/chromeStorePics/promo440280.png create mode 100644 ui/responsive/design/chromeStorePics/promo920680.png create mode 100644 ui/responsive/design/chromeStorePics/screen_dao_accounts.png create mode 100644 ui/responsive/design/chromeStorePics/screen_dao_locked.png create mode 100644 ui/responsive/design/chromeStorePics/screen_dao_notification.png create mode 100644 ui/responsive/design/chromeStorePics/screen_wei_account.png create mode 100644 ui/responsive/design/chromeStorePics/screen_wei_notification.png create mode 100644 ui/responsive/design/metamask-logo-eyes.png create mode 100644 ui/responsive/design/wireframes/1st_time_use.png create mode 100644 ui/responsive/design/wireframes/metamask_wfs_jan_13.pdf create mode 100644 ui/responsive/design/wireframes/metamask_wfs_jan_13.png create mode 100644 ui/responsive/design/wireframes/metamask_wfs_jan_18.pdf create mode 100644 ui/responsive/example.js create mode 100644 ui/responsive/index.html create mode 100644 ui/responsive/index.js create mode 100644 ui/responsive/lib/account-link.js create mode 100644 ui/responsive/lib/contract-namer.js create mode 100644 ui/responsive/lib/etherscan-prefix-for-network.js create mode 100644 ui/responsive/lib/explorer-link.js create mode 100644 ui/responsive/lib/icon-factory.js create mode 100644 ui/responsive/lib/lost-accounts-notice.js create mode 100644 ui/responsive/lib/persistent-form.js create mode 100644 ui/responsive/lib/tx-helper.js diff --git a/app/scripts/popup.js b/app/scripts/popup.js index 5f17f0651..13b98d1f6 100644 --- a/app/scripts/popup.js +++ b/app/scripts/popup.js @@ -1,5 +1,5 @@ const injectCss = require('inject-css') -const MetaMaskUiCss = require('../../ui/css') +const MetaMaskUiCss = require('../../ui/classic/css') const startPopup = require('./popup-core') const PortStream = require('./lib/port-stream.js') const isPopupOrNotification = require('./lib/is-popup-or-notification') diff --git a/app/scripts/responsive.js b/app/scripts/responsive.js index 512065309..0ff42a4cb 100644 --- a/app/scripts/responsive.js +++ b/app/scripts/responsive.js @@ -1,9 +1,9 @@ +const injectCss = require('inject-css') const startPopup = require('./responsive-core') +const MetaMaskUiCss = require('../../ui/responsive/css') const PortStream = require('./lib/port-stream.js') const ExtensionPlatform = require('./platforms/extension') const extension = require('extensionizer') -const NotificationManager = require('./lib/notification-manager') -const notificationManager = new NotificationManager() // create platform global global.platform = new ExtensionPlatform() @@ -20,9 +20,6 @@ const connectionStream = new PortStream(extensionPort) const container = document.getElementById('app-content') startPopup({ container, connectionStream }, (err, store) => { if (err) return displayCriticalError(err) - store.subscribe(() => { - const state = store.getState() - }) }) function displayCriticalError (err) { diff --git a/gulpfile.js b/gulpfile.js index cc723704a..628314b37 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -174,6 +174,7 @@ const jsFiles = [ 'contentscript', 'background', 'popup', + 'responsive' ] // bundle tasks diff --git a/ui/classic/app/actions.js b/ui/classic/app/actions.js index d99291e46..2c60448dd 100644 --- a/ui/classic/app/actions.js +++ b/ui/classic/app/actions.js @@ -1,4 +1,4 @@ -const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url') +const getBuyEthUrl = require('../../../app/scripts/lib/buy-eth-url') var actions = { _setBackgroundConnection: _setBackgroundConnection, diff --git a/ui/classic/app/components/pending-tx.js b/ui/classic/app/components/pending-tx.js index d7d602f31..962680d30 100644 --- a/ui/classic/app/components/pending-tx.js +++ b/ui/classic/app/components/pending-tx.js @@ -6,7 +6,7 @@ const clone = require('clone') const ethUtil = require('ethereumjs-util') const BN = ethUtil.BN -const hexToBn = require('../../../app/scripts/lib/hex-to-bn') +const hexToBn = require('../../../../app/scripts/lib/hex-to-bn') const util = require('../util') const MiniAccountPanel = require('./mini-account-panel') const Copyable = require('./copyable') diff --git a/ui/classic/app/conf-tx.js b/ui/classic/app/conf-tx.js index 747d3ce2b..63b77ef7f 100644 --- a/ui/classic/app/conf-tx.js +++ b/ui/classic/app/conf-tx.js @@ -6,7 +6,7 @@ const connect = require('react-redux').connect const actions = require('./actions') const NetworkIndicator = require('./components/network') const txHelper = require('../lib/tx-helper') -const isPopupOrNotification = require('../../app/scripts/lib/is-popup-or-notification') +const isPopupOrNotification = require('../../../app/scripts/lib/is-popup-or-notification') const PendingTx = require('./components/pending-tx') const PendingMsg = require('./components/pending-msg') diff --git a/ui/classic/css.js b/ui/classic/css.js index 043363cd7..7c394a87b 100644 --- a/ui/classic/css.js +++ b/ui/classic/css.js @@ -9,8 +9,8 @@ var cssFiles = { 'lib.css': fs.readFileSync(path.join(__dirname, '/app/css/lib.css'), 'utf8'), 'index.css': fs.readFileSync(path.join(__dirname, '/app/css/index.css'), 'utf8'), 'transitions.css': fs.readFileSync(path.join(__dirname, '/app/css/transitions.css'), 'utf8'), - 'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'), - 'react-css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'), + 'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'), + 'react-css': fs.readFileSync(path.join(__dirname, '..', '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'), } function bundleCss () { diff --git a/ui/responsive/.gitignore b/ui/responsive/.gitignore new file mode 100644 index 000000000..c6b1254b5 --- /dev/null +++ b/ui/responsive/.gitignore @@ -0,0 +1,66 @@ + +# Created by https://www.gitignore.io/api/osx,node + +### OSX ### +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### Node ### +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git +node_modules + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + diff --git a/ui/responsive/app/account-detail.js b/ui/responsive/app/account-detail.js new file mode 100644 index 000000000..bed05a7fb --- /dev/null +++ b/ui/responsive/app/account-detail.js @@ -0,0 +1,311 @@ +const inherits = require('util').inherits +const extend = require('xtend') +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const CopyButton = require('./components/copyButton') +const AccountInfoLink = require('./components/account-info-link') +const actions = require('./actions') +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +const valuesFor = require('./util').valuesFor + +const Identicon = require('./components/identicon') +const EthBalance = require('./components/eth-balance') +const TransactionList = require('./components/transaction-list') +const ExportAccountView = require('./components/account-export') +const ethUtil = require('ethereumjs-util') +const EditableLabel = require('./components/editable-label') +const Tooltip = require('./components/tooltip') +const TabBar = require('./components/tab-bar') +const TokenList = require('./components/token-list') + +module.exports = connect(mapStateToProps)(AccountDetailScreen) + +function mapStateToProps (state) { + return { + metamask: state.metamask, + identities: state.metamask.identities, + accounts: state.metamask.accounts, + address: state.metamask.selectedAddress, + accountDetail: state.appState.accountDetail, + network: state.metamask.network, + unapprovedMsgs: valuesFor(state.metamask.unapprovedMsgs), + shapeShiftTxList: state.metamask.shapeShiftTxList, + transactions: state.metamask.selectedAddressTxList || [], + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + currentAccountTab: state.metamask.currentAccountTab, + tokens: state.metamask.tokens, + } +} + +inherits(AccountDetailScreen, Component) +function AccountDetailScreen () { + Component.call(this) +} + +AccountDetailScreen.prototype.render = function () { + var props = this.props + var selected = props.address || Object.keys(props.accounts)[0] + var checksumAddress = selected && ethUtil.toChecksumAddress(selected) + var identity = props.identities[selected] + var account = props.accounts[selected] + const { network, conversionRate, currentCurrency } = props + + return ( + + h('.account-detail-section', [ + + // identicon, label, balance, etc + h('.account-data-subsection', { + style: { + margin: '0 20px', + }, + }, [ + + // header - identicon + nav + h('div', { + style: { + paddingTop: '20px', + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'flex-start', + }, + }, [ + + // large identicon and addresses + h('.identicon-wrapper.select-none', [ + h(Identicon, { + diameter: 62, + address: selected, + }), + ]), + h('flex-column', { + style: { + lineHeight: '10px', + marginLeft: '15px', + }, + }, [ + h(EditableLabel, { + textValue: identity ? identity.name : '', + state: { + isEditingLabel: false, + }, + saveText: (text) => { + props.dispatch(actions.saveAccountLabel(selected, text)) + }, + }, [ + + // What is shown when not editing + edit text: + h('label.editing-label', [h('.edit-text', 'edit')]), + h('h2.font-medium.color-forest', {name: 'edit'}, identity && identity.name), + ]), + h('.flex-row', { + style: { + width: '15em', + justifyContent: 'space-between', + alignItems: 'baseline', + }, + }, [ + + // address + + h('div', { + style: { + overflow: 'hidden', + textOverflow: 'ellipsis', + paddingTop: '3px', + width: '5em', + fontSize: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + marginTop: '10px', + marginBottom: '15px', + color: '#AEAEAE', + }, + }, checksumAddress), + + // copy and export + + h('.flex-row', { + style: { + justifyContent: 'flex-end', + }, + }, [ + + h(AccountInfoLink, { selected, network }), + + h(CopyButton, { + value: checksumAddress, + }), + + h(Tooltip, { + title: 'QR Code', + }, [ + h('i.fa.fa-qrcode.pointer.pop-hover', { + onClick: () => props.dispatch(actions.showQrView(selected, identity ? identity.name : '')), + style: { + fontSize: '18px', + position: 'relative', + color: 'rgb(247, 134, 28)', + top: '5px', + marginLeft: '3px', + marginRight: '3px', + }, + }), + ]), + + h(Tooltip, { + title: 'Export Private Key', + }, [ + h('div', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + h('img.cursor-pointer.color-orange', { + src: 'images/key-32.png', + onClick: () => this.requestAccountExport(selected), + style: { + height: '19px', + }, + }), + ]), + ]), + ]), + ]), + + // account ballence + + ]), + ]), + h('.flex-row', { + style: { + justifyContent: 'space-between', + alignItems: 'flex-start', + }, + }, [ + + h(EthBalance, { + value: account && account.balance, + conversionRate, + currentCurrency, + style: { + lineHeight: '7px', + marginTop: '10px', + }, + }), + + h('button', { + onClick: () => props.dispatch(actions.buyEthView(selected)), + style: { + marginBottom: '20px', + marginRight: '8px', + position: 'absolute', + left: '219px', + }, + }, 'BUY'), + + h('button', { + onClick: () => props.dispatch(actions.showSendPage()), + style: { + marginBottom: '20px', + marginRight: '8px', + }, + }, 'SEND'), + + ]), + ]), + + // subview (tx history, pk export confirm, buy eth warning) + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + this.subview(), + ]), + + ]) + ) +} + +AccountDetailScreen.prototype.subview = function () { + var subview + try { + subview = this.props.accountDetail.subview + } catch (e) { + subview = null + } + + switch (subview) { + case 'transactions': + return this.tabSections() + case 'export': + var state = extend({key: 'export'}, this.props) + return h(ExportAccountView, state) + default: + return this.tabSections() + } +} + +AccountDetailScreen.prototype.tabSections = function () { + const { currentAccountTab } = this.props + + return h('section.tabSection', [ + + h(TabBar, { + tabs: [ + { content: 'Sent', key: 'history' }, + { content: 'Tokens', key: 'tokens' }, + ], + defaultTab: currentAccountTab || 'history', + tabSelected: (key) => { + this.props.dispatch(actions.setCurrentAccountTab(key)) + }, + }), + + this.tabSwitchView(), + ]) +} + +AccountDetailScreen.prototype.tabSwitchView = function () { + const props = this.props + const { address, network } = props + const { currentAccountTab, tokens } = this.props + + switch (currentAccountTab) { + case 'tokens': + return h(TokenList, { + userAddress: address, + network, + tokens, + addToken: () => this.props.dispatch(actions.showAddTokenPage()), + }) + default: + return this.transactionList() + } +} + +AccountDetailScreen.prototype.transactionList = function () { + const {transactions, unapprovedMsgs, address, + network, shapeShiftTxList, conversionRate } = this.props + + return h(TransactionList, { + transactions: transactions.sort((a, b) => b.time - a.time), + network, + unapprovedMsgs, + conversionRate, + address, + shapeShiftTxList, + viewPendingTx: (txId) => { + this.props.dispatch(actions.viewPendingTx(txId)) + }, + }) +} + +AccountDetailScreen.prototype.requestAccountExport = function () { + this.props.dispatch(actions.requestExportAccount()) +} diff --git a/ui/responsive/app/accounts/account-list-item.js b/ui/responsive/app/accounts/account-list-item.js new file mode 100644 index 000000000..10a0b6cc7 --- /dev/null +++ b/ui/responsive/app/accounts/account-list-item.js @@ -0,0 +1,91 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') + +const EthBalance = require('../components/eth-balance') +const CopyButton = require('../components/copyButton') +const Identicon = require('../components/identicon') + +module.exports = AccountListItem + +inherits(AccountListItem, Component) +function AccountListItem () { + Component.call(this) +} + +AccountListItem.prototype.render = function () { + const { identity, selectedAddress, accounts, onShowDetail, + conversionRate, currentCurrency } = this.props + + const checksumAddress = identity && identity.address && ethUtil.toChecksumAddress(identity.address) + const isSelected = selectedAddress === identity.address + const account = accounts[identity.address] + const selectedClass = isSelected ? '.selected' : '' + + return ( + h(`.accounts-list-option.flex-row.flex-space-between.pointer.hover-white${selectedClass}`, { + key: `account-panel-${identity.address}`, + onClick: (event) => onShowDetail(identity.address, event), + }, [ + + h('.identicon-wrapper.flex-column.flex-center.select-none', [ + this.pendingOrNot(), + this.indicateIfLoose(), + h(Identicon, { + address: identity.address, + imageify: true, + }), + ]), + + // account address, balance + h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', { + style: { + width: '200px', + }, + }, [ + h('span', identity.name), + h('span.font-small', { + style: { + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + }, checksumAddress), + h(EthBalance, { + value: account && account.balance, + currentCurrency, + conversionRate, + style: { + lineHeight: '7px', + marginTop: '10px', + }, + }), + ]), + + // copy button + h('.identity-copy.flex-column', { + style: { + margin: '0 20px', + }, + }, [ + h(CopyButton, { + value: checksumAddress, + }), + ]), + ]) + ) +} + +AccountListItem.prototype.indicateIfLoose = function () { + try { // Sometimes keyrings aren't loaded yet: + const type = this.props.keyring.type + const isLoose = type !== 'HD Key Tree' + return isLoose ? h('.keyring-label', 'LOOSE') : null + } catch (e) { return } +} + +AccountListItem.prototype.pendingOrNot = function () { + const pending = this.props.pending + if (pending.length === 0) return null + return h('.pending-dot', pending.length) +} diff --git a/ui/responsive/app/accounts/import/index.js b/ui/responsive/app/accounts/import/index.js new file mode 100644 index 000000000..97b387229 --- /dev/null +++ b/ui/responsive/app/accounts/import/index.js @@ -0,0 +1,100 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../../actions') +import Select from 'react-select' + +// Subviews +const JsonImportView = require('./json.js') +const PrivateKeyImportView = require('./private-key.js') + +const menuItems = [ + 'Private Key', + 'JSON File', +] + +module.exports = connect(mapStateToProps)(AccountImportSubview) + +function mapStateToProps (state) { + return { + menuItems, + } +} + +inherits(AccountImportSubview, Component) +function AccountImportSubview () { + Component.call(this) +} + +AccountImportSubview.prototype.render = function () { + const props = this.props + const state = this.state || {} + const { menuItems } = props + const { type } = state + + return ( + h('div', { + style: { + }, + }, [ + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + props.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Import Accounts'), + ]), + h('div', { + style: { + padding: '10px', + color: 'rgb(174, 174, 174)', + }, + }, [ + + h('h3', { style: { padding: '3px' } }, 'SELECT TYPE'), + + h('style', ` + .has-value.Select--single > .Select-control .Select-value .Select-value-label, .Select-value-label { + color: rgb(174,174,174); + } + `), + + h(Select, { + name: 'import-type-select', + clearable: false, + value: type || menuItems[0], + options: menuItems.map((type) => { + return { + value: type, + label: type, + } + }), + onChange: (opt) => { + this.setState({ type: opt.value }) + }, + }), + ]), + + this.renderImportView(), + ]) + ) +} + +AccountImportSubview.prototype.renderImportView = function () { + const props = this.props + const state = this.state || {} + const { type } = state + const { menuItems } = props + const current = type || menuItems[0] + + switch (current) { + case 'Private Key': + return h(PrivateKeyImportView) + case 'JSON File': + return h(JsonImportView) + default: + return h(JsonImportView) + } +} diff --git a/ui/responsive/app/accounts/import/json.js b/ui/responsive/app/accounts/import/json.js new file mode 100644 index 000000000..158a3c923 --- /dev/null +++ b/ui/responsive/app/accounts/import/json.js @@ -0,0 +1,100 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../../actions') +const FileInput = require('react-simple-file-input').default + +const HELP_LINK = 'https://github.com/MetaMask/faq/blob/master/README.md#q-i-cant-use-the-import-feature-for-uploading-a-json-file-the-window-keeps-closing-when-i-try-to-select-a-file' + +module.exports = connect(mapStateToProps)(JsonImportSubview) + +function mapStateToProps (state) { + return { + error: state.appState.warning, + } +} + +inherits(JsonImportSubview, Component) +function JsonImportSubview () { + Component.call(this) +} + +JsonImportSubview.prototype.render = function () { + const { error } = this.props + + return ( + h('div', { + style: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: '5px 15px 0px 15px', + }, + }, [ + + h('p', 'Used by a variety of different clients'), + h('a.warning', { href: HELP_LINK, target: '_blank' }, 'File import not working? Click here!'), + + h(FileInput, { + readAs: 'text', + onLoad: this.onLoad.bind(this), + style: { + margin: '20px 0px 12px 20px', + fontSize: '15px', + }, + }), + + h('input.large-input.letter-spacey', { + type: 'password', + placeholder: 'Enter password', + id: 'json-password-box', + onKeyPress: this.createKeyringOnEnter.bind(this), + style: { + width: 260, + marginTop: 12, + }, + }), + + h('button.primary', { + onClick: this.createNewKeychain.bind(this), + style: { + margin: 12, + }, + }, 'Import'), + + error ? h('span.error', error) : null, + ]) + ) +} + +JsonImportSubview.prototype.onLoad = function (event, file) { + this.setState({file: file, fileContents: event.target.result}) +} + +JsonImportSubview.prototype.createKeyringOnEnter = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.createNewKeychain() + } +} + +JsonImportSubview.prototype.createNewKeychain = function () { + const state = this.state + const { fileContents } = state + + if (!fileContents) { + const message = 'You must select a file to import.' + return this.props.dispatch(actions.displayWarning(message)) + } + + const passwordInput = document.getElementById('json-password-box') + const password = passwordInput.value + + if (!password) { + const message = 'You must enter a password for the selected file.' + return this.props.dispatch(actions.displayWarning(message)) + } + + this.props.dispatch(actions.importNewAccount('JSON File', [ fileContents, password ])) +} diff --git a/ui/responsive/app/accounts/import/private-key.js b/ui/responsive/app/accounts/import/private-key.js new file mode 100644 index 000000000..68ccee58e --- /dev/null +++ b/ui/responsive/app/accounts/import/private-key.js @@ -0,0 +1,67 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../../actions') + +module.exports = connect(mapStateToProps)(PrivateKeyImportView) + +function mapStateToProps (state) { + return { + error: state.appState.warning, + } +} + +inherits(PrivateKeyImportView, Component) +function PrivateKeyImportView () { + Component.call(this) +} + +PrivateKeyImportView.prototype.render = function () { + const { error } = this.props + + return ( + h('div', { + style: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: '5px 15px 0px 15px', + }, + }, [ + h('span', 'Paste your private key string here'), + + h('input.large-input.letter-spacey', { + type: 'password', + id: 'private-key-box', + onKeyPress: this.createKeyringOnEnter.bind(this), + style: { + width: 260, + marginTop: 12, + }, + }), + + h('button.primary', { + onClick: this.createNewKeychain.bind(this), + style: { + margin: 12, + }, + }, 'Import'), + + error ? h('span.error', error) : null, + ]) + ) +} + +PrivateKeyImportView.prototype.createKeyringOnEnter = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.createNewKeychain() + } +} + +PrivateKeyImportView.prototype.createNewKeychain = function () { + const input = document.getElementById('private-key-box') + const privateKey = input.value + this.props.dispatch(actions.importNewAccount('Private Key', [ privateKey ])) +} diff --git a/ui/responsive/app/accounts/import/seed.js b/ui/responsive/app/accounts/import/seed.js new file mode 100644 index 000000000..b4a7c0afa --- /dev/null +++ b/ui/responsive/app/accounts/import/seed.js @@ -0,0 +1,30 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect + +module.exports = connect(mapStateToProps)(SeedImportSubview) + +function mapStateToProps (state) { + return {} +} + +inherits(SeedImportSubview, Component) +function SeedImportSubview () { + Component.call(this) +} + +SeedImportSubview.prototype.render = function () { + return ( + h('div', { + style: { + }, + }, [ + `Paste your seed phrase here!`, + h('textarea'), + h('br'), + h('button', 'Submit'), + ]) + ) +} + diff --git a/ui/responsive/app/accounts/index.js b/ui/responsive/app/accounts/index.js new file mode 100644 index 000000000..ac2615cd7 --- /dev/null +++ b/ui/responsive/app/accounts/index.js @@ -0,0 +1,164 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../actions') +const valuesFor = require('../util').valuesFor +const findDOMNode = require('react-dom').findDOMNode +const AccountListItem = require('./account-list-item') + +module.exports = connect(mapStateToProps)(AccountsScreen) + +function mapStateToProps (state) { + const pendingTxs = valuesFor(state.metamask.unapprovedTxs) + .filter(txMeta => txMeta.metamaskNetworkId === state.metamask.network) + const pendingMsgs = valuesFor(state.metamask.unapprovedMsgs) + const pending = pendingTxs.concat(pendingMsgs) + + return { + accounts: state.metamask.accounts, + identities: state.metamask.identities, + unapprovedTxs: state.metamask.unapprovedTxs, + selectedAddress: state.metamask.selectedAddress, + scrollToBottom: state.appState.scrollToBottom, + pending, + keyrings: state.metamask.keyrings, + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + } +} + +inherits(AccountsScreen, Component) +function AccountsScreen () { + Component.call(this) +} + +AccountsScreen.prototype.render = function () { + const props = this.props + const { keyrings, conversionRate, currentCurrency } = props + const identityList = valuesFor(props.identities) + const unapprovedTxList = valuesFor(props.unapprovedTxs) + + return ( + + h('.accounts-section.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: this.goHome.bind(this), + }), + h('h2.page-subtitle', 'Select Account'), + ]), + + h('hr.horizontal-line'), + + // identity selection + h('section.identity-section', { + style: { + height: '418px', + overflowY: 'auto', + overflowX: 'hidden', + }, + }, + [ + identityList.map((identity) => { + const pending = this.props.pending.filter((txOrMsg) => { + if ('txParams' in txOrMsg) { + return txOrMsg.txParams.from === identity.address + } else if ('msgParams' in txOrMsg) { + return txOrMsg.msgParams.from === identity.address + } else { + return false + } + }) + + const simpleAddress = identity.address.substring(2).toLowerCase() + const keyring = keyrings.find((kr) => { + return kr.accounts.includes(simpleAddress) || + kr.accounts.includes(identity.address) + }) + + return h(AccountListItem, { + key: `acct-panel-${identity.address}`, + identity, + selectedAddress: this.props.selectedAddress, + conversionRate, + currentCurrency, + accounts: this.props.accounts, + onShowDetail: this.onShowDetail.bind(this), + pending, + keyring, + }) + }), + + h('hr.horizontal-line'), + h('div.footer.hover-white.pointer', { + key: 'reveal-account-bar', + onClick: () => { + this.addNewAccount() + }, + style: { + display: 'flex', + height: '40px', + padding: '10px', + justifyContent: 'center', + alignItems: 'center', + }, + }, [ + h('i.fa.fa-plus.fa-lg', {key: ''}), + ]), + h('hr.horizontal-line'), + ]), + + unapprovedTxList.length ? ( + + h('.unconftx-link.flex-row.flex-center', { + onClick: this.navigateToConfTx.bind(this), + }, [ + h('span', 'Unconfirmed Txs'), + h('i.fa.fa-arrow-right.fa-lg'), + ]) + + ) : ( + null + ), + ]) + ) +} + +// If a new account was revealed, scroll to the bottom +AccountsScreen.prototype.componentDidUpdate = function () { + const scrollToBottom = this.props.scrollToBottom + + if (scrollToBottom) { + var container = findDOMNode(this) + var scrollable = container.querySelector('.identity-section') + scrollable.scrollTop = scrollable.scrollHeight + } +} + +AccountsScreen.prototype.navigateToConfTx = function () { + event.stopPropagation() + this.props.dispatch(actions.showConfTxPage()) +} + +AccountsScreen.prototype.onShowDetail = function (address, event) { + event.stopPropagation() + this.props.dispatch(actions.showAccountDetail(address)) +} + +AccountsScreen.prototype.addNewAccount = function () { + this.props.dispatch(actions.addNewAccount(0)) +} + +/* An optional view proposed in this design: + * https://consensys.quip.com/zZVrAysM5znY +AccountsScreen.prototype.addNewAccount = function () { + this.props.dispatch(actions.navigateToNewAccountScreen()) +} +*/ + +AccountsScreen.prototype.goHome = function () { + this.props.dispatch(actions.goHome()) +} diff --git a/ui/responsive/app/actions.js b/ui/responsive/app/actions.js new file mode 100644 index 000000000..2c60448dd --- /dev/null +++ b/ui/responsive/app/actions.js @@ -0,0 +1,1031 @@ +const getBuyEthUrl = require('../../../app/scripts/lib/buy-eth-url') + +var actions = { + _setBackgroundConnection: _setBackgroundConnection, + + GO_HOME: 'GO_HOME', + goHome: goHome, + // menu state + getNetworkStatus: 'getNetworkStatus', + // transition state + TRANSITION_FORWARD: 'TRANSITION_FORWARD', + TRANSITION_BACKWARD: 'TRANSITION_BACKWARD', + transitionForward, + transitionBackward, + // remote state + UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE', + updateMetamaskState: updateMetamaskState, + // notices + MARK_NOTICE_READ: 'MARK_NOTICE_READ', + markNoticeRead: markNoticeRead, + SHOW_NOTICE: 'SHOW_NOTICE', + showNotice: showNotice, + CLEAR_NOTICES: 'CLEAR_NOTICES', + clearNotices: clearNotices, + markAccountsFound, + // intialize screen + CREATE_NEW_VAULT_IN_PROGRESS: 'CREATE_NEW_VAULT_IN_PROGRESS', + SHOW_CREATE_VAULT: 'SHOW_CREATE_VAULT', + SHOW_RESTORE_VAULT: 'SHOW_RESTORE_VAULT', + FORGOT_PASSWORD: 'FORGOT_PASSWORD', + forgotPassword: forgotPassword, + SHOW_INIT_MENU: 'SHOW_INIT_MENU', + SHOW_NEW_VAULT_SEED: 'SHOW_NEW_VAULT_SEED', + SHOW_INFO_PAGE: 'SHOW_INFO_PAGE', + SHOW_IMPORT_PAGE: 'SHOW_IMPORT_PAGE', + unlockMetamask: unlockMetamask, + unlockFailed: unlockFailed, + showCreateVault: showCreateVault, + showRestoreVault: showRestoreVault, + showInitializeMenu: showInitializeMenu, + showImportPage, + createNewVaultAndKeychain: createNewVaultAndKeychain, + createNewVaultAndRestore: createNewVaultAndRestore, + createNewVaultInProgress: createNewVaultInProgress, + addNewKeyring, + importNewAccount, + addNewAccount, + NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN', + navigateToNewAccountScreen, + showNewVaultSeed: showNewVaultSeed, + showInfoPage: showInfoPage, + // seed recovery actions + REVEAL_SEED_CONFIRMATION: 'REVEAL_SEED_CONFIRMATION', + revealSeedConfirmation: revealSeedConfirmation, + requestRevealSeed: requestRevealSeed, + // unlock screen + UNLOCK_IN_PROGRESS: 'UNLOCK_IN_PROGRESS', + UNLOCK_FAILED: 'UNLOCK_FAILED', + UNLOCK_METAMASK: 'UNLOCK_METAMASK', + LOCK_METAMASK: 'LOCK_METAMASK', + tryUnlockMetamask: tryUnlockMetamask, + lockMetamask: lockMetamask, + unlockInProgress: unlockInProgress, + // error handling + displayWarning: displayWarning, + DISPLAY_WARNING: 'DISPLAY_WARNING', + HIDE_WARNING: 'HIDE_WARNING', + hideWarning: hideWarning, + // accounts screen + SET_SELECTED_ACCOUNT: 'SET_SELECTED_ACCOUNT', + SHOW_ACCOUNT_DETAIL: 'SHOW_ACCOUNT_DETAIL', + SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE', + SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE', + SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE', + SET_CURRENT_FIAT: 'SET_CURRENT_FIAT', + setCurrentCurrency: setCurrentCurrency, + setCurrentAccountTab, + // account detail screen + SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', + showSendPage: showSendPage, + ADD_TO_ADDRESS_BOOK: 'ADD_TO_ADDRESS_BOOK', + addToAddressBook: addToAddressBook, + REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT', + requestExportAccount: requestExportAccount, + EXPORT_ACCOUNT: 'EXPORT_ACCOUNT', + exportAccount: exportAccount, + SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY', + showPrivateKey: showPrivateKey, + SAVE_ACCOUNT_LABEL: 'SAVE_ACCOUNT_LABEL', + saveAccountLabel: saveAccountLabel, + // tx conf screen + COMPLETED_TX: 'COMPLETED_TX', + TRANSACTION_ERROR: 'TRANSACTION_ERROR', + NEXT_TX: 'NEXT_TX', + PREVIOUS_TX: 'PREV_TX', + signMsg: signMsg, + cancelMsg: cancelMsg, + signPersonalMsg, + cancelPersonalMsg, + sendTx: sendTx, + signTx: signTx, + updateAndApproveTx, + cancelTx: cancelTx, + completedTx: completedTx, + txError: txError, + nextTx: nextTx, + previousTx: previousTx, + viewPendingTx: viewPendingTx, + VIEW_PENDING_TX: 'VIEW_PENDING_TX', + // app messages + confirmSeedWords: confirmSeedWords, + showAccountDetail: showAccountDetail, + BACK_TO_ACCOUNT_DETAIL: 'BACK_TO_ACCOUNT_DETAIL', + backToAccountDetail: backToAccountDetail, + showAccountsPage: showAccountsPage, + showConfTxPage: showConfTxPage, + // config screen + SHOW_CONFIG_PAGE: 'SHOW_CONFIG_PAGE', + SET_RPC_TARGET: 'SET_RPC_TARGET', + SET_DEFAULT_RPC_TARGET: 'SET_DEFAULT_RPC_TARGET', + SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE', + USE_ETHERSCAN_PROVIDER: 'USE_ETHERSCAN_PROVIDER', + useEtherscanProvider: useEtherscanProvider, + showConfigPage, + SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', + showAddTokenPage, + addToken, + setRpcTarget: setRpcTarget, + setDefaultRpcTarget: setDefaultRpcTarget, + setProviderType: setProviderType, + // loading overlay + SHOW_LOADING: 'SHOW_LOADING_INDICATION', + HIDE_LOADING: 'HIDE_LOADING_INDICATION', + showLoadingIndication: showLoadingIndication, + hideLoadingIndication: hideLoadingIndication, + // buy Eth with coinbase + BUY_ETH: 'BUY_ETH', + buyEth: buyEth, + buyEthView: buyEthView, + BUY_ETH_VIEW: 'BUY_ETH_VIEW', + COINBASE_SUBVIEW: 'COINBASE_SUBVIEW', + coinBaseSubview: coinBaseSubview, + SHAPESHIFT_SUBVIEW: 'SHAPESHIFT_SUBVIEW', + shapeShiftSubview: shapeShiftSubview, + PAIR_UPDATE: 'PAIR_UPDATE', + pairUpdate: pairUpdate, + coinShiftRquest: coinShiftRquest, + SHOW_SUB_LOADING_INDICATION: 'SHOW_SUB_LOADING_INDICATION', + showSubLoadingIndication: showSubLoadingIndication, + HIDE_SUB_LOADING_INDICATION: 'HIDE_SUB_LOADING_INDICATION', + hideSubLoadingIndication: hideSubLoadingIndication, +// QR STUFF: + SHOW_QR: 'SHOW_QR', + showQrView: showQrView, + reshowQrCode: reshowQrCode, + SHOW_QR_VIEW: 'SHOW_QR_VIEW', +// FORGOT PASSWORD: + BACK_TO_INIT_MENU: 'BACK_TO_INIT_MENU', + goBackToInitView: goBackToInitView, + RECOVERY_IN_PROGRESS: 'RECOVERY_IN_PROGRESS', + BACK_TO_UNLOCK_VIEW: 'BACK_TO_UNLOCK_VIEW', + backToUnlockView: backToUnlockView, + // SHOWING KEYCHAIN + SHOW_NEW_KEYCHAIN: 'SHOW_NEW_KEYCHAIN', + showNewKeychain: showNewKeychain, + + callBackgroundThenUpdate, + forceUpdateMetamaskState, +} + +module.exports = actions + +var background = null +function _setBackgroundConnection (backgroundConnection) { + background = backgroundConnection +} + +function goHome () { + return { + type: actions.GO_HOME, + } +} + +// async actions + +function tryUnlockMetamask (password) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + dispatch(actions.unlockInProgress()) + log.debug(`background.submitPassword`) + background.submitPassword(password, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + dispatch(actions.unlockFailed(err.message)) + } else { + dispatch(actions.transitionForward()) + forceUpdateMetamaskState(dispatch) + } + }) + } +} + +function transitionForward () { + return { + type: this.TRANSITION_FORWARD, + } +} + +function transitionBackward () { + return { + type: this.TRANSITION_BACKWARD, + } +} + +function confirmSeedWords () { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.clearSeedWordCache`) + background.clearSeedWordCache((err, account) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + + log.info('Seed word cache cleared. ' + account) + dispatch(actions.showAccountDetail(account)) + }) + } +} + +function createNewVaultAndRestore (password, seed) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.createNewVaultAndRestore`) + background.createNewVaultAndRestore(password, seed, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.showAccountsPage()) + }) + } +} + +function createNewVaultAndKeychain (password) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.createNewVaultAndKeychain`) + background.createNewVaultAndKeychain(password, (err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + log.debug(`background.placeSeedWords`) + background.placeSeedWords((err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch(actions.hideLoadingIndication()) + forceUpdateMetamaskState(dispatch) + }) + }) + } +} + +function revealSeedConfirmation () { + return { + type: this.REVEAL_SEED_CONFIRMATION, + } +} + +function requestRevealSeed (password) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.submitPassword`) + background.submitPassword(password, (err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + log.debug(`background.placeSeedWords`) + background.placeSeedWords((err, result) => { + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.hideLoadingIndication()) + dispatch(actions.showNewVaultSeed(result)) + }) + }) + } +} + +function addNewKeyring (type, opts) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.addNewKeyring`) + background.addNewKeyring(type, opts, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.showAccountsPage()) + }) + } +} + +function importNewAccount (strategy, args) { + return (dispatch) => { + dispatch(actions.showLoadingIndication('This may take a while, be patient.')) + log.debug(`background.importAccountWithStrategy`) + background.importAccountWithStrategy(strategy, args, (err) => { + if (err) return dispatch(actions.displayWarning(err.message)) + log.debug(`background.getState`) + background.getState((err, newState) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch(actions.updateMetamaskState(newState)) + dispatch({ + type: actions.SHOW_ACCOUNT_DETAIL, + value: newState.selectedAddress, + }) + }) + }) + } +} + +function navigateToNewAccountScreen () { + return { + type: this.NEW_ACCOUNT_SCREEN, + } +} + +function addNewAccount () { + log.debug(`background.addNewAccount`) + return callBackgroundThenUpdate(background.addNewAccount) +} + +function showInfoPage () { + return { + type: actions.SHOW_INFO_PAGE, + } +} + +function setCurrentCurrency (currencyCode) { + return (dispatch) => { + dispatch(this.showLoadingIndication()) + log.debug(`background.setCurrentCurrency`) + background.setCurrentCurrency(currencyCode, (err, data) => { + dispatch(this.hideLoadingIndication()) + if (err) { + log.error(err.stack) + return dispatch(actions.displayWarning(err.message)) + } + dispatch({ + type: this.SET_CURRENT_FIAT, + value: { + currentCurrency: data.currentCurrency, + conversionRate: data.conversionRate, + conversionDate: data.conversionDate, + }, + }) + }) + } +} + +function signMsg (msgData) { + log.debug('action - signMsg') + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + + log.debug(`actions calling background.signMessage`) + background.signMessage(msgData, (err, newState) => { + log.debug('signMessage called back') + dispatch(actions.updateMetamaskState(newState)) + dispatch(actions.hideLoadingIndication()) + + if (err) log.error(err) + if (err) return dispatch(actions.displayWarning(err.message)) + + dispatch(actions.completedTx(msgData.metamaskId)) + }) + } +} + +function signPersonalMsg (msgData) { + log.debug('action - signPersonalMsg') + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + + log.debug(`actions calling background.signPersonalMessage`) + background.signPersonalMessage(msgData, (err, newState) => { + log.debug('signPersonalMessage called back') + dispatch(actions.updateMetamaskState(newState)) + dispatch(actions.hideLoadingIndication()) + + if (err) log.error(err) + if (err) return dispatch(actions.displayWarning(err.message)) + + dispatch(actions.completedTx(msgData.metamaskId)) + }) + } +} + +function signTx (txData) { + return (dispatch) => { + global.ethQuery.sendTransaction(txData, (err, data) => { + dispatch(actions.hideLoadingIndication()) + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.hideWarning()) + }) + dispatch(this.showConfTxPage()) + } +} + +function sendTx (txData) { + log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`) + return (dispatch) => { + log.debug(`actions calling background.approveTransaction`) + background.approveTransaction(txData.id, (err) => { + if (err) { + dispatch(actions.txError(err)) + return log.error(err.message) + } + dispatch(actions.completedTx(txData.id)) + }) + } +} + +function updateAndApproveTx (txData) { + log.info('actions: updateAndApproveTx: ' + JSON.stringify(txData)) + return (dispatch) => { + log.debug(`actions calling background.updateAndApproveTx`) + background.updateAndApproveTransaction(txData, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + dispatch(actions.txError(err)) + return log.error(err.message) + } + dispatch(actions.completedTx(txData.id)) + }) + } +} + +function completedTx (id) { + return { + type: actions.COMPLETED_TX, + value: id, + } +} + +function txError (err) { + return { + type: actions.TRANSACTION_ERROR, + message: err.message, + } +} + +function cancelMsg (msgData) { + log.debug(`background.cancelMessage`) + background.cancelMessage(msgData.id) + return actions.completedTx(msgData.id) +} + +function cancelPersonalMsg (msgData) { + const id = msgData.id + background.cancelPersonalMessage(id) + return actions.completedTx(id) +} + +function cancelTx (txData) { + log.debug(`background.cancelTransaction`) + background.cancelTransaction(txData.id) + return actions.completedTx(txData.id) +} + +// +// initialize screen +// + +function showCreateVault () { + return { + type: actions.SHOW_CREATE_VAULT, + } +} + +function showRestoreVault () { + return { + type: actions.SHOW_RESTORE_VAULT, + } +} + +function forgotPassword () { + return { + type: actions.FORGOT_PASSWORD, + } +} + +function showInitializeMenu () { + return { + type: actions.SHOW_INIT_MENU, + } +} + +function showImportPage () { + return { + type: actions.SHOW_IMPORT_PAGE, + } +} + +function createNewVaultInProgress () { + return { + type: actions.CREATE_NEW_VAULT_IN_PROGRESS, + } +} + +function showNewVaultSeed (seed) { + return { + type: actions.SHOW_NEW_VAULT_SEED, + value: seed, + } +} + +function backToUnlockView () { + return { + type: actions.BACK_TO_UNLOCK_VIEW, + } +} + +function showNewKeychain () { + return { + type: actions.SHOW_NEW_KEYCHAIN, + } +} + +// +// unlock screen +// + +function unlockInProgress () { + return { + type: actions.UNLOCK_IN_PROGRESS, + } +} + +function unlockFailed (message) { + return { + type: actions.UNLOCK_FAILED, + value: message, + } +} + +function unlockMetamask (account) { + return { + type: actions.UNLOCK_METAMASK, + value: account, + } +} + +function updateMetamaskState (newState) { + return { + type: actions.UPDATE_METAMASK_STATE, + value: newState, + } +} + +function lockMetamask () { + log.debug(`background.setLocked`) + return callBackgroundThenUpdate(background.setLocked) +} + +function setCurrentAccountTab (newTabName) { + log.debug(`background.setCurrentAccountTab: ${newTabName}`) + return callBackgroundThenUpdateNoSpinner(background.setCurrentAccountTab, newTabName) +} + +function showAccountDetail (address) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.setSelectedAddress`) + background.setSelectedAddress(address, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch({ + type: actions.SHOW_ACCOUNT_DETAIL, + value: address, + }) + }) + } +} + +function backToAccountDetail (address) { + return { + type: actions.BACK_TO_ACCOUNT_DETAIL, + value: address, + } +} + +function showAccountsPage () { + return { + type: actions.SHOW_ACCOUNTS_PAGE, + } +} + +function showConfTxPage (transForward = true) { + return { + type: actions.SHOW_CONF_TX_PAGE, + transForward: transForward, + } +} + +function nextTx () { + return { + type: actions.NEXT_TX, + } +} + +function viewPendingTx (txId) { + return { + type: actions.VIEW_PENDING_TX, + value: txId, + } +} + +function previousTx () { + return { + type: actions.PREVIOUS_TX, + } +} + +function showConfigPage (transitionForward = true) { + return { + type: actions.SHOW_CONFIG_PAGE, + value: transitionForward, + } +} + +function showAddTokenPage (transitionForward = true) { + return { + type: actions.SHOW_ADD_TOKEN_PAGE, + value: transitionForward, + } +} + +function addToken (address, symbol, decimals) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + background.addToken(address, symbol, decimals, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + setTimeout(() => { + dispatch(actions.goHome()) + }, 250) + }) + } +} + +function goBackToInitView () { + return { + type: actions.BACK_TO_INIT_MENU, + } +} + +// +// notice +// + +function markNoticeRead (notice) { + return (dispatch) => { + dispatch(this.showLoadingIndication()) + log.debug(`background.markNoticeRead`) + background.markNoticeRead(notice, (err, notice) => { + dispatch(this.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err)) + } + if (notice) { + return dispatch(actions.showNotice(notice)) + } else { + dispatch(this.clearNotices()) + return { + type: actions.SHOW_ACCOUNTS_PAGE, + } + } + }) + } +} + +function showNotice (notice) { + return { + type: actions.SHOW_NOTICE, + value: notice, + } +} + +function clearNotices () { + return { + type: actions.CLEAR_NOTICES, + } +} + +function markAccountsFound () { + log.debug(`background.markAccountsFound`) + return callBackgroundThenUpdate(background.markAccountsFound) +} + +// +// config +// + +// default rpc target refers to localhost:8545 in this instance. +function setDefaultRpcTarget (rpcList) { + log.debug(`background.setDefaultRpcTarget`) + return (dispatch) => { + background.setDefaultRpc((err, result) => { + if (err) { + log.error(err) + return dispatch(self.displayWarning('Had a problem changing networks.')) + } + }) + } +} + +function setRpcTarget (newRpc) { + log.debug(`background.setRpcTarget`) + return (dispatch) => { + background.setCustomRpc(newRpc, (err, result) => { + if (err) { + log.error(err) + return dispatch(self.displayWarning('Had a problem changing networks!')) + } + }) + } +} + +// Calls the addressBookController to add a new address. +function addToAddressBook (recipient, nickname) { + log.debug(`background.addToAddressBook`) + return (dispatch) => { + background.setAddressBook(recipient, nickname, (err, result) => { + if (err) { + log.error(err) + return dispatch(self.displayWarning('Address book failed to update')) + } + }) + } +} + +function setProviderType (type) { + log.debug(`background.setProviderType`) + background.setProviderType(type) + return { + type: actions.SET_PROVIDER_TYPE, + value: type, + } +} + +function useEtherscanProvider () { + log.debug(`background.useEtherscanProvider`) + background.useEtherscanProvider() + return { + type: actions.USE_ETHERSCAN_PROVIDER, + } +} + +function showLoadingIndication (message) { + return { + type: actions.SHOW_LOADING, + value: message, + } +} + +function hideLoadingIndication () { + return { + type: actions.HIDE_LOADING, + } +} + +function showSubLoadingIndication () { + return { + type: actions.SHOW_SUB_LOADING_INDICATION, + } +} + +function hideSubLoadingIndication () { + return { + type: actions.HIDE_SUB_LOADING_INDICATION, + } +} + +function displayWarning (text) { + return { + type: actions.DISPLAY_WARNING, + value: text, + } +} + +function hideWarning () { + return { + type: actions.HIDE_WARNING, + } +} + +function requestExportAccount () { + return { + type: actions.REQUEST_ACCOUNT_EXPORT, + } +} + +function exportAccount (password, address) { + var self = this + + return function (dispatch) { + dispatch(self.showLoadingIndication()) + + log.debug(`background.submitPassword`) + background.submitPassword(password, function (err) { + if (err) { + log.error('Error in submiting password.') + dispatch(self.hideLoadingIndication()) + return dispatch(self.displayWarning('Incorrect Password.')) + } + log.debug(`background.exportAccount`) + background.exportAccount(address, function (err, result) { + dispatch(self.hideLoadingIndication()) + + if (err) { + log.error(err) + return dispatch(self.displayWarning('Had a problem exporting the account.')) + } + + dispatch(self.showPrivateKey(result)) + }) + }) + } +} + +function showPrivateKey (key) { + return { + type: actions.SHOW_PRIVATE_KEY, + value: key, + } +} + +function saveAccountLabel (account, label) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.saveAccountLabel`) + background.saveAccountLabel(account, label, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch({ + type: actions.SAVE_ACCOUNT_LABEL, + value: { account, label }, + }) + }) + } +} + +function showSendPage () { + return { + type: actions.SHOW_SEND_PAGE, + } +} + +function buyEth (opts) { + return (dispatch) => { + const url = getBuyEthUrl(opts) + global.platform.openWindow({ url }) + dispatch({ + type: actions.BUY_ETH, + }) + } +} + +function buyEthView (address) { + return { + type: actions.BUY_ETH_VIEW, + value: address, + } +} + +function coinBaseSubview () { + return { + type: actions.COINBASE_SUBVIEW, + } +} + +function pairUpdate (coin) { + return (dispatch) => { + dispatch(actions.showSubLoadingIndication()) + dispatch(actions.hideWarning()) + shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { + dispatch(actions.hideSubLoadingIndication()) + dispatch({ + type: actions.PAIR_UPDATE, + value: { + marketinfo: mktResponse, + }, + }) + }) + } +} + +function shapeShiftSubview (network) { + var pair = 'btc_eth' + + return (dispatch) => { + dispatch(actions.showSubLoadingIndication()) + shapeShiftRequest('marketinfo', {pair}, (mktResponse) => { + shapeShiftRequest('getcoins', {}, (response) => { + dispatch(actions.hideSubLoadingIndication()) + if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) + dispatch({ + type: actions.SHAPESHIFT_SUBVIEW, + value: { + marketinfo: mktResponse, + coinOptions: response, + }, + }) + }) + }) + } +} + +function coinShiftRquest (data, marketData) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + shapeShiftRequest('shift', { method: 'POST', data}, (response) => { + dispatch(actions.hideLoadingIndication()) + if (response.error) return dispatch(actions.displayWarning(response.error)) + var message = ` + Deposit your ${response.depositType} to the address bellow:` + log.debug(`background.createShapeShiftTx`) + background.createShapeShiftTx(response.deposit, response.depositType) + dispatch(actions.showQrView(response.deposit, [message].concat(marketData))) + }) + } +} + +function showQrView (data, message) { + return { + type: actions.SHOW_QR_VIEW, + value: { + message: message, + data: data, + }, + } +} +function reshowQrCode (data, coin) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { + if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) + + var message = [ + `Deposit your ${coin} to the address bellow:`, + `Deposit Limit: ${mktResponse.limit}`, + `Deposit Minimum:${mktResponse.minimum}`, + ] + + dispatch(actions.hideLoadingIndication()) + return dispatch(actions.showQrView(data, message)) + }) + } +} + +function shapeShiftRequest (query, options, cb) { + var queryResponse, method + !options ? options = {} : null + options.method ? method = options.method : method = 'GET' + + var requestListner = function (request) { + queryResponse = JSON.parse(this.responseText) + cb ? cb(queryResponse) : null + return queryResponse + } + + var shapShiftReq = new XMLHttpRequest() + shapShiftReq.addEventListener('load', requestListner) + shapShiftReq.open(method, `https://shapeshift.io/${query}/${options.pair ? options.pair : ''}`, true) + + if (options.method === 'POST') { + var jsonObj = JSON.stringify(options.data) + shapShiftReq.setRequestHeader('Content-Type', 'application/json') + return shapShiftReq.send(jsonObj) + } else { + return shapShiftReq.send() + } +} + +// Call Background Then Update +// +// A function generator for a common pattern wherein: +// We show loading indication. +// We call a background method. +// We hide loading indication. +// If it errored, we show a warning. +// If it didn't, we update the state. +function callBackgroundThenUpdateNoSpinner (method, ...args) { + return (dispatch) => { + method.call(background, ...args, (err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + forceUpdateMetamaskState(dispatch) + }) + } +} + +function callBackgroundThenUpdate (method, ...args) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + method.call(background, ...args, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + forceUpdateMetamaskState(dispatch) + }) + } +} + +function forceUpdateMetamaskState (dispatch) { + log.debug(`background.getState`) + background.getState((err, newState) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch(actions.updateMetamaskState(newState)) + }) +} diff --git a/ui/responsive/app/add-token.js b/ui/responsive/app/add-token.js new file mode 100644 index 000000000..b303b5c0d --- /dev/null +++ b/ui/responsive/app/add-token.js @@ -0,0 +1,219 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') + +const ethUtil = require('ethereumjs-util') +const abi = require('human-standard-token-abi') +const Eth = require('ethjs-query') +const EthContract = require('ethjs-contract') + +const emptyAddr = '0x0000000000000000000000000000000000000000' + +module.exports = connect(mapStateToProps)(AddTokenScreen) + +function mapStateToProps (state) { + return { + } +} + +inherits(AddTokenScreen, Component) +function AddTokenScreen () { + this.state = { + warning: null, + address: null, + symbol: 'TOKEN', + decimals: 18, + } + Component.call(this) +} + +AddTokenScreen.prototype.render = function () { + const state = this.state + const props = this.props + const { warning, symbol, decimals } = state + + return ( + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + props.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Add Token'), + ]), + + h('.error', { + style: { + display: warning ? 'block' : 'none', + padding: '0 20px', + textAlign: 'center', + }, + }, warning), + + // conf view + h('.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-space-around', { + style: { + padding: '20px', + }, + }, [ + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Token Address'), + ]), + + h('section.flex-row.flex-center', [ + h('input#token-address', { + name: 'address', + placeholder: 'Token Address', + onChange: this.tokenAddressDidChange.bind(this), + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + }), + ]), + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Token Sybmol'), + ]), + + h('div', { style: {display: 'flex'} }, [ + h('input#token_symbol', { + placeholder: `Like "ETH"`, + value: symbol, + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onChange: (event) => { + var element = event.target + var symbol = element.value + this.setState({ symbol }) + }, + }), + ]), + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Decimals of Precision'), + ]), + + h('div', { style: {display: 'flex'} }, [ + h('input#token_decimals', { + value: decimals, + type: 'number', + min: 0, + max: 36, + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onChange: (event) => { + var element = event.target + var decimals = element.value.trim() + this.setState({ decimals }) + }, + }), + ]), + + h('button', { + style: { + alignSelf: 'center', + }, + onClick: (event) => { + const valid = this.validateInputs() + if (!valid) return + + const { address, symbol, decimals } = this.state + this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) + }, + }, 'Add'), + ]), + ]), + ]) + ) +} + +AddTokenScreen.prototype.componentWillMount = function () { + if (typeof global.ethereumProvider === 'undefined') return + + this.eth = new Eth(global.ethereumProvider) + this.contract = new EthContract(this.eth) + this.TokenContract = this.contract(abi) +} + +AddTokenScreen.prototype.tokenAddressDidChange = function (event) { + const el = event.target + const address = el.value.trim() + if (ethUtil.isValidAddress(address) && address !== emptyAddr) { + this.setState({ address }) + this.attemptToAutoFillTokenParams(address) + } +} + +AddTokenScreen.prototype.validateInputs = function () { + let msg = '' + const state = this.state + const { address, symbol, decimals } = state + + const validAddress = ethUtil.isValidAddress(address) + if (!validAddress) { + msg += 'Address is invalid. ' + } + + const validDecimals = decimals >= 0 && decimals < 36 + if (!validDecimals) { + msg += 'Decimals must be at least 0, and not over 36. ' + } + + const symbolLen = symbol.trim().length + const validSymbol = symbolLen > 0 && symbolLen < 10 + if (!validSymbol) { + msg += 'Symbol must be between 0 and 10 characters.' + } + + const isValid = validAddress && validDecimals + + if (!isValid) { + this.setState({ + warning: msg, + }) + } else { + this.setState({ warning: null }) + } + + return isValid +} + +AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) { + const contract = this.TokenContract.at(address) + + const results = await Promise.all([ + contract.symbol(), + contract.decimals(), + ]) + + const [ symbol, decimals ] = results + if (symbol && decimals) { + console.log('SETTING SYMBOL AND DECIMALS', { symbol, decimals }) + this.setState({ symbol: symbol[0], decimals: decimals[0].toString() }) + } +} + diff --git a/ui/responsive/app/app.js b/ui/responsive/app/app.js new file mode 100644 index 000000000..1a63002e1 --- /dev/null +++ b/ui/responsive/app/app.js @@ -0,0 +1,591 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('./actions') +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +// init +const InitializeMenuScreen = require('./first-time/init-menu') +const NewKeyChainScreen = require('./new-keychain') +// unlock +const UnlockScreen = require('./unlock') +// accounts +const AccountsScreen = require('./accounts') +const AccountDetailScreen = require('./account-detail') +const SendTransactionScreen = require('./send') +const ConfirmTxScreen = require('./conf-tx') +// notice +const NoticeScreen = require('./components/notice') +const generateLostAccountsNotice = require('../lib/lost-accounts-notice') +// other views +const ConfigScreen = require('./config') +const AddTokenScreen = require('./add-token') +const Import = require('./accounts/import') +const InfoScreen = require('./info') +const Loading = require('./components/loading') +const SandwichExpando = require('sandwich-expando') +const MenuDroppo = require('menu-droppo') +const DropMenuItem = require('./components/drop-menu-item') +const NetworkIndicator = require('./components/network') +const Tooltip = require('./components/tooltip') +const BuyView = require('./components/buy-button-subview') +const QrView = require('./components/qr-code') +const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete') +const HDRestoreVaultScreen = require('./keychains/hd/restore-vault') +const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation') + +module.exports = connect(mapStateToProps)(App) + +inherits(App, Component) +function App () { Component.call(this) } + +function mapStateToProps (state) { + return { + // state from plugin + isLoading: state.appState.isLoading, + loadingMessage: state.appState.loadingMessage, + noActiveNotices: state.metamask.noActiveNotices, + isInitialized: state.metamask.isInitialized, + isUnlocked: state.metamask.isUnlocked, + currentView: state.appState.currentView, + activeAddress: state.appState.activeAddress, + transForward: state.appState.transForward, + seedWords: state.metamask.seedWords, + unapprovedTxs: state.metamask.unapprovedTxs, + unapprovedMsgs: state.metamask.unapprovedMsgs, + menuOpen: state.appState.menuOpen, + network: state.metamask.network, + provider: state.metamask.provider, + forgottenPassword: state.appState.forgottenPassword, + lastUnreadNotice: state.metamask.lastUnreadNotice, + lostAccounts: state.metamask.lostAccounts, + frequentRpcList: state.metamask.frequentRpcList || [], + } +} + +App.prototype.render = function () { + var props = this.props + const { isLoading, loadingMessage, transForward, network } = props + const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config' + const loadMessage = loadingMessage || isLoadingNetwork ? + `Connecting to ${this.getNetworkName()}` : null + + log.debug('Main ui render function') + + return ( + + h('.flex-column.flex-grow.full-height', { + style: { + // Windows was showing a vertical scroll bar: + overflow: 'hidden', + position: 'relative', + }, + }, [ + + // app bar + this.renderAppBar(), + this.renderNetworkDropdown(), + this.renderDropdown(), + + h(Loading, { + isLoading: isLoading || isLoadingNetwork, + loadingMessage: loadMessage, + }), + + // panel content + h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), { + style: { + height: '380px', + width: '360px', + }, + }, [ + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + this.renderPrimary(), + ]), + ]), + ]) + ) +} + +App.prototype.renderAppBar = function () { + if (window.METAMASK_UI_TYPE === 'notification') { + return null + } + + const props = this.props + const state = this.state || {} + const isNetworkMenuOpen = state.isNetworkMenuOpen || false + + return ( + + h('div', [ + + h('.app-header.flex-row.flex-space-between', { + style: { + alignItems: 'center', + visibility: props.isUnlocked ? 'visible' : 'none', + background: props.isUnlocked ? 'white' : 'none', + height: '36px', + position: 'relative', + zIndex: 12, + }, + }, [ + + h('div.left-menu-section', { + style: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + }, + }, [ + + // mini logo + h('img', { + height: 24, + width: 24, + src: '/images/icon-128.png', + }), + + h(NetworkIndicator, { + network: this.props.network, + provider: this.props.provider, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen }) + }, + }), + ]), + + // metamask name + props.isUnlocked && h('h1', { + style: { + position: 'relative', + left: '9px', + }, + }, 'MetaMask'), + + props.isUnlocked && h('div', { + style: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + }, + }, [ + + // small accounts nav + props.isUnlocked && h(Tooltip, { title: 'Switch Accounts' }, [ + h('img.cursor-pointer.color-orange', { + src: 'images/switch_acc.svg', + style: { + width: '23.5px', + marginRight: '8px', + }, + onClick: (event) => { + event.stopPropagation() + this.props.dispatch(actions.showAccountsPage()) + }, + }), + ]), + + // hamburger + props.isUnlocked && h(SandwichExpando, { + width: 16, + barHeight: 2, + padding: 0, + isOpen: state.isMainMenuOpen, + color: 'rgb(247,146,30)', + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + this.setState({ isMainMenuOpen: !state.isMainMenuOpen }) + }, + }), + ]), + ]), + ]) + ) +} + +App.prototype.renderNetworkDropdown = function () { + const props = this.props + const rpcList = props.frequentRpcList + const state = this.state || {} + const isOpen = state.isNetworkMenuOpen + + return h(MenuDroppo, { + isOpen, + onClickOutside: (event) => { + this.setState({ isNetworkMenuOpen: !isOpen }) + }, + zIndex: 11, + style: { + position: 'absolute', + left: 0, + top: '36px', + }, + innerStyle: { + background: 'white', + boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', + }, + }, [ // DROP MENU ITEMS + h('style', ` + .drop-menu-item:hover { background:rgb(235, 235, 235); } + .drop-menu-item i { margin: 11px; } + `), + + h(DropMenuItem, { + label: 'Main Ethereum Network', + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setProviderType('mainnet')), + icon: h('.menu-icon.diamond'), + activeNetworkRender: props.network, + provider: props.provider, + }), + + h(DropMenuItem, { + label: 'Ropsten Test Network', + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setProviderType('ropsten')), + icon: h('.menu-icon.red-dot'), + activeNetworkRender: props.network, + provider: props.provider, + }), + + h(DropMenuItem, { + label: 'Kovan Test Network', + closeMenu: () => this.setState({ isNetworkMenuOpen: false}), + action: () => props.dispatch(actions.setProviderType('kovan')), + icon: h('.menu-icon.hollow-diamond'), + activeNetworkRender: props.network, + provider: props.provider, + }), + + h(DropMenuItem, { + label: 'Rinkeby Test Network', + closeMenu: () => this.setState({ isNetworkMenuOpen: false}), + action: () => props.dispatch(actions.setProviderType('rinkeby')), + icon: h('.menu-icon.golden-square'), + activeNetworkRender: props.network, + provider: props.provider, + }), + + h(DropMenuItem, { + label: 'Localhost 8545', + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setDefaultRpcTarget(rpcList)), + icon: h('i.fa.fa-question-circle.fa-lg'), + activeNetworkRender: props.provider.rpcTarget, + }), + + this.renderCustomOption(props.provider), + this.renderCommonRpc(rpcList, props.provider), + + h(DropMenuItem, { + label: 'Custom RPC', + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => this.props.dispatch(actions.showConfigPage()), + icon: h('i.fa.fa-question-circle.fa-lg'), + }), + + ]) +} + +App.prototype.renderDropdown = function () { + const state = this.state || {} + const isOpen = state.isMainMenuOpen + + return h(MenuDroppo, { + isOpen: isOpen, + zIndex: 11, + onClickOutside: (event) => { + this.setState({ isMainMenuOpen: !isOpen }) + }, + style: { + position: 'absolute', + right: 0, + top: '36px', + }, + innerStyle: { + background: 'white', + boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', + }, + }, [ // DROP MENU ITEMS + h('style', ` + .drop-menu-item:hover { background:rgb(235, 235, 235); } + .drop-menu-item i { margin: 11px; } + `), + + h(DropMenuItem, { + label: 'Settings', + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + action: () => this.props.dispatch(actions.showConfigPage()), + icon: h('i.fa.fa-gear.fa-lg'), + }), + + h(DropMenuItem, { + label: 'Import Account', + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + action: () => this.props.dispatch(actions.showImportPage()), + icon: h('i.fa.fa-arrow-circle-o-up.fa-lg'), + }), + + h(DropMenuItem, { + label: 'Lock', + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + action: () => this.props.dispatch(actions.lockMetamask()), + icon: h('i.fa.fa-lock.fa-lg'), + }), + + h(DropMenuItem, { + label: 'Info/Help', + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + action: () => this.props.dispatch(actions.showInfoPage()), + icon: h('i.fa.fa-question.fa-lg'), + }), + ]) +} + +App.prototype.renderBackButton = function (style, justArrow = false) { + var props = this.props + return ( + h('.flex-row', { + key: 'leftArrow', + style: style, + onClick: () => props.dispatch(actions.goBackToInitView()), + }, [ + h('i.fa.fa-arrow-left.cursor-pointer'), + justArrow ? null : h('div.cursor-pointer', { + style: { + marginLeft: '3px', + }, + onClick: () => props.dispatch(actions.goBackToInitView()), + }, 'BACK'), + ]) + ) +} + +App.prototype.renderPrimary = function () { + log.debug('rendering primary') + var props = this.props + + // notices + if (!props.noActiveNotices) { + log.debug('rendering notice screen for unread notices.') + return h(NoticeScreen, { + notice: props.lastUnreadNotice, + key: 'NoticeScreen', + onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)), + }) + } else if (props.lostAccounts && props.lostAccounts.length > 0) { + log.debug('rendering notice screen for lost accounts view.') + return h(NoticeScreen, { + notice: generateLostAccountsNotice(props.lostAccounts), + key: 'LostAccountsNotice', + onConfirm: () => props.dispatch(actions.markAccountsFound()), + }) + } + + if (props.seedWords) { + log.debug('rendering seed words') + return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'}) + } + + // show initialize screen + if (!props.isInitialized || props.forgottenPassword) { + // show current view + log.debug('rendering an initialize screen') + switch (props.currentView.name) { + + case 'restoreVault': + log.debug('rendering restore vault screen') + return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) + + default: + log.debug('rendering menu screen') + return h(InitializeMenuScreen, {key: 'menuScreenInit'}) + } + } + + // show unlock screen + if (!props.isUnlocked) { + switch (props.currentView.name) { + + case 'restoreVault': + log.debug('rendering restore vault screen') + return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) + + case 'config': + log.debug('rendering config screen from unlock screen.') + return h(ConfigScreen, {key: 'config'}) + + default: + log.debug('rendering locked screen') + return h(UnlockScreen, {key: 'locked'}) + } + } + + // show current view + switch (props.currentView.name) { + + case 'accounts': + log.debug('rendering accounts screen') + return h(AccountsScreen, {key: 'accounts'}) + + case 'accountDetail': + log.debug('rendering account detail screen') + return h(AccountDetailScreen, {key: 'account-detail'}) + + case 'sendTransaction': + log.debug('rendering send tx screen') + return h(SendTransactionScreen, {key: 'send-transaction'}) + + case 'newKeychain': + log.debug('rendering new keychain screen') + return h(NewKeyChainScreen, {key: 'new-keychain'}) + + case 'confTx': + log.debug('rendering confirm tx screen') + return h(ConfirmTxScreen, {key: 'confirm-tx'}) + + case 'add-token': + log.debug('rendering add-token screen from unlock screen.') + return h(AddTokenScreen, {key: 'add-token'}) + + case 'config': + log.debug('rendering config screen') + return h(ConfigScreen, {key: 'config'}) + + case 'import-menu': + log.debug('rendering import screen') + return h(Import, {key: 'import-menu'}) + + case 'reveal-seed-conf': + log.debug('rendering reveal seed confirmation screen') + return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'}) + + case 'info': + log.debug('rendering info screen') + return h(InfoScreen, {key: 'info'}) + + case 'buyEth': + log.debug('rendering buy ether screen') + return h(BuyView, {key: 'buyEthView'}) + + case 'qr': + log.debug('rendering show qr screen') + return h('div', { + style: { + position: 'absolute', + height: '100%', + top: '0px', + left: '0px', + }, + }, [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { + onClick: () => props.dispatch(actions.backToAccountDetail(props.activeAddress)), + style: { + marginLeft: '10px', + marginTop: '50px', + }, + }), + h('div', { + style: { + position: 'absolute', + left: '44px', + width: '285px', + }, + }, [ + h(QrView, {key: 'qr'}), + ]), + ]) + + default: + log.debug('rendering default, account detail screen') + return h(AccountDetailScreen, {key: 'account-detail'}) + } +} + +App.prototype.toggleMetamaskActive = function () { + if (!this.props.isUnlocked) { + // currently inactive: redirect to password box + var passwordBox = document.querySelector('input[type=password]') + if (!passwordBox) return + passwordBox.focus() + } else { + // currently active: deactivate + this.props.dispatch(actions.lockMetamask(false)) + } +} + +App.prototype.renderCustomOption = function (provider) { + const { rpcTarget, type } = provider + if (type !== 'rpc') return null + + // Concatenate long URLs + let label = rpcTarget + if (rpcTarget.length > 31) { + label = label.substr(0, 34) + '...' + } + + switch (rpcTarget) { + + case 'http://localhost:8545': + return null + + default: + return h(DropMenuItem, { + label, + key: rpcTarget, + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + icon: h('i.fa.fa-question-circle.fa-lg'), + activeNetworkRender: 'custom', + }) + } +} + +App.prototype.getNetworkName = function () { + const { provider } = this.props + const providerName = provider.type + + let name + + if (providerName === 'mainnet') { + name = 'Main Ethereum Network' + } else if (providerName === 'ropsten') { + name = 'Ropsten Test Network' + } else if (providerName === 'kovan') { + name = 'Kovan Test Network' + } else if (providerName === 'rinkeby') { + name = 'Rinkeby Test Network' + } else { + name = 'Unknown Private Network' + } + + return name +} + +App.prototype.renderCommonRpc = function (rpcList, provider) { + const { rpcTarget } = provider + const props = this.props + + return rpcList.map((rpc) => { + if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { + return null + } else { + return h(DropMenuItem, { + label: rpc, + key: rpc, + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setRpcTarget(rpc)), + icon: h('i.fa.fa-question-circle.fa-lg'), + activeNetworkRender: rpc, + }) + } + }) +} diff --git a/ui/responsive/app/components/account-export.js b/ui/responsive/app/components/account-export.js new file mode 100644 index 000000000..394d878f7 --- /dev/null +++ b/ui/responsive/app/components/account-export.js @@ -0,0 +1,122 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const copyToClipboard = require('copy-to-clipboard') +const actions = require('../actions') +const ethUtil = require('ethereumjs-util') +const connect = require('react-redux').connect + +module.exports = connect(mapStateToProps)(ExportAccountView) + +inherits(ExportAccountView, Component) +function ExportAccountView () { + Component.call(this) +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +ExportAccountView.prototype.render = function () { + var state = this.props + var accountDetail = state.accountDetail + + if (!accountDetail) return h('div') + var accountExport = accountDetail.accountExport + + var notExporting = accountExport === 'none' + var exportRequested = accountExport === 'requested' + var accountExported = accountExport === 'completed' + + if (notExporting) return h('div') + + if (exportRequested) { + var warning = `Export private keys at your own risk.` + return ( + h('div', { + style: { + display: 'inline-block', + textAlign: 'center', + }, + }, + [ + h('div', { + key: 'exporting', + style: { + margin: '0 20px', + }, + }, [ + h('p.error', warning), + h('input#exportAccount.sizing-input', { + type: 'password', + placeholder: 'confirm password', + onKeyPress: this.onExportKeyPress.bind(this), + style: { + position: 'relative', + top: '1.5px', + marginBottom: '7px', + }, + }), + ]), + h('div', { + key: 'buttons', + style: { + margin: '0 20px', + }, + }, + [ + h('button', { + onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }), + style: { + marginRight: '10px', + }, + }, 'Submit'), + h('button', { + onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), + }, 'Cancel'), + ]), + (this.props.warning) && ( + h('span.error', { + style: { + margin: '20px', + }, + }, this.props.warning.split('-')) + ), + ]) + ) + } + + if (accountExported) { + return h('div.privateKey', { + style: { + margin: '0 20px', + }, + }, [ + h('label', 'Your private key (click to copy):'), + h('p.error.cursor-pointer', { + style: { + textOverflow: 'ellipsis', + overflow: 'hidden', + webkitUserSelect: 'text', + width: '100%', + }, + onClick: function (event) { + copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey)) + }, + }, ethUtil.stripHexPrefix(accountDetail.privateKey)), + h('button', { + onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), + }, 'Done'), + ]) + } +} + +ExportAccountView.prototype.onExportKeyPress = function (event) { + if (event.key !== 'Enter') return + event.preventDefault() + + var input = document.getElementById('exportAccount').value + this.props.dispatch(actions.exportAccount(input, this.props.address)) +} diff --git a/ui/responsive/app/components/account-info-link.js b/ui/responsive/app/components/account-info-link.js new file mode 100644 index 000000000..6526ab502 --- /dev/null +++ b/ui/responsive/app/components/account-info-link.js @@ -0,0 +1,41 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Tooltip = require('./tooltip') +const genAccountLink = require('../../lib/account-link') + +module.exports = AccountInfoLink + +inherits(AccountInfoLink, Component) +function AccountInfoLink () { + Component.call(this) +} + +AccountInfoLink.prototype.render = function () { + const { selected, network } = this.props + const title = 'View account on Etherscan' + const url = genAccountLink(selected, network) + + if (!url) { + return null + } + + return h('.account-info-link', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + + h(Tooltip, { + title, + }, [ + h('i.fa.fa-info-circle.cursor-pointer.color-orange', { + style: { + margin: '5px', + }, + onClick () { global.platform.openWindow({ url }) }, + }), + ]), + ]) +} diff --git a/ui/responsive/app/components/account-panel.js b/ui/responsive/app/components/account-panel.js new file mode 100644 index 000000000..abaaf8163 --- /dev/null +++ b/ui/responsive/app/components/account-panel.js @@ -0,0 +1,86 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const Identicon = require('./identicon') +const formatBalance = require('../util').formatBalance +const addressSummary = require('../util').addressSummary + +module.exports = AccountPanel + + +inherits(AccountPanel, Component) +function AccountPanel () { + Component.call(this) +} + +AccountPanel.prototype.render = function () { + var state = this.props + var identity = state.identity || {} + var account = state.account || {} + var isFauceting = state.isFauceting + + var panelState = { + key: `accountPanel${identity.address}`, + identiconKey: identity.address, + identiconLabel: identity.name || '', + attributes: [ + { + key: 'ADDRESS', + value: addressSummary(identity.address), + }, + balanceOrFaucetingIndication(account, isFauceting), + ], + } + + return ( + + h('.identity-panel.flex-row.flex-space-between', { + style: { + flex: '1 0 auto', + cursor: panelState.onClick ? 'pointer' : undefined, + }, + onClick: panelState.onClick, + }, [ + + // account identicon + h('.identicon-wrapper.flex-column.select-none', [ + h(Identicon, { + address: panelState.identiconKey, + imageify: state.imageifyIdenticons, + }), + h('span.font-small', panelState.identiconLabel.substring(0, 7) + '...'), + ]), + + // account address, balance + h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [ + + panelState.attributes.map((attr) => { + return h('.flex-row.flex-space-between', { + key: '' + Math.round(Math.random() * 1000000), + }, [ + h('label.font-small.no-select', attr.key), + h('span.font-small', attr.value), + ]) + }), + ]), + + ]) + + ) +} + +function balanceOrFaucetingIndication (account, isFauceting) { + // Temporarily deactivating isFauceting indication + // because it shows fauceting for empty restored accounts. + if (/* isFauceting*/ false) { + return { + key: 'Account is auto-funding.', + value: 'Please wait.', + } + } else { + return { + key: 'BALANCE', + value: formatBalance(account.balance), + } + } +} diff --git a/ui/responsive/app/components/balance.js b/ui/responsive/app/components/balance.js new file mode 100644 index 000000000..57ca84564 --- /dev/null +++ b/ui/responsive/app/components/balance.js @@ -0,0 +1,89 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const formatBalance = require('../util').formatBalance +const generateBalanceObject = require('../util').generateBalanceObject +const Tooltip = require('./tooltip.js') +const FiatValue = require('./fiat-value.js') + +module.exports = EthBalanceComponent + +inherits(EthBalanceComponent, Component) +function EthBalanceComponent () { + Component.call(this) +} + +EthBalanceComponent.prototype.render = function () { + var props = this.props + let { value } = props + var style = props.style + var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true + value = value ? formatBalance(value, 6, needsParse) : '...' + var width = props.width + + return ( + + h('.ether-balance.ether-balance-amount', { + style: style, + }, [ + h('div', { + style: { + display: 'inline', + width: width, + }, + }, this.renderBalance(value)), + ]) + + ) +} +EthBalanceComponent.prototype.renderBalance = function (value) { + var props = this.props + if (value === 'None') return value + if (value === '...') return value + var balanceObj = generateBalanceObject(value, props.shorten ? 1 : 3) + var balance + var splitBalance = value.split(' ') + var ethNumber = splitBalance[0] + var ethSuffix = splitBalance[1] + const showFiat = 'showFiat' in props ? props.showFiat : true + + if (props.shorten) { + balance = balanceObj.shortBalance + } else { + balance = balanceObj.balance + } + + var label = balanceObj.label + + return ( + h(Tooltip, { + position: 'bottom', + title: `${ethNumber} ${ethSuffix}`, + }, h('div.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('div', { + style: { + width: '100%', + textAlign: 'right', + }, + }, this.props.incoming ? `+${balance}` : balance), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + }, + }, label), + ]), + + showFiat ? h(FiatValue, { value: props.value }) : null, + ])) + ) +} diff --git a/ui/responsive/app/components/binary-renderer.js b/ui/responsive/app/components/binary-renderer.js new file mode 100644 index 000000000..0b6a1f5c2 --- /dev/null +++ b/ui/responsive/app/components/binary-renderer.js @@ -0,0 +1,46 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const extend = require('xtend') + +module.exports = BinaryRenderer + +inherits(BinaryRenderer, Component) +function BinaryRenderer () { + Component.call(this) +} + +BinaryRenderer.prototype.render = function () { + const props = this.props + const { value, style } = props + const text = this.hexToText(value) + + const defaultStyle = extend({ + width: '315px', + maxHeight: '210px', + resize: 'none', + border: 'none', + background: 'white', + padding: '3px', + }, style) + + return ( + h('textarea.font-small', { + readOnly: true, + style: defaultStyle, + defaultValue: text, + }) + ) +} + +BinaryRenderer.prototype.hexToText = function (hex) { + try { + const stripped = ethUtil.stripHexPrefix(hex) + const buff = Buffer.from(stripped, 'hex') + return buff.toString('utf8') + } catch (e) { + return hex + } +} + diff --git a/ui/responsive/app/components/bn-as-decimal-input.js b/ui/responsive/app/components/bn-as-decimal-input.js new file mode 100644 index 000000000..f3ace4720 --- /dev/null +++ b/ui/responsive/app/components/bn-as-decimal-input.js @@ -0,0 +1,174 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const extend = require('xtend') + +module.exports = BnAsDecimalInput + +inherits(BnAsDecimalInput, Component) +function BnAsDecimalInput () { + this.state = { invalid: null } + Component.call(this) +} + +/* Bn as Decimal Input + * + * A component for allowing easy, decimal editing + * of a passed in bn string value. + * + * On change, calls back its `onChange` function parameter + * and passes it an updated bn string. + */ + +BnAsDecimalInput.prototype.render = function () { + const props = this.props + const state = this.state + + const { value, scale, precision, onChange, min, max } = props + + const suffix = props.suffix + const style = props.style + const valueString = value.toString(10) + const newValue = this.downsize(valueString, scale, precision) + + return ( + h('.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('input.hex-input', { + type: 'number', + step: 'any', + required: true, + min, + max, + style: extend({ + display: 'block', + textAlign: 'right', + backgroundColor: 'transparent', + border: '1px solid #bdbdbd', + + }, style), + value: newValue, + onBlur: (event) => { + this.updateValidity(event) + }, + onChange: (event) => { + this.updateValidity(event) + const value = (event.target.value === '') ? '' : event.target.value + + + const scaledNumber = this.upsize(value, scale, precision) + const precisionBN = new BN(scaledNumber, 10) + onChange(precisionBN, event.target.checkValidity()) + }, + onInvalid: (event) => { + const msg = this.constructWarning() + if (msg === state.invalid) { + return + } + this.setState({ invalid: msg }) + event.preventDefault() + return false + }, + }), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + marginRight: '6px', + width: '20px', + }, + }, suffix), + ]), + + state.invalid ? h('span.error', { + style: { + position: 'absolute', + right: '0px', + textAlign: 'right', + transform: 'translateY(26px)', + padding: '3px', + background: 'rgba(255,255,255,0.85)', + zIndex: '1', + textTransform: 'capitalize', + border: '2px solid #E20202', + }, + }, state.invalid) : null, + ]) + ) +} + +BnAsDecimalInput.prototype.setValid = function (message) { + this.setState({ invalid: null }) +} + +BnAsDecimalInput.prototype.updateValidity = function (event) { + const target = event.target + const value = this.props.value + const newValue = target.value + + if (value === newValue) { + return + } + + const valid = target.checkValidity() + + if (valid) { + this.setState({ invalid: null }) + } +} + +BnAsDecimalInput.prototype.constructWarning = function () { + const { name, min, max } = this.props + let message = name ? name + ' ' : '' + + if (min && max) { + message += `must be greater than or equal to ${min} and less than or equal to ${max}.` + } else if (min) { + message += `must be greater than or equal to ${min}.` + } else if (max) { + message += `must be less than or equal to ${max}.` + } else { + message += 'Invalid input.' + } + + return message +} + + +BnAsDecimalInput.prototype.downsize = function (number, scale, precision) { + // if there is no scaling, simply return the number + if (scale === 0) { + return Number(number) + } else { + // if the scale is the same as the precision, account for this edge case. + var decimals = (scale === precision) ? -1 : scale - precision + return Number(number.slice(0, -scale) + '.' + number.slice(-scale, decimals)) + } +} + +BnAsDecimalInput.prototype.upsize = function (number, scale, precision) { + var stringArray = number.toString().split('.') + var decimalLength = stringArray[1] ? stringArray[1].length : 0 + var newString = stringArray[0] + + // If there is scaling and decimal parts exist, integrate them in. + if ((scale !== 0) && (decimalLength !== 0)) { + newString += stringArray[1].slice(0, precision) + } + + // Add 0s to account for the upscaling. + for (var i = decimalLength; i < scale; i++) { + newString += '0' + } + return newString +} diff --git a/ui/responsive/app/components/buy-button-subview.js b/ui/responsive/app/components/buy-button-subview.js new file mode 100644 index 000000000..87084f92d --- /dev/null +++ b/ui/responsive/app/components/buy-button-subview.js @@ -0,0 +1,197 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const actions = require('../actions') +const CoinbaseForm = require('./coinbase-form') +const ShapeshiftForm = require('./shapeshift-form') +const Loading = require('./loading') +const AccountPanel = require('./account-panel') +const RadioList = require('./custom-radio-list') + +module.exports = connect(mapStateToProps)(BuyButtonSubview) + +function mapStateToProps (state) { + return { + identity: state.appState.identity, + account: state.metamask.accounts[state.appState.buyView.buyAddress], + warning: state.appState.warning, + buyView: state.appState.buyView, + network: state.metamask.network, + provider: state.metamask.provider, + context: state.appState.currentView.context, + isSubLoading: state.appState.isSubLoading, + } +} + +inherits(BuyButtonSubview, Component) +function BuyButtonSubview () { + Component.call(this) +} + +BuyButtonSubview.prototype.render = function () { + const props = this.props + const isLoading = props.isSubLoading + + return ( + h('.buy-eth-section.flex-column', { + style: { + alignItems: 'center', + }, + }, [ + // back button + h('.flex-row', { + style: { + alignItems: 'center', + justifyContent: 'center', + }, + }, [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { + onClick: this.backButtonContext.bind(this), + style: { + position: 'absolute', + left: '10px', + }, + }), + h('h2.text-transform-uppercase.flex-center', { + style: { + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', + }, + }, 'Buy Eth'), + ]), + h('div', { + style: { + position: 'absolute', + top: '57vh', + left: '49vw', + }, + }, [ + h(Loading, {isLoading}), + ]), + h('div', { + style: { + width: '80%', + }, + }, [ + h(AccountPanel, { + showFullAddress: true, + identity: props.identity, + account: props.account, + }), + ]), + h('h3.text-transform-uppercase', { + style: { + paddingLeft: '15px', + fontFamily: 'Montserrat Light', + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', + }, + }, 'Select Service'), + h('.flex-row.selected-exchange', { + style: { + position: 'relative', + right: '35px', + marginTop: '20px', + marginBottom: '20px', + }, + }, [ + h(RadioList, { + defaultFocus: props.buyView.subview, + labels: [ + 'Coinbase', + 'ShapeShift', + ], + subtext: { + 'Coinbase': 'Crypto/FIAT (USA only)', + 'ShapeShift': 'Crypto', + }, + onClick: this.radioHandler.bind(this), + }), + ]), + h('h3.text-transform-uppercase', { + style: { + paddingLeft: '15px', + fontFamily: 'Montserrat Light', + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', + }, + }, props.buyView.subview), + this.formVersionSubview(), + ]) + ) +} + +BuyButtonSubview.prototype.formVersionSubview = function () { + const network = this.props.network + if (network === '1') { + if (this.props.buyView.formView.coinbase) { + return h(CoinbaseForm, this.props) + } else if (this.props.buyView.formView.shapeshift) { + return h(ShapeshiftForm, this.props) + } + } else { + return h('div.flex-column', { + style: { + alignItems: 'center', + margin: '50px', + }, + }, [ + h('h3.text-transform-uppercase', { + style: { + width: '225px', + marginBottom: '15px', + }, + }, 'In order to access this feature, please switch to the Main Network'), + ((network === '3') || (network === '4') || (network === '42')) ? h('h3.text-transform-uppercase', 'or go to the') : null, + (network === '3') ? h('button.text-transform-uppercase', { + onClick: () => this.props.dispatch(actions.buyEth({ network })), + style: { + marginTop: '15px', + }, + }, 'Ropsten Test Faucet') : null, + (network === '4') ? h('button.text-transform-uppercase', { + onClick: () => this.props.dispatch(actions.buyEth({ network })), + style: { + marginTop: '15px', + }, + }, 'Rinkeby Test Faucet') : null, + (network === '42') ? h('button.text-transform-uppercase', { + onClick: () => this.props.dispatch(actions.buyEth({ network })), + style: { + marginTop: '15px', + }, + }, 'Kovan Test Faucet') : null, + ]) + } +} + +BuyButtonSubview.prototype.navigateTo = function (url) { + global.platform.openWindow({ url }) +} + +BuyButtonSubview.prototype.backButtonContext = function () { + if (this.props.context === 'confTx') { + this.props.dispatch(actions.showConfTxPage(false)) + } else { + this.props.dispatch(actions.goHome()) + } +} + +BuyButtonSubview.prototype.radioHandler = function (event) { + switch (event.target.title) { + case 'Coinbase': + return this.props.dispatch(actions.coinBaseSubview()) + case 'ShapeShift': + return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type)) + } +} diff --git a/ui/responsive/app/components/coinbase-form.js b/ui/responsive/app/components/coinbase-form.js new file mode 100644 index 000000000..f44d86045 --- /dev/null +++ b/ui/responsive/app/components/coinbase-form.js @@ -0,0 +1,63 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const actions = require('../actions') + +module.exports = connect(mapStateToProps)(CoinbaseForm) + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +inherits(CoinbaseForm, Component) + +function CoinbaseForm () { + Component.call(this) +} + +CoinbaseForm.prototype.render = function () { + var props = this.props + + return h('.flex-column', { + style: { + marginTop: '35px', + padding: '25px', + width: '100%', + }, + }, [ + h('.flex-row', { + style: { + justifyContent: 'space-around', + margin: '33px', + marginTop: '0px', + }, + }, [ + h('button.btn-green', { + onClick: this.toCoinbase.bind(this), + }, 'Continue to Coinbase'), + + h('button.btn-red', { + onClick: () => props.dispatch(actions.backTobuyView(props.accounts.address)), + }, 'Cancel'), + ]), + ]) +} + +CoinbaseForm.prototype.toCoinbase = function () { + const props = this.props + const address = props.buyView.buyAddress + props.dispatch(actions.buyEth({ network: '1', address, amount: 0 })) +} + +CoinbaseForm.prototype.renderLoading = function () { + return h('img', { + style: { + width: '27px', + marginRight: '-27px', + }, + src: 'images/loading.svg', + }) +} diff --git a/ui/responsive/app/components/copyButton.js b/ui/responsive/app/components/copyButton.js new file mode 100644 index 000000000..a25d0719c --- /dev/null +++ b/ui/responsive/app/components/copyButton.js @@ -0,0 +1,59 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const copyToClipboard = require('copy-to-clipboard') + +const Tooltip = require('./tooltip') + +module.exports = CopyButton + +inherits(CopyButton, Component) +function CopyButton () { + Component.call(this) +} + +// As parameters, accepts: +// "value", which is the value to copy (mandatory) +// "title", which is the text to show on hover (optional, defaults to 'Copy') +CopyButton.prototype.render = function () { + const props = this.props + const state = this.state || {} + + const value = props.value + const copied = state.copied + + const message = copied ? 'Copied' : props.title || ' Copy ' + + return h('.copy-button', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + + h(Tooltip, { + title: message, + }, [ + h('i.fa.fa-clipboard.cursor-pointer.color-orange', { + style: { + margin: '5px', + }, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(value) + this.debounceRestore() + }, + }), + ]), + + ]) +} + +CopyButton.prototype.debounceRestore = function () { + this.setState({ copied: true }) + clearTimeout(this.timeout) + this.timeout = setTimeout(() => { + this.setState({ copied: false }) + }, 850) +} diff --git a/ui/responsive/app/components/copyable.js b/ui/responsive/app/components/copyable.js new file mode 100644 index 000000000..a4f6f4bc6 --- /dev/null +++ b/ui/responsive/app/components/copyable.js @@ -0,0 +1,46 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const Tooltip = require('./tooltip') +const copyToClipboard = require('copy-to-clipboard') + +module.exports = Copyable + +inherits(Copyable, Component) +function Copyable () { + Component.call(this) + this.state = { + copied: false, + } +} + +Copyable.prototype.render = function () { + const props = this.props + const state = this.state + const { value, children } = props + const { copied } = state + + return h(Tooltip, { + title: copied ? 'Copied!' : 'Copy', + position: 'bottom', + }, h('span', { + style: { + cursor: 'pointer', + }, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(value) + this.debounceRestore() + }, + }, children)) +} + +Copyable.prototype.debounceRestore = function () { + this.setState({ copied: true }) + clearTimeout(this.timeout) + this.timeout = setTimeout(() => { + this.setState({ copied: false }) + }, 850) +} diff --git a/ui/responsive/app/components/custom-radio-list.js b/ui/responsive/app/components/custom-radio-list.js new file mode 100644 index 000000000..a4c525396 --- /dev/null +++ b/ui/responsive/app/components/custom-radio-list.js @@ -0,0 +1,60 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = RadioList + +inherits(RadioList, Component) +function RadioList () { + Component.call(this) +} + +RadioList.prototype.render = function () { + const props = this.props + const activeClass = '.custom-radio-selected' + const inactiveClass = '.custom-radio-inactive' + const { + labels, + defaultFocus, + } = props + + + return ( + h('.flex-row', { + style: { + fontSize: '12px', + }, + }, [ + h('.flex-column.custom-radios', { + style: { + marginRight: '5px', + }, + }, + labels.map((lable, i) => { + let isSelcted = (this.state !== null) + isSelcted = isSelcted ? (this.state.selected === lable) : (defaultFocus === lable) + return h(isSelcted ? activeClass : inactiveClass, { + title: lable, + onClick: (event) => { + this.setState({selected: event.target.title}) + props.onClick(event) + }, + }) + }) + ), + h('.text', {}, + labels.map((lable) => { + if (props.subtext) { + return h('.flex-row', {}, [ + h('.radio-titles', lable), + h('.radio-titles-subtext', `- ${props.subtext[lable]}`), + ]) + } else { + return h('.radio-titles', lable) + } + }) + ), + ]) + ) +} + diff --git a/ui/responsive/app/components/drop-menu-item.js b/ui/responsive/app/components/drop-menu-item.js new file mode 100644 index 000000000..e42948209 --- /dev/null +++ b/ui/responsive/app/components/drop-menu-item.js @@ -0,0 +1,59 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = DropMenuItem + +inherits(DropMenuItem, Component) +function DropMenuItem () { + Component.call(this) +} + +DropMenuItem.prototype.render = function () { + return h('li.drop-menu-item', { + onClick: () => { + this.props.closeMenu() + this.props.action() + }, + style: { + listStyle: 'none', + padding: '6px 16px 6px 5px', + fontFamily: 'Montserrat Regular', + color: 'rgb(125, 128, 130)', + cursor: 'pointer', + display: 'flex', + justifyContent: 'flex-start', + }, + }, [ + this.props.icon, + this.props.label, + this.activeNetworkRender(), + ]) +} + +DropMenuItem.prototype.activeNetworkRender = function () { + const activeNetwork = this.props.activeNetworkRender + const { provider } = this.props + const providerType = provider ? provider.type : null + if (activeNetwork === undefined) return + + switch (this.props.label) { + case 'Main Ethereum Network': + if (providerType === 'mainnet') return h('.check', '✓') + break + case 'Ropsten Test Network': + if (providerType === 'ropsten') return h('.check', '✓') + break + case 'Kovan Test Network': + if (providerType === 'kovan') return h('.check', '✓') + break + case 'Rinkeby Test Network': + if (providerType === 'rinkeby') return h('.check', '✓') + break + case 'Localhost 8545': + if (activeNetwork === 'http://localhost:8545') return h('.check', '✓') + break + default: + if (activeNetwork === 'custom') return h('.check', '✓') + } +} diff --git a/ui/responsive/app/components/editable-label.js b/ui/responsive/app/components/editable-label.js new file mode 100644 index 000000000..41936f5e0 --- /dev/null +++ b/ui/responsive/app/components/editable-label.js @@ -0,0 +1,51 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const findDOMNode = require('react-dom').findDOMNode + +module.exports = EditableLabel + +inherits(EditableLabel, Component) +function EditableLabel () { + Component.call(this) +} + +EditableLabel.prototype.render = function () { + const props = this.props + const state = this.state + + if (state && state.isEditingLabel) { + return h('div.editable-label', [ + h('input.sizing-input', { + defaultValue: props.textValue, + maxLength: '20', + onKeyPress: (event) => { + this.saveIfEnter(event) + }, + }), + h('button.editable-button', { + onClick: () => this.saveText(), + }, 'Save'), + ]) + } else { + return h('div.name-label', { + onClick: (event) => { + this.setState({ isEditingLabel: true }) + }, + }, this.props.children) + } +} + +EditableLabel.prototype.saveIfEnter = function (event) { + if (event.key === 'Enter') { + this.saveText() + } +} + +EditableLabel.prototype.saveText = function () { + var container = findDOMNode(this) + var text = container.querySelector('.editable-label input').value + var truncatedText = text.substring(0, 20) + this.props.saveText(truncatedText) + this.setState({ isEditingLabel: false, textLabel: truncatedText }) +} diff --git a/ui/responsive/app/components/ens-input.js b/ui/responsive/app/components/ens-input.js new file mode 100644 index 000000000..3a33ebf74 --- /dev/null +++ b/ui/responsive/app/components/ens-input.js @@ -0,0 +1,170 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const extend = require('xtend') +const debounce = require('debounce') +const copyToClipboard = require('copy-to-clipboard') +const ENS = require('ethjs-ens') +const networkMap = require('ethjs-ens/lib/network-map.json') +const ensRE = /.+\.eth$/ +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + + +module.exports = EnsInput + +inherits(EnsInput, Component) +function EnsInput () { + Component.call(this) +} + +EnsInput.prototype.render = function () { + const props = this.props + const opts = extend(props, { + list: 'addresses', + onChange: () => { + const network = this.props.network + const networkHasEnsSupport = getNetworkEnsSupport(network) + if (!networkHasEnsSupport) return + + const recipient = document.querySelector('input[name="address"]').value + if (recipient.match(ensRE) === null) { + return this.setState({ + loadingEns: false, + ensResolution: null, + ensFailure: null, + }) + } + + this.setState({ + loadingEns: true, + }) + this.checkName() + }, + }) + return h('div', { + style: { width: '100%' }, + }, [ + h('input.large-input', opts), + // The address book functionality. + h('datalist#addresses', + [ + // Corresponds to the addresses owned. + Object.keys(props.identities).map((key) => { + const identity = props.identities[key] + return h('option', { + value: identity.address, + label: identity.name, + key: identity.address, + }) + }), + // Corresponds to previously sent-to addresses. + props.addressBook.map((identity) => { + return h('option', { + value: identity.address, + label: identity.name, + key: identity.address, + }) + }), + ]), + this.ensIcon(), + ]) +} + +EnsInput.prototype.componentDidMount = function () { + const network = this.props.network + const networkHasEnsSupport = getNetworkEnsSupport(network) + this.setState({ ensResolution: ZERO_ADDRESS }) + + if (networkHasEnsSupport) { + const provider = global.ethereumProvider + this.ens = new ENS({ provider, network }) + this.checkName = debounce(this.lookupEnsName.bind(this), 200) + } +} + +EnsInput.prototype.lookupEnsName = function () { + const recipient = document.querySelector('input[name="address"]').value + const { ensResolution } = this.state + + log.info(`ENS attempting to resolve name: ${recipient}`) + this.ens.lookup(recipient.trim()) + .then((address) => { + if (address === ZERO_ADDRESS) throw new Error('No address has been set for this name.') + if (address !== ensResolution) { + this.setState({ + loadingEns: false, + ensResolution: address, + nickname: recipient.trim(), + hoverText: address + '\nClick to Copy', + ensFailure: false, + }) + } + }) + .catch((reason) => { + log.error(reason) + return this.setState({ + loadingEns: false, + ensResolution: ZERO_ADDRESS, + ensFailure: true, + hoverText: reason.message, + }) + }) +} + +EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) { + const state = this.state || {} + const ensResolution = state.ensResolution + // If an address is sent without a nickname, meaning not from ENS or from + // the user's own accounts, a default of a one-space string is used. + const nickname = state.nickname || ' ' + if (prevState && ensResolution && this.props.onChange && + ensResolution !== prevState.ensResolution) { + this.props.onChange(ensResolution, nickname) + } +} + +EnsInput.prototype.ensIcon = function (recipient) { + const { hoverText } = this.state || {} + return h('span', { + title: hoverText, + style: { + position: 'absolute', + padding: '9px', + transform: 'translatex(-40px)', + }, + }, this.ensIconContents(recipient)) +} + +EnsInput.prototype.ensIconContents = function (recipient) { + const { loadingEns, ensFailure, ensResolution } = this.state || { ensResolution: ZERO_ADDRESS} + + if (loadingEns) { + return h('img', { + src: 'images/loading.svg', + style: { + width: '30px', + height: '30px', + transform: 'translateY(-6px)', + }, + }) + } + + if (ensFailure) { + return h('i.fa.fa-warning.fa-lg.warning') + } + + if (ensResolution && (ensResolution !== ZERO_ADDRESS)) { + return h('i.fa.fa-check-circle.fa-lg.cursor-pointer', { + style: { color: 'green' }, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(ensResolution) + }, + }) + } +} + +function getNetworkEnsSupport (network) { + return Boolean(networkMap[network]) +} diff --git a/ui/responsive/app/components/eth-balance.js b/ui/responsive/app/components/eth-balance.js new file mode 100644 index 000000000..4f538fd31 --- /dev/null +++ b/ui/responsive/app/components/eth-balance.js @@ -0,0 +1,89 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const formatBalance = require('../util').formatBalance +const generateBalanceObject = require('../util').generateBalanceObject +const Tooltip = require('./tooltip.js') +const FiatValue = require('./fiat-value.js') + +module.exports = EthBalanceComponent + +inherits(EthBalanceComponent, Component) +function EthBalanceComponent () { + Component.call(this) +} + +EthBalanceComponent.prototype.render = function () { + var props = this.props + let { value } = props + const { style, width } = props + var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true + value = value ? formatBalance(value, 6, needsParse) : '...' + + return ( + + h('.ether-balance.ether-balance-amount', { + style, + }, [ + h('div', { + style: { + display: 'inline', + width, + }, + }, this.renderBalance(value)), + ]) + + ) +} +EthBalanceComponent.prototype.renderBalance = function (value) { + var props = this.props + const { conversionRate, shorten, incoming, currentCurrency } = props + if (value === 'None') return value + if (value === '...') return value + var balanceObj = generateBalanceObject(value, shorten ? 1 : 3) + var balance + var splitBalance = value.split(' ') + var ethNumber = splitBalance[0] + var ethSuffix = splitBalance[1] + const showFiat = 'showFiat' in props ? props.showFiat : true + + if (shorten) { + balance = balanceObj.shortBalance + } else { + balance = balanceObj.balance + } + + var label = balanceObj.label + + return ( + h(Tooltip, { + position: 'bottom', + title: `${ethNumber} ${ethSuffix}`, + }, h('div.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('div', { + style: { + width: '100%', + textAlign: 'right', + }, + }, incoming ? `+${balance}` : balance), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + }, + }, label), + ]), + + showFiat ? h(FiatValue, { value: props.value, conversionRate, currentCurrency }) : null, + ])) + ) +} diff --git a/ui/responsive/app/components/fiat-value.js b/ui/responsive/app/components/fiat-value.js new file mode 100644 index 000000000..8a64a1cfc --- /dev/null +++ b/ui/responsive/app/components/fiat-value.js @@ -0,0 +1,63 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const formatBalance = require('../util').formatBalance + +module.exports = FiatValue + +inherits(FiatValue, Component) +function FiatValue () { + Component.call(this) +} + +FiatValue.prototype.render = function () { + const props = this.props + const { conversionRate, currentCurrency } = props + + const value = formatBalance(props.value, 6) + + if (value === 'None') return value + var fiatDisplayNumber, fiatTooltipNumber + var splitBalance = value.split(' ') + + if (conversionRate !== 0) { + fiatTooltipNumber = Number(splitBalance[0]) * conversionRate + fiatDisplayNumber = fiatTooltipNumber.toFixed(2) + } else { + fiatDisplayNumber = 'N/A' + fiatTooltipNumber = 'Unknown' + } + + return fiatDisplay(fiatDisplayNumber, currentCurrency) +} + +function fiatDisplay (fiatDisplayNumber, fiatSuffix) { + if (fiatDisplayNumber !== 'N/A') { + return h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('div', { + style: { + width: '100%', + textAlign: 'right', + fontSize: '12px', + color: '#333333', + }, + }, fiatDisplayNumber), + h('div', { + style: { + color: '#AEAEAE', + marginLeft: '5px', + fontSize: '12px', + }, + }, fiatSuffix), + ]) + } else { + return h('div') + } +} diff --git a/ui/responsive/app/components/hex-as-decimal-input.js b/ui/responsive/app/components/hex-as-decimal-input.js new file mode 100644 index 000000000..4a71e9585 --- /dev/null +++ b/ui/responsive/app/components/hex-as-decimal-input.js @@ -0,0 +1,154 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const extend = require('xtend') + +module.exports = HexAsDecimalInput + +inherits(HexAsDecimalInput, Component) +function HexAsDecimalInput () { + this.state = { invalid: null } + Component.call(this) +} + +/* Hex as Decimal Input + * + * A component for allowing easy, decimal editing + * of a passed in hex string value. + * + * On change, calls back its `onChange` function parameter + * and passes it an updated hex string. + */ + +HexAsDecimalInput.prototype.render = function () { + const props = this.props + const state = this.state + + const { value, onChange, min, max } = props + + const toEth = props.toEth + const suffix = props.suffix + const decimalValue = decimalize(value, toEth) + const style = props.style + + return ( + h('.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('input.hex-input', { + type: 'number', + required: true, + min: min, + max: max, + style: extend({ + display: 'block', + textAlign: 'right', + backgroundColor: 'transparent', + border: '1px solid #bdbdbd', + + }, style), + value: parseInt(decimalValue), + onBlur: (event) => { + this.updateValidity(event) + }, + onChange: (event) => { + this.updateValidity(event) + const hexString = (event.target.value === '') ? '' : hexify(event.target.value) + onChange(hexString) + }, + onInvalid: (event) => { + const msg = this.constructWarning() + if (msg === state.invalid) { + return + } + this.setState({ invalid: msg }) + event.preventDefault() + return false + }, + }), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + marginRight: '6px', + width: '20px', + }, + }, suffix), + ]), + + state.invalid ? h('span.error', { + style: { + position: 'absolute', + right: '0px', + textAlign: 'right', + transform: 'translateY(26px)', + padding: '3px', + background: 'rgba(255,255,255,0.85)', + zIndex: '1', + textTransform: 'capitalize', + border: '2px solid #E20202', + }, + }, state.invalid) : null, + ]) + ) +} + +HexAsDecimalInput.prototype.setValid = function (message) { + this.setState({ invalid: null }) +} + +HexAsDecimalInput.prototype.updateValidity = function (event) { + const target = event.target + const value = this.props.value + const newValue = target.value + + if (value === newValue) { + return + } + + const valid = target.checkValidity() + if (valid) { + this.setState({ invalid: null }) + } +} + +HexAsDecimalInput.prototype.constructWarning = function () { + const { name, min, max } = this.props + let message = name ? name + ' ' : '' + + if (min && max) { + message += `must be greater than or equal to ${min} and less than or equal to ${max}.` + } else if (min) { + message += `must be greater than or equal to ${min}.` + } else if (max) { + message += `must be less than or equal to ${max}.` + } else { + message += 'Invalid input.' + } + + return message +} + +function hexify (decimalString) { + const hexBN = new BN(parseInt(decimalString), 10) + return '0x' + hexBN.toString('hex') +} + +function decimalize (input, toEth) { + if (input === '') { + return '' + } else { + const strippedInput = ethUtil.stripHexPrefix(input) + const inputBN = new BN(strippedInput, 'hex') + return inputBN.toString(10) + } +} diff --git a/ui/responsive/app/components/identicon.js b/ui/responsive/app/components/identicon.js new file mode 100644 index 000000000..c754bc6ba --- /dev/null +++ b/ui/responsive/app/components/identicon.js @@ -0,0 +1,72 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const isNode = require('detect-node') +const findDOMNode = require('react-dom').findDOMNode +const jazzicon = require('jazzicon') +const iconFactoryGen = require('../../lib/icon-factory') +const iconFactory = iconFactoryGen(jazzicon) + +module.exports = IdenticonComponent + +inherits(IdenticonComponent, Component) +function IdenticonComponent () { + Component.call(this) + + this.defaultDiameter = 46 +} + +IdenticonComponent.prototype.render = function () { + var props = this.props + var diameter = props.diameter || this.defaultDiameter + return ( + h('div', { + key: 'identicon-' + this.props.address, + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: diameter, + width: diameter, + borderRadius: diameter / 2, + overflow: 'hidden', + }, + }) + ) +} + +IdenticonComponent.prototype.componentDidMount = function () { + var props = this.props + const { address } = props + + if (!address) return + + var container = findDOMNode(this) + + var diameter = props.diameter || this.defaultDiameter + if (!isNode) { + var img = iconFactory.iconForAddress(address, diameter) + container.appendChild(img) + } +} + +IdenticonComponent.prototype.componentDidUpdate = function () { + var props = this.props + const { address } = props + + if (!address) return + + var container = findDOMNode(this) + + var children = container.children + for (var i = 0; i < children.length; i++) { + container.removeChild(children[i]) + } + + var diameter = props.diameter || this.defaultDiameter + if (!isNode) { + var img = iconFactory.iconForAddress(address, diameter) + container.appendChild(img) + } +} + diff --git a/ui/responsive/app/components/loading.js b/ui/responsive/app/components/loading.js new file mode 100644 index 000000000..87d6f5d20 --- /dev/null +++ b/ui/responsive/app/components/loading.js @@ -0,0 +1,53 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') + + +inherits(LoadingIndicator, Component) +module.exports = LoadingIndicator + +function LoadingIndicator () { + Component.call(this) +} + +LoadingIndicator.prototype.render = function () { + const { isLoading, loadingMessage } = this.props + + return ( + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'loader', + transitionEnterTimeout: 150, + transitionLeaveTimeout: 150, + }, [ + + isLoading ? h('div', { + style: { + zIndex: 10, + position: 'absolute', + flexDirection: 'column', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '100%', + width: '100%', + background: 'rgba(255, 255, 255, 0.8)', + }, + }, [ + h('img', { + src: 'images/loading.svg', + }), + + h('br'), + + showMessageIfAny(loadingMessage), + ]) : null, + ]) + ) +} + +function showMessageIfAny (loadingMessage) { + if (!loadingMessage) return null + return h('span', loadingMessage) +} diff --git a/ui/responsive/app/components/mascot.js b/ui/responsive/app/components/mascot.js new file mode 100644 index 000000000..973ec2cad --- /dev/null +++ b/ui/responsive/app/components/mascot.js @@ -0,0 +1,59 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const metamaskLogo = require('metamask-logo') +const debounce = require('debounce') + +module.exports = Mascot + +inherits(Mascot, Component) +function Mascot () { + Component.call(this) + this.logo = metamaskLogo({ + followMouse: true, + pxNotRatio: true, + width: 200, + height: 200, + }) + + this.refollowMouse = debounce(this.logo.setFollowMouse.bind(this.logo, true), 1000) + this.unfollowMouse = this.logo.setFollowMouse.bind(this.logo, false) +} + +Mascot.prototype.render = function () { + // this is a bit hacky + // the event emitter is on `this.props` + // and we dont get that until render + this.handleAnimationEvents() + + return h('#metamask-mascot-container', { + style: { zIndex: 0 }, + }) +} + +Mascot.prototype.componentDidMount = function () { + var targetDivId = 'metamask-mascot-container' + var container = document.getElementById(targetDivId) + container.appendChild(this.logo.container) +} + +Mascot.prototype.componentWillUnmount = function () { + this.animations = this.props.animationEventEmitter + this.animations.removeAllListeners() + this.logo.container.remove() + this.logo.stopAnimation() +} + +Mascot.prototype.handleAnimationEvents = function () { + // only setup listeners once + if (this.animations) return + this.animations = this.props.animationEventEmitter + this.animations.on('point', this.lookAt.bind(this)) + this.animations.on('setFollowMouse', this.logo.setFollowMouse.bind(this.logo)) +} + +Mascot.prototype.lookAt = function (target) { + this.unfollowMouse() + this.logo.lookAt(target) + this.refollowMouse() +} diff --git a/ui/responsive/app/components/mini-account-panel.js b/ui/responsive/app/components/mini-account-panel.js new file mode 100644 index 000000000..c09cf5b7a --- /dev/null +++ b/ui/responsive/app/components/mini-account-panel.js @@ -0,0 +1,74 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const Identicon = require('./identicon') + +module.exports = AccountPanel + + +inherits(AccountPanel, Component) +function AccountPanel () { + Component.call(this) +} + +AccountPanel.prototype.render = function () { + var props = this.props + var picOrder = props.picOrder || 'left' + const { imageSeed } = props + + return ( + + h('.identity-panel.flex-row.flex-left', { + style: { + cursor: props.onClick ? 'pointer' : undefined, + }, + onClick: props.onClick, + }, [ + + this.genIcon(imageSeed, picOrder), + + h('div.flex-column.flex-justify-center', { + style: { + lineHeight: '15px', + order: 2, + display: 'flex', + alignItems: picOrder === 'left' ? 'flex-begin' : 'flex-end', + }, + }, this.props.children), + ]) + ) +} + +AccountPanel.prototype.genIcon = function (seed, picOrder) { + const props = this.props + + // When there is no seed value, this is a contract creation. + // We then show the contract icon. + if (!seed) { + return h('.identicon-wrapper.flex-column.select-none', { + style: { + order: picOrder === 'left' ? 1 : 3, + }, + }, [ + h('i.fa.fa-file-text-o.fa-lg', { + style: { + fontSize: '42px', + transform: 'translate(0px, -16px)', + }, + }), + ]) + } + + // If there was a seed, we return an identicon for that address. + return h('.identicon-wrapper.flex-column.select-none', { + style: { + order: picOrder === 'left' ? 1 : 3, + }, + }, [ + h(Identicon, { + address: seed, + imageify: props.imageifyIdenticons, + }), + ]) +} + diff --git a/ui/responsive/app/components/network.js b/ui/responsive/app/components/network.js new file mode 100644 index 000000000..d5d3e18cd --- /dev/null +++ b/ui/responsive/app/components/network.js @@ -0,0 +1,125 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = Network + +inherits(Network, Component) + +function Network () { + Component.call(this) +} + +Network.prototype.render = function () { + const props = this.props + const networkNumber = props.network + let providerName + try { + providerName = props.provider.type + } catch (e) { + providerName = null + } + let iconName, hoverText + + if (networkNumber === 'loading') { + return h('span', { + style: { + display: 'flex', + alignItems: 'center', + flexDirection: 'row', + }, + onClick: (event) => this.props.onClick(event), + }, [ + h('img', { + title: 'Attempting to connect to blockchain.', + style: { + width: '27px', + }, + src: 'images/loading.svg', + }), + h('i.fa.fa-sort-desc'), + ]) + + } else if (providerName === 'mainnet') { + hoverText = 'Main Ethereum Network' + iconName = 'ethereum-network' + } else if (providerName === 'ropsten') { + hoverText = 'Ropsten Test Network' + iconName = 'ropsten-test-network' + } else if (parseInt(networkNumber) === 3) { + hoverText = 'Ropsten Test Network' + iconName = 'ropsten-test-network' + } else if (providerName === 'kovan') { + hoverText = 'Kovan Test Network' + iconName = 'kovan-test-network' + } else if (providerName === 'rinkeby') { + hoverText = 'Rinkeby Test Network' + iconName = 'rinkeby-test-network' + } else { + hoverText = 'Unknown Private Network' + iconName = 'unknown-private-network' + } + + return ( + h('#network_component.pointer', { + title: hoverText, + onClick: (event) => this.props.onClick(event), + }, [ + (function () { + switch (iconName) { + case 'ethereum-network': + return h('.network-indicator', [ + h('.menu-icon.diamond'), + h('.network-name', { + style: { + color: '#039396', + }}, + 'Ethereum Main Net'), + ]) + case 'ropsten-test-network': + return h('.network-indicator', [ + h('.menu-icon.red-dot'), + h('.network-name', { + style: { + color: '#ff6666', + }}, + 'Ropsten Test Net'), + ]) + case 'kovan-test-network': + return h('.network-indicator', [ + h('.menu-icon.hollow-diamond'), + h('.network-name', { + style: { + color: '#690496', + }}, + 'Kovan Test Net'), + ]) + case 'rinkeby-test-network': + return h('.network-indicator', [ + h('.menu-icon.golden-square'), + h('.network-name', { + style: { + color: '#e7a218', + }}, + 'Rinkeby Test Net'), + ]) + default: + return h('.network-indicator', [ + h('i.fa.fa-question-circle.fa-lg', { + style: { + margin: '10px', + color: 'rgb(125, 128, 130)', + }, + }), + + h('.network-name', { + style: { + color: '#AEAEAE', + }}, + 'Private Network'), + ]) + } + })(), + ]) + ) +} diff --git a/ui/responsive/app/components/notice.js b/ui/responsive/app/components/notice.js new file mode 100644 index 000000000..d9f0067cd --- /dev/null +++ b/ui/responsive/app/components/notice.js @@ -0,0 +1,126 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const ReactMarkdown = require('react-markdown') +const linker = require('extension-link-enabler') +const findDOMNode = require('react-dom').findDOMNode + +module.exports = Notice + +inherits(Notice, Component) +function Notice () { + Component.call(this) +} + +Notice.prototype.render = function () { + const { notice, onConfirm } = this.props + const { title, date, body } = notice + const state = this.state || { disclaimerDisabled: true } + const disabled = state.disclaimerDisabled + + return ( + h('.flex-column.flex-center.flex-grow', [ + h('h3.flex-center.text-transform-uppercase.terms-header', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + width: '100%', + fontSize: '20px', + textAlign: 'center', + padding: 6, + }, + }, [ + title, + ]), + + h('h5.flex-center.text-transform-uppercase.terms-header', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginBottom: 24, + width: '100%', + fontSize: '20px', + textAlign: 'center', + padding: 6, + }, + }, [ + date, + ]), + + h('style', ` + + .markdown { + overflow-x: hidden; + } + + .markdown h1, .markdown h2, .markdown h3 { + margin: 10px 0; + font-weight: bold; + } + + .markdown strong { + font-weight: bold; + } + .markdown em { + font-style: italic; + } + + .markdown p { + margin: 10px 0; + } + + .markdown a { + color: #df6b0e; + } + + `), + + h('div.markdown', { + onScroll: (e) => { + var object = e.currentTarget + if (object.offsetHeight + object.scrollTop + 100 >= object.scrollHeight) { + this.setState({disclaimerDisabled: false}) + } + }, + style: { + background: 'rgb(235, 235, 235)', + height: '310px', + padding: '6px', + width: '90%', + overflowY: 'scroll', + scroll: 'auto', + }, + }, [ + h(ReactMarkdown, { + className: 'notice-box', + source: body, + skipHtml: true, + }), + ]), + + h('button', { + disabled, + onClick: () => { + this.setState({disclaimerDisabled: true}) + onConfirm() + }, + style: { + marginTop: '18px', + }, + }, 'Accept'), + ]) + ) +} + +Notice.prototype.componentDidMount = function () { + var node = findDOMNode(this) + linker.setupListener(node) + if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) { + this.setState({disclaimerDisabled: false}) + } +} + +Notice.prototype.componentWillUnmount = function () { + var node = findDOMNode(this) + linker.teardownListener(node) +} diff --git a/ui/responsive/app/components/pending-msg-details.js b/ui/responsive/app/components/pending-msg-details.js new file mode 100644 index 000000000..16308d121 --- /dev/null +++ b/ui/responsive/app/components/pending-msg-details.js @@ -0,0 +1,50 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const AccountPanel = require('./account-panel') + +module.exports = PendingMsgDetails + +inherits(PendingMsgDetails, Component) +function PendingMsgDetails () { + Component.call(this) +} + +PendingMsgDetails.prototype.render = function () { + var state = this.props + var msgData = state.txData + + var msgParams = msgData.msgParams || {} + var address = msgParams.from || state.selectedAddress + var identity = state.identities[address] || { address: address } + var account = state.accounts[address] || { address: address } + + return ( + h('div', { + key: msgData.id, + style: { + margin: '10px 20px', + }, + }, [ + + // account that will sign + h(AccountPanel, { + showFullAddress: true, + identity: identity, + account: account, + imageifyIdenticons: state.imageifyIdenticons, + }), + + // message data + h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-row.flex-space-between', [ + h('label.font-small', 'MESSAGE'), + h('span.font-small', msgParams.data), + ]), + ]), + + ]) + ) +} + diff --git a/ui/responsive/app/components/pending-msg.js b/ui/responsive/app/components/pending-msg.js new file mode 100644 index 000000000..b2cac164a --- /dev/null +++ b/ui/responsive/app/components/pending-msg.js @@ -0,0 +1,56 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const PendingTxDetails = require('./pending-msg-details') + +module.exports = PendingMsg + +inherits(PendingMsg, Component) +function PendingMsg () { + Component.call(this) +} + +PendingMsg.prototype.render = function () { + var state = this.props + var msgData = state.txData + + return ( + + h('div', { + key: msgData.id, + }, [ + + // header + h('h3', { + style: { + fontWeight: 'bold', + textAlign: 'center', + }, + }, 'Sign Message'), + + h('.error', { + style: { + margin: '10px', + }, + }, `Signing this message can have + dangerous side effects. Only sign messages from + sites you fully trust with your entire account. + This will be fixed in a future version.`), + + // message details + h(PendingTxDetails, state), + + // sign + cancel + h('.flex-row.flex-space-around', [ + h('button', { + onClick: state.cancelMessage, + }, 'Cancel'), + h('button', { + onClick: state.signMessage, + }, 'Sign'), + ]), + ]) + + ) +} + diff --git a/ui/responsive/app/components/pending-personal-msg-details.js b/ui/responsive/app/components/pending-personal-msg-details.js new file mode 100644 index 000000000..1050513f2 --- /dev/null +++ b/ui/responsive/app/components/pending-personal-msg-details.js @@ -0,0 +1,60 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const AccountPanel = require('./account-panel') +const BinaryRenderer = require('./binary-renderer') + +module.exports = PendingMsgDetails + +inherits(PendingMsgDetails, Component) +function PendingMsgDetails () { + Component.call(this) +} + +PendingMsgDetails.prototype.render = function () { + var state = this.props + var msgData = state.txData + + var msgParams = msgData.msgParams || {} + var address = msgParams.from || state.selectedAddress + var identity = state.identities[address] || { address: address } + var account = state.accounts[address] || { address: address } + + var { data } = msgParams + + return ( + h('div', { + key: msgData.id, + style: { + margin: '10px 20px', + }, + }, [ + + // account that will sign + h(AccountPanel, { + showFullAddress: true, + identity: identity, + account: account, + imageifyIdenticons: state.imageifyIdenticons, + }), + + // message data + h('div', { + style: { + height: '260px', + }, + }, [ + h('label.font-small', { style: { display: 'block' } }, 'MESSAGE'), + h(BinaryRenderer, { + value: data, + style: { + height: '215px', + }, + }), + ]), + + ]) + ) +} + diff --git a/ui/responsive/app/components/pending-personal-msg.js b/ui/responsive/app/components/pending-personal-msg.js new file mode 100644 index 000000000..4542adb28 --- /dev/null +++ b/ui/responsive/app/components/pending-personal-msg.js @@ -0,0 +1,47 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const PendingTxDetails = require('./pending-personal-msg-details') + +module.exports = PendingMsg + +inherits(PendingMsg, Component) +function PendingMsg () { + Component.call(this) +} + +PendingMsg.prototype.render = function () { + var state = this.props + var msgData = state.txData + + return ( + + h('div', { + key: msgData.id, + }, [ + + // header + h('h3', { + style: { + fontWeight: 'bold', + textAlign: 'center', + }, + }, 'Sign Message'), + + // message details + h(PendingTxDetails, state), + + // sign + cancel + h('.flex-row.flex-space-around', [ + h('button', { + onClick: state.cancelPersonalMessage, + }, 'Cancel'), + h('button', { + onClick: state.signPersonalMessage, + }, 'Sign'), + ]), + ]) + + ) +} + diff --git a/ui/responsive/app/components/pending-tx.js b/ui/responsive/app/components/pending-tx.js new file mode 100644 index 000000000..962680d30 --- /dev/null +++ b/ui/responsive/app/components/pending-tx.js @@ -0,0 +1,480 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const actions = require('../actions') +const clone = require('clone') + +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const hexToBn = require('../../../../app/scripts/lib/hex-to-bn') +const util = require('../util') +const MiniAccountPanel = require('./mini-account-panel') +const Copyable = require('./copyable') +const EthBalance = require('./eth-balance') +const addressSummary = util.addressSummary +const nameForAddress = require('../../lib/contract-namer') +const BNInput = require('./bn-as-decimal-input') + +const MIN_GAS_PRICE_GWEI_BN = new BN(2) +const GWEI_FACTOR = new BN(1e9) +const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) +const MIN_GAS_LIMIT_BN = new BN(21000) + +module.exports = PendingTx +inherits(PendingTx, Component) +function PendingTx () { + Component.call(this) + this.state = { + valid: true, + txData: null, + submitting: false, + } +} + +PendingTx.prototype.render = function () { + const props = this.props + const { currentCurrency, blockGasLimit } = props + + const conversionRate = props.conversionRate + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} + + // Account Details + const address = txParams.from || props.selectedAddress + const identity = props.identities[address] || { address: address } + const account = props.accounts[address] + const balance = account ? account.balance : '0x0' + + // recipient check + const isValidAddress = !txParams.to || util.isValidAddress(txParams.to) + + // Gas + const gas = txParams.gas + const gasBn = hexToBn(gas) + const gasLimit = new BN(parseInt(blockGasLimit)) + const safeGasLimit = this.bnMultiplyByFraction(gasLimit, 19, 20).toString(10) + + // Gas Price + const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) + const gasPriceBn = hexToBn(gasPrice) + + const txFeeBn = gasBn.mul(gasPriceBn) + const valueBn = hexToBn(txParams.value) + const maxCost = txFeeBn.add(valueBn) + + const dataLength = txParams.data ? (txParams.data.length - 2) / 2 : 0 + + const balanceBn = hexToBn(balance) + const insufficientBalance = balanceBn.lt(maxCost) + + this.inputs = [] + + return ( + + h('div', { + key: txMeta.id, + }, [ + + h('form#pending-tx-form', { + onSubmit: this.onSubmit.bind(this), + + }, [ + + // tx info + h('div', [ + + h('.flex-row.flex-center', { + style: { + maxWidth: '100%', + }, + }, [ + + h(MiniAccountPanel, { + imageSeed: address, + picOrder: 'right', + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, identity.name), + + h(Copyable, { + value: ethUtil.toChecksumAddress(address), + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, addressSummary(address, 6, 4, false)), + ]), + + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, [ + h(EthBalance, { + value: balance, + conversionRate, + currentCurrency, + inline: true, + labelColor: '#F7861C', + }), + ]), + ]), + + forwardCarrat(), + + this.miniAccountPanelForRecipient(), + ]), + + h('style', ` + .table-box { + margin: 7px 0px 0px 0px; + width: 100%; + } + .table-box .row { + margin: 0px; + background: rgb(236,236,236); + display: flex; + justify-content: space-between; + font-family: Montserrat Light, sans-serif; + font-size: 13px; + padding: 5px 25px; + } + .table-box .row .value { + font-family: Montserrat Regular; + } + `), + + h('.table-box', [ + + // Ether Value + // Currently not customizable, but easily modified + // in the way that gas and gasLimit currently are. + h('.row', [ + h('.cell.label', 'Amount'), + h(EthBalance, { value: txParams.value, currentCurrency, conversionRate }), + ]), + + // Gas Limit (customizable) + h('.cell.row', [ + h('.cell.label', 'Gas Limit'), + h('.cell.value', { + }, [ + h(BNInput, { + name: 'Gas Limit', + value: gasBn, + precision: 0, + scale: 0, + // The hard lower limit for gas. + min: MIN_GAS_LIMIT_BN.toString(10), + max: safeGasLimit, + suffix: 'UNITS', + style: { + position: 'relative', + top: '5px', + }, + onChange: this.gasLimitChanged.bind(this), + + ref: (hexInput) => { this.inputs.push(hexInput) }, + }), + ]), + ]), + + // Gas Price (customizable) + h('.cell.row', [ + h('.cell.label', 'Gas Price'), + h('.cell.value', { + }, [ + h(BNInput, { + name: 'Gas Price', + value: gasPriceBn, + precision: 9, + scale: 9, + suffix: 'GWEI', + min: MIN_GAS_PRICE_GWEI_BN.toString(10), + style: { + position: 'relative', + top: '5px', + }, + onChange: this.gasPriceChanged.bind(this), + ref: (hexInput) => { this.inputs.push(hexInput) }, + }), + ]), + ]), + + // Max Transaction Fee (calculated) + h('.cell.row', [ + h('.cell.label', 'Max Transaction Fee'), + h(EthBalance, { value: txFeeBn.toString(16), currentCurrency, conversionRate }), + ]), + + h('.cell.row', { + style: { + fontFamily: 'Montserrat Regular', + background: 'white', + padding: '10px 25px', + }, + }, [ + h('.cell.label', 'Max Total'), + h('.cell.value', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + h(EthBalance, { + value: maxCost.toString(16), + currentCurrency, + conversionRate, + inline: true, + labelColor: 'black', + fontSize: '16px', + }), + ]), + ]), + + // Data size row: + h('.cell.row', { + style: { + background: '#f7f7f7', + paddingBottom: '0px', + }, + }, [ + h('.cell.label'), + h('.cell.value', { + style: { + fontFamily: 'Montserrat Light', + fontSize: '11px', + }, + }, `Data included: ${dataLength} bytes`), + ]), + ]), // End of Table + + ]), + + h('style', ` + .conf-buttons button { + margin-left: 10px; + text-transform: uppercase; + } + `), + + txMeta.simulationFails ? + h('.error', { + style: { + marginLeft: 50, + fontSize: '0.9em', + }, + }, 'Transaction Error. Exception thrown in contract code.') + : null, + + !isValidAddress ? + h('.error', { + style: { + marginLeft: 50, + fontSize: '0.9em', + }, + }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.') + : null, + + insufficientBalance ? + h('span.error', { + style: { + marginLeft: 50, + fontSize: '0.9em', + }, + }, 'Insufficient balance for transaction') + : null, + + // send + cancel + h('.flex-row.flex-space-around.conf-buttons', { + style: { + display: 'flex', + justifyContent: 'flex-end', + margin: '14px 25px', + }, + }, [ + + + insufficientBalance ? + h('button.btn-green', { + onClick: props.buyEth, + }, 'Buy Ether') + : null, + + h('button', { + onClick: (event) => { + this.resetGasFields() + event.preventDefault() + }, + }, 'Reset'), + + // Accept Button + h('input.confirm.btn-green', { + type: 'submit', + value: 'SUBMIT', + style: { marginLeft: '10px' }, + disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, + }), + + h('button.cancel.btn-red', { + onClick: props.cancelTransaction, + }, 'Reject'), + ]), + ]), + ]) + ) +} + +PendingTx.prototype.miniAccountPanelForRecipient = function () { + const props = this.props + const txData = props.txData + const txParams = txData.txParams || {} + const isContractDeploy = !('to' in txParams) + + // If it's not a contract deploy, send to the account + if (!isContractDeploy) { + return h(MiniAccountPanel, { + imageSeed: txParams.to, + picOrder: 'left', + }, [ + + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, nameForAddress(txParams.to, props.identities)), + + h(Copyable, { + value: ethUtil.toChecksumAddress(txParams.to), + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, addressSummary(txParams.to, 6, 4, false)), + ]), + + ]) + } else { + return h(MiniAccountPanel, { + picOrder: 'left', + }, [ + + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, 'New Contract'), + + ]) + } +} + +PendingTx.prototype.gasPriceChanged = function (newBN, valid) { + log.info(`Gas price changed to: ${newBN.toString(10)}`) + const txMeta = this.gatherTxMeta() + txMeta.txParams.gasPrice = '0x' + newBN.toString('hex') + this.setState({ + txData: clone(txMeta), + valid, + }) +} + +PendingTx.prototype.gasLimitChanged = function (newBN, valid) { + log.info(`Gas limit changed to ${newBN.toString(10)}`) + const txMeta = this.gatherTxMeta() + txMeta.txParams.gas = '0x' + newBN.toString('hex') + this.setState({ + txData: clone(txMeta), + valid, + }) +} + +PendingTx.prototype.resetGasFields = function () { + log.debug(`pending-tx resetGasFields`) + + this.inputs.forEach((hexInput) => { + if (hexInput) { + hexInput.setValid() + } + }) + + this.setState({ + txData: null, + valid: true, + }) +} + +PendingTx.prototype.onSubmit = function (event) { + event.preventDefault() + const txMeta = this.gatherTxMeta() + const valid = this.checkValidity() + this.setState({ valid, submitting: true }) + if (valid && this.verifyGasParams()) { + this.props.sendTransaction(txMeta, event) + } else { + this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) + this.setState({ submitting: false }) + } +} + +PendingTx.prototype.checkValidity = function () { + const form = this.getFormEl() + const valid = form.checkValidity() + return valid +} + +PendingTx.prototype.getFormEl = function () { + const form = document.querySelector('form#pending-tx-form') + // Stub out form for unit tests: + if (!form) { + return { checkValidity () { return true } } + } + return form +} + +// After a customizable state value has been updated, +PendingTx.prototype.gatherTxMeta = function () { + log.debug(`pending-tx gatherTxMeta`) + const props = this.props + const state = this.state + const txData = clone(state.txData) || clone(props.txData) + + log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) + return txData +} + +PendingTx.prototype.verifyGasParams = function () { + // We call this in case the gas has not been modified at all + if (!this.state) { return true } + return ( + this._notZeroOrEmptyString(this.state.gas) && + this._notZeroOrEmptyString(this.state.gasPrice) + ) +} + +PendingTx.prototype._notZeroOrEmptyString = function (obj) { + return obj !== '' && obj !== '0x0' +} + +PendingTx.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) { + const numBN = new BN(numerator) + const denomBN = new BN(denominator) + return targetBN.mul(numBN).div(denomBN) +} + +function forwardCarrat () { + return ( + h('img', { + src: 'images/forward-carrat.svg', + style: { + padding: '5px 6px 0px 10px', + height: '37px', + }, + }) + ) +} diff --git a/ui/responsive/app/components/qr-code.js b/ui/responsive/app/components/qr-code.js new file mode 100644 index 000000000..06b9aed9b --- /dev/null +++ b/ui/responsive/app/components/qr-code.js @@ -0,0 +1,79 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const qrCode = require('qrcode-npm').qrcode +const inherits = require('util').inherits +const connect = require('react-redux').connect +const isHexPrefixed = require('ethereumjs-util').isHexPrefixed +const CopyButton = require('./copyButton') + +module.exports = connect(mapStateToProps)(QrCodeView) + +function mapStateToProps (state) { + return { + Qr: state.appState.Qr, + buyView: state.appState.buyView, + warning: state.appState.warning, + } +} + +inherits(QrCodeView, Component) + +function QrCodeView () { + Component.call(this) +} + +QrCodeView.prototype.render = function () { + const props = this.props + const Qr = props.Qr + const address = `${isHexPrefixed(Qr.data) ? 'ethereum:' : ''}${Qr.data}` + const qrImage = qrCode(4, 'M') + qrImage.addData(address) + qrImage.make() + return h('.main-container.flex-column', { + key: 'qr', + style: { + justifyContent: 'center', + paddingBottom: '45px', + paddingLeft: '45px', + paddingRight: '45px', + alignItems: 'center', + }, + }, [ + Array.isArray(Qr.message) ? h('.message-container', this.renderMultiMessage()) : h('.qr-header', Qr.message), + + this.props.warning ? this.props.warning && h('span.error.flex-center', { + style: { + textAlign: 'center', + width: '229px', + height: '82px', + }, + }, + this.props.warning) : null, + + h('#qr-container.flex-column', { + style: { + marginTop: '25px', + marginBottom: '15px', + }, + dangerouslySetInnerHTML: { + __html: qrImage.createTableTag(4), + }, + }), + h('.flex-row', [ + h('h3.ellip-address', { + style: { + width: '247px', + }, + }, Qr.data), + h(CopyButton, { + value: Qr.data, + }), + ]), + ]) +} + +QrCodeView.prototype.renderMultiMessage = function () { + var Qr = this.props.Qr + var multiMessage = Qr.message.map((message) => h('.qr-message', message)) + return multiMessage +} diff --git a/ui/responsive/app/components/range-slider.js b/ui/responsive/app/components/range-slider.js new file mode 100644 index 000000000..823f5eb01 --- /dev/null +++ b/ui/responsive/app/components/range-slider.js @@ -0,0 +1,58 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = RangeSlider + +inherits(RangeSlider, Component) +function RangeSlider () { + Component.call(this) +} + +RangeSlider.prototype.render = function () { + const state = this.state || {} + const props = this.props + const onInput = props.onInput || function () {} + const name = props.name + const { + min = 0, + max = 100, + increment = 1, + defaultValue = 50, + mirrorInput = false, + } = this.props.options + const {container, input, range} = props.style + + return ( + h('.flex-row', { + style: container, + }, [ + h('input', { + type: 'range', + name: name, + min: min, + max: max, + step: increment, + style: range, + value: state.value || defaultValue, + onChange: mirrorInput ? this.mirrorInputs.bind(this, event) : onInput, + }), + + // Mirrored input for range + mirrorInput ? h('input.large-input', { + type: 'number', + name: `${name}Mirror`, + min: min, + max: max, + value: state.value || defaultValue, + step: increment, + style: input, + onChange: this.mirrorInputs.bind(this, event), + }) : null, + ]) + ) +} + +RangeSlider.prototype.mirrorInputs = function (event) { + this.setState({value: event.target.value}) +} diff --git a/ui/responsive/app/components/shapeshift-form.js b/ui/responsive/app/components/shapeshift-form.js new file mode 100644 index 000000000..e0a720426 --- /dev/null +++ b/ui/responsive/app/components/shapeshift-form.js @@ -0,0 +1,306 @@ +const PersistentForm = require('../../lib/persistent-form') +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +const actions = require('../actions') +const Qr = require('./qr-code') +const isValidAddress = require('../util').isValidAddress +module.exports = connect(mapStateToProps)(ShapeshiftForm) + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + isSubLoading: state.appState.isSubLoading, + qrRequested: state.appState.qrRequested, + } +} + +inherits(ShapeshiftForm, PersistentForm) + +function ShapeshiftForm () { + PersistentForm.call(this) + this.persistentFormParentId = 'shapeshift-buy-form' +} + +ShapeshiftForm.prototype.render = function () { + return h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + this.props.qrRequested ? h(Qr, {key: 'qr'}) : this.renderMain(), + ]) +} + +ShapeshiftForm.prototype.renderMain = function () { + const marketinfo = this.props.buyView.formView.marketinfo + const coinOptions = this.props.buyView.formView.coinOptions + var coin = marketinfo.pair.split('_')[0].toUpperCase() + + return h('.flex-column', { + style: { + // marginTop: '10px', + padding: '25px', + paddingTop: '5px', + width: '100%', + minHeight: '215px', + alignItems: 'center', + overflowY: 'auto', + }, + }, [ + h('.flex-row', { + style: { + justifyContent: 'center', + alignItems: 'baseline', + height: '42px', + }, + }, [ + h('img', { + src: coinOptions[coin].image, + width: '25px', + height: '25px', + style: { + marginRight: '5px', + }, + }), + + h('.input-container', [ + h('input#fromCoin.buy-inputs.ex-coins', { + type: 'text', + list: 'coinList', + autoFocus: true, + dataset: { + persistentFormId: 'input-coin', + }, + style: { + boxSizing: 'border-box', + }, + onChange: this.handleLiveInput.bind(this), + defaultValue: 'BTC', + }), + + this.renderCoinList(), + + h('i.fa.fa-pencil-square-o.edit-text', { + style: { + fontSize: '12px', + color: '#F7861C', + position: 'relative', + bottom: '48px', + left: '106px', + }, + }), + ]), + + h('.icon-control', [ + h('i.fa.fa-refresh.fa-4.orange', { + style: { + bottom: '5px', + left: '5px', + color: '#F7861C', + }, + onClick: this.updateCoin.bind(this), + }), + h('i.fa.fa-chevron-right.fa-4.orange', { + style: { + position: 'relative', + bottom: '26px', + left: '10px', + color: '#F7861C', + }, + onClick: this.updateCoin.bind(this), + }), + ]), + + h('#toCoin.ex-coins', marketinfo.pair.split('_')[1].toUpperCase()), + + h('img', { + src: coinOptions[marketinfo.pair.split('_')[1].toUpperCase()].image, + width: '25px', + height: '25px', + style: { + marginLeft: '5px', + }, + }), + ]), + h('.flex-column', { + style: { + alignItems: 'flex-start', + }, + }, [ + this.props.warning ? this.props.warning && h('span.error.flex-center', { + style: { + textAlign: 'center', + width: '229px', + height: '82px', + }, + }, + this.props.warning) : this.renderInfo(), + ]), + + h(this.activeToggle('.input-container'), { + style: { + padding: '10px', + paddingTop: '0px', + width: '100%', + }, + }, [ + + h('div', `${coin} Address:`), + + h('input#fromCoinAddress.buy-inputs', { + type: 'text', + placeholder: `Your ${coin} Refund Address`, + dataset: { + persistentFormId: 'refund-address', + }, + style: { + boxSizing: 'border-box', + width: '227px', + height: '30px', + padding: ' 5px ', + }, + }), + + h('i.fa.fa-pencil-square-o.edit-text', { + style: { + fontSize: '12px', + color: '#F7861C', + position: 'relative', + bottom: '10px', + right: '11px', + }, + }), + h('.flex-row', { + style: { + justifyContent: 'flex-end', + }, + }, [ + h('button', { + onClick: this.shift.bind(this), + style: { + marginTop: '10px', + position: 'relative', + bottom: '40px', + }, + }, + 'Submit'), + ]), + ]), + ]) +} + +ShapeshiftForm.prototype.shift = function () { + var props = this.props + var withdrawal = this.props.buyView.buyAddress + var returnAddress = document.getElementById('fromCoinAddress').value + var pair = this.props.buyView.formView.marketinfo.pair + var data = { + 'withdrawal': withdrawal, + 'pair': pair, + 'returnAddress': returnAddress, + // Public api key + 'apiKey': '803d1f5df2ed1b1476e4b9e6bcd089e34d8874595dda6a23b67d93c56ea9cc2445e98a6748b219b2b6ad654d9f075f1f1db139abfa93158c04e825db122c14b6', + } + var message = [ + `Deposit Limit: ${props.buyView.formView.marketinfo.limit}`, + `Deposit Minimum:${props.buyView.formView.marketinfo.minimum}`, + ] + if (isValidAddress(withdrawal)) { + this.props.dispatch(actions.coinShiftRquest(data, message)) + } +} + +ShapeshiftForm.prototype.renderCoinList = function () { + var list = Object.keys(this.props.buyView.formView.coinOptions).map((item) => { + return h('option', { + value: item, + }, item) + }) + + return h('datalist#coinList', { + onClick: (event) => { + event.preventDefault() + }, + }, list) +} + +ShapeshiftForm.prototype.updateCoin = function (event) { + event.preventDefault() + const props = this.props + var coinOptions = this.props.buyView.formView.coinOptions + var coin = document.getElementById('fromCoin').value + + if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { + var message = 'Not a valid coin' + return props.dispatch(actions.displayWarning(message)) + } else { + return props.dispatch(actions.pairUpdate(coin)) + } +} + +ShapeshiftForm.prototype.handleLiveInput = function () { + const props = this.props + var coinOptions = this.props.buyView.formView.coinOptions + var coin = document.getElementById('fromCoin').value + + if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { + return null + } else { + return props.dispatch(actions.pairUpdate(coin)) + } +} + +ShapeshiftForm.prototype.renderInfo = function () { + const marketinfo = this.props.buyView.formView.marketinfo + const coinOptions = this.props.buyView.formView.coinOptions + var coin = marketinfo.pair.split('_')[0].toUpperCase() + + return h('span', { + style: { + }, + }, [ + h('h3.flex-row.text-transform-uppercase', { + style: { + color: '#868686', + paddingTop: '4px', + justifyContent: 'space-around', + textAlign: 'center', + fontSize: '17px', + }, + }, `Market Info for ${marketinfo.pair.replace('_', ' to ').toUpperCase()}:`), + h('.marketinfo', ['Status : ', `${coinOptions[coin].status}`]), + h('.marketinfo', ['Exchange Rate: ', `${marketinfo.rate}`]), + h('.marketinfo', ['Limit: ', `${marketinfo.limit}`]), + h('.marketinfo', ['Minimum : ', `${marketinfo.minimum}`]), + ]) +} + +ShapeshiftForm.prototype.activeToggle = function (elementType) { + if (!this.props.buyView.formView.response || this.props.warning) return elementType + return `${elementType}.inactive` +} + +ShapeshiftForm.prototype.renderLoading = function () { + return h('span', { + style: { + position: 'absolute', + left: '70px', + bottom: '194px', + background: 'transparent', + width: '229px', + height: '82px', + display: 'flex', + justifyContent: 'center', + }, + }, [ + h('img', { + style: { + width: '60px', + }, + src: 'images/loading.svg', + }), + ]) +} diff --git a/ui/responsive/app/components/shift-list-item.js b/ui/responsive/app/components/shift-list-item.js new file mode 100644 index 000000000..32bfbeda4 --- /dev/null +++ b/ui/responsive/app/components/shift-list-item.js @@ -0,0 +1,204 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const vreme = new (require('vreme')) +const explorerLink = require('../../lib/explorer-link') +const actions = require('../actions') +const addressSummary = require('../util').addressSummary + +const CopyButton = require('./copyButton') +const EthBalance = require('./eth-balance') +const Tooltip = require('./tooltip') + + +module.exports = connect(mapStateToProps)(ShiftListItem) + +function mapStateToProps (state) { + return { + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + } +} + +inherits(ShiftListItem, Component) + +function ShiftListItem () { + Component.call(this) +} + +ShiftListItem.prototype.render = function () { + return ( + h('.transaction-list-item.flex-row', { + style: { + paddingTop: '20px', + paddingBottom: '20px', + justifyContent: 'space-around', + alignItems: 'center', + }, + }, [ + h('div', { + style: { + width: '0px', + position: 'relative', + bottom: '19px', + }, + }, [ + h('img', { + src: 'https://info.shapeshift.io/sites/default/files/logo.png', + style: { + height: '35px', + width: '132px', + position: 'absolute', + clip: 'rect(0px,23px,34px,0px)', + }, + }), + ]), + + this.renderInfo(), + this.renderUtilComponents(), + ]) + ) +} + +function formatDate (date) { + return vreme.format(new Date(date), 'March 16 2014 14:30') +} + +ShiftListItem.prototype.renderUtilComponents = function () { + var props = this.props + const { conversionRate, currentCurrency } = props + + switch (props.response.status) { + case 'no_deposits': + return h('.flex-row', [ + h(CopyButton, { + value: this.props.depositAddress, + }), + h(Tooltip, { + title: 'QR Code', + }, [ + h('i.fa.fa-qrcode.pointer.pop-hover', { + onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType)), + style: { + margin: '5px', + marginLeft: '23px', + marginRight: '12px', + fontSize: '20px', + color: '#F7861C', + }, + }), + ]), + ]) + case 'received': + return h('.flex-row') + + case 'complete': + return h('.flex-row', [ + h(CopyButton, { + value: this.props.response.transaction, + }), + h(EthBalance, { + value: `${props.response.outgoingCoin}`, + conversionRate, + currentCurrency, + width: '55px', + shorten: true, + needsParse: false, + incoming: true, + style: { + fontSize: '15px', + color: '#01888C', + }, + }), + ]) + + case 'failed': + return '' + default: + return '' + } +} + +ShiftListItem.prototype.renderInfo = function () { + var props = this.props + switch (props.response.status) { + case 'no_deposits': + return h('.flex-column', { + style: { + width: '200px', + overflow: 'hidden', + }, + }, [ + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, `${props.depositType} to ETH via ShapeShift`), + h('div', 'No deposits received'), + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, formatDate(props.time)), + ]) + case 'received': + return h('.flex-column', { + style: { + width: '200px', + overflow: 'hidden', + }, + }, [ + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, `${props.depositType} to ETH via ShapeShift`), + h('div', 'Conversion in progress'), + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, formatDate(props.time)), + ]) + case 'complete': + var url = explorerLink(props.response.transaction, parseInt('1')) + + return h('.flex-column.pointer', { + style: { + width: '200px', + overflow: 'hidden', + }, + onClick: () => global.platform.openWindow({ url }), + }, [ + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, 'From ShapeShift'), + h('div', formatDate(props.time)), + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, addressSummary(props.response.transaction)), + ]) + + case 'failed': + return h('span.error', '(Failed)') + default: + return '' + } +} diff --git a/ui/responsive/app/components/tab-bar.js b/ui/responsive/app/components/tab-bar.js new file mode 100644 index 000000000..6295e7dd9 --- /dev/null +++ b/ui/responsive/app/components/tab-bar.js @@ -0,0 +1,36 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = TabBar + +inherits(TabBar, Component) +function TabBar () { + Component.call(this) +} + +TabBar.prototype.render = function () { + const props = this.props + const state = this.state || {} + const { tabs = [], defaultTab, tabSelected } = props + const { subview = defaultTab } = state + + return ( + h('.flex-row.space-around.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + paddingTop: '4px', + }, + }, tabs.map((tab) => { + const { key, content } = tab + return h(subview === key ? '.activeForm' : '.inactiveForm.pointer', { + onClick: () => { + this.setState({ subview: key }) + tabSelected(key) + }, + }, content) + })) + ) +} + diff --git a/ui/responsive/app/components/template.js b/ui/responsive/app/components/template.js new file mode 100644 index 000000000..b6ed8eaa0 --- /dev/null +++ b/ui/responsive/app/components/template.js @@ -0,0 +1,18 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = NewComponent + +inherits(NewComponent, Component) +function NewComponent () { + Component.call(this) +} + +NewComponent.prototype.render = function () { + const props = this.props + + return ( + h('span', props.message) + ) +} diff --git a/ui/responsive/app/components/token-cell.js b/ui/responsive/app/components/token-cell.js new file mode 100644 index 000000000..19d7139bb --- /dev/null +++ b/ui/responsive/app/components/token-cell.js @@ -0,0 +1,72 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Identicon = require('./identicon') +const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') + +module.exports = TokenCell + +inherits(TokenCell, Component) +function TokenCell () { + Component.call(this) +} + +TokenCell.prototype.render = function () { + const props = this.props + const { address, symbol, string, network, userAddress } = props + + return ( + h('li.token-cell', { + style: { cursor: network === '1' ? 'pointer' : 'default' }, + onClick: this.view.bind(this, address, userAddress, network), + }, [ + + h(Identicon, { + diameter: 50, + address, + network, + }), + + h('h3', `${string || 0} ${symbol}`), + + h('span', { style: { flex: '1 0 auto' } }), + + /* + h('button', { + onClick: this.send.bind(this, address), + }, 'SEND'), + */ + + ]) + ) +} + +TokenCell.prototype.send = function (address, event) { + event.preventDefault() + event.stopPropagation() + const url = tokenFactoryFor(address) + if (url) { + navigateTo(url) + } +} + +TokenCell.prototype.view = function (address, userAddress, network, event) { + const url = etherscanLinkFor(address, userAddress, network) + if (url) { + navigateTo(url) + } +} + +function navigateTo (url) { + global.platform.openWindow({ url }) +} + +function etherscanLinkFor (tokenAddress, address, network) { + const prefix = prefixForNetwork(network) + return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}` +} + +function tokenFactoryFor (tokenAddress) { + return `https://tokenfactory.surge.sh/#/token/${tokenAddress}` +} + diff --git a/ui/responsive/app/components/token-list.js b/ui/responsive/app/components/token-list.js new file mode 100644 index 000000000..fed7e9f7a --- /dev/null +++ b/ui/responsive/app/components/token-list.js @@ -0,0 +1,194 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const TokenTracker = require('eth-token-tracker') +const TokenCell = require('./token-cell.js') +const normalizeAddress = require('eth-sig-util').normalize + +const defaultTokens = [] +/* +const contracts = require('eth-contract-metadata') +for (const address in contracts) { + const contract = contracts[address] + if (contract.erc20) { + contract.address = address + defaultTokens.push(contract) + } +} +*/ + +module.exports = TokenList + +inherits(TokenList, Component) +function TokenList () { + this.state = { + tokens: [], + isLoading: true, + network: null, + } + Component.call(this) +} + +TokenList.prototype.render = function () { + const state = this.state + const { tokens, isLoading, error } = state + const { userAddress, network } = this.props + + if (isLoading) { + return this.message('Loading') + } + + if (error) { + log.error(error) + return this.message('There was a problem loading your token balances.') + } + + const tokenViews = tokens.map((tokenData) => { + tokenData.network = network + tokenData.userAddress = userAddress + return h(TokenCell, tokenData) + }) + + return h('div', [ + h('ol', { + style: { + height: '260px', + overflowY: 'auto', + display: 'flex', + flexDirection: 'column', + }, + }, [ + h('style', ` + + li.token-cell { + display: flex; + flex-direction: row; + align-items: center; + padding: 10px; + } + + li.token-cell > h3 { + margin-left: 12px; + } + + li.token-cell:hover { + background: white; + cursor: pointer; + } + + `), + ...tokenViews, + tokenViews.length ? null : this.message('No Tokens Found.'), + ]), + this.addTokenButtonElement(), + ]) +} + +TokenList.prototype.addTokenButtonElement = function () { + return h('div', [ + h('div.footer.hover-white.pointer', { + key: 'reveal-account-bar', + onClick: () => { + this.props.addToken() + }, + style: { + display: 'flex', + height: '40px', + padding: '10px', + justifyContent: 'center', + alignItems: 'center', + }, + }, [ + h('i.fa.fa-plus.fa-lg'), + ]), + ]) +} + +TokenList.prototype.message = function (body) { + return h('div', { + style: { + display: 'flex', + height: '250px', + alignItems: 'center', + justifyContent: 'center', + padding: '30px', + }, + }, body) +} + +TokenList.prototype.componentDidMount = function () { + this.createFreshTokenTracker() +} + +TokenList.prototype.createFreshTokenTracker = function () { + if (this.tracker) { + // Clean up old trackers when refreshing: + this.tracker.stop() + this.tracker.removeListener('update', this.balanceUpdater) + this.tracker.removeListener('error', this.showError) + } + + if (!global.ethereumProvider) return + const { userAddress } = this.props + this.tracker = new TokenTracker({ + userAddress, + provider: global.ethereumProvider, + tokens: uniqueMergeTokens(defaultTokens, this.props.tokens), + pollingInterval: 8000, + }) + + + // Set up listener instances for cleaning up + this.balanceUpdater = this.updateBalances.bind(this) + this.showError = (error) => { + this.setState({ error, isLoading: false }) + } + this.tracker.on('update', this.balanceUpdater) + this.tracker.on('error', this.showError) + + this.tracker.updateBalances() + .then(() => { + this.updateBalances(this.tracker.serialize()) + }) + .catch((reason) => { + log.error(`Problem updating balances`, reason) + this.setState({ isLoading: false }) + }) +} + +TokenList.prototype.componentWillUpdate = function (nextProps) { + if (nextProps.network === 'loading') return + const oldNet = this.props.network + const newNet = nextProps.network + + if (oldNet && newNet && newNet !== oldNet) { + this.setState({ isLoading: true }) + this.createFreshTokenTracker() + } +} + +TokenList.prototype.updateBalances = function (tokens) { + const heldTokens = tokens.filter(token => { + return token.balance !== '0' && token.string !== '0.000' + }) + this.setState({ tokens: heldTokens, isLoading: false }) +} + +TokenList.prototype.componentWillUnmount = function () { + if (!this.tracker) return + this.tracker.stop() +} + +function uniqueMergeTokens (tokensA, tokensB) { + const uniqueAddresses = [] + const result = [] + tokensA.concat(tokensB).forEach((token) => { + const normal = normalizeAddress(token.address) + if (!uniqueAddresses.includes(normal)) { + uniqueAddresses.push(normal) + result.push(token) + } + }) + return result +} + diff --git a/ui/responsive/app/components/tooltip.js b/ui/responsive/app/components/tooltip.js new file mode 100644 index 000000000..edbc074bb --- /dev/null +++ b/ui/responsive/app/components/tooltip.js @@ -0,0 +1,22 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ReactTooltip = require('react-tooltip-component') + +module.exports = Tooltip + +inherits(Tooltip, Component) +function Tooltip () { + Component.call(this) +} + +Tooltip.prototype.render = function () { + const props = this.props + const { position, title, children } = props + + return h(ReactTooltip, { + position: position || 'left', + title, + fixed: false, + }, children) +} diff --git a/ui/responsive/app/components/transaction-list-item-icon.js b/ui/responsive/app/components/transaction-list-item-icon.js new file mode 100644 index 000000000..431054340 --- /dev/null +++ b/ui/responsive/app/components/transaction-list-item-icon.js @@ -0,0 +1,68 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Tooltip = require('./tooltip') + +const Identicon = require('./identicon') + +module.exports = TransactionIcon + +inherits(TransactionIcon, Component) +function TransactionIcon () { + Component.call(this) +} + +TransactionIcon.prototype.render = function () { + const { transaction, txParams, isMsg } = this.props + switch (transaction.status) { + case 'unapproved': + return h(!isMsg ? '.unapproved-tx-icon' : 'i.fa.fa-certificate.fa-lg') + + case 'rejected': + return h('i.fa.fa-exclamation-triangle.fa-lg.warning', { + style: { + width: '24px', + }, + }) + + case 'failed': + return h('i.fa.fa-exclamation-triangle.fa-lg.error', { + style: { + width: '24px', + }, + }) + + case 'submitted': + return h(Tooltip, { + title: 'Pending', + position: 'bottom', + }, [ + h('i.fa.fa-ellipsis-h', { + style: { + fontSize: '27px', + }, + }), + ]) + } + + if (isMsg) { + return h('i.fa.fa-certificate.fa-lg', { + style: { + width: '24px', + }, + }) + } + + if (txParams.to) { + return h(Identicon, { + diameter: 24, + address: txParams.to || transaction.hash, + }) + } else { + return h('i.fa.fa-file-text-o.fa-lg', { + style: { + width: '24px', + }, + }) + } +} diff --git a/ui/responsive/app/components/transaction-list-item.js b/ui/responsive/app/components/transaction-list-item.js new file mode 100644 index 000000000..dbda66a31 --- /dev/null +++ b/ui/responsive/app/components/transaction-list-item.js @@ -0,0 +1,165 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const EthBalance = require('./eth-balance') +const addressSummary = require('../util').addressSummary +const explorerLink = require('../../lib/explorer-link') +const CopyButton = require('./copyButton') +const vreme = new (require('vreme')) +const Tooltip = require('./tooltip') +const numberToBN = require('number-to-bn') + +const TransactionIcon = require('./transaction-list-item-icon') +const ShiftListItem = require('./shift-list-item') +module.exports = TransactionListItem + +inherits(TransactionListItem, Component) +function TransactionListItem () { + Component.call(this) +} + +TransactionListItem.prototype.render = function () { + const { transaction, network, conversionRate, currentCurrency } = this.props + if (transaction.key === 'shapeshift') { + if (network === '1') return h(ShiftListItem, transaction) + } + var date = formatDate(transaction.time) + + let isLinkable = false + const numericNet = parseInt(network) + isLinkable = numericNet === 1 || numericNet === 3 || numericNet === 4 || numericNet === 42 + + var isMsg = ('msgParams' in transaction) + var isTx = ('txParams' in transaction) + var isPending = transaction.status === 'unapproved' + let txParams + if (isTx) { + txParams = transaction.txParams + } else if (isMsg) { + txParams = transaction.msgParams + } + + const nonce = txParams.nonce ? numberToBN(txParams.nonce).toString(10) : '' + + const isClickable = ('hash' in transaction && isLinkable) || isPending + return ( + h(`.transaction-list-item.flex-row.flex-space-between${isClickable ? '.pointer' : ''}`, { + onClick: (event) => { + if (isPending) { + this.props.showTx(transaction.id) + } + event.stopPropagation() + if (!transaction.hash || !isLinkable) return + var url = explorerLink(transaction.hash, parseInt(network)) + global.platform.openWindow({ url }) + }, + style: { + padding: '20px 0', + }, + }, [ + + h('.identicon-wrapper.flex-column.flex-center.select-none', [ + h('.pop-hover', { + onClick: (event) => { + event.stopPropagation() + if (!isTx || isPending) return + var url = `https://metamask.github.io/eth-tx-viz/?tx=${transaction.hash}` + global.platform.openWindow({ url }) + }, + }, [ + h(TransactionIcon, { txParams, transaction, isTx, isMsg }), + ]), + ]), + + h(Tooltip, { + title: 'Transaction Number', + position: 'bottom', + }, [ + h('span', { + style: { + display: 'flex', + cursor: 'normal', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + padding: '10px', + }, + }, nonce), + ]), + + h('.flex-column', {style: {width: '200px', overflow: 'hidden'}}, [ + domainField(txParams), + h('div', date), + recipientField(txParams, transaction, isTx, isMsg), + ]), + + // Places a copy button if tx is successful, else places a placeholder empty div. + transaction.hash ? h(CopyButton, { value: transaction.hash }) : h('div', {style: { display: 'flex', alignItems: 'center', width: '26px' }}), + + isTx ? h(EthBalance, { + value: txParams.value, + conversionRate, + currentCurrency, + width: '55px', + shorten: true, + showFiat: false, + style: {fontSize: '15px'}, + }) : h('.flex-column'), + ]) + ) +} + +function domainField (txParams) { + return h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + overflow: 'hidden', + textOverflow: 'ellipsis', + width: '100%', + }, + }, [ + txParams.origin, + ]) +} + +function recipientField (txParams, transaction, isTx, isMsg) { + let message + + if (isMsg) { + message = 'Signature Requested' + } else if (txParams.to) { + message = addressSummary(txParams.to) + } else { + message = 'Contract Published' + } + + return h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + }, + }, [ + message, + failIfFailed(transaction), + ]) +} + +function formatDate (date) { + return vreme.format(new Date(date), 'March 16 2014 14:30') +} + +function failIfFailed (transaction) { + if (transaction.status === 'rejected') { + return h('span.error', ' (Rejected)') + } + if (transaction.err) { + return h(Tooltip, { + title: transaction.err.message, + position: 'bottom', + }, [ + h('span.error', ' (Failed)'), + ]) + } +} diff --git a/ui/responsive/app/components/transaction-list.js b/ui/responsive/app/components/transaction-list.js new file mode 100644 index 000000000..3b4ba741e --- /dev/null +++ b/ui/responsive/app/components/transaction-list.js @@ -0,0 +1,79 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const TransactionListItem = require('./transaction-list-item') + +module.exports = TransactionList + + +inherits(TransactionList, Component) +function TransactionList () { + Component.call(this) +} + +TransactionList.prototype.render = function () { + const { transactions, network, unapprovedMsgs, conversionRate } = this.props + + var shapeShiftTxList + if (network === '1') { + shapeShiftTxList = this.props.shapeShiftTxList + } + const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList) + .sort((a, b) => b.time - a.time) + + return ( + + h('section.transaction-list', [ + + h('style', ` + .transaction-list .transaction-list-item:not(:last-of-type) { + border-bottom: 1px solid #D4D4D4; + } + .transaction-list .transaction-list-item .ether-balance-label { + display: block !important; + font-size: small; + } + `), + + h('.tx-list', { + style: { + overflowY: 'auto', + height: '300px', + padding: '0 20px', + textAlign: 'center', + }, + }, [ + + txsToRender.length + ? txsToRender.map((transaction, i) => { + let key + switch (transaction.key) { + case 'shapeshift': + const { depositAddress, time } = transaction + key = `shift-tx-${depositAddress}-${time}-${i}` + break + default: + key = `tx-${transaction.id}-${i}` + } + return h(TransactionListItem, { + transaction, i, network, key, + conversionRate, + showTx: (txId) => { + this.props.viewPendingTx(txId) + }, + }) + }) + : h('.flex-center', { + style: { + flexDirection: 'column', + height: '100%', + }, + }, [ + 'No transaction history.', + ]), + ]), + ]) + ) +} + diff --git a/ui/responsive/app/conf-tx.js b/ui/responsive/app/conf-tx.js new file mode 100644 index 000000000..63b77ef7f --- /dev/null +++ b/ui/responsive/app/conf-tx.js @@ -0,0 +1,213 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') +const NetworkIndicator = require('./components/network') +const txHelper = require('../lib/tx-helper') +const isPopupOrNotification = require('../../../app/scripts/lib/is-popup-or-notification') + +const PendingTx = require('./components/pending-tx') +const PendingMsg = require('./components/pending-msg') +const PendingPersonalMsg = require('./components/pending-personal-msg') +const Loading = require('./components/loading') + +module.exports = connect(mapStateToProps)(ConfirmTxScreen) + +function mapStateToProps (state) { + return { + identities: state.metamask.identities, + accounts: state.metamask.accounts, + selectedAddress: state.metamask.selectedAddress, + unapprovedTxs: state.metamask.unapprovedTxs, + unapprovedMsgs: state.metamask.unapprovedMsgs, + unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs, + index: state.appState.currentView.context, + warning: state.appState.warning, + network: state.metamask.network, + provider: state.metamask.provider, + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + blockGasLimit: state.metamask.currentBlockGasLimit, + } +} + +inherits(ConfirmTxScreen, Component) +function ConfirmTxScreen () { + Component.call(this) +} + +ConfirmTxScreen.prototype.render = function () { + const props = this.props + const { network, provider, unapprovedTxs, currentCurrency, + unapprovedMsgs, unapprovedPersonalMsgs, conversionRate, blockGasLimit } = props + + var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) + + var txData = unconfTxList[props.index] || {} + var txParams = txData.params || {} + var isNotification = isPopupOrNotification() === 'notification' + + + log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`) + if (unconfTxList.length === 0) return h(Loading, { isLoading: true }) + + return ( + + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + !isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: this.goHome.bind(this), + }) : null, + h('h2.page-subtitle', 'Confirm Transaction'), + isNotification ? h(NetworkIndicator, { + network: network, + provider: provider, + }) : null, + ]), + + h('h3', { + style: { + alignSelf: 'center', + display: unconfTxList.length > 1 ? 'block' : 'none', + }, + }, [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + style: { + display: props.index === 0 ? 'none' : 'inline-block', + }, + onClick: () => props.dispatch(actions.previousTx()), + }), + ` ${props.index + 1} of ${unconfTxList.length} `, + h('i.fa.fa-arrow-right.fa-lg.cursor-pointer', { + style: { + display: props.index + 1 === unconfTxList.length ? 'none' : 'inline-block', + }, + onClick: () => props.dispatch(actions.nextTx()), + }), + ]), + + warningIfExists(props.warning), + + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + + currentTxView({ + // Properties + txData: txData, + key: txData.id, + selectedAddress: props.selectedAddress, + accounts: props.accounts, + identities: props.identities, + conversionRate, + currentCurrency, + blockGasLimit, + // Actions + buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), + sendTransaction: this.sendTransaction.bind(this), + cancelTransaction: this.cancelTransaction.bind(this, txData), + signMessage: this.signMessage.bind(this, txData), + signPersonalMessage: this.signPersonalMessage.bind(this, txData), + cancelMessage: this.cancelMessage.bind(this, txData), + cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), + }), + + ]), + ]) + ) +} + +function currentTxView (opts) { + log.info('rendering current tx view') + const { txData } = opts + const { txParams, msgParams, type } = txData + + if (txParams) { + log.debug('txParams detected, rendering pending tx') + return h(PendingTx, opts) + } else if (msgParams) { + log.debug('msgParams detected, rendering pending msg') + + if (type === 'eth_sign') { + log.debug('rendering eth_sign message') + return h(PendingMsg, opts) + } else if (type === 'personal_sign') { + log.debug('rendering personal_sign message') + return h(PendingPersonalMsg, opts) + } + } +} + +ConfirmTxScreen.prototype.buyEth = function (address, event) { + event.preventDefault() + this.props.dispatch(actions.buyEthView(address)) +} + +ConfirmTxScreen.prototype.sendTransaction = function (txData, event) { + this.stopPropagation(event) + this.props.dispatch(actions.updateAndApproveTx(txData)) +} + +ConfirmTxScreen.prototype.cancelTransaction = function (txData, event) { + this.stopPropagation(event) + event.preventDefault() + this.props.dispatch(actions.cancelTx(txData)) +} + +ConfirmTxScreen.prototype.signMessage = function (msgData, event) { + log.info('conf-tx.js: signing message') + var params = msgData.msgParams + params.metamaskId = msgData.id + this.stopPropagation(event) + this.props.dispatch(actions.signMsg(params)) +} + +ConfirmTxScreen.prototype.stopPropagation = function (event) { + if (event.stopPropagation) { + event.stopPropagation() + } +} + +ConfirmTxScreen.prototype.signPersonalMessage = function (msgData, event) { + log.info('conf-tx.js: signing personal message') + var params = msgData.msgParams + params.metamaskId = msgData.id + this.stopPropagation(event) + this.props.dispatch(actions.signPersonalMsg(params)) +} + +ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) { + log.info('canceling message') + this.stopPropagation(event) + this.props.dispatch(actions.cancelMsg(msgData)) +} + +ConfirmTxScreen.prototype.cancelPersonalMessage = function (msgData, event) { + log.info('canceling personal message') + this.stopPropagation(event) + this.props.dispatch(actions.cancelPersonalMsg(msgData)) +} + +ConfirmTxScreen.prototype.goHome = function (event) { + this.stopPropagation(event) + this.props.dispatch(actions.goHome()) +} + +function warningIfExists (warning) { + if (warning && + // Do not display user rejections on this screen: + warning.indexOf('User denied transaction signature') === -1) { + return h('.error', { + style: { + margin: 'auto', + }, + }, warning) + } +} diff --git a/ui/responsive/app/config.js b/ui/responsive/app/config.js new file mode 100644 index 000000000..62785c49b --- /dev/null +++ b/ui/responsive/app/config.js @@ -0,0 +1,211 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') +const currencies = require('./conversion.json').rows +const validUrl = require('valid-url') +const copyToClipboard = require('copy-to-clipboard') + +module.exports = connect(mapStateToProps)(ConfigScreen) + +function mapStateToProps (state) { + return { + metamask: state.metamask, + warning: state.appState.warning, + } +} + +inherits(ConfigScreen, Component) +function ConfigScreen () { + Component.call(this) +} + +ConfigScreen.prototype.render = function () { + var state = this.props + var metamaskState = state.metamask + var warning = state.warning + + return ( + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + state.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Settings'), + ]), + + h('.error', { + style: { + display: warning ? 'block' : 'none', + padding: '0 20px', + textAlign: 'center', + }, + }, warning), + + // conf view + h('.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-space-around', { + style: { + padding: '20px', + }, + }, [ + + currentProviderDisplay(metamaskState), + + h('div', { style: {display: 'flex'} }, [ + h('input#new_rpc', { + placeholder: 'New RPC URL', + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onKeyPress (event) { + if (event.key === 'Enter') { + var element = event.target + var newRpc = element.value + rpcValidation(newRpc, state) + } + }, + }), + h('button', { + style: { + alignSelf: 'center', + }, + onClick (event) { + event.preventDefault() + var element = document.querySelector('input#new_rpc') + var newRpc = element.value + rpcValidation(newRpc, state) + }, + }, 'Save'), + ]), + + h('hr.horizontal-line'), + + currentConversionInformation(metamaskState, state), + + h('hr.horizontal-line'), + + h('div', { + style: { + marginTop: '20px', + }, + }, [ + h('p', { + style: { + fontFamily: 'Montserrat Light', + fontSize: '13px', + }, + }, `State logs contain your public account addresses and sent transactions.`), + h('br'), + h('button', { + style: { + alignSelf: 'center', + }, + onClick (event) { + copyToClipboard(window.logState()) + }, + }, 'Copy State Logs'), + ]), + + h('hr.horizontal-line'), + + h('div', { + style: { + marginTop: '20px', + }, + }, [ + h('button', { + style: { + alignSelf: 'center', + }, + onClick (event) { + event.preventDefault() + state.dispatch(actions.revealSeedConfirmation()) + }, + }, 'Reveal Seed Words'), + ]), + + ]), + ]), + ]) + ) +} + +function rpcValidation (newRpc, state) { + if (validUrl.isWebUri(newRpc)) { + state.dispatch(actions.setRpcTarget(newRpc)) + } else { + var appendedRpc = `http://${newRpc}` + if (validUrl.isWebUri(appendedRpc)) { + state.dispatch(actions.displayWarning('URIs require the appropriate HTTP/HTTPS prefix.')) + } else { + state.dispatch(actions.displayWarning('Invalid RPC URI')) + } + } +} + +function currentConversionInformation (metamaskState, state) { + var currentCurrency = metamaskState.currentCurrency + var conversionDate = metamaskState.conversionDate + return h('div', [ + h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, 'Current Conversion'), + h('span', {style: { fontWeight: 'bold', paddingRight: '10px', fontSize: '13px'}}, `Updated ${Date(conversionDate)}`), + h('select#currentCurrency', { + onChange (event) { + event.preventDefault() + var element = document.getElementById('currentCurrency') + var newCurrency = element.value + state.dispatch(actions.setCurrentCurrency(newCurrency)) + }, + defaultValue: currentCurrency, + }, currencies.map((currency) => { + return h('option', {key: currency.code, value: currency.code}, `${currency.code} - ${currency.name}`) + }) + ), + ]) +} + +function currentProviderDisplay (metamaskState) { + var provider = metamaskState.provider + var title, value + + switch (provider.type) { + + case 'mainnet': + title = 'Current Network' + value = 'Main Ethereum Network' + break + + case 'ropsten': + title = 'Current Network' + value = 'Ropsten Test Network' + break + + case 'kovan': + title = 'Current Network' + value = 'Kovan Test Network' + break + + case 'rinkeby': + title = 'Current Network' + value = 'Rinkeby Test Network' + break + + default: + title = 'Current RPC' + value = metamaskState.provider.rpcTarget + } + + return h('div', [ + h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, title), + h('span', value), + ]) +} diff --git a/ui/responsive/app/conversion.json b/ui/responsive/app/conversion.json new file mode 100644 index 000000000..155ffc4fc --- /dev/null +++ b/ui/responsive/app/conversion.json @@ -0,0 +1,207 @@ +{ + "rows": [ + { + "code": "REP", + "name": "Augur", + "statuses": [ + "primary" + ] + }, + { + "code": "BCN", + "name": "Bytecoin", + "statuses": [ + "primary" + ] + }, + { + "code": "BTC", + "name": "Bitcoin", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "BTS", + "name": "BitShares", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "BLK", + "name": "Blackcoin", + "statuses": [ + "primary" + ] + }, + { + "code": "GBP", + "name": "British Pound Sterling", + "statuses": [ + "secondary" + ] + }, + { + "code": "CAD", + "name": "Canadian Dollar", + "statuses": [ + "secondary" + ] + }, + { + "code": "CNY", + "name": "Chinese Yuan", + "statuses": [ + "secondary" + ] + }, + { + "code": "DSH", + "name": "Dashcoin", + "statuses": [ + "primary" + ] + }, + { + "code": "DOGE", + "name": "Dogecoin", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "ETC", + "name": "Ethereum Classic", + "statuses": [ + "primary" + ] + }, + { + "code": "EUR", + "name": "Euro", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "GNO", + "name": "GNO", + "statuses": [ + "primary" + ] + }, + { + "code": "GNT", + "name": "GNT", + "statuses": [ + "primary" + ] + }, + { + "code": "JPY", + "name": "Japanese Yen", + "statuses": [ + "secondary" + ] + }, + { + "code": "LTC", + "name": "Litecoin", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "MAID", + "name": "MaidSafeCoin", + "statuses": [ + "primary" + ] + }, + { + "code": "XEM", + "name": "NEM", + "statuses": [ + "primary" + ] + }, + { + "code": "XLM", + "name": "Stellar", + "statuses": [ + "primary" + ] + }, + { + "code": "XMR", + "name": "Monero", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "XRP", + "name": "Ripple", + "statuses": [ + "primary" + ] + }, + { + "code": "RUR", + "name": "Ruble", + "statuses": [ + "secondary" + ] + }, + { + "code": "STEEM", + "name": "Steem", + "statuses": [ + "primary" + ] + }, + { + "code": "STRAT", + "name": "STRAT", + "statuses": [ + "primary" + ] + }, + { + "code": "UAH", + "name": "Ukrainian Hryvnia", + "statuses": [ + "secondary" + ] + }, + { + "code": "USD", + "name": "US Dollar", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "WAVES", + "name": "WAVES", + "statuses": [ + "primary" + ] + }, + { + "code": "ZEC", + "name": "Zcash", + "statuses": [ + "primary" + ] + } + ] +} diff --git a/ui/responsive/app/css/debug.css b/ui/responsive/app/css/debug.css new file mode 100644 index 000000000..3e125bcd4 --- /dev/null +++ b/ui/responsive/app/css/debug.css @@ -0,0 +1,21 @@ +/* +debug / dev +*/ + +#app-content { + border: 2px solid green; +} + +#design-container { + position: absolute; + left: 360px; + top: -42px; + width: calc(100vw - 360px); + height: 100vh; + overflow: scroll; +} + +#design-container img { + width: 2000px; + margin-right: 600px; +} \ No newline at end of file diff --git a/ui/responsive/app/css/fonts.css b/ui/responsive/app/css/fonts.css new file mode 100644 index 000000000..3b9f581b9 --- /dev/null +++ b/ui/responsive/app/css/fonts.css @@ -0,0 +1,36 @@ +@import url(https://fonts.googleapis.com/css?family=Roboto:300,500); +@import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css); + +@font-face { + font-family: 'Montserrat Regular'; + src: url('/fonts/Montserrat/Montserrat-Regular.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; + font-size: 'small'; + +} + +@font-face { + font-family: 'Montserrat Bold'; + src: url('/fonts/Montserrat/Montserrat-Bold.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-Bold.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Montserrat Light'; + src: url('/fonts/Montserrat/Montserrat-Light.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-Light.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Montserrat UltraLight'; + src: url('/fonts/Montserrat/Montserrat-UltraLight.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-UltraLight.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} diff --git a/ui/responsive/app/css/index.css b/ui/responsive/app/css/index.css new file mode 100644 index 000000000..808aafb4c --- /dev/null +++ b/ui/responsive/app/css/index.css @@ -0,0 +1,667 @@ +/* +faint orange (textfield shades) #FAF6F0 +light orange (button shades): #F5C26D +dark orange (text): #F5A623 +borders/font/any gray: #4A4A4A +*/ + +/* +application specific styles +*/ + +* { + box-sizing: border-box; +} + +html, body { + font-family: 'Montserrat Regular', Arial; + color: #4D4D4D; + font-weight: 300; + line-height: 1.4em; + background: #F7F7F7; +} + +input:focus, textarea:focus { + outline: none; +} + +#app-content { + overflow-x: hidden; + min-width: 357px; + width: 360px; + height: 500px; +} + +button, input[type="submit"] { + font-family: 'Montserrat Bold'; + outline: none; + cursor: pointer; + padding: 8px 12px; + border: none; + color: white; + transform-origin: center center; + transition: transform 50ms ease-in; + /* default orange */ + background: rgba(247, 134, 28, 1); + box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); +} + +.btn-green, input[type="submit"].btn-green { + background: rgba(106, 195, 96, 1); + box-shadow: 0px 3px 6px rgba(106, 195, 96, 0.36); +} + +.btn-red { + background: rgba(254, 35, 17, 1); + box-shadow: 0px 3px 6px rgba(254, 35, 17, 0.36); +} + +button[disabled], input[type="submit"][disabled] { + cursor: not-allowed; + background: rgba(197, 197, 197, 1); + box-shadow: 0px 3px 6px rgba(197, 197, 197, 0.36); +} + +button.spaced { + margin: 2px; +} + +button:not([disabled]):hover, input[type="submit"]:not([disabled]):hover { + transform: scale(1.1); +} +button:not([disabled]):active, input[type="submit"]:not([disabled]):active { + transform: scale(0.95); +} + +a { + text-decoration: none; + color: inherit; +} + +a:hover{ + color: #df6b0e; +} + +/* +app +*/ + +.active { + color: #909090; +} + +button.primary { + padding: 8px 12px; + background: #F7861C; + box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); + color: white; + font-size: 1.1em; + font-family: 'Montserrat Regular'; + text-transform: uppercase; +} + +button.btn-thin { + border: 1px solid; + border-color: #4D4D4D; + color: #4D4D4D; + background: rgb(255, 174, 41); + border-radius: 4px; + min-width: 200px; + margin: 12px 0; + padding: 6px; + font-size: 13px; +} + +.app-header { + padding: 6px 8px; +} + +.app-header h1 { + font-family: 'Montserrat Regular'; + text-transform: uppercase; + color: #AEAEAE; +} + +h2.page-subtitle { + font-family: 'Montserrat Regular'; + text-transform: uppercase; + color: #AEAEAE; + font-size: 1em; + margin: 12px; +} + +.app-primary { + +} + +.app-footer { + padding-bottom: 10px; + align-items: center; +} + +.identicon { + height: 46px; + width: 46px; + background-size: cover; + border-radius: 100%; + border: 3px solid gray; +} + +textarea.twelve-word-phrase { + padding: 12px; + width: 300px; + height: 140px; + font-size: 16px; + background: white; + resize: none; +} + +.network-indicator { + display: flex; + align-items: center; + font-size: 0.6em; + +} + +.network-name { + width: 5.2em; + line-height: 9px; + text-rendering: geometricPrecision; +} + +.check { + margin-left: 7px; + color: #F7861C; + flex: 1 0 auto; + display: flex; + justify-content: flex-end; +} +/* +app sections +*/ + +/* initialize */ + +.initialize-screen hr { + width: 60px; + margin: 12px; + border-color: #F7861C; + border-style: solid; +} + +.initialize-screen label { + margin-top: 20px; +} + +.initialize-screen button.create-vault { + margin-top: 40px; +} + +.initialize-screen .warning { + font-size: 14px; + margin: 0 16px; +} + +/* unlock */ +.error { + color: #E20202; +} + +.warning { + color: #FFAE00; +} + +.lock { + width: 50px; + height: 50px; +} + +.lock.locked { + transform: scale(1.5); + opacity: 0.0; + transition: opacity 400ms ease-in, transform 400ms ease-in; +} +.lock.unlocked { + transform: scale(1); + opacity: 1; + transition: opacity 500ms ease-out, transform 500ms ease-out, background 200ms ease-in; +} + +.lock.locked .lock-top { + transform: scaleX(1) translateX(0); + transition: transform 250ms ease-in; +} +.lock.unlocked .lock-top { + transform: scaleX(-1) translateX(-12px); + transition: transform 250ms ease-in; +} +.lock.unlocked:hover { + border-radius: 4px; + background: #e5e5e5; + border: 1px solid #b1b1b1; +} +.lock.unlocked:active { + background: #c3c3c3; +} + +.section-title .fa-arrow-left { + margin: -2px 8px 0px -8px; +} + +.unlock-screen #metamask-mascot-container { + margin-top: 24px; +} + +.unlock-screen h1 { + margin-top: -28px; + margin-bottom: 42px; +} + +.unlock-screen input[type=password] { + width: 260px; + /*height: 36px; + margin-bottom: 24px; + padding: 8px;*/ +} + +.sizing-input{ + font-size: 14px; + height: 30px; + padding-left: 5px; +} +.editable-label{ + display: flex; +} +/* Webkit */ +.unlock-screen input::-webkit-input-placeholder { + text-align: center; + font-size: 1.2em; +} +/* Firefox 18- */ +.unlock-screen input:-moz-placeholder { + text-align: center; + font-size: 1.2em; +} +/* Firefox 19+ */ +.unlock-screen input::-moz-placeholder { + text-align: center; + font-size: 1.2em; +} +/* IE */ +.unlock-screen input:-ms-input-placeholder { + text-align: center; + font-size: 1.2em; +} + +input.large-input, textarea.large-input { + /*margin-bottom: 24px;*/ + padding: 8px; +} + +input.large-input { + height: 36px; +} + +.letter-spacey { + letter-spacing: 0.1em; +} + + + +/* accounts */ + +.accounts-section { + margin: 0 0px; +} + +.accounts-section .horizontal-line { + margin: 0px 18px; +} + +.accounts-list-option { + height: 120px; +} + +.accounts-list-option .identicon-wrapper { + width: 100px; +} + +.unconftx-link { + margin-top: 24px; + cursor: pointer; +} + +.unconftx-link .fa-arrow-right { + margin: 0px -8px 0px 8px; +} + +/* identity panel */ + +.identity-panel { + font-weight: 500; +} + +.identity-panel .identicon-wrapper { + margin: 4px; + margin-top: 8px; + display: flex; + align-items: center; +} + +.identity-panel .identicon-wrapper span { + margin: 0 auto; +} + +.identity-panel .identity-data { + margin: 8px 8px 8px 18px; +} + +.identity-panel i { + margin-top: 32px; + margin-right: 6px; + color: #B9B9B9; +} + +.identity-panel .arrow-right { + padding-left: 18px; + width: 42px; + min-width: 18px; + height: 100%; +} + +.identity-copy.flex-column { + flex: 0.25 0 auto; + justify-content: center; +} + +/* accounts screen */ + +.identity-section { + +} + +.identity-section .identity-panel { + background: #E9E9E9; + border-bottom: 1px solid #B1B1B1; + cursor: pointer; +} + +.identity-section .identity-panel.selected { + background: white; + color: #F3C83E; +} + +.identity-section .identity-panel.selected .identicon { + border-color: orange; +} + +.identity-section .accounts-list-option:hover, +.identity-section .accounts-list-option.selected { + background:white; +} + +/* account detail screen */ + +.account-detail-section { + +} +.name-label{ + +} + +.unapproved-tx-icon { + height: 16px; + width: 16px; + background: rgb(47, 174, 244); + border-color: #AEAEAE; + border-radius: 13px; +} + +.edit-text { + height: 100%; + visibility: hidden; +} +.editing-label { + display: flex; + justify-content: flex-start; + margin-left: 50px; + margin-bottom: 2px; + font-size: 11px; + text-rendering: geometricPrecision; + color: #F7861C; +} +.name-label:hover .edit-text { + visibility: visible; +} +/* tx confirm */ + +.unconftx-section input[type=password] { + height: 22px; + padding: 2px; + margin: 12px; + margin-bottom: 24px; + border-radius: 4px; + border: 2px solid #F3C83E; + background: #FAF6F0; +} + +/* Send Screen */ + +.send-screen { + +} + +.send-screen section { + margin: 8px 16px; +} + +.send-screen input { + width: 100%; + font-size: 12px; +} + +/* Ether Balance Widget */ + +.ether-balance-amount { + color: #F7861C; +} + +.ether-balance-label { + color: #ABA9AA; +} + +/* Info screen */ +.info-gray{ + font-family: 'Montserrat Regular'; + text-transform: uppercase; + color: #AEAEAE; +} + +.icon-size{ + width: 20px; +} + +.info{ + font-family: 'Montserrat Regular', Arial; + padding-bottom: 10px; + display: inline-block; + padding-left: 5px; +} + +/* buy eth warning screen */ +.custom-radios { + justify-content: space-around; + align-items: center; +} + + +.custom-radio-selected { + width: 17px; + height: 17px; + border: solid; + border-style: double; + border-radius: 15px; + border-width: 5px; + background: rgba(247, 134, 28, 1); + border-color: #F7F7F7; +} + +.custom-radio-inactive { + width: 14px; + height: 14px; + border: solid; + border-width: 1px; + border-radius: 24px; + border-color: #AEAEAE; +} + +.radio-titles { + color: rgba(247, 134, 28, 1); +} + +.radio-titles-subtext { + +} + +.selected-exchange { + +} + +.buy-radio { + +} + +.eth-warning{ + transition: opacity 400ms ease-in, transform 400ms ease-in; +} + +.buy-subview{ + transition: opacity 400ms ease-in, transform 400ms ease-in; +} + +.input-container:hover .edit-text{ + visibility: visible; +} + +.buy-inputs{ + font-family: 'Montserrat Light'; + font-size: 13px; + height: 20px; + background: transparent; + box-sizing: border-box; + border: solid; + border-color: transparent; + border-width: 0.5px; + border-radius: 2px; + +} +.input-container:hover .buy-inputs{ + box-sizing: inherit; + border: solid; + border-color: #F7861C; + border-width: 0.5px; + border-radius: 2px; +} + +.buy-inputs:focus{ + border: solid; + border-color: #F7861C; + border-width: 0.5px; + border-radius: 2px; +} + +.activeForm { + background: #F7F7F7; + border: none; + border-radius: 8px 8px 0px 0px; + width: 50%; + text-align: center; + padding-bottom: 4px; + +} + +.inactiveForm { + border: none; + border-radius: 8px 8px 0px 0px; + width: 50%; + text-align: center; + padding-bottom: 4px; +} + +.ex-coins { + font-family: 'Montserrat Regular'; + text-transform: uppercase; + text-align: center; + font-size: 33px; + width: 118px; + height: 42px; + padding: 1px; + color: #4D4D4D; +} + +.marketinfo{ + font-family: 'Montserrat light'; + color: #AEAEAE; + font-size: 15px; + line-height: 17px; +} + +#fromCoin::-webkit-calendar-picker-indicator { + display: none; +} + +#coinList { + width: 400px; + height: 500px; + overflow: scroll; +} + +.icon-control .fa-refresh{ + visibility: hidden; +} + +.icon-control:hover .fa-refresh{ + visibility: visible; +} + +.icon-control:hover .fa-chevron-right{ + visibility: hidden; +} + +.inactive { + color: #AEAEAE; +} + +.inactive button{ + background: #AEAEAE; + color: white; +} + +.ellip-address { + overflow: hidden; + text-overflow: ellipsis; + width: 5em; + font-size: 14px; + font-family: "Montserrat Light"; + margin-left: 5px; +} + +.qr-header { + font-size: 25px; + margin-top: 40px; +} + +.qr-message { + font-size: 12px; + color: #F7861C; +} + +div.message-container > div:first-child { + margin-top: 18px; + font-size: 15px; + color: #4D4D4D; +} + +.pop-hover:hover { + transform: scale(1.1); +} diff --git a/ui/responsive/app/css/lib.css b/ui/responsive/app/css/lib.css new file mode 100644 index 000000000..910a24ee2 --- /dev/null +++ b/ui/responsive/app/css/lib.css @@ -0,0 +1,268 @@ +/* color */ + +.color-orange { + color: #F7861C; +} + +.color-forest { + color: #0A5448; +} + +/* lib */ + +.full-width { + width: 100%; +} + +.full-height { + height: 100%; +} + +.flex-column { + display: flex; + flex-direction: column; +} + +.space-between { + justify-content: space-between; +} + +.space-around { + justify-content: space-around; +} + +.flex-column-bottom { + display: flex; + flex-direction: column-reverse; +} + +.flex-row { + display: flex; + flex-direction: row; +} + +.flex-space-between { + justify-content: space-between; +} + +.flex-space-around { + justify-content: space-around; +} + +.flex-right { + display: flex; + flex-direction: row; + justify-content: flex-end; +} + +.flex-left { + display: flex; + flex-direction: row; + justify-content: flex-start; +} + +.flex-fixed { + flex: none; +} + +.flex-basis-auto { + flex-basis: auto; +} + +.flex-grow { + flex: 1 1 auto; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.flex-center { + display: flex; + justify-content: center; + align-items: center; +} + +.flex-justify-center { + justify-content: center; +} + +.flex-align-center { + align-items: center; +} + +.flex-self-end { + align-self: flex-end; +} + +.flex-self-stretch { + align-self: stretch; +} + +.flex-vertical { + flex-direction: column; +} + +.z-bump { + z-index: 1; +} + +.select-none { + cursor: inherit; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.pointer { + cursor: pointer; +} +.cursor-pointer { + cursor: pointer; + transform-origin: center center; + transition: transform 50ms ease-in-out; +} +.cursor-pointer:hover { + transform: scale(1.1); +} +.cursor-pointer:active { + transform: scale(0.95); +} + +.cursor-disabled { + cursor: not-allowed; +} + +.margin-bottom-sml { + margin-bottom: 20px; +} + +.margin-bottom-med { + margin-bottom: 40px; +} + +.margin-right-left { + margin: 0 20px; +} + +.bold { + font-weight: bold; +} + +.text-transform-uppercase { + text-transform: uppercase; +} + +.font-small { + font-size: 12px; +} + +.font-medium { + font-size: 1.2em; +} + +hr.horizontal-line { + display: block; + height: 1px; + border: 0; + border-top: 1px solid #ccc; + margin: 1em 0; + padding: 0; +} + +.hover-white:hover { + background: white; +} + +.red-dot { + background: #E91550; + color: white; + border-radius: 10px; +} + +.diamond { + transform: rotate(45deg); + background: #038789; +} + +.hollow-diamond { + transform: rotate(45deg); + border: 3px solid #690496; +} + +.golden-square { + background: #EBB33F; +} + +.pending-dot { + background: red; + left: 14px; + top: 14px; + color: white; + border-radius: 10px; + height: 20px; + min-width: 20px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + z-index: 1; +} + +.keyring-label { + z-index: 1; + font-size: 11px; + background: rgba(255,0,0,0.8); + bottom: -47px; + color: white; + border-radius: 10px; + height: 20px; + min-width: 20px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding: 4px; +} + +.ether-balance { + display: flex; + align-items: center; +} + +.menu-icon { + display: inline-block; + height: 9px; + min-width: 9px; + margin: 13px; +} +.ether-icon { + background: rgb(0, 163, 68); + border-radius: 20px; +} +.testnet-icon { + background: #2465E1; +} + +.drop-menu-item { + display: flex; + align-items: center; +} + +.invisible { + visibility: hidden; +} + +.one-line-concat { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.critical-error { + text-align: center; + margin-top: 20px; + color: red; +} diff --git a/ui/responsive/app/css/reset.css b/ui/responsive/app/css/reset.css new file mode 100644 index 000000000..9ce89e8bc --- /dev/null +++ b/ui/responsive/app/css/reset.css @@ -0,0 +1,48 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +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; +} \ No newline at end of file diff --git a/ui/responsive/app/css/transitions.css b/ui/responsive/app/css/transitions.css new file mode 100644 index 000000000..393a944f9 --- /dev/null +++ b/ui/responsive/app/css/transitions.css @@ -0,0 +1,42 @@ +/* universal */ +.app-primary .main-enter { + position: absolute; + width: 100%; +} + +/* center position */ +.app-primary.from-right .main-enter-active, +.app-primary.from-left .main-enter-active { + overflow-x: hidden; + transform: translateX(0px); + transition: transform 300ms ease-in; +} + +/* exited positions */ +.app-primary.from-left .main-leave-active { + transform: translateX(360px); + transition: transform 300ms ease-in; +} +.app-primary.from-right .main-leave-active { + transform: translateX(-360px); + transition: transform 300ms ease-in; +} + +/* loader transitions */ +.loader-enter, .loader-leave-active { + opacity: 0.0; + transition: opacity 150 ease-in; +} +.loader-enter-active, .loader-leave { + opacity: 1.0; + transition: opacity 150 ease-in; +} + +/* entering positions */ +.app-primary.from-right .main-enter:not(.main-enter-active) { + transform: translateX(360px); +} +.app-primary.from-left .main-enter:not(.main-enter-active) { + transform: translateX(-360px); +} + diff --git a/ui/responsive/app/first-time/init-menu.js b/ui/responsive/app/first-time/init-menu.js new file mode 100644 index 000000000..cc7c51bd3 --- /dev/null +++ b/ui/responsive/app/first-time/init-menu.js @@ -0,0 +1,179 @@ +const inherits = require('util').inherits +const EventEmitter = require('events').EventEmitter +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const Mascot = require('../components/mascot') +const actions = require('../actions') +const Tooltip = require('../components/tooltip') +const getCaretCoordinates = require('textarea-caret') + +module.exports = connect(mapStateToProps)(InitializeMenuScreen) + +inherits(InitializeMenuScreen, Component) +function InitializeMenuScreen () { + Component.call(this) + this.animationEventEmitter = new EventEmitter() +} + +function mapStateToProps (state) { + return { + // state from plugin + currentView: state.appState.currentView, + warning: state.appState.warning, + } +} + +InitializeMenuScreen.prototype.render = function () { + var state = this.props + + switch (state.currentView.name) { + + default: + return this.renderMenu(state) + + } +} + +// InitializeMenuScreen.prototype.componentDidMount = function(){ +// document.getElementById('password-box').focus() +// } + +InitializeMenuScreen.prototype.renderMenu = function (state) { + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + h(Mascot, { + animationEventEmitter: this.animationEventEmitter, + }), + + h('h1', { + style: { + fontSize: '1.3em', + textTransform: 'uppercase', + color: '#7F8082', + marginBottom: 10, + }, + }, 'MetaMask'), + + + h('div', [ + h('h3', { + style: { + fontSize: '0.8em', + color: '#7F8082', + display: 'inline', + }, + }, 'Encrypt your new DEN'), + + h(Tooltip, { + title: 'Your DEN is your password-encrypted storage within MetaMask.', + }, [ + h('i.fa.fa-question-circle.pointer', { + style: { + fontSize: '18px', + position: 'relative', + color: 'rgb(247, 134, 28)', + top: '2px', + marginLeft: '4px', + }, + }), + ]), + ]), + + h('span.in-progress-notification', state.warning), + + // password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: 'New Password (min 8 chars)', + onInput: this.inputChanged.bind(this), + style: { + width: 260, + marginTop: 12, + }, + }), + + // confirm password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box-confirm', + placeholder: 'Confirm Password', + onKeyPress: this.createVaultOnEnter.bind(this), + onInput: this.inputChanged.bind(this), + style: { + width: 260, + marginTop: 16, + }, + }), + + + h('button.primary', { + onClick: this.createNewVaultAndKeychain.bind(this), + style: { + margin: 12, + }, + }, 'Create'), + + h('.flex-row.flex-center.flex-grow', [ + h('p.pointer', { + onClick: this.showRestoreVault.bind(this), + style: { + fontSize: '0.8em', + color: 'rgb(247, 134, 28)', + textDecoration: 'underline', + }, + }, 'Import Existing DEN'), + ]), + + ]) + ) +} + +InitializeMenuScreen.prototype.createVaultOnEnter = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.createNewVaultAndKeychain() + } +} + +InitializeMenuScreen.prototype.componentDidMount = function () { + document.getElementById('password-box').focus() +} + +InitializeMenuScreen.prototype.showRestoreVault = function () { + this.props.dispatch(actions.showRestoreVault()) +} + +InitializeMenuScreen.prototype.createNewVaultAndKeychain = function () { + var passwordBox = document.getElementById('password-box') + var password = passwordBox.value + var passwordConfirmBox = document.getElementById('password-box-confirm') + var passwordConfirm = passwordConfirmBox.value + + if (password.length < 8) { + this.warning = 'password not long enough' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + if (password !== passwordConfirm) { + this.warning = 'passwords don\'t match' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + + this.props.dispatch(actions.createNewVaultAndKeychain(password)) +} + +InitializeMenuScreen.prototype.inputChanged = function (event) { + // tell mascot to look at page action + var element = event.target + var boundingRect = element.getBoundingClientRect() + var coordinates = getCaretCoordinates(element, element.selectionEnd) + this.animationEventEmitter.emit('point', { + x: boundingRect.left + coordinates.left - element.scrollLeft, + y: boundingRect.top + coordinates.top - element.scrollTop, + }) +} diff --git a/ui/responsive/app/img/identicon-tardigrade.png b/ui/responsive/app/img/identicon-tardigrade.png new file mode 100644 index 000000000..1742a32b8 Binary files /dev/null and b/ui/responsive/app/img/identicon-tardigrade.png differ diff --git a/ui/responsive/app/img/identicon-walrus.png b/ui/responsive/app/img/identicon-walrus.png new file mode 100644 index 000000000..d58fae912 Binary files /dev/null and b/ui/responsive/app/img/identicon-walrus.png differ diff --git a/ui/responsive/app/info.js b/ui/responsive/app/info.js new file mode 100644 index 000000000..e8470de97 --- /dev/null +++ b/ui/responsive/app/info.js @@ -0,0 +1,154 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') + +module.exports = connect(mapStateToProps)(InfoScreen) + +function mapStateToProps (state) { + return {} +} + +inherits(InfoScreen, Component) +function InfoScreen () { + Component.call(this) +} + +InfoScreen.prototype.render = function () { + const state = this.props + const version = global.platform.getVersion() + + return ( + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + state.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Info'), + ]), + + // main view + h('.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-space-around', { + style: { + padding: '20px', + }, + }, [ + // current version number + + h('.info.info-gray', [ + h('div', 'Metamask'), + h('div', { + style: { + marginBottom: '10px', + }, + }, `Version: ${version}`), + ]), + + h('div', { + style: { + marginBottom: '5px', + }}, + [ + h('div', [ + h('a', { + href: 'https://metamask.io/privacy.html', + target: '_blank', + onClick (event) { this.navigateTo(event.target.href) }, + }, [ + h('div.info', 'Privacy Policy'), + ]), + ]), + h('div', [ + h('a', { + href: 'https://metamask.io/terms.html', + target: '_blank', + onClick (event) { this.navigateTo(event.target.href) }, + }, [ + h('div.info', 'Terms of Use'), + ]), + ]), + h('div', [ + h('a', { + href: 'https://metamask.io/attributions.html', + target: '_blank', + onClick (event) { this.navigateTo(event.target.href) }, + }, [ + h('div.info', 'Attributions'), + ]), + ]), + ] + ), + + h('hr', { + style: { + margin: '10px 0 ', + width: '7em', + }, + }), + + h('div', { + style: { + paddingLeft: '30px', + }}, + [ + h('div.fa.fa-github', [ + h('a.info', { + href: 'https://github.com/MetaMask/faq', + target: '_blank', + }, 'Need Help? Read our FAQ!'), + ]), + h('div', [ + h('a', { + href: 'https://metamask.io/', + target: '_blank', + }, [ + h('img.icon-size', { + src: 'images/icon-128.png', + style: { + // IE6-9 + filter: 'grayscale(100%)', + // Microsoft Edge and Firefox 35+ + WebkitFilter: 'grayscale(100%)', + }, + }), + h('div.info', 'Visit our web site'), + ]), + ]), + h('div.fa.fa-slack', [ + h('a.info', { + href: 'http://slack.metamask.io', + target: '_blank', + }, 'Join the conversation on Slack'), + ]), + + h('div.fa.fa-twitter', [ + h('a.info', { + href: 'https://twitter.com/metamask_io', + target: '_blank', + }, 'Follow us on Twitter'), + ]), + + h('div.fa.fa-envelope', [ + h('a.info', { + target: '_blank', + style: { width: '85vw' }, + href: 'mailto:help@metamask.io?subject=Feedback', + }, 'Email us!'), + ]), + ]), + ]), + ]), + ]) + ) +} + +InfoScreen.prototype.navigateTo = function (url) { + global.platform.openWindow({ url }) +} + diff --git a/ui/responsive/app/keychains/hd/create-vault-complete.js b/ui/responsive/app/keychains/hd/create-vault-complete.js new file mode 100644 index 000000000..a318a9b50 --- /dev/null +++ b/ui/responsive/app/keychains/hd/create-vault-complete.js @@ -0,0 +1,78 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('../../actions') + +module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen) + +inherits(CreateVaultCompleteScreen, Component) +function CreateVaultCompleteScreen () { + Component.call(this) +} + +function mapStateToProps (state) { + return { + seed: state.appState.currentView.seedWords, + cachedSeed: state.metamask.seedWords, + } +} + +CreateVaultCompleteScreen.prototype.render = function () { + var state = this.props + var seed = state.seed || state.cachedSeed || '' + + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + // // subtitle and nav + // h('.section-title.flex-row.flex-center', [ + // h('h2.page-subtitle', 'Vault Created'), + // ]), + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginTop: 36, + marginBottom: 8, + width: '100%', + fontSize: '20px', + padding: 6, + }, + }, [ + 'Vault Created', + ]), + + h('div', { + style: { + width: '360px', + height: '78px', + fontSize: '1em', + marginTop: '10px', + textAlign: 'center', + }, + }, [ + h('span.error', 'These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret.'), + ]), + + h('textarea.twelve-word-phrase', { + readOnly: true, + value: seed, + }), + + h('button.primary', { + onClick: () => this.confirmSeedWords(), + style: { + margin: '24px', + fontSize: '0.9em', + }, + }, 'I\'ve copied it somewhere safe'), + ]) + ) +} + +CreateVaultCompleteScreen.prototype.confirmSeedWords = function () { + this.props.dispatch(actions.confirmSeedWords()) +} diff --git a/ui/responsive/app/keychains/hd/recover-seed/confirmation.js b/ui/responsive/app/keychains/hd/recover-seed/confirmation.js new file mode 100644 index 000000000..4ccbec9fc --- /dev/null +++ b/ui/responsive/app/keychains/hd/recover-seed/confirmation.js @@ -0,0 +1,118 @@ +const inherits = require('util').inherits + +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('../../../actions') + +module.exports = connect(mapStateToProps)(RevealSeedConfirmation) + +inherits(RevealSeedConfirmation, Component) +function RevealSeedConfirmation () { + Component.call(this) +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +RevealSeedConfirmation.prototype.render = function () { + const props = this.props + + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginBottom: 24, + width: '100%', + fontSize: '20px', + padding: 6, + }, + }, [ + 'Reveal Seed Words', + ]), + + h('.div', { + style: { + display: 'flex', + flexDirection: 'column', + padding: '20px', + justifyContent: 'center', + }, + }, [ + + h('h4', 'Do not recover your seed words in a public place! These words can be used to steal all your accounts.'), + + // confirmation + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: 'Enter your password to confirm', + onKeyPress: this.checkConfirmation.bind(this), + style: { + width: 260, + marginTop: '12px', + }, + }), + + h('.flex-row.flex-space-between', { + style: { + marginTop: 30, + width: '50%', + }, + }, [ + // cancel + h('button.primary', { + onClick: this.goHome.bind(this), + }, 'CANCEL'), + + // submit + h('button.primary', { + onClick: this.revealSeedWords.bind(this), + }, 'OK'), + + ]), + + (props.warning) && ( + h('span.error', { + style: { + margin: '20px', + }, + }, props.warning.split('-')) + ), + + props.inProgress && ( + h('span.in-progress-notification', 'Generating Seed...') + ), + ]), + ]) + ) +} + +RevealSeedConfirmation.prototype.componentDidMount = function () { + document.getElementById('password-box').focus() +} + +RevealSeedConfirmation.prototype.goHome = function () { + this.props.dispatch(actions.showConfigPage(false)) +} + +// create vault + +RevealSeedConfirmation.prototype.checkConfirmation = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.revealSeedWords() + } +} + +RevealSeedConfirmation.prototype.revealSeedWords = function () { + var password = document.getElementById('password-box').value + this.props.dispatch(actions.requestRevealSeed(password)) +} diff --git a/ui/responsive/app/keychains/hd/restore-vault.js b/ui/responsive/app/keychains/hd/restore-vault.js new file mode 100644 index 000000000..06e51d9b3 --- /dev/null +++ b/ui/responsive/app/keychains/hd/restore-vault.js @@ -0,0 +1,152 @@ +const inherits = require('util').inherits +const PersistentForm = require('../../../lib/persistent-form') +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('../../actions') + +module.exports = connect(mapStateToProps)(RestoreVaultScreen) + +inherits(RestoreVaultScreen, PersistentForm) +function RestoreVaultScreen () { + PersistentForm.call(this) +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + forgottenPassword: state.appState.forgottenPassword, + } +} + +RestoreVaultScreen.prototype.render = function () { + var state = this.props + this.persistentFormParentId = 'restore-vault-form' + + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginBottom: 24, + width: '100%', + fontSize: '20px', + padding: 6, + }, + }, [ + 'Restore Vault', + ]), + + // wallet seed entry + h('h3', 'Wallet Seed'), + h('textarea.twelve-word-phrase.letter-spacey', { + dataset: { + persistentFormId: 'wallet-seed', + }, + placeholder: 'Enter your secret twelve word phrase here to restore your vault.', + }), + + // password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: 'New Password (min 8 chars)', + dataset: { + persistentFormId: 'password', + }, + style: { + width: 260, + marginTop: 12, + }, + }), + + // confirm password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box-confirm', + placeholder: 'Confirm Password', + onKeyPress: this.createOnEnter.bind(this), + dataset: { + persistentFormId: 'password-confirmation', + }, + style: { + width: 260, + marginTop: 16, + }, + }), + + (state.warning) && ( + h('span.error.in-progress-notification', state.warning) + ), + + // submit + + h('.flex-row.flex-space-between', { + style: { + marginTop: 30, + width: '50%', + }, + }, [ + + // cancel + h('button.primary', { + onClick: this.showInitializeMenu.bind(this), + }, 'CANCEL'), + + // submit + h('button.primary', { + onClick: this.createNewVaultAndRestore.bind(this), + }, 'OK'), + + ]), + ]) + + ) +} + +RestoreVaultScreen.prototype.showInitializeMenu = function () { + if (this.props.forgottenPassword) { + this.props.dispatch(actions.backToUnlockView()) + } else { + this.props.dispatch(actions.showInitializeMenu()) + } +} + +RestoreVaultScreen.prototype.createOnEnter = function (event) { + if (event.key === 'Enter') { + this.createNewVaultAndRestore() + } +} + +RestoreVaultScreen.prototype.createNewVaultAndRestore = function () { + // check password + var passwordBox = document.getElementById('password-box') + var password = passwordBox.value + var passwordConfirmBox = document.getElementById('password-box-confirm') + var passwordConfirm = passwordConfirmBox.value + if (password.length < 8) { + this.warning = 'Password not long enough' + + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + if (password !== passwordConfirm) { + this.warning = 'Passwords don\'t match' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + // check seed + var seedBox = document.querySelector('textarea.twelve-word-phrase') + var seed = seedBox.value.trim() + if (seed.split(' ').length !== 12) { + this.warning = 'seed phrases are 12 words long' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + // submit + this.warning = null + this.props.dispatch(actions.displayWarning(this.warning)) + this.props.dispatch(actions.createNewVaultAndRestore(password, seed)) +} diff --git a/ui/responsive/app/new-keychain.js b/ui/responsive/app/new-keychain.js new file mode 100644 index 000000000..cc9633166 --- /dev/null +++ b/ui/responsive/app/new-keychain.js @@ -0,0 +1,29 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect + +module.exports = connect(mapStateToProps)(NewKeychain) + +function mapStateToProps (state) { + return {} +} + +inherits(NewKeychain, Component) +function NewKeychain () { + Component.call(this) +} + +NewKeychain.prototype.render = function () { + // const props = this.props + + return ( + h('div', { + style: { + background: 'blue', + }, + }, [ + h('h1', `Here's a list!!!!`), + ]) + ) +} diff --git a/ui/responsive/app/reducers.js b/ui/responsive/app/reducers.js new file mode 100644 index 000000000..11efca529 --- /dev/null +++ b/ui/responsive/app/reducers.js @@ -0,0 +1,52 @@ +const extend = require('xtend') + +// +// Sub-Reducers take in the complete state and return their sub-state +// +const reduceIdentities = require('./reducers/identities') +const reduceMetamask = require('./reducers/metamask') +const reduceApp = require('./reducers/app') + +window.METAMASK_CACHED_LOG_STATE = null + +module.exports = rootReducer + +function rootReducer (state, action) { + // clone + state = extend(state) + + if (action.type === 'GLOBAL_FORCE_UPDATE') { + return action.value + } + + // + // Identities + // + + state.identities = reduceIdentities(state, action) + + // + // MetaMask + // + + state.metamask = reduceMetamask(state, action) + + // + // AppState + // + + state.appState = reduceApp(state, action) + + window.METAMASK_CACHED_LOG_STATE = state + return state +} + +window.logState = function () { + var stateString = JSON.stringify(window.METAMASK_CACHED_LOG_STATE, removeSeedWords, 2) + console.log(stateString) + return stateString +} + +function removeSeedWords (key, value) { + return key === 'seedWords' ? undefined : value +} diff --git a/ui/responsive/app/reducers/app.js b/ui/responsive/app/reducers/app.js new file mode 100644 index 000000000..2fcc9bfe0 --- /dev/null +++ b/ui/responsive/app/reducers/app.js @@ -0,0 +1,585 @@ +const extend = require('xtend') +const actions = require('../actions') +const txHelper = require('../../lib/tx-helper') + +module.exports = reduceApp + + +function reduceApp (state, action) { + log.debug('App Reducer got ' + action.type) + // clone and defaults + const selectedAddress = state.metamask.selectedAddress + const hasUnconfActions = checkUnconfActions(state) + let name = 'accounts' + if (selectedAddress) { + name = 'accountDetail' + } + if (hasUnconfActions) { + log.debug('pending txs detected, defaulting to conf-tx view.') + name = 'confTx' + } + + var defaultView = { + name, + detailView: null, + context: selectedAddress, + } + + // confirm seed words + var seedWords = state.metamask.seedWords + var seedConfView = { + name: 'createVaultComplete', + seedWords, + } + + // default state + var appState = extend({ + shouldClose: false, + menuOpen: false, + currentView: seedWords ? seedConfView : defaultView, + accountDetail: { + subview: 'transactions', + }, + transForward: true, // Used to render transition direction + isLoading: false, // Used to display loading indicator + warning: null, // Used to display error text + }, state.appState) + + switch (action.type) { + + // transition methods + + case actions.TRANSITION_FORWARD: + return extend(appState, { + transForward: true, + }) + + case actions.TRANSITION_BACKWARD: + return extend(appState, { + transForward: false, + }) + + // intialize + + case actions.SHOW_CREATE_VAULT: + return extend(appState, { + currentView: { + name: 'createVault', + }, + transForward: true, + warning: null, + }) + + case actions.SHOW_RESTORE_VAULT: + return extend(appState, { + currentView: { + name: 'restoreVault', + }, + transForward: true, + forgottenPassword: true, + }) + + case actions.FORGOT_PASSWORD: + return extend(appState, { + currentView: { + name: 'restoreVault', + }, + transForward: false, + forgottenPassword: true, + }) + + case actions.SHOW_INIT_MENU: + return extend(appState, { + currentView: defaultView, + transForward: false, + }) + + case actions.SHOW_CONFIG_PAGE: + return extend(appState, { + currentView: { + name: 'config', + context: appState.currentView.context, + }, + transForward: action.value, + }) + + case actions.SHOW_ADD_TOKEN_PAGE: + return extend(appState, { + currentView: { + name: 'add-token', + context: appState.currentView.context, + }, + transForward: action.value, + }) + + case actions.SHOW_IMPORT_PAGE: + + return extend(appState, { + currentView: { + name: 'import-menu', + }, + transForward: true, + }) + + case actions.SHOW_INFO_PAGE: + return extend(appState, { + currentView: { + name: 'info', + context: appState.currentView.context, + }, + transForward: true, + }) + + case actions.CREATE_NEW_VAULT_IN_PROGRESS: + return extend(appState, { + currentView: { + name: 'createVault', + inProgress: true, + }, + transForward: true, + isLoading: true, + }) + + case actions.SHOW_NEW_VAULT_SEED: + return extend(appState, { + currentView: { + name: 'createVaultComplete', + seedWords: action.value, + }, + transForward: true, + isLoading: false, + }) + + case actions.NEW_ACCOUNT_SCREEN: + return extend(appState, { + currentView: { + name: 'new-account', + context: appState.currentView.context, + }, + transForward: true, + }) + + case actions.SHOW_SEND_PAGE: + return extend(appState, { + currentView: { + name: 'sendTransaction', + context: appState.currentView.context, + }, + transForward: true, + warning: null, + }) + + case actions.SHOW_NEW_KEYCHAIN: + return extend(appState, { + currentView: { + name: 'newKeychain', + context: appState.currentView.context, + }, + transForward: true, + }) + + // unlock + + case actions.UNLOCK_METAMASK: + return extend(appState, { + forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null, + detailView: {}, + transForward: true, + isLoading: false, + warning: null, + }) + + case actions.LOCK_METAMASK: + return extend(appState, { + currentView: defaultView, + transForward: false, + warning: null, + }) + + case actions.BACK_TO_INIT_MENU: + return extend(appState, { + warning: null, + transForward: false, + forgottenPassword: true, + currentView: { + name: 'InitMenu', + }, + }) + + case actions.BACK_TO_UNLOCK_VIEW: + return extend(appState, { + warning: null, + transForward: true, + forgottenPassword: false, + currentView: { + name: 'UnlockScreen', + }, + }) + // reveal seed words + + case actions.REVEAL_SEED_CONFIRMATION: + return extend(appState, { + currentView: { + name: 'reveal-seed-conf', + }, + transForward: true, + warning: null, + }) + + // accounts + + case actions.SET_SELECTED_ACCOUNT: + return extend(appState, { + activeAddress: action.value, + }) + + case actions.GO_HOME: + return extend(appState, { + currentView: extend(appState.currentView, { + name: 'accountDetail', + }), + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + transForward: false, + warning: null, + }) + + case actions.SHOW_ACCOUNT_DETAIL: + return extend(appState, { + forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null, + currentView: { + name: 'accountDetail', + context: action.value, + }, + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + transForward: false, + }) + + case actions.BACK_TO_ACCOUNT_DETAIL: + return extend(appState, { + currentView: { + name: 'accountDetail', + context: action.value, + }, + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + transForward: false, + }) + + case actions.SHOW_ACCOUNTS_PAGE: + return extend(appState, { + currentView: { + name: seedWords ? 'createVaultComplete' : 'accounts', + seedWords, + }, + transForward: true, + isLoading: false, + warning: null, + scrollToBottom: false, + forgottenPassword: false, + }) + + case actions.SHOW_NOTICE: + return extend(appState, { + transForward: true, + isLoading: false, + }) + + case actions.REVEAL_ACCOUNT: + return extend(appState, { + scrollToBottom: true, + }) + + case actions.SHOW_CONF_TX_PAGE: + return extend(appState, { + currentView: { + name: 'confTx', + context: 0, + }, + transForward: action.transForward, + warning: null, + isLoading: false, + }) + + case actions.SHOW_CONF_MSG_PAGE: + return extend(appState, { + currentView: { + name: hasUnconfActions ? 'confTx' : 'account-detail', + context: 0, + }, + transForward: true, + warning: null, + isLoading: false, + }) + + case actions.COMPLETED_TX: + log.debug('reducing COMPLETED_TX for tx ' + action.value) + const otherUnconfActions = getUnconfActionList(state) + .filter(tx => tx.id !== action.value) + const hasOtherUnconfActions = otherUnconfActions.length > 0 + + if (hasOtherUnconfActions) { + log.debug('reducer detected txs - rendering confTx view') + return extend(appState, { + transForward: false, + currentView: { + name: 'confTx', + context: 0, + }, + warning: null, + }) + } else { + log.debug('attempting to close popup') + return extend(appState, { + // indicate notification should close + shouldClose: true, + transForward: false, + warning: null, + currentView: { + name: 'accountDetail', + context: state.metamask.selectedAddress, + }, + accountDetail: { + subview: 'transactions', + }, + }) + } + + case actions.NEXT_TX: + return extend(appState, { + transForward: true, + currentView: { + name: 'confTx', + context: ++appState.currentView.context, + warning: null, + }, + }) + + case actions.VIEW_PENDING_TX: + const context = indexForPending(state, action.value) + return extend(appState, { + transForward: true, + currentView: { + name: 'confTx', + context, + warning: null, + }, + }) + + case actions.PREVIOUS_TX: + return extend(appState, { + transForward: false, + currentView: { + name: 'confTx', + context: --appState.currentView.context, + warning: null, + }, + }) + + case actions.TRANSACTION_ERROR: + return extend(appState, { + currentView: { + name: 'confTx', + errorMessage: 'There was a problem submitting this transaction.', + }, + }) + + case actions.UNLOCK_FAILED: + return extend(appState, { + warning: action.value || 'Incorrect password. Try again.', + }) + + case actions.SHOW_LOADING: + return extend(appState, { + isLoading: true, + loadingMessage: action.value, + }) + + case actions.HIDE_LOADING: + return extend(appState, { + isLoading: false, + }) + + case actions.SHOW_SUB_LOADING_INDICATION: + return extend(appState, { + isSubLoading: true, + }) + + case actions.HIDE_SUB_LOADING_INDICATION: + return extend(appState, { + isSubLoading: false, + }) + case actions.CLEAR_SEED_WORD_CACHE: + return extend(appState, { + transForward: true, + currentView: {}, + isLoading: false, + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + }) + + case actions.DISPLAY_WARNING: + return extend(appState, { + warning: action.value, + isLoading: false, + }) + + case actions.HIDE_WARNING: + return extend(appState, { + warning: undefined, + }) + + case actions.REQUEST_ACCOUNT_EXPORT: + return extend(appState, { + transForward: true, + currentView: { + name: 'accountDetail', + context: appState.currentView.context, + }, + accountDetail: { + subview: 'export', + accountExport: 'requested', + }, + }) + + case actions.EXPORT_ACCOUNT: + return extend(appState, { + accountDetail: { + subview: 'export', + accountExport: 'completed', + }, + }) + + case actions.SHOW_PRIVATE_KEY: + return extend(appState, { + accountDetail: { + subview: 'export', + accountExport: 'completed', + privateKey: action.value, + }, + }) + + case actions.BUY_ETH_VIEW: + return extend(appState, { + transForward: true, + currentView: { + name: 'buyEth', + context: appState.currentView.name, + }, + identity: state.metamask.identities[action.value], + buyView: { + subview: 'Coinbase', + amount: '15.00', + buyAddress: action.value, + formView: { + coinbase: true, + shapeshift: false, + }, + }, + }) + + case actions.COINBASE_SUBVIEW: + return extend(appState, { + buyView: { + subview: 'Coinbase', + formView: { + coinbase: true, + shapeshift: false, + }, + buyAddress: appState.buyView.buyAddress, + amount: appState.buyView.amount, + }, + }) + + case actions.SHAPESHIFT_SUBVIEW: + return extend(appState, { + buyView: { + subview: 'ShapeShift', + formView: { + coinbase: false, + shapeshift: true, + marketinfo: action.value.marketinfo, + coinOptions: action.value.coinOptions, + }, + buyAddress: appState.buyView.buyAddress, + amount: appState.buyView.amount, + }, + }) + + case actions.PAIR_UPDATE: + return extend(appState, { + buyView: { + subview: 'ShapeShift', + formView: { + coinbase: false, + shapeshift: true, + marketinfo: action.value.marketinfo, + coinOptions: appState.buyView.formView.coinOptions, + }, + buyAddress: appState.buyView.buyAddress, + amount: appState.buyView.amount, + warning: null, + }, + }) + + case actions.SHOW_QR: + return extend(appState, { + qrRequested: true, + transForward: true, + + Qr: { + message: action.value.message, + data: action.value.data, + }, + }) + + case actions.SHOW_QR_VIEW: + return extend(appState, { + currentView: { + name: 'qr', + context: appState.currentView.context, + }, + transForward: true, + Qr: { + message: action.value.message, + data: action.value.data, + }, + }) + default: + return appState + } +} + +function checkUnconfActions (state) { + const unconfActionList = getUnconfActionList(state) + const hasUnconfActions = unconfActionList.length > 0 + return hasUnconfActions +} + +function getUnconfActionList (state) { + const { unapprovedTxs, unapprovedMsgs, + unapprovedPersonalMsgs, network } = state.metamask + + const unconfActionList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) + return unconfActionList +} + +function indexForPending (state, txId) { + const unconfTxList = getUnconfActionList(state) + const match = unconfTxList.find((tx) => tx.id === txId) + const index = unconfTxList.indexOf(match) + return index +} diff --git a/ui/responsive/app/reducers/identities.js b/ui/responsive/app/reducers/identities.js new file mode 100644 index 000000000..341a404e7 --- /dev/null +++ b/ui/responsive/app/reducers/identities.js @@ -0,0 +1,15 @@ +const extend = require('xtend') + +module.exports = reduceIdentities + +function reduceIdentities (state, action) { + // clone + defaults + var idState = extend({ + + }, state.identities) + + switch (action.type) { + default: + return idState + } +} diff --git a/ui/responsive/app/reducers/metamask.js b/ui/responsive/app/reducers/metamask.js new file mode 100644 index 000000000..e0c416c2d --- /dev/null +++ b/ui/responsive/app/reducers/metamask.js @@ -0,0 +1,137 @@ +const extend = require('xtend') +const actions = require('../actions') + +module.exports = reduceMetamask + +function reduceMetamask (state, action) { + let newState + + // clone + defaults + var metamaskState = extend({ + isInitialized: false, + isUnlocked: false, + rpcTarget: 'https://rawtestrpc.metamask.io/', + identities: {}, + unapprovedTxs: {}, + noActiveNotices: true, + lastUnreadNotice: undefined, + frequentRpcList: [], + addressBook: [], + }, state.metamask) + + switch (action.type) { + + case actions.SHOW_ACCOUNTS_PAGE: + newState = extend(metamaskState) + delete newState.seedWords + return newState + + case actions.SHOW_NOTICE: + return extend(metamaskState, { + noActiveNotices: false, + lastUnreadNotice: action.value, + }) + + case actions.CLEAR_NOTICES: + return extend(metamaskState, { + noActiveNotices: true, + }) + + case actions.UPDATE_METAMASK_STATE: + return extend(metamaskState, action.value) + + case actions.UNLOCK_METAMASK: + return extend(metamaskState, { + isUnlocked: true, + isInitialized: true, + selectedAddress: action.value, + }) + + case actions.LOCK_METAMASK: + return extend(metamaskState, { + isUnlocked: false, + }) + + case actions.SET_RPC_LIST: + return extend(metamaskState, { + frequentRpcList: action.value, + }) + + case actions.SET_RPC_TARGET: + return extend(metamaskState, { + provider: { + type: 'rpc', + rpcTarget: action.value, + }, + }) + + case actions.SET_PROVIDER_TYPE: + return extend(metamaskState, { + provider: { + type: action.value, + }, + }) + + case actions.COMPLETED_TX: + var stringId = String(action.id) + newState = extend(metamaskState, { + unapprovedTxs: {}, + unapprovedMsgs: {}, + }) + for (const id in metamaskState.unapprovedTxs) { + if (id !== stringId) { + newState.unapprovedTxs[id] = metamaskState.unapprovedTxs[id] + } + } + for (const id in metamaskState.unapprovedMsgs) { + if (id !== stringId) { + newState.unapprovedMsgs[id] = metamaskState.unapprovedMsgs[id] + } + } + return newState + + case actions.SHOW_NEW_VAULT_SEED: + return extend(metamaskState, { + isUnlocked: true, + isInitialized: false, + seedWords: action.value, + }) + + case actions.CLEAR_SEED_WORD_CACHE: + newState = extend(metamaskState, { + isUnlocked: true, + isInitialized: true, + selectedAddress: action.value, + }) + delete newState.seedWords + return newState + + case actions.SHOW_ACCOUNT_DETAIL: + newState = extend(metamaskState, { + isUnlocked: true, + isInitialized: true, + selectedAddress: action.value, + }) + delete newState.seedWords + return newState + + case actions.SAVE_ACCOUNT_LABEL: + const account = action.value.account + const name = action.value.label + var id = {} + id[account] = extend(metamaskState.identities[account], { name }) + var identities = extend(metamaskState.identities, id) + return extend(metamaskState, { identities }) + + case actions.SET_CURRENT_FIAT: + return extend(metamaskState, { + currentCurrency: action.value.currentCurrency, + conversionRate: action.value.conversionRate, + conversionDate: action.value.conversionDate, + }) + + default: + return metamaskState + + } +} diff --git a/ui/responsive/app/root.js b/ui/responsive/app/root.js new file mode 100644 index 000000000..9e7314b20 --- /dev/null +++ b/ui/responsive/app/root.js @@ -0,0 +1,22 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const Provider = require('react-redux').Provider +const h = require('react-hyperscript') +const App = require('./app') + +module.exports = Root + +inherits(Root, Component) +function Root () { Component.call(this) } + +Root.prototype.render = function () { + return ( + + h(Provider, { + store: this.props.store, + }, [ + h(App), + ]) + + ) +} diff --git a/ui/responsive/app/send.js b/ui/responsive/app/send.js new file mode 100644 index 000000000..a21a219eb --- /dev/null +++ b/ui/responsive/app/send.js @@ -0,0 +1,288 @@ +const inherits = require('util').inherits +const PersistentForm = require('../lib/persistent-form') +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const Identicon = require('./components/identicon') +const actions = require('./actions') +const util = require('./util') +const numericBalance = require('./util').numericBalance +const addressSummary = require('./util').addressSummary +const isHex = require('./util').isHex +const EthBalance = require('./components/eth-balance') +const EnsInput = require('./components/ens-input') +const ethUtil = require('ethereumjs-util') +module.exports = connect(mapStateToProps)(SendTransactionScreen) + +function mapStateToProps (state) { + var result = { + address: state.metamask.selectedAddress, + accounts: state.metamask.accounts, + identities: state.metamask.identities, + warning: state.appState.warning, + network: state.metamask.network, + addressBook: state.metamask.addressBook, + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + } + + result.error = result.warning && result.warning.split('.')[0] + + result.account = result.accounts[result.address] + result.identity = result.identities[result.address] + result.balance = result.account ? numericBalance(result.account.balance) : null + + return result +} + +inherits(SendTransactionScreen, PersistentForm) +function SendTransactionScreen () { + PersistentForm.call(this) +} + +SendTransactionScreen.prototype.render = function () { + this.persistentFormParentId = 'send-tx-form' + + const props = this.props + const { + address, + account, + identity, + network, + identities, + addressBook, + conversionRate, + currentCurrency, + } = props + + return ( + + h('.send-screen.flex-column.flex-grow', [ + + // + // Sender Profile + // + + h('.account-data-subsection.flex-row.flex-grow', { + style: { + margin: '0 20px', + }, + }, [ + + // header - identicon + nav + h('.flex-row.flex-space-between', { + style: { + marginTop: '15px', + }, + }, [ + // back button + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { + onClick: this.back.bind(this), + }), + + // large identicon + h('.identicon-wrapper.flex-column.flex-center.select-none', [ + h(Identicon, { + diameter: 62, + address: address, + }), + ]), + + // invisible place holder + h('i.fa.fa-users.fa-lg.invisible', { + style: { + marginTop: '28px', + }, + }), + + ]), + + // account label + + h('.flex-column', { + style: { + marginTop: '10px', + alignItems: 'flex-start', + }, + }, [ + h('h2.font-medium.color-forest.flex-center', { + style: { + paddingTop: '8px', + marginBottom: '8px', + }, + }, identity && identity.name), + + // address and getter actions + h('.flex-row.flex-center', { + style: { + marginBottom: '8px', + }, + }, [ + + h('div', { + style: { + lineHeight: '16px', + }, + }, addressSummary(address)), + + ]), + + // balance + h('.flex-row.flex-center', [ + + h(EthBalance, { + value: account && account.balance, + conversionRate, + currentCurrency, + }), + + ]), + ]), + ]), + + // + // Required Fields + // + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginTop: '15px', + marginBottom: '16px', + }, + }, [ + 'Send Transaction', + ]), + + // error message + props.error && h('span.error.flex-center', props.error), + + // 'to' field + h('section.flex-row.flex-center', [ + h(EnsInput, { + name: 'address', + placeholder: 'Recipient Address', + onChange: this.recipientDidChange.bind(this), + network, + identities, + addressBook, + }), + ]), + + // 'amount' and send button + h('section.flex-row.flex-center', [ + + h('input.large-input', { + name: 'amount', + placeholder: 'Amount', + type: 'number', + style: { + marginRight: '6px', + }, + dataset: { + persistentFormId: 'tx-amount', + }, + }), + + h('button.primary', { + onClick: this.onSubmit.bind(this), + style: { + textTransform: 'uppercase', + }, + }, 'Next'), + + ]), + + // + // Optional Fields + // + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginTop: '16px', + marginBottom: '16px', + }, + }, [ + 'Transaction Data (optional)', + ]), + + // 'data' field + h('section.flex-column.flex-center', [ + h('input.large-input', { + name: 'txData', + placeholder: '0x01234', + style: { + width: '100%', + resize: 'none', + }, + dataset: { + persistentFormId: 'tx-data', + }, + }), + ]), + ]) + ) +} + +SendTransactionScreen.prototype.navigateToAccounts = function (event) { + event.stopPropagation() + this.props.dispatch(actions.showAccountsPage()) +} + +SendTransactionScreen.prototype.back = function () { + var address = this.props.address + this.props.dispatch(actions.backToAccountDetail(address)) +} + +SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickname) { + this.setState({ + recipient: recipient, + nickname: nickname, + }) +} + +SendTransactionScreen.prototype.onSubmit = function () { + const state = this.state || {} + const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '') + const nickname = state.nickname || ' ' + const input = document.querySelector('input[name="amount"]').value + const value = util.normalizeEthStringToWei(input) + const txData = document.querySelector('input[name="txData"]').value + const balance = this.props.balance + let message + + if (value.gt(balance)) { + message = 'Insufficient funds.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if (input < 0) { + message = 'Can not send negative amounts of ETH.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) { + message = 'Recipient address is invalid.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if (!isHex(ethUtil.stripHexPrefix(txData)) && txData) { + message = 'Transaction data must be hex string.' + return this.props.dispatch(actions.displayWarning(message)) + } + + this.props.dispatch(actions.hideWarning()) + + this.props.dispatch(actions.addToAddressBook(recipient, nickname)) + + var txParams = { + from: this.props.address, + value: '0x' + value.toString(16), + } + + if (recipient) txParams.to = ethUtil.addHexPrefix(recipient) + if (txData) txParams.data = txData + + this.props.dispatch(actions.signTx(txParams)) +} diff --git a/ui/responsive/app/settings.js b/ui/responsive/app/settings.js new file mode 100644 index 000000000..454cc95e0 --- /dev/null +++ b/ui/responsive/app/settings.js @@ -0,0 +1,59 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') + +module.exports = connect(mapStateToProps)(AppSettingsPage) + +function mapStateToProps (state) { + return {} +} + +inherits(AppSettingsPage, Component) +function AppSettingsPage () { + Component.call(this) +} + +AppSettingsPage.prototype.render = function () { + return ( + + h('.account-detail-section.flex-column.flex-grow', [ + + // subtitle and nav + h('.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: this.navigateToAccounts.bind(this), + }), + h('h2.page-subtitle', 'Settings'), + ]), + + h('label', { + htmlFor: 'settings-rpc-endpoint', + }, 'RPC Endpoint:'), + h('input', { + type: 'url', + id: 'settings-rpc-endpoint', + onKeyPress: this.onKeyPress.bind(this), + }), + + ]) + + ) +} + +AppSettingsPage.prototype.componentDidMount = function () { + document.querySelector('input').focus() +} + +AppSettingsPage.prototype.onKeyPress = function (event) { + // get submit event + if (event.key === 'Enter') { + // this.submitPassword(event) + } +} + +AppSettingsPage.prototype.navigateToAccounts = function (event) { + event.stopPropagation() + this.props.dispatch(actions.showAccountsPage()) +} diff --git a/ui/responsive/app/store.js b/ui/responsive/app/store.js new file mode 100644 index 000000000..ba9e58b49 --- /dev/null +++ b/ui/responsive/app/store.js @@ -0,0 +1,21 @@ +const createStore = require('redux').createStore +const applyMiddleware = require('redux').applyMiddleware +const thunkMiddleware = require('redux-thunk') +const rootReducer = require('./reducers') +const createLogger = require('redux-logger') + +global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' + +module.exports = configureStore + +const loggerMiddleware = createLogger({ + predicate: () => global.METAMASK_DEBUG, +}) + +const middlewares = [thunkMiddleware, loggerMiddleware] + +const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore) + +function configureStore (initialState) { + return createStoreWithMiddleware(rootReducer, initialState) +} diff --git a/ui/responsive/app/template.js b/ui/responsive/app/template.js new file mode 100644 index 000000000..d15b30fd2 --- /dev/null +++ b/ui/responsive/app/template.js @@ -0,0 +1,30 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect + +module.exports = connect(mapStateToProps)(COMPONENTNAME) + +function mapStateToProps (state) { + return {} +} + +inherits(COMPONENTNAME, Component) +function COMPONENTNAME () { + Component.call(this) +} + +COMPONENTNAME.prototype.render = function () { + const props = this.props + + return ( + h('div', { + style: { + background: 'blue', + }, + }, [ + `Hello, ${props.sender}`, + ]) + ) +} + diff --git a/ui/responsive/app/unlock.js b/ui/responsive/app/unlock.js new file mode 100644 index 000000000..1aee3c5d0 --- /dev/null +++ b/ui/responsive/app/unlock.js @@ -0,0 +1,118 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') +const getCaretCoordinates = require('textarea-caret') +const EventEmitter = require('events').EventEmitter + +const Mascot = require('./components/mascot') + +module.exports = connect(mapStateToProps)(UnlockScreen) + +inherits(UnlockScreen, Component) +function UnlockScreen () { + Component.call(this) + this.animationEventEmitter = new EventEmitter() +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +UnlockScreen.prototype.render = function () { + const state = this.props + const warning = state.warning + return ( + h('.flex-column', [ + h('.unlock-screen.flex-column.flex-center.flex-grow', [ + + h(Mascot, { + animationEventEmitter: this.animationEventEmitter, + }), + + h('h1', { + style: { + fontSize: '1.4em', + textTransform: 'uppercase', + color: '#7F8082', + }, + }, 'MetaMask'), + + h('input.large-input', { + type: 'password', + id: 'password-box', + placeholder: 'enter password', + style: { + + }, + onKeyPress: this.onKeyPress.bind(this), + onInput: this.inputChanged.bind(this), + }), + + h('.error', { + style: { + display: warning ? 'block' : 'none', + padding: '0 20px', + textAlign: 'center', + }, + }, warning), + + h('button.primary.cursor-pointer', { + onClick: this.onSubmit.bind(this), + style: { + margin: 10, + }, + }, 'Unlock'), + ]), + + h('.flex-row.flex-center.flex-grow', [ + h('p.pointer', { + onClick: () => this.props.dispatch(actions.forgotPassword()), + style: { + fontSize: '0.8em', + color: 'rgb(247, 134, 28)', + textDecoration: 'underline', + }, + }, 'I forgot my password.'), + ]), + ]) + ) +} + +UnlockScreen.prototype.componentDidMount = function () { + document.getElementById('password-box').focus() +} + +UnlockScreen.prototype.onSubmit = function (event) { + const input = document.getElementById('password-box') + const password = input.value + this.props.dispatch(actions.tryUnlockMetamask(password)) +} + +UnlockScreen.prototype.onKeyPress = function (event) { + if (event.key === 'Enter') { + this.submitPassword(event) + } +} + +UnlockScreen.prototype.submitPassword = function (event) { + var element = event.target + var password = element.value + // reset input + element.value = '' + this.props.dispatch(actions.tryUnlockMetamask(password)) +} + +UnlockScreen.prototype.inputChanged = function (event) { + // tell mascot to look at page action + var element = event.target + var boundingRect = element.getBoundingClientRect() + var coordinates = getCaretCoordinates(element, element.selectionEnd) + this.animationEventEmitter.emit('point', { + x: boundingRect.left + coordinates.left - element.scrollLeft, + y: boundingRect.top + coordinates.top - element.scrollTop, + }) +} diff --git a/ui/responsive/app/util.js b/ui/responsive/app/util.js new file mode 100644 index 000000000..ac3f42c6b --- /dev/null +++ b/ui/responsive/app/util.js @@ -0,0 +1,217 @@ +const ethUtil = require('ethereumjs-util') + +var valueTable = { + wei: '1000000000000000000', + kwei: '1000000000000000', + mwei: '1000000000000', + gwei: '1000000000', + szabo: '1000000', + finney: '1000', + ether: '1', + kether: '0.001', + mether: '0.000001', + gether: '0.000000001', + tether: '0.000000000001', +} +var bnTable = {} +for (var currency in valueTable) { + bnTable[currency] = new ethUtil.BN(valueTable[currency], 10) +} + +module.exports = { + valuesFor: valuesFor, + addressSummary: addressSummary, + miniAddressSummary: miniAddressSummary, + isAllOneCase: isAllOneCase, + isValidAddress: isValidAddress, + numericBalance: numericBalance, + parseBalance: parseBalance, + formatBalance: formatBalance, + generateBalanceObject: generateBalanceObject, + dataSize: dataSize, + readableDate: readableDate, + normalizeToWei: normalizeToWei, + normalizeEthStringToWei: normalizeEthStringToWei, + normalizeNumberToWei: normalizeNumberToWei, + valueTable: valueTable, + bnTable: bnTable, + isHex: isHex, +} + +function valuesFor (obj) { + if (!obj) return [] + return Object.keys(obj) + .map(function (key) { return obj[key] }) +} + +function addressSummary (address, firstSegLength = 10, lastSegLength = 4, includeHex = true) { + if (!address) return '' + let checked = ethUtil.toChecksumAddress(address) + if (!includeHex) { + checked = ethUtil.stripHexPrefix(checked) + } + return checked ? checked.slice(0, firstSegLength) + '...' + checked.slice(checked.length - lastSegLength) : '...' +} + +function miniAddressSummary (address) { + if (!address) return '' + var checked = ethUtil.toChecksumAddress(address) + return checked ? checked.slice(0, 4) + '...' + checked.slice(-4) : '...' +} + +function isValidAddress (address) { + var prefixed = ethUtil.addHexPrefix(address) + if (address === '0x0000000000000000000000000000000000000000') return false + return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed) +} + +function isAllOneCase (address) { + if (!address) return true + var lower = address.toLowerCase() + var upper = address.toUpperCase() + return address === lower || address === upper +} + +// Takes wei Hex, returns wei BN, even if input is null +function numericBalance (balance) { + if (!balance) return new ethUtil.BN(0, 16) + var stripped = ethUtil.stripHexPrefix(balance) + return new ethUtil.BN(stripped, 16) +} + +// Takes hex, returns [beforeDecimal, afterDecimal] +function parseBalance (balance) { + var beforeDecimal, afterDecimal + const wei = numericBalance(balance) + var weiString = wei.toString() + const trailingZeros = /0+$/ + + beforeDecimal = weiString.length > 18 ? weiString.slice(0, weiString.length - 18) : '0' + afterDecimal = ('000000000000000000' + wei).slice(-18).replace(trailingZeros, '') + if (afterDecimal === '') { afterDecimal = '0' } + return [beforeDecimal, afterDecimal] +} + +// Takes wei hex, returns an object with three properties. +// Its "formatted" property is what we generally use to render values. +function formatBalance (balance, decimalsToKeep, needsParse = true) { + var parsed = needsParse ? parseBalance(balance) : balance.split('.') + var beforeDecimal = parsed[0] + var afterDecimal = parsed[1] + var formatted = 'None' + if (decimalsToKeep === undefined) { + if (beforeDecimal === '0') { + if (afterDecimal !== '0') { + var sigFigs = afterDecimal.match(/^0*(.{2})/) // default: grabs 2 most significant digits + if (sigFigs) { afterDecimal = sigFigs[0] } + formatted = '0.' + afterDecimal + ' ETH' + } + } else { + formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ' ETH' + } + } else { + afterDecimal += Array(decimalsToKeep).join('0') + formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ' ETH' + } + return formatted +} + + +function generateBalanceObject (formattedBalance, decimalsToKeep = 1) { + var balance = formattedBalance.split(' ')[0] + var label = formattedBalance.split(' ')[1] + var beforeDecimal = balance.split('.')[0] + var afterDecimal = balance.split('.')[1] + var shortBalance = shortenBalance(balance, decimalsToKeep) + + if (beforeDecimal === '0' && afterDecimal.substr(0, 5) === '00000') { + // eslint-disable-next-line eqeqeq + if (afterDecimal == 0) { + balance = '0' + } else { + balance = '<1.0e-5' + } + } else if (beforeDecimal !== '0') { + balance = `${beforeDecimal}.${afterDecimal.slice(0, decimalsToKeep)}` + } + + return { balance, label, shortBalance } +} + +function shortenBalance (balance, decimalsToKeep = 1) { + var truncatedValue + var convertedBalance = parseFloat(balance) + if (convertedBalance > 1000000) { + truncatedValue = (balance / 1000000).toFixed(decimalsToKeep) + return `${truncatedValue}m` + } else if (convertedBalance > 1000) { + truncatedValue = (balance / 1000).toFixed(decimalsToKeep) + return `${truncatedValue}k` + } else if (convertedBalance === 0) { + return '0' + } else if (convertedBalance < 0.001) { + return '<0.001' + } else if (convertedBalance < 1) { + var stringBalance = convertedBalance.toString() + if (stringBalance.split('.')[1].length > 3) { + return convertedBalance.toFixed(3) + } else { + return stringBalance + } + } else { + return convertedBalance.toFixed(decimalsToKeep) + } +} + +function dataSize (data) { + var size = data ? ethUtil.stripHexPrefix(data).length : 0 + return size + ' bytes' +} + +// Takes a BN and an ethereum currency name, +// returns a BN in wei +function normalizeToWei (amount, currency) { + try { + return amount.mul(bnTable.wei).div(bnTable[currency]) + } catch (e) {} + return amount +} + +function normalizeEthStringToWei (str) { + const parts = str.split('.') + let eth = new ethUtil.BN(parts[0], 10).mul(bnTable.wei) + if (parts[1]) { + var decimal = parts[1] + while (decimal.length < 18) { + decimal += '0' + } + const decimalBN = new ethUtil.BN(decimal, 10) + eth = eth.add(decimalBN) + } + return eth +} + +var multiple = new ethUtil.BN('10000', 10) +function normalizeNumberToWei (n, currency) { + var enlarged = n * 10000 + var amount = new ethUtil.BN(String(enlarged), 10) + return normalizeToWei(amount, currency).div(multiple) +} + +function readableDate (ms) { + var date = new Date(ms) + var month = date.getMonth() + var day = date.getDate() + var year = date.getFullYear() + var hours = date.getHours() + var minutes = '0' + date.getMinutes() + var seconds = '0' + date.getSeconds() + + var dateStr = `${month}/${day}/${year}` + var time = `${hours}:${minutes.substr(-2)}:${seconds.substr(-2)}` + return `${dateStr} ${time}` +} + +function isHex (str) { + return Boolean(str.match(/^(0x)?[0-9a-fA-F]+$/)) +} diff --git a/ui/responsive/css.js b/ui/responsive/css.js new file mode 100644 index 000000000..7c394a87b --- /dev/null +++ b/ui/responsive/css.js @@ -0,0 +1,29 @@ +const fs = require('fs') +const path = require('path') + +module.exports = bundleCss + +var cssFiles = { + 'fonts.css': fs.readFileSync(path.join(__dirname, '/app/css/fonts.css'), 'utf8'), + 'reset.css': fs.readFileSync(path.join(__dirname, '/app/css/reset.css'), 'utf8'), + 'lib.css': fs.readFileSync(path.join(__dirname, '/app/css/lib.css'), 'utf8'), + 'index.css': fs.readFileSync(path.join(__dirname, '/app/css/index.css'), 'utf8'), + 'transitions.css': fs.readFileSync(path.join(__dirname, '/app/css/transitions.css'), 'utf8'), + 'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'), + 'react-css': fs.readFileSync(path.join(__dirname, '..', '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'), +} + +function bundleCss () { + var cssBundle = Object.keys(cssFiles).reduce(function (bundle, fileName) { + var fileContent = cssFiles[fileName] + var output = String() + + output += '/*========== ' + fileName + ' ==========*/\n\n' + output += fileContent + output += '\n\n' + + return bundle + output + }, String()) + + return cssBundle +} diff --git a/ui/responsive/design/00-metamask-SignIn.jpg b/ui/responsive/design/00-metamask-SignIn.jpg new file mode 100644 index 000000000..2becdb032 Binary files /dev/null and b/ui/responsive/design/00-metamask-SignIn.jpg differ diff --git a/ui/responsive/design/01-metamask-SelectAcc.jpg b/ui/responsive/design/01-metamask-SelectAcc.jpg new file mode 100644 index 000000000..239091a98 Binary files /dev/null and b/ui/responsive/design/01-metamask-SelectAcc.jpg differ diff --git a/ui/responsive/design/02-metamask-AccDetails.jpg b/ui/responsive/design/02-metamask-AccDetails.jpg new file mode 100644 index 000000000..d7d408ffc Binary files /dev/null and b/ui/responsive/design/02-metamask-AccDetails.jpg differ diff --git a/ui/responsive/design/02a-metamask-AccDetails-OverToken.jpg b/ui/responsive/design/02a-metamask-AccDetails-OverToken.jpg new file mode 100644 index 000000000..f26ff31e8 Binary files /dev/null and b/ui/responsive/design/02a-metamask-AccDetails-OverToken.jpg differ diff --git a/ui/responsive/design/02a-metamask-AccDetails-OverTransaction.jpg b/ui/responsive/design/02a-metamask-AccDetails-OverTransaction.jpg new file mode 100644 index 000000000..8a06be6b9 Binary files /dev/null and b/ui/responsive/design/02a-metamask-AccDetails-OverTransaction.jpg differ diff --git a/ui/responsive/design/02a-metamask-AccDetails.jpg b/ui/responsive/design/02a-metamask-AccDetails.jpg new file mode 100644 index 000000000..c37e0f539 Binary files /dev/null and b/ui/responsive/design/02a-metamask-AccDetails.jpg differ diff --git a/ui/responsive/design/02b-metamask-AccDetails-Send.jpg b/ui/responsive/design/02b-metamask-AccDetails-Send.jpg new file mode 100644 index 000000000..10f2d27fd Binary files /dev/null and b/ui/responsive/design/02b-metamask-AccDetails-Send.jpg differ diff --git a/ui/responsive/design/03-metamask-Qr.jpg b/ui/responsive/design/03-metamask-Qr.jpg new file mode 100644 index 000000000..9c09de42f Binary files /dev/null and b/ui/responsive/design/03-metamask-Qr.jpg differ diff --git a/ui/responsive/design/05-metamask-Menu.jpg b/ui/responsive/design/05-metamask-Menu.jpg new file mode 100644 index 000000000..0a43d7b2a Binary files /dev/null and b/ui/responsive/design/05-metamask-Menu.jpg differ diff --git a/ui/responsive/design/chromeStorePics/final_screen_dao_accounts.png b/ui/responsive/design/chromeStorePics/final_screen_dao_accounts.png new file mode 100644 index 000000000..805cc96b6 Binary files /dev/null and b/ui/responsive/design/chromeStorePics/final_screen_dao_accounts.png differ diff --git a/ui/responsive/design/chromeStorePics/final_screen_dao_locked.png b/ui/responsive/design/chromeStorePics/final_screen_dao_locked.png new file mode 100644 index 000000000..9d9e33930 Binary files /dev/null and b/ui/responsive/design/chromeStorePics/final_screen_dao_locked.png differ diff --git a/ui/responsive/design/chromeStorePics/final_screen_dao_notification.png b/ui/responsive/design/chromeStorePics/final_screen_dao_notification.png new file mode 100644 index 000000000..d56a5ce62 Binary files /dev/null and b/ui/responsive/design/chromeStorePics/final_screen_dao_notification.png differ diff --git a/ui/responsive/design/chromeStorePics/final_screen_wei_account.png b/ui/responsive/design/chromeStorePics/final_screen_wei_account.png new file mode 100644 index 000000000..d503ff301 Binary files /dev/null and b/ui/responsive/design/chromeStorePics/final_screen_wei_account.png differ diff --git a/ui/responsive/design/chromeStorePics/final_screen_wei_notification.png b/ui/responsive/design/chromeStorePics/final_screen_wei_notification.png new file mode 100644 index 000000000..3560c51ff Binary files /dev/null and b/ui/responsive/design/chromeStorePics/final_screen_wei_notification.png differ diff --git a/ui/responsive/design/chromeStorePics/icon-128.png b/ui/responsive/design/chromeStorePics/icon-128.png new file mode 100644 index 000000000..ae687147d Binary files /dev/null and b/ui/responsive/design/chromeStorePics/icon-128.png differ diff --git a/ui/responsive/design/chromeStorePics/icon-64.png b/ui/responsive/design/chromeStorePics/icon-64.png new file mode 100644 index 000000000..7062cf4f1 Binary files /dev/null and b/ui/responsive/design/chromeStorePics/icon-64.png differ diff --git a/ui/responsive/design/chromeStorePics/metamask_icon.ai b/ui/responsive/design/chromeStorePics/metamask_icon.ai new file mode 100644 index 000000000..27400c5a4 --- /dev/null +++ b/ui/responsive/design/chromeStorePics/metamask_icon.ai @@ -0,0 +1,2383 @@ +%PDF-1.5 % +1 0 obj <>/OCGs[5 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + metamask_icon + + + Adobe Illustrator CC 2015 (Macintosh) + 2016-06-15T14:23:12-04:00 + 2016-06-15T14:23:12-04:00 + 2016-06-15T14:23:12-04:00 + + + + 240 + 256 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAADwAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXnP5r/mvB5Tg/RmnAT6/cJyUMKx28bVAkf+ZjT4U+k7UDYuo1HBsObl6bTce5+l5X+Wf5t6jonm KZtfu5rzTNVcG+lkZpHilACLOAamgUBWC/sgUrxAzEwakxl6uRczUaYSj6eYfS9vcQXEEdxA6ywT KskUimqsjCqsCOoIObUG3UkUvxQ7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXln5rfnHb+X1l0bQnWfXCCs0+zR2tfEGoaTwXoO/hmJqNTw7Dm5mm0plvL6fvfO U889xPJcXEjTTzM0ksshLO7saszMdySTUk5qybdsBSzAl7R+Rv5ni0dPKutTqto5ppNw/KqyOwH1 c0BHFuVVJpTpvUUz9Jnr0n4Ov1mnv1D4ve82LrHYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FUn1Pzl5W0yF5r3VLeNI9no4kYfNU5N+GY89XijsZC/mfkHIhpMstxE18h8ynCmqg7iorQ7HMhx 3Yq7FXYq7FXYq7FXjf5v/nG2nPL5e8tXAN9Ro9Rv039A7D04WBp6nUM1Ph7fFXjg6nU16Y83P0ul v1S5PAWZmYsxJYmpJ3JJzWu0axV2KuxV9Ifkr+Zq67py6FrF1y121+G3eUjlcwKtQeRPxyIAeXci jbnkc2mlz8Qo83U6vT8J4h9L1PMxwnYq7FXYq7FXYq7FXYq7FXYq7FXYqlN95s8t2I/0jUYQQaFE b1HHzWPk34Zi5Nbhh9Uh9/3OVi0Waf0xP3fex7UfzX0WEOtjBNdSCoR2AjjPgak8/wDhcwcvbWIf SDL7B+v7HOxdi5T9REftP6vtYre/mf5ouD+5eK0UdoowxPzMnqfhmsydsZpcqj7h+u3aY+x8Eedy 95/VTFdc803n1dptVv5powSVjeRmqx7IhNMxPEy5jRJPx2cwY8WEWAB8N0l8gRXnm/z/AKXazpXT 7WX65NCo5II4PjHqVrXk3FDX+btXNtodLETDqddqiYH7H1LnQPOuxV2KuxV2KuZlVSzEBQKknYAD FXhX5t/nOsyzaB5XuCEDBbzVoXZSSrA8Ld0I2qKM/foNt81+o1X8Mfm7LTaT+KXyeI5r3YuxV2Ku xV2KqtpdXNpdQ3VrI0NzA6yQyoaMroaqwPiCMINboIsUX1j+XH5g6f5w0WOcNHDq0I439irbqwoD IiklvTaux7dK1GbnBmEx5ukz4DjPky3Lmh2KuxV2KuxV2KuxVKb/AM2eW7CoudQhDA0KI3qMD7rH yYZjZdbhh9Uh9/3OVi0Waf0xP3fex6//ADY0OHktnbzXTKaKxpFGw8QTyb/hcwMnbWIfSDL7B+Pg 5+PsXKa4iI/afx8WO3/5ra9OJEtYIbVG2RqNJIo/1iQv/C5gZe2sp+kCP2n8fBz8XYuIVxEy+wfj 4sVvdY1a+FLy8muFBLBZJGZQT4KTQZrMmfJP6pE/F2ePBjh9MQPghMqbXYqler+YLPTgUJ9W57Qq en+se2X4dPKfuaMueMPewW+vrm9uGnuH5Ox2G/FR/KoPQZtYQERQdZOZkbL3L/nG7y8Y7PVPMEqj lOy2VqxBDBEpJKQSPsszINu6nNpoYbGTqdfPcRe1ZnuvdirsVdirsVeF/n1+Y96l1N5O04+lCERt Vn35uXAkWBfBOJVmI+1XjsAeWv1ec3wD4uy0eAVxn4PEM17sXYq7FXYq7FXYq7FU18seZNU8uaxD qumymOeKqsBQh0YUZCGDDceI2O+ESlH6TRYmEZbSFh7bbfmL5mubeO4iv6xSqHQ+lD0YV/kzVy7U 1MTRl9kf1Owj2XpiLEftP61T/Hvmv/lt/wCSUP8AzRkf5W1P877I/qZfyTp/5v2n9bv8e+a/+W3/ AJJQ/wDNGP8AK2p/nfZH9S/yTp/5v2n9bv8AHvmv/lt/5JQ/80Y/ytqf532R/Uv8k6f+b9p/W7/H vmv/AJbf+SUP/NGP8ran+d9kf1L/ACTp/wCb9p/W7/Hvmv8A5bf+SUP/ADRj/K2p/nfZH9S/yTp/ 5v2n9bHtW1/WtUeuoXTz8eiGioCNtkUKv4ZDNqsmX6zbdh0uPF9ApL8ob3Yq7FXYq7FWO695pW0d rWyo9wtRJKd1Q+A8WH3ZmYNLxby5OJn1PDtHmw6SR5JGkclnclmY9STuTmzArZ1xN7uhhlmmSGJS 8sjBI0HUsxoAPpwhiS+zvLGhxaF5e0/SIiGFlAkTOoIDuB8b0JNOb1bN5jhwxAdBknxSJ70zybB2 KuxV2KpX5o12HQfL2oaxNxK2UDyKjHiHkpSOOu9ObkL9OQyT4Yks8cOKQHe+Nr6+u7+8mvLyVp7q 4cyTTOaszNuSc0ZJJsu/AAFBRwJdirsVdirsVdirsVdirKvI2tmC6OmzN+5uDWEmlFkp03/mp9/z zX67BY4hzDm6PNR4T1Z5mpdo7FXYq7FXYqg7laSn33y2PJgVLJIdirsVWu6IjO7BUUEsxNAAOpJO IFqTTD9c81yT1gsGaKIH4px8LtT+Xuo/HNlg0gG8ubrs2qJ2ixzM1w3Yqzz8k/L66z5/s2kAMGmK 2oSAkgkwkCKlO4ldDTwBzJ0sOKfucbVz4YHz2fU+bd0rsVdirsVdirxT/nIzzWi2ll5ZtZ1Mkj/W dRjQnkqoB6KPTajli9D/ACqcwNbk2EQ7DQ49zIvB81zs3Yq7FXYq7FXYq7FXYq7FW0d0dXRirqQV YGhBG4IIxItQXp3ljWjqunc5Cv1qI8J1Xb/Van+UPxrmh1WDw5bcnc6fNxx35pvmO5DsVdirsVQ1 2v2W+gnJwYlD5YxdiqheXttZwGe4cRxjap6k+AHc5KEDI0GM5iIssE1fzBeakeB/dWw6Qqevux7n Nth08Ye91eXPKfuSzL2h2KuxV9If84/eVk03ys+tyEm51lqhSKcIYHdEArv8Zq3uKZtdHjqN97qN bkuVdz1PMtw3Yq7FXYqp3V1b2ltNdXMgit4EaWaVjRVRByZifAAYCaSBez418169Nr/mPUdYl5Vv Z2kjVyCyRVpEhIp9iMKv0Zo8k+KRLv8AHDhiB3JVkGbsVdirsVdirsVdirsVdirsVTPy7q50vU45 2J9B/guFHdD36H7J3/DKNTh8SFdejdgy8Er6PUYpY5okljblHIoZGHQqwqDmhIINF3QNiwuwJdir sVUrlaxH23yUeaCg8tYJfq2t2Wmx/vW5TleUcC/abtv/ACj3OXYsEp8uTVlzRhz5sD1DULm+uGnu GLE/ZX9lR/KozbY8YgKDqsmQyNlDZNg7FXYqiNN0+51HUbXT7UBrm8mjggUmgLysEWp7bnDEWaRK VCy+0tM0+307TbXT7YEW9nDHbwgmp4RKEWp+QzfRFCnnpSs2UThQ7FXYq7FXmX59ebIdL8ovpEMw Goauyx+mrFXW2U8pH2/Zbj6dD15HwOYmryVGupczR4uKd9A+ac1Tt3Yq7FXYq7FXYq7FXYq7FXYq 7FXYqzXyJrSem2lztRgS9sSQAQT8SD3ruPpzV6/Bvxj4ux0Wb+EsxzWuwdirsVadeSlfEEYhDE9b 8zwWLNb24E10pKuK/AhHjTqa9s2ODSme52Dh5tSI7DcsJmmlmkaWVy8jmrOxqTm0AAFB1hJJsrMK HYq7FXYq9S/5x+8qvqXmp9bkIFtoq1CEV5zTo6IBXb4RVq9jTMzR47lfc4WtyVHh730jm0dS7FXY q7FXYq+X/wA+dQmuvzGu4JPsWMFvBF/qtGJ/+JTHNTq5Xk9zudHGsY83nmYrlOxV2KuxV2KuxV2K uxV2KuxV2KtqrMwVQSxNABuSTirN/Lfl9LBVurhQ1626g7iMeA/yvE/R89VqdRx7D6XZ6fT8O55s tUggEdDuM1zmt4pdirsVYZ5s8s+pLJfWS0lNXmhH7fcsv+V4jv8APrs9JqaHDJ1+p01+qLDM2brn Yq7FXYq7FX0n/wA48to48kyR2cwfUPrLyanEdmjZvhiA2rwMaAj35ZtdHXBtzdRrr49+XR6hmW4b sVdirsVdir5I/Ne/+vfmJrs1QeFx6G3/AC7osP8AzLzTag3Mu800axhieUN7sVdirsVdirsVdirs VdirsVdirMPLHl8wAXt4hE5/uYmFCg/mI8f1ZrdVqL9MeTsdNgr1HmyXMJzEXatWOndf1ZVMbswr ZFLsVdiqGu13VvoOTgWJYV5o8vFS9/aL8O7XEQ7eLj28c2ml1H8MnXanT/xBi+Z7guxV2KuxVP8A yLf+abLzPZP5ZDyarI4SO3XdZVO7JKKgenQVYkjiPiqKVFuKUhIcPNqzRiYni5PsKIymJDKFWXiP UCElQ1N6EgEivtm7dCuxV2KuxV4P+Zn5i/m7o189tNbRaNYszi2urVBOsqEkD/SJAw5bV2VG8QNs 12fNlie4Oy0+DFId5eL3FxPczyXFxI01xMzSTSuSzu7GrMzHckk1JzBJt2AFLMCXYq7FXYq7FXYq 7FXYq7FXYqyvyv5fZWF9ex0IobaNvv5kfq/2s1+q1H8Mfi5+mwfxS+DKswHOdiqrbuVkA7Nsf4ZG Q2SEZlTN2KuxVTnTlEfbcfRhid0FBZcwYd5k8uNAz3tkg+rdZYl6oe7KKfZ/V8umy02pv0y5uu1G nr1R5MbzNcN2KuxVnH5Z/mdP5MuXjayhudPu5Fa9cJS6CAEUjkqoIFa8W2/1ak5kYM/B02cbUafx Ou76X8s+ZdL8x6RDqumGU2s32TLG8R5DZl+IUbi1VJQlajrm1hMSFh1GTGYGimmTYOxV2KqN9HZS Wk0d8sT2boVuEnCmIodiHDfDT54DVbpF3s+b/wAyfI/kCy9fU/LvmWzJZix0f1BOQSWJWF4OZXsq q6/N81efFAbxkPc7bBmmdpRPveZZiOY7FXYq7FXYq7FXYq7FXYqybyz5cMhjv7xaRD4oYSPteDN/ k+Hj8uuDqdTXpi5um09+osvzXOwdirsVdiqYIwZA3iMoIZt4pdirsVS9l4sV8DTLg1tEAih6YVYT 5k8vGzY3dsC1qxJdf99knpt+z4ZtNNqOLY83W6jT8O45JBmW4jsVZP8Alp5c07zH5z0/SNQd1tZz I7rH1f0o2l4V/ZDcNzl2CAlMAtOomYQJD65gggt4I7e3jWGCFRHFFGAqIiiiqqjYADYAZugKdGTa /FDsVdirHfNvkDyv5rjUava87iNGSC7iYxzRhvBhs1DuA4I9sqyYYz5tuLNKHJ4v5q/5x58xWBlu NAuE1S1G6270iuQCTtv+7fitN+QJ7LmDk0Uh9O7sMeuifq2eUzQzQTPDMjRTRMUkjcFWVlNCrA7g g5hkU5oNrMCXYq7FXYq7FXYq7FWR+XfLJuAl5eDjBUGKEjdx4nwX9fy64Wo1NemPNzNPpr9UuTMs 1rsXYq7FXYq7FUVavVSh6jcfLK5hkFfIMnYq7FUHdLSWviK/wyyHJgVLJoWuiOjI4DIwKsp3BB2I OINKRbB/MPl19Pb6xb1ezY713MZPY+3gfo+e10+o49j9Tq9Rp+DcckkzKcZmP5P3EsH5k6G8cTTM ZZIyqgkhZIXRm27IrFj7DL9Mf3gcfVC8ZfWWbl0jsVdirsVdirwr87POX5jaTqj6dFIdP0O4VTaX dorK8o6lXuDusgZTVUI+HrUHfX6rLOJrkHZaTFjkL5l4jmvdi7FXYq7FXYq7FXYqn/lzy6t8purr kLZTREG3Mg77/wAvbbMTU6jg2HNy9Pp+Lc8maqqooVQFVRRVGwAHYZqyXZAN4q7FXYq7FXYqvhfj Ip7dD9ORkNkhHZUzdirsVULpKoG/l/jkoFiULlrF2KrJYYpozHKiyRt9pGAIP0HCCQbCCAdiwTzD obabcB4qm0lJ9M7nif5Sf1ZttPn4xvzdXqMPAduT6E/Jz8tofLejx6pqMStrt+iyNzSj20bLtCOY DK9G/edN/h7VO+02DhFnmXn9Vn4zQ+kPSMynEdirsVdirsVS7zBoGl6/pM+l6nCJrScUI6MrD7Lo ezKehyM4CQos4TMTYfKfn3yJq3lDWHs7pWkspCTYX1KJMgp4Vo61oy9vlQ5p82EwNF3WHMMgsMYq MpbnVGKuxVvFXYqnnlvQDfSfWbhSLRDsP9+MOw9h3zF1Oo4BQ5uVp8HEbPJm6IiIqIoVFACqBQAD YADNUTbswKXYq7FXYq7FXYq7FXYqjoX5xg9+h+eUyFFmF+BLsVWyLyRl8RtiChAZewdirsVTjyfZ JeeZ9NidOarOkpUio/dH1Afo45mdnx4s8R5/du4faE+HBI+X37Pds7N4t2KuxV2KuxV2KuxV5Z+Y esC91gWcZBhsKpUb1kahf7qcfozke2dTx5eEcoff1/U9b2PpuDFxHnP7un62K5p3buxV2KuxV1Bi qFukowcdDsfnlkCxKhk2LsVdirsVdirsVdirsVRFo/xFfHcZCYZBE5WydirsVQEq8ZGHv+vLgdmB W4UOxVmH5WQCTzOzn/dFvI4+kqn/ABvm27GiDm90T+h1PbUiMI85D9L17OpeVdirsVdirsVdiqG1 K/i0+wuL2X7ECFyK0qR0Ue7HbKs+UY4GZ6Btw4jkmIjqXh000k0zzSsWkkYu7HqWY1Jzz+UjIknm XvYxEQAOQWYGTsVdirsVdiq2ROaFfHp88INIKAIIJB6jrlrB2FXYq7FXYq7FXYq7FV0bcXDeBwEJ CPylm7FXYqhbtTyDdiKfTlkCxKhk2LsVZ7+UduW1S+uO0cCx/TI4P/MvN32HC5yl3Cvn/Y6PtydQ jHvN/L+16jnSPNuxV2KuxV2KuxVhP5l60IrOPSY6+rccZZj29NSeI+l1r9GaHtzUgQGMc5bn3f2/ c73sTTEzOQ8o7D3/ANn3vOM5d6d2KuxV2KuxV2KuxVCXiFT6iqWB+1Sn8csgejEhBfW4/Bvw/rlv Chr64n8px4Va+uD+T8ceBWvrv+R+P9mHgVv67/kfj/ZjwK19cP8AL+OPArX1yT+UY8KtfXJfBfx/ rjwhU2s5Ge3Ut9rv/DMeYosgr5FLsVUL3l9WZlFWX4hX26/hkoc0FKDdSnwHyGZPCGKhZ6oLy3We CTlGxIBoBupKnt4jLMuA45cMhu1Yc0ckeKPJ6F+UmpSxapdQMapPGrGvjG1BT/kYc2vYs6nKPeL+ X9rqO3IeiMu418/7HsA3GdE807FXYq7FXYq7FXi/mfVP0nrl1dA1i5cIaGo9NPhUj/Wpy+nOF7Qz +LmlLpyHuH4t7jQYPCwxj15n3n8UlWYbmOxV2KuxV2KuxV2KuIB2PTFUDdWaV5caqe/cZbCbAhAy WjCpQ8h4d8tElUCCDQ9ckrWKpZrHmHTtKUeuxeY9II6F6eJBIoMztJoMmf6dh3nk4Os7Rxaceo2e 4c2Far5x1a+5JE31W3bb04z8RHu/X7qZ0ul7IxYtz6pef6v7XltX2zmy7A8EfL9f9id+R9d9WP8A Rc5q8YLW7kjdR1Tfeo6j2+WaztrRcJ8WPI8/1/jr73adh6/iHgy5jl7u78dPcy5RyYL4mmc+9GnN o1HK9iP1ZjzCQisrZOxVp1DoynowIPyOIKscl/dc+ewSvL2p1zNiL5NZNCy888na79QvDa3DgWlw d2Y0CPTZvDfofo8M67tfQ+LDiiPXH7Q8b2Nr/CnwSPol9h7/ANb2PytcvZ6rYSqwX96odu3FzRq/ Qc5fRZTDPEjvr57PT6/GJ4ZA91/J79A3KJT7Z2bxS/FXYq7FXYqk/m7VRpug3UyvwnkX0oN6Hm+1 V91FWzC7Rz+FhkevIe8/i3N7PweLmiOnM/D8U8azhnt3Yq7FXYq7FXYq7FXYq7FXEAih6Yqg54Ch 5L9j9WWRlbAhQeNHFGFcmChJ9b0DXL6ALpN8lt2kVwysfcSLyI+hfpzP0WqwY5XliZfju/b8HB12 DPkjWKQj+O/9nxYTe/l55tilc+gt11Zpo5VPInc7OUcn6M6XD23pSBvw+RH6rDzGXsXUgnbi8wf1 0Uiu9K1SzAa7s5rdTsGljZAfkWAzZYtTjyfTKMvcQ67Jp8kPqiY+8KNrczWtxHcQNwliYMje4yeX HGcTGXIscWWWOQlHmHrei39tqUMVzAQyMKuvdGAqVb3GcDqsEsMjGX9vm+g6bUxzQE4/2eSco3Fw 3gcwyHITAEEAjodxlLJ2KXYqx/X7J5UubeJgj3MThHaoVWcFakgHau+Z2kyiMoyPKJH2OPqMZnjl EcyCEB5f/LjSLBEm1AC+ux1Dbwqd+iftf7KvyGZ2t7dy5DUPRH7fn+p1ej7DxYxc/XL7Pl+tkDoI 3KqAoX7IGwA7UzUg3u7iq2fQWlzGfTbadl4mWJHK+HJQaZ30JcUQe8PAzjwyI7iiskxdirsVdirz L8y9S9fV4rFSeFmnxj/iySjH/heOcp25n4sogP4R9p/ZT1PYmDhxmZ/iP2D9tsPzSO7dirsVdirs VdirsVdirsVdiriARQ9MVQc8BT4hun6ssjK2BDdq1JCP5h+IxmNkhF5WydiqAu9A0O8Ltc2FvLJJ 9uQxrzP+zpy/HMnHrc0KEZyAHnt8nGyaPDO+KEST5b/NRsPLOlaYH/R0RgEn205u6kjvRy1D8snn 12TNXiG68h+hjp9Hjw2MYoHzP6VVlZTRhQ5SC5CJt5l4hCaEdK98hKKQVfIMnYqo3dstxEV2DjdG 8DkoSooKhp8jrW2mqJE3UH+XJZB1ChddLSWviMYcmJfQsESwwRxL9mNQo+QFM9BAfPyV+FDsVdiq jeXdvZ2st1cOEhhUs7HwGQyZIwiZS2AZ48cpyEY7kvD7+7kvL2e7kFHuJGkYDoORrQfLOAzZDOZk ept73FjEICI6ClDK2x2KuxV2KuxV2KuxV2KuxV2KuxVxAIoeh64qhZIjE4dd0B+7LAbY1SKBBAI6 HplbJ2KuxV2KrJI1kWh69jhBpBCElhaM77jscsErYkK8NwGor/a7HxyEopBV8iydiqhcW5kKyRkL Mn2GPT3ByUZVz5IbVDcyQLTizuI2U9mYgZZijcuEdWGSXDEnufQeegPn7sVdirsVYb+ZmqGDS4dP Q/Fdvyk6f3cRBp47tT7s0fbmfhxiA/iP2D9tO67EwcWQzP8ACPtP7LeaZyr1TsVdirsVdirsVdir sVdirsVdirsVWvJGgq7BR2qaYgEoQc2qW4BVVMn4A/x/DLRiKLVIbscAGQjbpWtPbtgMFtEJIj/Z NfbvlZFJtdil2KuxVogEUIqMUIWa3K1ZN18O4yyMkEKkFxyoj9ex8cEoqCr5Bk7FUTpEdv8Apmxe YqkQuYWmZjReIcVJJ2+z3zI0kgMsCeXEPvcfVRJxSA58J+57pnfPBuxV2KuxV5B531M3/mK4INYr b/R46eEZPL/hy2cV2rn8TOe6O3y/bb2fZeHw8A75b/P9lJDmudi7FXYq7FXYq7FXYq7FXYqoSXtq nWQE+A3/AFZIQJRaEk1c7iOP5Fj/AAH9csGHvRaFkv7t+shA8F2/VvlgxgLagSSanqckhVtY+UnI 9F3+nBIqjcrQ7FVRZ5V/aqPA75ExCbVUuwdnFPcZEwTauro32SDkCEt4pdiqhNbhqsmzeHY5KMmJ DoJzXhJs3YnDKPUKCr5Bk7FWd+RvOPp+npOoyfu9ltJ2/Z7CNj4fy+HTpnQ9k9pVWKZ/qn9H6vk8 92r2dd5YD3j9P6/m9CzpXnHYq7FXhnnLS30vzHeW4BWF3M1vQED05PiAX2U1X6M4vX4PDzSHTmPj +Ke00GfxMMT15H4fi0l5N4nMOnMdybxONK7kfHFXVPjirVT44q6p8cVdU+OKuqcKrZEEiFT9B8Di DSoB0ZGKt1GWgpW4q7FWwCTQdT0xVHxR+mgXv1Jysm0L8CuxV2KuxV2KqiXEq96jwORMQm1dbpD9 oFfxGQMCm1VWVhVSD8sjSVssSyDfY9jhBpSFkbsh4S9f2W7HCRfJVbIpdir0fyR5zW4WPStRci5A 421wxr6ngjH+bw8fn16jsvtTjrHk+roe/wAvf9/v58x2n2Zw3kx/T1Hd5+77vdym2b50TsVef/m1 pJktbTVY1FYCYJyAa8X3Qk+CtUf7LNH21guImOmx/H45u97Ez1IwPXcfj8cnmOc49G7FXYq7FXYq 7FXYq7FXYqpTw+otR9odMINKgiCDQ9csS1iqItI6vzPRenzyMiqLyCHYq7FXYq7FXYq7FXYq4Eg1 HXFVVLmRdj8Q9+uRMQm1UXETijinz3GR4SE2vRgopXkg6N1p88iUoTWNf0bRoBNqd3Hao1eAc1Zq UrxQVZqV3oMtw6fJlNQFtWbUQxC5mmMXn5w+TbYqYJbi8J7wRFeP/I4xfhmwx9i6g86j7z+q3Ayd sYByuXuH66ehfl//AM5Q+UtVuk0rzAH0mT4Y7XUp6GGToo9dgW9JjWpY/B1qV79Tp4zEAJkGQeX1 BgZkwFRe3wzRTRJNC6yRSANHIhDKyncEEbEHL2hC6xpcGq6ZcafOSIp1oWHUEEMp+hgDlWfCMkDA 8i24MxxTExzDwG5t5ra5ltpl4zQO0ci9aMh4kfeM4ecDEkHmHuYTEoiQ5FTyLJ2KuxV2KuxV2Kux V2KuxVDXMNayL/shkolKGAJIA6nYZNUwRAihR2yolC7FXYq7FXYq7FW1VmPFQWJ6AbnEC+Skgc0V DpGpzbpbPTxYcR/w1MyIaTLLlEuPPV4o85BEDy3rJNDBT3Lp/A5aOzs3837Q1HtHD/O+wq48pamR UvCPYs38Fy4dk5e+P4+DSe1sXdL7P1q8Xk+cj97cqp/yVLfrK5ZHsiXWX4+xql2vHpH8farR+Tog f3l0zDwVAv6y2Wx7IHWX2Ncu1z0j9quvlLTlIPqzVH+Uv/NOWfyTi75fZ+pq/lbL3R+39b5x/OyS VfzBvrLmWt7JII7ZDT4VeFJW6UrV5Cc2uk00MUKiHV6rUTyyuRYJmS4zsVfU/wDziJp/mFdA1bUJ 72QaC8/oWOnHiUNwqq00+68h8JRBxeh+LkKgHCgvoLFDx/8AM3SBZeYfrMakQ36erWlF9RfhkA8e zH55yna+Dgy8Q5S+/r+v4vV9kZ+PFwnnHb4dP1fBiOat2rsVdirsVdirsVdirsVdiqvaWF9euUtL eW4cdViRnI+fEHJ48Up/SCfcwyZYw+oge9PY/wArfMqQrdskahhX0ORaVK+KqD+GbQdkZjGzQdbL tnCDQstDybIrFJboJKv2k4EkfOrKckOxz1l9jWe2B0j9v7FaLyfbj++uHf8A1AF/XyyyPZEesj+P m1y7Xl0iPv8A1Ky+U9MBqXlYeBZf4KMsHZWLvl+Pg1HtXKekfx8VceW9G/5Z6+/N/wDmrLv5Ow/z ftP62r+Uc3877B+pXTSNLQUFrER/lKGP3tXLY6TEP4R8mqWryn+I/NWis7SI1igjjPiqgfqGWRww jyAHwapZpy5kn4q2WNbsVdirsVdirsVdir5U/O7/AMmdrP8A0bf9QsWZEOTRPmwbJMU28p+XL3zL 5l03QbIH6xqNwkAcKX9NWPxysq78Y0q7ewOKv0B8teXNJ8t6FZ6HpEPoafYpwgjJLHclmZierMzF ifE4WKZYqxn8wtFj1Hy7PMIw11YqZoHrSiggyj6UB28QM13amAZMJPWO/wCv7HY9l6g48wH8Mtv1 fa8XzkXr3Yq7FXYq7FXYqmOm+Xdc1Ir9SspZkbYS8eMe3/FjUT8cvxaXLk+mJP3fNx82qxY/qkB9 /wAubK9I/KjUpZEfVJ0t4OrRRHnL8q04D51ObTB2LMm8hoeXP9X3usz9tQArGLPny/X9zL9N/L3y tYgH6r9akH+7Lk+pX5rtH/wubTF2Zgh0s+e/7PsdVm7Tzz60PLb9v2sghhhhiWKFFjiQUSNAFUD2 A2zPjEAUOTgSkSbPNfhQo3NnaXScLmFJV3oHANK+HhjSQUovPKNhIpNo720gHwgMXSvuG5fgcrOM MxkKQ3fl7zBa1IjW6Qb8o9zT/V+E/cMgcZbBkCXNdCNzHNG8Ug2ZWFCCPHvkKZ2vW4gbo4+nb9eB VTFLsUOxV2KuxV2KuxVpnVBViAPE4q8U89flBq3mfzvf6yt/b2un3Xo8Kh3mAjhSNqpRF6pt8eWx nQa5Qsr7b/nH3yssai51C+llH2mjaKNT/sTHIR/wWPiFfDD178qfyf8AKnli6/Ttrp3p35jMVrcT SPI4jcDm4ViVUsNgwANKjocnC+rXOuj1DJsHYq0yqylWAZWFGU7gg9sVeBa/pbaXrN3YN0gkIQ+K H4kP0qQc4fVYfDySj3H+x7nS5vExxl3j+1L8ob21VmYKoJYmgA3JOICkp5p3kjzRfjlFYPHHt8c9 IhuKggPQn6Bmbi7OzT5Rr37OFl7RwQ5yv3bsp0z8o3qj6nfLQN8cNupNV9pG40/4DNli7E6zl8B+ v9jrM3bnSEfif1ftZlp3lLy5p9Da2EQdSGWRx6jgjuGfkR9GbbFosOP6Yj7/AL3U5dbmyfVI/d9y bZlOK7FXYq7FXYq7FXYq7FVKe1tbheM8KSgdA6huvz+WNKCkWoeSdNnFbVjav4CrqfoJr+OQOMNg yFILvylrlpVolE6AVLQtv16cTRvuyswLYMgSx5b23k9OZWRx1SRSp/GhyBDMFUXUF/aQj5Gv9MaV VW7ganxUJ7EYFVVZWFVII8RviriQBUmgHUnFUNNfINo/iPiemGlQTu7mrmpxVrFWReVfLr3cyXt0 g+poaorb+ow26fyg9a/LxyyEba5zrZneXNDsVdirsVYV538iXeuajBe2Uscb8PSnEpIFFJKsOIap 3pmo7Q7OlmmJRIG1G3cdndpRwwMZAnexShp35S6ZEQ1/eS3J2PCICJfcGvMn6KZDF2JAfVIy+xnl 7bmfpiI/b+plmk+X9H0lWXT7VYOf22BLMfYsxZqfTmzwabHi+gU6vPqcmX6zaYZe0OxV2KuxV2Ku xV2KuxV2KuxV2KuxV2KuxVRu7K1vITDcxiSM9j/AjcYCLSDSQ3vkbTpSWtZHtif2f7xfuJDf8NkD jDMZCkV55O1m3HJEW4UVJMR3FP8AJbifurkDAsxkCXjRtX/5Yrjb/ip/6YOEsuIK40PX5lANpKQO nIcev+tTHgK8YVB5S8wEf7y/8lI/+asPAUcYVU8ma4w3RE9mcfwrj4ZR4gRlr5EvfXT61NGIK/vP TLF6eAqoGSGNByBmccaRRrHGOKIAqqOwAoBlrSuxV2Kv/9k= + + + + proof:pdf + uuid:65E6390686CF11DBA6E2D887CEACB407 + xmp.did:d4d07395-aa96-47c2-a9e5-d0351947bb0c + uuid:c63c1031-e157-9748-9c58-86481308e954 + + uuid:1abccb90-0c26-4942-b156-fd2eb962e3e1 + xmp.did:58fdc1b8-1448-3a44-9e20-282d8ec1cf95 + uuid:65E6390686CF11DBA6E2D887CEACB407 + proof:pdf + + + + + saved + xmp.iid:d4d07395-aa96-47c2-a9e5-d0351947bb0c + 2016-06-15T14:23:10-04:00 + Adobe Illustrator CC 2015 (Macintosh) + / + + + + Web + Document + 1 + True + False + + 128.000000 + 128.000000 + Pixels + + + + Cyan + Magenta + Yellow + Black + + + + + + Default Swatch Group + 0 + + + + White + RGB + PROCESS + 255 + 255 + 255 + + + Black + RGB + PROCESS + 0 + 0 + 0 + + + RGB Red + RGB + PROCESS + 255 + 0 + 0 + + + RGB Yellow + RGB + PROCESS + 255 + 255 + 0 + + + RGB Green + RGB + PROCESS + 0 + 255 + 0 + + + RGB Cyan + RGB + PROCESS + 0 + 255 + 255 + + + RGB Blue + RGB + PROCESS + 0 + 0 + 255 + + + RGB Magenta + RGB + PROCESS + 255 + 0 + 255 + + + R=193 G=39 B=45 + RGB + PROCESS + 193 + 39 + 45 + + + R=237 G=28 B=36 + RGB + PROCESS + 237 + 28 + 36 + + + R=241 G=90 B=36 + RGB + PROCESS + 241 + 90 + 36 + + + R=247 G=147 B=30 + RGB + PROCESS + 247 + 147 + 30 + + + R=251 G=176 B=59 + RGB + PROCESS + 251 + 176 + 59 + + + R=252 G=238 B=33 + RGB + PROCESS + 252 + 238 + 33 + + + R=217 G=224 B=33 + RGB + PROCESS + 217 + 224 + 33 + + + R=140 G=198 B=63 + RGB + PROCESS + 140 + 198 + 63 + + + R=57 G=181 B=74 + RGB + PROCESS + 57 + 181 + 74 + + + R=0 G=146 B=69 + RGB + PROCESS + 0 + 146 + 69 + + + R=0 G=104 B=55 + RGB + PROCESS + 0 + 104 + 55 + + + R=34 G=181 B=115 + RGB + PROCESS + 34 + 181 + 115 + + + R=0 G=169 B=157 + RGB + PROCESS + 0 + 169 + 157 + + + R=41 G=171 B=226 + RGB + PROCESS + 41 + 171 + 226 + + + R=0 G=113 B=188 + RGB + PROCESS + 0 + 113 + 188 + + + R=46 G=49 B=146 + RGB + PROCESS + 46 + 49 + 146 + + + R=27 G=20 B=100 + RGB + PROCESS + 27 + 20 + 100 + + + R=102 G=45 B=145 + RGB + PROCESS + 102 + 45 + 145 + + + R=147 G=39 B=143 + RGB + PROCESS + 147 + 39 + 143 + + + R=158 G=0 B=93 + RGB + PROCESS + 158 + 0 + 93 + + + R=212 G=20 B=90 + RGB + PROCESS + 212 + 20 + 90 + + + R=237 G=30 B=121 + RGB + PROCESS + 237 + 30 + 121 + + + R=199 G=178 B=153 + RGB + PROCESS + 199 + 178 + 153 + + + R=153 G=134 B=117 + RGB + PROCESS + 153 + 134 + 117 + + + R=115 G=99 B=87 + RGB + PROCESS + 115 + 99 + 87 + + + R=83 G=71 B=65 + RGB + PROCESS + 83 + 71 + 65 + + + R=198 G=156 B=109 + RGB + PROCESS + 198 + 156 + 109 + + + R=166 G=124 B=82 + RGB + PROCESS + 166 + 124 + 82 + + + R=140 G=98 B=57 + RGB + PROCESS + 140 + 98 + 57 + + + R=117 G=76 B=36 + RGB + PROCESS + 117 + 76 + 36 + + + R=96 G=56 B=19 + RGB + PROCESS + 96 + 56 + 19 + + + R=66 G=33 B=11 + RGB + PROCESS + 66 + 33 + 11 + + + + + + Grays + 1 + + + + R=0 G=0 B=0 + RGB + PROCESS + 0 + 0 + 0 + + + R=26 G=26 B=26 + RGB + PROCESS + 26 + 26 + 26 + + + R=51 G=51 B=51 + RGB + PROCESS + 51 + 51 + 51 + + + R=77 G=77 B=77 + RGB + PROCESS + 77 + 77 + 77 + + + R=102 G=102 B=102 + RGB + PROCESS + 102 + 102 + 102 + + + R=128 G=128 B=128 + RGB + PROCESS + 128 + 128 + 128 + + + R=153 G=153 B=153 + RGB + PROCESS + 153 + 153 + 153 + + + R=179 G=179 B=179 + RGB + PROCESS + 179 + 179 + 179 + + + R=204 G=204 B=204 + RGB + PROCESS + 204 + 204 + 204 + + + R=230 G=230 B=230 + RGB + PROCESS + 230 + 230 + 230 + + + R=242 G=242 B=242 + RGB + PROCESS + 242 + 242 + 242 + + + + + + Web Color Group + 1 + + + + R=63 G=169 B=245 + RGB + PROCESS + 63 + 169 + 245 + + + R=122 G=201 B=67 + RGB + PROCESS + 122 + 201 + 67 + + + R=255 G=147 B=30 + RGB + PROCESS + 255 + 147 + 30 + + + R=255 G=29 B=37 + RGB + PROCESS + 255 + 29 + 37 + + + R=255 G=123 B=172 + RGB + PROCESS + 255 + 123 + 172 + + + R=189 G=204 B=212 + RGB + PROCESS + 189 + 204 + 212 + + + + + + + Adobe PDF library 15.00 + + + + + + + + + + + + + + + + + + + + + + + + + endstream endobj 3 0 obj <> endobj 7 0 obj <>/Resources<>/ExtGState<>/ProcSet[/PDF/ImageC]/Properties<>/XObject<>>>/Thumb 14 0 R/TrimBox[0.0 0.0 128.0 128.0]/Type/Page>> endobj 8 0 obj <>stream +HwVu6PprqV*234R04S32P4ճT(J +W*w6PH/H+X)Hwr.gK>W /@.ӊ endstream endobj 9 0 obj <> endobj 14 0 obj <>stream +8;W:dYmnJk$j=`^PKX*GV"-/6MPPhMW4o*jFegTA5n:ROqi. +8M?-(/t#IN>re.=TbIMqYWQK1D%b&pOLGa]H?hKs'8Gqa4A/k;[i&\e-=4:h!/H6BW;~> endstream endobj 16 0 obj [/Indexed/DeviceRGB 255 17 0 R] endobj 17 0 obj <>stream +8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 +b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` +E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn +6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( +l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 13 0 obj <>/Filter/FlateDecode/Height 947/Intent/RelativeColorimetric/Length 90241/Name/X/SMask 18 0 R/Subtype/Image/Type/XObject/Width 880>>stream +Hoi@Hy&8_nyA'?6G3+ZHҥYakOj6gיoU GHII_AgK/EcF6LrchI 2$҆ԘU4w$5_7BQUm"Ť>&k2W$%Nib;Iߓuavտ,HJ \u.&1ٌ^₞@Ǥl_Lrs:#ј,32] IJ7d+65i1$Lb#d]G>&Y=g답*_/*:p.uʙcRIf") ˬ#q4Ό=sL&=(P{ HJ+b~n+cSFsm0'&&cܼXI=3zER,D#0)2=r +I Ә}즟 (9?l?ݳ;݃Q~twoo `41)"g476WxzGMݞx7hpqh{ƃn\ w Zᶂ37{M23>)25Eܩo|+>8q/8m y3=??~wL#I\dΕ/doޱ=Hh|d]ү$*wOc} yz<*\@~R/}}FRHxw G]as &lu9x")`m;=-Ƀ -O8Cmȑ{mG.&?){3];,V01o`it4)'ѭU, ]?b<ݳN=;.]Lں*_w6}hL[I$np+bdjlb46[ܩ0k`I{-ɯG_>]zt8rI_K}4јغOinӭng`HUN4ݛ|yRr #+x/>骞SyXAQȃękfUXDvE#&sBe< HQ%\Ωdg'sǟá:>xQ +!K +W<* '%Y%Vmao!ǩkv>w u{=Q<\ȃ*fƸmqY%ŏRpV{ ueW&)!)sE2Jݓ?ҋӆgohԎeɝiFGb}/g. +,%m.7'FX!TՀITV $y9Iפ?_ȼ0;Wi;h9:FQ]itB)`5yIe[j*lraպS"u 3$hۯNT´A}ٷO.ϡqˤ3!"Iڻkha˳J)@) iq1J٦oչrEIWt+>]hlrW9,-nr_}i#eR=椔 5 ](?"aIK9;z>g9d68 =FGY/Հ@@ 9ۋFJT2[̟~>:ekG<Q2B&M)}YƢ}\ekfeJ#.-3 +iHF'>hd,I#_ыTj~Q5cR`n:s e8 P/di]Ҩm +!g֝V=@nI${%3Tj[ԣ`Į;m$XOT4==Aŵ͆ޭ|hˑ"6XWvZY,{&y` wɵs/NٮMDz3z2 F^ęA r۝gB7hu ȲhI})CF +WWzٶ:lgl7ɃHiJ&/ Ӻg.}C'dD|V֪'9TL*4I]6 x74MVK%X T8ENZ!f8ah@&͚-AsׄOQ"2sO-ʃ#db-tgHIFHVj.Y!х@Cdҕ@2ǯeqyJΎC43nw0"D2ȥXiQϖJ'&:?Ed!ªGIKSيb$utõǭ2'}~dbNct`d K񕨈\29juC ڣy 5,ҧ9.~&g(r\&$zkddjd_&n,dk딝]|ڷт$lw)-H](H&, tHU4ѐxIE$\+Kl֓ȁI*?^^O/N*)iit<~O&=۠SZ0LK578hsZ?5ĬeV"k K +>#gV-?}= TjOK<>Nh auOBnY#qkB)fiQ@ 7@Olo-n 9 =)~erŗzC9z9Ilr&JO-NbW3Ӳh adR<+}D?NJiPJG%:?5pn2TI2v֋rBk &l f'<[wm}2I4KtwH r"!hͣ .:17f͝$Wq`Z Z 'R\%n~2/f|i|a*J&r>-fj@eRc}'yGBvr*'r +>|.WB~0ɱ{H ܌232ɤMRe7r!ic/_Ȗm͇OJ!dfH)OtE9#jԝj'C]aպWf+>KctȁL&rK%SͧH&1'ejuQA2p!Ϟ* UKW?-02hZ!nKO.? ZdzѤ٣wrLI*΍Sі2+TI,5N$6ぺ G7 whQXI4:?5ƫhq-Ǿ(v,vHz&.aKiݵdTOph2E ɤ0J>-zBb8,A6Mgd͝$K I,E[ 8ƶ0yTS rlS]|ѩ+&_9Ezb(Jr2h+ɓ)⇻A!_:۪.%ٱ4E3w~ sOq9F$~EH(M"0>"C|)4 {edUŭ߿/|}e.tD _(^u)TJ 8([E;ZgbDR!TY;g$÷=W__h8pü8SqO㠸*5:Mb)r2`Ny@:iXg*-X=zb-osW̟Y[V$J|!h|$p6V2LRwsU""YA(\A[u0#0j>k6NZ *P$Idv`;K?29G3@/)hqGaLH&)#f2ݥ:"@ +c1BuUU!hB +m?IXqBf=O-uS]*pb Lp=a d0 '%}QJ1kv-E&Y%͇ѓ!L6y֯-ZNſw@ME<V ++Qf1XGbu.AL}{;j:1XM;`m)ݒr2??bӥ^"T.4{7V:7cO n]&IIʴ׭]׳L&ټ~e?618qW 6$уS-J+j &HR#Y(u3C"vaVO qˤg/{Nd* w4~8`ਡODT +( ƃ(rVu6z0F|iU6_Up_ |7//y e26kaE9JTh<'| e\xy(7QQ1Z)7#5eoӈ0g+ۨwCxc XSg")-nkEJN[* FAKLLR7R.LBME#@* +~U݊tEEVOt7U콊ؾxԜ'isjf=O[ZO ((A>&]r"т|f0`A|0/2}+58{:!ELTǝuB1HzGQ0g[|Q_[VSor^Gy?lD$g=?ȩՕLN9 +K*RYģpu0%K'*- lpD ID2MnݾbL)OYׯR3OF"R j"iO8%{Pqs ?Hee}-XJNm\-H/}GϩZ&L/u>7&I wQ) /+P.bP"$N55B]={2`[Hnk?-s\粓y*dqP9I,1k[`^A/n3ՕcVna-_s%YSM{b FǠoZnkE%yt$mAs%Ev]JW^xA Xk0v_KӉ i$Fߪ/u( jLIO%k/Sr7B>Y,770ݙs)]ubQ9OΈL12$jn*2*쾊O&iV"sr+ 2LjȞCxҜJ 9:Jʌ H(hxA&xk i&Iu$6ԕj:.E Q\-^*%V@V;RM]dLW<}*w&Kߊ{-7@-&;. +C66 @9TBUfI[#v1r`,f/5n TLֹޤosIwT&\ߍ#UBXJ&uo6TE-EHLu[FUf +x謖Xz{FEr6qiVd>սl +\Uv^dKCR&p6kڄo@)ɛzxZZfBv5nFC `r{Lŷy7g2H&;x@kASYQC,29Wp +c!{)r*Rj!&#8ˁScM}Zi*H&Mf$\P +Ŵ\Id eDҐIЍF1|CeH ldԬ6i.2K8t׎&t(Q+ZfB*R&~?g4|W~ !$1[NIkqS(T['iͧ4*m~@?>KT)ΕJ3t +dEÀ."!|g_F>";,o)%OQ~Z2FBt-Ŵ=ݗJ)Rbd/}0i +3%f_,%.u;}oZ_`>19)ۂ֙Ĥ b- 2z[&;BEz1i6Cӯ!G9htj9'I#8ˁB!=*t-T:zTG\2z;F3}ZgLhHӍָ!fiVL:,N0EwHVsR I !-U֐L1#4zvB`V|u 4i$\"&^Xjbޟ mR q6H JER/-N&w',͌ƹӿ8!fI|1TBS?D%}35fW̊CȊ\!/5n:VC1@#&R&2rÚ!"H OS&c1[pUyuN'v96c9)Ӿ/I W%f<}*Gz{-/0D֧)T!LSXva?:n[ʜUX% *R&~o㹤w-dr>و'r*+*1=9)zKy$Sp$"ӔIWcQ@&~!AZۑ[W *'W|쌰95n}ăF.i(xyB [(58|i+&Yjhz>˜2m^?fA)\('N,1^{,gI&}<Ǿ dTVԀaI$7XUkt sU dl:OOٯ<2w)6E =Lq<{ hK|7%FE=ikA(#cia78Hw:;i'}h%Z^N^7VҺuQIHNcMft+ +0 '0$:HJ\ ;``pPL=NM.H1Eb~ԓvsK`TO=hwѓ7|GOYZaȎL7e Ջ[녤&Ɍ;b1lx"Iw!}9pXfv`7xgV~π\\zπD-/؁_~ɬebJz!QyrTS蕴.}?]$i;o"):F)(&ۂS^/Ǧ1IG )!bZ1 p8ĕT-@Kp +m crE?m}F!e_JRPF +7b1T}<x4zV,&읲yeTJ=q#cz>)Rv:5[QГO"5o) c^mXyTعT=%o-oK2U~c͠B>(h1*h|:6Ll' ޑ4 =ui7eGo{s͈KjDE1!3e00R,I %y)1]5ά>#Vgr|%vVV>&I5lS<v Z ;RLj/1'{uO +ؐsz( o?;I&mT@L>`|wdF[pY!=;Yk AR;o^2Lh ~_ٛ|GdO q/zRqcw_}9~eiq$i[?~$N%Y72zىSEx1,HDlEg3JJy}F}7)?'|(GoŤ*isFGO=`r3R8)p/MM$j٪u=9(&˜|m5(t75zM'O \]]ITٱ3u0Ǜe/$iF1Da*b1Tzz_W8/wzg'=srV~@?];(&0H1ʿ[P=kW˻zBf`d.d*XJZC^mt.'h V QLqr9wTҺ辣QEx=D19-d!}?d!}3Z#)nmDzly_|1^Nd`Q0l9'0NnbX9T0ZdWf˂8d=pJl#&BsԨA7FDqڇJZ*レT=eSH'YTo4>doE|~ $#&1¾W` ڑ1)د6:'P}O࿠ne*Fرs|q +(iC4P+ $ +cT6^b-4je˷O|zS~?_qCjRr̖E˓>jEk.C-n#E<IO{vE5ӄ&EN& ݆)-:< I'%}8HwяV4d=Yv 1|Dh*; <Okr(Ny%*~*Yy&WA4!x2zsY-L"=\Le5ƔRFI%IF\3_87 0>hq x2`+IǙBh#R8-w*Ó1YeVO[x!mh?[ <$|`2s2l嚽O EҌX)#Z!Sр4Yoy?ªf8jO1O_9O"%z dy.nNY2u5UV\Q~ɲ|kxrd'?apK tE7s!m`jqZv[>hZ-%6}az,ڜdKɷGM)،xd"6T\l,&c<'Uf; +w&B gw)#„S]\QVQ>$I_jJX,\^YSd|'zD%{o1!䅇qx';qڈ>
khYӷ@mwGyxbr ~=ͱ{9hsۈ!x2< !f!mf" e!ONYW,B-%'>,;} ^&CE"OtzK68]dGRfɈt}B)ILN-^3d[ɿ/3GlV&77ug#K1P)^I\D}&/o>߭SHh+<1Iy_uA^ +sMzC*d\'\z1zADd& +9$Y"?LtzK_14*Y|!ԯ)7$URyuf۲/ɖ$yhs:ڏa<#<(){;qSdLt&}ZHy$yyLܘ: ew}\yYj|aIQ%#?xCE"Oq1Nb5˵I~t֌ZcEb-%Շ8}@i?qqN~ Ndl'\z¤.o !جlݜ(B],YoSO&w"0fr +L&\Pby?ޓur!m FZKGbrΓͲc)eE+WqytT?w ]]}["֫K5Ez~J+T3YnO26hSEH'㷢MO$ta0?j9VuK'/K~CcζI-OV/KѸmkҡuΦ)"&㸔]LyĂX)cML=yn\Ω۬ crї'ma5r(E=pu7< + [rd{d7.`w(d;wr(M=zRy +7]=!Ij9Cidy!NmSiǯƆX9r R:wP<+y^{᬴$eYn;ﷂ2^%)uũ Kw Eߊ. UxlidW)I5Ip΍y%gMGƔd Z9"~t]utڵɲ2E#{Xtp7 #i4i>f-2x jL?bGœ{y9k +AQש'=FE4b2&al6>` +hB");Is*QY9c"1鲒z2klvy0 7>`%JN dXn; ŘWO8@g,צ)_cvH$q\ѾM_@%Ƈ؛XBRcΜJcRΆ1xZU]è-9NEw'c뜠I=]c fi~>?!NI&ļfb2 Z8,W䥌e|a(!me?MQH'cMsY*+!\VuSLQ5Kp#}֓mj:SHú5\bØC)I0> jYn>_+c t57*pT̛6=nW%4EQ4z2~+ɜEO1$|T9c^Jt,Ύ9AŵK2Ɏ'+{,uCL/ڻAZ" +d֕MW.oBgDtʿv(uX4Kp}Gߓ-8,]o^ѓ[J^c(NY]$eo h9[ƣ:1vt᥌e| q'Kp4 b>HAB t2n{'ͩ(*rGOӡz>(-ɘ-d6=г.k؉NO&37{UGɓ*Ysgzҋ>XA[ &f.oqC󢔱gs.$e/;?n>.0&cT51}<;(*FOkE;a\.S+S=˖&EǗo3rLdA˿}yb/IE\E"*;pIыeCbuZ ){ٻ #=I_cN|k)rd@Q?h.3Ed^ts*g5 xQX욮&VO䩪(Idt1>ðh[[AEX̕ϺܓyK{./T˱^>xL,Mo'svy[/*|OJgC{::EQ1SDLk%>>Ѕ<I t -eo$7-gHIΆ(&-^^rs *BadVؓ-W*j͉IvHʞNLO:W%_31dӨXܸvH^|Tݓ+9/Hrdz_wq\e?@MQ̙ݙ"NuhEA(iK̙}v(AHQ"@D(WDUlMĎ-'Eyf&٦Q|~GV ?|mzR3|}pӬ3 QJoJpd\nc\7n,Aײmɏzs ޡ22JywoK2/X'& ځNJ* pbR3|w) A7ɤbrc )#f dp[M_&g][ه?@O*v'd5Ä-`˨[ GOFntLλ=95fPL +&!&;=@OK1Ŏ=5:2J.778&$k4RJGL*sͽ֨+IN,Iij&}`K闍sEvDRϿd}OIb_93Da`(r\|@O@Moߎ$gi)R~cb]_+$H!n~F]1GL*vZFi2T'{ z\ARFMbz~wb1Qe5+zRb25em-3_~Ȯ) ֧I/_Ox^JIQOyT=F9CW鉣)fʴRڴ1[Ig + &NA@Ot\ᏻNoV0[%dRїbۤARJwu oX7h4b0$m~a'U:냰6G8XГOГWuNVL1҅ Qp00bIe΍{֠ 6 =q(B:w6Ñrޯ[?^ORLRIv+/gRJ:A{MAOzIN0n/{Nac5H$,+ԓr~ ~OWllfC+0+ГwځZWBA9%gѓ-y唋w-GF)Uk(5?C%;=GLj/bIIzYw~<HיմFi1GL*t\۵ŤH5$gP!||5Y,_;$8hdẂHh C~aГʜ` 2$R 2-D=yq{IeMИJ)1 wT2#&:=FI'@L&ƥfZDRʲ2n%%AL)~(_"H1IW0J W'91S;nZk PJk3=) {=^)&/75fPOG3-#&7@.d{LO\ ~OywtALL%h//8IeN@7sbHbۼ1W2\jn} bRn@z4M6 +'?Ztw +٫0T?^Г" [od% I'{}q{LII6WeVgROS8 K@D\>&;sp6"l\Z= SԞ89c-|~ۘlr\iƌU?D'3&Haaőom“ߓ?wP9Ô"BH$&;m5wI'6WvyCi~tw g#̵͎.p9 FЧziۈ$)&$#})r%R+ [=ړ~t; ՛!Qײ^Gzr}\10Ouc+#C͂&(_HP(D +d!թXKtkcZ*yͅ (  w¯<\*Db,$=OVp'1K.:|/lӥ+i&o练Ɏ[UlL=t _wHY{D'C%A)Cyt)8tDzPˠ|!B!DDEg ̓~WF/Vs'A\U/! +.a{0Ç)zfnڛ>< +.ĕ#_uMLzb)ZOVfc+UA)" +4D')58=26L">^&Ư~nc#{Uҭ' T Z; $U:ri +_͒K 쳷x#LJ4K\4^mΔX][XVBf@)5:'7}OV2L=Piϲgc0Yh-8iҧVk6\o'Nq|$T($)y6Aߓg O"Hb1flsarEtku*F?L$ |)> R ѸoB(L7H>IwUhc}[3;/)go2qCJ= RHOY$BkѧJ6)b Fl{h-8թrbVyZgcF;HҢt@ʰߓŤA#v齌3BILODxRzI;e5 Rkw'w9OD)Ý$)Cy|O[X)7PG$E,̿6D\GLJ_VO,Zhֻ\/of>!Ç6A0ߓN(+MC d.ia1^j&VmXuw}q:ZLa\RM24Iaw ! +yš|%0KeX\vIِ)H{fZ;qRC{ /Dny]&OkꉥUS=l'凯Gw%)H3ct:BxO 3$0.e#PɶO +|XZE_\(ZODhғŔA-]%f.>nEѫpE/zT(ЄOJs-M*_*PEJ}{ Pm3\)W>[w*]d:@,wZ$IQ@97,aNEd$KeR,}jV]RDP($)]ߎccC$BhGlkFbRz@>ZVN|H[l$R=y:QE>HiϯFxbJjVV5ܮyR^ +rk'eG!% :W!G{DNhJ\9\wACl +wϱR>"j'3J)_PKwG&) wZtݠVwgc)ßHaO&nr#󬦲l'3yxY}fdKJdARH9E}%TTҌS4<zbtXG٧WgB'WVD1T}gLbi[wǚS$B Lٹ#VLXC+;ݪd #+oIA{0]ceR(d֙F3$Bxt@V IғVm̴dE!)yJ>D*:!Zy1Mz/PBIf0 Ҏɸ; Gځ*z͹6HOI`)\NW~㠧£aITVߓMӬ$B'ctL&ݪ\Ylik>Wccyh)GՋ`Ji\1W݅JHrmL*w@ʡxѲHzCx /+΅cf%&B_$Gc&/ ɴ.>)=yV`pB_5B#:ʹv't,;fs8KUeD 0p1>$Bhg91jILJup6ꭕ7k6$?:L2zקb`};=R=fyq($&jkeMkoxs9["L +UB.t/MO0tx!Dn}~yLҿV]=2f^CQ_uyp%(I͹䈬!I-yBs95kIAQnԩ{Sѯp篧Gm2=d7R&Ӻ4M 54;=NGc2ncRt`AJxw&ӷ4p#BΣ)Óe6y0)%L^_׫rhe{-G^O9&%4j2Q/ +LAHiL"CɇvMå)30PfۿՂXa0)QqVgsIV^UB~l׋ Gސp3 L4RI9&M?V !$)n+Ye6\V0YO'j Sm,u^)dw>R CҠ/eO 5`2 j'#=%JXHPe9zTw?pf}iTnQntwR)OX"pO%tT&Ϗ]3)$7ePf[pr޸||l5pndғ+ĽH9zK6]mŤ zͿR8!>u=oD!h`EE}^:zO)vNVB%Ϩtg13nՅvkj,2Y'@]>';qQ)dzٳbT O? :wu\hvߟ#zٜl]CV rneu&w{LJw'R-FE?!R/DJrI$d<12,REV%U<7g:fg^d`bcꩶ[:+7>VΫq ҴP+}coVD0+ [uBKZ~ RyUs.++3UgD0:=/mCԁ*#iU}$]9e88 ?N!"::-_:kúXQG9zչ'Iy\8R*KƸ/!~Tk4NFIʞ.9])3#ByoL>{cRnn[n7V>)WKRQY#UzO?'s=Рg]duC. R> +'}nMty!׸/0y([v7t%OZ`bsI=P'L'ol3{%RJ }&|DQ5M,$)KBcuq\B銞SV'Ofw57'H#RΘs9'R/)Ȗ1BrTk,pY +}+;B(Ř ɯGE'ts+ mF\wO;KlTȒ_SOcf@=-M//:GnW?qPgE9oO%`^ xm{5Hܞ^Ĕ™M:'Z!2Wƶ4ro+nopEfDjtKГAbk&V=Bj%ܡHIc8gRchJ\#YYZi\\h<]R]Q_^vЮPv7>>i <]NlskT?'P5mIF@OU)xUaT}#F;B@ =~_ `rm,Z="]H ?jjR*~yT TI*{zԢ,HF +W!DdUb;<޵s[#Vۦ-Q|_緍Thwr+PKR*B@E` kR.Itiai^ArȥkP_ڃbDJl2ˣfgIrlX?gw>X<}"W +*e Oh)5ЋPŅ]lxh7&\B{ԭxhvRzE,Y0C>yF UJA)_~D7DAA$;)Q&%AGt)K^y4ν* o{MO8pr\r@x"Bqبrۑ]JƾИk7lJ){'@oJMNJ"\ Fc}IJӴq%M d* ,l(:KU+͌'ᏏGJ)23,I#kLZ 9:F-G'\Aθnqޒ`w.kY=ʆ7 ?>:ϕJ1+4V(*il"K!ʓ2G9.]*ӱU@)[r7]>cےuLDMbV [FQ )zeK W2|2& %n0I]4ukǢYNfh;Afbke2$op푮^J2\2޴ )HR>}yRE*oyd } iAbI_ :KUli +d4<@{d΍b}rJ4E7l;|@*w&$!ϧUke2t-:] +,!߼l]}~ =oz{֤X5Q$>eL)Lҹא/] +Og6*beC>7WH+#bX&8)rXu$|RGmkÛVlR޼lURՙ+ڑB#0UIEKأ9~,bԲ surX`,Pvx#Er TUUnMt)NOsT,pa]~C@0 蓉-#$Z|f—)E\%.rolϛ͂ / ߍbE2yL{L& "t{~@C"a(R +tRuf:NReؚ3CQJXkl cfS,hICc=u0_Wfk>knL1ז^O> ~Q'tz`'#W xV +t`O=?7F{Nvfowvv*QJ*0 +D?ޙa B J_$<z;i{wF#e={\&C[r!7&'kn¼~Ѻ{]2 @ *n{Q^Qw+eǔwT>~',U)+DBGbe!z/E"-|tʌWXbvF<6NHP&?pdrA[_Wm_ +5?&PF1J'3p|R]]9M]9LL2 Q +LrHP<ɤv4ΒV^ZYv?`vFRB(M(  +H4JoէX)Ϣ G)<Ʈ@C*p&̟\q7H&5UQ^Z^u-R)E7?A|^u60H%LϐORКr{$$A@$n|^v$zn₰WSo_Z[sSrdRޛ>||R +% +X3J*%0|,ϙ"g,39!+\JdR"NtgQ^ҊRlr?R)i'a,P * Jycq?DVI1? IM<(.-i[-gb\~{ ֟!ɥOZ:,Ø9{ٵJ:36pVݕII- o޾Ѳcݷ85kk,K(;9g' 8r[a/#<4+, +:VInI(o d^r@ԛ/{w?p_&4(eDRcёD>]+Xkdqj22y{6pdRw S^yK RE)10Kҟ(. E6L, bT!ЕLnTνe%U-V* [}iIX+ٯUBT&C86OʳDP1[]\/&ְUڪKKjn#2LvɩO%JzQΎ~.' τ9+RTDL.tdR>"V+[Wo__w7B/W3 O.9+Sl>t]ӉO"oOB|r +VNͪ^32 X)mP ?sbֺVf{D0/#o`7ΒVm_Z_~ BpSETTzaLtZv2Z)8Jq%S&I?IHd:A7.ɲG=Pkɳ)?(_;YDlgO_!;FJ**e' )2H& zӏz,T=Y]=+eQ/0g"cb|蛲IkF $!YrڪKK4.»5Jj;>i:':nA){;,nXepx}P<4MXyd@=K?IFmr[ŬUT=Nr I!ԡ +b(9qarJans=Iu f'-'Lu,QJ RtRJHEx<'*\aOpw@ӣe nhzuFa·-T_~M2I<L3+vm n a`Vx|€q*&L:t+/,C&MXeUH8mL^UI2IV9{5)uR +ƏA)(#nao$<2cIGG&/Zw$Q ھފ}!iD_~ϾzdÈ40ɴb)+2 dtd}wCxkW 1  >U ~lUH&6(^q10Po=&L_v|ș$+Aer@B6.䉳:/ Jy'Qʹ sx7o}'oLO sSao^<I) -2 +$lWS/`_wt7U6?J)p0g%z*u#"#eDy ڔן8ȈggnW4c[4R~zŰ:,,G L}ʳAHLBoHxVۗ~,9ʛ;`:A)10C|E"Edxvۭ +tbX:OZ` RFyxQh$TIcz78'a0y&'2Yd̟L/b]U/`_w[Q|$wYKRwEWp =NdcTWd@gBDHvrPXx^`aL?$I&Q`OnfY)*͎LL cz$GD<Rѕ۳dIⅉvkLn{yL2U+»=J$FwB! LouM_9[ƵR`#JA }IlVk^ݍ[[$<9Z; z>I)E߆Eܘ&LiÐ/Q/|e0A EߊH&~ȑq?, TUt<d`_3MG*&􏩧w +H\A=xraOjVDEI`NI&Ό8隧ϗ7E69t}y^&+tz“/޽%vp\ԧ">AnB.WC(yS&rt+YHJ W"U|C~KJǬwRŔ!3c[1 FdI2qǐxCo)Bi)LN0&%6$5KL#xG;n䟴T%m8<9%S4o6 I@Kq=ow;Gnۊhх|!`Jd}<v,Yl6I&ozIki[іp뺤u13O*QL#dI'LH;, o+R1 ;ϝ DDX| R&K!R]?y;i'|WKCr0X,>{;P7`Hdt~ìsVBF *`V7'98QN;&Uqk¤Th:0l.0Ϲm>h7JmTEHy R}ӿ_J9pIcT~B{Lh"axov% L"%˛:0Nܮ7Jm7XۊdJY>;)E{d ;L*;[0&|X$Hl٘LD'qYTjd]#mcw%R/oSa~/؉0+ ^&? +\CD}#IgJE>Bm'Rӆ"ixؐcχn' =B3]LItf-"`*E'GI)\R&'vؒNK)¤V3,L*> E}o|| +Dң[+lvu,ޒfZ˭+G_2T$fvS3DJUzDJF+7$; S2[+K}t]aBz1e m л~> R"eΙFc!έ|F W"%FI O)KHrڰ8:3Rƿ$ %R$Z㼩{W"=q t)pmyۊd@}zY:3JJ[F,qB&$Haos\7h=$%L&8ͤj߹4n1ȡ3)틤 % +n^_G,hZm|R0e"<9XiI$O.>j<3!R i^L LrД/15D6ĖmEl6uA]W6<=H"Hk!.I4yIܬEKLj6#k[F|r1 [C YI2ܟ!M]t-u:~lDAVtĥ3LMU߬JI瑒:vT +Q$EmHfDSɼ߽4Z&Q0"v%Ls|'瑒PJV4 h0yd…F e3LV[җZ.)Hm^sH=10 H^;ly,pg/B~\:Ôi| ٘F,& z BF +&H㑒#RʆBl, m+ +L`ڪlѠ6~TK''W"y0i%#gL襔AOI,g~et)AAII(kWu%2?X[E"\NHZ[Ѯ&QPs/Ƞ)_bɆ+Z%\yѿ^% cG_ I҆)щ7dDo·=D >V`%^n_&|l!#Of!!e +D'a?t1<|)zXZgz9x;$"%v)i)юec^$RӔ/7hJd]kUҳg}qOb&3IYJD~Fە30k1.zmE[_=.FZA<#VNɁ.g*|Rܨ=ާ&Ir}dEGwL~F4ƤD&sQ> &nl.]bj` Džؠvuf|c0~̈ Dh_ne6v/r+.& -BcO"N*HsVpIzQ.h% P@ Oq-ko&zcǛwy4;k5$wxPѰ@cl.7x[:qΙPJ0${p!YKVb1`= 5Z-0~),ȸgG)OEI)[K@#$|=+qPODo[kVf'g1sg-gf"p]N@w 3߈%-Y,p|Zyǂiy1ո*DIOu=tNZací( (8s '%$!@6/cAѲ*e}Y>Qc33gC)CB2de 2 ?eGneel LY(Q4:]rD *l= aϼ/_-t쯥,vAh,XM}ow#$D筫3y(% t0b3F+d/O8 &d3cAjBtl/hA;];ICJQJJ d©o-Tz*iʉXF:72'zڬvaf@eJ;}3R=L3/NFL>tZyZb*UVf/9!l{JL@). d]h +V$*#%RJsci$—0R.dL@&@@R+Q,8+δqh_=DXq(%8wr⧟L ڼj,zIQ6ǃj\kNA[fk$^rθ%rď?;s +2 h"V <44^WGúZU6v=JIF. +ẅ́c=M~_ghf]Sɷϩ`6SVOVd-6秋1}ᓈ/U# ??I}G> ;9G'#~,CڹI9=3 z+{ak?qz8gd%$}Ye喱CBN=sXlQOO"~ɫ#旗Y[>}OM ʫߌON 6L_=>~|'ޙOFLYO9ϙ`hg&W$m=4óI@A3+YLYyrAg;5MWځL"~6r+WԻEI0 KVs[ dE-irB ʿy?oGxzt/DhG╯O]Ͼ0&ѽsg#>|Ȩɿ?+V / cAeò1?7|M<\ݷ.I[GK3Sԭ7.J|7ux纇>?<{_}d)*n?&LC+YСLmp$x>}QҘe1LWe^9RgΈ7QdDGp#oU]t;w%ǂF 09IJO"~Jh(^_^Tfm34{ɞ~{xyiIR'k$4; $hY4J D|suIng=UW'#4&+qDO8YuH&UY.q|2ho,J,4҂ک]zTe{lڼOM5ZI'1G+a#5!aa@ʉ͔*ڢGhs'io K0Hr$>,ffQֲm[lE:>"KVF={jٕm (hql!!&$ +g8& Dỷ^/Z,iUJ|β-d@[I$ٿ̀Uո*bw'C7|B4<});B/R^`|2&V溳CI7Rr}Ew `]iiJ @_hF65'7leDőh|[)v9|a6'E̊X @hF I>l'(kYӝeO"=~͌h_VDK#-JZ $zf@lO&F[uΨ \|]T-V~ +$HOkidm'W'sY37%{HVЀT)R04+q`8AW'v_+owQ87+PUOKi 4k ybk:jO"1ƥh_FiEIwsgh@jh)+ T㪨UEKc rI`KЀTOkpʊSK-*|aJȜ7xLO}iz,iU.O|ƂmXmSuF>Er$SMQ\M&> +<}!jqj!!GuW'g!$”P'/\FϩeI+T=FZH3'H~6aF50t +J)H ®A]T*Cp&NlLnfUYT*V%6a50C00D?Փ+os9ؿAtjJ|LGq'l/-~zG7 0OhAW\m5-2V*Z<!Q=dF-V"`R!ZZͿ |;?E*80K2HGܲ,K̡x9Ulu,hOҰUEĵ.d쑭J (h5iI5˾:b-#0o#%E?+IFxl'Qw`4smH*3Ͽ9b#|9.Ī:Id .2}'lY; {oCEM+>#XJ=5k vi-:ӿl-ŗ^ao}^ty`$u-ž+fg+jZб>mHȢ+[wQ>j`"!jU.ZF'GeXM)p_0D[߉+vhh5ֳҵPZyhRUhs|_Wm(ًz%QOC)MMrЫcK3;>;|z2 vA' F;Ͽҳ*G@ΩV,|oakh9> nI4E##ɋD\[4s"%6 *Ap'6Mrg> ^L* uKg>9ڜOя>5,)^I^4sKj[EYӸJSՔ$2;A9+[:hZyt>#kIw.=5-3(zv||Wasrx8qYOIM$Uwъ -k)>%`tsg^. +{0y=k=+78\ɲE*'k_k>1+m;QOD= `7twKKj"T,)'t_S>)yvtp2 v*(lKj"Y+ldTmNwv*'q$me -znh)2ҵ8'VfE">|ڐI0&`@6,{IMd(X\_IO"`ƾa߉'D# `7) ^*R[US$qrأsm`?opW.I]D6rh*s'dJ\^LEgoĬQ솖bsB!΂& +=Sb#VS2H'?]/},6P. +w0iO6si"=[Դ-=ұ7'#_Gp[rHsē%^ lJR +$EFj-3>YM#D?Vx

`t ~9:X%I$i(%@A-#kY>?v|:H$'GG߉ eZ$@QӪ[_O٢+X(z uȅ333dC%H#{0C&iǽ& F,N>y=?})'~fso l?3tb]L$kI;:֙OfVTN|I"qn蓉D>g^mboz%HKnX9`yi[EY2WԔȨc~xLtrId?Βw;}lUנV3'kiJ4'g~/W.$.p}蓉DJ[A`Y/>Cz%QOP +C#-%4 l0VE>љxH$D#=>D += $AYH\4:襑SO|#܊⟞={G.\?}|ٿS+Kڸ'Kz%QOC)JJ6sµ,LT&)Ъ?n8dU%璤䓉D[EJb_yv.`ti- {7͂^z6eBC4G?㙙SHO>u|tr/}A`~ +s}tHOFBx7q!D5z[Z_ϊGG77?EI#^x:{i:<.! |2뭧 oc4Ό lf`̈'苪RJmݒ؀9Ćv~JժR/zҶ `!Y`se睱6Cד< 3 e+vPa>z^dPϦ5%JiB(.vnBn=4f]WyFd~Usd/0_OUOJu"aǰm$%[/MJ(1M*%H Z {X1Ԯ(e4}K1%.ghjR^6'Kj'!f+-ubY?GOb< +8TSsm֕$+F".P(. +Źڬ6:TQߵO"4TJ͕Wr'x(9$ IO= XN=? ++38B0 gS[=%;ˋ/qUb'D}$C*,=\C8/C1ԮԊ@;mx""5r tHoN ekk+k1r#@`>vt[#™ƨ'KfM d'S|%3YBWR/YCDk'(κh +@S8e1[ gY4UrgI(9+ʝ'%ItuKkK8ӤDtT0|vƐ8{bRI6eGX(Z9-A:E1/'|Ŵɲnw4e驒/tDyC=nzu ^<CO / QL#8K&<'A˰ßɋ$;)rOt:D姻e3}lߛDObh{@Rbxi"/žI)(*~]xAp=q S͸CfT>|{1~05$Ia6e?*/W5;glkJ,h,v(uP}J>0:x)[KUW:Rc}?)% + JZ$O|v؟ _ +P 3>o tC, U͂d7; V %gI${r5Tpi`ԓNߛDObZjwW,[\{S󥐏D|~H՗(/)K$ 0"'?nLv$Nv;U 5K$tpvx~Oe=)N#|=!%0#\ vF +sS0Hb<)V:o(Ic\&zb2|1$m$;āko`\}|0O%_߁RȧEr |'Puqn9dԜ;x@߇uZH?Jm K]T{I.;aCk(9 +ji4.;Nꌒi2:dm\xLd>v`n+̿>.ҟTQy$K!߼>^U+qGp)gQ9ݔw6' $惩ڝ ^f{w>Ki8` _~\j07Yf;0,u8l'u:kn 7)0k ;]cwՁEiUQS7b`ޒ*0{򹌽v.ԮOUK)6tۉ-XnF+6bTj&ٓC5S6{;/$_"U2lh/qsH}_  +-vY`+Iѩ"[pi4agi.uR1Nɬ[x_zBRamOv KjbgHvPJQImHoT'iWB?ZF|2.u/S(rc*'}JJvfT"xL_?;Hɂ6eEk nU[_NdQaUJZkшvw1qR +5mYlQlDne6$@ڡO?wd[:(Ԛyo5bxpmZ >ū +VkkWX 2.$<y =VCyY_)*=)$OwJRozj?D?@h|8և77_!xK}rBv6!f'up-0mA J~̀|%G||RWqTmήtkC%n'OJɕXB"ÉMRd|Or i/@#5<֊@/wy |rayxU6E)|/Od^msN̸RvIٙ^pN}I-נ nvTSST>rOZq ,|2J}WB)mVJ`ٞ)ia K c=>r 6qvHBgzf;&_%\ai/^3# {a5U{-铚d; $څu&(ڻ(\'u'Q5ݪ7j}($TPR -)w.G#ʛT&){xf LZeG>ӁVͱBY*kR 3]+U73k[)g]'{0v,6~ ASyZɺAF̞{ cmcS°/f@g~R*ӖZ]I]|ׂO*q|rQd*I7ʞr3\mV""2)vY3Jqq Vs~k}b9EM +dKz9A|X|/㬶#/ÀR*b}LԦVӄd.-uךxge#V϶# &O +.KwfZiS痧2.&>)=bxIǫv|'QMMJ)vZRp_cVn-" aLJ pZe&9 +B/Gne^;͓SufuG%A}C<*xKVߜ('5froo? b,*^uZ vš\>k'_27ɼ<ņ$xt{]Y)V“>ʜ D 8Ҏ<'gy'G&zʃp}0c7ӳDo]BGr "$\x7533> +olMze[nw hyɞI>j[IJ)J"`>enX +EZU%RܨCRe]`&Q0,Oo2L~r ?L8vVS>'"+=r!cTVPv D)/n_) +YʙJ* Vلfتy&R'=KWnH'EUvHD7YIpB0/ZpmU-)BǙг[I_xQAvX Sٵ&($U%cں8$Ϲcشݾ`M% &Iv/ɦj*R2MUjkތ1yH3̐tUsJB˵WGWߗs~x < "<{`8LVѢ)QV)U<}BSTG{XØ~!7J~pOsW֗dy%#Qqdd=a딈('lHɟpL/8t y7>S{&JMa$ )38qf R$~,]r@#,O>LM%~[Mp ~a'N +,gGCO֗$Ħ223؍{UQ0!"z^"eT*'TмM9%Tkժ $e:;__r'8j)Iԫ.]k#8O +ϓpC`:Tjϓu4-ZCIOKÅV7~fyuJoyR]eJsDx2O-vGض^DDY? pUΞ*b6IYq oe Ӳ|x9 7}tp΅ƻDJҴ%-4BDe<7JZsOټጭҵ8q $DAiB<8DϜ9lJ.fO'AP `h);ă\A%vy^;ʀ'J RUa2yr ^njtH_@JÃ8؏'{$~rH\3NH?)4ij,2 Y7Os%Ӌ A1d]-k|p"hQ6ɣTaQYVeUjNo2&I <]HIx2dZ$"]E9H fN-OişbJ|NW~1ӷbS.J_rBP.Def>]0ICkަ1td'h4Xzl)<OR#dy xD}V^v[ZE?MP""arO:%[*/bIr΅~Js}},(:A1@7}| ؃3nhҩ"jeU +cA + 4"E(@cC㝣2H!:ovj'+j' *'nb`rZb$"24RrqpUwL%@`_FJYAZG̺>- Iy *Waq;zGh9 @^ ;[qPAC`5OjZU6EU3]i&IW +PJPpL>L:_HIWi͊ +5U +{2-nt IHR2{r,҉B܀1`u s% L^IJwM./?O¦x6yp8"SgQw%aTB6P!J.ԘsFbJ'\ :b҅E&VR]z94x I(3 <)1 )[*nE u$Eg`CTȠMz1+b;jAeF]/~\0B^j-ڃqK)Td9; KյIFCaH*J?!*@v<·=˄ǮjsFӍڬ Y8|vG=EO@b/FѵL~"a2?h.+ ؕt~ }A)4N=05@vI x2[[M<&^9ӳ^:$u두l$,) \ijāa&F=64Մ>?A_xc$L)vK2bs7u x́'SQ?qoLՅEqD;p +4OҋcHJ{2cr2l'5)Ry<8ϡ"dAqx-,On!LPP` ɦTO\RFr!~F 'cA￙c. ݆cʇ_{;:1kڸ9G(j2fV-9iL7j. ޓو[[E '-D ePv|&oo8>3}=y/<2!/#ړgme_ +./g MA~5xR/Ynne@=c$5!“?iHfUφ8Ucl3]\cZ$<%cat3jؙx2b^HHH"dPejHkX5!\;hGЅ(jO45qd=sQ"y$~zfp3kVyD7%s3/iȜLn +B~ri~?5c2 $HCDaAř$""WW$uwjfvvڭڣfv-P rprk췻!NBw߫OP <}- O[|'O#e#g`RJ13C_^SlFI?}I8nf`N$|@ e,ydMUn$9K1|N=@~{ 太7$ŻI_2FB"l3~n@♨z2|Rا:Dx|r])O03[<+$xa0.uN3zގZk`QS}m>@,5:,kQ0P~"@#!񰊿 ^)\3g%݋N0O|?Z}1I +DPÁ2$BGFťs>7gO` 伍r_`bc RJX䨙m^ CWۘZ=u[͂\ mRJݦֶ̗́{CG>0P KqQ>UD .ҐstӢSV6 &a!0ZtЄ~wQ>$bUI`5`SJ`qmN(`فX{VF)y}U|RWT"< b?<ɾRꑙ-O,_2"ƼZYk>sa8 o +r+9g[9mj6FO&@FZ{->9_b uR +'TYXSpmx5t1۪Od%N?`jb9nyƎDwOe$o>9lBރT1S G%įNL&6'$;ۘXMY L+`" |2;[2}r 3ye -/1 1JY+v"X꫟vC0d-1K$0(\.Uiiڗt ĒeBߎ(i&©Y) UjL6E+Ep%L \!@}co1q쳢VLѥϸy-e>La 9;\  :fYJYC=i[IqK=&\CkZn%a|Ju>~W-m(SJTӼ غdÄDl.탄<' ί".2rA Quj2&jBWb̌}2d.p! vGZb0#~6z`^[<3-;iP0Gne䝒_D*(ֻ)2Rh-܆Of ۳ådWዄ7<r7x9KrPTY'~a\ުPY\zllfLt'v"<:Rn|BrKbƊ%3˔[_Dr*#B}ĩR_!/ +]bfi"p~}SL<'(%Dp)"`G~) MĬ5lkz9o'hHpoW|>"WyxbjZꁣڍ&X?B{O¬V'u~` zb3Ta0AC.B@.9ȱjIdªH bAJ' +|;d!I쓻b0[K家т>Uʑjۀ͚Khw+VN-=ƨ_SgM 23Le0/!׽ѫx$#}k.b+9@BRJ.O-cv{e ߛI3㓐-—>UJ`J +Z\z服`/~κMTQ)=p HoJ b!Orw?tTŇC"b3L-E!r[FCWI_g4d`}]yyjIIddV&m?Ttwb`vTJ،96!=i,Ҟ;ƨqi T/8L\KJ`s:=ީ'z PG>9PF +tC9O皇WI=f~"Xu>:63;;n3>Њ<"*%,Z-.;гv`Vrʈ__:\?HO9S[sKf^p)UDxɡ +ꑙ&5Ԩ]U.D*K&NWl3|M7呜OTTO-Fx?֢hG bm f;M)a%5̮$y-5{.@&A)W8üܝj=P+?vu܉pHʷ)Sdœ t ԿE{RV \ܧw(xXEX5 ~OBHO鑚/دۧ;Й1sZiW*AY+,i)jʃ" +< ‹ng:w7}v:ӝo;l _');-T[L)AGuD5KO   wQ@L0Rj$vϜ$ 긣dV_𹒚- #l5VOJܗhr*-:*LW\VA_x#?(fouʊglkԖVRp0 [1Hv}#eQF5 òGRf!C1 HvY CMƜoG-4ƿR9үx'MwL?! +veGT +^txZ`vIr@ 1P^A]t3snZ9zO*O>q*eɍOB,0LfZmq'SVuǖ֡&Rj= =aٳәqG}'O>w`&mI6d`nāRid!`KaS^x{x/c瀵_]=ٲŨpqÙ_pN:XG`z·uIwEr?0OadmjV2D炝CnE:r\IMO"&udV01wJfrc"<Ò6ZDT ĔW1eqN8{մW~~'U'ږ-/xA*hxKrӓ6UGR;@!dFg0 CK-w@5%&ГlJբ>aՏD f7&'Hi NF-iWI77]>RYiyEEqYM +s_Iǘ'JO⾑[q#%O#V\/HRDa)dfhjoeN[CBy9zRM)`w>/ %I LwzÖ ⪟9p_x;ieeHuJni]tEJ Яxő&4'E {BvhZWyڌWM.ozil2rw#> ;YpQuϪ+>&ܿۗM[1gt,1湐Tjג"ela`F-xJI$-d2'1OuHd(0LNeEnrow"jS<$e:K ? (1hNpxI2i)̥]oU+Mdoa 񻸄16>)a?R%]E8)€TI=XVd) %EJVXp.idڌЛL&^ɇ9Fx 2)Fv_|d#΀xcrs̵sXd`6ҟ)fMÊWl!g۴{RۤQ (H=߶:(m/ 6)-DS9H`OJ SĤ +)AdH LXZwyøEKЛL'jjE/ &)6*sę|,$CJ`v1Rk݄'%$zRK='1L2)+wO"JSVs$'IO҆I65Gd 2cnx'udV/8<4_ &5RZCDOrJ8kcY)tFlEA hT9mr^9MO6MM{O +'?K6H2$li0gmN:Bk"%& +X8rKfãÒ2-wsh9Ȓ U6!KR<<^B>aBIk  >Ʀ%W*aKkջ)h7'k'G x>2Â{f*v@ReK쮨L@43Lzj Jyd/nj[C-=/$~$+1N>ۯ+u>?1Է: Z)g,'S(XJYO7!JI_s6R:L\,I9n?'CVOMN)iVK` d3ZA@)gY7QʷtۓIԢa仇>Pܟ&\f4+ѝtj>iyIJrcH>' vvƨb|#~z"!)7O(5SycPJjteO9C5n@)CɢH䓞j5-8 +oH\6_?৖ +AEdR r+TOnגRTY%rwͅJI_O,^cId(ʕɞNM[0r3 v;wŁJI⌎<*D +-QO^g*J= \gyhTԕh$y(VC)C;V7wͅJIΰEg|䓥m$|2 eS$`LW)w~RC!c)eJO)[i_)I554<|2䧂!zIݭ3UY3`CR̒$igC(𔲙/oi}rkQ{~C'FOpj|OR4SsՆGk}ROSIVƝT?N+ʱ"Td/ojd畒<]}u(k _m@>YI@&CPnF:zL= nJ9 ؉~82Rc\ʏ5:fe _Nz߶bKK>ڐ}^ʻ=`LE) +ոͩ.;'sRrO^)s2t"CwUuŲ^cN譛g^p9H*XxhyILa55GO ڻZwE6УS(a Ԝ瓿jT 'Hrf= Pŕ&KwR1rعW)ê"ƽr%>'7h$4)*DjDEd@v,7L *%MLո} ,8'AT)ͅo|-fR)],E|2 u'|Hꁻd?F_P)Տ|lwɲw6UJ}Э&mK'i*>83.āe(0 ČWJFm3;ǝ#~}G\J)m-:Yt%8'O_ pLW1Aabx +%RqY(%m~ apink_)%II_)9N2?'Kl[* |2%i/ytTw)2R)eatV?ͻ$tPJ1֓Mm\Њt!܋f˚+ ^\өX2e +LyY&SJ27.H+*1; ܏7JIAѷ~3lGy'=O;kd $JĻ쓝 %f +K@) IcUR#O|\);i(d= pm\ ?5Sy[@_R铕:AbR +۩D֢vV)rmjhg%dKJ *ھ{ @3RTThyYi(/H wg}Ma餔9ZcF^槕R$]?~RM~d2}23RD{S+q.4, _k#e;l˚HX~?+'Tr}0}\`JIP%ftW3"$LD $jlN9~O"2{U7!TQ.AsS%ftŝ +% opV 􁧔RnǙ3T6(ja?!%QO\m&@*(m;Uaq(e |qC?VEuN?,Fc.>rs3LcaH*tԥa ^{2mέ L͕+/ɀՆLSftq_o'ϻ+J ekO>ד>^~oGy2wg |2H%dVy[kzhr)~_f;wՇ,:J +X\H)iN/wp'*>'0+O6d݂5sP"H$jIcR.fC3˱Lh`z֢݇TJV)AWed~/\qHRepU?}m F+`y C_ycc.#1r[2iN}5ՌeszِŃLTr8Fk?#d kn'@R[<04.o)|rS2FOvqs[[ .ᓣ器+5rRJ$ApRH(uВ_MYro_bK;z'G&:DGz`F)iNPm?!R\KJ0}8ߙQJ\d%sSҕ*I>92k`͔8p^|{\!y"?jR1JYgsy5TRS"繝zi<\H|rtBբw].;7E,'I-?^?d AYJ)3إ;Hm[O(#B)vn-(eHbi@t9o (e<^/ﰭ)xGE.߅]/Q U[>>֛ +9qD`dIyPJR%)?cY> ePЅh5n 2$򞬯*`j?(9_dXcyf=7jO@*d7HOv!kRd9n-ҷG^/Fxs:91@~6mwQ$03 =-)AJy%kY~ӺOŘ^6$* N lsUU8p /#_ 'Q~PJK'9v:>үuNj#<2rǷH#-Y~65 ϺF,!?<A$~R۹t#̆\.hǔOӌ +Z֛HRzbۙa[]{Tх$5myS:~ I)3 jRg<$'IK4@Cxg9մ9 |=Y9~?$[PbXh IuŨkZbJKj-5S$OyaadƦPT+j_ȏߋ^4w*q^<}yy]=ܾ/pDΈḙRRkA){)&3rϛN?O'$HFW#ZRfG?-XĒps(eƯ&[ZjBe{#~FF/aX?d;3J igQ~h$8ZR܆z Nn{RC3?>i'~A׆~-o Hy}ٜt6Ccwg=730nyOEd).{7?*:20>쟕'JFZ[SH3I+q~w؛{4r@"= zb-<r{ojڪ(,E)P nդřIJFJkH(-Yj!h"~0qDT|ӞkJ=U +lÆHFO=Ac7RNk%7F֕FWE%Ώy!EL)Cwa)K>˓&e= Q 2@(!$xPCgۏ)v؄4& ~!vپtR3XY0vэQ'gv4 Ő<%{kJ|H; Α{F03ΈԾEiM +hT`HN90/,9cka َqvЅEqH#uۀ,X;: R>R嬢p?|,c ד02󖢨T ?/: IF{rgkazX,E^-)Ja"ŜHF}):^W{)zZ\i|wmL +ifɍIp q!,iT*X6},I[G"c59G6@BOvV E#)qeȿb;$[/p'g"]8WvJ:HJ;5)z֧b3C"Ͽb[S +ӭ?2g+XiT\$$lR_0(LʠRu_Nt)Hȓw8Qw0uL ӾEu=x43ȋ8Dq #9䘱3˵_ʷl ,qe}>l)+K  +JEFYxGðZڼp6//g}C՘(Y0vf8'ILp<B*ZQYK×{A "HILeOw8?^:'Ӿ|EuPG;'Iq;6'QdvTɝ9~dmֿ,,&.I#:YvRRiJQ@1)1ɹYIJ GQ}SC6`ȋ8//ۚ2Gɍ # l0BUq2,qܐ?u&<N";^RHyDR&MTs"巆aΚ}({u12< ߟh1Oz98L + ՘ X>egQQLeNVH Po$dzHa#f#YdEB- ߛCC*ƈi/ ,S;3KuiQFqo(u/ '%h԰՗ۛ()F+eȿb;3 9tl`ܚo*KERXv~KRB^((Cօ]ey R1^lfj_RШ 0/,9ckȵ^|~@,S,DXF@A ʒ7,MH2©VL59^{-iiǮbEB%۞fhHb{r؞DVx,1e79L,]g%_' bO2O"<,}u}<8_"ߗiE޿VO:ug몴S2ރ(+/+=m(x&P=K쳔| TqtDܹZ)Uw3ĤLIkAQbd-!*a^\0ٯ64Πg眝G[1vplK*ef]#!گ +F|Oo6riwȐhsv{ɓN-߳e9,1 Kp5$Lh@֤l ?ehZ)_$񗃺Z'ъ3T`0醴*,޿6툢,1rj]dQnpW?R g=r`0E$+HĞ0og NߵACFؙc}4sȏ;{l[D] +7DH;~аLf +Sf 6D~^#eAqnuMx!ruA<(`W8[eWha dڂƤ=uN2,.ۙ@JH3s@_n#HŐ8*kZd:B0("(.3Fp@*))߃eނe5I K0A)^)fgk|1LC0R3Kl[A9,1^_e4YZi^I7QL)'e+c4"Lwƥe*YT$(77׏ "%z{ 끚KPAI%!&H9j R*wzR*E};S΢Hy97D/oN8Xx˒MOlkx76)O ָ/wҦr.8=)ʅ))=_ j(DD-I N+5qT{{xRֈ@M~e/҃*)OJX_raJ,ϼA>0e@9|ϖ%# `paXa6`N=x?3z6|IHiH +}!ORԤ{6XrK H~P.A^ +㨨%Dx`U@4nrEʙrh߳஻ Re0; F +sr KU+m)7{biƬw"X,wrI 3 ak')jB= D;`)T (?et@T +Hhe/ 斗5~,(YΡoOrkW8:<}g7GQ_ކuC4,AtI0RH)úlgŅ\DWXAIIQL%A8>2IXbe)^0"3iC4f +<&)j#H9`za>(jrٰ,*QjL6t4.~XDʷYѓf(*RJ6N(Er>pCC_k٪AX⼙l0lzUh"0}\@vZhz@|?M&oyH9`V_?2WȆig HiQt ]5^#Ð$=ا 3~ͧ)RCx;< >rw ^K}~F;S-ϰĆFљ`oLJ²)j){^(̐ RRԤ{̴ `>aVWUكqT,[Uo 8/(9)ZykUjzOI֢sJFQn "ay#_? "t94_s]0R4R"BnqD%H=NJ%;dʩ|@yUМGr~#R23D_G\*ᠨD9"j 3i>ib8 qRͲ&kaҡ8m3ug=r`vu&r_}X<o8 pΠHY [-SE][5 +Rv! 6&uW,3t9Fw*ʃ{ٰuK8C0p%<'[i>vw& +s.}93e(;=aÇ.4s@_5 ``V +Y\e0I:T'%ybH͌HٽTۄ<BHD{(jJTPR2ϓ†eF7TKp8I9?ɌO3LL9Г9zi#8οvwIxΜːV6j+5UWizjWEj6UwەY !!`\CUO"c}Z. !m`n1$ߙ, ig`~W3g,j.,Xh#&HE׽^,eD8٤>fugy5sR宺_y(iMF2lnL^UKa+;*[.2cڙ%j>TyYI SKcJg)exJ_7HJ +sZG~58cL2a~ɁeRZXa PҖՄ _"!1aRDgT.c 5!`f#Mt'$>0r`9-E* 9 M;6НH')ZL>oVs* +MȺ6v1zDR>Փ.1|(aKvX.(Xfj"C>1L@d"'Fփ(W+ +J|=jR ? ~cU>H[09Dڄ°fX·82ӫ蒋1vJw[I{-NKnp6D`6KNsKv9g{,Ťu +N8W5@/32WP-;E/jRF- ,RFŃ/Zmҙ _lox `cGvȹ!#M4.cg)p31R'c&SA_ Z>&)Oü<<^HJǓ/3+a^<@߲Q |MwuR߹@7`X˅n(i0")K=S'Zs떚po!)1LWtxC 0V-T-vseGPq)]Z@z&3_x3Rj3yYO‰߽H7`+Ii +Mn-MHx .b[k`&]Oz5^B뿓1̳Tu1̻Ik*i!%)/a3Sw@w!f6rݗy.(JlI-e1$O7LpEsߣ?8I^3 g`A)ڭ•T#ud3"0LKWvӓcL50m fHwq`H)[?f l&}x?gr97 Ai3giۏSwOAWUFu2dmb$xj55LS 3R XoT .1v9&H#l1 {*mD:Wn[3Pk.#eI^{?/w%kI[[f(@r8\ td`%}sO:*+++ŹI~ڞJ <<\pZas``]j/OBlQ+jgc~mc?Go;cֿI5F2Ɨ+VRDm=7M +^jRV͉ao6pj#MNb(ޟ ,sT;T\K=('HJ!!8JngDoi rǘ_u* > r;U:;Pg^\Kô'V>ܨ~{{-Lu0lm JDr!pOw +{DJУj1 o + 娟C_+gO'Z%;0$l$SㄘQJ6)N5@A˹ߐwi^`r9._d 24-g"/=pn6 c:) {7lC+?WYz7@W*CUFUu <@#K@>#sDYR}t*xB;0fzH9@[o'#FM|*7ѩ z.njWMJX:|cKjg̓DJ8;|h9JoSBb)X^K=0j{6p;{mo%Ga kki!5q! ;>KI)s$#iM^# ?Z1̳ſt_X,W"b&)T=ej뱺0j ̀Nj'%R[ŏKJ}aVHZ㗙vJ;=aVe)2}.Ul +΅s#%a-ƲJdeJY>UJYIɁIiVaӽK 0;oU0m)Uc6VcΟ8W4վZ`(LiKmɴIai0iPsؒm|M(%@6`1i2+r8@6͸ϻ+ɒ6vw֫]iw0aDJ*W3e09StM+}E[%djj+bilLY=<&>!Ϭ*cF%>X7ɃaXrj]YORu6fat +`鰼!o@U-@ʁLFV{[De%:BgO<><9P>=aҦ)"0lZw6{`U ++ԗ֒oxػ*b R*ŰJId>a0 Q00`RYç=0l'jY"o߃*j2Mhm`)Z !)؍d~q3$ɟ#ؘqtO6  þ3$Aw]`q"6AQ7Soq%jo~<˚e%ɇHiTr|n=@K(aX4ݻ6iI Y'vaؔ*Fp.c+mnG֊j%˂zWߛi"o338n='/;@JI̜0R e-?iI#0,M 4GheEfh$9Z|%|Z#zihǛ= z/ptDLq9iY!Z9s-誴ZZ!/ů Ib(\fOq=m~0 ıJB7E3YW4-V޺,ݿ z?pd=jo^O&wݭjQ]ptysYГ< ΢EٓqR{d OQ zY2 e>8wI2m$G#i lZ| +bFoUpvhRJ_ß<ǕmNyjȸ+M5*+rgC<=bb?&&ޔ,/ iķQ]3d̓kxa0J1+Ŋ:A]՟&jK@]+ |mm>9s3^,_(yYHĨt5}ؐD ++e]iEOyXfA0Ko0,}jݳnҐ2 9ُ ace(Սbt#h)EZ|tQ-_zh|w۰zsrIjZ|]GcW(;Gq(>f13ƄdΓpa5IJ$*/ &ia'mI aXGK1h^b,]~ּYx8(6T.f\ m:X-=FPbZ4>ѓI({ndaبIb0$- Ű za 4ԍNc̈ۦS˗Kõ? }ЮsQ14z4]rO%pZ ĸlPbИ)7'$Vkf=94tb=x-/#n ȟ#YaNMJ4n#݊q\Kj 7C{9'n6\:Kwe'cυu1&)#z=i/ZN)t?|0 Linw )Z^u 8`ؔ*FG8 ( ]ֈqu%ڌgi Zx1\jxC{|7IDb^ϹC#JI J|蘄FA\/%L`cRIEZ8P>;Ma.;nI g"&1,mb:4:.11hLF(9juW. Zط*pd};]9_t|\<\:`9]&a46q=ԞV{ɡ":HJ Ib0\&p.e} ıs H4R74mk_׹|_|9w~\Y.Уբlp$iFaS\%2mg;wתk8wkQaDQgT +>BxFgydI=i{?~>IEؠud~iZ#L˄>IR@y-= rVrzl/fa1+xr[/|/PHݗEo8x45OD??чD(<<|4&cEGNk(j|٦8)$(ta}i\TŨQb"auTIFA) x;T xl_q_[yN꾄bo[N(`^a!q8ч-nwkQ?ߑFF򤮘LKI<$,/rV|Ơ(j86gA'!ٮ0W% ܞhKŕxr|f\AO*s]oDZrPVԸ莡KɍyraO2L wkI%~bUYg͟c~?}5$ ?+e?@%*Eaq41sCC*-\V|kW? fwJ|mFZv(`(Gt#p("1&0 `R=cSb{¡(jxȴ|,F[֯uUٴ] p:?en\ʄ!W _soR&uWb-qY2"Flo.;XPBԋG &W:GE'%pU8_euјF8,c$oô݀hgbqrts[/)p9;Ǥ*V+ +#*T4{U1^\F@Ejݨ?xU<6|Ѩ'gЏ8+=JulY+IP]* Y4Ba]l FbُC U/}G9RK}Yې1btrЈ9]MPDT1Ӫ>rziE-@Rt:%ȐS}l2GňhdЍU=ǔe[B,t5{uo}w.J=WŁ&a DbD+@c܍oIb'YI +Orx_GȓR, %.4>"Jc,mZ +Ew~=|ts||f|~7>򵞖ګˢ[lFF0Уq0w87OjEL <^*)a2FuⰊ_ua(Ƹ3-* r1?%b7Lb2&;ʑ \Prտ~Xk)/z0$ Fc!DD^6w9t$(RyRK`I6Or}"OR+T`E`zЪ@X\7XG##uEqf(%o O$|Q1^KsFi[끐Wցح~xA4)"O03Ƀ<X<^8,>bU=R86gy>Lj 暧_؅dRܘnۀhteOn}sOY[_{uI)?SWsfHa̭rѭޣ4%&H%gIH`I*ODIqZIS.EQΟc>&ŃfЪ,Ŕs)gShZcV[0ä.!W +^iFrLj.ub0 +2FC1=tGHȓ'SSg 33GL0>v9RZ)9H̳̚zhPhԔXS[`/gSk4N:S1T,uTKݗEOD̍{~0!vƚgE'!3 3|3 }R|?]S…f@>)2HBn.Y%"V"yЍX.}3\%\5T#x+{ B:p0%n8=XO@ąHh^ȓXbR}qxVaS-9VDZgä:.U tAt1b7#aܤݬ+tP{r.5{u -eRG'ychcg\R8E%C'- +&Lr{1Sb$E8oS\>N)BBU~PJ4h[WuhҤp~iRPB;qqbqǹlv9>vHy$&  y?z~>me)+qe~j$RZFJѤSkL=x@)v*bhbbLCI@W+6"~رN6;\ +\8ꁋ3!OSk?'険QI}VBa&5{R<)mM{F-E)[JO + D!H _E*߮h$l4.4/Y*ɥpEKXQX%QLȎ'/~7"m(. +V4 +^~q|zh=ԊtT|bzR'7RJk}>z&[`߾]ѽZ9aExS*)&|/?SQi]=µI45 O$E@cfg`{G=7fJf.VFai%@B͔O(1f]7NŮňnİ5=0)}OJl3 MIaf+1){rf{y nV B1/ױ㿛=ٿP1X"G#]B} 1<VLdP\YM VM̓k)hߪ6ʓa#pn2L +oiГL&chbP\bf»ӈ?b|eoϴTDZFaP0h'ꑹ$Iؓ)bİM_s۶mRxR"0 Up0PYՉ&C© R~wh\JJfa&YV,ͣ14%'ErX9qx[英 {glHu:{64|mN>*F>hhYa*JIكIvGRiaC(7oieR=~̭F32icInTIFYӉڭTGvĎvkцweÑΏC&aN;0(n>ab~ wht24.w#T +=j&Zy 7*ѓ.Vޣf3.W$dɏR~,׈"*&=?BD?o gދtԅ]gB?σ YE[ҟdJK hD7bJjF2L.)~bL6e=pl Ё'C{򓥚T&Yn(Z65?Mz.~wΞ=1:=BsK Ђ!(.\Tbf(F ,zR$ I)ѓh'1\$yzTݎ'j_"j"pt! ~;U(qb2@/Qh%Q=TIYI=3F"a>`$Rf*ɛU[F;sf]z봽n*x[c=d8}؀0'=]Qa:S' +!%Ub#$FOI P0E)yٚ0O +wՀǕ/}e_t8C7#F Vj휜@O$`$ݪ6ГEC^ݩ5-SrcRZ7dSI4`x]}Hi#oV)#1II1^mXX[vRF>C8vXp3AmeO;j vx%0l}Oj +uI >hߪ6'1%rbQuwkV("͑Lpɦ 09nt:),G:ݶqɾ+^;RCðuٺQ~~IZ|_ 1j[H!F^P6.u֬lRKֵ>12m{Ntuϱ%qr#ݝ=T'%X-Ap|{>"nyIꪢP(IQ&u:K`5ׄy)goX#!, n4O3FJaF؀1*|L`lq7v !GolN/[Xƈ[v#c}-) +eYJ'P2)v~MMsc5 sI,b>De?(lH\v8oq42֡.pW$VJޚ˼[K].pˉN.-Jp:.}Q(T}ZT +%쓂Ї5c*%R FF3I"LjC悈@2%%zZS-Y$+㕜az[Eǔ6v'"ީ-CBY{J'5*P2IYϚ[jRw7.]$g+#Y?1 LҀۦ_ݹd+F.;EVI&fE.c}|*`JR$࿌ty&"ɘ:)GPֆRI_a}RC{A\#PhRۜBg +_33̭f"&l_H&MwBhJo{^+HOPؕ>N\Q薶cu}? PP6'Eut Q*UBYP("lă|R1n41')wobC*UmȎ-CrS +)8[\ب+&|l  M!hO'qK]jH*P(I:g:FFTj~mpHR%z掯}h/̚쓒;du|R;kgz+eikwL,p/s d2pcn6klx=J-Lcc3MP@t2$yR({d=!5*B^UUQYRR1mٽ~G^<{̟|mgY,^AffQ5*yS(_FEtn%(&Lv!l2EgUq`j(\Iۢkbٞ.d- l:hr'"ީe%A'Eu $qHryEeٖe堔ۿ\g=z虧BˉglhKӖco*` /F=nW]9szv) *z 7A#L2۠Q_sL&+Kqo[_ّўNH:R/zt>vt%B5Bi,|PGRV(UUe[*|`cowO + r ADPoK8 I(φRedЭ̤yR rŗ^.F&k6|nq#R&ȨF~Q*|LA XX8o]Uet-S~|pkC2lsMǥ/P +(:F4BU] ƀF* ޯ?xgק;p} +8ǃ@B=d9a®36&w֬>Iٸd5Ks̭fuMm!X4>!pprl"&C6l|!:>?[tKNuOT6:랈xh$&b7)BG٣F#Q]UrR Vco{/'^=fwIm( b!S[ωWsn]*JhOf*Rr7oi/p3!!܀B +$$.VH+jsݴ$!^ ,4cO$>Ӻ)Hۍ$mzb+ϙuv$c;Gq{k_w9{*ֹsm6.e۲F6{1N&CxLjƬ1R+Dp GD _fS0%A,O ᓐɎ';N8ucǎ=zȑc>j~ ׿~zgu/5HHbih{hRY#caÝJ@&=頺(;\e(eil3DLUC#7|YgS>Ҋҵ>m![5UUu 녈 +,d"0 0&y9`-Ǽ!ˆ&Rp{GYNԯ3|䓟z)|{?zo]u:Oކ.!샓q1-Fuo|:q>ol3t 7\Ds$%+]UX%*̾zs'8NFWR=np\ZJ +PsmA?!3Ҙ~3C!۵$$ŸG'vr{_k (%& {v9 H0r?aL1ƬQ"65OmE8!En?0H&wK赑Fw?{FP?G"'3thq4RH@%< %mZp<aUPQVַ}1'fsLq\Qq0sܻ 44R\ر4Yu8d쳩Р\aFwSl*]#$ggq4< ?7 +uwcݽ\wқxZ|J^:/A0I8+0IfPC H#}1̵k\,#}9F*&TI[iwQ^?6@W[ɤ"U8_jhCoE$.ؓ=ǾʇAͦBŨWBaRPՉzxp>Z<Ơ"ki2YKQ"|Jٶ|9\2|Uτ›Wᒱ<2i߹`oh>FR=Qīy9 YW +pp*`z<&9Lj-}dr0\C d&)d"lc݄k^^sJ.YX#J l%DXיcu}mf9$~VTU52i\+FP;eF^9raKXe[hbO7.KG1ʂfzzDp4cA3 +M Zx2%d?3<]vK r>ak3Xrf%êG?$%r!!/J VEN\6c+PQHչ?H^@-yL%`2/i "ZDWJfS\r 96ܑ3Í`!0(qت2nRCRc%?ﻥnʹfضT^g3i~#l"RJnS'Tpa$ ,cۈb.WuxRi5!~5/M- +(,⠨8 dk{Wp^Uk}eiH6H,G:g"Ckg[9n9<\Ӆ^B,M$$Y}Э&l'DmP-XgR6ǰǪ0IC#_k?rvw؋;ZGmFRs1USG]XΦP1.gu%JCsh!nIr1vI biẆ=1z%ç{;WMdwE{O&!ﺤn & Q{ ᐤT 'E[o؍aV+UZR"" i6 NHJJp`sN>b'Ƙd}0vubh;{#UbgVV$ò?닳[9gK2ެRIҰG}d%^awXgڌT.yNG!b',*!㉓AR ,eY_dl5:*Yq&:c4/][J5[[H6Bp%^2c,I]uIM_B q񋠎6R~p' 8M/:xEnߝ<H2\RW2aFanfj lC|bR^4]^-hi^} ^W0$ |M#ߘ +s' w a/f8 +?|"tABeQF#m4^D8oMӋ 8A #1 ab/3.,rj}>Q6Y6qx fgan`˽2%w`лc}q<#I[[HښTqhN%y-'(v s0)EXVP<o9H"3`.'yoS)-lkT5+QL;(޸OڲFX6j&Xk,k`K IX3Q'd,_H M:ܽ=y|M÷'˪hH +"Щ$KyǒP=ԖeSլ{j{R2L|}qRO~V +XF6UMNJ$Iӭ-‹Yθo=^?=$^z›OnHAO',yr{Ԑw#2lE3R2dw'YC6JcL VNLD[ޠbQI.kL c$IqSn0B_ѓKK(Ey@vsLxf$uH/k)zw|f6sZ𲚎7)ɳg}u>QL#tu^F% wV[y;턩!<dsW !#1kA V( pT2Źp'] F͙., jMjng;- +/`n3vYZTu+jgwya6WSbF fHJ Z|_2 FE8`nT-e1> +S%̻Յd͊3*eϖeSclj'-U]VcEscqpZx<<%]9y1CM'$ez0OOmmà?L#cwπ*!Z2.*aUұFVf,y W\nVbcin{Eظ+[PW;%{ʥ*2tM491bB$v Ꭾ+5d!"0EXh(kfCrZD8#K`F&t%6E}6d:R2;UƴR}[U닳em'/{)+SI6aWgIhR fqy7'-]_$;aݴ ;2C<-rA ,RѐlDZOM# G3[fk lj{E- '<0 <7] ,?fziWSbCP_H$ rJC^^{9uw)IR8NF8֬DJT3ļZ(jT;5ڲWXVIv  c.IZ- +H1WI+=7 +"dGoO8($y4`0c?Y^&jdђFgeT6jV܅e2l Nfbk ɭ`P%kg]>/qWIضh6) t MRR^'#$u``>lGpf1)h2 |r3*2#C([N^-`˽-p@9b$L)KZ^BsWz着ՔؕS EXM <{𭳷xc0 +0"ꌭSIںsgtЊ Ll,5liA$oZ&<ZIYxIV@tI=A||"8򝺥T !#1DgU*` ېZLF*pOK J,|ǚ_[HnfS;ypxWSkV%qVGs%ɾV/Hy[Dl'AO;i=:ACb0al#KUrkmasCjϖ8/r%78_U7ڶ3hՋJ[cje怱f7؎RloY{osƬc_(@8o}m \;3 0h]34_ +뻉9'+||Ig5ȓr6I`Ħd* ճ뽳_wO^w@`0 w?[H$H#`J&R2%xYVB^X%,z +&\eb׀&`ͿX_Ƙo#b\CގJN'T 8Rҹjq0RnE\.$۳ZV.x%גߠ+ڶ젫To.[~Tg I+|kKpBL^(wѮ:A%TϏ{zM.L´Ur4d f`B)1pH 0e [0͵B*^DWfkIzt&]od^ʬQoL0YA h +X%_z7HJC }H{_|raf8! f0#~-5ogj˰2X9˹R6*%%d+z>ԼTtR Q53 9ҜAL[J_wݧr UvUUv鯺g$O;`0gv.,=Z Zv`Y2t$"–p&[DWf ϟR +.-mSc9sNRD nx[<@Fx^@=֒.k0RN'7?%nҋhzG0 fC)mpH6euA'-LZ>R0[H9ğ-$7z*vlsW1[(Z wZPa/y@OGoH8#OʷF#dCb˧.`0 YӼZ*wgCHm.߲e%lObz}ް^HçL|yMJMR*y3I0Rs۔#~b~!S+]Wj"#1-Hk A j6VIGZjQe!ؒlf7uaK`Y/$KJ&|K.%#X_`2d}0W}i}`&]W8rard?{~} `0:ҬXNGVf@S!!m[CJ& +n \r^¾SxdVZh^ _}H<)*$89C?## 'gwϛx.d$\/⇔۲k$$2{}9@˹R6M%SKaK fd5ՐUԥՈp5_N +#mq<  p`-$[7vL0`07XgAga U'HQA*JGG$RǼo@tǁ>y%F?$i;/Q!}B}fH [(MR=jAy}u.)T,,i-Us9_uፐێQ~FUݖs:@mudW#F^Ģ\ l+W`Qg I y (!ܔeZ 2 _(OA}G~mqܧC?z8#sdϫ|Ey~2! 3@R_,#0XWfr:Pm}[⸞O"3bCHKnƵ鼃-< <[tx~O!ECb0P4Eq:\FLAvu. Bey>kx={ɧ^ lc}yGMz\H PJ,=RƸtfPnidb0Tm쀴6({P.:HqNzl8lyh8@l۠#t§ |u!^1B6,7 6*t`%TUsqqsZ#;*9P$IzNp㡏N(L(ޡ gGoEFb0̇3PYf>[e"fQp}d"t`>q=#?Fnp㡟'py`]vOX koVqI( &&u ^K0@l b^۹uhc;׶4J+lC$Mz5Z'- Ǒ EQW56T@R]v*9lDʏd~VpK%U|nRlrg^2uL#)'(BNrv04gƱl %JL[[,)+n,E;B(/pc긿cl}mlss,1j|Iu)^75iN2$EQu~qΣTr۳`rtKg)1~ab%*.j`>Rkթ0Xr lWb=9Bt}l㞎 ȐBE8FÕD&&V.׵S;#>4_ EQԅ9|$= @聭 -"rͼs1dE3bD v*n5!̳?:5 ݻ$v{i,=F2’+d6N >nzw+v=WqhJo꣎i[H(Ҏ7H wI ASj.S1N7`M7TsbJ_ƕ6# +!Ü.Vimt«~P"Sg]C(]*yg?'Ckr晷,eB%e_ԩii@UʴY &X.n,/l0PP0Ca+.;zgu-,:b߄({gE؞((Tv&tMߑ̓~d;8A#D:GRUbpn5ەv긜RapI0&|hs}G-*"zƝ= JqnL^An5ԏ((TҷgW.&\[,/߄btb1>y@g-65ki8<DT =%; b h\1떹R+bkYJO)=*:REQEQ,Ym=z ms&< /.\2”۱>mJʱ:򆣇;:\ns3>4nһVL%cS3 +EQEQuFLXBʊ0&;|UnL2|TOH*1RbiQEQEQ0GOvcu)((9C7y(JREQEQE` endstream endobj 11 0 obj [/ICCBased 19 0 R] endobj 18 0 obj <>/Filter/FlateDecode/Height 947/Intent/RelativeColorimetric/Length 20505/Name/X/Subtype/Image/Type/XObject/Width 880>>stream +H pT/!&_aBA+ʄbJZDhjhb"SlSF[qvBP +P)-7Bd I&;=&_{{|!;Lsw9W=!ግPڇ#=^v^fV$Ax 6G<=\ k˰n3,- gGX+)G`{[-6t 8W{17# 8nz GJyH&AwB tTV \D06ns'Ab G.⬊FI8 dw AZ-hV7%\( "~,z2YlKA bYqM V&u8minO1"`y;A^[7`:58vԆw^Z"VPQ $A.c'nnJm4RD,8>F75RI8; pћ"AрMV7Q#(Ag^PD ,=A$ۂXtSADXtSZM#%A} '&3>A$ u]NgO Z,;ȘuS+\$Aǥ|Z⚠nƩnc; 6,n>DACƔaw 0CKDSDr * FVFw w? GM!Unٕ#A݊a2, CŮ~ :vW74g58hr׹Z܉&j,v&pu$A\ t_7%܆4R8g ?#n#N{1L9# KM?)Txԯm4Q eS/1uC&n>v3>'$x i} RBsNOuSnk GH1>pRfDݦJuS­LOEne9< +]J{X/-0GOKK;ֺW̷Baפ =R2XG8ciLaԾG\},Rԗ)2NZʄs$\Ed\1f9t]RE?%H7%$\QO>KUsj Á]Sb}X & q kR"D-JS2sO}Uj9(%INtSsR"=Hla|9.X@S0 ~}}?X;8bcG[ʇ +|f|k|njMV Ϙ0"# F‘~$K֦F`__C KsM]f[:pfMeVtmdkP!(2K g\^t4饝T7HAھHL%}eu]EȨ;\߸`@7s%\X:]{x~'a,Z=lcx⼑\†n Nd{pnqY8ҕ2rr(Km{i]ذ xx;?GuGƼ#Mo' _qScm5k?bަTpt57mbtSLg ("dw2{J0ۅ}.6DŽ'<20äFFD@pa=x]aO) 've_ pPY+EЦI7H8cdo x".@״lᛇUrB7mDy=D yrEcU'tf<'_21uMq!~pz7Gqc:{loP%ZH:kծJp_ky6\ME}ߍrAp3j Nk˃iqV(8dݔp$\Bk9~{6& %dngZ8A(S7WSdZg AZ=$7`&&X护M5*~Z${S:ƎVBLJ`Y+TH$\gR|%1닶43hXz4J w @(_R `_ g,]ƜrlY.8.lg;jȾX8w޸`oO:ݔpZDE-#*5D^A;LI56(VX 2"sAVãTl1qVx`L2% 4MTદpuwVUn2"`Cpٯ+ sAGETdTQXRP#m5)hՖg}Ԉ $!)f,Q*>ZEB ַG0ujEq@{usIg0ufs_7ZY_z_W9.Or?KSy/?X?'[Ư +q'+l魌~_$ۘ\zFܠ=F_.LkQG+Y I1ӠMå;o]syk.Z5׽FĖ .?Ǐ}Q$^k0p2E~L4Z ^b7k'7-Xx]#]Z\)߽+k"IJj~ނU7:>"p3{+P֬&Y}4cI,ҝ~eݣV_y{&!DqK;]F>pqO׳UQ+fRn r[mEeZv7a񺷛p^k͖wpR{YP^:y-[W!=AvZk^w7nquEٍ+y*+E/lͼn=XN|qڏۏ}kO*lXZu^{Nj7hk k!9n>JTO2A: 8A ߆xm8 Wgqys޾;[Y5kmx] ~G7K~ t"\ZjU\^y[sΜ ~lm}r~̜~=oT*ZKc2 J +에T{aku)\`f+Qg>q,^yײjv}5}_CC9?׍5py¸<{{n9ZFp699%D@7˚qDuX7MA +0PGDrz-oD]][,Sghۃ,o)ZOXSK[j_Md,o) =doٽ]w:I߭Vym: NQ)#5Pya:1Mda +0D3ܩIF) rv7u2Vysr5NZ_kWG,7.W1n*I'cT/#^ "DA7Yy#H^GN(JPysRn@ͽZWm$Bd8bnPy!X,o*%`b ͵vaDm9*k`U74\U䷌*G`R 7{~oỲIFAJYAj'c*ay#Pvs$^e#'$;#%#7Btu /]PY(n6 }j2%5JoN3o*(SQZ@{`%(n~̼*8_$75vQ89m}3e>F7 ȼ2v3NzeVoNƔ7?NFbW6]l' ׍H)Jt!N7i_o}2M\FtIXV:}w7Q{ER*2(D8d,#YV0 'n u vqﰼ8&2M7ew"ER"IuSQ{9'Jt շ틉@;7•#%,o,Hm7as^N~FNzp┷H~2(k'ÔH5 N;Ɉ&ˊD,o,0[$'o$-#4i'c`ck9-#4O N27BVֹ0qs!NokHv2Q{p.$Zb*7'?`y#`y +Nyʼ0 Nz$ 7k'{7uv!D P´vYsv.Jy3wh9#5P%(-+y#d^oɡ7B"Cd\¼2k'qIYiT!J}q#d!7'a/:$nVަ4A7'FٯX2>c:$Ok +a "TZKX3@-DL~sCBؚJRa#jdSaSB=Pj~>?7}uݼH:ӕ8Ps%!kQ}լ'0@5s7y1zޚ: ym184hR3 ̷݃]0@5O u |d TsZB tS%dN=ycՌyh :z˨oy:I9jJym TFM~V /ɡ@N#bnױMJo St>Ղ9G|hFqK>IEou_yvX' ]0@7[Trߦc44 Is(C= T79-fo1rq},*Z Я ଔIۤ t|b- A|i{o畤79ϑ(rq@3;z(e7M>)Y'QynAy: kN`rb$z + 6 ؿɨEqWMz -AYtQNÎz:wEny gI/#Mɯ7Hq|qAJn$ʅ "o47e !en&3_$Cr[Zd3 RQVtJş y8no ~IئɋݧA"<UnƘ8MsљGm gSD;[߳8s GUd'.z$H>#+?G&/j|_KbyBg*y3]p8r T6mܭϝ8st9 skw?"cb4G1k[[[i-rgClPN^P}ŝ A-)Kt[yS2Dl;/#7\QW{!cA2fSߠ*'<A)QB^AOʓj{_l*9u3Z0694zKɈ=kԦkgX5œC/Duj+du#j{\mT W5,\BBfTuնr5Uˮ&,ZcjnEqD5򑩕\F>>qPS,*@n[1⠆+Uyk8*;傥's$d#RS.[T 9Oᅡ 56܈iOEqin5n:9 :b .lq6rX*! 75F0h3X*!/eu89~2*#6}ن~agoqԼ 8LޛUF;2ʼX>Y0R\£*C# ~}1@)kȄARl)U8t8ڏ(oEi`9sH ay|AS#3'yuwѼRR8^5xo.}?Y9GH }C/>?f69^ȣ5HWPNZ,2χP7 Uk]R'*_U|>{r Ns7RUVT J1Em(-'P6Κ#7%6FO%ɏrsλre֐e}^ݿŵQ,rm[%Hogqn 9U$Mw X~LI&M{.@;*BR"J _-e Ax!mX렯{.z?Xo˛t>g\q WIge9frBnI  Drpx+p77A,w41ތ Kc-6{3 dr+nY1@bUJ?ފYf9M2Vj_}*oR[vz[ `9N}n2}ox,7Ie;K{nR=θJBd^`rΫ"8<v{u.0me`BkX!8}x H3Cn  wzop e#8K!7*K%\X~@[AH}xe 8scs nyv6Pu fJ}WZ;I R@?G?| WJPѡ?z`A,oEoEps8zWhPyEFTj#wŕ!|o){n9@?O%ߙiͷr +pNUo1cpMCp?!;24 J9o[o 8)ur> 8ЩʏG7$8\)An^F=3u|lRbz=,p%s[ p}h{3t~7P$翛h{Ki1rM,oMbqEkoM/gi\ko e+1@[fha7Dݑ&5IPE8sְ x91JhiXGQ;AM.Wo ˹x7[J2byCs%Tz?X^]_417CcN9>$%C3W)B9fbo xD՛+N`R9`LυRd7P4w: J AIz&8CN7PJzMo =@+٥xz3t&PuuB0@-'-gi$.1@-'F˥X\JS7)JU54xoTWʢ-11@/ mn*G2 r斆L^pk鳄%s-2Ny(%-PR;: 7tHr]Ku'羷=Io*Z[L.;0@19Ul[B\xèЁK7P󰼷}DeP&-gh1fsK h e)L_o1@7#d5ԕw4}`~Yq?3;33n(Mj6J1zAŠ5P\`$1i0FBQ"4 mѴTUD:ҨW +5|k҆@1J NPlgƶfҟMTo8NV>P'z  y(ɘos|l]k^.dRgsНӟl Ut*P9bf p\&Q߰c3K"^ʉZK%ԛGk^T "zo>8nT2}Ǡ<7,$پ|Ypɸ͠RƛppgH}{%Uo9y/YkZ9T^gXaYqdQ+5e>(HOnorT,8CYR^LHꅽkzϦP: I0Mȏ\552\)uIheCkW5ToL2.fp=@3 '&r!M)4fQE655[\-G\Ћ{ZѪW +0N-8$_])yts\YOlӉX4bBoޅ tPp ˛…\sZ#N˚5[6iHom^=vh\gSr#7Г\`|tPߗQ߄VMc=-4$aykh؟uP څu4%ݔ|`Yͷě=J=>KzwR1j5L҄~[V\rg ;pmU +tR͡Ƴjlor7|HR̦z 7#ۍ@1J"9!wc,w{{j6PjޛGkNcQ[SԪloOu'70jb.7}{s~L4NZ)͡1IB>5[˺(Ǒ@sB.|m{si٫X'_yoDwb+H>Ų-9 +2/jyi$!C|.M{{ , }{si 7-"zt+XC|>mo.5:h.E<ձ7օj!'3x9܃ Grxu8{r}Ft#7G닩t<u!%ߑi,eAT.¥=_Kn$Hհ7J9#l>״7>2CIǣ_!q \>d\ͣx|ojV QuG1]{NU:3u]oy"7K7=i`bxSmIzhٛCu ` ]y$E򹛂}-I#B٭ш_o.M$XFoڝo݌|I06O5ͣ![$źiNϳ $X'Ro`FXGt!' #ycoN|HvK_~4w^62[o>8>ަ]oj˝^?NQȣ`Gp٩6kכKzIv^\* l$X4m^֬7Do`!ϔHͥI`%: V^9 @qi+X'Nt>7ݤUo.Eo`)՛GeMJx[ ެF>_1[uǿ A5lMeH:f` C.50ӦJij>d+ְrRqfP@&E~߷﹵\}=:sޯk'ΖZޑͦRR +X2q etӴ"ݓ +H9{I5+H7*Po|s -=z +N욾!yۙ7G~2o(1Ņ-ۅ U]Ϸ_tPZ;2"˻dPf^OZޡ%#UC3AW{2ZrC}آqN̼nY"9so۰A$B-Y(5Oe)D1o(7)FoLZMo(7w.Fo!詺ϷHF.aPr;[Ho(9?,Dov^>o) +qOƲJ~/(ȾUzC~R_9PvNɿXXμBkG ׬r +My9 +䝛W +꿖9-gPAwo7Tg["W*k3r-v&_HZ4&"Z{+Dʼ".38{;m`˲E*#aT~E2'pN:*+8;'傼}8'Q)^9CS|K7Tӭ%HF?d;P)N%rk*[fqtJd'Tw[,]XIo } .C?7 n>\PQ($ rNlkI߂d>$~g7TWP +ˇ6ͩߟB=$1(#ѴnҜtO*i=.71o'upL{y4]'|βhzĞG .Y=:$&KaLn٭0YpL9 pB,+kUpW](y"D?-qet@x<({aހt^erӳn2zށ{+[3А +(x@-Sz506{xgF?PP9"Q].Lpe۵g +ƣ .3ug[,< ӧ -V08]55刭4O镅Hj+ h x],ݥg0OXl\6 ˔AzK& Ɉ8(lf\"s8(Ƭs3 .3p@c^w? Pp|.<M8|DE88+'\"'=>}`E24tۧn{M9}dB}|? +PxqK~ h.][ '_Z` 8n$2yzZ`\u\$О5pe} ;]p|;I92'\,=qaT"YJo@>;b9q?prrEj^p@R7\8UNCL{ހV8XFhEpc߀<]w\"fY.D UNIȜW(681_⎂d}Z;[7\"(?&.u7Ʀk~o46X2Ercɸk_\,cW2p@E҂79[8]m U\"S_7}ܶ+RkkF,ڔNܺSǶ5q5舷py[Ge,$83VܫWgE-w%ЩwIZmfTzT1ӂki_Z6 +#Q˙AC?3 +"{QiL- s@7V&7;WǞq̓;Np@wgܚkM'.1孢k\\$=KlT\"6qP uFWc}KnL{^pjYV܋osd#XKȰ v@մ@ .8 +6qs-aJR_E!([>)ɰwȆAy9d 39p96J ^_ޤ7{}ݮNi еjz{ZyNM-[8K^׾jj&WAyIz{ӵSZ-)9(_QUw?啘 +$AQ#+X +>x4 "2h;NA* +% a)Pa HA) ;gY{w?;ݻ;{o4Hz%"ADo)E$8P"HH $9Lo8T98I"/!S9R{K[# #xVVX)Aȏ| NUl@p Po&vPfKo|& ]`M~.48qmX͡BQreI(6Lli-c%ob)IJ)70qDPQvm{uJ:7\* 6b 1X_5؁ L4X vb3{#$@D9Z}hp9L +8'%& {PnE$"4)m778&Lwnoepxo%?L,pn\㨷1Ln+5qtA7X]6%xPrɋ `(9gwre';ZvQ.Yt^T$ m7 +O!_k#5ݯ4cH $JIz j{.i&-y1%L.Vbp=ymAlzov>BQrEP̌"Eme^M?#K\ `4`z8YtЪ2:ʑn/qHw7&yU]T]5)ly=5HN\o'`9rsGn&=/l+洄nYMjJc!YC{쒀,J(`b__4߀vMdd`U,{7aZ~ҏ1r~En$7Ħ7ʯ<൦!x\oou~lQTlaboқ|La(L(pݳ2EFӑ-z]\ш`b;$B^4yV}["l{ e+Oã1 *-{#$@@Szj.l 6a䬮TΔa}ܫ)rQ i"eͿ?Ckĭ4z\*ʡۻgo`pSEo>gz>iJbӄzJ|1.ڛGS:qdT.D64b 6lκpy $Ey&ZI,OMUXjL6rw&ܖͻY{tL..]IyJ +sN/ߗs) K.|"T= Kz3H=7t}wjjC,"[_)ZzD%E_/bL!8Ӂ)R&x-wޏ/0rރn ʜT$2fRUWY.{j0y *674[*%gP[qlo.y &}XIFqcҡ53u tFZYE?W{>,Ɇ&q h!7!6Mh1/ԋE%&*z?M&yI]^ TUoeAP6g.$HlΉEy:i NF6{jwb[B?+fA4D{7bk}RLlN?TEgkaad \` Fr+b"ٶ!k=UŦ euɶ/@6UqM8`} ?Mcp̟@Qr3:-f:^$gvcVL.5`,;q(%Dr)g `yٔPZ fTY.3mqYH\[c)7-w'8W&?,Me"{7c Uqb*7(BT_|fUo)d"SVJ.0q!c~^6h D62t!Z4L $z3HE&Tp +Gmd aҲrJVʂ>l̘ n/Jj{Sư5B҇d[$g$;|gp\-AW-<>nT="8 ydt|C}aLÀkA܄3Q1.r?)xұ[V +h3 d t"=T͖ '[wFeK!) R6V +49{Yx} -m?zΛPQ;Fvw(97uUK6S7M^uJ.*cGׄ .$SF\ˌ_kD0;$c .gPBTs1h3NrMACD2<'jp!eOd(AA7cM(/Va*g_i)Wc֜c(k'ە(KԮzy$ 򏭢3C@$0?!vi! +%QSE@EXݒ?lVC]A Eإ +*hE`/ymiiݖ7ܙs?sWO.X}[2t6BmE`6NBnn l@) '?X>Y e ^#S?YB\ܑnƒskl_m^FPB-/+?faP!E2 .dup}R-Ԩg + Y1T.k'Ql o܅φoKll}aW%rɳ&$Rxؾb11ԁ'lO\)d9ҭ)>Mp]7Z‚G':u΀(q) +u$dlM +'wk S-| O;y] +1ԍ]7T5ھGOݸ$kTF`OV|PčM]tMcY }v,n \fcj{.)ٚUŠrLV$B@u*B7F6y2|rr;+\[ιrptrCx!r](P +g=c(1 fB8P +G]d i9++m'AUdQn3%ilrO8Ӫo+9ETOiSWhrFdw3:{ϣlW .6lY{ex!8t_xW>mΈȀ5{J=WFsLhPv$}_RMפ} +˴{Bzr5x.UI&${C[#Eqx(կa9EP\%wEOwS8q'^ӘD٢#AJSTY+v0ZGM0٪]lQm>Pvy$+^D & H}Bp8o/wo$_u.7Zհ 7D-z VwԼuYa0N@Ye囍 'cmt-ƗYkC#01*SuS,٩p܅[C_qbhjF Y.&L0W7O.(Uf?UʍlE0%ݹ@"qy*`@vyIy%YqpF$Xu{9"yFmNp*{BeQ-f^}szs>^D lZu4']G67TN@]6dA3$6(OY99Q#a9q 9\=Mx$U /> o$Co0:')8\٣!=zЎˋ5~Q6#]1rZ1O/h_ ~]e7yasrvlRB$@Ifs3FJ1!]Cmhykr-!إ?ns෣Ξr ONۯ0BBʆt/6ds;t>u>-.3oJM:h$c۶2kEȫ`KE9 +~&enc*wnjIcw5kΗ@,'k;}Y.Z.\\)+;=V~M+\`I)^*-O0 ! AB\u߃7%˵ .}w[<Ċsdr#Go?"Z-6K1 +$d/:0\}]7> +vTUC:ˉA€e>Ś>stream +HyTSwoɞc [5laQIBHADED2mtFOE.c}08׎8GNg9w߽'0 ֠Jb  + 2y.-;!KZ ^i"L0- @8(r;q7Ly&Qq4j|9 +V)gB0iW8#8wթ8_٥ʨQQj@&A)/g>'Kt;\ ӥ$պFZUn(4T%)뫔0C&Zi8bxEB;Pӓ̹A om?W= +x-[0}y)7ta>jT7@tܛ`q2ʀ&6ZLĄ?_yxg)˔zçLU*uSkSeO4?׸c. R ߁-25 S>ӣVd`rn~Y&+`;A4 A9=-tl`;~p Gp| [`L`< "A YA+Cb(R,*T2B- +ꇆnQt}MA0alSx k&^>0|>_',G!"F$H:R!zFQd?r 9\A&G rQ hE]a4zBgE#H *B=0HIpp0MxJ$D1D, VĭKĻYdE"EI2EBGt4MzNr!YK ?%_&#(0J:EAiQ(()ӔWT6U@P+!~mD eԴ!hӦh/']B/ҏӿ?a0nhF!X8܌kc&5S6lIa2cKMA!E#ƒdV(kel }}Cq9 +N')].uJr + wG xR^[oƜchg`>b$*~ :Eb~,m,-ݖ,Y¬*6X[ݱF=3뭷Y~dó ti zf6~`{v.Ng#{}}jc1X6fm;'_9 r:8q:˜O:ϸ8uJqnv=MmR 4 +n3ܣkGݯz=[==<=GTB(/S,]6*-W:#7*e^YDY}UjAyT`#D="b{ų+ʯ:!kJ4Gmt}uC%K7YVfFY .=b?SƕƩȺy چ k5%4m7lqlioZlG+Zz͹mzy]?uuw|"űNwW&e֥ﺱ*|j5kyݭǯg^ykEklD_p߶7Dmo꿻1ml{Mś nLl<9O[$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! +zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)Km endstream endobj 5 0 obj <> endobj 20 0 obj [/View/Design] endobj 21 0 obj <>>> endobj 12 0 obj <> endobj 10 0 obj <> endobj 22 0 obj <> endobj 23 0 obj <>stream +%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 17.0 %%AI8_CreatorVersion: 19.2.1 %%For: (Zachary Mitton) () %%Title: (metamask_icon) %%CreationDate: 6/15/16 2:23 PM %%Canvassize: 16383 %%BoundingBox: 98 -140 188 -44 %%HiResBoundingBox: 98.7919746568114 -140 188 -44 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 13.0 %AI12_BuildNumber: 147 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBProcessColor: 0 0 0 ([Registration]) %AI3_Cropmarks: 79 -156 207 -28 %AI3_TemplateBox: 180.5 -120.5 180.5 -120.5 %AI3_TileBox: -163 -488 449 304 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 6 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 1 %AI17_Begin_Content_if_version_gt:17 1 %AI9_OpenToView: -39.6666666666679 23.666666666667 3 1419 866 18 0 0 -5 38 0 0 0 1 1 0 1 1 0 1 %AI17_Alternate_Content %AI9_OpenToView: -39.6666666666679 23.666666666667 3 1419 866 18 0 0 -5 38 0 0 0 1 1 0 1 1 0 1 %AI17_End_Versioned_Content %AI5_OpenViewLayers: 7 %%PageOrigin:-220 -420 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 24 0 obj <>stream +%%BoundingBox: 98 -140 188 -44 %%HiResBoundingBox: 98.7919746568114 -140 188 -44 %AI7_Thumbnail: 120 128 8 %%BeginData: 22554 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C45FD24FFA776FD75FFA04A4AA1FD73FFA04A754A75A8FD71FF7C4475 %4A6F4A6FA8FD6FFFA04A754B754B754A75FD6EFF764A6F4A754A6F4A754A %76FD6CFF764A754B754A754B754A754AA1FD69FFA8754A6F4A754A6F4A75 %4A6F4A6F4AA1FD44FFA7C9A075A8FD1EFFA8754A754B754B754B754B754B %754B754ACAFD3FFFCFC9C299C1997476FD1EFFA76F4A754A6F4A754A6F4A %754A6F4A754A4B4AFD3BFFA7C99FC198BB98C198754AA8FD1DFFA8754A75 %4A754B754A754B754A754B754A754B6F76FD37FFC9C99FC198C199C199C1 %99754A75FD1DFFA74B4A754A6F4A754A6F4A754A6F4A754A6F4A754A4A76 %FD31FFA8C9A0C1999998C1999F99C199C1746F4A4A76FD1CFFA1754A754B %754B754B754B754B754B754B754B754B754B6F7CFD2CFFCAC9C89FC198C1 %99C199C199C199C1C1C175754B754ACAFD1BFF7D4A4A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A6FA8FD27FFCAC9A1C2989998C199C199 %C199C199C199C199C16E4B4A754A75A8FD1AFF7C6F4A754B754A754B754A %754B754A754B754A754B754A754B754A75FD24FFC9C99FC199C199C199C1 %99C199C199C199C199C1C1994A754B754A6F76FD1AFF764A4A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A76A8FD1DFFA7C9A0C1 %99BB989998C1999F99C1999F99C1999F99C199C199994A4B4A754A6F4AA8 %FD19FF756F4B754B754B754B754B754B754B754B754B754B754B754B754B %754B754A76FD1AFFCAC299C198C199C199C199C199C199C199C199C199C1 %99C199C199994B754B754B754A7CFD19FF764A4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A99FD04C199C1C1C199C1 %C1C199C1C1C199C1C1C199C1C1C199C198C199C199C199C199C199C199C1 %99C199C199C199C199C199754A754A6F4A754A4A7DFD18FF7C6E4B754A75 %4B754A754B754A754B754A754B754A754B754A754B754A754B754A754BFD %05C1BBC1C1C1BBC1C1C1BBC1C1C1BBC1C1C1BBC1C1C199C199C199C199C1 %99C199C199C199C199C199C199C199C199754B754A754B754A754BFD19FF %754A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A4B74C199C199C199C199C199C199C199C199C199C199C199C199 %9F99C1999F99C1999F99C1999F99C1999F99C1999F99C1994B4A754A6F4A %754A6F4A76FD18FFA14A754B754B754B754B754B754B754B754B754B754B %754B754B754B754B754B754B754B7599C2FD16C199C199C199C199C199C1 %99C199C199C199C199C199C175754B754B754B754B754B75A1FD18FF4A4B %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1 %99C199C199C199C199C199C199C199C199C199C16E4B4A6F4A754A6F4A75 %4A6F4AFD18FFA16F4B754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B75FD16C199C199C199C199C199C199 %C199C199C199C1999F6F754A754B754A754B754A754A7CFD18FF764A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A9999C199C199C199C199C199C199C199C199C199C199C199 %9F99C1999F99C1999F99C1999F99C199994A6F4A6F4A754A6F4A754A6F4A %4A7DFD17FFCA4A754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B7575FD17C199C199C199C199C199C1 %99C199C1C1994B754B754B754B754B754B754B754BFD18FF764A4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A4B74C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1 %99C199C199C199C199C199C199754A6F4A754A6F4A754A6F4A754A6F4A7C %FD18FF754B754A754B754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B7599FD13C1BBC199C199C199C199C1 %99C199C199754A754B754A754B754A754B754A754B75A8FD17FFA04A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A7599C199C199C199C199C199C199C199C199C199 %C199C1999F99C1999F99C199C174754A6F4A754A6F4A754A6F4A754A6F4A %6F75FD18FF4A754B754B754B754B754B754B754B754B754B754B754B754B %754B754B754B754B754B754B754B754B754B754A9FFD14C199C199C199C1 %99C199C175754B754B754B754B754B754B754B754B754AA7FD17FF7D4A4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A4B4AC1C1C199C1BBC199C1BBC199C1BBC199 %C1BBC199C199C199C199C199C16F4B4A754A6F4A754A6F4A754A6F4A754A %6F4A75A8FD17FF764A754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B7575FD13C199C1 %99C199C1BBC16F754B754A754B754A754B754A754B754A754B6F75FD17FF %A84A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A4B74C199C199C199C199C199 %C199C199C199C199C199C199C199994A4B4A754A6F4A754A6F4A754A6F4A %754A6F4A754AA1FD17FF76754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %9FFD11C199C199C199994B754B754B754B754B754B754B754B754B754B75 %4B75A8FD16FFA86F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A99C1C1 %99C1BBC199C1BBC199C1BBC199C1BBC199C199994A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A6F4A6F75FD17FFA74A754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A754B754A754B754A754B754A %754B754A754B754AFD13C199754B754A754B754A754B754A754B754A754B %754A754B754ACAFD16FFCA4A6F4A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A4B4AC1BBC199C199C199C199C199C199C199C1996F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A75FD17FF7D6F4B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B7599FD10C19F4A754B754B754B754B754B %754B754B754B754B754B754B6F7CFD17FF764A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A7599C1BBC199C1BBC199C1BBC199C1BBC199C1C199 %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754AA8FD17FF75754A %754B754A754B754A754B754A754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A7599C199FD12C1994A754B75 %4A754B754A754B754A754B754A754B754A75FD17FFA8754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A7599C1999999C199C199C199C199C199C199 %C199C199C199994A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A7CFD %17FF75754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B7599C199C199FD13C1 %99994B754B754B754B754B754B754B754B754B754B754A7CFD15FFA8754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A7599C199C199C199C1BBC199C1BB %C199C1BBC199C1BBC199C199C199994A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A76A8FD13FFA84A754B754A754B754A754B754A754B754A75 %4B754A754B754A754B754A754B754A754B754A754B754A754B754A754B75 %99C199C199C199C199FD0FC1BBC199C199994B754A754B754A754B754A75 %4B754A754B754A754AFD14FFA14A4A754A6F4A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %75C199C1999F99C199C199C199C199C199C199C199C199C199C199C199C1 %99754A6F4A754A6F4A754A6F4A754A6F4A754A4B4AA8FD14FF7C4A754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B7599C199C199C199C199C199FD11C199C199C1 %C1754A754B754B754B754B754B754B754B7576FD12FFA8A151754A6F4A75 %4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A4B74C199C199C199C199C199C199C1BBC199C1 %BBC199C1BBC199C1BBC199C199C199C199754A754A6F4A754A6F4A754A6F %4A754A757DFD11FFA14A4A4A754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A754B754A754B754A7575C199 %C199C199C199C199C199C1BBFD0FC199C199C199C199754A754B754A754B %754A754B754A754A4A75CFFD10FFA8754A4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %4B6EC1999F99C1999F99C1999F99C199C199C199C199C199C199C199C199 %C199C199C1999F99C199754A754A6F4A754A6F4A754A6F4A754A4A4ACAFD %11FFA8754A754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B7575C199C199C199C199C199C199C1 %99C199FD11C199C199C199C199754B754B754B754B754B754B754B754ACA %FD13FFA87C4A4B4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A756EC199C199C199C199C199C199C199 %C199C199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C199754A %6F4A754A6F4A754A6F4A754AA7FD17FF75754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B756FC199C199C1 %99C199C199C199C199C199C199FD11C199C199C199C199C199754B754A75 %4B754A754B754A76FD13FFA8CA7DA176754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A4B6EC199C1999F99 %C1999F99C1999F99C1999F99C199C199C199C199C199C199C199C199C199 %9F99C1999F99C199C198754A6F4A754A6F4A754A4B4AFD12FFA87C4A4A4A %754B754B754B754B754B754B754B754B754B754B754B754B754B754B754B %754B754B754B7575C199C199C199C199C199C199C199C199C199C199FD11 %C199C199C199C199C199C199754B754B754B754B754A75FD14FFA8754A4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A4B4A9F99C199C199C199C199C199C199C199C199C199C199C199 %C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C199C1994B4A754A %6F4A754A6F4AFD16FFA8A14B754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A7575C199C199C199C199C199C199C199 %C199C199C199C199C199FD11C199C199C199C199C199C199754A754B754A %754B6FA7FD18FF516F4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A4B4AC1999F99C1999F99C1999F99C1999F99C1999F99 %C1999F99C199C199C199C199C199C199C199C199C1999F99C1999F99C199 %9F99C1754B4A754A6F4A6F4AA8FD17FFA1754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754BC1C1C199C199C199C199C1 %99C199C199C199C199C199C199C199FD11C199C199C199C199C199C199C1 %75754B754B754AA7FD15FFA8A14B4A4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A756E9999C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C1BBC199C1BBC199C1BBC199C199 %C199C199C199C199C199C199C174754A6F4AA1FD17FF4B6F4B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B754AC1C1C199C1 %99C199C199C199C199C199C199C199C199C199C199C199FD11C199C199C1 %99C199C199C199C199C175754A76FD18FFCA4B4B4A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A6F4A9999C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C199C199C199C199C199C1 %99C199C199C1999F99C1999F99C1999F99C199C16E4BA8FD1AFF756F4B75 %4B754B754B754B754B754B754B754B754B754B754B754B756F9F99C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199FD0FC1 %99C199C199C199C199C199C199C199C1A1FD1CFF4B4B4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A4B4A9F99C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C1BBC199C1BBC1 %99C1BBC199C199C199C199C199C199C199C199C198CAFD1CFFCF4A754A75 %4B754A754B754A754B754A754B754A754B754A754B9999C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199FD0FC199C1 %99C199C199C199C199C199C199C1CAFD1DFFA84A6F4A754A6F4A754A6F4A %754A754A754A754A754A6F4A99999F99C1999F99C1999F99C1999F99C199 %9F99C1999F99C1999F99C1999F99C1999F99C199C199C199C199C199C199 %C199C199C1999F99C1999F99C1999F99C199FD1FFFC299C1C1C19FFD0FC1 %99C1C1C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199FD11C199C199C199C199C199C199C199C2FD1EFFCABBC1 %99C1C1C199C1C1C199C1C1C199C1C1C199C1C1C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1BBC199C1BBC199C1BBC199C199C199C199C199C199C199C1A0FD1EFF %FD18C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199FD0FC199C199C199C199C199C199C198C9FD1DFF %C9C199C199C199C199C199C199C199C199C199C199C199C199C199C1999F %99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1 %999F99C199C199C199C199C199C199C199C199C1999F99C1999F99C19999 %A1FD1DFFC9BBFD19C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199FD0FC199C199C199C199C199C199C198 %C9FD1DFFC1C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C1 %99C199BBA7FD1CFFC9FD1BC199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199FD0FC199C199C199C199C1 %99C199CFFD1CFFC298C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C1999F99C1999F99C1999F99C1999F99C1999F99C1999F %99C1999F99C1999F99C1999F99C199C199C199C199C199C199C1999F99C1 %999F99C1999F98C1A8FD1CFFFD1EC199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199FD0FC199C199C199C199 %C199C199FD1CFFC9C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C199C199C199C199C199C199C199C199C199C199C199 %C199C1999999C1999999C1999999C1BBC199C1BBC199C1BBC199C1999999 %C19999989999999899A8FD1BFFC2FD1EC1BBC199C199C199C199C199C199 %C1999999C1999999C1999999C199BB99C1999999C1999999FD0DC1999999 %C1999999C1999998C9FD1AFFC998C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199999899999998999999989999 %99989999999899999998BB999998999999989999C199C199C199C199C199 %C199C199C199999899279998999999A0FD1AFFC2FD23C199C199C199C199 %C199C199C199C199C199C199C1999F515299C199C199C199C199FD0DC199 %9999C1992E4BC199C199C2A8FD18FFCAC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C19999989999 %99989999999899999998C175510528057598999999989999C199C1BBC199 %C1BBC199C1BBC199C19999989927286FBB99C198C9FD18FFC9BBFD25C199 %C199C199C1999999C1999999C1BB994B2E0628272E279999C1999999C199 %FD0DC1BBC199BB992E282E99C199C1A0FD18FF9FC199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C1999F99 %C1999998999999989999BB98752706052827280528279998FD0499C199C1 %99C199C199C199C199C199C1989998990528054B98C198A0A9FD16FFCAFD %29C199C199C199C199C1999F7552282E272E272E272E272875C199C199C1 %99FD0FC199C1752E062E51C1BBC199FD17FFC998C1BBC199C1BBC199C1BB %C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C199C199C199C199992706052805280528272805280528989999C199C199 %C199C1BBC199C1BBC199C1BBC199999951057699C199C1999FA8FD16FFFD %2AC199C199C199C199C199A07576272E2728052E2728272E277599C199C1 %99C199FD0DC199C1759FFD04C199C199CAFD15FFA7C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C1999F99C199C1BBC1C1C1999F7575272827280528057598C1 %999F99C199C199C199C199C199C199C199C198BBC1C199C1999F98BBA7FD %15FFC2FD2EC199C199C199FD0BC17576512E27C19FC199C199FD0FC199FD %05C199C199CFFD14FFCA98C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1999F99C1 %999F99C1BBC199C1BBC199FD05C1999F99C199C199C199C199C1BBC199C1 %BBC199C1BBC1999999C199C1BBC198C1CAFD14FFC2FD31C199C199FD11C1 %99C199C199C199FD0DC199C199FD05C199FD14FFA8C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C1999F99C199C199C199C199C199C199C199C199C1 %99C199C1999F99C199C199C199C199C199C199C199C1999999C199C199C1 %CAFD13FFCFFD32C199C199FD15C199C199C199FD0FC1BBC1C1C199FD14FF %A0C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1 %BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C199C199C1BBC199C1BBC199C1 %BBC199C1999999C199C1CAFD13FFC1BBFD33C199C199FD15C199C199C199 %FD0DC199C1C1C1C2FD13FFC998C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1999F99C199C199C199C199C199C199C199C198C2FD13FFFD52C199C1 %99FD0DC199C1A1FD12FFA7C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1 %BBC199C1BBC199C199C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C199C199C199C199C1BBC199C1BBC199C1BBC198C9FD12FFC2BBFD35C1 %99C199C199C199FD17C199C199FD0DC1C9FD12FF99C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199999899999998C1999999C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %98C2FD11FFC9FD31C199C199BB99C199BB99C199C199C199C199FD15C199 %C199FD0BC1BAC9FD10FFC2BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1FD0499 %98999999989999C199C199C199C199C199C199C199C1BBC199C1BBC199C1 %BBC199C1BBC199C1999F99C1BBC199C1BBC199C1BBC198C9FD0EFFCFBBFD %2BC199C1999999C1999999C1999999C199C199C199C199C199C199C199C1 %99FD11C199C199FD0BC1BAC9FD0DFFA0C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C19999989999 %99989999999899999998FD0499C1999F99C1999F99C1999F99C1999F99C1 %99C199C199C199C199C199C199C199C1999F99C199C199C199C199C199C1 %98C9FD0CFFC299C19FC199C19FC199C19FC199C199C199C199C199C199C1 %99C199C199C199C199C199C199C1999999C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C19FFD0FC199FD %0CC1CFFD0BFFA79999C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C19999989999999899999998999999 %989999C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %BBC199C1BBC199C1BBC199C199C199C1BBC199C1BBC199C199CFFD0BFF99 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C1999999C1999999C1999999C1999999C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C19FC1BBFD09C199 %FD0CC1CFFD0AFFC2989F99C1999F99C1999F99C1999F99C1999F99C1999F %99C1999F99C1999F99C1999F99C199C1FD0499989999999899999998FD04 %99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1 %999F99C199C199C199C199C199C199C199C199C199C199C199CFFD09FFCA %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %FD07C199FD0CC1FD0AFF99C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199999899999998999999 %989999C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C1C1C199C1BBC199C1BBC199FD04C1 %FD09FFC999C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C1999999C1999999C1999999C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C1C1C199FD07C19975754B27A8FD07FFA8C1999F99 %C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C199 %9F99C1999F99C199999899999998FD0499C1999F99C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F %99C199C199C199C14A27F827F805F8F8F852A8FD06FF9FC199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1BBC175270027F82727272027F82752FD05FFCA98C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C1999998999999989999C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C198BB98C198C199C199C199C2A0A0A0 %C9A127F827F827F827F827F827F8F87DFD05FFC299C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C1999999C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C299C199C2A0C3A0C9A1CAA7CAA7CAA8CAA8CAA8A8 %2727F8272727F8272727F8274BFD06FFA0BB999F99C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C19999 %98FD0499C1999F99C1999F99C1999F98C1999998C198BB98C1999F99C199 %A09FA1A1A7A1A8A1A8A1A8A8A8A7A8A7A8A1A8A7A8A1A8A7A8A127F827F8 %27F827F827F827F8A8FD06FFCF99C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C29FC199C2A0C9A0C9A0C9A7CAA7CAA7CAA8 %CAA8CAA8CAA8CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA8CA27272027272720 %272727F852FD08FFC299C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C1989998BB99C199C199 %C2A0A0A0C3A0A7A1A8A7A8A1FD07A8A7A8A7A8A1A8A7A8A1A8A7A8A1A8A7 %A8A1A8A7A8A1A8A7A8A1A8A7A8A127F827F827F827F827F827A8FD08FFA1 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C198C2A1C9A0C9A1CAA7CAA7CAA8CAA8CAA8CAA7 %CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8 %CAA7CAA8CAA7CAA8A8FD0427F8272727F82752FD09FFCF98C1999F99C199 %9F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999998 %BB98C1A0C9CAFFAFFFA8A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7 %A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1 %CAA127F827F827F827F827F8A8FD0AFFC299C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C2C9CFFD0AFFA8A8 %A7A8A7CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CA %A8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8A8FD0427F827F827F87DFD0BFF %A8C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %989999C9A7CFFD10FFA8A87DA7A1A8A7A8A7CAA7A8A1A8A7A8A1A8A7A8A1 %A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1CAA127F82727 %5252767CA1A8FD0CFF9FC199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C2C9CFFD16FFA8A8A1A8A7A8A7CAA8CAA7CAA8CAA7CA %A8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7A8527D %7DA8A7CAA7A8A8FD0DFFC998C1999F99C1999F99C1999F99C1999F99C199 %9F98BB989999C9A7FD1CFFCFA7A87DA17DA8A1A8A1A8A7A8A1A8A7A8A1A8 %A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A1A77DA8A1A77DA7A1A1 %A7FD0EFFCAC199C199C199C199C199C199C199C199C199C199C2A0C9CAFD %23FFA8CAA1A8A1A8A7A8A7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CA %A7CAA8CAA7CAA7A8A1A8A1A8A1A8A1A8A8FD10FFA0C199C199C199C199C1 %99C199C199BB98C199C9CAFD29FFA8A77DA7A1A77DA8A1A8A1A8A7A8A7CA %A7A8A1A8A7A8A1A8A7A8A1CAA7A87DA8A1A77DA8A1A77DA7A1FD11FFCA98 %C199C199C199C199C199C198C2A0C9CAFD2FFFA8A8A1A7A1A8A1A8A1A8A7 %A8A7CAA8CAA7CAA8CAA7CAA8CAA7A8A1A8A1A8A1A8A1A8A1A8A1FD12FFCA %C198C1999F99C1999998C2A0CAA8FD33FFA8FFA8A87DA77DA77DA77DA77D %A8A1A8A1A8A7A8A1A8A1A77DA7A1A77DA7A1A77DA7A1FD14FFA0C199C199 %C199C1A0FD3DFFA8A8A1A8A1A8A1A8A1A8A1A8A7A8A7A8A1A8A1A8A1A8A1 %A8A1A8A1A8A1FD15FFCA98BB99C2A0CFFD41FFCFA7A8A1A17DA8A1A77DA8 %A1A77DA8A1A77DA8A1A77DA77DA17DCAFD16FFC9A7FD49FFA8A8A1A8A1A7 %A1A8A1A8A1A8A7A8A1FD04A8FFA8FD66FFA8CAA8A8A8FFA8FFA8FFFFFFA8 %FD12FFFF %%EndData endstream endobj 25 0 obj <>stream +%AI12_CompressedDatax$&?d& C;\;fJ-n[[YUZ(xd&,f #+3q98OxOq< ?wo ~7_{ˏ~/&G4M~Vۯ߼LG{4?oqwo^_^ޡݻ篞g/PWnC} 4`e߱=&7Ô?L/ޣvu&3%Co^|O߾yq7/߼W?>y~^|0;qv~c7/o^a|t/_/tqzWw0R<3ٯOaC)?l/Jo|ۿi[Lݘج{K̬̂1??J[x?~W~NguG|˻FѤS7_ܽD/ H1oɦ >Kz3 3y}vgӭw2IM~?WyOtҺ2!Lj}.6(^{_G<Ѽb |aoSl߿DŽkѽ_bٚO1';=I~R4!o;H]cձW?y=5w7_j|ӷR}To?~_^bqQ>mŃߔ?뿏 Bӛ{U>}|CsL!޽zn +!}Ho_wg߾܎Ͽzz_~˧ߩO n_|=w.̙/|ܿ8~_xNuTOX[˷Zߥfi>$_>ksP3|q-y=?F?!]϶j~xx7/~tS/>\?߿cwwI|+r;鳶|0e> loq\߼3L/pba,H=;0?)vU\) ߀%%Ӧ\ \܌x[f`*nU-LDIo^iSi.繜5Jz7ѵ][*FAiUU*'-VmӯVuY[a^^Zd]fU͛V+ebe7g]k;la]/WkdYԬU)۵Z)*և:YeXtMe@CY#չk)7ܲԓŗYUeLIɭ̍zʵؔF2 ssλɝP-Vx>'O[L .C +S +p7v v)esUsSʧ|o-&7 LNyni̕W*^rdNONtemwp|:[l6#ut}>_\ތXwoM7 usnnnn#n1aoz^m7r*U9զL _^*qSªUq 8R$l!z7M9kӪ\ʴ*ySҪU M*vU̪KS>_֣_WENf]Z%. bXv 2ʌd)v64o򬡙R&)$%JR\)vWL%Ϫ?')WLRr)8K R|)%Ѓֵ;zeY eeگed&G/fdndbN2yw|Q^Z^Jd^Fq``2Ϡ[W_t,yP5 j>H73_J F1aoטt@H tr@x#HuN D;x;p{{@}w & +D5CpqnX. K׌ucq7g\DW);2UnC|Ey x;Rٙ-خv8Pz? gx'H˗v؅0ܮH *`Cld!2r.y 9&*w"6`ټ.b/+>P,"eXalX~PdS }bٛ2<duXE3Ϩr;.E9J\d('}(bs=U-l4W=9"}ZӬYAL6%Ts끼VJ{OX 2=ye[3UCwD翇d?Sԇos<;XԵ?ʏOBF7mLE߰s8҆+ H$" (" tAy\( !DʢJBFǭH|! ,DiȪ4$uN"e(rE"R,RQш‘Q ,e$JI OeSB%'ЈjFĥkK(2Qhؔ|J5t[듖|97nI?FյX4̲P,~)uuxIx" ?4 Qs E6< KCvD1l4맕aýE|r.VOoGEq3- -U +ͪLTTJэEUZ*mXM]JY\9UDi%%2r5=Ҵъ %s(It8i4fCT +a);12y?Ӌ+[ @c&*PV5m\86 ŸV+-7bU[#w)Zf{% 0<@jXp1frb=]9It=G9 [>EXy+3KoJO+ï~QQ0:1L<98Ilj> -Ѿ8Jl+u!3)]Hh% \hB#ڸLo{[Z&qk"fÊmIWCv8x}i؎u'^{߇qmCIJϞ=c@09Bk۱iccwt6sM&;XݸCI1TQoZ@,qx)G7?}HH|5FPH!H58RZ$n҇U}|OV#pDnyu:q/#7I0c (0 ++tǵl⟿Z +R>qV#6Cxji7~zQfd @,zv4./_bS;;c4`&# 4؜6%0ySIRм m{=jP]||s|<:@> lm/zr._S  +_rUXBa3|!<4S<0< Sy+De&W@AAr-^uUjKTˈXōuXᒗe?#uFnfVŜzQ27sPf'z3 mdq<9+r>3_;ug{YʪHNEa십]ԕd1U w]<[ )8c(bb<;]=),IȥId,v+POD QuVoʬ*rk$V_ba_U[X.}7IʲC#rOL,lEkY, En%ʦ"ƞD1V] ֯$XJ:vl`{6VY=+2ҡYAnVط6{;,Y ugeV^am|O;>ͩ3/Յ8/+w J.zꓨW|٘X \bU鵧_o(r = wxw<}e|m>rz|/GTy/Ҡ `+0{ط>WG|/\,O_{ُF-I}34Yݵuw}\&vřTi: }S͍2.",W(^QFl~34mٷ!,ԅl#AiEqڐ9,%#s֡%bn#'g=<r9?3:Iq?:?'3{T|ʞ:/yE X0=QnEk Zi:EBm9Ɍ \:H͕}g02x˩yj4>/=?S=w טiąYpo8&joEI'Oo>sQcb J"cTՎb&)xEUܔ&`dgFzv%ś9rJAKqj6Myt a\~ا~+7^ݽJhgyus]j/iR:SnBL%4#Kq _ KX*8^Kz\e>l/ư7TVrǩ0 7U/ޅA[lH8$&qOTqʅZ%.B5!% KJ)@(|GY:>LBaK\NKyUoItfm6ŭݐ\¦?)NUWe6>%BE6Q%J>q唚mWCyZy"obݮuc*qj+}ZX9>p,JͫoD Dޤ$;;mXGƕl_f#2-dpeQSEN[cnm4;h\; Vymff g,Ju}o14Q$:i\-.ns8ič+9m6VkϚ|['hZzvvAuaG`}t`}YhG_:m8GN84hu*, _ CkA|,|aGWuw#my{"Vފݹ(|مavWJů9jZ~,wr Ez_dxZ_KY˻JR EV 襠qwU)y@R޸0fwnxQ4;9LAǦ6 +wz·2_}q|t0>\v,нe| +(Cq7o]ͷ`['sثj[d˧1rQNت8 ETԑ<}>S|efk Hʾ_>Ctu;,0D9,'%CS9 .N[ZcqaO΃q5Ovi7bE@Om>nzw)6>FaCmq$8I?I7 ,BԽMٻ +M^A*V*#9Փi]nLʼnigLìQ&[,_?5@'Q +oDT7 F\'uY6@  ,[eh>O*r.V+÷h#exZ:i0$3QN +ߑ6V<ϒ^XEH92kV'jX\+KdP8SGYр3ɡeNqV1 e'%:U9J1џP:vlu{L[5*F6Ԯ/+B ƪ-; eфaǓJږ߶<' Oo_KM ]"\EkF0EDϤE Ȕθϔog|VUu^v)H! {'xT:UjCITҒآ(ZJ(Z4KY薮lZJa\"6Bi(D9P{i{::6KOyíO0i5*alX!X%RK努Q>E(c4,Zۙ;^o;r%( />1]3HJW.=Cq Q; JliJB%۵C^ײҫjL[ExDZ䤵 nI˴~ԻzMj,v8'2<9> Oo_êD Oس&F$(0SNeb + +0jhMMop-~v*fPj0<м4A͗ƪ]\ /0)zdp[_bݫjZL]kmrkX6ָ՚.XƬuɨ1i=d.LYO^IS)exZ 2< NO' +O' +xm7?a>Ykpi>d8$\09 f‡B'zgFxqO/e⎭cT1;ɸC0tmojj{ (qp͂C@z Nim)x?NvU!qnܺ//rvEi]ŧMi|T3ٷO'ؤ\L+u6u&٦|t,JtѲK]j=?Q`%֜k,pHyTak5u1'@IQAl P8MJ&7nJiQ<YOdF0DG3yvhCx/ew9ˣ/h-UPhnu&rSwOfc[x^$؝lxqq}csnٖIİX#!uDmw2<@7[I%~d<>WMeuv]a v2K%ѳ.ڟsq\\40Ž[Ѭ 7A 7_,~bN|v#n`^E.'[dՊ>R: ^G=,>($&K}id5VZڨpk ,ٴ 0 JJTgb60rOd`WIj>Q)3G^<-g@GpAYj,D9&# n-ƄA2 m2v'}2(W]c~TQ9oXVͨt?{<% rv 3nl=ثcvL !crU8+}Nߓ%@q+%ǭ$"~(2^ +Ma\Qݙ>Y+|Z[1z[gz[1Ԙ۴mJUr5Y|Vnodw`xNRKisl\7P΍||TGYugnKE$%V`t +6>+j::T\Phel銻PnC%oS5 +YSh +fWX1V *:I2SOBI44!eVk?xܓ'cOLj4-oKYuUh% JLfd-ytu<+qeYmˇ_CUVN`7(<ˌpaN ƍ]崂v + 6<žr[D\,-9n^$ D8aw2\0[ԡz*--ZLTFh7@K`nQ@밆#^MnRD_yrwQvpѹჲpb?McCa7Mt;HsM8)4y%qi$!wb-jqo1Pm+?:W]:HC'tJ!v\Y"fzw)n.(RD +K gѢ_ \߹D!˔] bjQ"#ΐSm2a+|;rudȼ]A>Q?ulR0Ԝn`uEss9 +Q37fQ;NLthp) n)n6WZnE*:]yu}; ӕdqYJZ,=*SZ< JӐH~[͎csߥ ÅR$Azae+̬䜽)Ò)SA$ZܬKV׬q:k)=As<2mS/LtMo@] ?}S>wihx9{JgU|>i?)D=:Kb%5ީ xyN$ȫik. ~s>4P}h/=vY@M~ ƽ#j&btan"k9hj/$s.!OE8]O$opLs*~$neqb:<7BB.oN4;rN/+"V}ZC^2TX5wb!xS`*ffs9 : i.!JUfQ?H*F!uV[6Nա$ͲWa⇰0IRnJ:'hp!bBY +`E;p8O +n2 ;aN(FXg `ᡭXbmZbw k7ax-(ÅS[/|um*][Wm!n;2._=11i8cT]#w-ՇI~ݔ÷Q~^/CQJ Rz$-Hi[Ȥ"k?hR{W5qn UVi+~ + +whpC=C~ӷݿOVrbXݽ} ?9DaSt~;X43a{GOl6O2+o:O?1'?O}t+T>:oZv{{~ywo^?ïD{ӛ7/1y)wo>;=WL?ܿ{݋w8w|ȯZ>@ȒgEYkpZtSacBM~˵)\Y$gq81A`EtY5$fL!Lzv#`!1<5ق6$CkG9`g9A&0=޲;z|ŏM?!0}<&|;t> C0/6R[¾3>8bx 7鈹3]Gg #.6ÄJ*1*ɁDݡ ؚ#{6+#Xu<*ln[T8lǀ`!k_&.GN΄bia&P,. G_;3GxbcY16TCG)D[^6Wc,d:ɓGd1IN'LH0%@;K'Y(G(MO#6%] @PbsO' d$S߆d-J4Rwg4udo+qo5!|~lb>Y Vg= !9P`W^éȝGV` jTZYBؓuZuvd1elYSg#n +}[Ϲ0qufۿdf7_/ ʃs7_ٛ? ojõL{آ!TfEbѝ(xL(2f㴸˸$n +,L"C"zJЄ \7T[ʤd + 6vBoeW]!DW1oHwm%H2kdRq'CՎXZ[i;Fx#B޵] yoeNl3_cD9D/*m% +dބSy]- u•HJbcx[ w?* rYrBʟ:3̑nsS.eUdSik3G>fT9i\h?qsiC;+x#@E:PlXJŽKun|ƛy$ 8C(A]Z0Oׄm x{\F0TOEȓHfwaaJ@Cr`%@|R%RU#>Lo;yp"MfP"0=Xpn |9APr- +23A(LOř\'"Dӂ3 +|=g>/ݡR҉Ѩ#3'7=.{&a1[f^qɷb!qVxT,@m gS+ + ~ +gZh+6,QYޚYռ9>sǹ %ґ?l mm]㽃)Lp轑ChM + SI8\Յ<%44.i+഻2>=ρo?Rݵ41?U58?!Yި&2qňLeLf9m#p\ Kdve~e K¶#}0UOe$|xA*4>:Q[# + LTU$ ֎&&4yr($bx(5M0Ɉprwݗ#-F6<_fn.(`wy|nME&ڑFȄ/;b)q&L{30D>fL4Z$.H=%7}+.Xl Vnp4s. .N:іDkP'_C:QӍŪNT)/#*tNN."PtHݼBsEn>nlG؊ TV7SBH`dp,d kEڴqƑ-@#v∣ؔfAR !D^Kh׊309Pd.WjZQJ`t-vߩkNM7nv[y=Anu =U^d*NIq<4Q) +4֣VԻs9)߲zNWQFm;-:ϱ?3RONYϹ4wJ?Y1F7lֵ, F8J\oY}68j @)7TەTEਟ +ic c&N-pdd?ѭLc̍w0SkY~x;(&68`d]@i U}q@3ŭOʨ4|nނ, xʀ1MO#iڸ&H-nqdq]$v0qTɣR;{#OR?WjJ'$q+M[1MFU7$#C tm!NLa\h{:ldneMq @G׾QKr>2k;AMx_}puB^C?1*!jT-tC"GӰ׏-8`Ǹ;EN/ik0C?* RFnvn~Uh:}|KCpބTmOM4{ޭ8Ix!@cnY`LXV@--4%"Epj|E$GΈdɄx%hkQQomh=mCQoOwN $. +4"Fj0Z̝eHӐKaDcJj{ eY[El]lB8y@A;^|Dڋ(@\▃@:usFtΐ"hνIB/>RzH#wm=Y sΈr ̎\DK墿8 W&:nХb%uV7K$3)ގI^c_SroT#xCAa#4[So+8ո{|*&D +l0;ut0ۙ\L2(D/qpoTWF\ n6!XH/@]/"#RBեx9D +1 fEh5X1FHXPIX X|5^ɓIr=t]F3}7Q0Yuoe%D>9tɆ/Ge#* BBu Vѫ!-W'0rǺ~,!^iTvtItu:+! H{D>GYƕn\/ iǩ'+z&;vʈ!5p O| ߩw{$${b[BPO;y,Ͼ YKfa5;-gLP>]yl?w"CuQ*v>ͫ֋БGqE{'{: +豛ɗWi+vwf7V'˒Qv]j.gT쒑hL~^eY݄6:oCت(^@Úd7^_y“-]- T?Q{jԿYgOcV/ɂ3xo]:y_~lukjȣ^}V%/VQ92RඈPūV Xԓ;Nev=ϻJbOӪwCi֠cû3P.v^z:p}:5I /P'U`fxX;Eu} e?HS߼N"7m&E&<0A4۴-fRny|WF$C2KaR`/v&ӋKD?:\ + DLsL^:~"r|ws5mn%n!#\ +얍y09ġxJxI&0!Sl <ཽAz#௑(k$2JS` L%NO;/< &*0yf0 +XOV:GKoe'o/^wDFFWfn +8ݱ ɉ)Z\ɹxf#~2`gWuSq!nH "74w`>`ő<`z*Po1գguΐ!?穋H#o1)g 5k78'*%PbNHj2pWFpJO^6GA0SXb;VF h=Yt՘‡Ob4Q4d1VTGKN,"6ΜF %3a-W3e^\ zr] Ki +/ƺ7EN)(ꦑ6~,VE VSӢRfn~:}t0%GQ Dj[1"FQQb3Q]?h+&@zLYB +,%Mw'Ia$ Q=uYsTB -ݓU g&TQLQLgyiP6Ǝ}]7iR^!FKBᦢ5Jd2Ɲ:qu#U +H)4v-@BN ifSeO>I"Ntl?Us*Oi4K;!ql%1- "%(ѥܹLoSSᕝm( +֌ܪ+sFFWod,QbdB(*dtI$  F`3fP"0 }̼aiටkp=YS‰7-b$'186h^H6 +Gbgy@h <):o^i&망n( +"deA53cK^3C"xfB+DuŅ*MfHV@ cX=dԥ bkug(]ꓚVI`4f !m :Ez8ӿR@LM7P[y=A + D T C׊Vc}jxɠj)BН+e <*%2.Aܖnb;_G韬蓺VɕVv,]ߠwjڵm?cDtp4Gb@ +(•<j"y/R;θ:Q4rS%f6tw"zSv[NC*FJ""9XvZ4OZYե洣ԏ8^/\NMe4c ݲ +X dp> .hۈ~$t$kUeGʀ<K^ԷxQ$d B(^먭ŞBע}*M694O +XΛ +u,_w-/ߢZ {[;=qUZ*Qu஺/[etl mѾQZ7nv`܆EU vY};ڏ %^VDoɻ^taecDX)pÉꮅm3׭8mP d\K 6oѼЋaNt43ҌGS!xzFж^pݴt3zT9BET"C ":] È@:~$@":$:Jz^.8DׁCt|8D8D5lסD0};-^_  $C@"*_ 1CAB~-D *.D*`Pwa*&`P;P P6Cp~` U-.C:Eoha;px( .N8ZH] P"ҷ~~۱tk(M+%X8ag$ ٿQBT'z7[e 'bh3PW$PfOVtE9JF;z␙q@2\!"y҆Dp-T>jhGV,J;#[4oajRAq膂i_-􍚉ick4>ْ`86:~rѴcu{BXX,VR0HP]0OθN*E >eat <~*6Gۑ"ok ԯLpǖǻ*L{U8HJtRFmKr }#B;Ӷ > +|\oa\=y)t3!-m߈7p@y7E:B޵Ҕ=׼{R?G|?Va$T1Oب*Ed\5!soD 4@.3b$nJ{Us8^}]U~9)-ר>M!:4iR#e6ݏiXeiig#[%.hb7Ϡ=[/abrQkG0^3Y'{: Ė: virwb+VUcն$4M?ʼӽP짠<-1$[&mI1_VbFVcF2i@TkʸLŰA[#4S~^Ʀ5u<4O>NJE(پv +s.MCoELIənܶx`;(VLG+qv^BdIkQdX佗ak4Y|X;;iӍ4h|^3\ĺ&qeߩiQfn~:\l(neGQj~ZߊU!n+*Z3Vbk%Iu3f퀴$݇ϢzYTW$Kn_ $\VbE Xnfa}\":*K5*Z z*[^ŕb:Qw3H`P=)I`M]aU +3ryDgQMtz8EECẠ~ +E{,6A2 xA @ɹkAv*«;- dCBDn}'dхt_"1chFZ@*̓dZި\yřLb +---8 "d/]T̴t>PI(v u 4O8oub){4W`Chr)8E h`hK}LZ*hE4i[4gcj#;DKeuk#i[Ni9sR ,i@`-~k9N.mm]+[Tɹ@tWiߣķϩ{@:Kg~LAǴB_y22O8:}UaFE#b)#N( + ,0+\10WsZpyt{˵ jD$}4E} o~߼}wwᗿynovw_^߿~ֿgx۷o^_:7_m]iFϻ/ٛw n޽}qR0Y߾y|YڛgWqn^QпOw_޿./GEQ ɺQ1FF>gg|p4smYn^۬Ù߉|@c5eD!'1ծ뀑V+Fky>٩* ~ y#ogclJ)+J&H4 +f`E +ZT:УRS9@3%O,#/hF1CvU@uyPk%dK0^;:#x#vbΜ%gǤ_YR'$MhAܖFJÒI_G5@T0{7+\Τ7710 0@,n&V 0RUBn>GRW9E&?Y#Њ +lJFIVE [VW}-^pG|L8Mo F&ЃP=c0a`oM }8&\&)x,l'PX&8gc`RIM$h8t8*sȘV<$XzUAtDTTK{K(f@Z`@Nb}Koiٗ.F|ȀGԈ4тk ɳ`KZ9M5XXI3m"3'!Dk:$$'5A:ы;I0ђ#Hs9虙x3$f +TىVl K+nKv b@LjHE# +&4240=#%sM9I $Q,*WNXYI*J GFO%]POH(ߢTseѲWT<0ƬF&v +FB&?iRa}4J[1Ez.GLLĞ=ܻͻqt[ C5>N]d`Q8nvr-[2o5ȓo";fQlk=V1s!&imI*VpdJ11GA[.T¨heg4UȔ(1qK2g ] D`~K+U։SSZcIӚ\ܬu>B|59F$x8X3=,'(SOHc\|(6sZ3ʕqpǒ:Uh!v%,z8)Js [1I9(zdŅvj>i"eې4NRcy ;j@6WIF})Gbc-I * ̜!+(+oe#BC]Ѧ K*`R/}Wq 9W|.<(8"d譙PɕLU+Y/|d^+¬[I,$ےB L +W aҏe + +/AxC!A]U8VwC !oOsᓗCj%\B[.|&HpxQ3t+d`X)dVǩ +4+GNIeΥ3I bDI `xV4HE=sJeg %h>g8H\;nZne3ٙYL4tR9%\UصءIT|>T~>T*YIxYq;gn0+)["|=8:W4nDL_BQI>lC$;w$tд;#/%~ԥbœoG_0;hAr^9\o'“ +QWT &?H8 5`RoF9Hl Iev#Y B\j'ADUÒCTG|z!VXV."=X>w 8Fi$RJ[JW|_poe-v8GO| !v6*,W`-eIMu_HBRg_T]$:߆6P8|!= +IEmla˴Rvg *t-#dD| n1w81ge/$R`i qE8+e^2e[5ע>)~-~(i"^Yш*W#CJ '~L#?ϸKOՀhJu38IL/EQYɭ@E'R]3D"8Y!.=cVL7I/n[s .O99s}?B8AMQ2z݋RYajQ2u?Oύ}$)%M*CE[Br3:[!Q䗩rd(0-.J9*&rcDC +R1cţߑt0w7E%IKMj7؄^hqIj!HBT (M)j@%$q)dFF! TSF&7DaSGT'S= k +!#.ZUzZi)Vy ~TD܄tYx w &:GX~i+;$2d9&Ee'i! VAY)VQAEṑHTjŭQaW]2Ĭ HdffT7G2ydVgVZ*=IHyt 뢹rq=*7 ~&\jYP0ⓖE +j/P݉2L8zt?PsiFOIqK&W'5!4ĥj(YQ( +XվUPdlV@Dw<%ߜbF=EQ30YUJY9A}7 +jO*ijI[9p>;2U%Dc&ƴ0'NˎcyB *¥$ @k[ _K@(w˚fIP},PǼ(gn$:PIc㘊O0gT@t;)-",$U 1=ȗN/m3x6i^dMf., &b HdIЊQHğ5\j"Gs`c~\|P$=Dz8N4jsM$PJq^GY׈Ҡ4ptkO +} +%EEWg8չƩA]xnY!gŐIYW)¨{D*$q F'wc2xL=h+)WXQ/ Gf[4%XCJ損Pa^04yB +3ٙĊL$-8hRdwrzjsKlWMxI)v3d>J3CnYvܼhv 9z|`1m +`N_7y:x5uQO`̚($C#Q&fMΆuEXRQ`L{Y2gI@a!cհѧK-Kdr08OOQ[ptNL]6,J`I#ĕu=ADQL/@^I|ſGc!qʦL{>ggf0.JR]rOԸR]w 5{i, X]ź G+)%k\i"5(&)TJ' P^f/E Qԍ:5Kk5gPﲑFL55[!_DhXW] ă'$^υyt!D8 +YOlOEa)J2jBޘgA^($p$轕dpeՊłd ec9d_ +PHEw21hH#u;(DMv,ٓ-  ;IdgzW{8C@_al=0` eC<+ܟkR<0pg3"L2bgCh49r0GGu'x,Z0y2jLղ=;(fnzӸWl88@-aF`"Z̵I6$py30ܚ8oDXr82Ռ~Zq&nMDt 0ng=ܞq) l  {Fn^ +4Zc[,kl\bI+(5Sgw!9~qpT40c/[FbʞaRE7kg~D8ࡉ1`V aKb%J*c9dм(=L N7"Lbu9%6W`_ +2Ar+Pj#؂\͍i&YS~IY6e mCۨjk!atZ=x9[m5{z`fyv>C',|du2M JOjߑfI b಴^˥ǐ%0~VjA`Xl_nZCu$ (f_Ŝr}A'V e쳄mgB+ |%v1_#NjJم10tfW +'-L#!<؍IMMΪn0ǟ` cu + n-K#!!?FT 4ISiAKXZ^!TztAwJ6:7~:*GCb!1; 8[]>mS*|)겍@ .(W <:; :զ3 +h8qML(=\2)@xYȫ3{!n ؿ? +mD=ߞ+#QZ)N,czA-\7t6lN B{7qes P)|ïMCcK-8>4 34Lh=^Q HW,7)ӣ3?P8 +!FRd% Hi&v * r*Y6zELS "XG}v `ͿMA7R-̫{f4-?BEf/3~bpGhvcPS|]rߘd 2J9|J6 +m!Dr< K9o)ζ !~H㓃6-$ж|PN *>q<QRpQU?9/amK,?hca&ť6htKwO- G +U!|j l_p$7G:j'Rdq! U~~־ Em;np A*&<8'cQiTr[LmG@^J).bf_yXz|+ǞaV'%Sf'\<]1)fv=%LVe%wb$T*돐Cʉ뱉|^@r|,9Ff g*;7;v-n9,Vl(Z5420 2( νD͗8Q)XE4}<ZM7Jh]5y曲71RddYl=yEpw_L!;!N?P Gk6H~)H$(Ңa~=ЋorUO _7t~o }\vS)uIMaSw }҆߇ORޞ @1Bʰ'p PےZ(<A[lg ym <FumI ! ~mm.?pٓ~4袽H 22w΍kDSRꢺP)a戀~>$4B;>P_]{|WG(tϐf(r>Rǜgˎڵko +nSVfNMԴG<qҘ<35\Ҏ]5ۖt0ɰB;/1'YV4%AN$ErrR:u`+R_.5luG&Bv.̯iUsh_jә!f?PzE2U|\WFU:wX#= +ψ5vW=JEאd;3]助Q2ztN+_`}{"}4*T*QГB~_d)봳4Fړ{f) $yضi9ؿ`W@hMZ,[P B0@Z,a$|3Y4st(?~N!$s0 ͵ +ku{aR9'tv5e +K'S>!1=@tPWfl;Mu8Z]oTXó.?@i d30P^6@(AG`Se'.`+V]ˠ^B|,r:ҢcI]^_T6 tzU{9S5L-H AFce5GiH x7)G~9ї{>&0 +?:2˴elX,&6IGt&s۠qJr6:Z$Q6D0N{+X POM#; +g +2㧢Ѓ^Nω*0sʤ>G VrljIfmP%YU, yB h;+bPi,hvC%5x,=V_Kdt5첷f0L6OL:l[Wk"qƼre̋899C!)Bqpu»!^ҵ HAbB?Ww7 lgHH BW3|[(@"UZK!餍m=V̥Wx`p}{]$B~EjQf9!jBt<ފI \=)GW(fKQŲg.+sC?#Du>zoh.0C0i˔ē&鈀¤|3nJ `.GVk>nX9l +@0*'7v ?b՘ᒏZa6`P"̎垔]Ur'2Q ΨWDH B%}C[7c"))C_Fgl9B s^op"T6 XEoIdtĄ N"'L|[R=8;by!MRZLk&}9EoB1ws44&䩡4"t}n U (as!Sf6CeU3<  3ޖ,8aEv~@3ڑ*BDzL%gƟS mA92@E h1tzE-h 6= |^qD*bT`[BDˋM9W._ Cgz#_iYn!T{Z^ =xYahT<RQ{%"'M86P,vKnr-XJTi2sh2Gaɳj͇Gy6N +]l뫜%[U>C 8L޷8d8(s7QF (a6Ķ)2Li(X +G8x^+g+)}ǯxeQ@!= + X{3Y=aYLRIN+v\)3a +i, +MH^ƒd_w)sC~IPLzfW$dm&*n뫜ThN*m6RT)RŠc U>1n=PKG{ah̼r #moaN#Yl粄~ 术I_3gX Xus]|fAJ̗վ +8bʋң9rK֚FHU[O]Ad xހ?7L|' 8e%1`KQ,S +JytwFWr-ԼgT9ǁ77 MVDFO%a=WV8s{9DUpfB)R|N9E.6݊'5&%5*G*عJ pm}jHg/!I̼޸rqޕ9TVr(zD+="F$qG9mFpU,T·T3s1He>w#,fY\5Whߘ ˆYȽ ^(T=z sUU(K5ڟ 0F9RՆ ă蜉p{nakS-V'q˂Iɧ] +o}un`׵\YZHiQסostqRj{6aΟȡ1K mFvʫRBP%D-Ά9 xg + &\[SYg?Q] {I>xu/e|ڱOBɪXq*3o-D:ET]p?BtuG41E,4 Ż}d()#գKv66o +OX@(X8bZgw@C!'AQ{`w+9qVr6%}L +u I)#Eq&GUӒJCxzC>s4fYHx{DdǒԱ p\lwMgn0.PQV<S72=rj륯pTխQd:35k3;wPgRlx _lBGR׼:TbOqZno6A&F?FyP+Ytg3dk5^l4xSy`áͤWbw}5m:OX:If\i,o%6H2_57b;s7Z~C 8 Or;UMg,{2rfc:Sm?VXY0;ܾOX@u2M,6ª3e3xc 9YxAEIA`YZh`aQ+I?exBMZ0d 8ܾ !I{D>2@T:sYvpDvF>"'oz3ι0\2;zc@fC!!.(zUsF)WcќpCG`GRs^9(-J\s +7\%`7Ľ?#O MMV>żyH|+D3Sry,$GyЀ@@ceJ% =5tn+C'P|oUa$4{,g0 t _}8PHG PbΛS$@Ǣ*_A%$I)rmD1׎ʶFe^;^F‘|ȫ|7B<쉀.-- +AQe|(UXjno*I!CuԐEVAd\d[V%$M-V;C <36لdi$s̳8ȅ(ߧ5y- dnѽW "^m1$d,_zDD9Ƕ>4#XhD;uܦ$Ks9MGjToRn_Ds??O?ӿOO?OOO޾ͭ0'Ϸ`<១6`V1g ܪn17Zr7ŭa~|f{ 1S=V!osT^!<!# +I#*6(g}|щ|BȺb+[)>$;fqZvBIUsB0W0HQLslT Rg/?/zKԾ{#'A =fy_ݧo(?[.&ʧ}! 틐09 +a__$Z_ )Y=LٞG}okzJWGz)o#z>HBd|垦2oC?W-O˷<#Iʀ~6Ĵxm]<4O.)s6Wl[KG'xoξH}'Wݻp+Avos.~tqs6=| s~wB~Xryw6gj˓rmŎ"0ۂ} xf;'BOu>=hKPE:t{o/u1#D.;q2]N jendYG!,aq[m L1}QRP.C̲|>-tyahJG8402~~'woy| DQ?% 1QSnduh5OhZRQ9 ++t/ksG55x/FQ6J7lq((Π4~8/moct: .? /d`!_>+/Bnz1uNi[s!˰!Afy[ _~V۶ܱ[Y9nQ2L^ї!#W~G/ݮ5(}O%0"(߲oyX3Đ|C7!Dn4\m&ᔿ NJa !X?YEŒf]"j7Fp;,=/u[{7.OG<[|0.Sy۶jpaEnZOnS +mҝZ<'Y yF~FIDpFżvp<})?m-)e 4fZjOmIm]SlScⴎne$FV厍 +m 4uΧ6~gq!?k,ݚ=Mˈ}?\]T8o/ND۰H|7QA1n{j+5[+53 یfӃKmip7bt-P>Z~rf /jk8Lp҄\vYExI4$WmhNsy fDa _3J.oh B}l2{噿A=͝9E d;9smsFn,ݤDK:fIJ+@mD͉2y!кn2xOX[S2k\<#c[ݕ]Se6JGy)'Pd+S^~ɇ% e 6G.nrx.ܞȔK{Y!_a 2 +(=NZK*#69ON/scu/scdiS^V遼>H,a Q{HlpEJ|0#Ƕ1R.^8|)YnIak4OgA*&-yY v'v>T 7{Aέ.,̂r=*f *Qxdw'`V@ Gƿ棞Km{V.d9+`b|:g2=%Ǜ/BmVC.L{w>zYQ0wŲlg^\ko>"ۇVx:?Qo +c؋1.qZ{Ɓ6C&im\#f>o2/r/l6kqiO{[rp҇R}brE +1mu0~[2Xtч-TbW~ǖ;meq3d-yɟ?R.TIb%ݦgY+ Du}ڻ=5S$Vk&zʴ=]=htoGX,2_޳Oωg*QYJT=oWWGjCUw.iWb Neۻf4>we8&]8MH,W?e9?e5iQ쀄˛+ TtU~1{`_y1h>q žËm^="㵔7Wb/UCl4,U39\UiT 9\}w%Ai:yȵtܭy{k4Frկ*`r0nW,ίle6'=25R(1\ί=`˄bRsˣA|<`amg_֯4E=C/XzHgH hl^HG4ڗW|r􂅜-kg/X@8Yx?` B4Zxsei+?&@L`9윆)96G?{Bd1@x.;}ZVlWe9>+=1}8R@=9u 7Lvί/|< k畈A,l?6zw  Fv]t;AWͨн_ikwY +v| p5!)ެ_(cթm8iƑOW!)Zcێ"Ηe+ۈʲInåi6V$JyWQ\aTBv|WL2'(k#_{ۆӒNEMaU!<`zZÈtz}:ӻ{2N?RC&u^;: #Ӎz ~{֙s>3fX@t:TIo۾B:)"Mڛ i3=oR^ádlz2}4=m:m)sQt;.(^^x~ۈ9ú9)ܛkԏnP0IZGn'6Ed6NқRnflV|->Ufa$.kOe\@ -G/϶-۾/qa։/`<:NBd,#'xrv+R$SwMf[l{lX ;ŶyMeU32l->L2 @1I)ļTn'L#-@n'LP`敍y8acNB&pGSj+Lo|`N8 O4 %ˉd:e9r%I W٥W{s1. wTbP4^xذe9 +G78oKBt^'EmzIF&KXe!II"M4֥"Drm/Me3,ߍ7_3 +=U YD[".s^Z)& 4rS0(50x&6T0)o6JlL('F 0%׷ ﺝ0mcu\ }ZRUn(Pb! ezү oڑN+Ey(4+΀2yxb~E2I3.ٺfSs.3SuVggOOLɟ!cqeC\R)paĔVM o +$ϮgH( ?ζD:oLF@ΗñIb+d64M /ME+P2 RAV49_\2P6ΧexT7CgDɥsiѦ +z>&jkҷϥY}^A +lWKl3K)᩼yXAA a[Wvݎ]fTɱS2:*;-%+]ێvқdDZmsZN$I= &;e2e0˪ƒm7~HS6-&"֛"wɷsmE=MD+ vƞ&މtܖ\GҍJ.ȔN\-"ZULe9%އnC9j=!QZBf%_ml!,Ů o1zಷ}!oٮG+jl]^KxZba9mHx?OBJgߙ ڮ-6< {yeslvO`6"<59K63+.!I͏O#B/%4o#B#FfWq[! B0A)4 $$ +tֈ*H̨k 6/] v E(YUm@{θ/ ׷Dc/6HugSj&-|y ^aDRf_NO +6S/R¿cT ?0hpj4 M͂//>3ı,Wr[y42i!:޼ec/h%ؠoAb7\~}Y2Pg-N:IG!U*@ܯUFg N9nS@!n h?[(voK@Rbv{J6Vy4N/G@j,#@@L:6}s8ZFrT0EX`Cɂ2{8;# Pn'@`բ\ޜgo'@ I/Hhu9N`u(*< r: ]oF|Vumu'o6J5g/@Q9yWmHr rU9&4QQx31&01ڪ Г) +9<t4)}gJ A+y6q2^~@E6䜁J})"ڰzO?(Z[h05WEWRX|xsӇĐv@vP@;|y\S9( +v3}hw!\y;y:RpiwG-FQ$>AiO; f|8@iy +6QdDZ$]w']ZsIߑ{Q j + ݟ&xv~ q(/jESSyQrlx}7.?."R7Z}EsD}iI҆@`?w7ට׋93t{GMmO?qJ=nj㯚E7Zy_(۝gYj۪f+}T $J,cۏoޛ MpWX) oqs]p| +TR͎%^=M]oӷjo U.7e樟ooTŀYl_pܺ6o&n@nﶞBr[O6аsCZicWm6~UDF6[=BhZ=՚^vTXH-g٦ZG.-u3 ,Y=TDht7|V%73sdKY[|7+Wnqq +-j~0>f{ːYe=O2U5[ɲu͒l˹n'XdU1a2C/Tebʝ0@N|l+14(=djzui%,`}v{ ߬EEeU']:#ܼBb4꛻WE( k#"#Miu)vBC/.F/O׹eP()#pcqAW͸X.@)%C!&.,4@X@֗-BZs`xi/-c%gemLlY2-i\Klr/*y^f(zx]lvbwU,Р8C+xcj∐J۪|UQe"}}2@x.8D3m? :{[yOd!0"b{MEvh[L)W7g)f!5Mּ}G4 +uh&5ET!KT,~_O\.m51ށAhcmtiH/z;;QKcۆN^}@Q/x7 vX˵)̄GT&Md/6-O'}l0n+jmT5(l>=mԗrA z8 Qu58IuP]hI5_&H k28tFw[ roH*1;Lo~Gcin#SFdRK{352)_2Ůuys.b۱h@2*%џhE R yTߨ>i:@n!͆d 7!Tr+ng! /C!1~&okj>]Igz8 Yѐ,H0bϗO?Yv?Âm|4Rz=}ˋGOKZ%6={d,_~(K Ӟ/ӕCǻʷ$H/?x4r4inx켎N[r_%,O|,Lto wNQN 4,s/xѯq((۝S6))#oޗ Zownb@6WgDyXp=/7e[S`ӭ8c!LjJ +7MA'K( ۉ3_1 h_2) :Y%,iC:Oq7D/G]_Nb=@ $k5sכ"D"ߌ\EK /&46v'[xfc䑊chsi2JLI:5e99K)`9ˁS vOj3yӰ1Cx\DB( <ޗ`zb^<ʄc*fZGѹ@P0!Ӛ+5+ v:{_P;>whK90%ud`]թO.ѩa"ۊJ + LHxNb0,sjnWEaV֜9)DtM(6gQ{*hK4{U6^DI-1JصME˯sN +V4C}H~s#W0h.sZD&?B:?UXiZ m0!NBwcPv@ +TbOd[3FՊ+=DRי+u3Ϝ[)w@3x8JhL-hZG(uA) x%f }ZQKѹa7 Cjb.uv4v[XVb:R X| 8Eu˅WS͜7Mm:B(:7RfYYWG:gxsc¢Y@vM}*0^Fo +# ܄zMъYV!:{ac=ؗ5}y9eS,v"88@flf"jQ̕ED,Cp0dZђmM} K\qs:A"LeY5̶ׁs8ey+¥oq$Ґl;AdMwU~j+l.фeuVQ1ΐ–*1Ѓ,: 5$M̼ayhSZA)ex$@V9PGCc.huzE^8P0L8Or7A|ʯ͸ (/z7=m+J8U Ĥ@:|=20UNXD;e)+SkPg/ΡUE³Nyuy(dqI#z 7Efb'ZKN}K\j*h@>7,-k +.E[Afx^nFVQIh86|ƟcyϲR6icTPqtaijI#zkVRsa=?Cʤ%8L\{47T6^qj}h"JM`V%qmN=$@F!DFK%hT?$x!P1W^Sy/t b +BZB6 36tB(qj_JϑG/B'e~5~+եL%NglfHtng[̙6h>8ה)3a'ё0#z!'Luѷ)';f[4 uTʪL +&E"1@Ys'8˹B䮴jk_]%W_6J IRZ,u B&9 V5Wp9K[[[ZFVs ۩]M{mP?4|yI"lCN~|Jo}Q|S}hb(?y,1h]aŋ M!wH:PJq'!He1vumel~MO!=!?ΑqKGK +3 zOPBpc'Oj%`E6!h2&Q&ufv"b;ח]*\HhEi6v;z=[z-9#v-" +%MGZ`v;)T <k"AZT)}R5%gD (T!!=c_=fdpWBFD-݅4R0;J3J``\IV3km{e?Xu6WĄi TLQ-?SH_ˢDmEW2&,ނA.clӌӹ]p=Ѱ1Bz&AΑ ]!2yg];\Hy> s/ ø%46©hbj2.޾9l5-tZ7g:(q*m:VBYw}.C,'3D u6kSaT<(Y~2RC&ZI!s* 3G%Vi dԔC_5=z?ap*:=b_ PH& c:NmɽT`$ǖ *3maWBѰ &f)W̯F nyh8$Ա!k)la iFMkG#$H4VpTL{׼RRlPA ӎe8ڭ;ؼĚ'5ch5WL^{̢3|XD: X {bY-[|ܱm9.3 }cFԂ5$[bO8J 2챾 6DZsrQFNa)KfeN>SLLV6^9Q4"9Kf$-ܑkxaWk,uv e%Ѳm#9sY229dd< 5va͹#d@;זfe4{ uWIk_!}E)oV3!ORfFx \S) S/"yORRcjY;0*fDB (&6:%!G~/۹ 2g~%LIۿ ,1 Y^.Y:A+{/$HvX>ƕ9^AR,(Yβ! =5*݈vrɫ5i1nmeц̝Cns  ➿|ϯVZ`2~ o^LjQl"  Z+PG;6{F{ߔy=n,ߔs(_,uك,% 2as+P54$1Odkc#iIwntLىTsnCr!+>-,]r@!)\ +^C19+lIoy +4FDbe =;~[pp5WaBqrd (0]e/~1vm^젅@'U G8fSpud< tC- xm~l\E_#@ 8tSFaD/3vqaȸBp%36 J(m瘘q̰G6%|o60RkFJX,wfڢ /x N#;ٗ>qz<=J Kݒ9ߏoiA8CMuW.^{Q/YMsޅl~Y/˒# $Jit65Sև M(F:-1z9 EQ(cH&4Cl~\; +bP~lhґ Z#;[ ͊pm/,:%f'4h5H͋G,NC:l؇(5ۙFh +Bj hP3N +dM#/P\p7DHq F +4| gJyk52=c +{n=qXF$_q[V(Ʀ@e}CUz =PitSAfɴ ]MzT4=ۻvMM|)i`XXIc9!r;Iٱ\RfWt*_Q#-`m` 6ъ]W?.HNF:='K,M{=1b|FiLzhRSئxm`uyΦ~#d8֍yhi)Z2(.kTˏ/IcXU]pz:uiP/\b8WerP\bs:L!;ju?֒Qb2~),h|K:-IoX6Vu @)YhjXX, e8 8;Nbߊj"^pCH4\案,Z%:/dXu몐C.:G2J/BlQ GhXbAE@ OD\?`FsK_` ̀<"v=pzQKᕗ/BI4Vt6qL5?P`[SwҔ~PͰ-]%xLQ.-]UBҘq*SE I& +q IqԍzCPt+eBSABOz!yI/D"]R)pQ;Y AFRW  @mHGcc5RH|hv }9KIN0nA-دVH K },x8y)\&I]3\.g& wDD`;aiL; +mR!gotF:U DZؼ:;˾#[_sgb@.L?DCbK43o/]jU&ӒHzﲂ>?=Y+#dK/rx+ ,^k=_,JP4g%hS_%HMxU ഗgfIPIЃJ8\x̀?mq?̺۫x[ul7:2߯T +Bn/\qgXSpҁ ܄[ 4{|?M0"q^F 抌8{^%f'}QiXom=0缊ױ,o=b%zr̀ܶC-S "=ܨ5yhaJ/ҕEbd>A5{tC.rudp 뛨:Q.]ak]ݤU^Wh/Iy`^#(8yr-X06񎖾[`YK"LW*X7k/-%A(` +3@ pa3a.@${.0N {W6Q:4{B8Gyd&.Z!hEdE "<-5)D%jAL/rU ^b9" ߏb`T)o,/r:uDƽvp`0-9 elO%=Q)@DQ9o]2ywz)1q'9I`CX#C45JFklm]N$:'^W ]G#&p6zJl*#??Jwp! wHA ¹sZ'B8x}=HkrDԋpo) '6'Q*J@'r2X + -g{BHkKi&-Ez\ %@O9GӤ}5>>}C=qu0ˁ_wwr6EqTjD;=:7nmai_uVi9Ic|ǜH0QF~T&BqW丹KCCȏ1ѵ6<%;>6Sb#B pf:IΜp}A⇸"#X=9/9Rt D19M=Q\+,w(M@mr՜דșRS$\;I-zkpTqiVZ?|+{ ڱv"@`ZPߤ"[yAt8#H¤HU\{}G\R&aaVYRBNN_jо5KKUo?k) 3a;֩6{"ͽZեo9X}N+)$gϺ:(>ǽWJ odQDe-!ʽ(g&8mW@2zIǸkQemG* {=E8k\a9L J 4JZCf_xxHbפpQl8Kqe@}eO&lc= +fsii1v)\ 'ÿ^KSD'g,pD%CP A:"DRo`g{QuJzj*9]05ԃ i.2 "|\ 6+s=<sT3cM4rHc+Ňnؕ;jdRZnn B[a$ʪTwJToo~cZ;|Qfp998+Cru/F|"P 4lt+ 5Ab|Mj7RRhg!/7|]dr\AQSjo<?mޚXGdUd~N~]rpQb?¬M5ucIw)7f^1yga\oq {Ѵi{c _"[Kw=PC6* +x=1F_CvcRhd-gT'jm$+9/SH_W9ToĂVB +2 /`0@xTJxgTH/ϠLC4}V0 $$kX0\,ZXWk %I!@LwEHP\η>LK٫uv5x|B05Nޏ[4`BDIL9^0t +?dd4d|n:DtN߱q-GZN)HTk) W04JU* fZ 﫻Mֱ8sVu5O5-Dzᣌ(vP9)T6Tx.'/-/w$`ok՝Pv0(0^!uƂi +zWi+&## mQ2  S8=b8 m؅3@r^ŝ]=䒣$:Q׾@^014~]ԃDi l]%'U`0I,#;uG=r%w|0[t@'G*w63OuZpʁhQCW +> ԡ3˭l7I|m +JT ) )F^dEIRU=b9_EJ#C1#EzөqB(Hs9ԢMΔ(%7nzF+qbr6b؂Mλ0 !܊@ɍT:/M +ra5qQ9!J)7ԫHwSe{\g* gIᠺK`f(MH~ KT* e~ ı FӔ@ !Bg^& +ux5x EZ xY#4!A,# |"u5n#7 JAU}r)^[xbU=13e +OJCDA BG8Z;0J(N5䝖9b-'n;Jj׫# ^Fw"ʾ^%SsX)HW=lTS[D9Pt2 D2Ca"јiK&Fg`])qLKVa!%u l){$9.`1b sߥPx!(=ٗ#*`זej1&0Շ"PA:ɘOjgTϱ%BSEےװc#PΌSdlb|?eV&NƣC̽S#(F7YyQ3be={(0JmQ9+_\,UkZptw2l#NNXPcՄ72.Nw;[;)?+aQKHAWO=.fJ Xcy]2)/e !$2֯$gX4P/28 +\Ӎ{U0]n8Sݏ *IT'bS,C#ߢ =lm5t=RO5 [=8xK n\F[Z`(o%o+=|q,Ӏ7~1n:Y 3ڵ;@(݋~U0Fc.6@1UXfa9a(@>p w^oD&bp2ɋ.qc&ˢkZ)INV#MuU7(XÅ#NтH~D{Rr2֠ghjկd(qP 1@N1:m6U[2=닿c)s!ș(rw +4yeMrj5@qhcP/Pj(!C.SI*\d 4oe.PسW,݆ɘ%V=epnWg"2+p 9j\eD!i5ӞVP#z JT -5vE4Lft{V,cnwE* 6l9#SSh9$VXC\b) ۪`Y[/Yu 9YA\šnb'jI h/i/6D@iJC@%Z=VĉæK?{CWC` W\=(G%B[JI;9UzٰH҈[bInh`Mw;ȬyUK͓ ۸p^-.+D|JINw !жSʝuZ\1@yc!M2 +xطh^wCe [= +ɏW)YMDRyD$Ujsn$y.f߱Y/f&`xq-dӫ|sM`\ u +P{''"@l'D m +L"ќ mاEm=NFI +w(!Mj챙}ǜc-~SX@ܯrN9f;ЃPF/[vHlzmj܉`v<´B'Tdz s{b/K~'qwsT!]OL~2#eȓ v"Kb^LOF(wF H-Tԋ<$F؂ĽĴ\Pn)e}S jX2|()F'`2B{ /&O*k\6cͻ-Rpa+6K*lIQ\"2BSV^bi(aϳ% +M\V)!d!B'h|ԍ(B +,MQִ*R4!M,K x !rH<$@ H Li"S4zc9);{ړ]\0?])%{}ZIa9w@j 잤"v* Xl[B}!֦^uGT77hœޙ%ǜ2n QW7)7Ȏ,5mpa\~EOxzOM1gfFL]b$d`ثN[|[3LA5Pkcdh SDʂ6L]PGp rZu"9@^M A!Wuu1EPI7" Hc`Tv2X6&c¸ė-5Gf{%S=AUrKҰ5X-vlrRu*zH +e_iZ0{ +;kAje_) 'E\3P3q7BzJo4K[k)hk3$':oIQ(!r\!:!L[1^x\1@ +M!"(cT|˱H {#K=?Cz~I%Yy„f+K<lyC< Z쁎M+oLjFSWU~!+].TrZaՇJ79+A-Rryf4?!R)S:c'"fllxmBb!بÉe4("rRt.D1rG}$?_uYgKH:sSIpfBV҄5[e!d^L1`6^J̛,n->.=X=e10&Rg>b--`|;`>40VIA(KHݣ+1iڥWWy ++Ib˩;B}Jh;O/ i~eRBdrR8cHA=X$4Y\L>("D7y+.mT>,Hނ<$#ju{"eɷp`a%׌ƜAOD7=X&޹n$O.qb{؃D iRoRl6Cկ @YU.`Qd6`{e""R>AiWsXAm2^D!9jrZ@&QHx|`7%_Jx(z)~,ȭ>vw5{Vv|s8`Q4h[lO[A( dO_ijFA^΃+2݋Mu5]D endstream endobj 26 0 obj <>stream +hFux(cŻ,ыqyh +.GQSC +ѫ!dPPfD2ӍWUЈw[H8K{hzH#0'V%s_DnQ|?$D *#U..yr(mȺּqmvU~WMfd)~u[%ggKe$pKN!p1Wﲩu͎>?ţD^T_ߟߨ>_#Mn1أ#ܤ#0sJ\ +Tct9\\"$H"=lRW| 9 iŃS] lB*.=R|>U=h \<pᤗLxt!\>GF$ H/$b51h3Ov~?`Co9K7-,K"j5w])r#[IC ~DS[Esx#U1tuEg:ԁsEzv` +d] n*T_:Je)CI[uŅeZ5KZ"@R]ZVw{NhA:tW?(r9$ӂ7y^MӖKboMh +v9ٛ+?M)dD_uO瑒90{q8v_m#DdӤf"qdTPv\ud4|*b/K=O769X7+!m)M0%)$Ԭ~xuٮPOz ~c"8h; Y%l&f;G6lI{~hb iyCuKۖ"`{u c:6|*PK!tʧ !2O&tUwa7Cߣ n &QUa*3w$W߯XlmA +i;=&GaށRµ%܊%ރfGjǯ5} Xa@vuS +ᐰ1PWy-_C}7t`TdҘn6#-#*2^Z=qlȿi3砺L!OfA +͍M-8ŐKƍ)I;JΑ}\.=]տE~+4PKUWmpqKAJD'Cu86 ^J'Vq}bH0RVXGJy +{L-,;B/;@=W3^RMKy1Mצ* ۏgGoT/9lFNR E]RWWЯ-S@}Џ(;b!g)YéԜ)t&5?o7ﳐ̘jL%I-,űw,>ʗF+@s-I3iLŎ܂Xmܾpąo<(ry>v>gqwv2P:z,)v\Cn%n&܏A@Ra;6qSu Bo6 _ʤp5}NnpGaYInvA@Φ9_hh7!Hmz($*PP|) MޏHua]o/NS@BmdvK/E) +yu^඗覙 .p0ir0dUzQ`(lY; 1x#p63Y4]0ʤnId̃+23ҟ)*e~j6꯳k)?s3Ik2ih{5~'Z;F#Ě:/ɭ2ЇkAɟ!g'wG[/d[^ 23[-%} ȗHKݰ_ +~)>Ct<8 jΰ(H*ֶv͎vP$G.d~UWKc +|? +oHDiQ`)Y_awxupKTff>">B5 8Ԝ.ݗFcQ>`EjR; Bg1|RmCe!Vodz=$.a)lm#(RRA7s^vۄi/9X +)׋ͬ`MA5}8=  [/4`C* )_K)ч4^a@7Wvܱg\J䙺@a=[m 6Z|pE0X,k2YԞ&Gi\?.;|vX[x= +E1_ :Pv:k;W/$M;~ZF"E K嶄5;vaCTn5WXA] m$hv 95B@\"pm{ldEA{0-%C6GwĞՕ؏:\-Q!P~`x6<QRaD6AηgZ$x0Ym]>p :X2X+y5*Uռb9IcR B&'QtbTj{PQi0 CH]I+Ps= ˫!VYby˓{\7Ce )!9P?ڟ&ܾv4&i.&'aY5*,o2cBC,ޛ_ғ<Òk\хԀّV H6+Lb{*S}G-I"SjztSM2yR=Xa?cbg#/l$ow\crb!I5޽%xL^e(DBJsUGoF2ILn]譒6%'݃\}q$UCυ(RYdKMU"!s|PfBC+UQJi#_7T 'ЎIV :Rm^\ujiKCV+|q$e4aaAY#;pipR=Hh3z-;=YZ+I YK(uڊHd¼١o"&aQ ڑk!@ҟMCT "qpgG.7G2Zz`o ;OK9Apj?19-iP={ 0+|HQwP9o3y`Ҏ wz *xWS;]w(ˠr.zڦ ^+&} OP%q9@IPE,Nרw4硦I1S@v`BbRn%Z@0'эHCaf`XƢ_D*?x{(j~27 121X$3 O*zQBHrPL(Ӑm p |Zo2i(-qgd`=r$&E,ɉQS{%P[(#N7l F!գ bbJ!F携ZW* VŒ#k80\H!Q%8L4ນ\ 47K+헓:P%_ZPLg)YVݸ}o#YN@O5U"w(9=RAdO5zF1n>I-,T!3B76cp<@!5Y{/H4/z}I&b鹯jI-Q2*ôH8KJ}B8= k, Yl"ZCa2Lw[r~"!2nfj)-5HcJg]m{u&恹;Q,fڹ`-~gb+/C&L]r~‘ʕP M܄eSU=CkC{oT\J%U(d!aIn;W/9(W{Kh;x!# #\W(C0Kþ#sYSp@hc"/P۪8.;)&W\CE(l%9 +*sEKV3Q).I/i +|ĥdlVFf6\n=|Gy'45%tX;wnb$(.&:n *r1U"K%!H.^*ݳac?]1N#29NҼ‰"/!ds4unxr A T:@mAv"{.tkuMUP(2~oq؂ 5J:jVgH4xDlSĞQ۸Uسt>%(SIJÍ6O\(&BZaE @3RzĮ")CbJS6ݸ?`*jJJ#䀷 +̻t}7݁ᑯ-xɺsUlw  HX a?=7z-#jnFC,0 +8A`b0G`K/R1)w\Sy>K +bwd~k[ Pq VyAt1$DHR}Pcn⒟j@[3w,)S3-۪ʑ!?qNh@=zP̶ |OgVKa/G-D^ 9~8?ؕO x*L+(&q5X'wU_R:(G(5NJ9Yt@?~il?ǵj`I2XZ>YLoaKPpo&EK{\$鶷Q;<4!^OEF?_)0ՒK"XU~HsEgnpv! Gq nlWµK`n ܠ|Yg[J+0JkP 5{lDM_%]7<صʡ8R߃b #+h\Sbc}ΕN7،Y%n({s.Bݷ2L@}v0 `J%$1fACfP1 Qo$ofk9KR ̠ab'i>eiq>l04k7GȎ~pwh/_.,{sʛ4Y 鰽~vAq~9'`wn%U E%$#mw!q>V вO8WLE (\,?!KdƁ/Y+A OZͨ~([ +XͣJF ePlIHC()PV>}ciuT +ʉ.1&t 'Lɴ-Ety/$Y@`k?R=T~( ,TmѪʯF{Ԭk&5T~+*$upqX+|G-CY"G3w/_d!/PɄr9m'2G +B)1aj^($ !@*%(OGWFL[RM-;=J TD2zh,|y +/P+Hx!v`^cAЀ%P[#Ģg 36:S9Fpa7Xo ' 0ako%m<\C=;ƃ6krƬ 񊬪0C301N<-}|<(RR+~!Ȇ렰"o:5K<$K:0FEBAA}^xÍW|@L`3RÓBG8_v-> ^ H4=3(S$D롪fHI6Nľ_^OOWDvA2ܑlх9!A'*UGqF^{C!b{ϩnRlCI9$րfy3Bl!E)M;@iiD$`S0vr-I@ek2lBs4^%xUBRd,VjP·\$N.K-2Oe-VB (XHBH9_ag8_[5M՞ȤP6ܞe-!xjI m^uMN52Q΋%NB9$>㼬+*jCN/K^IW g( + lO͌(e \l<}K.&e' wqx{`uJh|5ǶTK?6/0]KMrv$1` 7idx +Ri/}R fA噀.sDX0(&uґwMhb,F̻yF s4A:ɥ 1GJC6{ 2ʜ!Ez6Kꏑ:v -͚d*fk/p#0m *GdjF*%({Q4 8Ƶ[k,v* t٦â̧ol^` CBTc|Wā:`ԉ^s0fߗFj(0j!дD420PR&YxOC@jIz4@"i;Iդg-7R(HrI逮 N QOSULlj<%!t_@N$@('7d_;=f&T*%Ca%E%SXE匨LSvn_ImkC?.m. ~X?`Z\O +^t|v%ugK*k8#s tt] +Rl䰊 _CV8Cᤴ:h%qu?__0CQZ$MwV?љւE{їn@ޥyb݈.rDPc3mwܢfT>A)dk0/G{29Wܷ&n= +ZhT"ہLOszqe_Y/  *ɰ-GPQwݾ;9HĪ7 X9DjՔ훹2̓¨I\sl<.Y,w"{ΗS +ؿ\/r] XROjd:4*pxu}TQϢ@א\-G^ bCXBô8L) ;(,\5вw!`o^i,u(eB +ZԂ'"Ԙd n g$̂ȄDP;17-RJYz$1D,~%Fi⑭A]3ܡ?7T/_1$"98*⮴_r{_WK88D@J`+x?gWs\߯YPbCYv~l'mˢ$D]JCR\RN +xmsG"IC%v7-Edဧd$ ʟO|eH%(g9";A}$f;dogDo"{ߐ$ +T Lq?DG६׶(#%}î_UF=jo?K#kELlL@>T@# +1{Ű{I|ҫ1͆Uң J0jΣF!Tk|(Š$RLg͈7 zS=*$u*@ %էiu䡁]ڋ I6(SY)b;w<|3pHW cU0&9~H2FLTLFTZ)c'M{HHm=+u!? $J$"f&G 6>[w? !Q\QE')TX^7pHz;@w[$4Jr{ŷ6wrMk-HE'- )Z+)nRf^Zj5ɠ%bFr:L :if=-Xޛ$NQ`?uu?j֎ $Z,42t`M@zz}U]BeNG1vmVU=ˣoI,4?F"uW?5@ ],='8zcbN'}}YM!u]8SVqF\Z21DQ a^IP; WfX9SʋI1ō`ygǐ=U|{x;gx9铧- +)A&Uv4;+I2\a9N|q I@de]y]VnWI ' ^쾉gtfDZ)Vċ!Lwzln +[gѐT QAf@I(E_+,9=q3K΀87zU_ C1א%9i ʁX+/2뼔'cO]xŘM,*)tqV%&0ij 2qd4?3Ե kPޯL}~34+S`v +ȝYRl4w4% ySG%uz+߁҆W'1 q>n\Qaر'E8◙,G2=x5C"`_qeD~IK"fm)'5}n'5kl#e^tˏJ0ebŧG<.F8(]!3u<yF Ug.FN܆]16RV +@V빃.+H({T;f$[Pi/]&$U׮eU-#c J'v@T2XPviQ4z &=B} ʋ Q﷟M-Il?$A޹.C{T>85^F//Vvl&Tgs&E6 +!]Ԁ!Kn1%>?^.O4ChcƙtOaYÁɦ6et,S.%Bd剅ap~$,ݻdf"aGIP"w-< +Vk/MKBN@"s:T}ܕLS-faG~IGl8д$yi~~r +ݓVN6c{om~اzXf?dT&/R.b_%~5EZ`FSUbp;'q(uɲ,~TTo2MA-$DrX{,\Ҝ8-9}%lCfڙ%}z8:OboG$2]ΐd'A>Ȅjp`aT8b[6|`َMA;> +ET^W?юs]g'P~0qoaGꦀlRA3`IV|Fp,N<J!HshNDjͰRWj:cJEyHt$Ԋ"?ïMe ~3Lb PM{ +E]<5R幈ZSQՖwJXsИe|[cI(X tJ5'CAfPP [l, 3a,C Q| A9'%6-C0lM!YQ}QM7vU׳xcX}u9IbKNtոTW+A}áA2TA* ,Pbf`w/r֪̒\/B++ꥃځ$,[rR#xlIk)gmӞ |6 O#~ 8CGf&X*i-Hb4{fLM3H|zij3ّ1CaT-'DnǛ|c"][+?2N`HOt ץ3ќʆk4%MB*7֛} Jpy'jyn$sf*JQ a!jz1n r@3 &H q@$@3ULiS3ڗ/um_K-ݟXi{ +]+nah2A8t$[en2 BK%.kg(PIӁn*_xEұ W )?uɶCZdJu|^(8fQwGO%bR 2Qe'4)Chv,ě/C( C mbp @"~J%EMHctKTxhxN`dC +Hpٽ QW`Kع~¥ƾj!=fZDKOLY88ntqZ9m-knj^%L_vH) Rv$~_ʗ ׀ޖT@KL_ho/<%XE> f埭[M)b]LnC@ 7"AdžbqXjv|['6i1"qvQK+2˦ +BV +40[ J# 0 .t:ܫ)s Jt1&}ZuIfD*K)ME4>b&/*'☣lX
s.5pu;Y1R[y\{dV\0- 1:MPma~{Mo Y_`V$mɇp ++f]uײc ,"tPYM]Yʬn@y_:ph{]1J`cC!ֽ+J5ʒ{62@+`F;x@ۙ+6"!U@eԲdw +.;m^K:VJev|8{iA=_̣jxD,j)[KeIG67L-L3nޑBr`&SRW>`bnb'ThZq:cJ])(-PtIqn_Ot:DX)pMLF83iVj(oj}"qBL"6(+)V.aDW(h^TIB JډO~bcng#ܔrhT~]j|A({ڭ #Nb*.w2ETͨy<ɳnNbĊNmOVj=w֞9KUu+hjʅ`J⬆yUR@E$V<|nV/•Ϋ\Xu2[O8=@uiɆ}Vjp'RCRFXkﰠ"m%IQ-W("Qa +=G}:bEWa"n#2+5x!Kxfs?r9#:ň*Bʴ~0Ywlazx d<f[k9YV!F 8nq:@BogwT/ovsAK ʣ*mH& T>/FR|0bH{:Wߵry9S"s%/:()uK*dtg-W&=W`}̭r ܦҥ| 8J( ̏$R +$u,u^]yB}jB(b!Tn[} xBz5zX.ϸ* +CNőudY~"A܎[]iGدs:Dwأ &[eͶ@|=>h<;8^W0]ቼ9h(Qm()9 BEut&OCrDp_kF&L/I6S4eDz-+nK;o[)N3)o?Oo7o?7?~W~?wo_ =~'w?t}O_?n7ww?[j~|lS~ x͇WW귿Sο?ovʻ̑#㷿m_\oRq+/u{ko_W_6Ibq"~{!yYb 퇓L9o_$pw rIXZEYu%d;D!KG Zį Qû{^_}! ;=SE#*j|э7q|ظ||~su`HsXq*dyr#d ~ +wGJ?uBqʛ~pRqNtsx`3wD9~FWϗ|X%H )8,*eZ=/'` ^ |WC+C4ƙ 58A8-?=eh pv{jJ4x~'􍞍D<_/b9وN4ӈ[ st:<&3>Ύ+ GZ< +2ʖ?O+h\IiΩ|D4q ĝz En. Fˢq8X7y_ Qx^xb+U9+=#-uij^ +NPO5Ϗw'xyibYqiTUNSW [6^> k|ncw^A=;~f<8Wk'1{`jqĸrN %Kgh1C[ _ @U>0٫zvdU/55daL373Zk;ss;I?}ICco|>b 9<+9 {QU+LDi1֕8o~ξ1()w^A`ިE^k\"ƙOA I:o=!'1 5rN< +HٯUO*P\A1&12\3H=X8i+x)ΉFE[#*M& '8sŁ3h( _7`+3L76s>s\?)zXqa ~W]^}bm6_m{%q8CWϛw(;_w!T4kFعz7tsyi U||^_1c8|e79yL%zs#'qS4"2v(_ +ԝ<'ڮ◰;F8"xHD#hl2cѲohDcobfSkzij'#P_~- j)Y{Uc_?.׻95]9pz/aM4fј>l'YZNWl n9TpǷ &kjmB9mnbcl7;UF9|PhuA#)ϙJ?gs=ƹvD}'%WnV$[h9.7K73iO+4W+rNGNHŧ%z\ֽ\g >}DJ!oaBܾeEh^ y)϶ʟKcȠdg}|%ϔZ(4휠?DdPN^s;>2N$˒["ª +p=k}+ɸ|\@zyظgA)9a;)ۻ:2=zyDL]2,vO(R;A:Cɷ6X4^H_2|1;J j>ypc.o$lONPy邲3crx3{Oc ly/WS (@dڟ/R&V5VFDU+bIgX=x_9Zjفb~LNPW_$lFd/ +˵w<Ӳxv}ˍٜ#yVmą P"n#{yEL5q~yYk[Wl'Z։FLJAԒnrc;  LJ}/ o :^FMgcN4QŤu Z8YryLEWR6"쨈O{u裝v{sWzYcjGXep4SF;\Nl (l|V;.kn,R\h#$(|HgW}G'oyZ)g:kW{[FI)_"N ]9 u-f|6.,ˏ##kq.9/,CS̬S`؞}0jMAK)Ol<)೾ )O}`F㨙.( Y#f>1`?_؅Iگ^Y9аcOQߥl_!+O سc|2Tc\]40Yj%'w;6~4`AP':nZ7's4 oF{k kE.@Qys^9zoyhfSoQŬ}YKɾbFEۈ)NrSvO3hgnH0/|x9@try9g/4ڍ̜j PU8H3 +"w*iiVk.0x^iyȭ-?+<IW ֿ udew]q[/йfc;1o}v6%>69dN(<V{'n7{?gsԅGE˷<$fGg3}Ŵg@9끷wqs20%{w ެZb09cV-.(?tVghgKT!l}i9`PxR,w*;:+#y fF+y#X NNsͧp+Ϲ9|Z<\$UN4A +E!jYES̩2} |FvL4=b :Hrn<]½+u/ZS0YFZDQ$i8MMjq_Q}QO3S O ߍQB30\R n(23 W9\!ٕ-͑''Fz]}9^տSA6'"#|2EĉM Oj'gE*S/AK$ s-u]gA3O\IchHzWc@X,, u?_1\jL +I뙪{}q%ڕtu2 d}ؕ29ۙ E=\{բmmfX=AC_jG^W~r'/F=MOLyb1Ȭ+skM*CaO姜p9}(g5WR͚+Y65nq"QQ x61Ո|JN=*U3{]~K&֥ḽTc04#y۽1+* f>r\×8՜F>RǞC^{ԨOw{yaRȑ?@9hZjvqW't[w#ժ'rC7.~k;f6g:A ΓBVoD=d:6sF98"%D.p?D\ +]Ɖ#e[3.C|I:Ʈ+{*h泍ƽQ8th;״6E|PYON䏽ڕC~y+ZY]m4WΗyϼvzwkƔ됕ƉhȀI 2xGh\'i9UvrOj^4]zw8uZyH}hHa_sU6aq'+\'x?^l_'ONWlɵ5#XrB9ih~h;Nq1HcɞP^+dv6$m}ngD~W"l/0'Oczʅ|$5?4+! :)I,#",s=n\+aW,ܫHcgOeZ>=PD5j)ܠOԺѳא~=TCKz +}y·8A + P܋EΠo=_ש-@ +ٶg˙ IS,9U;(OkYN)2w,lA9ܘ;1NPj\s'fAcs̼HuhM:9oL)A=._g}sIm3K$lLSBi<2=|&~>|6TI_˾9U3D3xwBA8T!c 8HrYgx'ȒCwhl9fkJn+?8T!dt G'xmLJ߄un5+$I.! JN|sإ;'י<",d*?nwוdW䥮@IY=SW*~V3U*V ^~™2E. '*]#V+N0F6gV&g Ȱd]\tN%#2ΚJ~I +/bH렽ƻhH o 'ȓH}}MۜąF` rRdJ4bCWT8 n|YpvE/p, GCu͚[Yя9Uצy]OQPߣ,_29I9=iNk Npbe r"^'Z/vئ Q, +\g'H(t'yo +/z_ +A{LXQ'xr^{E?6f}*nqZJ^؍xa  |r'YVߣ3G}#>gBIh*}۩(KK!U5qGWő4o{.FrqEF].~nwg=@ٞ߯@RZjU,|UBL^:xU[qH__xc9id"dMBF)F}=*A@?(Sc- "*S}U5jQI25ā*&f"kRn,lӇb6P0fj`>@(5 * +~Wf*Oz@fߧ +O IH _7pZpZh) G_JW7NЫfd~:8;X;L4d2+ +LK^„_VPL;XU$Fը)bս 4M=8D|XK*v fP( J&0cUQdkZDb*fPPGG |jQTi5UL#`HhBBx*2x_}ʄ #U23o /w"/f&%sՍYCxS58F(Xg*KU6 (lõTTW . n6|@M2vBTLQ4zv +TW9a&bh( +3&S/㭎Û75$DfVi FJeB 蘚è5jd-Jn͸QFFRME]Z +ex U9 J +h'PUɍLj,rtu5Hkh F 5h5D苾T%-S3S5k`5ve*mIlR9Iwv3hоf q/՗]ƒT`WyMՁpLP_k*bGa]8AZ6iz&Qo&̔ %UFPGIQo ʽp/!/S5LT(' e* 2T2S~Ѓ$Tc\Bi +EhJ ! +,[+z.*k[Ruؾ-̭>T:a+YpH d + F}K-?=q]jR`5)tF33C͔n ʣ(XQjl,\+SF_ƚ1|Ш+o'eve`"RTsvezFaߪۨ:0cpMf+L]E>*؎u[eb(rYF!,E3]δj/Q|#Fd⪌j8b)^UVt1& +jjbҗo~0R\Hc.M|^hNdF8\}3HbY$j P4ZC3bQ1M57>AmJE$K4ePUK#-S5 +)@ß;Дǀfqqje :T[̠Q8}@]P[MkMRoi nXP*Ə؁NDk w"a,kjG*pPz}}B.<+B,RS+ XLjNJdh;1D(Ǘd*K' Ȍ 4Lq1*:h" w6C#Z31L_(R(3!V4GakM,gXtԆ7Im F]4&}MT oZ*dh,V."JPLT¬oTfFMe7 獪53AOV6*qҾB؀f 2Uh43KUs5jeB'hD.Dt|h:MdgUzP^ #ԵXEoYZHKM?TqbJkuڨSO#@su)]h4ƌBH 8!.ŝWxUeGv&}Pc!j U_j"W׫˪kM]KbyOo(ȝ-7Ԝڢbth>(T!IOJbj;)5]"WL 5S#"p@?fX .;\bR"4DEjMJMl c.13c?(ri&⻙諪Iy$lH•BBJ +܁N-y{+5ybۯn4EɮrPTO`/^+s0 J=Vb5Չ:xMZ ◉F7c1rPW[6S: oX_fj`*l+>0եLUUDui,V3PRj7i XD#7r,W>| Raȵ,TIdLF#S#qbwQ_R3d* D:|o)DC0cB'QK)˚bmj3[%VxR>e8S h6GHŚ3&j+5*1U:OۧvPTb,͚R6ҭZ柺$n,VѓKY}Xoa VF+fm45F3 /lV'^.>mЁz}^A]3FA=/uiH]D@1QCՈC+5\Z_VZȋثQ,m R4QR?UVjd"tWMl&aϫ:Eoz.nvt.`7XdY曑H5JHSoÍFu<ʸQ@¾zu+3ygbZB9f1TepU+F?dMs\eo&d )ԁ!k5447)LS/>O* S;5vS_7WhYlgNJ/U 妿cx۟NqfuNJFO EDXEu^'`pLR'j֔YA]*0B G,, +<";U.'UE6U vڤ9鏵܄󐄍,Z5xZLфXPXRG j*U,Uo&$jND|CaCDn \PPal"f5RO}Q2SUVH':kՇbu }\cm]5 +%!j%mi ͌LjPa:UQ)̇E<̇e=Cn!/Nf="1Ssfl #>&w\C#١Cў9ĺ|JpoyIJa>4k,=$ W8 +G_]Uu&u/$)$3SIL`p9\\d>~;L#2#A oOIΆ"NŇN3,ds.C3^1C( \B4.n2KmÅgRG2ICA6lJtS7TtLD6(g48ԆGsrFπ9a<'S^a||t:(m$2 "y#XǚՖ3h.uc͸kmHl$Vr~r61p +AkޡB)fnZ:cC.:H X*Y.~{ :x 0Ȇ׆{ t@#~=Ϗ{  o!OhSaY!rWۆCzGuO…Gs^QC8A|h8exx!?ޟLGg2e3Q"yC3Cr){6d҉LK|b4>,{5RLlh^?L`t(< Cxx&8k4%.yL6"&g,^MjB}M<sSo*b&n*?dEr6 bz,:R>}6U?U4k%%?2à6HmzXX+7{g S}*}j+l 8 ]P~֤}? Q6P{@-l s曁8@GuH~}S1ѥQ4W)TNoNg˦dž#uiKa\oPxɇ5;t I e.y6K3@+v9F1ly „qS<#܃׆,D:*b?7`;C>x"p\Xx.,c < 6dtHf.:Pt$0lXh>aX +K&$k4 tUJ .f<'-Q6䐏>I3l<:s<:TytT$:at&\ǦM7MerF.YHɒ3\ڮ9l$h,|O#4*@IkDMlzM2d68g4M8L=t@eLZcHn"dz>IHH`l(7Ⱦh33\zC]؆~ Yy݌*8,$r.`Dz>tTo«FWp klxx6bR6éȢq Z\ropЩ; Lm& 4-n9Dt% +,۾Y#[nV5_r}lꯋ)؏ +2WLyt@2yX:c{&ml*B:}]9.@EŠR_Dd;ZRzjɃ1_0kXk`R!'S"10; , +X_dc0yc{V`>D4{_)j{& +N,b맂|bܨ:p5!'٭ Gxv`~ |LȾ0ȾƘ2<]48,p CCg<oa{]DdCXSl]7l-35&rM9ÄeϠa;gC::q{)hgH!{G9"WIe~ڂ dž}J#K!΁lցSXc=$t@H'g6(kq®³=iCܑMG\` 6X_`P$YhS|H{(}u~:dP;Zrh- ^n!l ;iac |ֶ qjӮhm{Oݢ2ap6q$4GRw&k{ ^_b'*:ޙwcM33* ]_qr'+Gw!K4 Aa|`b|8|(KW nQ6U0oP't0O."} &8i; +k#ԟDؔʇ l3E1g#^<Zd}=Ekn]pt)5A=裢sFT'Vǫ)7!Q!y#51鉮] ~!kCGb\]x`&03pt $w`Xk XǢ߇.lXQ. j{!UKN;kqAoHCκjä[=Il΄Gki=t@ +qK[O#NAQHv"#yn#c,Z atC:;a\`hFށl4hXW%OK蒳23"KlQ}Q|ˈ)x "\){ߑQi#?a +ߨǃ[=)jMb>Cx yH@-:Xe,멣illyLk!7G .p_%!Y ̧V`3[璥eĶ'+_  Μx[KE PE `'^%+ЅʉIȁlv=Y&yS?Sac ftL}* +4F]*=)Ej>K_0I0>F6B:슱| 7U|)wO`s"2v$)L ~-? :0 ||#AF"=9L^1OF>it%ljLN7(K!1 +THH KttRt̟wcK6=& 0_`' Ɇ˩Tb4Ld$举2yc:u *8o4l^9)z;+ooҋ&>sK£K ii$Z-/a$ox53 |v&wuTX805ȒLjt. *sɂSK# s;Q1֔2 +|z`l |X/$H߂L8<%bĄmj55Wt%ws d81[2(=O~=HIYGN"w@;$qNG!?9Ħ8$\ph> +|b y> {4x%N~p} A5_ | hL2wys ?tM陜#@'UWLR9AF?P >'KD%cEM3*\7w84]ވ{';$\Z,)x%ٔ'M Et:W}w%[`vh>` #r5kT1]?SemD3Ll|-Ͼ:`%S3b=К A)<&q I( 5Im_%T ܣ 1oC\>;{2'>"_ -a.tXT|K,ɣp| #ȉm\\_G82;|3Y;1iۿ~&oga[2T fd9sGŝa} _Hڄg|0_Ll0:'!y`r/fvʧaΤψ:O0Uxl)Y~Έ,8|\ Rc+'CLIBeźԽhY0b{`bgI x 1)&6;>OR l~:{c,8z߈!v ~5OLL؁^ c5+U~g3Ph٦3y^;VՈc>Wgl9omVwF^uۘLc{2NM ؆-8O_6o/~ :'tq)Dٔz=L\C+#,{B؇>PgzOP+*ܴ@r z0`AG <(u!@;!ǻsgC HwqH^<]trF-z.zlR46񈬝HbKd L`XqQV[BܓFv֓cp{^O؇W5N=>@8b; I ̡7LNA,֥;F@\ o8X3*=aHY^CσG*=pż-4u4Z2j ۏx%'2W )g Cy) ݔcI稁ce Nɻ#E|Im3!V\@V^7%ko|M]FU}rf[0!oyRS_c ^(>o37 ;s#~gZm  x).J8?1 wćN6א"Sn9O`.!=m-r~re6CF|ŏ9&%̰ΛJ!Gy(rH9 &'s8ँr +JAl)? O~9'=Jyk@)|o9> p!UBe~s >|=릵~)YFoK6n$kqC<,riH3IGDv{C쳏ΟI5zG=L5[+ 1e鍳;G6gdˋdRtX't91"nQ5 J>ZT\TRC;)53 kHPhIm}FP.h.vn뮅.Ą'n~MW\1>@`Q6K7j|ڜhl/dۋDӣ"| H_yE〗:_>?qΡytCI>,.cOQx!CF#A>97uk,$+VlXg%q<p'poQxhh8aָ|"!|Aiĥݹƛ 26{W1x[z۳ duTE 3 +fy \|蚩\%L\`(٠D@ š|tdr7_P[=uqK@Er,U XuV5cϛ(`>B)\jLC+OS{,a~){/ko.o[3 wVgA7PswArB [3DWl :tZ,1aI#s !k/=g4. `lSgStʈڷ`va+0uwVэ7W,`6~ +wDE}*2"ͧ +PY @ +]yr@Α2r<9r:Ta*(8}G|02G5=xG|+|uڷ*Ύww_n >{/j*=|tx(:o|jWfLM:6I,9 ekolg?o +:j^G^1fZ3}U: 0q<)T!.Dpn#B +y㍯ĶD@H2t,yz`:|^\RtS1U~l  Oe +醻+54v UxZJ?(>42O,cVT\1_-֤l*Ϡn: y p#Kت٦udErGbq$vo'ax7+`-gPaa5/8p@H*jϛMW#%c(>:'yfGT]2!PrGW0av^IiCLRfr.B'"R1 l>Z`' `b l6>_Ŝ6uU{}Www-[w5>_trfgZ?"aP V(k GHn[rw!.:i@\@r1tT,0 A.@7Zzٻ2uS3.0dD8 ]1ȖbumdVߓqт|X} 4s7DϤ=%+pΑAx C3'z2+jҩ\֮K2˜du[9<[sB!`c͝g$aW8>Y$S{@J\V8Upt1ؤzm3K)jn{fe ')KW^2񟄚i%nFdb@D9ς|#0هQ53w/9}v&^ʹ̤kو3)zp@kQ Y1>8H?1! 1?%}c3t mi1Z>D`0Kfm Flaq*bts%\ܽN;0 M gF8[k6-p,~Wx @vAo@TxwU;šg7GS|/BmA~Y1x*RۧI +|=[fL9FkocgĊ w&jnن/XĤ_8/4 ,hD 4csGn8W|ΤFMfGs%Wl.Fp?bS s;cۏGw!{ +u~4*G|K6>3Vx VW@.OP:q*UxސekAax ұ!7pE叧c&A^ +]p@5egK&DnƴfC'>/[Qi3Z{YpÌi}l}{ cKȊD2= qTďͦlyh!]tl)6=YEzdIh yw!ܽuCh~hz*!q9Te)Z6HlzmLu| 8< q)פxJ\CqwdۗdyG81Gn=:iL*|E9/`񊠢QDtD2atEMbw[lvF~[~%J}O;>oT}^M~XC{eÝvxMQ-ѕWM~ ƌޏd}NӍ];٭ͷM7٪+@ʘZ<+{߀?# 7HM0D؞n{jj~jx&!ko\s2ySwʛ/M'l g9ٶ[@θ̽ /z ~ҩgE'׍-W+l{hԳR[xF+j~rrM4=[#7k#:\a;y͓Ne</NPP^A:mrv7{ˎ>" +oWLZF}I:=;;d '++SW̸;Ozk9Uu+6jxR^zYlYf{讣{Nd/kɂKlʑyl%pL+E`)?A1PCo'C%zCsHƷM%wLWiYPˉsK'/,[mn:O~b^?O!/S޾aμu&O|`S9CO^9([:5|Tcj{y% +N.,Xm@Oײ'WKGO Hm:)Y`3$oA#})MyЧmeO>pa>ufw=#k_[^.w?>(4,~BdE`ExF_?hwZ{Zyo^@ sT ck'eڟlnQ${0TKZ{9>eP4?_h}fw=6+Dɂ>4qOƕ=r+でs;ֳ>#_6ˏEq'>E~g~aOw~lw7ஒѳj{.>oxknzF`kvtU[t9!j{Rm_k[[%˷?!Iǜm{n}mNg}(OgK^3 y59|zM+5ͬuY^UXwzW(~.eg0ݧ$ݯ ko?S}xǾ~|0RקgkwS8r䏿leοv=P{x+w0C|zinP=VpW^n{J뗒߈tzY*߮F";Go{aBc.jTyuk:o>_>e>?Ϣ|~E..p3I{OOsy[i/Ͻwy~R\Yw6yEO_ǏNV*?,v];G:w=b~b= otu\Hcrs/_n>DxNjpÏx<[W_dm]ihX"sa>LUU|Q`dpr'#mn͍SY~g7(LsGcYR1FhcO?r=w;Xy'#[}RqM{^xÝ}͝x~Aq͊c)dsO;ww3_])q~ɮ:חޝd?Ieov??{۽\lӽ=A몃Ww5Ty_F`Noo l~n]CGBU&t@WK6?̨(~XWSNl z߲Y9׳rd>X M2yu='/^w}wXl>ӿLY߳gGWŽ,O7wMq-.Zn>ۻ֢rثOVl9Mu7un\K%&pzяYnOW;>;Qp]PwȮҪ{eKގ/?›]!W:".މ,Vp^XMwËn܈*>z3fBY*G͕(`?tf;:Qx_~6'zkCՆ:edI5J27_o󘗏Sϓ Ϸ oe)({{z2?m+ K;rf4mw|y{%JW]^\RYp/`GtEVx < SQ%GкkPVByԻ9-n2E7]rGor.ݸdJk%.?yUwn9w+l)9w6?RʜOs&?NeĠF'{^A|ݻk%/ _)._Uy?q[zAۋ5IݹǗ.ρ%ߣy:w+쵸Sb܈-\lgq;Uy#ԋq?(}/ +F4$Y*nVwV_K̹]G|ocuW+Oy2kVl\ W}}ᝌzwlćq/nw7?۫kSAG{y;}mq~y~t;}+0~a+|ݍ,s/dQ%W5=py}u^7we[oqvOtTͨTz%,nYÆ2=7^*tϻr'Y=9C2&Z*=STY?©_;\^+wxwYUGH}p>ލ[~m>1}Kxh[ n%޾x+^b󫽥V vӛ^n^~^}/c/V HHQsǽܦ{]뉰ב`]̽Ʋu"ws]g샻 GZ:J"VT~r\IjOW).vx}ՑYMuO*J#_a^w!yJ=UURd2{W?E/~J>m*1 zWX͛n^kXz3īdNȎ]/bݞIWwR|%G.%ᅵPzr\鶫N/}wk֝쪓WcK/\.jG:!y +u+qoA./o[y;ģ#"Sn岿{hRo<ݻ|Ktuګ$+%F_}'16[)14^!7VxDBLoMԉQk{k,*v.|Ϗeʫg7_L.-SOW[M;ŮOT!\TSx3rjն;nR8ϦGkFwz-}ݣ[Fn=8~cU=?o8?2c_W>M2Kod/%zgw'&"*$>#п&HJIfѓ,5|gW<5]gso_e3]שg9nVuV\N-k\RR)ҋeSʿ[ː>.=})Njq7J,/Zʷ |X埽/V%8[.W$sI-'Yc淋!exֱk"gOד Jt轴$Z렖i㶡iK YD wWJV.+YZq!jZYmSʶ_H.;x>̅ңK!ZŗWvfTqݜO3Ny\QCOB ++*ֿ򭭻dޤyHGgJ ''ZmJaF%LHD(Y1{crxⅸIeWː)>{%(N^)A6_-9p5ZJӶ29?b;ĕXkuSn a ~ a"~m_?v}߻n0oZ ,z2n͞[>/ޤ*sx|Ϊ{EmޓD%"( Q9CUb2DI3JT$&0ۆVYQn[y}κsLsyyS#59b1l ⦳ȸ/=q1W]įJX/B 1 }9~E߯وh4# ,;E+sV4<_|b~ -'|bA峅.4nVP0T۲yƦCӰo0sI㜰ؽss꣈ wmN\ b?aK?AZP_s)F4A^u%ν _4n9[sڎa6BV;bď?߃gM.B(2{=\u3\'?MpeÄ=/nq)!GW=\F޶r]otRAӶMUJ4̭/O?:t~Smo$9Qhtᔊ*:V"#y3 [\,vba//}!v}#< +jq|ohSAw=kiL~.g6ѰB8`dcDd5Ҝ 5ll4 \Wr(^yKo6+ +p&cOa_ٍw`zzvóip͙!£i|hel+?SGNEc qd 2̌!Syt\d7>=3g4eA8rڭ6 ؊^ ёOTQ %p{qŒ_]+y{xǷ +~n;s;r-,? O2ײFFQx!n}D4FLt쐩4vds?sPd#BYEcWvJ4}IVL?sEd [ c \!ye眩D(0aY,m2['I.9hj@%t'vJ}Y5(g^OKԵr]]8ܦ8 ^iz9vNˡ ׻_Y/ =D*S sd= 8l8bL}<2ױ\7푥J4q2lƠi+45M\7 VRmW_ZbIa轰}9w [8%]ϗ]5[럴^u£[YdR2T r,$8ziXg} 2לDlcmf&z6Do1z&23Yۅ 9hX4eEP&T!V&>ptI½͵.n:W yunf}7evݺy>7)v d8)C 0՘G%cenmv8aMb, q|2&:9XX屁c`jHX<$8-6Iu˚l9sill~vۇ]?<|s'P%5x?D6Kq\p?YOpFy8!WBfc}&gxX,ǭDVNhdΠ {#FsWS_Pxkg' {rIXZ~xxigjn_(z)&//#K(ՋWBMaƫ&m:$;/K,'Q%SGhC:C&O\4y qKpN)Bf3h<9朄T9L+_ƂWK>9V~-|"Eow%Qo{9#qOvy~Skm!^oPsֆ-2jۯ7z~ks&c@bhdNGA_jYd!36aAg";ϵȖ@#4ʯh/ȹi'5ZTzKwQ3E\8!a}S33a {) +zr88wλEĈ = ާ<}GU|]1>cDwdn+F]֢9T)MZ0fC}ժ3x|\K-zh8z\y%W5-ۣ"pes疟ƻ/B׏)܏W|WM+Ƨ-Wmÿo_>[ʏ!?V:q/Zqyw +*:)4L5!0ӌGN¹4Z& +F6hG^ќh=vGhb-Ԗ$tU]qWpy(BɍjVʤ?.L믳}^+ +bM !'9&w_U1>KnJٝ_p߼(.chpÖ⡚fΏ&[/R6{yDo +\. D <UhYlҺkF. 3=~$b:eOd!iz߅5"/'!uk!ثx߸zmK0qޞB }~˽mQ&?`G1Zh[E;XF06Mƾ@(& d~#Ċn \iKN\|J^:ݷtK!o֒m?x1-7SgSg3ʏ]S(o] +yt?Mэr`/e=eCRU-t_1i ':z4@56_&$:+*9mף>vNjOdn$D梄՚uEy%~Ml +>'%ܓC%r)Ԟ_e5L-7rŹqܙag?ٝʕ;P'u&M_I?8f̞+CK +53N $B +1??,þ{C'Ox|x䭗ɵw?m +{ChH}v~7Ƿ炓j4z_|R 7b" J !JAt@?ۊisTեd ی'I$FktR qmB4AԜތs>-v}$u"o*B>{̃w#sSW?+<)R>}PP:|}RJY%)n6WVa}в =爐{dmc i\]WKTGR/$Ԯoܨ֛&&76SMvl 4a+훈'0[jX p~^|_?G-o`*E`Q1a+v~0irbc8
 s0^qQd;?':&G K-Y;ڃzfU)}^~ionsv?З>F0ަ=Un/XzջcFx^pkOv'm۟-~\":"20hr'4zZ`id^(4~n䆜1eO,̕ъlHG#dٕTi'"GasVFo+ іhɢHע]xJJn̓T*8t;J qoy7Ln (aR`UQ{QҚ33̞tJ 9cdWxz#++)!2U:Sf)m4_zqtE{mQ;ߺG^r_d_IlASyޚ竂n^S^^;UPYz>Exl:c2Tc2 +1F&#q=Py"wiS[~y*93'Kn6r[ĒDz4ʿ*OHhYh̅(+4Sr#sMBVe֌;//}GԱ$ݯWzf+쫕Cr;zI:ͩ,&~2f?jshd7a-`ץ~{m^(< g':#m>eퟐ퉖Μ\(lR>hhG"dMlv%-]})̩4g +1˨+L^d!þPyvlӥo[#S'+.}k2gKҽuƲ/mŗ ȋO =O'L=:#ya;! Ia̼|?>'wtrY 3u=/b禎lmXBГ.yJ6^4vC}40d۟9GUJ'o22635GgBqpOD/nEɏ=Q*> va_u~{>$KCґrrYң?^sЀWB7B͏jZ%Bb% +PiHRG +WDNi=\6̢=<r"Ua!I=SBg@opxq_Z~kWYN]c;'YTj4J_7'g졷 uٰM{'SelŇ~u Ӫy4(e +(;e&Ӂ/ZLd5KT[bb;z@KOZnZTkog#=f|i +Sk%aZ4M1FϿhG蔊Ѳ]8ü @3tDMUJ担|3$뽻 4 T*o\LnXo"zA}퓝/w `l:=9X.'u?]}h:]kJ嵎dw(zm(}*M݃WUdju?q,XyU<u\ uru\}EDX$ V1vhyD/RCR&kguOyl(Lqz~,ӌj2_]r`2ĊXu +۪PšJzp s^+:c q` +hR=Oq qdI>+iVYB/rcӨ`TW*]Aa>"%S5t#Gf:}]L2jgj3WOa;dzi[L#zCy$;^L{b|Ͼ .|E{/Ճ>^.W*Dϟ QՀ-zSݾR1ÜywBOim>2?G~,DoЖ٠VHӖk:\HxlTr!' TX'*Rʌ 3*:C KRu@+,V|`}Hӫ}=AאI2⳷3}]'E]=U}Ӄ̎Ѓ4w``zg15g8AS"$ZH.-//du-ù펽oi+$UjeTe^de6nD5.bd>{!eν +a/OfpLtv[IctDt߃A61 >,2 /k;>:4dOOI;AԲJ +I;IO%d :L]]@7:8*lJ3SD БsʨtUAdmph=6bNL>^twbw?>Wj- ҽO{KO%o%~pOc䗞O?]}-~neՏjϔMzOQ uFTj1~*񚽬T:BZvd,99̱G+p<%8HUDd'dbSGf*K7ٺCv̮<|jh.1S/nOeC7t7VKfC)Wr` +6̭lf9I6łhl`ŋvLds[,dF:!SN(#z;ے)[5ρ>|ШԆ>i*૾C'Rj Lq0_߹Jvqv}pu?[m<0 A{ +k=D kXS<]/CeJt{gf@w↬B;j޲  XW7Ug<[9~o)􏃞?hGJxiPڀ +B*h' #.b4o2hSrE}4MKcGJkJ5A94U3/}މ=Jq]Z(}GG|cpl֋>Mw L땪W>™tc9\&i,޷~ЋKWoI,Rƅ7Q=dGR!pfk}Y{1FdV& f)\%AS/]&4 +t4h{Hwum3/hu2C&˒)q,LVٔj4q}وIb̬5 %4n*Jw^:W5jpzb./@cM).̾OÏܾR@3NP^[s @{ 4GOie0g@ÌLR>Wny [ @l`AHA]&U[;.qr_yDhE&81z}B&@!6Z _ +MW:$IdXӆhǍ|YD+t|S㧓C߈{= +<ܑңD˛C^^^9*QB_>d _̑D톃/K֔tu峖 o\kGLP&k° ClX6hO jFb@+ +%ϦךkRF@V~v[zF <ХM%B sGxǏL!аgpNY<6$P[,xMm4@j}hi|.zluŞ/ϯ8Ezо!ц?樬=rg Ч5<2!ؗ`Rp|$U*: j?Yr?tIӴD o\zq.sHy4oe!j Ӡ_ +tXRCi5LAnH?M '{^'^ѧdGtq͆&js@ Ult6ԱWf00W_q4𺁯Aw\wd{Bnl :媓wbh qw]Al}Y-Gsms{9cLb!꾸 UVvIOO!]1H18YzǁIPu& Y\b^hcȵ_Z8o|.5`lեlUԘ\r0(Z*&@Yyh6hm*vߓлd֕Bc5 tdYdwaXԙh1F1<+U/:Vp``- ؝_{)7lv86ے5_=w_z*)1]W Q^mL-Cу oYLbk7(D/'SzdSsTlي3'#NNQ\-ѕ==|i.X`}: +w`{_yc*poNXhD&hA\$ܬUFJw0ҁyHW8MYߗkY7cHW lY!ccA[Pl є M戦m>T޴k +H- n̨cK h甠1To|4ĵr#/714.?l<x$zvgq&, g(udpR%CG\iUBXmkl +†'}@lv̍16N]m X6Ԍ|Z^=Edt'|m`gU; wqwv['y \:+(T eVms k18FDKGQk4D" -P6xuAYm K8+{"ȑ^sb=>zXچ,0AwA;ЬhAgx80>T1yR$d }a}Kv +E3I.0C4YcEJ`gEdTewMמWLSxFtnWf8P̬02 +YqG=?? +4lieFM}ӂ:}B uSy*?ux! ME4mX0[_c 1YTVGh sqGoЖ2ja;$uţ=HbLtx& D(yes.!?w/Βr +5Ov$X#( +Pw /#Q:ty|dH&|q+lB{»זk>OVh8 |N^&aMdv7V +Z\J1_f\]gIa PՃ}#U; [7׊YiIXN8f; { R`gAbgV YK?Y,c5!Rj)0wA_r – gB; z~>񊒮ɠM}k4_<Tg< tLXLcvw}+Y`<NX`Zo1m Dz x$#r$"ՠv>0rI> P A*JCYi*6 8P)ĝv@Xe=)7Wwv!恝{% u`g16`+W(R +GŦGOf8~ do +0F΁k>kz3U^50*[}ȩ@ 7uLf!FB;96η]T9 1yzAYeu[s_.\Lvr"=ӨŶt$쬜V`gi\T$sy;@7&,%𓁝U8R;+eATr^`m}oo@N,0ejV uFՀK9"`jdXx<='^? Bv൯2RQH~$-G#wB զ7x]X4f 6 zlt*[-mKJy'ŵ ^Kߊԙ̏@#=6]GOvV:*7G^{^\6Z* QmW̬3;{JPr![z ,|}:2g$*M.; ֗9@J*) +X^fm;K^l`WsaqX7QwZ 9'ܘvk]rqUtp"wb9':pbuH\R!BJ!a=KAStL 15م5O\4X/am}ChK`mVwJۈPT+ǵ*0p`}s)\m<̡1ԭ'ya ׄ}' `F!ly|㥥+}`8?sm[&Ģi|-'OܯS u.]5_W6 +Ø0 ٺp_r>r}1Nb7=\\nxвfȚXϪY֋ /Y|K FV󕹭V$~-3ͧZ|eݓI5_k"µ{.ɥF\\ïߖt݃+mjp>8>GbDi5fb="ԉjpEj#k+K'$!Vhr!iڰX[`TEL2”s[LX2j q=( 6D0uc4KW:]PyVKvNToÂeB.6O0€$:0 g{|F}䥇aMD^;Y-Q l2U^90@oqh E%,6n>Ol3 ͇gÚ#Аk$YO;?m!u] +,2C[s|6 XiYx";s8S- c]Qfu{&=Z8UsֳƖrgHB֬gk6\){  SxոﹻJ.Mڗt=vp]Qy|77Orc'\T +dnz3"ENK|o +{ݸܑHzQQg&UQgpNM}bhu,R$1c8/"X)T#,O{mo"`S9Z֢بX-`m|>lCfst(ZM(=ty鄏B}"p`mz3pϊ0a LEVwYpnIѮiXz +&5{9\RÆ&iS}=ޗDl25>:kRqt4-HZu`/ oxj!+ze߅ͤ1XĤ}3C.%O/6"yJv8eQ5';zvxb.\C=/ǩ0Up\!>L' 8o?=[ϖ9[B>!y(57=cw> n/g-a^OžG =Y[ g%>% q֮ >ܛЀb,>{2`-8El:-$h4*pNpo\sZ9a{iMyϑWkprFyR!ab 97eityŰ>#/ߟ .n `rtį+}I9`r0?X[l u6d˫=hVd Q/',.zjUl|vw +ɝ]St-rplgyߪ|TEA%Wpl>y}%p3a"nDzTx}r&C^%1Ϛ޲ϖH^ZL5_^T66Kl\|{H +vOWHo2VX8媶c$M3ue*dLσk|iJ)y4&Pu< rf bL^9Y +'A<-aϖnifP;˨73 ~Ŝ6V@[XPnBXM>p^a0ǹ-LJO#+`:;cT| +"A\-d꿜9!b>O~(aʌȽRM?1i8߆m,:+tP'66EW] B/ڂRuL qas6uM&vZӆ|r]u|Vo +97~alE%j'\Jg_p_s53Qq`L(3$csOǧt|:>Oǧt|:>Oǧt|:>Oǧt|:>Oǧt|:>SBC X/ދגuIS:'%E%G'ć$[;·X?/$=")h^} eֶ'ΰuޜs%!ֶ/jl-JߔF 20-,~jqH2~J7]"sӷmpk]kmE3 Y; +D[#td Vyr A?T!CkmMw7?DRe4-(Tk4@K'@]WcVǟG."!||(.yW/cԁJƿ +Zj}4A)2QLVSחʩ(Ϭ52|=Ӷ*Y*AAZe +zD+쇋MաSk)񊆳K'9;.>MzIa8h6B6@L0 4(uZg2tPEt&cru٤}Te7a>PC4-KVmf2iOGkk@CBm#zfɫQ6q9zlB.9z37[Ht/.[C_$3=-ԗ*X`{ ؞w2h3>\|.](6VwL֤֗$)GǍEjqY]\\Pp>I<ߔ)>QCL֤C¯QQ/;Qk T<Ĕ +~+?esF@?W~:b*\-R#K3 +t$ s=|=Й z{AIlLHb="QC~4(AS@Oe2XIׁVUF7em&gx|t kI4U$C/#KC)a,_}b.NBo7uM {DYV=3Db4ep0gB scd>vej’~fU)>zHR#bo/+S*MeF<sZDg "'A %c-zOd<~>ItXLD, YAGiB/Hpu&b6:AlNӂa[sE +{%SL@tz@CC\m :nRĪˡ'*_ +^ zDW^+}!EL.h.ȓFoRvM8*ʺl'm2G=-gPUsl :'*J +4**ߤظ|2u1hkZ -u#Ub|]^;tEh#@i ,Ķ}Чђ#m=dԙF%sWMzx..G)gN6+xm#Vv-sAS@YY{2 +2=S|b!ѓz]WbHbzz9&_ G|*A$[Mtב た}p=@O]ʪԀF㸰&GjdM.4Ct@c~RDCNa z + +ob>n˦A/5s*"M/-+nӡöDG.,Yþ9yh:o6~x 誱DI6xq‰@lqzwGXs¼ĞAk 3ȅ@f S7v0D%qfDi}hEJ S¶>$l3g>Pv=2cea$Ea5H<_a^n%¼=9чh5C#-l=lA4؊ö Z"gMmﱂUҏm (@4\5Ұ/ۗ ~ДekoWնOs 4fD100PE7 +bآHw#9g;p{{~~b˜s}c>umh- -]'8΢0k0k&Ff(.<} ;>4(hs)S^*]=Kc)Et[@n Z:&:U?y{"k7yÆ̞ h"XcZsW/~^rA9lEӄo,a l!f& c>qJ 4 @' +h3g.O`R|PIQ6{DC8@ /A8mt Ԭ11&%w6?d5C|9p5hB5nM-P`h@YT_@Ƙ2 +{`dV *жx&fpFqAӁު瓷~ե6Ocq|)EБf_:6hnxtIQ/+1{:, +_%>j +Z1Tоחc?O0p, ŶA +!FY/{-`/~S$b|.#n[ը_dlˍ(f7t阒TJ^ +]Is!p%#+&ԩjԄ 媠2K\X 40.{bej e@\:+S~Ih"?({9^ ʟf#ӁFhV[ -&C^=aЁ]xgccg}&9HsWNbC O" w}2`=rmH/FM\SCg@-/ ߀^aMx(O`|S.sqOzH>W~DQˀvz}VKAxu5޸ +TCE<97Z=fND~e;G AA Z#rg +WAW[Ș?~uõ'M X 럻NDUng2~(D@&EF|!Zq< Qqx/#c`MHۏ~ \ze.=BnGF7Ħx?=&ȧ R*G4t`͝FӦNx.x*-}4(?)1C:͐?R+^/So:? b]!>8քt'Z +̛ iZM=|koN/ Y 9-ƄZvL!u =>a\e어~|j{hPӠw-:ksi%QM]4OD539T}:5y@+kTטpTb^oev Ʒ0f+ޖ+2wH=*,М?i;r|+ }^䞤$vMx"j +_>G/71W"DcކhX-I/:OΆ_ Is0_4Gcb蹘">hlq+s1yBk˱^DaJ(Pxdvj =?99"KyJ|V~HKq`n9guT`%п~P<X!va:h9b=D < fC:)1WCsu^k\}o.52b 뒥Do19ȯb4%GIנۏZf- ~*G endstream endobj 27 0 obj <>stream +A<(wQnPaE~)m[1ᝤO ?.?`ln/ G ➠ +ixq[;\w>hA_qܝyWɔ.:C{&QW:|u"E#=Iw zc >75,wcZ௨fB|(x< p^пuB3ZМ1a,A{LhӄEGGKkܟAxu2<5t!!>ycr&iDz(N:ZkAzX| +Ό|"F1J". .N)d Qmýwxr8l\Mo!9!fzLX8?Z=&n~y)[D5%1s$ +pK#%spŐX\xd,\Fo(xB( F zFJ/xU\W,* +:mt%~+5<&1QWѥ-l55+*ŰCu%8j=<>/ :q2SV:wT˄!7W Þm^q0T;a脢 +56TN)S3^nDyk)P ++\\YJ=[sa]_ +csX W1لJ-J|z^Y9)_|)DejMx?فoϵ^U2` xxLmqzH@r9m$ g4p@l6qTCoEW.)3QwVJҗ@UuhOH6({sFOIxBh)d TkI۱'$xpGm}оW0|1g UuQ2qS7(J*xL96h}"}u.x2x3Wb iG\]=/{|111!= +>Xa)J TQg+UuORTa|' +?|'/S!-r q5rQj&Z^XK4#nPO^<:Q@eAK&%|.p2Ή@NmQxFxP{.+P}Gu^|axZX.9b}eX;eE!h3宒d,5! (a}%g'^Q1"ģ/<{7ǹx!!vY?W4 '^1hre_k􄄱%3K6)xck:ڄv2JNnPf# +|yf.o=:/x]#q>ĕl:Xq(F2w4wO@\%_P'`?^5t-P%YK_ŜR&ὲ8RCTNOO^vɫS<؇P,b+nd4kחaOH N9π^ 셡mf[53!倣D:BD +~X}9Gdg{@?bjhh5Ox +Ç|O̎xޣ {rր rX=XU'+}kp=pԥiT`s!p^rЫF]_ { ^ ~veGxc{Tp1b'L@v0.XA>^{瘹xOehZ]]k&Z2j/_{")o3"rU-~o?gb^n!eyp2%7V=}.񾽄J/ڧ'| ?.eYKg0Xr0h.nm `]qJ랰uƘ>qi4<hPKY@<'g)yaiL%apfB6v%'1;e7N!֗%qK U Vo "50f k؋~ B }~ q_LQk )85C<\\'Ea6'_y&6֤ ASӎ>BiBkAb )OɔJekrF~(~_lƾAVVgawCg'3[Ԡx\Ϻ+>+ e;/74_6ƹ7Z1{Oxr6<EпZ}X煞ǡ +7YZĚS(cx$ar}b1zEh,@a)l:FM{m[c{Q?2@M`||hO[1yq,yf$ 2 HGm~fQ\ {s+WKaC6 ݃YPS9kmE<ʭװO=y楀GqbO*XS\Y [b5lDVYل+{ + zK//lh&K.Q,#lk(pҗ #=ScRy[i/ +iLX&h[`DzTӸ֡蓉jӂoYkՎFe?$55):p2jj>}ذ}CJ%B5k+aXGEx3EC# {@z!P{G@ށ'LZ>*[ +#aen/EMgg^ѩmDMz෎\pX< +JuUqP>a/!=ɐrxcJͅL@ +R.`VX*l +4v͞OFn":@_.OfLhwLl!̥Lr>A feV3j!oփZP״|zmFdÊB 8DV]d Һ\񧙴K<|3wa\|)]\o^iHWa N+j/z'"YXiI{fҌ$J]TwIiڏr.W*j&UE|J%Jj?] \-a!x9Wa8?/(A}dn07؜6i浙STXKѩh!yoUш;`m{]|QHtY,ɭ9dW|N҄Io&ӻTѡ1DŽ^q 1F_fjz0bS_zެ25Tj +>6 忤&eX >(W{]z]G=Gq}֋@7K(a_vFxK[N rO>l#77!_| +K[ĥ&]0yn֬dhຂQ"閰,Imj'̬%>/6JŷEVJr5k2٫ڬU[FGož[<-"Jo}ТSY_T;x~(H^g!󒸿6PC|9M5f>}\%zGo# uvtEmKUz(_=(>..l8+"ީ11-;ye3R2 uz'_[sKR;=caf>C+ەoVKoi,BF&}'i25C|E@? +ߴwgJG}y[vt~)>üd~>FqzX*dʔuYJJ}$}Ee5E&!3>هbqn){eZuȫjV飊#ҧe;- }ˈޡ'F4 +R7jw?걷H*O>ꢨ>)0ے-8Ǽ:T|8Tv^ .jWD  tA!Je>\<$h,y59Sa˼=LeҢP 4q.Gxakx&Hޔ_4wQ_?ZͿ]MG3C>Y +bz%jI3[H{UǥolK\n_b'~QsBrI$j7XJ&496_ a­b+q5OŃ%O_|د}@x4xkV4Wy`\x嘣d.⾦#ĚtJڛڎ~Z2ٗi*FK2'W=GK,~hmR$.mn>)*n=')T9T\o7e_vӯ;Q;2F4P+nXӯLgjz-n%X6Y']mLh͒0=_Oi"mJ66] S>ne + 1kˏ4-?,PǫhaU軩N|!H=mcDN7s:^NNjLO?0Ѹ{]k4|?髊s&CD[V 2h $ބ<1Uu9*̲9%ZpWNԱ[1`qsΗ2aRY5]'K+_<9/yq~2Z^DZoϼy'ƽU–7qgwQΪ㤠1gSj%vs19D&.7M za?9^O\M-wIp Ip N<ߜh F"L5H-@5Pès)[e}?C@ӊ*6h`W&[- W6m6b/n+ueIR7IiI }ud5ȧg GK;JEjK#.4&&z&y&&WzJZ麶"]BS}ܢV9Gf8߬p@]' xP#T}aAfJ5l(~pL7D dsv+ɖגN6JtTş-x%j~#+~-gium1q]녃=ŲCy2Ǟ)<ŔA8ɐ+zP#+?kX۝CO=HjJKkrZy!":bpQmXa]XIuHeuH)&><1jkTPoeCZq@YדH3'z>sy+=8(ѐI.I3Ҏ ަ agC_*TJ ?IKLz+#N愴齷*Ĥm[Mpr@O+:$EwVMKDLH^CdQMXauF7k~Mh~M*4R+]eer7We@SAA)'4 +YƩ +Ĺ%oϚ4G6d }j&Qaɛ BT_~1zT:WGE +[-k }`MnV)'Zo%\mW!Zr;Gf$KLuuOlLa;\u܍wo +LuYJh^G;>+v-q-:ޞ% loVW]Z7X˫anD_Fq/+v/u +M* (O;{\^s|^#w(OAeE^E2(_II>IѹQzo9=לO6I +ʶlaޙ6 +λ׆^qe]jLjr{5E a GPvpnjqH~l$ر*"6'&=jG}eT䨳)Ǜ"=oͺEJzC$%ͭ~Gn]GfMQg2SO7g%p7`C=Ϣ}Ȟln7ѷ#ef}"9 #j#c]#_6xg~ۢdMTǤMQbCcd]"~urƺ=XqB~̌rg~pV*O{'勓^ '֍jZfӤ8$.n5=r/ѱ:*%;j[mcl2@Xֵۉ*]% g2"m+cnuC9y<ܬ0ڼ+'BsR_KMx≶tGpܘmYqcccbT7;^-g_ܡ;S'8{V1~JkewLZ[${7"J"N6eDw܈`<[էèa;ߍJ%{7;o]sd;ÝC}})!, 嬒JAQrGZ-;guJSLQӥ' v9 vQM^ɢePMDSwyR~y=|1}6y^b}ƭ{5[U[ eq)[QiP_ڝJ"_zn +/ +="C /#p13VkU~n,E񡥾 ob߻ɲn.o +Kߞ5Zinh:rX5WX8\O(silt(WMطODh4n;w)s9,[ +dJK iks7+V([ -}>3vUqBAV[gKwYo=b +:-v /( {1"-:+ֻ2 ѭ2$!Wm4jSt)b0C>7@k8ޘm1Y鞂N;%/rE!S;n +Xip"BIa1b1CE$"1PA,DfD5 W|j<&N w+gc"D/lt5u{}s*0.WTZG7(/F]-=.v|!J[Ǒ8G.t/|Z&knuY~iI\p9o-j ۰SXTXf9nzFN󚂶lW"&(b1@%>r?^z,'yx˄"b/JW?*727*BoYzgԕ{nQ/ +\"s \"rbEQoK}*ٯ䯟\/DǼjF5bhts.Nqb(sJ_8t#rj7L F N$FMD? fNYMrU@ucsB9갸9zlVGTasշQYܢP +r 5yYSwΑŞQmQ_[>X7;GΝ/{=w]Bi t DgO@4ET|?\_{h.{Pk~[?e;zAf-#d.Zb^BX$aoc+Bg';QrkGh9-CVGGqx!5&-}Kt"7nMj_uF){R*K( r bƄixnwyuwpMc~H9_$jĂebEb8q؞<Ϝ[I/|e<^ž-pM(+pK)-vNz^[RX[EPѕnhr:m__P6,/c:{cѻa3I8MC9ii#/&&/"YMT8@Tv!v}=B[8!Ԅ(hぼM(D=E25ɯZXt)&2aKwiK/cC\ ?U/`N@Y IqbM;YӶ3 +S''ZGL +ߏ@(GJ,xf%?[n3oxJ(`{/<Pk}\5Ǖ2׌JJRD]ʹǿ \/JĔIh)7ČˉiVƬ!H̜Xa? gb#}I{Fq+ש导 9b,~~?1?QqBb696s6%,Q'-2$f-sԉ bN(˱e[n AQz\,#hw +~AX{Ǩ7Qo5F~[i'<9g84b42!75v91k:b̽Ē5bS sR̥$1s1CQ=~o/T;A6kwٲ<50Ts n Ǿ.rP3P_c;PXST'Y5fp0F=<{CM@N!F"[BP”5h fMM(NML_&Ebbẓ +m_b{;ps[?rG] <#5SuCT[S<̹ZǴ*z䷥NKr{_еxMELG ͫihM^OLDc8 F-"f_m#0k+OU&ļĂ}}bO fOv6C@_X&2{܇}|yYd֬uv-`DTZ6N E(G949]N]bEĴq PnD䕄tiJkL Ă '{bmm^5p*<]*0 ȊWztm%OK">Tل~oK.vHL*q0}a:fC?~+ a1hơ6,|LP<5:llgPN',&g 4&h-o i7KۨZm3 k>Ee(wƼBXhtscBgCRoC:J甎 +G%Ejp[&/q(LDׂ/%-t*Ĭj(W( +3Q L4\;k71g^b +1w1oM,xXJmt+!y/~Wm zy ,L/*t~M/~9mV7F WŻh℅u ն1BQT4N +VsP=^XaM,fF,Y#CCr5耚;{;”Ă4`XzXHcGo 7[SV z9 H󭰓uч:Dl̊M}'TӵӞ(ڈԲ2ې +b}Axm#Ly28ᯀA _N1ah>*SGDL@Xs[xR1lwf읶=k;.=ǯ|j.b-j;k;-Re(lc|$95w-t=QZb!j9SA4C|2V5;e-pF !r};r1<ގ^wuy#x<~o-Foӣb¾ܚ}n"flnf-S t8n'r.VDfBٹ2,ƀ6$!$r 9+g1xR ӍjZnR?Sea$zݔ}T+>>&.h9 8#qtA?f H#2!m;A,հ!<{6SIJBb+0lǣvݗUvg˙_; }~w훵;=j:7zA G*8SAiցPzYYKc{];`{I?U!!AjI *m7RXh6n11gVb&#bIbA]'7E R3ω>=u!3s?mx6Yh~x:9WC39X0WdM󠑺6;vC39#ŢFܑ;Ӓ-$!L؋ zy+6LE*q0O>&qEqt \obW#Oܙ)~3GΉ5q;U˸MݜQڈ2 +HK7hxm|m>Pi4q.`O{ o턶:Q>F2QAd1W|*l9w0wIZT1z!1g ؔG8 aIˉ[f$Am AI_9KOa j9R#usz`3Nq adC R7+8L?rּ6B7ϭ9J-ԵG,?db5Wc$}'ٛtImLuLnc2@]hP1vQuEyq\byԉ B,XCӳ!5|wݷZ% '/=<_(8:V偢+Ni*p#^:X 4 zm 9(EGi9QWHv5z-^^-eDALxvNUI6͌-j>!~_z(k`7u2CQ]A*AsY,ۋy~LE5c:7i%}s(_ Qݥ|s ܠe{mvMb Fz9iLnM>ԛ+>TpBS1xvl4 яԳvg$y1f 'Nﻤ̣v1׿K_7^\ gd7Ulzg6-~b z)n&/m)[8)1)-򔾨=%i [ydf~2d BQam w0 +WT#VX-o[ш_vqBDvvkBPF<%MZ؍7sMY*.<\#-M\< NJلƾmY8GlOϳth 1HВ&w S4tjfTuYΒWOZÿU*%SwI~4kY#T-Gi\z8IɴY_Ѓnǹ:,jS6ZoeNڏFޭBSMl3$:3q!˳91{_mG~M]`*zu)u|cftOu˥mLHمcgG^ L1ίH`xy nO[풫O,^EJ,Ey*eb)`0E^|t<@y?gi^Q0 +( uai  RgRb6I3ihsSi:f7tG5Iv;-6-e?tt1n*5x~olxqP=;Z9vKٓU V3ݶ²o|Z!ϔh>[N?Y͗ۗ"DS%Lf.8M5?4RTқ]Wބmٟ5yY]{ً[Vp/m 6uy($E.b?a}c@%Ulᅽ^aֽ00C߷^6kp;59}_9 ]t_r:Fk?U;> (+?"7O\SE|Kg]lyfB-a62W{waJfû?ԌU6+]o1U9qF}.+ e 3r)M1 3C +Sٺضd9gVB;hx $};bݥ%_#>wiF>էE?\Camg)shxoga&75ɇy׾lQa~ &VIm· lgagSחsUܧЦ_Jli.ٍ\nHSݩ:BdAf*}xhfn1srBtqгM´Tj j/r4 $n5r=Hz{oUV!ww'xTUq!n4hhwq~ߨ1ҡTR3#}N<8y/bŻ#p׿uݯ7}/w|y|F|W>ᘣp6ac4cYݴqf]7k{׌3Y'dnų +1[\9&cf W_:q/<7./5ܜc\zᕁn\GeB^T>fXrr6kz{^>s3g[꣊p_R4g>/:/wsEgbTw(ڝan^Irz-_o[_nn6HO ^=Zlp/wb^g>E"&ՌᓪFN,]x_/Žg ݟ} Ӟ=_(; yX`~P?)u٣{<<+?WY<~ftyf~Q IօZ{XrI:c5[ܺv#ظr!:NdX!G%ڪ ŎČ +I,Dh%'y矩…)|rF{k($U3j Own|ܐG}oߋ݋4)L:ۙ\: ']{eiG*5޹ 7i6ݩqٸOs@`9KmxK4l4hW>930g8rmpOoV/kDc%iW8Jɫ,Jx&K/׵<\wWOpز.u7rmO7qO7}?^u~P7;~oxpVx{?ׁ? ,&nm<0{͞O܇ ckFe B4׏f0MgU.܊p7w<.<ˆ-!tx~ )&Yue&<w?{~aNCb_c,| je%ڞn G2l􃿹pL\}˵=?n:t͎{=4-,qY7{f]vi\{hvcnޒC';B{3Z֟sZ{@sb\tP>4[ڑbYgh%/6`?ՇgQO~7}Jd__&b~Mx[2;Gzmį;0ܡhcTI*mWI޾ɖ;6k6/ޠٵiKCkt>Ֆѹ#4[mp%h'78I7CqjppE +07's{f-6A |fxth&ld8 sRũ{(/]g|QW|y>if՚=[kKcYIp4:aL%d~SDv/soX0JN 757nۤBjTxn6X|Zvl<⤘44uRY=rbVDҒLiGQvS|P3 Ny<sШJƜ4 [K\G{"줬X/ݩ;^no]/H%B:B7;!ɤa͝~4x^ J "Ts?r;7WVrW'W0r ȑCڑ:\j=l?Yw+:hfFI%3M)Ngrk GZ>V4 [+OҸQfB*]l=n]_!6oI{f8Nrp0>ҍo}Gӌ>+7~q>,]} +&Z8~g]ybfm}Dr'K'wD.g>ېnd\1f c*F!q4Ux`taEΟM7 ͸pEfʘb!s,/D+qRJ~t꣄BB_;Z, nq@Hs1 \.\7ۡ!uN#VQs?]x!1`$4\3=WՋ_3_P#@3*?=_,tJhπ ³:?+w/{uݯ6a@L&6{:y .,pwY- t`&l\d`w}h>,k8{r[ۥ<556 :"s>]l J.i!fZL4]yXKhɈ=0T\w\q~i"}UO_0$8J\Ik Ah7_}A-fU{,b.<}㭥zV>_iy;]4׸N7c +Feu0ӟMl |Z>EiavЊbr0 qpäˡKM%U…ߴQ(0 B)o-jxfĢ3gQWFZB۟;\b@.j.2<;12Gz)V4I&'p{%␉q2)1oZ:x_B[.==zu,4gB"ҌJt!>=_o_X6]K;BbXfR`xS0oĠa!bfD>tO1ut< 5'Qfr3}@_9ZNHHV~fx6۷A7QjuTmiU_Zf'}'xVϙF҆lxo֨Ո/ʟœjx +~̗Z,>vNb+GWŧ5C_o}~͆y5hth WVriC*!A/?g*'<@6G?[c8l8R+/-&‚}!y=|9w'KkՀ;z0i{]E┱<@Le1=lN>0īdM(OYʱ +Fl-eLEJ%rh`UU]YV[].0Đ(kԉo+,o5B-^7|gŞ;A3'CS9L1x{*-cTh +)NBD> + )1K/lM,.gp֗'AW`1zWN"L~phեc1-BRbtLU'=)9.{Qx)7W.e=ssb.\mRF NN`۟m3xm$jB;.4[4炒p\D+WO<;5lXVͼ-:GșBBhCK8‰jF-}3P4}">yAqg7{_SѩbHe˱eR%|rݽuЊG 4h%)7bF$1k2U.^mkzty+XJr=Zy 9S)(P_FҊ>0 + +:bW8s\ ~c#/8P3]9JT-4;pg^{;'h2z*,8B|(褋%7]z)('cv1T`Aq34z zۂ7+ҟ,91G9&Bs~eT5 ZHs/Z0 bڰ{ Q}F.W^bE|B=4Ÿ|YʲCnރ g 'g=A`챘1 +||O.' 9:&v]ӝ·Q󂙅 +g _֛rz+5#t~M脢'B׹,VC׮BwwBZ9^?luZseE|3XP:[>fTwq ӳ{U|i|;/ooJEP,ҷ;x~z_RLjE@j@p=CfA$1b}:;[+ˍsJt(%ydhXj5fؿxZzu1_h `^Pw9z[k]-:CBs/JIn1t9ӉSw`w=w{Mw7Z塾foxs<!( +qY1JA!6Xwj\HAhBYu?ګx81`gʷ'X--b9 Q;0NHS`D9#HSl`@$cWX!qtq f$TrSE93R:0;z)]~-fvyiL wyÆ{W>˧knhbgMI`g R?ם屟\*$Vm 6c .jV"$U9A\xyYھag{5hX%iY|`^K梁-E#Pk#XH\H.$|&hz;kH-U< +:IK.;1+u'rde ,g,CLZjytf|5^w%Ьy4Fژ)0e;JJ3꧊CO2j<3IN CR#lggM;J߰Ӈ3:)WNUY<#\[vL"bZ8Ʃ73_oG]{g-HsL +jطl5xH\{n~m4u_nw̓5Ӄj\zrZ$?Tkk*f'a=\0X쉅즩Z kŅB]cy2{5w#vNR!&zz5,#>Sl !6J`09(e8|4Xħj4MRlwt:0~H>yr}7)(gehޠW +-n)Q`|3 rRl[5<FU3bfL⏌,b&7G;KyBUk +'/L,#}-_ Uck]h+,q1f";dabICo_:^- mb!h|>ŧ* 3`K# Ycƣ؉|krk V"b'k<@:E%vD?CjĖk?X\R*/h{h51V>X6t6E)>;b {)@ʝU +yY. "j;׬V-*I `T]_F}T&>Bb$#m#=Yuo1q!,7aO:덧(+/-;r-;Klb#2βpZT̥&6!bgO;+2+;+ ;+abTYގ충9 X`b>&@gu̞jfR!g _qe}l]K%"ێd!Vқ&G?\%&9` /j5"~]g5y`0˩N#"~Ei.?'JW6UA-$2\7Q-/,n'%ZFN%[q}/\d쬤Y6FbgY\^!~ZujOAl<[S=vt鱷|x,KCZaZrŖO7˝v n[ 

jNg/JhܣZzy1j114͖kS6#X-kKYlwWNdˆe'Yq!y߰_sJ%]sʳKܚs[ӋwWJ/,RbXr:ţV&nLQ3~CrÑ լ +0-pĎF{` 28Au"sx.ri,_]}ᚊEbXN 4#vR MJ'=[{LO6D;5L>Y>H cPp|)3DYxy=ȥ (FF]{{b|;K>oe1ߐ,Zeﻸk\=XsԆk'-\FX}_R=n+'RCJD!(J:= cy6z_lZ?\K|'VcQQvyVG'yj41RKFGuU K5$9I_E,qxVy{z]l[0h0B<5(#i!y +]쎩`nȅ31cB}D+QrLaĀaT[ bGڋI#S.=g&߈"q` Ả%w| +4RTՐ\> Xf5} /*rٵ<>t/.FOCj(v?!DZ굲WĨɮD/pސE䏠~¿Ć;nm"^[|\hw}PbR8ҔYcxU1l# k61Y= @ 8CڋK똍mPB= ; W=bkTd|mG<;wFÞZ~m)L)}Ih>2<z` ^3|>Kص/陃nRpp\nTR錡˩a 6Cz4:Q# C~%%X=9L̈R3 |+_"&Qō׀99Z(18 +-\OebE#F!6!e+Ezjzۘ%kƞ1Ä]_[!oƻܒ\C%{7Ǖ2 kK{9$F<k$֍Ź7s_+JמbMAWoZ?ߪ #9,磜/PrOK0yo[%;YOׂXj5㐳ag/{¹{]o>-P" QV8^88Ea+DL;6L' A\YNh.ǜT=3gR~zz؃DΨ9hHxmkџQק_\ %׏ř1"VIǒ~I +XQv_A`m v|lGd 2j/A>Bj0 f;/'Og.sC9|Nj|ۣMyJ9%UB~=ҁzb}%:q|]~EzJIb֘o2яM9r4#?I.'[ -PK[L81ř2?e*6Nƫ+Ď϶J>a)qX,+sZ>^O5'cI|͙\ǧr?XYT1<|L1"F)C|8!)k&V6G>C.Mk_RSJb9)c5yFK{k5:N< ]\wIoJ)6d`LkuLY92VEܢGZ:XMYAWLX5.\3#O#X賳5$k'ȋ`Z%49Z>'~(OT$o,mnGNxeاK+sBv6]ˋ\bvDžDE&;oė9]KYbM]\ٿ3ط8/f?|˴~Mՙ}36$8$}QhM~ɺ V,]bWZzuKW-__/%//9d/G{opY;ǘŞd ~p~ٮ$wg^q^Lo?^:W-_vyڕoO`?IZ:̡W[ykً\4]lAsOg>wYgVhCf,k92Aڀ+/5|f}=ƃ30Ƙ{].ޚ}{4@22"rm)VZσ>,ôI>ݼ w4ZlQ߿ߗ~[ +1ヒ!ebH "cv4cԕdzs0L-Gc8zqLC~t`?c`<'i˪129"Ö$(g%4FDIxI4 ~#HAX!I"Dgؑ4B`#żьS!Gi! +#c!$ n!V8 oǒ $tKBBU5DㇱbWWKO.0dwO(b&[AZ{l߃P\#I(}70Cl$9qUcZ'!8{!Rt8>dMh&>> L8>O!fKG2CF2̞Af^Z?3w/U%Fps<_`^Qaggה?0wگx)d]B[.V0 +ݞ/荡ti0>-cv@9Vdk9rW=Z(́h6Ys i^_3Hu@H`)-$k545B/Y Վqs={5+I5NjFdCN3 Ng$9R'؏hÇ$Z-@"jCfDlGLHIӘIe:l'n1&dgH*sdI2udw +򃐁}B,H+ ˲c3G`Ҙql +|<%(Æ$NȕT$g +[CFqb`$$f E12o(=HԐxti'Fe19"И^r8sT6cQ4IE1y#tv/=qḡ"mi$ѐ>]/%&QFxAVRzd)"K4߳ݍ=֛!R+G[0v/,9Cxwф1FZdL`"[f^QU>ȵ0~#D~`-2{YH6:Cd)0AO(FLՐH Kh Ok&Ռ5Ė;vjX FQ)$:XcUJQ1Hf`T[Bc@ y +18 +n]Ըj -55z=$?Jo2!فaf_agwO[ 0S,oC^6&Yak #R4^sH/`P1rCՁWY?]{5n`l&8ǖ󍱄<ɠK2Ga)HPbG7BjT MHZ9 c-9'44<ӆ|Xb  Ҩ4ojڱcZ;S!)Q/,oRKNχ/2$8B +K -[!a4UM-.-'׎ ɐA0cgs1hcXCo$^ !~$𞰖0_\L#wWa csB>CR9YmS!S1#,fwW-͆4hh!c*'94ÅӓcGbT|*m;Duq?s0&sh4bA aq!4Ot0J_s/],d 9 |Pcd]4d2'1̱\ L"G@zJ:raRtq>ɾYI̗h([> ?>ZȠ$-cM .cFÓ(T#hLw8#X,d9avgHð&6C.IQ)' pIB%qɵH)`P:䏘mGQe6@XiSŎj^3wiWKLbl+ +9N$S3;.4ј"ls! *|bCc I#E鞊1SdHCr$i|9̾CVͷ'$FщY`8$r =9:df*I2_˳B3 A#l1L ʷvx >K5mdW2xaI6P*o.jFZo{mK75pՅ4U LPlyEtQ੅PR)UȵWa4]A CF`e<,~d _#KŐyJa>B5%ȃho$&hc&l}l +˅l;|Wl(\K>Ƌc,~f %foZht_JQgA#//,62UkWGbV>H QHy@SH#_LH'fB^ebR:KUWcXe/8s8^dm0 +AՂYRt|*d7h<׶~j-lAV2[dE 3-*j[0ײK$f BHxkɅ@FY~L##VbF`ĬI3:&Yst bL=Zc9rRYMfd;(urH(q5cX@|TYA~YvrjI9tt;$ņb"\㍐kn,;|@A3|jH!b54F>', ߢ 9,,Sl%uh-XARnK\N +Mz f\P@֜<7dX;X;(Ll⭰HJMXnQdVC[&|b֣t>qG+$!dkc``Q@G:ctKj $xmpsPsC}gg!&\F) RlH'efE[b,).Ʌ$[C2ZVd-ћ"xxK/J`z '$t)$}8$ V{< gi}eVO:mqӟun&Sc-% 7R+JG. +=R` 1#7άCǧ@FИrjT-O5d  S o5EYd 9NYn )q*ǒe1  `ggK5W$\0~'|"YPv.dd@מy|'5-p Ŕ8RQ x 2ڧ4q B>?j H5pc$ւzY\p}&[~Vִ~^ +2²m%SȐqDql- 4p(c8joVx|4޲JYC::9%|R-i1%|',)Q/2}Ȩ;X|(!#H|fd5.$Sqgc?_j`5Qyꌄ1oB;Ÿ⑈E|ېHA +{rzJe'cvtߐ +f05[AiɇBT9BPS:Uk<=C5j!$Q;咞Y|->J,~&?5PJ {_AxOK5wWBP+X > X"OIқ& ?B҆Cv\hb|+I1Yh7G- =.w<)w\Iŕ)KIK@:S~>+Kţukˀ]pכufWXS`u?Iȇ&Gdhm2G)Afu dѷCd?5$ A$Sn-GW_F0daqc*G}ͥL9(% s8lC Wfvߙ^7A-=g@rQH$.o$OZ?Yǝ/_ ca^j9FkEXOP~)GȩOIE_R2z}襰|!Bob*cLrC,e~j}Ce@1ɓ$QL5B>bj#_ GA6egL䦫-rON횄\ qB*Hnxz1o⢜:Qlxg96໤bB#b^a`cwW_:TjuHא2EBFΗXNbBB zv1ʿ !DysV4_H"{%ᴚ@B(ѩvX;_3!!'.rNjxG ߏ/w$W[X]yz$!w\fn] "'5+8=טlwm"!c;o[>D2xlxgD8gj:7pdgaF:rnIU^" 9Q#?ʅÿ=θIk!)XaoGݕﬠ׆CÙRo_$5fCOg%'B/Fa}`.m9|H0`!p@֮<Ѝ'( '@M5}Q,[lVo,%fB.\t=/ OCҋ{] g ߀+O{BEh'%7s&Ր<0 =,=7@*ug GA6#L.Cq\NkNN[c@D1evi*rbk%BP>tB΄z {TD[l!#7N籫4 Bo A2r,{ !pEB䛐,ҁٍ1@Xk/tNGHlRR?ow[A|N{?aV1,a8 +ȃ=:$MQ =<547CèrEC46kvl٧BV]P^ME {X=ȁIZ+[GQ $7PBׅd=^ԟŕ5@b +9p3|I#HPMw5ҡF7䡤ߴiEppJQnQ}f9Jݵ3!{ + $7Z=Q[JWWH_A~{(dKf+=o=p\BB_l $qGDL P.l +!!rMH_oւoS=a{lXI=GL=:^-<7Ov JީYrFda=Q kK8>[0EԍE]juԿ@x"CM Jn* d>!/+4|FEAƝԪ!{w{j\vոxJS| r/GK?cMDյ8[K{W?\FJ'Ylȶ8sHߺb "w$Dp!!.W25[O<Ξ\Ӑ4ـPצ `X3q梠o6|O Θ;)jQLHn,O_-E6}`tZq$ѻ 3-;uoٽABjg7e'?^vxp6xC􇒭|7HfeS.5Nb\sf\wkZvnX젳=^ ml씺܎޳PJ([˵}hX|ePuk1K NENG9!?0[jYj{5zlף\MnW:q7\|g8R*o.R \l tіޢ7S "z &^XOG ?Y i|HU~R,G@߄`5[j8rM6eyz$0bAtDTp(w"r QR[Pg!]΄_cu*I +K]'>z=`{ݔ?v=?ƹ`sk߇4zJBG+p`;'RQxj${QwhO䂵 TY*&w=jMK|rR_턽|m~‹jYW#3쌈#@7Vйb%~ +!cgpEReWrf`O/aذGv/qV >yjLHʴQX}jRsc$2կWH+Q_}c3&&A/ +}|jTǑG[n./}?SxT&±B`iov)t>&5|V9raR88OU`O{YJBӁ/G=obr|D-^lldXx~pЄoci$՜["1P84ߛ*մsŶϷ(w _`N߱vO]9LZc1[$yS ?ADӳpF@)q<孥rFdj3^P gguYƊ+KAo>D+Sa{CQଂLʁoPa,J#D 32,!rԌh zƩj٩8+ሜ S}߷Bd1;>؈ $ɷ&*>=84*WuSn RVu‘3scq&„#bO; Q;A؝7B*Ȅ,8pCtqvk]/| YJ氚a0.$ + }TԔӎc r] p&c{~bp*}c!| 1@wNb etΠ\jrFQyo<,2NeD>{nE/<7*,"tI.΁gXE} +[YccbBE?`&g|/壇^%$u'HEf P^B =O;cP=(\UN͝.~ҹ?ԛ/ =Q)*WjtOqcDtv]3 :'WZ.u=i<u*͟l#=n6K|.<Gr`pw6:r}11;SQJuE <KpK=(\V?Mr +y'sWgz<]uvooOO߂u_xtJW#}K87=Z6t<'v|&WbvoS^fR~s)jE߼ꉾ};C&`q3bh%KgSу^ mƂ.~,?lv|,XX=LPE {+;ω+k]^:=-=!)yd΀>:7*h;l\.P:̨.-9E$A 5}I^tߔ.ou$$ D9Zx.]Ev2E;P"ydQv(>ٯ%!LvCm1ÍpeI9ї-- ';u3A<&,i7뭒z"i(&|˚/H_tHwTY*r2TvJ|^|v +P1<~ZCktN!jvz)7nm +•]N_\f&zxTNX苐x:,uģ!~mI:J}& &+O϶xoW_h9Yg/骼,.V?B"wTK:]t^A=SZ(2֣Etrj#>:F }&cN!jN?2o~5 +>S0[=CR5UoOU!N5Β=#bAbbP  {4͊l8GGG[„=[>;=7x'}" +P٨.Ci(>*~Tkх=ɻjG/%ɍ:d[u >ؓB0ap ^Q43[wTٴۢYQEq6(ۄU:qև9N¤]ogOPhE(CK_ %:tT̕lϊ MpV>x-f2@G-hqiO^Z skbY#꒗fUG8O,.ou2( WoDeg$%Β:wE$EEW=6ě^ fh5vŵ35ҷW|?a+?Od谨9@W!^/[C. *0*~|%>|tG[%"m&?5NA.~3z۸&zbcA^ZXc-)h1ޯ4-3'u'س>za#zrRIq%V{8'a^uڈkj\E^Ҷ}%1G?u^O={?zUh>PQ lp"^Q坧z/1V/uJoJ:לИw5=XgnHQ9:'l8hG|&REymA1 ߆b"^ǝd]E2`gV=6aws}FC|)e) y%&K˞qv]εfgvǻr%U#k:6J]Ck +ػqh}]$õWqdˈ3c~#$~3{pV!,|#?n7RYddM9a÷#d_'qJW_uWrFpP1ߏu\K̬IHM +iz=#զ~KMsg+D3Īa֌䣝qC?Ev8?K>\GElC+o{w`ך.* _+~igZS# s~Ɣmm7aOG1IcR=OT>4V875y'4fJ:.M]g͇Ke-/G_6G߬퍽TRRs9|sBj`>I>1g[6HtAJV6kydu'ɪIѮڲ]Zen1a1תdwb"+[K.D׸DU48E;ETŸk**\T\ LoJ0~!i{"Ú8%xw ȇ"ѣ :#-%{A2FvP$ÍOՑȀ;H:\e6X+#;z`>` ǥm$ufe> !͗3϶\}', p p߭x玘zunF׭NcQsdQs:טB82k㲫ҫ}jCކs+UFhFD{}w*|ZJBͻbD[O sG+CZ6Q]W_,% !5L]y y0dPDkM8y+l¢x¸ mq)^ 9۶ ٷ-4ů981Kv-n18r,L|VWP!K=ڝ+BwH.oN'qu+Юオ}#"BS/7=SXU`rbI@|v_[S,ȚiG#cXvw2#BC;o]-Z/Ug}#qBǫZ1^1w\cJZ2[${[3OfObCj/'$W^+u?՜{9#h{L:.ܲL2!*i b!k˶m9'sS(H|k5,}ՙ} ;s;r̤9f>zPr^wsLiSEkƐm'"s2=m'mI"|Xo^bdobROM-ɯY^h3!6fHQ+ssVO}Ǿ|0`Fmx]+qYwSc3b/^IК) [nQSd%t˕4Ϻ )7}qjN>yt:ڲ8ަ0=-3)(ӫ!:xWN[Eǝo:ڦA]WnC{bbBOb@Sp +=|s72cxotQdle /aj|d0n*Cbڮ܈!G O0Z{--sʅ>9b1# 1'. (ǢWп=kuy)r8l,UY`>Ps>_;n.4w}Ga~>sofisl``Ftvfkf`1s{Ɗ~ެT/{2跕.QOKcҪ}+/ՆFH͎,bBtx2РkkTBCp8C>- W/z7 L3;0tAakдRo /^Z0L$0LT*+UMLA('i$.icty__g`uW p+b]hRج~|e/^+{\!{S}'cw7ŞљU>q5IL}._΄Ɉ;١FYn^-jb~jm~J^?oڶV,Z )` "{WRw9 ϊIo?)M׀[E`,* %> ſ /X|).?[ػE>/zy;;e^%+]/׆$S_{́/;^p笆v XxVl+L:S97ɳAz`2+7>Ncf׃;Y7F裬C!:'76'%Q~5aO"“m>zSP)K,=֕KBT\xtg 7Pܛœf6c OJi񋀊 +0Uq9 L̞֪yG(ie6Qo=`"ńXxӑaۮȠ} +}e #נ*en17agrV|%xdPTǭlsgsTvՙ,``M@yFOfM7K,jv*gVs{[ۡvuW#Qnvl +}|_.,:P}e+{#-#]Ω +o_~)hT銪pQS~%L@ez2~1i+=X~:`+.Δ? cIH|{Ť"Ībw)Şy%-} R8z\LXIn_~ gqipUplgmsVhx`2TiKU$` wLVlx",[]}\%Y\B /0+/wm|#Qfs,\~ٶ0Nۅ83(spL3g9V97y$``vx֊8qLUm0w&~<8Yb-[^ My[/oJ[ߔ>Tx$Ը% 7$46Tx#Y3tSmϘ&wYcyW9U7oo3Tm _&K7ف5`]=} ՚eNGᦼ@>X;S}[lWG2sݍY]u]ͮ+=`-XN߶͟mx,?(+ + +8f(,-SNe`m[9;}j@䀹[`;X kdO[,uZ%,;X.::]hL9̽`{=1===-g̾9S|CE +@uئg'È={± `ެ`R ,v <xZ^`3X},v1`=S՟0kǙPߢg/=Ooz]TjsɉŰNhre!E_]Tì3gӿ\ 0z)X6q1ˀRAڦ +ϐ۷̞ L\}#<f@,`oKx l-+qRFv'#_~'y VQ-CsFvzԴ +DN1x8Z\p{PXTnbJuAC0­p3 } +[nTj%:@uX9PXŽ{9j5VfmZ.G'}gkKCosOmpKlrK_S֒d wҸ)?cxXXCY}5ٻτ εsys`R.XKڂ`0l:@~Oո->f1W٪p]L&..)'NP݂10TwIcSTT:yڮm<"/9ͲP q5u=-6`XXK (m9%,YO%E`i +6`g +[#{&~fԴ>0QƆ;ߊkʉcךRbڛˉ'Rbc2.D 8G& 1Skưi@ٜ"r/g(-9|8U:S69K 25zx rh"gjO=fVOۜA}xUMsA50k52&ӼOIKIУӭ2Tϡ嚏QZ=5-v3# Y|m3#Dy``:CAX;U6Q` l *M(nr#jŽI;'p/өh^ɚ ~2YZ*FFװu1"62bP΂ڕ sgڨ.ƚ4$7hw{(lՁ.1C~}*̇gYjXy +lz"$ێ]W0e@%Zff-}!Mw(lX=vוxUN9ءuV˜:P_J| Z/bQlj^Dj<#?8iP0=nN=lNJmo;N8%8Dazi\+X3v+u?Xwl<` m`j~' vb!r>Bͬ/թc AæfsEoN?\ ;J/} o0ۍ+!5e$F}|o*N;hq3a폣Too]DjMx%KRUm0 Ser0oNb تkva~`OU]:]V1gDzaָ?ڜ83Gu3\ ;#1X3з bڄ1*f U'<gX׍=w϶f뾓b8'yYvK|>NnS-TVn<eZiwP*zWcmk1{5mY~gLb̫>,F4@y}l׫btm99%U>CFczL^cn` >Jt#ժND"r2_|"j:`!`KZ"pQ5 ~+̛m*< XSW;h4M~X +>łs>1ֆ-42c^3c)a\Lz*`x <킚߭/GƉ3c'/p1?qYx4J *`/c/wwLDJŒW!^s*m] }|F +Wr+r>l*-S:zXl2vRuz[_U9BU.s}U׌VrÍdprW84xSpAܓGg΍zO1{̨1Z7ł|X8) v>lEnM_Q/,JGܑߴ-Gm 苅 FЙ܉KVd)Ø~졂4_N=T˨~d&FÌ)bo>Q_#/M߭ v8gLZK=Oud:%[ >?xskǍo?bԹ'g+/Jޣzn7d">映"0=`h"IB}Dr>7MQ1OxD$eӱKlgc.]*i +0V9/ZA_W._ 1?jÏhr^&&KFO[}%ԃ1v!m!߻ɕyOK.?4&mruCaBGl?}8(k%X4p6,h{K!l$} ?XΚ,\ڃǬ`X_Akt# eмY+=-0 +382;c%_q +yc{ѱf.ĢMG= lT!#c K1%Kh}:d˭DffEƮvzAp^ٓSEM؋Wx7kt''uh׃YbORR s"JN#AU٠9[g񽥢{6v,%&! x1ss +^\2g *sl|Dd=-} x\^(5'| nݎ'<&Ly6E4{]%w7 iry$090Z/c I)h tv5m@ؽB,;'wSl+z}7Xb,j"`y*QveNKD{0[M"a?tG-I~7!U'c rt4m+ 4y/ ~0_N0~![|4u3NnYO_z'(o=VcOV$`umĖ +V6g=T,NyNCҏͰ|,.?b,8qO&L w#~|B*vu<3Sٲ1bjvެf=`txwC{n}uf[f012͌Q)cy1%chtI1K{ +`E@s.`q ysbh"/·89}^Y@VJ܃Uɓ>HŒ}Xz"b3i7G}eq^❥er+ImRZZB27碕!W>6EO:nOkNu,s6w}f#s]ǯe,9^9 M1l6ڣ4B{.óԅ<'RqHOMF{ȫM7>\ň!Ҧr}~9\+\H2[8hʕ<:,z*6YP׆ ´d֨6STx7~V^,ZauoF&`Ƶ0f,8=97c6F>Jjj\˓!vإ $@W]tEC@_\ +]d.7[I|=T^yAbAg-{,BaGul "82IxuIQY``\HQE7b>)sqeZ&׿$Un^SskR7IH_S]0$vn&/:FNr;]+Vw#8p[qh/bIU~ kio +!},+y=rϘG&C<Ca-m]Ig>7-AG5AMAt:  By]OmEJ.>Lzy/.͉Zo@Uc SiL +]Aӕ)y"?*WοM@ #C4aN7T13W:=Fgᙕ¬=Āޮ ٤_" 폧._Y<^hK>z-͢GtmC<_O\~:A<7?bxh*-r86xg7t隊vBPxVrB8e)jt + M ־C@k~`le9\&9✺4Md}\ mE/SwKr{ +}m3LIXtH7.i0-auRǠtY :$OڨMO9B&h|6IK~7\g +OLDbx"C[>X  +0a\ xR^2]qψ :ho\ڪk8)W|| +~>l:~A6C | ~auaOJ<ͨ؃a鰈WM1e.3;oṟ%OVW/%1n庂`rn"8o{a1&1cs weذS{ω|sy+蓎Y([IEqY6ʯLҗ"nB{$nk䯡N*4 Y!8?M=}xPQ j +>SwpՎHG84.QO7b)M}A=vYM\A4!u +{ɷ>Ľoq\tԹ8^p칈xwDOGۍh +7bHŽ{NM"2a<Y짏 +\U#25=\<_mh0m0~:jYt7|X*2z~?>tTIDUœ &fb!wVbVeY{y7 u9`۪`Ì`M`СC@߈c +pz<< tVFA_mZD?`~\Tf!<]11{DprzGrFl YFy&EL,FPIBv[\,6E Ó>#W RJM]V[C_Ve2+gģOx1)cl6-\ jFo +҈Dlx%i:Hw q/.X=L9h?AWV}0,$eoaZ>!Q|JIevO{\6y +b_lƣn$  +8DA?E twey"v,p mz3g%CG8=}Xo܉ 1a^82?8wؑ߭e”=G{JL%jeIM`DP_h ڛo-_5iӄ>9yÚ(߁)X`çКWl%rZ+3wYsM̜5C]GR^h kK{IӊHeX9*N13'q]ѳćƙv/%KI 1pU<0w)rhP's; >W*jijZ5D7ĝ6 wP&xh;B +r[K9HϘXݵ[bah*p9(cCjxlvaGKT448`@@:HK 0,$;ET.$ 69ݺXVoe2KX(=Lj . aq0 1Ckm%4BXtH8S^ga}AB_AfQ3Y§Ĺ I)8H醰:S ǃI_j:'tCxHi3!71\FVn(@5bF!|c5y1}1b=[{iNGQ]Af>4gÌB3='Q> Pނ]?DZtwM`-j2D^[l149| .<8FxcmuU%> LhY:(Ѿ) Gq^' lt )`LXC銛VD,g_[ƲT㥼t` +::tgr )rGW|ዳw˭}GǚF%]̻"^9*߮'Ѿ| =(50| k0*QqZ<nktb)IFHXrRgqVѐk % - UӤC,;DĊB%O:E:Eq[rZjqx3!zQ0'~~K,C {"6*&bk978j E\عigL$s Ro9q𰣒1B + N2 XG `q4P>S *ˈڅtP +` Ⱥnˌr8!j>X-Xjʻ8৽' l0?ucJaJn1~Wd'oBBHXˑ6cQ !SyvʎyfbvTld.@1( pSJH)hϨ9H sÊ۹Gd<<V.csӉ.c1SD!V{*xu97ҨGdTS̻{f'_oZ<s3'ӛ5R z7Q,[%'>=T#+af}Q1Щh +wIj#~#gnf V{}Xj`. sH-!&7O#~)bgay6 +@mcvHn6Ғzo=.K^_1wOL0:+ظ\gcG*΢}He> ӫK8Ehq\9HY* +[tɜA"oАm,)r6`ycJSO6-]tИ4&"}e5O!R|=F*CҕdI2H Q:\Xg@|c+{s=XKƖ> $Y\ZJ!'¹Q䓴P9WU(ӤS+mbs2WX41dΰ!7h-:)AظKW#ĥC2f2;>H/RgмGkN-Df8gSOj=H(Xݫֈ{ <'m, pg~|1o˷J ZM񈢍#a&ZրHwKCDLH"hw|򰼑#m/n_]I .&rMC63Ȩ., WĻ>=iQjga0/?lbj By.>eIQ*u_x(ֲ0".>0~KҠc\g JTgOLΕnD_g6Doq06Wj6g=&#m +Vf/Dgd{igX;sO m!y1j6"E蜨)kױH_^pAaJoAၹ+a9먘EiuڈKeu"#ڪc˝ǢgH +--_y5q[kuCwm̮+'^@k|suLüuIV9 +圬^1Eby؊X6Sc.WΎA96EڍY,طig#,{M{GX {jg'al|HpJBSBeR +m(eV1vMlT"gBLo{rF:[0NiH5rrj:h7XAyZ=,L'&Ҷ?YܬPR^34w؝ YGc{.j|HyNBQ⓮jgE/ s 刴Ez&cN 酡8$;?OH;e:NꖾPb'3{4Pt t,%^/qw"kߑ/wx8~~-sbV#&OB[=qrJӴJ:[7ew;߹͚;/}44n)E^ًϏAk£R”QGYB etTn.sQC~Յ1Lh'tgcO64/]KdBDh}Q-tιB`jg (%hD.G +8|_lQ|X;v/u/>7q|4f=b抽 It=w i|sA(o\bٓ]DֲmIu:VXܔC2&7R4PNE=&FXm,%hZ@<jN:$ +p'7,YTy-=\N󠝥e(jƃjSVS,%֣g,?va--%ہy\8&rO4c@={+G-bt>L%=<Ǖ%r/ET4E1[  e%?@Pne +@FϾ +k-E\Arrۀ>xPm|F t ' +hsn1e 6簇1R|4hR\IC|.e4V¾-T; ,E;˳jg#]\$b!CokJRY-wQ}ke|SKCW@3G {8_!ԯgȻ94)uKSK*k2ԗ[8R9'8>f0Or},jg5$TtͰp  cn-wvKJ1RґRhXCC3ŌˋqgARt b㨛(C z9./1Qzx-b=fHAcEV^߂=wq)WdktZ!iGKFP gPu +-&aErY{EqN~,8[M6MN4pǘol|{+]!U |2rlTĈ cb{@_|#zb\ejU{~HBS}Kc$kTg5Oa=%U_R⩹"UfpҰ*CS/,pDw-`z, :N}zu{"U]pgRۄ?ESG`|MXʛ OҙA?N@GLvFƚ>Ƅu u)6|Enhg)gť>z"T6\,9_NzVrrMzޣ%d[1[IW卧X}h9 i亣K"˾ZUR|﩮-\vE U~qŵ҆佒Oτt>$şڹh ɇg!z.mP}'c\#,!OBI&֌Bo5=ttP%'=#k-Lo.Cf$_f 4P7 QSqB'Y9=&{E5<8j[zxmxL:>˹51|"49-|ŰqЬ}.S DNI5 +$n xEqz>:~tSNBMC:,)׫5^2sը j|A 8%| 'vyu!B igˊ!yB+MI>2'jgEm#s* +XMy߲oa4ubT>l.Rbc̫КUx E|mН"sK"|YJHBq䅡v9a]J8;,yla}ݙ n6Gک*ؽukH> _$'cIr}uQGМ9r'CBLM5CAw#q2%a|؜;Q d Rjyc'A +&Q99M9@rYԢ8{gmhm~rI'g9+{AGkbѩTB`tPF/vu֬!j3СVa փ-@  ,.Bž򼻋f1Ƥ3灥Dd} /ңkBĿ+-gu&+ ++EaT&2﹤8I޲xœ'P޿{ϸMTn]XS__ĕ^Op3˥XD}.0+ޗ2{ ' q+EVçҵik[-佈xU{2P NgSܲG/{44UpMXc{l&Vd.2G1tP (z X$F#ܔQW>#F#'E{gĞ% K_ Nвw qy14~ 3y6]G$ 2 y]romrr Vs@K%qF=J S~j})T=k~חb m;jȷXQ'ϿOk=#)RO_.ϻDsszQ9g~\1T{HB' +$GAP6|bGf&I5ko/Л|p;{ aGu>3|M 3 9.;p[yb~Զ1MV-;K@O lI{'aŢW.rgoH?9WF8WSxL]h:S=aW ̧tnQN>7꼢fBp(8zA8sX{F'EA-d3/8=uR=1BuɽҵR&V<5BE6F{` +^G Ov6)f&c+tA_#{  .ƥ㳩&8f>d"ӸI 3gs^#aKOTcM:;|aaWu*OeF;}80GhFM_z|=_z|=_z|=_z|=_z|=?wl[gZۤzbK]::sW{Z`ur<mo^\oko[y,7m,NכE^:o΂Ezsm򤅍mK K-~yK}?o9,Z^p΢K9xz1}{7qYgCLg޴z#cֻyr-4.zi%u&n$_ͣ8oƑH((,&M ĀLM3%4"2Fj$ck!h(l=%?s- gAX:D;Dm) YMQ  ùClܵ fF޽I])C92Uf]Z6鰕Hp [݋UbzK.AEϨ!i _.6sRGHc!z iPIIJ7;p[߱"7sIkĴ Icë%(JX%![ȟIrQzx Pzŏ%:dNm+FaЦNA9hUdC+lhЏgE34BpncɈؾ~G/]dQCA;^ 60[S8b#Ж)4(W|QKa M݆&2ƌۦf |ؘKM̘[h [as>׈ Ra<]I@XyFF.9kh/rJJb/% >Br#|0|#HT9G~hK&!%8$Irǂp¼__J'6/"+&p2n_\P9chs#vCJ@pw.%"NmGAl CYchs9)JŠИL4|ż&1o@,#RoqAG2Cri&lͷqGZ\i  ,>62l<hic(5bd2=dmė{Ak8|?m9p*e l'qڀ݉($#w 2^9F1%kN;_BP/H|AկCŹtn7;`!62.\Jxfmtcȧ_Ļ$ 21v}u㳅sN]Dw A%q d\=I"QFo2|g] N/EB8ئZ tMdhs-o3h8qO0 ;/4BHRD$-n|*>6cIr-HY~Ͱۣ _X!O3j$3NÜr|% +7]4I%zM(ß 1gft6A&=S чs3A䎆`v8\P7x(@ M y@FϹG@\ LJ + !qWpj$V >J$ߠ;#(IEqj;1r(R/=R=j[[>Ch,`~Hd@G,//s[+麴I$$opO)a HMf/9,D~GEULIG"~\ qh6Om3JA>m$gtBȑx` H\VnNs=\O'3 +0;$։[ +!w&(3Me$WQXi\sc-;k$qqh L|b2ĸ30An{īNvnd{x3n7|AhNB^M;(<{"_wh4!}Yil's֣/i@UXħ\{& oF {@MBrq HT!S|b0 nǜɵ F j+l #T]dApqze,Pl'>hj2zf@I#Sk:()%q |M rw@|{b{k.Nkn `+Qrۇ`a h<%&$0GuWC鏹ۤ> >y!qYyk#`n٭ ex;M)?6O| + 3 %sH}$6#9%c&hPL<|خ_ob"_\ v>1eV;{9F|,$E:DyQ_k)K3gFܱ#lQdIQ\#u?LJlW9U]l*\w{>5 +NQk!#w%U6R>"?w4!le֟(Y,rE=! .v_tPܴV([ "2FWB!/D D\K^=C?=O8˺XƠ02Į0)9㞾"rN?ԍMb Hu{[퍜dU W=3(/& Bz$!ul4y~Gq[`_&+C;#i$Ԩzh)hQ @v֣,S∢o}2Ph,|O 34Q8p%<'~s`њ>jHzd}Se@"-Z +$_jJOS}G)J$͡b@hxd*)yuנ"+Mkll"(A rf7uM=CQ_D9?1B4v9|Kx!N gf%/VqY sa". +~cs+!LG۝ELFrx޷ք1{PBIF޳Hax_.C[S-*E՛ S삷 Z br:ag0-wF!?ڔD쐒s03;RQ +pB )qRA]=r,% /Ty:͛k 5|5 g OςOT[ʥ^%æÇ(HmC )#ʿ>w +ȹ9\܉[ 4Ԯ$=|@q$ Bnl ++I)Lrׂ5bӵAƀH&q +.&%17o 'ٔƼsş:0ST4Éy [!<1XO +-@I <@p)B.,>Bm$W #Dآ?|}12E|9oB 8s'_| }F endstream endobj 28 0 obj <>stream +vbw3$_[ZwLe<+`%nB]T '69Wr2*v@6ݴV+9@:0GJ".3PO" $-.fiLui" yC;lNIs5Ki@Bqs"PQTy]LyJk*[*v!}ߚIl"gXk/bQ:'~ v!ܡB*+z.ĂBqZ\C9; @r/U(It\)9^)w @ k ,UX-|l؆/f]׭@ 6q Wu"k?{lM,4 ֳyjB[`A.KV.ifvۤy5>Q@<ĺ k.=.etJ:@yw_꟢+_bJרA~ؚ|кG +~ +B-$֣Lݽɷ*3?.J׊7~viQA/i$=.0R?]]G*ݨIdۗM !%;RA2P7djwqG +/}-Zͦ_OQ^<=Ct+Q?1 +9=<ȩ!2/+ kE.خibjo1eHޯ<ȇ)/BuFYܐY&ⰨZ8@,WĨa  LIsz!j!H + 37Vty̩؅f[ja %\]Co輸״>ݫ/ƛkЦ_ܴqDg ?@a!-2/󮯠~Ne\\e_[ H)|}~Ajh^uTJ_lm/),oRVXH:M؊ '|ܡ)T{]- -cUO@D +~G|*v#v!؅۟bY+-I.քIgjs- 5CGh5J<:+\{m(ui1$!p,[յBr47%#*1I#!N +dW3b\Xq.`1o%jR>%͍Mk7271&#]dK.QòP@Xɽ7'./(ݳ7Ori\TZ˸X"}F?!mG~of*vzzlF /b*ő9 v Em͂' ^(F{>˕ չ'u$%ǞUW@suqs*T;D1S k^j7b|!E}m~JeuPRϾ~jri.~<7œWb3E}XQl#[O uD2)%)Q2!,!")LH{{XVEkڵlU_Ԟٌ[ v#B;yaSKJ89b\)tcxBᕓ|a* "5AWJ,kȦݞۡkB Z*rtI$iRX_<׸՘ l9w(#DDׁw& 8h8YLH@l=bh$Mȉ1M{&Aػ;Ufh1B~ǥ#/oI^p){X==BKg*DuuE +HQ +B}&tEiXPG fA\whӇ}ػsT_:yj*_a0ಘj}3̇@ &y_:*rJ~9{ օOPX{b͏qc:{{[KabDuYh#Oxܑ`*=!fpP>f0t֢8lCJtOKz/䭨J3(K(^+g@$؇|fpz6kXcbBE?r" a͔^ +z(Z3l4<*z:ֹ"&Q5 S'@~a /o[ qJaw05pΙ!h+ NACr. j +O +QWL )~2,ɧf#g0UkQ "Maj=G˄ki] CMqniX+7"=f 1~Y/`5 an/b_o5-v% U8н)b]96M/KEg|3GVD>] +H}#t+}&M?~w +;Fݣ{QPGY:쩷qڒj>!>tQI4/ ɉ33S7b_R|%dOLP&oa/|ՅKbN) 46W&uGԌ#K'PD♢Olt5jDܰ/z~u!byPaOXGb`lهSL  !Nce&YEJ.;CYq +I#4.*;כE-+ݧEQ$f-:*ɏ-F #ŝɧKk>b([VN"U9Ц4/(^.}V.B+kW4:8-{Fh3;7 9*KнA̒өBy|iZ +:qkyܺ\̻ +/{P{RQx3bʕؗ(~F4$؞z~cȕ[ϕ<_u< D\yJbem?ski]O9?SQٹ+n_KΤ#3h-=>~ _ClŹl%kmkʶMbuPٲq a'kEY*a1C;BnQ'Y7p^[^tHS8y*7Yd6wV+KNp"c~e$?a'#űO&*[^+73/G54[{9j~5X~g^v߬W~gRm.ߡ<ޠqUo6 h\!HqR8Zɗ_`_|V-3^Z;ުunז*H~Pofv1_ڀ??b]|쉩f H͸E!J_ +7_;J5;k-XG.[>W.|W^VlpLv["2P3:2U{,>`ky +&[޹-TtmduK۬īOW[\dE~{J]ʑVŞ{ ;m[m;ݹoݸ?;(nVq7Kٵ_ܣWn統qb# k[q0],SδY'[xkPDuI8G_cYTp鹵㟌#ӝsT_mG];\.ŕwؤVT|!{'N 쭶m\s- cKͼNn˗Vqf6P˰I}9W?m~ +;u{_ݣ,xK97G3]_ZTe,ߖ%7:'UwX N͇~ҕ&g6Q4XeۍX7LD7^ߴTvSyo6۟/?^ [_pEHjSU]˳3En:][SyV7IζIr?ݸkmeUXfuQ|p={ꣂJz[u$KvN@[vO'n'nVZ{Vj3z孇w,*^-H8o״ؿ7m^|z(U1U|+k^&^Yvk/Q}u:\}{⶗ײ>g((+zqa +MVv)/t9+e<9˿qgv]w^(|ۘ!tI/c|٘tn87S٥ +3gܫ!m//dkNeIㅦUJ݆mst+ -Ol- oJ)^ZdTtgo>فlw3,}鱿b"~FBٙ|꽅\kCsOv(C[V>)K?)r/QQŲ6LY[$\xc-Yf\yʝs?ʕOj÷Thr6I|e6swkQQ䓤'IE%yQOʔ51绬W4dT63e͊N|X՜L͛{Wkg7{K-v?n&~j2ƥ@G.GJ]ٚRwyÿ965ڽb}&{\a;RE)wTJ_ar#m*l+ת΋zМYY>$ =)}o?lNmxZK~kI?Lm`femhVrm\[Ky⇦Wnq\q-cYpgߊ⹮O\/YoP۵[-Yxſ||$M\~x&z0mx`qK/&gUۃTwaM)%ͱeګm{y9[^˹_Ϸu$ǧ#37qD'>[Ӯz!_o>I'Ri2B> +. ˎjHU}bR-?n{p(yUytaMU˽$ΚLnZyŻRuL:Vyutk(D&'?VY?n>sEQR_6pފB[kgkI#Vo_x3]Mksק}۲ +> /r*Qw= u:^ޖTќ$D}1  8]{䯾Zw&( u^mDc,SGz-z"=|->hcnk]'dץ4D&%/߄0ovNS򣸂{Q9{Ed<"U\V[HMcn#F.R$=N>H9|KPhWJ2?[y?^wzYK`㹊?^v0{.w9̼Ć܂Y"rܛKLG7x2avҽe7ݒnn}7gnw{5F7F%Ȩ[ڒ6TFݿoj}79v( +|UUST(,Ϸ5L!Qhn vOBo[Rw78ӻ9yu6oؽ:,i|YHSF\ٮ@u+uv<ݞ4Px7?'6 O~\%-oaʁqkW}GCrL}BnyM={aLLC|_g[֮ N Q_{yNhoCޏȱ}Ib{ yoW'c۞Oq } +mn{#Zry]hSnJi*$ސxh_v^^e2xv7uV40,YYd53oя%yKL@voPTZLYneߎPJ̉&*Ox&U}.*H+,AXr",Z!׈~wڱN -F>/6VYk(231{2]f3<3~FYFd6KUaZW̔㘱1.3Dft/=f\)9qW2Mݘ5#7][OmݟݬF޾q&0V_ua99Qy1 Ea wr1 SߏxE/Pܦ6=)d.̠dӁ?l +<4bQ ]äz~-68۽ Udѓ=F2:f ӏOOHfp=fR3'tU^.8~C[F_wcJU1\l{%Ltxq$71)nTV;j*oGf Rx&,c;3n?({U޾;[/}x3ѹ4Nt/^qckSY157G5WK3e0L/O\h~yf UOZ߷:әy DEc3wWyԷ9rGgݎ{;2k߭7ò +Qyzhq27ՄO κ[ېX''s螎$?ۓU񏖎̴1ӈ#sCJ^fyL58zhҫ+qt>CALoA䧑衳91u?;([]{SYogT ϪE|L{yo.E>Jɇmy~:&2yUGG"֔)&vY}c0zGkKϿ79C|Cia;|߮5?]~#\3*gwRu6$t\t8Ք$;ueMb dcO_Ⱦv;|[!Y-5A9]2O5 tuﺝ-~{rc;f(Q_n}fDcf/_uoR|~8Z|9od_{^p9WoܹsfhV8uh}ZnJm\Nr]Lve=ɻՇk3k#p{#VjΝ9`?z4עvz,7}:z%ÌfjOfƎZLYb]{痁֯%\Q|ح, y%^YqoSVj<AHfM=X;Ihͻj/^i}Əz{?_z{;ӟܡ`:{{3df$fhYhM̌!7W˿DpĄSm凫mjc._YJ| 0>[oۼ2ӻ=3n /`dFk Jg3zB˙1ÿgF\[̌Йnj36`3 $]L^YjY@4n֣I.ҀN\*$ٝ"+:ͣЊ !{67!ZS׽d`Fi1C{"? &OXK&0#{Mc 71p3z:imf%2_5Fx}#(Z|Ko=Ww'aPyw䝸uk|Hݫg]?&|D|/#|?(7d_Č]Č3V=3~)3f3f)3jzfwqS9faC`ΪsW2$gQr'65fٹ5j3s{E bf=u^PFߺ.sJќp9C ~/ =>ghd03Rs434fй?1w0ّ3vȌ`FO3'0#Ggf\fE/7붵i=VWTx)!5 +Iϻq/PaPM~y5= *&5{rb՘S6 zy{=!Ìό85lݡ[ƌ5|3bOMbMs]q̢m't55K?u? zjD[dd= *|X1y Kĥ}kV010-!y5̹ZsAd >gk[YBs5IcV%nwk}f:_LW3m.jHȕٷ5^{nFȈ{ϰu{R"vQ{/(vAXkU ґARl ņ]cXc&{[v.9g=?Hu͙k˘?i^wȹpin\/F_4tӢw[=>3,g=n7{D*{A Q䜆{'!۶ߌ3,&Hj:̰K9iv,3nI3s53ֽ4qaFx2Ťq=cxcX!p5?:;\*|x j 74]!uÛytRÅJ#CLc)͵#eJb#N Ϗ$9f@r~VNLQ7GI@{|ǩ(f8f"fL`#3ʷuإՒÆ $}uMF0Lc5ؠ3Y:f @f_fP7f(OfЌf2jYGMl|d0d-,[ϿE=^[uu}C'[vF>S{mA^wڿUM> +'yָ\Ha=qƌr! N|Rf`?IkÇ,b ]8 reV0ffjfL_qxV3Z.8mˮfy4(8UQ~aM_^.zhK%[7~ S|l :N+I_=/ a뚫ٌ_ϫ'Y^ҒfYC~d #x(fg3%;QŌq rT@̛ : +f5CÌsKa0ӄ&yG&꾱aR  ˣgkķm길ڦ'C~c͛9_ʮk^9=29́ |ؙNec87\aɥ ! 6?o~oZ<{/4L\~C/D?ܐe95OvRk7kxy混}^$߽mZaȌ 7=ُL೙Q̔S>3seytzy97-j=kf]Ͳӆ-=hpr}:}a}C b w5"gy%sm +A,cx"'~be.3/_ҘA$  d/Kfqcf7qg<ۆ9n-~on~fo.zm×6"_ !ޥK?]- {Wƫc+/K| +lI+HsP쏡W:1O`SUC{XL=*f%LU3SӘic<3~3a'3e媍楞5?ykaW5?^TKo~ Mrs RAP2&JK_]꣭//lUpmsJew6޺VVwF;7U=<#j 3l\f fW<•1|fa\k~jA4[borU?f~J_ + *_ : _^4x|2/+W}yʢէ A7oc_hHS_i T0bDXnvC^v.M'tO>6Pzh:w ^Sf)a6cZۦ ɳHg3f8/}ot)5+jH +! \M!@ξ5}kS4H M/*ˎ:(:sVGb, C*N8ߟw-|NRob&Wwn +z~p^5ռЇTg -n5NO>C\}wFq \?˒>&Ù;QÌLf\zѡ~۰'bs;,h|O6o_^1(6R$0UJ3Wo9+|\.=~<:GaòLc 76֖ 5̓}^|/.]}FF޷Kxp{qw!:S9U{F_B=H̘2eK +Y~A!!k]_J_ޕo3wL&0),+okEtd=i U]rસh f8凿y}%rb݇_b2>΄r/nwZ-8C^B6qUPVfE)̨!4Ft?ҌkBx6rPs) K|fг}Tuo7ޟYpu/ſkūn<-Nr9mQ-2j<_^ʖkUlnR_[ +B3]B 1͵_>Nb@2j(`0f2\鲳⃕o$Of^=ѱBx9XoR&pLUyc'3K&f1!pBaE}Ob|aw+G/ޗKNn>'7 +eb6 ?U[osq yBnuǣܾ[sT&*:mCx`~k9'}Rf k!({仞EJAO}b z]|^>G00؝Obqkm vBI?u< +DBh:݉a7BˋlWwv?Uu g& NV?\mS}e J-avcK̖.U0^J4Qoa3~:Շqx-^ޣE#'{z2n1>Z3}IhJmHIǘв ҕG?c_k?[eF]?2W'V[~w]~*}%^̭=0+>_>:l<{5ߘvn`O\At?tAWC?]CE[+w2?jz>[?}QO)GΝϸ,1')^6tUbM|pO#/KG +jakTe0@*h&2[5P;7!O%r~TRuO˄S q.cS +i\և.¹w*c=]jy"#GS +OZ +Ɓm}N5gjUflH_i̿R{uü X K2էWkk?-w;nw;y?|G;_Q(:X,^/Kkw:G-ɏw]\:PxDn[77"^:{i*mHE}ƞ\6D+#-vUunKѽ3H4np9`ৠbbbM.N򚾚83+_ nRVם=L_ 6}6S+_~x48cت0g:~_{cq;1`r)N#A~]%G:ZV?)5 zN#W^cG0_a5&Bqp+Q'<{'z[ ]ݸ=O/" 373k9$}n;/hW%w^*DU4wZu7OߖqVR2$[}tg o_'S!PQ/u43efHs8/2sdS.2\O3u],!sTK]o+$YB+ KmzՌɶM GJGZA\Ep>N`Bfu?9 끻̵gW!܍Ta/I%G 5'|B푉$OUvջ*;S{zy!h; PERWat{^b()@w?;`!`7G&kvޘ#z$| + ?yɟ~)+렎+P^`S6󻆂GR1w$[!G'з"t +-2k#$2P#kOLn.ڟ.|'Z΃ >N> "Tվ7B-=o?y!?UއM̷yz L8ꃱk3T-Pu:+0=~3~42f:p,f匏 ܛ6Iox{*o_% O4aA6X`|1"4q[/͕:,;%[ ? U{`btgZ +nޟ7ߝk(¯~Y8*H>"B}^ ؚS\:^/PW7Sƾl2,ӔKhZg> 3}8feR7 4"׮?AvS讁C^3A +zE Cn880B(4H(mu`۞,}4Ui$xbF+hJOgRf g FUǨw};}p;%'wn1'2TXmVYAA&9 +L=uH8{?+V v NLޟ #>|r-_d;R,;n4 }8|V;y;B>H?+Q` vE NDo"ׄK_.hNm˖t WUfb{Zb5 6˜rZnVw ;d.[,2YЛإ TZnF2i5kN1%eDdY3SU0P;8:B#E뢳,;G76_m_w4h~U}R 4#OT꽯T\Ɵ~-h)㺸s$أMbluEd>.)6}2oPyW}_-uv~6P:U|n>,# |Pe0W.3K\2lzkeo};U'ư[퐋z.fk0 FVG#{}CΦ`CW zxuעCa}DU~ ,:hqLw JH%H|i^<+&Q~BF;!Pb'$C!TT`,fۣzupC*1jՒ: Š]^h31$E8|V{N:x_hP`=.{ <}o}}b΀a&A&Z*iq@nX~ AAM jh(\BEJ` ;2Ü-ͯ7mWiͱpc$3\x\}8 VPHQGyILcDC`)`ɈۉTsM8k\}d2e;2|~&}.ObNK =yȇ(XKǟ+t/Xrz*+/z 즏a~1[xnJfy tq3zĺDceX"pF 2{*op唝[jVtKu|;yTzt`*qMg Gߪc?~Z u^&9eG3AH#zbA'YSUklj +,㉯Xg'740  1p'XaKġ12J9UeyA,nxH sad t(3*C>·K/@`xa6.3:|jk6$Ƅ mcJvybV{&8H$Q4{!MC>x!gA!+)g19JRfd3:e\T?Xa,NcI`F i5^j)r$ IUƐ})BRéyჩvv=6Ki> +>xKOِgcM叟W%O5<8BH(e+Mw Rj`65?nȟ?~4QEduI&>vuV\䐚dCRT35ṁ2jo>GwTk>Bمe-#ɧg*1xkqdMfJYCNkbNļ: -GƙI*} Pˌk*K/+T< z`i6J$Ò(tmrAJI4Nll\sp2=ڔ5} } ;r30Cnva`j{\2 g'O"#Ė[q+-4 \T9ka棐Hθ̟:p3Hgg kk*ΨA'>؏~,fRٶ (ܫP_4\ˋےaGj ] 'X[@AF=t4qZF-MщyRRfFJ-WZ/.7~8v-D&n:_l/v ͵]Ƿ: aڲ[t +X|ڽwUoUlX)9>Z{2t=|W~4w=q,cĘ +K1В#OE\iT" XqQØa- l"q^T1uX>O?s_xڟ ;\-DMVT1,Cp|cˁNN7_+}\|뼫麭~"4%#tEuC5'fחɭ7܉mМ6BŮQ$M<*[hbm{IݯnuR𠅊4?fXF k;.چ[mC/g-\P m-w]׈ԵJfШ֟whivTs\ m-qg-CRlzgvwj-ɟGP^ߑ ++^Gw!w= +Jztऋkov"RTYp6G}`|xJ!F1"n7x|dOzit}tY`|PjwKSzqt1sO)ŭ56d\B5]}_] t=X.7̧ڀ/B?B+X{Jw:xΘPo91};wܛOl6;n/@ ,u=``O-_."7t5#츹PP\Ih}D㡏rzwp95UfXsn<1SsG箠i7>R`IeU5]_+:^,*ǕNoBH)u}5֨uH uD)mS?HB3؝J6VS1U/U/5NJ[O&izoX6w BF|zj[hIhgq[I^vR{Aev(^x) LA8_]A]f 0P[E$ނO+ h niJ+l֝ThldZPltY=,nqcD}`q2|nA] )5e b[.V$ڋI^W9@aq|:kuxhJE5.+Gf#6kRK kз>V"P#qWj qY?A9uzCPJ~ڪ3Q=$1N1Cʹ}(s9{>zG\g;S]a`*j/qݱܮG37ϝKםBu;_wnt1Xh>zGb=e<_UI$ 1SoNTb+D%!.Rݬ~J;0ɳӦkHc\nEW/M=|➯}v@;kE:<ڲgŠqlu1V8Lٰ s2m)Sy12ЀȨ nw +6NWI(fm?Yܚ^עC_,?`3˕LNӶ,uS=lO}=DvTg(i(X]vwԇ\G;jPmBhSPmxʥ3lO,m˩9Yb[Кkjȩ|ZS>I."zA_^|o|4^g#~ 4ëĚ}ƳΚ;JϽxZ/%1r;?o=u'_P@!H P{$^oGkNL\pUd)X2usBI MDmZAs=^l]$W=TsMEЎ&Yhg58{a}PʀvQ*g :]0!m|YEyjgE;/7NK\mIurR uбorK_G|!WMLsz`VþDbtk ~'Mwɝןދ4hhjwd3ͱ&%!C7+ 3R!F,tJ;7!ϕsۇ"Aۏ1W !F*!vbJuXWZEtu=zmkM:/.$ t~,MԒֈ5 +kD@="%I[kɲD{GCS6os- .|uv[$'~`v͆S5Mm#]w<k`qKvyOIwܐb PSd+¾U9 +-TAH[ۇ O4ZN$fv֎;λXKS; V?Yέ^쿵Jz3E|ѧ#1w׮C+6l8rxv/E^a ^u|h0frM.Wɫ3]==+v,Ifq=)Jq+i0J)#ֳZb%J e(/0*FGrԗmtTqP-ʃcǞԬ܁} 7@암 +#@;8!G~ץM/eSjF9"\H4$ .媮1@];Y˭%Rwd6u8BBn<9SM ׶o!PJwo.es.G$ߺL_i.*$qHp홠%OLɃ|ۅjgQOvU,?EmTnMv/A?Y%=YJS;+c‰Ԓ9/㋶; $eT8 +CVp|t3PdKz8ǻhɵYQ}^modBr!&_Γj5Z'ħCM}4'5yd~/<'P1j}ScuDox_a@w O!RWmn?OabLCN/+IܖOKj9ja󉛼?#=.BO?,T;4MYgmje_5jm+6 \>&8[zv1-x.bz\5'f-8XׇŘB :RY+=wh ߻i6Ξj8ǭZխrU_]?pMŊG16B-Fƈ4?SdTaFbx..w`h'KLvdܫX>_JE,t&Ju{~HA.8+hAc$kTgkn4q6[5xzttٛS}fĝfǧkH + oh +P<3ߐPMh\&@  hzCy yq_EΏXj:MhX4Nn +:_z~_۲D'tO2:e;Fʫ*>DH̷})sH`w>wvD|H{GKT; Z!XZO쳲9_hghgɛ?#6]A[I69\}h uyW;FQ=cgH[/ΣZO_HhUuOn=jOLt>]wj.{!*L=8kwyEtm+\Dj,Zk;>EnDre$*Rh9|ɫmͲev|j D.z(HIuضcauc`]`T4RW4]6hnGDŽ3t;W_&XEè %MɸA5Vۈ4G8:^zmD4坣-m!\Wԣ/ljRe ʮ3r=ۤFCOul;z2GJjg9XזryߎZ BzxT Ry5x WA|r۠;EXCj;K5 ܵOM&ȇCO]]uLRϢo ]_.#3|Ձ%9rt^7)8Z37]Xn֯4HNM5Ww9BwK$gc &Q 1KKzQ-OY5Nз%IT}f9s ,֢83hmzjj?.|0kbLK62k*(^/4ZFBg k%;<&$mСpiӅܒΣ=t? +c&q4ڵyϗiRJk!i2`_xECfCtk!՝>1Cl|횽㩦94oIGs>@Su4gg󻞸sM]oqeV4&?8 ~&S7Օ:{BĿMpOO:y&TS*Ze< y2ŚDe6 DC^{&~ @ۿ\$n8sK\$x {= +b%g6DΊ>%^B h֫nth ^Xh=X NL +D2*8'VVȫ]s=zNӬ?0kQRbK5HF嶺i<73 +bi+M`O_`ɂ%yh'[aRnx4~ u0lz/4ȠWwd*[ NFw_NxZ/v^C=ԪnAo{~g!|f=%XT㱛v5tbpj}Wѽ'Xk +BrǝlMf{:$vv7nFb={6ɑG`͝=c,3Vts@NH̦A2?e췪;Vz0zləb%Ҧ.O/+F +v:r͟|bE0Ǥ.G~ \۵\ӕE,)gYhKᰇdJCW[i VZ$wzh2l\ָ)MRVQ_.$>u8]#57p/S1AjA.;$oJ Զ1Mm yZs@O li˻w +5[vFIu=GЂ`O={%>EnMg'T[:XͩOp&yŚ C1ǥzO:WЊ:;c rJyl@Əgb +ϕk{zb0yNYُ+%94;N$6Vv8hQ2ܧ]g: u:?l8>^w` B7/EsrmtE#h\>:jsMjdD1=7{i|؊N2N%_J_H{ AGn,tzHH#+mtx?x?x?x?x?ر CSCmDon>sUR#SlmOwKI]9 'fEVB>?mh$BR"DfM8M1HD +f&DǰbJ.4DJ4|L9B1oIy=U͆0#-yU&:F 9Hc$@ϽrX:lWAG(g~^T)=J*h*U؊u:{!w.V)К2;)&(:]L9Ow@>5chcA`VE)UF~J-ڒ] R%9k(x :G՚xN);(t*Ĕ[r1lX +K _ioA紸_Pxo,R0l 2jHe֬h u }y;!F{FN7tIBTve.ZV767R[7;Yr;k27k;b˨a?R\aiJ0 [k7];+HvL tP*II+4g'R2tA+4UmABW< c"'Q"dlR`NJ}SKl5QTy5r\b9\ >`*F&$^򺪃55Ǧ26 |K9:@"BjH;JaڢxyrӍE` +z(BrZGƂ:N%Ѡ(P%b"3Ȝ7RrzU|?Zň^+A( !ڈ9[YJo/EPRL RX[l"iJGblVo2+Vd?hYJq.e6>Je.T}v@)]p6.12%Oǒbqd*;tR^z:Ny5]Bd +U쟠pjnZJq9Nn"k%>LthG*+167:a3~Z*)9]=357ukS+iRl^1ϭ]{kc@D;:Fj [_ۉ~ɅrQptNt#qS7A+12.59n&muo]WAwd,a!> 2o.7!`W +_ŀĊy&_ϰ2y=I|x2ā^f6t og?BdSh->dyi._-tGюq~e֚ څ&D?^ 3֠c&ؒ I6PI #ٖ#hRJ72*̳@7r:`[r,O2p0yI+E䰌O_{&2'/hCЄkjߤ]s`|.c(XbT"R9hV'g @1@<ۄqZԕ[BG,: +7JX#04':R5Mݶ f cpB>C)9 TLF_:`kvYWw.a.IN*9vܖM0Wrs,ѥG}* 6pIdVP"0|tz|FPWcH\XY` j*eO"1FE1@`m7c@BPu#ƐSIU7ҘjӫS (S͠#>s(} TVd(mI#:wi# Lc %9tGE?SO1)qݱ bDϤݔB,p@)$jᠭ̈/'ׂW~_ŔRQ +:![ImYHm lz%w%6B8}բ~5c.{Km@bWµ\~/H|ՀiICtn7om >/nh: +WD;J9̓N,9K5 +t&~TttkWs:AC(OƓ2k_d>js՗_#0K 5smi%GA7lt IY\2?(1BrB:O)> +RRGI㕶GR1y?:Ca3"Dώ||ڵLrHUv@% ǧMUj6R_If? + ;2*̑RfK/eݣA?]\T24%m#ړSy,.1]PT[IJOA͋pmI݂-Y*a_X!O)H2RXВVxcXm!ف$Pyig뚣Xb 1r偱b͇A hĬ{pP(kwA^ r+R!qy"(Qy-Z28KL 09-Su-4R0HcIs;?~O*$ s +Y.oEIUw9 + 5#~>s eGaQLR3ǙfI㡨zC傓iGd +$J1 1I% 2˜'ZOiUu_F:k]ɒZ1ZB|7!u{J&uJcC aBL&$/f_u}(~Bh)dF$xe-*RL6!$Z!iJaz dz#Œ:] UԸdU 5": +6 vY$y`RR?l0qb$i&l,m#y5T (5Plۤ>-}$qӏ!Yf(3͠4+EKE)X 8 e K@VH]Dj: Í(QO\Al72H렼sP@㣲1ِUtuKNRG| u#Q_ +E9pjFRゾ  y՟o E cq +*B7aÇS>E|lQ:%kOEA &z/m2@ZA4ԇ-+@*U'A[}$Xjsj{uChӉor1U=X뙔P4JS{9|sw~˨cӵy @3c+kn"Ua5g)o`BLnoDžm_/4Uk [{r&[n̗?pۿtVHJqpԥZ%OJi~Lu'&Kg de.؞zU u?ǯ|COѹIC͕\>΁i)"]\ +"ȍK/ +&lФ!_85wg9(_?0N ]SsɸiD`R#LNŴMt +Kק@~>&s6^W1ǟ`Ҵu'IX 㝳FVn]R~(M<@$ԫr㑼7_[VEд&v~,u4u7A +7,L$='s9Fs!^i{˩D٫DfҖG9E d6Cs\ Sl@"La 92!p^ćm@MHz-O~ek\!eGb.GQ0lSy? H ԭvޣ nR`,FSX<8gWa"sMIrӫ`2O0} _1d?M[c^[n_ ^7)jZR̝Wri\8۫Ӌ|׀O$#>]'?GGqݒ>a_e /TErDi9s1S4%qL ftFc^c6LMJ*:(HI*c4@%0P +ܹqƱ+ +MM( +0>~J:`9U̟MVh5NY0U0(Lb5Hj3d ("30W)D|}+s'_gu#ȿN +hyqGSEy_7|!ryg%_(򺱂ly&N "c(BSQk,Diط;85XERrNqh)؇ل1 $l"k^VJ9u +C><|(àHe[<%0.:LӃ\s<Ԏ/$cw߬1ɐdpN q!5PCgS2!0W2?m@ B|fNi1'Ĥ!5qt(I!Җ߿=&+,BFa˹rg.#bv}A%Z&sUq ؆NvWJGMuːǛN 2N__^o +{OˡF\r-㔡LvoQWs2#$kPǮi- )tݾ7r 1ǾK};En?NF0(xZN Z? @ wh2?/5sQ&F2A&q~4N wc<!}w&L5ǡ17L}&f3R#`^.؆ʏ}=2~/ss +gMgȩo B\ jRdl,AMA=8L8T j6_jWFM|EvkcrA5}⓹ +TȨ]tPzP+߅njףUDD6Pt7U\p7P3%uLrS@܊(ALNcnMx&vO5 蕀<p~ \>|Xxr&9OP&ڎEK3OQ ǀ?jĘ-~S.E4e;IM{Am#m]N B2]@'[%-/rmr&a8 +rXb@ɳab*W+?9^MYM0chg ԁ )>ۥD. V0@z;s} N-gPH'URJAe9< { qW5!585l72+́یw9;n/D#J$tQ T\$2s?S?p~(XS>~q46PdR7"(ya? x#zOʡ:ڿSۣ@=r1{!o5,;_q̄|!(q ^ƀc&`ryaR%ngJN.>S[A  x0 t\AOsjz2'shn`'\ 5,N>ZJ|P=}ey"8ld8n/\'s?DR aO7PaO6 MQ]Mf Ȥ~PCYa_~-a8BG -"r; ykǩAP%;85̟BO+"(BSv^*+_ qa$evTS.Ԅq"Kw1E;;׭G8i$qNhϤv둉ꐗ\,Kwhya5%ym2a4x9䏀NCX^-=}mUA&R!2`;y>`2'_rTA[5 1Ǹz(N*Z&ra HP:;~6A(uHsiX$T*˽[N[=Lr.l՟jGK95;`)[DB ƚRRx(ȵ ).0B=n)KWr~&m9%(~BT#H<4ca f(IT8_U*(+3a*:= K&zejvAМD^QJ8Gfkj%P!؏Isݸ[y))(7Y'u wD׈ox]`8vp=^Q1EELBP g*8nĠl~gS *@_^F=_(+Am 0͡x\Ӗ0J5X] +ypȭH4{8;6qo#y)9eI=reo^V=-=^ĒҔ#QjSoly +LŇ)\=0Fl63.JD`nqVI(nI ɟ'J:ba;Q>i+ %q^A A7-c95SiL|:X&ub(ڑݼ*%ujBߏ.C_0=\MبMlՄ@Uj!s$U=p(uUL |;Ŏa:آ~NY/y)q2#z(s>\_5x7 jG[=?8.`WjSB ؠF +>(IκNrGLj/`}s8fہZ>ܔf]ڋ˫CDS"I >:Mz{K7=*9d_hMoBPO 6q| r;`(RImI72!*Y+95]+A>6+Wr]'Q%@Q<:8quyͩʻ`/5Џpw-r+l{R~W9N뙶y*UkhGhd_A03#ldxߛ+9'ԇ8$[G+r%}܌Nifo.n-h*t:i<r- Cل0jf"GE+BV~X}Z#sdƐdcx Cj\T?_>xx +`v3]e^ j\rSA}jC^k2W P0O.MNxP>8#\vQsA rRg|UKz#`f*=P>r= +\C!py芝3oN \}೾Z8F*(JdKКAIU{mK)1D攚rpAvJ05۠dīDXF.oQO<20p (K=ozz\L/J3?\׀rkK1g +.ܤ|W೸ w6 +xm\\O7uA!f.=G>1uh>~hA8l&uP1ҕE[wq/=S$8r~ W.;[!zrofTKuq `l{g59cB y~״E\^jr|aqsΏ:ԚFm u.Ŝ2[A7'F-x:Ԥ +ꍢ~S5c_E.N +l IOJUȫpkrį )`=\82 ҲHoр\(햺qMV>+1e;D6ryi|6_6s}nZ1nmxEX#v뗅y!8Th*[W[Ctw +iq땄"ԮἋ[!;Se "|ru 9h:J.s#zcu (G꩷8=rKӊ3&Nُ%[qP(aӁymp@y z7Sz})L@ơ{:b*:J^ Tc9>}+5􉚺/x)PX|=g5K@ K╳ )P0+0`%pqSO`/L`fP}]ʠj$.FqJ!(sFx 8<C%Uױܳ*NI@Г >/8rKq^I0uLB.n圹n&n~ҡn n~f0"_3%>r'}"~2m@%TN'őE22$cwC~Fr3eqջA3 }:Oou@sIͪ&6%"J4d=O2]| ׬ +v&񼳊˥rY+GR*z* +aԴJkg4Di HxDLNk2] zG=zf>D?޽֝Dw-yLaIVZt_wY5̽&^Z qv&K66 UNF44zt@ z 谢"^90o'@>x7߬jiV!*Pp^/qk1u\M5|!" +.|_dIun]AW,i>rpYdU)|CH&% +3Y +퀠Z\uF|*z2}'K]$51_"yYBj YJ _])ӡc߳ xkU6֑6-u\bIg15m(jǹR(xB#}G7a&E>'2y/ ysDy@|1$0Z$N> +O?SL¿/D$W^h)iVlHkc@, +GdG([$k1YlĵiT40'j_h%{Q~F|T+iK3ZIzJԬ'N6bҺ'A͊~9%/VD~_+;Α/$tN |+&|"* ]rAa>X2'lfkXKEÆ~{Kj> x49_q)Wo]C!_|{I]E?^#]M_3M< #zUvue-c"k" ]Jl%/kNH6&VIF8$bL":_UM1Q͂(W2X+Sws k͐nN +( +.qN9𛓤f咞_i{7Uq 3AK!a}DҀ4YAI1A~%o2k)6_i>).nYZTi--7ʿ*~|AmI^9U%$cIi~YIAYLMRv +.Gl&kB{Ⱥd}U뢾Fo悒?̈_,_R>^ 6xJ?UZv=|j*>^u[<"*z2VQ'~`i,,4T4ʜ~&a~ϻU8$iQv "!hO˼jTT\V;ɚ*w{f·Q^E=%-+u,\d6`on;1g.F] +;y5#t /qrwCH~&vL[@&|Pn 1z v ^,0NB 1k`і'7-><`pQU軹CV]bHC=SDQ +b %uNݍИ|Qi}'5n3)h"fuy^?37w[gFm"b޻yx j:$T`H~NdFSag¥nuem!7|F7 XsX2㻶0ktEYyӎD !U/Ѳ*/ˎQGڟFVZ\$t vV'LO~7>&w' šoG:'ؓOեWLTo6NaQoɖr +(15b-FHTI>>~scB0'GA“d8?-| ( ؕb7[Ӵ:4MKS!o|Ԗj=iZQ"-p=1OR/\˗_O<$G><9"ZQ+Pa:Xr16ֽ7έ769ң7QR|k;oNpzxpKxZ<>vC^or0^>.GyQ% 'H /I7e7S̭fF/ k|v93\sjq'K;% +k~%*~56exI][S?kx8`wns.R=.^ J9i'xxל5& +0+wx9=`0ioGw n v _e'/*h +|hX?YZ>]bsXrsX|KGOS?`EĒ]sf%Ůf E!ⷍgDJ$I(yvHZxQRc/ҚZw +Djyyk\z ~-iXr1>D8" C$_vg4:EFԺƞiI`:꜏vdE6Ƹ{GF:oWÕUiӫ+baY%Rc홡->&YuUQ~III} + yUla^^偑NUqn>ޱTgVodɯ3:#<-̮.W\\`coYCP,퓍|3}2ϴIgmDe]k`zUir>$)~͕Y/=Qg/?9? +]Vz24l}|crYOEs1@W%zAE/~9dTz)=tsFYz_y.~՚v;q7̢ܤ=nOzOOΗZ#^7z)01\5cz Tᵍ¯J[8|+ ~oF ?ND#&fYS֠u[U38n G5oZ54K(1EAn~=O6:dsGoKUpc7761@g5^H + xMӱMşj0nuan,{3Y\Rjy]e4a:zHϵɛЪcwGhf +YC-U&^tCbhMK:EN1M.Mcj_u +9,#LnTqg ۷߽#hn;S˅h%Y;lO;3kyεawss߸Dw)wI(-v{QSP./ ˥/wyx`RVMu yCE5+˞gcO)fѤa3>M>i. дKѤKDhʘ5hLJ wEĄPpRӁ\M)rcBa_طɛ.ƜӪ9$;{H+ò]zR4Kqeo_`MJY79wӶ#̩[ QqT|F4sp $4Ca=v=e=;k/ZVo?mq4w]DY4kPDsp?[Gj'h~`C)OyZ-kLqK oJ"*"kl#jbkJb` ^$M=<8{C?:9 +)M]}J4sZ|vSw#ii{[a-EOzh;e+*FÇnx}YQ.!\{}R[CB[KxiԲ{_ZϦ*FS1Cj>s-@N״Qьk6U}c4Bs6*g}h7ZDdhLaw3TKٍToKn^!1k{! qam]mvy竣C4߲fOo_ =DJHiB9O{!1+?{7n=[ܙ{тEZ8Z +-rFK5Uk4_iVCԞ+yQWy!A]z_=9/) 8gˮpyHWWKg^op)c߿) _l38ۜߧ+[f[[}#&B3cX)5G,'В=hQZZUQ;n]vFqIiKI}CtWvk+s ~Vb:Smtm|Y]l\kɏkV=_d 5xgstşҤ^)6`;i>]#ZHiZ9_xր ;oU+M6lqݳ;8:j16\eYc[gJ\wcBG=-,ok?x-}_RG u:W L|Y7{7;g/-)hQB&Gr6Z`ܩk1o37g;Ɣ

9٘Zdܛ둧nĘ:Ն$UXFFFrH8?1ϣ:.e4}Z4{>Zٗs]]xR |wfN5eSU>E=]U7Ğ3Ğ})0{1Z|89`'] ^)-.t1CN4g,d4j.7eZZ>VheZc3|ۑޏvOգlV[=o7Z*oN kղUogjd˵kYmzVhƊ X`|:"0 +cgʠet !!$"ynw稊. [|` +ܞ_,54s&RZa;h qo*n󫙸;_5O%u_%.)*G׋Y'oT:qg4?a7a| k1xR͊t|J60(ZAXT}^F俩30?nT;}n*L݇gǘV7ת؊R +pU Tֆ>gx-As窠M&hvvh׉;MQ-dWi`OϜ_+lMg?~gi}gyڟYwVj=Hr3bt| m7Ҡ2nxskBu;ou^Ƥb }:FIf=I?'}ڪu!vܒ8߽M6lkh#{Oܙ*aۮ% ~e`}g%؝efnVwum<1$ET"|9=l{Zmڏ-:e5wg\|c|fU32=y;D%1IjTb۔ySASD9E9dgmBm5 KK.?lS,&@K8~m̝ٓƿg>/e 20x׳֤=+cm[|^bA'G֊7C3[SYz<"J6/*q;y}i97FshW>2_^)KoE$yhhąGoE Whۉ;÷]+Ҩdwyqs2qC%׬Tn{íh`?=*1*`y YS%8_] oе1e6Rٱ9?aٖD9ag^+KN|Ǭ5kdر82?t4Dx7]R49E}!"d'9I0)a>ݰH񒬠 apq&VQS ΅mx-ъ\>}؟v9MVqmf|m S 랣]Q |BVe>?ōDvx.<%mt6;29&n>ݹA;?thG[Ͽ5Fm"2}aDVC,='W2&tiz%+,6+-r=%n2[Dz~"d"6}.wo3'D?@kϠg]R_t"vk`t4ȄoLq D%{Gk h9h璹h7\+ }Eo${O3 c( 0KVߡ)JҢr7Q]M ]uA\tHm?zW=`U{Ԫ`ULq8YqỏdP?9JIOd|]K @/cuP CV8kqɍ{M.EMٽ[ W6͝:c-mg:3EzvTRQa'x _p=_5> i{5Zcժh9r hVV W;_OYzgбFΩ3Mn,4Iorw /v FN_J6qy1bc795:=AGCiދz8^Dj +P.Qs1[ȈǛ1oi6|.*G/7ϪV8[1-)vU_Ԅ*+!ysMu=w^ 54c26CWl!uǂɚM~&|t!uai[Ct#^^t ztKpx)hnCFHDfǹd,6 +%  k<"t+ᗳZN_\ܬg^%eKw2Wg3ϝdR>*f7l04TȚʼMn]]0W]x6H6{ 3?XkENvn6]rd5-920a m#*D=χOٯ{F + +=m_]SGf ŃZwvd,2pԛ9|"6?kj|;s1u}[ߴmQnH)lCfvQ -~\V~ЛnV>([W1ngwjFzAP50xbMY?S6<8⌢ɁӊzhǪh߶mHgȃF M}_vs |oIϔ<ϤeR{o%x*|0v߲HUfSNaJsO(yA[$ J?\"yrPZY}wl{AB"BTϋ,bH[3c/eO:1^! |J)})T3kD;TѾf(LnRuk,!x&OxN5],uMT”&U8sͥ k_*xD`0OgRܳ.61J<3q"%f0O虵8nq _UMJ7RTn|al3ԤowSgæ|A&DL^ +b,:4LMeڶtڳAsGu ϲ,=%i0~?֊fJtQ@eRNs}\J4ѳk okfšģ|o􏪂_Qz((}^  ymG1Lgq.3zj1tQgtQg.M6fϖI*-lG.7&)To=Rx5Yީƈ133`n1}pEEI:db0j/Ң3 dwn j2>vK^z0Ǣݩg繈hk/Wԓ ugm"ېZNN'#gf{[ zg6MWvYA85L_b#/*&6sDuU#h44SB#>C #9]&AⵂD%=c@MWfa1KF蒒z͘DN_̿>8C|YEx~+}Vw̴}=+51k}D[pQZVOX?CZ4)8f3pمj4^?؈3 ++D/\:ӌ:j=8=\t `, :MyTArN*KR}: wEϜV=6{YzJ׆g(y6Jj2jwo% +7Êכ؆N;1],89EC-n S,'2zHAjQyM2:"nWNκ߽ԵDxVN U{K0[O(w'\ +ZR!̹?4<0KsHKQ<(ȟ`[rŗ_:xn1O{f-'N8!ιq<`LZ>#Wqe}ԭ}ѭ۾eD=s„kjpY^){!oOZOCy_.LP'RL2R~RN[U"j7mK֣]6#]#_6LgD!-3FVSa xFf-¬?P 87*Lb~2YfvgQKMF;b]3}19fbn +9uRQv#?j_02zCsl;u nYAߴV}K'n3I)cY=a.'L2rmhOЮ0^r +i;-qD^]9t0/!8=L?}y,Hq+QqMۃDG-2Z&J̋/Lɳᛐ\AD duGBAj^"Lj6&Rh/y|9EuDof=89tD?q3f#`$8Ҟ+Z/qO9IFe:P 6ŦIrXFG#idi4-cq1w !JrJ!ޔ۾~㾵֩3#Ygʌ8֜}Z{U^{-:֞O: cG6rEӟtyw+Jų ۶ț56t'[x5Nóx~-nM_3wwUج=0XâIQkHqZvT9tm_uSfͼe}B_kؐ۲]+O_}<栤/m&vM?u6-{آ89nظ{]L1f`}vc'7x5ũO0pK*_F3?,?9tߑ+Bkw5 <:^-{,]gƆ)/oL {o/oX9 dP6熚2㉹|>kJLԲ1f*KG^CYpS#8{C < +;}19+1F`ɡdhSL\^ZKrŮo+O=lT^YENXXc*'9iim=6ۄ2g7}^1NA1 +<Ű#_.dž_6ǰTwFÓWk^csu lyP?1?Ǝ`tspmПnz0&Ptk;alI1cR,m=C_qFR{?y[~ 3|^;M|Ow>" ~;ػHwn, Ƽ7:'vN#~%AsvBbbn&1Z |k"o}˟FѸvB1xijh~z +<.g07H >L3,gמ^ŨY\ywo[m28F1s 'LĘ w,;aɚp qºֲ7--Ro]:)rӗ`q\w*-%'-.C5>cɄ(~sWP,U-Dv-J~RNgenOd[?e"|y:?cO/r$!<l)m?'UW֕]R6lDr `>ĺk[oZNSmrk΢iK=cEa19pQn7~7M-ηK1僯{}{n| +a:#gBKws[,1uЦ@9vx﹯c{<׹xSCm=;&W>aѲI  ladʱ Xڣc r?#rזs0.#yP1wdKWc:aZg/XwbF->bC P.X̧<]7H紆ۓoOiy12ʆ?TB˞\x[xc01!ԾD!ջ,rF]su0m1l?x1QE-wNE`Y|̉:Vdu%AGж`V /8Eao<{%7a3<ÏR̻U9;1w_OZ~Mr׻<ц?PG~EKL9=1k8; .{'EuהW+eoZPV߶|y`VR4$߮kOb@kf/hbgR^y m +<F:]K/_6o{;l3j0,pV0w^}ESz@hkNܜDxI*a|aLZ_,زH7 u\ZBG^AqAtMxU9= +˅9B .gjdb|C-b1F`3B~[ k`F_,GwD_]( +aMZbT\`ǦSz'aG(&1u ;N\Bx=Nw7e;XG|3>QNW| #0V39rwql+S)sȡĞ.9GḌ?3Gy|Jh*r7tzd2i` ߾/}G/yBlۼYs`>ʂK6cɷE;'N&ͫ S]&_%RJ2.:.l_2tY'cRe[ΎݽskF0gT_e +c1o}A~x 9zt@?V7ܹ eR[=8NQ'o|˛bDWZ~?;b? ?{߭<8I,оo¸_K֓)04yal0@_{Sc\8ͧ7<盞|s]Ƶ;.| x*j2d7={ȺiOs3BG1?|o pI1tpKIw)>\% Xm+N 'T07B c~3%][W* $:&aɔwٔ9"vÚ?S"kȇ3N{kSk[|x2dMsv&I0w"ƥwNj^EMky>~1`,(k>1x[ wN}'3û߿bQ_A o9p}W ?v䓺nZr崁~Bÿ +i]\Xzo9e=sw(Tq!|37pYC4uq& keOTC"+]Haog͋9p]6 |O`/BOF}Y$뇹f}SOo8&B+{qyƸMwmfg{\s=&Fvv`/UFƺ ox +{[Ӣ2?rugkn ozm +o>zŷ{ڦ7}?>F3]m`b 19HbwEknQc̭ 4m|pgE]OnΟ b!|x=9z,[kjȝzT pMķqK&@mZe-yq)ފMgE{9O}tkt7S.;7޴ _̱dʃ jbq>:-ʧuw?L64Wykšsmg?Y?m lhc=Ywt+)"7qá+Qco\zu${0It E׽xը+`"#z!-_s"{M{?Pm'&OWrpdD21.'`^^ӟϏmyNjqq ?9) }k>Bq1ߑ0(/._36W1TO m}cnn|pi>X\Tv|须ŜsOgŸ0r=7ֲևA}ok$7mga\U'㼋e֝rcb\nʝGsG?|#1;kZe©BK&b<N'Se ai4|+#s@,t&<3M?]9V(qצ)o]ټt7n\s:TJ#wtsW~9_,wWḙʴ= sGҫNrg=rq2̝TVWLyp u,?Eϝu΢xFĪIt/\yFq۵rCqɘDzqݣc,U _{OGyxV~Vcjߡ4jU>5G n:w>`ĝq_qFߋ/(0f/;|iâ'Dw}l7keBm Gznd Pan@]'O89l8r/xVl7.ۡk9}> -yk7̾rg± A3weF2vaS _'|jL8qʓ}'G'FW<sqS~5.{b䟡QS m˘n==}Պ\C964GsJ1wxgNoQ%ꤸM=%Zs*Pn .𷦏\N 3IFT9-bBy}ϑ&c2̝6rgrg5{x/MKFm 8w1g٨!m\%Go#ҺDʽ囗=t.xh6cnS>5ڸ'͛_&j=S.^*DbX̍GAv@p^]>s%b\=HYm$1q/ZVn#g/0'vxFhOY%#j֗׸s6<}AGrT\;\_1ѧmVK~ǺN[rgQ'ʝ;kn̝u|g/Xݔ)wV#OYYYi#w!-c΋>}1`.ea~s+Py;"wMT3P,kDsܕ1ec,{"zcć^ ?/:m5:zO +[-MD|fa21rɸ700﴿ 8?[` +=NCy eoˀ3wr=wVM;eotXf󙾆l;ƽƕ{79[=(Ϡ| 0/ Ų||y+Qgw%OkaC~~ƒGؖo_yIc~0e oc)`]~Q -rg)w'7Fѣ_܆>hP.uWXh$rIF,\_œ_Sb_$ ?ҵG~ 4ny:]Z}uu<?(o̦<,ʎ(&xk<|>(oI0}xоOo5s\v}Xfd᜺r7ݳr +ܿHЦВ'B mQv1Wl<N ֻNh|:ڷ+>s O?ڵC.8ȸW燻7ߘ>2}~@.rqKHF\< vz2\]o3qcQ~5{.FӼiضoO9pp}S)l-PSw#`7(' +]wq| u}<"`x̋y~B;ߚNcsˬ"YtU_ߌ3|_oءOǏ6yj`zK#==}`Sp_*K;ς |y3 +=4<5/XAZs4ʝBp=N/κW˝ybhO +2qI9[P>=!|ƃQGv#xWtV0ߖS>n< u=浘|`ַ+RWf#P/zXǽz-wVh׏x}Qd[, {?R~oh_qB{\iz.N`_6rgmw6 +zc=vWhقzWwQcƘ#YhjiXއ6;s4l}@?`{쁷7ٷ+oڛT=TmF^[bKhߡ)^W"ygDWxGУ^^/zR̳kx,GYQC\qkb^yMk%ͣ#W5> +׸ބ׮#>7ܽ<\yr{Rto_O?-l|賿 p&5;&O}_M91-|n{u"{B́z{|?TwL]o Lzkϣ.p)w?vA{='=Ǣ#}0:hD_A9U@۴}XA7$#`އ<59-<aO:ۍg羸sp\z]ʯWV @zΊDK~$p +?DJ{qh$pSgYˉ0 +{c;_p}~y"zE畱?aܫ\_KM'ֈ>@[^z-7{OwG74x,فc昖xυ;5y%G3ҝ"\os +u Z0șG3V[ iﰡ}~]郭wPc1Ȳ x{~&c_s7{/9xx}E V1C}'G{f +C{htG= [0fÝ+N9f.A_HSb(w-DW+lG?q.ȿQ[&# &|S+v9)0+C9c`[\k9xwYk&?Ə0)8sbqXtn=3OP/16uHaP?x.mt0+}['pn?8UkLu-7N1Kό:*;tog*)F4uL*3:ƣ1}>nnδ/̴,B T`*39E5_Zf"?zxObNwDgSj1v xjEgg rOw7\;@4=F6IhpcVA"d lŮೣ>ʒNqIw%tc/ݚZluYNtGW')`D;Htڄ힙 X| (&k/ X&cRz&uҴۻ@tǵ]ŵ]еƝ XF%dV%&RqXLƭZ1S6QOmVbi2 *޳uZRHU{{zΪAq,FksL2us6 RL-ޓ >3Jw7{*v[7SqUȝB_ښNg,[<-jW +4Qǰ+Z;1{ Лw^ޕLt:-cY(ΞL|HZKRs >N1UV)J[l!2v"udgYL[ӎ>CC{ vJǚqū-qіCs, 8dGD(jD;;"ls◙dfqG"|cd܏I.tE([Jrx,Ht/J %KO%*t.vGãQI8UөDbm:?"_.`V[d*|Lmu-1"%u;ޖuNENww-Nҋ1[/ws~Vȹ۸i#i.OCu8vڤX'9R[L/U#rp,@"8F1Z[ &"%se$S)Gm& +;U2VIOwhiq7.+>)EJ;(Ջ㝝/JȖ_pԑw5T$ 8J[̂xW$cɻd.v+]97:G}vġ*[%sq\Bq&^o8DZTq>|8?3[^2wE w)Uc;ܧA:f?ڳ"|Y{z=R}qwdJg"pCk<պ AVxwr2iGz Lw:Јvgdg"[w{赵%3ɥ gBۓT!>ש1LɎQ)wxqiufs%$یc73+WǙq8S-v:|rLgZǍ8bb8:ΰLjqq\Bu)F:ng +E>UƵWj|`JR,`xBQ=J/Fcۆq2r^, +\ڟ#%t 4._;_q#&o%Umv7[IrnrZys[)q7Wwscf6j.qa19˾gII$'cIN"u#:vmЮ/Vb5%'Rux_saGi,mt8DK> 'n70 :;c< =~7.YRXlK';ͱl]xfVVbsٔd DGz~-*RH:"?%0I_ck~`4vn`4 +bEZ]Zk{ K/8^`[2?=9O[2,AvE }gG--%g;Yܑ8JIEꔒ$ASǐ=8vsυ,%4wv&Y;3 Q*{7K9<{sv-s-ξg)ݦd{{oO: wWnvLº/Ш#חH˜bJ.Z+Z1xcs: /yP"ԍpUl"tD]ѱJRndq٪nuZ cۆqժr^oq-;z},sPڇms!AXE>W{݉uqZke# sDZP,3:/WF+0SeɶGwD!>Y㣎ej!=ʨV\= L[,}[ +QOi/(e9"G Xȥw.֙xNOY];؝i_&idn Tgʌc7ގL|iًqxObNwDgs "Ԩ#gh"hij-lҡ*0LuO `~&ޝpãx +&5^/DqLڹN*n4%7Ҁȹє{hJ#қd80%pDp:]=ոB+B$F %9Tí$nJtU +Cxػ;>stream +TU喻Snn;2h#iæ՗W0^ަ* z:+X˪{Y<U^"D,{*T*.`,FԔcQYT(x0 @/x/HP=+{y K*i6+ +'qJVD p) 멀j*^xlI +k%hUYI)W0:YU"C0%Ydh-v1/Z h&{Ut40hO!~*̪pmOAXVÊQeC +r}LD@?V!2+(B֒,,9emMig\S f]SļPה~,Q\&|66yH 2pf +;lH49c_Qe9 zJe_Yx_ne$mx9c6Ҭyױ8C@gDQK8.ׇ<$ 1G/,J؍l=< 5p@aXu;{$l Bo%` 0T />5$zh69m p`׿c?LqBeu+#CyLA p86nUËmhJ54DmhЙu 9AC9ă=0dVqUTT\xRFp`[f*c  V]Lpm.<:}CTZ>UÊ:&8XMpv :O\u1c) +>4&8XMp)_E"8 I]T6E(F@p E!Ʊ N&-(dK<ʪxU9 S5HNeC"#`".fPqя mi]\ ۠DaINC҂#y4^-Ia?1[765T6 +QnSIADGnH9i_c/)rЀօmQtqel3xB?TNhHRA$ FNY=1GKs]c~91nj;U!z*hDN7WdkoLM9Aup xUcal +s,#^ +Ί飡yMtM0CJ7jP?-ztƢk|q?V-ngdc־]o0eǻU9reN#{kSx +JW .UÄC0`[(8cenxfkxX鮶c:Z[Y ΘY Dt0QGƄRp@nL'.fE ӵdѪۂHV]j@fUTMNKT9b`oԚK2wOEEKy$MaWUUvԧ3fA(l/陾0OۀicKAI<9*# ysu6О0bӠAۖnI4WSL_*l6t6\8%S̲Fb;mF xgT+&" M ˵Ba8T$Yڨq8VX;-˕c@ $J ,%s(8ƙ[1F駫*kۖn9!Z2QmF.sW( + +I"f{Cw0lm+0mxU_j.'\p"065mk]~2?fmM0 G.(cb2\cvn~a8[qAMdʐ<)me\$NN Y!Zs`٬mS<\8'usvbtYbt 2:N>Z ɪWoX(V5g1%_/F5A);G]%)<#s:.Ж+" +s 9˲Xr$lapV*X0h?s~ cgN%B~#G^"k~ +!9TDZM~ cؽey[B]NF>tTcYԘY  v0@( J^CbZ>Ä~gxeE`x=:#ʁ*P +'Jt^-̳LRb%S¼;F䄀8ɲIsY^EIJ4b`xJv#fbFYW +)xRADgNez'`#YTAY@S@,kkmEEx0H ?b=Qz`B{Е5si;hYN%g%{#On`Nif|bqC>Ca䚡m dK,MR>Oh!hG),U=xG7 Ab>֝9S- "\wJWh{Y1c>jd S@vvf2RzXAjӒ4#6uе֝QjK :*lΩJcfrԡH@\E*'NwR*TK!(O¸WyZ hS۩<]t+-Pe,kJ3\UZͣw?FE 5|tMiCӄh s ˈ{ Py΂LeEr +V,-gbc,|:򨄤J٤=j`0pWTlȤHf"$ˣLY""$;d/˼b2]J^MѪ)fLAYj`f,8 KZN<ڐe 6AZ[,Jx`qY7v&Mf6N1 dRBz wۦBd&f1O^۽N&fhI6YP̊ d&Ȣ fTlbo>/&'51 ͉ỉCvfь3)afh3cT,2+Z`/bsZ2p0 E>v+.vu̢12kCӱheC ɀQqoɄdAN_Y42n@Ll:m:ra׿,drcHW,*1T(J" 1Id:FK- +(X &z{B԰+\ 3Ne, + +E"C'2:Y~oȄ$AemN_㭶̢vQu(9q0lJX4bHQ|As_е_uXߪ2Xq~܏g ǡ~f+zyjz Omw1-߇7ei' }5gKDaSD+--`cKmhes?i)aƎ\o$| +m!nPɼ+wC@vh[+Pa{ndoRy,6K;~Ɂ`:M* uCmt(1Qo`JI tMI7j(֐jdfMv fy۰AT ߲jltsCby[:@:M޶ Z l-dowRO6Ⓑ·1ml!(5D7XP6aED7ֱdZ XMlp![H6Z:F`kUȦl%Cc6,`ɦfv(M66Cl,eéj9u;(E·,u6Cؒ)YdʶT?rf3!lX,eʦfQv(W66 az )[MuH[[6}N!moY*oqipN7,N7, N7,N73jd[dj'ddJj'd[ejce\Aӱ4K94K 5K+SM| LɁrӾ%ݑ8Kd"I ^jJ 9`Y̐zr8\ e//iA"FXM9^xg"\J$쵃 Rv3j ŮBҞ7(Y:o%04e[CaU{CvU$ONXx:02Nu7A.eDQB_0byé^*dЖNqHlAjLnΆ .K`~"@*6/I6粄QEmEdm7uhiRێܦ <|*k[;Nr^U; `(;Q'-'hɸ1o D-5m#;lU]noQE3x:aCjL'k;5ŴӿcBݨnۺcQgM[o3k+:X Κ/͹Wd"u^!cA}_{ +" +M, +'[]F7^@xȽXsjZ=L{pGPpMY +_;o>_>#en1 +0Gi^=$).<%"y\(I*KNAu d=>f%xQk A;WD`cZͶ5+Ũu5y<3iTA3NEsgt*:a^f'(ZNLVX p}5FB€p^TH8aL +2-@ 2NQ/8Z H B;bqK +*I[ASEܚWQ x@VD{P0'\4`ڀC?>>i-BcU F5ګ{myTCC0߮pyLN +F`i$$4x}{M*i}f00[^ZnYX9YèZ Ψr<1|FS^Lu<3,jHșL/1kB&ƾ\rmYQY[I =#-^LK)rvYZx`*PXɏY5C0$ VPm,soVy5鬿Bg'F</ff,Jh.x^~PW&#cs<``wzF#s)(,6Ġ3fw +[ƽ$dn#ĵh +qkm6 + nwp@Ud"Y2. 2,b\b!R[sXWi+~`G_iy:) gB]v1mV!-:S{ԨHZU-LOs@*bU4Urә;[cg~KjFt5Bcߦxsf m!:B8jq<Ѯ!9 @rYUǼYNwղ8. +JԶVɑKEW!G x#W5i>`˱灑ㅖ5;OieN8m>&h ԬY?y4A@]ʢ<ys\53Y[d>l] +ڿ0[5ZUIF96xՏOCCHAe%UBS0'YPǪxT + .a%Eai` ~jf7·X%ڵw0JuԊwJ^p8 %F +}fJ55vCby;onOks=0YND:'Ak]t1v8+inʬ'TٿXg# jZuSTw7u/z% +*e2: d7]~Xtu%<*0OaF֍axFm^$W4hFEErC.E~Kږ;r-ggA3(nCyT\dd6{2^O}mz<S 1Kd?,z +(0' |CuqTEb4 NYEǻ[SlM)"@@2#^ދӌ^+z4M@Gsў90  Lt_/)>3n@OLěy/ tz%K#ӫ]_s)(?Cӣ=A5kmuO${ |xj$[ŻKsC[/5ڕ @=OAVc:}Dk/~yL؋?~6&tvYޓL^`^*Kcc&0őNYzj df( {jmo,AĬdOW*ާNAn 5Sk#fTjsfװޱ(īJ6 `DN'bţr+Z\W h8+NH&y$%SR=DwpIJMIp̾`,:-gao]I闱(ŭ򺳱g#D /32/q"uxNdJ,:;A#]PV5%GZLD=;9SQN\u% +FLLuJJ(~qL*՛,L3ĸȊ,'rxJ2'<+`MV׏gluH9, |=%1ϓlk,'2"fP;)YxPv䘮H eו&!X ֖PSA5Ɏ'zJA&p*.ELdI6܍y;x]l@à|"1*z#qzCT<栣QH>L3\Z~dPyOYQ00yeEa)ωfplM_xLj w^yXI Β,q"O Gxm N^I|&gHd3MM3{xn g̵U]]]QY#IhWV0i7dН!:bOSA:(n! UU  KrDr+@8m`$Hja _O $KRV_g?5ݮ&W),K7hIl|;87&Qz~UW\zÏZtWP&>Z4ׂNj4O3n/jdNЙ7# g)(,CBX$tbz.%<\ZB&[ +{)-ޮ7ioU)\aoKp`ED3KcܮMoxs_?𻙻b+a75a+tKs-OGuxDwpbo3@&Gvtmw+j\dN)5$ϨAgӠH_-t/!MD#j@H*H|l E>'ڡ8(aH2;@1?Yz@'fvx=jZ)_lj38g`j/kؿDkc,N;d<6@r S Y >Y-F1Y_'J0~S\ڞ#o/M`PmG'8Ym}40!ht7/8t~7z>lGVh$Jl(?dsBn;~MCA޼1H)nH6~@K#G)A&}Lnϔ?*)e+գJo2Z{\`Y$flxKxIdkbcSx|M2>D0{/djDR]1Am. +$>qO{QZ- G $ 9ހ$Z?  :1*\P"p'SVIobE-rQ*Xy'2e*Ea +0B/Tx}~pP>剡"GHHQE } pV0.za% N!|=b(g.'h>@т>@!D~tm!+~%̓BR Ä (]r_hIP"`c^ ; +Q,H~PBXԠ4"RP^@lU/':CGY& –&٪%sAm8F fs;ESh<Zl%:I oFD"b!C@;oF`KzX0Gz lȢp 0w?&3~?~:lC˒`XypYJˏFJQUK>V⠱SQ>T@#&*>$Rf]籑^4@]A IDgVBܱk @sAw0u5"PHa%(.E |>vjILSGQ5 `!u "$89v((_u h/Ddc@瓬nQfo,<`;!2 E剬#̇YP , z5I@gM$aI$5$]){a Pҗ9I`rXyQ&~v~%t9 x>DXbu +"Pa]hJ'G'`!nF p2xdG`s kL֥BD|$^ + !Ų>^䌁KPddRay bfȣM4P`ASH惩Ьc.V!-0C/'{8N˺D#x`"+F恁0 k0)-A"Q 9RJR +nǷ/XieNz}X3'Ë5Ff8h:ou!itGz +!}.6 +.k_VB Xh%NX1ȋ5ʢ]ހL"H\l_+S +k +bO/%&,, +''] >0hO.X^A\{)I첸ע~Jh}D=.Hc CN;kЕ0`w/:HcФt{H"rJHy0TVؚ PG*U ҋ + ݠcGOXHLGHKZ Cl|[lܞ, /&A`]c4whe ڏ bn&xy!V@[990E~q)hPO`p{qi!G +p8 tX.^bZJ6\ud*À!P^Bν0x,QlXA]PJ!OqLF+-r޲%^ K*7KGcꫨv0 U_q=)zX.!\%l3.rpk~mQg[+t׈%)kUZsޟ\_Ld lO6%I!4$"G]gm3`̐<$!X/~ ĬZ@'FGaXI\d-?D3w x2#X:ebpxcY0N +g͂, v[4Z  e&1' B$5$9iOlet0 ?ߔG<*0} 9 0ؘi J|*gسNăUÉZQR0 (г`Pb_MC ЀwX~ N<|_`B[ŀ〢ulBZ| /~Kp1>_84֤ž sT`|->$ϕAe*"Z"|;@=kYAP/hM6A +QSdr/F_ p%Y!GoSz{ۮ2 V >""(e)rCiD^K((Fe9f.\Jq.#6ș h;0QvȸgB&@ha{eZCx[|0<` B +h9D^'! esHځoEȥ89R2 $ *Aps0%O{-k.+v{02oڟĩDG|w$ ʨ`t׫fm]^ekmyBz=XFl*׮eg0;[{8 4ƨ*"v4oS-&1]o6#hmzx6Ϳig_]|~߄7&{*p$0|krǮ~=-؟.~^+phŶ9f?eVjGo L7vٶL;T60 Rg^4fGC-9tVC@06`h>>~_RTZ_uVzK59 ~{%ٿCJpC:Vv? t76n1@M CII1 Xn Q+35{EqPu<wꀎ-LZ 7IOM lO-#)^n58hng?j;F~a;%Qϯnv|vٓ(|gbc[,NHOSQ6#\eΆu@yCJ]DAڏuPhZ?)9`+͂q:S5Jčxfӳݾct*!o` ֺxNj78.`G\͸cd?1B[D~S?dgJ۞9CA),W   +XDSeHd`Kd75Z,Rr }_h(y4*ϋQ2oM2b4ȵzvs;. A6D ~X@X|n;a _b{-yytyc<"QFLq4t}h,Xvwjୋ +h~~a.R[ NHv@jbۉD^aF;| S?xB!O)vQMzoZ$(H}`_ sIo`49_ +Mg5PgtG7p5 o7 x7[3o{G]1oN!v"Q uKfM'*5@ÒM,|5Ih^pKbw1.1?!/b0Uk#ҖX=|}|[p6ŎҟLTf-YQyPmٛvtV51}M3hXۮQ +Fi$fbAS%(%!9;ux /X3` +gՑ]w :1u?LMyd_6EP.*}DAC:?b6QPr?Ba +L|vaj8Ww.V_C.:(h<>n&L%)jZytZLL +mJ)a3iRmъOD,5p&\[pE.!Xwg>=WnBor^,Nz?Fr@v~a5^ߞ7WU}*mi=zSt{Q\/C*Aq{Z xҌfsCGm8)M.Uex3AViB6r݋X[zr{#ۖͲ(2fx6S)@@D3F$cZv8:5r +o{(ˏh7x#JI`U eȬxg q9ֽoNIWJ"%@dc|0QCz.cÉA}o 6d ) ĭ|\?.,Nqʩ81OyVa V q=]Ah7t)ྙ, +% )I]jw6 O/pyѬ*pԴ߻ %5A(8h +?=Bt!X+&uX̛TpgFqPumЙ}zQ#}=g{IvMu>H4mGՕ}Yч Դ_X_gS_3fMB |g&=Z3e3I;eKzlɛҥI1a;ݔJ*m| -_qlB7]g*s%=eqM㘛<ԸΎw# ?aR~ώm!4;wxRe0$w뽛%;D}xgNf<|u<1+Wgҹ*7yX'l$[tiJ͢#S)Ȕxg^,(* g|M|ӣygS;||aS vjO8R0t$]taXܘ›\M}vP Z"@.nHfq.An]xO\n}zzJL<\y/Ftu:0ӥ>mJ+znl7vvx^bo!k]klzB3淐L댔31tѪx"r:Eg4:{)9Im\OcClH|yP{}QcY oЫѤ4|RaUOM ;sbbWF351:>~|}2J1&-Ƽ72VmEmw(g|j^bWJScw0n9I>DoL&}8X^M}oe )roĸaMJ)j35wL4Ѧi3,ͺaġfy1}/,a1stdKZj;K{fXi2;˲\x;XTguOV:HޚɆaaqS.m'Kz%9<)GƔ1]w;[=ڞc+f&@}-~)尻/;}s{k랽uE>6ysP#bwviw&g%vu䴾N*[wF\gf9yyoƹ.9wL1 WUZ׮xmW\sw}:L #RqnԲݭrEqވU=I]O5z]0U 5!9qef Zt?x +|c"{6lS#*㚡>Z|Ef ZTlYPTC=9 +L^*,^[>ۇ7x֘d4ƽIoWϳO9|c;nFηw~ܾیޟKLGO&igh=yGȏ RI_9JϠԨT &ͭM!3?FFX3duB^2.ꌦЬMߘbrp:mمrx.ZюV?O}"^mGuIEc-zߛ-c))1gE}^?W`c|rw]uSv =՜'`xjŇ&,v&=N|ZzӤ%2LdiCldΐlS}oT9jԸkOaŪ۝~kOGcL2`e_u̾d-]6c_r-\vProA%mDʧkyiŤlbYx| +ܶh;P(6ӷ5՗])mSSSaIgFZ,4gT3;>Ag^~2򚮺+p5%~e5jC}?Ƈ@vAVOy?z+๫/ yFj4uWG3м4k!k9ُA|)m3?ڠxu,?7Bt4/V:w"y,:r?-cx\vlqe-|] yyq6u" -#Wg_*|Ė} ׹_Bbž4u}x}ʷna ΢$NAAMO%PcLD>s6:NKd㦗ihM1͔=Mݾ=Bj.֜vvtn1v&>VojTo {ܹ?ׇ^Zni˨gn. 9ovF] F3ջ]De0 8_)BJs7,[gj.hv+sqvQ &rfv[֪GTw%!ɋn*[@,7-ْOe# {9j<볧]y^O#8vajR|^Q8=>_<ʗ\~ɫDѪBU8K'SAyE!U3{TE;C{Te튩$TFReXF U&Esճ\)J`l +X :{׾iG-ޓ5W]6&+]1sZ-YϽ3ҏ ms.T~TÀfJD|إ?'an{<1eبONx7jf v[&cYSyӟoI߳˙X˻D:bMҾ1D/ٙ krۅy-ڹtZs +1=~k3hK)4NN)L +aNWΨU}"XI&Qv4~wLI?%'h5o~]2I6}39H4ӟSzc?>kD5ǍW-41%};Kgzy')Qͦ?' I65@ԇ3@kYJTR6mX >~~pܵz_bt7D?\MDYjʩ74>"wi"b|Diӑ\R?lj`9n;`*Xt*s"9&؄?qɝ "gfFXD(k5ol)]gew̩:k$a!8,f&̎  j砖_}s|TJݘ&L'˝%]Ìad(*emE8?SXuc: F5g3i?]8n2rbiƞŪN4OF8u8Tb^hgEPE"[7l$GZyni'IX_=N 7j\<|8…$OKQLqOx$rCH9I>Cw~ dfg>=]5b㉕w^ΘA<"sr}%F鴦Noܘ-k~e=ooIDĕetzdPnoL' +'"~B>vz# ljp&;`SW0 w!&e1P_rzF0Τ1z,WÉ Lh`%1ߘ4pPw}a`: ţ:ŝ $pK%>%,өG+ C-PiTgV$~|I(;Vr䈞njW*Z'FYivq'&u(n|^uj̵9DBbONƒeR[I/䘊gǤ ii^dž씜+-1u:@ڪK~4 <,4f-&Xc0I9Q鄲:W]F\x8Iͺ`efUϜk p;С'.k +׳x[!޲X.%G*]7>\B(;{]zpB[$i'd?IٶBR<^U7"5Emp]Y좃'Z{* ,%?qN4&Rs5ᬾXYvg%po<}TLb3LsW< Hk-O>_HT0t"ctɝ.|23@ȟX֢LTK:Y<#"TѴr=yl_":ssC5ɹm` -8) +ar"'_~W⏺ 6# ܧSsCj spݰwZ !ts*;?81;.GH}vlfuUIYN% C-ڢ=tLF O/G㵙`~(o܋ܮ:ڊ sR!ߘN,oG_Ҿ]yw)bLx}W7|wHqK7 \Mߗ{Xj@;ճwnFGU0 _9O:_0tqk`yct~i[0c 0j{:*^"h+f +:Q|D=jy>@sW̗7 Y[O:䐝h~.#YT*#Zs6z֎CSI.u:mqcb!?2] ?ſ]IcMO|R69L<0Xb`ߝ XJLiu7ےx3L$ؖ8ׄ|kGr܉{m7YL7&9'[*']x)ۑQM@:GRfc#n0wcWr0:E͊ǖ4ccc*J8\mޓx˸ߟ a':z_ցMKȢ# +/Ims<,0霉lK M1/JZv~G]B,\? dϾ{gOӹi&Ffcm8k- ȹ|E2)=tpp.npgJ&f_G3> =<*;3{e~W + +nfNg˫.MXӨda::1JCʗr]Xz"& iiZgEOwbJ] :[:|;ȠYFu qYhȣh䂉+mdhs\4R~b!rH,z#+a@5D.h)'C1gz <6 bc+_/cG9<^g[Hθd"8Rj2,r-tݎGYw֐_wh 3Ps,AZ!EP=3C(FI/$v{ZV|\£y cc_KlE"|7 !o&<C/ޚXi0fE`o7qhN/*>m +HX m_` w4 BZ^Vo>ҭǷVKSӗyXxMJiXcٞ9=<85)<;$ [ {OI.FJwkL1AT4=:c_ xAl$;$N(89㜊34DRLc51v]fh84CT^K.\d3y'<}~Flb߃W&њX'lإXr,ݶx)cDii?0e[DThbXIKqu$Џ՗ON”}gm:3mdf1ƒyhJl|θ:Ix?:'fzgJ(Vp"ީ{K?4fN`tR Cj҅E-RjZKKٹ_OaOϒQ!X#$ASA> +c.LәXjkg}wÅjN0Q38iQ$&0`+?VO oRwGªs8a 5wA,]=7cG(.&((+U\1T`~s\æϡl3.S/Ql[I^GvY7&7[{prxPMa5-v{ 5C{*~YSf/ OgV#G|O&;>_$ +1k=Qq9{/D"!~5Ir!&vzJ9uzyQcߘtvgϾVfLf6HVrL [bʻ6kg0Θ}fbh&1@߁#әn.Ÿ5;i![AhC[hx>(=+L@#:vG@kOOg73؝YqO=K'# g{uDHٮdx,Rc%R\(m0 NȎg86{Sh.O5eK=ǚ(b?#6p!VF`mjx_ T&К"/7ggNfoߦ xfFRh.4c2zWFto;tTž˲PGx¼Y~Ju:D6HKg'R@oLD-+3V`*)9="hI t0{qYEE9f^YQf\$j1њ\e6b-v倦-"n@aWhiR@ƻM/5֝ݿ<۬@[Dhc@oLc}&" 5=f㑨4MQ/d2z2~RZh%y>I} (/cYmpI,WEk%K୩<ٲ( 4po.c6%^4wD\1P#-Pu: +V t6G9;hދ tnw1@KTpB4 ,Dйݳk J(5n +%D@h}ߢNt q.W}4o8a{HuHi\Np~z!A2o#^#FMnXjz +Z!*4@OD <}Zv(8|~*mہ9yܱOǖGhI}bbc%ri|+m{tO_ӡb?L](s_e[Tby6֕{Pze)_Ib(˽m֙ v٧D?}Z<:h 2l>͆3PlzEEiW>X,Pl(E07Y@O(eޘ?kDVZ6(ZȦh +fu<ǟK-势eZ҆n橪?IkKúDԩ4a s#.ZhyXMYG2csъOE,{IedY4qcYYs |p-\t6E@%=QtS[\̬zjFPr~︘XȠ=؏avNFP3 xDo2?!B/I +y B[qR;G1AZ%5?3/1>Nv|7<_C>I +>k̟gX +gV-~$VugJYʽ~]OyIqq)O%EeK(zlQcka' eͬ葦]U+ ,3dp#WҴtb[nUx:bxp޻VF\&H"vFbQjn37b4PZ$%aw{ |a3rOiirnȞђ8qoӵ#z'㠎tgΤt/]/u):Е=Aq. t?/&[dfJR O(zD_$/ypB>'Y, 2N +rJ_q9%ÜUb`35UK7k7hzZÜPNK>+^wEY]Ysh1%y8u7&m3^af fpeR4,\mytXi UkLPuQvb$ߪ1޷H1D0l/}lMX䥜A9VRASɧNE lUڪL>}s؋̣-6:Yq-ԉNjY5 mEBArOSl8) m=,{"QQ< +]\ᓳ$SnymT@<LP,A #)>dHA1]@(-ђ{ۛղVP8 ,H~[A=!ϱkSdm;KA.#OZ۱R"%.`/ u^cp."pKz%ZR,(ɊQ +Ɋ"$ˢЂqC04Bf0I%T7N^A>pli+Nܘt"(Ȣx yrSiLAlJ9FT.Mkhc2>Z ޻G"/v",,ﭗЃMyh|^:+~F4zS=ݘ8xG#M9Fw Hٲ@SC|[ Oա* ? +~ )}]rO )m'ռ [g!oE]%X47oRYSVy7:a퉳ts/G|M?뽓/љ`:%*`iZ)+; )|zcR_ r_'cD\N&RЗ@%nnhxOD0JdzD;zX%ڍ$%iUZ_h0kR\/.bl??h~vIiscJV[6Y[sGt_Ɩ/y12K:3NBg-UB$+f \-K <(0k&9 ޏ6^~~{qE;75%vpgfu!ρ 6_]?yLw?ZY؅6l_e+`Qg?_tZ !K-} p?T/'UPYb cm(Ѕ}b ~t$$$8])H:a[)ҩa'jQ:%s(=$"\5sгQ\iH$8GuySPK)G_A1Qɧlz|暌QHaqwm ݜ=Z31\*F(\gb |wk,b-7&4r42t,毬$= npnsuV7s%]T7cOny _]VЉ*]C\Ae/tՂW)WRC\A'~PC\A'A rBU5ttZjq?X +g: +:l *j-/_ $Jvрd7mV/NM_HKZ:_Zm:v֊tUK1sR :S6>S<>Qrh'zd*U"WJ(I̡\U4IdD ܞ +Wc ׇdǫ:.n4 3! bN9iĘ-v۶zIjnOZfAU3*u&L"/wlԗZ6^U))WڷƪCu%}.Cgjy`# +Iرɚ]U`ȳDń_*q zJ.mE:-tR^NԅLsP#KtAuICqu58{'Oٛ5;{rsх(0ϧS5}k ur4i*qS2(QUwJ5r7*e<񀔏S>Iա>U&,w*R{w E+R{J߯T~NE*7*RQ+RQ/Qv % Dԫl.nPT +'-~+fF)z)B)W?(A%pQA)t|LQ2 ~RT6WUˉB{,Vq&z"Ȩ3a.vsWѸt:/r)w^,{=GQ p^8iljF-}eM=l5:˴!zbBPbRncŎEGT3G=CU5KJ}1ԒS{.:>4]Yj??Nzɍ/"Mwzr;tߋ\[M'j:N*&(^/?n|5T,^:'tpkVUIurB7龩ڧ9_SM'UK1j:X߫]j:)㆟;;tRt2ARn5qzcj:Ȇa5+;UM'g[n5vNԕxOEk~N:(\M'["ʁj:) ^Neg䗪oTIlV5Z%TIsuv]ut-^TX}u(.oW'o]haNg* 2!QMa +2UrHP* +4.'ܘJbU.+$H!+apDZLݑŝ#͕#s۲.5ws4߹NvZ%Uri+Ӕ |gsl2t͝jDq6Ew?掭}SNѦ \yII^gQMlriO]tAj2:<+F5ihQ0O\_PH"Cԑ 9Y [`CSe,u6~Ofa  +J%\s6t?9 +:Ӗѭ،e߯T>|+(p87tT/̮o@E%dz-;LSa결SQgr11V0.YR6Hz߫RrKU]fP+zr9ԣW*SN'_oI\vU> &Ey?^uQxۋRV)l??N8.WQC!U;62li(d wJ; %+{ou7)U>`WnS'vSON7|*p'KR{Ew]ÝSQ k_f:S7sn:t+W>?Brι|Cn^z +SGVTtv.v"&(΋eL7eLZ,Ѯi1-eLAN]E)dT趟VeȪeUj)bDWb~UELrDDM{aT~a(qXS7j\SnSŐrtW]I)ou~h}׎T0U=ܔf+o}04T=׸Jj\2# h|NFƍ)}h4 r5\ݗ}z)KLfb'A]TPwcZ?T%-z𶇏)ɢ2<.WGL&W* Ƣnc%rGYB=vz:x@i; c>#U9ڬw/ )7&D`s2OR&6|s V\4g R@oR t`%4y +2=w>qE{#}v!ێ__I|C =:B}&arڪZhU͕%Eumɴ-0}|dl!],)>+*>_.}$>ْ3{+9V(':5qN)"hMDIZBœؔB%I>Au>}my%M Z!KG՗1]Mݡ-n:>fV))Yy^Rlyɧѹ٣MRCլτ\~v65ƶ\J̥ɚ|XȔ,n]ߠ5^_ngoeqBVhՂ% +-V6] ,-𸾝T`֏)$Qs~'ִqWVe\v=h 7򇬪BZ]"ķV`Z*uY>ɰgyڋ>lHe"쨴 +== ^nb7;"J{vj]G~]6T-B|ד#( ?Z^[zY]&/kN >co,5@XNG-%HSPeC,ÎecNK'$N>)׺S'>$ylk$,tGT'=V%F|q%? -Oml`y`]qq< SZ8 I7S{a8XV/Fc,ð0stҫ001ٜ-m"3A6qԇA6Db#-lw}rvj 3&JB{Yss\X'L?[mZ?}c>1 $dz^g[☏ͭc>Y?99W J 6Wab߿dwi 5ޥ ۸8%b9Rh汐5&7- +dXn?ow](]zfq.(]}HV[VQ|1SMc+.[Giء=C>fѶi WyuX6-?=:WqRXWƆJDjy]I|${F{Yr_9>VI[| +;bW%2S/-Vy2/=o&>m0 Od5lUkv]'*{ 5Pj[Re=@ tRf´LCkYŕy*YV<[]}z]7S+y)2=˧k\9,=tW$VEN[tl2_*IY J5V*V3HwJ2"->yZfl[ -iGfmUJ&ӗ $f¸R㽤l}܃ftζijk.'|$0~V'Nݛ)+7)0lםm2N’q>esoMn붷+c yg~;Pݮl h u.}C7:f0XqKY|38u3\xW񒹌ij!,z&-qt11S–⩤KR>%5H͖T67|0IW}i q?1V(Iؔ8T^218?1VT0Ky5K ;.`d c,M(5olKyKJjWiFY_X7AUԧ=_:в 9;˦ه ť'rhEkQcM=$pTaDr/؇ q☴zٜFıeG#FжrՕLQ&}]{lEƆ#J͖U,[V' Ok[#0|--rCnۉ&pvYYcR-4-Rqz_M%1*T0g0ÜU7g}^qLS͑O,*JuZOiOʕJmEw=E%MM-g9ti:UCmM}³A#z.4&?*>ROџ +T>u|Kg.9zgˮ]7#kx9mQ~p仧ɇ*qՓOgq; 9wb$~>=2yhjzUqwU␔P5CLDC1RXQ[hhX RZ} I L)&G \O2I^_˃t'*K!*&M,7' += 0p1>XM2%iZ)cUb&XpMU=Ĥ))ɯiU{p6eJV7I̘DU'7SMAnk)I͐䍾-HJ'!//cUmIa#C'DG~:@~Z5Ր5̔1֍1q8TܜK!)rf""]5{ˤURk 0aW4W![I{x] X.&ӥ$DXss( )rfXa!VdBUoMa'rzf~$@Jws_ .Rʝ㻗[1" [&kRgfݴ#Y +.&;kEfa$ד(ʊS  `RiWQ3f2TފS2VżaZA DxQL!1 +B SLFǙ,dGҶ?W(+2baeô, T] Q4쎲t7%^]vV_nO|W qBpڋ'Ԑ+s$۱U?.%Â3sKR+\q&dU䑐zO1I$I#Ҩ&xe@$DۥU7I {0 D䐊s#;.V <`yJWכGUנ*lnu\4IA"&oTO_O#(I ݲ*&F/9S3$)Tu3!{FcÝD #T@<MxK_IRLJy-,haNفD/ŪdU%uXpItoCO* 7oǨxHI8hdI{<A!yqSU2y7h̝nx!2yFa _ NZft|?pC.lV@rpJ iqaU;qƜ2frv{ӽ+qHJL__kڟ* ikfuPwox.i(5#dbL㾾Wg0'r +JKfX&m^9I͡n1wG L+v957=0BJCZ$/"GidR1I_Qd{X) caDR4w=I^{5E19,̜ eƉR?P!kS+) fLI219?{Z=]J"f# Hɰ[Q:?,'MyDS3q=A-׺LFRFH* FHe5`_>S0i}^ذ]2|n4涃47Z;68$%,DN9HHiDͲo&HN#;_uȟ: #&1F`{ Ӡɪjr2dzZv'7@*7CRy}H"vSGM" 5yȪd?l>\'Wn,0'LW,[ נb矫z +aHKުf4V@],υC):[jHFo6IW T|fh0s/F/La؈Q!0I!rHujn*^?ԻtIiOjqfh},zC/*^b2htYH 4itlkO-UZei)nwN7ug22br21Iv?kNVar2hd psg&wwGG-PLpk_o.RT@y&܌GyN) 6EQLLFOzq{,jx""襘ԥ~Xa !otϵHy۟i)iz(&o_߿C4y)_CZ`3oB"wsI#@nY9q~ַ#!ުFٗ4diȄdߞj\4`rVOjq X$MFif2B 9ު$pK]uz8D^f7zw7>d&)/2ÀI냀*O80G`4De$sgcznz0B"G۲.~UcV:jԋ}+F"oU3brD}KTʕ'%kuBG2,mH`L$)$%n-Vf|D Gr9FIMA.70g'9|*29Ƭ`C&jFY?씩;Bo  i6V3E,`*m&!Y*7dR +ZNJÌ(#]rHF1[UMׇ͏YS3#19Tq/Ywշc +uv.0]S1?|TE{ I5 +cJ\L~ea"42a:u<;{Π'AլHIC 9^cn}O wE-C)8و4&){=3`rLCG/n`EBGbՊ#(џ yyȱ}~= _g!E7"PͨD 57d]e5߻.tY#hgpAogoDNiJO QpiɋGi7&% V54r\u+틯u_WÚgЌFoߎO19ɿh>}۩r 9o"by!&#!p@pZU8%y;1w-u͘U8ftMUe %L@zhb^ΜܜKQ kyyyPjn$Qc.%iL\yжQy4K䕈hX{C!IoňHXjxŷ{QŦ5ƍǻ=mr8̀ɟ>%Zr哑5q$Qisal&tRQsj&xŸ6:\WO D:ތ!zH$MNvaʕOAHrTa.gV{j+ɓ4ӽ7RäբtS桏DFD@Z{yfY LtWNմvnh0HȤ"1` ɑgp| oT,1"YzO;[1OQc$ϾyK~0 Twjfj1.&;՝IT]9C^CR8>5/U[%>^`Z$`j7z>$ȎOLɏtL&&gP;t$B/vp0hzUT7 aȺ<8J<[oFw7y7kW.V7E5=[1V3hHnɬ?0,`&pEpBcnp +RiqDf$q\<0-y:4շb4&GvNAẌ09tQr$5fDcnţ{TN#ra=BY8TVU]8'V578m9̺&|v0םsm}ceG f/hhbE^!E3jOd^R,QPߗ'p !@f^uN*0N3T#Om2~Ivt(a2i[U͘ljH"FwÚ]АA&&G4W-2,`J# W $:@!U砣ssx +3rvU֪HSwotŨ'hۭp#ϾMz*&#\/XVFE`fy[Z#zI!$R]y#R"~Vf\$ˬ&J%)}K=`2"i1v1fI+*^+$X# 5p1W" f$Vn-X߀tnL9|I-#$mkoxH8t<̀_wȟoI,&ͷS2Mel)c81Vvz tQ1)t"m,wL7 Ez@S$4;~_>(S֖А`j4&$%`r,]O70 ,bz Q^Qec8~= ;AX .C'$b{}/Sͫ!'yjݩ,VQ~Ƙuߺ,6ſ۔]-?zUvoD*9!ѪirV9āZOeߘ{,*nX&.&h/NCB譸?sU9?~9Lwf;='+eo$OqYzg춆[= 4`2"j?>!>5ʕ'Hkz0$G$w&xb9RjYzL{}|k !4&_>Ue}$'K`AOc$iAY(<&Ɗ?C@Rx2SvK0iL]!/YveyriGEELj`JKG=o_x@n8d1E%8{r壚'wgqQWxIAKUA0Uo]E'ouKГGΰ&#SfX+[CF@G  +'#O`V/`탞gC09rZv嘘TdI'L!8:xI'AL? |F"CIpv_<( +4=ؚZQ + .r eTg@3OI'@4+tJArf /6Z}{=wwP˛Г`J4˕Y|L3YncideaB>JA.vT$rv 0sxW= Nk,`jwIUqEѳ0AŚt8 :A孞_G$T8M=z?q)阗O&9<5Z} 8e DC.}l=JAF.&!)A''䟷˟!J2ʕ''U#j sלQlQ(ՌgQT5Ѭ06Cr\yRFT0thI } A +ϳ&}V \n +%8Iߕ4 QnA++ `7t1Y*.ުA͖8y 1`3DLjh8&ӣzvk8I!Ch'A0A^/!1Y RbW9I+YE=qX8}2qSj}sWBF)9uKIROolxn԰e{L!8|M)>m $3^'& jn)&$]&8$f{XLZ4X$Xƾ] 8h+dߌ#twW(c s>so@|&&*V5-JFoDqP8zcH rG"#yp| ߻"$B(G\>V=)B-8݀ 8i9JW"ϡ¹Ih zYfTNIɶ0YQL7Һ.sa(L&M+ܟkܿ,pEUs\!?_=v;3`21JA0 +=v` +na(4-|U5c)N5VaNG}]4IK S6n4IQv;0Q:|W5[J=`2*S'INjI(-o4dIH}|`c'rN%B(s|@w/J/r;8r UPD8o= /Asis;=j<`*8PqxdbSTWghqe'#?^܃2A 8sjaj0:w$ +u_>#3wL6x0N:q֫n+^oB(s]&]/QȾRcժ#!oI4Nn) 8;M]ހ LBB3rXUpLi%uu\0szS@p+ܟ}c@~ルnPi$l.o0ȁZZPГ' $&;(G<=yb2HͰ=\Oةf3qzt`9DR^t}퓰R:d1AOf+SҋYr&xu8"'>UN%ha~&g'y$zA|v^gTd6(!zv#bJpGtzք^ovviΔ,`oNy{ TS8G& QVJ8bQ~UX){ћ|êZFoD !K='쎅\y +^M(ΆV-X'w> vĜ?-PQBOf5e!\bTf6]O2!n' uB~0dGP^(=wg:i꿡_;qрI犬 ?K4Br=a;Ѱ"P<׻6a&$3x_]/SL)1o,*L=Wd OSUtg7*9>]&k¸ܿ,!1!|2-v&p"y͈&S'yD&փn n6Xja E(G $WDL~UP!Iev񓀳9.aPCFPBOfyaU9ߕBG攙Oe,rR d123M1@FE7 P$/z<%~z2&뗏{`U͇U$0. &:qzRГ$Mכٲl?|ҰʅUDLI#{X$o1enz`l^Ü&tz_G$R6GcГ (&٩&_#`2S/CTD : Q=" &+ݟ:y2 ==~7Tʯո:J'{Mpv;=`DqGBo5}(sc4^19ɯv~ħrLiw氠Qv`zR -V 5 +mG}z*߼,@*I#`gO˟!Ji' =Aq:xQBpti$$Q|$;DL~iXWv'n1 ň⎅&(G |ɡ4~V\!bS Q7XuB.sl>>A4IjPg[2$e>Wg=GQL'yRIc搰}d 8<޷;x@BJN:0KҧNM)8LgMָ@03٥I޹GrAB.1%19o?\'7onASDnB7NnTcAp2\J' W )s4z2b:Ld,yI՛=T葓AVkG&19UIܦB$Ĥca^z5EEx+ބ99W7 &$_K=(4>brv0.vf>|"&F^na7Qݶ0 zsIgC zwG&0^݂99Er)ov#%gbaU(Wp!vGhIIByDm H `T>"镮AOPLɿ9FB:Ae'Q)ȱqLFQyc.E )o=yU/ڮвDLkKeb=\T DZ^Sh&A:r?xsГ9<&i}'t(WԌh8z,:.  &?#CbN\ј=9$QߚwA<xC-I"Oo؍ &@Of?vPN)&!C~Nq%avTn" I^ja'SgP2I>5[ $q˳4D"4ŭiEX{^\醊&G427f\T ǙdND  W&-9Г$yh?&mٚ9z,'s k> NEQ^&A9,+j>VLaUY3[nVܦvw1 `Zs?} X)w{!&bz#_Ej{ۥ `2#mJ'9L]U\1'A0s>=#$)9<`rD&q&3ŒQaﱈnn\$_:}eݺ5oƜ'k7ې}qea Y=DvH9 = GބOyQ2y~]gR=I_w M"z$2NתVQLN-LkȚ0락|cI0ʯtH;Io(p{sw ʘ;Z^Ü&\fGP3޷;;e5ۮHI2j򏞔>sHz4VbҙЍ[ ΠNg띕$Wm1!0Gww `2s zMZ5NL뮅B0N`>ˬ%$eN=˕P寞Iop2`/8NyyZ'm .#,7M=Ĝ#Ys^O +v~ׅ &Dzs 8 znŭ[A (!Q7$e6'vKRLLF<%}VDd&S32qZ/0N:EZ U +g7:<#޷;H4Q^>Sij)II A8叨dDo&Y[ҵӓ<`Y_-&2J!1TpgwTYl .5V;Cun"$eUno +D$Q +੔1{%Vv2 +=ul!ġm-T6Ua -$j^o$dBJ3`{ 4L\Ǫh8ʘ;d$עL:G}*<`rXIzfʳTL*KqIGj8 '9YW;&b_>鋱rqg5VƼ3L#'L /GEtFE٭CO:FEI 7%R pej2+emLIђ;[ɍZ8쓎n )Ă hKqo!;K8m!4`2" k/n+2i&z,:6VaH30QfodL|Sf73Iu)82q 'C.vCO heGU`|pD۵4J(WjaogMr;O#a^tU`qԌ~<sed?)}|]S/z=qn += DDGn^'@ZFPrK^߁!aA@O:9 =iΠ1='#ov*g>_2SOq xqҡQDN 8Rc}AM!vXt"ISa>DQf8|FfPY!`2IC 89UhD'ox. ^ Lqy8#9FBg7ʘ; z r H*]9b9W'I^} 饦4=qT +rt=Yn'AL (3v19y(Wde̅(āqiH0x?qS$OyNa(FzCRf&_ +%/WNOpwS LG8LO* L%KGo=CMaLތ٣'idDV9/^ &_Bi8٭,6 +F? A.F'}>Ĝ#{l'}0-W4`Q3Pg7N_A<]dL#:TPvrI^arD!Y7I%x>Vf},,rV&2l f);9(OD޿,XJ0I䏟>4z,Y`TW˿ո~.r3# R\mߎr 슄8ĤsB'A 4^>2ޙ R՜lJ+rnEﱈL$bg6a-0fyt~G,!,c{UIDO9i!z?J^iHχ 5x\chdv4<ɋvn֍7a\M0k1UG5-v"`҉CO: zŰ1FsV-zgovwC^ Xha솧i`WAyv)yUp/NOɟ˟9! &0!#'{ȊRq8 (?_7 e&PQM?ͲoP4"쓎^v#rbY)Ḿ$LPoN[r>@%_FEA  @!"65Mv>"'y7=U4`f5v_71'vx 턥a7 H]\a"]V9o]!wR*A&_? `stǢ` @"Ham?`VM<`?)}ZOg;NcDnj 41;iFT8DϏD'Ŀ'b;,`e3=QQhULOzuݲʱ$fwr$$Y_o5]094I$-9viQsu"awo5 F6 )^o{$k,`˕CLS-Dq'~$'AGSt8MqZΔ$q C.&_ɓ҈6IwzE h:x8| *+;GE'Z,mAd !jdTUhDE[x@J8EuII.((N_YF/0=Y;Oh’ɁZ.J7)Y&z(m[cC(\^V!)HV%fV{qIɖ(> ’I=$PLr2mou ju|s.)mcBF2t; zk;4 DuAc&'-ODyǏ,~1v.)%{6y缧M_[ ےX\%JdbDZ-ﲭ}Ip_=mI:ik7i$y['qӦf'M#.qQINw`\`}p\o;s?{YB m {QƼ˰ \&ɗ<-e4d/VR\ cl LJR۳R +m#8Sx7WU~& I܄= NҗVfRS{(x<qRt 47*b'qZ`Ad=O6IF~`0\&J,yq18M{4R(+-wԔt"o JibX s`DOFGnvX{z>{GTQ2tR>WR~Jivm{^(e᠖Z|1 07b"I[ݒx*$u*nH`{Ձ[kmQݷFbkd1s߼hZhbv\LGŔR(Yu"o=c 89Z.Ko KY1^kMQ +B\K8L[ +;EKf[ҚWrQʿyȎnG6/sfX0+=XL-bm&Tu? ߦJYX|Xe_w 2G7ZvN1B&f4 #ʫu6d)M'bͻE=P}T&O2(3-ybgK#)OV^ MWJK#PJ > 0.?Eyp#sF[VqMQδĔtSєsX}C4J5}9U_rRʯ+m7(є ߦ yo(-ѳ3iIF}2{Qbo20kttwoj|2B^ .we؞;&66[ҼXCL)tsADs.܁zNd?2[F!,3!Rǯ 쒚LC{)M'⓸sC>h/2A{/Vn?h./·AҜI]0׻bJ~2܄S9fYN' W2(R0zwiq{-XnL/lOʢe&eӎ@) L-ma N勵N#Ey a@ rcV27J龕HJ]ڂb L1bJ>9V;[V@~ᦌ.}/I(eUJ%^Kyz]B$]-p蒯^}?ȁbE v̷cI/Wò eLUX~1 Z2b;_{0)kw1\hTy90wTRmQKWg.A#L}JM$D|)ݪAӼ+=,Rb;Cu伶Jd߷Kjɤ$M + +g'+]a>۠|V_v+eJo}2vVc0WeE0.XǾe)OVXkkRY<5RKmls9+W{1 xqrSJuԽEPvVI(fH7:.9ܜ:32F1A/:} qHU;$?Z_bRzH2->z-h Hl@&ދ(DEh ?}j(b<"ƘN 9 IL Kˤ"WZ=;YE_/X67!_qgUK`; RK-jqv<̉&8{PXB!l2NJ˽rkYJ3œ5&2Y늠wGd-jۑQYxr"'+9'ypP_ՠ|1*]j./Fdp ҒA`;Cum82e))~~!ڞ;e;aCμLJb(9Pc1d^oZQ0343Yb9^z;K{1ƋFzLo!\&UddIł3KLFtj8%c;O8>&%zr9ɼ v^}b),ZP,0b 8Ė'xE"wC XT)2 =L 2G/֡ bLEe4y|g&Z&O^ b;{u%iCwے9IrYR>"$ojLBRv d2"ZC&OBfons r=wD Q*puR &g>9)ZI%N:I&5ދЪA+eGJ0(DL8<ӈw[xJ,=`;ߨR&uɬ@RSR\)OVXkR Ks="fWE/~BG6 tR6Ljbָښ-dQ/`v5b{&o`;C*'Ah(ICe^l/ZX|1=:R<|a s *^k2i.vVHDe{&_\Ldr2re75TtWj|tf)[4`d>GQQQhJP5VIHf&=XGEˤz/f-/WJ2dKo`d~HrZצZX Q{k^d# bָ.i/k͕4A)#[ ' Fes9d-n%}xi|SD ?<UJw+6b&ATҌ(6J̢~5no$40#%ů?l'sk {r{q(fGx]SJwѶ\l_׎OHhd,I*6[8 /k0d :IHQDD~gᗛnr`JGc@#\f`; *4S L:ZDVd`bvr,7K>y[]YP}Y)Ld轘:sh e JGw XUmN{-XƢ"XeznOXFY2+Y{&+!HͳP8%WRr$cg%Лb%Лm܈hM!,TZɟ*scI/}pșv΂ދYJ=QlA)s~6UFpT'RKC <$Ho=@@,0MJ/R.5W3(gZ,u.}RK?+eD)ܷ<; +ƀ=a.uW.wZp1* UJ?2#ViWپJd)mSe['CX=yy +zcB&3s rQUFQxFɾؽÕLP܁+hn'3ZLjyn.7 +ɀmAd.Wb`&XՠɾXr8F/M7UrOffT%DDgi)_t@v2IG|Lf$~e8S9FI}rU }g ?}M^g/"D'jBIB;S_5OBs +xs_.|%o!ƞUeaR)ELlnK^QTTFml]n)9O2iE2%OtK[o(O ݳ/H[0I%T턐4,E]-w:JJLrKF2EK3q+r'3.s kL &X6:4cW7XouiU`2<֯&;Ȥ@—qP_iɧGʼnB&s),!ڨ?G<-d2_r;y +-D-!EiѪA3iwUy[X6in d2"nSeS-֖>ZBn#%Y9d&b O1Ĕ-t/%V v endstream endobj 30 0 obj <>stream +dI"![xi n/9Őנbvi΃# Wj+R?Gs{A.w-up;i{h[adv[e,yl#uVA)uCI۳Њ7@3èIq΀ދFC~B?WUo!gb(e/sC&+&ov6Jrr?6xſO- ~}\)@PyGǔRC#IXLcT|#%AX%B2Yb!2 p _a@&b[(RQ{1u2YaIȤ HT.moAL2en@>.3)[ՠܧ?|B)SxsAȤB;HE^z}yM d-uVY-\j/]f9_AB>xR&ctRuk2iig zi5VwYl.+.i(6f[& Й~cJ)a˨Zm4g?/BX +dO;..]&f]^$_(LbR_5/Warj,QJL@&s ^RtN&`@A'|;|u /IuPѷzjB}!d2gKA^4nrK'r1mPs6rS <9I}g N!E(~SeEe]uZ$Vx8+d2W(W/S2%_khfOk ʹkۡSwA /Bq Rkkі k| d$|B zh$5SL:[}e6w' .Ox=C!t ]Lu3|j2Q +Np<>R?$vM%,U`5* ?QʘL 4dŠ&SAe^4w}3SJtc C-Ehy+z "{Q*7jwȇBLdd2nދF2!¯Æe)_҅ovKկBIGNgS*yZU%> @jd5jbH ?WʷHU*2X-jzf< #27yh9H _hhLPv@KE_eCFNRz*Al %Jq#oDl=k+VGU +ypO@p<נ`;mM{;L*ee+Ƀq/xI]4EKoo`iU}N^flk 4LZEeV:^bڨÔ_qVʶ +[%r>W߼ֺG5Pz+rp8dS5&߆C *d2({Y; +zk20BWJ0l52 CEE3'.4yj,RnmHׯi@/ukg*L+t[EEjZ싥R6F9ϒ֊hIJ:x=UrB+~{:iJ)*:~砪|qU-̙IV,ɜ Zg {!~62k.H0)LQ`aKNaX5!ěa{ۭ +Le&ODPRhD̢zMI|VM2mC r6@&MGyMBE#`UL! + _CcJa^rP + MTFΐK2 8X_by~݃ty 2iz{8얮I/qJ!{5f%((A)*J龕mh @eGBfd/c8c JyABRJ.*ͬL%v %h[&7Iw3=m!z/PPj *Rz +e>ɖ->RjUF+7*3AȤI!pYw.z^%XJ٨|>[,|OXJ!&視7:EJ}eM)i2 4%%NyTKpPGW؎98D \ V\#VVpR]>qp x2Y\Œ1Q0{ՠx!tL!(e7< ̥ z*̛y HLzL~cUL^4ᦂ+³*OynͿG|c#drPLphx2%W[_b!<@~_鮶^)Y_qynGVڞ5ѦA$*Е<d +{QAas#6~=X*nWN[aGs>nWz?*:;&dɤ[/OILt; l b [ꪲ6xL{ Rl'$JTW *!΀)b[[EaNK/z/ɕQ!UϝRs9=g™G/ +½2ǀiGnmSYgFowK9 VH.2YH@@PVCE?t׻#Is;/. +Zj z!` +%o?PX-LA,yƋ/ƔRPGhC'oFfm]QC_iU1p(%8$ +"UAN|Zj^?(%0\&LS< +Qxa7^eGӱ y_8?Y'eˬ2 +@&p쨲t $/D9- w0A9WʷaEYy5zZJf`/%ker(DLb"IދqM~qs^<#ѓ7oE-d'Z=b9ݢ[Ƞ%I H~*g-y㬷uh8Ӑ#ydUR&)3J7" n TLTXɓy2 ҂ ^4^mȱQGYo=#ן G7$V )+o27C46ǀ +CGNlC'=t29c{)J=V ;4s74 2I^Ӹ2 2f)^X<’lPH㎆K@su*M^[' 8{K/a_"Z.| ݀8  Q +0qYO>r?ͭFOqN)e[7]Ɓ ޿[C36Q LwLz/ +031-]i+\)ŔRc6K:ˬ*kB E|wE̦zCW"0 VjU76g/K9mt=j{JpdC轨3WZ_ټJYc%JiS/>k2iкoV~C>'S لom`fbҡ(eH}RWJ-]dMچX?-DwSTؐ&Fe?A&A!3,x @8d T ґVASzm2e)CNڤ2Y Q+8*J LXc=BAEpSg0&@PyS'ԶMJ-fDB놣 C)PYf˽҃yuOfBT)UVz}k‹IޢB)I9]nRki蒯+dʷgf ]TY`t$QPQ`%(yv>@DeJdr(V ' Q ԋNyU}47:R(ޤ[듂(Ў簝PJ@<39$h"^L1KMXgdD)OR|YFL";eR|>IPB&uA'K\*Fb "7f C)A^Q6/Z9v Z5ug)OrTaj5zg^? ">M2 d dFE}J}砪J+ Y A$MJ HLA&A‹Hc[/&XELDK,3 ]uC܈"60Km h[`L! @Eݹ&/n6xqur8A(cuk gZKA$E!JJє锒akZe%M">Bj렔 G2)Vo=ދB/8Zs="JI>J)Z$ '7>o= /A轨7\l^a%2٢rDJiXzL$Shf,y4^4lZ;Nº#XFd|/ +B) L~>zuM +Q@'mxMzp~;17j!+e?ǻX]J(%2LB&A%oi ~? _ +; x+xhA}jd "_ L AE}$`~9,S؂$BnXAep^\|7@ qa;cْl4!I>}"^)F5cd$0轨 ]vV{}AHI~ށB%>/³DCGsA(bSD ø'V~[zfJYl)J;dP5z$f +`2Ȑ:{QKޗmd^mvI$Bf)+XV B=]UOc8{E2ug_Qa@Db]2Đ=’>uǁ16W_AIe)&?L^.c`0'm^DwX(Z:fh]J(%ȘD `h!O-I JAD){O"RzD)y!f.27B/y.\o>熞."O'_n*TYJ"4~gC&P8\kIFoCXPͻ >B +ɣhi S^2 +^T Z|2 .<#ҩK י 1TPT.\mѓX3AkYR"K wR&* +@v!uaCFE=ˣ{@d'x+RB) `{&c p!>9Ʃ ʍp7QPR"K d[)U!.$XΜ Z5HFG- Îy$x`@dhJ(Z "~oyIg$[Pk;]Lz/v&g_9nDD(N)$bNKW?'?)7 DMtpCJdLs=m҄h{:RfW_y>x ᓈM)*uUwovW[*kZaM|tt:ˁ!Ň7L H qf7o'tь1QG/C&rI?h ~z ]F1sD[-=!_>({?cO;{OS⟷ٿl_ǵ?K6Y[+mCL T9Ĝ9 vfZy*čmZg;۠@&rw{d(3*]rlj`kvu:#/9ۣ +yA(E?SãZP0HK|E;.T9N4PN2'n;S_pxsJGlyо$|O/Y_8Ĩۻ!UKf(VK2މ:p`̱؍ȝ`295F t9g,wE)A7uQ2B#q L>ڜ:?!1+m>U[n $Q!^4^Vy l +O"r&OxVW#%!8ΤTdՙ;gxL .ϳa4܄g;s{/b;sy +fN '}|6&f<%?\AA)^{Qc$m/h^JUJ$"ѣ= +&HQ (D c2fJIl{f&.v {/cfT J-fR,DHС8ޭD^r +(@(3dU 'mF>mDB6r< OQ +NBXNӍvAf^◯4Xn$"u:'ȋ9 `ne!FՠtIZ>ȍ6H]2v[ǯ 7aA'NtEF۔ ҕᐳ?Z|2DX"%sB轘ZؔaBnI'SA/\"HfQH!b#AȑD :>|~SeE?L@n%sB{/6[mR_LA2Aּ>'sRz/fX'P%ּ{wU$BPwXHL22NGDڴ6bPfMN'I%z/<9<'J7O"DEIWe.q.ALK!~ͮ>?-"{.8KQ/D9-a'=:Q5(9Xkͮޠ`c<>2-1r`v %Ʉf,y'bsQd|h[$H`vY!JL-]eU05[|+xh@d=q&:RyẌދ~ +] |2c"B+VSrFT =~'_z7"_qˁ$Ћ-x0=|fGg3g[:9"F-[vD cd"6_R[ ⓈX!@O.`%CߤyX'{]}srtᓈlA 2AW^L^ÒwZw. k-y_9>Z6EM@_& Yֆ#9i>['? '_oe3<| G$`&}ދI}2ڊKE5oA>?܍݈l/-!&z,#@ ŠB޹ߧXIQ,y*HLePu ~ֶ>R͓KI`8hDi0ƃ&C|a~QQ|d1?~-pF 7`a +C9>yaAQQ'擣I%&`:z\kp(2 U`Wh1wvQ Iث] Yc 2Ҍ"iщ$z/$2I9`qVCYa({QE!4nO:6zɺ"(%¸.z(ވAVA-JZW$/~zǯ']:ƻt$6ZLfSJ ++f>++=R)b!:& =$/(; jQ0~*g /3Ƌ?BF'IWÂI<pb9Ή''KNvDP)( +/D)/AxPhs|ȂE jkkc)J,y# tqD; +(Hf UC! n|'$B`#[F" 0-#NNKMX򦇻ɏOOnuvQ"E5^g=q7:nI +.(7v]'Kag%OmJ,E DDHN] '轘ە.OktwBw1E b)J$B`>GUsr +/D LZ5(,_D|aM%{֮-^)JҍnǺA${/TNLtK&A'S.}V9ةmhA (:,"_> r ZM"ÇC.]u{ӝwX^(n9*@rH/yTNݒ:%VչHw9奄keQJphAwh$hrԢ {q(S\C}Cvw-PG|(yys(?- cQ-@E @vދs#=JOtA>D>X:wX9틉F޹(.Jh!AqQ;^e'AN%a(`;Ui0O},șzѣQHTD8qjn;ݎwQe{?߳՛W >(.JJ`m\n'w7OnS1i:f LQNp/WIp|+= DNA'AnB^vP4=xѠ+2.rWis,Z_}{+֊D."ʘ(A;K2`}6pU՛]+/Dɱ.Gd;Y@-J7hԿ*d_D?١]IyM:oR 2ɍ0vot~O/YSu7/[U)Z'{sh-2|Rwrsv0VB읏!5/!=?`},OFR妍pg8C1C3N;G/tw++dx{[rIo}ѺMwx*$&Z_ v'D$l?m1A^v9iw(5lrszϯve_^i|.OB~o-am7v!:E#&W7^2P$B2܄S)@~JC\8I_J^3~R΁E nwњWoVܴv(|Uv& .rԢ4:/Z'V? =v9w(qhe_cqğVHaMO"%aޮ`)YؒǩY$ g[/VFaŵ.w˽/yrn>%"S<9_z:$݄,_E>^9yr<=UWVܹr*VB{wϧ$RقXJ7k&zy:KIcZeSr#:@>B.|ON^5(,_ﴝqC<82v`t۳uKrO.x|)JLv?oR"/#9 -Ǜfm7vMs#َ>A%aϤ$רXN^ww8~#KY9WhsspbjZ +Q.^UA|_F1m|^gw  .51UYop1 8C@/T!3ใ,3sߙDyRJ#ڴn-N$S9IltD~G!#t'*z8"z;0ɁkKvr[Jv׬>'z&. hxL!yuK%kK W%o$UZ%2$U"J9,#X 9k/p]<^'v$`g Gί?yb<[O]w(*~*$/D DLlHu9>~ThJIiapIi6 XN3rO.())%>YU3ݒJELIƵ׏.]f-cO]W΢uItC':e' JiTU6) ~"Azkm +Do}v:zK;%l<3{?a܂;q0qd1v'o+nj^\H|rŲ'){' Jߊ:$s.yRDL~ o{f:I 薆CRvŃ䒵sٽUغg)J2T߼/br eyؖuU5| E]kK'q*1m|Э$0{)=C*Ҡ;'s婭6)2BI>4Xk҉l<1tHS\SM _K=?yFYJzʓ= 4>-ˏ֎F9VE ݵ2z*'>vNrD^O33U)\%-.wՠp19ΎH8G#NfH:}qmj% %}O^xcgVm[donv\iEh>y1WntYjuެ֖y;o(%"GOr|G 0;+e,sDrr˻lڅ}47.޾i?=0%ӆ#%IX~Ľ/%ygqͫk.V֥dME.9;9`䊻7\^vὼ,ha@d=qɉoE)9VYhj{vK}bͫnsX2v' >yQŒwwF’ ;'w]z^'/U=#2"4F E,_G}r{vʛ|YXd,E ,`e'?`gp>IP0̡1ˬ2Z.=A${Kuh˪{Ui/>~m?3$%ow_Pg{/>ӧڲ;B/*uawjnqƋk\M߄7Y7.^$KQA) <ި'KIPr4Ѝn^*/=r֝KjM$Nt;yr%~[X"q7\pǽUM{!}޺ ?^|<6IIO/!>x[( OhA vg}߿dR YVװC:/km,L&T/yNnz}?=Wo_[GnϼcRskͮ3 Ƕ_pOBɫ/$_y %NyjvSDsPȤ1ay9i@IbP }2wO&;o(KX@Lw r*E=@T+HQ&,yg~]lE7 %Cgi k-c ?[n涒 +{|ɛɹy[( -xO9MN3d+UjdR>Q} L#ח6>OrU̖O'$fRtQ\1ٳC^gʋJ%|[(O78_z'qVҺ.%o' ŒJ/yʺ/b e!+;9 >fZُ:ـ(}V'}&)dڲw[v4$1'z2h|jK'^j W>=ms邞JKn1% W|]]]kI( ){nxy'JׁUxa-?q6sJs՛}\蘓dpE?і{u鲵U?$߹i ;Ҫ;<9WArCފ7.]_}r٪: W%ɁOVNZLG'MW'|{ڍ_~hY.IZ [(vխ]pOէח#' 'y  +dMNrOM RriD{LmnNi-^_ɞ9irǦ5KI1d +s]FW[뺅Owg|;GWJv՛w^'ɗ+O^[v]\zw _'(ĉ]H^P&d0B)h+p)^ŅRiIIfU:#e)wmZsr=Ut;ؠ ;c9ejx'o]ᓅp  <3W:U-юo_'XQY jtҘz@17).~]T4r_5|rݢLɥk Iz˔r8w˖Shix' & Mt>~^A1`Ԭgyw~7вTaM2>EIhw0V2M34_e9_VB}Z|τ_)Y͓wK!G葜-T,\U]rOz}?e'$cRjO*̃kܯ=|ÕL=t#$Rn(u0>Sd>]Rzy'D7Byjnh.ge4">w׬zc}ɹw̽ Wo]M|8$?ûxO瓨Biэ!'gGS +; =7]T[;sD;6!yj[tr46Oj0%v6<9߽,!8v;α}>4{G:l+xn\n$vNI(^mƻw%yG2]PϬrW%%)rY}k ¦N9 }mtBˇo/e;E˖a-ϫHQ&`Uu[#|㛏.k]E&H/,6ٙzᏎ]&>ɻpL 4l6&|x> @dC)cЦ*N38m{V[͛L_)YrsYϞUNز$n{OC݄2܄I!)O^ +Gd~8!-[dR.amߴG7O֧?H.79Yhuk=MHO#޼7|@F=vN>RA0{U.G~ۜm+O8} ݸ_^G/hvCګϒtwx'L'ݱd?}V8OHq'Mך]g]eeOuUuZ~2"|NNm{(:eVy9"j:~]zsjks#|B_OΟeQߍIw{R7'gɛX_GcdLG,+h+h'ᐳ|'^#>95B ,=HN#Vh!t嬉B@ qp5>΋d܄N?^{zxRgSy2')o7T0eAMkheay? 6E.9dp-wT\r٪ū*z7| %'[l7:e$z޸*i2 IwD#K(_w[L Ө'}m= s覸]lZD jPJǣN>_y |dysC |r5'yI[&mN3Ar0ՅI Х{:N햾,wkǷc{ZWۥ솮z8z yKz܌g)wоE+0BF3 ޵]}džr^2tg˫$R +snt$z,`20Y],s$u{n(z9UR29Ds`Fn-38%$UB<ޒOoup]ЊQˉO.>O8ƻ\UpvCjT=ϞQ:z6.ٕrOwU8-Z;ZҾvO~ yGOMw^Q6]J|>Z_}R+i4iKuKF x8<$b_zpyew2nZo_[.F(:yeisq;L1;=êAp;bqܘqի{|i/X|ɚ;hz>YA/5yc&Q>^~GPM''v|.-0?Yk't6זm Rht^oRu@@:lQV\jcc++ޅ#agO d@vnp[K|rC4ҵf:#Z\EƑsJ[7g:Y0IV׷.}rmO.Zᓱ/]k@hjLo@iqQ83m)pTiˠ(oXGyrcww`ѥf3VBpœ&>rH]E|I? ~ R&Xncs{i9lkB@f]ݞ >P>R+x. +3$=dR\ow֭D,}v35M?}io*>L)4emWmeO<?1D wD9[Bdr㧝|%ҺG/(“w4/&mj϶P_Kܛ{DIđJɋ ;,#}<}c-;˽^MIGLن_mx]|^AYgU(/7K's{7-JXϪWZy6 wGCu:&&ֻ9C ^w-gI]ADnđssfTss{+K=|3eI̔~ܧqc՛}>x+HQ}wt eK'{K;\tly'y& +$?x{Uq"⓷Ts\h'ᓦ +vny$id$יU,yIIb֥|%Z B;t0po(6=R0a_͓aY]LtKGY>~#x,+mD##/(29\?ڡ:T)>`Jzgu•uIF$j;RrUNBOИw>ZSY^mIΦs&R9qsLj8>IAŒV4Z 8_yEk$ndU9'LMh\JrA3#!zyO5]vW{ʻ?|ӻ@Hb,$BIYa=K:-rCȞ| F)pRZ $l e{;a{XdK:H|ر-{ށZkA}@&N`#")z* +K+}r!y1~jvT<\R3Nrʇ7(e\ab\ ]<+y=~E59N'#$ǢHiΎؕ-[K~yuLF͢~rE?bzr!_Гqxg+682udshcR_<ԾF@<~{׍W)Ma?X2ӓ9֟5zux{RÚ;/UOuEOLI~Q#9F8{H]vZz]{;'~A v%JƏv@ιAX}61rzjM' =V3>C oь} $One<ڷi ?LħCvˑ渕&fᚳ\u'g]~Г~Г^|mW$a&IӨ,ŷ,Z %8Dž%Xӵ{Oގ28䏝y֟,6G %R2pM4cɏoť擌fd$hLES>M]<7PmHazr›]xً|AO:~;x,E3YRͤnROɘ{~-m$/vRԀ!7kiݝW,o&/b-?Sl (+pfޯOsm]rAG aqR!I9%yUi9?cE?1=9g$-R%54$%{>].0nR3֘s_Z6/Xwwr9;XMo|۳W$qbs̰ҔN%, ֑5v63ThɉД]k1=k d\-i9}N\㕋;JN[HX2+g᜝w t8bR^&0?<(ۘC84o+֟26p +՜}`zr߽go[y'RS%rHAyg3=y_O + SOZ-&er"3qpΒj/Wbrә)g}v K.Ȼ_q4[M$mi-?W2*5rd[mA{INl CAr_| of$h_{:EGf(lю?ŋ) =?Kd=} +:qp $e$܆'3՞,4e?/䦴f더cxsSޓ_~ۢkqz.ϪKQEy͢VĜɵcyr'> s *[*tܭ];nLv8D:ӗR:οzhx7qamI:fp8F2C #)=\a:}=".l_q׌7Ef(L9!N1>H؟ k' } T=IC_*_n4NX΢4nV[ozœ]bM/_RIDեRCgam0<()zEc\xc"QE_jY!x.oQl,[h~ GTr,MO)KK{M_|0 +ԓk"I/>Y_&bsIFCc6'kˮB6's^]>?xW^uݍL}΅#k/Ĝ_|?t̾ $%+U#SN|ahs[<[c"iJNd-8Uz|YLO۠'lJUd%p4]Q-OFr3u- 3yݷ_~pͣi[-k^ +)w:+v[mXjv׬uM!]5k\tdb'od\3'S]xm&=I'/6l9"vU`s_`(,UޤK=a">7龋ux 7𽠾ˆ۲Rd#*(Ԍȝ~"Kb!Rލa'(e;vQ v6EڢkL6gi31 =vܩTvz#'l+))坩ƶ.Rs]=w/k.1Q{iP+jx!J&<ʖP"UcQG:=ec:#ВVĸ9\&&'EX]xRbi: p\~oW,xbzc8iqN-ƥ eÜiWb]6gL,ޭ}'E{…(t2ڴbuəl$%{% +Ye&$0-. ՙR(5=񇋖n~^reSՖ/ݿSbXʘ;JG~Rnn!*Wz +>ݲ-Wtk[y… 'x[F tUM7 տ-UH=3z\;/榠dz7_jY+Kv j[G-TjʓG=>LQOx4v0rf]~ Г^\O >- HR2i4/ iKH CuAks}`~=Ϋn麋Fi.Bk^J#D)'-l Mj,8>ޛURXR alϕd`GB_~J]zE'b8N\|mƎoyp2^/ l") hv|;D<ֿ!%Y9={.͝W_cLc6w*&rW}-l#1#zW /MRTGOsT!6'tSL;HJ P_I[>&I=1 3~qOnYtᕷ*/#i/6zf'.)saitG1Zapr:J[V2{._N]ӌNafY-nCR +!mYHyϸ:(g_i䤾~`O.+̳y=97۳Vʟ/qU-OvrxHyR9>A,adla's DfJT'.|/V 8 @b)oԵ-R귛T*w/ +/9;^P[|m7]pQxn_KerZf@I_Xцf]9>pQs3Qv=8Ɉt:-IbΓ\]J?SVaD*{Ba>L >x_Oޯ/Wzu|^Jv22p7gT$<;!sssewJI&eHO't1[$"$6K<7S0/yk=|)L/>u>d~/H9b/=Qb#;0yWs'ceZ?bz =%R?I|BX))[8? >Fy) Y嵲v:Cl8;!- !¸!Zvnn#=g^"$'žx`ҧk4[PB){ ,U;"0s9$Y8Mz Nc6>1HjGSHU^6m M&)Wb ax2;:ՆIN\OX-\zҩ'Oj` +CRVT?גPUtR&,r6M2]i +A4$¯&jޖM]~bv/|0`b/) >cr^Hㅲ<4SJTȍdZrJGN +{3q'xdAj?D4`Tb,)Rޱ@iΚKt8+BDD)C9bf2 aI]kϡd.eJzҹX:Uڤx@kIV"ȉX } g !F)2xcN^C' n5[Itkb0!rŠX?đ:% ] +ER!]y%|!ԓ-YLOj zҁ`= IJ3>!|IӤ*+)AȰ;{cz~UxGtќ$') =ŋ'7Hy⒘Izڞ k-6 +(|%lɌd21>ȦZa=Xⶓr$!A`|LQ}6(D2=XD1"9_74!뭹| 5'ܪhÁtM.1:.Pq|yr"zsY3q;C;ٹnʏ7j,'vﬓ%ࣰL$XN>{e6wIٝ/W`tz!S=I&&i2%gBL:j8'K>n;i1p(іJفs}8eht j)^%Dq{0$w#et⨘"Q;⃆̗-#pH!"f"VCԓM*;kKLOX =d'-lS&ڒ)oS0zMas|x94qK ag')V?zanQѓ"i&"m$$`*YWnGRV!?۸Փ5IG8FpH%IJڋ9{`6X>FrrȐƑG9L;HX⇾+p*==ԗl6!!)ocw7$ex?5M._HSf;%cdVN1U +-O` D×)oBctrEr0X~<!uŁTG*zKA2g@Tx4C&4}iX >^HS0yK<(%-[rG(KuVhK$bn@F3O<8Y0_nn$eXSJ&)VCIad?'})Fdz K8|`4 w9ֿ)0LεdV} II>箱|Ιl5DҎA ?s/,N˃['BDTR5R޶rsάZ%MsDڔ'2IONt +E@ !I iQVr; z +f )ÅI,y,S/]F xvyR,NI/ 6uLQRv3Jk߃vŔbcW^lӹeI',ccMRSOL+rߦDeO\vSJuaQvx&AbآOMJmMPW~ b{N%ct؟c:#SJ&|x˜ew&9'֧`3LT)N%]d?՜1ĶRlK:=RŊ(%'әbcՓ V?KI.5f-%ӖQQ]OdX =i;Ji?v8B1b&Jy< 8 6, IBRvJ~sn͚כQc[Idda8-o~#۹[Pp$F)w pg:b8F2Dy(S;;`d %ӓ n)绡'MPHE8B:-aҥ+_nY2Ҝ1)6f=w]M]C;ctIiJ_8qTKm\OR!NhŃ%W<  Y !)'snaMcadiľ(HmqP-Ƒ'LPiΒc&)C|Η!|vcDт}v ՗AfA0΅M KR"址xё*$x|z5E)k6o ;ٙIiy|NI,A_28xcIS`+$6ct )'DRv+wʗ}>iwl}1Q(8Q%e[mF|21:dCRb ._bz)S|qRRn@2RRzy)< eGd\$w ;&SI&D|.˟.Fg7 ER./: IV\]=LR SʕRO} b\p"3TavZxX:L7 1"-A<ir˟?jHK&͍ٖQe:Ob8n8X|G n3Q}(-)ezCbE+ =)ГvXv2N8C`;{ 4Q#$ec`׮VI?cp5׵TSI1@!$e[eH )4tKonʄћSTӇy{xحtyR T2XV.16hA|ԐvTEs$_׮;Dֻs:X ۣ5dh|N|I@"bZr\atѳÔ2WfђV:sS|:=lғaHP=dnM",J,I& Dy$ĻDȕZlw0=ٜ5ꪡg']\O +?L'EoݪҤR1/v@k%"JsMd Bz23ГV.cK"8F@\n@B䟝?fH+V/XBSjHɄ%og~*̛cG7$eH;/B+K$7Q:qGۺi@pcz4䱇2uSZ[F}[.O##~ +(с >~:w4ۭ4VHv!#ȕu~TB:WC0yY6ƍޯ- a),EQ}A\{u3X,cO3:qP&p.ECcDF[8׵fQUdWre3<!M׫빑 0­?(2s(rTvh"Pz31œ]t^lBg7yP:?V'ХڵrJ^94d2W}LB- 9ĄhšI9Qjя=iڱ?6A`4h4װ(P.W2d19,8fXy>J *,-P 6Fg$;z< =i3q +/v >Nȓ\FxGjTK+\cUE"&˖ӧbr˅b>l7$ _;D=i:/86 lM#ʖ5dk(ן +'X2 =tcHʘU)/t5Г,G3Q'/?#ȺuRJi*r +208 Zvcq;P{ܖhHNH|Q.P{$5ۡ'X|,wQd(U4|yU!RN grztGo!̾>FQQ6{Lmh/B$zLyԨʓ[dzkȣ<8ŌXR2 Wws"!HQa)xlR6zrI+0`-ovm*f Y6DVCASXrq"KmмBdķonZ!&!)chSc& fuMNtBKU%o4+#?$\`Xf;:S &)D%GmNgߝ  oHX,c#\yGPUxn9/MV 9$& N/,uUD,Otx_)4 _4®6B虐^O=2mo15~"bTEfu+RΚ$e䨪ɽDv$娜*%UN@%'%d~HUTٞý"26UIbҬ3.D5΅ё#7jQJHh-ݤX| !#|*rζ\C'WOf²1:plPrTx-9!{ARFaqk $a9/mYU֥' NNq%r;zw]crߐ#9ɢR>͉zmG5|\cjHϮ|UE'8#89X2aM,с7$e(sMt @' te$"WKe\Bgblq1%„eGNc?BRF-Uq*?V8ZȈ@U9Sn ڦؚ굲M<"*"Yr{.E1K)tHIʶ\ߑM LR҉$%1Ffg = ")UiTYΫ"m_L/q]TzNң}gAeԟȊdҤ9Yj )xd4 @4̓OR:rlD~㎕Ԭ#kG˅_tU?nt<JH@|9ߔCX]7=- Hd$2@iUK%pr42 +Nkaޜ wtӑG{*"JH ڨ=7]!~Q|E4ל2 NԒ){S] vkaX +w|@D)ྔ EXa/n_ww+ %7EsMOʽ"k\b|v\jH{k+m7]Ba)(b;1 rJhtH NH^~g"t@gtͺ"P`L,s(Z"/"2FrEʗ*{HBJ9r@1)I&r=-T:r%Yulir2rp"4]q c ,}Ƅe[&tEbj)WߘڱX$UtǘFUdWܚ%{7"H]CǗJui Z90%31:"J~71I)$PH6Al >6d~DsMwܖ#7ڵrٲēCzrj5K/,ѹcB2Uߖk©֖RP){ nVSL ”2g=کMp`W*2Ө,[FUS+1@aNˈIGtې5>CD5$$xp4@|77UGk֐*>?oY0wt3op*ct%%8)jϹ_J%⺺7 !#-#QU4㐉7q(8zM";z4љD[ph;@R%Q>tXFg7fW]۲"@:;zb=.M]O )H;Q SCҏS,$崄 z +]Œ,h3*4g*uʕM?|22zrr3nXAXF|yrct )3zKVRd@4$%PFtvOґ+5ilMR)D#c)&/Ӡ'h 2D1:}CR{OBy0M)SVݬbF'QadNԒ%{7H+TEZ'+WCL:!wtP/74vo$f蜄I񊐑=T٘]+/JgCCZ#&J>LOt*kpGGd̛H!HR ~D*1Ka;DXٕ'7TY "mk &Ē \%}d'I)TÞ?<VRHhGQ;#9B@@m"`9Pa6hcK!#mXb;G)B|(bup%JRDkR,:;+{ +c"P@mH:!'|dol44Q_pŹll$%fb|&LWBpCEB(BcNRe)(r4{R-??DoR +6XHb +7>psߐZ_[w7pot F.+$%3a)Z‡س?Ii&aϦz&JGN +RDtl9-ǗJui B W;zڞ'rbN(}ߦe[XT+>%)'>Ctv $zr5+1 ,(jpGOΝn!쀤,t)le^3VV7(V d2LWS +oɦb=2eGȹo!;=ֿf|D܂sڱ8$%G|['ћc Uk0]DCXf(MjKڑ&j +q$AE)icl.KR[93v:(09=N(a a5=A1:!)}|7]z`E)uMГD Ǟuwǘ)/,*TbCVB%"J)jxY<5t=)AM4ހ;'xKX&;zoHJX*,$ֹ&QUt &1Ise„ew<ݜ-CRžݤBxX\{u٧k9Dvy+%'a٨]=ct>ޖᏐ'y(|ƬJ Lc@zZ X;6ğ;zW>hHPzF"'EʻQwJ (*09&"8i^ 3΄esXct 5GIxo4/K]$CRFvhQmMA䱥u64-CjaYFbaܷ[*,蜾 '9ww&]uu*W7'zmU,b%ctIDep6Pk$AR[hY?)I2§{ L#,&J>TON(ѳ@8 IY,QHJRnw7d'2%$%QEטyWjIHM,l^Cz3%hN63sBt!ʦtڹD]]ؚz01ɔ9 sGRpG\L sߐZʮM3I9YR 5 ]9Ĉ5͉^ 1 +#[=JI痔Es˧+m<ٍ7ǛTT,[O9OLaIK*ZEB(os4%evoaD٦Emf +BO +N/k<8V;H;-box;cM9\R,{Cs*)rȈgbEL'b' @PKþ~"Kj)rD>U8JNi|ϭt)095ټôL 8AXW4pVBC竍Z?a9XROWr TyPpr+D z KX\GSQk&)^ҁҘ+9@LyԮ~`~~{G,;ƴ +RZ:@ʗ2i3'Rܢl'1.My(MTRQD6,,{ + J&>(ٝ<7 LΗ|"&m@7cᱯ;:s.k)~@Jg:s)iϲO7Ę5(q*WPq;zG5o[ĉvZk6>sJ*dǗIuiN6fKx3X-̄nhct )$>/Z%L#J=% AX@} +djx0yM,^C +Z51zlxMYjO2I0 nP5[/nsT$qXEzLXR}(f&@w,Y1:2Ιvx2̚Fj8ft尽֛j @"S&tE'$,O bNPF)F`b Kl%QEB !J,ibdz'"NٱsGSsq/)ꑬzy $o7)lvSS4[*y e9Ah>\49I7>`T<8u-9SE6=0"-G\@yp|D(sL~.g.]{~ig$ϓ$CSR_OWZՇ.K♜GRk7>`\M,~sKjEҖ}&A~^9* H_r,M`}Ҧ\$]7/y$'%f%͘4+INNv%%%''=Bқ-}J~wKi7'}i%Iuz3<4̙$# 'q CLUY9kR`D =H%Sp2 f*Dږtˑ5ctH*m&)}TUF&#WuJ^)6oHsA%s$9=$ ]$=&' IWvPq6WVًRų<LZ$g=0f-ct!)n>sA{d|4#?uE M>?eJ:팤{=$q&ӣPRh"r:6Jݜ1I #J@}\,{stE6 D*"ЅBX&# Fhd#S9t{]uys2i3$x3gq +[@*m,!!)k2)ɮty`=V^9z==$qׯ-s5DSS3Uo4k)@ЯL$e[pR׼ԤY3I1HdV@"9CPʑE]XUũ9{Se] pb2pGL19'!) B O-k\7Od&iIO!]"ldh( ȱ5*T:Oygna9O`*Wa!ч @ pGRle`IJQ#%&KR9#)ifpr1tU)%uAc! tzmSpr$@M,CQ\ +:XYy<(aTΚYUTC>]aamoL0l=WΗC,R:]uɹNb" XjkmT8 +x;З׌<^g +3-%'+bI Ocz7/z s" 8 +eCeܙOCBRX7'(47+AARR4)D QN\+"&˖>ωlݣv]|J|SC2b2PUx TFt:mg큱96&UEp)4BdC^LԐ.yӒ>Ywf&g']$OOp%PevGLQFoZ1{u+eTN)Co1ŘcrCZRÆ{¾[Lff]XWf˭UO>;.N3P$tlF$%Rؒtl}{sj֡`0*#"_4%s8÷no\4* x=]X_xY}Gn}⮚g~~mՎ]*?Yew?7>sf9]_U&%ERLӞvE/^:\!rDј[?a=ӍO2H&= .+]Tϫ꿯 jqŁ';?|^;ށOSߛ;g3'Bqƙ7o^_R&Tm54G&t тg,eiC4jR7<12Ÿ&٥ϯz1}8tez7roEү`8?㓯3=yG8HwJ3f`cJ*WRR|ECZ E?E)[\PWr%SV?jWK*wod?B/{?yS72XA>I(o<90*_Г#LΣ"&Ry?S/\\m38B7* 3)udC1F*eϳҋ5}j[⻯EO䦙n2E&&?@r 8P  D4B^] + 9{JOkQtp[{otS9QKѥ%Wmnc|a1}~I?.R(h|ӈ1qM T⨢qzr*zRmQdg>It`wt&KP9 @ %1-_ܒ7k.1/tm?}'cU[D4oDޠL4Qc,3A!''-&h'ڱvk06XkrDQ㊢?Z" mћsn}_+ן-?nR}Tx$| ]#|QՓwrct͗+c9MW\O LF:1Ec`7gN\&J|cw/;2I.xgЎw^);"Ȥch*bX# [rE]Lg* 7/r?i%&FpbQþ;/hHF3ȾxV<?/n"J?uOWToPR[ɾݝ+^EI1"Q8vE#X"DI|S +I ѵnrKIIt$A2}f.1~ϐ}?V?jkJybU-7Js:#Ì*'''&]Ib$& J}.;krSPE е굱.^6Hwwk~iH)i&)%}CT<@Yzy8?o>WK*1gRF뵜GOF|%R޾ )S5nT*tEpF䣇:_Fݦ}7͞[_ti]E_T?lxŁH1!Ҽ=^Z|+hӞ;=9m4ʢդ>ܚ2(e:/1n;Ⱦ{:=/z*y9ϛ;>o~}ɕ^<@+_ίܵz^|#|!n&ΗC"a-uFFhz2fzR@LzrB +uyZ]ZM//_!!8 ĈӢcJ'u֞EF/L.\L~r)?[+Tݤb}B/:F3;n 'CII̙VI!S9ҨHy &Qw׮mD!nehiӾ{ 1fCڇoOQ6wŕ{-凶3h +F}7׊12`r'Gד<8iԍw(ADڊUֿAG+qnҐ^=>bɾ٥U3^ʭܽr#\i=E'btГɤ$mtkd˧k)V_pa>|֛bKypm <z3"ơQVTzaߝ5duQ+Tqi*e|kk;/IQ}y(Q@OXV9.,v^+>p79gr 'XÌÊO/<%=빠>K>Ú'Ỹ4%beR/]R{^)fϗkݣ8x#%@Oɘ{Nx}$#J1 T"hrNՒDHކ+cwB݅mی_YʗWzurh>nz #;_.@O(.Zў= y$%'Ip)b:FKz۴烈\@jXK(+؝5o˫vdSˁ'i[[~J&$ڥH#OIΗ"FEU'O{zROZs$΃LΗM[LWOi}ƾ1nEsL^./*C1>gڮWO#Y4MӘ1qԎiU@OaL+9s+9)>(p~Iɚ21aV@#zJG~[Ӫ^̭hzWihӾh)f ldX2U&&aC9MW\.{rcLOa5["w‹#$_)ؽbxh;|ڐ}wn@O +=I[gi)R8[X9w:qp 3)ΚC 6./O)F`̮o{yJE0c"eGFaD#W'[K[wGZ)>r t9,s +Np6fǴ᫓L gOd41 ܌uȏbƚ':̗ݛ+Њd߽@Q2}T"ӍΛ?"t#D#H4' IxWNWK+p"V^9441i/BE3D#9xg(+[tQݖjq?ɾ{cc|{XRF|žc_0Ip=$'IlgK%(&hrOx50ׂ:_w0WafzsΣΗҫ6PiT[jq?Vx6(dC+ /ef Id=ެ%mŤX\Rzr_~ +Zrp4d_mY84%Ⱦ1?NuWm?}W4e&+woaruK!ďߡ"aݦMtYO*\O +fk=]Q|fdΤ>}s)(b |O+%{7|g~}Eu>zgyWF釩Kŷ_,{û@Q1q}7G^h $A5hPϮdtnr|4]qTfJimDڟ)ica\wƜeǸڇoyU/fS$TAn}hӥEq/! 'x}_o;Nǯch~ؕѓ⭩MvS 9D Ilyy5${I +N +2S9(!@FNMe9+{/?տ޵탽{˴e$Ax_ftܱXʙݐACHt'2S:rR ;/Yϫkzj~NmWT{ٽ=;{>:=hO_{oʾ{ %Dѓq4Ez3x%$Q+pW!OdRF{zz +:CH_sT%[dzwogb_W>O=>ܷ/~. .{=Qՠy +>β9K>]>]񺵆tͷn٨tv55mww]L[ncb?:^`û?{}q{=ɐ,8~ %vwc9on{nƔ$:%6rH=(^ +tas۶mzJ*xo^O$/?9"mvfƙ,L|xj$w,t0y(`<Sl*=i7mR5Q[6+|p]t|o5|=*L^nLv]K=LJkKדX%% 1 H4-WDGgYuY>tCA% JI9s`CA%I pi&#'/1~ZzEwbWm| }iûۻ?}KCaf%#-Qu @"zm8A]2޼T 5bvS_Í3J|i|_.VPΠvCGk/nP*|P8Ӗo*BG+U$')_rGM_OC:FWH`/6jsfsa_>| ?_5 {^`J|^h@XRd?ط/$ՓB:2t͚ۖ%;WqM]QA2 +#@'EbFFt pAVnjAzxꞭb0Qy@OO"m}駟~}/*s5_rǍW7XM>A|˟^d ~E:{k/=c5ײԳ{ks&8,/owk,{gN6w } g+] _EU3Qkmg4]i}Cθ_5 =sGv=oW{ {ÃlRݢ8ό KOU8g<(>>'[w[Հ @8 PB +%@Bc܀GI@dl5wclq{l|3wSI73=!}m#:eN}129!1wY\tW]=>k/Lzfx{KGl!\GL 7sǥkbny~D˨OoDr7D-V+_~gRD 0-$nXsH= ,zY,7.6)'^3'ŬƊynn, XX[5IY.+G:՛usG?᧿O?r޿Lsy\W!_̇\VUz9顱&%DBVaUOQ𧼇Uxѕ% +cT FJbSi }E3s F+?(\4T2i!nxKXXm$ E|rMƭxt<0μvʼnOg̷x*Z0}1#5v+1g2hNfeх;<5s%;zQ'qkJ.i՞ +#Uրtuy9{gQΛrLo ]Q틼id4_`^{tB¸ λ庫ͯS{Rud?Y4].2ﱞZǚcxN&S1K2g&W0>9,)Fv^?=ܿrVΛY7p}ˀnqwyḎo%tyok~uO=O:&s0.xНtֱMJ9oP'i`pPaƚ[;XQxLx yʋ$kzYFjNK򘿗.La\>>ǒ?ǦnA⹛0D +,i9n?&X^Ͷl1q~fu$Z{0EcOw]%?uN 6]!//^QHKsSk]`.a3%fn:?C QwC-6,7#pU: Q^ŜܬH} ʊ8+gSObJ!]]Iޤ<  ABJzk8vL7pF˛wv/Ʈ^ȇC̴Df b{Ug`ʒ{|l[*'G|=C{@XZ`7m^N27r[J _{>0s~@-DZeq2fsֳ)9qZ9oEsIDu QŠT϶`Ğ=P߲?D}Ƈ[F̌G(Y6 +V')'sj'ɶx7툅ҕϋrMޤ) 'fߌHL^ov^]#•yEtnƲ}3V/n1O-4^n {f+I$hMVzrqf& dʼnl+ݾܖHoo<9us>j QG%*+cÒܕ~LqO8 +4u}oOٞJOb5fC$ēݏ >aމ"hC!wV,q?y6Z%!,[/+c:Z+|n*S* w%Hp7,7D|mHycճB}Ia3śXG;hH@풧r_*(߲e"v*&>=AKŞO=nj/1Y?j4 +azy SXa_{ɉ܍ ww=3'K@2EuEiAb)*k Hok^-.n.u,l/'ֱ-RqVs]9XQx+MЇ sȸޡLJ粃P^.U›w2jH +QeUGO[ +޾4'Y*2JgNA림w2\(^]}Ըm>9,,@uXM OfGfOod%8 -+--%%K.?3YX]^M rҍN˝ &Nn&B;/LG&| ?J}"E_M=sO_7hF̠=๙U҃>^D}ˠggeCԱZi},na} g_校ۼ^nWAX /wOjմkDCVoQ 5@sΖ8+s#yad}ț 2-~䧕*W͟(g;Nz#^RT d=[|Sq!X/VfUnҗU(~YKx'#[&H~̺7#sR|5$Hd uByXg,\?j2&wtI RMqWQBk`[jv/F|z;x{+!5E9/NYܱ %Oz5pm,_p/U*Y&YUJJ?a> lTr$ybV]*{~_xU99y'77z97f ,E'.7,9D}Ni6ݬ[ď>ډ['$TLUz14Mc&ri\Dt1zVi`:jt,gl謺tOY8gEt7\tҸ<Ւ r*lOࠓ"kb$܉۽RvLNvIFM]җ|<|L/+'kZ]'_Նgq]F|Km2z$kfGD.egQBY͍ qLDm}vsLJ5[Cniy6YIȝx8c'B*'GTT K#6|˕f0O{✏n,w t Zpvit]6 $"JmOIZsEԑEa$#Dq~Mo^orIFeZ/WI+#|Lv5V!:&U]'ݰ M7MY+Q^lR ʝ^䳻Jyk"dn()ĿLsQ v+r33K}~\ H_Ƭu^u wz$'ugƃǎg&.ǙMX%04/cq&zpAlZ{ߖY9Fz&8&ś{ }orPǛwrN`@zv*kC2Wf.?%m>nLaM7X̙ddQ޻ΤZxalMIM+kFq)Q _x|G7yY452(0kQ_&ŃC? {\?͗.ZdZ8{ +o,g}Z>"ҟv #+Jo|uk#4ڝc^X1Kŀ(B!̽tLt IkżQ8[EDy!eN-f3rd^{Rb mU/3xs y{jvՀH,X)[t!Aig/p#+į'ę?{^W{ +#1 }Už̲ _cq? m+2}/I 7&}d!}y(Oeb7~kJ +#XZJ_ZYȀeXQ5 t&={19]ܖFwrN&=p=p#Lw2Lsˑz.ʫ_|A_1XYV&?kՂYe܏}iU{wwĎTQ )If#@#oY?E4@cNVR_ƀXuUޫ-|K%./fk|>kt-akÒM3o^^D>nKM:0%i]y{7nkJ?1qNmbe%inpՀt<*V%keynqY~˘X8-]s\Q8{ka_*wK[K/qw璼ikf+qpp˄EI$V-}BL/~ /ɗkΫr볬 ]g!5]49+`lן q ځ,a_3/NJE\1/gXViN3W_[ Bo,7@R{@ziwj HgN=!b+VC("w]xy03s_Y%yl3GXNZ/8' p 2]9?y}EEEMqW\kOefd +(}/L.^_7@[QY+-e2n)j )u+!-y<32޺![6Ӫ,a>g>//xEnKSQi20/1d<p2P +pH=e".e{ )vn,9ŀM^WvR.f_ծod88]ynj,?ԗYV7pFQrrZrQbU k5S@ACfBLb[ɞGJkQZ#8>u_J >ܨ@f.m16] aw +?aQP2=`ܸ঵+ +NaSǩ_OGb<݋r¡tᢞ~qϮtf[t(03%`SVxQ ߯ Mm +n_(0=GjyOfeQ}ZƮ5[U'6d,1[8߽~SZGbߏ{rf@pz/7y U6@USM=, fW 1{^)?j1#On޸ܜM\Ra ]KlV ~!_\6=s8]H;_/jC9L=0cPIc6bVw.Nm:r٤Y+ubӯyȀ9\VG/kM*}3ImYUEvd͝v]qCx탯ʹjM|(Ο&SԶ5\ubBo߯NC{a֣F6yVAXW_&Iwp +a=`.o:{k-;S[LփE?N ؘqMs*g1rHVvD0P +Iد%Oŝjn1EWƮۍhex.Tql/#SدWM1~*[>ycrS"hm2w7Bc-vwpՉM +ɗ9{{jn1,w-,u,'!$3>vS}-ši<$uW6,VB_c7n1Z7_kĦoN5=ӬͩlTZaB~S*#MRnha?οuk5o}$EdljA#/[7 [ޯ7EԁM[ZGOBØnĭװh0ɹk5#P{մo/ +~"DC}M=>:ȓm-}goM2L~~׿WhVYCJ!vq[+݈4{7<$?rf^:5c=o,!.{kĢ34+Wgwq)n5$?WW תP>n9[YXtqoXV0‘~cn[!2bFjeL*rpoy 0l_{#C*Hfo+#7Qؕ4CH]^U\Lf7C+2IP~{N7 k|ߗ~΃|&6ݠSCFPn?W t0cU1gs H_^H=db)5 +`.NZ:!Ups60YMt׷&5U٧򙜯+ɰBmZ.j{6^hC!B^o3w[4dc P؃r.0q:;C:vcvqh.{u&M^}h đ.q<1g(KxHcpdGT3>Fe}2 Eп0G=Ƽ2{XMgŢ {¥l`cv+֋DпXt(- ^J}^BO N0kA,-6D )8𼇲P6Ʀޠ!]_U]nn [=0mZW8l]ƽ/0:Z+?AZuث? <4CP:T#< c{e:_U(ׇcXH88w?# +GۯA{H|%uD <DϾ?C+56}TkxūeY ]T>HK~rWkbApѳOv{mX7MҘIUcٿ}d"ݵhf'{?٪rQItC4v2b[- !mycNGLb(r/7$k 'R^іaONS~wY7*>_&Jߘ&lw;hfTJ{&1On"Hq2n7~:v?E=ĘBu(T[CI/^^xj'¢)!!^TnD5x:JuܯUWvZbHL=jĦSp잣耸4H]yHWMJ&Ʊ4b&3oS[wǦk:({C=$nr$ݏkovggXZd=||mM%7ZwhKۨh{>ু/jԃj_j |(!8/1?d h3iO=ۥ4%7 o7 [8zaCCDI-ހv'7Bo@3nKLF:/f A}/K 7 B0 r.:s6 6)3߳z1ն6= .G٤)KQh h{D$p) W[[{. n$ǢtLpT!L[ϳZrJ]ý}(Sf;S@_Aƹy( +rGqMYxnkVm\l1Y=JթJ?}߳Ak d++on3L]o[سh*irg.  D33VOI*;C&B$J{vE^Ξchao~Bv Ozyt9< ٽƁ?_OMl2W܅Yb2F"wۨcí+a@b}?ks6gjQo :,zӛ.a`D%m78r>,zaҦr#㻑цEo;뭣Ɓ¢o̔o^[ +J!4|d\ǿDnD LnGJn~%ǁ@Ta3JX|)`\rZSI7@nĭt>J1tF4i.}!7y/%" ĢF,Px%RɭTr#6@q@7,F*G(#܈[|JCJ*7"&{>b/Lf:VnS9p*8t~?FsT ph^nub-q$#P-i.%o6=|Ï?;vq@ezs-j &@e*3*?W SI_;:>I?NaဢX4Mo a5Ŀb"a/1v9ŦY Zh (Cw/e5_nx%P~{tz3%>X箄րX6܃M~\Klh Ebƕ9^-mR`g"? 1)caJԀF'۩wTo˂Ħ"Pvs/_Xawma՛Aztj, $MpB nȶ 59?3 }#hvUobi89Lo8\qw+W }azqI>CߎAo\qe:i+AT@[@7$~Fq0I|xM Ǧ{K7wϫ:b#(q_HaSYoxzK(@3j#1cc8 +h{S#-:W>ǡoJhi N>tzsC'1?ytVo=Q{<<@bSYohM.W&"~Ƴ J66=pz+.iq{dW{I>HaOxFW7P(E恞*P*(%Ej4fxBەőn4~8H!126\JwZrqڀzb*MMn4ǦX&ܶu*^J æz(M2uqx(MJM}%@Ml/Tn"};EC:Il޸=(b]0oRy;0 E׍RBn<(91(MR[{7S%01bHyj-c%-y^urb.kW XtyˍY m:6#ܘھ7$Pf/* " 4n)@},ˍKw"RTǦ^+D2({ܛ7&!84M:`ވܕ@p@a, eZL mAp@YlzMJz/Ԅm^7ټXZrt9 +m@p@IZyj֍twp$P y#\p(E7&yџ*j8P ?)0~LbBp@+ 2!@Z6DaoR`3!8!JXIqg iqSԷo~cOG}>Z n.@nl:zcA+,ƢQ;V$@f,\EP/}@&FEZ&2P_p9q7?Jo$}sz鍽킅Rbӕ4I1ɥ.꧛ވ9hv{mo I%}M;&NkV ~nH?ita.M' 8 Ro|O;ȅC] 8 6XxaOF7~w@,fv Lp ! -/]E  JZ;I ȃMؼsz{ HC{cb2~wz$T_H1 3Q^$¦i7 HECR!7 6-3t՛I84[jCE2{_Uoy d]fSwü`ꍽN`zʍeNp}8¦p=1I 6>HOfa_#: ¦7y 6-誥 ·3dX-rA7 6y7 6n Mrfx-62vl7 \N 5V!MA HM7'i3H p'tt@ fހtXtfW$7 }ӣLr0o@>*; $W1i#8\s zdSA-ʻ۵Go&9K$eǘ81q7 -6=rn& =ia endstream endobj 31 0 obj <>stream +:yǑ bm:wlGyqhq.]ڷBo2jڥ& wHCӵ[ؘ "&0rΗIDL"-c 0H ۣ}{wڱCG&/@b~}wԡ)8I 1LoԿw.c+!w!d= اgfbTכAfc  ԯgNb)7 56zAwazS$ ȍMf<%оcM2xZG  \G^iѴ\3q'tCYm ¼ٱ釣F Ч3p1 + 2I!$ش#&ewpO-cӣōE(S#c֑t)1(M_:Mg8HH6瞒8t@޽DD yȥG mLo{S{2Ґjz)#Л%&&I # Eo?u!Lo;+7܀ % v?84clRܐ@R< )NC?<7ݣkXLԖIi9Էww=wH'}6zSF r+(6pI )Lo'5&~`7QA)=v+IyvRFߗ7^%Lr0o@VN|to짆wM_Orh ?ӛ !@ ѿO̾:$c2ճs8I6 hE?WN cF8\opCSz(L{.q@,?Л7/a߳8TB1wr.7'zVo܅=zt$ԛAb8t|nBԖpP"-:0ɿpH?vۮ) o o@#l:Ror'o@'l몮۷pf#cN;ws!y:a;0gޘ}{7]Po& +-AV]th/\bO:.b79}p'^8b< w{+ I KLrN"-Z?[TsCH7$whM_^Ɠ0o@32&UtæΏPor:qvtP 'O IJƃqL2d ~Oo܁-1+4plSF7CeLU Ja$SlAE ӐMo184h {No̻}c0]+܈!Lo7H7<Zc OV]tĦvh . b7%6Џȵ3I0BbVo&qeIzBo :qlp@ +C1x ZaІ3ygmXJi[Nٚ]eA; fmh+1w2 D56m.ReX6 wD7][x 辋 +C~N6mu0o[[p2vI8M L}%qhvN4ep'p]'8ij3HNajx +MLo`SgvEHE+ok!yr%N:6{-1[Ap&ycM_*i0oĢZN tC+:z,Z7iC2:pl?܂{9 +Iq:s7#o +Ya{^݂I#̂%Fj.æUp?<sW8K)c砞 zΟOpGDLib&& ;8Ǣ-IE`5ceLr`g2 tNX8{E8tnX8$8LpSq\JN/ }ax)e疗R%YRzHb@#`:2 "4ҔVvy'`h =~ogah=qG!^dh=pYp&9k8Ew\ҢA7EiL㈘H,:3H Xlv(i1XA=Xl%Y7_Ji_xyzl<$38OJ) "Rp4Zw7Wprax4A:-i4yH$,ǤY& q_b@Gfv1#B @`)9& p(hL2kc8t_ @qh|sSM< MƱ(-II1(Z0}QI-:}J7q_߽I&.܆ f&9& ̈́)qMT84JJ?}&d7 ͅ;ye-@2͆'ynT#J< %@ʏ~סQo +Wp^cL`hLp6qem_4hLu)JZT}!`'WAh9LF{o 7!cw8n-JxB {>!7&{'=_ 'Jo?$Ep#?$g킁 \0wC10pJ$ vDL6=p*L+8ƒE8!:4o7M\lApNk8tσC @顳:C~ao6xݧ7X)ܹK$YK3{9s9νz7 2O!@|JOpp;Q tEH98'ɷz@DrpOP{ 8'l P37Y{C |'_ҽs9&PVS㭹QJŗh@@x N0Ǜ$ߤ#@x e4 %'Wȧ.ڽ+-wr08P""wzxdXW!ހ +}MbFF|7vn%P<ܨ]98sp0߆Ș܍1y2ǧ$ಾES\_7ްzw7$#龻7@;c7 =w@_p3N dpGS}N$t+P{ Ԇ~Ñ/Tpaf4 g;wQ=yYP.X!c`@y鳨=tdgn]9ҙ~ΘAAn#9vp޼37S](JR _~>#P,tg%'i{K="8/SNͥdU Fp_{%̏yȏ lⴓC.(YPQ,|9w`b@m16t=9J|ppΐ_"I93i)?2Iڷ/b9@ëXto ciHGpq d+:.5 l PwU|y;r"Vsz1@gK57YGpv=ɡ%pnl4L|q$J|L'Jг78${g&z '!~x7N~f=]bsLO4(үΔ›Q)\ddo0?rf + ct,+@pf$yʀ/_9bGf|X +_l}8rr'_j-AUoif~tn=R],%_q"p)v՛nYN+y`#3L)CMئB&I;#”  r) 3 ZGސ44 GHANOprFYɠtp%m>hʄ”~'0DoN927KCMR `bt_V\D鷱=d+lҲ{6,Øu9/|/Ks<o1btft2 ȣɜPv}Ҳ{n]JIs奙 #T S})f{c#g_ B6LD鑙ES'@Q)[p]j-S_6C #C^65d89\*s յqnnUr@ɬ;[- :c0ӚcuDpΠn w=sp_`+ du{ 01rlB7VڟBC:dX +?gOBP涋mL=C) +~КVQGKf ytETmTdE8Y?^MՓ$8qJ\02&VQG_<26OGݴoX &9&Lz<9p(dl^K/r3"]0^2܋|% ?~ЛN-"ŖΟc#Ǣ#KS~69>QtG` oca+C o޾5olah/pnix %*O A^+cenGziYJS/ܨ _;8w}=M$>+gWTQ,7ҀHn d]޷ye< Rnp7S +G&ǀx47L:0{[ixgyrlKs4?yx9r4.|)6;08y it ei7 ba(s;SOms{sCE{]-.irms8ITx#L>7/ lHR졥±[y77`p+y3뎟릜%]!⽽^kahfտ>FYNp`koqɽ}'p(O\xMM2H;8fף/<XBB>L~ (fǬ=(x dl /"+D- s(Jf_3IbLl[i;8d馽P CdZ<}l7q}DX1R[i=pZeE\>5g&H0ZNB*AfcJ /#A S +WE>!pr@͍˿r׮1H^ҋ/P6dd2T\ 6#ܥ*#FƏI]ç/঍?/0iml3R +Mڐr#rM7AԱc}m߸᧫V2(&C@S +_Zv׿Xnt8<]>xG~~݆9_u!ԍd~Z@.]Zaz1M †8/xs3ݵ 1qK]17|s|@X +G#_o/-m5YQgM ңKf}]\{Ւ>X" Lƈ0y0psDB9z|(1/ bJS_JCqZ3V6 /+ՆgF/-)6*t5fMsRs"Kc(N4S_>U$P&)+mE GceQk\~']6Tgmm6Լ^Wc5g*cbE Ćx:)>n%ӶZפmЬ7g袲YzGsW`.#-ɘ7+Fkmj}|֛#W?k¦FCAQ]rCF˺ڼx}A`lN6}[Mb[fLQe5E&;Őܬ.]ї9یMٵm%re|v`-Ԣj-2R樾 +C2l27kʲB]_zuEkFV\L6ڠQw>j0#ݽ˴5EZr%}n\bj{RuD93rx G^r0&ϤꯏfΡ@ݗѮh rfENMݘST4 +mIT:VQ +}Vowö^5ݍ{cs"fjZL}wQc02c;ӝ>Rl[)qW6z]P"~`s+[EbZBDQnSMC(ΑXߝ:؝[d2csbm֖ ߥ,mj0QoӒX3@ggT*+ kkʛu9jg8^ng2R' )UUҟѕnHoo.p4$GMΠFh2s\^m1e^#<tNZ`TimYPG>t ~TP:-Jԓ+\e6ZU*SSuԷv)6Z,EI}v^ұ/МߝC40w#d*]Ѣn+ ڢWeO16UeFVk]Uݥ+Փf!r؃&ܙs͚5GPF[SRBY.ĤkY-Ime)U&92ͨn.6W-3[Zcu ZmkIF[\e'yP'+RB兆h&+ٸ^Zҥ5e)v}JSTȭv9QNhlNXƩ]ָ3Ra6%SJ][i[M9efBJwŕJ +"'9h,dRLQZJӔjcIAǪ|٘֘ڣcIi)-2IUV:*q[< = +p*EwmlعU{O쫀mش*0L1YU_{̻˳/T Vɭ +xTs4> +LL*\q3Q5 J,(I endstream endobj 15 0 obj [/ICCBased 19 0 R] endobj 6 0 obj [5 0 R] endobj 32 0 obj <> endobj xref 0 33 0000000000 65535 f +0000000016 00000 n +0000000144 00000 n +0000047649 00000 n +0000000000 00000 f +0000163121 00000 n +0000593503 00000 n +0000047700 00000 n +0000048109 00000 n +0000048283 00000 n +0000163420 00000 n +0000139682 00000 n +0000163307 00000 n +0000049181 00000 n +0000048344 00000 n +0000593468 00000 n +0000048620 00000 n +0000048668 00000 n +0000139717 00000 n +0000160473 00000 n +0000163191 00000 n +0000163222 00000 n +0000163494 00000 n +0000163800 00000 n +0000165099 00000 n +0000187851 00000 n +0000253439 00000 n +0000319027 00000 n +0000384615 00000 n +0000450203 00000 n +0000515791 00000 n +0000581379 00000 n +0000593526 00000 n +trailer <]>> startxref 593722 %%EOF \ No newline at end of file diff --git a/ui/responsive/design/chromeStorePics/promo1400560.png b/ui/responsive/design/chromeStorePics/promo1400560.png new file mode 100644 index 000000000..d3637ecc8 Binary files /dev/null and b/ui/responsive/design/chromeStorePics/promo1400560.png differ diff --git a/ui/responsive/design/chromeStorePics/promo440280.png b/ui/responsive/design/chromeStorePics/promo440280.png new file mode 100644 index 000000000..c1f92b1c0 Binary files /dev/null and b/ui/responsive/design/chromeStorePics/promo440280.png differ diff --git a/ui/responsive/design/chromeStorePics/promo920680.png b/ui/responsive/design/chromeStorePics/promo920680.png new file mode 100644 index 000000000..726bd810a Binary files /dev/null and b/ui/responsive/design/chromeStorePics/promo920680.png differ diff --git a/ui/responsive/design/chromeStorePics/screen_dao_accounts.png b/ui/responsive/design/chromeStorePics/screen_dao_accounts.png new file mode 100644 index 000000000..1a2e8052c Binary files /dev/null and b/ui/responsive/design/chromeStorePics/screen_dao_accounts.png differ diff --git a/ui/responsive/design/chromeStorePics/screen_dao_locked.png b/ui/responsive/design/chromeStorePics/screen_dao_locked.png new file mode 100644 index 000000000..6592c17e4 Binary files /dev/null and b/ui/responsive/design/chromeStorePics/screen_dao_locked.png differ diff --git a/ui/responsive/design/chromeStorePics/screen_dao_notification.png b/ui/responsive/design/chromeStorePics/screen_dao_notification.png new file mode 100644 index 000000000..baeb2ec39 Binary files /dev/null and b/ui/responsive/design/chromeStorePics/screen_dao_notification.png differ diff --git a/ui/responsive/design/chromeStorePics/screen_wei_account.png b/ui/responsive/design/chromeStorePics/screen_wei_account.png new file mode 100644 index 000000000..23301e4bf Binary files /dev/null and b/ui/responsive/design/chromeStorePics/screen_wei_account.png differ diff --git a/ui/responsive/design/chromeStorePics/screen_wei_notification.png b/ui/responsive/design/chromeStorePics/screen_wei_notification.png new file mode 100644 index 000000000..7a763e5df Binary files /dev/null and b/ui/responsive/design/chromeStorePics/screen_wei_notification.png differ diff --git a/ui/responsive/design/metamask-logo-eyes.png b/ui/responsive/design/metamask-logo-eyes.png new file mode 100644 index 000000000..c29331b28 Binary files /dev/null and b/ui/responsive/design/metamask-logo-eyes.png differ diff --git a/ui/responsive/design/wireframes/1st_time_use.png b/ui/responsive/design/wireframes/1st_time_use.png new file mode 100644 index 000000000..c18ced5e2 Binary files /dev/null and b/ui/responsive/design/wireframes/1st_time_use.png differ diff --git a/ui/responsive/design/wireframes/metamask_wfs_jan_13.pdf b/ui/responsive/design/wireframes/metamask_wfs_jan_13.pdf new file mode 100644 index 000000000..c77c9274a Binary files /dev/null and b/ui/responsive/design/wireframes/metamask_wfs_jan_13.pdf differ diff --git a/ui/responsive/design/wireframes/metamask_wfs_jan_13.png b/ui/responsive/design/wireframes/metamask_wfs_jan_13.png new file mode 100644 index 000000000..d71d7bdb4 Binary files /dev/null and b/ui/responsive/design/wireframes/metamask_wfs_jan_13.png differ diff --git a/ui/responsive/design/wireframes/metamask_wfs_jan_18.pdf b/ui/responsive/design/wireframes/metamask_wfs_jan_18.pdf new file mode 100644 index 000000000..592ba8532 Binary files /dev/null and b/ui/responsive/design/wireframes/metamask_wfs_jan_18.pdf differ diff --git a/ui/responsive/example.js b/ui/responsive/example.js new file mode 100644 index 000000000..4627c0e9c --- /dev/null +++ b/ui/responsive/example.js @@ -0,0 +1,123 @@ +const injectCss = require('inject-css') +const MetaMaskUi = require('./index.js') +const MetaMaskUiCss = require('./css.js') +const EventEmitter = require('events').EventEmitter + +// account management + +var identities = { + '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111': { + name: 'Walrus', + img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', + address: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', + balance: 220, + txCount: 4, + }, + '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222': { + name: 'Tardus', + img: 'QmQYaRdrf2EhRhJWaHnts8Meu1mZiXrNib5W1P6cYmXWRL', + address: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222', + balance: 10.005, + txCount: 16, + }, + '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333': { + name: 'Gambler', + img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', + address: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333', + balance: 0.000001, + txCount: 1, + }, +} + +var unapprovedTxs = {} +addUnconfTx({ + from: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222', + to: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', + value: '0x123', +}) +addUnconfTx({ + from: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', + to: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333', + value: '0x0000', + data: '0x000462427bcc9133bb46e88bcbe39cd7ef0e7000', +}) + +function addUnconfTx (txParams) { + var time = (new Date()).getTime() + var id = createRandomId() + unapprovedTxs[id] = { + id: id, + txParams: txParams, + time: time, + } +} + +var isUnlocked = false +var selectedAccount = null + +function getState () { + return { + isUnlocked: isUnlocked, + identities: isUnlocked ? identities : {}, + unapprovedTxs: isUnlocked ? unapprovedTxs : {}, + selectedAccount: selectedAccount, + } +} + +var accountManager = new EventEmitter() + +accountManager.getState = function (cb) { + cb(null, getState()) +} + +accountManager.setLocked = function () { + isUnlocked = false + this._didUpdate() +} + +accountManager.submitPassword = function (password, cb) { + if (password === 'test') { + isUnlocked = true + cb(null, getState()) + this._didUpdate() + } else { + cb(new Error('Bad password -- try "test"')) + } +} + +accountManager.setSelectedAccount = function (address, cb) { + selectedAccount = address + cb(null, getState()) + this._didUpdate() +} + +accountManager.signTransaction = function (txParams, cb) { + alert('signing tx....') +} + +accountManager._didUpdate = function () { + this.emit('update', getState()) +} + +// start app + +var container = document.getElementById('app-content') + +var css = MetaMaskUiCss() +injectCss(css) + +MetaMaskUi({ + container: container, + accountManager: accountManager, +}) + +// util + +function createRandomId () { + // 13 time digits + var datePart = new Date().getTime() * Math.pow(10, 3) + // 3 random digits + var extraPart = Math.floor(Math.random() * Math.pow(10, 3)) + // 16 digits + return datePart + extraPart +} diff --git a/ui/responsive/index.html b/ui/responsive/index.html new file mode 100644 index 000000000..9dfaefbb3 --- /dev/null +++ b/ui/responsive/index.html @@ -0,0 +1,20 @@ + + + + + MetaMask + + + + +

+ + + + +
+ +
+ + + diff --git a/ui/responsive/index.js b/ui/responsive/index.js new file mode 100644 index 000000000..a729138d3 --- /dev/null +++ b/ui/responsive/index.js @@ -0,0 +1,58 @@ +const render = require('react-dom').render +const h = require('react-hyperscript') +const Root = require('./app/root') +const actions = require('./app/actions') +const configureStore = require('./app/store') +const txHelper = require('./lib/tx-helper') +global.log = require('loglevel') + +module.exports = launchMetamaskUi + + +log.setLevel(global.METAMASK_DEBUG ? 'debug' : 'warn') + +function launchMetamaskUi (opts, cb) { + var accountManager = opts.accountManager + actions._setBackgroundConnection(accountManager) + // check if we are unlocked first + accountManager.getState(function (err, metamaskState) { + if (err) return cb(err) + const store = startApp(metamaskState, accountManager, opts) + cb(null, store) + }) +} + +function startApp (metamaskState, accountManager, opts) { + // parse opts + const store = configureStore({ + + // metamaskState represents the cross-tab state + metamask: metamaskState, + + // appState represents the current tab's popup state + appState: {}, + + // Which blockchain we are using: + networkVersion: opts.networkVersion, + }) + + // if unconfirmed txs, start on txConf page + const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.network) + if (unapprovedTxsAll.length > 0) { + store.dispatch(actions.showConfTxPage()) + } + + accountManager.on('update', function (metamaskState) { + store.dispatch(actions.updateMetamaskState(metamaskState)) + }) + + // start app + render( + h(Root, { + // inject initial state + store: store, + } + ), opts.container) + + return store +} diff --git a/ui/responsive/lib/account-link.js b/ui/responsive/lib/account-link.js new file mode 100644 index 000000000..d061d0ad1 --- /dev/null +++ b/ui/responsive/lib/account-link.js @@ -0,0 +1,26 @@ +module.exports = function (address, network) { + const net = parseInt(network) + let link + switch (net) { + case 1: // main net + link = `http://etherscan.io/address/${address}` + break + case 2: // morden test net + link = `http://morden.etherscan.io/address/${address}` + break + case 3: // ropsten test net + link = `http://ropsten.etherscan.io/address/${address}` + break + case 4: // rinkeby test net + link = `http://rinkeby.etherscan.io/address/${address}` + break + case 42: // kovan test net + link = `http://kovan.etherscan.io/address/${address}` + break + default: + link = '' + break + } + + return link +} diff --git a/ui/responsive/lib/contract-namer.js b/ui/responsive/lib/contract-namer.js new file mode 100644 index 000000000..f05e770cc --- /dev/null +++ b/ui/responsive/lib/contract-namer.js @@ -0,0 +1,33 @@ +/* CONTRACT NAMER + * + * Takes an address, + * Returns a nicname if we have one stored, + * otherwise returns null. + */ + +const contractMap = require('eth-contract-metadata') +const ethUtil = require('ethereumjs-util') + +module.exports = function (addr, identities = {}) { + const checksummed = ethUtil.toChecksumAddress(addr) + if (contractMap[checksummed] && contractMap[checksummed].name) { + return contractMap[checksummed].name + } + + const address = addr.toLowerCase() + const ids = hashFromIdentities(identities) + return addrFromHash(address, ids) +} + +function hashFromIdentities (identities) { + const result = {} + for (const key in identities) { + result[key] = identities[key].name + } + return result +} + +function addrFromHash (addr, hash) { + const address = addr.toLowerCase() + return hash[address] || null +} diff --git a/ui/responsive/lib/etherscan-prefix-for-network.js b/ui/responsive/lib/etherscan-prefix-for-network.js new file mode 100644 index 000000000..2c1904f1c --- /dev/null +++ b/ui/responsive/lib/etherscan-prefix-for-network.js @@ -0,0 +1,21 @@ +module.exports = function (network) { + const net = parseInt(network) + let prefix + switch (net) { + case 1: // main net + prefix = '' + break + case 3: // ropsten test net + prefix = 'ropsten.' + break + case 4: // rinkeby test net + prefix = 'rinkeby.' + break + case 42: // kovan test net + prefix = 'kovan.' + break + default: + prefix = '' + } + return prefix +} diff --git a/ui/responsive/lib/explorer-link.js b/ui/responsive/lib/explorer-link.js new file mode 100644 index 000000000..3b82ecd5f --- /dev/null +++ b/ui/responsive/lib/explorer-link.js @@ -0,0 +1,6 @@ +const prefixForNetwork = require('./etherscan-prefix-for-network') + +module.exports = function (hash, network) { + const prefix = prefixForNetwork(network) + return `http://${prefix}etherscan.io/tx/${hash}` +} diff --git a/ui/responsive/lib/icon-factory.js b/ui/responsive/lib/icon-factory.js new file mode 100644 index 000000000..27a74de66 --- /dev/null +++ b/ui/responsive/lib/icon-factory.js @@ -0,0 +1,65 @@ +var iconFactory +const isValidAddress = require('ethereumjs-util').isValidAddress +const toChecksumAddress = require('ethereumjs-util').toChecksumAddress +const contractMap = require('eth-contract-metadata') + +module.exports = function (jazzicon) { + if (!iconFactory) { + iconFactory = new IconFactory(jazzicon) + } + return iconFactory +} + +function IconFactory (jazzicon) { + this.jazzicon = jazzicon + this.cache = {} +} + +IconFactory.prototype.iconForAddress = function (address, diameter) { + const addr = toChecksumAddress(address) + if (iconExistsFor(addr)) { + return imageElFor(addr) + } + + return this.generateIdenticonSvg(address, diameter) +} + +// returns svg dom element +IconFactory.prototype.generateIdenticonSvg = function (address, diameter) { + var cacheId = `${address}:${diameter}` + // check cache, lazily generate and populate cache + var identicon = this.cache[cacheId] || (this.cache[cacheId] = this.generateNewIdenticon(address, diameter)) + // create a clean copy so you can modify it + var cleanCopy = identicon.cloneNode(true) + return cleanCopy +} + +// creates a new identicon +IconFactory.prototype.generateNewIdenticon = function (address, diameter) { + var numericRepresentation = jsNumberForAddress(address) + var identicon = this.jazzicon(diameter, numericRepresentation) + return identicon +} + +// util + +function iconExistsFor (address) { + return contractMap[address] && isValidAddress(address) && contractMap[address].logo +} + +function imageElFor (address) { + const contract = contractMap[address] + const fileName = contract.logo + const path = `images/contract/${fileName}` + const img = document.createElement('img') + img.src = path + img.style.width = '75%' + return img +} + +function jsNumberForAddress (address) { + var addr = address.slice(2, 10) + var seed = parseInt(addr, 16) + return seed +} + diff --git a/ui/responsive/lib/lost-accounts-notice.js b/ui/responsive/lib/lost-accounts-notice.js new file mode 100644 index 000000000..948b13db6 --- /dev/null +++ b/ui/responsive/lib/lost-accounts-notice.js @@ -0,0 +1,23 @@ +const summary = require('../app/util').addressSummary + +module.exports = function (lostAccounts) { + return { + date: new Date().toDateString(), + title: 'Account Problem Caught', + body: `MetaMask has fixed a bug where some accounts were previously mis-generated. This was a rare issue, but you were affected! + +We have successfully imported the accounts that were mis-generated, but they will no longer be recovered with your normal seed phrase. + +We have marked the affected accounts as "Loose", and recommend you transfer ether and tokens away from those accounts, or export & back them up elsewhere. + +Your affected accounts are: +${lostAccounts.map(acct => ` - ${summary(acct)}`).join('\n')} + +These accounts have been marked as "Loose" so they will be easy to recognize in the account list. + +For more information, please read [our blog post.][1] + +[1]: https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd#.7d8ktj4h3 + `, + } +} diff --git a/ui/responsive/lib/persistent-form.js b/ui/responsive/lib/persistent-form.js new file mode 100644 index 000000000..d4dc20b03 --- /dev/null +++ b/ui/responsive/lib/persistent-form.js @@ -0,0 +1,61 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const defaultKey = 'persistent-form-default' +const eventName = 'keyup' + +module.exports = PersistentForm + +function PersistentForm () { + Component.call(this) +} + +inherits(PersistentForm, Component) + +PersistentForm.prototype.componentDidMount = function () { + const fields = document.querySelectorAll('[data-persistent-formid]') + const store = this.getPersistentStore() + + for (var i = 0; i < fields.length; i++) { + const field = fields[i] + const key = field.getAttribute('data-persistent-formid') + const cached = store[key] + if (cached !== undefined) { + field.value = cached + } + + field.addEventListener(eventName, this.persistentFieldDidUpdate.bind(this)) + } +} + +PersistentForm.prototype.getPersistentStore = function () { + let store = window.localStorage[this.persistentFormParentId || defaultKey] + if (store && store !== 'null') { + store = JSON.parse(store) + } else { + store = {} + } + return store +} + +PersistentForm.prototype.setPersistentStore = function (newStore) { + window.localStorage[this.persistentFormParentId || defaultKey] = JSON.stringify(newStore) +} + +PersistentForm.prototype.persistentFieldDidUpdate = function (event) { + const field = event.target + const store = this.getPersistentStore() + const key = field.getAttribute('data-persistent-formid') + const val = field.value + store[key] = val + this.setPersistentStore(store) +} + +PersistentForm.prototype.componentWillUnmount = function () { + const fields = document.querySelectorAll('[data-persistent-formid]') + for (var i = 0; i < fields.length; i++) { + const field = fields[i] + field.removeEventListener(eventName, this.persistentFieldDidUpdate.bind(this)) + } + this.setPersistentStore({}) +} + diff --git a/ui/responsive/lib/tx-helper.js b/ui/responsive/lib/tx-helper.js new file mode 100644 index 000000000..ec19daf64 --- /dev/null +++ b/ui/responsive/lib/tx-helper.js @@ -0,0 +1,17 @@ +const valuesFor = require('../app/util').valuesFor + +module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, network) { + log.debug('tx-helper called with params:') + log.debug({ unapprovedTxs, unapprovedMsgs, personalMsgs, network }) + + const txValues = network ? valuesFor(unapprovedTxs).filter(txMeta => txMeta.metamaskNetworkId === network) : valuesFor(unapprovedTxs) + log.debug(`tx helper found ${txValues.length} unapproved txs`) + const msgValues = valuesFor(unapprovedMsgs) + log.debug(`tx helper found ${msgValues.length} unsigned messages`) + let allValues = txValues.concat(msgValues) + const personalValues = valuesFor(personalMsgs) + log.debug(`tx helper found ${personalValues.length} unsigned personal messages`) + allValues = allValues.concat(personalValues) + + return allValues.sort(txMeta => txMeta.time) +} -- cgit v1.2.3 From 0e5ec5b86ddf74c17ba7bd359f86014847b58695 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 3 Jul 2017 16:48:45 -0700 Subject: Add responsive UI dev guide --- README.md | 1 + docs/responsive-ui-dev.md | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 docs/responsive-ui-dev.md diff --git a/README.md b/README.md index afeb96ae5..e9fe682fd 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ To write tests that will be run in the browser using QUnit, add your test files - [Publishing Guide](./docs/publishing.md) - [How to develop an in-browser mocked UI](./docs/ui-mock-mode.md) - [How to live reload on local dependency changes](./docs/developing-on-deps.md) +- [How to Edit our New Responsive UI](./docs/responsive-ui-dev.md) - [How to add new networks to the Provider Menu](./docs/adding-new-networks.md) - [How to manage notices that appear when the app starts up](./docs/notices.md) - [How to generate a visualization of this repository's development](./docs/development-visualization.md) diff --git a/docs/responsive-ui-dev.md b/docs/responsive-ui-dev.md new file mode 100644 index 000000000..280c78020 --- /dev/null +++ b/docs/responsive-ui-dev.md @@ -0,0 +1,11 @@ +# Developing our Responsive UI + +To allow parallel development of a new responsive version of our interface, we have forked our `ui` folder into two sub-folders: + +- ui/classic (our original extension UI, fixed dimensions) +- ui/responsive (our new, responsive UI) + +To visit this new responsive ui while in development mode (`npm start`) simply visit: + +[chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/home.html](chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/home.html) + -- cgit v1.2.3 From b72861fc9848a474badac076951d5286a996d2e8 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 3 Jul 2017 17:18:26 -0700 Subject: Make responsive UI more flexy --- ui/responsive/app/account-detail.js | 1 + ui/responsive/app/accounts/index.js | 1 - ui/responsive/app/app.js | 7 +------ ui/responsive/app/css/index.css | 13 ++++++++++--- ui/responsive/app/keychains/hd/create-vault-complete.js | 2 -- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ui/responsive/app/account-detail.js b/ui/responsive/app/account-detail.js index bed05a7fb..ff5c2aadb 100644 --- a/ui/responsive/app/account-detail.js +++ b/ui/responsive/app/account-detail.js @@ -60,6 +60,7 @@ AccountDetailScreen.prototype.render = function () { h('.account-data-subsection', { style: { margin: '0 20px', + maxWidth: '320px', }, }, [ diff --git a/ui/responsive/app/accounts/index.js b/ui/responsive/app/accounts/index.js index ac2615cd7..3e0830b63 100644 --- a/ui/responsive/app/accounts/index.js +++ b/ui/responsive/app/accounts/index.js @@ -56,7 +56,6 @@ AccountsScreen.prototype.render = function () { // identity selection h('section.identity-section', { style: { - height: '418px', overflowY: 'auto', overflowX: 'hidden', }, diff --git a/ui/responsive/app/app.js b/ui/responsive/app/app.js index 1a63002e1..e7bde9605 100644 --- a/ui/responsive/app/app.js +++ b/ui/responsive/app/app.js @@ -93,12 +93,7 @@ App.prototype.render = function () { }), // panel content - h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), { - style: { - height: '380px', - width: '360px', - }, - }, [ + h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), [ h(ReactCSSTransitionGroup, { className: 'css-transition-group', transitionName: 'main', diff --git a/ui/responsive/app/css/index.css b/ui/responsive/app/css/index.css index 808aafb4c..c82c1b21b 100644 --- a/ui/responsive/app/css/index.css +++ b/ui/responsive/app/css/index.css @@ -19,6 +19,14 @@ html, body { font-weight: 300; line-height: 1.4em; background: #F7F7F7; + width: 100%; + height: 100%; + margin: 0; + padding: 0; +} + +.css-transition-group { + flex: 1; } input:focus, textarea:focus { @@ -28,8 +36,6 @@ input:focus, textarea:focus { #app-content { overflow-x: hidden; min-width: 357px; - width: 360px; - height: 500px; } button, input[type="submit"] { @@ -403,7 +409,8 @@ input.large-input { /* account detail screen */ .account-detail-section { - + display: flex; + flex-wrap: wrap; } .name-label{ diff --git a/ui/responsive/app/keychains/hd/create-vault-complete.js b/ui/responsive/app/keychains/hd/create-vault-complete.js index a318a9b50..c32751fff 100644 --- a/ui/responsive/app/keychains/hd/create-vault-complete.js +++ b/ui/responsive/app/keychains/hd/create-vault-complete.js @@ -47,8 +47,6 @@ CreateVaultCompleteScreen.prototype.render = function () { h('div', { style: { - width: '360px', - height: '78px', fontSize: '1em', marginTop: '10px', textAlign: 'center', -- cgit v1.2.3 From af8015c1c58589386acd6a2d00111944cffac44f Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 3 Jul 2017 18:06:47 -0700 Subject: Version 3.8.2 --- CHANGELOG.md | 2 ++ app/manifest.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07f55bf2b..bcbd81e30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +## 3.8.2 2017-7-3 + - No longer show network loading indication on config screen, to allow selecting custom RPCs. - Visually indicate that network spinner is a menu. - Indicate what network is being searched for when disconnected. diff --git a/app/manifest.json b/app/manifest.json index c0d9af8a0..12ff6c2ea 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.8.1", + "version": "3.8.2", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From 68fc3603dfe72721e080a80b9a4103408e113c6c Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 4 Jul 2017 12:48:00 -0700 Subject: metamask - append dapp origin domain to rpc request --- app/scripts/metamask-controller.js | 11 +++++++++-- package.json | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 782641b3f..73093dfad 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -184,7 +184,9 @@ module.exports = class MetamaskController extends EventEmitter { eth_syncing: false, web3_clientVersion: `MetaMask/v${version}`, }, + // rpc data source rpcUrl: this.networkController.getCurrentRpcAddress(), + originHttpHeaderKey: 'X-Metamask-Origin', // account mgmt getAccounts: (cb) => { const isUnlocked = this.keyringController.memStore.getState().isUnlocked @@ -356,8 +358,13 @@ module.exports = class MetamaskController extends EventEmitter { } setupProviderConnection (outStream, originDomain) { - streamIntoProvider(outStream, this.provider, logger) - function logger (err, request, response) { + streamIntoProvider(outStream, this.provider, onRequest, onResponse) + // append dapp origin domain to request + function onRequest (request) { + request.origin = originDomain + } + // log rpc activity + function onResponse (err, request, response) { if (err) return console.error(err) if (response.error) { console.error('Error in RPC response:\n', response.error) diff --git a/package.json b/package.json index 3b608af0e..27fe7a84a 100644 --- a/package.json +++ b/package.json @@ -124,8 +124,8 @@ "valid-url": "^1.0.9", "vreme": "^3.0.2", "web3": "0.19.1", - "web3-provider-engine": "^13.0.3", - "web3-stream-provider": "^2.0.6", + "web3-provider-engine": "^13.1.1", + "web3-stream-provider": "^3.0.1", "xtend": "^4.0.1" }, "devDependencies": { -- cgit v1.2.3 From 52a6b9f103fecfd92f860188daf15e1fa943ab5f Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 5 Jul 2017 10:30:48 -0700 Subject: Reenable Default Token List Looks pretty clear to me now that the heavy traffic spike was not this feature, but was the EOS crowdsale. Now that we've mitigated their traffic spike, I think we can safely re-introduce this feature. --- CHANGELOG.md | 3 +++ ui/app/components/token-list.js | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcbd81e30..e7934dc77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Current Master +- Re-enable default token list. +- Add origin header to dapp-bound requests to allow providers to throttle sites. + ## 3.8.2 2017-7-3 - No longer show network loading indication on config screen, to allow selecting custom RPCs. diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index fed7e9f7a..20cfa897e 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -6,7 +6,6 @@ const TokenCell = require('./token-cell.js') const normalizeAddress = require('eth-sig-util').normalize const defaultTokens = [] -/* const contracts = require('eth-contract-metadata') for (const address in contracts) { const contract = contracts[address] @@ -15,7 +14,6 @@ for (const address in contracts) { defaultTokens.push(contract) } } -*/ module.exports = TokenList -- cgit v1.2.3 From 6d2cddaac9348b0e9c8a6cc4a6621927765e7c17 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 5 Jul 2017 12:00:42 -0700 Subject: fix nonce calculation order --- app/scripts/lib/nonce-tracker.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js index 9ef7706f9..ab2893b10 100644 --- a/app/scripts/lib/nonce-tracker.js +++ b/app/scripts/lib/nonce-tracker.js @@ -12,14 +12,16 @@ class NonceTracker { // releaseLock must be called // releaseLock must be called after adding signed tx to pending transactions (or discarding) async getNonceLock (address) { - const pendingTransactions = this.getPendingTransactions(address) // await lock free - if (pendingTransactions.length) await this.lockMap[address] - else if (this.lockMap[address]) await this.lockMap[address]() + await this.lockMap[address] // take lock const releaseLock = this._takeLock(address) // calculate next nonce - const baseCount = await this._getTxCount(address) + // we need to make sure our base count + // and pending count are from the same block + const currentBlock = await this._getCurrentBlock() + const pendingTransactions = this.getPendingTransactions(address) + const baseCount = await this._getTxCount(address, currentBlock) const nextNonce = parseInt(baseCount) + pendingTransactions.length // return next nonce and release cb return { nextNonce: nextNonce.toString(16), releaseLock } @@ -43,8 +45,7 @@ class NonceTracker { return releaseLock } - async _getTxCount (address) { - const currentBlock = await this._getCurrentBlock() + async _getTxCount (address, currentBlock) { const blockNumber = currentBlock.number return new Promise((resolve, reject) => { this.ethQuery.getTransactionCount(address, blockNumber, (err, result) => { -- cgit v1.2.3 From 51ff6d74e884b599f78b5454b33f2bc1a046f0b2 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 5 Jul 2017 12:07:34 -0700 Subject: clean up unused code from old noncelock --- app/scripts/controllers/transactions.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index c74427cd5..2b40f9456 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -1,7 +1,6 @@ const EventEmitter = require('events') const async = require('async') const extend = require('xtend') -const Semaphore = require('semaphore') const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') const denodeify = require('denodeify') @@ -41,7 +40,6 @@ module.exports = class TransactionController extends EventEmitter { this.blockTracker.on('latest', this.resubmitPendingTxs.bind(this)) this.blockTracker.on('sync', this.queryPendingTxs.bind(this)) this.signEthTx = opts.signTransaction - this.nonceLock = Semaphore(1) this.ethStore = opts.ethStore // memstore is computed from a few different stores this._updateMemstore() -- cgit v1.2.3 From 5c1ccc657c171e65a8cf8e2bc10fcb6267c44d4a Mon Sep 17 00:00:00 2001 From: tmashuang Date: Wed, 5 Jul 2017 13:42:15 -0700 Subject: Fix spelling --- ui/app/add-token.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/add-token.js b/ui/app/add-token.js index b303b5c0d..15ef7a852 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -86,7 +86,7 @@ AddTokenScreen.prototype.render = function () { h('div', [ h('span', { style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Token Sybmol'), + }, 'Token Symbol'), ]), h('div', { style: {display: 'flex'} }, [ -- cgit v1.2.3 From a915dfdeaa51c80c599b15f4e1ec14c90ac00fbf Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 5 Jul 2017 22:36:52 -0700 Subject: Add failing test for retrying an over-spending tx --- test/unit/tx-controller-test.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index 0d35cd62c..b5df7f970 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -322,4 +322,41 @@ describe('Transaction Controller', function () { }) }) }) + + describe('#_resubmitTx with a too-low balance', function () { + const from = '0xda0da0' + const txMeta = { + id: 1, + status: 'submitted' + txParams: { + from, + nonce: '0x1' + }, + } + + const lowBalance = '0x0' + const fakeStoreState = {} + fakeStoreState[from] = { + balance: lowBalance, + nonce: '0x0', + } + + // Stubbing out current account state: + const getStateStub = sinon.stub(txController.ethStore, 'getState') + .returns(fakeStoreState) + + // Adding the fake tx: + txController.addTx(txMeta, noop) + + it('should fail the transaction', function (done) { + txController._resubmitTx(txMeta, function (err) { + assert.ifError('should not throw an error') + const updatedMeta = txController.getTx(txMeta.id) + assert.notEqual(updatedMeta.status, txMeta.status, 'status changed.') + assert.notEqual(updatedMeta.status, 'failed', 'tx set to failed.') + }) + }) + }) + }) + -- cgit v1.2.3 From 3abceac55d16e41b37116a8dda565644ed0a9f52 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 5 Jul 2017 22:06:39 -0700 Subject: Fail pending txs with low balance or invalid nonce --- app/scripts/controllers/transactions.js | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 52251d66e..3f5834756 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -428,10 +428,28 @@ module.exports = class TransactionController extends EventEmitter { const gtBalance = Number.parseInt(txMeta.txParams.value) > Number.parseInt(balance) if (!('retryCount' in txMeta)) txMeta.retryCount = 0 - // if the value of the transaction is greater then the balance - // or the nonce of the transaction is lower then the accounts nonce - // dont resubmit the tx - if (gtBalance || txNonce < nonce) return cb() + // if the value of the transaction is greater then the balance, fail. + if (gtBalance) { + txMeta.err = { + isWarning: true, + message: 'Insufficient balance.', + } + this.updateTx(txMeta) + cb() + return log.error(txMeta.err.message) + } + + // if the nonce of the transaction is lower then the accounts nonce, fail. + if (txNonce < nonce) { + txMeta.err = { + isWarning: true, + message: 'Invalid nonce.', + } + this.updateTx(txMeta) + cb() + return log.error(txMeta.err.message) + } + // Only auto-submit already-signed txs: if (!('rawTx' in txMeta)) return cb() -- cgit v1.2.3 From ef1282b55648ad5e787b170cc06e5f8b292f5983 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 5 Jul 2017 22:48:11 -0700 Subject: Typo fix --- test/unit/tx-controller-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index b5df7f970..074e6c954 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -327,7 +327,7 @@ describe('Transaction Controller', function () { const from = '0xda0da0' const txMeta = { id: 1, - status: 'submitted' + status: 'submitted', txParams: { from, nonce: '0x1' -- cgit v1.2.3 From 96df7ad8d36b68e521e670d28e3efda38e41972f Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 5 Jul 2017 23:04:51 -0700 Subject: Add missing done --- test/unit/tx-controller-test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index 074e6c954..7b0ad66bd 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -342,6 +342,7 @@ describe('Transaction Controller', function () { } // Stubbing out current account state: + txController.ethStore = { getState: noop } const getStateStub = sinon.stub(txController.ethStore, 'getState') .returns(fakeStoreState) @@ -354,9 +355,9 @@ describe('Transaction Controller', function () { const updatedMeta = txController.getTx(txMeta.id) assert.notEqual(updatedMeta.status, txMeta.status, 'status changed.') assert.notEqual(updatedMeta.status, 'failed', 'tx set to failed.') + done() }) }) }) - }) -- cgit v1.2.3 From 07d4e4fe6f31d99a9f15c3862671c5c07831ff2a Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 5 Jul 2017 23:23:57 -0700 Subject: Fix failing test --- app/scripts/controllers/transactions.js | 18 ++++------- test/unit/tx-controller-test.js | 56 +++++++++++++++++---------------- 2 files changed, 35 insertions(+), 39 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 3f5834756..7946d10d1 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -430,24 +430,18 @@ module.exports = class TransactionController extends EventEmitter { // if the value of the transaction is greater then the balance, fail. if (gtBalance) { - txMeta.err = { - isWarning: true, - message: 'Insufficient balance.', - } - this.updateTx(txMeta) + const message = 'Insufficient balance.' + this.setTxStatusFailed(txMeta.id, message) cb() - return log.error(txMeta.err.message) + return log.error(message) } // if the nonce of the transaction is lower then the accounts nonce, fail. if (txNonce < nonce) { - txMeta.err = { - isWarning: true, - message: 'Invalid nonce.', - } - this.updateTx(txMeta) + const message = 'Invalid nonce.' + this.setTxStatusFailed(txMeta.id, message) cb() - return log.error(txMeta.err.message) + return log.error(message) } // Only auto-submit already-signed txs: diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index 7b0ad66bd..01a498820 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -19,6 +19,7 @@ describe('Transaction Controller', function () { txController = new TransactionController({ networkStore: new ObservableStore(currentNetworkId), txHistoryLimit: 10, + ethStore: { getState: noop }, provider: { _blockTracker: new EventEmitter()}, blockTracker: new EventEmitter(), ethQuery: new EthQuery(new EventEmitter()), @@ -324,37 +325,38 @@ describe('Transaction Controller', function () { }) describe('#_resubmitTx with a too-low balance', function () { - const from = '0xda0da0' - const txMeta = { - id: 1, - status: 'submitted', - txParams: { - from, - nonce: '0x1' - }, - } - - const lowBalance = '0x0' - const fakeStoreState = {} - fakeStoreState[from] = { - balance: lowBalance, - nonce: '0x0', - } - - // Stubbing out current account state: - txController.ethStore = { getState: noop } - const getStateStub = sinon.stub(txController.ethStore, 'getState') - .returns(fakeStoreState) - - // Adding the fake tx: - txController.addTx(txMeta, noop) - it('should fail the transaction', function (done) { + const from = '0xda0da0' + const txMeta = { + id: 1, + status: 'submitted', + metamaskNetworkId: currentNetworkId, + txParams: { + from, + nonce: '0x1', + value: '0xfffff', + }, + } + + const lowBalance = '0x0' + const fakeStoreState = { accounts: {} } + fakeStoreState.accounts[from] = { + balance: lowBalance, + nonce: '0x0', + } + + // Stubbing out current account state: + const getStateStub = sinon.stub(txController.ethStore, 'getState') + .returns(fakeStoreState) + + // Adding the fake tx: + txController.addTx(clone(txMeta)) + txController._resubmitTx(txMeta, function (err) { - assert.ifError('should not throw an error') + assert.ifError(err, 'should not throw an error') const updatedMeta = txController.getTx(txMeta.id) assert.notEqual(updatedMeta.status, txMeta.status, 'status changed.') - assert.notEqual(updatedMeta.status, 'failed', 'tx set to failed.') + assert.equal(updatedMeta.status, 'failed', 'tx set to failed.') done() }) }) -- cgit v1.2.3 From b87d10ab1dff539c4cb32ab32ccc1069598fb11b Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 5 Jul 2017 23:26:58 -0700 Subject: Bump changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7934dc77..3a471108c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Re-enable default token list. - Add origin header to dapp-bound requests to allow providers to throttle sites. +- Fix bug that could sometimes resubmit a transaction that had been stalled due to low balance after balance was restored. ## 3.8.2 2017-7-3 -- cgit v1.2.3 From 289fdfb7015d2e09306246b7a6871cdd40063118 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 6 Jul 2017 10:05:51 -0700 Subject: Version 3.8.3 --- CHANGELOG.md | 2 ++ app/manifest.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a471108c..3966ea1bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +## 3.8.3 2017-7-6 + - Re-enable default token list. - Add origin header to dapp-bound requests to allow providers to throttle sites. - Fix bug that could sometimes resubmit a transaction that had been stalled due to low balance after balance was restored. diff --git a/app/manifest.json b/app/manifest.json index 12ff6c2ea..aafc33e66 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.8.2", + "version": "3.8.3", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From 11b744bb87b4858dd2ef982c7d27e9751d8a09a1 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 6 Jul 2017 22:30:25 -0700 Subject: if an error happens during a tx publication set tx status to fail --- app/scripts/controllers/transactions.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 7946d10d1..8d3445c6f 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -240,7 +240,16 @@ module.exports = class TransactionController extends EventEmitter { this.updateTx(txMeta) this.txProviderUtils.publishTransaction(rawTx, (err, txHash) => { - if (err) return cb(err) + if (err) { + const errorMessage = err.message.toLowerCase() + if (errorMessage !== 'replacement transaction underpriced' + && errorMessage !== 'gas price too low to replace' + && !errorMessage.startsWith('known transaction') + ) { + this.setTxStatusFailed(txId) + } + return cb(err) + } this.setTxHash(txId, txHash) this.setTxStatusSubmitted(txId) cb() -- cgit v1.2.3 From 99556684096ed788ef01c909ff4cb4b0e61d3a05 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 6 Jul 2017 22:34:54 -0700 Subject: add comment --- app/scripts/controllers/transactions.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 8d3445c6f..14de786b7 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -241,11 +241,17 @@ module.exports = class TransactionController extends EventEmitter { this.txProviderUtils.publishTransaction(rawTx, (err, txHash) => { if (err) { - const errorMessage = err.message.toLowerCase() - if (errorMessage !== 'replacement transaction underpriced' - && errorMessage !== 'gas price too low to replace' - && !errorMessage.startsWith('known transaction') - ) { + const errorMessage = err.message.toLowerCase() + /* + Dont marked as failed if the error is because + it's a "known" transaction + "there is already a transaction with the same sender-nonce + but higher/same gas price" + */ + + if (errorMessage !== 'replacement transaction underpriced' // geth + && errorMessage !== 'gas price too low to replace' // parity + && !errorMessage.startsWith('known transaction')) { // geth this.setTxStatusFailed(txId) } return cb(err) -- cgit v1.2.3 From 8661989f516ae4455117e5158a97b4a6912a1980 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 7 Jul 2017 01:37:45 -0700 Subject: tx controller - move comments --- app/scripts/controllers/transactions.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 14de786b7..42baaaadc 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -249,9 +249,13 @@ module.exports = class TransactionController extends EventEmitter { but higher/same gas price" */ - if (errorMessage !== 'replacement transaction underpriced' // geth - && errorMessage !== 'gas price too low to replace' // parity - && !errorMessage.startsWith('known transaction')) { // geth + // geth + if (errorMessage !== 'replacement transaction underpriced' + // geth + && !errorMessage.startsWith('known transaction') + // parity + && errorMessage !== 'gas price too low to replace' + ) { this.setTxStatusFailed(txId) } return cb(err) -- cgit v1.2.3 From 34e2f6650d0db42b9f820d56a7acf9b72ca14da2 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 7 Jul 2017 01:50:48 -0700 Subject: tx controller - clean code --- app/scripts/controllers/transactions.js | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 42baaaadc..b855f910c 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -241,23 +241,24 @@ module.exports = class TransactionController extends EventEmitter { this.txProviderUtils.publishTransaction(rawTx, (err, txHash) => { if (err) { - const errorMessage = err.message.toLowerCase() /* - Dont marked as failed if the error is because - it's a "known" transaction + Dont marked as failed if the error is a "known" transaction warning "there is already a transaction with the same sender-nonce but higher/same gas price" */ - - // geth - if (errorMessage !== 'replacement transaction underpriced' - // geth - && !errorMessage.startsWith('known transaction') - // parity - && errorMessage !== 'gas price too low to replace' - ) { - this.setTxStatusFailed(txId) - } + const errorMessage = err.message.toLowerCase() + const isKnownTx = ( + // geth + errorMessage === 'replacement transaction underpriced' + || errorMessage.startsWith('known transaction') + // parity + || errorMessage === 'gas price too low to replace' + ) + // ignore resubmit warnings, return early + if (isKnownTx) return cb() + + // encountered unknown error, set status to failed + this.setTxStatusFailed(txId, err.message) return cb(err) } this.setTxHash(txId, txHash) -- cgit v1.2.3 From 092a9c9defd4d9bd2db7f969a8076c8b624d30bb Mon Sep 17 00:00:00 2001 From: frankiebee Date: Fri, 7 Jul 2017 03:05:39 -0700 Subject: fail transactions that fail in resubmit --- app/scripts/controllers/transactions.js | 47 +++++++++++++++------------------ 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index b855f910c..41d70194e 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -23,7 +23,10 @@ module.exports = class TransactionController extends EventEmitter { this.query = opts.ethQuery this.txProviderUtils = new TxProviderUtil(this.query) this.blockTracker.on('rawBlock', this.checkForTxInBlock.bind(this)) - this.blockTracker.on('latest', this.resubmitPendingTxs.bind(this)) + // this is a little messy but until ethstore has been either + // removed or redone this is to guard against the race condition + // where ethStore hasent been populated by the results yet + this.blockTracker.once('latest', () => this.blockTracker.on('latest', this.resubmitPendingTxs.bind(this))) this.blockTracker.on('sync', this.queryPendingTxs.bind(this)) this.signEthTx = opts.signTransaction this.nonceLock = Semaphore(1) @@ -240,27 +243,7 @@ module.exports = class TransactionController extends EventEmitter { this.updateTx(txMeta) this.txProviderUtils.publishTransaction(rawTx, (err, txHash) => { - if (err) { - /* - Dont marked as failed if the error is a "known" transaction warning - "there is already a transaction with the same sender-nonce - but higher/same gas price" - */ - const errorMessage = err.message.toLowerCase() - const isKnownTx = ( - // geth - errorMessage === 'replacement transaction underpriced' - || errorMessage.startsWith('known transaction') - // parity - || errorMessage === 'gas price too low to replace' - ) - // ignore resubmit warnings, return early - if (isKnownTx) return cb() - - // encountered unknown error, set status to failed - this.setTxStatusFailed(txId, err.message) - return cb(err) - } + if (err) return cb(err) this.setTxHash(txId, txHash) this.setTxStatusSubmitted(txId) cb() @@ -434,10 +417,24 @@ module.exports = class TransactionController extends EventEmitter { // only try resubmitting if their are transactions to resubmit if (!pending.length) return const resubmit = denodeify(this._resubmitTx.bind(this)) - Promise.all(pending.map(txMeta => resubmit(txMeta))) + pending.forEach((txMeta) => resubmit(txMeta) .catch((reason) => { - log.info('Problem resubmitting tx', reason) - }) + /* + Dont marked as failed if the error is a "known" transaction warning + "there is already a transaction with the same sender-nonce + but higher/same gas price" + */ + const errorMessage = reason.message.toLowerCase() + const isKnownTx = ( + // geth + errorMessage === 'replacement transaction underpriced' + || errorMessage.startsWith('known transaction') + // parity + || errorMessage === 'gas price too low to replace' + ) + // ignore resubmit warnings, return early + if (!isKnownTx) this.setTxStatusFailed(txMeta.id, reason.message) + })) } _resubmitTx (txMeta, cb) { -- cgit v1.2.3 From 04a0b949a2ce5b0335625236d9cac1dc7153dbf1 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 7 Jul 2017 11:24:33 -0700 Subject: Version 3.8.4 --- CHANGELOG.md | 4 ++++ app/manifest.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3966ea1bb..696d68345 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Current Master +## 3.8.4 2017-7-7 + +- Improve transaction resubmit logic to fail more eagerly when a user would expect it to. + ## 3.8.3 2017-7-6 - Re-enable default token list. diff --git a/app/manifest.json b/app/manifest.json index aafc33e66..d386e43aa 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.8.3", + "version": "3.8.4", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From ab8bae421e6b172f811694a597536c7d222043cd Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 7 Jul 2017 14:26:52 -0700 Subject: test - tx-controller - stub block-tracker method --- test/unit/tx-controller-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index c9eff5d01..9dfe0b982 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -18,7 +18,7 @@ describe('Transaction Controller', function () { txController = new TransactionController({ networkStore: new ObservableStore(currentNetworkId), txHistoryLimit: 10, - blockTracker: { getCurrentBlock: noop, on: noop }, + blockTracker: { getCurrentBlock: noop, on: noop, once: noop }, provider: { sendAsync: noop }, ethQuery: new EthQuery({ sendAsync: noop }), ethStore: { getState: noop }, -- cgit v1.2.3 From f5de16c91174fbbf208e5aef8f542d3bbbb3cb93 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 7 Jul 2017 14:32:03 -0700 Subject: test - tx controller - fix promise handling --- test/unit/tx-controller-test.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index 9dfe0b982..a5af13915 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -270,7 +270,7 @@ describe('Transaction Controller', function () { }) - it('does not overwrite set values', function (done) { + it('does not overwrite set values', function () { this.timeout(15000) const wrongValue = '0x05' @@ -289,9 +289,7 @@ describe('Transaction Controller', function () { const pubStub = sinon.stub(txController.txProviderUtils, 'publishTransaction') .callsArgWithAsync(1, null, originalValue) - txController.approveTransaction(txMeta.id).then((err) => { - assert.ifError(err, 'should not error') - + return txController.approveTransaction(txMeta.id).then(() => { const result = txController.getTx(txMeta.id) const params = result.txParams @@ -303,7 +301,6 @@ describe('Transaction Controller', function () { priceStub.restore() signStub.restore() pubStub.restore() - done() }) }) }) -- cgit v1.2.3 From 4fa999e4deae5451e73c126a80e541db6e3d0dc3 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 7 Jul 2017 19:02:34 -0700 Subject: tx controller - resubmit - recognize parity known hash message --- app/scripts/controllers/transactions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 41d70194e..bcce1bf8f 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -431,6 +431,7 @@ module.exports = class TransactionController extends EventEmitter { || errorMessage.startsWith('known transaction') // parity || errorMessage === 'gas price too low to replace' + || errorMessage === 'transaction with the same hash was already imported.' ) // ignore resubmit warnings, return early if (!isKnownTx) this.setTxStatusFailed(txMeta.id, reason.message) -- cgit v1.2.3 From c53aac398a29ff7cfe0efa6c844653693d78157b Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 7 Jul 2017 19:09:32 -0700 Subject: tx controller - correctly set error message on resubmit error --- app/scripts/controllers/transactions.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index bcce1bf8f..e1eaba232 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -417,14 +417,13 @@ module.exports = class TransactionController extends EventEmitter { // only try resubmitting if their are transactions to resubmit if (!pending.length) return const resubmit = denodeify(this._resubmitTx.bind(this)) - pending.forEach((txMeta) => resubmit(txMeta) - .catch((reason) => { + pending.forEach((txMeta) => resubmit(txMeta).catch((err) => { /* Dont marked as failed if the error is a "known" transaction warning "there is already a transaction with the same sender-nonce but higher/same gas price" */ - const errorMessage = reason.message.toLowerCase() + const errorMessage = err.message.toLowerCase() const isKnownTx = ( // geth errorMessage === 'replacement transaction underpriced' @@ -434,7 +433,12 @@ module.exports = class TransactionController extends EventEmitter { || errorMessage === 'transaction with the same hash was already imported.' ) // ignore resubmit warnings, return early - if (!isKnownTx) this.setTxStatusFailed(txMeta.id, reason.message) + if (isKnownTx) return + // encountered real error - transition to error state + this.setTxStatusFailed(txMeta.id, { + errCode: err.errCode || err, + message: err.message, + }) })) } -- cgit v1.2.3 From c425ad4ec71c6a5d5cc7af3d2a4d42c56c3ca125 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 7 Jul 2017 19:13:06 -0700 Subject: tx controller - resubmit - correctly set error on bad nonce/balance --- app/scripts/controllers/transactions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index e1eaba232..18bb245de 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -453,7 +453,7 @@ module.exports = class TransactionController extends EventEmitter { // if the value of the transaction is greater then the balance, fail. if (gtBalance) { const message = 'Insufficient balance.' - this.setTxStatusFailed(txMeta.id, message) + this.setTxStatusFailed(txMeta.id, { message }) cb() return log.error(message) } @@ -461,7 +461,7 @@ module.exports = class TransactionController extends EventEmitter { // if the nonce of the transaction is lower then the accounts nonce, fail. if (txNonce < nonce) { const message = 'Invalid nonce.' - this.setTxStatusFailed(txMeta.id, message) + this.setTxStatusFailed(txMeta.id, { message }) cb() return log.error(message) } -- cgit v1.2.3 From 512b6cae81ee0e71b567a72418ac224804dfc3e6 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 7 Jul 2017 19:31:27 -0700 Subject: migration 16 - move resubmit warning back to submitted state --- app/scripts/migrations/016.js | 41 +++++++++++++++++++++++++++++++++++++++++ app/scripts/migrations/index.js | 1 + 2 files changed, 42 insertions(+) create mode 100644 app/scripts/migrations/016.js diff --git a/app/scripts/migrations/016.js b/app/scripts/migrations/016.js new file mode 100644 index 000000000..4fc534f1c --- /dev/null +++ b/app/scripts/migrations/016.js @@ -0,0 +1,41 @@ +const version = 16 + +/* + +This migration sets transactions with the 'Gave up submitting tx.' err message +to a 'failed' stated + +*/ + +const clone = require('clone') + +module.exports = { + version, + + migrate: function (originalVersionedData) { + const versionedData = clone(originalVersionedData) + versionedData.meta.version = version + try { + const state = versionedData.data + const newState = transformState(state) + versionedData.data = newState + } catch (err) { + console.warn(`MetaMask Migration #${version}` + err.stack) + } + return Promise.resolve(versionedData) + }, +} + +function transformState (state) { + const newState = state + const transactions = newState.TransactionController.transactions + newState.TransactionController.transactions = transactions.map((txMeta) => { + if (!txMeta.err) return txMeta + if (txMeta.err === 'transaction with the same hash was already imported.') { + txMeta.status = 'submitted' + delete txMeta.err + } + return txMeta + }) + return newState +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 651ee6a9c..a4f9c7c4d 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -26,4 +26,5 @@ module.exports = [ require('./013'), require('./014'), require('./015'), + require('./016'), ] -- cgit v1.2.3 From de967d2dfd2119d2468263ecb9646fd0a92df195 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 7 Jul 2017 20:05:03 -0700 Subject: 3.8.5 --- CHANGELOG.md | 4 ++++ app/manifest.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 696d68345..2c61c31b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Current Master +## 3.8.5 2017-7-7 + +- Fix transaction resubmit logic to fail slightly less eagerly. + ## 3.8.4 2017-7-7 - Improve transaction resubmit logic to fail more eagerly when a user would expect it to. diff --git a/app/manifest.json b/app/manifest.json index d386e43aa..f3a1ebeff 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.8.4", + "version": "3.8.5", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From 11d57adc5c6fa124cbb83a907a55a0aa733bb1cc Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 11 Jul 2017 11:57:42 -0700 Subject: add "Gateway timeout" to ignored errors when resubmiting and use .includes over .startsWith --- app/scripts/controllers/transactions.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 18bb245de..933c079d2 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -427,10 +427,12 @@ module.exports = class TransactionController extends EventEmitter { const isKnownTx = ( // geth errorMessage === 'replacement transaction underpriced' - || errorMessage.startsWith('known transaction') + || errorMessage.includes('known transaction') // parity || errorMessage === 'gas price too low to replace' || errorMessage === 'transaction with the same hash was already imported.' + // other + || errorMessage.includes('gateway timeout') ) // ignore resubmit warnings, return early if (isKnownTx) return -- cgit v1.2.3 From d97c6533b87b0a9dd6937c1ca57ec05129ac619b Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 11 Jul 2017 11:59:56 -0700 Subject: Remove local nonce error setting. --- CHANGELOG.md | 2 ++ app/scripts/controllers/transactions.js | 8 -------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 696d68345..f53bdead5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- No longer validate nonce client-side in retry loop. + ## 3.8.4 2017-7-7 - Improve transaction resubmit logic to fail more eagerly when a user would expect it to. diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 18bb245de..02487c385 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -458,14 +458,6 @@ module.exports = class TransactionController extends EventEmitter { return log.error(message) } - // if the nonce of the transaction is lower then the accounts nonce, fail. - if (txNonce < nonce) { - const message = 'Invalid nonce.' - this.setTxStatusFailed(txMeta.id, { message }) - cb() - return log.error(message) - } - // Only auto-submit already-signed txs: if (!('rawTx' in txMeta)) return cb() -- cgit v1.2.3 From 611338c4e0b6de670f1a3ac6a0f1ddd3a2c063f7 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 11 Jul 2017 12:01:59 -0700 Subject: use .includes --- app/scripts/controllers/transactions.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 933c079d2..c0d4841a9 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -426,11 +426,11 @@ module.exports = class TransactionController extends EventEmitter { const errorMessage = err.message.toLowerCase() const isKnownTx = ( // geth - errorMessage === 'replacement transaction underpriced' + errorMessage.includes('replacement transaction underpriced') || errorMessage.includes('known transaction') // parity - || errorMessage === 'gas price too low to replace' - || errorMessage === 'transaction with the same hash was already imported.' + || errorMessage.includes('gas price too low to replace') + || errorMessage.includes('transaction with the same hash was already imported') // other || errorMessage.includes('gateway timeout') ) -- cgit v1.2.3 From c121ac21ec3bed0381e36de7ead1b583a3da148c Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 11 Jul 2017 12:16:08 -0700 Subject: remove irrelevan code --- app/scripts/controllers/transactions.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 2b40f9456..14b423d5d 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -30,7 +30,6 @@ module.exports = class TransactionController extends EventEmitter { from: address, status: 'submitted', err: undefined, - ignore: undefined, }) }, }) -- cgit v1.2.3 From c7b9e3fb1878cebbab26d5343cc18084a601c6bb Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 11 Jul 2017 12:18:07 -0700 Subject: Improve insufficient balance checking in retry loop --- CHANGELOG.md | 1 + app/scripts/controllers/transactions.js | 5 +---- app/scripts/lib/tx-utils.js | 9 ++++++++ test/unit/tx-utils-test.js | 38 +++++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f53bdead5..395454b41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Current Master - No longer validate nonce client-side in retry loop. +- Fix bug where insufficient balance error was sometimes shown on successful transactions. ## 3.8.4 2017-7-7 diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 02487c385..ca379a7ff 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -445,13 +445,10 @@ module.exports = class TransactionController extends EventEmitter { _resubmitTx (txMeta, cb) { const address = txMeta.txParams.from const balance = this.ethStore.getState().accounts[address].balance - const nonce = Number.parseInt(this.ethStore.getState().accounts[address].nonce) - const txNonce = Number.parseInt(txMeta.txParams.nonce) - const gtBalance = Number.parseInt(txMeta.txParams.value) > Number.parseInt(balance) if (!('retryCount' in txMeta)) txMeta.retryCount = 0 // if the value of the transaction is greater then the balance, fail. - if (gtBalance) { + if (!this.txProviderUtils.sufficientBalance(txMeta.txParams, balance)) { const message = 'Insufficient balance.' this.setTxStatusFailed(txMeta.id, { message }) cb() diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js index 149d93102..4e780fcc0 100644 --- a/app/scripts/lib/tx-utils.js +++ b/app/scripts/lib/tx-utils.js @@ -118,6 +118,15 @@ module.exports = class txProviderUtils { } } + sufficientBalance (tx, hexBalance) { + const balance = hexToBn(hexBalance) + const value = hexToBn(tx.value) + const gasLimit = hexToBn(tx.gas) + const gasPrice = hexToBn(tx.gasPrice) + + const maxCost = value.add(gasLimit.mul(gasPrice)) + return balance.gte(maxCost) + } } diff --git a/test/unit/tx-utils-test.js b/test/unit/tx-utils-test.js index 7ace1f587..a43bcfb35 100644 --- a/test/unit/tx-utils-test.js +++ b/test/unit/tx-utils-test.js @@ -16,6 +16,44 @@ describe('txUtils', function () { })) }) + describe('#sufficientBalance', function () { + it('returns true if max tx cost is equal to balance.', function () { + const tx = { + 'value': '0x1', + 'gas': '0x2', + 'gasPrice': '0x3', + } + const balance = '0x8' + + const result = txUtils.sufficientBalance(tx, balance) + assert.ok(result, 'sufficient balance found.') + }) + + it('returns true if max tx cost is less than balance.', function () { + const tx = { + 'value': '0x1', + 'gas': '0x2', + 'gasPrice': '0x3', + } + const balance = '0x9' + + const result = txUtils.sufficientBalance(tx, balance) + assert.ok(result, 'sufficient balance found.') + }) + + it('returns false if max tx cost is more than balance.', function () { + const tx = { + 'value': '0x1', + 'gas': '0x2', + 'gasPrice': '0x3', + } + const balance = '0x6' + + const result = txUtils.sufficientBalance(tx, balance) + assert.ok(!result, 'insufficient balance found.') + }) + }) + describe('chain Id', function () { it('prepares a transaction with the provided chainId', function () { const txParams = { -- cgit v1.2.3 From 6587f6eabda08629b7fce431820f92ab275bbf75 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 11 Jul 2017 12:43:15 -0700 Subject: deps - bump prov-eng for retry on gateway timeout --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 27fe7a84a..47f5db15d 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "valid-url": "^1.0.9", "vreme": "^3.0.2", "web3": "0.19.1", - "web3-provider-engine": "^13.1.1", + "web3-provider-engine": "^13.2.0", "web3-stream-provider": "^3.0.1", "xtend": "^4.0.1" }, -- cgit v1.2.3 From 231ad48564758895bace7b0e750cdfa5577128f8 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 11 Jul 2017 12:52:56 -0700 Subject: Use txParams --- app/scripts/lib/tx-utils.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js index 4e780fcc0..aa0cb624f 100644 --- a/app/scripts/lib/tx-utils.js +++ b/app/scripts/lib/tx-utils.js @@ -118,11 +118,11 @@ module.exports = class txProviderUtils { } } - sufficientBalance (tx, hexBalance) { + sufficientBalance (txParams, hexBalance) { const balance = hexToBn(hexBalance) - const value = hexToBn(tx.value) - const gasLimit = hexToBn(tx.gas) - const gasPrice = hexToBn(tx.gasPrice) + const value = hexToBn(txParams.value) + const gasLimit = hexToBn(txParams.gas) + const gasPrice = hexToBn(txParams.gasPrice) const maxCost = value.add(gasLimit.mul(gasPrice)) return balance.gte(maxCost) -- cgit v1.2.3 From 9f46984fee8f4fa8268ac9bd70081c58708ac8ea Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 11 Jul 2017 14:17:47 -0700 Subject: metamask - on rpc err show whole error body --- app/scripts/metamask-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 73093dfad..0e7ccbd66 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -367,7 +367,7 @@ module.exports = class MetamaskController extends EventEmitter { function onResponse (err, request, response) { if (err) return console.error(err) if (response.error) { - console.error('Error in RPC response:\n', response.error) + console.error('Error in RPC response:\n', response) } if (request.isMetamaskInternal) return log.info(`RPC (${originDomain}):`, request, '->', response) -- cgit v1.2.3 From 0cc60fda8f14174b978e49a9b1e97e0accbce31d Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 11 Jul 2017 14:18:09 -0700 Subject: deps - bump prov-eng for fetch retry --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 47f5db15d..fb278b398 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "valid-url": "^1.0.9", "vreme": "^3.0.2", "web3": "0.19.1", - "web3-provider-engine": "^13.2.0", + "web3-provider-engine": "^13.2.7", "web3-stream-provider": "^3.0.1", "xtend": "^4.0.1" }, -- cgit v1.2.3 From 23d44e53996c04fb0e6fb2e1c2ca90728397aa0c Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 11 Jul 2017 15:30:36 -0700 Subject: tests - disable infura test --- test/unit/infura-controller-test.js | 66 ++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/test/unit/infura-controller-test.js b/test/unit/infura-controller-test.js index 7a2a114f9..912867764 100644 --- a/test/unit/infura-controller-test.js +++ b/test/unit/infura-controller-test.js @@ -1,34 +1,34 @@ // polyfill fetch -global.fetch = function () {return Promise.resolve({ - json: () => { return Promise.resolve({"mainnet": "ok", "ropsten": "degraded", "kovan": "down", "rinkeby": "ok"}) }, - }) -} -const assert = require('assert') -const InfuraController = require('../../app/scripts/controllers/infura') - -describe('infura-controller', function () { - var infuraController - - beforeEach(function () { - infuraController = new InfuraController() - }) - - describe('network status queries', function () { - describe('#checkInfuraNetworkStatus', function () { - it('should return an object reflecting the network statuses', function (done) { - this.timeout(15000) - infuraController.checkInfuraNetworkStatus() - .then(() => { - const networkStatus = infuraController.store.getState().infuraNetworkStatus - assert.equal(Object.keys(networkStatus).length, 4) - assert.equal(networkStatus.mainnet, 'ok') - assert.equal(networkStatus.ropsten, 'degraded') - assert.equal(networkStatus.kovan, 'down') - }) - .then(() => done()) - .catch(done) - - }) - }) - }) -}) +// global.fetch = function () {return Promise.resolve({ +// json: () => { return Promise.resolve({"mainnet": "ok", "ropsten": "degraded", "kovan": "down", "rinkeby": "ok"}) }, +// }) +// } +// const assert = require('assert') +// const InfuraController = require('../../app/scripts/controllers/infura') +// +// describe('infura-controller', function () { +// var infuraController +// +// beforeEach(function () { +// infuraController = new InfuraController() +// }) +// +// describe('network status queries', function () { +// describe('#checkInfuraNetworkStatus', function () { +// it('should return an object reflecting the network statuses', function (done) { +// this.timeout(15000) +// infuraController.checkInfuraNetworkStatus() +// .then(() => { +// const networkStatus = infuraController.store.getState().infuraNetworkStatus +// assert.equal(Object.keys(networkStatus).length, 4) +// assert.equal(networkStatus.mainnet, 'ok') +// assert.equal(networkStatus.ropsten, 'degraded') +// assert.equal(networkStatus.kovan, 'down') +// }) +// .then(() => done()) +// .catch(done) +// +// }) +// }) +// }) +// }) -- cgit v1.2.3 From 1448090ec76a5ab9274b27e4c71aef9970ca0214 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 11 Jul 2017 15:33:12 -0700 Subject: deps - bump prov-eng --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fb278b398..10b175975 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "valid-url": "^1.0.9", "vreme": "^3.0.2", "web3": "0.19.1", - "web3-provider-engine": "^13.2.7", + "web3-provider-engine": "^13.2.8", "web3-stream-provider": "^3.0.1", "xtend": "^4.0.1" }, -- cgit v1.2.3 From eddc8cfee7fa161b0901483998fce868d3cc4077 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 11 Jul 2017 16:00:20 -0700 Subject: Version 3.8.6 --- CHANGELOG.md | 3 +++ app/manifest.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb3fcfb83..535fa32f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Current Master +## 3.8.6 2017-7-11 + +- Make transaction resubmission more resilient. - No longer validate nonce client-side in retry loop. - Fix bug where insufficient balance error was sometimes shown on successful transactions. diff --git a/app/manifest.json b/app/manifest.json index f3a1ebeff..2b1f6d69f 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.8.5", + "version": "3.8.6", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From 52b92fbe40e221c53e1c93a2e998c65833c2334d Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 12 Jul 2017 13:09:20 -0700 Subject: Add first version of phishing site warning Links to my own blacklist for now, since I added a package.json for easy importing. We can point at the main 408H repository once this is merged: https://github.com/409H/EtherAddressLookup/pull/24 Redirects detected phishing sites [here](https://metamask.io/phishing.html). --- app/manifest.json | 6 ++++++ app/scripts/blacklister.js | 13 +++++++++++++ gulpfile.js | 1 + package.json | 1 + 4 files changed, 21 insertions(+) create mode 100644 app/scripts/blacklister.js diff --git a/app/manifest.json b/app/manifest.json index f3a1ebeff..ac6364059 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -52,6 +52,12 @@ ], "run_at": "document_start", "all_frames": true + }, + { + "run_at": "document_end", + "matches": ["http://*/*", "https://*/*"], + "js": ["scripts/blacklister.js"], + "css": ["css/blacklister.css"] } ], "permissions": [ diff --git a/app/scripts/blacklister.js b/app/scripts/blacklister.js new file mode 100644 index 000000000..a45265a75 --- /dev/null +++ b/app/scripts/blacklister.js @@ -0,0 +1,13 @@ +const blacklistedDomains = require('etheraddresslookup/blacklists/domains.json') + +function detectBlacklistedDomain() { + var strCurrentTab = window.location.hostname + if (blacklistedDomains && blacklistedDomains.includes(strCurrentTab)) { + window.location.href = 'https://metamask.io/phishing.html' + } +} + +window.addEventListener('load', function() { + detectBlacklistedDomain() +}) + diff --git a/gulpfile.js b/gulpfile.js index cc723704a..53de7a7d9 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -172,6 +172,7 @@ gulp.task('default', ['lint'], function () { const jsFiles = [ 'inpage', 'contentscript', + 'blacklister', 'background', 'popup', ] diff --git a/package.json b/package.json index 10b175975..87312b8d1 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "eth-sig-util": "^1.1.1", "eth-simple-keyring": "^1.1.1", "eth-token-tracker": "^1.1.2", + "etheraddresslookup": "github:flyswatter/EtherAddressLookup#AddPackageJson", "ethereumjs-tx": "^1.3.0", "ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", -- cgit v1.2.3 From 2c5b9da06a9b6e6455361136390784e3e774b5aa Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 12 Jul 2017 13:14:18 -0700 Subject: Bump changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb3fcfb83..02bebbb4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Current Master +- Now detects and blocks known phishing sites. - No longer validate nonce client-side in retry loop. - Fix bug where insufficient balance error was sometimes shown on successful transactions. -- cgit v1.2.3 From 0079126b7d46f0e20592117563e543531b96c36e Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 12 Jul 2017 14:33:03 -0700 Subject: Point blacklist at main repository --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 87312b8d1..54addd51c 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "eth-sig-util": "^1.1.1", "eth-simple-keyring": "^1.1.1", "eth-token-tracker": "^1.1.2", - "etheraddresslookup": "github:flyswatter/EtherAddressLookup#AddPackageJson", + "etheraddresslookup": "github:407H/EtherAddressLookup", "ethereumjs-tx": "^1.3.0", "ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", -- cgit v1.2.3 From da35f6744e3201bb75b896d3127d4f30d7e4d789 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 12 Jul 2017 15:06:49 -0700 Subject: use new nodeify --- app/scripts/lib/nodeify.js | 27 ++++++--------------------- app/scripts/metamask-controller.js | 38 +++++++++++++++----------------------- 2 files changed, 21 insertions(+), 44 deletions(-) diff --git a/app/scripts/lib/nodeify.js b/app/scripts/lib/nodeify.js index 51d89a8fb..4e111c8b2 100644 --- a/app/scripts/lib/nodeify.js +++ b/app/scripts/lib/nodeify.js @@ -1,24 +1,9 @@ -module.exports = function (promiseFn) { - return function () { - var args = [] - for (var i = 0; i < arguments.length - 1; i++) { - args.push(arguments[i]) - } - var cb = arguments[arguments.length - 1] +const promiseToCallback = require('promise-to-callback'); - const nodeified = promiseFn.apply(this, args) - - if (!nodeified) { - const methodName = String(promiseFn).split('(')[0] - throw new Error(`The ${methodName} did not return a Promise, but was nodeified.`) - } - nodeified.then(function (result) { - cb(null, result) - }) - .catch(function (reason) { - cb(reason) - }) - - return nodeified +module.exports = function(fn, context) { + return function(){ + const args = [].slice.call(arguments) + const callback = args.pop() + promiseToCallback(fn.apply(context, args))(callback) } } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 573594b39..f1f21b29b 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -294,34 +294,33 @@ module.exports = class MetamaskController extends EventEmitter { submitPassword: this.submitPassword.bind(this), // PreferencesController - setSelectedAddress: nodeify(preferencesController.setSelectedAddress).bind(preferencesController), - addToken: nodeify(preferencesController.addToken).bind(preferencesController), - setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab).bind(preferencesController), - setDefaultRpc: nodeify(this.setDefaultRpc).bind(this), - setCustomRpc: nodeify(this.setCustomRpc).bind(this), + setSelectedAddress: nodeify(preferencesController.setSelectedAddress, preferencesController), + addToken: nodeify(preferencesController.addToken, preferencesController), + setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController), + setDefaultRpc: nodeify(this.setDefaultRpc, this), + setCustomRpc: nodeify(this.setCustomRpc, this), // AddressController - setAddressBook: nodeify(addressBookController.setAddressBook).bind(addressBookController), + setAddressBook: nodeify(addressBookController.setAddressBook, addressBookController), // KeyringController - setLocked: nodeify(keyringController.setLocked).bind(keyringController), - createNewVaultAndKeychain: nodeify(keyringController.createNewVaultAndKeychain).bind(keyringController), - createNewVaultAndRestore: nodeify(keyringController.createNewVaultAndRestore).bind(keyringController), - addNewKeyring: nodeify(keyringController.addNewKeyring).bind(keyringController), - saveAccountLabel: nodeify(keyringController.saveAccountLabel).bind(keyringController), - exportAccount: nodeify(keyringController.exportAccount).bind(keyringController), + setLocked: nodeify(keyringController.setLocked, keyringController), + createNewVaultAndKeychain: nodeify(keyringController.createNewVaultAndKeychain, keyringController), + createNewVaultAndRestore: nodeify(keyringController.createNewVaultAndRestore, keyringController), + addNewKeyring: nodeify(keyringController.addNewKeyring, keyringController), + saveAccountLabel: nodeify(keyringController.saveAccountLabel, keyringController), + exportAccount: nodeify(keyringController.exportAccount, keyringController), // txController - approveTransaction: nodeify(txController.approveTransaction).bind(txController), cancelTransaction: txController.cancelTransaction.bind(txController), - updateAndApproveTransaction: this.updateAndApproveTx.bind(this), + updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController), // messageManager - signMessage: nodeify(this.signMessage).bind(this), + signMessage: nodeify(this.signMessage, this), cancelMessage: this.cancelMessage.bind(this), // personalMessageManager - signPersonalMessage: nodeify(this.signPersonalMessage).bind(this), + signPersonalMessage: nodeify(this.signPersonalMessage, this), cancelPersonalMessage: this.cancelPersonalMessage.bind(this), // notices @@ -502,13 +501,6 @@ module.exports = class MetamaskController extends EventEmitter { }) } - updateAndApproveTx (txMeta, cb) { - log.debug(`MetaMaskController - updateAndApproveTx: ${JSON.stringify(txMeta)}`) - const txController = this.txController - txController.updateTx(txMeta) - txController.approveTransaction(txMeta.id, cb) - } - signMessage (msgParams, cb) { log.info('MetaMaskController - signMessage') const msgId = msgParams.metamaskId -- cgit v1.2.3 From bd26ec46aa8f2d144939573a8c427e2a7743d25c Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 12 Jul 2017 15:07:56 -0700 Subject: mv updateAndApproveTx to txController --- app/scripts/controllers/transactions.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 45c6fb25a..a2842ae44 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -183,7 +183,7 @@ module.exports = class TransactionController extends EventEmitter { }, {}) } - async approveTransaction (txId, cb = warn) { + async approveTransaction (txId) { let nonceLock try { // approve @@ -199,7 +199,6 @@ module.exports = class TransactionController extends EventEmitter { await this.publishTransaction(txId, rawTx) // must set transaction to submitted/failed before releasing lock nonceLock.releaseLock() - cb() } catch (err) { this.setTxStatusFailed(txId, { errCode: err.errCode || err, @@ -208,7 +207,7 @@ module.exports = class TransactionController extends EventEmitter { // must set transaction to submitted/failed before releasing lock if (nonceLock) nonceLock.releaseLock() // continue with error chain - cb(err) + throw err } } @@ -217,6 +216,11 @@ module.exports = class TransactionController extends EventEmitter { cb() } + async updateAndApproveTransaction (txMeta) { + this.updateTx(txMeta) + await this.approveTransaction(txMeta.id) + } + getChainId () { const networkState = this.networkStore.getState() const getChainId = parseInt(networkState) -- cgit v1.2.3 From ed272dcbc082ebf9abbd7f17da1386163013c023 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 12 Jul 2017 15:09:01 -0700 Subject: Bump node version --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 1f018ac24..66eed17d7 100644 --- a/circle.yml +++ b/circle.yml @@ -1,6 +1,6 @@ machine: node: - version: 8.0.0 + version: 8.1.4 dependencies: pre: - "npm i -g testem" -- cgit v1.2.3 From aeefcbd75bcf419833b9cc5b734e2d86f47ba6d1 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 12 Jul 2017 15:10:52 -0700 Subject: Fix test to match behavior --- app/scripts/lib/nodeify.js | 2 +- test/unit/nodeify-test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/lib/nodeify.js b/app/scripts/lib/nodeify.js index 4e111c8b2..299bfe624 100644 --- a/app/scripts/lib/nodeify.js +++ b/app/scripts/lib/nodeify.js @@ -1,4 +1,4 @@ -const promiseToCallback = require('promise-to-callback'); +const promiseToCallback = require('promise-to-callback') module.exports = function(fn, context) { return function(){ diff --git a/test/unit/nodeify-test.js b/test/unit/nodeify-test.js index 5aed758fa..06241334d 100644 --- a/test/unit/nodeify-test.js +++ b/test/unit/nodeify-test.js @@ -11,7 +11,7 @@ describe('nodeify', function () { } it('should retain original context', function (done) { - var nodified = nodeify(obj.promiseFunc).bind(obj) + var nodified = nodeify(obj.promiseFunc, obj) nodified('baz', function (err, res) { assert.equal(res, 'barbaz') done() -- cgit v1.2.3 From aec813eace6db96984ccbb29d8b98d60097b22e2 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 12 Jul 2017 15:15:19 -0700 Subject: Correct github link --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 54addd51c..16f567dd2 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "eth-sig-util": "^1.1.1", "eth-simple-keyring": "^1.1.1", "eth-token-tracker": "^1.1.2", - "etheraddresslookup": "github:407H/EtherAddressLookup", + "etheraddresslookup": "github:409H/EtherAddressLookup", "ethereumjs-tx": "^1.3.0", "ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", -- cgit v1.2.3 From 76a2a59ec54cb20cd482adf724815100916d5d3e Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 12 Jul 2017 15:24:59 -0700 Subject: Refresh blacklist before dist --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 16f567dd2..a1b1afae1 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "start": "npm run dev", "dev": "gulp dev --debug", "disc": "gulp disc --debug", - "dist": "npm install && gulp dist", + "dist": "rm -rf node_modules/etheraddresslookup && npm install && gulp dist", "test": "npm run lint && npm run test-unit && npm run test-integration", "test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"", "test-integration": "npm run buildMock && npm run buildCiUnits && testem ci -P 2", -- cgit v1.2.3 From ebe76664266d3c712ec14ac4aef7fc48b3f1b5c3 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 12 Jul 2017 15:31:29 -0700 Subject: Update eth-contract-metadata on build --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 10b175975..8a394ad75 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "start": "npm run dev", "dev": "gulp dev --debug", "disc": "gulp disc --debug", - "dist": "npm install && gulp dist", + "clear": "rm -rf node_modules/eth-contract-metadata", + "dist": "npm run clear && npm install && gulp dist", "test": "npm run lint && npm run test-unit && npm run test-integration", "test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"", "test-integration": "npm run buildMock && npm run buildCiUnits && testem ci -P 2", @@ -62,7 +63,7 @@ "end-of-stream": "^1.1.0", "ensnare": "^1.0.0", "eth-bin-to-ops": "^1.0.1", - "eth-contract-metadata": "^1.1.3", + "eth-contract-metadata": "^1.1.4", "eth-hd-keyring": "^1.1.1", "eth-query": "^2.1.2", "eth-sig-util": "^1.1.1", -- cgit v1.2.3 From 414b97921982b21ab5618c6f88192d4a984abf45 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 12 Jul 2017 16:38:56 -0700 Subject: Version 3.9.0 --- CHANGELOG.md | 2 ++ app/manifest.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03f18201a..ee8589735 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +## 3.9.0 2017-7-12 + - Now detects and blocks known phishing sites. ## 3.8.6 2017-7-11 diff --git a/app/manifest.json b/app/manifest.json index ed1d68190..30de25b6d 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.8.6", + "version": "3.9.0", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From 1357526dfc11caa7993e45895e52e47f27116fe5 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 12 Jul 2017 16:42:24 -0700 Subject: Remove css reference --- app/manifest.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/manifest.json b/app/manifest.json index 30de25b6d..269e88e64 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -56,8 +56,7 @@ { "run_at": "document_end", "matches": ["http://*/*", "https://*/*"], - "js": ["scripts/blacklister.js"], - "css": ["css/blacklister.css"] + "js": ["scripts/blacklister.js"] } ], "permissions": [ -- cgit v1.2.3 From 27cb02bc5868673af794e0ad597fea87b2ddf175 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 12 Jul 2017 18:54:01 -0700 Subject: add "nonce too low" to the ignored errs list for tx retrys --- app/scripts/controllers/transactions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 43735a691..4d037ce98 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -433,6 +433,7 @@ module.exports = class TransactionController extends EventEmitter { || errorMessage.includes('transaction with the same hash was already imported') // other || errorMessage.includes('gateway timeout') + || errorMessage.includes('nonce too low') ) // ignore resubmit warnings, return early if (isKnownTx) return -- cgit v1.2.3 From de0cd6e66375f690965f60c58e70900660f565f2 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 12 Jul 2017 18:56:50 -0700 Subject: write a migration for resubmit tx's to get put back into a submitted state --- app/scripts/migrations/017.js | 40 ++++++++++++++++++++++++++++++++++++++++ app/scripts/migrations/index.js | 1 + 2 files changed, 41 insertions(+) create mode 100644 app/scripts/migrations/017.js diff --git a/app/scripts/migrations/017.js b/app/scripts/migrations/017.js new file mode 100644 index 000000000..c9898ead7 --- /dev/null +++ b/app/scripts/migrations/017.js @@ -0,0 +1,40 @@ +const version = 17 + +/* + +This migration sets transactions who were retried and marked as failed to submitted + +*/ + +const clone = require('clone') + +module.exports = { + version, + + migrate: function (originalVersionedData) { + const versionedData = clone(originalVersionedData) + versionedData.meta.version = version + try { + const state = versionedData.data + const newState = transformState(state) + versionedData.data = newState + } catch (err) { + console.warn(`MetaMask Migration #${version}` + err.stack) + } + return Promise.resolve(versionedData) + }, +} + +function transformState (state) { + const newState = state + const transactions = newState.TransactionController.transactions + newState.TransactionController.transactions = transactions.map((txMeta) => { + if (!txMeta.status === 'failed') return txMeta + if (txMeta.retryCount > 0) { + txMeta.status = 'submitted' + delete txMeta.err + } + return txMeta + }) + return newState +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index a4f9c7c4d..f4c87499f 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -27,4 +27,5 @@ module.exports = [ require('./014'), require('./015'), require('./016'), + require('./017'), ] -- cgit v1.2.3 From 6086bcdf0d1346633bc41fde88bb315ead7f9227 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 12 Jul 2017 20:01:07 -0700 Subject: limit the range for retryCount --- app/scripts/migrations/017.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/migrations/017.js b/app/scripts/migrations/017.js index c9898ead7..24959cd3a 100644 --- a/app/scripts/migrations/017.js +++ b/app/scripts/migrations/017.js @@ -30,7 +30,7 @@ function transformState (state) { const transactions = newState.TransactionController.transactions newState.TransactionController.transactions = transactions.map((txMeta) => { if (!txMeta.status === 'failed') return txMeta - if (txMeta.retryCount > 0) { + if (txMeta.retryCount > 0 && txMeta.retryCount < 2) { txMeta.status = 'submitted' delete txMeta.err } -- cgit v1.2.3 From a49e5e158a03d4c2d89ddbeba853325d6f35cf29 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Thu, 13 Jul 2017 00:40:00 -0700 Subject: Implement redesigned dropdown --- ui/responsive/app/components/dropdown.js | 71 ++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 ui/responsive/app/components/dropdown.js diff --git a/ui/responsive/app/components/dropdown.js b/ui/responsive/app/components/dropdown.js new file mode 100644 index 000000000..d7f0e158a --- /dev/null +++ b/ui/responsive/app/components/dropdown.js @@ -0,0 +1,71 @@ +const Component = require('react').Component; +const PropTypes = require('react').PropTypes; +const h = require('react-hyperscript'); +const MenuDroppo = require('menu-droppo'); + +class Dropdown extends Component { + render() { + const { isOpen, onClickOutside, style, children } = this.props; + + return h( + MenuDroppo, + { + isOpen, + zIndex: 11, + onClickOutside, + style, + innerStyle: { + borderRadius: '4px', + padding: '8px 16px', + background: 'rgba(0, 0, 0, 0.8)', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + }, + }, + children, + ); + } +} + +Dropdown.propTypes = { + isOpen: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, + children: PropTypes.node, + style: PropTypes.object.isRequired, +} + +class DropdownMenuItem extends Component { + render() { + const { onClick, closeMenu, children } = this.props; + + return h( + 'li', + { + onClick, + closeMenu, + style: { + listStyle: 'none', + padding: '8px 0px 8px 0px', + fontSize: '12px', + fontStyle: 'normal', + fontFamily: 'Montserrat Regular', + color: 'rgb(185, 185, 185)', + cursor: 'pointer', + display: 'flex', + justifyContent: 'flex-start', + }, + }, + children + ); + } +} + +DropdownMenuItem.propTypes = { + closeMenu: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, + children: PropTypes.node, +}; + +module.exports = { + Dropdown, + DropdownMenuItem, +}; \ No newline at end of file -- cgit v1.2.3 From 1507da139d8c4f54451c7ecdd02a589332c97c8e Mon Sep 17 00:00:00 2001 From: sdtsui Date: Thu, 13 Jul 2017 00:40:22 -0700 Subject: Add tests for new dropdown component --- package.json | 1 + test/unit/responsive/components/dropdown-test.js | 51 ++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 test/unit/responsive/components/dropdown-test.js diff --git a/package.json b/package.json index 27fe7a84a..a587a4507 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "dist": "npm install && gulp dist", "test": "npm run lint && npm run test-unit && npm run test-integration", "test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"", + "test-responsive": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/responsive/**/*.js\"", "test-integration": "npm run buildMock && npm run buildCiUnits && testem ci -P 2", "lint": "gulp lint", "buildCiUnits": "node test/integration/index.js", diff --git a/test/unit/responsive/components/dropdown-test.js b/test/unit/responsive/components/dropdown-test.js new file mode 100644 index 000000000..feadc792e --- /dev/null +++ b/test/unit/responsive/components/dropdown-test.js @@ -0,0 +1,51 @@ +var assert = require('assert'); + +const additions = require('react-testutils-additions'); +const h = require('react-hyperscript'); +const ReactTestUtils = require('react-addons-test-utils'); +const sinon = require('sinon'); +const path = require('path'); +const Dropdown = require(path.join(__dirname, '..', '..', '..', '..', 'ui', 'responsive', 'app', 'components', 'dropdown.js')).Dropdown; +const DropdownMenuItem = require(path.join(__dirname, '..', '..', '..', '..', 'ui', 'responsive', 'app', 'components', 'dropdown.js')).DropdownMenuItem; + +describe('Dropdown components', function () { + it('can render two items', function () { + const renderer = ReactTestUtils.createRenderer() + + const onClickOutside = sinon.spy(); + const closeMenu = sinon.spy(); + const onClick = sinon.spy(); + + const dropdownComponent = h(Dropdown, { + isOpen: true, + zIndex: 11, + onClickOutside, + style: { + position: 'absolute', + right: 0, + top: '36px', + }, + innerStyle: {}, + }, [ // DROP MENU ITEMS + h('style', ` + .drop-menu-item:hover { background:rgb(235, 235, 235); } + .drop-menu-item i { margin: 11px; } + `), + + h(DropdownMenuItem, { + closeMenu, + onClick, + }, 'Item 1'), + + h(DropdownMenuItem, { + closeMenu, + onClick, + }, 'Item 2'), + ]) + + const component = additions.renderIntoDocument(dropdownComponent); + renderer.render(dropdownComponent); + const items = additions.find(component, 'li'); + assert.equal(items.length, 2); + }); +}); \ No newline at end of file -- cgit v1.2.3 From 5e31fc97cd5a30d6f750dde7f13664e3a731ebca Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 13 Jul 2017 10:38:56 -0700 Subject: Redirect from malicious sites faster --- CHANGELOG.md | 2 ++ app/manifest.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee8589735..7cb79bee8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Now redirects from known malicious sites faster. + ## 3.9.0 2017-7-12 - Now detects and blocks known phishing sites. diff --git a/app/manifest.json b/app/manifest.json index 269e88e64..7bf757d4c 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -54,7 +54,7 @@ "all_frames": true }, { - "run_at": "document_end", + "run_at": "document_start", "matches": ["http://*/*", "https://*/*"], "js": ["scripts/blacklister.js"] } -- cgit v1.2.3 From d6001daab81f1ad8b011363635dbe61322c1482a Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 13 Jul 2017 15:24:19 -0400 Subject: remove denodeify --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 201713617..06da15179 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ "copy-to-clipboard": "^2.0.0", "debounce": "^1.0.0", "deep-extend": "^0.4.1", - "denodeify": "^1.2.1", "detect-node": "^2.0.3", "disc": "^1.3.2", "dnode": "^1.2.2", -- cgit v1.2.3 From 7eccf5905a830853bbb1932dde9a7f4536d43f55 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 13 Jul 2017 15:25:43 -0400 Subject: make publishTransaction and signTransaction async methods --- app/scripts/controllers/transactions.js | 31 ++++++++++++------------------- app/scripts/lib/tx-utils.js | 9 +++++++-- test/unit/tx-controller-test.js | 20 +++++++++----------- 3 files changed, 28 insertions(+), 32 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index a2842ae44..707543c87 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -3,7 +3,6 @@ const async = require('async') const extend = require('xtend') const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') -const denodeify = require('denodeify') const TxProviderUtil = require('../lib/tx-utils') const createId = require('../lib/random-id') const NonceTracker = require('../lib/nonce-tracker') @@ -195,7 +194,7 @@ module.exports = class TransactionController extends EventEmitter { txMeta.txParams.nonce = nonceLock.nextNonce this.updateTx(txMeta) // sign transaction - const rawTx = await denodeify(this.signTransaction.bind(this))(txId) + const rawTx = await this.signTransaction(txId) await this.publishTransaction(txId, rawTx) // must set transaction to submitted/failed before releasing lock nonceLock.releaseLock() @@ -231,32 +230,27 @@ module.exports = class TransactionController extends EventEmitter { } } - signTransaction (txId, cb) { + async signTransaction (txId) { const txMeta = this.getTx(txId) const txParams = txMeta.txParams const fromAddress = txParams.from // add network/chain id txParams.chainId = this.getChainId() const ethTx = this.txProviderUtils.buildEthTxFromParams(txParams) - this.signEthTx(ethTx, fromAddress).then(() => { + const rawTx = await this.signEthTx(ethTx, fromAddress).then(() => { this.setTxStatusSigned(txMeta.id) - cb(null, ethUtil.bufferToHex(ethTx.serialize())) - }).catch((err) => { - cb(err) + return ethUtil.bufferToHex(ethTx.serialize()) }) + return rawTx } - publishTransaction (txId, rawTx) { + async publishTransaction (txId, rawTx) { const txMeta = this.getTx(txId) txMeta.rawTx = rawTx this.updateTx(txMeta) - return new Promise((resolve, reject) => { - this.txProviderUtils.publishTransaction(rawTx, (err, txHash) => { - if (err) reject(err) - this.setTxHash(txId, txHash) - this.setTxStatusSubmitted(txId) - resolve() - }) + await this.txProviderUtils.publishTransaction(rawTx).then((txHash) => { + this.setTxHash(txId, txHash) + this.setTxStatusSubmitted(txId) }) } @@ -435,8 +429,7 @@ module.exports = class TransactionController extends EventEmitter { const pending = this.getTxsByMetaData('status', 'submitted') // only try resubmitting if their are transactions to resubmit if (!pending.length) return - const resubmit = denodeify(this._resubmitTx.bind(this)) - pending.forEach((txMeta) => resubmit(txMeta).catch((err) => { + pending.forEach((txMeta) => this._resubmitTx(txMeta).catch((err) => { /* Dont marked as failed if the error is a "known" transaction warning "there is already a transaction with the same sender-nonce @@ -463,7 +456,7 @@ module.exports = class TransactionController extends EventEmitter { })) } - _resubmitTx (txMeta, cb) { + async _resubmitTx (txMeta, cb) { const address = txMeta.txParams.from const balance = this.ethStore.getState().accounts[address].balance if (!('retryCount' in txMeta)) txMeta.retryCount = 0 @@ -482,7 +475,7 @@ module.exports = class TransactionController extends EventEmitter { // Increment a try counter. txMeta.retryCount++ const rawTx = txMeta.rawTx - this.txProviderUtils.publishTransaction(rawTx, cb) + return await this.txProviderUtils.publishTransaction(rawTx, cb) } // checks the network for signed txs and diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js index aa0cb624f..8f6943937 100644 --- a/app/scripts/lib/tx-utils.js +++ b/app/scripts/lib/tx-utils.js @@ -106,8 +106,13 @@ module.exports = class txProviderUtils { return ethTx } - publishTransaction (rawTx, cb) { - this.query.sendRawTransaction(rawTx, cb) + publishTransaction (rawTx) { + return new Promise((resolve, reject) => { + this.query.sendRawTransaction(rawTx, (err, ress) => { + if (err) reject(err) + else resolve(ress) + }) + }) } validateTxParams (txParams, cb) { diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index a5af13915..7b86cfe14 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -270,7 +270,7 @@ describe('Transaction Controller', function () { }) - it('does not overwrite set values', function () { + it('does not overwrite set values', function (done) { this.timeout(15000) const wrongValue = '0x05' @@ -283,37 +283,35 @@ describe('Transaction Controller', function () { .callsArgWithAsync(0, null, wrongValue) - const signStub = sinon.stub(txController, 'signTransaction') - .callsArgWithAsync(1, null, noop) + const signStub = sinon.stub(txController, 'signTransaction', () => Promise.resolve()) - const pubStub = sinon.stub(txController.txProviderUtils, 'publishTransaction') - .callsArgWithAsync(1, null, originalValue) + const pubStub = sinon.stub(txController.txProviderUtils, 'publishTransaction', () => Promise.resolve(originalValue)) - return txController.approveTransaction(txMeta.id).then(() => { + txController.approveTransaction(txMeta.id).then(() => { const result = txController.getTx(txMeta.id) const params = result.txParams assert.equal(params.gas, originalValue, 'gas unmodified') assert.equal(params.gasPrice, originalValue, 'gas price unmodified') - assert.equal(result.hash, originalValue, 'hash was set') + assert.equal(result.hash, originalValue, `hash was set \n got: ${result.hash} \n expected: ${originalValue}`) estimateStub.restore() priceStub.restore() signStub.restore() pubStub.restore() - }) + done() + }).catch(done) }) }) describe('#sign replay-protected tx', function () { it('prepares a tx with the chainId set', function (done) { txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) - txController.signTransaction('1', (err, rawTx) => { - if (err) return done('it should not fail') + txController.signTransaction('1').then((rawTx) => { const ethTx = new EthTx(ethUtil.toBuffer(rawTx)) assert.equal(ethTx.getChainId(), currentNetworkId) done() - }) + }).catch(done) }) }) -- cgit v1.2.3 From d01b5c927d9ae874cc8a7d68fbd1f8649dbba291 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Thu, 13 Jul 2017 22:39:44 -0700 Subject: Brighten dropdown menu item\'s text --- ui/responsive/app/components/dropdown.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/ui/responsive/app/components/dropdown.js b/ui/responsive/app/components/dropdown.js index d7f0e158a..6e09cd133 100644 --- a/ui/responsive/app/components/dropdown.js +++ b/ui/responsive/app/components/dropdown.js @@ -21,7 +21,16 @@ class Dropdown extends Component { boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', }, }, - children, + [ + h( + 'style', + ` + li.dropdown-menu-item:hover { color:rgb(225, 225, 225); } + li.dropdown-menu-item { color: rgb(185, 185, 185); } + ` + ), + ...children, + ], ); } } @@ -38,7 +47,7 @@ class DropdownMenuItem extends Component { const { onClick, closeMenu, children } = this.props; return h( - 'li', + 'li.dropdown-menu-item', { onClick, closeMenu, @@ -48,7 +57,6 @@ class DropdownMenuItem extends Component { fontSize: '12px', fontStyle: 'normal', fontFamily: 'Montserrat Regular', - color: 'rgb(185, 185, 185)', cursor: 'pointer', display: 'flex', justifyContent: 'flex-start', -- cgit v1.2.3 From bda52f7cba9cd866d2244c402fed2e82e1366005 Mon Sep 17 00:00:00 2001 From: tmashuang Date: Fri, 14 Jul 2017 10:34:03 -0700 Subject: Infura Network response tests --- app/scripts/controllers/infura.js | 1 + test/unit/infura-controller-test.js | 92 +++++++++++++++++++++++-------------- 2 files changed, 59 insertions(+), 34 deletions(-) diff --git a/app/scripts/controllers/infura.js b/app/scripts/controllers/infura.js index 98375b446..b34b0bc03 100644 --- a/app/scripts/controllers/infura.js +++ b/app/scripts/controllers/infura.js @@ -26,6 +26,7 @@ class InfuraController { this.store.updateState({ infuraNetworkStatus: parsedResponse, }) + return parsedResponse }) } diff --git a/test/unit/infura-controller-test.js b/test/unit/infura-controller-test.js index 912867764..b9050f4c2 100644 --- a/test/unit/infura-controller-test.js +++ b/test/unit/infura-controller-test.js @@ -1,34 +1,58 @@ -// polyfill fetch -// global.fetch = function () {return Promise.resolve({ -// json: () => { return Promise.resolve({"mainnet": "ok", "ropsten": "degraded", "kovan": "down", "rinkeby": "ok"}) }, -// }) -// } -// const assert = require('assert') -// const InfuraController = require('../../app/scripts/controllers/infura') -// -// describe('infura-controller', function () { -// var infuraController -// -// beforeEach(function () { -// infuraController = new InfuraController() -// }) -// -// describe('network status queries', function () { -// describe('#checkInfuraNetworkStatus', function () { -// it('should return an object reflecting the network statuses', function (done) { -// this.timeout(15000) -// infuraController.checkInfuraNetworkStatus() -// .then(() => { -// const networkStatus = infuraController.store.getState().infuraNetworkStatus -// assert.equal(Object.keys(networkStatus).length, 4) -// assert.equal(networkStatus.mainnet, 'ok') -// assert.equal(networkStatus.ropsten, 'degraded') -// assert.equal(networkStatus.kovan, 'down') -// }) -// .then(() => done()) -// .catch(done) -// -// }) -// }) -// }) -// }) +const assert = require('assert') +const InfuraController = require('../../app/scripts/controllers/infura') + +describe('infura-controller', function () { + var infuraController + let response + + before(async function () { + infuraController = new InfuraController() + response = await infuraController.checkInfuraNetworkStatus() + }) + + describe('Network status queries', function () { + it('should return object/json', function () { + assert.equal(typeof response, 'object') + }) + + describe('Mainnet', function () { + it('should have Mainnet', function () { + assert.equal(Object.keys(response)[0], 'mainnet') + }) + + it('should have a value for Mainnet status', function () { + assert(response.mainnet, 'Mainnet status') + }) + }) + + describe('Ropsten', function () { + it('should have Ropsten', function () { + assert.equal(Object.keys(response)[1], 'ropsten') + }) + + it('should have a value for Ropsten status', function () { + assert(response.ropsten, 'Ropsten status') + }) + }) + + describe('Kovan', function () { + it('should have Kovan', function () { + assert.equal(Object.keys(response)[2], 'kovan') + }) + + it('should have a value for Kovan status', function () { + assert(response.kovan, 'Kovan status') + }) + }) + + describe('Rinkeby', function () { + it('should have Rinkeby', function () { + assert.equal(Object.keys(response)[3], 'rinkeby') + }) + + it('should have a value for Rinkeby status', function () { + assert(response.rinkeby, 'Rinkeby status') + }) + }) + }) +}) -- cgit v1.2.3 From 02cf65d513b12f3310c2c42ad1ea87165b663e4d Mon Sep 17 00:00:00 2001 From: tmashuang Date: Fri, 14 Jul 2017 11:50:41 -0700 Subject: Use Let instead of var --- test/unit/infura-controller-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/infura-controller-test.js b/test/unit/infura-controller-test.js index b9050f4c2..37cdabe3a 100644 --- a/test/unit/infura-controller-test.js +++ b/test/unit/infura-controller-test.js @@ -2,7 +2,7 @@ const assert = require('assert') const InfuraController = require('../../app/scripts/controllers/infura') describe('infura-controller', function () { - var infuraController + let infuraController let response before(async function () { -- cgit v1.2.3 From 6cf2a956c1df56aa7bdc04d94f89752b0c578f87 Mon Sep 17 00:00:00 2001 From: tmashuang Date: Fri, 14 Jul 2017 13:05:56 -0700 Subject: Update Sinon --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1f2d0d591..e0bb303bf 100644 --- a/package.json +++ b/package.json @@ -176,7 +176,7 @@ "react-addons-test-utils": "^15.5.1", "react-test-renderer": "^15.5.4", "react-testutils-additions": "^15.2.0", - "sinon": "^1.17.3", + "sinon": "^2.3.8", "tape": "^4.5.1", "testem": "^1.10.3", "uglifyify": "^3.0.1", -- cgit v1.2.3 From a4c7d95d0de01a37c1e9debea51eb9e2fd8bc2a7 Mon Sep 17 00:00:00 2001 From: tmashuang Date: Fri, 14 Jul 2017 13:06:42 -0700 Subject: Sinon stub infura network status --- test/unit/infura-controller-test.js | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/test/unit/infura-controller-test.js b/test/unit/infura-controller-test.js index 37cdabe3a..605305efa 100644 --- a/test/unit/infura-controller-test.js +++ b/test/unit/infura-controller-test.js @@ -1,57 +1,61 @@ const assert = require('assert') +const sinon = require('sinon') const InfuraController = require('../../app/scripts/controllers/infura') describe('infura-controller', function () { - let infuraController - let response + let infuraController, sandbox, networkStatus + const response = {'mainnet': 'degraded', 'ropsten': 'ok', 'kovan': 'ok', 'rinkeby': 'down'} before(async function () { infuraController = new InfuraController() - response = await infuraController.checkInfuraNetworkStatus() + sandbox = sinon.sandbox.create() + sinon.stub(infuraController, 'checkInfuraNetworkStatus').resolves(response) + networkStatus = await infuraController.checkInfuraNetworkStatus() + }) + + after(function () { + sandbox.restore() }) describe('Network status queries', function () { - it('should return object/json', function () { - assert.equal(typeof response, 'object') - }) describe('Mainnet', function () { it('should have Mainnet', function () { - assert.equal(Object.keys(response)[0], 'mainnet') + assert.equal(Object.keys(networkStatus)[0], 'mainnet') }) it('should have a value for Mainnet status', function () { - assert(response.mainnet, 'Mainnet status') + assert.equal(networkStatus.mainnet, 'degraded') }) }) describe('Ropsten', function () { it('should have Ropsten', function () { - assert.equal(Object.keys(response)[1], 'ropsten') + assert.equal(Object.keys(networkStatus)[1], 'ropsten') }) it('should have a value for Ropsten status', function () { - assert(response.ropsten, 'Ropsten status') + assert.equal(networkStatus.ropsten, 'ok') }) }) describe('Kovan', function () { it('should have Kovan', function () { - assert.equal(Object.keys(response)[2], 'kovan') + assert.equal(Object.keys(networkStatus)[2], 'kovan') }) it('should have a value for Kovan status', function () { - assert(response.kovan, 'Kovan status') + assert.equal(networkStatus.kovan, 'ok') }) }) describe('Rinkeby', function () { it('should have Rinkeby', function () { - assert.equal(Object.keys(response)[3], 'rinkeby') + assert.equal(Object.keys(networkStatus)[3], 'rinkeby') }) it('should have a value for Rinkeby status', function () { - assert(response.rinkeby, 'Rinkeby status') + assert.equal(networkStatus.rinkeby, 'down') }) }) }) -- cgit v1.2.3 From a4e567ffc5a01cb54c73c724c5117988c056fa49 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 17 Jul 2017 09:56:25 -0700 Subject: Add support page link to help page Also adjust github link to be a new bug link, which goes to the new issue page. --- CHANGELOG.md | 1 + ui/app/info.js | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cb79bee8..d7b6316db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Current Master - Now redirects from known malicious sites faster. +- Added a link to our new support page to the help screen. ## 3.9.0 2017-7-12 diff --git a/ui/app/info.js b/ui/app/info.js index e8470de97..cb2e41f5b 100644 --- a/ui/app/info.js +++ b/ui/app/info.js @@ -97,11 +97,17 @@ InfoScreen.prototype.render = function () { paddingLeft: '30px', }}, [ + h('div.fa.fa-support', [ + h('a.info', { + href: 'http://metamask.consensyssupport.happyfox.com', + target: '_blank', + }, 'Visit our Support Center'), + ]), h('div.fa.fa-github', [ h('a.info', { - href: 'https://github.com/MetaMask/faq', + href: 'https://github.com/MetaMask/metamask-extension/issues/new', target: '_blank', - }, 'Need Help? Read our FAQ!'), + }, 'Found a bug? Report it!'), ]), h('div', [ h('a', { -- cgit v1.2.3 From 614501e743a0c1584062c78a25e6b9a3ddf10aab Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 17 Jul 2017 14:16:39 -0700 Subject: Fix transaction confirmation ordering Newest tx or message will now always appear last, and a new tx proposed after the user has a confirmation box open will never change the confirmation to a different tx proposed. Fixes #1637 --- CHANGELOG.md | 1 + test/unit/tx-helper-test.js | 17 +++++++++++++++++ ui/lib/tx-helper.js | 6 +++++- 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 test/unit/tx-helper-test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index d7b6316db..c9149287f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Now redirects from known malicious sites faster. - Added a link to our new support page to the help screen. +- Fixed bug where a new transaction would be shown over the current transaction, creating a possible timing attack against user confirmation. ## 3.9.0 2017-7-12 diff --git a/test/unit/tx-helper-test.js b/test/unit/tx-helper-test.js new file mode 100644 index 000000000..cc6543c30 --- /dev/null +++ b/test/unit/tx-helper-test.js @@ -0,0 +1,17 @@ +const assert = require('assert') +const txHelper = require('../../ui/lib/tx-helper') + +describe('txHelper', function () { + it('always shows the oldest tx first', function () { + const metamaskNetworkId = 1 + const txs = { + a: { metamaskNetworkId, time: 3 }, + b: { metamaskNetworkId, time: 1 }, + c: { metamaskNetworkId, time: 2 }, + } + + const sorted = txHelper(txs, null, null, metamaskNetworkId) + assert.equal(sorted[0].time, 1, 'oldest tx first') + assert.equal(sorted[2].time, 3, 'newest tx last') + }) +}) diff --git a/ui/lib/tx-helper.js b/ui/lib/tx-helper.js index ec19daf64..afc62e7b6 100644 --- a/ui/lib/tx-helper.js +++ b/ui/lib/tx-helper.js @@ -12,6 +12,10 @@ module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, network) const personalValues = valuesFor(personalMsgs) log.debug(`tx helper found ${personalValues.length} unsigned personal messages`) allValues = allValues.concat(personalValues) + allValues = allValues.sort((a, b) => { + return a.time > b.time + }) - return allValues.sort(txMeta => txMeta.time) + return allValues } + -- cgit v1.2.3 From 948f3880a3b1af86c3b587d250d0376bbf547d06 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Mon, 17 Jul 2017 16:38:44 -0400 Subject: turn off auto faucet and remove file --- CHANGELOG.md | 1 + app/scripts/lib/auto-faucet.js | 20 -------------------- app/scripts/metamask-controller.js | 4 ---- 3 files changed, 1 insertion(+), 24 deletions(-) delete mode 100644 app/scripts/lib/auto-faucet.js diff --git a/CHANGELOG.md b/CHANGELOG.md index d7b6316db..ddb07e6a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Current Master +- No longer automatically request 1 ropsten ether for the first account in a new vault. - Now redirects from known malicious sites faster. - Added a link to our new support page to the help screen. diff --git a/app/scripts/lib/auto-faucet.js b/app/scripts/lib/auto-faucet.js deleted file mode 100644 index 38d54ba5e..000000000 --- a/app/scripts/lib/auto-faucet.js +++ /dev/null @@ -1,20 +0,0 @@ -const uri = 'https://faucet.metamask.io/' -const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' -const env = process.env.METAMASK_ENV - -module.exports = function (address) { - // Don't faucet in development or test - if (METAMASK_DEBUG === true || env === 'test') return - global.log.info('auto-fauceting:', address) - const data = address - const headers = new Headers() - headers.append('Content-type', 'application/rawdata') - fetch(uri, { - method: 'POST', - headers, - body: data, - }) - .catch((err) => { - console.error(err) - }) -} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index c6c3fde1e..11dcde2c1 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -20,7 +20,6 @@ const MessageManager = require('./lib/message-manager') const PersonalMessageManager = require('./lib/personal-message-manager') const TransactionController = require('./controllers/transactions') const ConfigManager = require('./lib/config-manager') -const autoFaucet = require('./lib/auto-faucet') const nodeify = require('./lib/nodeify') const accountImporter = require('./account-import-strategies') const getBuyEthUrl = require('./lib/buy-eth-url') @@ -90,9 +89,6 @@ module.exports = class MetamaskController extends EventEmitter { this.keyringController.on('newAccount', (address) => { this.preferencesController.setSelectedAddress(address) }) - this.keyringController.on('newVault', (address) => { - autoFaucet(address) - }) // address book controller this.addressBookController = new AddressBookController({ -- cgit v1.2.3 From 433fb4d24201d30eb84350bb1bd649f5bb22ad92 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Fri, 14 Jul 2017 00:53:54 -0700 Subject: Cleanup Fix lint error breaking gulp build Add presentational options menus --- ui/classic/app/components/editable-label.js | 3 + ui/responsive/app/account-detail.js | 9 ++- ui/responsive/app/app.js | 45 +++++-------- .../app/components/account-options-menus.js | 77 ++++++++++++++++++++++ ui/responsive/app/components/editable-label.js | 7 +- 5 files changed, 108 insertions(+), 33 deletions(-) create mode 100644 ui/responsive/app/components/account-options-menus.js diff --git a/ui/classic/app/components/editable-label.js b/ui/classic/app/components/editable-label.js index 41936f5e0..48ba5060e 100644 --- a/ui/classic/app/components/editable-label.js +++ b/ui/classic/app/components/editable-label.js @@ -13,6 +13,7 @@ function EditableLabel () { EditableLabel.prototype.render = function () { const props = this.props const state = this.state + console.log("editing:", state.isEditingLabel); if (state && state.isEditingLabel) { return h('div.editable-label', [ @@ -30,6 +31,8 @@ EditableLabel.prototype.render = function () { } else { return h('div.name-label', { onClick: (event) => { + debugger; + console.log("event", event.target); this.setState({ isEditingLabel: true }) }, }, this.props.children) diff --git a/ui/responsive/app/account-detail.js b/ui/responsive/app/account-detail.js index ff5c2aadb..9a837a121 100644 --- a/ui/responsive/app/account-detail.js +++ b/ui/responsive/app/account-detail.js @@ -18,6 +18,8 @@ const EditableLabel = require('./components/editable-label') const Tooltip = require('./components/tooltip') const TabBar = require('./components/tab-bar') const TokenList = require('./components/token-list') +const AccountOptionsMenus = require('./components/account-options-menus').AccountOptionsMenus; +console.log("AOM",AccountOptionsMenus); module.exports = connect(mapStateToProps)(AccountDetailScreen) @@ -51,6 +53,8 @@ AccountDetailScreen.prototype.render = function () { var identity = props.identities[selected] var account = props.accounts[selected] const { network, conversionRate, currentCurrency } = props + console.log("identity:", identity); + console.log("result:", identity && identity.name); return ( @@ -99,7 +103,10 @@ AccountDetailScreen.prototype.render = function () { // What is shown when not editing + edit text: h('label.editing-label', [h('.edit-text', 'edit')]), - h('h2.font-medium.color-forest', {name: 'edit'}, identity && identity.name), + h('h2.font-medium.color-forest', {name: 'edit'}, [ + identity && identity.name, + h(AccountOptionsMenus, { style: { marginLeft: '35%' }}, []), + ]), ]), h('.flex-row', { style: { diff --git a/ui/responsive/app/app.js b/ui/responsive/app/app.js index e7bde9605..f829dc8fa 100644 --- a/ui/responsive/app/app.js +++ b/ui/responsive/app/app.js @@ -26,6 +26,7 @@ const Loading = require('./components/loading') const SandwichExpando = require('sandwich-expando') const MenuDroppo = require('menu-droppo') const DropMenuItem = require('./components/drop-menu-item') +import { Dropdown, DropdownMenuItem } from './components/dropdown'; const NetworkIndicator = require('./components/network') const Tooltip = require('./components/tooltip') const BuyView = require('./components/buy-button-subview') @@ -295,7 +296,7 @@ App.prototype.renderDropdown = function () { const state = this.state || {} const isOpen = state.isMainMenuOpen - return h(MenuDroppo, { + return h(Dropdown, { isOpen: isOpen, zIndex: 11, onClickOutside: (event) => { @@ -306,43 +307,27 @@ App.prototype.renderDropdown = function () { right: 0, top: '36px', }, - innerStyle: { - background: 'white', - boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', - }, + innerStyle: {}, }, [ // DROP MENU ITEMS - h('style', ` - .drop-menu-item:hover { background:rgb(235, 235, 235); } - .drop-menu-item i { margin: 11px; } - `), - - h(DropMenuItem, { - label: 'Settings', + h(DropdownMenuItem, { closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.showConfigPage()), - icon: h('i.fa.fa-gear.fa-lg'), - }), + onClick: () => this.props.dispatch(actions.showConfigPage()), + }, 'Settings'), - h(DropMenuItem, { - label: 'Import Account', + h(DropdownMenuItem, { closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.showImportPage()), - icon: h('i.fa.fa-arrow-circle-o-up.fa-lg'), - }), + onClick: () => this.props.dispatch(actions.showImportPage()), + }, 'Import Account'), - h(DropMenuItem, { - label: 'Lock', + h(DropdownMenuItem, { closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.lockMetamask()), - icon: h('i.fa.fa-lock.fa-lg'), - }), + onClick: () => this.props.dispatch(actions.lockMetamask()), + }, 'Lock'), - h(DropMenuItem, { - label: 'Info/Help', + h(DropdownMenuItem, { closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.showInfoPage()), - icon: h('i.fa.fa-question.fa-lg'), - }), + onClick: () => this.props.dispatch(actions.showInfoPage()), + }, 'Info/Help'), ]) } diff --git a/ui/responsive/app/components/account-options-menus.js b/ui/responsive/app/components/account-options-menus.js new file mode 100644 index 000000000..acaf53c9e --- /dev/null +++ b/ui/responsive/app/components/account-options-menus.js @@ -0,0 +1,77 @@ +const Component = require('react').Component; +const PropTypes = require('react').PropTypes; +const h = require('react-hyperscript'); +const Dropdown = require('./dropdown').Dropdown; +const DropdownMenuItem = require('./dropdown').DropdownMenuItem; + +class AccountOptionsMenus extends Component { + constructor(props) { + super(props); + this.state = { + overflowMenuActive: false, + switchingMenuActive: false, + }; + console.log("state:", this.state); + } + + render() { + console.log("RENDERING AcountOptionsMenus"); + return h( + 'span', + { + style: this.props.style, + }, + [ + h( + 'i.fa.fa-angle-down', + { + onClick: (event) => { + event.stopPropagation(); + this.setState({ switchingMenuActive: !this.state.switchingMenuActive }) + } + }, + [ + h( + Dropdown, + { + isOpen: this.state.switchingMenuActive, + onClickOutside: () => { this.setState({ switchingMenuActive: false})} + }, + [ + h(DropdownMenuItem, { + }, 'Settings'), + ] + ) + ], + ), + h( + 'i.fa.fa-ellipsis-h', + { + style: { 'marginLeft': '10px'}, + onClick: () => { this.setState({ switchingMenuActive: !this.state.switchingMenuActive }) } + }, + [ + h( + Dropdown, + { + isOpen: this.state.overflowMenuActive, + onClickOutside: (event) => { + event.stopPropagation(); + this.setState({ overflowMenuActive: false}) + } + }, + [ + h(DropdownMenuItem, { + }, 'Settings'), + ] + ) + ] + ) + ] + ) + } +} + +module.exports = { + AccountOptionsMenus, +}; \ No newline at end of file diff --git a/ui/responsive/app/components/editable-label.js b/ui/responsive/app/components/editable-label.js index 41936f5e0..43841bdd8 100644 --- a/ui/responsive/app/components/editable-label.js +++ b/ui/responsive/app/components/editable-label.js @@ -30,12 +30,15 @@ EditableLabel.prototype.render = function () { } else { return h('div.name-label', { onClick: (event) => { - this.setState({ isEditingLabel: true }) + if (event.target.getAttribute('name') === 'edit') { + this.setState({ isEditingLabel: true }) + } }, }, this.props.children) } } - +// class = edit-text +// name = edit EditableLabel.prototype.saveIfEnter = function (event) { if (event.key === 'Enter') { this.saveText() -- cgit v1.2.3 From b05775bfa40f5a36d3da223908c94eec50415214 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Fri, 14 Jul 2017 02:49:07 -0700 Subject: Fix click handlers on AccountOptionsMenus --- ui/responsive/app/components/account-options-menus.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/responsive/app/components/account-options-menus.js b/ui/responsive/app/components/account-options-menus.js index acaf53c9e..ce2699b38 100644 --- a/ui/responsive/app/components/account-options-menus.js +++ b/ui/responsive/app/components/account-options-menus.js @@ -48,17 +48,17 @@ class AccountOptionsMenus extends Component { 'i.fa.fa-ellipsis-h', { style: { 'marginLeft': '10px'}, - onClick: () => { this.setState({ switchingMenuActive: !this.state.switchingMenuActive }) } + onClick: (event) => { + event.stopPropagation(); + this.setState({ overflowMenuActive: !this.state.overflowMenuActive }) + } }, [ h( Dropdown, { isOpen: this.state.overflowMenuActive, - onClickOutside: (event) => { - event.stopPropagation(); - this.setState({ overflowMenuActive: false}) - } + onClickOutside: () => { this.setState({ overflowMenuActive: false})} }, [ h(DropdownMenuItem, { -- cgit v1.2.3 From ccf3e0e2512c05c024ec5f2e70e2d682a4968041 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Tue, 18 Jul 2017 05:23:02 -0700 Subject: Bump version of menu-droppo to include bugfix --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a587a4507..0129583f7 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "inject-css": "^0.1.1", "jazzicon": "^1.2.0", "loglevel": "^1.4.1", - "menu-droppo": "^1.1.0", + "menu-droppo": "1.1.6", "metamask-logo": "^2.1.2", "mississippi": "^1.2.0", "mkdirp": "^0.5.1", -- cgit v1.2.3 From 2adfce772c91e28cb25145ad0beda40bd5aed7d4 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Tue, 18 Jul 2017 05:23:25 -0700 Subject: Add new tests for dropdown component --- test/unit/responsive/components/dropdown-test.js | 108 ++++++++++++++++++----- 1 file changed, 86 insertions(+), 22 deletions(-) diff --git a/test/unit/responsive/components/dropdown-test.js b/test/unit/responsive/components/dropdown-test.js index feadc792e..4d417d394 100644 --- a/test/unit/responsive/components/dropdown-test.js +++ b/test/unit/responsive/components/dropdown-test.js @@ -9,14 +9,18 @@ const Dropdown = require(path.join(__dirname, '..', '..', '..', '..', 'ui', 'res const DropdownMenuItem = require(path.join(__dirname, '..', '..', '..', '..', 'ui', 'responsive', 'app', 'components', 'dropdown.js')).DropdownMenuItem; describe('Dropdown components', function () { - it('can render two items', function () { - const renderer = ReactTestUtils.createRenderer() + let onClickOutside; + let closeMenu; + let onClick; - const onClickOutside = sinon.spy(); - const closeMenu = sinon.spy(); - const onClick = sinon.spy(); + let dropdownComponentProps; + const renderer = ReactTestUtils.createRenderer() + beforeEach(function () { + onClickOutside = sinon.spy(); + closeMenu = sinon.spy(); + onClick = sinon.spy(); - const dropdownComponent = h(Dropdown, { + dropdownComponentProps = { isOpen: true, zIndex: 11, onClickOutside, @@ -26,26 +30,86 @@ describe('Dropdown components', function () { top: '36px', }, innerStyle: {}, - }, [ // DROP MENU ITEMS - h('style', ` - .drop-menu-item:hover { background:rgb(235, 235, 235); } - .drop-menu-item i { margin: 11px; } - `), - - h(DropdownMenuItem, { - closeMenu, - onClick, - }, 'Item 1'), - - h(DropdownMenuItem, { - closeMenu, - onClick, - }, 'Item 2'), - ]) + } + }); + + it('can render two items', function () { + const dropdownComponent = h( + Dropdown, + dropdownComponentProps, + [ + h('style', ` + .drop-menu-item:hover { background:rgb(235, 235, 235); } + .drop-menu-item i { margin: 11px; } + `), + h(DropdownMenuItem, { + closeMenu, + onClick, + }, 'Item 1'), + h(DropdownMenuItem, { + closeMenu, + onClick, + }, 'Item 2'), + ] + ) const component = additions.renderIntoDocument(dropdownComponent); renderer.render(dropdownComponent); const items = additions.find(component, 'li'); assert.equal(items.length, 2); }); + + it('closes when item clicked', function() { + const dropdownComponent = h( + Dropdown, + dropdownComponentProps, + [ + h('style', ` + .drop-menu-item:hover { background:rgb(235, 235, 235); } + .drop-menu-item i { margin: 11px; } + `), + h(DropdownMenuItem, { + closeMenu, + onClick, + }, 'Item 1'), + h(DropdownMenuItem, { + closeMenu, + onClick, + }, 'Item 2'), + ] + ) + const component = additions.renderIntoDocument(dropdownComponent); + renderer.render(dropdownComponent); + const items = additions.find(component, 'li'); + const node = items[0]; + ReactTestUtils.Simulate.click(node); + assert.equal(closeMenu.calledOnce, true); + }); + + it('invokes click handler when item clicked', function() { + const dropdownComponent = h( + Dropdown, + dropdownComponentProps, + [ + h('style', ` + .drop-menu-item:hover { background:rgb(235, 235, 235); } + .drop-menu-item i { margin: 11px; } + `), + h(DropdownMenuItem, { + closeMenu, + onClick, + }, 'Item 1'), + h(DropdownMenuItem, { + closeMenu, + onClick, + }, 'Item 2'), + ] + ) + const component = additions.renderIntoDocument(dropdownComponent); + renderer.render(dropdownComponent); + const items = additions.find(component, 'li'); + const node = items[0]; + ReactTestUtils.Simulate.click(node); + assert.equal(onClick.calledOnce, true); + }); }); \ No newline at end of file -- cgit v1.2.3 From fce7bf3a1ca3c3b1b84173355965d8dc511effdc Mon Sep 17 00:00:00 2001 From: sdtsui Date: Tue, 18 Jul 2017 05:25:16 -0700 Subject: Remove accounts screen --- ui/responsive/app/accounts/account-list-item.js | 91 --------- ui/responsive/app/accounts/index.js | 163 ---------------- ui/responsive/app/components/account-dropdowns.js | 217 ++++++++++++++++++++++ ui/responsive/app/components/dropdown.js | 44 +++-- 4 files changed, 244 insertions(+), 271 deletions(-) delete mode 100644 ui/responsive/app/accounts/account-list-item.js delete mode 100644 ui/responsive/app/accounts/index.js create mode 100644 ui/responsive/app/components/account-dropdowns.js diff --git a/ui/responsive/app/accounts/account-list-item.js b/ui/responsive/app/accounts/account-list-item.js deleted file mode 100644 index 10a0b6cc7..000000000 --- a/ui/responsive/app/accounts/account-list-item.js +++ /dev/null @@ -1,91 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ethUtil = require('ethereumjs-util') - -const EthBalance = require('../components/eth-balance') -const CopyButton = require('../components/copyButton') -const Identicon = require('../components/identicon') - -module.exports = AccountListItem - -inherits(AccountListItem, Component) -function AccountListItem () { - Component.call(this) -} - -AccountListItem.prototype.render = function () { - const { identity, selectedAddress, accounts, onShowDetail, - conversionRate, currentCurrency } = this.props - - const checksumAddress = identity && identity.address && ethUtil.toChecksumAddress(identity.address) - const isSelected = selectedAddress === identity.address - const account = accounts[identity.address] - const selectedClass = isSelected ? '.selected' : '' - - return ( - h(`.accounts-list-option.flex-row.flex-space-between.pointer.hover-white${selectedClass}`, { - key: `account-panel-${identity.address}`, - onClick: (event) => onShowDetail(identity.address, event), - }, [ - - h('.identicon-wrapper.flex-column.flex-center.select-none', [ - this.pendingOrNot(), - this.indicateIfLoose(), - h(Identicon, { - address: identity.address, - imageify: true, - }), - ]), - - // account address, balance - h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', { - style: { - width: '200px', - }, - }, [ - h('span', identity.name), - h('span.font-small', { - style: { - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - }, checksumAddress), - h(EthBalance, { - value: account && account.balance, - currentCurrency, - conversionRate, - style: { - lineHeight: '7px', - marginTop: '10px', - }, - }), - ]), - - // copy button - h('.identity-copy.flex-column', { - style: { - margin: '0 20px', - }, - }, [ - h(CopyButton, { - value: checksumAddress, - }), - ]), - ]) - ) -} - -AccountListItem.prototype.indicateIfLoose = function () { - try { // Sometimes keyrings aren't loaded yet: - const type = this.props.keyring.type - const isLoose = type !== 'HD Key Tree' - return isLoose ? h('.keyring-label', 'LOOSE') : null - } catch (e) { return } -} - -AccountListItem.prototype.pendingOrNot = function () { - const pending = this.props.pending - if (pending.length === 0) return null - return h('.pending-dot', pending.length) -} diff --git a/ui/responsive/app/accounts/index.js b/ui/responsive/app/accounts/index.js deleted file mode 100644 index 3e0830b63..000000000 --- a/ui/responsive/app/accounts/index.js +++ /dev/null @@ -1,163 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../actions') -const valuesFor = require('../util').valuesFor -const findDOMNode = require('react-dom').findDOMNode -const AccountListItem = require('./account-list-item') - -module.exports = connect(mapStateToProps)(AccountsScreen) - -function mapStateToProps (state) { - const pendingTxs = valuesFor(state.metamask.unapprovedTxs) - .filter(txMeta => txMeta.metamaskNetworkId === state.metamask.network) - const pendingMsgs = valuesFor(state.metamask.unapprovedMsgs) - const pending = pendingTxs.concat(pendingMsgs) - - return { - accounts: state.metamask.accounts, - identities: state.metamask.identities, - unapprovedTxs: state.metamask.unapprovedTxs, - selectedAddress: state.metamask.selectedAddress, - scrollToBottom: state.appState.scrollToBottom, - pending, - keyrings: state.metamask.keyrings, - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - } -} - -inherits(AccountsScreen, Component) -function AccountsScreen () { - Component.call(this) -} - -AccountsScreen.prototype.render = function () { - const props = this.props - const { keyrings, conversionRate, currentCurrency } = props - const identityList = valuesFor(props.identities) - const unapprovedTxList = valuesFor(props.unapprovedTxs) - - return ( - - h('.accounts-section.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: this.goHome.bind(this), - }), - h('h2.page-subtitle', 'Select Account'), - ]), - - h('hr.horizontal-line'), - - // identity selection - h('section.identity-section', { - style: { - overflowY: 'auto', - overflowX: 'hidden', - }, - }, - [ - identityList.map((identity) => { - const pending = this.props.pending.filter((txOrMsg) => { - if ('txParams' in txOrMsg) { - return txOrMsg.txParams.from === identity.address - } else if ('msgParams' in txOrMsg) { - return txOrMsg.msgParams.from === identity.address - } else { - return false - } - }) - - const simpleAddress = identity.address.substring(2).toLowerCase() - const keyring = keyrings.find((kr) => { - return kr.accounts.includes(simpleAddress) || - kr.accounts.includes(identity.address) - }) - - return h(AccountListItem, { - key: `acct-panel-${identity.address}`, - identity, - selectedAddress: this.props.selectedAddress, - conversionRate, - currentCurrency, - accounts: this.props.accounts, - onShowDetail: this.onShowDetail.bind(this), - pending, - keyring, - }) - }), - - h('hr.horizontal-line'), - h('div.footer.hover-white.pointer', { - key: 'reveal-account-bar', - onClick: () => { - this.addNewAccount() - }, - style: { - display: 'flex', - height: '40px', - padding: '10px', - justifyContent: 'center', - alignItems: 'center', - }, - }, [ - h('i.fa.fa-plus.fa-lg', {key: ''}), - ]), - h('hr.horizontal-line'), - ]), - - unapprovedTxList.length ? ( - - h('.unconftx-link.flex-row.flex-center', { - onClick: this.navigateToConfTx.bind(this), - }, [ - h('span', 'Unconfirmed Txs'), - h('i.fa.fa-arrow-right.fa-lg'), - ]) - - ) : ( - null - ), - ]) - ) -} - -// If a new account was revealed, scroll to the bottom -AccountsScreen.prototype.componentDidUpdate = function () { - const scrollToBottom = this.props.scrollToBottom - - if (scrollToBottom) { - var container = findDOMNode(this) - var scrollable = container.querySelector('.identity-section') - scrollable.scrollTop = scrollable.scrollHeight - } -} - -AccountsScreen.prototype.navigateToConfTx = function () { - event.stopPropagation() - this.props.dispatch(actions.showConfTxPage()) -} - -AccountsScreen.prototype.onShowDetail = function (address, event) { - event.stopPropagation() - this.props.dispatch(actions.showAccountDetail(address)) -} - -AccountsScreen.prototype.addNewAccount = function () { - this.props.dispatch(actions.addNewAccount(0)) -} - -/* An optional view proposed in this design: - * https://consensys.quip.com/zZVrAysM5znY -AccountsScreen.prototype.addNewAccount = function () { - this.props.dispatch(actions.navigateToNewAccountScreen()) -} -*/ - -AccountsScreen.prototype.goHome = function () { - this.props.dispatch(actions.goHome()) -} diff --git a/ui/responsive/app/components/account-dropdowns.js b/ui/responsive/app/components/account-dropdowns.js new file mode 100644 index 000000000..cbb97b2cb --- /dev/null +++ b/ui/responsive/app/components/account-dropdowns.js @@ -0,0 +1,217 @@ +const Component = require('react').Component +const PropTypes = require('react').PropTypes +const h = require('react-hyperscript') +const actions = require('../actions') +const genAccountLink = require('../../lib/account-link.js') +const connect = require('react-redux').connect +const Dropdown = require('./dropdown').Dropdown +const DropdownMenuItem = require('./dropdown').DropdownMenuItem +const Identicon = require('./identicon') +const ethUtil = require('ethereumjs-util') +const copyToClipboard = require('copy-to-clipboard') + +class AccountDropdowns extends Component { + constructor (props) { + super(props) + this.state = { + overflowMenuActive: false, + switchingMenuActive: false, + } + } + + getAccounts () { + const { identities, selected } = this.props + + return Object.keys(identities).map((key) => { + const identity = identities[key] + const isSelected = identity.address === selected + + return h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + this.props.actions.showAccountDetail(identity.address) + }, + }, + [ + h( + Identicon, + { + address: identity.address, + diameter: 16, + }, + ), + h('span', { style: { marginLeft: '10px' } }, identity.name || ''), + h('span', { style: { marginLeft: '10px' } }, isSelected ? h('.check', '✓') : null), + ] + ) + }) + } + + render () { + const { style, actions } = this.props + const { switchingMenuActive, overflowMenuActive } = this.state + + return h( + 'span', + { + style: style, + }, + [ + h( + 'i.fa.fa-angle-down', + { + style: {}, + onClick: (event) => { + event.stopPropagation() + this.setState({ + switchingMenuActive: !switchingMenuActive, + overflowMenuActive: false, + }) + }, + }, + [ + h( + Dropdown, + { + style: { + marginLeft: '-140px', + minWidth: '180px', + }, + isOpen: switchingMenuActive, + onClickOutside: () => { this.setState({ switchingMenuActive: false}) }, + }, + [ + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => actions.showConfigPage(), + }, + 'Account Settings', + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + const { selected, network } = this.props + const url = genAccountLink(selected, network) + global.platform.openWindow({ url }) + }, + }, + 'View account on Etherscan', + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + const { selected } = this.props + const checkSumAddress = selected && ethUtil.toChecksumAddress(selected) + copyToClipboard(checkSumAddress) + }, + }, + 'Copy Address to clipboard', + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + actions.requestAccountExport() + }, + }, + 'Export Private Key', + ), + ] + ), + ], + ), + h( + 'i.fa.fa-ellipsis-h', + { + style: { 'marginLeft': '10px'}, + onClick: (event) => { + event.stopPropagation() + this.setState({ + overflowMenuActive: !overflowMenuActive, + switchingMenuActive: false, + }) + }, + }, + [ + h( + Dropdown, + { + style: { + marginLeft: '-155px', + minWidth: '180px', + }, + isOpen: overflowMenuActive, + onClickOutside: () => { this.setState({ overflowMenuActive: false}) }, + }, + [ + ...this.getAccounts(), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => actions.addNewAccount(), + }, + [ + h( + Identicon, + { + diameter: 16, + }, + ), + h('span', { style: { marginLeft: '10px' } }, 'Create Account'), + ], + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => actions.showImportPage(), + }, + [ + h( + Identicon, + { + diameter: 16, + }, + ), + h('span', { style: { marginLeft: '10px' } }, 'Import Account'), + ] + ), + ] + ), + ] + ), + ] + ) + } +} + +AccountDropdowns.propTypes = { + identities: PropTypes.objectOf(PropTypes.object), + selected: PropTypes.string, +} + +const mapDispatchToProps = (dispatch, ownProps) => { + return { + actions: { + showConfigPage: () => dispatch(actions.showConfigPage()), + requestAccountExport: () => dispatch(actions.requestExportAccount()), + showAccountDetail: (address) => dispatch(actions.showAccountDetail(address)), + addNewAccount: () => dispatch(actions.addNewAccount()), + showImportPage: () => dispatch(actions.showImportPage()), + }, + } +} + +module.exports = { + AccountDropdowns: connect(null, mapDispatchToProps)(AccountDropdowns), +} diff --git a/ui/responsive/app/components/dropdown.js b/ui/responsive/app/components/dropdown.js index 6e09cd133..e77b4c40c 100644 --- a/ui/responsive/app/components/dropdown.js +++ b/ui/responsive/app/components/dropdown.js @@ -1,11 +1,13 @@ -const Component = require('react').Component; -const PropTypes = require('react').PropTypes; -const h = require('react-hyperscript'); -const MenuDroppo = require('menu-droppo'); +const Component = require('react').Component +const PropTypes = require('react').PropTypes +const h = require('react-hyperscript') +const MenuDroppo = require('menu-droppo') + +const noop = () => {} class Dropdown extends Component { - render() { - const { isOpen, onClickOutside, style, children } = this.props; + render () { + const { isOpen, onClickOutside, style, children } = this.props return h( MenuDroppo, @@ -30,27 +32,34 @@ class Dropdown extends Component { ` ), ...children, - ], - ); + ] + ) } } +Dropdown.defaultProps = { + isOpen: false, + onClick: noop, +} + Dropdown.propTypes = { - isOpen: PropTypes.func.isRequired, + isOpen: PropTypes.bool.isRequired, onClick: PropTypes.func.isRequired, children: PropTypes.node, - style: PropTypes.object.isRequired, + style: PropTypes.object.isRequired, } class DropdownMenuItem extends Component { - render() { - const { onClick, closeMenu, children } = this.props; + render () { + const { onClick, closeMenu, children } = this.props return h( 'li.dropdown-menu-item', { - onClick, - closeMenu, + onClick: () => { + onClick() + closeMenu() + }, style: { listStyle: 'none', padding: '8px 0px 8px 0px', @@ -60,10 +69,11 @@ class DropdownMenuItem extends Component { cursor: 'pointer', display: 'flex', justifyContent: 'flex-start', + alignItems: 'center', }, }, children - ); + ) } } @@ -71,9 +81,9 @@ DropdownMenuItem.propTypes = { closeMenu: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired, children: PropTypes.node, -}; +} module.exports = { Dropdown, DropdownMenuItem, -}; \ No newline at end of file +} -- cgit v1.2.3 From b9dfb3cd1e825961dd3e32065d2bf377f2f59355 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Tue, 18 Jul 2017 05:25:58 -0700 Subject: Remove account options icons --- ui/responsive/app/components/account-info-link.js | 41 ------------ .../app/components/account-options-menus.js | 77 ---------------------- ui/responsive/app/components/drop-menu-item.js | 59 ----------------- 3 files changed, 177 deletions(-) delete mode 100644 ui/responsive/app/components/account-info-link.js delete mode 100644 ui/responsive/app/components/account-options-menus.js delete mode 100644 ui/responsive/app/components/drop-menu-item.js diff --git a/ui/responsive/app/components/account-info-link.js b/ui/responsive/app/components/account-info-link.js deleted file mode 100644 index 6526ab502..000000000 --- a/ui/responsive/app/components/account-info-link.js +++ /dev/null @@ -1,41 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const Tooltip = require('./tooltip') -const genAccountLink = require('../../lib/account-link') - -module.exports = AccountInfoLink - -inherits(AccountInfoLink, Component) -function AccountInfoLink () { - Component.call(this) -} - -AccountInfoLink.prototype.render = function () { - const { selected, network } = this.props - const title = 'View account on Etherscan' - const url = genAccountLink(selected, network) - - if (!url) { - return null - } - - return h('.account-info-link', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - - h(Tooltip, { - title, - }, [ - h('i.fa.fa-info-circle.cursor-pointer.color-orange', { - style: { - margin: '5px', - }, - onClick () { global.platform.openWindow({ url }) }, - }), - ]), - ]) -} diff --git a/ui/responsive/app/components/account-options-menus.js b/ui/responsive/app/components/account-options-menus.js deleted file mode 100644 index ce2699b38..000000000 --- a/ui/responsive/app/components/account-options-menus.js +++ /dev/null @@ -1,77 +0,0 @@ -const Component = require('react').Component; -const PropTypes = require('react').PropTypes; -const h = require('react-hyperscript'); -const Dropdown = require('./dropdown').Dropdown; -const DropdownMenuItem = require('./dropdown').DropdownMenuItem; - -class AccountOptionsMenus extends Component { - constructor(props) { - super(props); - this.state = { - overflowMenuActive: false, - switchingMenuActive: false, - }; - console.log("state:", this.state); - } - - render() { - console.log("RENDERING AcountOptionsMenus"); - return h( - 'span', - { - style: this.props.style, - }, - [ - h( - 'i.fa.fa-angle-down', - { - onClick: (event) => { - event.stopPropagation(); - this.setState({ switchingMenuActive: !this.state.switchingMenuActive }) - } - }, - [ - h( - Dropdown, - { - isOpen: this.state.switchingMenuActive, - onClickOutside: () => { this.setState({ switchingMenuActive: false})} - }, - [ - h(DropdownMenuItem, { - }, 'Settings'), - ] - ) - ], - ), - h( - 'i.fa.fa-ellipsis-h', - { - style: { 'marginLeft': '10px'}, - onClick: (event) => { - event.stopPropagation(); - this.setState({ overflowMenuActive: !this.state.overflowMenuActive }) - } - }, - [ - h( - Dropdown, - { - isOpen: this.state.overflowMenuActive, - onClickOutside: () => { this.setState({ overflowMenuActive: false})} - }, - [ - h(DropdownMenuItem, { - }, 'Settings'), - ] - ) - ] - ) - ] - ) - } -} - -module.exports = { - AccountOptionsMenus, -}; \ No newline at end of file diff --git a/ui/responsive/app/components/drop-menu-item.js b/ui/responsive/app/components/drop-menu-item.js deleted file mode 100644 index e42948209..000000000 --- a/ui/responsive/app/components/drop-menu-item.js +++ /dev/null @@ -1,59 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = DropMenuItem - -inherits(DropMenuItem, Component) -function DropMenuItem () { - Component.call(this) -} - -DropMenuItem.prototype.render = function () { - return h('li.drop-menu-item', { - onClick: () => { - this.props.closeMenu() - this.props.action() - }, - style: { - listStyle: 'none', - padding: '6px 16px 6px 5px', - fontFamily: 'Montserrat Regular', - color: 'rgb(125, 128, 130)', - cursor: 'pointer', - display: 'flex', - justifyContent: 'flex-start', - }, - }, [ - this.props.icon, - this.props.label, - this.activeNetworkRender(), - ]) -} - -DropMenuItem.prototype.activeNetworkRender = function () { - const activeNetwork = this.props.activeNetworkRender - const { provider } = this.props - const providerType = provider ? provider.type : null - if (activeNetwork === undefined) return - - switch (this.props.label) { - case 'Main Ethereum Network': - if (providerType === 'mainnet') return h('.check', '✓') - break - case 'Ropsten Test Network': - if (providerType === 'ropsten') return h('.check', '✓') - break - case 'Kovan Test Network': - if (providerType === 'kovan') return h('.check', '✓') - break - case 'Rinkeby Test Network': - if (providerType === 'rinkeby') return h('.check', '✓') - break - case 'Localhost 8545': - if (activeNetwork === 'http://localhost:8545') return h('.check', '✓') - break - default: - if (activeNetwork === 'custom') return h('.check', '✓') - } -} -- cgit v1.2.3 From f329c232a23e849a178381e92f3042d1d97303f2 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Tue, 18 Jul 2017 05:26:31 -0700 Subject: Hook up new dropdown components --- ui/classic/app/components/editable-label.js | 3 - ui/classic/app/components/network.js | 1 - ui/responsive/app/account-detail.js | 102 ++++------ ui/responsive/app/app.js | 224 +++++++++++----------- ui/responsive/app/components/account-dropdowns.js | 2 +- ui/responsive/app/components/editable-label.js | 8 +- ui/responsive/app/components/network.js | 1 - 7 files changed, 157 insertions(+), 184 deletions(-) diff --git a/ui/classic/app/components/editable-label.js b/ui/classic/app/components/editable-label.js index 48ba5060e..41936f5e0 100644 --- a/ui/classic/app/components/editable-label.js +++ b/ui/classic/app/components/editable-label.js @@ -13,7 +13,6 @@ function EditableLabel () { EditableLabel.prototype.render = function () { const props = this.props const state = this.state - console.log("editing:", state.isEditingLabel); if (state && state.isEditingLabel) { return h('div.editable-label', [ @@ -31,8 +30,6 @@ EditableLabel.prototype.render = function () { } else { return h('div.name-label', { onClick: (event) => { - debugger; - console.log("event", event.target); this.setState({ isEditingLabel: true }) }, }, this.props.children) diff --git a/ui/classic/app/components/network.js b/ui/classic/app/components/network.js index d5d3e18cd..698a0bbb9 100644 --- a/ui/classic/app/components/network.js +++ b/ui/classic/app/components/network.js @@ -39,7 +39,6 @@ Network.prototype.render = function () { }), h('i.fa.fa-sort-desc'), ]) - } else if (providerName === 'mainnet') { hoverText = 'Main Ethereum Network' iconName = 'ethereum-network' diff --git a/ui/responsive/app/account-detail.js b/ui/responsive/app/account-detail.js index 9a837a121..da1ddf98b 100644 --- a/ui/responsive/app/account-detail.js +++ b/ui/responsive/app/account-detail.js @@ -3,23 +3,18 @@ const extend = require('xtend') const Component = require('react').Component const h = require('react-hyperscript') const connect = require('react-redux').connect -const CopyButton = require('./components/copyButton') -const AccountInfoLink = require('./components/account-info-link') const actions = require('./actions') const ReactCSSTransitionGroup = require('react-addons-css-transition-group') const valuesFor = require('./util').valuesFor - const Identicon = require('./components/identicon') const EthBalance = require('./components/eth-balance') const TransactionList = require('./components/transaction-list') const ExportAccountView = require('./components/account-export') const ethUtil = require('ethereumjs-util') const EditableLabel = require('./components/editable-label') -const Tooltip = require('./components/tooltip') const TabBar = require('./components/tab-bar') const TokenList = require('./components/token-list') -const AccountOptionsMenus = require('./components/account-options-menus').AccountOptionsMenus; -console.log("AOM",AccountOptionsMenus); +const AccountDropdowns = require('./components/account-dropdowns').AccountDropdowns module.exports = connect(mapStateToProps)(AccountDetailScreen) @@ -53,8 +48,6 @@ AccountDetailScreen.prototype.render = function () { var identity = props.identities[selected] var account = props.accounts[selected] const { network, conversionRate, currentCurrency } = props - console.log("identity:", identity); - console.log("result:", identity && identity.name); return ( @@ -103,10 +96,41 @@ AccountDetailScreen.prototype.render = function () { // What is shown when not editing + edit text: h('label.editing-label', [h('.edit-text', 'edit')]), - h('h2.font-medium.color-forest', {name: 'edit'}, [ - identity && identity.name, - h(AccountOptionsMenus, { style: { marginLeft: '35%' }}, []), - ]), + h( + 'div', + { + style: { + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'center', + }, + }, + [ + h( + 'h2.font-medium.color-forest', + { + name: 'edit', + style: { + }, + }, + [ + identity && identity.name, + ] + ), + h( + AccountDropdowns, + { + style: { + marginRight: '8px', + marginLeft: 'auto', + }, + selected, + network, + identities: props.identities, + }, + ), + ] + ), ]), h('.flex-row', { style: { @@ -132,56 +156,6 @@ AccountDetailScreen.prototype.render = function () { color: '#AEAEAE', }, }, checksumAddress), - - // copy and export - - h('.flex-row', { - style: { - justifyContent: 'flex-end', - }, - }, [ - - h(AccountInfoLink, { selected, network }), - - h(CopyButton, { - value: checksumAddress, - }), - - h(Tooltip, { - title: 'QR Code', - }, [ - h('i.fa.fa-qrcode.pointer.pop-hover', { - onClick: () => props.dispatch(actions.showQrView(selected, identity ? identity.name : '')), - style: { - fontSize: '18px', - position: 'relative', - color: 'rgb(247, 134, 28)', - top: '5px', - marginLeft: '3px', - marginRight: '3px', - }, - }), - ]), - - h(Tooltip, { - title: 'Export Private Key', - }, [ - h('div', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - h('img.cursor-pointer.color-orange', { - src: 'images/key-32.png', - onClick: () => this.requestAccountExport(selected), - style: { - height: '19px', - }, - }), - ]), - ]), - ]), ]), // account ballence @@ -313,7 +287,3 @@ AccountDetailScreen.prototype.transactionList = function () { }, }) } - -AccountDetailScreen.prototype.requestAccountExport = function () { - this.props.dispatch(actions.requestExportAccount()) -} diff --git a/ui/responsive/app/app.js b/ui/responsive/app/app.js index f829dc8fa..1ac9bea78 100644 --- a/ui/responsive/app/app.js +++ b/ui/responsive/app/app.js @@ -10,7 +10,6 @@ const NewKeyChainScreen = require('./new-keychain') // unlock const UnlockScreen = require('./unlock') // accounts -const AccountsScreen = require('./accounts') const AccountDetailScreen = require('./account-detail') const SendTransactionScreen = require('./send') const ConfirmTxScreen = require('./conf-tx') @@ -24,11 +23,9 @@ const Import = require('./accounts/import') const InfoScreen = require('./info') const Loading = require('./components/loading') const SandwichExpando = require('sandwich-expando') -const MenuDroppo = require('menu-droppo') -const DropMenuItem = require('./components/drop-menu-item') -import { Dropdown, DropdownMenuItem } from './components/dropdown'; +const Dropdown = require('./components/dropdown').Dropdown +const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem const NetworkIndicator = require('./components/network') -const Tooltip = require('./components/tooltip') const BuyView = require('./components/buy-button-subview') const QrView = require('./components/qr-code') const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete') @@ -126,7 +123,7 @@ App.prototype.renderAppBar = function () { alignItems: 'center', visibility: props.isUnlocked ? 'visible' : 'none', background: props.isUnlocked ? 'white' : 'none', - height: '36px', + height: '38px', position: 'relative', zIndex: 12, }, @@ -174,21 +171,6 @@ App.prototype.renderAppBar = function () { }, }, [ - // small accounts nav - props.isUnlocked && h(Tooltip, { title: 'Switch Accounts' }, [ - h('img.cursor-pointer.color-orange', { - src: 'images/switch_acc.svg', - style: { - width: '23.5px', - marginRight: '8px', - }, - onClick: (event) => { - event.stopPropagation() - this.props.dispatch(actions.showAccountsPage()) - }, - }), - ]), - // hamburger props.isUnlocked && h(SandwichExpando, { width: 16, @@ -210,11 +192,12 @@ App.prototype.renderAppBar = function () { App.prototype.renderNetworkDropdown = function () { const props = this.props + const { provider: { type: providerType, rpcTarget: activeNetwork } } = props const rpcList = props.frequentRpcList const state = this.state || {} const isOpen = state.isNetworkMenuOpen - return h(MenuDroppo, { + return h(Dropdown, { isOpen, onClickOutside: (event) => { this.setState({ isNetworkMenuOpen: !isOpen }) @@ -222,73 +205,90 @@ App.prototype.renderNetworkDropdown = function () { zIndex: 11, style: { position: 'absolute', - left: 0, + left: '2px', top: '36px', }, - innerStyle: { - background: 'white', - boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', - }, - }, [ // DROP MENU ITEMS - h('style', ` - .drop-menu-item:hover { background:rgb(235, 235, 235); } - .drop-menu-item i { margin: 11px; } - `), - - h(DropMenuItem, { - label: 'Main Ethereum Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setProviderType('mainnet')), - icon: h('.menu-icon.diamond'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Ropsten Test Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setProviderType('ropsten')), - icon: h('.menu-icon.red-dot'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Kovan Test Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false}), - action: () => props.dispatch(actions.setProviderType('kovan')), - icon: h('.menu-icon.hollow-diamond'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Rinkeby Test Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false}), - action: () => props.dispatch(actions.setProviderType('rinkeby')), - icon: h('.menu-icon.golden-square'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Localhost 8545', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setDefaultRpcTarget(rpcList)), - icon: h('i.fa.fa-question-circle.fa-lg'), - activeNetworkRender: props.provider.rpcTarget, - }), + innerStyle: {}, + }, [ + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setProviderType('mainnet')), + }, + [ + h('.menu-icon.diamond'), + 'Main Ethereum Network', + providerType === 'mainnet' ? h('.check', '✓') : null, + ] + ), + + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setProviderType('ropsten')), + }, + [ + h('.menu-icon.red-dot'), + 'Ropsten Test Network', + providerType === 'ropsten' ? h('.check', '✓') : null, + ]), + + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setProviderType('kovan')), + }, + [ + h('.menu-icon.hollow-diamond'), + 'Kovan Test Network', + providerType === 'kovan' ? h('.check', '✓') : null, + ] + ), + + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setProviderType('rinkeby')), + }, + [ + h('.menu-icon.golden-square'), + 'Rinkeby Test Network', + providerType === 'rinkeby' ? h('.check', '✓') : null, + ] + ), + + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setDefaultRpcTarget(rpcList)), + }, + [ + h('i.fa.fa-question-circle.fa-lg.menu-icon'), + 'Localhost 8545', + activeNetwork === 'http://localhost:8545' ? h('.check', '✓') : null, + ] + ), this.renderCustomOption(props.provider), this.renderCommonRpc(rpcList, props.provider), - h(DropMenuItem, { - label: 'Custom RPC', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => this.props.dispatch(actions.showConfigPage()), - icon: h('i.fa.fa-question-circle.fa-lg'), - }), - + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => this.props.dispatch(actions.showConfigPage()), + }, + [ + h('i.fa.fa-question-circle.fa-lg.menu-icon'), + 'Custom RPC', + activeNetwork === 'custom' ? h('.check', '✓') : null, + ] + ), ]) } @@ -304,29 +304,29 @@ App.prototype.renderDropdown = function () { }, style: { position: 'absolute', - right: 0, - top: '36px', + right: '2px', + top: '38px', }, innerStyle: {}, - }, [ // DROP MENU ITEMS + }, [ h(DropdownMenuItem, { closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - onClick: () => this.props.dispatch(actions.showConfigPage()), + onClick: () => { this.props.dispatch(actions.showConfigPage()) }, }, 'Settings'), h(DropdownMenuItem, { closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - onClick: () => this.props.dispatch(actions.showImportPage()), + onClick: () => { this.props.dispatch(actions.showImportPage()) }, }, 'Import Account'), h(DropdownMenuItem, { closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - onClick: () => this.props.dispatch(actions.lockMetamask()), + onClick: () => { this.props.dispatch(actions.lockMetamask()) }, }, 'Lock'), h(DropdownMenuItem, { closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - onClick: () => this.props.dispatch(actions.showInfoPage()), + onClick: () => { this.props.dispatch(actions.showInfoPage()) }, }, 'Info/Help'), ]) } @@ -413,10 +413,6 @@ App.prototype.renderPrimary = function () { // show current view switch (props.currentView.name) { - case 'accounts': - log.debug('rendering accounts screen') - return h(AccountsScreen, {key: 'accounts'}) - case 'accountDetail': log.debug('rendering account detail screen') return h(AccountDetailScreen, {key: 'account-detail'}) @@ -519,13 +515,18 @@ App.prototype.renderCustomOption = function (provider) { return null default: - return h(DropMenuItem, { - label, - key: rpcTarget, - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - icon: h('i.fa.fa-question-circle.fa-lg'), - activeNetworkRender: 'custom', - }) + return h( + DropdownMenuItem, + { + key: rpcTarget, + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + }, + [ + h('i.fa.fa-question-circle.fa-lg.menu-icon'), + label, + h('.check', '✓'), + ] + ) } } @@ -558,14 +559,19 @@ App.prototype.renderCommonRpc = function (rpcList, provider) { if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { return null } else { - return h(DropMenuItem, { - label: rpc, - key: rpc, - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setRpcTarget(rpc)), - icon: h('i.fa.fa-question-circle.fa-lg'), - activeNetworkRender: rpc, - }) + return h( + DropdownMenuItem, + { + key: rpc, + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setRpcTarget(rpc)), + }, + [ + h('i.fa.fa-question-circle.fa-lg.menu-icon'), + rpc, + h('.check', '✓'), + ] + ) } }) } diff --git a/ui/responsive/app/components/account-dropdowns.js b/ui/responsive/app/components/account-dropdowns.js index cbb97b2cb..f77d2fe9c 100644 --- a/ui/responsive/app/components/account-dropdowns.js +++ b/ui/responsive/app/components/account-dropdowns.js @@ -200,7 +200,7 @@ AccountDropdowns.propTypes = { selected: PropTypes.string, } -const mapDispatchToProps = (dispatch, ownProps) => { +const mapDispatchToProps = (dispatch) => { return { actions: { showConfigPage: () => dispatch(actions.showConfigPage()), diff --git a/ui/responsive/app/components/editable-label.js b/ui/responsive/app/components/editable-label.js index 43841bdd8..167be7eaf 100644 --- a/ui/responsive/app/components/editable-label.js +++ b/ui/responsive/app/components/editable-label.js @@ -30,15 +30,17 @@ EditableLabel.prototype.render = function () { } else { return h('div.name-label', { onClick: (event) => { - if (event.target.getAttribute('name') === 'edit') { + const nameAttribute = event.target.getAttribute('name') + // checks for class to handle smaller CTA above the account name + const classAttribute = event.target.getAttribute('class') + if (nameAttribute === 'edit' || classAttribute === 'edit-text') { this.setState({ isEditingLabel: true }) } }, }, this.props.children) } } -// class = edit-text -// name = edit + EditableLabel.prototype.saveIfEnter = function (event) { if (event.key === 'Enter') { this.saveText() diff --git a/ui/responsive/app/components/network.js b/ui/responsive/app/components/network.js index d5d3e18cd..698a0bbb9 100644 --- a/ui/responsive/app/components/network.js +++ b/ui/responsive/app/components/network.js @@ -39,7 +39,6 @@ Network.prototype.render = function () { }), h('i.fa.fa-sort-desc'), ]) - } else if (providerName === 'mainnet') { hoverText = 'Main Ethereum Network' iconName = 'ethereum-network' -- cgit v1.2.3 From 0ebec8d0a8da85177128902210ba7b7a0a5c3d38 Mon Sep 17 00:00:00 2001 From: tmashuang Date: Tue, 18 Jul 2017 09:08:24 -0700 Subject: Change Ci Badge url for readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index afeb96ae5..309423f4c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# MetaMask Plugin [![Build Status](https://circleci.com/gh/MetaMask/metamask-plugin.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-plugin) +# MetaMask Plugin [![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=svg&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) ## Developing Compatible Dapps -- cgit v1.2.3 From d9a195f86b88b4c52b200fdba6ed3c0658251f11 Mon Sep 17 00:00:00 2001 From: tmashuang Date: Tue, 18 Jul 2017 09:20:48 -0700 Subject: Shield looks better --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 309423f4c..45fb68c78 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# MetaMask Plugin [![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=svg&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) +# MetaMask Plugin [![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) ## Developing Compatible Dapps -- cgit v1.2.3 From 9e8e445695585f47e9cc3f63b2ec8313b4fc4eb8 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Tue, 18 Jul 2017 09:29:52 -0700 Subject: Reorder Account Selector and Account Options --- ui/responsive/app/app.js | 5 +- ui/responsive/app/components/account-dropdowns.js | 238 +++++++++++----------- 2 files changed, 128 insertions(+), 115 deletions(-) diff --git a/ui/responsive/app/app.js b/ui/responsive/app/app.js index 1ac9bea78..1cfa2d7a9 100644 --- a/ui/responsive/app/app.js +++ b/ui/responsive/app/app.js @@ -210,6 +210,7 @@ App.prototype.renderNetworkDropdown = function () { }, innerStyle: {}, }, [ + h( DropdownMenuItem, { @@ -233,7 +234,8 @@ App.prototype.renderNetworkDropdown = function () { h('.menu-icon.red-dot'), 'Ropsten Test Network', providerType === 'ropsten' ? h('.check', '✓') : null, - ]), + ] + ), h( DropdownMenuItem, @@ -289,6 +291,7 @@ App.prototype.renderNetworkDropdown = function () { activeNetwork === 'custom' ? h('.check', '✓') : null, ] ), + ]) } diff --git a/ui/responsive/app/components/account-dropdowns.js b/ui/responsive/app/components/account-dropdowns.js index f77d2fe9c..d1d319477 100644 --- a/ui/responsive/app/components/account-dropdowns.js +++ b/ui/responsive/app/components/account-dropdowns.js @@ -14,12 +14,12 @@ class AccountDropdowns extends Component { constructor (props) { super(props) this.state = { - overflowMenuActive: false, - switchingMenuActive: false, + accountSelectorActive: false, + optionsMenuActive: false, } } - getAccounts () { + renderAccounts () { const { identities, selected } = this.props return Object.keys(identities).map((key) => { @@ -49,9 +49,122 @@ class AccountDropdowns extends Component { }) } + renderAccountSelector () { + const { actions } = this.props + const { accountSelectorActive } = this.state + + return h( + Dropdown, + { + style: { + marginLeft: '-125px', + minWidth: '180px', + }, + isOpen: accountSelectorActive, + onClickOutside: () => { this.setState({ accountSelectorActive: false }) }, + }, + [ + ...this.renderAccounts(), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => actions.addNewAccount(), + }, + [ + h( + Identicon, + { + diameter: 16, + }, + ), + h('span', { style: { marginLeft: '10px' } }, 'Create Account'), + ], + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => actions.showImportPage(), + }, + [ + h( + Identicon, + { + diameter: 16, + }, + ), + h('span', { style: { marginLeft: '10px' } }, 'Import Account'), + ] + ), + ] + ) + } + + renderAccountOptions () { + const { actions } = this.props + const { optionsMenuActive } = this.state + + return h( + Dropdown, + { + style: { + marginLeft: '-162px', + minWidth: '180px', + }, + isOpen: optionsMenuActive, + onClickOutside: () => { this.setState({ optionsMenuActive: false }) }, + }, + [ + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => actions.showConfigPage(), + }, + 'Account Settings', + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + const { selected, network } = this.props + const url = genAccountLink(selected, network) + global.platform.openWindow({ url }) + }, + }, + 'View account on Etherscan', + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + const { selected } = this.props + const checkSumAddress = selected && ethUtil.toChecksumAddress(selected) + copyToClipboard(checkSumAddress) + }, + }, + 'Copy Address to clipboard', + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + actions.requestAccountExport() + }, + }, + 'Export Private Key', + ), + ] + ) + } + render () { - const { style, actions } = this.props - const { switchingMenuActive, overflowMenuActive } = this.state + const { style } = this.props + const { optionsMenuActive, accountSelectorActive } = this.state return h( 'span', @@ -66,68 +179,12 @@ class AccountDropdowns extends Component { onClick: (event) => { event.stopPropagation() this.setState({ - switchingMenuActive: !switchingMenuActive, - overflowMenuActive: false, + accountSelectorActive: !accountSelectorActive, + optionsMenuActive: false, }) }, }, - [ - h( - Dropdown, - { - style: { - marginLeft: '-140px', - minWidth: '180px', - }, - isOpen: switchingMenuActive, - onClickOutside: () => { this.setState({ switchingMenuActive: false}) }, - }, - [ - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => actions.showConfigPage(), - }, - 'Account Settings', - ), - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => { - const { selected, network } = this.props - const url = genAccountLink(selected, network) - global.platform.openWindow({ url }) - }, - }, - 'View account on Etherscan', - ), - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => { - const { selected } = this.props - const checkSumAddress = selected && ethUtil.toChecksumAddress(selected) - copyToClipboard(checkSumAddress) - }, - }, - 'Copy Address to clipboard', - ), - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => { - actions.requestAccountExport() - }, - }, - 'Export Private Key', - ), - ] - ), - ], + this.renderAccountSelector(), ), h( 'i.fa.fa-ellipsis-h', @@ -136,59 +193,12 @@ class AccountDropdowns extends Component { onClick: (event) => { event.stopPropagation() this.setState({ - overflowMenuActive: !overflowMenuActive, - switchingMenuActive: false, + accountSelectorActive: false, + optionsMenuActive: !optionsMenuActive, }) }, }, - [ - h( - Dropdown, - { - style: { - marginLeft: '-155px', - minWidth: '180px', - }, - isOpen: overflowMenuActive, - onClickOutside: () => { this.setState({ overflowMenuActive: false}) }, - }, - [ - ...this.getAccounts(), - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => actions.addNewAccount(), - }, - [ - h( - Identicon, - { - diameter: 16, - }, - ), - h('span', { style: { marginLeft: '10px' } }, 'Create Account'), - ], - ), - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => actions.showImportPage(), - }, - [ - h( - Identicon, - { - diameter: 16, - }, - ), - h('span', { style: { marginLeft: '10px' } }, 'Import Account'), - ] - ), - ] - ), - ] + this.renderAccountOptions() ), ] ) -- cgit v1.2.3 From 4f9fc8014a1830c9ec7bd35b77f5ee4f5a089fb8 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 18 Jul 2017 12:48:16 -0700 Subject: nonce-tracker - validate nonce calc components --- app/scripts/lib/nonce-tracker.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js index ab2893b10..cea798915 100644 --- a/app/scripts/lib/nonce-tracker.js +++ b/app/scripts/lib/nonce-tracker.js @@ -1,4 +1,5 @@ const EthQuery = require('eth-query') +const assert = require('assert') class NonceTracker { @@ -21,10 +22,15 @@ class NonceTracker { // and pending count are from the same block const currentBlock = await this._getCurrentBlock() const pendingTransactions = this.getPendingTransactions(address) - const baseCount = await this._getTxCount(address, currentBlock) - const nextNonce = parseInt(baseCount) + pendingTransactions.length + const pendingCount = pendingTransactions.length + assert(Number.isInteger(pendingCount), 'nonce-tracker - pendingCount is an integer') + const baseCountHex = await this._getTxCount(address, currentBlock) + const baseCount = parseInt(baseCountHex, 16) + assert(Number.isInteger(baseCount), 'nonce-tracker - baseCount is an integer') + const nextNonce = baseCount + pendingCount + assert(Number.isInteger(nextNonce), 'nonce-tracker - nextNonce is an integer') // return next nonce and release cb - return { nextNonce: nextNonce.toString(16), releaseLock } + return { nextNonce: '0x' + nextNonce.toString(16), releaseLock } } async _getCurrentBlock () { -- cgit v1.2.3 From d249da77d7f9602507f708b4778c2128737d4dc5 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 18 Jul 2017 13:59:56 -0700 Subject: nonce-tracker - return nonce as integer --- app/scripts/lib/nonce-tracker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js index cea798915..ba05fd124 100644 --- a/app/scripts/lib/nonce-tracker.js +++ b/app/scripts/lib/nonce-tracker.js @@ -30,7 +30,7 @@ class NonceTracker { const nextNonce = baseCount + pendingCount assert(Number.isInteger(nextNonce), 'nonce-tracker - nextNonce is an integer') // return next nonce and release cb - return { nextNonce: '0x' + nextNonce.toString(16), releaseLock } + return { nextNonce, releaseLock } } async _getCurrentBlock () { -- cgit v1.2.3 From 67fdba5e42d8deac1dcbb4a82fd3d22b944e639a Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 18 Jul 2017 14:00:43 -0700 Subject: transaction - promisify _checkPendingTxs --- app/scripts/controllers/transactions.js | 68 ++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 61e96ca13..1fc48aadd 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -3,6 +3,7 @@ const async = require('async') const extend = require('xtend') const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') +const pify = require('pify') const TxProviderUtil = require('../lib/tx-utils') const createId = require('../lib/random-id') const NonceTracker = require('../lib/nonce-tracker') @@ -481,35 +482,48 @@ module.exports = class TransactionController extends EventEmitter { // checks the network for signed txs and // if confirmed sets the tx status as 'confirmed' - _checkPendingTxs () { - var signedTxList = this.getFilteredTxList({status: 'submitted'}) - if (!signedTxList.length) return - signedTxList.forEach((txMeta) => { - var txHash = txMeta.hash - var txId = txMeta.id - if (!txHash) { - const errReason = { - errCode: 'No hash was provided', - message: 'We had an error while submitting this transaction, please try again.', - } - return this.setTxStatusFailed(txId, errReason) + async _checkPendingTxs () { + const signedTxList = this.getFilteredTxList({status: 'submitted'}) + try { + await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta))) + } catch (err) { + console.error('TransactionController - Error updating pending transactions') + console.error(err) + } + } + + async _checkPendingTx (txMeta) { + const txHash = txMeta.hash + const txId = txMeta.id + // extra check in case there was an uncaught error during the + // signature and submission process + if (!txHash) { + const errReason = { + errCode: 'No hash was provided', + message: 'We had an error while submitting this transaction, please try again.', } - this.query.getTransactionByHash(txHash, (err, txParams) => { - if (err || !txParams) { - if (!txParams) return - txMeta.err = { - isWarning: true, - errorCode: err, - message: 'There was a problem loading this transaction.', - } - this.updateTx(txMeta) - return log.error(err) - } - if (txParams.blockNumber) { - this.setTxStatusConfirmed(txId) + this.setTxStatusFailed(txId, errReason) + return + } + // get latest transaction status + let txParams + try { + txParams = await pify((cb) => this.query.getTransactionByHash(txHash, cb))() + if (!txParams) return + if (txParams.blockNumber) { + this.setTxStatusConfirmed(txId) + } + } catch (err) { + if (err || !txParams) { + txMeta.err = { + isWarning: true, + errorCode: err, + message: 'There was a problem loading this transaction.', } - }) - }) + this.updateTx(txMeta) + log.error(err) + } + } } } -- cgit v1.2.3 From aa48ed34c458874914c44400fb68885069625a6f Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 18 Jul 2017 15:11:29 -0700 Subject: nonce-tracker - fix lock mechanism to be a real mutex --- app/scripts/lib/nonce-tracker.js | 26 +++++++++++++++----------- package.json | 1 + 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js index ba05fd124..7a450cb78 100644 --- a/app/scripts/lib/nonce-tracker.js +++ b/app/scripts/lib/nonce-tracker.js @@ -1,5 +1,6 @@ const EthQuery = require('eth-query') const assert = require('assert') +const Mutex = require('await-semaphore').Mutex class NonceTracker { @@ -13,10 +14,8 @@ class NonceTracker { // releaseLock must be called // releaseLock must be called after adding signed tx to pending transactions (or discarding) async getNonceLock (address) { - // await lock free - await this.lockMap[address] - // take lock - const releaseLock = this._takeLock(address) + // await lock free, then take lock + const releaseLock = await this._takeMutex(address) // calculate next nonce // we need to make sure our base count // and pending count are from the same block @@ -41,13 +40,18 @@ class NonceTracker { }) } - _takeLock (lockId) { - let releaseLock = null - // create and store lock - const lock = new Promise((resolve, reject) => { releaseLock = resolve }) - this.lockMap[lockId] = lock - // setup lock teardown - lock.then(() => delete this.lockMap[lockId]) + _lookupMutex (lockId) { + let mutex = this.lockMap[lockId] + if (!mutex) { + mutex = new Mutex() + this.lockMap[lockId] = mutex + } + return mutex + } + + async _takeMutex (lockId) { + const mutex = this._lookupMutex(lockId) + const releaseLock = await mutex.acquire() return releaseLock } diff --git a/package.json b/package.json index e0bb303bf..d40ad068b 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ }, "dependencies": { "async": "^1.5.2", + "await-semaphore": "^0.1.1", "babel-runtime": "^6.23.0", "bip39": "^2.2.0", "bluebird": "^3.5.0", -- cgit v1.2.3 From 12d6f2162791b421bd51313b0063e144b47ed868 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 18 Jul 2017 15:27:15 -0700 Subject: transactions - block nonce-tracker while updating pending transactions --- app/scripts/controllers/transactions.js | 3 +++ app/scripts/lib/nonce-tracker.js | 43 ++++++++++++++++++++++----------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 1fc48aadd..5f3d84ebe 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -484,12 +484,15 @@ module.exports = class TransactionController extends EventEmitter { // if confirmed sets the tx status as 'confirmed' async _checkPendingTxs () { const signedTxList = this.getFilteredTxList({status: 'submitted'}) + // in order to keep the nonceTracker accurate we block it while updating pending transactions + const nonceGlobalLock = await this.nonceTracker.getGlobalLock() try { await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta))) } catch (err) { console.error('TransactionController - Error updating pending transactions') console.error(err) } + nonceGlobalLock.releaseLock() } async _checkPendingTx (txMeta) { diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js index 7a450cb78..b76dac4e8 100644 --- a/app/scripts/lib/nonce-tracker.js +++ b/app/scripts/lib/nonce-tracker.js @@ -11,9 +11,18 @@ class NonceTracker { this.lockMap = {} } + async getGlobalLock () { + const globalMutex = this._lookupMutex('global') + // await global mutex free + const releaseLock = await globalMutex.acquire() + return { releaseLock } + } + // releaseLock must be called // releaseLock must be called after adding signed tx to pending transactions (or discarding) async getNonceLock (address) { + // await global mutex free + await this._globalMutexFree() // await lock free, then take lock const releaseLock = await this._takeMutex(address) // calculate next nonce @@ -40,13 +49,19 @@ class NonceTracker { }) } - _lookupMutex (lockId) { - let mutex = this.lockMap[lockId] - if (!mutex) { - mutex = new Mutex() - this.lockMap[lockId] = mutex - } - return mutex + async _getTxCount (address, currentBlock) { + const blockNumber = currentBlock.number + return new Promise((resolve, reject) => { + this.ethQuery.getTransactionCount(address, blockNumber, (err, result) => { + err ? reject(err) : resolve(result) + }) + }) + } + + async _globalMutexFree () { + const globalMutex = this._lookupMutex('global') + const release = await globalMutex.acquire() + release() } async _takeMutex (lockId) { @@ -55,13 +70,13 @@ class NonceTracker { return releaseLock } - async _getTxCount (address, currentBlock) { - const blockNumber = currentBlock.number - return new Promise((resolve, reject) => { - this.ethQuery.getTransactionCount(address, blockNumber, (err, result) => { - err ? reject(err) : resolve(result) - }) - }) + _lookupMutex (lockId) { + let mutex = this.lockMap[lockId] + if (!mutex) { + mutex = new Mutex() + this.lockMap[lockId] = mutex + } + return mutex } } -- cgit v1.2.3 From 2832713a46f74131ecf8111a49572ec98dd73bb6 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 18 Jul 2017 15:30:32 -0700 Subject: changelog - add nonce-tracker note --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d15b3512..4d265d318 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Now redirects from known malicious sites faster. - Added a link to our new support page to the help screen. - Fixed bug where a new transaction would be shown over the current transaction, creating a possible timing attack against user confirmation. +- Fixed bug in nonce tracker where an incorrect nonce would be calculated. ## 3.9.0 2017-7-12 -- cgit v1.2.3 From 82aa0d48d4e98c36bc1d90800d439a938b51ab03 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 18 Jul 2017 22:41:30 +0000 Subject: chore(package): update dependencies --- package.json | 64 ++++++++++++++++++++++++++++++------------------------------ 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index d40ad068b..2e66080d6 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ ] }, "dependencies": { - "async": "^1.5.2", + "async": "^2.5.0", "await-semaphore": "^0.1.1", "babel-runtime": "^6.23.0", "bip39": "^2.2.0", @@ -54,10 +54,10 @@ "browser-passworder": "^2.0.3", "browserify-derequire": "^0.9.4", "client-sw-ready-event": "^3.3.0", - "clone": "^1.0.2", - "copy-to-clipboard": "^2.0.0", + "clone": "^2.1.1", + "copy-to-clipboard": "^3.0.6", "debounce": "^1.0.0", - "deep-extend": "^0.4.1", + "deep-extend": "^0.5.0", "detect-node": "^2.0.3", "disc": "^1.3.2", "dnode": "^1.2.2", @@ -78,12 +78,12 @@ "express": "^4.14.0", "extension-link-enabler": "^1.0.0", "extensionizer": "^1.0.0", - "gulp-eslint": "^2.0.0", + "gulp-eslint": "^4.0.0", "hat": "0.0.3", - "idb-global": "^1.0.0", - "identicon.js": "^1.2.1", + "idb-global": "^2.1.0", + "identicon.js": "^2.3.1", "iframe": "^1.0.0", - "iframe-stream": "^1.0.2", + "iframe-stream": "^3.0.0", "inject-css": "^0.1.1", "jazzicon": "^1.2.0", "loglevel": "^1.4.1", @@ -98,7 +98,7 @@ "ping-pong-stream": "^1.0.0", "pojo-migrator": "^2.1.0", "polyfill-crypto.getrandomvalues": "^1.0.0", - "post-message-stream": "^1.0.0", + "post-message-stream": "^3.0.0", "promise-filter": "^1.1.0", "promise-to-callback": "^1.0.0", "pump": "^1.0.2", @@ -107,33 +107,33 @@ "react": "^15.0.2", "react-addons-css-transition-group": "^15.0.2", "react-dom": "^15.5.4", - "react-hyperscript": "^2.2.2", + "react-hyperscript": "^3.0.0", "react-markdown": "^2.3.0", - "react-redux": "^4.4.5", + "react-redux": "^5.0.5", "react-select": "^1.0.0-rc.2", "react-simple-file-input": "^1.0.0", "react-tooltip-component": "^0.3.0", "readable-stream": "^2.1.2", "redux": "^3.0.5", - "redux-logger": "^2.10.2", - "redux-thunk": "^1.0.2", + "redux-logger": "^3.0.6", + "redux-thunk": "^2.2.0", "request-promise": "^4.1.1", "sandwich-expando": "^1.0.5", "semaphore": "^1.0.5", "sw-stream": "^2.0.0", "textarea-caret": "^3.0.1", - "three.js": "^0.73.2", + "three.js": "^0.77.1", "through2": "^2.0.1", "valid-url": "^1.0.9", "vreme": "^3.0.2", - "web3": "0.19.1", + "web3": "0.20.1", "web3-provider-engine": "^13.2.8", "web3-stream-provider": "^3.0.1", "xtend": "^4.0.1" }, "devDependencies": { "babel-core": "^6.24.1", - "babel-eslint": "^6.0.5", + "babel-eslint": "^7.2.3", "babel-plugin-transform-async-to-generator": "^6.24.1", "babel-plugin-transform-runtime": "^6.23.0", "babel-polyfill": "^6.23.0", @@ -142,45 +142,45 @@ "babelify": "^7.2.0", "beefy": "^2.1.5", "brfs": "^1.4.3", - "browserify": "^13.0.0", - "chai": "^3.5.0", + "browserify": "^14.4.0", + "chai": "^4.1.0", "deep-freeze-strict": "^1.1.1", - "del": "^2.2.0", + "del": "^3.0.0", "envify": "^4.0.0", "enzyme": "^2.8.2", "eslint-plugin-chai": "0.0.1", "eslint-plugin-mocha": "^4.9.0", - "fs-promise": "^1.0.0", + "fs-promise": "^2.0.3", "gulp": "github:gulpjs/gulp#4.0", "gulp-if": "^2.0.1", "gulp-json-editor": "^2.2.1", "gulp-livereload": "^3.8.1", - "gulp-replace": "^0.5.4", - "gulp-sourcemaps": "^1.6.0", + "gulp-replace": "^0.6.1", + "gulp-sourcemaps": "^2.6.0", "gulp-util": "^3.0.7", "gulp-watch": "^4.3.5", - "gulp-zip": "^3.2.0", + "gulp-zip": "^4.0.0", "isomorphic-fetch": "^2.2.1", - "jsdom": "^8.1.0", - "jsdom-global": "^1.7.0", - "jshint-stylish": "~0.1.5", + "jsdom": "^11.1.0", + "jsdom-global": "^3.0.2", + "jshint-stylish": "~2.2.1", "lodash.assign": "^4.0.6", - "mocha": "^2.4.5", - "mocha-eslint": "^2.1.1", + "mocha": "^3.4.2", + "mocha-eslint": "^4.0.0", "mocha-jsdom": "^1.1.0", - "mocha-sinon": "^1.1.5", - "nock": "^8.0.0", + "mocha-sinon": "^2.0.0", + "nock": "^9.0.14", "open": "0.0.5", "prompt": "^1.0.0", "qs": "^6.2.0", - "qunit": "^0.9.1", + "qunit": "^1.0.0", "react-addons-test-utils": "^15.5.1", "react-test-renderer": "^15.5.4", "react-testutils-additions": "^15.2.0", "sinon": "^2.3.8", "tape": "^4.5.1", "testem": "^1.10.3", - "uglifyify": "^3.0.1", + "uglifyify": "^4.0.2", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0", "watchify": "^3.7.0" -- cgit v1.2.3 From eff9a578f7c6f9435d7fabc3ec5efb7619b825d9 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 18 Jul 2017 22:41:33 +0000 Subject: docs(readme): add Greenkeeper badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 45fb68c78..90598d4c3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # MetaMask Plugin [![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) +[![Greenkeeper badge](https://badges.greenkeeper.io/MetaMask/metamask-extension.svg)](https://greenkeeper.io/) + ## Developing Compatible Dapps If you're a web dapp developer, we've got two types of guides for you: -- cgit v1.2.3 From 51c5bebdf5ec4c4ac0e2878bd503e39a379d79c2 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 19 Jul 2017 11:44:00 -0700 Subject: Lowered minimum gas price to 1 gwei --- CHANGELOG.md | 1 + ui/app/components/pending-tx.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d265d318..e68a7ab89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Added a link to our new support page to the help screen. - Fixed bug where a new transaction would be shown over the current transaction, creating a possible timing attack against user confirmation. - Fixed bug in nonce tracker where an incorrect nonce would be calculated. +- Lowered minimum gas price to 1 Gwei. ## 3.9.0 2017-7-12 diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index d7d602f31..5324ccd64 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -15,7 +15,7 @@ const addressSummary = util.addressSummary const nameForAddress = require('../../lib/contract-namer') const BNInput = require('./bn-as-decimal-input') -const MIN_GAS_PRICE_GWEI_BN = new BN(2) +const MIN_GAS_PRICE_GWEI_BN = new BN(1) const GWEI_FACTOR = new BN(1e9) const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) const MIN_GAS_LIMIT_BN = new BN(21000) -- cgit v1.2.3 From dcf025782b01265710a61a63da60d7d006df06b0 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 19 Jul 2017 11:56:53 -0700 Subject: Version 3.9.1 --- CHANGELOG.md | 2 ++ app/manifest.json | 2 +- package.json | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e68a7ab89..bf18bb361 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +## 3.9.1 2017-7-19 + - No longer automatically request 1 ropsten ether for the first account in a new vault. - Now redirects from known malicious sites faster. - Added a link to our new support page to the help screen. diff --git a/app/manifest.json b/app/manifest.json index 7bf757d4c..eadd99590 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.9.0", + "version": "3.9.1", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", diff --git a/package.json b/package.json index d40ad068b..375902d09 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "eth-contract-metadata": "^1.1.4", "eth-hd-keyring": "^1.1.1", "eth-query": "^2.1.2", - "eth-sig-util": "^1.1.1", + "eth-sig-util": "^1.2.2", "eth-simple-keyring": "^1.1.1", "eth-token-tracker": "^1.1.2", "etheraddresslookup": "github:409H/EtherAddressLookup", -- cgit v1.2.3 From 9018810ac20e7f73a34af564f208231196179f57 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 20 Jul 2017 09:55:10 -0700 Subject: Update tutorial links --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 45fb68c78..bf1f37d1f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,13 @@ If you're a web dapp developer, we've got two types of guides for you: -- If you've never built a Dapp before, we've got a gentle introduction on [Developing Dapps with Truffle and MetaMask](https://blog.metamask.io/developing-for-metamask-with-truffle/). +### New Dapp Developers + +- We recommend this [Learning Solidity](https://karl.tech/learning-solidity-part-1-deploy-a-contract/) tutorial series by Karl Floersch. +- We wrote a (slightly outdated now) gentle introduction on [Developing Dapps with Truffle and MetaMask](https://medium.com/metamask/developing-ethereum-dapps-with-truffle-and-metamask-aa8ad7e363ba). + +### Current Dapp Developers + - If you have a Dapp, and you want to ensure compatibility, [here is our guide on building MetaMask-compatible Dapps](https://github.com/MetaMask/faq/blob/master/DEVELOPERS.md) ## Building locally -- cgit v1.2.3 From 366ddc62488c99494b3973e157bf9a1c9d5af0cb Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 20 Jul 2017 09:55:47 -0700 Subject: Add user support link --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index bf1f37d1f..d7086ae91 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # MetaMask Plugin [![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) +## Support + +If you're a user seeking support, [here is our support site](http://metamask.consensyssupport.happyfox.com). + ## Developing Compatible Dapps If you're a web dapp developer, we've got two types of guides for you: -- cgit v1.2.3 From 86d367957fe8ac04462f716fe0ba2bfa4e5ff3f6 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 20 Jul 2017 12:38:38 -0700 Subject: Move responsive ui into its own folder for easier merges --- app/scripts/popup-core.js | 2 +- app/scripts/popup.js | 2 +- app/scripts/responsive-core.js | 2 +- app/scripts/responsive.js | 2 +- responsive-ui/.gitignore | 66 + responsive-ui/app/account-detail.js | 289 +++ responsive-ui/app/accounts/import/index.js | 100 + responsive-ui/app/accounts/import/json.js | 100 + responsive-ui/app/accounts/import/private-key.js | 67 + responsive-ui/app/accounts/import/seed.js | 30 + responsive-ui/app/actions.js | 1031 +++++++++ responsive-ui/app/add-token.js | 219 ++ responsive-ui/app/app.js | 580 +++++ responsive-ui/app/components/account-dropdowns.js | 227 ++ responsive-ui/app/components/account-export.js | 122 + responsive-ui/app/components/account-panel.js | 86 + responsive-ui/app/components/balance.js | 89 + responsive-ui/app/components/binary-renderer.js | 46 + .../app/components/bn-as-decimal-input.js | 174 ++ responsive-ui/app/components/buy-button-subview.js | 197 ++ responsive-ui/app/components/coinbase-form.js | 63 + responsive-ui/app/components/copyButton.js | 59 + responsive-ui/app/components/copyable.js | 46 + responsive-ui/app/components/custom-radio-list.js | 60 + responsive-ui/app/components/dropdown.js | 89 + responsive-ui/app/components/editable-label.js | 56 + responsive-ui/app/components/ens-input.js | 170 ++ responsive-ui/app/components/eth-balance.js | 89 + responsive-ui/app/components/fiat-value.js | 63 + .../app/components/hex-as-decimal-input.js | 154 ++ responsive-ui/app/components/identicon.js | 72 + responsive-ui/app/components/loading.js | 53 + responsive-ui/app/components/mascot.js | 59 + responsive-ui/app/components/mini-account-panel.js | 74 + responsive-ui/app/components/network.js | 124 + responsive-ui/app/components/notice.js | 126 ++ .../app/components/pending-msg-details.js | 50 + responsive-ui/app/components/pending-msg.js | 56 + .../app/components/pending-personal-msg-details.js | 60 + .../app/components/pending-personal-msg.js | 47 + responsive-ui/app/components/pending-tx.js | 480 ++++ responsive-ui/app/components/qr-code.js | 79 + responsive-ui/app/components/range-slider.js | 58 + responsive-ui/app/components/shapeshift-form.js | 306 +++ responsive-ui/app/components/shift-list-item.js | 204 ++ responsive-ui/app/components/tab-bar.js | 36 + responsive-ui/app/components/template.js | 18 + responsive-ui/app/components/token-cell.js | 72 + responsive-ui/app/components/token-list.js | 192 ++ responsive-ui/app/components/tooltip.js | 22 + .../app/components/transaction-list-item-icon.js | 68 + .../app/components/transaction-list-item.js | 165 ++ responsive-ui/app/components/transaction-list.js | 79 + responsive-ui/app/conf-tx.js | 213 ++ responsive-ui/app/config.js | 211 ++ responsive-ui/app/conversion.json | 207 ++ responsive-ui/app/css/debug.css | 21 + responsive-ui/app/css/fonts.css | 36 + responsive-ui/app/css/index.css | 674 ++++++ responsive-ui/app/css/lib.css | 268 +++ responsive-ui/app/css/reset.css | 48 + responsive-ui/app/css/transitions.css | 42 + responsive-ui/app/first-time/init-menu.js | 179 ++ responsive-ui/app/img/identicon-tardigrade.png | Bin 0 -> 141119 bytes responsive-ui/app/img/identicon-walrus.png | Bin 0 -> 388973 bytes responsive-ui/app/info.js | 154 ++ .../app/keychains/hd/create-vault-complete.js | 76 + .../app/keychains/hd/recover-seed/confirmation.js | 118 + responsive-ui/app/keychains/hd/restore-vault.js | 152 ++ responsive-ui/app/new-keychain.js | 29 + responsive-ui/app/reducers.js | 52 + responsive-ui/app/reducers/app.js | 585 +++++ responsive-ui/app/reducers/identities.js | 15 + responsive-ui/app/reducers/metamask.js | 137 ++ responsive-ui/app/root.js | 22 + responsive-ui/app/send.js | 288 +++ responsive-ui/app/settings.js | 59 + responsive-ui/app/store.js | 21 + responsive-ui/app/template.js | 30 + responsive-ui/app/unlock.js | 118 + responsive-ui/app/util.js | 217 ++ responsive-ui/css.js | 29 + responsive-ui/design/00-metamask-SignIn.jpg | Bin 0 -> 57848 bytes responsive-ui/design/01-metamask-SelectAcc.jpg | Bin 0 -> 76063 bytes responsive-ui/design/02-metamask-AccDetails.jpg | Bin 0 -> 75780 bytes .../design/02a-metamask-AccDetails-OverToken.jpg | Bin 0 -> 121847 bytes .../02a-metamask-AccDetails-OverTransaction.jpg | Bin 0 -> 122075 bytes responsive-ui/design/02a-metamask-AccDetails.jpg | Bin 0 -> 117570 bytes .../design/02b-metamask-AccDetails-Send.jpg | Bin 0 -> 110143 bytes responsive-ui/design/03-metamask-Qr.jpg | Bin 0 -> 66052 bytes responsive-ui/design/05-metamask-Menu.jpg | Bin 0 -> 130264 bytes .../chromeStorePics/final_screen_dao_accounts.png | Bin 0 -> 249708 bytes .../chromeStorePics/final_screen_dao_locked.png | Bin 0 -> 220295 bytes .../final_screen_dao_notification.png | Bin 0 -> 214405 bytes .../chromeStorePics/final_screen_wei_account.png | Bin 0 -> 253382 bytes .../final_screen_wei_notification.png | Bin 0 -> 193865 bytes responsive-ui/design/chromeStorePics/icon-128.png | Bin 0 -> 5770 bytes responsive-ui/design/chromeStorePics/icon-64.png | Bin 0 -> 3573 bytes .../design/chromeStorePics/metamask_icon.ai | 2383 ++++++++++++++++++++ .../design/chromeStorePics/promo1400560.png | Bin 0 -> 261644 bytes .../design/chromeStorePics/promo440280.png | Bin 0 -> 57471 bytes .../design/chromeStorePics/promo920680.png | Bin 0 -> 206713 bytes .../design/chromeStorePics/screen_dao_accounts.png | Bin 0 -> 517598 bytes .../design/chromeStorePics/screen_dao_locked.png | Bin 0 -> 287108 bytes .../chromeStorePics/screen_dao_notification.png | Bin 0 -> 296498 bytes .../design/chromeStorePics/screen_wei_account.png | Bin 0 -> 653633 bytes .../chromeStorePics/screen_wei_notification.png | Bin 0 -> 402486 bytes responsive-ui/design/metamask-logo-eyes.png | Bin 0 -> 146076 bytes responsive-ui/design/wireframes/1st_time_use.png | Bin 0 -> 937556 bytes .../design/wireframes/metamask_wfs_jan_13.pdf | Bin 0 -> 452413 bytes .../design/wireframes/metamask_wfs_jan_13.png | Bin 0 -> 419066 bytes .../design/wireframes/metamask_wfs_jan_18.pdf | Bin 0 -> 612778 bytes responsive-ui/example.js | 123 + responsive-ui/index.html | 20 + responsive-ui/index.js | 58 + responsive-ui/lib/account-link.js | 26 + responsive-ui/lib/contract-namer.js | 33 + responsive-ui/lib/etherscan-prefix-for-network.js | 21 + responsive-ui/lib/explorer-link.js | 6 + responsive-ui/lib/icon-factory.js | 65 + responsive-ui/lib/lost-accounts-notice.js | 23 + responsive-ui/lib/persistent-form.js | 61 + responsive-ui/lib/tx-helper.js | 17 + test/unit/responsive/components/dropdown-test.js | 6 +- ui/app/account-detail.js | 311 +++ ui/app/accounts/account-list-item.js | 91 + ui/app/accounts/import/index.js | 100 + ui/app/accounts/import/json.js | 100 + ui/app/accounts/import/private-key.js | 67 + ui/app/accounts/import/seed.js | 30 + ui/app/accounts/index.js | 164 ++ ui/app/actions.js | 1031 +++++++++ ui/app/add-token.js | 219 ++ ui/app/app.js | 591 +++++ ui/app/components/account-export.js | 122 + ui/app/components/account-info-link.js | 41 + ui/app/components/account-panel.js | 86 + ui/app/components/balance.js | 89 + ui/app/components/binary-renderer.js | 46 + ui/app/components/bn-as-decimal-input.js | 174 ++ ui/app/components/buy-button-subview.js | 197 ++ ui/app/components/coinbase-form.js | 63 + ui/app/components/copyButton.js | 59 + ui/app/components/copyable.js | 46 + ui/app/components/custom-radio-list.js | 60 + ui/app/components/drop-menu-item.js | 59 + ui/app/components/editable-label.js | 51 + ui/app/components/ens-input.js | 170 ++ ui/app/components/eth-balance.js | 89 + ui/app/components/fiat-value.js | 63 + ui/app/components/hex-as-decimal-input.js | 154 ++ ui/app/components/identicon.js | 72 + ui/app/components/loading.js | 53 + ui/app/components/mascot.js | 59 + ui/app/components/mini-account-panel.js | 74 + ui/app/components/network.js | 124 + ui/app/components/notice.js | 126 ++ ui/app/components/pending-msg-details.js | 50 + ui/app/components/pending-msg.js | 56 + ui/app/components/pending-personal-msg-details.js | 60 + ui/app/components/pending-personal-msg.js | 47 + ui/app/components/pending-tx.js | 480 ++++ ui/app/components/qr-code.js | 79 + ui/app/components/range-slider.js | 58 + ui/app/components/shapeshift-form.js | 306 +++ ui/app/components/shift-list-item.js | 204 ++ ui/app/components/tab-bar.js | 36 + ui/app/components/template.js | 18 + ui/app/components/token-cell.js | 72 + ui/app/components/token-list.js | 192 ++ ui/app/components/tooltip.js | 22 + ui/app/components/transaction-list-item-icon.js | 68 + ui/app/components/transaction-list-item.js | 165 ++ ui/app/components/transaction-list.js | 79 + ui/app/conf-tx.js | 213 ++ ui/app/config.js | 211 ++ ui/app/conversion.json | 207 ++ ui/app/css/debug.css | 21 + ui/app/css/fonts.css | 36 + ui/app/css/index.css | 667 ++++++ ui/app/css/lib.css | 268 +++ ui/app/css/reset.css | 48 + ui/app/css/transitions.css | 42 + ui/app/first-time/init-menu.js | 179 ++ ui/app/img/identicon-tardigrade.png | Bin 0 -> 141119 bytes ui/app/img/identicon-walrus.png | Bin 0 -> 388973 bytes ui/app/info.js | 154 ++ ui/app/keychains/hd/create-vault-complete.js | 78 + ui/app/keychains/hd/recover-seed/confirmation.js | 118 + ui/app/keychains/hd/restore-vault.js | 152 ++ ui/app/new-keychain.js | 29 + ui/app/reducers.js | 52 + ui/app/reducers/app.js | 585 +++++ ui/app/reducers/identities.js | 15 + ui/app/reducers/metamask.js | 137 ++ ui/app/root.js | 22 + ui/app/send.js | 288 +++ ui/app/settings.js | 59 + ui/app/store.js | 21 + ui/app/template.js | 30 + ui/app/unlock.js | 118 + ui/app/util.js | 217 ++ ui/classic/.gitignore | 66 - ui/classic/app/account-detail.js | 311 --- ui/classic/app/accounts/account-list-item.js | 91 - ui/classic/app/accounts/import/index.js | 100 - ui/classic/app/accounts/import/json.js | 100 - ui/classic/app/accounts/import/private-key.js | 67 - ui/classic/app/accounts/import/seed.js | 30 - ui/classic/app/accounts/index.js | 164 -- ui/classic/app/actions.js | 1031 --------- ui/classic/app/add-token.js | 219 -- ui/classic/app/app.js | 591 ----- ui/classic/app/components/account-export.js | 122 - ui/classic/app/components/account-info-link.js | 41 - ui/classic/app/components/account-panel.js | 86 - ui/classic/app/components/balance.js | 89 - ui/classic/app/components/binary-renderer.js | 46 - ui/classic/app/components/bn-as-decimal-input.js | 174 -- ui/classic/app/components/buy-button-subview.js | 197 -- ui/classic/app/components/coinbase-form.js | 63 - ui/classic/app/components/copyButton.js | 59 - ui/classic/app/components/copyable.js | 46 - ui/classic/app/components/custom-radio-list.js | 60 - ui/classic/app/components/drop-menu-item.js | 59 - ui/classic/app/components/editable-label.js | 51 - ui/classic/app/components/ens-input.js | 170 -- ui/classic/app/components/eth-balance.js | 89 - ui/classic/app/components/fiat-value.js | 63 - ui/classic/app/components/hex-as-decimal-input.js | 154 -- ui/classic/app/components/identicon.js | 72 - ui/classic/app/components/loading.js | 53 - ui/classic/app/components/mascot.js | 59 - ui/classic/app/components/mini-account-panel.js | 74 - ui/classic/app/components/network.js | 124 - ui/classic/app/components/notice.js | 126 -- ui/classic/app/components/pending-msg-details.js | 50 - ui/classic/app/components/pending-msg.js | 56 - .../app/components/pending-personal-msg-details.js | 60 - ui/classic/app/components/pending-personal-msg.js | 47 - ui/classic/app/components/pending-tx.js | 480 ---- ui/classic/app/components/qr-code.js | 79 - ui/classic/app/components/range-slider.js | 58 - ui/classic/app/components/shapeshift-form.js | 306 --- ui/classic/app/components/shift-list-item.js | 204 -- ui/classic/app/components/tab-bar.js | 36 - ui/classic/app/components/template.js | 18 - ui/classic/app/components/token-cell.js | 72 - ui/classic/app/components/token-list.js | 192 -- ui/classic/app/components/tooltip.js | 22 - .../app/components/transaction-list-item-icon.js | 68 - ui/classic/app/components/transaction-list-item.js | 165 -- ui/classic/app/components/transaction-list.js | 79 - ui/classic/app/conf-tx.js | 213 -- ui/classic/app/config.js | 211 -- ui/classic/app/conversion.json | 207 -- ui/classic/app/css/debug.css | 21 - ui/classic/app/css/fonts.css | 36 - ui/classic/app/css/index.css | 667 ------ ui/classic/app/css/lib.css | 268 --- ui/classic/app/css/reset.css | 48 - ui/classic/app/css/transitions.css | 42 - ui/classic/app/first-time/init-menu.js | 179 -- ui/classic/app/img/identicon-tardigrade.png | Bin 141119 -> 0 bytes ui/classic/app/img/identicon-walrus.png | Bin 388973 -> 0 bytes ui/classic/app/info.js | 154 -- .../app/keychains/hd/create-vault-complete.js | 78 - .../app/keychains/hd/recover-seed/confirmation.js | 118 - ui/classic/app/keychains/hd/restore-vault.js | 152 -- ui/classic/app/new-keychain.js | 29 - ui/classic/app/reducers.js | 52 - ui/classic/app/reducers/app.js | 585 ----- ui/classic/app/reducers/identities.js | 15 - ui/classic/app/reducers/metamask.js | 137 -- ui/classic/app/root.js | 22 - ui/classic/app/send.js | 288 --- ui/classic/app/settings.js | 59 - ui/classic/app/store.js | 21 - ui/classic/app/template.js | 30 - ui/classic/app/unlock.js | 118 - ui/classic/app/util.js | 217 -- ui/classic/css.js | 29 - ui/classic/design/00-metamask-SignIn.jpg | Bin 57848 -> 0 bytes ui/classic/design/01-metamask-SelectAcc.jpg | Bin 76063 -> 0 bytes ui/classic/design/02-metamask-AccDetails.jpg | Bin 75780 -> 0 bytes .../design/02a-metamask-AccDetails-OverToken.jpg | Bin 121847 -> 0 bytes .../02a-metamask-AccDetails-OverTransaction.jpg | Bin 122075 -> 0 bytes ui/classic/design/02a-metamask-AccDetails.jpg | Bin 117570 -> 0 bytes ui/classic/design/02b-metamask-AccDetails-Send.jpg | Bin 110143 -> 0 bytes ui/classic/design/03-metamask-Qr.jpg | Bin 66052 -> 0 bytes ui/classic/design/05-metamask-Menu.jpg | Bin 130264 -> 0 bytes .../chromeStorePics/final_screen_dao_accounts.png | Bin 249708 -> 0 bytes .../chromeStorePics/final_screen_dao_locked.png | Bin 220295 -> 0 bytes .../final_screen_dao_notification.png | Bin 214405 -> 0 bytes .../chromeStorePics/final_screen_wei_account.png | Bin 253382 -> 0 bytes .../final_screen_wei_notification.png | Bin 193865 -> 0 bytes ui/classic/design/chromeStorePics/icon-128.png | Bin 5770 -> 0 bytes ui/classic/design/chromeStorePics/icon-64.png | Bin 3573 -> 0 bytes ui/classic/design/chromeStorePics/metamask_icon.ai | 2383 -------------------- ui/classic/design/chromeStorePics/promo1400560.png | Bin 261644 -> 0 bytes ui/classic/design/chromeStorePics/promo440280.png | Bin 57471 -> 0 bytes ui/classic/design/chromeStorePics/promo920680.png | Bin 206713 -> 0 bytes .../design/chromeStorePics/screen_dao_accounts.png | Bin 517598 -> 0 bytes .../design/chromeStorePics/screen_dao_locked.png | Bin 287108 -> 0 bytes .../chromeStorePics/screen_dao_notification.png | Bin 296498 -> 0 bytes .../design/chromeStorePics/screen_wei_account.png | Bin 653633 -> 0 bytes .../chromeStorePics/screen_wei_notification.png | Bin 402486 -> 0 bytes ui/classic/design/metamask-logo-eyes.png | Bin 146076 -> 0 bytes ui/classic/design/wireframes/1st_time_use.png | Bin 937556 -> 0 bytes .../design/wireframes/metamask_wfs_jan_13.pdf | Bin 452413 -> 0 bytes .../design/wireframes/metamask_wfs_jan_13.png | Bin 419066 -> 0 bytes .../design/wireframes/metamask_wfs_jan_18.pdf | Bin 612778 -> 0 bytes ui/classic/example.js | 123 - ui/classic/index.html | 20 - ui/classic/index.js | 58 - ui/classic/lib/account-link.js | 26 - ui/classic/lib/contract-namer.js | 33 - ui/classic/lib/etherscan-prefix-for-network.js | 21 - ui/classic/lib/explorer-link.js | 6 - ui/classic/lib/icon-factory.js | 65 - ui/classic/lib/lost-accounts-notice.js | 23 - ui/classic/lib/persistent-form.js | 61 - ui/classic/lib/tx-helper.js | 17 - ui/css.js | 29 + ui/design/00-metamask-SignIn.jpg | Bin 0 -> 57848 bytes ui/design/01-metamask-SelectAcc.jpg | Bin 0 -> 76063 bytes ui/design/02-metamask-AccDetails.jpg | Bin 0 -> 75780 bytes ui/design/02a-metamask-AccDetails-OverToken.jpg | Bin 0 -> 121847 bytes .../02a-metamask-AccDetails-OverTransaction.jpg | Bin 0 -> 122075 bytes ui/design/02a-metamask-AccDetails.jpg | Bin 0 -> 117570 bytes ui/design/02b-metamask-AccDetails-Send.jpg | Bin 0 -> 110143 bytes ui/design/03-metamask-Qr.jpg | Bin 0 -> 66052 bytes ui/design/05-metamask-Menu.jpg | Bin 0 -> 130264 bytes .../chromeStorePics/final_screen_dao_accounts.png | Bin 0 -> 249708 bytes .../chromeStorePics/final_screen_dao_locked.png | Bin 0 -> 220295 bytes .../final_screen_dao_notification.png | Bin 0 -> 214405 bytes .../chromeStorePics/final_screen_wei_account.png | Bin 0 -> 253382 bytes .../final_screen_wei_notification.png | Bin 0 -> 193865 bytes ui/design/chromeStorePics/icon-128.png | Bin 0 -> 5770 bytes ui/design/chromeStorePics/icon-64.png | Bin 0 -> 3573 bytes ui/design/chromeStorePics/metamask_icon.ai | 2383 ++++++++++++++++++++ ui/design/chromeStorePics/promo1400560.png | Bin 0 -> 261644 bytes ui/design/chromeStorePics/promo440280.png | Bin 0 -> 57471 bytes ui/design/chromeStorePics/promo920680.png | Bin 0 -> 206713 bytes ui/design/chromeStorePics/screen_dao_accounts.png | Bin 0 -> 517598 bytes ui/design/chromeStorePics/screen_dao_locked.png | Bin 0 -> 287108 bytes .../chromeStorePics/screen_dao_notification.png | Bin 0 -> 296498 bytes ui/design/chromeStorePics/screen_wei_account.png | Bin 0 -> 653633 bytes .../chromeStorePics/screen_wei_notification.png | Bin 0 -> 402486 bytes ui/design/metamask-logo-eyes.png | Bin 0 -> 146076 bytes ui/design/wireframes/1st_time_use.png | Bin 0 -> 937556 bytes ui/design/wireframes/metamask_wfs_jan_13.pdf | Bin 0 -> 452413 bytes ui/design/wireframes/metamask_wfs_jan_13.png | Bin 0 -> 419066 bytes ui/design/wireframes/metamask_wfs_jan_18.pdf | Bin 0 -> 612778 bytes ui/example.js | 123 + ui/index.html | 20 + ui/index.js | 58 + ui/lib/account-link.js | 26 + ui/lib/contract-namer.js | 33 + ui/lib/etherscan-prefix-for-network.js | 21 + ui/lib/explorer-link.js | 6 + ui/lib/icon-factory.js | 65 + ui/lib/lost-accounts-notice.js | 23 + ui/lib/persistent-form.js | 61 + ui/lib/tx-helper.js | 17 + ui/responsive/.gitignore | 66 - ui/responsive/app/account-detail.js | 289 --- ui/responsive/app/accounts/import/index.js | 100 - ui/responsive/app/accounts/import/json.js | 100 - ui/responsive/app/accounts/import/private-key.js | 67 - ui/responsive/app/accounts/import/seed.js | 30 - ui/responsive/app/actions.js | 1031 --------- ui/responsive/app/add-token.js | 219 -- ui/responsive/app/app.js | 580 ----- ui/responsive/app/components/account-dropdowns.js | 227 -- ui/responsive/app/components/account-export.js | 122 - ui/responsive/app/components/account-panel.js | 86 - ui/responsive/app/components/balance.js | 89 - ui/responsive/app/components/binary-renderer.js | 46 - .../app/components/bn-as-decimal-input.js | 174 -- ui/responsive/app/components/buy-button-subview.js | 197 -- ui/responsive/app/components/coinbase-form.js | 63 - ui/responsive/app/components/copyButton.js | 59 - ui/responsive/app/components/copyable.js | 46 - ui/responsive/app/components/custom-radio-list.js | 60 - ui/responsive/app/components/dropdown.js | 89 - ui/responsive/app/components/editable-label.js | 56 - ui/responsive/app/components/ens-input.js | 170 -- ui/responsive/app/components/eth-balance.js | 89 - ui/responsive/app/components/fiat-value.js | 63 - .../app/components/hex-as-decimal-input.js | 154 -- ui/responsive/app/components/identicon.js | 72 - ui/responsive/app/components/loading.js | 53 - ui/responsive/app/components/mascot.js | 59 - ui/responsive/app/components/mini-account-panel.js | 74 - ui/responsive/app/components/network.js | 124 - ui/responsive/app/components/notice.js | 126 -- .../app/components/pending-msg-details.js | 50 - ui/responsive/app/components/pending-msg.js | 56 - .../app/components/pending-personal-msg-details.js | 60 - .../app/components/pending-personal-msg.js | 47 - ui/responsive/app/components/pending-tx.js | 480 ---- ui/responsive/app/components/qr-code.js | 79 - ui/responsive/app/components/range-slider.js | 58 - ui/responsive/app/components/shapeshift-form.js | 306 --- ui/responsive/app/components/shift-list-item.js | 204 -- ui/responsive/app/components/tab-bar.js | 36 - ui/responsive/app/components/template.js | 18 - ui/responsive/app/components/token-cell.js | 72 - ui/responsive/app/components/token-list.js | 192 -- ui/responsive/app/components/tooltip.js | 22 - .../app/components/transaction-list-item-icon.js | 68 - .../app/components/transaction-list-item.js | 165 -- ui/responsive/app/components/transaction-list.js | 79 - ui/responsive/app/conf-tx.js | 213 -- ui/responsive/app/config.js | 211 -- ui/responsive/app/conversion.json | 207 -- ui/responsive/app/css/debug.css | 21 - ui/responsive/app/css/fonts.css | 36 - ui/responsive/app/css/index.css | 674 ------ ui/responsive/app/css/lib.css | 268 --- ui/responsive/app/css/reset.css | 48 - ui/responsive/app/css/transitions.css | 42 - ui/responsive/app/first-time/init-menu.js | 179 -- ui/responsive/app/img/identicon-tardigrade.png | Bin 141119 -> 0 bytes ui/responsive/app/img/identicon-walrus.png | Bin 388973 -> 0 bytes ui/responsive/app/info.js | 154 -- .../app/keychains/hd/create-vault-complete.js | 76 - .../app/keychains/hd/recover-seed/confirmation.js | 118 - ui/responsive/app/keychains/hd/restore-vault.js | 152 -- ui/responsive/app/new-keychain.js | 29 - ui/responsive/app/reducers.js | 52 - ui/responsive/app/reducers/app.js | 585 ----- ui/responsive/app/reducers/identities.js | 15 - ui/responsive/app/reducers/metamask.js | 137 -- ui/responsive/app/root.js | 22 - ui/responsive/app/send.js | 288 --- ui/responsive/app/settings.js | 59 - ui/responsive/app/store.js | 21 - ui/responsive/app/template.js | 30 - ui/responsive/app/unlock.js | 118 - ui/responsive/app/util.js | 217 -- ui/responsive/css.js | 29 - ui/responsive/design/00-metamask-SignIn.jpg | Bin 57848 -> 0 bytes ui/responsive/design/01-metamask-SelectAcc.jpg | Bin 76063 -> 0 bytes ui/responsive/design/02-metamask-AccDetails.jpg | Bin 75780 -> 0 bytes .../design/02a-metamask-AccDetails-OverToken.jpg | Bin 121847 -> 0 bytes .../02a-metamask-AccDetails-OverTransaction.jpg | Bin 122075 -> 0 bytes ui/responsive/design/02a-metamask-AccDetails.jpg | Bin 117570 -> 0 bytes .../design/02b-metamask-AccDetails-Send.jpg | Bin 110143 -> 0 bytes ui/responsive/design/03-metamask-Qr.jpg | Bin 66052 -> 0 bytes ui/responsive/design/05-metamask-Menu.jpg | Bin 130264 -> 0 bytes .../chromeStorePics/final_screen_dao_accounts.png | Bin 249708 -> 0 bytes .../chromeStorePics/final_screen_dao_locked.png | Bin 220295 -> 0 bytes .../final_screen_dao_notification.png | Bin 214405 -> 0 bytes .../chromeStorePics/final_screen_wei_account.png | Bin 253382 -> 0 bytes .../final_screen_wei_notification.png | Bin 193865 -> 0 bytes ui/responsive/design/chromeStorePics/icon-128.png | Bin 5770 -> 0 bytes ui/responsive/design/chromeStorePics/icon-64.png | Bin 3573 -> 0 bytes .../design/chromeStorePics/metamask_icon.ai | 2383 -------------------- .../design/chromeStorePics/promo1400560.png | Bin 261644 -> 0 bytes .../design/chromeStorePics/promo440280.png | Bin 57471 -> 0 bytes .../design/chromeStorePics/promo920680.png | Bin 206713 -> 0 bytes .../design/chromeStorePics/screen_dao_accounts.png | Bin 517598 -> 0 bytes .../design/chromeStorePics/screen_dao_locked.png | Bin 287108 -> 0 bytes .../chromeStorePics/screen_dao_notification.png | Bin 296498 -> 0 bytes .../design/chromeStorePics/screen_wei_account.png | Bin 653633 -> 0 bytes .../chromeStorePics/screen_wei_notification.png | Bin 402486 -> 0 bytes ui/responsive/design/metamask-logo-eyes.png | Bin 146076 -> 0 bytes ui/responsive/design/wireframes/1st_time_use.png | Bin 937556 -> 0 bytes .../design/wireframes/metamask_wfs_jan_13.pdf | Bin 452413 -> 0 bytes .../design/wireframes/metamask_wfs_jan_13.png | Bin 419066 -> 0 bytes .../design/wireframes/metamask_wfs_jan_18.pdf | Bin 612778 -> 0 bytes ui/responsive/example.js | 123 - ui/responsive/index.html | 20 - ui/responsive/index.js | 58 - ui/responsive/lib/account-link.js | 26 - ui/responsive/lib/contract-namer.js | 33 - ui/responsive/lib/etherscan-prefix-for-network.js | 21 - ui/responsive/lib/explorer-link.js | 6 - ui/responsive/lib/icon-factory.js | 65 - ui/responsive/lib/lost-accounts-notice.js | 23 - ui/responsive/lib/persistent-form.js | 61 - ui/responsive/lib/tx-helper.js | 17 - 484 files changed, 27221 insertions(+), 27287 deletions(-) create mode 100644 responsive-ui/.gitignore create mode 100644 responsive-ui/app/account-detail.js create mode 100644 responsive-ui/app/accounts/import/index.js create mode 100644 responsive-ui/app/accounts/import/json.js create mode 100644 responsive-ui/app/accounts/import/private-key.js create mode 100644 responsive-ui/app/accounts/import/seed.js create mode 100644 responsive-ui/app/actions.js create mode 100644 responsive-ui/app/add-token.js create mode 100644 responsive-ui/app/app.js create mode 100644 responsive-ui/app/components/account-dropdowns.js create mode 100644 responsive-ui/app/components/account-export.js create mode 100644 responsive-ui/app/components/account-panel.js create mode 100644 responsive-ui/app/components/balance.js create mode 100644 responsive-ui/app/components/binary-renderer.js create mode 100644 responsive-ui/app/components/bn-as-decimal-input.js create mode 100644 responsive-ui/app/components/buy-button-subview.js create mode 100644 responsive-ui/app/components/coinbase-form.js create mode 100644 responsive-ui/app/components/copyButton.js create mode 100644 responsive-ui/app/components/copyable.js create mode 100644 responsive-ui/app/components/custom-radio-list.js create mode 100644 responsive-ui/app/components/dropdown.js create mode 100644 responsive-ui/app/components/editable-label.js create mode 100644 responsive-ui/app/components/ens-input.js create mode 100644 responsive-ui/app/components/eth-balance.js create mode 100644 responsive-ui/app/components/fiat-value.js create mode 100644 responsive-ui/app/components/hex-as-decimal-input.js create mode 100644 responsive-ui/app/components/identicon.js create mode 100644 responsive-ui/app/components/loading.js create mode 100644 responsive-ui/app/components/mascot.js create mode 100644 responsive-ui/app/components/mini-account-panel.js create mode 100644 responsive-ui/app/components/network.js create mode 100644 responsive-ui/app/components/notice.js create mode 100644 responsive-ui/app/components/pending-msg-details.js create mode 100644 responsive-ui/app/components/pending-msg.js create mode 100644 responsive-ui/app/components/pending-personal-msg-details.js create mode 100644 responsive-ui/app/components/pending-personal-msg.js create mode 100644 responsive-ui/app/components/pending-tx.js create mode 100644 responsive-ui/app/components/qr-code.js create mode 100644 responsive-ui/app/components/range-slider.js create mode 100644 responsive-ui/app/components/shapeshift-form.js create mode 100644 responsive-ui/app/components/shift-list-item.js create mode 100644 responsive-ui/app/components/tab-bar.js create mode 100644 responsive-ui/app/components/template.js create mode 100644 responsive-ui/app/components/token-cell.js create mode 100644 responsive-ui/app/components/token-list.js create mode 100644 responsive-ui/app/components/tooltip.js create mode 100644 responsive-ui/app/components/transaction-list-item-icon.js create mode 100644 responsive-ui/app/components/transaction-list-item.js create mode 100644 responsive-ui/app/components/transaction-list.js create mode 100644 responsive-ui/app/conf-tx.js create mode 100644 responsive-ui/app/config.js create mode 100644 responsive-ui/app/conversion.json create mode 100644 responsive-ui/app/css/debug.css create mode 100644 responsive-ui/app/css/fonts.css create mode 100644 responsive-ui/app/css/index.css create mode 100644 responsive-ui/app/css/lib.css create mode 100644 responsive-ui/app/css/reset.css create mode 100644 responsive-ui/app/css/transitions.css create mode 100644 responsive-ui/app/first-time/init-menu.js create mode 100644 responsive-ui/app/img/identicon-tardigrade.png create mode 100644 responsive-ui/app/img/identicon-walrus.png create mode 100644 responsive-ui/app/info.js create mode 100644 responsive-ui/app/keychains/hd/create-vault-complete.js create mode 100644 responsive-ui/app/keychains/hd/recover-seed/confirmation.js create mode 100644 responsive-ui/app/keychains/hd/restore-vault.js create mode 100644 responsive-ui/app/new-keychain.js create mode 100644 responsive-ui/app/reducers.js create mode 100644 responsive-ui/app/reducers/app.js create mode 100644 responsive-ui/app/reducers/identities.js create mode 100644 responsive-ui/app/reducers/metamask.js create mode 100644 responsive-ui/app/root.js create mode 100644 responsive-ui/app/send.js create mode 100644 responsive-ui/app/settings.js create mode 100644 responsive-ui/app/store.js create mode 100644 responsive-ui/app/template.js create mode 100644 responsive-ui/app/unlock.js create mode 100644 responsive-ui/app/util.js create mode 100644 responsive-ui/css.js create mode 100644 responsive-ui/design/00-metamask-SignIn.jpg create mode 100644 responsive-ui/design/01-metamask-SelectAcc.jpg create mode 100644 responsive-ui/design/02-metamask-AccDetails.jpg create mode 100644 responsive-ui/design/02a-metamask-AccDetails-OverToken.jpg create mode 100644 responsive-ui/design/02a-metamask-AccDetails-OverTransaction.jpg create mode 100644 responsive-ui/design/02a-metamask-AccDetails.jpg create mode 100644 responsive-ui/design/02b-metamask-AccDetails-Send.jpg create mode 100644 responsive-ui/design/03-metamask-Qr.jpg create mode 100644 responsive-ui/design/05-metamask-Menu.jpg create mode 100644 responsive-ui/design/chromeStorePics/final_screen_dao_accounts.png create mode 100644 responsive-ui/design/chromeStorePics/final_screen_dao_locked.png create mode 100644 responsive-ui/design/chromeStorePics/final_screen_dao_notification.png create mode 100644 responsive-ui/design/chromeStorePics/final_screen_wei_account.png create mode 100644 responsive-ui/design/chromeStorePics/final_screen_wei_notification.png create mode 100644 responsive-ui/design/chromeStorePics/icon-128.png create mode 100644 responsive-ui/design/chromeStorePics/icon-64.png create mode 100644 responsive-ui/design/chromeStorePics/metamask_icon.ai create mode 100644 responsive-ui/design/chromeStorePics/promo1400560.png create mode 100644 responsive-ui/design/chromeStorePics/promo440280.png create mode 100644 responsive-ui/design/chromeStorePics/promo920680.png create mode 100644 responsive-ui/design/chromeStorePics/screen_dao_accounts.png create mode 100644 responsive-ui/design/chromeStorePics/screen_dao_locked.png create mode 100644 responsive-ui/design/chromeStorePics/screen_dao_notification.png create mode 100644 responsive-ui/design/chromeStorePics/screen_wei_account.png create mode 100644 responsive-ui/design/chromeStorePics/screen_wei_notification.png create mode 100644 responsive-ui/design/metamask-logo-eyes.png create mode 100644 responsive-ui/design/wireframes/1st_time_use.png create mode 100644 responsive-ui/design/wireframes/metamask_wfs_jan_13.pdf create mode 100644 responsive-ui/design/wireframes/metamask_wfs_jan_13.png create mode 100644 responsive-ui/design/wireframes/metamask_wfs_jan_18.pdf create mode 100644 responsive-ui/example.js create mode 100644 responsive-ui/index.html create mode 100644 responsive-ui/index.js create mode 100644 responsive-ui/lib/account-link.js create mode 100644 responsive-ui/lib/contract-namer.js create mode 100644 responsive-ui/lib/etherscan-prefix-for-network.js create mode 100644 responsive-ui/lib/explorer-link.js create mode 100644 responsive-ui/lib/icon-factory.js create mode 100644 responsive-ui/lib/lost-accounts-notice.js create mode 100644 responsive-ui/lib/persistent-form.js create mode 100644 responsive-ui/lib/tx-helper.js create mode 100644 ui/app/account-detail.js create mode 100644 ui/app/accounts/account-list-item.js create mode 100644 ui/app/accounts/import/index.js create mode 100644 ui/app/accounts/import/json.js create mode 100644 ui/app/accounts/import/private-key.js create mode 100644 ui/app/accounts/import/seed.js create mode 100644 ui/app/accounts/index.js create mode 100644 ui/app/actions.js create mode 100644 ui/app/add-token.js create mode 100644 ui/app/app.js create mode 100644 ui/app/components/account-export.js create mode 100644 ui/app/components/account-info-link.js create mode 100644 ui/app/components/account-panel.js create mode 100644 ui/app/components/balance.js create mode 100644 ui/app/components/binary-renderer.js create mode 100644 ui/app/components/bn-as-decimal-input.js create mode 100644 ui/app/components/buy-button-subview.js create mode 100644 ui/app/components/coinbase-form.js create mode 100644 ui/app/components/copyButton.js create mode 100644 ui/app/components/copyable.js create mode 100644 ui/app/components/custom-radio-list.js create mode 100644 ui/app/components/drop-menu-item.js create mode 100644 ui/app/components/editable-label.js create mode 100644 ui/app/components/ens-input.js create mode 100644 ui/app/components/eth-balance.js create mode 100644 ui/app/components/fiat-value.js create mode 100644 ui/app/components/hex-as-decimal-input.js create mode 100644 ui/app/components/identicon.js create mode 100644 ui/app/components/loading.js create mode 100644 ui/app/components/mascot.js create mode 100644 ui/app/components/mini-account-panel.js create mode 100644 ui/app/components/network.js create mode 100644 ui/app/components/notice.js create mode 100644 ui/app/components/pending-msg-details.js create mode 100644 ui/app/components/pending-msg.js create mode 100644 ui/app/components/pending-personal-msg-details.js create mode 100644 ui/app/components/pending-personal-msg.js create mode 100644 ui/app/components/pending-tx.js create mode 100644 ui/app/components/qr-code.js create mode 100644 ui/app/components/range-slider.js create mode 100644 ui/app/components/shapeshift-form.js create mode 100644 ui/app/components/shift-list-item.js create mode 100644 ui/app/components/tab-bar.js create mode 100644 ui/app/components/template.js create mode 100644 ui/app/components/token-cell.js create mode 100644 ui/app/components/token-list.js create mode 100644 ui/app/components/tooltip.js create mode 100644 ui/app/components/transaction-list-item-icon.js create mode 100644 ui/app/components/transaction-list-item.js create mode 100644 ui/app/components/transaction-list.js create mode 100644 ui/app/conf-tx.js create mode 100644 ui/app/config.js create mode 100644 ui/app/conversion.json create mode 100644 ui/app/css/debug.css create mode 100644 ui/app/css/fonts.css create mode 100644 ui/app/css/index.css create mode 100644 ui/app/css/lib.css create mode 100644 ui/app/css/reset.css create mode 100644 ui/app/css/transitions.css create mode 100644 ui/app/first-time/init-menu.js create mode 100644 ui/app/img/identicon-tardigrade.png create mode 100644 ui/app/img/identicon-walrus.png create mode 100644 ui/app/info.js create mode 100644 ui/app/keychains/hd/create-vault-complete.js create mode 100644 ui/app/keychains/hd/recover-seed/confirmation.js create mode 100644 ui/app/keychains/hd/restore-vault.js create mode 100644 ui/app/new-keychain.js create mode 100644 ui/app/reducers.js create mode 100644 ui/app/reducers/app.js create mode 100644 ui/app/reducers/identities.js create mode 100644 ui/app/reducers/metamask.js create mode 100644 ui/app/root.js create mode 100644 ui/app/send.js create mode 100644 ui/app/settings.js create mode 100644 ui/app/store.js create mode 100644 ui/app/template.js create mode 100644 ui/app/unlock.js create mode 100644 ui/app/util.js delete mode 100644 ui/classic/.gitignore delete mode 100644 ui/classic/app/account-detail.js delete mode 100644 ui/classic/app/accounts/account-list-item.js delete mode 100644 ui/classic/app/accounts/import/index.js delete mode 100644 ui/classic/app/accounts/import/json.js delete mode 100644 ui/classic/app/accounts/import/private-key.js delete mode 100644 ui/classic/app/accounts/import/seed.js delete mode 100644 ui/classic/app/accounts/index.js delete mode 100644 ui/classic/app/actions.js delete mode 100644 ui/classic/app/add-token.js delete mode 100644 ui/classic/app/app.js delete mode 100644 ui/classic/app/components/account-export.js delete mode 100644 ui/classic/app/components/account-info-link.js delete mode 100644 ui/classic/app/components/account-panel.js delete mode 100644 ui/classic/app/components/balance.js delete mode 100644 ui/classic/app/components/binary-renderer.js delete mode 100644 ui/classic/app/components/bn-as-decimal-input.js delete mode 100644 ui/classic/app/components/buy-button-subview.js delete mode 100644 ui/classic/app/components/coinbase-form.js delete mode 100644 ui/classic/app/components/copyButton.js delete mode 100644 ui/classic/app/components/copyable.js delete mode 100644 ui/classic/app/components/custom-radio-list.js delete mode 100644 ui/classic/app/components/drop-menu-item.js delete mode 100644 ui/classic/app/components/editable-label.js delete mode 100644 ui/classic/app/components/ens-input.js delete mode 100644 ui/classic/app/components/eth-balance.js delete mode 100644 ui/classic/app/components/fiat-value.js delete mode 100644 ui/classic/app/components/hex-as-decimal-input.js delete mode 100644 ui/classic/app/components/identicon.js delete mode 100644 ui/classic/app/components/loading.js delete mode 100644 ui/classic/app/components/mascot.js delete mode 100644 ui/classic/app/components/mini-account-panel.js delete mode 100644 ui/classic/app/components/network.js delete mode 100644 ui/classic/app/components/notice.js delete mode 100644 ui/classic/app/components/pending-msg-details.js delete mode 100644 ui/classic/app/components/pending-msg.js delete mode 100644 ui/classic/app/components/pending-personal-msg-details.js delete mode 100644 ui/classic/app/components/pending-personal-msg.js delete mode 100644 ui/classic/app/components/pending-tx.js delete mode 100644 ui/classic/app/components/qr-code.js delete mode 100644 ui/classic/app/components/range-slider.js delete mode 100644 ui/classic/app/components/shapeshift-form.js delete mode 100644 ui/classic/app/components/shift-list-item.js delete mode 100644 ui/classic/app/components/tab-bar.js delete mode 100644 ui/classic/app/components/template.js delete mode 100644 ui/classic/app/components/token-cell.js delete mode 100644 ui/classic/app/components/token-list.js delete mode 100644 ui/classic/app/components/tooltip.js delete mode 100644 ui/classic/app/components/transaction-list-item-icon.js delete mode 100644 ui/classic/app/components/transaction-list-item.js delete mode 100644 ui/classic/app/components/transaction-list.js delete mode 100644 ui/classic/app/conf-tx.js delete mode 100644 ui/classic/app/config.js delete mode 100644 ui/classic/app/conversion.json delete mode 100644 ui/classic/app/css/debug.css delete mode 100644 ui/classic/app/css/fonts.css delete mode 100644 ui/classic/app/css/index.css delete mode 100644 ui/classic/app/css/lib.css delete mode 100644 ui/classic/app/css/reset.css delete mode 100644 ui/classic/app/css/transitions.css delete mode 100644 ui/classic/app/first-time/init-menu.js delete mode 100644 ui/classic/app/img/identicon-tardigrade.png delete mode 100644 ui/classic/app/img/identicon-walrus.png delete mode 100644 ui/classic/app/info.js delete mode 100644 ui/classic/app/keychains/hd/create-vault-complete.js delete mode 100644 ui/classic/app/keychains/hd/recover-seed/confirmation.js delete mode 100644 ui/classic/app/keychains/hd/restore-vault.js delete mode 100644 ui/classic/app/new-keychain.js delete mode 100644 ui/classic/app/reducers.js delete mode 100644 ui/classic/app/reducers/app.js delete mode 100644 ui/classic/app/reducers/identities.js delete mode 100644 ui/classic/app/reducers/metamask.js delete mode 100644 ui/classic/app/root.js delete mode 100644 ui/classic/app/send.js delete mode 100644 ui/classic/app/settings.js delete mode 100644 ui/classic/app/store.js delete mode 100644 ui/classic/app/template.js delete mode 100644 ui/classic/app/unlock.js delete mode 100644 ui/classic/app/util.js delete mode 100644 ui/classic/css.js delete mode 100644 ui/classic/design/00-metamask-SignIn.jpg delete mode 100644 ui/classic/design/01-metamask-SelectAcc.jpg delete mode 100644 ui/classic/design/02-metamask-AccDetails.jpg delete mode 100644 ui/classic/design/02a-metamask-AccDetails-OverToken.jpg delete mode 100644 ui/classic/design/02a-metamask-AccDetails-OverTransaction.jpg delete mode 100644 ui/classic/design/02a-metamask-AccDetails.jpg delete mode 100644 ui/classic/design/02b-metamask-AccDetails-Send.jpg delete mode 100644 ui/classic/design/03-metamask-Qr.jpg delete mode 100644 ui/classic/design/05-metamask-Menu.jpg delete mode 100644 ui/classic/design/chromeStorePics/final_screen_dao_accounts.png delete mode 100644 ui/classic/design/chromeStorePics/final_screen_dao_locked.png delete mode 100644 ui/classic/design/chromeStorePics/final_screen_dao_notification.png delete mode 100644 ui/classic/design/chromeStorePics/final_screen_wei_account.png delete mode 100644 ui/classic/design/chromeStorePics/final_screen_wei_notification.png delete mode 100644 ui/classic/design/chromeStorePics/icon-128.png delete mode 100644 ui/classic/design/chromeStorePics/icon-64.png delete mode 100644 ui/classic/design/chromeStorePics/metamask_icon.ai delete mode 100644 ui/classic/design/chromeStorePics/promo1400560.png delete mode 100644 ui/classic/design/chromeStorePics/promo440280.png delete mode 100644 ui/classic/design/chromeStorePics/promo920680.png delete mode 100644 ui/classic/design/chromeStorePics/screen_dao_accounts.png delete mode 100644 ui/classic/design/chromeStorePics/screen_dao_locked.png delete mode 100644 ui/classic/design/chromeStorePics/screen_dao_notification.png delete mode 100644 ui/classic/design/chromeStorePics/screen_wei_account.png delete mode 100644 ui/classic/design/chromeStorePics/screen_wei_notification.png delete mode 100644 ui/classic/design/metamask-logo-eyes.png delete mode 100644 ui/classic/design/wireframes/1st_time_use.png delete mode 100644 ui/classic/design/wireframes/metamask_wfs_jan_13.pdf delete mode 100644 ui/classic/design/wireframes/metamask_wfs_jan_13.png delete mode 100644 ui/classic/design/wireframes/metamask_wfs_jan_18.pdf delete mode 100644 ui/classic/example.js delete mode 100644 ui/classic/index.html delete mode 100644 ui/classic/index.js delete mode 100644 ui/classic/lib/account-link.js delete mode 100644 ui/classic/lib/contract-namer.js delete mode 100644 ui/classic/lib/etherscan-prefix-for-network.js delete mode 100644 ui/classic/lib/explorer-link.js delete mode 100644 ui/classic/lib/icon-factory.js delete mode 100644 ui/classic/lib/lost-accounts-notice.js delete mode 100644 ui/classic/lib/persistent-form.js delete mode 100644 ui/classic/lib/tx-helper.js create mode 100644 ui/css.js create mode 100644 ui/design/00-metamask-SignIn.jpg create mode 100644 ui/design/01-metamask-SelectAcc.jpg create mode 100644 ui/design/02-metamask-AccDetails.jpg create mode 100644 ui/design/02a-metamask-AccDetails-OverToken.jpg create mode 100644 ui/design/02a-metamask-AccDetails-OverTransaction.jpg create mode 100644 ui/design/02a-metamask-AccDetails.jpg create mode 100644 ui/design/02b-metamask-AccDetails-Send.jpg create mode 100644 ui/design/03-metamask-Qr.jpg create mode 100644 ui/design/05-metamask-Menu.jpg create mode 100644 ui/design/chromeStorePics/final_screen_dao_accounts.png create mode 100644 ui/design/chromeStorePics/final_screen_dao_locked.png create mode 100644 ui/design/chromeStorePics/final_screen_dao_notification.png create mode 100644 ui/design/chromeStorePics/final_screen_wei_account.png create mode 100644 ui/design/chromeStorePics/final_screen_wei_notification.png create mode 100644 ui/design/chromeStorePics/icon-128.png create mode 100644 ui/design/chromeStorePics/icon-64.png create mode 100644 ui/design/chromeStorePics/metamask_icon.ai create mode 100644 ui/design/chromeStorePics/promo1400560.png create mode 100644 ui/design/chromeStorePics/promo440280.png create mode 100644 ui/design/chromeStorePics/promo920680.png create mode 100644 ui/design/chromeStorePics/screen_dao_accounts.png create mode 100644 ui/design/chromeStorePics/screen_dao_locked.png create mode 100644 ui/design/chromeStorePics/screen_dao_notification.png create mode 100644 ui/design/chromeStorePics/screen_wei_account.png create mode 100644 ui/design/chromeStorePics/screen_wei_notification.png create mode 100644 ui/design/metamask-logo-eyes.png create mode 100644 ui/design/wireframes/1st_time_use.png create mode 100644 ui/design/wireframes/metamask_wfs_jan_13.pdf create mode 100644 ui/design/wireframes/metamask_wfs_jan_13.png create mode 100644 ui/design/wireframes/metamask_wfs_jan_18.pdf create mode 100644 ui/example.js create mode 100644 ui/index.html create mode 100644 ui/index.js create mode 100644 ui/lib/account-link.js create mode 100644 ui/lib/contract-namer.js create mode 100644 ui/lib/etherscan-prefix-for-network.js create mode 100644 ui/lib/explorer-link.js create mode 100644 ui/lib/icon-factory.js create mode 100644 ui/lib/lost-accounts-notice.js create mode 100644 ui/lib/persistent-form.js create mode 100644 ui/lib/tx-helper.js delete mode 100644 ui/responsive/.gitignore delete mode 100644 ui/responsive/app/account-detail.js delete mode 100644 ui/responsive/app/accounts/import/index.js delete mode 100644 ui/responsive/app/accounts/import/json.js delete mode 100644 ui/responsive/app/accounts/import/private-key.js delete mode 100644 ui/responsive/app/accounts/import/seed.js delete mode 100644 ui/responsive/app/actions.js delete mode 100644 ui/responsive/app/add-token.js delete mode 100644 ui/responsive/app/app.js delete mode 100644 ui/responsive/app/components/account-dropdowns.js delete mode 100644 ui/responsive/app/components/account-export.js delete mode 100644 ui/responsive/app/components/account-panel.js delete mode 100644 ui/responsive/app/components/balance.js delete mode 100644 ui/responsive/app/components/binary-renderer.js delete mode 100644 ui/responsive/app/components/bn-as-decimal-input.js delete mode 100644 ui/responsive/app/components/buy-button-subview.js delete mode 100644 ui/responsive/app/components/coinbase-form.js delete mode 100644 ui/responsive/app/components/copyButton.js delete mode 100644 ui/responsive/app/components/copyable.js delete mode 100644 ui/responsive/app/components/custom-radio-list.js delete mode 100644 ui/responsive/app/components/dropdown.js delete mode 100644 ui/responsive/app/components/editable-label.js delete mode 100644 ui/responsive/app/components/ens-input.js delete mode 100644 ui/responsive/app/components/eth-balance.js delete mode 100644 ui/responsive/app/components/fiat-value.js delete mode 100644 ui/responsive/app/components/hex-as-decimal-input.js delete mode 100644 ui/responsive/app/components/identicon.js delete mode 100644 ui/responsive/app/components/loading.js delete mode 100644 ui/responsive/app/components/mascot.js delete mode 100644 ui/responsive/app/components/mini-account-panel.js delete mode 100644 ui/responsive/app/components/network.js delete mode 100644 ui/responsive/app/components/notice.js delete mode 100644 ui/responsive/app/components/pending-msg-details.js delete mode 100644 ui/responsive/app/components/pending-msg.js delete mode 100644 ui/responsive/app/components/pending-personal-msg-details.js delete mode 100644 ui/responsive/app/components/pending-personal-msg.js delete mode 100644 ui/responsive/app/components/pending-tx.js delete mode 100644 ui/responsive/app/components/qr-code.js delete mode 100644 ui/responsive/app/components/range-slider.js delete mode 100644 ui/responsive/app/components/shapeshift-form.js delete mode 100644 ui/responsive/app/components/shift-list-item.js delete mode 100644 ui/responsive/app/components/tab-bar.js delete mode 100644 ui/responsive/app/components/template.js delete mode 100644 ui/responsive/app/components/token-cell.js delete mode 100644 ui/responsive/app/components/token-list.js delete mode 100644 ui/responsive/app/components/tooltip.js delete mode 100644 ui/responsive/app/components/transaction-list-item-icon.js delete mode 100644 ui/responsive/app/components/transaction-list-item.js delete mode 100644 ui/responsive/app/components/transaction-list.js delete mode 100644 ui/responsive/app/conf-tx.js delete mode 100644 ui/responsive/app/config.js delete mode 100644 ui/responsive/app/conversion.json delete mode 100644 ui/responsive/app/css/debug.css delete mode 100644 ui/responsive/app/css/fonts.css delete mode 100644 ui/responsive/app/css/index.css delete mode 100644 ui/responsive/app/css/lib.css delete mode 100644 ui/responsive/app/css/reset.css delete mode 100644 ui/responsive/app/css/transitions.css delete mode 100644 ui/responsive/app/first-time/init-menu.js delete mode 100644 ui/responsive/app/img/identicon-tardigrade.png delete mode 100644 ui/responsive/app/img/identicon-walrus.png delete mode 100644 ui/responsive/app/info.js delete mode 100644 ui/responsive/app/keychains/hd/create-vault-complete.js delete mode 100644 ui/responsive/app/keychains/hd/recover-seed/confirmation.js delete mode 100644 ui/responsive/app/keychains/hd/restore-vault.js delete mode 100644 ui/responsive/app/new-keychain.js delete mode 100644 ui/responsive/app/reducers.js delete mode 100644 ui/responsive/app/reducers/app.js delete mode 100644 ui/responsive/app/reducers/identities.js delete mode 100644 ui/responsive/app/reducers/metamask.js delete mode 100644 ui/responsive/app/root.js delete mode 100644 ui/responsive/app/send.js delete mode 100644 ui/responsive/app/settings.js delete mode 100644 ui/responsive/app/store.js delete mode 100644 ui/responsive/app/template.js delete mode 100644 ui/responsive/app/unlock.js delete mode 100644 ui/responsive/app/util.js delete mode 100644 ui/responsive/css.js delete mode 100644 ui/responsive/design/00-metamask-SignIn.jpg delete mode 100644 ui/responsive/design/01-metamask-SelectAcc.jpg delete mode 100644 ui/responsive/design/02-metamask-AccDetails.jpg delete mode 100644 ui/responsive/design/02a-metamask-AccDetails-OverToken.jpg delete mode 100644 ui/responsive/design/02a-metamask-AccDetails-OverTransaction.jpg delete mode 100644 ui/responsive/design/02a-metamask-AccDetails.jpg delete mode 100644 ui/responsive/design/02b-metamask-AccDetails-Send.jpg delete mode 100644 ui/responsive/design/03-metamask-Qr.jpg delete mode 100644 ui/responsive/design/05-metamask-Menu.jpg delete mode 100644 ui/responsive/design/chromeStorePics/final_screen_dao_accounts.png delete mode 100644 ui/responsive/design/chromeStorePics/final_screen_dao_locked.png delete mode 100644 ui/responsive/design/chromeStorePics/final_screen_dao_notification.png delete mode 100644 ui/responsive/design/chromeStorePics/final_screen_wei_account.png delete mode 100644 ui/responsive/design/chromeStorePics/final_screen_wei_notification.png delete mode 100644 ui/responsive/design/chromeStorePics/icon-128.png delete mode 100644 ui/responsive/design/chromeStorePics/icon-64.png delete mode 100644 ui/responsive/design/chromeStorePics/metamask_icon.ai delete mode 100644 ui/responsive/design/chromeStorePics/promo1400560.png delete mode 100644 ui/responsive/design/chromeStorePics/promo440280.png delete mode 100644 ui/responsive/design/chromeStorePics/promo920680.png delete mode 100644 ui/responsive/design/chromeStorePics/screen_dao_accounts.png delete mode 100644 ui/responsive/design/chromeStorePics/screen_dao_locked.png delete mode 100644 ui/responsive/design/chromeStorePics/screen_dao_notification.png delete mode 100644 ui/responsive/design/chromeStorePics/screen_wei_account.png delete mode 100644 ui/responsive/design/chromeStorePics/screen_wei_notification.png delete mode 100644 ui/responsive/design/metamask-logo-eyes.png delete mode 100644 ui/responsive/design/wireframes/1st_time_use.png delete mode 100644 ui/responsive/design/wireframes/metamask_wfs_jan_13.pdf delete mode 100644 ui/responsive/design/wireframes/metamask_wfs_jan_13.png delete mode 100644 ui/responsive/design/wireframes/metamask_wfs_jan_18.pdf delete mode 100644 ui/responsive/example.js delete mode 100644 ui/responsive/index.html delete mode 100644 ui/responsive/index.js delete mode 100644 ui/responsive/lib/account-link.js delete mode 100644 ui/responsive/lib/contract-namer.js delete mode 100644 ui/responsive/lib/etherscan-prefix-for-network.js delete mode 100644 ui/responsive/lib/explorer-link.js delete mode 100644 ui/responsive/lib/icon-factory.js delete mode 100644 ui/responsive/lib/lost-accounts-notice.js delete mode 100644 ui/responsive/lib/persistent-form.js delete mode 100644 ui/responsive/lib/tx-helper.js diff --git a/app/scripts/popup-core.js b/app/scripts/popup-core.js index 156be795a..f1eb394d7 100644 --- a/app/scripts/popup-core.js +++ b/app/scripts/popup-core.js @@ -2,7 +2,7 @@ const EventEmitter = require('events').EventEmitter const async = require('async') const Dnode = require('dnode') const EthQuery = require('eth-query') -const launchMetamaskUi = require('../../ui/classic') +const launchMetamaskUi = require('../../ui') const StreamProvider = require('web3-stream-provider') const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex diff --git a/app/scripts/popup.js b/app/scripts/popup.js index 13b98d1f6..5f17f0651 100644 --- a/app/scripts/popup.js +++ b/app/scripts/popup.js @@ -1,5 +1,5 @@ const injectCss = require('inject-css') -const MetaMaskUiCss = require('../../ui/classic/css') +const MetaMaskUiCss = require('../../ui/css') const startPopup = require('./popup-core') const PortStream = require('./lib/port-stream.js') const isPopupOrNotification = require('./lib/is-popup-or-notification') diff --git a/app/scripts/responsive-core.js b/app/scripts/responsive-core.js index 3760facfa..c3fa6700d 100644 --- a/app/scripts/responsive-core.js +++ b/app/scripts/responsive-core.js @@ -2,7 +2,7 @@ const EventEmitter = require('events').EventEmitter const async = require('async') const Dnode = require('dnode') const EthQuery = require('eth-query') -const launchMetamaskUi = require('../../ui/responsive') +const launchMetamaskUi = require('../../responsive-ui') const StreamProvider = require('web3-stream-provider') const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex diff --git a/app/scripts/responsive.js b/app/scripts/responsive.js index 0ff42a4cb..6525b833b 100644 --- a/app/scripts/responsive.js +++ b/app/scripts/responsive.js @@ -1,6 +1,6 @@ const injectCss = require('inject-css') const startPopup = require('./responsive-core') -const MetaMaskUiCss = require('../../ui/responsive/css') +const MetaMaskUiCss = require('../../responsive-ui/css') const PortStream = require('./lib/port-stream.js') const ExtensionPlatform = require('./platforms/extension') const extension = require('extensionizer') diff --git a/responsive-ui/.gitignore b/responsive-ui/.gitignore new file mode 100644 index 000000000..c6b1254b5 --- /dev/null +++ b/responsive-ui/.gitignore @@ -0,0 +1,66 @@ + +# Created by https://www.gitignore.io/api/osx,node + +### OSX ### +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### Node ### +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git +node_modules + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + diff --git a/responsive-ui/app/account-detail.js b/responsive-ui/app/account-detail.js new file mode 100644 index 000000000..da1ddf98b --- /dev/null +++ b/responsive-ui/app/account-detail.js @@ -0,0 +1,289 @@ +const inherits = require('util').inherits +const extend = require('xtend') +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +const valuesFor = require('./util').valuesFor +const Identicon = require('./components/identicon') +const EthBalance = require('./components/eth-balance') +const TransactionList = require('./components/transaction-list') +const ExportAccountView = require('./components/account-export') +const ethUtil = require('ethereumjs-util') +const EditableLabel = require('./components/editable-label') +const TabBar = require('./components/tab-bar') +const TokenList = require('./components/token-list') +const AccountDropdowns = require('./components/account-dropdowns').AccountDropdowns + +module.exports = connect(mapStateToProps)(AccountDetailScreen) + +function mapStateToProps (state) { + return { + metamask: state.metamask, + identities: state.metamask.identities, + accounts: state.metamask.accounts, + address: state.metamask.selectedAddress, + accountDetail: state.appState.accountDetail, + network: state.metamask.network, + unapprovedMsgs: valuesFor(state.metamask.unapprovedMsgs), + shapeShiftTxList: state.metamask.shapeShiftTxList, + transactions: state.metamask.selectedAddressTxList || [], + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + currentAccountTab: state.metamask.currentAccountTab, + tokens: state.metamask.tokens, + } +} + +inherits(AccountDetailScreen, Component) +function AccountDetailScreen () { + Component.call(this) +} + +AccountDetailScreen.prototype.render = function () { + var props = this.props + var selected = props.address || Object.keys(props.accounts)[0] + var checksumAddress = selected && ethUtil.toChecksumAddress(selected) + var identity = props.identities[selected] + var account = props.accounts[selected] + const { network, conversionRate, currentCurrency } = props + + return ( + + h('.account-detail-section', [ + + // identicon, label, balance, etc + h('.account-data-subsection', { + style: { + margin: '0 20px', + maxWidth: '320px', + }, + }, [ + + // header - identicon + nav + h('div', { + style: { + paddingTop: '20px', + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'flex-start', + }, + }, [ + + // large identicon and addresses + h('.identicon-wrapper.select-none', [ + h(Identicon, { + diameter: 62, + address: selected, + }), + ]), + h('flex-column', { + style: { + lineHeight: '10px', + marginLeft: '15px', + }, + }, [ + h(EditableLabel, { + textValue: identity ? identity.name : '', + state: { + isEditingLabel: false, + }, + saveText: (text) => { + props.dispatch(actions.saveAccountLabel(selected, text)) + }, + }, [ + + // What is shown when not editing + edit text: + h('label.editing-label', [h('.edit-text', 'edit')]), + h( + 'div', + { + style: { + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'center', + }, + }, + [ + h( + 'h2.font-medium.color-forest', + { + name: 'edit', + style: { + }, + }, + [ + identity && identity.name, + ] + ), + h( + AccountDropdowns, + { + style: { + marginRight: '8px', + marginLeft: 'auto', + }, + selected, + network, + identities: props.identities, + }, + ), + ] + ), + ]), + h('.flex-row', { + style: { + width: '15em', + justifyContent: 'space-between', + alignItems: 'baseline', + }, + }, [ + + // address + + h('div', { + style: { + overflow: 'hidden', + textOverflow: 'ellipsis', + paddingTop: '3px', + width: '5em', + fontSize: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + marginTop: '10px', + marginBottom: '15px', + color: '#AEAEAE', + }, + }, checksumAddress), + ]), + + // account ballence + + ]), + ]), + h('.flex-row', { + style: { + justifyContent: 'space-between', + alignItems: 'flex-start', + }, + }, [ + + h(EthBalance, { + value: account && account.balance, + conversionRate, + currentCurrency, + style: { + lineHeight: '7px', + marginTop: '10px', + }, + }), + + h('button', { + onClick: () => props.dispatch(actions.buyEthView(selected)), + style: { + marginBottom: '20px', + marginRight: '8px', + position: 'absolute', + left: '219px', + }, + }, 'BUY'), + + h('button', { + onClick: () => props.dispatch(actions.showSendPage()), + style: { + marginBottom: '20px', + marginRight: '8px', + }, + }, 'SEND'), + + ]), + ]), + + // subview (tx history, pk export confirm, buy eth warning) + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + this.subview(), + ]), + + ]) + ) +} + +AccountDetailScreen.prototype.subview = function () { + var subview + try { + subview = this.props.accountDetail.subview + } catch (e) { + subview = null + } + + switch (subview) { + case 'transactions': + return this.tabSections() + case 'export': + var state = extend({key: 'export'}, this.props) + return h(ExportAccountView, state) + default: + return this.tabSections() + } +} + +AccountDetailScreen.prototype.tabSections = function () { + const { currentAccountTab } = this.props + + return h('section.tabSection', [ + + h(TabBar, { + tabs: [ + { content: 'Sent', key: 'history' }, + { content: 'Tokens', key: 'tokens' }, + ], + defaultTab: currentAccountTab || 'history', + tabSelected: (key) => { + this.props.dispatch(actions.setCurrentAccountTab(key)) + }, + }), + + this.tabSwitchView(), + ]) +} + +AccountDetailScreen.prototype.tabSwitchView = function () { + const props = this.props + const { address, network } = props + const { currentAccountTab, tokens } = this.props + + switch (currentAccountTab) { + case 'tokens': + return h(TokenList, { + userAddress: address, + network, + tokens, + addToken: () => this.props.dispatch(actions.showAddTokenPage()), + }) + default: + return this.transactionList() + } +} + +AccountDetailScreen.prototype.transactionList = function () { + const {transactions, unapprovedMsgs, address, + network, shapeShiftTxList, conversionRate } = this.props + + return h(TransactionList, { + transactions: transactions.sort((a, b) => b.time - a.time), + network, + unapprovedMsgs, + conversionRate, + address, + shapeShiftTxList, + viewPendingTx: (txId) => { + this.props.dispatch(actions.viewPendingTx(txId)) + }, + }) +} diff --git a/responsive-ui/app/accounts/import/index.js b/responsive-ui/app/accounts/import/index.js new file mode 100644 index 000000000..97b387229 --- /dev/null +++ b/responsive-ui/app/accounts/import/index.js @@ -0,0 +1,100 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../../actions') +import Select from 'react-select' + +// Subviews +const JsonImportView = require('./json.js') +const PrivateKeyImportView = require('./private-key.js') + +const menuItems = [ + 'Private Key', + 'JSON File', +] + +module.exports = connect(mapStateToProps)(AccountImportSubview) + +function mapStateToProps (state) { + return { + menuItems, + } +} + +inherits(AccountImportSubview, Component) +function AccountImportSubview () { + Component.call(this) +} + +AccountImportSubview.prototype.render = function () { + const props = this.props + const state = this.state || {} + const { menuItems } = props + const { type } = state + + return ( + h('div', { + style: { + }, + }, [ + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + props.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Import Accounts'), + ]), + h('div', { + style: { + padding: '10px', + color: 'rgb(174, 174, 174)', + }, + }, [ + + h('h3', { style: { padding: '3px' } }, 'SELECT TYPE'), + + h('style', ` + .has-value.Select--single > .Select-control .Select-value .Select-value-label, .Select-value-label { + color: rgb(174,174,174); + } + `), + + h(Select, { + name: 'import-type-select', + clearable: false, + value: type || menuItems[0], + options: menuItems.map((type) => { + return { + value: type, + label: type, + } + }), + onChange: (opt) => { + this.setState({ type: opt.value }) + }, + }), + ]), + + this.renderImportView(), + ]) + ) +} + +AccountImportSubview.prototype.renderImportView = function () { + const props = this.props + const state = this.state || {} + const { type } = state + const { menuItems } = props + const current = type || menuItems[0] + + switch (current) { + case 'Private Key': + return h(PrivateKeyImportView) + case 'JSON File': + return h(JsonImportView) + default: + return h(JsonImportView) + } +} diff --git a/responsive-ui/app/accounts/import/json.js b/responsive-ui/app/accounts/import/json.js new file mode 100644 index 000000000..158a3c923 --- /dev/null +++ b/responsive-ui/app/accounts/import/json.js @@ -0,0 +1,100 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../../actions') +const FileInput = require('react-simple-file-input').default + +const HELP_LINK = 'https://github.com/MetaMask/faq/blob/master/README.md#q-i-cant-use-the-import-feature-for-uploading-a-json-file-the-window-keeps-closing-when-i-try-to-select-a-file' + +module.exports = connect(mapStateToProps)(JsonImportSubview) + +function mapStateToProps (state) { + return { + error: state.appState.warning, + } +} + +inherits(JsonImportSubview, Component) +function JsonImportSubview () { + Component.call(this) +} + +JsonImportSubview.prototype.render = function () { + const { error } = this.props + + return ( + h('div', { + style: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: '5px 15px 0px 15px', + }, + }, [ + + h('p', 'Used by a variety of different clients'), + h('a.warning', { href: HELP_LINK, target: '_blank' }, 'File import not working? Click here!'), + + h(FileInput, { + readAs: 'text', + onLoad: this.onLoad.bind(this), + style: { + margin: '20px 0px 12px 20px', + fontSize: '15px', + }, + }), + + h('input.large-input.letter-spacey', { + type: 'password', + placeholder: 'Enter password', + id: 'json-password-box', + onKeyPress: this.createKeyringOnEnter.bind(this), + style: { + width: 260, + marginTop: 12, + }, + }), + + h('button.primary', { + onClick: this.createNewKeychain.bind(this), + style: { + margin: 12, + }, + }, 'Import'), + + error ? h('span.error', error) : null, + ]) + ) +} + +JsonImportSubview.prototype.onLoad = function (event, file) { + this.setState({file: file, fileContents: event.target.result}) +} + +JsonImportSubview.prototype.createKeyringOnEnter = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.createNewKeychain() + } +} + +JsonImportSubview.prototype.createNewKeychain = function () { + const state = this.state + const { fileContents } = state + + if (!fileContents) { + const message = 'You must select a file to import.' + return this.props.dispatch(actions.displayWarning(message)) + } + + const passwordInput = document.getElementById('json-password-box') + const password = passwordInput.value + + if (!password) { + const message = 'You must enter a password for the selected file.' + return this.props.dispatch(actions.displayWarning(message)) + } + + this.props.dispatch(actions.importNewAccount('JSON File', [ fileContents, password ])) +} diff --git a/responsive-ui/app/accounts/import/private-key.js b/responsive-ui/app/accounts/import/private-key.js new file mode 100644 index 000000000..68ccee58e --- /dev/null +++ b/responsive-ui/app/accounts/import/private-key.js @@ -0,0 +1,67 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../../actions') + +module.exports = connect(mapStateToProps)(PrivateKeyImportView) + +function mapStateToProps (state) { + return { + error: state.appState.warning, + } +} + +inherits(PrivateKeyImportView, Component) +function PrivateKeyImportView () { + Component.call(this) +} + +PrivateKeyImportView.prototype.render = function () { + const { error } = this.props + + return ( + h('div', { + style: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: '5px 15px 0px 15px', + }, + }, [ + h('span', 'Paste your private key string here'), + + h('input.large-input.letter-spacey', { + type: 'password', + id: 'private-key-box', + onKeyPress: this.createKeyringOnEnter.bind(this), + style: { + width: 260, + marginTop: 12, + }, + }), + + h('button.primary', { + onClick: this.createNewKeychain.bind(this), + style: { + margin: 12, + }, + }, 'Import'), + + error ? h('span.error', error) : null, + ]) + ) +} + +PrivateKeyImportView.prototype.createKeyringOnEnter = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.createNewKeychain() + } +} + +PrivateKeyImportView.prototype.createNewKeychain = function () { + const input = document.getElementById('private-key-box') + const privateKey = input.value + this.props.dispatch(actions.importNewAccount('Private Key', [ privateKey ])) +} diff --git a/responsive-ui/app/accounts/import/seed.js b/responsive-ui/app/accounts/import/seed.js new file mode 100644 index 000000000..b4a7c0afa --- /dev/null +++ b/responsive-ui/app/accounts/import/seed.js @@ -0,0 +1,30 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect + +module.exports = connect(mapStateToProps)(SeedImportSubview) + +function mapStateToProps (state) { + return {} +} + +inherits(SeedImportSubview, Component) +function SeedImportSubview () { + Component.call(this) +} + +SeedImportSubview.prototype.render = function () { + return ( + h('div', { + style: { + }, + }, [ + `Paste your seed phrase here!`, + h('textarea'), + h('br'), + h('button', 'Submit'), + ]) + ) +} + diff --git a/responsive-ui/app/actions.js b/responsive-ui/app/actions.js new file mode 100644 index 000000000..2c60448dd --- /dev/null +++ b/responsive-ui/app/actions.js @@ -0,0 +1,1031 @@ +const getBuyEthUrl = require('../../../app/scripts/lib/buy-eth-url') + +var actions = { + _setBackgroundConnection: _setBackgroundConnection, + + GO_HOME: 'GO_HOME', + goHome: goHome, + // menu state + getNetworkStatus: 'getNetworkStatus', + // transition state + TRANSITION_FORWARD: 'TRANSITION_FORWARD', + TRANSITION_BACKWARD: 'TRANSITION_BACKWARD', + transitionForward, + transitionBackward, + // remote state + UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE', + updateMetamaskState: updateMetamaskState, + // notices + MARK_NOTICE_READ: 'MARK_NOTICE_READ', + markNoticeRead: markNoticeRead, + SHOW_NOTICE: 'SHOW_NOTICE', + showNotice: showNotice, + CLEAR_NOTICES: 'CLEAR_NOTICES', + clearNotices: clearNotices, + markAccountsFound, + // intialize screen + CREATE_NEW_VAULT_IN_PROGRESS: 'CREATE_NEW_VAULT_IN_PROGRESS', + SHOW_CREATE_VAULT: 'SHOW_CREATE_VAULT', + SHOW_RESTORE_VAULT: 'SHOW_RESTORE_VAULT', + FORGOT_PASSWORD: 'FORGOT_PASSWORD', + forgotPassword: forgotPassword, + SHOW_INIT_MENU: 'SHOW_INIT_MENU', + SHOW_NEW_VAULT_SEED: 'SHOW_NEW_VAULT_SEED', + SHOW_INFO_PAGE: 'SHOW_INFO_PAGE', + SHOW_IMPORT_PAGE: 'SHOW_IMPORT_PAGE', + unlockMetamask: unlockMetamask, + unlockFailed: unlockFailed, + showCreateVault: showCreateVault, + showRestoreVault: showRestoreVault, + showInitializeMenu: showInitializeMenu, + showImportPage, + createNewVaultAndKeychain: createNewVaultAndKeychain, + createNewVaultAndRestore: createNewVaultAndRestore, + createNewVaultInProgress: createNewVaultInProgress, + addNewKeyring, + importNewAccount, + addNewAccount, + NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN', + navigateToNewAccountScreen, + showNewVaultSeed: showNewVaultSeed, + showInfoPage: showInfoPage, + // seed recovery actions + REVEAL_SEED_CONFIRMATION: 'REVEAL_SEED_CONFIRMATION', + revealSeedConfirmation: revealSeedConfirmation, + requestRevealSeed: requestRevealSeed, + // unlock screen + UNLOCK_IN_PROGRESS: 'UNLOCK_IN_PROGRESS', + UNLOCK_FAILED: 'UNLOCK_FAILED', + UNLOCK_METAMASK: 'UNLOCK_METAMASK', + LOCK_METAMASK: 'LOCK_METAMASK', + tryUnlockMetamask: tryUnlockMetamask, + lockMetamask: lockMetamask, + unlockInProgress: unlockInProgress, + // error handling + displayWarning: displayWarning, + DISPLAY_WARNING: 'DISPLAY_WARNING', + HIDE_WARNING: 'HIDE_WARNING', + hideWarning: hideWarning, + // accounts screen + SET_SELECTED_ACCOUNT: 'SET_SELECTED_ACCOUNT', + SHOW_ACCOUNT_DETAIL: 'SHOW_ACCOUNT_DETAIL', + SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE', + SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE', + SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE', + SET_CURRENT_FIAT: 'SET_CURRENT_FIAT', + setCurrentCurrency: setCurrentCurrency, + setCurrentAccountTab, + // account detail screen + SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', + showSendPage: showSendPage, + ADD_TO_ADDRESS_BOOK: 'ADD_TO_ADDRESS_BOOK', + addToAddressBook: addToAddressBook, + REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT', + requestExportAccount: requestExportAccount, + EXPORT_ACCOUNT: 'EXPORT_ACCOUNT', + exportAccount: exportAccount, + SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY', + showPrivateKey: showPrivateKey, + SAVE_ACCOUNT_LABEL: 'SAVE_ACCOUNT_LABEL', + saveAccountLabel: saveAccountLabel, + // tx conf screen + COMPLETED_TX: 'COMPLETED_TX', + TRANSACTION_ERROR: 'TRANSACTION_ERROR', + NEXT_TX: 'NEXT_TX', + PREVIOUS_TX: 'PREV_TX', + signMsg: signMsg, + cancelMsg: cancelMsg, + signPersonalMsg, + cancelPersonalMsg, + sendTx: sendTx, + signTx: signTx, + updateAndApproveTx, + cancelTx: cancelTx, + completedTx: completedTx, + txError: txError, + nextTx: nextTx, + previousTx: previousTx, + viewPendingTx: viewPendingTx, + VIEW_PENDING_TX: 'VIEW_PENDING_TX', + // app messages + confirmSeedWords: confirmSeedWords, + showAccountDetail: showAccountDetail, + BACK_TO_ACCOUNT_DETAIL: 'BACK_TO_ACCOUNT_DETAIL', + backToAccountDetail: backToAccountDetail, + showAccountsPage: showAccountsPage, + showConfTxPage: showConfTxPage, + // config screen + SHOW_CONFIG_PAGE: 'SHOW_CONFIG_PAGE', + SET_RPC_TARGET: 'SET_RPC_TARGET', + SET_DEFAULT_RPC_TARGET: 'SET_DEFAULT_RPC_TARGET', + SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE', + USE_ETHERSCAN_PROVIDER: 'USE_ETHERSCAN_PROVIDER', + useEtherscanProvider: useEtherscanProvider, + showConfigPage, + SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', + showAddTokenPage, + addToken, + setRpcTarget: setRpcTarget, + setDefaultRpcTarget: setDefaultRpcTarget, + setProviderType: setProviderType, + // loading overlay + SHOW_LOADING: 'SHOW_LOADING_INDICATION', + HIDE_LOADING: 'HIDE_LOADING_INDICATION', + showLoadingIndication: showLoadingIndication, + hideLoadingIndication: hideLoadingIndication, + // buy Eth with coinbase + BUY_ETH: 'BUY_ETH', + buyEth: buyEth, + buyEthView: buyEthView, + BUY_ETH_VIEW: 'BUY_ETH_VIEW', + COINBASE_SUBVIEW: 'COINBASE_SUBVIEW', + coinBaseSubview: coinBaseSubview, + SHAPESHIFT_SUBVIEW: 'SHAPESHIFT_SUBVIEW', + shapeShiftSubview: shapeShiftSubview, + PAIR_UPDATE: 'PAIR_UPDATE', + pairUpdate: pairUpdate, + coinShiftRquest: coinShiftRquest, + SHOW_SUB_LOADING_INDICATION: 'SHOW_SUB_LOADING_INDICATION', + showSubLoadingIndication: showSubLoadingIndication, + HIDE_SUB_LOADING_INDICATION: 'HIDE_SUB_LOADING_INDICATION', + hideSubLoadingIndication: hideSubLoadingIndication, +// QR STUFF: + SHOW_QR: 'SHOW_QR', + showQrView: showQrView, + reshowQrCode: reshowQrCode, + SHOW_QR_VIEW: 'SHOW_QR_VIEW', +// FORGOT PASSWORD: + BACK_TO_INIT_MENU: 'BACK_TO_INIT_MENU', + goBackToInitView: goBackToInitView, + RECOVERY_IN_PROGRESS: 'RECOVERY_IN_PROGRESS', + BACK_TO_UNLOCK_VIEW: 'BACK_TO_UNLOCK_VIEW', + backToUnlockView: backToUnlockView, + // SHOWING KEYCHAIN + SHOW_NEW_KEYCHAIN: 'SHOW_NEW_KEYCHAIN', + showNewKeychain: showNewKeychain, + + callBackgroundThenUpdate, + forceUpdateMetamaskState, +} + +module.exports = actions + +var background = null +function _setBackgroundConnection (backgroundConnection) { + background = backgroundConnection +} + +function goHome () { + return { + type: actions.GO_HOME, + } +} + +// async actions + +function tryUnlockMetamask (password) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + dispatch(actions.unlockInProgress()) + log.debug(`background.submitPassword`) + background.submitPassword(password, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + dispatch(actions.unlockFailed(err.message)) + } else { + dispatch(actions.transitionForward()) + forceUpdateMetamaskState(dispatch) + } + }) + } +} + +function transitionForward () { + return { + type: this.TRANSITION_FORWARD, + } +} + +function transitionBackward () { + return { + type: this.TRANSITION_BACKWARD, + } +} + +function confirmSeedWords () { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.clearSeedWordCache`) + background.clearSeedWordCache((err, account) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + + log.info('Seed word cache cleared. ' + account) + dispatch(actions.showAccountDetail(account)) + }) + } +} + +function createNewVaultAndRestore (password, seed) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.createNewVaultAndRestore`) + background.createNewVaultAndRestore(password, seed, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.showAccountsPage()) + }) + } +} + +function createNewVaultAndKeychain (password) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.createNewVaultAndKeychain`) + background.createNewVaultAndKeychain(password, (err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + log.debug(`background.placeSeedWords`) + background.placeSeedWords((err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch(actions.hideLoadingIndication()) + forceUpdateMetamaskState(dispatch) + }) + }) + } +} + +function revealSeedConfirmation () { + return { + type: this.REVEAL_SEED_CONFIRMATION, + } +} + +function requestRevealSeed (password) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.submitPassword`) + background.submitPassword(password, (err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + log.debug(`background.placeSeedWords`) + background.placeSeedWords((err, result) => { + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.hideLoadingIndication()) + dispatch(actions.showNewVaultSeed(result)) + }) + }) + } +} + +function addNewKeyring (type, opts) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.addNewKeyring`) + background.addNewKeyring(type, opts, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.showAccountsPage()) + }) + } +} + +function importNewAccount (strategy, args) { + return (dispatch) => { + dispatch(actions.showLoadingIndication('This may take a while, be patient.')) + log.debug(`background.importAccountWithStrategy`) + background.importAccountWithStrategy(strategy, args, (err) => { + if (err) return dispatch(actions.displayWarning(err.message)) + log.debug(`background.getState`) + background.getState((err, newState) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch(actions.updateMetamaskState(newState)) + dispatch({ + type: actions.SHOW_ACCOUNT_DETAIL, + value: newState.selectedAddress, + }) + }) + }) + } +} + +function navigateToNewAccountScreen () { + return { + type: this.NEW_ACCOUNT_SCREEN, + } +} + +function addNewAccount () { + log.debug(`background.addNewAccount`) + return callBackgroundThenUpdate(background.addNewAccount) +} + +function showInfoPage () { + return { + type: actions.SHOW_INFO_PAGE, + } +} + +function setCurrentCurrency (currencyCode) { + return (dispatch) => { + dispatch(this.showLoadingIndication()) + log.debug(`background.setCurrentCurrency`) + background.setCurrentCurrency(currencyCode, (err, data) => { + dispatch(this.hideLoadingIndication()) + if (err) { + log.error(err.stack) + return dispatch(actions.displayWarning(err.message)) + } + dispatch({ + type: this.SET_CURRENT_FIAT, + value: { + currentCurrency: data.currentCurrency, + conversionRate: data.conversionRate, + conversionDate: data.conversionDate, + }, + }) + }) + } +} + +function signMsg (msgData) { + log.debug('action - signMsg') + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + + log.debug(`actions calling background.signMessage`) + background.signMessage(msgData, (err, newState) => { + log.debug('signMessage called back') + dispatch(actions.updateMetamaskState(newState)) + dispatch(actions.hideLoadingIndication()) + + if (err) log.error(err) + if (err) return dispatch(actions.displayWarning(err.message)) + + dispatch(actions.completedTx(msgData.metamaskId)) + }) + } +} + +function signPersonalMsg (msgData) { + log.debug('action - signPersonalMsg') + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + + log.debug(`actions calling background.signPersonalMessage`) + background.signPersonalMessage(msgData, (err, newState) => { + log.debug('signPersonalMessage called back') + dispatch(actions.updateMetamaskState(newState)) + dispatch(actions.hideLoadingIndication()) + + if (err) log.error(err) + if (err) return dispatch(actions.displayWarning(err.message)) + + dispatch(actions.completedTx(msgData.metamaskId)) + }) + } +} + +function signTx (txData) { + return (dispatch) => { + global.ethQuery.sendTransaction(txData, (err, data) => { + dispatch(actions.hideLoadingIndication()) + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.hideWarning()) + }) + dispatch(this.showConfTxPage()) + } +} + +function sendTx (txData) { + log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`) + return (dispatch) => { + log.debug(`actions calling background.approveTransaction`) + background.approveTransaction(txData.id, (err) => { + if (err) { + dispatch(actions.txError(err)) + return log.error(err.message) + } + dispatch(actions.completedTx(txData.id)) + }) + } +} + +function updateAndApproveTx (txData) { + log.info('actions: updateAndApproveTx: ' + JSON.stringify(txData)) + return (dispatch) => { + log.debug(`actions calling background.updateAndApproveTx`) + background.updateAndApproveTransaction(txData, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + dispatch(actions.txError(err)) + return log.error(err.message) + } + dispatch(actions.completedTx(txData.id)) + }) + } +} + +function completedTx (id) { + return { + type: actions.COMPLETED_TX, + value: id, + } +} + +function txError (err) { + return { + type: actions.TRANSACTION_ERROR, + message: err.message, + } +} + +function cancelMsg (msgData) { + log.debug(`background.cancelMessage`) + background.cancelMessage(msgData.id) + return actions.completedTx(msgData.id) +} + +function cancelPersonalMsg (msgData) { + const id = msgData.id + background.cancelPersonalMessage(id) + return actions.completedTx(id) +} + +function cancelTx (txData) { + log.debug(`background.cancelTransaction`) + background.cancelTransaction(txData.id) + return actions.completedTx(txData.id) +} + +// +// initialize screen +// + +function showCreateVault () { + return { + type: actions.SHOW_CREATE_VAULT, + } +} + +function showRestoreVault () { + return { + type: actions.SHOW_RESTORE_VAULT, + } +} + +function forgotPassword () { + return { + type: actions.FORGOT_PASSWORD, + } +} + +function showInitializeMenu () { + return { + type: actions.SHOW_INIT_MENU, + } +} + +function showImportPage () { + return { + type: actions.SHOW_IMPORT_PAGE, + } +} + +function createNewVaultInProgress () { + return { + type: actions.CREATE_NEW_VAULT_IN_PROGRESS, + } +} + +function showNewVaultSeed (seed) { + return { + type: actions.SHOW_NEW_VAULT_SEED, + value: seed, + } +} + +function backToUnlockView () { + return { + type: actions.BACK_TO_UNLOCK_VIEW, + } +} + +function showNewKeychain () { + return { + type: actions.SHOW_NEW_KEYCHAIN, + } +} + +// +// unlock screen +// + +function unlockInProgress () { + return { + type: actions.UNLOCK_IN_PROGRESS, + } +} + +function unlockFailed (message) { + return { + type: actions.UNLOCK_FAILED, + value: message, + } +} + +function unlockMetamask (account) { + return { + type: actions.UNLOCK_METAMASK, + value: account, + } +} + +function updateMetamaskState (newState) { + return { + type: actions.UPDATE_METAMASK_STATE, + value: newState, + } +} + +function lockMetamask () { + log.debug(`background.setLocked`) + return callBackgroundThenUpdate(background.setLocked) +} + +function setCurrentAccountTab (newTabName) { + log.debug(`background.setCurrentAccountTab: ${newTabName}`) + return callBackgroundThenUpdateNoSpinner(background.setCurrentAccountTab, newTabName) +} + +function showAccountDetail (address) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.setSelectedAddress`) + background.setSelectedAddress(address, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch({ + type: actions.SHOW_ACCOUNT_DETAIL, + value: address, + }) + }) + } +} + +function backToAccountDetail (address) { + return { + type: actions.BACK_TO_ACCOUNT_DETAIL, + value: address, + } +} + +function showAccountsPage () { + return { + type: actions.SHOW_ACCOUNTS_PAGE, + } +} + +function showConfTxPage (transForward = true) { + return { + type: actions.SHOW_CONF_TX_PAGE, + transForward: transForward, + } +} + +function nextTx () { + return { + type: actions.NEXT_TX, + } +} + +function viewPendingTx (txId) { + return { + type: actions.VIEW_PENDING_TX, + value: txId, + } +} + +function previousTx () { + return { + type: actions.PREVIOUS_TX, + } +} + +function showConfigPage (transitionForward = true) { + return { + type: actions.SHOW_CONFIG_PAGE, + value: transitionForward, + } +} + +function showAddTokenPage (transitionForward = true) { + return { + type: actions.SHOW_ADD_TOKEN_PAGE, + value: transitionForward, + } +} + +function addToken (address, symbol, decimals) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + background.addToken(address, symbol, decimals, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + setTimeout(() => { + dispatch(actions.goHome()) + }, 250) + }) + } +} + +function goBackToInitView () { + return { + type: actions.BACK_TO_INIT_MENU, + } +} + +// +// notice +// + +function markNoticeRead (notice) { + return (dispatch) => { + dispatch(this.showLoadingIndication()) + log.debug(`background.markNoticeRead`) + background.markNoticeRead(notice, (err, notice) => { + dispatch(this.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err)) + } + if (notice) { + return dispatch(actions.showNotice(notice)) + } else { + dispatch(this.clearNotices()) + return { + type: actions.SHOW_ACCOUNTS_PAGE, + } + } + }) + } +} + +function showNotice (notice) { + return { + type: actions.SHOW_NOTICE, + value: notice, + } +} + +function clearNotices () { + return { + type: actions.CLEAR_NOTICES, + } +} + +function markAccountsFound () { + log.debug(`background.markAccountsFound`) + return callBackgroundThenUpdate(background.markAccountsFound) +} + +// +// config +// + +// default rpc target refers to localhost:8545 in this instance. +function setDefaultRpcTarget (rpcList) { + log.debug(`background.setDefaultRpcTarget`) + return (dispatch) => { + background.setDefaultRpc((err, result) => { + if (err) { + log.error(err) + return dispatch(self.displayWarning('Had a problem changing networks.')) + } + }) + } +} + +function setRpcTarget (newRpc) { + log.debug(`background.setRpcTarget`) + return (dispatch) => { + background.setCustomRpc(newRpc, (err, result) => { + if (err) { + log.error(err) + return dispatch(self.displayWarning('Had a problem changing networks!')) + } + }) + } +} + +// Calls the addressBookController to add a new address. +function addToAddressBook (recipient, nickname) { + log.debug(`background.addToAddressBook`) + return (dispatch) => { + background.setAddressBook(recipient, nickname, (err, result) => { + if (err) { + log.error(err) + return dispatch(self.displayWarning('Address book failed to update')) + } + }) + } +} + +function setProviderType (type) { + log.debug(`background.setProviderType`) + background.setProviderType(type) + return { + type: actions.SET_PROVIDER_TYPE, + value: type, + } +} + +function useEtherscanProvider () { + log.debug(`background.useEtherscanProvider`) + background.useEtherscanProvider() + return { + type: actions.USE_ETHERSCAN_PROVIDER, + } +} + +function showLoadingIndication (message) { + return { + type: actions.SHOW_LOADING, + value: message, + } +} + +function hideLoadingIndication () { + return { + type: actions.HIDE_LOADING, + } +} + +function showSubLoadingIndication () { + return { + type: actions.SHOW_SUB_LOADING_INDICATION, + } +} + +function hideSubLoadingIndication () { + return { + type: actions.HIDE_SUB_LOADING_INDICATION, + } +} + +function displayWarning (text) { + return { + type: actions.DISPLAY_WARNING, + value: text, + } +} + +function hideWarning () { + return { + type: actions.HIDE_WARNING, + } +} + +function requestExportAccount () { + return { + type: actions.REQUEST_ACCOUNT_EXPORT, + } +} + +function exportAccount (password, address) { + var self = this + + return function (dispatch) { + dispatch(self.showLoadingIndication()) + + log.debug(`background.submitPassword`) + background.submitPassword(password, function (err) { + if (err) { + log.error('Error in submiting password.') + dispatch(self.hideLoadingIndication()) + return dispatch(self.displayWarning('Incorrect Password.')) + } + log.debug(`background.exportAccount`) + background.exportAccount(address, function (err, result) { + dispatch(self.hideLoadingIndication()) + + if (err) { + log.error(err) + return dispatch(self.displayWarning('Had a problem exporting the account.')) + } + + dispatch(self.showPrivateKey(result)) + }) + }) + } +} + +function showPrivateKey (key) { + return { + type: actions.SHOW_PRIVATE_KEY, + value: key, + } +} + +function saveAccountLabel (account, label) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.saveAccountLabel`) + background.saveAccountLabel(account, label, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch({ + type: actions.SAVE_ACCOUNT_LABEL, + value: { account, label }, + }) + }) + } +} + +function showSendPage () { + return { + type: actions.SHOW_SEND_PAGE, + } +} + +function buyEth (opts) { + return (dispatch) => { + const url = getBuyEthUrl(opts) + global.platform.openWindow({ url }) + dispatch({ + type: actions.BUY_ETH, + }) + } +} + +function buyEthView (address) { + return { + type: actions.BUY_ETH_VIEW, + value: address, + } +} + +function coinBaseSubview () { + return { + type: actions.COINBASE_SUBVIEW, + } +} + +function pairUpdate (coin) { + return (dispatch) => { + dispatch(actions.showSubLoadingIndication()) + dispatch(actions.hideWarning()) + shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { + dispatch(actions.hideSubLoadingIndication()) + dispatch({ + type: actions.PAIR_UPDATE, + value: { + marketinfo: mktResponse, + }, + }) + }) + } +} + +function shapeShiftSubview (network) { + var pair = 'btc_eth' + + return (dispatch) => { + dispatch(actions.showSubLoadingIndication()) + shapeShiftRequest('marketinfo', {pair}, (mktResponse) => { + shapeShiftRequest('getcoins', {}, (response) => { + dispatch(actions.hideSubLoadingIndication()) + if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) + dispatch({ + type: actions.SHAPESHIFT_SUBVIEW, + value: { + marketinfo: mktResponse, + coinOptions: response, + }, + }) + }) + }) + } +} + +function coinShiftRquest (data, marketData) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + shapeShiftRequest('shift', { method: 'POST', data}, (response) => { + dispatch(actions.hideLoadingIndication()) + if (response.error) return dispatch(actions.displayWarning(response.error)) + var message = ` + Deposit your ${response.depositType} to the address bellow:` + log.debug(`background.createShapeShiftTx`) + background.createShapeShiftTx(response.deposit, response.depositType) + dispatch(actions.showQrView(response.deposit, [message].concat(marketData))) + }) + } +} + +function showQrView (data, message) { + return { + type: actions.SHOW_QR_VIEW, + value: { + message: message, + data: data, + }, + } +} +function reshowQrCode (data, coin) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { + if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) + + var message = [ + `Deposit your ${coin} to the address bellow:`, + `Deposit Limit: ${mktResponse.limit}`, + `Deposit Minimum:${mktResponse.minimum}`, + ] + + dispatch(actions.hideLoadingIndication()) + return dispatch(actions.showQrView(data, message)) + }) + } +} + +function shapeShiftRequest (query, options, cb) { + var queryResponse, method + !options ? options = {} : null + options.method ? method = options.method : method = 'GET' + + var requestListner = function (request) { + queryResponse = JSON.parse(this.responseText) + cb ? cb(queryResponse) : null + return queryResponse + } + + var shapShiftReq = new XMLHttpRequest() + shapShiftReq.addEventListener('load', requestListner) + shapShiftReq.open(method, `https://shapeshift.io/${query}/${options.pair ? options.pair : ''}`, true) + + if (options.method === 'POST') { + var jsonObj = JSON.stringify(options.data) + shapShiftReq.setRequestHeader('Content-Type', 'application/json') + return shapShiftReq.send(jsonObj) + } else { + return shapShiftReq.send() + } +} + +// Call Background Then Update +// +// A function generator for a common pattern wherein: +// We show loading indication. +// We call a background method. +// We hide loading indication. +// If it errored, we show a warning. +// If it didn't, we update the state. +function callBackgroundThenUpdateNoSpinner (method, ...args) { + return (dispatch) => { + method.call(background, ...args, (err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + forceUpdateMetamaskState(dispatch) + }) + } +} + +function callBackgroundThenUpdate (method, ...args) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + method.call(background, ...args, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + forceUpdateMetamaskState(dispatch) + }) + } +} + +function forceUpdateMetamaskState (dispatch) { + log.debug(`background.getState`) + background.getState((err, newState) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch(actions.updateMetamaskState(newState)) + }) +} diff --git a/responsive-ui/app/add-token.js b/responsive-ui/app/add-token.js new file mode 100644 index 000000000..b303b5c0d --- /dev/null +++ b/responsive-ui/app/add-token.js @@ -0,0 +1,219 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') + +const ethUtil = require('ethereumjs-util') +const abi = require('human-standard-token-abi') +const Eth = require('ethjs-query') +const EthContract = require('ethjs-contract') + +const emptyAddr = '0x0000000000000000000000000000000000000000' + +module.exports = connect(mapStateToProps)(AddTokenScreen) + +function mapStateToProps (state) { + return { + } +} + +inherits(AddTokenScreen, Component) +function AddTokenScreen () { + this.state = { + warning: null, + address: null, + symbol: 'TOKEN', + decimals: 18, + } + Component.call(this) +} + +AddTokenScreen.prototype.render = function () { + const state = this.state + const props = this.props + const { warning, symbol, decimals } = state + + return ( + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + props.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Add Token'), + ]), + + h('.error', { + style: { + display: warning ? 'block' : 'none', + padding: '0 20px', + textAlign: 'center', + }, + }, warning), + + // conf view + h('.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-space-around', { + style: { + padding: '20px', + }, + }, [ + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Token Address'), + ]), + + h('section.flex-row.flex-center', [ + h('input#token-address', { + name: 'address', + placeholder: 'Token Address', + onChange: this.tokenAddressDidChange.bind(this), + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + }), + ]), + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Token Sybmol'), + ]), + + h('div', { style: {display: 'flex'} }, [ + h('input#token_symbol', { + placeholder: `Like "ETH"`, + value: symbol, + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onChange: (event) => { + var element = event.target + var symbol = element.value + this.setState({ symbol }) + }, + }), + ]), + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Decimals of Precision'), + ]), + + h('div', { style: {display: 'flex'} }, [ + h('input#token_decimals', { + value: decimals, + type: 'number', + min: 0, + max: 36, + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onChange: (event) => { + var element = event.target + var decimals = element.value.trim() + this.setState({ decimals }) + }, + }), + ]), + + h('button', { + style: { + alignSelf: 'center', + }, + onClick: (event) => { + const valid = this.validateInputs() + if (!valid) return + + const { address, symbol, decimals } = this.state + this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) + }, + }, 'Add'), + ]), + ]), + ]) + ) +} + +AddTokenScreen.prototype.componentWillMount = function () { + if (typeof global.ethereumProvider === 'undefined') return + + this.eth = new Eth(global.ethereumProvider) + this.contract = new EthContract(this.eth) + this.TokenContract = this.contract(abi) +} + +AddTokenScreen.prototype.tokenAddressDidChange = function (event) { + const el = event.target + const address = el.value.trim() + if (ethUtil.isValidAddress(address) && address !== emptyAddr) { + this.setState({ address }) + this.attemptToAutoFillTokenParams(address) + } +} + +AddTokenScreen.prototype.validateInputs = function () { + let msg = '' + const state = this.state + const { address, symbol, decimals } = state + + const validAddress = ethUtil.isValidAddress(address) + if (!validAddress) { + msg += 'Address is invalid. ' + } + + const validDecimals = decimals >= 0 && decimals < 36 + if (!validDecimals) { + msg += 'Decimals must be at least 0, and not over 36. ' + } + + const symbolLen = symbol.trim().length + const validSymbol = symbolLen > 0 && symbolLen < 10 + if (!validSymbol) { + msg += 'Symbol must be between 0 and 10 characters.' + } + + const isValid = validAddress && validDecimals + + if (!isValid) { + this.setState({ + warning: msg, + }) + } else { + this.setState({ warning: null }) + } + + return isValid +} + +AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) { + const contract = this.TokenContract.at(address) + + const results = await Promise.all([ + contract.symbol(), + contract.decimals(), + ]) + + const [ symbol, decimals ] = results + if (symbol && decimals) { + console.log('SETTING SYMBOL AND DECIMALS', { symbol, decimals }) + this.setState({ symbol: symbol[0], decimals: decimals[0].toString() }) + } +} + diff --git a/responsive-ui/app/app.js b/responsive-ui/app/app.js new file mode 100644 index 000000000..1cfa2d7a9 --- /dev/null +++ b/responsive-ui/app/app.js @@ -0,0 +1,580 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('./actions') +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +// init +const InitializeMenuScreen = require('./first-time/init-menu') +const NewKeyChainScreen = require('./new-keychain') +// unlock +const UnlockScreen = require('./unlock') +// accounts +const AccountDetailScreen = require('./account-detail') +const SendTransactionScreen = require('./send') +const ConfirmTxScreen = require('./conf-tx') +// notice +const NoticeScreen = require('./components/notice') +const generateLostAccountsNotice = require('../lib/lost-accounts-notice') +// other views +const ConfigScreen = require('./config') +const AddTokenScreen = require('./add-token') +const Import = require('./accounts/import') +const InfoScreen = require('./info') +const Loading = require('./components/loading') +const SandwichExpando = require('sandwich-expando') +const Dropdown = require('./components/dropdown').Dropdown +const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem +const NetworkIndicator = require('./components/network') +const BuyView = require('./components/buy-button-subview') +const QrView = require('./components/qr-code') +const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete') +const HDRestoreVaultScreen = require('./keychains/hd/restore-vault') +const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation') + +module.exports = connect(mapStateToProps)(App) + +inherits(App, Component) +function App () { Component.call(this) } + +function mapStateToProps (state) { + return { + // state from plugin + isLoading: state.appState.isLoading, + loadingMessage: state.appState.loadingMessage, + noActiveNotices: state.metamask.noActiveNotices, + isInitialized: state.metamask.isInitialized, + isUnlocked: state.metamask.isUnlocked, + currentView: state.appState.currentView, + activeAddress: state.appState.activeAddress, + transForward: state.appState.transForward, + seedWords: state.metamask.seedWords, + unapprovedTxs: state.metamask.unapprovedTxs, + unapprovedMsgs: state.metamask.unapprovedMsgs, + menuOpen: state.appState.menuOpen, + network: state.metamask.network, + provider: state.metamask.provider, + forgottenPassword: state.appState.forgottenPassword, + lastUnreadNotice: state.metamask.lastUnreadNotice, + lostAccounts: state.metamask.lostAccounts, + frequentRpcList: state.metamask.frequentRpcList || [], + } +} + +App.prototype.render = function () { + var props = this.props + const { isLoading, loadingMessage, transForward, network } = props + const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config' + const loadMessage = loadingMessage || isLoadingNetwork ? + `Connecting to ${this.getNetworkName()}` : null + + log.debug('Main ui render function') + + return ( + + h('.flex-column.flex-grow.full-height', { + style: { + // Windows was showing a vertical scroll bar: + overflow: 'hidden', + position: 'relative', + }, + }, [ + + // app bar + this.renderAppBar(), + this.renderNetworkDropdown(), + this.renderDropdown(), + + h(Loading, { + isLoading: isLoading || isLoadingNetwork, + loadingMessage: loadMessage, + }), + + // panel content + h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), [ + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + this.renderPrimary(), + ]), + ]), + ]) + ) +} + +App.prototype.renderAppBar = function () { + if (window.METAMASK_UI_TYPE === 'notification') { + return null + } + + const props = this.props + const state = this.state || {} + const isNetworkMenuOpen = state.isNetworkMenuOpen || false + + return ( + + h('div', [ + + h('.app-header.flex-row.flex-space-between', { + style: { + alignItems: 'center', + visibility: props.isUnlocked ? 'visible' : 'none', + background: props.isUnlocked ? 'white' : 'none', + height: '38px', + position: 'relative', + zIndex: 12, + }, + }, [ + + h('div.left-menu-section', { + style: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + }, + }, [ + + // mini logo + h('img', { + height: 24, + width: 24, + src: '/images/icon-128.png', + }), + + h(NetworkIndicator, { + network: this.props.network, + provider: this.props.provider, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen }) + }, + }), + ]), + + // metamask name + props.isUnlocked && h('h1', { + style: { + position: 'relative', + left: '9px', + }, + }, 'MetaMask'), + + props.isUnlocked && h('div', { + style: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + }, + }, [ + + // hamburger + props.isUnlocked && h(SandwichExpando, { + width: 16, + barHeight: 2, + padding: 0, + isOpen: state.isMainMenuOpen, + color: 'rgb(247,146,30)', + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + this.setState({ isMainMenuOpen: !state.isMainMenuOpen }) + }, + }), + ]), + ]), + ]) + ) +} + +App.prototype.renderNetworkDropdown = function () { + const props = this.props + const { provider: { type: providerType, rpcTarget: activeNetwork } } = props + const rpcList = props.frequentRpcList + const state = this.state || {} + const isOpen = state.isNetworkMenuOpen + + return h(Dropdown, { + isOpen, + onClickOutside: (event) => { + this.setState({ isNetworkMenuOpen: !isOpen }) + }, + zIndex: 11, + style: { + position: 'absolute', + left: '2px', + top: '36px', + }, + innerStyle: {}, + }, [ + + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setProviderType('mainnet')), + }, + [ + h('.menu-icon.diamond'), + 'Main Ethereum Network', + providerType === 'mainnet' ? h('.check', '✓') : null, + ] + ), + + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setProviderType('ropsten')), + }, + [ + h('.menu-icon.red-dot'), + 'Ropsten Test Network', + providerType === 'ropsten' ? h('.check', '✓') : null, + ] + ), + + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setProviderType('kovan')), + }, + [ + h('.menu-icon.hollow-diamond'), + 'Kovan Test Network', + providerType === 'kovan' ? h('.check', '✓') : null, + ] + ), + + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setProviderType('rinkeby')), + }, + [ + h('.menu-icon.golden-square'), + 'Rinkeby Test Network', + providerType === 'rinkeby' ? h('.check', '✓') : null, + ] + ), + + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setDefaultRpcTarget(rpcList)), + }, + [ + h('i.fa.fa-question-circle.fa-lg.menu-icon'), + 'Localhost 8545', + activeNetwork === 'http://localhost:8545' ? h('.check', '✓') : null, + ] + ), + + this.renderCustomOption(props.provider), + this.renderCommonRpc(rpcList, props.provider), + + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => this.props.dispatch(actions.showConfigPage()), + }, + [ + h('i.fa.fa-question-circle.fa-lg.menu-icon'), + 'Custom RPC', + activeNetwork === 'custom' ? h('.check', '✓') : null, + ] + ), + + ]) +} + +App.prototype.renderDropdown = function () { + const state = this.state || {} + const isOpen = state.isMainMenuOpen + + return h(Dropdown, { + isOpen: isOpen, + zIndex: 11, + onClickOutside: (event) => { + this.setState({ isMainMenuOpen: !isOpen }) + }, + style: { + position: 'absolute', + right: '2px', + top: '38px', + }, + innerStyle: {}, + }, [ + h(DropdownMenuItem, { + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + onClick: () => { this.props.dispatch(actions.showConfigPage()) }, + }, 'Settings'), + + h(DropdownMenuItem, { + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + onClick: () => { this.props.dispatch(actions.showImportPage()) }, + }, 'Import Account'), + + h(DropdownMenuItem, { + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + onClick: () => { this.props.dispatch(actions.lockMetamask()) }, + }, 'Lock'), + + h(DropdownMenuItem, { + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + onClick: () => { this.props.dispatch(actions.showInfoPage()) }, + }, 'Info/Help'), + ]) +} + +App.prototype.renderBackButton = function (style, justArrow = false) { + var props = this.props + return ( + h('.flex-row', { + key: 'leftArrow', + style: style, + onClick: () => props.dispatch(actions.goBackToInitView()), + }, [ + h('i.fa.fa-arrow-left.cursor-pointer'), + justArrow ? null : h('div.cursor-pointer', { + style: { + marginLeft: '3px', + }, + onClick: () => props.dispatch(actions.goBackToInitView()), + }, 'BACK'), + ]) + ) +} + +App.prototype.renderPrimary = function () { + log.debug('rendering primary') + var props = this.props + + // notices + if (!props.noActiveNotices) { + log.debug('rendering notice screen for unread notices.') + return h(NoticeScreen, { + notice: props.lastUnreadNotice, + key: 'NoticeScreen', + onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)), + }) + } else if (props.lostAccounts && props.lostAccounts.length > 0) { + log.debug('rendering notice screen for lost accounts view.') + return h(NoticeScreen, { + notice: generateLostAccountsNotice(props.lostAccounts), + key: 'LostAccountsNotice', + onConfirm: () => props.dispatch(actions.markAccountsFound()), + }) + } + + if (props.seedWords) { + log.debug('rendering seed words') + return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'}) + } + + // show initialize screen + if (!props.isInitialized || props.forgottenPassword) { + // show current view + log.debug('rendering an initialize screen') + switch (props.currentView.name) { + + case 'restoreVault': + log.debug('rendering restore vault screen') + return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) + + default: + log.debug('rendering menu screen') + return h(InitializeMenuScreen, {key: 'menuScreenInit'}) + } + } + + // show unlock screen + if (!props.isUnlocked) { + switch (props.currentView.name) { + + case 'restoreVault': + log.debug('rendering restore vault screen') + return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) + + case 'config': + log.debug('rendering config screen from unlock screen.') + return h(ConfigScreen, {key: 'config'}) + + default: + log.debug('rendering locked screen') + return h(UnlockScreen, {key: 'locked'}) + } + } + + // show current view + switch (props.currentView.name) { + + case 'accountDetail': + log.debug('rendering account detail screen') + return h(AccountDetailScreen, {key: 'account-detail'}) + + case 'sendTransaction': + log.debug('rendering send tx screen') + return h(SendTransactionScreen, {key: 'send-transaction'}) + + case 'newKeychain': + log.debug('rendering new keychain screen') + return h(NewKeyChainScreen, {key: 'new-keychain'}) + + case 'confTx': + log.debug('rendering confirm tx screen') + return h(ConfirmTxScreen, {key: 'confirm-tx'}) + + case 'add-token': + log.debug('rendering add-token screen from unlock screen.') + return h(AddTokenScreen, {key: 'add-token'}) + + case 'config': + log.debug('rendering config screen') + return h(ConfigScreen, {key: 'config'}) + + case 'import-menu': + log.debug('rendering import screen') + return h(Import, {key: 'import-menu'}) + + case 'reveal-seed-conf': + log.debug('rendering reveal seed confirmation screen') + return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'}) + + case 'info': + log.debug('rendering info screen') + return h(InfoScreen, {key: 'info'}) + + case 'buyEth': + log.debug('rendering buy ether screen') + return h(BuyView, {key: 'buyEthView'}) + + case 'qr': + log.debug('rendering show qr screen') + return h('div', { + style: { + position: 'absolute', + height: '100%', + top: '0px', + left: '0px', + }, + }, [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { + onClick: () => props.dispatch(actions.backToAccountDetail(props.activeAddress)), + style: { + marginLeft: '10px', + marginTop: '50px', + }, + }), + h('div', { + style: { + position: 'absolute', + left: '44px', + width: '285px', + }, + }, [ + h(QrView, {key: 'qr'}), + ]), + ]) + + default: + log.debug('rendering default, account detail screen') + return h(AccountDetailScreen, {key: 'account-detail'}) + } +} + +App.prototype.toggleMetamaskActive = function () { + if (!this.props.isUnlocked) { + // currently inactive: redirect to password box + var passwordBox = document.querySelector('input[type=password]') + if (!passwordBox) return + passwordBox.focus() + } else { + // currently active: deactivate + this.props.dispatch(actions.lockMetamask(false)) + } +} + +App.prototype.renderCustomOption = function (provider) { + const { rpcTarget, type } = provider + if (type !== 'rpc') return null + + // Concatenate long URLs + let label = rpcTarget + if (rpcTarget.length > 31) { + label = label.substr(0, 34) + '...' + } + + switch (rpcTarget) { + + case 'http://localhost:8545': + return null + + default: + return h( + DropdownMenuItem, + { + key: rpcTarget, + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + }, + [ + h('i.fa.fa-question-circle.fa-lg.menu-icon'), + label, + h('.check', '✓'), + ] + ) + } +} + +App.prototype.getNetworkName = function () { + const { provider } = this.props + const providerName = provider.type + + let name + + if (providerName === 'mainnet') { + name = 'Main Ethereum Network' + } else if (providerName === 'ropsten') { + name = 'Ropsten Test Network' + } else if (providerName === 'kovan') { + name = 'Kovan Test Network' + } else if (providerName === 'rinkeby') { + name = 'Rinkeby Test Network' + } else { + name = 'Unknown Private Network' + } + + return name +} + +App.prototype.renderCommonRpc = function (rpcList, provider) { + const { rpcTarget } = provider + const props = this.props + + return rpcList.map((rpc) => { + if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { + return null + } else { + return h( + DropdownMenuItem, + { + key: rpc, + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setRpcTarget(rpc)), + }, + [ + h('i.fa.fa-question-circle.fa-lg.menu-icon'), + rpc, + h('.check', '✓'), + ] + ) + } + }) +} diff --git a/responsive-ui/app/components/account-dropdowns.js b/responsive-ui/app/components/account-dropdowns.js new file mode 100644 index 000000000..d1d319477 --- /dev/null +++ b/responsive-ui/app/components/account-dropdowns.js @@ -0,0 +1,227 @@ +const Component = require('react').Component +const PropTypes = require('react').PropTypes +const h = require('react-hyperscript') +const actions = require('../actions') +const genAccountLink = require('../../lib/account-link.js') +const connect = require('react-redux').connect +const Dropdown = require('./dropdown').Dropdown +const DropdownMenuItem = require('./dropdown').DropdownMenuItem +const Identicon = require('./identicon') +const ethUtil = require('ethereumjs-util') +const copyToClipboard = require('copy-to-clipboard') + +class AccountDropdowns extends Component { + constructor (props) { + super(props) + this.state = { + accountSelectorActive: false, + optionsMenuActive: false, + } + } + + renderAccounts () { + const { identities, selected } = this.props + + return Object.keys(identities).map((key) => { + const identity = identities[key] + const isSelected = identity.address === selected + + return h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + this.props.actions.showAccountDetail(identity.address) + }, + }, + [ + h( + Identicon, + { + address: identity.address, + diameter: 16, + }, + ), + h('span', { style: { marginLeft: '10px' } }, identity.name || ''), + h('span', { style: { marginLeft: '10px' } }, isSelected ? h('.check', '✓') : null), + ] + ) + }) + } + + renderAccountSelector () { + const { actions } = this.props + const { accountSelectorActive } = this.state + + return h( + Dropdown, + { + style: { + marginLeft: '-125px', + minWidth: '180px', + }, + isOpen: accountSelectorActive, + onClickOutside: () => { this.setState({ accountSelectorActive: false }) }, + }, + [ + ...this.renderAccounts(), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => actions.addNewAccount(), + }, + [ + h( + Identicon, + { + diameter: 16, + }, + ), + h('span', { style: { marginLeft: '10px' } }, 'Create Account'), + ], + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => actions.showImportPage(), + }, + [ + h( + Identicon, + { + diameter: 16, + }, + ), + h('span', { style: { marginLeft: '10px' } }, 'Import Account'), + ] + ), + ] + ) + } + + renderAccountOptions () { + const { actions } = this.props + const { optionsMenuActive } = this.state + + return h( + Dropdown, + { + style: { + marginLeft: '-162px', + minWidth: '180px', + }, + isOpen: optionsMenuActive, + onClickOutside: () => { this.setState({ optionsMenuActive: false }) }, + }, + [ + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => actions.showConfigPage(), + }, + 'Account Settings', + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + const { selected, network } = this.props + const url = genAccountLink(selected, network) + global.platform.openWindow({ url }) + }, + }, + 'View account on Etherscan', + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + const { selected } = this.props + const checkSumAddress = selected && ethUtil.toChecksumAddress(selected) + copyToClipboard(checkSumAddress) + }, + }, + 'Copy Address to clipboard', + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + actions.requestAccountExport() + }, + }, + 'Export Private Key', + ), + ] + ) + } + + render () { + const { style } = this.props + const { optionsMenuActive, accountSelectorActive } = this.state + + return h( + 'span', + { + style: style, + }, + [ + h( + 'i.fa.fa-angle-down', + { + style: {}, + onClick: (event) => { + event.stopPropagation() + this.setState({ + accountSelectorActive: !accountSelectorActive, + optionsMenuActive: false, + }) + }, + }, + this.renderAccountSelector(), + ), + h( + 'i.fa.fa-ellipsis-h', + { + style: { 'marginLeft': '10px'}, + onClick: (event) => { + event.stopPropagation() + this.setState({ + accountSelectorActive: false, + optionsMenuActive: !optionsMenuActive, + }) + }, + }, + this.renderAccountOptions() + ), + ] + ) + } +} + +AccountDropdowns.propTypes = { + identities: PropTypes.objectOf(PropTypes.object), + selected: PropTypes.string, +} + +const mapDispatchToProps = (dispatch) => { + return { + actions: { + showConfigPage: () => dispatch(actions.showConfigPage()), + requestAccountExport: () => dispatch(actions.requestExportAccount()), + showAccountDetail: (address) => dispatch(actions.showAccountDetail(address)), + addNewAccount: () => dispatch(actions.addNewAccount()), + showImportPage: () => dispatch(actions.showImportPage()), + }, + } +} + +module.exports = { + AccountDropdowns: connect(null, mapDispatchToProps)(AccountDropdowns), +} diff --git a/responsive-ui/app/components/account-export.js b/responsive-ui/app/components/account-export.js new file mode 100644 index 000000000..394d878f7 --- /dev/null +++ b/responsive-ui/app/components/account-export.js @@ -0,0 +1,122 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const copyToClipboard = require('copy-to-clipboard') +const actions = require('../actions') +const ethUtil = require('ethereumjs-util') +const connect = require('react-redux').connect + +module.exports = connect(mapStateToProps)(ExportAccountView) + +inherits(ExportAccountView, Component) +function ExportAccountView () { + Component.call(this) +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +ExportAccountView.prototype.render = function () { + var state = this.props + var accountDetail = state.accountDetail + + if (!accountDetail) return h('div') + var accountExport = accountDetail.accountExport + + var notExporting = accountExport === 'none' + var exportRequested = accountExport === 'requested' + var accountExported = accountExport === 'completed' + + if (notExporting) return h('div') + + if (exportRequested) { + var warning = `Export private keys at your own risk.` + return ( + h('div', { + style: { + display: 'inline-block', + textAlign: 'center', + }, + }, + [ + h('div', { + key: 'exporting', + style: { + margin: '0 20px', + }, + }, [ + h('p.error', warning), + h('input#exportAccount.sizing-input', { + type: 'password', + placeholder: 'confirm password', + onKeyPress: this.onExportKeyPress.bind(this), + style: { + position: 'relative', + top: '1.5px', + marginBottom: '7px', + }, + }), + ]), + h('div', { + key: 'buttons', + style: { + margin: '0 20px', + }, + }, + [ + h('button', { + onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }), + style: { + marginRight: '10px', + }, + }, 'Submit'), + h('button', { + onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), + }, 'Cancel'), + ]), + (this.props.warning) && ( + h('span.error', { + style: { + margin: '20px', + }, + }, this.props.warning.split('-')) + ), + ]) + ) + } + + if (accountExported) { + return h('div.privateKey', { + style: { + margin: '0 20px', + }, + }, [ + h('label', 'Your private key (click to copy):'), + h('p.error.cursor-pointer', { + style: { + textOverflow: 'ellipsis', + overflow: 'hidden', + webkitUserSelect: 'text', + width: '100%', + }, + onClick: function (event) { + copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey)) + }, + }, ethUtil.stripHexPrefix(accountDetail.privateKey)), + h('button', { + onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), + }, 'Done'), + ]) + } +} + +ExportAccountView.prototype.onExportKeyPress = function (event) { + if (event.key !== 'Enter') return + event.preventDefault() + + var input = document.getElementById('exportAccount').value + this.props.dispatch(actions.exportAccount(input, this.props.address)) +} diff --git a/responsive-ui/app/components/account-panel.js b/responsive-ui/app/components/account-panel.js new file mode 100644 index 000000000..abaaf8163 --- /dev/null +++ b/responsive-ui/app/components/account-panel.js @@ -0,0 +1,86 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const Identicon = require('./identicon') +const formatBalance = require('../util').formatBalance +const addressSummary = require('../util').addressSummary + +module.exports = AccountPanel + + +inherits(AccountPanel, Component) +function AccountPanel () { + Component.call(this) +} + +AccountPanel.prototype.render = function () { + var state = this.props + var identity = state.identity || {} + var account = state.account || {} + var isFauceting = state.isFauceting + + var panelState = { + key: `accountPanel${identity.address}`, + identiconKey: identity.address, + identiconLabel: identity.name || '', + attributes: [ + { + key: 'ADDRESS', + value: addressSummary(identity.address), + }, + balanceOrFaucetingIndication(account, isFauceting), + ], + } + + return ( + + h('.identity-panel.flex-row.flex-space-between', { + style: { + flex: '1 0 auto', + cursor: panelState.onClick ? 'pointer' : undefined, + }, + onClick: panelState.onClick, + }, [ + + // account identicon + h('.identicon-wrapper.flex-column.select-none', [ + h(Identicon, { + address: panelState.identiconKey, + imageify: state.imageifyIdenticons, + }), + h('span.font-small', panelState.identiconLabel.substring(0, 7) + '...'), + ]), + + // account address, balance + h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [ + + panelState.attributes.map((attr) => { + return h('.flex-row.flex-space-between', { + key: '' + Math.round(Math.random() * 1000000), + }, [ + h('label.font-small.no-select', attr.key), + h('span.font-small', attr.value), + ]) + }), + ]), + + ]) + + ) +} + +function balanceOrFaucetingIndication (account, isFauceting) { + // Temporarily deactivating isFauceting indication + // because it shows fauceting for empty restored accounts. + if (/* isFauceting*/ false) { + return { + key: 'Account is auto-funding.', + value: 'Please wait.', + } + } else { + return { + key: 'BALANCE', + value: formatBalance(account.balance), + } + } +} diff --git a/responsive-ui/app/components/balance.js b/responsive-ui/app/components/balance.js new file mode 100644 index 000000000..57ca84564 --- /dev/null +++ b/responsive-ui/app/components/balance.js @@ -0,0 +1,89 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const formatBalance = require('../util').formatBalance +const generateBalanceObject = require('../util').generateBalanceObject +const Tooltip = require('./tooltip.js') +const FiatValue = require('./fiat-value.js') + +module.exports = EthBalanceComponent + +inherits(EthBalanceComponent, Component) +function EthBalanceComponent () { + Component.call(this) +} + +EthBalanceComponent.prototype.render = function () { + var props = this.props + let { value } = props + var style = props.style + var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true + value = value ? formatBalance(value, 6, needsParse) : '...' + var width = props.width + + return ( + + h('.ether-balance.ether-balance-amount', { + style: style, + }, [ + h('div', { + style: { + display: 'inline', + width: width, + }, + }, this.renderBalance(value)), + ]) + + ) +} +EthBalanceComponent.prototype.renderBalance = function (value) { + var props = this.props + if (value === 'None') return value + if (value === '...') return value + var balanceObj = generateBalanceObject(value, props.shorten ? 1 : 3) + var balance + var splitBalance = value.split(' ') + var ethNumber = splitBalance[0] + var ethSuffix = splitBalance[1] + const showFiat = 'showFiat' in props ? props.showFiat : true + + if (props.shorten) { + balance = balanceObj.shortBalance + } else { + balance = balanceObj.balance + } + + var label = balanceObj.label + + return ( + h(Tooltip, { + position: 'bottom', + title: `${ethNumber} ${ethSuffix}`, + }, h('div.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('div', { + style: { + width: '100%', + textAlign: 'right', + }, + }, this.props.incoming ? `+${balance}` : balance), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + }, + }, label), + ]), + + showFiat ? h(FiatValue, { value: props.value }) : null, + ])) + ) +} diff --git a/responsive-ui/app/components/binary-renderer.js b/responsive-ui/app/components/binary-renderer.js new file mode 100644 index 000000000..0b6a1f5c2 --- /dev/null +++ b/responsive-ui/app/components/binary-renderer.js @@ -0,0 +1,46 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const extend = require('xtend') + +module.exports = BinaryRenderer + +inherits(BinaryRenderer, Component) +function BinaryRenderer () { + Component.call(this) +} + +BinaryRenderer.prototype.render = function () { + const props = this.props + const { value, style } = props + const text = this.hexToText(value) + + const defaultStyle = extend({ + width: '315px', + maxHeight: '210px', + resize: 'none', + border: 'none', + background: 'white', + padding: '3px', + }, style) + + return ( + h('textarea.font-small', { + readOnly: true, + style: defaultStyle, + defaultValue: text, + }) + ) +} + +BinaryRenderer.prototype.hexToText = function (hex) { + try { + const stripped = ethUtil.stripHexPrefix(hex) + const buff = Buffer.from(stripped, 'hex') + return buff.toString('utf8') + } catch (e) { + return hex + } +} + diff --git a/responsive-ui/app/components/bn-as-decimal-input.js b/responsive-ui/app/components/bn-as-decimal-input.js new file mode 100644 index 000000000..f3ace4720 --- /dev/null +++ b/responsive-ui/app/components/bn-as-decimal-input.js @@ -0,0 +1,174 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const extend = require('xtend') + +module.exports = BnAsDecimalInput + +inherits(BnAsDecimalInput, Component) +function BnAsDecimalInput () { + this.state = { invalid: null } + Component.call(this) +} + +/* Bn as Decimal Input + * + * A component for allowing easy, decimal editing + * of a passed in bn string value. + * + * On change, calls back its `onChange` function parameter + * and passes it an updated bn string. + */ + +BnAsDecimalInput.prototype.render = function () { + const props = this.props + const state = this.state + + const { value, scale, precision, onChange, min, max } = props + + const suffix = props.suffix + const style = props.style + const valueString = value.toString(10) + const newValue = this.downsize(valueString, scale, precision) + + return ( + h('.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('input.hex-input', { + type: 'number', + step: 'any', + required: true, + min, + max, + style: extend({ + display: 'block', + textAlign: 'right', + backgroundColor: 'transparent', + border: '1px solid #bdbdbd', + + }, style), + value: newValue, + onBlur: (event) => { + this.updateValidity(event) + }, + onChange: (event) => { + this.updateValidity(event) + const value = (event.target.value === '') ? '' : event.target.value + + + const scaledNumber = this.upsize(value, scale, precision) + const precisionBN = new BN(scaledNumber, 10) + onChange(precisionBN, event.target.checkValidity()) + }, + onInvalid: (event) => { + const msg = this.constructWarning() + if (msg === state.invalid) { + return + } + this.setState({ invalid: msg }) + event.preventDefault() + return false + }, + }), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + marginRight: '6px', + width: '20px', + }, + }, suffix), + ]), + + state.invalid ? h('span.error', { + style: { + position: 'absolute', + right: '0px', + textAlign: 'right', + transform: 'translateY(26px)', + padding: '3px', + background: 'rgba(255,255,255,0.85)', + zIndex: '1', + textTransform: 'capitalize', + border: '2px solid #E20202', + }, + }, state.invalid) : null, + ]) + ) +} + +BnAsDecimalInput.prototype.setValid = function (message) { + this.setState({ invalid: null }) +} + +BnAsDecimalInput.prototype.updateValidity = function (event) { + const target = event.target + const value = this.props.value + const newValue = target.value + + if (value === newValue) { + return + } + + const valid = target.checkValidity() + + if (valid) { + this.setState({ invalid: null }) + } +} + +BnAsDecimalInput.prototype.constructWarning = function () { + const { name, min, max } = this.props + let message = name ? name + ' ' : '' + + if (min && max) { + message += `must be greater than or equal to ${min} and less than or equal to ${max}.` + } else if (min) { + message += `must be greater than or equal to ${min}.` + } else if (max) { + message += `must be less than or equal to ${max}.` + } else { + message += 'Invalid input.' + } + + return message +} + + +BnAsDecimalInput.prototype.downsize = function (number, scale, precision) { + // if there is no scaling, simply return the number + if (scale === 0) { + return Number(number) + } else { + // if the scale is the same as the precision, account for this edge case. + var decimals = (scale === precision) ? -1 : scale - precision + return Number(number.slice(0, -scale) + '.' + number.slice(-scale, decimals)) + } +} + +BnAsDecimalInput.prototype.upsize = function (number, scale, precision) { + var stringArray = number.toString().split('.') + var decimalLength = stringArray[1] ? stringArray[1].length : 0 + var newString = stringArray[0] + + // If there is scaling and decimal parts exist, integrate them in. + if ((scale !== 0) && (decimalLength !== 0)) { + newString += stringArray[1].slice(0, precision) + } + + // Add 0s to account for the upscaling. + for (var i = decimalLength; i < scale; i++) { + newString += '0' + } + return newString +} diff --git a/responsive-ui/app/components/buy-button-subview.js b/responsive-ui/app/components/buy-button-subview.js new file mode 100644 index 000000000..87084f92d --- /dev/null +++ b/responsive-ui/app/components/buy-button-subview.js @@ -0,0 +1,197 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const actions = require('../actions') +const CoinbaseForm = require('./coinbase-form') +const ShapeshiftForm = require('./shapeshift-form') +const Loading = require('./loading') +const AccountPanel = require('./account-panel') +const RadioList = require('./custom-radio-list') + +module.exports = connect(mapStateToProps)(BuyButtonSubview) + +function mapStateToProps (state) { + return { + identity: state.appState.identity, + account: state.metamask.accounts[state.appState.buyView.buyAddress], + warning: state.appState.warning, + buyView: state.appState.buyView, + network: state.metamask.network, + provider: state.metamask.provider, + context: state.appState.currentView.context, + isSubLoading: state.appState.isSubLoading, + } +} + +inherits(BuyButtonSubview, Component) +function BuyButtonSubview () { + Component.call(this) +} + +BuyButtonSubview.prototype.render = function () { + const props = this.props + const isLoading = props.isSubLoading + + return ( + h('.buy-eth-section.flex-column', { + style: { + alignItems: 'center', + }, + }, [ + // back button + h('.flex-row', { + style: { + alignItems: 'center', + justifyContent: 'center', + }, + }, [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { + onClick: this.backButtonContext.bind(this), + style: { + position: 'absolute', + left: '10px', + }, + }), + h('h2.text-transform-uppercase.flex-center', { + style: { + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', + }, + }, 'Buy Eth'), + ]), + h('div', { + style: { + position: 'absolute', + top: '57vh', + left: '49vw', + }, + }, [ + h(Loading, {isLoading}), + ]), + h('div', { + style: { + width: '80%', + }, + }, [ + h(AccountPanel, { + showFullAddress: true, + identity: props.identity, + account: props.account, + }), + ]), + h('h3.text-transform-uppercase', { + style: { + paddingLeft: '15px', + fontFamily: 'Montserrat Light', + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', + }, + }, 'Select Service'), + h('.flex-row.selected-exchange', { + style: { + position: 'relative', + right: '35px', + marginTop: '20px', + marginBottom: '20px', + }, + }, [ + h(RadioList, { + defaultFocus: props.buyView.subview, + labels: [ + 'Coinbase', + 'ShapeShift', + ], + subtext: { + 'Coinbase': 'Crypto/FIAT (USA only)', + 'ShapeShift': 'Crypto', + }, + onClick: this.radioHandler.bind(this), + }), + ]), + h('h3.text-transform-uppercase', { + style: { + paddingLeft: '15px', + fontFamily: 'Montserrat Light', + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', + }, + }, props.buyView.subview), + this.formVersionSubview(), + ]) + ) +} + +BuyButtonSubview.prototype.formVersionSubview = function () { + const network = this.props.network + if (network === '1') { + if (this.props.buyView.formView.coinbase) { + return h(CoinbaseForm, this.props) + } else if (this.props.buyView.formView.shapeshift) { + return h(ShapeshiftForm, this.props) + } + } else { + return h('div.flex-column', { + style: { + alignItems: 'center', + margin: '50px', + }, + }, [ + h('h3.text-transform-uppercase', { + style: { + width: '225px', + marginBottom: '15px', + }, + }, 'In order to access this feature, please switch to the Main Network'), + ((network === '3') || (network === '4') || (network === '42')) ? h('h3.text-transform-uppercase', 'or go to the') : null, + (network === '3') ? h('button.text-transform-uppercase', { + onClick: () => this.props.dispatch(actions.buyEth({ network })), + style: { + marginTop: '15px', + }, + }, 'Ropsten Test Faucet') : null, + (network === '4') ? h('button.text-transform-uppercase', { + onClick: () => this.props.dispatch(actions.buyEth({ network })), + style: { + marginTop: '15px', + }, + }, 'Rinkeby Test Faucet') : null, + (network === '42') ? h('button.text-transform-uppercase', { + onClick: () => this.props.dispatch(actions.buyEth({ network })), + style: { + marginTop: '15px', + }, + }, 'Kovan Test Faucet') : null, + ]) + } +} + +BuyButtonSubview.prototype.navigateTo = function (url) { + global.platform.openWindow({ url }) +} + +BuyButtonSubview.prototype.backButtonContext = function () { + if (this.props.context === 'confTx') { + this.props.dispatch(actions.showConfTxPage(false)) + } else { + this.props.dispatch(actions.goHome()) + } +} + +BuyButtonSubview.prototype.radioHandler = function (event) { + switch (event.target.title) { + case 'Coinbase': + return this.props.dispatch(actions.coinBaseSubview()) + case 'ShapeShift': + return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type)) + } +} diff --git a/responsive-ui/app/components/coinbase-form.js b/responsive-ui/app/components/coinbase-form.js new file mode 100644 index 000000000..f44d86045 --- /dev/null +++ b/responsive-ui/app/components/coinbase-form.js @@ -0,0 +1,63 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const actions = require('../actions') + +module.exports = connect(mapStateToProps)(CoinbaseForm) + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +inherits(CoinbaseForm, Component) + +function CoinbaseForm () { + Component.call(this) +} + +CoinbaseForm.prototype.render = function () { + var props = this.props + + return h('.flex-column', { + style: { + marginTop: '35px', + padding: '25px', + width: '100%', + }, + }, [ + h('.flex-row', { + style: { + justifyContent: 'space-around', + margin: '33px', + marginTop: '0px', + }, + }, [ + h('button.btn-green', { + onClick: this.toCoinbase.bind(this), + }, 'Continue to Coinbase'), + + h('button.btn-red', { + onClick: () => props.dispatch(actions.backTobuyView(props.accounts.address)), + }, 'Cancel'), + ]), + ]) +} + +CoinbaseForm.prototype.toCoinbase = function () { + const props = this.props + const address = props.buyView.buyAddress + props.dispatch(actions.buyEth({ network: '1', address, amount: 0 })) +} + +CoinbaseForm.prototype.renderLoading = function () { + return h('img', { + style: { + width: '27px', + marginRight: '-27px', + }, + src: 'images/loading.svg', + }) +} diff --git a/responsive-ui/app/components/copyButton.js b/responsive-ui/app/components/copyButton.js new file mode 100644 index 000000000..a25d0719c --- /dev/null +++ b/responsive-ui/app/components/copyButton.js @@ -0,0 +1,59 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const copyToClipboard = require('copy-to-clipboard') + +const Tooltip = require('./tooltip') + +module.exports = CopyButton + +inherits(CopyButton, Component) +function CopyButton () { + Component.call(this) +} + +// As parameters, accepts: +// "value", which is the value to copy (mandatory) +// "title", which is the text to show on hover (optional, defaults to 'Copy') +CopyButton.prototype.render = function () { + const props = this.props + const state = this.state || {} + + const value = props.value + const copied = state.copied + + const message = copied ? 'Copied' : props.title || ' Copy ' + + return h('.copy-button', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + + h(Tooltip, { + title: message, + }, [ + h('i.fa.fa-clipboard.cursor-pointer.color-orange', { + style: { + margin: '5px', + }, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(value) + this.debounceRestore() + }, + }), + ]), + + ]) +} + +CopyButton.prototype.debounceRestore = function () { + this.setState({ copied: true }) + clearTimeout(this.timeout) + this.timeout = setTimeout(() => { + this.setState({ copied: false }) + }, 850) +} diff --git a/responsive-ui/app/components/copyable.js b/responsive-ui/app/components/copyable.js new file mode 100644 index 000000000..a4f6f4bc6 --- /dev/null +++ b/responsive-ui/app/components/copyable.js @@ -0,0 +1,46 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const Tooltip = require('./tooltip') +const copyToClipboard = require('copy-to-clipboard') + +module.exports = Copyable + +inherits(Copyable, Component) +function Copyable () { + Component.call(this) + this.state = { + copied: false, + } +} + +Copyable.prototype.render = function () { + const props = this.props + const state = this.state + const { value, children } = props + const { copied } = state + + return h(Tooltip, { + title: copied ? 'Copied!' : 'Copy', + position: 'bottom', + }, h('span', { + style: { + cursor: 'pointer', + }, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(value) + this.debounceRestore() + }, + }, children)) +} + +Copyable.prototype.debounceRestore = function () { + this.setState({ copied: true }) + clearTimeout(this.timeout) + this.timeout = setTimeout(() => { + this.setState({ copied: false }) + }, 850) +} diff --git a/responsive-ui/app/components/custom-radio-list.js b/responsive-ui/app/components/custom-radio-list.js new file mode 100644 index 000000000..a4c525396 --- /dev/null +++ b/responsive-ui/app/components/custom-radio-list.js @@ -0,0 +1,60 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = RadioList + +inherits(RadioList, Component) +function RadioList () { + Component.call(this) +} + +RadioList.prototype.render = function () { + const props = this.props + const activeClass = '.custom-radio-selected' + const inactiveClass = '.custom-radio-inactive' + const { + labels, + defaultFocus, + } = props + + + return ( + h('.flex-row', { + style: { + fontSize: '12px', + }, + }, [ + h('.flex-column.custom-radios', { + style: { + marginRight: '5px', + }, + }, + labels.map((lable, i) => { + let isSelcted = (this.state !== null) + isSelcted = isSelcted ? (this.state.selected === lable) : (defaultFocus === lable) + return h(isSelcted ? activeClass : inactiveClass, { + title: lable, + onClick: (event) => { + this.setState({selected: event.target.title}) + props.onClick(event) + }, + }) + }) + ), + h('.text', {}, + labels.map((lable) => { + if (props.subtext) { + return h('.flex-row', {}, [ + h('.radio-titles', lable), + h('.radio-titles-subtext', `- ${props.subtext[lable]}`), + ]) + } else { + return h('.radio-titles', lable) + } + }) + ), + ]) + ) +} + diff --git a/responsive-ui/app/components/dropdown.js b/responsive-ui/app/components/dropdown.js new file mode 100644 index 000000000..e77b4c40c --- /dev/null +++ b/responsive-ui/app/components/dropdown.js @@ -0,0 +1,89 @@ +const Component = require('react').Component +const PropTypes = require('react').PropTypes +const h = require('react-hyperscript') +const MenuDroppo = require('menu-droppo') + +const noop = () => {} + +class Dropdown extends Component { + render () { + const { isOpen, onClickOutside, style, children } = this.props + + return h( + MenuDroppo, + { + isOpen, + zIndex: 11, + onClickOutside, + style, + innerStyle: { + borderRadius: '4px', + padding: '8px 16px', + background: 'rgba(0, 0, 0, 0.8)', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + }, + }, + [ + h( + 'style', + ` + li.dropdown-menu-item:hover { color:rgb(225, 225, 225); } + li.dropdown-menu-item { color: rgb(185, 185, 185); } + ` + ), + ...children, + ] + ) + } +} + +Dropdown.defaultProps = { + isOpen: false, + onClick: noop, +} + +Dropdown.propTypes = { + isOpen: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, + children: PropTypes.node, + style: PropTypes.object.isRequired, +} + +class DropdownMenuItem extends Component { + render () { + const { onClick, closeMenu, children } = this.props + + return h( + 'li.dropdown-menu-item', + { + onClick: () => { + onClick() + closeMenu() + }, + style: { + listStyle: 'none', + padding: '8px 0px 8px 0px', + fontSize: '12px', + fontStyle: 'normal', + fontFamily: 'Montserrat Regular', + cursor: 'pointer', + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'center', + }, + }, + children + ) + } +} + +DropdownMenuItem.propTypes = { + closeMenu: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, + children: PropTypes.node, +} + +module.exports = { + Dropdown, + DropdownMenuItem, +} diff --git a/responsive-ui/app/components/editable-label.js b/responsive-ui/app/components/editable-label.js new file mode 100644 index 000000000..167be7eaf --- /dev/null +++ b/responsive-ui/app/components/editable-label.js @@ -0,0 +1,56 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const findDOMNode = require('react-dom').findDOMNode + +module.exports = EditableLabel + +inherits(EditableLabel, Component) +function EditableLabel () { + Component.call(this) +} + +EditableLabel.prototype.render = function () { + const props = this.props + const state = this.state + + if (state && state.isEditingLabel) { + return h('div.editable-label', [ + h('input.sizing-input', { + defaultValue: props.textValue, + maxLength: '20', + onKeyPress: (event) => { + this.saveIfEnter(event) + }, + }), + h('button.editable-button', { + onClick: () => this.saveText(), + }, 'Save'), + ]) + } else { + return h('div.name-label', { + onClick: (event) => { + const nameAttribute = event.target.getAttribute('name') + // checks for class to handle smaller CTA above the account name + const classAttribute = event.target.getAttribute('class') + if (nameAttribute === 'edit' || classAttribute === 'edit-text') { + this.setState({ isEditingLabel: true }) + } + }, + }, this.props.children) + } +} + +EditableLabel.prototype.saveIfEnter = function (event) { + if (event.key === 'Enter') { + this.saveText() + } +} + +EditableLabel.prototype.saveText = function () { + var container = findDOMNode(this) + var text = container.querySelector('.editable-label input').value + var truncatedText = text.substring(0, 20) + this.props.saveText(truncatedText) + this.setState({ isEditingLabel: false, textLabel: truncatedText }) +} diff --git a/responsive-ui/app/components/ens-input.js b/responsive-ui/app/components/ens-input.js new file mode 100644 index 000000000..3a33ebf74 --- /dev/null +++ b/responsive-ui/app/components/ens-input.js @@ -0,0 +1,170 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const extend = require('xtend') +const debounce = require('debounce') +const copyToClipboard = require('copy-to-clipboard') +const ENS = require('ethjs-ens') +const networkMap = require('ethjs-ens/lib/network-map.json') +const ensRE = /.+\.eth$/ +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + + +module.exports = EnsInput + +inherits(EnsInput, Component) +function EnsInput () { + Component.call(this) +} + +EnsInput.prototype.render = function () { + const props = this.props + const opts = extend(props, { + list: 'addresses', + onChange: () => { + const network = this.props.network + const networkHasEnsSupport = getNetworkEnsSupport(network) + if (!networkHasEnsSupport) return + + const recipient = document.querySelector('input[name="address"]').value + if (recipient.match(ensRE) === null) { + return this.setState({ + loadingEns: false, + ensResolution: null, + ensFailure: null, + }) + } + + this.setState({ + loadingEns: true, + }) + this.checkName() + }, + }) + return h('div', { + style: { width: '100%' }, + }, [ + h('input.large-input', opts), + // The address book functionality. + h('datalist#addresses', + [ + // Corresponds to the addresses owned. + Object.keys(props.identities).map((key) => { + const identity = props.identities[key] + return h('option', { + value: identity.address, + label: identity.name, + key: identity.address, + }) + }), + // Corresponds to previously sent-to addresses. + props.addressBook.map((identity) => { + return h('option', { + value: identity.address, + label: identity.name, + key: identity.address, + }) + }), + ]), + this.ensIcon(), + ]) +} + +EnsInput.prototype.componentDidMount = function () { + const network = this.props.network + const networkHasEnsSupport = getNetworkEnsSupport(network) + this.setState({ ensResolution: ZERO_ADDRESS }) + + if (networkHasEnsSupport) { + const provider = global.ethereumProvider + this.ens = new ENS({ provider, network }) + this.checkName = debounce(this.lookupEnsName.bind(this), 200) + } +} + +EnsInput.prototype.lookupEnsName = function () { + const recipient = document.querySelector('input[name="address"]').value + const { ensResolution } = this.state + + log.info(`ENS attempting to resolve name: ${recipient}`) + this.ens.lookup(recipient.trim()) + .then((address) => { + if (address === ZERO_ADDRESS) throw new Error('No address has been set for this name.') + if (address !== ensResolution) { + this.setState({ + loadingEns: false, + ensResolution: address, + nickname: recipient.trim(), + hoverText: address + '\nClick to Copy', + ensFailure: false, + }) + } + }) + .catch((reason) => { + log.error(reason) + return this.setState({ + loadingEns: false, + ensResolution: ZERO_ADDRESS, + ensFailure: true, + hoverText: reason.message, + }) + }) +} + +EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) { + const state = this.state || {} + const ensResolution = state.ensResolution + // If an address is sent without a nickname, meaning not from ENS or from + // the user's own accounts, a default of a one-space string is used. + const nickname = state.nickname || ' ' + if (prevState && ensResolution && this.props.onChange && + ensResolution !== prevState.ensResolution) { + this.props.onChange(ensResolution, nickname) + } +} + +EnsInput.prototype.ensIcon = function (recipient) { + const { hoverText } = this.state || {} + return h('span', { + title: hoverText, + style: { + position: 'absolute', + padding: '9px', + transform: 'translatex(-40px)', + }, + }, this.ensIconContents(recipient)) +} + +EnsInput.prototype.ensIconContents = function (recipient) { + const { loadingEns, ensFailure, ensResolution } = this.state || { ensResolution: ZERO_ADDRESS} + + if (loadingEns) { + return h('img', { + src: 'images/loading.svg', + style: { + width: '30px', + height: '30px', + transform: 'translateY(-6px)', + }, + }) + } + + if (ensFailure) { + return h('i.fa.fa-warning.fa-lg.warning') + } + + if (ensResolution && (ensResolution !== ZERO_ADDRESS)) { + return h('i.fa.fa-check-circle.fa-lg.cursor-pointer', { + style: { color: 'green' }, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(ensResolution) + }, + }) + } +} + +function getNetworkEnsSupport (network) { + return Boolean(networkMap[network]) +} diff --git a/responsive-ui/app/components/eth-balance.js b/responsive-ui/app/components/eth-balance.js new file mode 100644 index 000000000..4f538fd31 --- /dev/null +++ b/responsive-ui/app/components/eth-balance.js @@ -0,0 +1,89 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const formatBalance = require('../util').formatBalance +const generateBalanceObject = require('../util').generateBalanceObject +const Tooltip = require('./tooltip.js') +const FiatValue = require('./fiat-value.js') + +module.exports = EthBalanceComponent + +inherits(EthBalanceComponent, Component) +function EthBalanceComponent () { + Component.call(this) +} + +EthBalanceComponent.prototype.render = function () { + var props = this.props + let { value } = props + const { style, width } = props + var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true + value = value ? formatBalance(value, 6, needsParse) : '...' + + return ( + + h('.ether-balance.ether-balance-amount', { + style, + }, [ + h('div', { + style: { + display: 'inline', + width, + }, + }, this.renderBalance(value)), + ]) + + ) +} +EthBalanceComponent.prototype.renderBalance = function (value) { + var props = this.props + const { conversionRate, shorten, incoming, currentCurrency } = props + if (value === 'None') return value + if (value === '...') return value + var balanceObj = generateBalanceObject(value, shorten ? 1 : 3) + var balance + var splitBalance = value.split(' ') + var ethNumber = splitBalance[0] + var ethSuffix = splitBalance[1] + const showFiat = 'showFiat' in props ? props.showFiat : true + + if (shorten) { + balance = balanceObj.shortBalance + } else { + balance = balanceObj.balance + } + + var label = balanceObj.label + + return ( + h(Tooltip, { + position: 'bottom', + title: `${ethNumber} ${ethSuffix}`, + }, h('div.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('div', { + style: { + width: '100%', + textAlign: 'right', + }, + }, incoming ? `+${balance}` : balance), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + }, + }, label), + ]), + + showFiat ? h(FiatValue, { value: props.value, conversionRate, currentCurrency }) : null, + ])) + ) +} diff --git a/responsive-ui/app/components/fiat-value.js b/responsive-ui/app/components/fiat-value.js new file mode 100644 index 000000000..8a64a1cfc --- /dev/null +++ b/responsive-ui/app/components/fiat-value.js @@ -0,0 +1,63 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const formatBalance = require('../util').formatBalance + +module.exports = FiatValue + +inherits(FiatValue, Component) +function FiatValue () { + Component.call(this) +} + +FiatValue.prototype.render = function () { + const props = this.props + const { conversionRate, currentCurrency } = props + + const value = formatBalance(props.value, 6) + + if (value === 'None') return value + var fiatDisplayNumber, fiatTooltipNumber + var splitBalance = value.split(' ') + + if (conversionRate !== 0) { + fiatTooltipNumber = Number(splitBalance[0]) * conversionRate + fiatDisplayNumber = fiatTooltipNumber.toFixed(2) + } else { + fiatDisplayNumber = 'N/A' + fiatTooltipNumber = 'Unknown' + } + + return fiatDisplay(fiatDisplayNumber, currentCurrency) +} + +function fiatDisplay (fiatDisplayNumber, fiatSuffix) { + if (fiatDisplayNumber !== 'N/A') { + return h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('div', { + style: { + width: '100%', + textAlign: 'right', + fontSize: '12px', + color: '#333333', + }, + }, fiatDisplayNumber), + h('div', { + style: { + color: '#AEAEAE', + marginLeft: '5px', + fontSize: '12px', + }, + }, fiatSuffix), + ]) + } else { + return h('div') + } +} diff --git a/responsive-ui/app/components/hex-as-decimal-input.js b/responsive-ui/app/components/hex-as-decimal-input.js new file mode 100644 index 000000000..4a71e9585 --- /dev/null +++ b/responsive-ui/app/components/hex-as-decimal-input.js @@ -0,0 +1,154 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const extend = require('xtend') + +module.exports = HexAsDecimalInput + +inherits(HexAsDecimalInput, Component) +function HexAsDecimalInput () { + this.state = { invalid: null } + Component.call(this) +} + +/* Hex as Decimal Input + * + * A component for allowing easy, decimal editing + * of a passed in hex string value. + * + * On change, calls back its `onChange` function parameter + * and passes it an updated hex string. + */ + +HexAsDecimalInput.prototype.render = function () { + const props = this.props + const state = this.state + + const { value, onChange, min, max } = props + + const toEth = props.toEth + const suffix = props.suffix + const decimalValue = decimalize(value, toEth) + const style = props.style + + return ( + h('.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('input.hex-input', { + type: 'number', + required: true, + min: min, + max: max, + style: extend({ + display: 'block', + textAlign: 'right', + backgroundColor: 'transparent', + border: '1px solid #bdbdbd', + + }, style), + value: parseInt(decimalValue), + onBlur: (event) => { + this.updateValidity(event) + }, + onChange: (event) => { + this.updateValidity(event) + const hexString = (event.target.value === '') ? '' : hexify(event.target.value) + onChange(hexString) + }, + onInvalid: (event) => { + const msg = this.constructWarning() + if (msg === state.invalid) { + return + } + this.setState({ invalid: msg }) + event.preventDefault() + return false + }, + }), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + marginRight: '6px', + width: '20px', + }, + }, suffix), + ]), + + state.invalid ? h('span.error', { + style: { + position: 'absolute', + right: '0px', + textAlign: 'right', + transform: 'translateY(26px)', + padding: '3px', + background: 'rgba(255,255,255,0.85)', + zIndex: '1', + textTransform: 'capitalize', + border: '2px solid #E20202', + }, + }, state.invalid) : null, + ]) + ) +} + +HexAsDecimalInput.prototype.setValid = function (message) { + this.setState({ invalid: null }) +} + +HexAsDecimalInput.prototype.updateValidity = function (event) { + const target = event.target + const value = this.props.value + const newValue = target.value + + if (value === newValue) { + return + } + + const valid = target.checkValidity() + if (valid) { + this.setState({ invalid: null }) + } +} + +HexAsDecimalInput.prototype.constructWarning = function () { + const { name, min, max } = this.props + let message = name ? name + ' ' : '' + + if (min && max) { + message += `must be greater than or equal to ${min} and less than or equal to ${max}.` + } else if (min) { + message += `must be greater than or equal to ${min}.` + } else if (max) { + message += `must be less than or equal to ${max}.` + } else { + message += 'Invalid input.' + } + + return message +} + +function hexify (decimalString) { + const hexBN = new BN(parseInt(decimalString), 10) + return '0x' + hexBN.toString('hex') +} + +function decimalize (input, toEth) { + if (input === '') { + return '' + } else { + const strippedInput = ethUtil.stripHexPrefix(input) + const inputBN = new BN(strippedInput, 'hex') + return inputBN.toString(10) + } +} diff --git a/responsive-ui/app/components/identicon.js b/responsive-ui/app/components/identicon.js new file mode 100644 index 000000000..c754bc6ba --- /dev/null +++ b/responsive-ui/app/components/identicon.js @@ -0,0 +1,72 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const isNode = require('detect-node') +const findDOMNode = require('react-dom').findDOMNode +const jazzicon = require('jazzicon') +const iconFactoryGen = require('../../lib/icon-factory') +const iconFactory = iconFactoryGen(jazzicon) + +module.exports = IdenticonComponent + +inherits(IdenticonComponent, Component) +function IdenticonComponent () { + Component.call(this) + + this.defaultDiameter = 46 +} + +IdenticonComponent.prototype.render = function () { + var props = this.props + var diameter = props.diameter || this.defaultDiameter + return ( + h('div', { + key: 'identicon-' + this.props.address, + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: diameter, + width: diameter, + borderRadius: diameter / 2, + overflow: 'hidden', + }, + }) + ) +} + +IdenticonComponent.prototype.componentDidMount = function () { + var props = this.props + const { address } = props + + if (!address) return + + var container = findDOMNode(this) + + var diameter = props.diameter || this.defaultDiameter + if (!isNode) { + var img = iconFactory.iconForAddress(address, diameter) + container.appendChild(img) + } +} + +IdenticonComponent.prototype.componentDidUpdate = function () { + var props = this.props + const { address } = props + + if (!address) return + + var container = findDOMNode(this) + + var children = container.children + for (var i = 0; i < children.length; i++) { + container.removeChild(children[i]) + } + + var diameter = props.diameter || this.defaultDiameter + if (!isNode) { + var img = iconFactory.iconForAddress(address, diameter) + container.appendChild(img) + } +} + diff --git a/responsive-ui/app/components/loading.js b/responsive-ui/app/components/loading.js new file mode 100644 index 000000000..87d6f5d20 --- /dev/null +++ b/responsive-ui/app/components/loading.js @@ -0,0 +1,53 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') + + +inherits(LoadingIndicator, Component) +module.exports = LoadingIndicator + +function LoadingIndicator () { + Component.call(this) +} + +LoadingIndicator.prototype.render = function () { + const { isLoading, loadingMessage } = this.props + + return ( + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'loader', + transitionEnterTimeout: 150, + transitionLeaveTimeout: 150, + }, [ + + isLoading ? h('div', { + style: { + zIndex: 10, + position: 'absolute', + flexDirection: 'column', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '100%', + width: '100%', + background: 'rgba(255, 255, 255, 0.8)', + }, + }, [ + h('img', { + src: 'images/loading.svg', + }), + + h('br'), + + showMessageIfAny(loadingMessage), + ]) : null, + ]) + ) +} + +function showMessageIfAny (loadingMessage) { + if (!loadingMessage) return null + return h('span', loadingMessage) +} diff --git a/responsive-ui/app/components/mascot.js b/responsive-ui/app/components/mascot.js new file mode 100644 index 000000000..973ec2cad --- /dev/null +++ b/responsive-ui/app/components/mascot.js @@ -0,0 +1,59 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const metamaskLogo = require('metamask-logo') +const debounce = require('debounce') + +module.exports = Mascot + +inherits(Mascot, Component) +function Mascot () { + Component.call(this) + this.logo = metamaskLogo({ + followMouse: true, + pxNotRatio: true, + width: 200, + height: 200, + }) + + this.refollowMouse = debounce(this.logo.setFollowMouse.bind(this.logo, true), 1000) + this.unfollowMouse = this.logo.setFollowMouse.bind(this.logo, false) +} + +Mascot.prototype.render = function () { + // this is a bit hacky + // the event emitter is on `this.props` + // and we dont get that until render + this.handleAnimationEvents() + + return h('#metamask-mascot-container', { + style: { zIndex: 0 }, + }) +} + +Mascot.prototype.componentDidMount = function () { + var targetDivId = 'metamask-mascot-container' + var container = document.getElementById(targetDivId) + container.appendChild(this.logo.container) +} + +Mascot.prototype.componentWillUnmount = function () { + this.animations = this.props.animationEventEmitter + this.animations.removeAllListeners() + this.logo.container.remove() + this.logo.stopAnimation() +} + +Mascot.prototype.handleAnimationEvents = function () { + // only setup listeners once + if (this.animations) return + this.animations = this.props.animationEventEmitter + this.animations.on('point', this.lookAt.bind(this)) + this.animations.on('setFollowMouse', this.logo.setFollowMouse.bind(this.logo)) +} + +Mascot.prototype.lookAt = function (target) { + this.unfollowMouse() + this.logo.lookAt(target) + this.refollowMouse() +} diff --git a/responsive-ui/app/components/mini-account-panel.js b/responsive-ui/app/components/mini-account-panel.js new file mode 100644 index 000000000..c09cf5b7a --- /dev/null +++ b/responsive-ui/app/components/mini-account-panel.js @@ -0,0 +1,74 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const Identicon = require('./identicon') + +module.exports = AccountPanel + + +inherits(AccountPanel, Component) +function AccountPanel () { + Component.call(this) +} + +AccountPanel.prototype.render = function () { + var props = this.props + var picOrder = props.picOrder || 'left' + const { imageSeed } = props + + return ( + + h('.identity-panel.flex-row.flex-left', { + style: { + cursor: props.onClick ? 'pointer' : undefined, + }, + onClick: props.onClick, + }, [ + + this.genIcon(imageSeed, picOrder), + + h('div.flex-column.flex-justify-center', { + style: { + lineHeight: '15px', + order: 2, + display: 'flex', + alignItems: picOrder === 'left' ? 'flex-begin' : 'flex-end', + }, + }, this.props.children), + ]) + ) +} + +AccountPanel.prototype.genIcon = function (seed, picOrder) { + const props = this.props + + // When there is no seed value, this is a contract creation. + // We then show the contract icon. + if (!seed) { + return h('.identicon-wrapper.flex-column.select-none', { + style: { + order: picOrder === 'left' ? 1 : 3, + }, + }, [ + h('i.fa.fa-file-text-o.fa-lg', { + style: { + fontSize: '42px', + transform: 'translate(0px, -16px)', + }, + }), + ]) + } + + // If there was a seed, we return an identicon for that address. + return h('.identicon-wrapper.flex-column.select-none', { + style: { + order: picOrder === 'left' ? 1 : 3, + }, + }, [ + h(Identicon, { + address: seed, + imageify: props.imageifyIdenticons, + }), + ]) +} + diff --git a/responsive-ui/app/components/network.js b/responsive-ui/app/components/network.js new file mode 100644 index 000000000..698a0bbb9 --- /dev/null +++ b/responsive-ui/app/components/network.js @@ -0,0 +1,124 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = Network + +inherits(Network, Component) + +function Network () { + Component.call(this) +} + +Network.prototype.render = function () { + const props = this.props + const networkNumber = props.network + let providerName + try { + providerName = props.provider.type + } catch (e) { + providerName = null + } + let iconName, hoverText + + if (networkNumber === 'loading') { + return h('span', { + style: { + display: 'flex', + alignItems: 'center', + flexDirection: 'row', + }, + onClick: (event) => this.props.onClick(event), + }, [ + h('img', { + title: 'Attempting to connect to blockchain.', + style: { + width: '27px', + }, + src: 'images/loading.svg', + }), + h('i.fa.fa-sort-desc'), + ]) + } else if (providerName === 'mainnet') { + hoverText = 'Main Ethereum Network' + iconName = 'ethereum-network' + } else if (providerName === 'ropsten') { + hoverText = 'Ropsten Test Network' + iconName = 'ropsten-test-network' + } else if (parseInt(networkNumber) === 3) { + hoverText = 'Ropsten Test Network' + iconName = 'ropsten-test-network' + } else if (providerName === 'kovan') { + hoverText = 'Kovan Test Network' + iconName = 'kovan-test-network' + } else if (providerName === 'rinkeby') { + hoverText = 'Rinkeby Test Network' + iconName = 'rinkeby-test-network' + } else { + hoverText = 'Unknown Private Network' + iconName = 'unknown-private-network' + } + + return ( + h('#network_component.pointer', { + title: hoverText, + onClick: (event) => this.props.onClick(event), + }, [ + (function () { + switch (iconName) { + case 'ethereum-network': + return h('.network-indicator', [ + h('.menu-icon.diamond'), + h('.network-name', { + style: { + color: '#039396', + }}, + 'Ethereum Main Net'), + ]) + case 'ropsten-test-network': + return h('.network-indicator', [ + h('.menu-icon.red-dot'), + h('.network-name', { + style: { + color: '#ff6666', + }}, + 'Ropsten Test Net'), + ]) + case 'kovan-test-network': + return h('.network-indicator', [ + h('.menu-icon.hollow-diamond'), + h('.network-name', { + style: { + color: '#690496', + }}, + 'Kovan Test Net'), + ]) + case 'rinkeby-test-network': + return h('.network-indicator', [ + h('.menu-icon.golden-square'), + h('.network-name', { + style: { + color: '#e7a218', + }}, + 'Rinkeby Test Net'), + ]) + default: + return h('.network-indicator', [ + h('i.fa.fa-question-circle.fa-lg', { + style: { + margin: '10px', + color: 'rgb(125, 128, 130)', + }, + }), + + h('.network-name', { + style: { + color: '#AEAEAE', + }}, + 'Private Network'), + ]) + } + })(), + ]) + ) +} diff --git a/responsive-ui/app/components/notice.js b/responsive-ui/app/components/notice.js new file mode 100644 index 000000000..d9f0067cd --- /dev/null +++ b/responsive-ui/app/components/notice.js @@ -0,0 +1,126 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const ReactMarkdown = require('react-markdown') +const linker = require('extension-link-enabler') +const findDOMNode = require('react-dom').findDOMNode + +module.exports = Notice + +inherits(Notice, Component) +function Notice () { + Component.call(this) +} + +Notice.prototype.render = function () { + const { notice, onConfirm } = this.props + const { title, date, body } = notice + const state = this.state || { disclaimerDisabled: true } + const disabled = state.disclaimerDisabled + + return ( + h('.flex-column.flex-center.flex-grow', [ + h('h3.flex-center.text-transform-uppercase.terms-header', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + width: '100%', + fontSize: '20px', + textAlign: 'center', + padding: 6, + }, + }, [ + title, + ]), + + h('h5.flex-center.text-transform-uppercase.terms-header', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginBottom: 24, + width: '100%', + fontSize: '20px', + textAlign: 'center', + padding: 6, + }, + }, [ + date, + ]), + + h('style', ` + + .markdown { + overflow-x: hidden; + } + + .markdown h1, .markdown h2, .markdown h3 { + margin: 10px 0; + font-weight: bold; + } + + .markdown strong { + font-weight: bold; + } + .markdown em { + font-style: italic; + } + + .markdown p { + margin: 10px 0; + } + + .markdown a { + color: #df6b0e; + } + + `), + + h('div.markdown', { + onScroll: (e) => { + var object = e.currentTarget + if (object.offsetHeight + object.scrollTop + 100 >= object.scrollHeight) { + this.setState({disclaimerDisabled: false}) + } + }, + style: { + background: 'rgb(235, 235, 235)', + height: '310px', + padding: '6px', + width: '90%', + overflowY: 'scroll', + scroll: 'auto', + }, + }, [ + h(ReactMarkdown, { + className: 'notice-box', + source: body, + skipHtml: true, + }), + ]), + + h('button', { + disabled, + onClick: () => { + this.setState({disclaimerDisabled: true}) + onConfirm() + }, + style: { + marginTop: '18px', + }, + }, 'Accept'), + ]) + ) +} + +Notice.prototype.componentDidMount = function () { + var node = findDOMNode(this) + linker.setupListener(node) + if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) { + this.setState({disclaimerDisabled: false}) + } +} + +Notice.prototype.componentWillUnmount = function () { + var node = findDOMNode(this) + linker.teardownListener(node) +} diff --git a/responsive-ui/app/components/pending-msg-details.js b/responsive-ui/app/components/pending-msg-details.js new file mode 100644 index 000000000..16308d121 --- /dev/null +++ b/responsive-ui/app/components/pending-msg-details.js @@ -0,0 +1,50 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const AccountPanel = require('./account-panel') + +module.exports = PendingMsgDetails + +inherits(PendingMsgDetails, Component) +function PendingMsgDetails () { + Component.call(this) +} + +PendingMsgDetails.prototype.render = function () { + var state = this.props + var msgData = state.txData + + var msgParams = msgData.msgParams || {} + var address = msgParams.from || state.selectedAddress + var identity = state.identities[address] || { address: address } + var account = state.accounts[address] || { address: address } + + return ( + h('div', { + key: msgData.id, + style: { + margin: '10px 20px', + }, + }, [ + + // account that will sign + h(AccountPanel, { + showFullAddress: true, + identity: identity, + account: account, + imageifyIdenticons: state.imageifyIdenticons, + }), + + // message data + h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-row.flex-space-between', [ + h('label.font-small', 'MESSAGE'), + h('span.font-small', msgParams.data), + ]), + ]), + + ]) + ) +} + diff --git a/responsive-ui/app/components/pending-msg.js b/responsive-ui/app/components/pending-msg.js new file mode 100644 index 000000000..b2cac164a --- /dev/null +++ b/responsive-ui/app/components/pending-msg.js @@ -0,0 +1,56 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const PendingTxDetails = require('./pending-msg-details') + +module.exports = PendingMsg + +inherits(PendingMsg, Component) +function PendingMsg () { + Component.call(this) +} + +PendingMsg.prototype.render = function () { + var state = this.props + var msgData = state.txData + + return ( + + h('div', { + key: msgData.id, + }, [ + + // header + h('h3', { + style: { + fontWeight: 'bold', + textAlign: 'center', + }, + }, 'Sign Message'), + + h('.error', { + style: { + margin: '10px', + }, + }, `Signing this message can have + dangerous side effects. Only sign messages from + sites you fully trust with your entire account. + This will be fixed in a future version.`), + + // message details + h(PendingTxDetails, state), + + // sign + cancel + h('.flex-row.flex-space-around', [ + h('button', { + onClick: state.cancelMessage, + }, 'Cancel'), + h('button', { + onClick: state.signMessage, + }, 'Sign'), + ]), + ]) + + ) +} + diff --git a/responsive-ui/app/components/pending-personal-msg-details.js b/responsive-ui/app/components/pending-personal-msg-details.js new file mode 100644 index 000000000..1050513f2 --- /dev/null +++ b/responsive-ui/app/components/pending-personal-msg-details.js @@ -0,0 +1,60 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const AccountPanel = require('./account-panel') +const BinaryRenderer = require('./binary-renderer') + +module.exports = PendingMsgDetails + +inherits(PendingMsgDetails, Component) +function PendingMsgDetails () { + Component.call(this) +} + +PendingMsgDetails.prototype.render = function () { + var state = this.props + var msgData = state.txData + + var msgParams = msgData.msgParams || {} + var address = msgParams.from || state.selectedAddress + var identity = state.identities[address] || { address: address } + var account = state.accounts[address] || { address: address } + + var { data } = msgParams + + return ( + h('div', { + key: msgData.id, + style: { + margin: '10px 20px', + }, + }, [ + + // account that will sign + h(AccountPanel, { + showFullAddress: true, + identity: identity, + account: account, + imageifyIdenticons: state.imageifyIdenticons, + }), + + // message data + h('div', { + style: { + height: '260px', + }, + }, [ + h('label.font-small', { style: { display: 'block' } }, 'MESSAGE'), + h(BinaryRenderer, { + value: data, + style: { + height: '215px', + }, + }), + ]), + + ]) + ) +} + diff --git a/responsive-ui/app/components/pending-personal-msg.js b/responsive-ui/app/components/pending-personal-msg.js new file mode 100644 index 000000000..4542adb28 --- /dev/null +++ b/responsive-ui/app/components/pending-personal-msg.js @@ -0,0 +1,47 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const PendingTxDetails = require('./pending-personal-msg-details') + +module.exports = PendingMsg + +inherits(PendingMsg, Component) +function PendingMsg () { + Component.call(this) +} + +PendingMsg.prototype.render = function () { + var state = this.props + var msgData = state.txData + + return ( + + h('div', { + key: msgData.id, + }, [ + + // header + h('h3', { + style: { + fontWeight: 'bold', + textAlign: 'center', + }, + }, 'Sign Message'), + + // message details + h(PendingTxDetails, state), + + // sign + cancel + h('.flex-row.flex-space-around', [ + h('button', { + onClick: state.cancelPersonalMessage, + }, 'Cancel'), + h('button', { + onClick: state.signPersonalMessage, + }, 'Sign'), + ]), + ]) + + ) +} + diff --git a/responsive-ui/app/components/pending-tx.js b/responsive-ui/app/components/pending-tx.js new file mode 100644 index 000000000..962680d30 --- /dev/null +++ b/responsive-ui/app/components/pending-tx.js @@ -0,0 +1,480 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const actions = require('../actions') +const clone = require('clone') + +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const hexToBn = require('../../../../app/scripts/lib/hex-to-bn') +const util = require('../util') +const MiniAccountPanel = require('./mini-account-panel') +const Copyable = require('./copyable') +const EthBalance = require('./eth-balance') +const addressSummary = util.addressSummary +const nameForAddress = require('../../lib/contract-namer') +const BNInput = require('./bn-as-decimal-input') + +const MIN_GAS_PRICE_GWEI_BN = new BN(2) +const GWEI_FACTOR = new BN(1e9) +const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) +const MIN_GAS_LIMIT_BN = new BN(21000) + +module.exports = PendingTx +inherits(PendingTx, Component) +function PendingTx () { + Component.call(this) + this.state = { + valid: true, + txData: null, + submitting: false, + } +} + +PendingTx.prototype.render = function () { + const props = this.props + const { currentCurrency, blockGasLimit } = props + + const conversionRate = props.conversionRate + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} + + // Account Details + const address = txParams.from || props.selectedAddress + const identity = props.identities[address] || { address: address } + const account = props.accounts[address] + const balance = account ? account.balance : '0x0' + + // recipient check + const isValidAddress = !txParams.to || util.isValidAddress(txParams.to) + + // Gas + const gas = txParams.gas + const gasBn = hexToBn(gas) + const gasLimit = new BN(parseInt(blockGasLimit)) + const safeGasLimit = this.bnMultiplyByFraction(gasLimit, 19, 20).toString(10) + + // Gas Price + const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) + const gasPriceBn = hexToBn(gasPrice) + + const txFeeBn = gasBn.mul(gasPriceBn) + const valueBn = hexToBn(txParams.value) + const maxCost = txFeeBn.add(valueBn) + + const dataLength = txParams.data ? (txParams.data.length - 2) / 2 : 0 + + const balanceBn = hexToBn(balance) + const insufficientBalance = balanceBn.lt(maxCost) + + this.inputs = [] + + return ( + + h('div', { + key: txMeta.id, + }, [ + + h('form#pending-tx-form', { + onSubmit: this.onSubmit.bind(this), + + }, [ + + // tx info + h('div', [ + + h('.flex-row.flex-center', { + style: { + maxWidth: '100%', + }, + }, [ + + h(MiniAccountPanel, { + imageSeed: address, + picOrder: 'right', + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, identity.name), + + h(Copyable, { + value: ethUtil.toChecksumAddress(address), + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, addressSummary(address, 6, 4, false)), + ]), + + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, [ + h(EthBalance, { + value: balance, + conversionRate, + currentCurrency, + inline: true, + labelColor: '#F7861C', + }), + ]), + ]), + + forwardCarrat(), + + this.miniAccountPanelForRecipient(), + ]), + + h('style', ` + .table-box { + margin: 7px 0px 0px 0px; + width: 100%; + } + .table-box .row { + margin: 0px; + background: rgb(236,236,236); + display: flex; + justify-content: space-between; + font-family: Montserrat Light, sans-serif; + font-size: 13px; + padding: 5px 25px; + } + .table-box .row .value { + font-family: Montserrat Regular; + } + `), + + h('.table-box', [ + + // Ether Value + // Currently not customizable, but easily modified + // in the way that gas and gasLimit currently are. + h('.row', [ + h('.cell.label', 'Amount'), + h(EthBalance, { value: txParams.value, currentCurrency, conversionRate }), + ]), + + // Gas Limit (customizable) + h('.cell.row', [ + h('.cell.label', 'Gas Limit'), + h('.cell.value', { + }, [ + h(BNInput, { + name: 'Gas Limit', + value: gasBn, + precision: 0, + scale: 0, + // The hard lower limit for gas. + min: MIN_GAS_LIMIT_BN.toString(10), + max: safeGasLimit, + suffix: 'UNITS', + style: { + position: 'relative', + top: '5px', + }, + onChange: this.gasLimitChanged.bind(this), + + ref: (hexInput) => { this.inputs.push(hexInput) }, + }), + ]), + ]), + + // Gas Price (customizable) + h('.cell.row', [ + h('.cell.label', 'Gas Price'), + h('.cell.value', { + }, [ + h(BNInput, { + name: 'Gas Price', + value: gasPriceBn, + precision: 9, + scale: 9, + suffix: 'GWEI', + min: MIN_GAS_PRICE_GWEI_BN.toString(10), + style: { + position: 'relative', + top: '5px', + }, + onChange: this.gasPriceChanged.bind(this), + ref: (hexInput) => { this.inputs.push(hexInput) }, + }), + ]), + ]), + + // Max Transaction Fee (calculated) + h('.cell.row', [ + h('.cell.label', 'Max Transaction Fee'), + h(EthBalance, { value: txFeeBn.toString(16), currentCurrency, conversionRate }), + ]), + + h('.cell.row', { + style: { + fontFamily: 'Montserrat Regular', + background: 'white', + padding: '10px 25px', + }, + }, [ + h('.cell.label', 'Max Total'), + h('.cell.value', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + h(EthBalance, { + value: maxCost.toString(16), + currentCurrency, + conversionRate, + inline: true, + labelColor: 'black', + fontSize: '16px', + }), + ]), + ]), + + // Data size row: + h('.cell.row', { + style: { + background: '#f7f7f7', + paddingBottom: '0px', + }, + }, [ + h('.cell.label'), + h('.cell.value', { + style: { + fontFamily: 'Montserrat Light', + fontSize: '11px', + }, + }, `Data included: ${dataLength} bytes`), + ]), + ]), // End of Table + + ]), + + h('style', ` + .conf-buttons button { + margin-left: 10px; + text-transform: uppercase; + } + `), + + txMeta.simulationFails ? + h('.error', { + style: { + marginLeft: 50, + fontSize: '0.9em', + }, + }, 'Transaction Error. Exception thrown in contract code.') + : null, + + !isValidAddress ? + h('.error', { + style: { + marginLeft: 50, + fontSize: '0.9em', + }, + }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.') + : null, + + insufficientBalance ? + h('span.error', { + style: { + marginLeft: 50, + fontSize: '0.9em', + }, + }, 'Insufficient balance for transaction') + : null, + + // send + cancel + h('.flex-row.flex-space-around.conf-buttons', { + style: { + display: 'flex', + justifyContent: 'flex-end', + margin: '14px 25px', + }, + }, [ + + + insufficientBalance ? + h('button.btn-green', { + onClick: props.buyEth, + }, 'Buy Ether') + : null, + + h('button', { + onClick: (event) => { + this.resetGasFields() + event.preventDefault() + }, + }, 'Reset'), + + // Accept Button + h('input.confirm.btn-green', { + type: 'submit', + value: 'SUBMIT', + style: { marginLeft: '10px' }, + disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, + }), + + h('button.cancel.btn-red', { + onClick: props.cancelTransaction, + }, 'Reject'), + ]), + ]), + ]) + ) +} + +PendingTx.prototype.miniAccountPanelForRecipient = function () { + const props = this.props + const txData = props.txData + const txParams = txData.txParams || {} + const isContractDeploy = !('to' in txParams) + + // If it's not a contract deploy, send to the account + if (!isContractDeploy) { + return h(MiniAccountPanel, { + imageSeed: txParams.to, + picOrder: 'left', + }, [ + + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, nameForAddress(txParams.to, props.identities)), + + h(Copyable, { + value: ethUtil.toChecksumAddress(txParams.to), + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, addressSummary(txParams.to, 6, 4, false)), + ]), + + ]) + } else { + return h(MiniAccountPanel, { + picOrder: 'left', + }, [ + + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, 'New Contract'), + + ]) + } +} + +PendingTx.prototype.gasPriceChanged = function (newBN, valid) { + log.info(`Gas price changed to: ${newBN.toString(10)}`) + const txMeta = this.gatherTxMeta() + txMeta.txParams.gasPrice = '0x' + newBN.toString('hex') + this.setState({ + txData: clone(txMeta), + valid, + }) +} + +PendingTx.prototype.gasLimitChanged = function (newBN, valid) { + log.info(`Gas limit changed to ${newBN.toString(10)}`) + const txMeta = this.gatherTxMeta() + txMeta.txParams.gas = '0x' + newBN.toString('hex') + this.setState({ + txData: clone(txMeta), + valid, + }) +} + +PendingTx.prototype.resetGasFields = function () { + log.debug(`pending-tx resetGasFields`) + + this.inputs.forEach((hexInput) => { + if (hexInput) { + hexInput.setValid() + } + }) + + this.setState({ + txData: null, + valid: true, + }) +} + +PendingTx.prototype.onSubmit = function (event) { + event.preventDefault() + const txMeta = this.gatherTxMeta() + const valid = this.checkValidity() + this.setState({ valid, submitting: true }) + if (valid && this.verifyGasParams()) { + this.props.sendTransaction(txMeta, event) + } else { + this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) + this.setState({ submitting: false }) + } +} + +PendingTx.prototype.checkValidity = function () { + const form = this.getFormEl() + const valid = form.checkValidity() + return valid +} + +PendingTx.prototype.getFormEl = function () { + const form = document.querySelector('form#pending-tx-form') + // Stub out form for unit tests: + if (!form) { + return { checkValidity () { return true } } + } + return form +} + +// After a customizable state value has been updated, +PendingTx.prototype.gatherTxMeta = function () { + log.debug(`pending-tx gatherTxMeta`) + const props = this.props + const state = this.state + const txData = clone(state.txData) || clone(props.txData) + + log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) + return txData +} + +PendingTx.prototype.verifyGasParams = function () { + // We call this in case the gas has not been modified at all + if (!this.state) { return true } + return ( + this._notZeroOrEmptyString(this.state.gas) && + this._notZeroOrEmptyString(this.state.gasPrice) + ) +} + +PendingTx.prototype._notZeroOrEmptyString = function (obj) { + return obj !== '' && obj !== '0x0' +} + +PendingTx.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) { + const numBN = new BN(numerator) + const denomBN = new BN(denominator) + return targetBN.mul(numBN).div(denomBN) +} + +function forwardCarrat () { + return ( + h('img', { + src: 'images/forward-carrat.svg', + style: { + padding: '5px 6px 0px 10px', + height: '37px', + }, + }) + ) +} diff --git a/responsive-ui/app/components/qr-code.js b/responsive-ui/app/components/qr-code.js new file mode 100644 index 000000000..06b9aed9b --- /dev/null +++ b/responsive-ui/app/components/qr-code.js @@ -0,0 +1,79 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const qrCode = require('qrcode-npm').qrcode +const inherits = require('util').inherits +const connect = require('react-redux').connect +const isHexPrefixed = require('ethereumjs-util').isHexPrefixed +const CopyButton = require('./copyButton') + +module.exports = connect(mapStateToProps)(QrCodeView) + +function mapStateToProps (state) { + return { + Qr: state.appState.Qr, + buyView: state.appState.buyView, + warning: state.appState.warning, + } +} + +inherits(QrCodeView, Component) + +function QrCodeView () { + Component.call(this) +} + +QrCodeView.prototype.render = function () { + const props = this.props + const Qr = props.Qr + const address = `${isHexPrefixed(Qr.data) ? 'ethereum:' : ''}${Qr.data}` + const qrImage = qrCode(4, 'M') + qrImage.addData(address) + qrImage.make() + return h('.main-container.flex-column', { + key: 'qr', + style: { + justifyContent: 'center', + paddingBottom: '45px', + paddingLeft: '45px', + paddingRight: '45px', + alignItems: 'center', + }, + }, [ + Array.isArray(Qr.message) ? h('.message-container', this.renderMultiMessage()) : h('.qr-header', Qr.message), + + this.props.warning ? this.props.warning && h('span.error.flex-center', { + style: { + textAlign: 'center', + width: '229px', + height: '82px', + }, + }, + this.props.warning) : null, + + h('#qr-container.flex-column', { + style: { + marginTop: '25px', + marginBottom: '15px', + }, + dangerouslySetInnerHTML: { + __html: qrImage.createTableTag(4), + }, + }), + h('.flex-row', [ + h('h3.ellip-address', { + style: { + width: '247px', + }, + }, Qr.data), + h(CopyButton, { + value: Qr.data, + }), + ]), + ]) +} + +QrCodeView.prototype.renderMultiMessage = function () { + var Qr = this.props.Qr + var multiMessage = Qr.message.map((message) => h('.qr-message', message)) + return multiMessage +} diff --git a/responsive-ui/app/components/range-slider.js b/responsive-ui/app/components/range-slider.js new file mode 100644 index 000000000..823f5eb01 --- /dev/null +++ b/responsive-ui/app/components/range-slider.js @@ -0,0 +1,58 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = RangeSlider + +inherits(RangeSlider, Component) +function RangeSlider () { + Component.call(this) +} + +RangeSlider.prototype.render = function () { + const state = this.state || {} + const props = this.props + const onInput = props.onInput || function () {} + const name = props.name + const { + min = 0, + max = 100, + increment = 1, + defaultValue = 50, + mirrorInput = false, + } = this.props.options + const {container, input, range} = props.style + + return ( + h('.flex-row', { + style: container, + }, [ + h('input', { + type: 'range', + name: name, + min: min, + max: max, + step: increment, + style: range, + value: state.value || defaultValue, + onChange: mirrorInput ? this.mirrorInputs.bind(this, event) : onInput, + }), + + // Mirrored input for range + mirrorInput ? h('input.large-input', { + type: 'number', + name: `${name}Mirror`, + min: min, + max: max, + value: state.value || defaultValue, + step: increment, + style: input, + onChange: this.mirrorInputs.bind(this, event), + }) : null, + ]) + ) +} + +RangeSlider.prototype.mirrorInputs = function (event) { + this.setState({value: event.target.value}) +} diff --git a/responsive-ui/app/components/shapeshift-form.js b/responsive-ui/app/components/shapeshift-form.js new file mode 100644 index 000000000..e0a720426 --- /dev/null +++ b/responsive-ui/app/components/shapeshift-form.js @@ -0,0 +1,306 @@ +const PersistentForm = require('../../lib/persistent-form') +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +const actions = require('../actions') +const Qr = require('./qr-code') +const isValidAddress = require('../util').isValidAddress +module.exports = connect(mapStateToProps)(ShapeshiftForm) + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + isSubLoading: state.appState.isSubLoading, + qrRequested: state.appState.qrRequested, + } +} + +inherits(ShapeshiftForm, PersistentForm) + +function ShapeshiftForm () { + PersistentForm.call(this) + this.persistentFormParentId = 'shapeshift-buy-form' +} + +ShapeshiftForm.prototype.render = function () { + return h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + this.props.qrRequested ? h(Qr, {key: 'qr'}) : this.renderMain(), + ]) +} + +ShapeshiftForm.prototype.renderMain = function () { + const marketinfo = this.props.buyView.formView.marketinfo + const coinOptions = this.props.buyView.formView.coinOptions + var coin = marketinfo.pair.split('_')[0].toUpperCase() + + return h('.flex-column', { + style: { + // marginTop: '10px', + padding: '25px', + paddingTop: '5px', + width: '100%', + minHeight: '215px', + alignItems: 'center', + overflowY: 'auto', + }, + }, [ + h('.flex-row', { + style: { + justifyContent: 'center', + alignItems: 'baseline', + height: '42px', + }, + }, [ + h('img', { + src: coinOptions[coin].image, + width: '25px', + height: '25px', + style: { + marginRight: '5px', + }, + }), + + h('.input-container', [ + h('input#fromCoin.buy-inputs.ex-coins', { + type: 'text', + list: 'coinList', + autoFocus: true, + dataset: { + persistentFormId: 'input-coin', + }, + style: { + boxSizing: 'border-box', + }, + onChange: this.handleLiveInput.bind(this), + defaultValue: 'BTC', + }), + + this.renderCoinList(), + + h('i.fa.fa-pencil-square-o.edit-text', { + style: { + fontSize: '12px', + color: '#F7861C', + position: 'relative', + bottom: '48px', + left: '106px', + }, + }), + ]), + + h('.icon-control', [ + h('i.fa.fa-refresh.fa-4.orange', { + style: { + bottom: '5px', + left: '5px', + color: '#F7861C', + }, + onClick: this.updateCoin.bind(this), + }), + h('i.fa.fa-chevron-right.fa-4.orange', { + style: { + position: 'relative', + bottom: '26px', + left: '10px', + color: '#F7861C', + }, + onClick: this.updateCoin.bind(this), + }), + ]), + + h('#toCoin.ex-coins', marketinfo.pair.split('_')[1].toUpperCase()), + + h('img', { + src: coinOptions[marketinfo.pair.split('_')[1].toUpperCase()].image, + width: '25px', + height: '25px', + style: { + marginLeft: '5px', + }, + }), + ]), + h('.flex-column', { + style: { + alignItems: 'flex-start', + }, + }, [ + this.props.warning ? this.props.warning && h('span.error.flex-center', { + style: { + textAlign: 'center', + width: '229px', + height: '82px', + }, + }, + this.props.warning) : this.renderInfo(), + ]), + + h(this.activeToggle('.input-container'), { + style: { + padding: '10px', + paddingTop: '0px', + width: '100%', + }, + }, [ + + h('div', `${coin} Address:`), + + h('input#fromCoinAddress.buy-inputs', { + type: 'text', + placeholder: `Your ${coin} Refund Address`, + dataset: { + persistentFormId: 'refund-address', + }, + style: { + boxSizing: 'border-box', + width: '227px', + height: '30px', + padding: ' 5px ', + }, + }), + + h('i.fa.fa-pencil-square-o.edit-text', { + style: { + fontSize: '12px', + color: '#F7861C', + position: 'relative', + bottom: '10px', + right: '11px', + }, + }), + h('.flex-row', { + style: { + justifyContent: 'flex-end', + }, + }, [ + h('button', { + onClick: this.shift.bind(this), + style: { + marginTop: '10px', + position: 'relative', + bottom: '40px', + }, + }, + 'Submit'), + ]), + ]), + ]) +} + +ShapeshiftForm.prototype.shift = function () { + var props = this.props + var withdrawal = this.props.buyView.buyAddress + var returnAddress = document.getElementById('fromCoinAddress').value + var pair = this.props.buyView.formView.marketinfo.pair + var data = { + 'withdrawal': withdrawal, + 'pair': pair, + 'returnAddress': returnAddress, + // Public api key + 'apiKey': '803d1f5df2ed1b1476e4b9e6bcd089e34d8874595dda6a23b67d93c56ea9cc2445e98a6748b219b2b6ad654d9f075f1f1db139abfa93158c04e825db122c14b6', + } + var message = [ + `Deposit Limit: ${props.buyView.formView.marketinfo.limit}`, + `Deposit Minimum:${props.buyView.formView.marketinfo.minimum}`, + ] + if (isValidAddress(withdrawal)) { + this.props.dispatch(actions.coinShiftRquest(data, message)) + } +} + +ShapeshiftForm.prototype.renderCoinList = function () { + var list = Object.keys(this.props.buyView.formView.coinOptions).map((item) => { + return h('option', { + value: item, + }, item) + }) + + return h('datalist#coinList', { + onClick: (event) => { + event.preventDefault() + }, + }, list) +} + +ShapeshiftForm.prototype.updateCoin = function (event) { + event.preventDefault() + const props = this.props + var coinOptions = this.props.buyView.formView.coinOptions + var coin = document.getElementById('fromCoin').value + + if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { + var message = 'Not a valid coin' + return props.dispatch(actions.displayWarning(message)) + } else { + return props.dispatch(actions.pairUpdate(coin)) + } +} + +ShapeshiftForm.prototype.handleLiveInput = function () { + const props = this.props + var coinOptions = this.props.buyView.formView.coinOptions + var coin = document.getElementById('fromCoin').value + + if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { + return null + } else { + return props.dispatch(actions.pairUpdate(coin)) + } +} + +ShapeshiftForm.prototype.renderInfo = function () { + const marketinfo = this.props.buyView.formView.marketinfo + const coinOptions = this.props.buyView.formView.coinOptions + var coin = marketinfo.pair.split('_')[0].toUpperCase() + + return h('span', { + style: { + }, + }, [ + h('h3.flex-row.text-transform-uppercase', { + style: { + color: '#868686', + paddingTop: '4px', + justifyContent: 'space-around', + textAlign: 'center', + fontSize: '17px', + }, + }, `Market Info for ${marketinfo.pair.replace('_', ' to ').toUpperCase()}:`), + h('.marketinfo', ['Status : ', `${coinOptions[coin].status}`]), + h('.marketinfo', ['Exchange Rate: ', `${marketinfo.rate}`]), + h('.marketinfo', ['Limit: ', `${marketinfo.limit}`]), + h('.marketinfo', ['Minimum : ', `${marketinfo.minimum}`]), + ]) +} + +ShapeshiftForm.prototype.activeToggle = function (elementType) { + if (!this.props.buyView.formView.response || this.props.warning) return elementType + return `${elementType}.inactive` +} + +ShapeshiftForm.prototype.renderLoading = function () { + return h('span', { + style: { + position: 'absolute', + left: '70px', + bottom: '194px', + background: 'transparent', + width: '229px', + height: '82px', + display: 'flex', + justifyContent: 'center', + }, + }, [ + h('img', { + style: { + width: '60px', + }, + src: 'images/loading.svg', + }), + ]) +} diff --git a/responsive-ui/app/components/shift-list-item.js b/responsive-ui/app/components/shift-list-item.js new file mode 100644 index 000000000..32bfbeda4 --- /dev/null +++ b/responsive-ui/app/components/shift-list-item.js @@ -0,0 +1,204 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const vreme = new (require('vreme')) +const explorerLink = require('../../lib/explorer-link') +const actions = require('../actions') +const addressSummary = require('../util').addressSummary + +const CopyButton = require('./copyButton') +const EthBalance = require('./eth-balance') +const Tooltip = require('./tooltip') + + +module.exports = connect(mapStateToProps)(ShiftListItem) + +function mapStateToProps (state) { + return { + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + } +} + +inherits(ShiftListItem, Component) + +function ShiftListItem () { + Component.call(this) +} + +ShiftListItem.prototype.render = function () { + return ( + h('.transaction-list-item.flex-row', { + style: { + paddingTop: '20px', + paddingBottom: '20px', + justifyContent: 'space-around', + alignItems: 'center', + }, + }, [ + h('div', { + style: { + width: '0px', + position: 'relative', + bottom: '19px', + }, + }, [ + h('img', { + src: 'https://info.shapeshift.io/sites/default/files/logo.png', + style: { + height: '35px', + width: '132px', + position: 'absolute', + clip: 'rect(0px,23px,34px,0px)', + }, + }), + ]), + + this.renderInfo(), + this.renderUtilComponents(), + ]) + ) +} + +function formatDate (date) { + return vreme.format(new Date(date), 'March 16 2014 14:30') +} + +ShiftListItem.prototype.renderUtilComponents = function () { + var props = this.props + const { conversionRate, currentCurrency } = props + + switch (props.response.status) { + case 'no_deposits': + return h('.flex-row', [ + h(CopyButton, { + value: this.props.depositAddress, + }), + h(Tooltip, { + title: 'QR Code', + }, [ + h('i.fa.fa-qrcode.pointer.pop-hover', { + onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType)), + style: { + margin: '5px', + marginLeft: '23px', + marginRight: '12px', + fontSize: '20px', + color: '#F7861C', + }, + }), + ]), + ]) + case 'received': + return h('.flex-row') + + case 'complete': + return h('.flex-row', [ + h(CopyButton, { + value: this.props.response.transaction, + }), + h(EthBalance, { + value: `${props.response.outgoingCoin}`, + conversionRate, + currentCurrency, + width: '55px', + shorten: true, + needsParse: false, + incoming: true, + style: { + fontSize: '15px', + color: '#01888C', + }, + }), + ]) + + case 'failed': + return '' + default: + return '' + } +} + +ShiftListItem.prototype.renderInfo = function () { + var props = this.props + switch (props.response.status) { + case 'no_deposits': + return h('.flex-column', { + style: { + width: '200px', + overflow: 'hidden', + }, + }, [ + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, `${props.depositType} to ETH via ShapeShift`), + h('div', 'No deposits received'), + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, formatDate(props.time)), + ]) + case 'received': + return h('.flex-column', { + style: { + width: '200px', + overflow: 'hidden', + }, + }, [ + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, `${props.depositType} to ETH via ShapeShift`), + h('div', 'Conversion in progress'), + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, formatDate(props.time)), + ]) + case 'complete': + var url = explorerLink(props.response.transaction, parseInt('1')) + + return h('.flex-column.pointer', { + style: { + width: '200px', + overflow: 'hidden', + }, + onClick: () => global.platform.openWindow({ url }), + }, [ + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, 'From ShapeShift'), + h('div', formatDate(props.time)), + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, addressSummary(props.response.transaction)), + ]) + + case 'failed': + return h('span.error', '(Failed)') + default: + return '' + } +} diff --git a/responsive-ui/app/components/tab-bar.js b/responsive-ui/app/components/tab-bar.js new file mode 100644 index 000000000..6295e7dd9 --- /dev/null +++ b/responsive-ui/app/components/tab-bar.js @@ -0,0 +1,36 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = TabBar + +inherits(TabBar, Component) +function TabBar () { + Component.call(this) +} + +TabBar.prototype.render = function () { + const props = this.props + const state = this.state || {} + const { tabs = [], defaultTab, tabSelected } = props + const { subview = defaultTab } = state + + return ( + h('.flex-row.space-around.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + paddingTop: '4px', + }, + }, tabs.map((tab) => { + const { key, content } = tab + return h(subview === key ? '.activeForm' : '.inactiveForm.pointer', { + onClick: () => { + this.setState({ subview: key }) + tabSelected(key) + }, + }, content) + })) + ) +} + diff --git a/responsive-ui/app/components/template.js b/responsive-ui/app/components/template.js new file mode 100644 index 000000000..b6ed8eaa0 --- /dev/null +++ b/responsive-ui/app/components/template.js @@ -0,0 +1,18 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = NewComponent + +inherits(NewComponent, Component) +function NewComponent () { + Component.call(this) +} + +NewComponent.prototype.render = function () { + const props = this.props + + return ( + h('span', props.message) + ) +} diff --git a/responsive-ui/app/components/token-cell.js b/responsive-ui/app/components/token-cell.js new file mode 100644 index 000000000..19d7139bb --- /dev/null +++ b/responsive-ui/app/components/token-cell.js @@ -0,0 +1,72 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Identicon = require('./identicon') +const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') + +module.exports = TokenCell + +inherits(TokenCell, Component) +function TokenCell () { + Component.call(this) +} + +TokenCell.prototype.render = function () { + const props = this.props + const { address, symbol, string, network, userAddress } = props + + return ( + h('li.token-cell', { + style: { cursor: network === '1' ? 'pointer' : 'default' }, + onClick: this.view.bind(this, address, userAddress, network), + }, [ + + h(Identicon, { + diameter: 50, + address, + network, + }), + + h('h3', `${string || 0} ${symbol}`), + + h('span', { style: { flex: '1 0 auto' } }), + + /* + h('button', { + onClick: this.send.bind(this, address), + }, 'SEND'), + */ + + ]) + ) +} + +TokenCell.prototype.send = function (address, event) { + event.preventDefault() + event.stopPropagation() + const url = tokenFactoryFor(address) + if (url) { + navigateTo(url) + } +} + +TokenCell.prototype.view = function (address, userAddress, network, event) { + const url = etherscanLinkFor(address, userAddress, network) + if (url) { + navigateTo(url) + } +} + +function navigateTo (url) { + global.platform.openWindow({ url }) +} + +function etherscanLinkFor (tokenAddress, address, network) { + const prefix = prefixForNetwork(network) + return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}` +} + +function tokenFactoryFor (tokenAddress) { + return `https://tokenfactory.surge.sh/#/token/${tokenAddress}` +} + diff --git a/responsive-ui/app/components/token-list.js b/responsive-ui/app/components/token-list.js new file mode 100644 index 000000000..20cfa897e --- /dev/null +++ b/responsive-ui/app/components/token-list.js @@ -0,0 +1,192 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const TokenTracker = require('eth-token-tracker') +const TokenCell = require('./token-cell.js') +const normalizeAddress = require('eth-sig-util').normalize + +const defaultTokens = [] +const contracts = require('eth-contract-metadata') +for (const address in contracts) { + const contract = contracts[address] + if (contract.erc20) { + contract.address = address + defaultTokens.push(contract) + } +} + +module.exports = TokenList + +inherits(TokenList, Component) +function TokenList () { + this.state = { + tokens: [], + isLoading: true, + network: null, + } + Component.call(this) +} + +TokenList.prototype.render = function () { + const state = this.state + const { tokens, isLoading, error } = state + const { userAddress, network } = this.props + + if (isLoading) { + return this.message('Loading') + } + + if (error) { + log.error(error) + return this.message('There was a problem loading your token balances.') + } + + const tokenViews = tokens.map((tokenData) => { + tokenData.network = network + tokenData.userAddress = userAddress + return h(TokenCell, tokenData) + }) + + return h('div', [ + h('ol', { + style: { + height: '260px', + overflowY: 'auto', + display: 'flex', + flexDirection: 'column', + }, + }, [ + h('style', ` + + li.token-cell { + display: flex; + flex-direction: row; + align-items: center; + padding: 10px; + } + + li.token-cell > h3 { + margin-left: 12px; + } + + li.token-cell:hover { + background: white; + cursor: pointer; + } + + `), + ...tokenViews, + tokenViews.length ? null : this.message('No Tokens Found.'), + ]), + this.addTokenButtonElement(), + ]) +} + +TokenList.prototype.addTokenButtonElement = function () { + return h('div', [ + h('div.footer.hover-white.pointer', { + key: 'reveal-account-bar', + onClick: () => { + this.props.addToken() + }, + style: { + display: 'flex', + height: '40px', + padding: '10px', + justifyContent: 'center', + alignItems: 'center', + }, + }, [ + h('i.fa.fa-plus.fa-lg'), + ]), + ]) +} + +TokenList.prototype.message = function (body) { + return h('div', { + style: { + display: 'flex', + height: '250px', + alignItems: 'center', + justifyContent: 'center', + padding: '30px', + }, + }, body) +} + +TokenList.prototype.componentDidMount = function () { + this.createFreshTokenTracker() +} + +TokenList.prototype.createFreshTokenTracker = function () { + if (this.tracker) { + // Clean up old trackers when refreshing: + this.tracker.stop() + this.tracker.removeListener('update', this.balanceUpdater) + this.tracker.removeListener('error', this.showError) + } + + if (!global.ethereumProvider) return + const { userAddress } = this.props + this.tracker = new TokenTracker({ + userAddress, + provider: global.ethereumProvider, + tokens: uniqueMergeTokens(defaultTokens, this.props.tokens), + pollingInterval: 8000, + }) + + + // Set up listener instances for cleaning up + this.balanceUpdater = this.updateBalances.bind(this) + this.showError = (error) => { + this.setState({ error, isLoading: false }) + } + this.tracker.on('update', this.balanceUpdater) + this.tracker.on('error', this.showError) + + this.tracker.updateBalances() + .then(() => { + this.updateBalances(this.tracker.serialize()) + }) + .catch((reason) => { + log.error(`Problem updating balances`, reason) + this.setState({ isLoading: false }) + }) +} + +TokenList.prototype.componentWillUpdate = function (nextProps) { + if (nextProps.network === 'loading') return + const oldNet = this.props.network + const newNet = nextProps.network + + if (oldNet && newNet && newNet !== oldNet) { + this.setState({ isLoading: true }) + this.createFreshTokenTracker() + } +} + +TokenList.prototype.updateBalances = function (tokens) { + const heldTokens = tokens.filter(token => { + return token.balance !== '0' && token.string !== '0.000' + }) + this.setState({ tokens: heldTokens, isLoading: false }) +} + +TokenList.prototype.componentWillUnmount = function () { + if (!this.tracker) return + this.tracker.stop() +} + +function uniqueMergeTokens (tokensA, tokensB) { + const uniqueAddresses = [] + const result = [] + tokensA.concat(tokensB).forEach((token) => { + const normal = normalizeAddress(token.address) + if (!uniqueAddresses.includes(normal)) { + uniqueAddresses.push(normal) + result.push(token) + } + }) + return result +} + diff --git a/responsive-ui/app/components/tooltip.js b/responsive-ui/app/components/tooltip.js new file mode 100644 index 000000000..edbc074bb --- /dev/null +++ b/responsive-ui/app/components/tooltip.js @@ -0,0 +1,22 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ReactTooltip = require('react-tooltip-component') + +module.exports = Tooltip + +inherits(Tooltip, Component) +function Tooltip () { + Component.call(this) +} + +Tooltip.prototype.render = function () { + const props = this.props + const { position, title, children } = props + + return h(ReactTooltip, { + position: position || 'left', + title, + fixed: false, + }, children) +} diff --git a/responsive-ui/app/components/transaction-list-item-icon.js b/responsive-ui/app/components/transaction-list-item-icon.js new file mode 100644 index 000000000..431054340 --- /dev/null +++ b/responsive-ui/app/components/transaction-list-item-icon.js @@ -0,0 +1,68 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Tooltip = require('./tooltip') + +const Identicon = require('./identicon') + +module.exports = TransactionIcon + +inherits(TransactionIcon, Component) +function TransactionIcon () { + Component.call(this) +} + +TransactionIcon.prototype.render = function () { + const { transaction, txParams, isMsg } = this.props + switch (transaction.status) { + case 'unapproved': + return h(!isMsg ? '.unapproved-tx-icon' : 'i.fa.fa-certificate.fa-lg') + + case 'rejected': + return h('i.fa.fa-exclamation-triangle.fa-lg.warning', { + style: { + width: '24px', + }, + }) + + case 'failed': + return h('i.fa.fa-exclamation-triangle.fa-lg.error', { + style: { + width: '24px', + }, + }) + + case 'submitted': + return h(Tooltip, { + title: 'Pending', + position: 'bottom', + }, [ + h('i.fa.fa-ellipsis-h', { + style: { + fontSize: '27px', + }, + }), + ]) + } + + if (isMsg) { + return h('i.fa.fa-certificate.fa-lg', { + style: { + width: '24px', + }, + }) + } + + if (txParams.to) { + return h(Identicon, { + diameter: 24, + address: txParams.to || transaction.hash, + }) + } else { + return h('i.fa.fa-file-text-o.fa-lg', { + style: { + width: '24px', + }, + }) + } +} diff --git a/responsive-ui/app/components/transaction-list-item.js b/responsive-ui/app/components/transaction-list-item.js new file mode 100644 index 000000000..dbda66a31 --- /dev/null +++ b/responsive-ui/app/components/transaction-list-item.js @@ -0,0 +1,165 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const EthBalance = require('./eth-balance') +const addressSummary = require('../util').addressSummary +const explorerLink = require('../../lib/explorer-link') +const CopyButton = require('./copyButton') +const vreme = new (require('vreme')) +const Tooltip = require('./tooltip') +const numberToBN = require('number-to-bn') + +const TransactionIcon = require('./transaction-list-item-icon') +const ShiftListItem = require('./shift-list-item') +module.exports = TransactionListItem + +inherits(TransactionListItem, Component) +function TransactionListItem () { + Component.call(this) +} + +TransactionListItem.prototype.render = function () { + const { transaction, network, conversionRate, currentCurrency } = this.props + if (transaction.key === 'shapeshift') { + if (network === '1') return h(ShiftListItem, transaction) + } + var date = formatDate(transaction.time) + + let isLinkable = false + const numericNet = parseInt(network) + isLinkable = numericNet === 1 || numericNet === 3 || numericNet === 4 || numericNet === 42 + + var isMsg = ('msgParams' in transaction) + var isTx = ('txParams' in transaction) + var isPending = transaction.status === 'unapproved' + let txParams + if (isTx) { + txParams = transaction.txParams + } else if (isMsg) { + txParams = transaction.msgParams + } + + const nonce = txParams.nonce ? numberToBN(txParams.nonce).toString(10) : '' + + const isClickable = ('hash' in transaction && isLinkable) || isPending + return ( + h(`.transaction-list-item.flex-row.flex-space-between${isClickable ? '.pointer' : ''}`, { + onClick: (event) => { + if (isPending) { + this.props.showTx(transaction.id) + } + event.stopPropagation() + if (!transaction.hash || !isLinkable) return + var url = explorerLink(transaction.hash, parseInt(network)) + global.platform.openWindow({ url }) + }, + style: { + padding: '20px 0', + }, + }, [ + + h('.identicon-wrapper.flex-column.flex-center.select-none', [ + h('.pop-hover', { + onClick: (event) => { + event.stopPropagation() + if (!isTx || isPending) return + var url = `https://metamask.github.io/eth-tx-viz/?tx=${transaction.hash}` + global.platform.openWindow({ url }) + }, + }, [ + h(TransactionIcon, { txParams, transaction, isTx, isMsg }), + ]), + ]), + + h(Tooltip, { + title: 'Transaction Number', + position: 'bottom', + }, [ + h('span', { + style: { + display: 'flex', + cursor: 'normal', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + padding: '10px', + }, + }, nonce), + ]), + + h('.flex-column', {style: {width: '200px', overflow: 'hidden'}}, [ + domainField(txParams), + h('div', date), + recipientField(txParams, transaction, isTx, isMsg), + ]), + + // Places a copy button if tx is successful, else places a placeholder empty div. + transaction.hash ? h(CopyButton, { value: transaction.hash }) : h('div', {style: { display: 'flex', alignItems: 'center', width: '26px' }}), + + isTx ? h(EthBalance, { + value: txParams.value, + conversionRate, + currentCurrency, + width: '55px', + shorten: true, + showFiat: false, + style: {fontSize: '15px'}, + }) : h('.flex-column'), + ]) + ) +} + +function domainField (txParams) { + return h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + overflow: 'hidden', + textOverflow: 'ellipsis', + width: '100%', + }, + }, [ + txParams.origin, + ]) +} + +function recipientField (txParams, transaction, isTx, isMsg) { + let message + + if (isMsg) { + message = 'Signature Requested' + } else if (txParams.to) { + message = addressSummary(txParams.to) + } else { + message = 'Contract Published' + } + + return h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + }, + }, [ + message, + failIfFailed(transaction), + ]) +} + +function formatDate (date) { + return vreme.format(new Date(date), 'March 16 2014 14:30') +} + +function failIfFailed (transaction) { + if (transaction.status === 'rejected') { + return h('span.error', ' (Rejected)') + } + if (transaction.err) { + return h(Tooltip, { + title: transaction.err.message, + position: 'bottom', + }, [ + h('span.error', ' (Failed)'), + ]) + } +} diff --git a/responsive-ui/app/components/transaction-list.js b/responsive-ui/app/components/transaction-list.js new file mode 100644 index 000000000..3b4ba741e --- /dev/null +++ b/responsive-ui/app/components/transaction-list.js @@ -0,0 +1,79 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const TransactionListItem = require('./transaction-list-item') + +module.exports = TransactionList + + +inherits(TransactionList, Component) +function TransactionList () { + Component.call(this) +} + +TransactionList.prototype.render = function () { + const { transactions, network, unapprovedMsgs, conversionRate } = this.props + + var shapeShiftTxList + if (network === '1') { + shapeShiftTxList = this.props.shapeShiftTxList + } + const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList) + .sort((a, b) => b.time - a.time) + + return ( + + h('section.transaction-list', [ + + h('style', ` + .transaction-list .transaction-list-item:not(:last-of-type) { + border-bottom: 1px solid #D4D4D4; + } + .transaction-list .transaction-list-item .ether-balance-label { + display: block !important; + font-size: small; + } + `), + + h('.tx-list', { + style: { + overflowY: 'auto', + height: '300px', + padding: '0 20px', + textAlign: 'center', + }, + }, [ + + txsToRender.length + ? txsToRender.map((transaction, i) => { + let key + switch (transaction.key) { + case 'shapeshift': + const { depositAddress, time } = transaction + key = `shift-tx-${depositAddress}-${time}-${i}` + break + default: + key = `tx-${transaction.id}-${i}` + } + return h(TransactionListItem, { + transaction, i, network, key, + conversionRate, + showTx: (txId) => { + this.props.viewPendingTx(txId) + }, + }) + }) + : h('.flex-center', { + style: { + flexDirection: 'column', + height: '100%', + }, + }, [ + 'No transaction history.', + ]), + ]), + ]) + ) +} + diff --git a/responsive-ui/app/conf-tx.js b/responsive-ui/app/conf-tx.js new file mode 100644 index 000000000..63b77ef7f --- /dev/null +++ b/responsive-ui/app/conf-tx.js @@ -0,0 +1,213 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') +const NetworkIndicator = require('./components/network') +const txHelper = require('../lib/tx-helper') +const isPopupOrNotification = require('../../../app/scripts/lib/is-popup-or-notification') + +const PendingTx = require('./components/pending-tx') +const PendingMsg = require('./components/pending-msg') +const PendingPersonalMsg = require('./components/pending-personal-msg') +const Loading = require('./components/loading') + +module.exports = connect(mapStateToProps)(ConfirmTxScreen) + +function mapStateToProps (state) { + return { + identities: state.metamask.identities, + accounts: state.metamask.accounts, + selectedAddress: state.metamask.selectedAddress, + unapprovedTxs: state.metamask.unapprovedTxs, + unapprovedMsgs: state.metamask.unapprovedMsgs, + unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs, + index: state.appState.currentView.context, + warning: state.appState.warning, + network: state.metamask.network, + provider: state.metamask.provider, + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + blockGasLimit: state.metamask.currentBlockGasLimit, + } +} + +inherits(ConfirmTxScreen, Component) +function ConfirmTxScreen () { + Component.call(this) +} + +ConfirmTxScreen.prototype.render = function () { + const props = this.props + const { network, provider, unapprovedTxs, currentCurrency, + unapprovedMsgs, unapprovedPersonalMsgs, conversionRate, blockGasLimit } = props + + var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) + + var txData = unconfTxList[props.index] || {} + var txParams = txData.params || {} + var isNotification = isPopupOrNotification() === 'notification' + + + log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`) + if (unconfTxList.length === 0) return h(Loading, { isLoading: true }) + + return ( + + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + !isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: this.goHome.bind(this), + }) : null, + h('h2.page-subtitle', 'Confirm Transaction'), + isNotification ? h(NetworkIndicator, { + network: network, + provider: provider, + }) : null, + ]), + + h('h3', { + style: { + alignSelf: 'center', + display: unconfTxList.length > 1 ? 'block' : 'none', + }, + }, [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + style: { + display: props.index === 0 ? 'none' : 'inline-block', + }, + onClick: () => props.dispatch(actions.previousTx()), + }), + ` ${props.index + 1} of ${unconfTxList.length} `, + h('i.fa.fa-arrow-right.fa-lg.cursor-pointer', { + style: { + display: props.index + 1 === unconfTxList.length ? 'none' : 'inline-block', + }, + onClick: () => props.dispatch(actions.nextTx()), + }), + ]), + + warningIfExists(props.warning), + + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + + currentTxView({ + // Properties + txData: txData, + key: txData.id, + selectedAddress: props.selectedAddress, + accounts: props.accounts, + identities: props.identities, + conversionRate, + currentCurrency, + blockGasLimit, + // Actions + buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), + sendTransaction: this.sendTransaction.bind(this), + cancelTransaction: this.cancelTransaction.bind(this, txData), + signMessage: this.signMessage.bind(this, txData), + signPersonalMessage: this.signPersonalMessage.bind(this, txData), + cancelMessage: this.cancelMessage.bind(this, txData), + cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), + }), + + ]), + ]) + ) +} + +function currentTxView (opts) { + log.info('rendering current tx view') + const { txData } = opts + const { txParams, msgParams, type } = txData + + if (txParams) { + log.debug('txParams detected, rendering pending tx') + return h(PendingTx, opts) + } else if (msgParams) { + log.debug('msgParams detected, rendering pending msg') + + if (type === 'eth_sign') { + log.debug('rendering eth_sign message') + return h(PendingMsg, opts) + } else if (type === 'personal_sign') { + log.debug('rendering personal_sign message') + return h(PendingPersonalMsg, opts) + } + } +} + +ConfirmTxScreen.prototype.buyEth = function (address, event) { + event.preventDefault() + this.props.dispatch(actions.buyEthView(address)) +} + +ConfirmTxScreen.prototype.sendTransaction = function (txData, event) { + this.stopPropagation(event) + this.props.dispatch(actions.updateAndApproveTx(txData)) +} + +ConfirmTxScreen.prototype.cancelTransaction = function (txData, event) { + this.stopPropagation(event) + event.preventDefault() + this.props.dispatch(actions.cancelTx(txData)) +} + +ConfirmTxScreen.prototype.signMessage = function (msgData, event) { + log.info('conf-tx.js: signing message') + var params = msgData.msgParams + params.metamaskId = msgData.id + this.stopPropagation(event) + this.props.dispatch(actions.signMsg(params)) +} + +ConfirmTxScreen.prototype.stopPropagation = function (event) { + if (event.stopPropagation) { + event.stopPropagation() + } +} + +ConfirmTxScreen.prototype.signPersonalMessage = function (msgData, event) { + log.info('conf-tx.js: signing personal message') + var params = msgData.msgParams + params.metamaskId = msgData.id + this.stopPropagation(event) + this.props.dispatch(actions.signPersonalMsg(params)) +} + +ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) { + log.info('canceling message') + this.stopPropagation(event) + this.props.dispatch(actions.cancelMsg(msgData)) +} + +ConfirmTxScreen.prototype.cancelPersonalMessage = function (msgData, event) { + log.info('canceling personal message') + this.stopPropagation(event) + this.props.dispatch(actions.cancelPersonalMsg(msgData)) +} + +ConfirmTxScreen.prototype.goHome = function (event) { + this.stopPropagation(event) + this.props.dispatch(actions.goHome()) +} + +function warningIfExists (warning) { + if (warning && + // Do not display user rejections on this screen: + warning.indexOf('User denied transaction signature') === -1) { + return h('.error', { + style: { + margin: 'auto', + }, + }, warning) + } +} diff --git a/responsive-ui/app/config.js b/responsive-ui/app/config.js new file mode 100644 index 000000000..62785c49b --- /dev/null +++ b/responsive-ui/app/config.js @@ -0,0 +1,211 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') +const currencies = require('./conversion.json').rows +const validUrl = require('valid-url') +const copyToClipboard = require('copy-to-clipboard') + +module.exports = connect(mapStateToProps)(ConfigScreen) + +function mapStateToProps (state) { + return { + metamask: state.metamask, + warning: state.appState.warning, + } +} + +inherits(ConfigScreen, Component) +function ConfigScreen () { + Component.call(this) +} + +ConfigScreen.prototype.render = function () { + var state = this.props + var metamaskState = state.metamask + var warning = state.warning + + return ( + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + state.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Settings'), + ]), + + h('.error', { + style: { + display: warning ? 'block' : 'none', + padding: '0 20px', + textAlign: 'center', + }, + }, warning), + + // conf view + h('.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-space-around', { + style: { + padding: '20px', + }, + }, [ + + currentProviderDisplay(metamaskState), + + h('div', { style: {display: 'flex'} }, [ + h('input#new_rpc', { + placeholder: 'New RPC URL', + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onKeyPress (event) { + if (event.key === 'Enter') { + var element = event.target + var newRpc = element.value + rpcValidation(newRpc, state) + } + }, + }), + h('button', { + style: { + alignSelf: 'center', + }, + onClick (event) { + event.preventDefault() + var element = document.querySelector('input#new_rpc') + var newRpc = element.value + rpcValidation(newRpc, state) + }, + }, 'Save'), + ]), + + h('hr.horizontal-line'), + + currentConversionInformation(metamaskState, state), + + h('hr.horizontal-line'), + + h('div', { + style: { + marginTop: '20px', + }, + }, [ + h('p', { + style: { + fontFamily: 'Montserrat Light', + fontSize: '13px', + }, + }, `State logs contain your public account addresses and sent transactions.`), + h('br'), + h('button', { + style: { + alignSelf: 'center', + }, + onClick (event) { + copyToClipboard(window.logState()) + }, + }, 'Copy State Logs'), + ]), + + h('hr.horizontal-line'), + + h('div', { + style: { + marginTop: '20px', + }, + }, [ + h('button', { + style: { + alignSelf: 'center', + }, + onClick (event) { + event.preventDefault() + state.dispatch(actions.revealSeedConfirmation()) + }, + }, 'Reveal Seed Words'), + ]), + + ]), + ]), + ]) + ) +} + +function rpcValidation (newRpc, state) { + if (validUrl.isWebUri(newRpc)) { + state.dispatch(actions.setRpcTarget(newRpc)) + } else { + var appendedRpc = `http://${newRpc}` + if (validUrl.isWebUri(appendedRpc)) { + state.dispatch(actions.displayWarning('URIs require the appropriate HTTP/HTTPS prefix.')) + } else { + state.dispatch(actions.displayWarning('Invalid RPC URI')) + } + } +} + +function currentConversionInformation (metamaskState, state) { + var currentCurrency = metamaskState.currentCurrency + var conversionDate = metamaskState.conversionDate + return h('div', [ + h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, 'Current Conversion'), + h('span', {style: { fontWeight: 'bold', paddingRight: '10px', fontSize: '13px'}}, `Updated ${Date(conversionDate)}`), + h('select#currentCurrency', { + onChange (event) { + event.preventDefault() + var element = document.getElementById('currentCurrency') + var newCurrency = element.value + state.dispatch(actions.setCurrentCurrency(newCurrency)) + }, + defaultValue: currentCurrency, + }, currencies.map((currency) => { + return h('option', {key: currency.code, value: currency.code}, `${currency.code} - ${currency.name}`) + }) + ), + ]) +} + +function currentProviderDisplay (metamaskState) { + var provider = metamaskState.provider + var title, value + + switch (provider.type) { + + case 'mainnet': + title = 'Current Network' + value = 'Main Ethereum Network' + break + + case 'ropsten': + title = 'Current Network' + value = 'Ropsten Test Network' + break + + case 'kovan': + title = 'Current Network' + value = 'Kovan Test Network' + break + + case 'rinkeby': + title = 'Current Network' + value = 'Rinkeby Test Network' + break + + default: + title = 'Current RPC' + value = metamaskState.provider.rpcTarget + } + + return h('div', [ + h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, title), + h('span', value), + ]) +} diff --git a/responsive-ui/app/conversion.json b/responsive-ui/app/conversion.json new file mode 100644 index 000000000..155ffc4fc --- /dev/null +++ b/responsive-ui/app/conversion.json @@ -0,0 +1,207 @@ +{ + "rows": [ + { + "code": "REP", + "name": "Augur", + "statuses": [ + "primary" + ] + }, + { + "code": "BCN", + "name": "Bytecoin", + "statuses": [ + "primary" + ] + }, + { + "code": "BTC", + "name": "Bitcoin", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "BTS", + "name": "BitShares", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "BLK", + "name": "Blackcoin", + "statuses": [ + "primary" + ] + }, + { + "code": "GBP", + "name": "British Pound Sterling", + "statuses": [ + "secondary" + ] + }, + { + "code": "CAD", + "name": "Canadian Dollar", + "statuses": [ + "secondary" + ] + }, + { + "code": "CNY", + "name": "Chinese Yuan", + "statuses": [ + "secondary" + ] + }, + { + "code": "DSH", + "name": "Dashcoin", + "statuses": [ + "primary" + ] + }, + { + "code": "DOGE", + "name": "Dogecoin", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "ETC", + "name": "Ethereum Classic", + "statuses": [ + "primary" + ] + }, + { + "code": "EUR", + "name": "Euro", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "GNO", + "name": "GNO", + "statuses": [ + "primary" + ] + }, + { + "code": "GNT", + "name": "GNT", + "statuses": [ + "primary" + ] + }, + { + "code": "JPY", + "name": "Japanese Yen", + "statuses": [ + "secondary" + ] + }, + { + "code": "LTC", + "name": "Litecoin", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "MAID", + "name": "MaidSafeCoin", + "statuses": [ + "primary" + ] + }, + { + "code": "XEM", + "name": "NEM", + "statuses": [ + "primary" + ] + }, + { + "code": "XLM", + "name": "Stellar", + "statuses": [ + "primary" + ] + }, + { + "code": "XMR", + "name": "Monero", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "XRP", + "name": "Ripple", + "statuses": [ + "primary" + ] + }, + { + "code": "RUR", + "name": "Ruble", + "statuses": [ + "secondary" + ] + }, + { + "code": "STEEM", + "name": "Steem", + "statuses": [ + "primary" + ] + }, + { + "code": "STRAT", + "name": "STRAT", + "statuses": [ + "primary" + ] + }, + { + "code": "UAH", + "name": "Ukrainian Hryvnia", + "statuses": [ + "secondary" + ] + }, + { + "code": "USD", + "name": "US Dollar", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "WAVES", + "name": "WAVES", + "statuses": [ + "primary" + ] + }, + { + "code": "ZEC", + "name": "Zcash", + "statuses": [ + "primary" + ] + } + ] +} diff --git a/responsive-ui/app/css/debug.css b/responsive-ui/app/css/debug.css new file mode 100644 index 000000000..3e125bcd4 --- /dev/null +++ b/responsive-ui/app/css/debug.css @@ -0,0 +1,21 @@ +/* +debug / dev +*/ + +#app-content { + border: 2px solid green; +} + +#design-container { + position: absolute; + left: 360px; + top: -42px; + width: calc(100vw - 360px); + height: 100vh; + overflow: scroll; +} + +#design-container img { + width: 2000px; + margin-right: 600px; +} \ No newline at end of file diff --git a/responsive-ui/app/css/fonts.css b/responsive-ui/app/css/fonts.css new file mode 100644 index 000000000..3b9f581b9 --- /dev/null +++ b/responsive-ui/app/css/fonts.css @@ -0,0 +1,36 @@ +@import url(https://fonts.googleapis.com/css?family=Roboto:300,500); +@import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css); + +@font-face { + font-family: 'Montserrat Regular'; + src: url('/fonts/Montserrat/Montserrat-Regular.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; + font-size: 'small'; + +} + +@font-face { + font-family: 'Montserrat Bold'; + src: url('/fonts/Montserrat/Montserrat-Bold.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-Bold.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Montserrat Light'; + src: url('/fonts/Montserrat/Montserrat-Light.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-Light.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Montserrat UltraLight'; + src: url('/fonts/Montserrat/Montserrat-UltraLight.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-UltraLight.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} diff --git a/responsive-ui/app/css/index.css b/responsive-ui/app/css/index.css new file mode 100644 index 000000000..c82c1b21b --- /dev/null +++ b/responsive-ui/app/css/index.css @@ -0,0 +1,674 @@ +/* +faint orange (textfield shades) #FAF6F0 +light orange (button shades): #F5C26D +dark orange (text): #F5A623 +borders/font/any gray: #4A4A4A +*/ + +/* +application specific styles +*/ + +* { + box-sizing: border-box; +} + +html, body { + font-family: 'Montserrat Regular', Arial; + color: #4D4D4D; + font-weight: 300; + line-height: 1.4em; + background: #F7F7F7; + width: 100%; + height: 100%; + margin: 0; + padding: 0; +} + +.css-transition-group { + flex: 1; +} + +input:focus, textarea:focus { + outline: none; +} + +#app-content { + overflow-x: hidden; + min-width: 357px; +} + +button, input[type="submit"] { + font-family: 'Montserrat Bold'; + outline: none; + cursor: pointer; + padding: 8px 12px; + border: none; + color: white; + transform-origin: center center; + transition: transform 50ms ease-in; + /* default orange */ + background: rgba(247, 134, 28, 1); + box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); +} + +.btn-green, input[type="submit"].btn-green { + background: rgba(106, 195, 96, 1); + box-shadow: 0px 3px 6px rgba(106, 195, 96, 0.36); +} + +.btn-red { + background: rgba(254, 35, 17, 1); + box-shadow: 0px 3px 6px rgba(254, 35, 17, 0.36); +} + +button[disabled], input[type="submit"][disabled] { + cursor: not-allowed; + background: rgba(197, 197, 197, 1); + box-shadow: 0px 3px 6px rgba(197, 197, 197, 0.36); +} + +button.spaced { + margin: 2px; +} + +button:not([disabled]):hover, input[type="submit"]:not([disabled]):hover { + transform: scale(1.1); +} +button:not([disabled]):active, input[type="submit"]:not([disabled]):active { + transform: scale(0.95); +} + +a { + text-decoration: none; + color: inherit; +} + +a:hover{ + color: #df6b0e; +} + +/* +app +*/ + +.active { + color: #909090; +} + +button.primary { + padding: 8px 12px; + background: #F7861C; + box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); + color: white; + font-size: 1.1em; + font-family: 'Montserrat Regular'; + text-transform: uppercase; +} + +button.btn-thin { + border: 1px solid; + border-color: #4D4D4D; + color: #4D4D4D; + background: rgb(255, 174, 41); + border-radius: 4px; + min-width: 200px; + margin: 12px 0; + padding: 6px; + font-size: 13px; +} + +.app-header { + padding: 6px 8px; +} + +.app-header h1 { + font-family: 'Montserrat Regular'; + text-transform: uppercase; + color: #AEAEAE; +} + +h2.page-subtitle { + font-family: 'Montserrat Regular'; + text-transform: uppercase; + color: #AEAEAE; + font-size: 1em; + margin: 12px; +} + +.app-primary { + +} + +.app-footer { + padding-bottom: 10px; + align-items: center; +} + +.identicon { + height: 46px; + width: 46px; + background-size: cover; + border-radius: 100%; + border: 3px solid gray; +} + +textarea.twelve-word-phrase { + padding: 12px; + width: 300px; + height: 140px; + font-size: 16px; + background: white; + resize: none; +} + +.network-indicator { + display: flex; + align-items: center; + font-size: 0.6em; + +} + +.network-name { + width: 5.2em; + line-height: 9px; + text-rendering: geometricPrecision; +} + +.check { + margin-left: 7px; + color: #F7861C; + flex: 1 0 auto; + display: flex; + justify-content: flex-end; +} +/* +app sections +*/ + +/* initialize */ + +.initialize-screen hr { + width: 60px; + margin: 12px; + border-color: #F7861C; + border-style: solid; +} + +.initialize-screen label { + margin-top: 20px; +} + +.initialize-screen button.create-vault { + margin-top: 40px; +} + +.initialize-screen .warning { + font-size: 14px; + margin: 0 16px; +} + +/* unlock */ +.error { + color: #E20202; +} + +.warning { + color: #FFAE00; +} + +.lock { + width: 50px; + height: 50px; +} + +.lock.locked { + transform: scale(1.5); + opacity: 0.0; + transition: opacity 400ms ease-in, transform 400ms ease-in; +} +.lock.unlocked { + transform: scale(1); + opacity: 1; + transition: opacity 500ms ease-out, transform 500ms ease-out, background 200ms ease-in; +} + +.lock.locked .lock-top { + transform: scaleX(1) translateX(0); + transition: transform 250ms ease-in; +} +.lock.unlocked .lock-top { + transform: scaleX(-1) translateX(-12px); + transition: transform 250ms ease-in; +} +.lock.unlocked:hover { + border-radius: 4px; + background: #e5e5e5; + border: 1px solid #b1b1b1; +} +.lock.unlocked:active { + background: #c3c3c3; +} + +.section-title .fa-arrow-left { + margin: -2px 8px 0px -8px; +} + +.unlock-screen #metamask-mascot-container { + margin-top: 24px; +} + +.unlock-screen h1 { + margin-top: -28px; + margin-bottom: 42px; +} + +.unlock-screen input[type=password] { + width: 260px; + /*height: 36px; + margin-bottom: 24px; + padding: 8px;*/ +} + +.sizing-input{ + font-size: 14px; + height: 30px; + padding-left: 5px; +} +.editable-label{ + display: flex; +} +/* Webkit */ +.unlock-screen input::-webkit-input-placeholder { + text-align: center; + font-size: 1.2em; +} +/* Firefox 18- */ +.unlock-screen input:-moz-placeholder { + text-align: center; + font-size: 1.2em; +} +/* Firefox 19+ */ +.unlock-screen input::-moz-placeholder { + text-align: center; + font-size: 1.2em; +} +/* IE */ +.unlock-screen input:-ms-input-placeholder { + text-align: center; + font-size: 1.2em; +} + +input.large-input, textarea.large-input { + /*margin-bottom: 24px;*/ + padding: 8px; +} + +input.large-input { + height: 36px; +} + +.letter-spacey { + letter-spacing: 0.1em; +} + + + +/* accounts */ + +.accounts-section { + margin: 0 0px; +} + +.accounts-section .horizontal-line { + margin: 0px 18px; +} + +.accounts-list-option { + height: 120px; +} + +.accounts-list-option .identicon-wrapper { + width: 100px; +} + +.unconftx-link { + margin-top: 24px; + cursor: pointer; +} + +.unconftx-link .fa-arrow-right { + margin: 0px -8px 0px 8px; +} + +/* identity panel */ + +.identity-panel { + font-weight: 500; +} + +.identity-panel .identicon-wrapper { + margin: 4px; + margin-top: 8px; + display: flex; + align-items: center; +} + +.identity-panel .identicon-wrapper span { + margin: 0 auto; +} + +.identity-panel .identity-data { + margin: 8px 8px 8px 18px; +} + +.identity-panel i { + margin-top: 32px; + margin-right: 6px; + color: #B9B9B9; +} + +.identity-panel .arrow-right { + padding-left: 18px; + width: 42px; + min-width: 18px; + height: 100%; +} + +.identity-copy.flex-column { + flex: 0.25 0 auto; + justify-content: center; +} + +/* accounts screen */ + +.identity-section { + +} + +.identity-section .identity-panel { + background: #E9E9E9; + border-bottom: 1px solid #B1B1B1; + cursor: pointer; +} + +.identity-section .identity-panel.selected { + background: white; + color: #F3C83E; +} + +.identity-section .identity-panel.selected .identicon { + border-color: orange; +} + +.identity-section .accounts-list-option:hover, +.identity-section .accounts-list-option.selected { + background:white; +} + +/* account detail screen */ + +.account-detail-section { + display: flex; + flex-wrap: wrap; +} +.name-label{ + +} + +.unapproved-tx-icon { + height: 16px; + width: 16px; + background: rgb(47, 174, 244); + border-color: #AEAEAE; + border-radius: 13px; +} + +.edit-text { + height: 100%; + visibility: hidden; +} +.editing-label { + display: flex; + justify-content: flex-start; + margin-left: 50px; + margin-bottom: 2px; + font-size: 11px; + text-rendering: geometricPrecision; + color: #F7861C; +} +.name-label:hover .edit-text { + visibility: visible; +} +/* tx confirm */ + +.unconftx-section input[type=password] { + height: 22px; + padding: 2px; + margin: 12px; + margin-bottom: 24px; + border-radius: 4px; + border: 2px solid #F3C83E; + background: #FAF6F0; +} + +/* Send Screen */ + +.send-screen { + +} + +.send-screen section { + margin: 8px 16px; +} + +.send-screen input { + width: 100%; + font-size: 12px; +} + +/* Ether Balance Widget */ + +.ether-balance-amount { + color: #F7861C; +} + +.ether-balance-label { + color: #ABA9AA; +} + +/* Info screen */ +.info-gray{ + font-family: 'Montserrat Regular'; + text-transform: uppercase; + color: #AEAEAE; +} + +.icon-size{ + width: 20px; +} + +.info{ + font-family: 'Montserrat Regular', Arial; + padding-bottom: 10px; + display: inline-block; + padding-left: 5px; +} + +/* buy eth warning screen */ +.custom-radios { + justify-content: space-around; + align-items: center; +} + + +.custom-radio-selected { + width: 17px; + height: 17px; + border: solid; + border-style: double; + border-radius: 15px; + border-width: 5px; + background: rgba(247, 134, 28, 1); + border-color: #F7F7F7; +} + +.custom-radio-inactive { + width: 14px; + height: 14px; + border: solid; + border-width: 1px; + border-radius: 24px; + border-color: #AEAEAE; +} + +.radio-titles { + color: rgba(247, 134, 28, 1); +} + +.radio-titles-subtext { + +} + +.selected-exchange { + +} + +.buy-radio { + +} + +.eth-warning{ + transition: opacity 400ms ease-in, transform 400ms ease-in; +} + +.buy-subview{ + transition: opacity 400ms ease-in, transform 400ms ease-in; +} + +.input-container:hover .edit-text{ + visibility: visible; +} + +.buy-inputs{ + font-family: 'Montserrat Light'; + font-size: 13px; + height: 20px; + background: transparent; + box-sizing: border-box; + border: solid; + border-color: transparent; + border-width: 0.5px; + border-radius: 2px; + +} +.input-container:hover .buy-inputs{ + box-sizing: inherit; + border: solid; + border-color: #F7861C; + border-width: 0.5px; + border-radius: 2px; +} + +.buy-inputs:focus{ + border: solid; + border-color: #F7861C; + border-width: 0.5px; + border-radius: 2px; +} + +.activeForm { + background: #F7F7F7; + border: none; + border-radius: 8px 8px 0px 0px; + width: 50%; + text-align: center; + padding-bottom: 4px; + +} + +.inactiveForm { + border: none; + border-radius: 8px 8px 0px 0px; + width: 50%; + text-align: center; + padding-bottom: 4px; +} + +.ex-coins { + font-family: 'Montserrat Regular'; + text-transform: uppercase; + text-align: center; + font-size: 33px; + width: 118px; + height: 42px; + padding: 1px; + color: #4D4D4D; +} + +.marketinfo{ + font-family: 'Montserrat light'; + color: #AEAEAE; + font-size: 15px; + line-height: 17px; +} + +#fromCoin::-webkit-calendar-picker-indicator { + display: none; +} + +#coinList { + width: 400px; + height: 500px; + overflow: scroll; +} + +.icon-control .fa-refresh{ + visibility: hidden; +} + +.icon-control:hover .fa-refresh{ + visibility: visible; +} + +.icon-control:hover .fa-chevron-right{ + visibility: hidden; +} + +.inactive { + color: #AEAEAE; +} + +.inactive button{ + background: #AEAEAE; + color: white; +} + +.ellip-address { + overflow: hidden; + text-overflow: ellipsis; + width: 5em; + font-size: 14px; + font-family: "Montserrat Light"; + margin-left: 5px; +} + +.qr-header { + font-size: 25px; + margin-top: 40px; +} + +.qr-message { + font-size: 12px; + color: #F7861C; +} + +div.message-container > div:first-child { + margin-top: 18px; + font-size: 15px; + color: #4D4D4D; +} + +.pop-hover:hover { + transform: scale(1.1); +} diff --git a/responsive-ui/app/css/lib.css b/responsive-ui/app/css/lib.css new file mode 100644 index 000000000..910a24ee2 --- /dev/null +++ b/responsive-ui/app/css/lib.css @@ -0,0 +1,268 @@ +/* color */ + +.color-orange { + color: #F7861C; +} + +.color-forest { + color: #0A5448; +} + +/* lib */ + +.full-width { + width: 100%; +} + +.full-height { + height: 100%; +} + +.flex-column { + display: flex; + flex-direction: column; +} + +.space-between { + justify-content: space-between; +} + +.space-around { + justify-content: space-around; +} + +.flex-column-bottom { + display: flex; + flex-direction: column-reverse; +} + +.flex-row { + display: flex; + flex-direction: row; +} + +.flex-space-between { + justify-content: space-between; +} + +.flex-space-around { + justify-content: space-around; +} + +.flex-right { + display: flex; + flex-direction: row; + justify-content: flex-end; +} + +.flex-left { + display: flex; + flex-direction: row; + justify-content: flex-start; +} + +.flex-fixed { + flex: none; +} + +.flex-basis-auto { + flex-basis: auto; +} + +.flex-grow { + flex: 1 1 auto; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.flex-center { + display: flex; + justify-content: center; + align-items: center; +} + +.flex-justify-center { + justify-content: center; +} + +.flex-align-center { + align-items: center; +} + +.flex-self-end { + align-self: flex-end; +} + +.flex-self-stretch { + align-self: stretch; +} + +.flex-vertical { + flex-direction: column; +} + +.z-bump { + z-index: 1; +} + +.select-none { + cursor: inherit; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.pointer { + cursor: pointer; +} +.cursor-pointer { + cursor: pointer; + transform-origin: center center; + transition: transform 50ms ease-in-out; +} +.cursor-pointer:hover { + transform: scale(1.1); +} +.cursor-pointer:active { + transform: scale(0.95); +} + +.cursor-disabled { + cursor: not-allowed; +} + +.margin-bottom-sml { + margin-bottom: 20px; +} + +.margin-bottom-med { + margin-bottom: 40px; +} + +.margin-right-left { + margin: 0 20px; +} + +.bold { + font-weight: bold; +} + +.text-transform-uppercase { + text-transform: uppercase; +} + +.font-small { + font-size: 12px; +} + +.font-medium { + font-size: 1.2em; +} + +hr.horizontal-line { + display: block; + height: 1px; + border: 0; + border-top: 1px solid #ccc; + margin: 1em 0; + padding: 0; +} + +.hover-white:hover { + background: white; +} + +.red-dot { + background: #E91550; + color: white; + border-radius: 10px; +} + +.diamond { + transform: rotate(45deg); + background: #038789; +} + +.hollow-diamond { + transform: rotate(45deg); + border: 3px solid #690496; +} + +.golden-square { + background: #EBB33F; +} + +.pending-dot { + background: red; + left: 14px; + top: 14px; + color: white; + border-radius: 10px; + height: 20px; + min-width: 20px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + z-index: 1; +} + +.keyring-label { + z-index: 1; + font-size: 11px; + background: rgba(255,0,0,0.8); + bottom: -47px; + color: white; + border-radius: 10px; + height: 20px; + min-width: 20px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding: 4px; +} + +.ether-balance { + display: flex; + align-items: center; +} + +.menu-icon { + display: inline-block; + height: 9px; + min-width: 9px; + margin: 13px; +} +.ether-icon { + background: rgb(0, 163, 68); + border-radius: 20px; +} +.testnet-icon { + background: #2465E1; +} + +.drop-menu-item { + display: flex; + align-items: center; +} + +.invisible { + visibility: hidden; +} + +.one-line-concat { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.critical-error { + text-align: center; + margin-top: 20px; + color: red; +} diff --git a/responsive-ui/app/css/reset.css b/responsive-ui/app/css/reset.css new file mode 100644 index 000000000..9ce89e8bc --- /dev/null +++ b/responsive-ui/app/css/reset.css @@ -0,0 +1,48 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +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; +} \ No newline at end of file diff --git a/responsive-ui/app/css/transitions.css b/responsive-ui/app/css/transitions.css new file mode 100644 index 000000000..393a944f9 --- /dev/null +++ b/responsive-ui/app/css/transitions.css @@ -0,0 +1,42 @@ +/* universal */ +.app-primary .main-enter { + position: absolute; + width: 100%; +} + +/* center position */ +.app-primary.from-right .main-enter-active, +.app-primary.from-left .main-enter-active { + overflow-x: hidden; + transform: translateX(0px); + transition: transform 300ms ease-in; +} + +/* exited positions */ +.app-primary.from-left .main-leave-active { + transform: translateX(360px); + transition: transform 300ms ease-in; +} +.app-primary.from-right .main-leave-active { + transform: translateX(-360px); + transition: transform 300ms ease-in; +} + +/* loader transitions */ +.loader-enter, .loader-leave-active { + opacity: 0.0; + transition: opacity 150 ease-in; +} +.loader-enter-active, .loader-leave { + opacity: 1.0; + transition: opacity 150 ease-in; +} + +/* entering positions */ +.app-primary.from-right .main-enter:not(.main-enter-active) { + transform: translateX(360px); +} +.app-primary.from-left .main-enter:not(.main-enter-active) { + transform: translateX(-360px); +} + diff --git a/responsive-ui/app/first-time/init-menu.js b/responsive-ui/app/first-time/init-menu.js new file mode 100644 index 000000000..cc7c51bd3 --- /dev/null +++ b/responsive-ui/app/first-time/init-menu.js @@ -0,0 +1,179 @@ +const inherits = require('util').inherits +const EventEmitter = require('events').EventEmitter +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const Mascot = require('../components/mascot') +const actions = require('../actions') +const Tooltip = require('../components/tooltip') +const getCaretCoordinates = require('textarea-caret') + +module.exports = connect(mapStateToProps)(InitializeMenuScreen) + +inherits(InitializeMenuScreen, Component) +function InitializeMenuScreen () { + Component.call(this) + this.animationEventEmitter = new EventEmitter() +} + +function mapStateToProps (state) { + return { + // state from plugin + currentView: state.appState.currentView, + warning: state.appState.warning, + } +} + +InitializeMenuScreen.prototype.render = function () { + var state = this.props + + switch (state.currentView.name) { + + default: + return this.renderMenu(state) + + } +} + +// InitializeMenuScreen.prototype.componentDidMount = function(){ +// document.getElementById('password-box').focus() +// } + +InitializeMenuScreen.prototype.renderMenu = function (state) { + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + h(Mascot, { + animationEventEmitter: this.animationEventEmitter, + }), + + h('h1', { + style: { + fontSize: '1.3em', + textTransform: 'uppercase', + color: '#7F8082', + marginBottom: 10, + }, + }, 'MetaMask'), + + + h('div', [ + h('h3', { + style: { + fontSize: '0.8em', + color: '#7F8082', + display: 'inline', + }, + }, 'Encrypt your new DEN'), + + h(Tooltip, { + title: 'Your DEN is your password-encrypted storage within MetaMask.', + }, [ + h('i.fa.fa-question-circle.pointer', { + style: { + fontSize: '18px', + position: 'relative', + color: 'rgb(247, 134, 28)', + top: '2px', + marginLeft: '4px', + }, + }), + ]), + ]), + + h('span.in-progress-notification', state.warning), + + // password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: 'New Password (min 8 chars)', + onInput: this.inputChanged.bind(this), + style: { + width: 260, + marginTop: 12, + }, + }), + + // confirm password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box-confirm', + placeholder: 'Confirm Password', + onKeyPress: this.createVaultOnEnter.bind(this), + onInput: this.inputChanged.bind(this), + style: { + width: 260, + marginTop: 16, + }, + }), + + + h('button.primary', { + onClick: this.createNewVaultAndKeychain.bind(this), + style: { + margin: 12, + }, + }, 'Create'), + + h('.flex-row.flex-center.flex-grow', [ + h('p.pointer', { + onClick: this.showRestoreVault.bind(this), + style: { + fontSize: '0.8em', + color: 'rgb(247, 134, 28)', + textDecoration: 'underline', + }, + }, 'Import Existing DEN'), + ]), + + ]) + ) +} + +InitializeMenuScreen.prototype.createVaultOnEnter = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.createNewVaultAndKeychain() + } +} + +InitializeMenuScreen.prototype.componentDidMount = function () { + document.getElementById('password-box').focus() +} + +InitializeMenuScreen.prototype.showRestoreVault = function () { + this.props.dispatch(actions.showRestoreVault()) +} + +InitializeMenuScreen.prototype.createNewVaultAndKeychain = function () { + var passwordBox = document.getElementById('password-box') + var password = passwordBox.value + var passwordConfirmBox = document.getElementById('password-box-confirm') + var passwordConfirm = passwordConfirmBox.value + + if (password.length < 8) { + this.warning = 'password not long enough' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + if (password !== passwordConfirm) { + this.warning = 'passwords don\'t match' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + + this.props.dispatch(actions.createNewVaultAndKeychain(password)) +} + +InitializeMenuScreen.prototype.inputChanged = function (event) { + // tell mascot to look at page action + var element = event.target + var boundingRect = element.getBoundingClientRect() + var coordinates = getCaretCoordinates(element, element.selectionEnd) + this.animationEventEmitter.emit('point', { + x: boundingRect.left + coordinates.left - element.scrollLeft, + y: boundingRect.top + coordinates.top - element.scrollTop, + }) +} diff --git a/responsive-ui/app/img/identicon-tardigrade.png b/responsive-ui/app/img/identicon-tardigrade.png new file mode 100644 index 000000000..1742a32b8 Binary files /dev/null and b/responsive-ui/app/img/identicon-tardigrade.png differ diff --git a/responsive-ui/app/img/identicon-walrus.png b/responsive-ui/app/img/identicon-walrus.png new file mode 100644 index 000000000..d58fae912 Binary files /dev/null and b/responsive-ui/app/img/identicon-walrus.png differ diff --git a/responsive-ui/app/info.js b/responsive-ui/app/info.js new file mode 100644 index 000000000..e8470de97 --- /dev/null +++ b/responsive-ui/app/info.js @@ -0,0 +1,154 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') + +module.exports = connect(mapStateToProps)(InfoScreen) + +function mapStateToProps (state) { + return {} +} + +inherits(InfoScreen, Component) +function InfoScreen () { + Component.call(this) +} + +InfoScreen.prototype.render = function () { + const state = this.props + const version = global.platform.getVersion() + + return ( + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + state.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Info'), + ]), + + // main view + h('.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-space-around', { + style: { + padding: '20px', + }, + }, [ + // current version number + + h('.info.info-gray', [ + h('div', 'Metamask'), + h('div', { + style: { + marginBottom: '10px', + }, + }, `Version: ${version}`), + ]), + + h('div', { + style: { + marginBottom: '5px', + }}, + [ + h('div', [ + h('a', { + href: 'https://metamask.io/privacy.html', + target: '_blank', + onClick (event) { this.navigateTo(event.target.href) }, + }, [ + h('div.info', 'Privacy Policy'), + ]), + ]), + h('div', [ + h('a', { + href: 'https://metamask.io/terms.html', + target: '_blank', + onClick (event) { this.navigateTo(event.target.href) }, + }, [ + h('div.info', 'Terms of Use'), + ]), + ]), + h('div', [ + h('a', { + href: 'https://metamask.io/attributions.html', + target: '_blank', + onClick (event) { this.navigateTo(event.target.href) }, + }, [ + h('div.info', 'Attributions'), + ]), + ]), + ] + ), + + h('hr', { + style: { + margin: '10px 0 ', + width: '7em', + }, + }), + + h('div', { + style: { + paddingLeft: '30px', + }}, + [ + h('div.fa.fa-github', [ + h('a.info', { + href: 'https://github.com/MetaMask/faq', + target: '_blank', + }, 'Need Help? Read our FAQ!'), + ]), + h('div', [ + h('a', { + href: 'https://metamask.io/', + target: '_blank', + }, [ + h('img.icon-size', { + src: 'images/icon-128.png', + style: { + // IE6-9 + filter: 'grayscale(100%)', + // Microsoft Edge and Firefox 35+ + WebkitFilter: 'grayscale(100%)', + }, + }), + h('div.info', 'Visit our web site'), + ]), + ]), + h('div.fa.fa-slack', [ + h('a.info', { + href: 'http://slack.metamask.io', + target: '_blank', + }, 'Join the conversation on Slack'), + ]), + + h('div.fa.fa-twitter', [ + h('a.info', { + href: 'https://twitter.com/metamask_io', + target: '_blank', + }, 'Follow us on Twitter'), + ]), + + h('div.fa.fa-envelope', [ + h('a.info', { + target: '_blank', + style: { width: '85vw' }, + href: 'mailto:help@metamask.io?subject=Feedback', + }, 'Email us!'), + ]), + ]), + ]), + ]), + ]) + ) +} + +InfoScreen.prototype.navigateTo = function (url) { + global.platform.openWindow({ url }) +} + diff --git a/responsive-ui/app/keychains/hd/create-vault-complete.js b/responsive-ui/app/keychains/hd/create-vault-complete.js new file mode 100644 index 000000000..c32751fff --- /dev/null +++ b/responsive-ui/app/keychains/hd/create-vault-complete.js @@ -0,0 +1,76 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('../../actions') + +module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen) + +inherits(CreateVaultCompleteScreen, Component) +function CreateVaultCompleteScreen () { + Component.call(this) +} + +function mapStateToProps (state) { + return { + seed: state.appState.currentView.seedWords, + cachedSeed: state.metamask.seedWords, + } +} + +CreateVaultCompleteScreen.prototype.render = function () { + var state = this.props + var seed = state.seed || state.cachedSeed || '' + + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + // // subtitle and nav + // h('.section-title.flex-row.flex-center', [ + // h('h2.page-subtitle', 'Vault Created'), + // ]), + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginTop: 36, + marginBottom: 8, + width: '100%', + fontSize: '20px', + padding: 6, + }, + }, [ + 'Vault Created', + ]), + + h('div', { + style: { + fontSize: '1em', + marginTop: '10px', + textAlign: 'center', + }, + }, [ + h('span.error', 'These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret.'), + ]), + + h('textarea.twelve-word-phrase', { + readOnly: true, + value: seed, + }), + + h('button.primary', { + onClick: () => this.confirmSeedWords(), + style: { + margin: '24px', + fontSize: '0.9em', + }, + }, 'I\'ve copied it somewhere safe'), + ]) + ) +} + +CreateVaultCompleteScreen.prototype.confirmSeedWords = function () { + this.props.dispatch(actions.confirmSeedWords()) +} diff --git a/responsive-ui/app/keychains/hd/recover-seed/confirmation.js b/responsive-ui/app/keychains/hd/recover-seed/confirmation.js new file mode 100644 index 000000000..4ccbec9fc --- /dev/null +++ b/responsive-ui/app/keychains/hd/recover-seed/confirmation.js @@ -0,0 +1,118 @@ +const inherits = require('util').inherits + +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('../../../actions') + +module.exports = connect(mapStateToProps)(RevealSeedConfirmation) + +inherits(RevealSeedConfirmation, Component) +function RevealSeedConfirmation () { + Component.call(this) +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +RevealSeedConfirmation.prototype.render = function () { + const props = this.props + + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginBottom: 24, + width: '100%', + fontSize: '20px', + padding: 6, + }, + }, [ + 'Reveal Seed Words', + ]), + + h('.div', { + style: { + display: 'flex', + flexDirection: 'column', + padding: '20px', + justifyContent: 'center', + }, + }, [ + + h('h4', 'Do not recover your seed words in a public place! These words can be used to steal all your accounts.'), + + // confirmation + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: 'Enter your password to confirm', + onKeyPress: this.checkConfirmation.bind(this), + style: { + width: 260, + marginTop: '12px', + }, + }), + + h('.flex-row.flex-space-between', { + style: { + marginTop: 30, + width: '50%', + }, + }, [ + // cancel + h('button.primary', { + onClick: this.goHome.bind(this), + }, 'CANCEL'), + + // submit + h('button.primary', { + onClick: this.revealSeedWords.bind(this), + }, 'OK'), + + ]), + + (props.warning) && ( + h('span.error', { + style: { + margin: '20px', + }, + }, props.warning.split('-')) + ), + + props.inProgress && ( + h('span.in-progress-notification', 'Generating Seed...') + ), + ]), + ]) + ) +} + +RevealSeedConfirmation.prototype.componentDidMount = function () { + document.getElementById('password-box').focus() +} + +RevealSeedConfirmation.prototype.goHome = function () { + this.props.dispatch(actions.showConfigPage(false)) +} + +// create vault + +RevealSeedConfirmation.prototype.checkConfirmation = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.revealSeedWords() + } +} + +RevealSeedConfirmation.prototype.revealSeedWords = function () { + var password = document.getElementById('password-box').value + this.props.dispatch(actions.requestRevealSeed(password)) +} diff --git a/responsive-ui/app/keychains/hd/restore-vault.js b/responsive-ui/app/keychains/hd/restore-vault.js new file mode 100644 index 000000000..06e51d9b3 --- /dev/null +++ b/responsive-ui/app/keychains/hd/restore-vault.js @@ -0,0 +1,152 @@ +const inherits = require('util').inherits +const PersistentForm = require('../../../lib/persistent-form') +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('../../actions') + +module.exports = connect(mapStateToProps)(RestoreVaultScreen) + +inherits(RestoreVaultScreen, PersistentForm) +function RestoreVaultScreen () { + PersistentForm.call(this) +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + forgottenPassword: state.appState.forgottenPassword, + } +} + +RestoreVaultScreen.prototype.render = function () { + var state = this.props + this.persistentFormParentId = 'restore-vault-form' + + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginBottom: 24, + width: '100%', + fontSize: '20px', + padding: 6, + }, + }, [ + 'Restore Vault', + ]), + + // wallet seed entry + h('h3', 'Wallet Seed'), + h('textarea.twelve-word-phrase.letter-spacey', { + dataset: { + persistentFormId: 'wallet-seed', + }, + placeholder: 'Enter your secret twelve word phrase here to restore your vault.', + }), + + // password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: 'New Password (min 8 chars)', + dataset: { + persistentFormId: 'password', + }, + style: { + width: 260, + marginTop: 12, + }, + }), + + // confirm password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box-confirm', + placeholder: 'Confirm Password', + onKeyPress: this.createOnEnter.bind(this), + dataset: { + persistentFormId: 'password-confirmation', + }, + style: { + width: 260, + marginTop: 16, + }, + }), + + (state.warning) && ( + h('span.error.in-progress-notification', state.warning) + ), + + // submit + + h('.flex-row.flex-space-between', { + style: { + marginTop: 30, + width: '50%', + }, + }, [ + + // cancel + h('button.primary', { + onClick: this.showInitializeMenu.bind(this), + }, 'CANCEL'), + + // submit + h('button.primary', { + onClick: this.createNewVaultAndRestore.bind(this), + }, 'OK'), + + ]), + ]) + + ) +} + +RestoreVaultScreen.prototype.showInitializeMenu = function () { + if (this.props.forgottenPassword) { + this.props.dispatch(actions.backToUnlockView()) + } else { + this.props.dispatch(actions.showInitializeMenu()) + } +} + +RestoreVaultScreen.prototype.createOnEnter = function (event) { + if (event.key === 'Enter') { + this.createNewVaultAndRestore() + } +} + +RestoreVaultScreen.prototype.createNewVaultAndRestore = function () { + // check password + var passwordBox = document.getElementById('password-box') + var password = passwordBox.value + var passwordConfirmBox = document.getElementById('password-box-confirm') + var passwordConfirm = passwordConfirmBox.value + if (password.length < 8) { + this.warning = 'Password not long enough' + + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + if (password !== passwordConfirm) { + this.warning = 'Passwords don\'t match' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + // check seed + var seedBox = document.querySelector('textarea.twelve-word-phrase') + var seed = seedBox.value.trim() + if (seed.split(' ').length !== 12) { + this.warning = 'seed phrases are 12 words long' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + // submit + this.warning = null + this.props.dispatch(actions.displayWarning(this.warning)) + this.props.dispatch(actions.createNewVaultAndRestore(password, seed)) +} diff --git a/responsive-ui/app/new-keychain.js b/responsive-ui/app/new-keychain.js new file mode 100644 index 000000000..cc9633166 --- /dev/null +++ b/responsive-ui/app/new-keychain.js @@ -0,0 +1,29 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect + +module.exports = connect(mapStateToProps)(NewKeychain) + +function mapStateToProps (state) { + return {} +} + +inherits(NewKeychain, Component) +function NewKeychain () { + Component.call(this) +} + +NewKeychain.prototype.render = function () { + // const props = this.props + + return ( + h('div', { + style: { + background: 'blue', + }, + }, [ + h('h1', `Here's a list!!!!`), + ]) + ) +} diff --git a/responsive-ui/app/reducers.js b/responsive-ui/app/reducers.js new file mode 100644 index 000000000..11efca529 --- /dev/null +++ b/responsive-ui/app/reducers.js @@ -0,0 +1,52 @@ +const extend = require('xtend') + +// +// Sub-Reducers take in the complete state and return their sub-state +// +const reduceIdentities = require('./reducers/identities') +const reduceMetamask = require('./reducers/metamask') +const reduceApp = require('./reducers/app') + +window.METAMASK_CACHED_LOG_STATE = null + +module.exports = rootReducer + +function rootReducer (state, action) { + // clone + state = extend(state) + + if (action.type === 'GLOBAL_FORCE_UPDATE') { + return action.value + } + + // + // Identities + // + + state.identities = reduceIdentities(state, action) + + // + // MetaMask + // + + state.metamask = reduceMetamask(state, action) + + // + // AppState + // + + state.appState = reduceApp(state, action) + + window.METAMASK_CACHED_LOG_STATE = state + return state +} + +window.logState = function () { + var stateString = JSON.stringify(window.METAMASK_CACHED_LOG_STATE, removeSeedWords, 2) + console.log(stateString) + return stateString +} + +function removeSeedWords (key, value) { + return key === 'seedWords' ? undefined : value +} diff --git a/responsive-ui/app/reducers/app.js b/responsive-ui/app/reducers/app.js new file mode 100644 index 000000000..2fcc9bfe0 --- /dev/null +++ b/responsive-ui/app/reducers/app.js @@ -0,0 +1,585 @@ +const extend = require('xtend') +const actions = require('../actions') +const txHelper = require('../../lib/tx-helper') + +module.exports = reduceApp + + +function reduceApp (state, action) { + log.debug('App Reducer got ' + action.type) + // clone and defaults + const selectedAddress = state.metamask.selectedAddress + const hasUnconfActions = checkUnconfActions(state) + let name = 'accounts' + if (selectedAddress) { + name = 'accountDetail' + } + if (hasUnconfActions) { + log.debug('pending txs detected, defaulting to conf-tx view.') + name = 'confTx' + } + + var defaultView = { + name, + detailView: null, + context: selectedAddress, + } + + // confirm seed words + var seedWords = state.metamask.seedWords + var seedConfView = { + name: 'createVaultComplete', + seedWords, + } + + // default state + var appState = extend({ + shouldClose: false, + menuOpen: false, + currentView: seedWords ? seedConfView : defaultView, + accountDetail: { + subview: 'transactions', + }, + transForward: true, // Used to render transition direction + isLoading: false, // Used to display loading indicator + warning: null, // Used to display error text + }, state.appState) + + switch (action.type) { + + // transition methods + + case actions.TRANSITION_FORWARD: + return extend(appState, { + transForward: true, + }) + + case actions.TRANSITION_BACKWARD: + return extend(appState, { + transForward: false, + }) + + // intialize + + case actions.SHOW_CREATE_VAULT: + return extend(appState, { + currentView: { + name: 'createVault', + }, + transForward: true, + warning: null, + }) + + case actions.SHOW_RESTORE_VAULT: + return extend(appState, { + currentView: { + name: 'restoreVault', + }, + transForward: true, + forgottenPassword: true, + }) + + case actions.FORGOT_PASSWORD: + return extend(appState, { + currentView: { + name: 'restoreVault', + }, + transForward: false, + forgottenPassword: true, + }) + + case actions.SHOW_INIT_MENU: + return extend(appState, { + currentView: defaultView, + transForward: false, + }) + + case actions.SHOW_CONFIG_PAGE: + return extend(appState, { + currentView: { + name: 'config', + context: appState.currentView.context, + }, + transForward: action.value, + }) + + case actions.SHOW_ADD_TOKEN_PAGE: + return extend(appState, { + currentView: { + name: 'add-token', + context: appState.currentView.context, + }, + transForward: action.value, + }) + + case actions.SHOW_IMPORT_PAGE: + + return extend(appState, { + currentView: { + name: 'import-menu', + }, + transForward: true, + }) + + case actions.SHOW_INFO_PAGE: + return extend(appState, { + currentView: { + name: 'info', + context: appState.currentView.context, + }, + transForward: true, + }) + + case actions.CREATE_NEW_VAULT_IN_PROGRESS: + return extend(appState, { + currentView: { + name: 'createVault', + inProgress: true, + }, + transForward: true, + isLoading: true, + }) + + case actions.SHOW_NEW_VAULT_SEED: + return extend(appState, { + currentView: { + name: 'createVaultComplete', + seedWords: action.value, + }, + transForward: true, + isLoading: false, + }) + + case actions.NEW_ACCOUNT_SCREEN: + return extend(appState, { + currentView: { + name: 'new-account', + context: appState.currentView.context, + }, + transForward: true, + }) + + case actions.SHOW_SEND_PAGE: + return extend(appState, { + currentView: { + name: 'sendTransaction', + context: appState.currentView.context, + }, + transForward: true, + warning: null, + }) + + case actions.SHOW_NEW_KEYCHAIN: + return extend(appState, { + currentView: { + name: 'newKeychain', + context: appState.currentView.context, + }, + transForward: true, + }) + + // unlock + + case actions.UNLOCK_METAMASK: + return extend(appState, { + forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null, + detailView: {}, + transForward: true, + isLoading: false, + warning: null, + }) + + case actions.LOCK_METAMASK: + return extend(appState, { + currentView: defaultView, + transForward: false, + warning: null, + }) + + case actions.BACK_TO_INIT_MENU: + return extend(appState, { + warning: null, + transForward: false, + forgottenPassword: true, + currentView: { + name: 'InitMenu', + }, + }) + + case actions.BACK_TO_UNLOCK_VIEW: + return extend(appState, { + warning: null, + transForward: true, + forgottenPassword: false, + currentView: { + name: 'UnlockScreen', + }, + }) + // reveal seed words + + case actions.REVEAL_SEED_CONFIRMATION: + return extend(appState, { + currentView: { + name: 'reveal-seed-conf', + }, + transForward: true, + warning: null, + }) + + // accounts + + case actions.SET_SELECTED_ACCOUNT: + return extend(appState, { + activeAddress: action.value, + }) + + case actions.GO_HOME: + return extend(appState, { + currentView: extend(appState.currentView, { + name: 'accountDetail', + }), + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + transForward: false, + warning: null, + }) + + case actions.SHOW_ACCOUNT_DETAIL: + return extend(appState, { + forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null, + currentView: { + name: 'accountDetail', + context: action.value, + }, + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + transForward: false, + }) + + case actions.BACK_TO_ACCOUNT_DETAIL: + return extend(appState, { + currentView: { + name: 'accountDetail', + context: action.value, + }, + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + transForward: false, + }) + + case actions.SHOW_ACCOUNTS_PAGE: + return extend(appState, { + currentView: { + name: seedWords ? 'createVaultComplete' : 'accounts', + seedWords, + }, + transForward: true, + isLoading: false, + warning: null, + scrollToBottom: false, + forgottenPassword: false, + }) + + case actions.SHOW_NOTICE: + return extend(appState, { + transForward: true, + isLoading: false, + }) + + case actions.REVEAL_ACCOUNT: + return extend(appState, { + scrollToBottom: true, + }) + + case actions.SHOW_CONF_TX_PAGE: + return extend(appState, { + currentView: { + name: 'confTx', + context: 0, + }, + transForward: action.transForward, + warning: null, + isLoading: false, + }) + + case actions.SHOW_CONF_MSG_PAGE: + return extend(appState, { + currentView: { + name: hasUnconfActions ? 'confTx' : 'account-detail', + context: 0, + }, + transForward: true, + warning: null, + isLoading: false, + }) + + case actions.COMPLETED_TX: + log.debug('reducing COMPLETED_TX for tx ' + action.value) + const otherUnconfActions = getUnconfActionList(state) + .filter(tx => tx.id !== action.value) + const hasOtherUnconfActions = otherUnconfActions.length > 0 + + if (hasOtherUnconfActions) { + log.debug('reducer detected txs - rendering confTx view') + return extend(appState, { + transForward: false, + currentView: { + name: 'confTx', + context: 0, + }, + warning: null, + }) + } else { + log.debug('attempting to close popup') + return extend(appState, { + // indicate notification should close + shouldClose: true, + transForward: false, + warning: null, + currentView: { + name: 'accountDetail', + context: state.metamask.selectedAddress, + }, + accountDetail: { + subview: 'transactions', + }, + }) + } + + case actions.NEXT_TX: + return extend(appState, { + transForward: true, + currentView: { + name: 'confTx', + context: ++appState.currentView.context, + warning: null, + }, + }) + + case actions.VIEW_PENDING_TX: + const context = indexForPending(state, action.value) + return extend(appState, { + transForward: true, + currentView: { + name: 'confTx', + context, + warning: null, + }, + }) + + case actions.PREVIOUS_TX: + return extend(appState, { + transForward: false, + currentView: { + name: 'confTx', + context: --appState.currentView.context, + warning: null, + }, + }) + + case actions.TRANSACTION_ERROR: + return extend(appState, { + currentView: { + name: 'confTx', + errorMessage: 'There was a problem submitting this transaction.', + }, + }) + + case actions.UNLOCK_FAILED: + return extend(appState, { + warning: action.value || 'Incorrect password. Try again.', + }) + + case actions.SHOW_LOADING: + return extend(appState, { + isLoading: true, + loadingMessage: action.value, + }) + + case actions.HIDE_LOADING: + return extend(appState, { + isLoading: false, + }) + + case actions.SHOW_SUB_LOADING_INDICATION: + return extend(appState, { + isSubLoading: true, + }) + + case actions.HIDE_SUB_LOADING_INDICATION: + return extend(appState, { + isSubLoading: false, + }) + case actions.CLEAR_SEED_WORD_CACHE: + return extend(appState, { + transForward: true, + currentView: {}, + isLoading: false, + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + }) + + case actions.DISPLAY_WARNING: + return extend(appState, { + warning: action.value, + isLoading: false, + }) + + case actions.HIDE_WARNING: + return extend(appState, { + warning: undefined, + }) + + case actions.REQUEST_ACCOUNT_EXPORT: + return extend(appState, { + transForward: true, + currentView: { + name: 'accountDetail', + context: appState.currentView.context, + }, + accountDetail: { + subview: 'export', + accountExport: 'requested', + }, + }) + + case actions.EXPORT_ACCOUNT: + return extend(appState, { + accountDetail: { + subview: 'export', + accountExport: 'completed', + }, + }) + + case actions.SHOW_PRIVATE_KEY: + return extend(appState, { + accountDetail: { + subview: 'export', + accountExport: 'completed', + privateKey: action.value, + }, + }) + + case actions.BUY_ETH_VIEW: + return extend(appState, { + transForward: true, + currentView: { + name: 'buyEth', + context: appState.currentView.name, + }, + identity: state.metamask.identities[action.value], + buyView: { + subview: 'Coinbase', + amount: '15.00', + buyAddress: action.value, + formView: { + coinbase: true, + shapeshift: false, + }, + }, + }) + + case actions.COINBASE_SUBVIEW: + return extend(appState, { + buyView: { + subview: 'Coinbase', + formView: { + coinbase: true, + shapeshift: false, + }, + buyAddress: appState.buyView.buyAddress, + amount: appState.buyView.amount, + }, + }) + + case actions.SHAPESHIFT_SUBVIEW: + return extend(appState, { + buyView: { + subview: 'ShapeShift', + formView: { + coinbase: false, + shapeshift: true, + marketinfo: action.value.marketinfo, + coinOptions: action.value.coinOptions, + }, + buyAddress: appState.buyView.buyAddress, + amount: appState.buyView.amount, + }, + }) + + case actions.PAIR_UPDATE: + return extend(appState, { + buyView: { + subview: 'ShapeShift', + formView: { + coinbase: false, + shapeshift: true, + marketinfo: action.value.marketinfo, + coinOptions: appState.buyView.formView.coinOptions, + }, + buyAddress: appState.buyView.buyAddress, + amount: appState.buyView.amount, + warning: null, + }, + }) + + case actions.SHOW_QR: + return extend(appState, { + qrRequested: true, + transForward: true, + + Qr: { + message: action.value.message, + data: action.value.data, + }, + }) + + case actions.SHOW_QR_VIEW: + return extend(appState, { + currentView: { + name: 'qr', + context: appState.currentView.context, + }, + transForward: true, + Qr: { + message: action.value.message, + data: action.value.data, + }, + }) + default: + return appState + } +} + +function checkUnconfActions (state) { + const unconfActionList = getUnconfActionList(state) + const hasUnconfActions = unconfActionList.length > 0 + return hasUnconfActions +} + +function getUnconfActionList (state) { + const { unapprovedTxs, unapprovedMsgs, + unapprovedPersonalMsgs, network } = state.metamask + + const unconfActionList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) + return unconfActionList +} + +function indexForPending (state, txId) { + const unconfTxList = getUnconfActionList(state) + const match = unconfTxList.find((tx) => tx.id === txId) + const index = unconfTxList.indexOf(match) + return index +} diff --git a/responsive-ui/app/reducers/identities.js b/responsive-ui/app/reducers/identities.js new file mode 100644 index 000000000..341a404e7 --- /dev/null +++ b/responsive-ui/app/reducers/identities.js @@ -0,0 +1,15 @@ +const extend = require('xtend') + +module.exports = reduceIdentities + +function reduceIdentities (state, action) { + // clone + defaults + var idState = extend({ + + }, state.identities) + + switch (action.type) { + default: + return idState + } +} diff --git a/responsive-ui/app/reducers/metamask.js b/responsive-ui/app/reducers/metamask.js new file mode 100644 index 000000000..e0c416c2d --- /dev/null +++ b/responsive-ui/app/reducers/metamask.js @@ -0,0 +1,137 @@ +const extend = require('xtend') +const actions = require('../actions') + +module.exports = reduceMetamask + +function reduceMetamask (state, action) { + let newState + + // clone + defaults + var metamaskState = extend({ + isInitialized: false, + isUnlocked: false, + rpcTarget: 'https://rawtestrpc.metamask.io/', + identities: {}, + unapprovedTxs: {}, + noActiveNotices: true, + lastUnreadNotice: undefined, + frequentRpcList: [], + addressBook: [], + }, state.metamask) + + switch (action.type) { + + case actions.SHOW_ACCOUNTS_PAGE: + newState = extend(metamaskState) + delete newState.seedWords + return newState + + case actions.SHOW_NOTICE: + return extend(metamaskState, { + noActiveNotices: false, + lastUnreadNotice: action.value, + }) + + case actions.CLEAR_NOTICES: + return extend(metamaskState, { + noActiveNotices: true, + }) + + case actions.UPDATE_METAMASK_STATE: + return extend(metamaskState, action.value) + + case actions.UNLOCK_METAMASK: + return extend(metamaskState, { + isUnlocked: true, + isInitialized: true, + selectedAddress: action.value, + }) + + case actions.LOCK_METAMASK: + return extend(metamaskState, { + isUnlocked: false, + }) + + case actions.SET_RPC_LIST: + return extend(metamaskState, { + frequentRpcList: action.value, + }) + + case actions.SET_RPC_TARGET: + return extend(metamaskState, { + provider: { + type: 'rpc', + rpcTarget: action.value, + }, + }) + + case actions.SET_PROVIDER_TYPE: + return extend(metamaskState, { + provider: { + type: action.value, + }, + }) + + case actions.COMPLETED_TX: + var stringId = String(action.id) + newState = extend(metamaskState, { + unapprovedTxs: {}, + unapprovedMsgs: {}, + }) + for (const id in metamaskState.unapprovedTxs) { + if (id !== stringId) { + newState.unapprovedTxs[id] = metamaskState.unapprovedTxs[id] + } + } + for (const id in metamaskState.unapprovedMsgs) { + if (id !== stringId) { + newState.unapprovedMsgs[id] = metamaskState.unapprovedMsgs[id] + } + } + return newState + + case actions.SHOW_NEW_VAULT_SEED: + return extend(metamaskState, { + isUnlocked: true, + isInitialized: false, + seedWords: action.value, + }) + + case actions.CLEAR_SEED_WORD_CACHE: + newState = extend(metamaskState, { + isUnlocked: true, + isInitialized: true, + selectedAddress: action.value, + }) + delete newState.seedWords + return newState + + case actions.SHOW_ACCOUNT_DETAIL: + newState = extend(metamaskState, { + isUnlocked: true, + isInitialized: true, + selectedAddress: action.value, + }) + delete newState.seedWords + return newState + + case actions.SAVE_ACCOUNT_LABEL: + const account = action.value.account + const name = action.value.label + var id = {} + id[account] = extend(metamaskState.identities[account], { name }) + var identities = extend(metamaskState.identities, id) + return extend(metamaskState, { identities }) + + case actions.SET_CURRENT_FIAT: + return extend(metamaskState, { + currentCurrency: action.value.currentCurrency, + conversionRate: action.value.conversionRate, + conversionDate: action.value.conversionDate, + }) + + default: + return metamaskState + + } +} diff --git a/responsive-ui/app/root.js b/responsive-ui/app/root.js new file mode 100644 index 000000000..9e7314b20 --- /dev/null +++ b/responsive-ui/app/root.js @@ -0,0 +1,22 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const Provider = require('react-redux').Provider +const h = require('react-hyperscript') +const App = require('./app') + +module.exports = Root + +inherits(Root, Component) +function Root () { Component.call(this) } + +Root.prototype.render = function () { + return ( + + h(Provider, { + store: this.props.store, + }, [ + h(App), + ]) + + ) +} diff --git a/responsive-ui/app/send.js b/responsive-ui/app/send.js new file mode 100644 index 000000000..a21a219eb --- /dev/null +++ b/responsive-ui/app/send.js @@ -0,0 +1,288 @@ +const inherits = require('util').inherits +const PersistentForm = require('../lib/persistent-form') +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const Identicon = require('./components/identicon') +const actions = require('./actions') +const util = require('./util') +const numericBalance = require('./util').numericBalance +const addressSummary = require('./util').addressSummary +const isHex = require('./util').isHex +const EthBalance = require('./components/eth-balance') +const EnsInput = require('./components/ens-input') +const ethUtil = require('ethereumjs-util') +module.exports = connect(mapStateToProps)(SendTransactionScreen) + +function mapStateToProps (state) { + var result = { + address: state.metamask.selectedAddress, + accounts: state.metamask.accounts, + identities: state.metamask.identities, + warning: state.appState.warning, + network: state.metamask.network, + addressBook: state.metamask.addressBook, + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + } + + result.error = result.warning && result.warning.split('.')[0] + + result.account = result.accounts[result.address] + result.identity = result.identities[result.address] + result.balance = result.account ? numericBalance(result.account.balance) : null + + return result +} + +inherits(SendTransactionScreen, PersistentForm) +function SendTransactionScreen () { + PersistentForm.call(this) +} + +SendTransactionScreen.prototype.render = function () { + this.persistentFormParentId = 'send-tx-form' + + const props = this.props + const { + address, + account, + identity, + network, + identities, + addressBook, + conversionRate, + currentCurrency, + } = props + + return ( + + h('.send-screen.flex-column.flex-grow', [ + + // + // Sender Profile + // + + h('.account-data-subsection.flex-row.flex-grow', { + style: { + margin: '0 20px', + }, + }, [ + + // header - identicon + nav + h('.flex-row.flex-space-between', { + style: { + marginTop: '15px', + }, + }, [ + // back button + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { + onClick: this.back.bind(this), + }), + + // large identicon + h('.identicon-wrapper.flex-column.flex-center.select-none', [ + h(Identicon, { + diameter: 62, + address: address, + }), + ]), + + // invisible place holder + h('i.fa.fa-users.fa-lg.invisible', { + style: { + marginTop: '28px', + }, + }), + + ]), + + // account label + + h('.flex-column', { + style: { + marginTop: '10px', + alignItems: 'flex-start', + }, + }, [ + h('h2.font-medium.color-forest.flex-center', { + style: { + paddingTop: '8px', + marginBottom: '8px', + }, + }, identity && identity.name), + + // address and getter actions + h('.flex-row.flex-center', { + style: { + marginBottom: '8px', + }, + }, [ + + h('div', { + style: { + lineHeight: '16px', + }, + }, addressSummary(address)), + + ]), + + // balance + h('.flex-row.flex-center', [ + + h(EthBalance, { + value: account && account.balance, + conversionRate, + currentCurrency, + }), + + ]), + ]), + ]), + + // + // Required Fields + // + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginTop: '15px', + marginBottom: '16px', + }, + }, [ + 'Send Transaction', + ]), + + // error message + props.error && h('span.error.flex-center', props.error), + + // 'to' field + h('section.flex-row.flex-center', [ + h(EnsInput, { + name: 'address', + placeholder: 'Recipient Address', + onChange: this.recipientDidChange.bind(this), + network, + identities, + addressBook, + }), + ]), + + // 'amount' and send button + h('section.flex-row.flex-center', [ + + h('input.large-input', { + name: 'amount', + placeholder: 'Amount', + type: 'number', + style: { + marginRight: '6px', + }, + dataset: { + persistentFormId: 'tx-amount', + }, + }), + + h('button.primary', { + onClick: this.onSubmit.bind(this), + style: { + textTransform: 'uppercase', + }, + }, 'Next'), + + ]), + + // + // Optional Fields + // + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginTop: '16px', + marginBottom: '16px', + }, + }, [ + 'Transaction Data (optional)', + ]), + + // 'data' field + h('section.flex-column.flex-center', [ + h('input.large-input', { + name: 'txData', + placeholder: '0x01234', + style: { + width: '100%', + resize: 'none', + }, + dataset: { + persistentFormId: 'tx-data', + }, + }), + ]), + ]) + ) +} + +SendTransactionScreen.prototype.navigateToAccounts = function (event) { + event.stopPropagation() + this.props.dispatch(actions.showAccountsPage()) +} + +SendTransactionScreen.prototype.back = function () { + var address = this.props.address + this.props.dispatch(actions.backToAccountDetail(address)) +} + +SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickname) { + this.setState({ + recipient: recipient, + nickname: nickname, + }) +} + +SendTransactionScreen.prototype.onSubmit = function () { + const state = this.state || {} + const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '') + const nickname = state.nickname || ' ' + const input = document.querySelector('input[name="amount"]').value + const value = util.normalizeEthStringToWei(input) + const txData = document.querySelector('input[name="txData"]').value + const balance = this.props.balance + let message + + if (value.gt(balance)) { + message = 'Insufficient funds.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if (input < 0) { + message = 'Can not send negative amounts of ETH.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) { + message = 'Recipient address is invalid.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if (!isHex(ethUtil.stripHexPrefix(txData)) && txData) { + message = 'Transaction data must be hex string.' + return this.props.dispatch(actions.displayWarning(message)) + } + + this.props.dispatch(actions.hideWarning()) + + this.props.dispatch(actions.addToAddressBook(recipient, nickname)) + + var txParams = { + from: this.props.address, + value: '0x' + value.toString(16), + } + + if (recipient) txParams.to = ethUtil.addHexPrefix(recipient) + if (txData) txParams.data = txData + + this.props.dispatch(actions.signTx(txParams)) +} diff --git a/responsive-ui/app/settings.js b/responsive-ui/app/settings.js new file mode 100644 index 000000000..454cc95e0 --- /dev/null +++ b/responsive-ui/app/settings.js @@ -0,0 +1,59 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') + +module.exports = connect(mapStateToProps)(AppSettingsPage) + +function mapStateToProps (state) { + return {} +} + +inherits(AppSettingsPage, Component) +function AppSettingsPage () { + Component.call(this) +} + +AppSettingsPage.prototype.render = function () { + return ( + + h('.account-detail-section.flex-column.flex-grow', [ + + // subtitle and nav + h('.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: this.navigateToAccounts.bind(this), + }), + h('h2.page-subtitle', 'Settings'), + ]), + + h('label', { + htmlFor: 'settings-rpc-endpoint', + }, 'RPC Endpoint:'), + h('input', { + type: 'url', + id: 'settings-rpc-endpoint', + onKeyPress: this.onKeyPress.bind(this), + }), + + ]) + + ) +} + +AppSettingsPage.prototype.componentDidMount = function () { + document.querySelector('input').focus() +} + +AppSettingsPage.prototype.onKeyPress = function (event) { + // get submit event + if (event.key === 'Enter') { + // this.submitPassword(event) + } +} + +AppSettingsPage.prototype.navigateToAccounts = function (event) { + event.stopPropagation() + this.props.dispatch(actions.showAccountsPage()) +} diff --git a/responsive-ui/app/store.js b/responsive-ui/app/store.js new file mode 100644 index 000000000..ba9e58b49 --- /dev/null +++ b/responsive-ui/app/store.js @@ -0,0 +1,21 @@ +const createStore = require('redux').createStore +const applyMiddleware = require('redux').applyMiddleware +const thunkMiddleware = require('redux-thunk') +const rootReducer = require('./reducers') +const createLogger = require('redux-logger') + +global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' + +module.exports = configureStore + +const loggerMiddleware = createLogger({ + predicate: () => global.METAMASK_DEBUG, +}) + +const middlewares = [thunkMiddleware, loggerMiddleware] + +const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore) + +function configureStore (initialState) { + return createStoreWithMiddleware(rootReducer, initialState) +} diff --git a/responsive-ui/app/template.js b/responsive-ui/app/template.js new file mode 100644 index 000000000..d15b30fd2 --- /dev/null +++ b/responsive-ui/app/template.js @@ -0,0 +1,30 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect + +module.exports = connect(mapStateToProps)(COMPONENTNAME) + +function mapStateToProps (state) { + return {} +} + +inherits(COMPONENTNAME, Component) +function COMPONENTNAME () { + Component.call(this) +} + +COMPONENTNAME.prototype.render = function () { + const props = this.props + + return ( + h('div', { + style: { + background: 'blue', + }, + }, [ + `Hello, ${props.sender}`, + ]) + ) +} + diff --git a/responsive-ui/app/unlock.js b/responsive-ui/app/unlock.js new file mode 100644 index 000000000..1aee3c5d0 --- /dev/null +++ b/responsive-ui/app/unlock.js @@ -0,0 +1,118 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') +const getCaretCoordinates = require('textarea-caret') +const EventEmitter = require('events').EventEmitter + +const Mascot = require('./components/mascot') + +module.exports = connect(mapStateToProps)(UnlockScreen) + +inherits(UnlockScreen, Component) +function UnlockScreen () { + Component.call(this) + this.animationEventEmitter = new EventEmitter() +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +UnlockScreen.prototype.render = function () { + const state = this.props + const warning = state.warning + return ( + h('.flex-column', [ + h('.unlock-screen.flex-column.flex-center.flex-grow', [ + + h(Mascot, { + animationEventEmitter: this.animationEventEmitter, + }), + + h('h1', { + style: { + fontSize: '1.4em', + textTransform: 'uppercase', + color: '#7F8082', + }, + }, 'MetaMask'), + + h('input.large-input', { + type: 'password', + id: 'password-box', + placeholder: 'enter password', + style: { + + }, + onKeyPress: this.onKeyPress.bind(this), + onInput: this.inputChanged.bind(this), + }), + + h('.error', { + style: { + display: warning ? 'block' : 'none', + padding: '0 20px', + textAlign: 'center', + }, + }, warning), + + h('button.primary.cursor-pointer', { + onClick: this.onSubmit.bind(this), + style: { + margin: 10, + }, + }, 'Unlock'), + ]), + + h('.flex-row.flex-center.flex-grow', [ + h('p.pointer', { + onClick: () => this.props.dispatch(actions.forgotPassword()), + style: { + fontSize: '0.8em', + color: 'rgb(247, 134, 28)', + textDecoration: 'underline', + }, + }, 'I forgot my password.'), + ]), + ]) + ) +} + +UnlockScreen.prototype.componentDidMount = function () { + document.getElementById('password-box').focus() +} + +UnlockScreen.prototype.onSubmit = function (event) { + const input = document.getElementById('password-box') + const password = input.value + this.props.dispatch(actions.tryUnlockMetamask(password)) +} + +UnlockScreen.prototype.onKeyPress = function (event) { + if (event.key === 'Enter') { + this.submitPassword(event) + } +} + +UnlockScreen.prototype.submitPassword = function (event) { + var element = event.target + var password = element.value + // reset input + element.value = '' + this.props.dispatch(actions.tryUnlockMetamask(password)) +} + +UnlockScreen.prototype.inputChanged = function (event) { + // tell mascot to look at page action + var element = event.target + var boundingRect = element.getBoundingClientRect() + var coordinates = getCaretCoordinates(element, element.selectionEnd) + this.animationEventEmitter.emit('point', { + x: boundingRect.left + coordinates.left - element.scrollLeft, + y: boundingRect.top + coordinates.top - element.scrollTop, + }) +} diff --git a/responsive-ui/app/util.js b/responsive-ui/app/util.js new file mode 100644 index 000000000..ac3f42c6b --- /dev/null +++ b/responsive-ui/app/util.js @@ -0,0 +1,217 @@ +const ethUtil = require('ethereumjs-util') + +var valueTable = { + wei: '1000000000000000000', + kwei: '1000000000000000', + mwei: '1000000000000', + gwei: '1000000000', + szabo: '1000000', + finney: '1000', + ether: '1', + kether: '0.001', + mether: '0.000001', + gether: '0.000000001', + tether: '0.000000000001', +} +var bnTable = {} +for (var currency in valueTable) { + bnTable[currency] = new ethUtil.BN(valueTable[currency], 10) +} + +module.exports = { + valuesFor: valuesFor, + addressSummary: addressSummary, + miniAddressSummary: miniAddressSummary, + isAllOneCase: isAllOneCase, + isValidAddress: isValidAddress, + numericBalance: numericBalance, + parseBalance: parseBalance, + formatBalance: formatBalance, + generateBalanceObject: generateBalanceObject, + dataSize: dataSize, + readableDate: readableDate, + normalizeToWei: normalizeToWei, + normalizeEthStringToWei: normalizeEthStringToWei, + normalizeNumberToWei: normalizeNumberToWei, + valueTable: valueTable, + bnTable: bnTable, + isHex: isHex, +} + +function valuesFor (obj) { + if (!obj) return [] + return Object.keys(obj) + .map(function (key) { return obj[key] }) +} + +function addressSummary (address, firstSegLength = 10, lastSegLength = 4, includeHex = true) { + if (!address) return '' + let checked = ethUtil.toChecksumAddress(address) + if (!includeHex) { + checked = ethUtil.stripHexPrefix(checked) + } + return checked ? checked.slice(0, firstSegLength) + '...' + checked.slice(checked.length - lastSegLength) : '...' +} + +function miniAddressSummary (address) { + if (!address) return '' + var checked = ethUtil.toChecksumAddress(address) + return checked ? checked.slice(0, 4) + '...' + checked.slice(-4) : '...' +} + +function isValidAddress (address) { + var prefixed = ethUtil.addHexPrefix(address) + if (address === '0x0000000000000000000000000000000000000000') return false + return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed) +} + +function isAllOneCase (address) { + if (!address) return true + var lower = address.toLowerCase() + var upper = address.toUpperCase() + return address === lower || address === upper +} + +// Takes wei Hex, returns wei BN, even if input is null +function numericBalance (balance) { + if (!balance) return new ethUtil.BN(0, 16) + var stripped = ethUtil.stripHexPrefix(balance) + return new ethUtil.BN(stripped, 16) +} + +// Takes hex, returns [beforeDecimal, afterDecimal] +function parseBalance (balance) { + var beforeDecimal, afterDecimal + const wei = numericBalance(balance) + var weiString = wei.toString() + const trailingZeros = /0+$/ + + beforeDecimal = weiString.length > 18 ? weiString.slice(0, weiString.length - 18) : '0' + afterDecimal = ('000000000000000000' + wei).slice(-18).replace(trailingZeros, '') + if (afterDecimal === '') { afterDecimal = '0' } + return [beforeDecimal, afterDecimal] +} + +// Takes wei hex, returns an object with three properties. +// Its "formatted" property is what we generally use to render values. +function formatBalance (balance, decimalsToKeep, needsParse = true) { + var parsed = needsParse ? parseBalance(balance) : balance.split('.') + var beforeDecimal = parsed[0] + var afterDecimal = parsed[1] + var formatted = 'None' + if (decimalsToKeep === undefined) { + if (beforeDecimal === '0') { + if (afterDecimal !== '0') { + var sigFigs = afterDecimal.match(/^0*(.{2})/) // default: grabs 2 most significant digits + if (sigFigs) { afterDecimal = sigFigs[0] } + formatted = '0.' + afterDecimal + ' ETH' + } + } else { + formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ' ETH' + } + } else { + afterDecimal += Array(decimalsToKeep).join('0') + formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ' ETH' + } + return formatted +} + + +function generateBalanceObject (formattedBalance, decimalsToKeep = 1) { + var balance = formattedBalance.split(' ')[0] + var label = formattedBalance.split(' ')[1] + var beforeDecimal = balance.split('.')[0] + var afterDecimal = balance.split('.')[1] + var shortBalance = shortenBalance(balance, decimalsToKeep) + + if (beforeDecimal === '0' && afterDecimal.substr(0, 5) === '00000') { + // eslint-disable-next-line eqeqeq + if (afterDecimal == 0) { + balance = '0' + } else { + balance = '<1.0e-5' + } + } else if (beforeDecimal !== '0') { + balance = `${beforeDecimal}.${afterDecimal.slice(0, decimalsToKeep)}` + } + + return { balance, label, shortBalance } +} + +function shortenBalance (balance, decimalsToKeep = 1) { + var truncatedValue + var convertedBalance = parseFloat(balance) + if (convertedBalance > 1000000) { + truncatedValue = (balance / 1000000).toFixed(decimalsToKeep) + return `${truncatedValue}m` + } else if (convertedBalance > 1000) { + truncatedValue = (balance / 1000).toFixed(decimalsToKeep) + return `${truncatedValue}k` + } else if (convertedBalance === 0) { + return '0' + } else if (convertedBalance < 0.001) { + return '<0.001' + } else if (convertedBalance < 1) { + var stringBalance = convertedBalance.toString() + if (stringBalance.split('.')[1].length > 3) { + return convertedBalance.toFixed(3) + } else { + return stringBalance + } + } else { + return convertedBalance.toFixed(decimalsToKeep) + } +} + +function dataSize (data) { + var size = data ? ethUtil.stripHexPrefix(data).length : 0 + return size + ' bytes' +} + +// Takes a BN and an ethereum currency name, +// returns a BN in wei +function normalizeToWei (amount, currency) { + try { + return amount.mul(bnTable.wei).div(bnTable[currency]) + } catch (e) {} + return amount +} + +function normalizeEthStringToWei (str) { + const parts = str.split('.') + let eth = new ethUtil.BN(parts[0], 10).mul(bnTable.wei) + if (parts[1]) { + var decimal = parts[1] + while (decimal.length < 18) { + decimal += '0' + } + const decimalBN = new ethUtil.BN(decimal, 10) + eth = eth.add(decimalBN) + } + return eth +} + +var multiple = new ethUtil.BN('10000', 10) +function normalizeNumberToWei (n, currency) { + var enlarged = n * 10000 + var amount = new ethUtil.BN(String(enlarged), 10) + return normalizeToWei(amount, currency).div(multiple) +} + +function readableDate (ms) { + var date = new Date(ms) + var month = date.getMonth() + var day = date.getDate() + var year = date.getFullYear() + var hours = date.getHours() + var minutes = '0' + date.getMinutes() + var seconds = '0' + date.getSeconds() + + var dateStr = `${month}/${day}/${year}` + var time = `${hours}:${minutes.substr(-2)}:${seconds.substr(-2)}` + return `${dateStr} ${time}` +} + +function isHex (str) { + return Boolean(str.match(/^(0x)?[0-9a-fA-F]+$/)) +} diff --git a/responsive-ui/css.js b/responsive-ui/css.js new file mode 100644 index 000000000..7c394a87b --- /dev/null +++ b/responsive-ui/css.js @@ -0,0 +1,29 @@ +const fs = require('fs') +const path = require('path') + +module.exports = bundleCss + +var cssFiles = { + 'fonts.css': fs.readFileSync(path.join(__dirname, '/app/css/fonts.css'), 'utf8'), + 'reset.css': fs.readFileSync(path.join(__dirname, '/app/css/reset.css'), 'utf8'), + 'lib.css': fs.readFileSync(path.join(__dirname, '/app/css/lib.css'), 'utf8'), + 'index.css': fs.readFileSync(path.join(__dirname, '/app/css/index.css'), 'utf8'), + 'transitions.css': fs.readFileSync(path.join(__dirname, '/app/css/transitions.css'), 'utf8'), + 'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'), + 'react-css': fs.readFileSync(path.join(__dirname, '..', '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'), +} + +function bundleCss () { + var cssBundle = Object.keys(cssFiles).reduce(function (bundle, fileName) { + var fileContent = cssFiles[fileName] + var output = String() + + output += '/*========== ' + fileName + ' ==========*/\n\n' + output += fileContent + output += '\n\n' + + return bundle + output + }, String()) + + return cssBundle +} diff --git a/responsive-ui/design/00-metamask-SignIn.jpg b/responsive-ui/design/00-metamask-SignIn.jpg new file mode 100644 index 000000000..2becdb032 Binary files /dev/null and b/responsive-ui/design/00-metamask-SignIn.jpg differ diff --git a/responsive-ui/design/01-metamask-SelectAcc.jpg b/responsive-ui/design/01-metamask-SelectAcc.jpg new file mode 100644 index 000000000..239091a98 Binary files /dev/null and b/responsive-ui/design/01-metamask-SelectAcc.jpg differ diff --git a/responsive-ui/design/02-metamask-AccDetails.jpg b/responsive-ui/design/02-metamask-AccDetails.jpg new file mode 100644 index 000000000..d7d408ffc Binary files /dev/null and b/responsive-ui/design/02-metamask-AccDetails.jpg differ diff --git a/responsive-ui/design/02a-metamask-AccDetails-OverToken.jpg b/responsive-ui/design/02a-metamask-AccDetails-OverToken.jpg new file mode 100644 index 000000000..f26ff31e8 Binary files /dev/null and b/responsive-ui/design/02a-metamask-AccDetails-OverToken.jpg differ diff --git a/responsive-ui/design/02a-metamask-AccDetails-OverTransaction.jpg b/responsive-ui/design/02a-metamask-AccDetails-OverTransaction.jpg new file mode 100644 index 000000000..8a06be6b9 Binary files /dev/null and b/responsive-ui/design/02a-metamask-AccDetails-OverTransaction.jpg differ diff --git a/responsive-ui/design/02a-metamask-AccDetails.jpg b/responsive-ui/design/02a-metamask-AccDetails.jpg new file mode 100644 index 000000000..c37e0f539 Binary files /dev/null and b/responsive-ui/design/02a-metamask-AccDetails.jpg differ diff --git a/responsive-ui/design/02b-metamask-AccDetails-Send.jpg b/responsive-ui/design/02b-metamask-AccDetails-Send.jpg new file mode 100644 index 000000000..10f2d27fd Binary files /dev/null and b/responsive-ui/design/02b-metamask-AccDetails-Send.jpg differ diff --git a/responsive-ui/design/03-metamask-Qr.jpg b/responsive-ui/design/03-metamask-Qr.jpg new file mode 100644 index 000000000..9c09de42f Binary files /dev/null and b/responsive-ui/design/03-metamask-Qr.jpg differ diff --git a/responsive-ui/design/05-metamask-Menu.jpg b/responsive-ui/design/05-metamask-Menu.jpg new file mode 100644 index 000000000..0a43d7b2a Binary files /dev/null and b/responsive-ui/design/05-metamask-Menu.jpg differ diff --git a/responsive-ui/design/chromeStorePics/final_screen_dao_accounts.png b/responsive-ui/design/chromeStorePics/final_screen_dao_accounts.png new file mode 100644 index 000000000..805cc96b6 Binary files /dev/null and b/responsive-ui/design/chromeStorePics/final_screen_dao_accounts.png differ diff --git a/responsive-ui/design/chromeStorePics/final_screen_dao_locked.png b/responsive-ui/design/chromeStorePics/final_screen_dao_locked.png new file mode 100644 index 000000000..9d9e33930 Binary files /dev/null and b/responsive-ui/design/chromeStorePics/final_screen_dao_locked.png differ diff --git a/responsive-ui/design/chromeStorePics/final_screen_dao_notification.png b/responsive-ui/design/chromeStorePics/final_screen_dao_notification.png new file mode 100644 index 000000000..d56a5ce62 Binary files /dev/null and b/responsive-ui/design/chromeStorePics/final_screen_dao_notification.png differ diff --git a/responsive-ui/design/chromeStorePics/final_screen_wei_account.png b/responsive-ui/design/chromeStorePics/final_screen_wei_account.png new file mode 100644 index 000000000..d503ff301 Binary files /dev/null and b/responsive-ui/design/chromeStorePics/final_screen_wei_account.png differ diff --git a/responsive-ui/design/chromeStorePics/final_screen_wei_notification.png b/responsive-ui/design/chromeStorePics/final_screen_wei_notification.png new file mode 100644 index 000000000..3560c51ff Binary files /dev/null and b/responsive-ui/design/chromeStorePics/final_screen_wei_notification.png differ diff --git a/responsive-ui/design/chromeStorePics/icon-128.png b/responsive-ui/design/chromeStorePics/icon-128.png new file mode 100644 index 000000000..ae687147d Binary files /dev/null and b/responsive-ui/design/chromeStorePics/icon-128.png differ diff --git a/responsive-ui/design/chromeStorePics/icon-64.png b/responsive-ui/design/chromeStorePics/icon-64.png new file mode 100644 index 000000000..7062cf4f1 Binary files /dev/null and b/responsive-ui/design/chromeStorePics/icon-64.png differ diff --git a/responsive-ui/design/chromeStorePics/metamask_icon.ai b/responsive-ui/design/chromeStorePics/metamask_icon.ai new file mode 100644 index 000000000..27400c5a4 --- /dev/null +++ b/responsive-ui/design/chromeStorePics/metamask_icon.ai @@ -0,0 +1,2383 @@ +%PDF-1.5 % +1 0 obj <>/OCGs[5 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + metamask_icon + + + Adobe Illustrator CC 2015 (Macintosh) + 2016-06-15T14:23:12-04:00 + 2016-06-15T14:23:12-04:00 + 2016-06-15T14:23:12-04:00 + + + + 240 + 256 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAADwAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXnP5r/mvB5Tg/RmnAT6/cJyUMKx28bVAkf+ZjT4U+k7UDYuo1HBsObl6bTce5+l5X+Wf5t6jonm KZtfu5rzTNVcG+lkZpHilACLOAamgUBWC/sgUrxAzEwakxl6uRczUaYSj6eYfS9vcQXEEdxA6ywT KskUimqsjCqsCOoIObUG3UkUvxQ7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXln5rfnHb+X1l0bQnWfXCCs0+zR2tfEGoaTwXoO/hmJqNTw7Dm5mm0plvL6fvfO U889xPJcXEjTTzM0ksshLO7saszMdySTUk5qybdsBSzAl7R+Rv5ni0dPKutTqto5ppNw/KqyOwH1 c0BHFuVVJpTpvUUz9Jnr0n4Ov1mnv1D4ve82LrHYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FUn1Pzl5W0yF5r3VLeNI9no4kYfNU5N+GY89XijsZC/mfkHIhpMstxE18h8ynCmqg7iorQ7HMhx 3Yq7FXYq7FXYq7FXjf5v/nG2nPL5e8tXAN9Ro9Rv039A7D04WBp6nUM1Ph7fFXjg6nU16Y83P0ul v1S5PAWZmYsxJYmpJ3JJzWu0axV2KuxV9Ifkr+Zq67py6FrF1y121+G3eUjlcwKtQeRPxyIAeXci jbnkc2mlz8Qo83U6vT8J4h9L1PMxwnYq7FXYq7FXYq7FXYq7FXYq7FXYqlN95s8t2I/0jUYQQaFE b1HHzWPk34Zi5Nbhh9Uh9/3OVi0Waf0xP3fex7UfzX0WEOtjBNdSCoR2AjjPgak8/wDhcwcvbWIf SDL7B+v7HOxdi5T9REftP6vtYre/mf5ouD+5eK0UdoowxPzMnqfhmsydsZpcqj7h+u3aY+x8Eedy 95/VTFdc803n1dptVv5powSVjeRmqx7IhNMxPEy5jRJPx2cwY8WEWAB8N0l8gRXnm/z/AKXazpXT 7WX65NCo5II4PjHqVrXk3FDX+btXNtodLETDqddqiYH7H1LnQPOuxV2KuxV2KuZlVSzEBQKknYAD FXhX5t/nOsyzaB5XuCEDBbzVoXZSSrA8Ld0I2qKM/foNt81+o1X8Mfm7LTaT+KXyeI5r3YuxV2Ku xV2KqtpdXNpdQ3VrI0NzA6yQyoaMroaqwPiCMINboIsUX1j+XH5g6f5w0WOcNHDq0I439irbqwoD IiklvTaux7dK1GbnBmEx5ukz4DjPky3Lmh2KuxV2KuxV2KuxVKb/AM2eW7CoudQhDA0KI3qMD7rH yYZjZdbhh9Uh9/3OVi0Waf0xP3fex6//ADY0OHktnbzXTKaKxpFGw8QTyb/hcwMnbWIfSDL7B+Pg 5+PsXKa4iI/afx8WO3/5ra9OJEtYIbVG2RqNJIo/1iQv/C5gZe2sp+kCP2n8fBz8XYuIVxEy+wfj 4sVvdY1a+FLy8muFBLBZJGZQT4KTQZrMmfJP6pE/F2ePBjh9MQPghMqbXYqler+YLPTgUJ9W57Qq en+se2X4dPKfuaMueMPewW+vrm9uGnuH5Ox2G/FR/KoPQZtYQERQdZOZkbL3L/nG7y8Y7PVPMEqj lOy2VqxBDBEpJKQSPsszINu6nNpoYbGTqdfPcRe1ZnuvdirsVdirsVeF/n1+Y96l1N5O04+lCERt Vn35uXAkWBfBOJVmI+1XjsAeWv1ec3wD4uy0eAVxn4PEM17sXYq7FXYq7FXYq7FU18seZNU8uaxD qumymOeKqsBQh0YUZCGDDceI2O+ESlH6TRYmEZbSFh7bbfmL5mubeO4iv6xSqHQ+lD0YV/kzVy7U 1MTRl9kf1Owj2XpiLEftP61T/Hvmv/lt/wCSUP8AzRkf5W1P877I/qZfyTp/5v2n9bv8e+a/+W3/ AJJQ/wDNGP8AK2p/nfZH9S/yTp/5v2n9bv8AHvmv/lt/5JQ/80Y/ytqf532R/Uv8k6f+b9p/W7/H vmv/AJbf+SUP/NGP8ran+d9kf1L/ACTp/wCb9p/W7/Hvmv8A5bf+SUP/ADRj/K2p/nfZH9S/yTp/ 5v2n9bHtW1/WtUeuoXTz8eiGioCNtkUKv4ZDNqsmX6zbdh0uPF9ApL8ob3Yq7FXYq7FWO695pW0d rWyo9wtRJKd1Q+A8WH3ZmYNLxby5OJn1PDtHmw6SR5JGkclnclmY9STuTmzArZ1xN7uhhlmmSGJS 8sjBI0HUsxoAPpwhiS+zvLGhxaF5e0/SIiGFlAkTOoIDuB8b0JNOb1bN5jhwxAdBknxSJ70zybB2 KuxV2KpX5o12HQfL2oaxNxK2UDyKjHiHkpSOOu9ObkL9OQyT4Yks8cOKQHe+Nr6+u7+8mvLyVp7q 4cyTTOaszNuSc0ZJJsu/AAFBRwJdirsVdirsVdirsVdirKvI2tmC6OmzN+5uDWEmlFkp03/mp9/z zX67BY4hzDm6PNR4T1Z5mpdo7FXYq7FXYqg7laSn33y2PJgVLJIdirsVWu6IjO7BUUEsxNAAOpJO IFqTTD9c81yT1gsGaKIH4px8LtT+Xuo/HNlg0gG8ubrs2qJ2ixzM1w3Yqzz8k/L66z5/s2kAMGmK 2oSAkgkwkCKlO4ldDTwBzJ0sOKfucbVz4YHz2fU+bd0rsVdirsVdirxT/nIzzWi2ll5ZtZ1Mkj/W dRjQnkqoB6KPTajli9D/ACqcwNbk2EQ7DQ49zIvB81zs3Yq7FXYq7FXYq7FXYq7FW0d0dXRirqQV YGhBG4IIxItQXp3ljWjqunc5Cv1qI8J1Xb/Van+UPxrmh1WDw5bcnc6fNxx35pvmO5DsVdirsVQ1 2v2W+gnJwYlD5YxdiqheXttZwGe4cRxjap6k+AHc5KEDI0GM5iIssE1fzBeakeB/dWw6Qqevux7n Nth08Ye91eXPKfuSzL2h2KuxV9If84/eVk03ys+tyEm51lqhSKcIYHdEArv8Zq3uKZtdHjqN97qN bkuVdz1PMtw3Yq7FXYqp3V1b2ltNdXMgit4EaWaVjRVRByZifAAYCaSBez418169Nr/mPUdYl5Vv Z2kjVyCyRVpEhIp9iMKv0Zo8k+KRLv8AHDhiB3JVkGbsVdirsVdirsVdirsVdirsVTPy7q50vU45 2J9B/guFHdD36H7J3/DKNTh8SFdejdgy8Er6PUYpY5okljblHIoZGHQqwqDmhIINF3QNiwuwJdir sVUrlaxH23yUeaCg8tYJfq2t2Wmx/vW5TleUcC/abtv/ACj3OXYsEp8uTVlzRhz5sD1DULm+uGnu GLE/ZX9lR/KozbY8YgKDqsmQyNlDZNg7FXYqiNN0+51HUbXT7UBrm8mjggUmgLysEWp7bnDEWaRK VCy+0tM0+307TbXT7YEW9nDHbwgmp4RKEWp+QzfRFCnnpSs2UThQ7FXYq7FXmX59ebIdL8ovpEMw Goauyx+mrFXW2U8pH2/Zbj6dD15HwOYmryVGupczR4uKd9A+ac1Tt3Yq7FXYq7FXYq7FXYq7FXYq 7FXYqzXyJrSem2lztRgS9sSQAQT8SD3ruPpzV6/Bvxj4ux0Wb+EsxzWuwdirsVadeSlfEEYhDE9b 8zwWLNb24E10pKuK/AhHjTqa9s2ODSme52Dh5tSI7DcsJmmlmkaWVy8jmrOxqTm0AAFB1hJJsrMK HYq7FXYq9S/5x+8qvqXmp9bkIFtoq1CEV5zTo6IBXb4RVq9jTMzR47lfc4WtyVHh730jm0dS7FXY q7FXYq+X/wA+dQmuvzGu4JPsWMFvBF/qtGJ/+JTHNTq5Xk9zudHGsY83nmYrlOxV2KuxV2KuxV2K uxV2KuxV2KtqrMwVQSxNABuSTirN/Lfl9LBVurhQ1626g7iMeA/yvE/R89VqdRx7D6XZ6fT8O55s tUggEdDuM1zmt4pdirsVYZ5s8s+pLJfWS0lNXmhH7fcsv+V4jv8APrs9JqaHDJ1+p01+qLDM2brn Yq7FXYq7FX0n/wA48to48kyR2cwfUPrLyanEdmjZvhiA2rwMaAj35ZtdHXBtzdRrr49+XR6hmW4b sVdirsVdir5I/Ne/+vfmJrs1QeFx6G3/AC7osP8AzLzTag3Mu800axhieUN7sVdirsVdirsVdirs VdirsVdirMPLHl8wAXt4hE5/uYmFCg/mI8f1ZrdVqL9MeTsdNgr1HmyXMJzEXatWOndf1ZVMbswr ZFLsVdiqGu13VvoOTgWJYV5o8vFS9/aL8O7XEQ7eLj28c2ml1H8MnXanT/xBi+Z7guxV2KuxVP8A yLf+abLzPZP5ZDyarI4SO3XdZVO7JKKgenQVYkjiPiqKVFuKUhIcPNqzRiYni5PsKIymJDKFWXiP UCElQ1N6EgEivtm7dCuxV2KuxV4P+Zn5i/m7o189tNbRaNYszi2urVBOsqEkD/SJAw5bV2VG8QNs 12fNlie4Oy0+DFId5eL3FxPczyXFxI01xMzSTSuSzu7GrMzHckk1JzBJt2AFLMCXYq7FXYq7FXYq 7FXYq7FXYqyvyv5fZWF9ex0IobaNvv5kfq/2s1+q1H8Mfi5+mwfxS+DKswHOdiqrbuVkA7Nsf4ZG Q2SEZlTN2KuxVTnTlEfbcfRhid0FBZcwYd5k8uNAz3tkg+rdZYl6oe7KKfZ/V8umy02pv0y5uu1G nr1R5MbzNcN2KuxVnH5Z/mdP5MuXjayhudPu5Fa9cJS6CAEUjkqoIFa8W2/1ak5kYM/B02cbUafx Ou76X8s+ZdL8x6RDqumGU2s32TLG8R5DZl+IUbi1VJQlajrm1hMSFh1GTGYGimmTYOxV2KqN9HZS Wk0d8sT2boVuEnCmIodiHDfDT54DVbpF3s+b/wAyfI/kCy9fU/LvmWzJZix0f1BOQSWJWF4OZXsq q6/N81efFAbxkPc7bBmmdpRPveZZiOY7FXYq7FXYq7FXYq7FXYqybyz5cMhjv7xaRD4oYSPteDN/ k+Hj8uuDqdTXpi5um09+osvzXOwdirsVdiqYIwZA3iMoIZt4pdirsVS9l4sV8DTLg1tEAih6YVYT 5k8vGzY3dsC1qxJdf99knpt+z4ZtNNqOLY83W6jT8O45JBmW4jsVZP8Alp5c07zH5z0/SNQd1tZz I7rH1f0o2l4V/ZDcNzl2CAlMAtOomYQJD65gggt4I7e3jWGCFRHFFGAqIiiiqqjYADYAZugKdGTa /FDsVdirHfNvkDyv5rjUava87iNGSC7iYxzRhvBhs1DuA4I9sqyYYz5tuLNKHJ4v5q/5x58xWBlu NAuE1S1G6270iuQCTtv+7fitN+QJ7LmDk0Uh9O7sMeuifq2eUzQzQTPDMjRTRMUkjcFWVlNCrA7g g5hkU5oNrMCXYq7FXYq7FXYq7FWR+XfLJuAl5eDjBUGKEjdx4nwX9fy64Wo1NemPNzNPpr9UuTMs 1rsXYq7FXYq7FUVavVSh6jcfLK5hkFfIMnYq7FUHdLSWviK/wyyHJgVLJoWuiOjI4DIwKsp3BB2I OINKRbB/MPl19Pb6xb1ezY713MZPY+3gfo+e10+o49j9Tq9Rp+DcckkzKcZmP5P3EsH5k6G8cTTM ZZIyqgkhZIXRm27IrFj7DL9Mf3gcfVC8ZfWWbl0jsVdirsVdirwr87POX5jaTqj6dFIdP0O4VTaX dorK8o6lXuDusgZTVUI+HrUHfX6rLOJrkHZaTFjkL5l4jmvdi7FXYq7FXYq7FXYqn/lzy6t8purr kLZTREG3Mg77/wAvbbMTU6jg2HNy9Pp+Lc8maqqooVQFVRRVGwAHYZqyXZAN4q7FXYq7FXYqvhfj Ip7dD9ORkNkhHZUzdirsVULpKoG/l/jkoFiULlrF2KrJYYpozHKiyRt9pGAIP0HCCQbCCAdiwTzD obabcB4qm0lJ9M7nif5Sf1ZttPn4xvzdXqMPAduT6E/Jz8tofLejx6pqMStrt+iyNzSj20bLtCOY DK9G/edN/h7VO+02DhFnmXn9Vn4zQ+kPSMynEdirsVdirsVS7zBoGl6/pM+l6nCJrScUI6MrD7Lo ezKehyM4CQos4TMTYfKfn3yJq3lDWHs7pWkspCTYX1KJMgp4Vo61oy9vlQ5p82EwNF3WHMMgsMYq MpbnVGKuxVvFXYqnnlvQDfSfWbhSLRDsP9+MOw9h3zF1Oo4BQ5uVp8HEbPJm6IiIqIoVFACqBQAD YADNUTbswKXYq7FXYq7FXYq7FXYqjoX5xg9+h+eUyFFmF+BLsVWyLyRl8RtiChAZewdirsVTjyfZ JeeZ9NidOarOkpUio/dH1Afo45mdnx4s8R5/du4faE+HBI+X37Pds7N4t2KuxV2KuxV2KuxV5Z+Y esC91gWcZBhsKpUb1kahf7qcfozke2dTx5eEcoff1/U9b2PpuDFxHnP7un62K5p3buxV2KuxV1Bi qFukowcdDsfnlkCxKhk2LsVdirsVdirsVdirsVRFo/xFfHcZCYZBE5WydirsVQEq8ZGHv+vLgdmB W4UOxVmH5WQCTzOzn/dFvI4+kqn/ABvm27GiDm90T+h1PbUiMI85D9L17OpeVdirsVdirsVdiqG1 K/i0+wuL2X7ECFyK0qR0Ue7HbKs+UY4GZ6Btw4jkmIjqXh000k0zzSsWkkYu7HqWY1Jzz+UjIknm XvYxEQAOQWYGTsVdirsVdiq2ROaFfHp88INIKAIIJB6jrlrB2FXYq7FXYq7FXYq7FV0bcXDeBwEJ CPylm7FXYqhbtTyDdiKfTlkCxKhk2LsVZ7+UduW1S+uO0cCx/TI4P/MvN32HC5yl3Cvn/Y6PtydQ jHvN/L+16jnSPNuxV2KuxV2KuxVhP5l60IrOPSY6+rccZZj29NSeI+l1r9GaHtzUgQGMc5bn3f2/ c73sTTEzOQ8o7D3/ANn3vOM5d6d2KuxV2KuxV2KuxVCXiFT6iqWB+1Sn8csgejEhBfW4/Bvw/rlv Chr64n8px4Va+uD+T8ceBWvrv+R+P9mHgVv67/kfj/ZjwK19cP8AL+OPArX1yT+UY8KtfXJfBfx/ rjwhU2s5Ge3Ut9rv/DMeYosgr5FLsVUL3l9WZlFWX4hX26/hkoc0FKDdSnwHyGZPCGKhZ6oLy3We CTlGxIBoBupKnt4jLMuA45cMhu1Yc0ckeKPJ6F+UmpSxapdQMapPGrGvjG1BT/kYc2vYs6nKPeL+ X9rqO3IeiMu418/7HsA3GdE807FXYq7FXYq7FXi/mfVP0nrl1dA1i5cIaGo9NPhUj/Wpy+nOF7Qz +LmlLpyHuH4t7jQYPCwxj15n3n8UlWYbmOxV2KuxV2KuxV2KuIB2PTFUDdWaV5caqe/cZbCbAhAy WjCpQ8h4d8tElUCCDQ9ckrWKpZrHmHTtKUeuxeY9II6F6eJBIoMztJoMmf6dh3nk4Os7Rxaceo2e 4c2Far5x1a+5JE31W3bb04z8RHu/X7qZ0ul7IxYtz6pef6v7XltX2zmy7A8EfL9f9id+R9d9WP8A Rc5q8YLW7kjdR1Tfeo6j2+WaztrRcJ8WPI8/1/jr73adh6/iHgy5jl7u78dPcy5RyYL4mmc+9GnN o1HK9iP1ZjzCQisrZOxVp1DoynowIPyOIKscl/dc+ewSvL2p1zNiL5NZNCy888na79QvDa3DgWlw d2Y0CPTZvDfofo8M67tfQ+LDiiPXH7Q8b2Nr/CnwSPol9h7/ANb2PytcvZ6rYSqwX96odu3FzRq/ Qc5fRZTDPEjvr57PT6/GJ4ZA91/J79A3KJT7Z2bxS/FXYq7FXYqk/m7VRpug3UyvwnkX0oN6Hm+1 V91FWzC7Rz+FhkevIe8/i3N7PweLmiOnM/D8U8azhnt3Yq7FXYq7FXYq7FXYq7FXEAih6Yqg54Ch 5L9j9WWRlbAhQeNHFGFcmChJ9b0DXL6ALpN8lt2kVwysfcSLyI+hfpzP0WqwY5XliZfju/b8HB12 DPkjWKQj+O/9nxYTe/l55tilc+gt11Zpo5VPInc7OUcn6M6XD23pSBvw+RH6rDzGXsXUgnbi8wf1 0Uiu9K1SzAa7s5rdTsGljZAfkWAzZYtTjyfTKMvcQ67Jp8kPqiY+8KNrczWtxHcQNwliYMje4yeX HGcTGXIscWWWOQlHmHrei39tqUMVzAQyMKuvdGAqVb3GcDqsEsMjGX9vm+g6bUxzQE4/2eSco3Fw 3gcwyHITAEEAjodxlLJ2KXYqx/X7J5UubeJgj3MThHaoVWcFakgHau+Z2kyiMoyPKJH2OPqMZnjl EcyCEB5f/LjSLBEm1AC+ux1Dbwqd+iftf7KvyGZ2t7dy5DUPRH7fn+p1ej7DxYxc/XL7Pl+tkDoI 3KqAoX7IGwA7UzUg3u7iq2fQWlzGfTbadl4mWJHK+HJQaZ30JcUQe8PAzjwyI7iiskxdirsVdirz L8y9S9fV4rFSeFmnxj/iySjH/heOcp25n4sogP4R9p/ZT1PYmDhxmZ/iP2D9tsPzSO7dirsVdirs VdirsVdirsVdiriARQ9MVQc8BT4hun6ssjK2BDdq1JCP5h+IxmNkhF5WydiqAu9A0O8Ltc2FvLJJ 9uQxrzP+zpy/HMnHrc0KEZyAHnt8nGyaPDO+KEST5b/NRsPLOlaYH/R0RgEn205u6kjvRy1D8snn 12TNXiG68h+hjp9Hjw2MYoHzP6VVlZTRhQ5SC5CJt5l4hCaEdK98hKKQVfIMnYqo3dstxEV2DjdG 8DkoSooKhp8jrW2mqJE3UH+XJZB1ChddLSWviMYcmJfQsESwwRxL9mNQo+QFM9BAfPyV+FDsVdiq jeXdvZ2st1cOEhhUs7HwGQyZIwiZS2AZ48cpyEY7kvD7+7kvL2e7kFHuJGkYDoORrQfLOAzZDOZk ept73FjEICI6ClDK2x2KuxV2KuxV2KuxV2KuxV2KuxVxAIoeh64qhZIjE4dd0B+7LAbY1SKBBAI6 HplbJ2KuxV2KrJI1kWh69jhBpBCElhaM77jscsErYkK8NwGor/a7HxyEopBV8iydiqhcW5kKyRkL Mn2GPT3ByUZVz5IbVDcyQLTizuI2U9mYgZZijcuEdWGSXDEnufQeegPn7sVdirsVYb+ZmqGDS4dP Q/Fdvyk6f3cRBp47tT7s0fbmfhxiA/iP2D9tO67EwcWQzP8ACPtP7LeaZyr1TsVdirsVdirsVdir sVdirsVdirsVWvJGgq7BR2qaYgEoQc2qW4BVVMn4A/x/DLRiKLVIbscAGQjbpWtPbtgMFtEJIj/Z NfbvlZFJtdil2KuxVogEUIqMUIWa3K1ZN18O4yyMkEKkFxyoj9ex8cEoqCr5Bk7FUTpEdv8Apmxe YqkQuYWmZjReIcVJJ2+z3zI0kgMsCeXEPvcfVRJxSA58J+57pnfPBuxV2KuxV5B531M3/mK4INYr b/R46eEZPL/hy2cV2rn8TOe6O3y/bb2fZeHw8A75b/P9lJDmudi7FXYq7FXYq7FXYq7FXYqoSXtq nWQE+A3/AFZIQJRaEk1c7iOP5Fj/AAH9csGHvRaFkv7t+shA8F2/VvlgxgLagSSanqckhVtY+UnI 9F3+nBIqjcrQ7FVRZ5V/aqPA75ExCbVUuwdnFPcZEwTauro32SDkCEt4pdiqhNbhqsmzeHY5KMmJ DoJzXhJs3YnDKPUKCr5Bk7FWd+RvOPp+npOoyfu9ltJ2/Z7CNj4fy+HTpnQ9k9pVWKZ/qn9H6vk8 92r2dd5YD3j9P6/m9CzpXnHYq7FXhnnLS30vzHeW4BWF3M1vQED05PiAX2U1X6M4vX4PDzSHTmPj +Ke00GfxMMT15H4fi0l5N4nMOnMdybxONK7kfHFXVPjirVT44q6p8cVdU+OKuqcKrZEEiFT9B8Di DSoB0ZGKt1GWgpW4q7FWwCTQdT0xVHxR+mgXv1Jysm0L8CuxV2KuxV2KqiXEq96jwORMQm1dbpD9 oFfxGQMCm1VWVhVSD8sjSVssSyDfY9jhBpSFkbsh4S9f2W7HCRfJVbIpdir0fyR5zW4WPStRci5A 421wxr6ngjH+bw8fn16jsvtTjrHk+roe/wAvf9/v58x2n2Zw3kx/T1Hd5+77vdym2b50TsVef/m1 pJktbTVY1FYCYJyAa8X3Qk+CtUf7LNH21guImOmx/H45u97Ez1IwPXcfj8cnmOc49G7FXYq7FXYq 7FXYq7FXYqpTw+otR9odMINKgiCDQ9csS1iqItI6vzPRenzyMiqLyCHYq7FXYq7FXYq7FXYq4Eg1 HXFVVLmRdj8Q9+uRMQm1UXETijinz3GR4SE2vRgopXkg6N1p88iUoTWNf0bRoBNqd3Hao1eAc1Zq UrxQVZqV3oMtw6fJlNQFtWbUQxC5mmMXn5w+TbYqYJbi8J7wRFeP/I4xfhmwx9i6g86j7z+q3Ayd sYByuXuH66ehfl//AM5Q+UtVuk0rzAH0mT4Y7XUp6GGToo9dgW9JjWpY/B1qV79Tp4zEAJkGQeX1 BgZkwFRe3wzRTRJNC6yRSANHIhDKyncEEbEHL2hC6xpcGq6ZcafOSIp1oWHUEEMp+hgDlWfCMkDA 8i24MxxTExzDwG5t5ra5ltpl4zQO0ci9aMh4kfeM4ecDEkHmHuYTEoiQ5FTyLJ2KuxV2KuxV2Kux V2KuxVDXMNayL/shkolKGAJIA6nYZNUwRAihR2yolC7FXYq7FXYq7FW1VmPFQWJ6AbnEC+Skgc0V DpGpzbpbPTxYcR/w1MyIaTLLlEuPPV4o85BEDy3rJNDBT3Lp/A5aOzs3837Q1HtHD/O+wq48pamR UvCPYs38Fy4dk5e+P4+DSe1sXdL7P1q8Xk+cj97cqp/yVLfrK5ZHsiXWX4+xql2vHpH8farR+Tog f3l0zDwVAv6y2Wx7IHWX2Ncu1z0j9quvlLTlIPqzVH+Uv/NOWfyTi75fZ+pq/lbL3R+39b5x/OyS VfzBvrLmWt7JII7ZDT4VeFJW6UrV5Cc2uk00MUKiHV6rUTyyuRYJmS4zsVfU/wDziJp/mFdA1bUJ 72QaC8/oWOnHiUNwqq00+68h8JRBxeh+LkKgHCgvoLFDx/8AM3SBZeYfrMakQ36erWlF9RfhkA8e zH55yna+Dgy8Q5S+/r+v4vV9kZ+PFwnnHb4dP1fBiOat2rsVdirsVdirsVdirsVdiqvaWF9euUtL eW4cdViRnI+fEHJ48Up/SCfcwyZYw+oge9PY/wArfMqQrdskahhX0ORaVK+KqD+GbQdkZjGzQdbL tnCDQstDybIrFJboJKv2k4EkfOrKckOxz1l9jWe2B0j9v7FaLyfbj++uHf8A1AF/XyyyPZEesj+P m1y7Xl0iPv8A1Ky+U9MBqXlYeBZf4KMsHZWLvl+Pg1HtXKekfx8VceW9G/5Z6+/N/wDmrLv5Ow/z ftP62r+Uc3877B+pXTSNLQUFrER/lKGP3tXLY6TEP4R8mqWryn+I/NWis7SI1igjjPiqgfqGWRww jyAHwapZpy5kn4q2WNbsVdirsVdirsVdir5U/O7/AMmdrP8A0bf9QsWZEOTRPmwbJMU28p+XL3zL 5l03QbIH6xqNwkAcKX9NWPxysq78Y0q7ewOKv0B8teXNJ8t6FZ6HpEPoafYpwgjJLHclmZierMzF ifE4WKZYqxn8wtFj1Hy7PMIw11YqZoHrSiggyj6UB28QM13amAZMJPWO/wCv7HY9l6g48wH8Mtv1 fa8XzkXr3Yq7FXYq7FXYqmOm+Xdc1Ir9SspZkbYS8eMe3/FjUT8cvxaXLk+mJP3fNx82qxY/qkB9 /wAubK9I/KjUpZEfVJ0t4OrRRHnL8q04D51ObTB2LMm8hoeXP9X3usz9tQArGLPny/X9zL9N/L3y tYgH6r9akH+7Lk+pX5rtH/wubTF2Zgh0s+e/7PsdVm7Tzz60PLb9v2sghhhhiWKFFjiQUSNAFUD2 A2zPjEAUOTgSkSbPNfhQo3NnaXScLmFJV3oHANK+HhjSQUovPKNhIpNo720gHwgMXSvuG5fgcrOM MxkKQ3fl7zBa1IjW6Qb8o9zT/V+E/cMgcZbBkCXNdCNzHNG8Ug2ZWFCCPHvkKZ2vW4gbo4+nb9eB VTFLsUOxV2KuxV2KuxVpnVBViAPE4q8U89flBq3mfzvf6yt/b2un3Xo8Kh3mAjhSNqpRF6pt8eWx nQa5Qsr7b/nH3yssai51C+llH2mjaKNT/sTHIR/wWPiFfDD178qfyf8AKnli6/Ttrp3p35jMVrcT SPI4jcDm4ViVUsNgwANKjocnC+rXOuj1DJsHYq0yqylWAZWFGU7gg9sVeBa/pbaXrN3YN0gkIQ+K H4kP0qQc4fVYfDySj3H+x7nS5vExxl3j+1L8ob21VmYKoJYmgA3JOICkp5p3kjzRfjlFYPHHt8c9 IhuKggPQn6Bmbi7OzT5Rr37OFl7RwQ5yv3bsp0z8o3qj6nfLQN8cNupNV9pG40/4DNli7E6zl8B+ v9jrM3bnSEfif1ftZlp3lLy5p9Da2EQdSGWRx6jgjuGfkR9GbbFosOP6Yj7/AL3U5dbmyfVI/d9y bZlOK7FXYq7FXYq7FXYq7FVKe1tbheM8KSgdA6huvz+WNKCkWoeSdNnFbVjav4CrqfoJr+OQOMNg yFILvylrlpVolE6AVLQtv16cTRvuyswLYMgSx5b23k9OZWRx1SRSp/GhyBDMFUXUF/aQj5Gv9MaV VW7ganxUJ7EYFVVZWFVII8RviriQBUmgHUnFUNNfINo/iPiemGlQTu7mrmpxVrFWReVfLr3cyXt0 g+poaorb+ow26fyg9a/LxyyEba5zrZneXNDsVdirsVYV538iXeuajBe2Uscb8PSnEpIFFJKsOIap 3pmo7Q7OlmmJRIG1G3cdndpRwwMZAnexShp35S6ZEQ1/eS3J2PCICJfcGvMn6KZDF2JAfVIy+xnl 7bmfpiI/b+plmk+X9H0lWXT7VYOf22BLMfYsxZqfTmzwabHi+gU6vPqcmX6zaYZe0OxV2KuxV2Ku xV2KuxV2KuxV2KuxV2KuxVRu7K1vITDcxiSM9j/AjcYCLSDSQ3vkbTpSWtZHtif2f7xfuJDf8NkD jDMZCkV55O1m3HJEW4UVJMR3FP8AJbifurkDAsxkCXjRtX/5Yrjb/ip/6YOEsuIK40PX5lANpKQO nIcev+tTHgK8YVB5S8wEf7y/8lI/+asPAUcYVU8ma4w3RE9mcfwrj4ZR4gRlr5EvfXT61NGIK/vP TLF6eAqoGSGNByBmccaRRrHGOKIAqqOwAoBlrSuxV2Kv/9k= + + + + proof:pdf + uuid:65E6390686CF11DBA6E2D887CEACB407 + xmp.did:d4d07395-aa96-47c2-a9e5-d0351947bb0c + uuid:c63c1031-e157-9748-9c58-86481308e954 + + uuid:1abccb90-0c26-4942-b156-fd2eb962e3e1 + xmp.did:58fdc1b8-1448-3a44-9e20-282d8ec1cf95 + uuid:65E6390686CF11DBA6E2D887CEACB407 + proof:pdf + + + + + saved + xmp.iid:d4d07395-aa96-47c2-a9e5-d0351947bb0c + 2016-06-15T14:23:10-04:00 + Adobe Illustrator CC 2015 (Macintosh) + / + + + + Web + Document + 1 + True + False + + 128.000000 + 128.000000 + Pixels + + + + Cyan + Magenta + Yellow + Black + + + + + + Default Swatch Group + 0 + + + + White + RGB + PROCESS + 255 + 255 + 255 + + + Black + RGB + PROCESS + 0 + 0 + 0 + + + RGB Red + RGB + PROCESS + 255 + 0 + 0 + + + RGB Yellow + RGB + PROCESS + 255 + 255 + 0 + + + RGB Green + RGB + PROCESS + 0 + 255 + 0 + + + RGB Cyan + RGB + PROCESS + 0 + 255 + 255 + + + RGB Blue + RGB + PROCESS + 0 + 0 + 255 + + + RGB Magenta + RGB + PROCESS + 255 + 0 + 255 + + + R=193 G=39 B=45 + RGB + PROCESS + 193 + 39 + 45 + + + R=237 G=28 B=36 + RGB + PROCESS + 237 + 28 + 36 + + + R=241 G=90 B=36 + RGB + PROCESS + 241 + 90 + 36 + + + R=247 G=147 B=30 + RGB + PROCESS + 247 + 147 + 30 + + + R=251 G=176 B=59 + RGB + PROCESS + 251 + 176 + 59 + + + R=252 G=238 B=33 + RGB + PROCESS + 252 + 238 + 33 + + + R=217 G=224 B=33 + RGB + PROCESS + 217 + 224 + 33 + + + R=140 G=198 B=63 + RGB + PROCESS + 140 + 198 + 63 + + + R=57 G=181 B=74 + RGB + PROCESS + 57 + 181 + 74 + + + R=0 G=146 B=69 + RGB + PROCESS + 0 + 146 + 69 + + + R=0 G=104 B=55 + RGB + PROCESS + 0 + 104 + 55 + + + R=34 G=181 B=115 + RGB + PROCESS + 34 + 181 + 115 + + + R=0 G=169 B=157 + RGB + PROCESS + 0 + 169 + 157 + + + R=41 G=171 B=226 + RGB + PROCESS + 41 + 171 + 226 + + + R=0 G=113 B=188 + RGB + PROCESS + 0 + 113 + 188 + + + R=46 G=49 B=146 + RGB + PROCESS + 46 + 49 + 146 + + + R=27 G=20 B=100 + RGB + PROCESS + 27 + 20 + 100 + + + R=102 G=45 B=145 + RGB + PROCESS + 102 + 45 + 145 + + + R=147 G=39 B=143 + RGB + PROCESS + 147 + 39 + 143 + + + R=158 G=0 B=93 + RGB + PROCESS + 158 + 0 + 93 + + + R=212 G=20 B=90 + RGB + PROCESS + 212 + 20 + 90 + + + R=237 G=30 B=121 + RGB + PROCESS + 237 + 30 + 121 + + + R=199 G=178 B=153 + RGB + PROCESS + 199 + 178 + 153 + + + R=153 G=134 B=117 + RGB + PROCESS + 153 + 134 + 117 + + + R=115 G=99 B=87 + RGB + PROCESS + 115 + 99 + 87 + + + R=83 G=71 B=65 + RGB + PROCESS + 83 + 71 + 65 + + + R=198 G=156 B=109 + RGB + PROCESS + 198 + 156 + 109 + + + R=166 G=124 B=82 + RGB + PROCESS + 166 + 124 + 82 + + + R=140 G=98 B=57 + RGB + PROCESS + 140 + 98 + 57 + + + R=117 G=76 B=36 + RGB + PROCESS + 117 + 76 + 36 + + + R=96 G=56 B=19 + RGB + PROCESS + 96 + 56 + 19 + + + R=66 G=33 B=11 + RGB + PROCESS + 66 + 33 + 11 + + + + + + Grays + 1 + + + + R=0 G=0 B=0 + RGB + PROCESS + 0 + 0 + 0 + + + R=26 G=26 B=26 + RGB + PROCESS + 26 + 26 + 26 + + + R=51 G=51 B=51 + RGB + PROCESS + 51 + 51 + 51 + + + R=77 G=77 B=77 + RGB + PROCESS + 77 + 77 + 77 + + + R=102 G=102 B=102 + RGB + PROCESS + 102 + 102 + 102 + + + R=128 G=128 B=128 + RGB + PROCESS + 128 + 128 + 128 + + + R=153 G=153 B=153 + RGB + PROCESS + 153 + 153 + 153 + + + R=179 G=179 B=179 + RGB + PROCESS + 179 + 179 + 179 + + + R=204 G=204 B=204 + RGB + PROCESS + 204 + 204 + 204 + + + R=230 G=230 B=230 + RGB + PROCESS + 230 + 230 + 230 + + + R=242 G=242 B=242 + RGB + PROCESS + 242 + 242 + 242 + + + + + + Web Color Group + 1 + + + + R=63 G=169 B=245 + RGB + PROCESS + 63 + 169 + 245 + + + R=122 G=201 B=67 + RGB + PROCESS + 122 + 201 + 67 + + + R=255 G=147 B=30 + RGB + PROCESS + 255 + 147 + 30 + + + R=255 G=29 B=37 + RGB + PROCESS + 255 + 29 + 37 + + + R=255 G=123 B=172 + RGB + PROCESS + 255 + 123 + 172 + + + R=189 G=204 B=212 + RGB + PROCESS + 189 + 204 + 212 + + + + + + + Adobe PDF library 15.00 + + + + + + + + + + + + + + + + + + + + + + + + + endstream endobj 3 0 obj <> endobj 7 0 obj <>/Resources<>/ExtGState<>/ProcSet[/PDF/ImageC]/Properties<>/XObject<>>>/Thumb 14 0 R/TrimBox[0.0 0.0 128.0 128.0]/Type/Page>> endobj 8 0 obj <>stream +HwVu6PprqV*234R04S32P4ճT(J +W*w6PH/H+X)Hwr.gK>W /@.ӊ endstream endobj 9 0 obj <> endobj 14 0 obj <>stream +8;W:dYmnJk$j=`^PKX*GV"-/6MPPhMW4o*jFegTA5n:ROqi. +8M?-(/t#IN>re.=TbIMqYWQK1D%b&pOLGa]H?hKs'8Gqa4A/k;[i&\e-=4:h!/H6BW;~> endstream endobj 16 0 obj [/Indexed/DeviceRGB 255 17 0 R] endobj 17 0 obj <>stream +8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 +b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` +E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn +6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( +l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 13 0 obj <>/Filter/FlateDecode/Height 947/Intent/RelativeColorimetric/Length 90241/Name/X/SMask 18 0 R/Subtype/Image/Type/XObject/Width 880>>stream +Hoi@Hy&8_nyA'?6G3+ZHҥYakOj6gיoU GHII_AgK/EcF6LrchI 2$҆ԘU4w$5_7BQUm"Ť>&k2W$%Nib;Iߓuavտ,HJ \u.&1ٌ^₞@Ǥl_Lrs:#ј,32] IJ7d+65i1$Lb#d]G>&Y=g답*_/*:p.uʙcRIf") ˬ#q4Ό=sL&=(P{ HJ+b~n+cSFsm0'&&cܼXI=3zER,D#0)2=r +I Ә}즟 (9?l?ݳ;݃Q~twoo `41)"g476WxzGMݞx7hpqh{ƃn\ w Zᶂ37{M23>)25Eܩo|+>8q/8m y3=??~wL#I\dΕ/doޱ=Hh|d]ү$*wOc} yz<*\@~R/}}FRHxw G]as &lu9x")`m;=-Ƀ -O8Cmȑ{mG.&?){3];,V01o`it4)'ѭU, ]?b<ݳN=;.]Lں*_w6}hL[I$np+bdjlb46[ܩ0k`I{-ɯG_>]zt8rI_K}4јغOinӭng`HUN4ݛ|yRr #+x/>骞SyXAQȃękfUXDvE#&sBe< HQ%\Ωdg'sǟá:>xQ +!K +W<* '%Y%Vmao!ǩkv>w u{=Q<\ȃ*fƸmqY%ŏRpV{ ueW&)!)sE2Jݓ?ҋӆgohԎeɝiFGb}/g. +,%m.7'FX!TՀITV $y9Iפ?_ȼ0;Wi;h9:FQ]itB)`5yIe[j*lraպS"u 3$hۯNT´A}ٷO.ϡqˤ3!"Iڻkha˳J)@) iq1J٦oչrEIWt+>]hlrW9,-nr_}i#eR=椔 5 ](?"aIK9;z>g9d68 =FGY/Հ@@ 9ۋFJT2[̟~>:ekG<Q2B&M)}YƢ}\ekfeJ#.-3 +iHF'>hd,I#_ыTj~Q5cR`n:s e8 P/di]Ҩm +!g֝V=@nI${%3Tj[ԣ`Į;m$XOT4==Aŵ͆ޭ|hˑ"6XWvZY,{&y` wɵs/NٮMDz3z2 F^ęA r۝gB7hu ȲhI})CF +WWzٶ:lgl7ɃHiJ&/ Ӻg.}C'dD|V֪'9TL*4I]6 x74MVK%X T8ENZ!f8ah@&͚-AsׄOQ"2sO-ʃ#db-tgHIFHVj.Y!х@Cdҕ@2ǯeqyJΎC43nw0"D2ȥXiQϖJ'&:?Ed!ªGIKSيb$utõǭ2'}~dbNct`d K񕨈\29juC ڣy 5,ҧ9.~&g(r\&$zkddjd_&n,dk딝]|ڷт$lw)-H](H&, tHU4ѐxIE$\+Kl֓ȁI*?^^O/N*)iit<~O&=۠SZ0LK578hsZ?5ĬeV"k K +>#gV-?}= TjOK<>Nh auOBnY#qkB)fiQ@ 7@Olo-n 9 =)~erŗzC9z9Ilr&JO-NbW3Ӳh adR<+}D?NJiPJG%:?5pn2TI2v֋rBk &l f'<[wm}2I4KtwH r"!hͣ .:17f͝$Wq`Z Z 'R\%n~2/f|i|a*J&r>-fj@eRc}'yGBvr*'r +>|.WB~0ɱ{H ܌232ɤMRe7r!ic/_Ȗm͇OJ!dfH)OtE9#jԝj'C]aպWf+>KctȁL&rK%SͧH&1'ejuQA2p!Ϟ* UKW?-02hZ!nKO.? ZdzѤ٣wrLI*΍Sі2+TI,5N$6ぺ G7 whQXI4:?5ƫhq-Ǿ(v,vHz&.aKiݵdTOph2E ɤ0J>-zBb8,A6Mgd͝$K I,E[ 8ƶ0yTS rlS]|ѩ+&_9Ezb(Jr2h+ɓ)⇻A!_:۪.%ٱ4E3w~ sOq9F$~EH(M"0>"C|)4 {edUŭ߿/|}e.tD _(^u)TJ 8([E;ZgbDR!TY;g$÷=W__h8pü8SqO㠸*5:Mb)r2`Ny@:iXg*-X=zb-osW̟Y[V$J|!h|$p6V2LRwsU""YA(\A[u0#0j>k6NZ *P$Idv`;K?29G3@/)hqGaLH&)#f2ݥ:"@ +c1BuUU!hB +m?IXqBf=O-uS]*pb Lp=a d0 '%}QJ1kv-E&Y%͇ѓ!L6y֯-ZNſw@ME<V ++Qf1XGbu.AL}{;j:1XM;`m)ݒr2??bӥ^"T.4{7V:7cO n]&IIʴ׭]׳L&ټ~e?618qW 6$уS-J+j &HR#Y(u3C"vaVO qˤg/{Nd* w4~8`ਡODT +( ƃ(rVu6z0F|iU6_Up_ |7//y e26kaE9JTh<'| e\xy(7QQ1Z)7#5eoӈ0g+ۨwCxc XSg")-nkEJN[* FAKLLR7R.LBME#@* +~U݊tEEVOt7U콊ؾxԜ'isjf=O[ZO ((A>&]r"т|f0`A|0/2}+58{:!ELTǝuB1HzGQ0g[|Q_[VSor^Gy?lD$g=?ȩՕLN9 +K*RYģpu0%K'*- lpD ID2MnݾbL)OYׯR3OF"R j"iO8%{Pqs ?Hee}-XJNm\-H/}GϩZ&L/u>7&I wQ) /+P.bP"$N55B]={2`[Hnk?-s\粓y*dqP9I,1k[`^A/n3ՕcVna-_s%YSM{b FǠoZnkE%yt$mAs%Ev]JW^xA Xk0v_KӉ i$Fߪ/u( jLIO%k/Sr7B>Y,770ݙs)]ubQ9OΈL12$jn*2*쾊O&iV"sr+ 2LjȞCxҜJ 9:Jʌ H(hxA&xk i&Iu$6ԕj:.E Q\-^*%V@V;RM]dLW<}*w&Kߊ{-7@-&;. +C66 @9TBUfI[#v1r`,f/5n TLֹޤosIwT&\ߍ#UBXJ&uo6TE-EHLu[FUf +x謖Xz{FEr6qiVd>սl +\Uv^dKCR&p6kڄo@)ɛzxZZfBv5nFC `r{Lŷy7g2H&;x@kASYQC,29Wp +c!{)r*Rj!&#8ˁScM}Zi*H&Mf$\P +Ŵ\Id eDҐIЍF1|CeH ldԬ6i.2K8t׎&t(Q+ZfB*R&~?g4|W~ !$1[NIkqS(T['iͧ4*m~@?>KT)ΕJ3t +dEÀ."!|g_F>";,o)%OQ~Z2FBt-Ŵ=ݗJ)Rbd/}0i +3%f_,%.u;}oZ_`>19)ۂ֙Ĥ b- 2z[&;BEz1i6Cӯ!G9htj9'I#8ˁB!=*t-T:zTG\2z;F3}ZgLhHӍָ!fiVL:,N0EwHVsR I !-U֐L1#4zvB`V|u 4i$\"&^Xjbޟ mR q6H JER/-N&w',͌ƹӿ8!fI|1TBS?D%}35fW̊CȊ\!/5n:VC1@#&R&2rÚ!"H OS&c1[pUyuN'v96c9)Ӿ/I W%f<}*Gz{-/0D֧)T!LSXva?:n[ʜUX% *R&~o㹤w-dr>و'r*+*1=9)zKy$Sp$"ӔIWcQ@&~!AZۑ[W *'W|쌰95n}ăF.i(xyB [(58|i+&Yjhz>˜2m^?fA)\('N,1^{,gI&}<Ǿ dTVԀaI$7XUkt sU dl:OOٯ<2w)6E =Lq<{ hK|7%FE=ikA(#cia78Hw:;i'}h%Z^N^7VҺuQIHNcMft+ +0 '0$:HJ\ ;``pPL=NM.H1Eb~ԓvsK`TO=hwѓ7|GOYZaȎL7e Ջ[녤&Ɍ;b1lx"Iw!}9pXfv`7xgV~π\\zπD-/؁_~ɬebJz!QyrTS蕴.}?]$i;o"):F)(&ۂS^/Ǧ1IG )!bZ1 p8ĕT-@Kp +m crE?m}F!e_JRPF +7b1T}<x4zV,&읲yeTJ=q#cz>)Rv:5[QГO"5o) c^mXyTعT=%o-oK2U~c͠B>(h1*h|:6Ll' ޑ4 =ui7eGo{s͈KjDE1!3e00R,I %y)1]5ά>#Vgr|%vVV>&I5lS<v Z ;RLj/1'{uO +ؐsz( o?;I&mT@L>`|wdF[pY!=;Yk AR;o^2Lh ~_ٛ|GdO q/zRqcw_}9~eiq$i[?~$N%Y72zىSEx1,HDlEg3JJy}F}7)?'|(GoŤ*isFGO=`r3R8)p/MM$j٪u=9(&˜|m5(t75zM'O \]]ITٱ3u0Ǜe/$iF1Da*b1Tzz_W8/wzg'=srV~@?];(&0H1ʿ[P=kW˻zBf`d.d*XJZC^mt.'h V QLqr9wTҺ辣QEx=D19-d!}?d!}3Z#)nmDzly_|1^Nd`Q0l9'0NnbX9T0ZdWf˂8d=pJl#&BsԨA7FDqڇJZ*レT=eSH'YTo4>doE|~ $#&1¾W` ڑ1)د6:'P}O࿠ne*Fرs|q +(iC4P+ $ +cT6^b-4je˷O|zS~?_qCjRr̖E˓>jEk.C-n#E<IO{vE5ӄ&EN& ݆)-:< I'%}8HwяV4d=Yv 1|Dh*; <Okr(Ny%*~*Yy&WA4!x2zsY-L"=\Le5ƔRFI%IF\3_87 0>hq x2`+IǙBh#R8-w*Ó1YeVO[x!mh?[ <$|`2s2l嚽O EҌX)#Z!Sр4Yoy?ªf8jO1O_9O"%z dy.nNY2u5UV\Q~ɲ|kxrd'?apK tE7s!m`jqZv[>hZ-%6}az,ڜdKɷGM)،xd"6T\l,&c<'Uf; +w&B gw)#„S]\QVQ>$I_jJX,\^YSd|'zD%{o1!䅇qx';qڈ>
khYӷ@mwGyxbr ~=ͱ{9hsۈ!x2< !f!mf" e!ONYW,B-%'>,;} ^&CE"OtzK68]dGRfɈt}B)ILN-^3d[ɿ/3GlV&77ug#K1P)^I\D}&/o>߭SHh+<1Iy_uA^ +sMzC*d\'\z1zADd& +9$Y"?LtzK_14*Y|!ԯ)7$URyuf۲/ɖ$yhs:ڏa<#<(){;qSdLt&}ZHy$yyLܘ: ew}\yYj|aIQ%#?xCE"Oq1Nb5˵I~t֌ZcEb-%Շ8}@i?qqN~ Ndl'\z¤.o !جlݜ(B],YoSO&w"0fr +L&\Pby?ޓur!m FZKGbrΓͲc)eE+WqytT?w ]]}["֫K5Ez~J+T3YnO26hSEH'㷢MO$ta0?j9VuK'/K~CcζI-OV/KѸmkҡuΦ)"&㸔]LyĂX)cML=yn\Ω۬ crї'ma5r(E=pu7< + [rd{d7.`w(d;wr(M=zRy +7]=!Ij9Cidy!NmSiǯƆX9r R:wP<+y^{᬴$eYn;ﷂ2^%)uũ Kw Eߊ. UxlidW)I5Ip΍y%gMGƔd Z9"~t]utڵɲ2E#{Xtp7 #i4i>f-2x jL?bGœ{y9k +AQש'=FE4b2&al6>` +hB");Is*QY9c"1鲒z2klvy0 7>`%JN dXn; ŘWO8@g,צ)_cvH$q\ѾM_@%Ƈ؛XBRcΜJcRΆ1xZU]è-9NEw'c뜠I=]c fi~>?!NI&ļfb2 Z8,W䥌e|a(!me?MQH'cMsY*+!\VuSLQ5Kp#}֓mj:SHú5\bØC)I0> jYn>_+c t57*pT̛6=nW%4EQ4z2~+ɜEO1$|T9c^Jt,Ύ9AŵK2Ɏ'+{,uCL/ڻAZ" +d֕MW.oBgDtʿv(uX4Kp}Gߓ-8,]o^ѓ[J^c(NY]$eo h9[ƣ:1vt᥌e| q'Kp4 b>HAB t2n{'ͩ(*rGOӡz>(-ɘ-d6=г.k؉NO&37{UGɓ*Ysgzҋ>XA[ &f.oqC󢔱gs.$e/;?n>.0&cT51}<;(*FOkE;a\.S+S=˖&EǗo3rLdA˿}yb/IE\E"*;pIыeCbuZ ){ٻ #=I_cN|k)rd@Q?h.3Ed^ts*g5 xQX욮&VO䩪(Idt1>ðh[[AEX̕ϺܓyK{./T˱^>xL,Mo'svy[/*|OJgC{::EQ1SDLk%>>Ѕ<I t -eo$7-gHIΆ(&-^^rs *BadVؓ-W*j͉IvHʞNLO:W%_31dӨXܸvH^|Tݓ+9/Hrdz_wq\e?@MQ̙ݙ"NuhEA(iK̙}v(AHQ"@D(WDUlMĎ-'Eyf&٦Q|~GV ?|mzR3|}pӬ3 QJoJpd\nc\7n,Aײmɏzs ޡ22JywoK2/X'& ځNJ* pbR3|w) A7ɤbrc )#f dp[M_&g][ه?@O*v'd5Ä-`˨[ GOFntLλ=95fPL +&!&;=@OK1Ŏ=5:2J.778&$k4RJGL*sͽ֨+IN,Iij&}`K闍sEvDRϿd}OIb_93Da`(r\|@O@Moߎ$gi)R~cb]_+$H!n~F]1GL*vZFi2T'{ z\ARFMbz~wb1Qe5+zRb25em-3_~Ȯ) ֧I/_Ox^JIQOyT=F9CW鉣)fʴRڴ1[Ig + &NA@Ot\ᏻNoV0[%dRїbۤARJwu oX7h4b0$m~a'U:냰6G8XГOГWuNVL1҅ Qp00bIe΍{֠ 6 =q(B:w6Ñrޯ[?^ORLRIv+/gRJ:A{MAOzIN0n/{Nac5H$,+ԓr~ ~OWllfC+0+ГwځZWBA9%gѓ-y唋w-GF)Uk(5?C%;=GLj/bIIzYw~<HיմFi1GL*t\۵ŤH5$gP!||5Y,_;$8hdẂHh C~aГʜ` 2$R 2-D=yq{IeMИJ)1 wT2#&:=FI'@L&ƥfZDRʲ2n%%AL)~(_"H1IW0J W'91S;nZk PJk3=) {=^)&/75fPOG3-#&7@.d{LO\ ~OywtALL%h//8IeN@7sbHbۼ1W2\jn} bRn@z4M6 +'?Ztw +٫0T?^Г" [od% I'{}q{LII6WeVgROS8 K@D\>&;sp6"l\Z= SԞ89c-|~ۘlr\iƌU?D'3&Haaőom“ߓ?wP9Ô"BH$&;m5wI'6WvyCi~tw g#̵͎.p9 FЧziۈ$)&$#})r%R+ [=ړ~t; ՛!Qײ^Gzr}\10Ouc+#C͂&(_HP(D +d!թXKtkcZ*yͅ (  w¯<\*Db,$=OVp'1K.:|/lӥ+i&o练Ɏ[UlL=t _wHY{D'C%A)Cyt)8tDzPˠ|!B!DDEg ̓~WF/Vs'A\U/! +.a{0Ç)zfnڛ>< +.ĕ#_uMLzb)ZOVfc+UA)" +4D')58=26L">^&Ư~nc#{Uҭ' T Z; $U:ri +_͒K 쳷x#LJ4K\4^mΔX][XVBf@)5:'7}OV2L=Piϲgc0Yh-8iҧVk6\o'Nq|$T($)y6Aߓg O"Hb1flsarEtku*F?L$ |)> R ѸoB(L7H>IwUhc}[3;/)go2qCJ= RHOY$BkѧJ6)b Fl{h-8թrbVyZgcF;HҢt@ʰߓŤA#v齌3BILODxRzI;e5 Rkw'w9OD)Ý$)Cy|O[X)7PG$E,̿6D\GLJ_VO,Zhֻ\/of>!Ç6A0ߓN(+MC d.ia1^j&VmXuw}q:ZLa\RM24Iaw ! +yš|%0KeX\vIِ)H{fZ;qRC{ /Dny]&OkꉥUS=l'凯Gw%)H3ct:BxO 3$0.e#PɶO +|XZE_\(ZODhғŔA-]%f.>nEѫpE/zT(ЄOJs-M*_*PEJ}{ Pm3\)W>[w*]d:@,wZ$IQ@97,aNEd$KeR,}jV]RDP($)]ߎccC$BhGlkFbRz@>ZVN|H[l$R=y:QE>HiϯFxbJjVV5ܮyR^ +rk'eG!% :W!G{DNhJ\9\wACl +wϱR>"j'3J)_PKwG&) wZtݠVwgc)ßHaO&nr#󬦲l'3yxY}fdKJdARH9E}%TTҌS4<zbtXG٧WgB'WVD1T}gLbi[wǚS$B Lٹ#VLXC+;ݪd #+oIA{0]ceR(d֙F3$Bxt@V IғVm̴dE!)yJ>D*:!Zy1Mz/PBIf0 Ҏɸ; Gځ*z͹6HOI`)\NW~㠧£aITVߓMӬ$B'ctL&ݪ\Ylik>Wccyh)GՋ`Ji\1W݅JHrmL*w@ʡxѲHzCx /+΅cf%&B_$Gc&/ ɴ.>)=yV`pB_5B#:ʹv't,;fs8KUeD 0p1>$Bhg91jILJup6ꭕ7k6$?:L2zקb`};=R=fyq($&jkeMkoxs9["L +UB.t/MO0tx!Dn}~yLҿV]=2f^CQ_uyp%(I͹䈬!I-yBs95kIAQnԩ{Sѯp篧Gm2=d7R&Ӻ4M 54;=NGc2ncRt`AJxw&ӷ4p#BΣ)Óe6y0)%L^_׫rhe{-G^O9&%4j2Q/ +LAHiL"CɇvMå)30PfۿՂXa0)QqVgsIV^UB~l׋ Gސp3 L4RI9&M?V !$)n+Ye6\V0YO'j Sm,u^)dw>R CҠ/eO 5`2 j'#=%JXHPe9zTw?pf}iTnQntwR)OX"pO%tT&Ϗ]3)$7ePf[pr޸||l5pndғ+ĽH9zK6]mŤ zͿR8!>u=oD!h`EE}^:zO)vNVB%Ϩtg13nՅvkj,2Y'@]>';qQ)dzٳbT O? :wu\hvߟ#zٜl]CV rneu&w{LJw'R-FE?!R/DJrI$d<12,REV%U<7g:fg^d`bcꩶ[:+7>VΫq ҴP+}coVD0+ [uBKZ~ RyUs.++3UgD0:=/mCԁ*#iU}$]9e88 ?N!"::-_:kúXQG9zչ'Iy\8R*KƸ/!~Tk4NFIʞ.9])3#ByoL>{cRnn[n7V>)WKRQY#UzO?'s=Рg]duC. R> +'}nMty!׸/0y([v7t%OZ`bsI=P'L'ol3{%RJ }&|DQ5M,$)KBcuq\B銞SV'Ofw57'H#RΘs9'R/)Ȗ1BrTk,pY +}+;B(Ř ɯGE'ts+ mF\wO;KlTȒ_SOcf@=-M//:GnW?qPgE9oO%`^ xm{5Hܞ^Ĕ™M:'Z!2Wƶ4ro+nopEfDjtKГAbk&V=Bj%ܡHIc8gRchJ\#YYZi\\h<]R]Q_^vЮPv7>>i <]NlskT?'P5mIF@OU)xUaT}#F;B@ =~_ `rm,Z="]H ?jjR*~yT TI*{zԢ,HF +W!DdUb;<޵s[#Vۦ-Q|_緍Thwr+PKR*B@E` kR.Itiai^ArȥkP_ڃbDJl2ˣfgIrlX?gw>X<}"W +*e Oh)5ЋPŅ]lxh7&\B{ԭxhvRzE,Y0C>yF UJA)_~D7DAA$;)Q&%AGt)K^y4ν* o{MO8pr\r@x"Bqبrۑ]JƾИk7lJ){'@oJMNJ"\ Fc}IJӴq%M d* ,l(:KU+͌'ᏏGJ)23,I#kLZ 9:F-G'\Aθnqޒ`w.kY=ʆ7 ?>:ϕJ1+4V(*il"K!ʓ2G9.]*ӱU@)[r7]>cےuLDMbV [FQ )zeK W2|2& %n0I]4ukǢYNfh;Afbke2$op푮^J2\2޴ )HR>}yRE*oyd } iAbI_ :KUli +d4<@{d΍b}rJ4E7l;|@*w&$!ϧUke2t-:] +,!߼l]}~ =oz{֤X5Q$>eL)Lҹא/] +Og6*beC>7WH+#bX&8)rXu$|RGmkÛVlR޼lURՙ+ڑB#0UIEKأ9~,bԲ surX`,Pvx#Er TUUnMt)NOsT,pa]~C@0 蓉-#$Z|f—)E\%.rolϛ͂ / ߍbE2yL{L& "t{~@C"a(R +tRuf:NReؚ3CQJXkl cfS,hICc=u0_Wfk>knL1ז^O> ~Q'tz`'#W xV +t`O=?7F{Nvfowvv*QJ*0 +D?ޙa B J_$<z;i{wF#e={\&C[r!7&'kn¼~Ѻ{]2 @ *n{Q^Qw+eǔwT>~',U)+DBGbe!z/E"-|tʌWXbvF<6NHP&?pdrA[_Wm_ +5?&PF1J'3p|R]]9M]9LL2 Q +LrHP<ɤv4ΒV^ZYv?`vFRB(M(  +H4JoէX)Ϣ G)<Ʈ@C*p&̟\q7H&5UQ^Z^u-R)E7?A|^u60H%LϐORКr{$$A@$n|^v$zn₰WSo_Z[sSrdRޛ>||R +% +X3J*%0|,ϙ"g,39!+\JdR"NtgQ^ҊRlr?R)i'a,P * Jycq?DVI1? IM<(.-i[-gb\~{ ֟!ɥOZ:,Ø9{ٵJ:36pVݕII- o޾Ѳcݷ85kk,K(;9g' 8r[a/#<4+, +:VInI(o d^r@ԛ/{w?p_&4(eDRcёD>]+Xkdqj22y{6pdRw S^yK RE)10Kҟ(. E6L, bT!ЕLnTνe%U-V* [}iIX+ٯUBT&C86OʳDP1[]\/&ְUڪKKjn#2LvɩO%JzQΎ~.' τ9+RTDL.tdR>"V+[Wo__w7B/W3 O.9+Sl>t]ӉO"oOB|r +VNͪ^32 X)mP ?sbֺVf{D0/#o`7ΒVm_Z_~ BpSETTzaLtZv2Z)8Jq%S&I?IHd:A7.ɲG=Pkɳ)?(_;YDlgO_!;FJ**e' )2H& zӏz,T=Y]=+eQ/0g"cb|蛲IkF $!YrڪKK4.»5Jj;>i:':nA){;,nXepx}P<4MXyd@=K?IFmr[ŬUT=Nr I!ԡ +b(9qarJans=Iu f'-'Lu,QJ RtRJHEx<'*\aOpw@ӣe nhzuFa·-T_~M2I<L3+vm n a`Vx|€q*&L:t+/,C&MXeUH8mL^UI2IV9{5)uR +ƏA)(#nao$<2cIGG&/Zw$Q ھފ}!iD_~ϾzdÈ40ɴb)+2 dtd}wCxkW 1  >U ~lUH&6(^q10Po=&L_v|ș$+Aer@B6.䉳:/ Jy'Qʹ sx7o}'oLO sSao^<I) -2 +$lWS/`_wt7U6?J)p0g%z*u#"#eDy ڔן8ȈggnW4c[4R~zŰ:,,G L}ʳAHLBoHxVۗ~,9ʛ;`:A)10C|E"Edxvۭ +tbX:OZ` RFyxQh$TIcz78'a0y&'2Yd̟L/b]U/`_w[Q|$wYKRwEWp =NdcTWd@gBDHvrPXx^`aL?$I&Q`OnfY)*͎LL cz$GD<Rѕ۳dIⅉvkLn{yL2U+»=J$FwB! LouM_9[ƵR`#JA }IlVk^ݍ[[$<9Z; z>I)E߆Eܘ&LiÐ/Q/|e0A EߊH&~ȑq?, TUt<d`_3MG*&􏩧w +H\A=xraOjVDEI`NI&Ό8隧ϗ7E69t}y^&+tz“/޽%vp\ԧ">AnB.WC(yS&rt+YHJ W"U|C~KJǬwRŔ!3c[1 FdI2qǐxCo)Bi)LN0&%6$5KL#xG;n䟴T%m8<9%S4o6 I@Kq=ow;Gnۊhх|!`Jd}<v,Yl6I&ozIki[іp뺤u13O*QL#dI'LH;, o+R1 ;ϝ DDX| R&K!R]?y;i'|WKCr0X,>{;P7`Hdt~ìsVBF *`V7'98QN;&Uqk¤Th:0l.0Ϲm>h7JmTEHy R}ӿ_J9pIcT~B{Lh"axov% L"%˛:0Nܮ7Jm7XۊdJY>;)E{d ;L*;[0&|X$Hl٘LD'qYTjd]#mcw%R/oSa~/؉0+ ^&? +\CD}#IgJE>Bm'Rӆ"ixؐcχn' =B3]LItf-"`*E'GI)\R&'vؒNK)¤V3,L*> E}o|| +Dң[+lvu,ޒfZ˭+G_2T$fvS3DJUzDJF+7$; S2[+K}t]aBz1e m л~> R"eΙFc!έ|F W"%FI O)KHrڰ8:3Rƿ$ %R$Z㼩{W"=q t)pmyۊd@}zY:3JJ[F,qB&$Haos\7h=$%L&8ͤj߹4n1ȡ3)틤 % +n^_G,hZm|R0e"<9XiI$O.>j<3!R i^L LrД/15D6ĖmEl6uA]W6<=H"Hk!.I4yIܬEKLj6#k[F|r1 [C YI2ܟ!M]t-u:~lDAVtĥ3LMU߬JI瑒:vT +Q$EmHfDSɼ߽4Z&Q0"v%Ls|'瑒PJV4 h0yd…F e3LV[җZ.)Hm^sH=10 H^;ly,pg/B~\:Ôi| ٘F,& z BF +&H㑒#RʆBl, m+ +L`ڪlѠ6~TK''W"y0i%#gL襔AOI,g~et)AAII(kWu%2?X[E"\NHZ[Ѯ&QPs/Ƞ)_bɆ+Z%\yѿ^% cG_ I҆)щ7dDo·=D >V`%^n_&|l!#Of!!e +D'a?t1<|)zXZgz9x;$"%v)i)юec^$RӔ/7hJd]kUҳg}qOb&3IYJD~Fە30k1.zmE[_=.FZA<#VNɁ.g*|Rܨ=ާ&Ir}dEGwL~F4ƤD&sQ> &nl.]bj` Džؠvuf|c0~̈ Dh_ne6v/r+.& -BcO"N*HsVpIzQ.h% P@ Oq-ko&zcǛwy4;k5$wxPѰ@cl.7x[:qΙPJ0${p!YKVb1`= 5Z-0~),ȸgG)OEI)[K@#$|=+qPODo[kVf'g1sg-gf"p]N@w 3߈%-Y,p|Zyǂiy1ո*DIOu=tNZací( (8s '%$!@6/cAѲ*e}Y>Qc33gC)CB2de 2 ?eGneel LY(Q4:]rD *l= aϼ/_-t쯥,vAh,XM}ow#$D筫3y(% t0b3F+d/O8 &d3cAjBtl/hA;];ICJQJJ d©o-Tz*iʉXF:72'zڬvaf@eJ;}3R=L3/NFL>tZyZb*UVf/9!l{JL@). d]h +V$*#%RJsci$—0R.dL@&@@R+Q,8+δqh_=DXq(%8wr⧟L ڼj,zIQ6ǃj\kNA[fk$^rθ%rď?;s +2 h"V <44^WGúZU6v=JIF. +ẅ́c=M~_ghf]Sɷϩ`6SVOVd-6秋1}ᓈ/U# ??I}G> ;9G'#~,CڹI9=3 z+{ak?qz8gd%$}Ye喱CBN=sXlQOO"~ɫ#旗Y[>}OM ʫߌON 6L_=>~|'ޙOFLYO9ϙ`hg&W$m=4óI@A3+YLYyrAg;5MWځL"~6r+WԻEI0 KVs[ dE-irB ʿy?oGxzt/DhG╯O]Ͼ0&ѽsg#>|Ȩɿ?+V / cAeò1?7|M<\ݷ.I[GK3Sԭ7.J|7ux纇>?<{_}d)*n?&LC+YСLmp$x>}QҘe1LWe^9RgΈ7QdDGp#oU]t;w%ǂF 09IJO"~Jh(^_^Tfm34{ɞ~{xyiIR'k$4; $hY4J D|suIng=UW'#4&+qDO8YuH&UY.q|2ho,J,4҂ک]zTe{lڼOM5ZI'1G+a#5!aa@ʉ͔*ڢGhs'io K0Hr$>,ffQֲm[lE:>"KVF={jٕm (hql!!&$ +g8& Dỷ^/Z,iUJ|β-d@[I$ٿ̀Uո*bw'C7|B4<});B/R^`|2&V溳CI7Rr}Ew `]iiJ @_hF65'7leDőh|[)v9|a6'E̊X @hF I>l'(kYӝeO"=~͌h_VDK#-JZ $zf@lO&F[uΨ \|]T-V~ +$HOkidm'W'sY37%{HVЀT)R04+q`8AW'v_+owQ87+PUOKi 4k ybk:jO"1ƥh_FiEIwsgh@jh)+ T㪨UEKc rI`KЀTOkpʊSK-*|aJȜ7xLO}iz,iU.O|ƂmXmSuF>Er$SMQ\M&> +<}!jqj!!GuW'g!$”P'/\FϩeI+T=FZH3'H~6aF50t +J)H ®A]T*Cp&NlLnfUYT*V%6a50C00D?Փ+os9ؿAtjJ|LGq'l/-~zG7 0OhAW\m5-2V*Z<!Q=dF-V"`R!ZZͿ |;?E*80K2HGܲ,K̡x9Ulu,hOҰUEĵ.d쑭J (h5iI5˾:b-#0o#%E?+IFxl'Qw`4smH*3Ͽ9b#|9.Ī:Id .2}'lY; {oCEM+>#XJ=5k vi-:ӿl-ŗ^ao}^ty`$u-ž+fg+jZб>mHȢ+[wQ>j`"!jU.ZF'GeXM)p_0D[߉+vhh5ֳҵPZyhRUhs|_Wm(ًz%QOC)MMrЫcK3;>;|z2 vA' F;Ͽҳ*G@ΩV,|oakh9> nI4E##ɋD\[4s"%6 *Ap'6Mrg> ^L* uKg>9ڜOя>5,)^I^4sKj[EYӸJSՔ$2;A9+[:hZyt>#kIw.=5-3(zv||Wasrx8qYOIM$Uwъ -k)>%`tsg^. +{0y=k=+78\ɲE*'k_k>1+m;QOD= `7twKKj"T,)'t_S>)yvtp2 v*(lKj"Y+ldTmNwv*'q$me -znh)2ҵ8'VfE">|ڐI0&`@6,{IMd(X\_IO"`ƾa߉'D# `7) ^*R[US$qrأsm`?opW.I]D6rh*s'dJ\^LEgoĬQ솖bsB!΂& +=Sb#VS2H'?]/},6P. +w0iO6si"=[Դ-=ұ7'#_Gp[rHsē%^ lJR +$EFj-3>YM#D?Vx

`t ~9:X%I$i(%@A-#kY>?v|:H$'GG߉ eZ$@QӪ[_O٢+X(z uȅ333dC%H#{0C&iǽ& F,N>y=?})'~fso l?3tb]L$kI;:֙OfVTN|I"qn蓉D>g^mboz%HKnX9`yi[EY2WԔȨc~xLtrId?Βw;}lUנV3'kiJ4'g~/W.$.p}蓉DJ[A`Y/>Cz%QOP +C#-%4 l0VE>љxH$D#=>D += $AYH\4:襑SO|#܊⟞={G.\?}|ٿS+Kڸ'Kz%QOC)JJ6sµ,LT&)Ъ?n8dU%璤䓉D[EJb_yv.`ti- {7͂^z6eBC4G?㙙SHO>u|tr/}A`~ +s}tHOFBx7q!D5z[Z_ϊGG77?EI#^x:{i:<.! |2뭧 oc4Ό lf`̈'苪RJmݒ؀9Ćv~JժR/zҶ `!Y`se睱6Cד< 3 e+vPa>z^dPϦ5%JiB(.vnBn=4f]WyFd~Usd/0_OUOJu"aǰm$%[/MJ(1M*%H Z {X1Ԯ(e4}K1%.ghjR^6'Kj'!f+-ubY?GOb< +8TSsm֕$+F".P(. +Źڬ6:TQߵO"4TJ͕Wr'x(9$ IO= XN=? ++38B0 gS[=%;ˋ/qUb'D}$C*,=\C8/C1ԮԊ@;mx""5r tHoN ekk+k1r#@`>vt[#™ƨ'KfM d'S|%3YBWR/YCDk'(κh +@S8e1[ gY4UrgI(9+ʝ'%ItuKkK8ӤDtT0|vƐ8{bRI6eGX(Z9-A:E1/'|Ŵɲnw4e驒/tDyC=nzu ^<CO / QL#8K&<'A˰ßɋ$;)rOt:D姻e3}lߛDObh{@Rbxi"/žI)(*~]xAp=q S͸CfT>|{1~05$Ia6e?*/W5;glkJ,h,v(uP}J>0:x)[KUW:Rc}?)% + JZ$O|v؟ _ +P 3>o tC, U͂d7; V %gI${r5Tpi`ԓNߛDObZjwW,[\{S󥐏D|~H՗(/)K$ 0"'?nLv$Nv;U 5K$tpvx~Oe=)N#|=!%0#\ vF +sS0Hb<)V:o(Ic\&zb2|1$m$;āko`\}|0O%_߁RȧEr |'Puqn9dԜ;x@߇uZH?Jm K]T{I.;aCk(9 +ji4.;Nꌒi2:dm\xLd>v`n+̿>.ҟTQy$K!߼>^U+qGp)gQ9ݔw6' $惩ڝ ^f{w>Ki8` _~\j07Yf;0,u8l'u:kn 7)0k ;]cwՁEiUQS7b`ޒ*0{򹌽v.ԮOUK)6tۉ-XnF+6bTj&ٓC5S6{;/$_"U2lh/qsH}_  +-vY`+Iѩ"[pi4agi.uR1Nɬ[x_zBRamOv KjbgHvPJQImHoT'iWB?ZF|2.u/S(rc*'}JJvfT"xL_?;Hɂ6eEk nU[_NdQaUJZkшvw1qR +5mYlQlDne6$@ڡO?wd[:(Ԛyo5bxpmZ >ū +VkkWX 2.$<y =VCyY_)*=)$OwJRozj?D?@h|8և77_!xK}rBv6!f'up-0mA J~̀|%G||RWqTmήtkC%n'OJɕXB"ÉMRd|Or i/@#5<֊@/wy |rayxU6E)|/Od^msN̸RvIٙ^pN}I-נ nvTSST>rOZq ,|2J}WB)mVJ`ٞ)ia K c=>r 6qvHBgzf;&_%\ai/^3# {a5U{-铚d; $څu&(ڻ(\'u'Q5ݪ7j}($TPR -)w.G#ʛT&){xf LZeG>ӁVͱBY*kR 3]+U73k[)g]'{0v,6~ ASyZɺAF̞{ cmcS°/f@g~R*ӖZ]I]|ׂO*q|rQd*I7ʞr3\mV""2)vY3Jqq Vs~k}b9EM +dKz9A|X|/㬶#/ÀR*b}LԦVӄd.-uךxge#V϶# &O +.KwfZiS痧2.&>)=bxIǫv|'QMMJ)vZRp_cVn-" aLJ pZe&9 +B/Gne^;͓SufuG%A}C<*xKVߜ('5froo? b,*^uZ vš\>k'_27ɼ<ņ$xt{]Y)V“>ʜ D 8Ҏ<'gy'G&zʃp}0c7ӳDo]BGr "$\x7533> +olMze[nw hyɞI>j[IJ)J"`>enX +EZU%RܨCRe]`&Q0,Oo2L~r ?L8vVS>'"+=r!cTVPv D)/n_) +YʙJ* Vلfتy&R'=KWnH'EUvHD7YIpB0/ZpmU-)BǙг[I_xQAvX Sٵ&($U%cں8$Ϲcشݾ`M% &Iv/ɦj*R2MUjkތ1yH3̐tUsJB˵WGWߗs~x < "<{`8LVѢ)QV)U<}BSTG{XØ~!7J~pOsW֗dy%#Qqdd=a딈('lHɟpL/8t y7>S{&JMa$ )38qf R$~,]r@#,O>LM%~[Mp ~a'N +,gGCO֗$Ħ223؍{UQ0!"z^"eT*'TмM9%Tkժ $e:;__r'8j)Iԫ.]k#8O +ϓpC`:Tjϓu4-ZCIOKÅV7~fyuJoyR]eJsDx2O-vGض^DDY? pUΞ*b6IYq oe Ӳ|x9 7}tp΅ƻDJҴ%-4BDe<7JZsOټጭҵ8q $DAiB<8DϜ9lJ.fO'AP `h);ă\A%vy^;ʀ'J RUa2yr ^njtH_@JÃ8؏'{$~rH\3NH?)4ij,2 Y7Os%Ӌ A1d]-k|p"hQ6ɣTaQYVeUjNo2&I <]HIx2dZ$"]E9H fN-OişbJ|NW~1ӷbS.J_rBP.Def>]0ICkަ1td'h4Xzl)<OR#dy xD}V^v[ZE?MP""arO:%[*/bIr΅~Js}},(:A1@7}| ؃3nhҩ"jeU +cA + 4"E(@cC㝣2H!:ovj'+j' *'nb`rZb$"24RrqpUwL%@`_FJYAZG̺>- Iy *Waq;zGh9 @^ ;[qPAC`5OjZU6EU3]i&IW +PJPpL>L:_HIWi͊ +5U +{2-nt IHR2{r,҉B܀1`u s% L^IJwM./?O¦x6yp8"SgQw%aTB6P!J.ԘsFbJ'\ :b҅E&VR]z94x I(3 <)1 )[*nE u$Eg`CTȠMz1+b;jAeF]/~\0B^j-ڃqK)Td9; KյIFCaH*J?!*@v<·=˄ǮjsFӍڬ Y8|vG=EO@b/FѵL~"a2?h.+ ؕt~ }A)4N=05@vI x2[[M<&^9ӳ^:$u두l$,) \ijāa&F=64Մ>?A_xc$L)vK2bs7u x́'SQ?qoLՅEqD;p +4OҋcHJ{2cr2l'5)Ry<8ϡ"dAqx-,On!LPP` ɦTO\RFr!~F 'cA￙c. ݆cʇ_{;:1kڸ9G(j2fV-9iL7j. ޓو[[E '-D ePv|&oo8>3}=y/<2!/#ړgme_ +./g MA~5xR/Ynne@=c$5!“?iHfUφ8Ucl3]\cZ$<%cat3jؙx2b^HHH"dPejHkX5!\;hGЅ(jO45qd=sQ"y$~zfp3kVyD7%s3/iȜLn +B~ri~?5c2 $HCDaAř$""WW$uwjfvvڭڣfv-P rprk췻!NBw߫OP <}- O[|'O#e#g`RJ13C_^SlFI?}I8nf`N$|@ e,ydMUn$9K1|N=@~{ 太7$ŻI_2FB"l3~n@♨z2|Rا:Dx|r])O03[<+$xa0.uN3zގZk`QS}m>@,5:,kQ0P~"@#!񰊿 ^)\3g%݋N0O|?Z}1I +DPÁ2$BGFťs>7gO` 伍r_`bc RJX䨙m^ CWۘZ=u[͂\ mRJݦֶ̗́{CG>0P KqQ>UD .ҐstӢSV6 &a!0ZtЄ~wQ>$bUI`5`SJ`qmN(`فX{VF)y}U|RWT"< b?<ɾRꑙ-O,_2"ƼZYk>sa8 o +r+9g[9mj6FO&@FZ{->9_b uR +'TYXSpmx5t1۪Od%N?`jb9nyƎDwOe$o>9lBރT1S G%įNL&6'$;ۘXMY L+`" |2;[2}r 3ye -/1 1JY+v"X꫟vC0d-1K$0(\.Uiiڗt ĒeBߎ(i&©Y) UjL6E+Ep%L \!@}co1q쳢VLѥϸy-e>La 9;\  :fYJYC=i[IqK=&\CkZn%a|Ju>~W-m(SJTӼ غdÄDl.탄<' ί".2rA Quj2&jBWb̌}2d.p! vGZb0#~6z`^[<3-;iP0Gne䝒_D*(ֻ)2Rh-܆Of ۳ådWዄ7<r7x9KrPTY'~a\ުPY\zllfLt'v"<:Rn|BrKbƊ%3˔[_Dr*#B}ĩR_!/ +]bfi"p~}SL<'(%Dp)"`G~) MĬ5lkz9o'hHpoW|>"WyxbjZꁣڍ&X?B{O¬V'u~` zb3Ta0AC.B@.9ȱjIdªH bAJ' +|;d!I쓻b0[K家т>Uʑjۀ͚Khw+VN-=ƨ_SgM 23Le0/!׽ѫx$#}k.b+9@BRJ.O-cv{e ߛI3㓐-—>UJ`J +Z\z服`/~κMTQ)=p HoJ b!Orw?tTŇC"b3L-E!r[FCWI_g4d`}]yyjIIddV&m?Ttwb`vTJ،96!=i,Ҟ;ƨqi T/8L\KJ`s:=ީ'z PG>9PF +tC9O皇WI=f~"Xu>:63;;n3>Њ<"*%,Z-.;гv`Vrʈ__:\?HO9S[sKf^p)UDxɡ +ꑙ&5Ԩ]U.D*K&NWl3|M7呜OTTO-Fx?֢hG bm f;M)a%5̮$y-5{.@&A)W8üܝj=P+?vu܉pHʷ)Sdœ t ԿE{RV \ܧw(xXEX5 ~OBHO鑚/دۧ;Й1sZiW*AY+,i)jʃ" +< ‹ng:w7}v:ӝo;l _');-T[L)AGuD5KO   wQ@L0Rj$vϜ$ 긣dV_𹒚- #l5VOJܗhr*-:*LW\VA_x#?(fouʊglkԖVRp0 [1Hv}#eQF5 òGRf!C1 HvY CMƜoG-4ƿR9үx'MwL?! +veGT +^txZ`vIr@ 1P^A]t3snZ9zO*O>q*eɍOB,0LfZmq'SVuǖ֡&Rj= =aٳәqG}'O>w`&mI6d`nāRid!`KaS^x{x/c瀵_]=ٲŨpqÙ_pN:XG`z·uIwEr?0OadmjV2D炝CnE:r\IMO"&udV01wJfrc"<Ò6ZDT ĔW1eqN8{մW~~'U'ږ-/xA*hxKrӓ6UGR;@!dFg0 CK-w@5%&ГlJբ>aՏD f7&'Hi NF-iWI77]>RYiyEEqYM +s_Iǘ'JO⾑[q#%O#V\/HRDa)dfhjoeN[CBy9zRM)`w>/ %I LwzÖ ⪟9p_x;ieeHuJni]tEJ Яxő&4'E {BvhZWyڌWM.ozil2rw#> ;YpQuϪ+>&ܿۗM[1gt,1湐Tjג"ela`F-xJI$-d2'1OuHd(0LNeEnrow"jS<$e:K ? (1hNpxI2i)̥]oU+Mdoa 񻸄16>)a?R%]E8)€TI=XVd) %EJVXp.idڌЛL&^ɇ9Fx 2)Fv_|d#΀xcrs̵sXd`6ҟ)fMÊWl!g۴{RۤQ (H=߶:(m/ 6)-DS9H`OJ SĤ +)AdH LXZwyøEKЛL'jjE/ &)6*sę|,$CJ`v1Rk݄'%$zRK='1L2)+wO"JSVs$'IO҆I65Gd 2cnx'udV/8<4_ &5RZCDOrJ8kcY)tFlEA hT9mr^9MO6MM{O +'?K6H2$li0gmN:Bk"%& +X8rKfãÒ2-wsh9Ȓ U6!KR<<^B>aBIk  >Ʀ%W*aKkջ)h7'k'G x>2Â{f*v@ReK쮨L@43Lzj Jyd/nj[C-=/$~$+1N>ۯ+u>?1Է: Z)g,'S(XJYO7!JI_s6R:L\,I9n?'CVOMN)iVK` d3ZA@)gY7QʷtۓIԢa仇>Pܟ&\f4+ѝtj>iyIJrcH>' vvƨb|#~z"!)7O(5SycPJjteO9C5n@)CɢH䓞j5-8 +oH\6_?৖ +AEdR r+TOnגRTY%rwͅJI_O,^cId(ʕɞNM[0r3 v;wŁJI⌎<*D +-QO^g*J= \gyhTԕh$y(VC)C;V7wͅJIΰEg|䓥m$|2 eS$`LW)w~RC!c)eJO)[i_)I554<|2䧂!zIݭ3UY3`CR̒$igC(𔲙/oi}rkQ{~C'FOpj|OR4SsՆGk}ROSIVƝT?N+ʱ"Td/ojd畒<]}u(k _m@>YI@&CPnF:zL= nJ9 ؉~82Rc\ʏ5:fe _Nz߶bKK>ڐ}^ʻ=`LE) +ոͩ.;'sRrO^)s2t"CwUuŲ^cN譛g^p9H*XxhyILa55GO ڻZwE6УS(a Ԝ瓿jT 'Hrf= Pŕ&KwR1rعW)ê"ƽr%>'7h$4)*DjDEd@v,7L *%MLո} ,8'AT)ͅo|-fR)],E|2 u'|Hꁻd?F_P)Տ|lwɲw6UJ}Э&mK'i*>83.āe(0 ČWJFm3;ǝ#~}G\J)m-:Yt%8'O_ pLW1Aabx +%RqY(%m~ apink_)%II_)9N2?'Kl[* |2%i/ytTw)2R)eatV?ͻ$tPJ1֓Mm\Њt!܋f˚+ ^\өX2e +LyY&SJ27.H+*1; ܏7JIAѷ~3lGy'=O;kd $JĻ쓝 %f +K@) IcUR#O|\);i(d= pm\ ?5Sy[@_R铕:AbR +۩D֢vV)rmjhg%dKJ *ھ{ @3RTThyYi(/H wg}Ma餔9ZcF^槕R$]?~RM~d2}23RD{S+q.4, _k#e;l˚HX~?+'Tr}0}\`JIP%ftW3"$LD $jlN9~O"2{U7!TQ.AsS%ftŝ +% opV 􁧔RnǙ3T6(ja?!%QO\m&@*(m;Uaq(e |qC?VEuN?,Fc.>rs3LcaH*tԥa ^{2mέ L͕+/ɀՆLSftq_o'ϻ+J ekO>ד>^~oGy2wg |2H%dVy[kzhr)~_f;wՇ,:J +X\H)iN/wp'*>'0+O6d݂5sP"H$jIcR.fC3˱Lh`z֢݇TJV)AWed~/\qHRepU?}m F+`y C_ycc.#1r[2iN}5ՌeszِŃLTr8Fk?#d kn'@R[<04.o)|rS2FOvqs[[ .ᓣ器+5rRJ$ApRH(uВ_MYro_bK;z'G&:DGz`F)iNPm?!R\KJ0}8ߙQJ\d%sSҕ*I>92k`͔8p^|{\!y"?jR1JYgsy5TRS"繝zi<\H|rtBբw].;7E,'I-?^?d AYJ)3إ;Hm[O(#B)vn-(eHbi@t9o (e<^/ﰭ)xGE.߅]/Q U[>>֛ +9qD`dIyPJR%)?cY> ePЅh5n 2$򞬯*`j?(9_dXcyf=7jO@*d7HOv!kRd9n-ҷG^/Fxs:91@~6mwQ$03 =-)AJy%kY~ӺOŘ^6$* N lsUU8p /#_ 'Q~PJK'9v:>үuNj#<2rǷH#-Y~65 ϺF,!?<A$~R۹t#̆\.hǔOӌ +Z֛HRzbۙa[]{Tх$5myS:~ I)3 jRg<$'IK4@Cxg9մ9 |=Y9~?$[PbXh IuŨkZbJKj-5S$OyaadƦPT+j_ȏߋ^4w*q^<}yy]=ܾ/pDΈḙRRkA){)&3rϛN?O'$HFW#ZRfG?-XĒps(eƯ&[ZjBe{#~FF/aX?d;3J igQ~h$8ZR܆z Nn{RC3?>i'~A׆~-o Hy}ٜt6Ccwg=730nyOEd).{7?*:20>쟕'JFZ[SH3I+q~w؛{4r@"= zb-<r{ojڪ(,E)P nդřIJFJkH(-Yj!h"~0qDT|ӞkJ=U +lÆHFO=Ac7RNk%7F֕FWE%Ώy!EL)Cwa)K>˓&e= Q 2@(!$xPCgۏ)v؄4& ~!vپtR3XY0vэQ'gv4 Ő<%{kJ|H; Α{F03ΈԾEiM +hT`HN90/,9cka َqvЅEqH#uۀ,X;: R>R嬢p?|,c ד02󖢨T ?/: IF{rgkazX,E^-)Ja"ŜHF}):^W{)zZ\i|wmL +ifɍIp q!,iT*X6},I[G"c59G6@BOvV E#)qeȿb;$[/p'g"]8WvJ:HJ;5)z֧b3C"Ͽb[S +ӭ?2g+XiT\$$lR_0(LʠRu_Nt)Hȓw8Qw0uL ӾEu=x43ȋ8Dq #9䘱3˵_ʷl ,qe}>l)+K  +JEFYxGðZڼp6//g}C՘(Y0vf8'ILp<B*ZQYK×{A "HILeOw8?^:'Ӿ|EuPG;'Iq;6'QdvTɝ9~dmֿ,,&.I#:YvRRiJQ@1)1ɹYIJ GQ}SC6`ȋ8//ۚ2Gɍ # l0BUq2,qܐ?u&<N";^RHyDR&MTs"巆aΚ}({u12< ߟh1Oz98L + ՘ X>egQQLeNVH Po$dzHa#f#YdEB- ߛCC*ƈi/ ,S;3KuiQFqo(u/ '%h԰՗ۛ()F+eȿb;3 9tl`ܚo*KERXv~KRB^((Cօ]ey R1^lfj_RШ 0/,9ckȵ^|~@,S,DXF@A ʒ7,MH2©VL59^{-iiǮbEB%۞fhHb{r؞DVx,1e79L,]g%_' bO2O"<,}u}<8_"ߗiE޿VO:ug몴S2ރ(+/+=m(x&P=K쳔| TqtDܹZ)Uw3ĤLIkAQbd-!*a^\0ٯ64Πg眝G[1vplK*ef]#!گ +F|Oo6riwȐhsv{ɓN-߳e9,1 Kp5$Lh@֤l ?ehZ)_$񗃺Z'ъ3T`0醴*,޿6툢,1rj]dQnpW?R g=r`0E$+HĞ0og NߵACFؙc}4sȏ;{l[D] +7DH;~аLf +Sf 6D~^#eAqnuMx!ruA<(`W8[eWha dڂƤ=uN2,.ۙ@JH3s@_n#HŐ8*kZd:B0("(.3Fp@*))߃eނe5I K0A)^)fgk|1LC0R3Kl[A9,1^_e4YZi^I7QL)'e+c4"Lwƥe*YT$(77׏ "%z{ 끚KPAI%!&H9j R*wzR*E};S΢Hy97D/oN8Xx˒MOlkx76)O ָ/wҦr.8=)ʅ))=_ j(DD-I N+5qT{{xRֈ@M~e/҃*)OJX_raJ,ϼA>0e@9|ϖ%# `paXa6`N=x?3z6|IHiH +}!ORԤ{6XrK H~P.A^ +㨨%Dx`U@4nrEʙrh߳஻ Re0; F +sr KU+m)7{biƬw"X,wrI 3 ak')jB= D;`)T (?et@T +Hhe/ 斗5~,(YΡoOrkW8:<}g7GQ_ކuC4,AtI0RH)úlgŅ\DWXAIIQL%A8>2IXbe)^0"3iC4f +<&)j#H9`za>(jrٰ,*QjL6t4.~XDʷYѓf(*RJ6N(Er>pCC_k٪AX⼙l0lzUh"0}\@vZhz@|?M&oyH9`V_?2WȆig HiQt ]5^#Ð$=ا 3~ͧ)RCx;< >rw ^K}~F;S-ϰĆFљ`oLJ²)j){^(̐ RRԤ{̴ `>aVWUكqT,[Uo 8/(9)ZykUjzOI֢sJFQn "ay#_? "t94_s]0R4R"BnqD%H=NJ%;dʩ|@yUМGr~#R23D_G\*ᠨD9"j 3i>ib8 qRͲ&kaҡ8m3ug=r`vu&r_}X<o8 pΠHY [-SE][5 +Rv! 6&uW,3t9Fw*ʃ{ٰuK8C0p%<'[i>vw& +s.}93e(;=aÇ.4s@_5 ``V +Y\e0I:T'%ybH͌HٽTۄ<BHD{(jJTPR2ϓ†eF7TKp8I9?ɌO3LL9Г9zi#8οvwIxΜːV6j+5UWizjWEj6UwەY !!`\CUO"c}Z. !m`n1$ߙ, ig`~W3g,j.,Xh#&HE׽^,eD8٤>fugy5sR宺_y(iMF2lnL^UKa+;*[.2cڙ%j>TyYI SKcJg)exJ_7HJ +sZG~58cL2a~ɁeRZXa PҖՄ _"!1aRDgT.c 5!`f#Mt'$>0r`9-E* 9 M;6НH')ZL>oVs* +MȺ6v1zDR>Փ.1|(aKvX.(Xfj"C>1L@d"'Fփ(W+ +J|=jR ? ~cU>H[09Dڄ°fX·82ӫ蒋1vJw[I{-NKnp6D`6KNsKv9g{,Ťu +N8W5@/32WP-;E/jRF- ,RFŃ/Zmҙ _lox `cGvȹ!#M4.cg)p31R'c&SA_ Z>&)Oü<<^HJǓ/3+a^<@߲Q |MwuR߹@7`X˅n(i0")K=S'Zs떚po!)1LWtxC 0V-T-vseGPq)]Z@z&3_x3Rj3yYO‰߽H7`+Ii +Mn-MHx .b[k`&]Oz5^B뿓1̳Tu1̻Ik*i!%)/a3Sw@w!f6rݗy.(JlI-e1$O7LpEsߣ?8I^3 g`A)ڭ•T#ud3"0LKWvӓcL50m fHwq`H)[?f l&}x?gr97 Ai3giۏSwOAWUFu2dmb$xj55LS 3R XoT .1v9&H#l1 {*mD:Wn[3Pk.#eI^{?/w%kI[[f(@r8\ td`%}sO:*+++ŹI~ڞJ <<\pZas``]j/OBlQ+jgc~mc?Go;cֿI5F2Ɨ+VRDm=7M +^jRV͉ao6pj#MNb(ޟ ,sT;T\K=('HJ!!8JngDoi rǘ_u* > r;U:;Pg^\Kô'V>ܨ~{{-Lu0lm JDr!pOw +{DJУj1 o + 娟C_+gO'Z%;0$l$SㄘQJ6)N5@A˹ߐwi^`r9._d 24-g"/=pn6 c:) {7lC+?WYz7@W*CUFUu <@#K@>#sDYR}t*xB;0fzH9@[o'#FM|*7ѩ z.njWMJX:|cKjg̓DJ8;|h9JoSBb)X^K=0j{6p;{mo%Ga kki!5q! ;>KI)s$#iM^# ?Z1̳ſt_X,W"b&)T=ej뱺0j ̀Nj'%R[ŏKJ}aVHZ㗙vJ;=aVe)2}.Ul +΅s#%a-ƲJdeJY>UJYIɁIiVaӽK 0;oU0m)Uc6VcΟ8W4վZ`(LiKmɴIai0iPsؒm|M(%@6`1i2+r8@6͸ϻ+ɒ6vw֫]iw0aDJ*W3e09StM+}E[%djj+bilLY=<&>!Ϭ*cF%>X7ɃaXrj]YORu6fat +`鰼!o@U-@ʁLFV{[De%:BgO<><9P>=aҦ)"0lZw6{`U ++ԗ֒oxػ*b R*ŰJId>a0 Q00`RYç=0l'jY"o߃*j2Mhm`)Z !)؍d~q3$ɟ#ؘqtO6  þ3$Aw]`q"6AQ7Soq%jo~<˚e%ɇHiTr|n=@K(aX4ݻ6iI Y'vaؔ*Fp.c+mnG֊j%˂zWߛi"o338n='/;@JI̜0R e-?iI#0,M 4GheEfh$9Z|%|Z#zihǛ= z/ptDLq9iY!Z9s-誴ZZ!/ů Ib(\fOq=m~0 ıJB7E3YW4-V޺,ݿ z?pd=jo^O&wݭjQ]ptysYГ< ΢EٓqR{d OQ zY2 e>8wI2m$G#i lZ| +bFoUpvhRJ_ß<ǕmNyjȸ+M5*+rgC<=bb?&&ޔ,/ iķQ]3d̓kxa0J1+Ŋ:A]՟&jK@]+ |mm>9s3^,_(yYHĨt5}ؐD ++e]iEOyXfA0Ko0,}jݳnҐ2 9ُ ace(Սbt#h)EZ|tQ-_zh|w۰zsrIjZ|]GcW(;Gq(>f13ƄdΓpa5IJ$*/ &ia'mI aXGK1h^b,]~ּYx8(6T.f\ m:X-=FPbZ4>ѓI({ndaبIb0$- Ű za 4ԍNc̈ۦS˗Kõ? }ЮsQ14z4]rO%pZ ĸlPbИ)7'$Vkf=94tb=x-/#n ȟ#YaNMJ4n#݊q\Kj 7C{9'n6\:Kwe'cυu1&)#z=i/ZN)t?|0 Linw )Z^u 8`ؔ*FG8 ( ]ֈqu%ڌgi Zx1\jxC{|7IDb^ϹC#JI J|蘄FA\/%L`cRIEZ8P>;Ma.;nI g"&1,mb:4:.11hLF(9juW. Zط*pd};]9_t|\<\:`9]&a46q=ԞV{ɡ":HJ Ib0\&p.e} ıs H4R74mk_׹|_|9w~\Y.Уբlp$iFaS\%2mg;wתk8wkQaDQgT +>BxFgydI=i{?~>IEؠud~iZ#L˄>IR@y-= rVrzl/fa1+xr[/|/PHݗEo8x45OD??чD(<<|4&cEGNk(j|٦8)$(ta}i\TŨQb"auTIFA) x;T xl_q_[yN꾄bo[N(`^a!q8ч-nwkQ?ߑFF򤮘LKI<$,/rV|Ơ(j86gA'!ٮ0W% ܞhKŕxr|f\AO*s]oDZrPVԸ莡KɍyraO2L wkI%~bUYg͟c~?}5$ ?+e?@%*Eaq41sCC*-\V|kW? fwJ|mFZv(`(Gt#p("1&0 `R=cSb{¡(jxȴ|,F[֯uUٴ] p:?en\ʄ!W _soR&uWb-qY2"Flo.;XPBԋG &W:GE'%pU8_euјF8,c$oô݀hgbqrts[/)p9;Ǥ*V+ +#*T4{U1^\F@Ejݨ?xU<6|Ѩ'gЏ8+=JulY+IP]* Y4Ba]l FbُC U/}G9RK}Yې1btrЈ9]MPDT1Ӫ>rziE-@Rt:%ȐS}l2GňhdЍU=ǔe[B,t5{uo}w.J=WŁ&a DbD+@c܍oIb'YI +Orx_GȓR, %.4>"Jc,mZ +Ew~=|ts||f|~7>򵞖ګˢ[lFF0Уq0w87OjEL <^*)a2FuⰊ_ua(Ƹ3-* r1?%b7Lb2&;ʑ \Prտ~Xk)/z0$ Fc!DD^6w9t$(RyRK`I6Or}"OR+T`E`zЪ@X\7XG##uEqf(%o O$|Q1^KsFi[끐Wցح~xA4)"O03Ƀ<X<^8,>bU=R86gy>Lj 暧_؅dRܘnۀhteOn}sOY[_{uI)?SWsfHa̭rѭޣ4%&H%gIH`I*ODIqZIS.EQΟc>&ŃfЪ,Ŕs)gShZcV[0ä.!W +^iFrLj.ub0 +2FC1=tGHȓ'SSg 33GL0>v9RZ)9H̳̚zhPhԔXS[`/gSk4N:S1T,uTKݗEOD̍{~0!vƚgE'!3 3|3 }R|?]S…f@>)2HBn.Y%"V"yЍX.}3\%\5T#x+{ B:p0%n8=XO@ąHh^ȓXbR}qxVaS-9VDZgä:.U tAt1b7#aܤݬ+tP{r.5{u -eRG'ychcg\R8E%C'- +&Lr{1Sb$E8oS\>N)BBU~PJ4h[WuhҤp~iRPB;qqbqǹlv9>vHy$&  y?z~>me)+qe~j$RZFJѤSkL=x@)v*bhbbLCI@W+6"~رN6;\ +\8ꁋ3!OSk?'険QI}VBa&5{R<)mM{F-E)[JO + D!H _E*߮h$l4.4/Y*ɥpEKXQX%QLȎ'/~7"m(. +V4 +^~q|zh=ԊtT|bzR'7RJk}>z&[`߾]ѽZ9aExS*)&|/?SQi]=µI45 O$E@cfg`{G=7fJf.VFai%@B͔O(1f]7NŮňnİ5=0)}OJl3 MIaf+1){rf{y nV B1/ױ㿛=ٿP1X"G#]B} 1<VLdP\YM VM̓k)hߪ6ʓa#pn2L +oiГL&chbP\bf»ӈ?b|eoϴTDZFaP0h'ꑹ$Iؓ)bİM_s۶mRxR"0 Up0PYՉ&C© R~wh\JJfa&YV,ͣ14%'ErX9qx[英 {glHu:{64|mN>*F>hhYa*JIكIvGRiaC(7oieR=~̭F32icInTIFYӉڭTGvĎvkцweÑΏC&aN;0(n>ab~ wht24.w#T +=j&Zy 7*ѓ.Vޣf3.W$dɏR~,׈"*&=?BD?o gދtԅ]gB?σ YE[ҟdJK hD7bJjF2L.)~bL6e=pl Ё'C{򓥚T&Yn(Z65?Mz.~wΞ=1:=BsK Ђ!(.\Tbf(F ,zR$ I)ѓh'1\$yzTݎ'j_"j"pt! ~;U(qb2@/Qh%Q=TIYI=3F"a>`$Rf*ɛU[F;sf]z봽n*x[c=d8}؀0'=]Qa:S' +!%Ub#$FOI P0E)yٚ0O +wՀǕ/}e_t8C7#F Vj휜@O$`$ݪ6ГEC^ݩ5-SrcRZ7dSI4`x]}Hi#oV)#1II1^mXX[vRF>C8vXp3AmeO;j vx%0l}Oj +uI >hߪ6'1%rbQuwkV("͑Lpɦ 09nt:),G:ݶqɾ+^;RCðuٺQ~~IZ|_ 1j[H!F^P6.u֬lRKֵ>12m{Ntuϱ%qr#ݝ=T'%X-Ap|{>"nyIꪢP(IQ&u:K`5ׄy)goX#!, n4O3FJaF؀1*|L`lq7v !GolN/[Xƈ[v#c}-) +eYJ'P2)v~MMsc5 sI,b>De?(lH\v8oq42֡.pW$VJޚ˼[K].pˉN.-Jp:.}Q(T}ZT +%쓂Ї5c*%R FF3I"LjC悈@2%%zZS-Y$+㕜az[Eǔ6v'"ީ-CBY{J'5*P2IYϚ[jRw7.]$g+#Y?1 LҀۦ_ݹd+F.;EVI&fE.c}|*`JR$࿌ty&"ɘ:)GPֆRI_a}RC{A\#PhRۜBg +_33̭f"&l_H&MwBhJo{^+HOPؕ>N\Q薶cu}? PP6'Eut Q*UBYP("lă|R1n41')wobC*UmȎ-CrS +)8[\ب+&|l  M!hO'qK]jH*P(I:g:FFTj~mpHR%z掯}h/̚쓒;du|R;kgz+eikwL,p/s d2pcn6klx=J-Lcc3MP@t2$yR({d=!5*B^UUQYRR1mٽ~G^<{̟|mgY,^AffQ5*yS(_FEtn%(&Lv!l2EgUq`j(\Iۢkbٞ.d- l:hr'"ީe%A'Eu $qHryEeٖe堔ۿ\g=z虧BˉglhKӖco*` /F=nW]9szv) *z 7A#L2۠Q_sL&+Kqo[_ّўNH:R/zt>vt%B5Bi,|PGRV(UUe[*|`cowO + r ADPoK8 I(φRedЭ̤yR rŗ^.F&k6|nq#R&ȨF~Q*|LA XX8o]Uet-S~|pkC2lsMǥ/P +(:F4BU] ƀF* ޯ?xgק;p} +8ǃ@B=d9a®36&w֬>Iٸd5Ks̭fuMm!X4>!pprl"&C6l|!:>?[tKNuOT6:랈xh$&b7)BG٣F#Q]UrR Vco{/'^=fwIm( b!S[ωWsn]*JhOf*Rr7oi/p3!!܀B +$$.VH+jsݴ$!^ ,4cO$>Ӻ)Hۍ$mzb+ϙuv$c;Gq{k_w9{*ֹsm6.e۲F6{1N&CxLjƬ1R+Dp GD _fS0%A,O ᓐɎ';N8ucǎ=zȑc>j~ ׿~zgu/5HHbih{hRY#caÝJ@&=頺(;\e(eil3DLUC#7|YgS>Ҋҵ>m![5UUu 녈 +,d"0 0&y9`-Ǽ!ˆ&Rp{GYNԯ3|䓟z)|{?zo]u:Oކ.!샓q1-Fuo|:q>ol3t 7\Ds$%+]UX%*̾zs'8NFWR=np\ZJ +PsmA?!3Ҙ~3C!۵$$ŸG'vr{_k (%& {v9 H0r?aL1ƬQ"65OmE8!En?0H&wK赑Fw?{FP?G"'3thq4RH@%< %mZp<aUPQVַ}1'fsLq\Qq0sܻ 44R\ر4Yu8d쳩Р\aFwSl*]#$ggq4< ?7 +uwcݽ\wқxZ|J^:/A0I8+0IfPC H#}1̵k\,#}9F*&TI[iwQ^?6@W[ɤ"U8_jhCoE$.ؓ=ǾʇAͦBŨWBaRPՉzxp>Z<Ơ"ki2YKQ"|Jٶ|9\2|Uτ›Wᒱ<2i߹`oh>FR=Qīy9 YW +pp*`z<&9Lj-}dr0\C d&)d"lc݄k^^sJ.YX#J l%DXיcu}mf9$~VTU52i\+FP;eF^9raKXe[hbO7.KG1ʂfzzDp4cA3 +M Zx2%d?3<]vK r>ak3Xrf%êG?$%r!!/J VEN\6c+PQHչ?H^@-yL%`2/i "ZDWJfS\r 96ܑ3Í`!0(qت2nRCRc%?ﻥnʹfضT^g3i~#l"RJnS'Tpa$ ,cۈb.WuxRi5!~5/M- +(,⠨8 dk{Wp^Uk}eiH6H,G:g"Ckg[9n9<\Ӆ^B,M$$Y}Э&l'DmP-XgR6ǰǪ0IC#_k?rvw؋;ZGmFRs1USG]XΦP1.gu%JCsh!nIr1vI biẆ=1z%ç{;WMdwE{O&!ﺤn & Q{ ᐤT 'E[o؍aV+UZR"" i6 NHJJp`sN>b'Ƙd}0vubh;{#UbgVV$ò?닳[9gK2ެRIҰG}d%^awXgڌT.yNG!b',*!㉓AR ,eY_dl5:*Yq&:c4/][J5[[H6Bp%^2c,I]uIM_B q񋠎6R~p' 8M/:xEnߝ<H2\RW2aFanfj lC|bR^4]^-hi^} ^W0$ |M#ߘ +s' w a/f8 +?|"tABeQF#m4^D8oMӋ 8A #1 ab/3.,rj}>Q6Y6qx fgan`˽2%w`лc}q<#I[[HښTqhN%y-'(v s0)EXVP<o9H"3`.'yoS)-lkT5+QL;(޸OڲFX6j&Xk,k`K IX3Q'd,_H M:ܽ=y|M÷'˪hH +"Щ$KyǒP=ԖeSլ{j{R2L|}qRO~V +XF6UMNJ$Iӭ-‹Yθo=^?=$^z›OnHAO',yr{Ԑw#2lE3R2dw'YC6JcL VNLD[ޠbQI.kL c$IqSn0B_ѓKK(Ey@vsLxf$uH/k)zw|f6sZ𲚎7)ɳg}u>QL#tu^F% wV[y;턩!<dsW !#1kA V( pT2Źp'] F͙., jMjng;- +/`n3vYZTu+jgwya6WSbF fHJ Z|_2 FE8`nT-e1> +S%̻Յd͊3*eϖeSclj'-U]VcEscqpZx<<%]9y1CM'$ez0OOmmà?L#cwπ*!Z2.*aUұFVf,y W\nVbcin{Eظ+[PW;%{ʥ*2tM491bB$v Ꭾ+5d!"0EXh(kfCrZD8#K`F&t%6E}6d:R2;UƴR}[U닳em'/{)+SI6aWgIhR fqy7'-]_$;aݴ ;2C<-rA ,RѐlDZOM# G3[fk lj{E- '<0 <7] ,?fziWSbCP_H$ rJC^^{9uw)IR8NF8֬DJT3ļZ(jT;5ڲWXVIv  c.IZ- +H1WI+=7 +"dGoO8($y4`0c?Y^&jdђFgeT6jV܅e2l Nfbk ɭ`P%kg]>/qWIضh6) t MRR^'#$u``>lGpf1)h2 |r3*2#C([N^-`˽-p@9b$L)KZ^BsWz着ՔؕS EXM <{𭳷xc0 +0"ꌭSIںsgtЊ Ll,5liA$oZ&<ZIYxIV@tI=A||"8򝺥T !#1DgU*` ېZLF*pOK J,|ǚ_[HnfS;ypxWSkV%qVGs%ɾV/Hy[Dl'AO;i=:ACb0al#KUrkmasCjϖ8/r%78_U7ڶ3hՋJ[cje怱f7؎RloY{osƬc_(@8o}m \;3 0h]34_ +뻉9'+||Ig5ȓr6I`Ħd* ճ뽳_wO^w@`0 w?[H$H#`J&R2%xYVB^X%,z +&\eb׀&`ͿX_Ƙo#b\CގJN'T 8Rҹjq0RnE\.$۳ZV.x%גߠ+ڶ젫To.[~Tg I+|kKpBL^(wѮ:A%TϏ{zM.L´Ur4d f`B)1pH 0e [0͵B*^DWfkIzt&]od^ʬQoL0YA h +X%_z7HJC }H{_|raf8! f0#~-5ogj˰2X9˹R6*%%d+z>ԼTtR Q53 9ҜAL[J_wݧr UvUUv鯺g$O;`0gv.,=Z Zv`Y2t$"–p&[DWf ϟR +.-mSc9sNRD nx[<@Fx^@=֒.k0RN'7?%nҋhzG0 fC)mpH6euA'-LZ>R0[H9ğ-$7z*vlsW1[(Z wZPa/y@OGoH8#OʷF#dCb˧.`0 YӼZ*wgCHm.߲e%lObz}ް^HçL|yMJMR*y3I0Rs۔#~b~!S+]Wj"#1-Hk A j6VIGZjQe!ؒlf7uaK`Y/$KJ&|K.%#X_`2d}0W}i}`&]W8rard?{~} `0:ҬXNGVf@S!!m[CJ& +n \r^¾SxdVZh^ _}H<)*$89C?## 'gwϛx.d$\/⇔۲k$$2{}9@˹R6M%SKaK fd5ՐUԥՈp5_N +#mq<  p`-$[7vL0`07XgAga U'HQA*JGG$RǼo@tǁ>y%F?$i;/Q!}B}fH [(MR=jAy}u.)T,,i-Us9_uፐێQ~FUݖs:@mudW#F^Ģ\ l+W`Qg I y (!ܔeZ 2 _(OA}G~mqܧC?z8#sdϫ|Ey~2! 3@R_,#0XWfr:Pm}[⸞O"3bCHKnƵ鼃-< <[tx~O!ECb0P4Eq:\FLAvu. Bey>kx={ɧ^ lc}yGMz\H PJ,=RƸtfPnidb0Tm쀴6({P.:HqNzl8lyh8@l۠#t§ |u!^1B6,7 6*t`%TUsqqsZ#;*9P$IzNp㡏N(L(ޡ gGoEFb0̇3PYf>[e"fQp}d"t`>q=#?Fnp㡟'py`]vOX koVqI( &&u ^K0@l b^۹uhc;׶4J+lC$Mz5Z'- Ǒ EQW56T@R]v*9lDʏd~VpK%U|nRlrg^2uL#)'(BNrv04gƱl %JL[[,)+n,E;B(/pc긿cl}mlss,1j|Iu)^75iN2$EQu~qΣTr۳`rtKg)1~ab%*.j`>Rkթ0Xr lWb=9Bt}l㞎 ȐBE8FÕD&&V.׵S;#>4_ EQԅ9|$= @聭 -"rͼs1dE3bD v*n5!̳?:5 ݻ$v{i,=F2’+d6N >nzw+v=WqhJo꣎i[H(Ҏ7H wI ASj.S1N7`M7TsbJ_ƕ6# +!Ü.Vimt«~P"Sg]C(]*yg?'Ckr晷,eB%e_ԩii@UʴY &X.n,/l0PP0Ca+.;zgu-,:b߄({gE؞((Tv&tMߑ̓~d;8A#D:GRUbpn5ەv긜RapI0&|hs}G-*"zƝ= JqnL^An5ԏ((TҷgW.&\[,/߄btb1>y@g-65ki8<DT =%; b h\1떹R+bkYJO)=*:REQEQ,Ym=z ms&< /.\2”۱>mJʱ:򆣇;:\ns3>4nһVL%cS3 +EQEQuFLXBʊ0&;|UnL2|TOH*1RbiQEQEQ0GOvcu)((9C7y(JREQEQE` endstream endobj 11 0 obj [/ICCBased 19 0 R] endobj 18 0 obj <>/Filter/FlateDecode/Height 947/Intent/RelativeColorimetric/Length 20505/Name/X/Subtype/Image/Type/XObject/Width 880>>stream +H pT/!&_aBA+ʄbJZDhjhb"SlSF[qvBP +P)-7Bd I&;=&_{{|!;Lsw9W=!ግPڇ#=^v^fV$Ax 6G<=\ k˰n3,- gGX+)G`{[-6t 8W{17# 8nz GJyH&AwB tTV \D06ns'Ab G.⬊FI8 dw AZ-hV7%\( "~,z2YlKA bYqM V&u8minO1"`y;A^[7`:58vԆw^Z"VPQ $A.c'nnJm4RD,8>F75RI8; pћ"AрMV7Q#(Ag^PD ,=A$ۂXtSADXtSZM#%A} '&3>A$ u]NgO Z,;ȘuS+\$Aǥ|Z⚠nƩnc; 6,n>DACƔaw 0CKDSDr * FVFw w? GM!Unٕ#A݊a2, CŮ~ :vW74g58hr׹Z܉&j,v&pu$A\ t_7%܆4R8g ?#n#N{1L9# KM?)Txԯm4Q eS/1uC&n>v3>'$x i} RBsNOuSnk GH1>pRfDݦJuS­LOEne9< +]J{X/-0GOKK;ֺW̷Baפ =R2XG8ciLaԾG\},Rԗ)2NZʄs$\Ed\1f9t]RE?%H7%$\QO>KUsj Á]Sb}X & q kR"D-JS2sO}Uj9(%INtSsR"=Hla|9.X@S0 ~}}?X;8bcG[ʇ +|f|k|njMV Ϙ0"# F‘~$K֦F`__C KsM]f[:pfMeVtmdkP!(2K g\^t4饝T7HAھHL%}eu]EȨ;\߸`@7s%\X:]{x~'a,Z=lcx⼑\†n Nd{pnqY8ҕ2rr(Km{i]ذ xx;?GuGƼ#Mo' _qScm5k?bަTpt57mbtSLg ("dw2{J0ۅ}.6DŽ'<20äFFD@pa=x]aO) 've_ pPY+EЦI7H8cdo x".@״lᛇUrB7mDy=D yrEcU'tf<'_21uMq!~pz7Gqc:{loP%ZH:kծJp_ky6\ME}ߍrAp3j Nk˃iqV(8dݔp$\Bk9~{6& %dngZ8A(S7WSdZg AZ=$7`&&X护M5*~Z${S:ƎVBLJ`Y+TH$\gR|%1닶43hXz4J w @(_R `_ g,]ƜrlY.8.lg;jȾX8w޸`oO:ݔpZDE-#*5D^A;LI56(VX 2"sAVãTl1qVx`L2% 4MTદpuwVUn2"`Cpٯ+ sAGETdTQXRP#m5)hՖg}Ԉ $!)f,Q*>ZEB ַG0ujEq@{usIg0ufs_7ZY_z_W9.Or?KSy/?X?'[Ư +q'+l魌~_$ۘ\zFܠ=F_.LkQG+Y I1ӠMå;o]syk.Z5׽FĖ .?Ǐ}Q$^k0p2E~L4Z ^b7k'7-Xx]#]Z\)߽+k"IJj~ނU7:>"p3{+P֬&Y}4cI,ҝ~eݣV_y{&!DqK;]F>pqO׳UQ+fRn r[mEeZv7a񺷛p^k͖wpR{YP^:y-[W!=AvZk^w7nquEٍ+y*+E/lͼn=XN|qڏۏ}kO*lXZu^{Nj7hk k!9n>JTO2A: 8A ߆xm8 Wgqys޾;[Y5kmx] ~G7K~ t"\ZjU\^y[sΜ ~lm}r~̜~=oT*ZKc2 J +에T{aku)\`f+Qg>q,^yײjv}5}_CC9?׍5py¸<{{n9ZFp699%D@7˚qDuX7MA +0PGDrz-oD]][,Sghۃ,o)ZOXSK[j_Md,o) =doٽ]w:I߭Vym: NQ)#5Pya:1Mda +0D3ܩIF) rv7u2Vysr5NZ_kWG,7.W1n*I'cT/#^ "DA7Yy#H^GN(JPysRn@ͽZWm$Bd8bnPy!X,o*%`b ͵vaDm9*k`U74\U䷌*G`R 7{~oỲIFAJYAj'c*ay#Pvs$^e#'$;#%#7Btu /]PY(n6 }j2%5JoN3o*(SQZ@{`%(n~̼*8_$75vQ89m}3e>F7 ȼ2v3NzeVoNƔ7?NFbW6]l' ׍H)Jt!N7i_o}2M\FtIXV:}w7Q{ER*2(D8d,#YV0 'n u vqﰼ8&2M7ew"ER"IuSQ{9'Jt շ틉@;7•#%,o,Hm7as^N~FNzp┷H~2(k'ÔH5 N;Ɉ&ˊD,o,0[$'o$-#4i'c`ck9-#4O N27BVֹ0qs!NokHv2Q{p.$Zb*7'?`y#`y +Nyʼ0 Nz$ 7k'{7uv!D P´vYsv.Jy3wh9#5P%(-+y#d^oɡ7B"Cd\¼2k'qIYiT!J}q#d!7'a/:$nVަ4A7'FٯX2>c:$Ok +a "TZKX3@-DL~sCBؚJRa#jdSaSB=Pj~>?7}uݼH:ӕ8Ps%!kQ}լ'0@5s7y1zޚ: ym184hR3 ̷݃]0@5O u |d TsZB tS%dN=ycՌyh :z˨oy:I9jJym TFM~V /ɡ@N#bnױMJo St>Ղ9G|hFqK>IEou_yvX' ]0@7[Trߦc44 Is(C= T79-fo1rq},*Z Я ଔIۤ t|b- A|i{o畤79ϑ(rq@3;z(e7M>)Y'QynAy: kN`rb$z + 6 ؿɨEqWMz -AYtQNÎz:wEny gI/#Mɯ7Hq|qAJn$ʅ "o47e !en&3_$Cr[Zd3 RQVtJş y8no ~IئɋݧA"<UnƘ8MsљGm gSD;[߳8s GUd'.z$H>#+?G&/j|_KbyBg*y3]p8r T6mܭϝ8st9 skw?"cb4G1k[[[i-rgClPN^P}ŝ A-)Kt[yS2Dl;/#7\QW{!cA2fSߠ*'<A)QB^AOʓj{_l*9u3Z0694zKɈ=kԦkgX5œC/Duj+du#j{\mT W5,\BBfTuնr5Uˮ&,ZcjnEqD5򑩕\F>>qPS,*@n[1⠆+Uyk8*;傥's$d#RS.[T 9Oᅡ 56܈iOEqin5n:9 :b .lq6rX*! 75F0h3X*!/eu89~2*#6}ن~agoqԼ 8LޛUF;2ʼX>Y0R\£*C# ~}1@)kȄARl)U8t8ڏ(oEi`9sH ay|AS#3'yuwѼRR8^5xo.}?Y9GH }C/>?f69^ȣ5HWPNZ,2χP7 Uk]R'*_U|>{r Ns7RUVT J1Em(-'P6Κ#7%6FO%ɏrsλre֐e}^ݿŵQ,rm[%Hogqn 9U$Mw X~LI&M{.@;*BR"J _-e Ax!mX렯{.z?Xo˛t>g\q WIge9frBnI  Drpx+p77A,w41ތ Kc-6{3 dr+nY1@bUJ?ފYf9M2Vj_}*oR[vz[ `9N}n2}ox,7Ie;K{nR=θJBd^`rΫ"8<v{u.0me`BkX!8}x H3Cn  wzop e#8K!7*K%\X~@[AH}xe 8scs nyv6Pu fJ}WZ;I R@?G?| WJPѡ?z`A,oEoEps8zWhPyEFTj#wŕ!|o){n9@?O%ߙiͷr +pNUo1cpMCp?!;24 J9o[o 8)ur> 8ЩʏG7$8\)An^F=3u|lRbz=,p%s[ p}h{3t~7P$翛h{Ki1rM,oMbqEkoM/gi\ko e+1@[fha7Dݑ&5IPE8sְ x91JhiXGQ;AM.Wo ˹x7[J2byCs%Tz?X^]_417CcN9>$%C3W)B9fbo xD՛+N`R9`LυRd7P4w: J AIz&8CN7PJzMo =@+٥xz3t&PuuB0@-'-gi$.1@-'F˥X\JS7)JU54xoTWʢ-11@/ mn*G2 r斆L^pk鳄%s-2Ny(%-PR;: 7tHr]Ku'羷=Io*Z[L.;0@19Ul[B\xèЁK7P󰼷}DeP&-gh1fsK h e)L_o1@7#d5ԕw4}`~Yq?3;33n(Mj6J1zAŠ5P\`$1i0FBQ"4 mѴTUD:ҨW +5|k҆@1J NPlgƶfҟMTo8NV>P'z  y(ɘos|l]k^.dRgsНӟl Ut*P9bf p\&Q߰c3K"^ʉZK%ԛGk^T "zo>8nT2}Ǡ<7,$پ|Ypɸ͠RƛppgH}{%Uo9y/YkZ9T^gXaYqdQ+5e>(HOnorT,8CYR^LHꅽkzϦP: I0Mȏ\552\)uIheCkW5ToL2.fp=@3 '&r!M)4fQE655[\-G\Ћ{ZѪW +0N-8$_])yts\YOlӉX4bBoޅ tPp ˛…\sZ#N˚5[6iHom^=vh\gSr#7Г\`|tPߗQ߄VMc=-4$aykh؟uP څu4%ݔ|`Yͷě=J=>KzwR1j5L҄~[V\rg ;pmU +tR͡Ƴjlor7|HR̦z 7#ۍ@1J"9!wc,w{{j6PjޛGkNcQ[SԪloOu'70jb.7}{s~L4NZ)͡1IB>5[˺(Ǒ@sB.|m{si٫X'_yoDwb+H>Ų-9 +2/jyi$!C|.M{{ , }{si 7-"zt+XC|>mo.5:h.E<ձ7օj!'3x9܃ Grxu8{r}Ft#7G닩t<u!%ߑi,eAT.¥=_Kn$Hհ7J9#l>״7>2CIǣ_!q \>d\ͣx|ojV QuG1]{NU:3u]oy"7K7=i`bxSmIzhٛCu ` ]y$E򹛂}-I#B٭ш_o.M$XFoڝo݌|I06O5ͣ![$źiNϳ $X'Ro`FXGt!' #ycoN|HvK_~4w^62[o>8>ަ]oj˝^?NQȣ`Gp٩6kכKzIv^\* l$X4m^֬7Do`!ϔHͥI`%: V^9 @qi+X'Nt>7ݤUo.Eo`)՛GeMJx[ ެF>_1[uǿ A5lMeH:f` C.50ӦJij>d+ְrRqfP@&E~߷﹵\}=:sޯk'ΖZޑͦRR +X2q etӴ"ݓ +H9{I5+H7*Po|s -=z +N욾!yۙ7G~2o(1Ņ-ۅ U]Ϸ_tPZ;2"˻dPf^OZޡ%#UC3AW{2ZrC}آqN̼nY"9so۰A$B-Y(5Oe)D1o(7)FoLZMo(7w.Fo!詺ϷHF.aPr;[Ho(9?,Dov^>o) +qOƲJ~/(ȾUzC~R_9PvNɿXXμBkG ׬r +My9 +䝛W +꿖9-gPAwo7Tg["W*k3r-v&_HZ4&"Z{+Dʼ".38{;m`˲E*#aT~E2'pN:*+8;'傼}8'Q)^9CS|K7Tӭ%HF?d;P)N%rk*[fqtJd'Tw[,]XIo } .C?7 n>\PQ($ rNlkI߂d>$~g7TWP +ˇ6ͩߟB=$1(#ѴnҜtO*i=.71o'upL{y4]'|βhzĞG .Y=:$&KaLn٭0YpL9 pB,+kUpW](y"D?-qet@x<({aހt^erӳn2zށ{+[3А +(x@-Sz506{xgF?PP9"Q].Lpe۵g +ƣ .3ug[,< ӧ -V08]55刭4O镅Hj+ h x],ݥg0OXl\6 ˔AzK& Ɉ8(lf\"s8(Ƭs3 .3p@c^w? Pp|.<M8|DE88+'\"'=>}`E24tۧn{M9}dB}|? +PxqK~ h.][ '_Z` 8n$2yzZ`\u\$О5pe} ;]p|;I92'\,=qaT"YJo@>;b9q?prrEj^p@R7\8UNCL{ހV8XFhEpc߀<]w\"fY.D UNIȜW(681_⎂d}Z;[7\"(?&.u7Ʀk~o46X2Ercɸk_\,cW2p@E҂79[8]m U\"S_7}ܶ+RkkF,ڔNܺSǶ5q5舷py[Ge,$83VܫWgE-w%ЩwIZmfTzT1ӂki_Z6 +#Q˙AC?3 +"{QiL- s@7V&7;WǞq̓;Np@wgܚkM'.1孢k\\$=KlT\"6qP uFWc}KnL{^pjYV܋osd#XKȰ v@մ@ .8 +6qs-aJR_E!([>)ɰwȆAy9d 39p96J ^_ޤ7{}ݮNi еjz{ZyNM-[8K^׾jj&WAyIz{ӵSZ-)9(_QUw?啘 +$AQ#+X +>x4 "2h;NA* +% a)Pa HA) ;gY{w?;ݻ;{o4Hz%"ADo)E$8P"HH $9Lo8T98I"/!S9R{K[# #xVVX)Aȏ| NUl@p Po&vPfKo|& ]`M~.48qmX͡BQreI(6Lli-c%ob)IJ)70qDPQvm{uJ:7\* 6b 1X_5؁ L4X vb3{#$@D9Z}hp9L +8'%& {PnE$"4)m778&Lwnoepxo%?L,pn\㨷1Ln+5qtA7X]6%xPrɋ `(9gwre';ZvQ.Yt^T$ m7 +O!_k#5ݯ4cH $JIz j{.i&-y1%L.Vbp=ymAlzov>BQrEP̌"Eme^M?#K\ `4`z8YtЪ2:ʑn/qHw7&yU]T]5)ly=5HN\o'`9rsGn&=/l+洄nYMjJc!YC{쒀,J(`b__4߀vMdd`U,{7aZ~ҏ1r~En$7Ħ7ʯ<൦!x\oou~lQTlaboқ|La(L(pݳ2EFӑ-z]\ш`b;$B^4yV}["l{ e+Oã1 *-{#$@@Szj.l 6a䬮TΔa}ܫ)rQ i"eͿ?Ckĭ4z\*ʡۻgo`pSEo>gz>iJbӄzJ|1.ڛGS:qdT.D64b 6lκpy $Ey&ZI,OMUXjL6rw&ܖͻY{tL..]IyJ +sN/ߗs) K.|"T= Kz3H=7t}wjjC,"[_)ZzD%E_/bL!8Ӂ)R&x-wޏ/0rރn ʜT$2fRUWY.{j0y *674[*%gP[qlo.y &}XIFqcҡ53u tFZYE?W{>,Ɇ&q h!7!6Mh1/ԋE%&*z?M&yI]^ TUoeAP6g.$HlΉEy:i NF6{jwb[B?+fA4D{7bk}RLlN?TEgkaad \` Fr+b"ٶ!k=UŦ euɶ/@6UqM8`} ?Mcp̟@Qr3:-f:^$gvcVL.5`,;q(%Dr)g `yٔPZ fTY.3mqYH\[c)7-w'8W&?,Me"{7c Uqb*7(BT_|fUo)d"SVJ.0q!c~^6h D62t!Z4L $z3HE&Tp +Gmd aҲrJVʂ>l̘ n/Jj{Sư5B҇d[$g$;|gp\-AW-<>nT="8 ydt|C}aLÀkA܄3Q1.r?)xұ[V +h3 d t"=T͖ '[wFeK!) R6V +49{Yx} -m?zΛPQ;Fvw(97uUK6S7M^uJ.*cGׄ .$SF\ˌ_kD0;$c .gPBTs1h3NrMACD2<'jp!eOd(AA7cM(/Va*g_i)Wc֜c(k'ە(KԮzy$ 򏭢3C@$0?!vi! +%QSE@EXݒ?lVC]A Eإ +*hE`/ymiiݖ7ܙs?sWO.X}[2t6BmE`6NBnn l@) '?X>Y e ^#S?YB\ܑnƒskl_m^FPB-/+?faP!E2 .dup}R-Ԩg + Y1T.k'Ql o܅φoKll}aW%rɳ&$Rxؾb11ԁ'lO\)d9ҭ)>Mp]7Z‚G':u΀(q) +u$dlM +'wk S-| O;y] +1ԍ]7T5ھGOݸ$kTF`OV|PčM]tMcY }v,n \fcj{.)ٚUŠrLV$B@u*B7F6y2|rr;+\[ιrptrCx!r](P +g=c(1 fB8P +G]d i9++m'AUdQn3%ilrO8Ӫo+9ETOiSWhrFdw3:{ϣlW .6lY{ex!8t_xW>mΈȀ5{J=WFsLhPv$}_RMפ} +˴{Bzr5x.UI&${C[#Eqx(կa9EP\%wEOwS8q'^ӘD٢#AJSTY+v0ZGM0٪]lQm>Pvy$+^D & H}Bp8o/wo$_u.7Zհ 7D-z VwԼuYa0N@Ye囍 'cmt-ƗYkC#01*SuS,٩p܅[C_qbhjF Y.&L0W7O.(Uf?UʍlE0%ݹ@"qy*`@vyIy%YqpF$Xu{9"yFmNp*{BeQ-f^}szs>^D lZu4']G67TN@]6dA3$6(OY99Q#a9q 9\=Mx$U /> o$Co0:')8\٣!=zЎˋ5~Q6#]1rZ1O/h_ ~]e7yasrvlRB$@Ifs3FJ1!]Cmhykr-!إ?ns෣Ξr ONۯ0BBʆt/6ds;t>u>-.3oJM:h$c۶2kEȫ`KE9 +~&enc*wnjIcw5kΗ@,'k;}Y.Z.\\)+;=V~M+\`I)^*-O0 ! AB\u߃7%˵ .}w[<Ċsdr#Go?"Z-6K1 +$d/:0\}]7> +vTUC:ˉA€e>Ś>stream +HyTSwoɞc [5laQIBHADED2mtFOE.c}08׎8GNg9w߽'0 ֠Jb  + 2y.-;!KZ ^i"L0- @8(r;q7Ly&Qq4j|9 +V)gB0iW8#8wթ8_٥ʨQQj@&A)/g>'Kt;\ ӥ$պFZUn(4T%)뫔0C&Zi8bxEB;Pӓ̹A om?W= +x-[0}y)7ta>jT7@tܛ`q2ʀ&6ZLĄ?_yxg)˔zçLU*uSkSeO4?׸c. R ߁-25 S>ӣVd`rn~Y&+`;A4 A9=-tl`;~p Gp| [`L`< "A YA+Cb(R,*T2B- +ꇆnQt}MA0alSx k&^>0|>_',G!"F$H:R!zFQd?r 9\A&G rQ hE]a4zBgE#H *B=0HIpp0MxJ$D1D, VĭKĻYdE"EI2EBGt4MzNr!YK ?%_&#(0J:EAiQ(()ӔWT6U@P+!~mD eԴ!hӦh/']B/ҏӿ?a0nhF!X8܌kc&5S6lIa2cKMA!E#ƒdV(kel }}Cq9 +N')].uJr + wG xR^[oƜchg`>b$*~ :Eb~,m,-ݖ,Y¬*6X[ݱF=3뭷Y~dó ti zf6~`{v.Ng#{}}jc1X6fm;'_9 r:8q:˜O:ϸ8uJqnv=MmR 4 +n3ܣkGݯz=[==<=GTB(/S,]6*-W:#7*e^YDY}UjAyT`#D="b{ų+ʯ:!kJ4Gmt}uC%K7YVfFY .=b?SƕƩȺy چ k5%4m7lqlioZlG+Zz͹mzy]?uuw|"űNwW&e֥ﺱ*|j5kyݭǯg^ykEklD_p߶7Dmo꿻1ml{Mś nLl<9O[$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! +zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)Km endstream endobj 5 0 obj <> endobj 20 0 obj [/View/Design] endobj 21 0 obj <>>> endobj 12 0 obj <> endobj 10 0 obj <> endobj 22 0 obj <> endobj 23 0 obj <>stream +%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 17.0 %%AI8_CreatorVersion: 19.2.1 %%For: (Zachary Mitton) () %%Title: (metamask_icon) %%CreationDate: 6/15/16 2:23 PM %%Canvassize: 16383 %%BoundingBox: 98 -140 188 -44 %%HiResBoundingBox: 98.7919746568114 -140 188 -44 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 13.0 %AI12_BuildNumber: 147 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBProcessColor: 0 0 0 ([Registration]) %AI3_Cropmarks: 79 -156 207 -28 %AI3_TemplateBox: 180.5 -120.5 180.5 -120.5 %AI3_TileBox: -163 -488 449 304 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 6 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 1 %AI17_Begin_Content_if_version_gt:17 1 %AI9_OpenToView: -39.6666666666679 23.666666666667 3 1419 866 18 0 0 -5 38 0 0 0 1 1 0 1 1 0 1 %AI17_Alternate_Content %AI9_OpenToView: -39.6666666666679 23.666666666667 3 1419 866 18 0 0 -5 38 0 0 0 1 1 0 1 1 0 1 %AI17_End_Versioned_Content %AI5_OpenViewLayers: 7 %%PageOrigin:-220 -420 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 24 0 obj <>stream +%%BoundingBox: 98 -140 188 -44 %%HiResBoundingBox: 98.7919746568114 -140 188 -44 %AI7_Thumbnail: 120 128 8 %%BeginData: 22554 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C45FD24FFA776FD75FFA04A4AA1FD73FFA04A754A75A8FD71FF7C4475 %4A6F4A6FA8FD6FFFA04A754B754B754A75FD6EFF764A6F4A754A6F4A754A %76FD6CFF764A754B754A754B754A754AA1FD69FFA8754A6F4A754A6F4A75 %4A6F4A6F4AA1FD44FFA7C9A075A8FD1EFFA8754A754B754B754B754B754B %754B754ACAFD3FFFCFC9C299C1997476FD1EFFA76F4A754A6F4A754A6F4A %754A6F4A754A4B4AFD3BFFA7C99FC198BB98C198754AA8FD1DFFA8754A75 %4A754B754A754B754A754B754A754B6F76FD37FFC9C99FC198C199C199C1 %99754A75FD1DFFA74B4A754A6F4A754A6F4A754A6F4A754A6F4A754A4A76 %FD31FFA8C9A0C1999998C1999F99C199C1746F4A4A76FD1CFFA1754A754B %754B754B754B754B754B754B754B754B754B6F7CFD2CFFCAC9C89FC198C1 %99C199C199C199C1C1C175754B754ACAFD1BFF7D4A4A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A6FA8FD27FFCAC9A1C2989998C199C199 %C199C199C199C199C16E4B4A754A75A8FD1AFF7C6F4A754B754A754B754A %754B754A754B754A754B754A754B754A75FD24FFC9C99FC199C199C199C1 %99C199C199C199C199C1C1994A754B754A6F76FD1AFF764A4A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A76A8FD1DFFA7C9A0C1 %99BB989998C1999F99C1999F99C1999F99C199C199994A4B4A754A6F4AA8 %FD19FF756F4B754B754B754B754B754B754B754B754B754B754B754B754B %754B754A76FD1AFFCAC299C198C199C199C199C199C199C199C199C199C1 %99C199C199994B754B754B754A7CFD19FF764A4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A99FD04C199C1C1C199C1 %C1C199C1C1C199C1C1C199C1C1C199C198C199C199C199C199C199C199C1 %99C199C199C199C199C199754A754A6F4A754A4A7DFD18FF7C6E4B754A75 %4B754A754B754A754B754A754B754A754B754A754B754A754B754A754BFD %05C1BBC1C1C1BBC1C1C1BBC1C1C1BBC1C1C1BBC1C1C199C199C199C199C1 %99C199C199C199C199C199C199C199C199754B754A754B754A754BFD19FF %754A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A4B74C199C199C199C199C199C199C199C199C199C199C199C199 %9F99C1999F99C1999F99C1999F99C1999F99C1999F99C1994B4A754A6F4A %754A6F4A76FD18FFA14A754B754B754B754B754B754B754B754B754B754B %754B754B754B754B754B754B754B7599C2FD16C199C199C199C199C199C1 %99C199C199C199C199C199C175754B754B754B754B754B75A1FD18FF4A4B %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1 %99C199C199C199C199C199C199C199C199C199C16E4B4A6F4A754A6F4A75 %4A6F4AFD18FFA16F4B754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B75FD16C199C199C199C199C199C199 %C199C199C199C1999F6F754A754B754A754B754A754A7CFD18FF764A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A9999C199C199C199C199C199C199C199C199C199C199C199 %9F99C1999F99C1999F99C1999F99C199994A6F4A6F4A754A6F4A754A6F4A %4A7DFD17FFCA4A754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B7575FD17C199C199C199C199C199C1 %99C199C1C1994B754B754B754B754B754B754B754BFD18FF764A4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A4B74C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1 %99C199C199C199C199C199C199754A6F4A754A6F4A754A6F4A754A6F4A7C %FD18FF754B754A754B754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B7599FD13C1BBC199C199C199C199C1 %99C199C199754A754B754A754B754A754B754A754B75A8FD17FFA04A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A7599C199C199C199C199C199C199C199C199C199 %C199C1999F99C1999F99C199C174754A6F4A754A6F4A754A6F4A754A6F4A %6F75FD18FF4A754B754B754B754B754B754B754B754B754B754B754B754B %754B754B754B754B754B754B754B754B754B754A9FFD14C199C199C199C1 %99C199C175754B754B754B754B754B754B754B754B754AA7FD17FF7D4A4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A4B4AC1C1C199C1BBC199C1BBC199C1BBC199 %C1BBC199C199C199C199C199C16F4B4A754A6F4A754A6F4A754A6F4A754A %6F4A75A8FD17FF764A754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B7575FD13C199C1 %99C199C1BBC16F754B754A754B754A754B754A754B754A754B6F75FD17FF %A84A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A4B74C199C199C199C199C199 %C199C199C199C199C199C199C199994A4B4A754A6F4A754A6F4A754A6F4A %754A6F4A754AA1FD17FF76754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %9FFD11C199C199C199994B754B754B754B754B754B754B754B754B754B75 %4B75A8FD16FFA86F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A99C1C1 %99C1BBC199C1BBC199C1BBC199C1BBC199C199994A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A6F4A6F75FD17FFA74A754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A754B754A754B754A754B754A %754B754A754B754AFD13C199754B754A754B754A754B754A754B754A754B %754A754B754ACAFD16FFCA4A6F4A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A4B4AC1BBC199C199C199C199C199C199C199C1996F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A75FD17FF7D6F4B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B7599FD10C19F4A754B754B754B754B754B %754B754B754B754B754B754B6F7CFD17FF764A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A7599C1BBC199C1BBC199C1BBC199C1BBC199C1C199 %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754AA8FD17FF75754A %754B754A754B754A754B754A754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A7599C199FD12C1994A754B75 %4A754B754A754B754A754B754A754B754A75FD17FFA8754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A7599C1999999C199C199C199C199C199C199 %C199C199C199994A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A7CFD %17FF75754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B7599C199C199FD13C1 %99994B754B754B754B754B754B754B754B754B754B754A7CFD15FFA8754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A7599C199C199C199C1BBC199C1BB %C199C1BBC199C1BBC199C199C199994A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A76A8FD13FFA84A754B754A754B754A754B754A754B754A75 %4B754A754B754A754B754A754B754A754B754A754B754A754B754A754B75 %99C199C199C199C199FD0FC1BBC199C199994B754A754B754A754B754A75 %4B754A754B754A754AFD14FFA14A4A754A6F4A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %75C199C1999F99C199C199C199C199C199C199C199C199C199C199C199C1 %99754A6F4A754A6F4A754A6F4A754A6F4A754A4B4AA8FD14FF7C4A754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B7599C199C199C199C199C199FD11C199C199C1 %C1754A754B754B754B754B754B754B754B7576FD12FFA8A151754A6F4A75 %4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A4B74C199C199C199C199C199C199C1BBC199C1 %BBC199C1BBC199C1BBC199C199C199C199754A754A6F4A754A6F4A754A6F %4A754A757DFD11FFA14A4A4A754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A754B754A754B754A7575C199 %C199C199C199C199C199C1BBFD0FC199C199C199C199754A754B754A754B %754A754B754A754A4A75CFFD10FFA8754A4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %4B6EC1999F99C1999F99C1999F99C199C199C199C199C199C199C199C199 %C199C199C1999F99C199754A754A6F4A754A6F4A754A6F4A754A4A4ACAFD %11FFA8754A754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B7575C199C199C199C199C199C199C1 %99C199FD11C199C199C199C199754B754B754B754B754B754B754B754ACA %FD13FFA87C4A4B4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A756EC199C199C199C199C199C199C199 %C199C199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C199754A %6F4A754A6F4A754A6F4A754AA7FD17FF75754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B756FC199C199C1 %99C199C199C199C199C199C199FD11C199C199C199C199C199754B754A75 %4B754A754B754A76FD13FFA8CA7DA176754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A4B6EC199C1999F99 %C1999F99C1999F99C1999F99C199C199C199C199C199C199C199C199C199 %9F99C1999F99C199C198754A6F4A754A6F4A754A4B4AFD12FFA87C4A4A4A %754B754B754B754B754B754B754B754B754B754B754B754B754B754B754B %754B754B754B7575C199C199C199C199C199C199C199C199C199C199FD11 %C199C199C199C199C199C199754B754B754B754B754A75FD14FFA8754A4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A4B4A9F99C199C199C199C199C199C199C199C199C199C199C199 %C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C199C1994B4A754A %6F4A754A6F4AFD16FFA8A14B754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A7575C199C199C199C199C199C199C199 %C199C199C199C199C199FD11C199C199C199C199C199C199754A754B754A %754B6FA7FD18FF516F4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A4B4AC1999F99C1999F99C1999F99C1999F99C1999F99 %C1999F99C199C199C199C199C199C199C199C199C1999F99C1999F99C199 %9F99C1754B4A754A6F4A6F4AA8FD17FFA1754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754BC1C1C199C199C199C199C1 %99C199C199C199C199C199C199C199FD11C199C199C199C199C199C199C1 %75754B754B754AA7FD15FFA8A14B4A4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A756E9999C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C1BBC199C1BBC199C1BBC199C199 %C199C199C199C199C199C199C174754A6F4AA1FD17FF4B6F4B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B754AC1C1C199C1 %99C199C199C199C199C199C199C199C199C199C199C199FD11C199C199C1 %99C199C199C199C199C175754A76FD18FFCA4B4B4A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A6F4A9999C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C199C199C199C199C199C1 %99C199C199C1999F99C1999F99C1999F99C199C16E4BA8FD1AFF756F4B75 %4B754B754B754B754B754B754B754B754B754B754B754B756F9F99C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199FD0FC1 %99C199C199C199C199C199C199C199C1A1FD1CFF4B4B4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A4B4A9F99C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C1BBC199C1BBC1 %99C1BBC199C199C199C199C199C199C199C199C198CAFD1CFFCF4A754A75 %4B754A754B754A754B754A754B754A754B754A754B9999C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199FD0FC199C1 %99C199C199C199C199C199C199C1CAFD1DFFA84A6F4A754A6F4A754A6F4A %754A754A754A754A754A6F4A99999F99C1999F99C1999F99C1999F99C199 %9F99C1999F99C1999F99C1999F99C1999F99C199C199C199C199C199C199 %C199C199C1999F99C1999F99C1999F99C199FD1FFFC299C1C1C19FFD0FC1 %99C1C1C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199FD11C199C199C199C199C199C199C199C2FD1EFFCABBC1 %99C1C1C199C1C1C199C1C1C199C1C1C199C1C1C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1BBC199C1BBC199C1BBC199C199C199C199C199C199C199C1A0FD1EFF %FD18C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199FD0FC199C199C199C199C199C199C198C9FD1DFF %C9C199C199C199C199C199C199C199C199C199C199C199C199C199C1999F %99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1 %999F99C199C199C199C199C199C199C199C199C1999F99C1999F99C19999 %A1FD1DFFC9BBFD19C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199FD0FC199C199C199C199C199C199C198 %C9FD1DFFC1C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C1 %99C199BBA7FD1CFFC9FD1BC199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199FD0FC199C199C199C199C1 %99C199CFFD1CFFC298C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C1999F99C1999F99C1999F99C1999F99C1999F99C1999F %99C1999F99C1999F99C1999F99C199C199C199C199C199C199C1999F99C1 %999F99C1999F98C1A8FD1CFFFD1EC199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199FD0FC199C199C199C199 %C199C199FD1CFFC9C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C199C199C199C199C199C199C199C199C199C199C199 %C199C1999999C1999999C1999999C1BBC199C1BBC199C1BBC199C1999999 %C19999989999999899A8FD1BFFC2FD1EC1BBC199C199C199C199C199C199 %C1999999C1999999C1999999C199BB99C1999999C1999999FD0DC1999999 %C1999999C1999998C9FD1AFFC998C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199999899999998999999989999 %99989999999899999998BB999998999999989999C199C199C199C199C199 %C199C199C199999899279998999999A0FD1AFFC2FD23C199C199C199C199 %C199C199C199C199C199C199C1999F515299C199C199C199C199FD0DC199 %9999C1992E4BC199C199C2A8FD18FFCAC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C19999989999 %99989999999899999998C175510528057598999999989999C199C1BBC199 %C1BBC199C1BBC199C19999989927286FBB99C198C9FD18FFC9BBFD25C199 %C199C199C1999999C1999999C1BB994B2E0628272E279999C1999999C199 %FD0DC1BBC199BB992E282E99C199C1A0FD18FF9FC199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C1999F99 %C1999998999999989999BB98752706052827280528279998FD0499C199C1 %99C199C199C199C199C199C1989998990528054B98C198A0A9FD16FFCAFD %29C199C199C199C199C1999F7552282E272E272E272E272875C199C199C1 %99FD0FC199C1752E062E51C1BBC199FD17FFC998C1BBC199C1BBC199C1BB %C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C199C199C199C199992706052805280528272805280528989999C199C199 %C199C1BBC199C1BBC199C1BBC199999951057699C199C1999FA8FD16FFFD %2AC199C199C199C199C199A07576272E2728052E2728272E277599C199C1 %99C199FD0DC199C1759FFD04C199C199CAFD15FFA7C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C1999F99C199C1BBC1C1C1999F7575272827280528057598C1 %999F99C199C199C199C199C199C199C199C198BBC1C199C1999F98BBA7FD %15FFC2FD2EC199C199C199FD0BC17576512E27C19FC199C199FD0FC199FD %05C199C199CFFD14FFCA98C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1999F99C1 %999F99C1BBC199C1BBC199FD05C1999F99C199C199C199C199C1BBC199C1 %BBC199C1BBC1999999C199C1BBC198C1CAFD14FFC2FD31C199C199FD11C1 %99C199C199C199FD0DC199C199FD05C199FD14FFA8C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C1999F99C199C199C199C199C199C199C199C199C1 %99C199C1999F99C199C199C199C199C199C199C199C1999999C199C199C1 %CAFD13FFCFFD32C199C199FD15C199C199C199FD0FC1BBC1C1C199FD14FF %A0C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1 %BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C199C199C1BBC199C1BBC199C1 %BBC199C1999999C199C1CAFD13FFC1BBFD33C199C199FD15C199C199C199 %FD0DC199C1C1C1C2FD13FFC998C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1999F99C199C199C199C199C199C199C199C198C2FD13FFFD52C199C1 %99FD0DC199C1A1FD12FFA7C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1 %BBC199C1BBC199C199C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C199C199C199C199C1BBC199C1BBC199C1BBC198C9FD12FFC2BBFD35C1 %99C199C199C199FD17C199C199FD0DC1C9FD12FF99C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199999899999998C1999999C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %98C2FD11FFC9FD31C199C199BB99C199BB99C199C199C199C199FD15C199 %C199FD0BC1BAC9FD10FFC2BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1FD0499 %98999999989999C199C199C199C199C199C199C199C1BBC199C1BBC199C1 %BBC199C1BBC199C1999F99C1BBC199C1BBC199C1BBC198C9FD0EFFCFBBFD %2BC199C1999999C1999999C1999999C199C199C199C199C199C199C199C1 %99FD11C199C199FD0BC1BAC9FD0DFFA0C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C19999989999 %99989999999899999998FD0499C1999F99C1999F99C1999F99C1999F99C1 %99C199C199C199C199C199C199C199C1999F99C199C199C199C199C199C1 %98C9FD0CFFC299C19FC199C19FC199C19FC199C199C199C199C199C199C1 %99C199C199C199C199C199C199C1999999C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C19FFD0FC199FD %0CC1CFFD0BFFA79999C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C19999989999999899999998999999 %989999C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %BBC199C1BBC199C1BBC199C199C199C1BBC199C1BBC199C199CFFD0BFF99 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C1999999C1999999C1999999C1999999C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C19FC1BBFD09C199 %FD0CC1CFFD0AFFC2989F99C1999F99C1999F99C1999F99C1999F99C1999F %99C1999F99C1999F99C1999F99C199C1FD0499989999999899999998FD04 %99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1 %999F99C199C199C199C199C199C199C199C199C199C199C199CFFD09FFCA %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %FD07C199FD0CC1FD0AFF99C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199999899999998999999 %989999C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C1C1C199C1BBC199C1BBC199FD04C1 %FD09FFC999C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C1999999C1999999C1999999C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C1C1C199FD07C19975754B27A8FD07FFA8C1999F99 %C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C199 %9F99C1999F99C199999899999998FD0499C1999F99C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F %99C199C199C199C14A27F827F805F8F8F852A8FD06FF9FC199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1BBC175270027F82727272027F82752FD05FFCA98C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C1999998999999989999C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C198BB98C198C199C199C199C2A0A0A0 %C9A127F827F827F827F827F827F8F87DFD05FFC299C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C1999999C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C299C199C2A0C3A0C9A1CAA7CAA7CAA8CAA8CAA8A8 %2727F8272727F8272727F8274BFD06FFA0BB999F99C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C19999 %98FD0499C1999F99C1999F99C1999F98C1999998C198BB98C1999F99C199 %A09FA1A1A7A1A8A1A8A1A8A8A8A7A8A7A8A1A8A7A8A1A8A7A8A127F827F8 %27F827F827F827F8A8FD06FFCF99C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C29FC199C2A0C9A0C9A0C9A7CAA7CAA7CAA8 %CAA8CAA8CAA8CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA8CA27272027272720 %272727F852FD08FFC299C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C1989998BB99C199C199 %C2A0A0A0C3A0A7A1A8A7A8A1FD07A8A7A8A7A8A1A8A7A8A1A8A7A8A1A8A7 %A8A1A8A7A8A1A8A7A8A1A8A7A8A127F827F827F827F827F827A8FD08FFA1 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C198C2A1C9A0C9A1CAA7CAA7CAA8CAA8CAA8CAA7 %CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8 %CAA7CAA8CAA7CAA8A8FD0427F8272727F82752FD09FFCF98C1999F99C199 %9F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999998 %BB98C1A0C9CAFFAFFFA8A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7 %A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1 %CAA127F827F827F827F827F8A8FD0AFFC299C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C2C9CFFD0AFFA8A8 %A7A8A7CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CA %A8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8A8FD0427F827F827F87DFD0BFF %A8C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %989999C9A7CFFD10FFA8A87DA7A1A8A7A8A7CAA7A8A1A8A7A8A1A8A7A8A1 %A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1CAA127F82727 %5252767CA1A8FD0CFF9FC199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C2C9CFFD16FFA8A8A1A8A7A8A7CAA8CAA7CAA8CAA7CA %A8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7A8527D %7DA8A7CAA7A8A8FD0DFFC998C1999F99C1999F99C1999F99C1999F99C199 %9F98BB989999C9A7FD1CFFCFA7A87DA17DA8A1A8A1A8A7A8A1A8A7A8A1A8 %A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A1A77DA8A1A77DA7A1A1 %A7FD0EFFCAC199C199C199C199C199C199C199C199C199C199C2A0C9CAFD %23FFA8CAA1A8A1A8A7A8A7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CA %A7CAA8CAA7CAA7A8A1A8A1A8A1A8A1A8A8FD10FFA0C199C199C199C199C1 %99C199C199BB98C199C9CAFD29FFA8A77DA7A1A77DA8A1A8A1A8A7A8A7CA %A7A8A1A8A7A8A1A8A7A8A1CAA7A87DA8A1A77DA8A1A77DA7A1FD11FFCA98 %C199C199C199C199C199C198C2A0C9CAFD2FFFA8A8A1A7A1A8A1A8A1A8A7 %A8A7CAA8CAA7CAA8CAA7CAA8CAA7A8A1A8A1A8A1A8A1A8A1A8A1FD12FFCA %C198C1999F99C1999998C2A0CAA8FD33FFA8FFA8A87DA77DA77DA77DA77D %A8A1A8A1A8A7A8A1A8A1A77DA7A1A77DA7A1A77DA7A1FD14FFA0C199C199 %C199C1A0FD3DFFA8A8A1A8A1A8A1A8A1A8A1A8A7A8A7A8A1A8A1A8A1A8A1 %A8A1A8A1A8A1FD15FFCA98BB99C2A0CFFD41FFCFA7A8A1A17DA8A1A77DA8 %A1A77DA8A1A77DA8A1A77DA77DA17DCAFD16FFC9A7FD49FFA8A8A1A8A1A7 %A1A8A1A8A1A8A7A8A1FD04A8FFA8FD66FFA8CAA8A8A8FFA8FFA8FFFFFFA8 %FD12FFFF %%EndData endstream endobj 25 0 obj <>stream +%AI12_CompressedDatax$&?d& C;\;fJ-n[[YUZ(xd&,f #+3q98OxOq< ?wo ~7_{ˏ~/&G4M~Vۯ߼LG{4?oqwo^_^ޡݻ篞g/PWnC} 4`e߱=&7Ô?L/ޣvu&3%Co^|O߾yq7/߼W?>y~^|0;qv~c7/o^a|t/_/tqzWw0R<3ٯOaC)?l/Jo|ۿi[Lݘج{K̬̂1??J[x?~W~NguG|˻FѤS7_ܽD/ H1oɦ >Kz3 3y}vgӭw2IM~?WyOtҺ2!Lj}.6(^{_G<Ѽb |aoSl߿DŽkѽ_bٚO1';=I~R4!o;H]cձW?y=5w7_j|ӷR}To?~_^bqQ>mŃߔ?뿏 Bӛ{U>}|CsL!޽zn +!}Ho_wg߾܎Ͽzz_~˧ߩO n_|=w.̙/|ܿ8~_xNuTOX[˷Zߥfi>$_>ksP3|q-y=?F?!]϶j~xx7/~tS/>\?߿cwwI|+r;鳶|0e> loq\߼3L/pba,H=;0?)vU\) ߀%%Ӧ\ \܌x[f`*nU-LDIo^iSi.繜5Jz7ѵ][*FAiUU*'-VmӯVuY[a^^Zd]fU͛V+ebe7g]k;la]/WkdYԬU)۵Z)*և:YeXtMe@CY#չk)7ܲԓŗYUeLIɭ̍zʵؔF2 ssλɝP-Vx>'O[L .C +S +p7v v)esUsSʧ|o-&7 LNyni̕W*^rdNONtemwp|:[l6#ut}>_\ތXwoM7 usnnnn#n1aoz^m7r*U9զL _^*qSªUq 8R$l!z7M9kӪ\ʴ*ySҪU M*vU̪KS>_֣_WENf]Z%. bXv 2ʌd)v64o򬡙R&)$%JR\)vWL%Ϫ?')WLRr)8K R|)%Ѓֵ;zeY eeگed&G/fdndbN2yw|Q^Z^Jd^Fq``2Ϡ[W_t,yP5 j>H73_J F1aoטt@H tr@x#HuN D;x;p{{@}w & +D5CpqnX. K׌ucq7g\DW);2UnC|Ey x;Rٙ-خv8Pz? gx'H˗v؅0ܮH *`Cld!2r.y 9&*w"6`ټ.b/+>P,"eXalX~PdS }bٛ2<duXE3Ϩr;.E9J\d('}(bs=U-l4W=9"}ZӬYAL6%Ts끼VJ{OX 2=ye[3UCwD翇d?Sԇos<;XԵ?ʏOBF7mLE߰s8҆+ H$" (" tAy\( !DʢJBFǭH|! ,DiȪ4$uN"e(rE"R,RQш‘Q ,e$JI OeSB%'ЈjFĥkK(2Qhؔ|J5t[듖|97nI?FյX4̲P,~)uuxIx" ?4 Qs E6< KCvD1l4맕aýE|r.VOoGEq3- -U +ͪLTTJэEUZ*mXM]JY\9UDi%%2r5=Ҵъ %s(It8i4fCT +a);12y?Ӌ+[ @c&*PV5m\86 ŸV+-7bU[#w)Zf{% 0<@jXp1frb=]9It=G9 [>EXy+3KoJO+ï~QQ0:1L<98Ilj> -Ѿ8Jl+u!3)]Hh% \hB#ڸLo{[Z&qk"fÊmIWCv8x}i؎u'^{߇qmCIJϞ=c@09Bk۱iccwt6sM&;XݸCI1TQoZ@,qx)G7?}HH|5FPH!H58RZ$n҇U}|OV#pDnyu:q/#7I0c (0 ++tǵl⟿Z +R>qV#6Cxji7~zQfd @,zv4./_bS;;c4`&# 4؜6%0ySIRм m{=jP]||s|<:@> lm/zr._S  +_rUXBa3|!<4S<0< Sy+De&W@AAr-^uUjKTˈXōuXᒗe?#uFnfVŜzQ27sPf'z3 mdq<9+r>3_;ug{YʪHNEa십]ԕd1U w]<[ )8c(bb<;]=),IȥId,v+POD QuVoʬ*rk$V_ba_U[X.}7IʲC#rOL,lEkY, En%ʦ"ƞD1V] ֯$XJ:vl`{6VY=+2ҡYAnVط6{;,Y ugeV^am|O;>ͩ3/Յ8/+w J.zꓨW|٘X \bU鵧_o(r = wxw<}e|m>rz|/GTy/Ҡ `+0{ط>WG|/\,O_{ُF-I}34Yݵuw}\&vřTi: }S͍2.",W(^QFl~34mٷ!,ԅl#AiEqڐ9,%#s֡%bn#'g=<r9?3:Iq?:?'3{T|ʞ:/yE X0=QnEk Zi:EBm9Ɍ \:H͕}g02x˩yj4>/=?S=w טiąYpo8&joEI'Oo>sQcb J"cTՎb&)xEUܔ&`dgFzv%ś9rJAKqj6Myt a\~ا~+7^ݽJhgyus]j/iR:SnBL%4#Kq _ KX*8^Kz\e>l/ư7TVrǩ0 7U/ޅA[lH8$&qOTqʅZ%.B5!% KJ)@(|GY:>LBaK\NKyUoItfm6ŭݐ\¦?)NUWe6>%BE6Q%J>q唚mWCyZy"obݮuc*qj+}ZX9>p,JͫoD Dޤ$;;mXGƕl_f#2-dpeQSEN[cnm4;h\; Vymff g,Ju}o14Q$:i\-.ns8ič+9m6VkϚ|['hZzvvAuaG`}t`}YhG_:m8GN84hu*, _ CkA|,|aGWuw#my{"Vފݹ(|مavWJů9jZ~,wr Ez_dxZ_KY˻JR EV 襠qwU)y@R޸0fwnxQ4;9LAǦ6 +wz·2_}q|t0>\v,нe| +(Cq7o]ͷ`['sثj[d˧1rQNت8 ETԑ<}>S|efk Hʾ_>Ctu;,0D9,'%CS9 .N[ZcqaO΃q5Ovi7bE@Om>nzw)6>FaCmq$8I?I7 ,BԽMٻ +M^A*V*#9Փi]nLʼnigLìQ&[,_?5@'Q +oDT7 F\'uY6@  ,[eh>O*r.V+÷h#exZ:i0$3QN +ߑ6V<ϒ^XEH92kV'jX\+KdP8SGYр3ɡeNqV1 e'%:U9J1џP:vlu{L[5*F6Ԯ/+B ƪ-; eфaǓJږ߶<' Oo_KM ]"\EkF0EDϤE Ȕθϔog|VUu^v)H! {'xT:UjCITҒآ(ZJ(Z4KY薮lZJa\"6Bi(D9P{i{::6KOyíO0i5*alX!X%RK努Q>E(c4,Zۙ;^o;r%( />1]3HJW.=Cq Q; JliJB%۵C^ײҫjL[ExDZ䤵 nI˴~ԻzMj,v8'2<9> Oo_êD Oس&F$(0SNeb + +0jhMMop-~v*fPj0<м4A͗ƪ]\ /0)zdp[_bݫjZL]kmrkX6ָ՚.XƬuɨ1i=d.LYO^IS)exZ 2< NO' +O' +xm7?a>Ykpi>d8$\09 f‡B'zgFxqO/e⎭cT1;ɸC0tmojj{ (qp͂C@z Nim)x?NvU!qnܺ//rvEi]ŧMi|T3ٷO'ؤ\L+u6u&٦|t,JtѲK]j=?Q`%֜k,pHyTak5u1'@IQAl P8MJ&7nJiQ<YOdF0DG3yvhCx/ew9ˣ/h-UPhnu&rSwOfc[x^$؝lxqq}csnٖIİX#!uDmw2<@7[I%~d<>WMeuv]a v2K%ѳ.ڟsq\\40Ž[Ѭ 7A 7_,~bN|v#n`^E.'[dՊ>R: ^G=,>($&K}id5VZڨpk ,ٴ 0 JJTgb60rOd`WIj>Q)3G^<-g@GpAYj,D9&# n-ƄA2 m2v'}2(W]c~TQ9oXVͨt?{<% rv 3nl=ثcvL !crU8+}Nߓ%@q+%ǭ$"~(2^ +Ma\Qݙ>Y+|Z[1z[gz[1Ԙ۴mJUr5Y|Vnodw`xNRKisl\7P΍||TGYugnKE$%V`t +6>+j::T\Phel銻PnC%oS5 +YSh +fWX1V *:I2SOBI44!eVk?xܓ'cOLj4-oKYuUh% JLfd-ytu<+qeYmˇ_CUVN`7(<ˌpaN ƍ]崂v + 6<žr[D\,-9n^$ D8aw2\0[ԡz*--ZLTFh7@K`nQ@밆#^MnRD_yrwQvpѹჲpb?McCa7Mt;HsM8)4y%qi$!wb-jqo1Pm+?:W]:HC'tJ!v\Y"fzw)n.(RD +K gѢ_ \߹D!˔] bjQ"#ΐSm2a+|;rudȼ]A>Q?ulR0Ԝn`uEss9 +Q37fQ;NLthp) n)n6WZnE*:]yu}; ӕdqYJZ,=*SZ< JӐH~[͎csߥ ÅR$Azae+̬䜽)Ò)SA$ZܬKV׬q:k)=As<2mS/LtMo@] ?}S>wihx9{JgU|>i?)D=:Kb%5ީ xyN$ȫik. ~s>4P}h/=vY@M~ ƽ#j&btan"k9hj/$s.!OE8]O$opLs*~$neqb:<7BB.oN4;rN/+"V}ZC^2TX5wb!xS`*ffs9 : i.!JUfQ?H*F!uV[6Nա$ͲWa⇰0IRnJ:'hp!bBY +`E;p8O +n2 ;aN(FXg `ᡭXbmZbw k7ax-(ÅS[/|um*][Wm!n;2._=11i8cT]#w-ՇI~ݔ÷Q~^/CQJ Rz$-Hi[Ȥ"k?hR{W5qn UVi+~ + +whpC=C~ӷݿOVrbXݽ} ?9DaSt~;X43a{GOl6O2+o:O?1'?O}t+T>:oZv{{~ywo^?ïD{ӛ7/1y)wo>;=WL?ܿ{݋w8w|ȯZ>@ȒgEYkpZtSacBM~˵)\Y$gq81A`EtY5$fL!Lzv#`!1<5ق6$CkG9`g9A&0=޲;z|ŏM?!0}<&|;t> C0/6R[¾3>8bx 7鈹3]Gg #.6ÄJ*1*ɁDݡ ؚ#{6+#Xu<*ln[T8lǀ`!k_&.GN΄bia&P,. G_;3GxbcY16TCG)D[^6Wc,d:ɓGd1IN'LH0%@;K'Y(G(MO#6%] @PbsO' d$S߆d-J4Rwg4udo+qo5!|~lb>Y Vg= !9P`W^éȝGV` jTZYBؓuZuvd1elYSg#n +}[Ϲ0qufۿdf7_/ ʃs7_ٛ? ojõL{آ!TfEbѝ(xL(2f㴸˸$n +,L"C"zJЄ \7T[ʤd + 6vBoeW]!DW1oHwm%H2kdRq'CՎXZ[i;Fx#B޵] yoeNl3_cD9D/*m% +dބSy]- u•HJbcx[ w?* rYrBʟ:3̑nsS.eUdSik3G>fT9i\h?qsiC;+x#@E:PlXJŽKun|ƛy$ 8C(A]Z0Oׄm x{\F0TOEȓHfwaaJ@Cr`%@|R%RU#>Lo;yp"MfP"0=Xpn |9APr- +23A(LOř\'"Dӂ3 +|=g>/ݡR҉Ѩ#3'7=.{&a1[f^qɷb!qVxT,@m gS+ + ~ +gZh+6,QYޚYռ9>sǹ %ґ?l mm]㽃)Lp轑ChM + SI8\Յ<%44.i+഻2>=ρo?Rݵ41?U58?!Yި&2qňLeLf9m#p\ Kdve~e K¶#}0UOe$|xA*4>:Q[# + LTU$ ֎&&4yr($bx(5M0Ɉprwݗ#-F6<_fn.(`wy|nME&ڑFȄ/;b)q&L{30D>fL4Z$.H=%7}+.Xl Vnp4s. .N:іDkP'_C:QӍŪNT)/#*tNN."PtHݼBsEn>nlG؊ TV7SBH`dp,d kEڴqƑ-@#v∣ؔfAR !D^Kh׊309Pd.WjZQJ`t-vߩkNM7nv[y=Anu =U^d*NIq<4Q) +4֣VԻs9)߲zNWQFm;-:ϱ?3RONYϹ4wJ?Y1F7lֵ, F8J\oY}68j @)7TەTEਟ +ic c&N-pdd?ѭLc̍w0SkY~x;(&68`d]@i U}q@3ŭOʨ4|nނ, xʀ1MO#iڸ&H-nqdq]$v0qTɣR;{#OR?WjJ'$q+M[1MFU7$#C tm!NLa\h{:ldneMq @G׾QKr>2k;AMx_}puB^C?1*!jT-tC"GӰ׏-8`Ǹ;EN/ik0C?* RFnvn~Uh:}|KCpބTmOM4{ޭ8Ix!@cnY`LXV@--4%"Epj|E$GΈdɄx%hkQQomh=mCQoOwN $. +4"Fj0Z̝eHӐKaDcJj{ eY[El]lB8y@A;^|Dڋ(@\▃@:usFtΐ"hνIB/>RzH#wm=Y sΈr ̎\DK墿8 W&:nХb%uV7K$3)ގI^c_SroT#xCAa#4[So+8ո{|*&D +l0;ut0ۙ\L2(D/qpoTWF\ n6!XH/@]/"#RBեx9D +1 fEh5X1FHXPIX X|5^ɓIr=t]F3}7Q0Yuoe%D>9tɆ/Ge#* BBu Vѫ!-W'0rǺ~,!^iTvtItu:+! H{D>GYƕn\/ iǩ'+z&;vʈ!5p O| ߩw{$${b[BPO;y,Ͼ YKfa5;-gLP>]yl?w"CuQ*v>ͫ֋БGqE{'{: +豛ɗWi+vwf7V'˒Qv]j.gT쒑hL~^eY݄6:oCت(^@Úd7^_y“-]- T?Q{jԿYgOcV/ɂ3xo]:y_~lukjȣ^}V%/VQ92RඈPūV Xԓ;Nev=ϻJbOӪwCi֠cû3P.v^z:p}:5I /P'U`fxX;Eu} e?HS߼N"7m&E&<0A4۴-fRny|WF$C2KaR`/v&ӋKD?:\ + DLsL^:~"r|ws5mn%n!#\ +얍y09ġxJxI&0!Sl <ཽAz#௑(k$2JS` L%NO;/< &*0yf0 +XOV:GKoe'o/^wDFFWfn +8ݱ ɉ)Z\ɹxf#~2`gWuSq!nH "74w`>`ő<`z*Po1գguΐ!?穋H#o1)g 5k78'*%PbNHj2pWFpJO^6GA0SXb;VF h=Yt՘‡Ob4Q4d1VTGKN,"6ΜF %3a-W3e^\ zr] Ki +/ƺ7EN)(ꦑ6~,VE VSӢRfn~:}t0%GQ Dj[1"FQQb3Q]?h+&@zLYB +,%Mw'Ia$ Q=uYsTB -ݓU g&TQLQLgyiP6Ǝ}]7iR^!FKBᦢ5Jd2Ɲ:qu#U +H)4v-@BN ifSeO>I"Ntl?Us*Oi4K;!ql%1- "%(ѥܹLoSSᕝm( +֌ܪ+sFFWod,QbdB(*dtI$  F`3fP"0 }̼aiටkp=YS‰7-b$'186h^H6 +Gbgy@h <):o^i&망n( +"deA53cK^3C"xfB+DuŅ*MfHV@ cX=dԥ bkug(]ꓚVI`4f !m :Ez8ӿR@LM7P[y=A + D T C׊Vc}jxɠj)BН+e <*%2.Aܖnb;_G韬蓺VɕVv,]ߠwjڵm?cDtp4Gb@ +(•<j"y/R;θ:Q4rS%f6tw"zSv[NC*FJ""9XvZ4OZYե洣ԏ8^/\NMe4c ݲ +X dp> .hۈ~$t$kUeGʀ<K^ԷxQ$d B(^먭ŞBע}*M694O +XΛ +u,_w-/ߢZ {[;=qUZ*Qu஺/[etl mѾQZ7nv`܆EU vY};ڏ %^VDoɻ^taecDX)pÉꮅm3׭8mP d\K 6oѼЋaNt43ҌGS!xzFж^pݴt3zT9BET"C ":] È@:~$@":$:Jz^.8DׁCt|8D8D5lסD0};-^_  $C@"*_ 1CAB~-D *.D*`Pwa*&`P;P P6Cp~` U-.C:Eoha;px( .N8ZH] P"ҷ~~۱tk(M+%X8ag$ ٿQBT'z7[e 'bh3PW$PfOVtE9JF;z␙q@2\!"y҆Dp-T>jhGV,J;#[4oajRAq膂i_-􍚉ick4>ْ`86:~rѴcu{BXX,VR0HP]0OθN*E >eat <~*6Gۑ"ok ԯLpǖǻ*L{U8HJtRFmKr }#B;Ӷ > +|\oa\=y)t3!-m߈7p@y7E:B޵Ҕ=׼{R?G|?Va$T1Oب*Ed\5!soD 4@.3b$nJ{Us8^}]U~9)-ר>M!:4iR#e6ݏiXeiig#[%.hb7Ϡ=[/abrQkG0^3Y'{: Ė: virwb+VUcն$4M?ʼӽP짠<-1$[&mI1_VbFVcF2i@TkʸLŰA[#4S~^Ʀ5u<4O>NJE(پv +s.MCoELIənܶx`;(VLG+qv^BdIkQdX佗ak4Y|X;;iӍ4h|^3\ĺ&qeߩiQfn~:\l(neGQj~ZߊU!n+*Z3Vbk%Iu3f퀴$݇ϢzYTW$Kn_ $\VbE Xnfa}\":*K5*Z z*[^ŕb:Qw3H`P=)I`M]aU +3ryDgQMtz8EECẠ~ +E{,6A2 xA @ɹkAv*«;- dCBDn}'dхt_"1chFZ@*̓dZި\yřLb +---8 "d/]T̴t>PI(v u 4O8oub){4W`Chr)8E h`hK}LZ*hE4i[4gcj#;DKeuk#i[Ni9sR ,i@`-~k9N.mm]+[Tɹ@tWiߣķϩ{@:Kg~LAǴB_y22O8:}UaFE#b)#N( + ,0+\10WsZpyt{˵ jD$}4E} o~߼}wwᗿynovw_^߿~ֿgx۷o^_:7_m]iFϻ/ٛw n޽}qR0Y߾y|YڛgWqn^QпOw_޿./GEQ ɺQ1FF>gg|p4smYn^۬Ù߉|@c5eD!'1ծ뀑V+Fky>٩* ~ y#ogclJ)+J&H4 +f`E +ZT:УRS9@3%O,#/hF1CvU@uyPk%dK0^;:#x#vbΜ%gǤ_YR'$MhAܖFJÒI_G5@T0{7+\Τ7710 0@,n&V 0RUBn>GRW9E&?Y#Њ +lJFIVE [VW}-^pG|L8Mo F&ЃP=c0a`oM }8&\&)x,l'PX&8gc`RIM$h8t8*sȘV<$XzUAtDTTK{K(f@Z`@Nb}Koiٗ.F|ȀGԈ4тk ɳ`KZ9M5XXI3m"3'!Dk:$$'5A:ы;I0ђ#Hs9虙x3$f +TىVl K+nKv b@LjHE# +&4240=#%sM9I $Q,*WNXYI*J GFO%]POH(ߢTseѲWT<0ƬF&v +FB&?iRa}4J[1Ez.GLLĞ=ܻͻqt[ C5>N]d`Q8nvr-[2o5ȓo";fQlk=V1s!&imI*VpdJ11GA[.T¨heg4UȔ(1qK2g ] D`~K+U։SSZcIӚ\ܬu>B|59F$x8X3=,'(SOHc\|(6sZ3ʕqpǒ:Uh!v%,z8)Js [1I9(zdŅvj>i"eې4NRcy ;j@6WIF})Gbc-I * ̜!+(+oe#BC]Ѧ K*`R/}Wq 9W|.<(8"d譙PɕLU+Y/|d^+¬[I,$ےB L +W aҏe + +/AxC!A]U8VwC !oOsᓗCj%\B[.|&HpxQ3t+d`X)dVǩ +4+GNIeΥ3I bDI `xV4HE=sJeg %h>g8H\;nZne3ٙYL4tR9%\UصءIT|>T~>T*YIxYq;gn0+)["|=8:W4nDL_BQI>lC$;w$tд;#/%~ԥbœoG_0;hAr^9\o'“ +QWT &?H8 5`RoF9Hl Iev#Y B\j'ADUÒCTG|z!VXV."=X>w 8Fi$RJ[JW|_poe-v8GO| !v6*,W`-eIMu_HBRg_T]$:߆6P8|!= +IEmla˴Rvg *t-#dD| n1w81ge/$R`i qE8+e^2e[5ע>)~-~(i"^Yш*W#CJ '~L#?ϸKOՀhJu38IL/EQYɭ@E'R]3D"8Y!.=cVL7I/n[s .O99s}?B8AMQ2z݋RYajQ2u?Oύ}$)%M*CE[Br3:[!Q䗩rd(0-.J9*&rcDC +R1cţߑt0w7E%IKMj7؄^hqIj!HBT (M)j@%$q)dFF! TSF&7DaSGT'S= k +!#.ZUzZi)Vy ~TD܄tYx w &:GX~i+;$2d9&Ee'i! VAY)VQAEṑHTjŭQaW]2Ĭ HdffT7G2ydVgVZ*=IHyt 뢹rq=*7 ~&\jYP0ⓖE +j/P݉2L8zt?PsiFOIqK&W'5!4ĥj(YQ( +XվUPdlV@Dw<%ߜbF=EQ30YUJY9A}7 +jO*ijI[9p>;2U%Dc&ƴ0'NˎcyB *¥$ @k[ _K@(w˚fIP},PǼ(gn$:PIc㘊O0gT@t;)-",$U 1=ȗN/m3x6i^dMf., &b HdIЊQHğ5\j"Gs`c~\|P$=Dz8N4jsM$PJq^GY׈Ҡ4ptkO +} +%EEWg8չƩA]xnY!gŐIYW)¨{D*$q F'wc2xL=h+)WXQ/ Gf[4%XCJ損Pa^04yB +3ٙĊL$-8hRdwrzjsKlWMxI)v3d>J3CnYvܼhv 9z|`1m +`N_7y:x5uQO`̚($C#Q&fMΆuEXRQ`L{Y2gI@a!cհѧK-Kdr08OOQ[ptNL]6,J`I#ĕu=ADQL/@^I|ſGc!qʦL{>ggf0.JR]rOԸR]w 5{i, X]ź G+)%k\i"5(&)TJ' P^f/E Qԍ:5Kk5gPﲑFL55[!_DhXW] ă'$^υyt!D8 +YOlOEa)J2jBޘgA^($p$轕dpeՊłd ec9d_ +PHEw21hH#u;(DMv,ٓ-  ;IdgzW{8C@_al=0` eC<+ܟkR<0pg3"L2bgCh49r0GGu'x,Z0y2jLղ=;(fnzӸWl88@-aF`"Z̵I6$py30ܚ8oDXr82Ռ~Zq&nMDt 0ng=ܞq) l  {Fn^ +4Zc[,kl\bI+(5Sgw!9~qpT40c/[FbʞaRE7kg~D8ࡉ1`V aKb%J*c9dм(=L N7"Lbu9%6W`_ +2Ar+Pj#؂\͍i&YS~IY6e mCۨjk!atZ=x9[m5{z`fyv>C',|du2M JOjߑfI b಴^˥ǐ%0~VjA`Xl_nZCu$ (f_Ŝr}A'V e쳄mgB+ |%v1_#NjJم10tfW +'-L#!<؍IMMΪn0ǟ` cu + n-K#!!?FT 4ISiAKXZ^!TztAwJ6:7~:*GCb!1; 8[]>mS*|)겍@ .(W <:; :զ3 +h8qML(=\2)@xYȫ3{!n ؿ? +mD=ߞ+#QZ)N,czA-\7t6lN B{7qes P)|ïMCcK-8>4 34Lh=^Q HW,7)ӣ3?P8 +!FRd% Hi&v * r*Y6zELS "XG}v `ͿMA7R-̫{f4-?BEf/3~bpGhvcPS|]rߘd 2J9|J6 +m!Dr< K9o)ζ !~H㓃6-$ж|PN *>q<QRpQU?9/amK,?hca&ť6htKwO- G +U!|j l_p$7G:j'Rdq! U~~־ Em;np A*&<8'cQiTr[LmG@^J).bf_yXz|+ǞaV'%Sf'\<]1)fv=%LVe%wb$T*돐Cʉ뱉|^@r|,9Ff g*;7;v-n9,Vl(Z5420 2( νD͗8Q)XE4}<ZM7Jh]5y曲71RddYl=yEpw_L!;!N?P Gk6H~)H$(Ңa~=ЋorUO _7t~o }\vS)uIMaSw }҆߇ORޞ @1Bʰ'p PےZ(<A[lg ym <FumI ! ~mm.?pٓ~4袽H 22w΍kDSRꢺP)a戀~>$4B;>P_]{|WG(tϐf(r>Rǜgˎڵko +nSVfNMԴG<qҘ<35\Ҏ]5ۖt0ɰB;/1'YV4%AN$ErrR:u`+R_.5luG&Bv.̯iUsh_jә!f?PzE2U|\WFU:wX#= +ψ5vW=JEאd;3]助Q2ztN+_`}{"}4*T*QГB~_d)봳4Fړ{f) $yضi9ؿ`W@hMZ,[P B0@Z,a$|3Y4st(?~N!$s0 ͵ +ku{aR9'tv5e +K'S>!1=@tPWfl;Mu8Z]oTXó.?@i d30P^6@(AG`Se'.`+V]ˠ^B|,r:ҢcI]^_T6 tzU{9S5L-H AFce5GiH x7)G~9ї{>&0 +?:2˴elX,&6IGt&s۠qJr6:Z$Q6D0N{+X POM#; +g +2㧢Ѓ^Nω*0sʤ>G VrljIfmP%YU, yB h;+bPi,hvC%5x,=V_Kdt5첷f0L6OL:l[Wk"qƼre̋899C!)Bqpu»!^ҵ HAbB?Ww7 lgHH BW3|[(@"UZK!餍m=V̥Wx`p}{]$B~EjQf9!jBt<ފI \=)GW(fKQŲg.+sC?#Du>zoh.0C0i˔ē&鈀¤|3nJ `.GVk>nX9l +@0*'7v ?b՘ᒏZa6`P"̎垔]Ur'2Q ΨWDH B%}C[7c"))C_Fgl9B s^op"T6 XEoIdtĄ N"'L|[R=8;by!MRZLk&}9EoB1ws44&䩡4"t}n U (as!Sf6CeU3<  3ޖ,8aEv~@3ڑ*BDzL%gƟS mA92@E h1tzE-h 6= |^qD*bT`[BDˋM9W._ Cgz#_iYn!T{Z^ =xYahT<RQ{%"'M86P,vKnr-XJTi2sh2Gaɳj͇Gy6N +]l뫜%[U>C 8L޷8d8(s7QF (a6Ķ)2Li(X +G8x^+g+)}ǯxeQ@!= + X{3Y=aYLRIN+v\)3a +i, +MH^ƒd_w)sC~IPLzfW$dm&*n뫜ThN*m6RT)RŠc U>1n=PKG{ah̼r #moaN#Yl粄~ 术I_3gX Xus]|fAJ̗վ +8bʋң9rK֚FHU[O]Ad xހ?7L|' 8e%1`KQ,S +JytwFWr-ԼgT9ǁ77 MVDFO%a=WV8s{9DUpfB)R|N9E.6݊'5&%5*G*عJ pm}jHg/!I̼޸rqޕ9TVr(zD+="F$qG9mFpU,T·T3s1He>w#,fY\5Whߘ ˆYȽ ^(T=z sUU(K5ڟ 0F9RՆ ă蜉p{nakS-V'q˂Iɧ] +o}un`׵\YZHiQסostqRj{6aΟȡ1K mFvʫRBP%D-Ά9 xg + &\[SYg?Q] {I>xu/e|ڱOBɪXq*3o-D:ET]p?BtuG41E,4 Ż}d()#գKv66o +OX@(X8bZgw@C!'AQ{`w+9qVr6%}L +u I)#Eq&GUӒJCxzC>s4fYHx{DdǒԱ p\lwMgn0.PQV<S72=rj륯pTխQd:35k3;wPgRlx _lBGR׼:TbOqZno6A&F?FyP+Ytg3dk5^l4xSy`áͤWbw}5m:OX:If\i,o%6H2_57b;s7Z~C 8 Or;UMg,{2rfc:Sm?VXY0;ܾOX@u2M,6ª3e3xc 9YxAEIA`YZh`aQ+I?exBMZ0d 8ܾ !I{D>2@T:sYvpDvF>"'oz3ι0\2;zc@fC!!.(zUsF)WcќpCG`GRs^9(-J\s +7\%`7Ľ?#O MMV>żyH|+D3Sry,$GyЀ@@ceJ% =5tn+C'P|oUa$4{,g0 t _}8PHG PbΛS$@Ǣ*_A%$I)rmD1׎ʶFe^;^F‘|ȫ|7B<쉀.-- +AQe|(UXjno*I!CuԐEVAd\d[V%$M-V;C <36لdi$s̳8ȅ(ߧ5y- dnѽW "^m1$d,_zDD9Ƕ>4#XhD;uܦ$Ks9MGjToRn_Ds??O?ӿOO?OOO޾ͭ0'Ϸ`<១6`V1g ܪn17Zr7ŭa~|f{ 1S=V!osT^!<!# +I#*6(g}|щ|BȺb+[)>$;fqZvBIUsB0W0HQLslT Rg/?/zKԾ{#'A =fy_ݧo(?[.&ʧ}! 틐09 +a__$Z_ )Y=LٞG}okzJWGz)o#z>HBd|垦2oC?W-O˷<#Iʀ~6Ĵxm]<4O.)s6Wl[KG'xoξH}'Wݻp+Avos.~tqs6=| s~wB~Xryw6gj˓rmŎ"0ۂ} xf;'BOu>=hKPE:t{o/u1#D.;q2]N jendYG!,aq[m L1}QRP.C̲|>-tyahJG8402~~'woy| DQ?% 1QSnduh5OhZRQ9 ++t/ksG55x/FQ6J7lq((Π4~8/moct: .? /d`!_>+/Bnz1uNi[s!˰!Afy[ _~V۶ܱ[Y9nQ2L^ї!#W~G/ݮ5(}O%0"(߲oyX3Đ|C7!Dn4\m&ᔿ NJa !X?YEŒf]"j7Fp;,=/u[{7.OG<[|0.Sy۶jpaEnZOnS +mҝZ<'Y yF~FIDpFżvp<})?m-)e 4fZjOmIm]SlScⴎne$FV厍 +m 4uΧ6~gq!?k,ݚ=Mˈ}?\]T8o/ND۰H|7QA1n{j+5[+53 یfӃKmip7bt-P>Z~rf /jk8Lp҄\vYExI4$WmhNsy fDa _3J.oh B}l2{噿A=͝9E d;9smsFn,ݤDK:fIJ+@mD͉2y!кn2xOX[S2k\<#c[ݕ]Se6JGy)'Pd+S^~ɇ% e 6G.nrx.ܞȔK{Y!_a 2 +(=NZK*#69ON/scu/scdiS^V遼>H,a Q{HlpEJ|0#Ƕ1R.^8|)YnIak4OgA*&-yY v'v>T 7{Aέ.,̂r=*f *Qxdw'`V@ Gƿ棞Km{V.d9+`b|:g2=%Ǜ/BmVC.L{w>zYQ0wŲlg^\ko>"ۇVx:?Qo +c؋1.qZ{Ɓ6C&im\#f>o2/r/l6kqiO{[rp҇R}brE +1mu0~[2Xtч-TbW~ǖ;meq3d-yɟ?R.TIb%ݦgY+ Du}ڻ=5S$Vk&zʴ=]=htoGX,2_޳Oωg*QYJT=oWWGjCUw.iWb Neۻf4>we8&]8MH,W?e9?e5iQ쀄˛+ TtU~1{`_y1h>q žËm^="㵔7Wb/UCl4,U39\UiT 9\}w%Ai:yȵtܭy{k4Frկ*`r0nW,ίle6'=25R(1\ί=`˄bRsˣA|<`amg_֯4E=C/XzHgH hl^HG4ڗW|r􂅜-kg/X@8Yx?` B4Zxsei+?&@L`9윆)96G?{Bd1@x.;}ZVlWe9>+=1}8R@=9u 7Lvί/|< k畈A,l?6zw  Fv]t;AWͨн_ikwY +v| p5!)ެ_(cթm8iƑOW!)Zcێ"Ηe+ۈʲInåi6V$JyWQ\aTBv|WL2'(k#_{ۆӒNEMaU!<`zZÈtz}:ӻ{2N?RC&u^;: #Ӎz ~{֙s>3fX@t:TIo۾B:)"Mڛ i3=oR^ádlz2}4=m:m)sQt;.(^^x~ۈ9ú9)ܛkԏnP0IZGn'6Ed6NқRnflV|->Ufa$.kOe\@ -G/϶-۾/qa։/`<:NBd,#'xrv+R$SwMf[l{lX ;ŶyMeU32l->L2 @1I)ļTn'L#-@n'LP`敍y8acNB&pGSj+Lo|`N8 O4 %ˉd:e9r%I W٥W{s1. wTbP4^xذe9 +G78oKBt^'EmzIF&KXe!II"M4֥"Drm/Me3,ߍ7_3 +=U YD[".s^Z)& 4rS0(50x&6T0)o6JlL('F 0%׷ ﺝ0mcu\ }ZRUn(Pb! ezү oڑN+Ey(4+΀2yxb~E2I3.ٺfSs.3SuVggOOLɟ!cqeC\R)paĔVM o +$ϮgH( ?ζD:oLF@ΗñIb+d64M /ME+P2 RAV49_\2P6ΧexT7CgDɥsiѦ +z>&jkҷϥY}^A +lWKl3K)᩼yXAA a[Wvݎ]fTɱS2:*;-%+]ێvқdDZmsZN$I= &;e2e0˪ƒm7~HS6-&"֛"wɷsmE=MD+ vƞ&މtܖ\GҍJ.ȔN\-"ZULe9%އnC9j=!QZBf%_ml!,Ů o1zಷ}!oٮG+jl]^KxZba9mHx?OBJgߙ ڮ-6< {yeslvO`6"<59K63+.!I͏O#B/%4o#B#FfWq[! B0A)4 $$ +tֈ*H̨k 6/] v E(YUm@{θ/ ׷Dc/6HugSj&-|y ^aDRf_NO +6S/R¿cT ?0hpj4 M͂//>3ı,Wr[y42i!:޼ec/h%ؠoAb7\~}Y2Pg-N:IG!U*@ܯUFg N9nS@!n h?[(voK@Rbv{J6Vy4N/G@j,#@@L:6}s8ZFrT0EX`Cɂ2{8;# Pn'@`բ\ޜgo'@ I/Hhu9N`u(*< r: ]oF|Vumu'o6J5g/@Q9yWmHr rU9&4QQx31&01ڪ Г) +9<t4)}gJ A+y6q2^~@E6䜁J})"ڰzO?(Z[h05WEWRX|xsӇĐv@vP@;|y\S9( +v3}hw!\y;y:RpiwG-FQ$>AiO; f|8@iy +6QdDZ$]w']ZsIߑ{Q j + ݟ&xv~ q(/jESSyQrlx}7.?."R7Z}EsD}iI҆@`?w7ට׋93t{GMmO?qJ=nj㯚E7Zy_(۝gYj۪f+}T $J,cۏoޛ MpWX) oqs]p| +TR͎%^=M]oӷjo U.7e樟ooTŀYl_pܺ6o&n@nﶞBr[O6аsCZicWm6~UDF6[=BhZ=՚^vTXH-g٦ZG.-u3 ,Y=TDht7|V%73sdKY[|7+Wnqq +-j~0>f{ːYe=O2U5[ɲu͒l˹n'XdU1a2C/Tebʝ0@N|l+14(=djzui%,`}v{ ߬EEeU']:#ܼBb4꛻WE( k#"#Miu)vBC/.F/O׹eP()#pcqAW͸X.@)%C!&.,4@X@֗-BZs`xi/-c%gemLlY2-i\Klr/*y^f(zx]lvbwU,Р8C+xcj∐J۪|UQe"}}2@x.8D3m? :{[yOd!0"b{MEvh[L)W7g)f!5Mּ}G4 +uh&5ET!KT,~_O\.m51ށAhcmtiH/z;;QKcۆN^}@Q/x7 vX˵)̄GT&Md/6-O'}l0n+jmT5(l>=mԗrA z8 Qu58IuP]hI5_&H k28tFw[ roH*1;Lo~Gcin#SFdRK{352)_2Ůuys.b۱h@2*%џhE R yTߨ>i:@n!͆d 7!Tr+ng! /C!1~&okj>]Igz8 Yѐ,H0bϗO?Yv?Âm|4Rz=}ˋGOKZ%6={d,_~(K Ӟ/ӕCǻʷ$H/?x4r4inx켎N[r_%,O|,Lto wNQN 4,s/xѯq((۝S6))#oޗ Zownb@6WgDyXp=/7e[S`ӭ8c!LjJ +7MA'K( ۉ3_1 h_2) :Y%,iC:Oq7D/G]_Nb=@ $k5sכ"D"ߌ\EK /&46v'[xfc䑊chsi2JLI:5e99K)`9ˁS vOj3yӰ1Cx\DB( <ޗ`zb^<ʄc*fZGѹ@P0!Ӛ+5+ v:{_P;>whK90%ud`]թO.ѩa"ۊJ + LHxNb0,sjnWEaV֜9)DtM(6gQ{*hK4{U6^DI-1JصME˯sN +V4C}H~s#W0h.sZD&?B:?UXiZ m0!NBwcPv@ +TbOd[3FՊ+=DRי+u3Ϝ[)w@3x8JhL-hZG(uA) x%f }ZQKѹa7 Cjb.uv4v[XVb:R X| 8Eu˅WS͜7Mm:B(:7RfYYWG:gxsc¢Y@vM}*0^Fo +# ܄zMъYV!:{ac=ؗ5}y9eS,v"88@flf"jQ̕ED,Cp0dZђmM} K\qs:A"LeY5̶ׁs8ey+¥oq$Ґl;AdMwU~j+l.фeuVQ1ΐ–*1Ѓ,: 5$M̼ayhSZA)ex$@V9PGCc.huzE^8P0L8Or7A|ʯ͸ (/z7=m+J8U Ĥ@:|=20UNXD;e)+SkPg/ΡUE³Nyuy(dqI#z 7Efb'ZKN}K\j*h@>7,-k +.E[Afx^nFVQIh86|ƟcyϲR6icTPqtaijI#zkVRsa=?Cʤ%8L\{47T6^qj}h"JM`V%qmN=$@F!DFK%hT?$x!P1W^Sy/t b +BZB6 36tB(qj_JϑG/B'e~5~+եL%NglfHtng[̙6h>8ה)3a'ё0#z!'Luѷ)';f[4 uTʪL +&E"1@Ys'8˹B䮴jk_]%W_6J IRZ,u B&9 V5Wp9K[[[ZFVs ۩]M{mP?4|yI"lCN~|Jo}Q|S}hb(?y,1h]aŋ M!wH:PJq'!He1vumel~MO!=!?ΑqKGK +3 zOPBpc'Oj%`E6!h2&Q&ufv"b;ח]*\HhEi6v;z=[z-9#v-" +%MGZ`v;)T <k"AZT)}R5%gD (T!!=c_=fdpWBFD-݅4R0;J3J``\IV3km{e?Xu6WĄi TLQ-?SH_ˢDmEW2&,ނA.clӌӹ]p=Ѱ1Bz&AΑ ]!2yg];\Hy> s/ ø%46©hbj2.޾9l5-tZ7g:(q*m:VBYw}.C,'3D u6kSaT<(Y~2RC&ZI!s* 3G%Vi dԔC_5=z?ap*:=b_ PH& c:NmɽT`$ǖ *3maWBѰ &f)W̯F nyh8$Ա!k)la iFMkG#$H4VpTL{׼RRlPA ӎe8ڭ;ؼĚ'5ch5WL^{̢3|XD: X {bY-[|ܱm9.3 }cFԂ5$[bO8J 2챾 6DZsrQFNa)KfeN>SLLV6^9Q4"9Kf$-ܑkxaWk,uv e%Ѳm#9sY229dd< 5va͹#d@;זfe4{ uWIk_!}E)oV3!ORfFx \S) S/"yORRcjY;0*fDB (&6:%!G~/۹ 2g~%LIۿ ,1 Y^.Y:A+{/$HvX>ƕ9^AR,(Yβ! =5*݈vrɫ5i1nmeц̝Cns  ➿|ϯVZ`2~ o^LjQl"  Z+PG;6{F{ߔy=n,ߔs(_,uك,% 2as+P54$1Odkc#iIwntLىTsnCr!+>-,]r@!)\ +^C19+lIoy +4FDbe =;~[pp5WaBqrd (0]e/~1vm^젅@'U G8fSpud< tC- xm~l\E_#@ 8tSFaD/3vqaȸBp%36 J(m瘘q̰G6%|o60RkFJX,wfڢ /x N#;ٗ>qz<=J Kݒ9ߏoiA8CMuW.^{Q/YMsޅl~Y/˒# $Jit65Sև M(F:-1z9 EQ(cH&4Cl~\; +bP~lhґ Z#;[ ͊pm/,:%f'4h5H͋G,NC:l؇(5ۙFh +Bj hP3N +dM#/P\p7DHq F +4| gJyk52=c +{n=qXF$_q[V(Ʀ@e}CUz =PitSAfɴ ]MzT4=ۻvMM|)i`XXIc9!r;Iٱ\RfWt*_Q#-`m` 6ъ]W?.HNF:='K,M{=1b|FiLzhRSئxm`uyΦ~#d8֍yhi)Z2(.kTˏ/IcXU]pz:uiP/\b8WerP\bs:L!;ju?֒Qb2~),h|K:-IoX6Vu @)YhjXX, e8 8;Nbߊj"^pCH4\案,Z%:/dXu몐C.:G2J/BlQ GhXbAE@ OD\?`FsK_` ̀<"v=pzQKᕗ/BI4Vt6qL5?P`[SwҔ~PͰ-]%xLQ.-]UBҘq*SE I& +q IqԍzCPt+eBSABOz!yI/D"]R)pQ;Y AFRW  @mHGcc5RH|hv }9KIN0nA-دVH K },x8y)\&I]3\.g& wDD`;aiL; +mR!gotF:U DZؼ:;˾#[_sgb@.L?DCbK43o/]jU&ӒHzﲂ>?=Y+#dK/rx+ ,^k=_,JP4g%hS_%HMxU ഗgfIPIЃJ8\x̀?mq?̺۫x[ul7:2߯T +Bn/\qgXSpҁ ܄[ 4{|?M0"q^F 抌8{^%f'}QiXom=0缊ױ,o=b%zr̀ܶC-S "=ܨ5yhaJ/ҕEbd>A5{tC.rudp 뛨:Q.]ak]ݤU^Wh/Iy`^#(8yr-X06񎖾[`YK"LW*X7k/-%A(` +3@ pa3a.@${.0N {W6Q:4{B8Gyd&.Z!hEdE "<-5)D%jAL/rU ^b9" ߏb`T)o,/r:uDƽvp`0-9 elO%=Q)@DQ9o]2ywz)1q'9I`CX#C45JFklm]N$:'^W ]G#&p6zJl*#??Jwp! wHA ¹sZ'B8x}=HkrDԋpo) '6'Q*J@'r2X + -g{BHkKi&-Ez\ %@O9GӤ}5>>}C=qu0ˁ_wwr6EqTjD;=:7nmai_uVi9Ic|ǜH0QF~T&BqW丹KCCȏ1ѵ6<%;>6Sb#B pf:IΜp}A⇸"#X=9/9Rt D19M=Q\+,w(M@mr՜דșRS$\;I-zkpTqiVZ?|+{ ڱv"@`ZPߤ"[yAt8#H¤HU\{}G\R&aaVYRBNN_jо5KKUo?k) 3a;֩6{"ͽZեo9X}N+)$gϺ:(>ǽWJ odQDe-!ʽ(g&8mW@2zIǸkQemG* {=E8k\a9L J 4JZCf_xxHbפpQl8Kqe@}eO&lc= +fsii1v)\ 'ÿ^KSD'g,pD%CP A:"DRo`g{QuJzj*9]05ԃ i.2 "|\ 6+s=<sT3cM4rHc+Ňnؕ;jdRZnn B[a$ʪTwJToo~cZ;|Qfp998+Cru/F|"P 4lt+ 5Ab|Mj7RRhg!/7|]dr\AQSjo<?mޚXGdUd~N~]rpQb?¬M5ucIw)7f^1yga\oq {Ѵi{c _"[Kw=PC6* +x=1F_CvcRhd-gT'jm$+9/SH_W9ToĂVB +2 /`0@xTJxgTH/ϠLC4}V0 $$kX0\,ZXWk %I!@LwEHP\η>LK٫uv5x|B05Nޏ[4`BDIL9^0t +?dd4d|n:DtN߱q-GZN)HTk) W04JU* fZ 﫻Mֱ8sVu5O5-Dzᣌ(vP9)T6Tx.'/-/w$`ok՝Pv0(0^!uƂi +zWi+&## mQ2  S8=b8 m؅3@r^ŝ]=䒣$:Q׾@^014~]ԃDi l]%'U`0I,#;uG=r%w|0[t@'G*w63OuZpʁhQCW +> ԡ3˭l7I|m +JT ) )F^dEIRU=b9_EJ#C1#EzөqB(Hs9ԢMΔ(%7nzF+qbr6b؂Mλ0 !܊@ɍT:/M +ra5qQ9!J)7ԫHwSe{\g* gIᠺK`f(MH~ KT* e~ ı FӔ@ !Bg^& +ux5x EZ xY#4!A,# |"u5n#7 JAU}r)^[xbU=13e +OJCDA BG8Z;0J(N5䝖9b-'n;Jj׫# ^Fw"ʾ^%SsX)HW=lTS[D9Pt2 D2Ca"јiK&Fg`])qLKVa!%u l){$9.`1b sߥPx!(=ٗ#*`זej1&0Շ"PA:ɘOjgTϱ%BSEےװc#PΌSdlb|?eV&NƣC̽S#(F7YyQ3be={(0JmQ9+_\,UkZptw2l#NNXPcՄ72.Nw;[;)?+aQKHAWO=.fJ Xcy]2)/e !$2֯$gX4P/28 +\Ӎ{U0]n8Sݏ *IT'bS,C#ߢ =lm5t=RO5 [=8xK n\F[Z`(o%o+=|q,Ӏ7~1n:Y 3ڵ;@(݋~U0Fc.6@1UXfa9a(@>p w^oD&bp2ɋ.qc&ˢkZ)INV#MuU7(XÅ#NтH~D{Rr2֠ghjկd(qP 1@N1:m6U[2=닿c)s!ș(rw +4yeMrj5@qhcP/Pj(!C.SI*\d 4oe.PسW,݆ɘ%V=epnWg"2+p 9j\eD!i5ӞVP#z JT -5vE4Lft{V,cnwE* 6l9#SSh9$VXC\b) ۪`Y[/Yu 9YA\šnb'jI h/i/6D@iJC@%Z=VĉæK?{CWC` W\=(G%B[JI;9UzٰH҈[bInh`Mw;ȬyUK͓ ۸p^-.+D|JINw !жSʝuZ\1@yc!M2 +xطh^wCe [= +ɏW)YMDRyD$Ujsn$y.f߱Y/f&`xq-dӫ|sM`\ u +P{''"@l'D m +L"ќ mاEm=NFI +w(!Mj챙}ǜc-~SX@ܯrN9f;ЃPF/[vHlzmj܉`v<´B'Tdz s{b/K~'qwsT!]OL~2#eȓ v"Kb^LOF(wF H-Tԋ<$F؂ĽĴ\Pn)e}S jX2|()F'`2B{ /&O*k\6cͻ-Rpa+6K*lIQ\"2BSV^bi(aϳ% +M\V)!d!B'h|ԍ(B +,MQִ*R4!M,K x !rH<$@ H Li"S4zc9);{ړ]\0?])%{}ZIa9w@j 잤"v* Xl[B}!֦^uGT77hœޙ%ǜ2n QW7)7Ȏ,5mpa\~EOxzOM1gfFL]b$d`ثN[|[3LA5Pkcdh SDʂ6L]PGp rZu"9@^M A!Wuu1EPI7" Hc`Tv2X6&c¸ė-5Gf{%S=AUrKҰ5X-vlrRu*zH +e_iZ0{ +;kAje_) 'E\3P3q7BzJo4K[k)hk3$':oIQ(!r\!:!L[1^x\1@ +M!"(cT|˱H {#K=?Cz~I%Yy„f+K<lyC< Z쁎M+oLjFSWU~!+].TrZaՇJ79+A-Rryf4?!R)S:c'"fllxmBb!بÉe4("rRt.D1rG}$?_uYgKH:sSIpfBV҄5[e!d^L1`6^J̛,n->.=X=e10&Rg>b--`|;`>40VIA(KHݣ+1iڥWWy ++Ib˩;B}Jh;O/ i~eRBdrR8cHA=X$4Y\L>("D7y+.mT>,Hނ<$#ju{"eɷp`a%׌ƜAOD7=X&޹n$O.qb{؃D iRoRl6Cկ @YU.`Qd6`{e""R>AiWsXAm2^D!9jrZ@&QHx|`7%_Jx(z)~,ȭ>vw5{Vv|s8`Q4h[lO[A( dO_ijFA^΃+2݋Mu5]D endstream endobj 26 0 obj <>stream +hFux(cŻ,ыqyh +.GQSC +ѫ!dPPfD2ӍWUЈw[H8K{hzH#0'V%s_DnQ|?$D *#U..yr(mȺּqmvU~WMfd)~u[%ggKe$pKN!p1Wﲩu͎>?ţD^T_ߟߨ>_#Mn1أ#ܤ#0sJ\ +Tct9\\"$H"=lRW| 9 iŃS] lB*.=R|>U=h \<pᤗLxt!\>GF$ H/$b51h3Ov~?`Co9K7-,K"j5w])r#[IC ~DS[Esx#U1tuEg:ԁsEzv` +d] n*T_:Je)CI[uŅeZ5KZ"@R]ZVw{NhA:tW?(r9$ӂ7y^MӖKboMh +v9ٛ+?M)dD_uO瑒90{q8v_m#DdӤf"qdTPv\ud4|*b/K=O769X7+!m)M0%)$Ԭ~xuٮPOz ~c"8h; Y%l&f;G6lI{~hb iyCuKۖ"`{u c:6|*PK!tʧ !2O&tUwa7Cߣ n &QUa*3w$W߯XlmA +i;=&GaށRµ%܊%ރfGjǯ5} Xa@vuS +ᐰ1PWy-_C}7t`TdҘn6#-#*2^Z=qlȿi3砺L!OfA +͍M-8ŐKƍ)I;JΑ}\.=]տE~+4PKUWmpqKAJD'Cu86 ^J'Vq}bH0RVXGJy +{L-,;B/;@=W3^RMKy1Mצ* ۏgGoT/9lFNR E]RWWЯ-S@}Џ(;b!g)YéԜ)t&5?o7ﳐ̘jL%I-,űw,>ʗF+@s-I3iLŎ܂Xmܾpąo<(ry>v>gqwv2P:z,)v\Cn%n&܏A@Ra;6qSu Bo6 _ʤp5}NnpGaYInvA@Φ9_hh7!Hmz($*PP|) MޏHua]o/NS@BmdvK/E) +yu^඗覙 .p0ir0dUzQ`(lY; 1x#p63Y4]0ʤnId̃+23ҟ)*e~j6꯳k)?s3Ik2ih{5~'Z;F#Ě:/ɭ2ЇkAɟ!g'wG[/d[^ 23[-%} ȗHKݰ_ +~)>Ct<8 jΰ(H*ֶv͎vP$G.d~UWKc +|? +oHDiQ`)Y_awxupKTff>">B5 8Ԝ.ݗFcQ>`EjR; Bg1|RmCe!Vodz=$.a)lm#(RRA7s^vۄi/9X +)׋ͬ`MA5}8=  [/4`C* )_K)ч4^a@7Wvܱg\J䙺@a=[m 6Z|pE0X,k2YԞ&Gi\?.;|vX[x= +E1_ :Pv:k;W/$M;~ZF"E K嶄5;vaCTn5WXA] m$hv 95B@\"pm{ldEA{0-%C6GwĞՕ؏:\-Q!P~`x6<QRaD6AηgZ$x0Ym]>p :X2X+y5*Uռb9IcR B&'QtbTj{PQi0 CH]I+Ps= ˫!VYby˓{\7Ce )!9P?ڟ&ܾv4&i.&'aY5*,o2cBC,ޛ_ғ<Òk\хԀّV H6+Lb{*S}G-I"SjztSM2yR=Xa?cbg#/l$ow\crb!I5޽%xL^e(DBJsUGoF2ILn]譒6%'݃\}q$UCυ(RYdKMU"!s|PfBC+UQJi#_7T 'ЎIV :Rm^\ujiKCV+|q$e4aaAY#;pipR=Hh3z-;=YZ+I YK(uڊHd¼١o"&aQ ڑk!@ҟMCT "qpgG.7G2Zz`o ;OK9Apj?19-iP={ 0+|HQwP9o3y`Ҏ wz *xWS;]w(ˠr.zڦ ^+&} OP%q9@IPE,Nרw4硦I1S@v`BbRn%Z@0'эHCaf`XƢ_D*?x{(j~27 121X$3 O*zQBHrPL(Ӑm p |Zo2i(-qgd`=r$&E,ɉQS{%P[(#N7l F!գ bbJ!F携ZW* VŒ#k80\H!Q%8L4ນ\ 47K+헓:P%_ZPLg)YVݸ}o#YN@O5U"w(9=RAdO5zF1n>I-,T!3B76cp<@!5Y{/H4/z}I&b鹯jI-Q2*ôH8KJ}B8= k, Yl"ZCa2Lw[r~"!2nfj)-5HcJg]m{u&恹;Q,fڹ`-~gb+/C&L]r~‘ʕP M܄eSU=CkC{oT\J%U(d!aIn;W/9(W{Kh;x!# #\W(C0Kþ#sYSp@hc"/P۪8.;)&W\CE(l%9 +*sEKV3Q).I/i +|ĥdlVFf6\n=|Gy'45%tX;wnb$(.&:n *r1U"K%!H.^*ݳac?]1N#29NҼ‰"/!ds4unxr A T:@mAv"{.tkuMUP(2~oq؂ 5J:jVgH4xDlSĞQ۸Uسt>%(SIJÍ6O\(&BZaE @3RzĮ")CbJS6ݸ?`*jJJ#䀷 +̻t}7݁ᑯ-xɺsUlw  HX a?=7z-#jnFC,0 +8A`b0G`K/R1)w\Sy>K +bwd~k[ Pq VyAt1$DHR}Pcn⒟j@[3w,)S3-۪ʑ!?qNh@=zP̶ |OgVKa/G-D^ 9~8?ؕO x*L+(&q5X'wU_R:(G(5NJ9Yt@?~il?ǵj`I2XZ>YLoaKPpo&EK{\$鶷Q;<4!^OEF?_)0ՒK"XU~HsEgnpv! Gq nlWµK`n ܠ|Yg[J+0JkP 5{lDM_%]7<صʡ8R߃b #+h\Sbc}ΕN7،Y%n({s.Bݷ2L@}v0 `J%$1fACfP1 Qo$ofk9KR ̠ab'i>eiq>l04k7GȎ~pwh/_.,{sʛ4Y 鰽~vAq~9'`wn%U E%$#mw!q>V вO8WLE (\,?!KdƁ/Y+A OZͨ~([ +XͣJF ePlIHC()PV>}ciuT +ʉ.1&t 'Lɴ-Ety/$Y@`k?R=T~( ,TmѪʯF{Ԭk&5T~+*$upqX+|G-CY"G3w/_d!/PɄr9m'2G +B)1aj^($ !@*%(OGWFL[RM-;=J TD2zh,|y +/P+Hx!v`^cAЀ%P[#Ģg 36:S9Fpa7Xo ' 0ako%m<\C=;ƃ6krƬ 񊬪0C301N<-}|<(RR+~!Ȇ렰"o:5K<$K:0FEBAA}^xÍW|@L`3RÓBG8_v-> ^ H4=3(S$D롪fHI6Nľ_^OOWDvA2ܑlх9!A'*UGqF^{C!b{ϩnRlCI9$րfy3Bl!E)M;@iiD$`S0vr-I@ek2lBs4^%xUBRd,VjP·\$N.K-2Oe-VB (XHBH9_ag8_[5M՞ȤP6ܞe-!xjI m^uMN52Q΋%NB9$>㼬+*jCN/K^IW g( + lO͌(e \l<}K.&e' wqx{`uJh|5ǶTK?6/0]KMrv$1` 7idx +Ri/}R fA噀.sDX0(&uґwMhb,F̻yF s4A:ɥ 1GJC6{ 2ʜ!Ez6Kꏑ:v -͚d*fk/p#0m *GdjF*%({Q4 8Ƶ[k,v* t٦â̧ol^` CBTc|Wā:`ԉ^s0fߗFj(0j!дD420PR&YxOC@jIz4@"i;Iդg-7R(HrI逮 N QOSULlj<%!t_@N$@('7d_;=f&T*%Ca%E%SXE匨LSvn_ImkC?.m. ~X?`Z\O +^t|v%ugK*k8#s tt] +Rl䰊 _CV8Cᤴ:h%qu?__0CQZ$MwV?љւE{їn@ޥyb݈.rDPc3mwܢfT>A)dk0/G{29Wܷ&n= +ZhT"ہLOszqe_Y/  *ɰ-GPQwݾ;9HĪ7 X9DjՔ훹2̓¨I\sl<.Y,w"{ΗS +ؿ\/r] XROjd:4*pxu}TQϢ@א\-G^ bCXBô8L) ;(,\5вw!`o^i,u(eB +ZԂ'"Ԙd n g$̂ȄDP;17-RJYz$1D,~%Fi⑭A]3ܡ?7T/_1$"98*⮴_r{_WK88D@J`+x?gWs\߯YPbCYv~l'mˢ$D]JCR\RN +xmsG"IC%v7-Edဧd$ ʟO|eH%(g9";A}$f;dogDo"{ߐ$ +T Lq?DG६׶(#%}î_UF=jo?K#kELlL@>T@# +1{Ű{I|ҫ1͆Uң J0jΣF!Tk|(Š$RLg͈7 zS=*$u*@ %էiu䡁]ڋ I6(SY)b;w<|3pHW cU0&9~H2FLTLFTZ)c'M{HHm=+u!? $J$"f&G 6>[w? !Q\QE')TX^7pHz;@w[$4Jr{ŷ6wrMk-HE'- )Z+)nRf^Zj5ɠ%bFr:L :if=-Xޛ$NQ`?uu?j֎ $Z,42t`M@zz}U]BeNG1vmVU=ˣoI,4?F"uW?5@ ],='8zcbN'}}YM!u]8SVqF\Z21DQ a^IP; WfX9SʋI1ō`ygǐ=U|{x;gx9铧- +)A&Uv4;+I2\a9N|q I@de]y]VnWI ' ^쾉gtfDZ)Vċ!Lwzln +[gѐT QAf@I(E_+,9=q3K΀87zU_ C1א%9i ʁX+/2뼔'cO]xŘM,*)tqV%&0ij 2qd4?3Ե kPޯL}~34+S`v +ȝYRl4w4% ySG%uz+߁҆W'1 q>n\Qaر'E8◙,G2=x5C"`_qeD~IK"fm)'5}n'5kl#e^tˏJ0ebŧG<.F8(]!3u<yF Ug.FN܆]16RV +@V빃.+H({T;f$[Pi/]&$U׮eU-#c J'v@T2XPviQ4z &=B} ʋ Q﷟M-Il?$A޹.C{T>85^F//Vvl&Tgs&E6 +!]Ԁ!Kn1%>?^.O4ChcƙtOaYÁɦ6et,S.%Bd剅ap~$,ݻdf"aGIP"w-< +Vk/MKBN@"s:T}ܕLS-faG~IGl8д$yi~~r +ݓVN6c{om~اzXf?dT&/R.b_%~5EZ`FSUbp;'q(uɲ,~TTo2MA-$DrX{,\Ҝ8-9}%lCfڙ%}z8:OboG$2]ΐd'A>Ȅjp`aT8b[6|`َMA;> +ET^W?юs]g'P~0qoaGꦀlRA3`IV|Fp,N<J!HshNDjͰRWj:cJEyHt$Ԋ"?ïMe ~3Lb PM{ +E]<5R幈ZSQՖwJXsИe|[cI(X tJ5'CAfPP [l, 3a,C Q| A9'%6-C0lM!YQ}QM7vU׳xcX}u9IbKNtոTW+A}áA2TA* ,Pbf`w/r֪̒\/B++ꥃځ$,[rR#xlIk)gmӞ |6 O#~ 8CGf&X*i-Hb4{fLM3H|zij3ّ1CaT-'DnǛ|c"][+?2N`HOt ץ3ќʆk4%MB*7֛} Jpy'jyn$sf*JQ a!jz1n r@3 &H q@$@3ULiS3ڗ/um_K-ݟXi{ +]+nah2A8t$[en2 BK%.kg(PIӁn*_xEұ W )?uɶCZdJu|^(8fQwGO%bR 2Qe'4)Chv,ě/C( C mbp @"~J%EMHctKTxhxN`dC +Hpٽ QW`Kع~¥ƾj!=fZDKOLY88ntqZ9m-knj^%L_vH) Rv$~_ʗ ׀ޖT@KL_ho/<%XE> f埭[M)b]LnC@ 7"AdžbqXjv|['6i1"qvQK+2˦ +BV +40[ J# 0 .t:ܫ)s Jt1&}ZuIfD*K)ME4>b&/*'☣lX
s.5pu;Y1R[y\{dV\0- 1:MPma~{Mo Y_`V$mɇp ++f]uײc ,"tPYM]Yʬn@y_:ph{]1J`cC!ֽ+J5ʒ{62@+`F;x@ۙ+6"!U@eԲdw +.;m^K:VJev|8{iA=_̣jxD,j)[KeIG67L-L3nޑBr`&SRW>`bnb'ThZq:cJ])(-PtIqn_Ot:DX)pMLF83iVj(oj}"qBL"6(+)V.aDW(h^TIB JډO~bcng#ܔrhT~]j|A({ڭ #Nb*.w2ETͨy<ɳnNbĊNmOVj=w֞9KUu+hjʅ`J⬆yUR@E$V<|nV/•Ϋ\Xu2[O8=@uiɆ}Vjp'RCRFXkﰠ"m%IQ-W("Qa +=G}:bEWa"n#2+5x!Kxfs?r9#:ň*Bʴ~0Ywlazx d<f[k9YV!F 8nq:@BogwT/ovsAK ʣ*mH& T>/FR|0bH{:Wߵry9S"s%/:()uK*dtg-W&=W`}̭r ܦҥ| 8J( ̏$R +$u,u^]yB}jB(b!Tn[} xBz5zX.ϸ* +CNőudY~"A܎[]iGدs:Dwأ &[eͶ@|=>h<;8^W0]ቼ9h(Qm()9 BEut&OCrDp_kF&L/I6S4eDz-+nK;o[)N3)o?Oo7o?7?~W~?wo_ =~'w?t}O_?n7ww?[j~|lS~ x͇WW귿Sο?ovʻ̑#㷿m_\oRq+/u{ko_W_6Ibq"~{!yYb 퇓L9o_$pw rIXZEYu%d;D!KG Zį Qû{^_}! ;=SE#*j|э7q|ظ||~su`HsXq*dyr#d ~ +wGJ?uBqʛ~pRqNtsx`3wD9~FWϗ|X%H )8,*eZ=/'` ^ |WC+C4ƙ 58A8-?=eh pv{jJ4x~'􍞍D<_/b9وN4ӈ[ st:<&3>Ύ+ GZ< +2ʖ?O+h\IiΩ|D4q ĝz En. Fˢq8X7y_ Qx^xb+U9+=#-uij^ +NPO5Ϗw'xyibYqiTUNSW [6^> k|ncw^A=;~f<8Wk'1{`jqĸrN %Kgh1C[ _ @U>0٫zvdU/55daL373Zk;ss;I?}ICco|>b 9<+9 {QU+LDi1֕8o~ξ1()w^A`ިE^k\"ƙOA I:o=!'1 5rN< +HٯUO*P\A1&12\3H=X8i+x)ΉFE[#*M& '8sŁ3h( _7`+3L76s>s\?)zXqa ~W]^}bm6_m{%q8CWϛw(;_w!T4kFعz7tsyi U||^_1c8|e79yL%zs#'qS4"2v(_ +ԝ<'ڮ◰;F8"xHD#hl2cѲohDcobfSkzij'#P_~- j)Y{Uc_?.׻95]9pz/aM4fј>l'YZNWl n9TpǷ &kjmB9mnbcl7;UF9|PhuA#)ϙJ?gs=ƹvD}'%WnV$[h9.7K73iO+4W+rNGNHŧ%z\ֽ\g >}DJ!oaBܾeEh^ y)϶ʟKcȠdg}|%ϔZ(4휠?DdPN^s;>2N$˒["ª +p=k}+ɸ|\@zyظgA)9a;)ۻ:2=zyDL]2,vO(R;A:Cɷ6X4^H_2|1;J j>ypc.o$lONPy邲3crx3{Oc ly/WS (@dڟ/R&V5VFDU+bIgX=x_9Zjفb~LNPW_$lFd/ +˵w<Ӳxv}ˍٜ#yVmą P"n#{yEL5q~yYk[Wl'Z։FLJAԒnrc;  LJ}/ o :^FMgcN4QŤu Z8YryLEWR6"쨈O{u裝v{sWzYcjGXep4SF;\Nl (l|V;.kn,R\h#$(|HgW}G'oyZ)g:kW{[FI)_"N ]9 u-f|6.,ˏ##kq.9/,CS̬S`؞}0jMAK)Ol<)೾ )O}`F㨙.( Y#f>1`?_؅Iگ^Y9аcOQߥl_!+O سc|2Tc\]40Yj%'w;6~4`AP':nZ7's4 oF{k kE.@Qys^9zoyhfSoQŬ}YKɾbFEۈ)NrSvO3hgnH0/|x9@try9g/4ڍ̜j PU8H3 +"w*iiVk.0x^iyȭ-?+<IW ֿ udew]q[/йfc;1o}v6%>69dN(<V{'n7{?gsԅGE˷<$fGg3}Ŵg@9끷wqs20%{w ެZb09cV-.(?tVghgKT!l}i9`PxR,w*;:+#y fF+y#X NNsͧp+Ϲ9|Z<\$UN4A +E!jYES̩2} |FvL4=b :Hrn<]½+u/ZS0YFZDQ$i8MMjq_Q}QO3S O ߍQB30\R n(23 W9\!ٕ-͑''Fz]}9^տSA6'"#|2EĉM Oj'gE*S/AK$ s-u]gA3O\IchHzWc@X,, u?_1\jL +I뙪{}q%ڕtu2 d}ؕ29ۙ E=\{բmmfX=AC_jG^W~r'/F=MOLyb1Ȭ+skM*CaO姜p9}(g5WR͚+Y65nq"QQ x61Ո|JN=*U3{]~K&֥ḽTc04#y۽1+* f>r\×8՜F>RǞC^{ԨOw{yaRȑ?@9hZjvqW't[w#ժ'rC7.~k;f6g:A ΓBVoD=d:6sF98"%D.p?D\ +]Ɖ#e[3.C|I:Ʈ+{*h泍ƽQ8th;״6E|PYON䏽ڕC~y+ZY]m4WΗyϼvzwkƔ됕ƉhȀI 2xGh\'i9UvrOj^4]zw8uZyH}hHa_sU6aq'+\'x?^l_'ONWlɵ5#XrB9ih~h;Nq1HcɞP^+dv6$m}ngD~W"l/0'Oczʅ|$5?4+! :)I,#",s=n\+aW,ܫHcgOeZ>=PD5j)ܠOԺѳא~=TCKz +}y·8A + P܋EΠo=_ש-@ +ٶg˙ IS,9U;(OkYN)2w,lA9ܘ;1NPj\s'fAcs̼HuhM:9oL)A=._g}sIm3K$lLSBi<2=|&~>|6TI_˾9U3D3xwBA8T!c 8HrYgx'ȒCwhl9fkJn+?8T!dt G'xmLJ߄un5+$I.! JN|sإ;'י<",d*?nwוdW䥮@IY=SW*~V3U*V ^~™2E. '*]#V+N0F6gV&g Ȱd]\tN%#2ΚJ~I +/bH렽ƻhH o 'ȓH}}MۜąF` rRdJ4bCWT8 n|YpvE/p, GCu͚[Yя9Uצy]OQPߣ,_29I9=iNk Npbe r"^'Z/vئ Q, +\g'H(t'yo +/z_ +A{LXQ'xr^{E?6f}*nqZJ^؍xa  |r'YVߣ3G}#>gBIh*}۩(KK!U5qGWő4o{.FrqEF].~nwg=@ٞ߯@RZjU,|UBL^:xU[qH__xc9id"dMBF)F}=*A@?(Sc- "*S}U5jQI25ā*&f"kRn,lӇb6P0fj`>@(5 * +~Wf*Oz@fߧ +O IH _7pZpZh) G_JW7NЫfd~:8;X;L4d2+ +LK^„_VPL;XU$Fը)bս 4M=8D|XK*v fP( J&0cUQdkZDb*fPPGG |jQTi5UL#`HhBBx*2x_}ʄ #U23o /w"/f&%sՍYCxS58F(Xg*KU6 (lõTTW . n6|@M2vBTLQ4zv +TW9a&bh( +3&S/㭎Û75$DfVi FJeB 蘚è5jd-Jn͸QFFRME]Z +ex U9 J +h'PUɍLj,rtu5Hkh F 5h5D苾T%-S3S5k`5ve*mIlR9Iwv3hоf q/՗]ƒT`WyMՁpLP_k*bGa]8AZ6iz&Qo&̔ %UFPGIQo ʽp/!/S5LT(' e* 2T2S~Ѓ$Tc\Bi +EhJ ! +,[+z.*k[Ruؾ-̭>T:a+YpH d + F}K-?=q]jR`5)tF33C͔n ʣ(XQjl,\+SF_ƚ1|Ш+o'eve`"RTsvezFaߪۨ:0cpMf+L]E>*؎u[eb(rYF!,E3]δj/Q|#Fd⪌j8b)^UVt1& +jjbҗo~0R\Hc.M|^hNdF8\}3HbY$j P4ZC3bQ1M57>AmJE$K4ePUK#-S5 +)@ß;Дǀfqqje :T[̠Q8}@]P[MkMRoi nXP*Ə؁NDk w"a,kjG*pPz}}B.<+B,RS+ XLjNJdh;1D(Ǘd*K' Ȍ 4Lq1*:h" w6C#Z31L_(R(3!V4GakM,gXtԆ7Im F]4&}MT oZ*dh,V."JPLT¬oTfFMe7 獪53AOV6*qҾB؀f 2Uh43KUs5jeB'hD.Dt|h:MdgUzP^ #ԵXEoYZHKM?TqbJkuڨSO#@su)]h4ƌBH 8!.ŝWxUeGv&}Pc!j U_j"W׫˪kM]KbyOo(ȝ-7Ԝڢbth>(T!IOJbj;)5]"WL 5S#"p@?fX .;\bR"4DEjMJMl c.13c?(ri&⻙諪Iy$lH•BBJ +܁N-y{+5ybۯn4EɮrPTO`/^+s0 J=Vb5Չ:xMZ ◉F7c1rPW[6S: oX_fj`*l+>0եLUUDui,V3PRj7i XD#7r,W>| Raȵ,TIdLF#S#qbwQ_R3d* D:|o)DC0cB'QK)˚bmj3[%VxR>e8S h6GHŚ3&j+5*1U:OۧvPTb,͚R6ҭZ柺$n,VѓKY}Xoa VF+fm45F3 /lV'^.>mЁz}^A]3FA=/uiH]D@1QCՈC+5\Z_VZȋثQ,m R4QR?UVjd"tWMl&aϫ:Eoz.nvt.`7XdY曑H5JHSoÍFu<ʸQ@¾zu+3ygbZB9f1TepU+F?dMs\eo&d )ԁ!k5447)LS/>O* S;5vS_7WhYlgNJ/U 妿cx۟NqfuNJFO EDXEu^'`pLR'j֔YA]*0B G,, +<";U.'UE6U vڤ9鏵܄󐄍,Z5xZLфXPXRG j*U,Uo&$jND|CaCDn \PPal"f5RO}Q2SUVH':kՇbu }\cm]5 +%!j%mi ͌LjPa:UQ)̇E<̇e=Cn!/Nf="1Ssfl #>&w\C#١Cў9ĺ|JpoyIJa>4k,=$ W8 +G_]Uu&u/$)$3SIL`p9\\d>~;L#2#A oOIΆ"NŇN3,ds.C3^1C( \B4.n2KmÅgRG2ICA6lJtS7TtLD6(g48ԆGsrFπ9a<'S^a||t:(m$2 "y#XǚՖ3h.uc͸kmHl$Vr~r61p +AkޡB)fnZ:cC.:H X*Y.~{ :x 0Ȇ׆{ t@#~=Ϗ{  o!OhSaY!rWۆCzGuO…Gs^QC8A|h8exx!?ޟLGg2e3Q"yC3Cr){6d҉LK|b4>,{5RLlh^?L`t(< Cxx&8k4%.yL6"&g,^MjB}M<sSo*b&n*?dEr6 bz,:R>}6U?U4k%%?2à6HmzXX+7{g S}*}j+l 8 ]P~֤}? Q6P{@-l s曁8@GuH~}S1ѥQ4W)TNoNg˦dž#uiKa\oPxɇ5;t I e.y6K3@+v9F1ly „qS<#܃׆,D:*b?7`;C>x"p\Xx.,c < 6dtHf.:Pt$0lXh>aX +K&$k4 tUJ .f<'-Q6䐏>I3l<:s<:TytT$:at&\ǦM7MerF.YHɒ3\ڮ9l$h,|O#4*@IkDMlzM2d68g4M8L=t@eLZcHn"dz>IHH`l(7Ⱦh33\zC]؆~ Yy݌*8,$r.`Dz>tTo«FWp klxx6bR6éȢq Z\ropЩ; Lm& 4-n9Dt% +,۾Y#[nV5_r}lꯋ)؏ +2WLyt@2yX:c{&ml*B:}]9.@EŠR_Dd;ZRzjɃ1_0kXk`R!'S"10; , +X_dc0yc{V`>D4{_)j{& +N,b맂|bܨ:p5!'٭ Gxv`~ |LȾ0ȾƘ2<]48,p CCg<oa{]DdCXSl]7l-35&rM9ÄeϠa;gC::q{)hgH!{G9"WIe~ڂ dž}J#K!΁lցSXc=$t@H'g6(kq®³=iCܑMG\` 6X_`P$YhS|H{(}u~:dP;Zrh- ^n!l ;iac |ֶ qjӮhm{Oݢ2ap6q$4GRw&k{ ^_b'*:ޙwcM33* ]_qr'+Gw!K4 Aa|`b|8|(KW nQ6U0oP't0O."} &8i; +k#ԟDؔʇ l3E1g#^<Zd}=Ekn]pt)5A=裢sFT'Vǫ)7!Q!y#51鉮] ~!kCGb\]x`&03pt $w`Xk XǢ߇.lXQ. j{!UKN;kqAoHCκjä[=Il΄Gki=t@ +qK[O#NAQHv"#yn#c,Z atC:;a\`hFށl4hXW%OK蒳23"KlQ}Q|ˈ)x "\){ߑQi#?a +ߨǃ[=)jMb>Cx yH@-:Xe,멣illyLk!7G .p_%!Y ̧V`3[璥eĶ'+_  Μx[KE PE `'^%+ЅʉIȁlv=Y&yS?Sac ftL}* +4F]*=)Ej>K_0I0>F6B:슱| 7U|)wO`s"2v$)L ~-? :0 ||#AF"=9L^1OF>it%ljLN7(K!1 +THH KttRt̟wcK6=& 0_`' Ɇ˩Tb4Ld$举2yc:u *8o4l^9)z;+ooҋ&>sK£K ii$Z-/a$ox53 |v&wuTX805ȒLjt. *sɂSK# s;Q1֔2 +|z`l |X/$H߂L8<%bĄmj55Wt%ws d81[2(=O~=HIYGN"w@;$qNG!?9Ħ8$\ph> +|b y> {4x%N~p} A5_ | hL2wys ?tM陜#@'UWLR9AF?P >'KD%cEM3*\7w84]ވ{';$\Z,)x%ٔ'M Et:W}w%[`vh>` #r5kT1]?SemD3Ll|-Ͼ:`%S3b=К A)<&q I( 5Im_%T ܣ 1oC\>;{2'>"_ -a.tXT|K,ɣp| #ȉm\\_G82;|3Y;1iۿ~&oga[2T fd9sGŝa} _Hڄg|0_Ll0:'!y`r/fvʧaΤψ:O0Uxl)Y~Έ,8|\ Rc+'CLIBeźԽhY0b{`bgI x 1)&6;>OR l~:{c,8z߈!v ~5OLL؁^ c5+U~g3Ph٦3y^;VՈc>Wgl9omVwF^uۘLc{2NM ؆-8O_6o/~ :'tq)Dٔz=L\C+#,{B؇>PgzOP+*ܴ@r z0`AG <(u!@;!ǻsgC HwqH^<]trF-z.zlR46񈬝HbKd L`XqQV[BܓFv֓cp{^O؇W5N=>@8b; I ̡7LNA,֥;F@\ o8X3*=aHY^CσG*=pż-4u4Z2j ۏx%'2W )g Cy) ݔcI稁ce Nɻ#E|Im3!V\@V^7%ko|M]FU}rf[0!oyRS_c ^(>o37 ;s#~gZm  x).J8?1 wćN6א"Sn9O`.!=m-r~re6CF|ŏ9&%̰ΛJ!Gy(rH9 &'s8ँr +JAl)? O~9'=Jyk@)|o9> p!UBe~s >|=릵~)YFoK6n$kqC<,riH3IGDv{C쳏ΟI5zG=L5[+ 1e鍳;G6gdˋdRtX't91"nQ5 J>ZT\TRC;)53 kHPhIm}FP.h.vn뮅.Ą'n~MW\1>@`Q6K7j|ڜhl/dۋDӣ"| H_yE〗:_>?qΡytCI>,.cOQx!CF#A>97uk,$+VlXg%q<p'poQxhh8aָ|"!|Aiĥݹƛ 26{W1x[z۳ duTE 3 +fy \|蚩\%L\`(٠D@ š|tdr7_P[=uqK@Er,U XuV5cϛ(`>B)\jLC+OS{,a~){/ko.o[3 wVgA7PswArB [3DWl :tZ,1aI#s !k/=g4. `lSgStʈڷ`va+0uwVэ7W,`6~ +wDE}*2"ͧ +PY @ +]yr@Α2r<9r:Ta*(8}G|02G5=xG|+|uڷ*Ύww_n >{/j*=|tx(:o|jWfLM:6I,9 ekolg?o +:j^G^1fZ3}U: 0q<)T!.Dpn#B +y㍯ĶD@H2t,yz`:|^\RtS1U~l  Oe +醻+54v UxZJ?(>42O,cVT\1_-֤l*Ϡn: y p#Kت٦udErGbq$vo'ax7+`-gPaa5/8p@H*jϛMW#%c(>:'yfGT]2!PrGW0av^IiCLRfr.B'"R1 l>Z`' `b l6>_Ŝ6uU{}Www-[w5>_trfgZ?"aP V(k GHn[rw!.:i@\@r1tT,0 A.@7Zzٻ2uS3.0dD8 ]1ȖbumdVߓqт|X} 4s7DϤ=%+pΑAx C3'z2+jҩ\֮K2˜du[9<[sB!`c͝g$aW8>Y$S{@J\V8Upt1ؤzm3K)jn{fe ')KW^2񟄚i%nFdb@D9ς|#0هQ53w/9}v&^ʹ̤kو3)zp@kQ Y1>8H?1! 1?%}c3t mi1Z>D`0Kfm Flaq*bts%\ܽN;0 M gF8[k6-p,~Wx @vAo@TxwU;šg7GS|/BmA~Y1x*RۧI +|=[fL9FkocgĊ w&jnن/XĤ_8/4 ,hD 4csGn8W|ΤFMfGs%Wl.Fp?bS s;cۏGw!{ +u~4*G|K6>3Vx VW@.OP:q*UxސekAax ұ!7pE叧c&A^ +]p@5egK&DnƴfC'>/[Qi3Z{YpÌi}l}{ cKȊD2= qTďͦlyh!]tl)6=YEzdIh yw!ܽuCh~hz*!q9Te)Z6HlzmLu| 8< q)פxJ\CqwdۗdyG81Gn=:iL*|E9/`񊠢QDtD2atEMbw[lvF~[~%J}O;>oT}^M~XC{eÝvxMQ-ѕWM~ ƌޏd}NӍ];٭ͷM7٪+@ʘZ<+{߀?# 7HM0D؞n{jj~jx&!ko\s2ySwʛ/M'l g9ٶ[@θ̽ /z ~ҩgE'׍-W+l{hԳR[xF+j~rrM4=[#7k#:\a;y͓Ne</NPP^A:mrv7{ˎ>" +oWLZF}I:=;;d '++SW̸;Ozk9Uu+6jxR^zYlYf{讣{Nd/kɂKlʑyl%pL+E`)?A1PCo'C%zCsHƷM%wLWiYPˉsK'/,[mn:O~b^?O!/S޾aμu&O|`S9CO^9([:5|Tcj{y% +N.,Xm@Oײ'WKGO Hm:)Y`3$oA#})MyЧmeO>pa>ufw=#k_[^.w?>(4,~BdE`ExF_?hwZ{Zyo^@ sT ck'eڟlnQ${0TKZ{9>eP4?_h}fw=6+Dɂ>4qOƕ=r+でs;ֳ>#_6ˏEq'>E~g~aOw~lw7ஒѳj{.>oxknzF`kvtU[t9!j{Rm_k[[%˷?!Iǜm{n}mNg}(OgK^3 y59|zM+5ͬuY^UXwzW(~.eg0ݧ$ݯ ko?S}xǾ~|0RקgkwS8r䏿leοv=P{x+w0C|zinP=VpW^n{J뗒߈tzY*߮F";Go{aBc.jTyuk:o>_>e>?Ϣ|~E..p3I{OOsy[i/Ͻwy~R\Yw6yEO_ǏNV*?,v];G:w=b~b= otu\Hcrs/_n>DxNjpÏx<[W_dm]ihX"sa>LUU|Q`dpr'#mn͍SY~g7(LsGcYR1FhcO?r=w;Xy'#[}RqM{^xÝ}͝x~Aq͊c)dsO;ww3_])q~ɮ:חޝd?Ieov??{۽\lӽ=A몃Ww5Ty_F`Noo l~n]CGBU&t@WK6?̨(~XWSNl z߲Y9׳rd>X M2yu='/^w}wXl>ӿLY߳gGWŽ,O7wMq-.Zn>ۻ֢rثOVl9Mu7un\K%&pzяYnOW;>;Qp]PwȮҪ{eKގ/?›]!W:".މ,Vp^XMwËn܈*>z3fBY*G͕(`?tf;:Qx_~6'zkCՆ:edI5J27_o󘗏Sϓ Ϸ oe)({{z2?m+ K;rf4mw|y{%JW]^\RYp/`GtEVx < SQ%GкkPVByԻ9-n2E7]rGor.ݸdJk%.?yUwn9w+l)9w6?RʜOs&?NeĠF'{^A|ݻk%/ _)._Uy?q[zAۋ5IݹǗ.ρ%ߣy:w+쵸Sb܈-\lgq;Uy#ԋq?(}/ +F4$Y*nVwV_K̹]G|ocuW+Oy2kVl\ W}}ᝌzwlćq/nw7?۫kSAG{y;}mq~y~t;}+0~a+|ݍ,s/dQ%W5=py}u^7we[oqvOtTͨTz%,nYÆ2=7^*tϻr'Y=9C2&Z*=STY?©_;\^+wxwYUGH}p>ލ[~m>1}Kxh[ n%޾x+^b󫽥V vӛ^n^~^}/c/V HHQsǽܦ{]뉰ב`]̽Ʋu"ws]g샻 GZ:J"VT~r\IjOW).vx}ՑYMuO*J#_a^w!yJ=UURd2{W?E/~J>m*1 zWX͛n^kXz3īdNȎ]/bݞIWwR|%G.%ᅵPzr\鶫N/}wk֝쪓WcK/\.jG:!y +u+qoA./o[y;ģ#"Sn岿{hRo<ݻ|Ktuګ$+%F_}'16[)14^!7VxDBLoMԉQk{k,*v.|Ϗeʫg7_L.-SOW[M;ŮOT!\TSx3rjն;nR8ϦGkFwz-}ݣ[Fn=8~cU=?o8?2c_W>M2Kod/%zgw'&"*$>#п&HJIfѓ,5|gW<5]gso_e3]שg9nVuV\N-k\RR)ҋeSʿ[ː>.=})Njq7J,/Zʷ |X埽/V%8[.W$sI-'Yc淋!exֱk"gOד Jt轴$Z렖i㶡iK YD wWJV.+YZq!jZYmSʶ_H.;x>̅ңK!ZŗWvfTqݜO3Ny\QCOB ++*ֿ򭭻dޤyHGgJ ''ZmJaF%LHD(Y1{crxⅸIeWː)>{%(N^)A6_-9p5ZJӶ29?b;ĕXkuSn a ~ a"~m_?v}߻n0oZ ,z2n͞[>/ޤ*sx|Ϊ{EmޓD%"( Q9CUb2DI3JT$&0ۆVYQn[y}κsLsyyS#59b1l ⦳ȸ/=q1W]įJX/B 1 }9~E߯وh4# ,;E+sV4<_|b~ -'|bA峅.4nVP0T۲yƦCӰo0sI㜰ؽss꣈ wmN\ b?aK?AZP_s)F4A^u%ν _4n9[sڎa6BV;bď?߃gM.B(2{=\u3\'?MpeÄ=/nq)!GW=\F޶r]otRAӶMUJ4̭/O?:t~Smo$9Qhtᔊ*:V"#y3 [\,vba//}!v}#< +jq|ohSAw=kiL~.g6ѰB8`dcDd5Ҝ 5ll4 \Wr(^yKo6+ +p&cOa_ٍw`zzvóip͙!£i|hel+?SGNEc qd 2̌!Syt\d7>=3g4eA8rڭ6 ؊^ ёOTQ %p{qŒ_]+y{xǷ +~n;s;r-,? O2ײFFQx!n}D4FLt쐩4vds?sPd#BYEcWvJ4}IVL?sEd [ c \!ye眩D(0aY,m2['I.9hj@%t'vJ}Y5(g^OKԵr]]8ܦ8 ^iz9vNˡ ׻_Y/ =D*S sd= 8l8bL}<2ױ\7푥J4q2lƠi+45M\7 VRmW_ZbIa轰}9w [8%]ϗ]5[럴^u£[YdR2T r,$8ziXg} 2לDlcmf&z6Do1z&23Yۅ 9hX4eEP&T!V&>ptI½͵.n:W yunf}7evݺy>7)v d8)C 0՘G%cenmv8aMb, q|2&:9XX屁c`jHX<$8-6Iu˚l9sill~vۇ]?<|s'P%5x?D6Kq\p?YOpFy8!WBfc}&gxX,ǭDVNhdΠ {#FsWS_Pxkg' {rIXZ~xxigjn_(z)&//#K(ՋWBMaƫ&m:$;/K,'Q%SGhC:C&O\4y qKpN)Bf3h<9朄T9L+_ƂWK>9V~-|"Eow%Qo{9#qOvy~Skm!^oPsֆ-2jۯ7z~ks&c@bhdNGA_jYd!36aAg";ϵȖ@#4ʯh/ȹi'5ZTzKwQ3E\8!a}S33a {) +zr88wλEĈ = ާ<}GU|]1>cDwdn+F]֢9T)MZ0fC}ժ3x|\K-zh8z\y%W5-ۣ"pes疟ƻ/B׏)܏W|WM+Ƨ-Wmÿo_>[ʏ!?V:q/Zqyw +*:)4L5!0ӌGN¹4Z& +F6hG^ќh=vGhb-Ԗ$tU]qWpy(BɍjVʤ?.L믳}^+ +bM !'9&w_U1>KnJٝ_p߼(.chpÖ⡚fΏ&[/R6{yDo +\. D <UhYlҺkF. 3=~$b:eOd!iz߅5"/'!uk!ثx߸zmK0qޞB }~˽mQ&?`G1Zh[E;XF06Mƾ@(& d~#Ċn \iKN\|J^:ݷtK!o֒m?x1-7SgSg3ʏ]S(o] +yt?Mэr`/e=eCRU-t_1i ':z4@56_&$:+*9mף>vNjOdn$D梄՚uEy%~Ml +>'%ܓC%r)Ԟ_e5L-7rŹqܙag?ٝʕ;P'u&M_I?8f̞+CK +53N $B +1??,þ{C'Ox|x䭗ɵw?m +{ChH}v~7Ƿ炓j4z_|R 7b" J !JAt@?ۊisTեd ی'I$FktR qmB4AԜތs>-v}$u"o*B>{̃w#sSW?+<)R>}PP:|}RJY%)n6WVa}в =爐{dmc i\]WKTGR/$Ԯoܨ֛&&76SMvl 4a+훈'0[jX p~^|_?G-o`*E`Q1a+v~0irbc8
 s0^qQd;?':&G K-Y;ڃzfU)}^~ionsv?З>F0ަ=Un/XzջcFx^pkOv'm۟-~\":"20hr'4zZ`id^(4~n䆜1eO,̕ъlHG#dٕTi'"GasVFo+ іhɢHע]xJJn̓T*8t;J qoy7Ln (aR`UQ{QҚ33̞tJ 9cdWxz#++)!2U:Sf)m4_zqtE{mQ;ߺG^r_d_IlASyޚ竂n^S^^;UPYz>Exl:c2Tc2 +1F&#q=Py"wiS[~y*93'Kn6r[ĒDz4ʿ*OHhYh̅(+4Sr#sMBVe֌;//}GԱ$ݯWzf+쫕Cr;zI:ͩ,&~2f?jshd7a-`ץ~{m^(< g':#m>eퟐ퉖Μ\(lR>hhG"dMlv%-]})̩4g +1˨+L^d!þPyvlӥo[#S'+.}k2gKҽuƲ/mŗ ȋO =O'L=:#ya;! Ia̼|?>'wtrY 3u=/b禎lmXBГ.yJ6^4vC}40d۟9GUJ'o22635GgBqpOD/nEɏ=Q*> va_u~{>$KCґrrYң?^sЀWB7B͏jZ%Bb% +PiHRG +WDNi=\6̢=<r"Ua!I=SBg@opxq_Z~kWYN]c;'YTj4J_7'g졷 uٰM{'SelŇ~u Ӫy4(e +(;e&Ӂ/ZLd5KT[bb;z@KOZnZTkog#=f|i +Sk%aZ4M1FϿhG蔊Ѳ]8ü @3tDMUJ担|3$뽻 4 T*o\LnXo"zA}퓝/w `l:=9X.'u?]}h:]kJ嵎dw(zm(}*M݃WUdju?q,XyU<u\ uru\}EDX$ V1vhyD/RCR&kguOyl(Lqz~,ӌj2_]r`2ĊXu +۪PšJzp s^+:c q` +hR=Oq qdI>+iVYB/rcӨ`TW*]Aa>"%S5t#Gf:}]L2jgj3WOa;dzi[L#zCy$;^L{b|Ͼ .|E{/Ճ>^.W*Dϟ QՀ-zSݾR1ÜywBOim>2?G~,DoЖ٠VHӖk:\HxlTr!' TX'*Rʌ 3*:C KRu@+,V|`}Hӫ}=AאI2⳷3}]'E]=U}Ӄ̎Ѓ4w``zg15g8AS"$ZH.-//du-ù펽oi+$UjeTe^de6nD5.bd>{!eν +a/OfpLtv[IctDt߃A61 >,2 /k;>:4dOOI;AԲJ +I;IO%d :L]]@7:8*lJ3SD БsʨtUAdmph=6bNL>^twbw?>Wj- ҽO{KO%o%~pOc䗞O?]}-~neՏjϔMzOQ uFTj1~*񚽬T:BZvd,99̱G+p<%8HUDd'dbSGf*K7ٺCv̮<|jh.1S/nOeC7t7VKfC)Wr` +6̭lf9I6łhl`ŋvLds[,dF:!SN(#z;ے)[5ρ>|ШԆ>i*૾C'Rj Lq0_߹Jvqv}pu?[m<0 A{ +k=D kXS<]/CeJt{gf@w↬B;j޲  XW7Ug<[9~o)􏃞?hGJxiPڀ +B*h' #.b4o2hSrE}4MKcGJkJ5A94U3/}މ=Jq]Z(}GG|cpl֋>Mw L땪W>™tc9\&i,޷~ЋKWoI,Rƅ7Q=dGR!pfk}Y{1FdV& f)\%AS/]&4 +t4h{Hwum3/hu2C&˒)q,LVٔj4q}وIb̬5 %4n*Jw^:W5jpzb./@cM).̾OÏܾR@3NP^[s @{ 4GOie0g@ÌLR>Wny [ @l`AHA]&U[;.qr_yDhE&81z}B&@!6Z _ +MW:$IdXӆhǍ|YD+t|S㧓C߈{= +<ܑңD˛C^^^9*QB_>d _̑D톃/K֔tu峖 o\kGLP&k° ClX6hO jFb@+ +%ϦךkRF@V~v[zF <ХM%B sGxǏL!аgpNY<6$P[,xMm4@j}hi|.zluŞ/ϯ8Ezо!ц?樬=rg Ч5<2!ؗ`Rp|$U*: j?Yr?tIӴD o\zq.sHy4oe!j Ӡ_ +tXRCi5LAnH?M '{^'^ѧdGtq͆&js@ Ult6ԱWf00W_q4𺁯Aw\wd{Bnl :媓wbh qw]Al}Y-Gsms{9cLb!꾸 UVvIOO!]1H18YzǁIPu& Y\b^hcȵ_Z8o|.5`lեlUԘ\r0(Z*&@Yyh6hm*vߓлd֕Bc5 tdYdwaXԙh1F1<+U/:Vp``- ؝_{)7lv86ے5_=w_z*)1]W Q^mL-Cу oYLbk7(D/'SzdSsTlي3'#NNQ\-ѕ==|i.X`}: +w`{_yc*poNXhD&hA\$ܬUFJw0ҁyHW8MYߗkY7cHW lY!ccA[Pl є M戦m>T޴k +H- n̨cK h甠1To|4ĵr#/714.?l<x$zvgq&, g(udpR%CG\iUBXmkl +†'}@lv̍16N]m X6Ԍ|Z^=Edt'|m`gU; wqwv['y \:+(T eVms k18FDKGQk4D" -P6xuAYm K8+{"ȑ^sb=>zXچ,0AwA;ЬhAgx80>T1yR$d }a}Kv +E3I.0C4YcEJ`gEdTewMמWLSxFtnWf8P̬02 +YqG=?? +4lieFM}ӂ:}B uSy*?ux! ME4mX0[_c 1YTVGh sqGoЖ2ja;$uţ=HbLtx& D(yes.!?w/Βr +5Ov$X#( +Pw /#Q:ty|dH&|q+lB{»זk>OVh8 |N^&aMdv7V +Z\J1_f\]gIa PՃ}#U; [7׊YiIXN8f; { R`gAbgV YK?Y,c5!Rj)0wA_r – gB; z~>񊒮ɠM}k4_<Tg< tLXLcvw}+Y`<NX`Zo1m Dz x$#r$"ՠv>0rI> P A*JCYi*6 8P)ĝv@Xe=)7Wwv!恝{% u`g16`+W(R +GŦGOf8~ do +0F΁k>kz3U^50*[}ȩ@ 7uLf!FB;96η]T9 1yzAYeu[s_.\Lvr"=ӨŶt$쬜V`gi\T$sy;@7&,%𓁝U8R;+eATr^`m}oo@N,0ejV uFՀK9"`jdXx<='^? Bv൯2RQH~$-G#wB զ7x]X4f 6 zlt*[-mKJy'ŵ ^Kߊԙ̏@#=6]GOvV:*7G^{^\6Z* QmW̬3;{JPr![z ,|}:2g$*M.; ֗9@J*) +X^fm;K^l`WsaqX7QwZ 9'ܘvk]rqUtp"wb9':pbuH\R!BJ!a=KAStL 15م5O\4X/am}ChK`mVwJۈPT+ǵ*0p`}s)\m<̡1ԭ'ya ׄ}' `F!ly|㥥+}`8?sm[&Ģi|-'OܯS u.]5_W6 +Ø0 ٺp_r>r}1Nb7=\\nxвfȚXϪY֋ /Y|K FV󕹭V$~-3ͧZ|eݓI5_k"µ{.ɥF\\ïߖt݃+mjp>8>GbDi5fb="ԉjpEj#k+K'$!Vhr!iڰX[`TEL2”s[LX2j q=( 6D0uc4KW:]PyVKvNToÂeB.6O0€$:0 g{|F}䥇aMD^;Y-Q l2U^90@oqh E%,6n>Ol3 ͇gÚ#Аk$YO;?m!u] +,2C[s|6 XiYx";s8S- c]Qfu{&=Z8UsֳƖrgHB֬gk6\){  SxոﹻJ.Mڗt=vp]Qy|77Orc'\T +dnz3"ENK|o +{ݸܑHzQQg&UQgpNM}bhu,R$1c8/"X)T#,O{mo"`S9Z֢بX-`m|>lCfst(ZM(=ty鄏B}"p`mz3pϊ0a LEVwYpnIѮiXz +&5{9\RÆ&iS}=ޗDl25>:kRqt4-HZu`/ oxj!+ze߅ͤ1XĤ}3C.%O/6"yJv8eQ5';zvxb.\C=/ǩ0Up\!>L' 8o?=[ϖ9[B>!y(57=cw> n/g-a^OžG =Y[ g%>% q֮ >ܛЀb,>{2`-8El:-$h4*pNpo\sZ9a{iMyϑWkprFyR!ab 97eityŰ>#/ߟ .n `rtį+}I9`r0?X[l u6d˫=hVd Q/',.zjUl|vw +ɝ]St-rplgyߪ|TEA%Wpl>y}%p3a"nDzTx}r&C^%1Ϛ޲ϖH^ZL5_^T66Kl\|{H +vOWHo2VX8媶c$M3ue*dLσk|iJ)y4&Pu< rf bL^9Y +'A<-aϖnifP;˨73 ~Ŝ6V@[XPnBXM>p^a0ǹ-LJO#+`:;cT| +"A\-d꿜9!b>O~(aʌȽRM?1i8߆m,:+tP'66EW] B/ڂRuL qas6uM&vZӆ|r]u|Vo +97~alE%j'\Jg_p_s53Qq`L(3$csOǧt|:>Oǧt|:>Oǧt|:>Oǧt|:>Oǧt|:>SBC X/ދגuIS:'%E%G'ć$[;·X?/$=")h^} eֶ'ΰuޜs%!ֶ/jl-JߔF 20-,~jqH2~J7]"sӷmpk]kmE3 Y; +D[#td Vyr A?T!CkmMw7?DRe4-(Tk4@K'@]WcVǟG."!||(.yW/cԁJƿ +Zj}4A)2QLVSחʩ(Ϭ52|=Ӷ*Y*AAZe +zD+쇋MաSk)񊆳K'9;.>MzIa8h6B6@L0 4(uZg2tPEt&cru٤}Te7a>PC4-KVmf2iOGkk@CBm#zfɫQ6q9zlB.9z37[Ht/.[C_$3=-ԗ*X`{ ؞w2h3>\|.](6VwL֤֗$)GǍEjqY]\\Pp>I<ߔ)>QCL֤C¯QQ/;Qk T<Ĕ +~+?esF@?W~:b*\-R#K3 +t$ s=|=Й z{AIlLHb="QC~4(AS@Oe2XIׁVUF7em&gx|t kI4U$C/#KC)a,_}b.NBo7uM {DYV=3Db4ep0gB scd>vej’~fU)>zHR#bo/+S*MeF<sZDg "'A %c-zOd<~>ItXLD, YAGiB/Hpu&b6:AlNӂa[sE +{%SL@tz@CC\m :nRĪˡ'*_ +^ zDW^+}!EL.h.ȓFoRvM8*ʺl'm2G=-gPUsl :'*J +4**ߤظ|2u1hkZ -u#Ub|]^;tEh#@i ,Ķ}Чђ#m=dԙF%sWMzx..G)gN6+xm#Vv-sAS@YY{2 +2=S|b!ѓz]WbHbzz9&_ G|*A$[Mtב た}p=@O]ʪԀF㸰&GjdM.4Ct@c~RDCNa z + +ob>n˦A/5s*"M/-+nӡöDG.,Yþ9yh:o6~x 誱DI6xq‰@lqzwGXs¼ĞAk 3ȅ@f S7v0D%qfDi}hEJ S¶>$l3g>Pv=2cea$Ea5H<_a^n%¼=9чh5C#-l=lA4؊ö Z"gMmﱂUҏm (@4\5Ұ/ۗ ~ДekoWնOs 4fD100PE7 +bآHw#9g;p{{~~b˜s}c>umh- -]'8΢0k0k&Ff(.<} ;>4(hs)S^*]=Kc)Et[@n Z:&:U?y{"k7yÆ̞ h"XcZsW/~^rA9lEӄo,a l!f& c>qJ 4 @' +h3g.O`R|PIQ6{DC8@ /A8mt Ԭ11&%w6?d5C|9p5hB5nM-P`h@YT_@Ƙ2 +{`dV *жx&fpFqAӁު瓷~ե6Ocq|)EБf_:6hnxtIQ/+1{:, +_%>j +Z1Tоחc?O0p, ŶA +!FY/{-`/~S$b|.#n[ը_dlˍ(f7t阒TJ^ +]Is!p%#+&ԩjԄ 媠2K\X 40.{bej e@\:+S~Ih"?({9^ ʟf#ӁFhV[ -&C^=aЁ]xgccg}&9HsWNbC O" w}2`=rmH/FM\SCg@-/ ߀^aMx(O`|S.sqOzH>W~DQˀvz}VKAxu5޸ +TCE<97Z=fND~e;G AA Z#rg +WAW[Ș?~uõ'M X 럻NDUng2~(D@&EF|!Zq< Qqx/#c`MHۏ~ \ze.=BnGF7Ħx?=&ȧ R*G4t`͝FӦNx.x*-}4(?)1C:͐?R+^/So:? b]!>8քt'Z +̛ iZM=|koN/ Y 9-ƄZvL!u =>a\e어~|j{hPӠw-:ksi%QM]4OD539T}:5y@+kTטpTb^oev Ʒ0f+ޖ+2wH=*,М?i;r|+ }^䞤$vMx"j +_>G/71W"DcކhX-I/:OΆ_ Is0_4Gcb蹘">hlq+s1yBk˱^DaJ(Pxdvj =?99"KyJ|V~HKq`n9guT`%п~P<X!va:h9b=D < fC:)1WCsu^k\}o.52b 뒥Do19ȯb4%GIנۏZf- ~*G endstream endobj 27 0 obj <>stream +A<(wQnPaE~)m[1ᝤO ?.?`ln/ G ➠ +ixq[;\w>hA_qܝyWɔ.:C{&QW:|u"E#=Iw zc >75,wcZ௨fB|(x< p^пuB3ZМ1a,A{LhӄEGGKkܟAxu2<5t!!>ycr&iDz(N:ZkAzX| +Ό|"F1J". .N)d Qmýwxr8l\Mo!9!fzLX8?Z=&n~y)[D5%1s$ +pK#%spŐX\xd,\Fo(xB( F zFJ/xU\W,* +:mt%~+5<&1QWѥ-l55+*ŰCu%8j=<>/ :q2SV:wT˄!7W Þm^q0T;a脢 +56TN)S3^nDyk)P ++\\YJ=[sa]_ +csX W1لJ-J|z^Y9)_|)DejMx?فoϵ^U2` xxLmqzH@r9m$ g4p@l6qTCoEW.)3QwVJҗ@UuhOH6({sFOIxBh)d TkI۱'$xpGm}оW0|1g UuQ2qS7(J*xL96h}"}u.x2x3Wb iG\]=/{|111!= +>Xa)J TQg+UuORTa|' +?|'/S!-r q5rQj&Z^XK4#nPO^<:Q@eAK&%|.p2Ή@NmQxFxP{.+P}Gu^|axZX.9b}eX;eE!h3宒d,5! (a}%g'^Q1"ģ/<{7ǹx!!vY?W4 '^1hre_k􄄱%3K6)xck:ڄv2JNnPf# +|yf.o=:/x]#q>ĕl:Xq(F2w4wO@\%_P'`?^5t-P%YK_ŜR&ὲ8RCTNOO^vɫS<؇P,b+nd4kחaOH N9π^ 셡mf[53!倣D:BD +~X}9Gdg{@?bjhh5Ox +Ç|O̎xޣ {rր rX=XU'+}kp=pԥiT`s!p^rЫF]_ { ^ ~veGxc{Tp1b'L@v0.XA>^{瘹xOehZ]]k&Z2j/_{")o3"rU-~o?gb^n!eyp2%7V=}.񾽄J/ڧ'| ?.eYKg0Xr0h.nm `]qJ랰uƘ>qi4<hPKY@<'g)yaiL%apfB6v%'1;e7N!֗%qK U Vo "50f k؋~ B }~ q_LQk )85C<\\'Ea6'_y&6֤ ASӎ>BiBkAb )OɔJekrF~(~_lƾAVVgawCg'3[Ԡx\Ϻ+>+ e;/74_6ƹ7Z1{Oxr6<EпZ}X煞ǡ +7YZĚS(cx$ar}b1zEh,@a)l:FM{m[c{Q?2@M`||hO[1yq,yf$ 2 HGm~fQ\ {s+WKaC6 ݃YPS9kmE<ʭװO=y楀GqbO*XS\Y [b5lDVYل+{ + zK//lh&K.Q,#lk(pҗ #=ScRy[i/ +iLX&h[`DzTӸ֡蓉jӂoYkՎFe?$55):p2jj>}ذ}CJ%B5k+aXGEx3EC# {@z!P{G@ށ'LZ>*[ +#aen/EMgg^ѩmDMz෎\pX< +JuUqP>a/!=ɐrxcJͅL@ +R.`VX*l +4v͞OFn":@_.OfLhwLl!̥Lr>A feV3j!oփZP״|zmFdÊB 8DV]d Һ\񧙴K<|3wa\|)]\o^iHWa N+j/z'"YXiI{fҌ$J]TwIiڏr.W*j&UE|J%Jj?] \-a!x9Wa8?/(A}dn07؜6i浙STXKѩh!yoUш;`m{]|QHtY,ɭ9dW|N҄Io&ӻTѡ1DŽ^q 1F_fjz0bS_zެ25Tj +>6 忤&eX >(W{]z]G=Gq}֋@7K(a_vFxK[N rO>l#77!_| +K[ĥ&]0yn֬dhຂQ"閰,Imj'̬%>/6JŷEVJr5k2٫ڬU[FGož[<-"Jo}ТSY_T;x~(H^g!󒸿6PC|9M5f>}\%zGo# uvtEmKUz(_=(>..l8+"ީ11-;ye3R2 uz'_[sKR;=caf>C+ەoVKoi,BF&}'i25C|E@? +ߴwgJG}y[vt~)>üd~>FqzX*dʔuYJJ}$}Ee5E&!3>هbqn){eZuȫjV飊#ҧe;- }ˈޡ'F4 +R7jw?걷H*O>ꢨ>)0ے-8Ǽ:T|8Tv^ .jWD  tA!Je>\<$h,y59Sa˼=LeҢP 4q.Gxakx&Hޔ_4wQ_?ZͿ]MG3C>Y +bz%jI3[H{UǥolK\n_b'~QsBrI$j7XJ&496_ a­b+q5OŃ%O_|د}@x4xkV4Wy`\x嘣d.⾦#ĚtJڛڎ~Z2ٗi*FK2'W=GK,~hmR$.mn>)*n=')T9T\o7e_vӯ;Q;2F4P+nXӯLgjz-n%X6Y']mLh͒0=_Oi"mJ66] S>ne + 1kˏ4-?,PǫhaU軩N|!H=mcDN7s:^NNjLO?0Ѹ{]k4|?髊s&CD[V 2h $ބ<1Uu9*̲9%ZpWNԱ[1`qsΗ2aRY5]'K+_<9/yq~2Z^DZoϼy'ƽU–7qgwQΪ㤠1gSj%vs19D&.7M za?9^O\M-wIp Ip N<ߜh F"L5H-@5Pès)[e}?C@ӊ*6h`W&[- W6m6b/n+ueIR7IiI }ud5ȧg GK;JEjK#.4&&z&y&&WzJZ麶"]BS}ܢV9Gf8߬p@]' xP#T}aAfJ5l(~pL7D dsv+ɖגN6JtTş-x%j~#+~-gium1q]녃=ŲCy2Ǟ)<ŔA8ɐ+zP#+?kX۝CO=HjJKkrZy!":bpQmXa]XIuHeuH)&><1jkTPoeCZq@YדH3'z>sy+=8(ѐI.I3Ҏ ަ agC_*TJ ?IKLz+#N愴齷*Ĥm[Mpr@O+:$EwVMKDLH^CdQMXauF7k~Mh~M*4R+]eer7We@SAA)'4 +YƩ +Ĺ%oϚ4G6d }j&Qaɛ BT_~1zT:WGE +[-k }`MnV)'Zo%\mW!Zr;Gf$KLuuOlLa;\u܍wo +LuYJh^G;>+v-q-:ޞ% loVW]Z7X˫anD_Fq/+v/u +M* (O;{\^s|^#w(OAeE^E2(_II>IѹQzo9=לO6I +ʶlaޙ6 +λ׆^qe]jLjr{5E a GPvpnjqH~l$ر*"6'&=jG}eT䨳)Ǜ"=oͺEJzC$%ͭ~Gn]GfMQg2SO7g%p7`C=Ϣ}Ȟln7ѷ#ef}"9 #j#c]#_6xg~ۢdMTǤMQbCcd]"~urƺ=XqB~̌rg~pV*O{'勓^ '֍jZfӤ8$.n5=r/ѱ:*%;j[mcl2@Xֵۉ*]% g2"m+cnuC9y<ܬ0ڼ+'BsR_KMx≶tGpܘmYqcccbT7;^-g_ܡ;S'8{V1~JkewLZ[${7"J"N6eDw܈`<[էèa;ߍJ%{7;o]sd;ÝC}})!, 嬒JAQrGZ-;guJSLQӥ' v9 vQM^ɢePMDSwyR~y=|1}6y^b}ƭ{5[U[ eq)[QiP_ڝJ"_zn +/ +="C /#p13VkU~n,E񡥾 ob߻ɲn.o +Kߞ5Zinh:rX5WX8\O(silt(WMطODh4n;w)s9,[ +dJK iks7+V([ -}>3vUqBAV[gKwYo=b +:-v /( {1"-:+ֻ2 ѭ2$!Wm4jSt)b0C>7@k8ޘm1Y鞂N;%/rE!S;n +Xip"BIa1b1CE$"1PA,DfD5 W|j<&N w+gc"D/lt5u{}s*0.WTZG7(/F]-=.v|!J[Ǒ8G.t/|Z&knuY~iI\p9o-j ۰SXTXf9nzFN󚂶lW"&(b1@%>r?^z,'yx˄"b/JW?*727*BoYzgԕ{nQ/ +\"s \"rbEQoK}*ٯ䯟\/DǼjF5bhts.Nqb(sJ_8t#rj7L F N$FMD? fNYMrU@ucsB9갸9zlVGTasշQYܢP +r 5yYSwΑŞQmQ_[>X7;GΝ/{=w]Bi t DgO@4ET|?\_{h.{Pk~[?e;zAf-#d.Zb^BX$aoc+Bg';QrkGh9-CVGGqx!5&-}Kt"7nMj_uF){R*K( r bƄixnwyuwpMc~H9_$jĂebEb8q؞<Ϝ[I/|e<^ž-pM(+pK)-vNz^[RX[EPѕnhr:m__P6,/c:{cѻa3I8MC9ii#/&&/"YMT8@Tv!v}=B[8!Ԅ(hぼM(D=E25ɯZXt)&2aKwiK/cC\ ?U/`N@Y IqbM;YӶ3 +S''ZGL +ߏ@(GJ,xf%?[n3oxJ(`{/<Pk}\5Ǖ2׌JJRD]ʹǿ \/JĔIh)7ČˉiVƬ!H̜Xa? gb#}I{Fq+ש导 9b,~~?1?QqBb696s6%,Q'-2$f-sԉ bN(˱e[n AQz\,#hw +~AX{Ǩ7Qo5F~[i'<9g84b42!75v91k:b̽Ē5bS sR̥$1s1CQ=~o/T;A6kwٲ<50Ts n Ǿ.rP3P_c;PXST'Y5fp0F=<{CM@N!F"[BP”5h fMM(NML_&Ebbẓ +m_b{;ps[?rG] <#5SuCT[S<̹ZǴ*z䷥NKr{_еxMELG ͫihM^OLDc8 F-"f_m#0k+OU&ļĂ}}bO fOv6C@_X&2{܇}|yYd֬uv-`DTZ6N E(G949]N]bEĴq PnD䕄tiJkL Ă '{bmm^5p*<]*0 ȊWztm%OK">Tل~oK.vHL*q0}a:fC?~+ a1hơ6,|LP<5:llgPN',&g 4&h-o i7KۨZm3 k>Ee(wƼBXhtscBgCRoC:J甎 +G%Ejp[&/q(LDׂ/%-t*Ĭj(W( +3Q L4\;k71g^b +1w1oM,xXJmt+!y/~Wm zy ,L/*t~M/~9mV7F WŻh℅u ն1BQT4N +VsP=^XaM,fF,Y#CCr5耚;{;”Ă4`XzXHcGo 7[SV z9 H󭰓uч:Dl̊M}'TӵӞ(ڈԲ2ې +b}Axm#Ly28ᯀA _N1ah>*SGDL@Xs[xR1lwf읶=k;.=ǯ|j.b-j;k;-Re(lc|$95w-t=QZb!j9SA4C|2V5;e-pF !r};r1<ގ^wuy#x<~o-Foӣb¾ܚ}n"flnf-S t8n'r.VDfBٹ2,ƀ6$!$r 9+g1xR ӍjZnR?Sea$zݔ}T+>>&.h9 8#qtA?f H#2!m;A,հ!<{6SIJBb+0lǣvݗUvg˙_; }~w훵;=j:7zA G*8SAiցPzYYKc{];`{I?U!!AjI *m7RXh6n11gVb&#bIbA]'7E R3ω>=u!3s?mx6Yh~x:9WC39X0WdM󠑺6;vC39#ŢFܑ;Ӓ-$!L؋ zy+6LE*q0O>&qEqt \obW#Oܙ)~3GΉ5q;U˸MݜQڈ2 +HK7hxm|m>Pi4q.`O{ o턶:Q>F2QAd1W|*l9w0wIZT1z!1g ؔG8 aIˉ[f$Am AI_9KOa j9R#usz`3Nq adC R7+8L?rּ6B7ϭ9J-ԵG,?db5Wc$}'ٛtImLuLnc2@]hP1vQuEyq\byԉ B,XCӳ!5|wݷZ% '/=<_(8:V偢+Ni*p#^:X 4 zm 9(EGi9QWHv5z-^^-eDALxvNUI6͌-j>!~_z(k`7u2CQ]A*AsY,ۋy~LE5c:7i%}s(_ Qݥ|s ܠe{mvMb Fz9iLnM>ԛ+>TpBS1xvl4 яԳvg$y1f 'Nﻤ̣v1׿K_7^\ gd7Ulzg6-~b z)n&/m)[8)1)-򔾨=%i [ydf~2d BQam w0 +WT#VX-o[ш_vqBDvvkBPF<%MZ؍7sMY*.<\#-M\< NJلƾmY8GlOϳth 1HВ&w S4tjfTuYΒWOZÿU*%SwI~4kY#T-Gi\z8IɴY_Ѓnǹ:,jS6ZoeNڏFޭBSMl3$:3q!˳91{_mG~M]`*zu)u|cftOu˥mLHمcgG^ L1ίH`xy nO[풫O,^EJ,Ey*eb)`0E^|t<@y?gi^Q0 +( uai  RgRb6I3ihsSi:f7tG5Iv;-6-e?tt1n*5x~olxqP=;Z9vKٓU V3ݶ²o|Z!ϔh>[N?Y͗ۗ"DS%Lf.8M5?4RTқ]Wބmٟ5yY]{ً[Vp/m 6uy($E.b?a}c@%Ulᅽ^aֽ00C߷^6kp;59}_9 ]t_r:Fk?U;> (+?"7O\SE|Kg]lyfB-a62W{waJfû?ԌU6+]o1U9qF}.+ e 3r)M1 3C +Sٺضd9gVB;hx $};bݥ%_#>wiF>էE?\Camg)shxoga&75ɇy׾lQa~ &VIm· lgagSחsUܧЦ_Jli.ٍ\nHSݩ:BdAf*}xhfn1srBtqгM´Tj j/r4 $n5r=Hz{oUV!ww'xTUq!n4hhwq~ߨ1ҡTR3#}N<8y/bŻ#p׿uݯ7}/w|y|F|W>ᘣp6ac4cYݴqf]7k{׌3Y'dnų +1[\9&cf W_:q/<7./5ܜc\zᕁn\GeB^T>fXrr6kz{^>s3g[꣊p_R4g>/:/wsEgbTw(ڝan^Irz-_o[_nn6HO ^=Zlp/wb^g>E"&ՌᓪFN,]x_/Žg ݟ} Ӟ=_(; yX`~P?)u٣{<<+?WY<~ftyf~Q IօZ{XrI:c5[ܺv#ظr!:NdX!G%ڪ ŎČ +I,Dh%'y矩…)|rF{k($U3j Own|ܐG}oߋ݋4)L:ۙ\: ']{eiG*5޹ 7i6ݩqٸOs@`9KmxK4l4hW>930g8rmpOoV/kDc%iW8Jɫ,Jx&K/׵<\wWOpز.u7rmO7qO7}?^u~P7;~oxpVx{?ׁ? ,&nm<0{͞O܇ ckFe B4׏f0MgU.܊p7w<.<ˆ-!tx~ )&Yue&<w?{~aNCb_c,| je%ڞn G2l􃿹pL\}˵=?n:t͎{=4-,qY7{f]vi\{hvcnޒC';B{3Z֟sZ{@sb\tP>4[ڑbYgh%/6`?ՇgQO~7}Jd__&b~Mx[2;Gzmį;0ܡhcTI*mWI޾ɖ;6k6/ޠٵiKCkt>Ֆѹ#4[mp%h'78I7CqjppE +07's{f-6A |fxth&ld8 sRũ{(/]g|QW|y>if՚=[kKcYIp4:aL%d~SDv/soX0JN 757nۤBjTxn6X|Zvl<⤘44uRY=rbVDҒLiGQvS|P3 Ny<sШJƜ4 [K\G{"줬X/ݩ;^no]/H%B:B7;!ɤa͝~4x^ J "Ts?r;7WVrW'W0r ȑCڑ:\j=l?Yw+:hfFI%3M)Ngrk GZ>V4 [+OҸQfB*]l=n]_!6oI{f8Nrp0>ҍo}Gӌ>+7~q>,]} +&Z8~g]ybfm}Dr'K'wD.g>ېnd\1f c*F!q4Ux`taEΟM7 ͸pEfʘb!s,/D+qRJ~t꣄BB_;Z, nq@Hs1 \.\7ۡ!uN#VQs?]x!1`$4\3=WՋ_3_P#@3*?=_,tJhπ ³:?+w/{uݯ6a@L&6{:y .,pwY- t`&l\d`w}h>,k8{r[ۥ<556 :"s>]l J.i!fZL4]yXKhɈ=0T\w\q~i"}UO_0$8J\Ik Ah7_}A-fU{,b.<}㭥zV>_iy;]4׸N7c +Feu0ӟMl |Z>EiavЊbr0 qpäˡKM%U…ߴQ(0 B)o-jxfĢ3gQWFZB۟;\b@.j.2<;12Gz)V4I&'p{%␉q2)1oZ:x_B[.==zu,4gB"ҌJt!>=_o_X6]K;BbXfR`xS0oĠa!bfD>tO1ut< 5'Qfr3}@_9ZNHHV~fx6۷A7QjuTmiU_Zf'}'xVϙF҆lxo֨Ո/ʟœjx +~̗Z,>vNb+GWŧ5C_o}~͆y5hth WVriC*!A/?g*'<@6G?[c8l8R+/-&‚}!y=|9w'KkՀ;z0i{]E┱<@Le1=lN>0īdM(OYʱ +Fl-eLEJ%rh`UU]YV[].0Đ(kԉo+,o5B-^7|gŞ;A3'CS9L1x{*-cTh +)NBD> + )1K/lM,.gp֗'AW`1zWN"L~phեc1-BRbtLU'=)9.{Qx)7W.e=ssb.\mRF NN`۟m3xm$jB;.4[4炒p\D+WO<;5lXVͼ-:GșBBhCK8‰jF-}3P4}">yAqg7{_SѩbHe˱eR%|rݽuЊG 4h%)7bF$1k2U.^mkzty+XJr=Zy 9S)(P_FҊ>0 + +:bW8s\ ~c#/8P3]9JT-4;pg^{;'h2z*,8B|(褋%7]z)('cv1T`Aq34z zۂ7+ҟ,91G9&Bs~eT5 ZHs/Z0 bڰ{ Q}F.W^bE|B=4Ÿ|YʲCnރ g 'g=A`챘1 +||O.' 9:&v]ӝ·Q󂙅 +g _֛rz+5#t~M脢'B׹,VC׮BwwBZ9^?luZseE|3XP:[>fTwq ӳ{U|i|;/ooJEP,ҷ;x~z_RLjE@j@p=CfA$1b}:;[+ˍsJt(%ydhXj5fؿxZzu1_h `^Pw9z[k]-:CBs/JIn1t9ӉSw`w=w{Mw7Z塾foxs<!( +qY1JA!6Xwj\HAhBYu?ګx81`gʷ'X--b9 Q;0NHS`D9#HSl`@$cWX!qtq f$TrSE93R:0;z)]~-fvyiL wyÆ{W>˧knhbgMI`g R?ם屟\*$Vm 6c .jV"$U9A\xyYھag{5hX%iY|`^K梁-E#Pk#XH\H.$|&hz;kH-U< +:IK.;1+u'rde ,g,CLZjytf|5^w%Ьy4Fژ)0e;JJ3꧊CO2j<3IN CR#lggM;J߰Ӈ3:)WNUY<#\[vL"bZ8Ʃ73_oG]{g-HsL +jطl5xH\{n~m4u_nw̓5Ӄj\zrZ$?Tkk*f'a=\0X쉅즩Z kŅB]cy2{5w#vNR!&zz5,#>Sl !6J`09(e8|4Xħj4MRlwt:0~H>yr}7)(gehޠW +-n)Q`|3 rRl[5<FU3bfL⏌,b&7G;KyBUk +'/L,#}-_ Uck]h+,q1f";dabICo_:^- mb!h|>ŧ* 3`K# Ycƣ؉|krk V"b'k<@:E%vD?CjĖk?X\R*/h{h51V>X6t6E)>;b {)@ʝU +yY. "j;׬V-*I `T]_F}T&>Bb$#m#=Yuo1q!,7aO:덧(+/-;r-;Klb#2βpZT̥&6!bgO;+2+;+ ;+abTYގ충9 X`b>&@gu̞jfR!g _qe}l]K%"ێd!Vқ&G?\%&9` /j5"~]g5y`0˩N#"~Ei.?'JW6UA-$2\7Q-/,n'%ZFN%[q}/\d쬤Y6FbgY\^!~ZujOAl<[S=vt鱷|x,KCZaZrŖO7˝v n[ 

jNg/JhܣZzy1j114͖kS6#X-kKYlwWNdˆe'Yq!y߰_sJ%]sʳKܚs[ӋwWJ/,RbXr:ţV&nLQ3~CrÑ լ +0-pĎF{` 28Au"sx.ri,_]}ᚊEbXN 4#vR MJ'=[{LO6D;5L>Y>H cPp|)3DYxy=ȥ (FF]{{b|;K>oe1ߐ,Zeﻸk\=XsԆk'-\FX}_R=n+'RCJD!(J:= cy6z_lZ?\K|'VcQQvyVG'yj41RKFGuU K5$9I_E,qxVy{z]l[0h0B<5(#i!y +]쎩`nȅ31cB}D+QrLaĀaT[ bGڋI#S.=g&߈"q` Ả%w| +4RTՐ\> Xf5} /*rٵ<>t/.FOCj(v?!DZ굲WĨɮD/pސE䏠~¿Ć;nm"^[|\hw}PbR8ҔYcxU1l# k61Y= @ 8CڋK똍mPB= ; W=bkTd|mG<;wFÞZ~m)L)}Ih>2<z` ^3|>Kص/陃nRpp\nTR錡˩a 6Cz4:Q# C~%%X=9L̈R3 |+_"&Qō׀99Z(18 +-\OebE#F!6!e+Ezjzۘ%kƞ1Ä]_[!oƻܒ\C%{7Ǖ2 kK{9$F<k$֍Ź7s_+JמbMAWoZ?ߪ #9,磜/PrOK0yo[%;YOׂXj5㐳ag/{¹{]o>-P" QV8^88Ea+DL;6L' A\YNh.ǜT=3gR~zz؃DΨ9hHxmkџQק_\ %׏ř1"VIǒ~I +XQv_A`m v|lGd 2j/A>Bj0 f;/'Og.sC9|Nj|ۣMyJ9%UB~=ҁzb}%:q|]~EzJIb֘o2яM9r4#?I.'[ -PK[L81ř2?e*6Nƫ+Ď϶J>a)qX,+sZ>^O5'cI|͙\ǧr?XYT1<|L1"F)C|8!)k&V6G>C.Mk_RSJb9)c5yFK{k5:N< ]\wIoJ)6d`LkuLY92VEܢGZ:XMYAWLX5.\3#O#X賳5$k'ȋ`Z%49Z>'~(OT$o,mnGNxeاK+sBv6]ˋ\bvDžDE&;oė9]KYbM]\ٿ3ط8/f?|˴~Mՙ}36$8$}QhM~ɺ V,]bWZzuKW-__/%//9d/G{opY;ǘŞd ~p~ٮ$wg^q^Lo?^:W-_vyڕoO`?IZ:̡W[ykً\4]lAsOg>wYgVhCf,k92Aڀ+/5|f}=ƃ30Ƙ{].ޚ}{4@22"rm)VZσ>,ôI>ݼ w4ZlQ߿ߗ~[ +1ヒ!ebH "cv4cԕdzs0L-Gc8zqLC~t`?c`<'i˪129"Ö$(g%4FDIxI4 ~#HAX!I"Dgؑ4B`#żьS!Gi! +#c!$ n!V8 oǒ $tKBBU5DㇱbWWKO.0dwO(b&[AZ{l߃P\#I(}70Cl$9qUcZ'!8{!Rt8>dMh&>> L8>O!fKG2CF2̞Af^Z?3w/U%Fps<_`^Qaggה?0wگx)d]B[.V0 +ݞ/荡ti0>-cv@9Vdk9rW=Z(́h6Ys i^_3Hu@H`)-$k545B/Y Վqs={5+I5NjFdCN3 Ng$9R'؏hÇ$Z-@"jCfDlGLHIӘIe:l'n1&dgH*sdI2udw +򃐁}B,H+ ˲c3G`Ҙql +|<%(Æ$NȕT$g +[CFqb`$$f E12o(=HԐxti'Fe19"И^r8sT6cQ4IE1y#tv/=qḡ"mi$ѐ>]/%&QFxAVRzd)"K4߳ݍ=֛!R+G[0v/,9Cxwф1FZdL`"[f^QU>ȵ0~#D~`-2{YH6:Cd)0AO(FLՐH Kh Ok&Ռ5Ė;vjX FQ)$:XcUJQ1Hf`T[Bc@ y +18 +n]Ըj -55z=$?Jo2!فaf_agwO[ 0S,oC^6&Yak #R4^sH/`P1rCՁWY?]{5n`l&8ǖ󍱄<ɠK2Ga)HPbG7BjT MHZ9 c-9'44<ӆ|Xb  Ҩ4ojڱcZ;S!)Q/,oRKNχ/2$8B +K -[!a4UM-.-'׎ ɐA0cgs1hcXCo$^ !~$𞰖0_\L#wWa csB>CR9YmS!S1#,fwW-͆4hh!c*'94ÅӓcGbT|*m;Duq?s0&sh4bA aq!4Ot0J_s/],d 9 |Pcd]4d2'1̱\ L"G@zJ:raRtq>ɾYI̗h([> ?>ZȠ$-cM .cFÓ(T#hLw8#X,d9avgHð&6C.IQ)' pIB%qɵH)`P:䏘mGQe6@XiSŎj^3wiWKLbl+ +9N$S3;.4ј"ls! *|bCc I#E鞊1SdHCr$i|9̾CVͷ'$FщY`8$r =9:df*I2_˳B3 A#l1L ʷvx >K5mdW2xaI6P*o.jFZo{mK75pՅ4U LPlyEtQ੅PR)UȵWa4]A CF`e<,~d _#KŐyJa>B5%ȃho$&hc&l}l +˅l;|Wl(\K>Ƌc,~f %foZht_JQgA#//,62UkWGbV>H QHy@SH#_LH'fB^ebR:KUWcXe/8s8^dm0 +AՂYRt|*d7h<׶~j-lAV2[dE 3-*j[0ײK$f BHxkɅ@FY~L##VbF`ĬI3:&Yst bL=Zc9rRYMfd;(urH(q5cX@|TYA~YvrjI9tt;$ņb"\㍐kn,;|@A3|jH!b54F>', ߢ 9,,Sl%uh-XARnK\N +Mz f\P@֜<7dX;X;(Ll⭰HJMXnQdVC[&|b֣t>qG+$!dkc``Q@G:ctKj $xmpsPsC}gg!&\F) RlH'efE[b,).Ʌ$[C2ZVd-ћ"xxK/J`z '$t)$}8$ V{< gi}eVO:mqӟun&Sc-% 7R+JG. +=R` 1#7άCǧ@FИrjT-O5d  S o5EYd 9NYn )q*ǒe1  `ggK5W$\0~'|"YPv.dd@מy|'5-p Ŕ8RQ x 2ڧ4q B>?j H5pc$ւzY\p}&[~Vִ~^ +2²m%SȐqDql- 4p(c8joVx|4޲JYC::9%|R-i1%|',)Q/2}Ȩ;X|(!#H|fd5.$Sqgc?_j`5Qyꌄ1oB;Ÿ⑈E|ېHA +{rzJe'cvtߐ +f05[AiɇBT9BPS:Uk<=C5j!$Q;咞Y|->J,~&?5PJ {_AxOK5wWBP+X > X"OIқ& ?B҆Cv\hb|+I1Yh7G- =.w<)w\Iŕ)KIK@:S~>+Kţukˀ]pכufWXS`u?Iȇ&Gdhm2G)Afu dѷCd?5$ A$Sn-GW_F0daqc*G}ͥL9(% s8lC Wfvߙ^7A-=g@rQH$.o$OZ?Yǝ/_ ca^j9FkEXOP~)GȩOIE_R2z}襰|!Bob*cLrC,e~j}Ce@1ɓ$QL5B>bj#_ GA6egL䦫-rON횄\ qB*Hnxz1o⢜:Qlxg96໤bB#b^a`cwW_:TjuHא2EBFΗXNbBB zv1ʿ !DysV4_H"{%ᴚ@B(ѩvX;_3!!'.rNjxG ߏ/w$W[X]yz$!w\fn] "'5+8=טlwm"!c;o[>D2xlxgD8gj:7pdgaF:rnIU^" 9Q#?ʅÿ=θIk!)XaoGݕﬠ׆CÙRo_$5fCOg%'B/Fa}`.m9|H0`!p@֮<Ѝ'( '@M5}Q,[lVo,%fB.\t=/ OCҋ{] g ߀+O{BEh'%7s&Ր<0 =,=7@*ug GA6#L.Cq\NkNN[c@D1evi*rbk%BP>tB΄z {TD[l!#7N籫4 Bo A2r,{ !pEB䛐,ҁٍ1@Xk/tNGHlRR?ow[A|N{?aV1,a8 +ȃ=:$MQ =<547CèrEC46kvl٧BV]P^ME {X=ȁIZ+[GQ $7PBׅd=^ԟŕ5@b +9p3|I#HPMw5ҡF7䡤ߴiEppJQnQ}f9Jݵ3!{ + $7Z=Q[JWWH_A~{(dKf+=o=p\BB_l $qGDL P.l +!!rMH_oւoS=a{lXI=GL=:^-<7Ov JީYrFda=Q kK8>[0EԍE]juԿ@x"CM Jn* d>!/+4|FEAƝԪ!{w{j\vոxJS| r/GK?cMDյ8[K{W?\FJ'Ylȶ8sHߺb "w$Dp!!.W25[O<Ξ\Ӑ4ـPצ `X3q梠o6|O Θ;)jQLHn,O_-E6}`tZq$ѻ 3-;uoٽABjg7e'?^vxp6xC􇒭|7HfeS.5Nb\sf\wkZvnX젳=^ ml씺܎޳PJ([˵}hX|ePuk1K NENG9!?0[jYj{5zlף\MnW:q7\|g8R*o.R \l tіޢ7S "z &^XOG ?Y i|HU~R,G@߄`5[j8rM6eyz$0bAtDTp(w"r QR[Pg!]΄_cu*I +K]'>z=`{ݔ?v=?ƹ`sk߇4zJBG+p`;'RQxj${QwhO䂵 TY*&w=jMK|rR_턽|m~‹jYW#3쌈#@7Vйb%~ +!cgpEReWrf`O/aذGv/qV >yjLHʴQX}jRsc$2կWH+Q_}c3&&A/ +}|jTǑG[n./}?SxT&±B`iov)t>&5|V9raR88OU`O{YJBӁ/G=obr|D-^lldXx~pЄoci$՜["1P84ߛ*մsŶϷ(w _`N߱vO]9LZc1[$yS ?ADӳpF@)q<孥rFdj3^P gguYƊ+KAo>D+Sa{CQଂLʁoPa,J#D 32,!rԌh zƩj٩8+ሜ S}߷Bd1;>؈ $ɷ&*>=84*WuSn RVu‘3scq&„#bO; Q;A؝7B*Ȅ,8pCtqvk]/| YJ氚a0.$ + }TԔӎc r] p&c{~bp*}c!| 1@wNb etΠ\jrFQyo<,2NeD>{nE/<7*,"tI.΁gXE} +[YccbBE?`&g|/壇^%$u'HEf P^B =O;cP=(\UN͝.~ҹ?ԛ/ =Q)*WjtOqcDtv]3 :'WZ.u=i<u*͟l#=n6K|.<Gr`pw6:r}11;SQJuE <KpK=(\V?Mr +y'sWgz<]uvooOO߂u_xtJW#}K87=Z6t<'v|&WbvoS^fR~s)jE߼ꉾ};C&`q3bh%KgSу^ mƂ.~,?lv|,XX=LPE {+;ω+k]^:=-=!)yd΀>:7*h;l\.P:̨.-9E$A 5}I^tߔ.ou$$ D9Zx.]Ev2E;P"ydQv(>ٯ%!LvCm1ÍpeI9ї-- ';u3A<&,i7뭒z"i(&|˚/H_tHwTY*r2TvJ|^|v +P1<~ZCktN!jvz)7nm +•]N_\f&zxTNX苐x:,uģ!~mI:J}& &+O϶xoW_h9Yg/骼,.V?B"wTK:]t^A=SZ(2֣Etrj#>:F }&cN!jN?2o~5 +>S0[=CR5UoOU!N5Β=#bAbbP  {4͊l8GGG[„=[>;=7x'}" +P٨.Ci(>*~Tkх=ɻjG/%ɍ:d[u >ؓB0ap ^Q43[wTٴۢYQEq6(ۄU:qև9N¤]ogOPhE(CK_ %:tT̕lϊ MpV>x-f2@G-hqiO^Z skbY#꒗fUG8O,.ou2( WoDeg$%Β:wE$EEW=6ě^ fh5vŵ35ҷW|?a+?Od谨9@W!^/[C. *0*~|%>|tG[%"m&?5NA.~3z۸&zbcA^ZXc-)h1ޯ4-3'u'س>za#zrRIq%V{8'a^uڈkj\E^Ҷ}%1G?u^O={?zUh>PQ lp"^Q坧z/1V/uJoJ:לИw5=XgnHQ9:'l8hG|&REymA1 ߆b"^ǝd]E2`gV=6aws}FC|)e) y%&K˞qv]εfgvǻr%U#k:6J]Ck +ػqh}]$õWqdˈ3c~#$~3{pV!,|#?n7RYddM9a÷#d_'qJW_uWrFpP1ߏu\K̬IHM +iz=#զ~KMsg+D3Īa֌䣝qC?Ev8?K>\GElC+o{w`ך.* _+~igZS# s~Ɣmm7aOG1IcR=OT>4V875y'4fJ:.M]g͇Ke-/G_6G߬퍽TRRs9|sBj`>I>1g[6HtAJV6kydu'ɪIѮڲ]Zen1a1תdwb"+[K.D׸DU48E;ETŸk**\T\ LoJ0~!i{"Ú8%xw ȇ"ѣ :#-%{A2FvP$ÍOՑȀ;H:\e6X+#;z`>` ǥm$ufe> !͗3϶\}', p p߭x玘zunF׭NcQsdQs:טB82k㲫ҫ}jCކs+UFhFD{}w*|ZJBͻbD[O sG+CZ6Q]W_,% !5L]y y0dPDkM8y+l¢x¸ mq)^ 9۶ ٷ-4ů981Kv-n18r,L|VWP!K=ڝ+BwH.oN'qu+Юオ}#"BS/7=SXU`rbI@|v_[S,ȚiG#cXvw2#BC;o]-Z/Ug}#qBǫZ1^1w\cJZ2[${[3OfObCj/'$W^+u?՜{9#h{L:.ܲL2!*i b!k˶m9'sS(H|k5,}ՙ} ;s;r̤9f>zPr^wsLiSEkƐm'"s2=m'mI"|Xo^bdobROM-ɯY^h3!6fHQ+ssVO}Ǿ|0`Fmx]+qYwSc3b/^IК) [nQSd%t˕4Ϻ )7}qjN>yt:ڲ8ަ0=-3)(ӫ!:xWN[Eǝo:ڦA]WnC{bbBOb@Sp +=|s72cxotQdle /aj|d0n*Cbڮ܈!G O0Z{--sʅ>9b1# 1'. (ǢWп=kuy)r8l,UY`>Ps>_;n.4w}Ga~>sofisl``Ftvfkf`1s{Ɗ~ެT/{2跕.QOKcҪ}+/ՆFH͎,bBtx2РkkTBCp8C>- W/z7 L3;0tAakдRo /^Z0L$0LT*+UMLA('i$.icty__g`uW p+b]hRج~|e/^+{\!{S}'cw7ŞљU>q5IL}._΄Ɉ;١FYn^-jb~jm~J^?oڶV,Z )` "{WRw9 ϊIo?)M׀[E`,* %> ſ /X|).?[ػE>/zy;;e^%+]/׆$S_{́/;^p笆v XxVl+L:S97ɳAz`2+7>Ncf׃;Y7F裬C!:'76'%Q~5aO"“m>zSP)K,=֕KBT\xtg 7Pܛœf6c OJi񋀊 +0Uq9 L̞֪yG(ie6Qo=`"ńXxӑaۮȠ} +}e #נ*en17agrV|%xdPTǭlsgsTvՙ,``M@yFOfM7K,jv*gVs{[ۡvuW#Qnvl +}|_.,:P}e+{#-#]Ω +o_~)hT銪pQS~%L@ez2~1i+=X~:`+.Δ? cIH|{Ť"Ībw)Şy%-} R8z\LXIn_~ gqipUplgmsVhx`2TiKU$` wLVlx",[]}\%Y\B /0+/wm|#Qfs,\~ٶ0Nۅ83(spL3g9V97y$``vx֊8qLUm0w&~<8Yb-[^ My[/oJ[ߔ>Tx$Ը% 7$46Tx#Y3tSmϘ&wYcyW9U7oo3Tm _&K7ف5`]=} ՚eNGᦼ@>X;S}[lWG2sݍY]u]ͮ+=`-XN߶͟mx,?(+ + +8f(,-SNe`m[9;}j@䀹[`;X kdO[,uZ%,;X.::]hL9̽`{=1===-g̾9S|CE +@uئg'È={± `ެ`R ,v <xZ^`3X},v1`=S՟0kǙPߢg/=Ooz]TjsɉŰNhre!E_]Tì3gӿ\ 0z)X6q1ˀRAڦ +ϐ۷̞ L\}#<f@,`oKx l-+qRFv'#_~'y VQ-CsFvzԴ +DN1x8Z\p{PXTnbJuAC0­p3 } +[nTj%:@uX9PXŽ{9j5VfmZ.G'}gkKCosOmpKlrK_S֒d wҸ)?cxXXCY}5ٻτ εsys`R.XKڂ`0l:@~Oո->f1W٪p]L&..)'NP݂10TwIcSTT:yڮm<"/9ͲP q5u=-6`XXK (m9%,YO%E`i +6`g +[#{&~fԴ>0QƆ;ߊkʉcךRbڛˉ'Rbc2.D 8G& 1Skưi@ٜ"r/g(-9|8U:S69K 25zx rh"gjO=fVOۜA}xUMsA50k52&ӼOIKIУӭ2Tϡ嚏QZ=5-v3# Y|m3#Dy``:CAX;U6Q` l *M(nr#jŽI;'p/өh^ɚ ~2YZ*FFװu1"62bP΂ڕ sgڨ.ƚ4$7hw{(lՁ.1C~}*̇gYjXy +lz"$ێ]W0e@%Zff-}!Mw(lX=vוxUN9ءuV˜:P_J| Z/bQlj^Dj<#?8iP0=nN=lNJmo;N8%8Dazi\+X3v+u?Xwl<` m`j~' vb!r>Bͬ/թc AæfsEoN?\ ;J/} o0ۍ+!5e$F}|o*N;hq3a폣Too]DjMx%KRUm0 Ser0oNb تkva~`OU]:]V1gDzaָ?ڜ83Gu3\ ;#1X3з bڄ1*f U'<gX׍=w϶f뾓b8'yYvK|>NnS-TVn<eZiwP*zWcmk1{5mY~gLb̫>,F4@y}l׫btm99%U>CFczL^cn` >Jt#ժND"r2_|"j:`!`KZ"pQ5 ~+̛m*< XSW;h4M~X +>łs>1ֆ-42c^3c)a\Lz*`x <킚߭/GƉ3c'/p1?qYx4J *`/c/wwLDJŒW!^s*m] }|F +Wr+r>l*-S:zXl2vRuz[_U9BU.s}U׌VrÍdprW84xSpAܓGg΍zO1{̨1Z7ł|X8) v>lEnM_Q/,JGܑߴ-Gm 苅 FЙ܉KVd)Ø~졂4_N=T˨~d&FÌ)bo>Q_#/M߭ v8gLZK=Oud:%[ >?xskǍo?bԹ'g+/Jޣzn7d">映"0=`h"IB}Dr>7MQ1OxD$eӱKlgc.]*i +0V9/ZA_W._ 1?jÏhr^&&KFO[}%ԃ1v!m!߻ɕyOK.?4&mruCaBGl?}8(k%X4p6,h{K!l$} ?XΚ,\ڃǬ`X_Akt# eмY+=-0 +382;c%_q +yc{ѱf.ĢMG= lT!#c K1%Kh}:d˭DffEƮvzAp^ٓSEM؋Wx7kt''uh׃YbORR s"JN#AU٠9[g񽥢{6v,%&! x1ss +^\2g *sl|Dd=-} x\^(5'| nݎ'<&Ly6E4{]%w7 iry$090Z/c I)h tv5m@ؽB,;'wSl+z}7Xb,j"`y*QveNKD{0[M"a?tG-I~7!U'c rt4m+ 4y/ ~0_N0~![|4u3NnYO_z'(o=VcOV$`umĖ +V6g=T,NyNCҏͰ|,.?b,8qO&L w#~|B*vu<3Sٲ1bjvެf=`txwC{n}uf[f012͌Q)cy1%chtI1K{ +`E@s.`q ysbh"/·89}^Y@VJ܃Uɓ>HŒ}Xz"b3i7G}eq^❥er+ImRZZB27碕!W>6EO:nOkNu,s6w}f#s]ǯe,9^9 M1l6ڣ4B{.óԅ<'RqHOMF{ȫM7>\ň!Ҧr}~9\+\H2[8hʕ<:,z*6YP׆ ´d֨6STx7~V^,ZauoF&`Ƶ0f,8=97c6F>Jjj\˓!vإ $@W]tEC@_\ +]d.7[I|=T^yAbAg-{,BaGul "82IxuIQY``\HQE7b>)sqeZ&׿$Un^SskR7IH_S]0$vn&/:FNr;]+Vw#8p[qh/bIU~ kio +!},+y=rϘG&C<Ca-m]Ig>7-AG5AMAt:  By]OmEJ.>Lzy/.͉Zo@Uc SiL +]Aӕ)y"?*WοM@ #C4aN7T13W:=Fgᙕ¬=Āޮ ٤_" 폧._Y<^hK>z-͢GtmC<_O\~:A<7?bxh*-r86xg7t隊vBPxVrB8e)jt + M ־C@k~`le9\&9✺4Md}\ mE/SwKr{ +}m3LIXtH7.i0-auRǠtY :$OڨMO9B&h|6IK~7\g +OLDbx"C[>X  +0a\ xR^2]qψ :ho\ڪk8)W|| +~>l:~A6C | ~auaOJ<ͨ؃a鰈WM1e.3;oṟ%OVW/%1n庂`rn"8o{a1&1cs weذS{ω|sy+蓎Y([IEqY6ʯLҗ"nB{$nk䯡N*4 Y!8?M=}xPQ j +>SwpՎHG84.QO7b)M}A=vYM\A4!u +{ɷ>Ľoq\tԹ8^p칈xwDOGۍh +7bHŽ{NM"2a<Y짏 +\U#25=\<_mh0m0~:jYt7|X*2z~?>tTIDUœ &fb!wVbVeY{y7 u9`۪`Ì`M`СC@߈c +pz<< tVFA_mZD?`~\Tf!<]11{DprzGrFl YFy&EL,FPIBv[\,6E Ó>#W RJM]V[C_Ve2+gģOx1)cl6-\ jFo +҈Dlx%i:Hw q/.X=L9h?AWV}0,$eoaZ>!Q|JIevO{\6y +b_lƣn$  +8DA?E twey"v,p mz3g%CG8=}Xo܉ 1a^82?8wؑ߭e”=G{JL%jeIM`DP_h ڛo-_5iӄ>9yÚ(߁)X`çКWl%rZ+3wYsM̜5C]GR^h kK{IӊHeX9*N13'q]ѳćƙv/%KI 1pU<0w)rhP's; >W*jijZ5D7ĝ6 wP&xh;B +r[K9HϘXݵ[bah*p9(cCjxlvaGKT448`@@:HK 0,$;ET.$ 69ݺXVoe2KX(=Lj . aq0 1Ckm%4BXtH8S^ga}AB_AfQ3Y§Ĺ I)8H醰:S ǃI_j:'tCxHi3!71\FVn(@5bF!|c5y1}1b=[{iNGQ]Af>4gÌB3='Q> Pނ]?DZtwM`-j2D^[l149| .<8FxcmuU%> LhY:(Ѿ) Gq^' lt )`LXC銛VD,g_[ƲT㥼t` +::tgr )rGW|ዳw˭}GǚF%]̻"^9*߮'Ѿ| =(50| k0*QqZ<nktb)IFHXrRgqVѐk % - UӤC,;DĊB%O:E:Eq[rZjqx3!zQ0'~~K,C {"6*&bk978j E\عigL$s Ro9q𰣒1B + N2 XG `q4P>S *ˈڅtP +` Ⱥnˌr8!j>X-Xjʻ8৽' l0?ucJaJn1~Wd'oBBHXˑ6cQ !SyvʎyfbvTld.@1( pSJH)hϨ9H sÊ۹Gd<<V.csӉ.c1SD!V{*xu97ҨGdTS̻{f'_oZ<s3'ӛ5R z7Q,[%'>=T#+af}Q1Щh +wIj#~#gnf V{}Xj`. sH-!&7O#~)bgay6 +@mcvHn6Ғzo=.K^_1wOL0:+ظ\gcG*΢}He> ӫK8Ehq\9HY* +[tɜA"oАm,)r6`ycJSO6-]tИ4&"}e5O!R|=F*CҕdI2H Q:\Xg@|c+{s=XKƖ> $Y\ZJ!'¹Q䓴P9WU(ӤS+mbs2WX41dΰ!7h-:)AظKW#ĥC2f2;>H/RgмGkN-Df8gSOj=H(Xݫֈ{ <'m, pg~|1o˷J ZM񈢍#a&ZրHwKCDLH"hw|򰼑#m/n_]I .&rMC63Ȩ., WĻ>=iQjga0/?lbj By.>eIQ*u_x(ֲ0".>0~KҠc\g JTgOLΕnD_g6Doq06Wj6g=&#m +Vf/Dgd{igX;sO m!y1j6"E蜨)kױH_^pAaJoAၹ+a9먘EiuڈKeu"#ڪc˝ǢgH +--_y5q[kuCwm̮+'^@k|suLüuIV9 +圬^1Eby؊X6Sc.WΎA96EڍY,طig#,{M{GX {jg'al|HpJBSBeR +m(eV1vMlT"gBLo{rF:[0NiH5rrj:h7XAyZ=,L'&Ҷ?YܬPR^34w؝ YGc{.j|HyNBQ⓮jgE/ s 刴Ez&cN 酡8$;?OH;e:NꖾPb'3{4Pt t,%^/qw"kߑ/wx8~~-sbV#&OB[=qrJӴJ:[7ew;߹͚;/}44n)E^ًϏAk£R”QGYB etTn.sQC~Յ1Lh'tgcO64/]KdBDh}Q-tιB`jg (%hD.G +8|_lQ|X;v/u/>7q|4f=b抽 It=w i|sA(o\bٓ]DֲmIu:VXܔC2&7R4PNE=&FXm,%hZ@<jN:$ +p'7,YTy-=\N󠝥e(jƃjSVS,%֣g,?va--%ہy\8&rO4c@={+G-bt>L%=<Ǖ%r/ET4E1[  e%?@Pne +@FϾ +k-E\Arrۀ>xPm|F t ' +hsn1e 6簇1R|4hR\IC|.e4V¾-T; ,E;˳jg#]\$b!CokJRY-wQ}ke|SKCW@3G {8_!ԯgȻ94)uKSK*k2ԗ[8R9'8>f0Or},jg5$TtͰp  cn-wvKJ1RґRhXCC3ŌˋqgARt b㨛(C z9./1Qzx-b=fHAcEV^߂=wq)WdktZ!iGKFP gPu +-&aErY{EqN~,8[M6MN4pǘol|{+]!U |2rlTĈ cb{@_|#zb\ejU{~HBS}Kc$kTg5Oa=%U_R⩹"UfpҰ*CS/,pDw-`z, :N}zu{"U]pgRۄ?ESG`|MXʛ OҙA?N@GLvFƚ>Ƅu u)6|Enhg)gť>z"T6\,9_NzVrrMzޣ%d[1[IW卧X}h9 i亣K"˾ZUR|﩮-\vE U~qŵ҆佒Oτt>$şڹh ɇg!z.mP}'c\#,!OBI&֌Bo5=ttP%'=#k-Lo.Cf$_f 4P7 QSqB'Y9=&{E5<8j[zxmxL:>˹51|"49-|ŰqЬ}.S DNI5 +$n xEqz>:~tSNBMC:,)׫5^2sը j|A 8%| 'vyu!B igˊ!yB+MI>2'jgEm#s* +XMy߲oa4ubT>l.Rbc̫КUx E|mН"sK"|YJHBq䅡v9a]J8;,yla}ݙ n6Gک*ؽukH> _$'cIr}uQGМ9r'CBLM5CAw#q2%a|؜;Q d Rjyc'A +&Q99M9@rYԢ8{gmhm~rI'g9+{AGkbѩTB`tPF/vu֬!j3СVa փ-@  ,.Bž򼻋f1Ƥ3灥Dd} /ңkBĿ+-gu&+ ++EaT&2﹤8I޲xœ'P޿{ϸMTn]XS__ĕ^Op3˥XD}.0+ޗ2{ ' q+EVçҵik[-佈xU{2P NgSܲG/{44UpMXc{l&Vd.2G1tP (z X$F#ܔQW>#F#'E{gĞ% K_ Nвw qy14~ 3y6]G$ 2 y]romrr Vs@K%qF=J S~j})T=k~חb m;jȷXQ'ϿOk=#)RO_.ϻDsszQ9g~\1T{HB' +$GAP6|bGf&I5ko/Л|p;{ aGu>3|M 3 9.;p[yb~Զ1MV-;K@O lI{'aŢW.rgoH?9WF8WSxL]h:S=aW ̧tnQN>7꼢fBp(8zA8sX{F'EA-d3/8=uR=1BuɽҵR&V<5BE6F{` +^G Ov6)f&c+tA_#{  .ƥ㳩&8f>d"ӸI 3gs^#aKOTcM:;|aaWu*OeF;}80GhFM_z|=_z|=_z|=_z|=_z|=?wl[gZۤzbK]::sW{Z`ur<mo^\oko[y,7m,NכE^:o΂Ezsm򤅍mK K-~yK}?o9,Z^p΢K9xz1}{7qYgCLg޴z#cֻyr-4.zi%u&n$_ͣ8oƑH((,&M ĀLM3%4"2Fj$ck!h(l=%?s- gAX:D;Dm) YMQ  ùClܵ fF޽I])C92Uf]Z6鰕Hp [݋UbzK.AEϨ!i _.6sRGHc!z iPIIJ7;p[߱"7sIkĴ Icë%(JX%![ȟIrQzx Pzŏ%:dNm+FaЦNA9hUdC+lhЏgE34BpncɈؾ~G/]dQCA;^ 60[S8b#Ж)4(W|QKa M݆&2ƌۦf |ؘKM̘[h [as>׈ Ra<]I@XyFF.9kh/rJJb/% >Br#|0|#HT9G~hK&!%8$Irǂp¼__J'6/"+&p2n_\P9chs#vCJ@pw.%"NmGAl CYchs9)JŠИL4|ż&1o@,#RoqAG2Cri&lͷqGZ\i  ,>62l<hic(5bd2=dmė{Ak8|?m9p*e l'qڀ݉($#w 2^9F1%kN;_BP/H|AկCŹtn7;`!62.\Jxfmtcȧ_Ļ$ 21v}u㳅sN]Dw A%q d\=I"QFo2|g] N/EB8ئZ tMdhs-o3h8qO0 ;/4BHRD$-n|*>6cIr-HY~Ͱۣ _X!O3j$3NÜr|% +7]4I%zM(ß 1gft6A&=S чs3A䎆`v8\P7x(@ M y@FϹG@\ LJ + !qWpj$V >J$ߠ;#(IEqj;1r(R/=R=j[[>Ch,`~Hd@G,//s[+麴I$$opO)a HMf/9,D~GEULIG"~\ qh6Om3JA>m$gtBȑx` H\VnNs=\O'3 +0;$։[ +!w&(3Me$WQXi\sc-;k$qqh L|b2ĸ30An{īNvnd{x3n7|AhNB^M;(<{"_wh4!}Yil's֣/i@UXħ\{& oF {@MBrq HT!S|b0 nǜɵ F j+l #T]dApqze,Pl'>hj2zf@I#Sk:()%q |M rw@|{b{k.Nkn `+Qrۇ`a h<%&$0GuWC鏹ۤ> >y!qYyk#`n٭ ex;M)?6O| + 3 %sH}$6#9%c&hPL<|خ_ob"_\ v>1eV;{9F|,$E:DyQ_k)K3gFܱ#lQdIQ\#u?LJlW9U]l*\w{>5 +NQk!#w%U6R>"?w4!le֟(Y,rE=! .v_tPܴV([ "2FWB!/D D\K^=C?=O8˺XƠ02Į0)9㞾"rN?ԍMb Hu{[퍜dU W=3(/& Bz$!ul4y~Gq[`_&+C;#i$Ԩzh)hQ @v֣,S∢o}2Ph,|O 34Q8p%<'~s`њ>jHzd}Se@"-Z +$_jJOS}G)J$͡b@hxd*)yuנ"+Mkll"(A rf7uM=CQ_D9?1B4v9|Kx!N gf%/VqY sa". +~cs+!LG۝ELFrx޷ք1{PBIF޳Hax_.C[S-*E՛ S삷 Z br:ag0-wF!?ڔD쐒s03;RQ +pB )qRA]=r,% /Ty:͛k 5|5 g OςOT[ʥ^%æÇ(HmC )#ʿ>w +ȹ9\܉[ 4Ԯ$=|@q$ Bnl ++I)Lrׂ5bӵAƀH&q +.&%17o 'ٔƼsş:0ST4Éy [!<1XO +-@I <@p)B.,>Bm$W #Dآ?|}12E|9oB 8s'_| }F endstream endobj 28 0 obj <>stream +vbw3$_[ZwLe<+`%nB]T '69Wr2*v@6ݴV+9@:0GJ".3PO" $-.fiLui" yC;lNIs5Ki@Bqs"PQTy]LyJk*[*v!}ߚIl"gXk/bQ:'~ v!ܡB*+z.ĂBqZ\C9; @r/U(It\)9^)w @ k ,UX-|l؆/f]׭@ 6q Wu"k?{lM,4 ֳyjB[`A.KV.ifvۤy5>Q@<ĺ k.=.etJ:@yw_꟢+_bJרA~ؚ|кG +~ +B-$֣Lݽɷ*3?.J׊7~viQA/i$=.0R?]]G*ݨIdۗM !%;RA2P7djwqG +/}-Zͦ_OQ^<=Ct+Q?1 +9=<ȩ!2/+ kE.خibjo1eHޯ<ȇ)/BuFYܐY&ⰨZ8@,WĨa  LIsz!j!H + 37Vty̩؅f[ja %\]Co輸״>ݫ/ƛkЦ_ܴqDg ?@a!-2/󮯠~Ne\\e_[ H)|}~Ajh^uTJ_lm/),oRVXH:M؊ '|ܡ)T{]- -cUO@D +~G|*v#v!؅۟bY+-I.քIgjs- 5CGh5J<:+\{m(ui1$!p,[յBr47%#*1I#!N +dW3b\Xq.`1o%jR>%͍Mk7271&#]dK.QòP@Xɽ7'./(ݳ7Ori\TZ˸X"}F?!mG~of*vzzlF /b*ő9 v Em͂' ^(F{>˕ չ'u$%ǞUW@suqs*T;D1S k^j7b|!E}m~JeuPRϾ~jri.~<7œWb3E}XQl#[O uD2)%)Q2!,!")LH{{XVEkڵlU_Ԟٌ[ v#B;yaSKJ89b\)tcxBᕓ|a* "5AWJ,kȦݞۡkB Z*rtI$iRX_<׸՘ l9w(#DDׁw& 8h8YLH@l=bh$Mȉ1M{&Aػ;Ufh1B~ǥ#/oI^p){X==BKg*DuuE +HQ +B}&tEiXPG fA\whӇ}ػsT_:yj*_a0ಘj}3̇@ &y_:*rJ~9{ օOPX{b͏qc:{{[KabDuYh#Oxܑ`*=!fpP>f0t֢8lCJtOKz/䭨J3(K(^+g@$؇|fpz6kXcbBE?r" a͔^ +z(Z3l4<*z:ֹ"&Q5 S'@~a /o[ qJaw05pΙ!h+ NACr. j +O +QWL )~2,ɧf#g0UkQ "Maj=G˄ki] CMqniX+7"=f 1~Y/`5 an/b_o5-v% U8н)b]96M/KEg|3GVD>] +H}#t+}&M?~w +;Fݣ{QPGY:쩷qڒj>!>tQI4/ ɉ33S7b_R|%dOLP&oa/|ՅKbN) 46W&uGԌ#K'PD♢Olt5jDܰ/z~u!byPaOXGb`lهSL  !Nce&YEJ.;CYq +I#4.*;כE-+ݧEQ$f-:*ɏ-F #ŝɧKk>b([VN"U9Ц4/(^.}V.B+kW4:8-{Fh3;7 9*KнA̒өBy|iZ +:qkyܺ\̻ +/{P{RQx3bʕؗ(~F4$؞z~cȕ[ϕ<_u< D\yJbem?ski]O9?SQٹ+n_KΤ#3h-=>~ _ClŹl%kmkʶMbuPٲq a'kEY*a1C;BnQ'Y7p^[^tHS8y*7Yd6wV+KNp"c~e$?a'#űO&*[^+73/G54[{9j~5X~g^v߬W~gRm.ߡ<ޠqUo6 h\!HqR8Zɗ_`_|V-3^Z;ުunז*H~Pofv1_ڀ??b]|쉩f H͸E!J_ +7_;J5;k-XG.[>W.|W^VlpLv["2P3:2U{,>`ky +&[޹-TtmduK۬īOW[\dE~{J]ʑVŞ{ ;m[m;ݹoݸ?;(nVq7Kٵ_ܣWn統qb# k[q0],SδY'[xkPDuI8G_cYTp鹵㟌#ӝsT_mG];\.ŕwؤVT|!{'N 쭶m\s- cKͼNn˗Vqf6P˰I}9W?m~ +;u{_ݣ,xK97G3]_ZTe,ߖ%7:'UwX N͇~ҕ&g6Q4XeۍX7LD7^ߴTvSyo6۟/?^ [_pEHjSU]˳3En:][SyV7IζIr?ݸkmeUXfuQ|p={ꣂJz[u$KvN@[vO'n'nVZ{Vj3z孇w,*^-H8o״ؿ7m^|z(U1U|+k^&^Yvk/Q}u:\}{⶗ײ>g((+zqa +MVv)/t9+e<9˿qgv]w^(|ۘ!tI/c|٘tn87S٥ +3gܫ!m//dkNeIㅦUJ݆mst+ -Ol- oJ)^ZdTtgo>فlw3,}鱿b"~FBٙ|꽅\kCsOv(C[V>)K?)r/QQŲ6LY[$\xc-Yf\yʝs?ʕOj÷Thr6I|e6swkQQ䓤'IE%yQOʔ51绬W4dT63e͊N|X՜L͛{Wkg7{K-v?n&~j2ƥ@G.GJ]ٚRwyÿ965ڽb}&{\a;RE)wTJ_ar#m*l+ת΋zМYY>$ =)}o?lNmxZK~kI?Lm`femhVrm\[Ky⇦Wnq\q-cYpgߊ⹮O\/YoP۵[-Yxſ||$M\~x&z0mx`qK/&gUۃTwaM)%ͱeګm{y9[^˹_Ϸu$ǧ#37qD'>[Ӯz!_o>I'Ri2B> +. ˎjHU}bR-?n{p(yUytaMU˽$ΚLnZyŻRuL:Vyutk(D&'?VY?n>sEQR_6pފB[kgkI#Vo_x3]Mksק}۲ +> /r*Qw= u:^ޖTќ$D}1  8]{䯾Zw&( u^mDc,SGz-z"=|->hcnk]'dץ4D&%/߄0ovNS򣸂{Q9{Ed<"U\V[HMcn#F.R$=N>H9|KPhWJ2?[y?^wzYK`㹊?^v0{.w9̼Ć܂Y"rܛKLG7x2avҽe7ݒnn}7gnw{5F7F%Ȩ[ڒ6TFݿoj}79v( +|UUST(,Ϸ5L!Qhn vOBo[Rw78ӻ9yu6oؽ:,i|YHSF\ٮ@u+uv<ݞ4Px7?'6 O~\%-oaʁqkW}GCrL}BnyM={aLLC|_g[֮ N Q_{yNhoCޏȱ}Ib{ yoW'c۞Oq } +mn{#Zry]hSnJi*$ސxh_v^^e2xv7uV40,YYd53oя%yKL@voPTZLYneߎPJ̉&*Ox&U}.*H+,AXr",Z!׈~wڱN -F>/6VYk(231{2]f3<3~FYFd6KUaZW̔㘱1.3Dft/=f\)9qW2Mݘ5#7][OmݟݬF޾q&0V_ua99Qy1 Ea wr1 SߏxE/Pܦ6=)d.̠dӁ?l +<4bQ ]äz~-68۽ Udѓ=F2:f ӏOOHfp=fR3'tU^.8~C[F_wcJU1\l{%Ltxq$71)nTV;j*oGf Rx&,c;3n?({U޾;[/}x3ѹ4Nt/^qckSY157G5WK3e0L/O\h~yf UOZ߷:әy DEc3wWyԷ9rGgݎ{;2k߭7ò +Qyzhq27ՄO κ[ېX''s螎$?ۓU񏖎̴1ӈ#sCJ^fyL58zhҫ+qt>CALoA䧑衳91u?;([]{SYogT ϪE|L{yo.E>Jɇmy~:&2yUGG"֔)&vY}c0zGkKϿ79C|Cia;|߮5?]~#\3*gwRu6$t\t8Ք$;ueMb dcO_Ⱦv;|[!Y-5A9]2O5 tuﺝ-~{rc;f(Q_n}fDcf/_uoR|~8Z|9od_{^p9WoܹsfhV8uh}ZnJm\Nr]Lve=ɻՇk3k#p{#VjΝ9`?z4עvz,7}:z%ÌfjOfƎZLYb]{痁֯%\Q|ح, y%^YqoSVj<AHfM=X;Ihͻj/^i}Əz{?_z{;ӟܡ`:{{3df$fhYhM̌!7W˿DpĄSm凫mjc._YJ| 0>[oۼ2ӻ=3n /`dFk Jg3zB˙1ÿgF\[̌Йnj36`3 $]L^YjY@4n֣I.ҀN\*$ٝ"+:ͣЊ !{67!ZS׽d`Fi1C{"? &OXK&0#{Mc 71p3z:imf%2_5Fx}#(Z|Ko=Ww'aPyw䝸uk|Hݫg]?&|D|/#|?(7d_Č]Č3V=3~)3f3f)3jzfwqS9faC`ΪsW2$gQr'65fٹ5j3s{E bf=u^PFߺ.sJќp9C ~/ =>ghd03Rs434fй?1w0ّ3vȌ`FO3'0#Ggf\fE/7붵i=VWTx)!5 +Iϻq/PaPM~y5= *&5{rb՘S6 zy{=!Ìό85lݡ[ƌ5|3bOMbMs]q̢m't55K?u? zjD[dd= *|X1y Kĥ}kV010-!y5̹ZsAd >gk[YBs5IcV%nwk}f:_LW3m.jHȕٷ5^{nFȈ{ϰu{R"vQ{/(vAXkU ґARl ņ]cXc&{[v.9g=?Hu͙k˘?i^wȹpin\/F_4tӢw[=>3,g=n7{D*{A Q䜆{'!۶ߌ3,&Hj:̰K9iv,3nI3s53ֽ4qaFx2Ťq=cxcX!p5?:;\*|x j 74]!uÛytRÅJ#CLc)͵#eJb#N Ϗ$9f@r~VNLQ7GI@{|ǩ(f8f"fL`#3ʷuإՒÆ $}uMF0Lc5ؠ3Y:f @f_fP7f(OfЌf2jYGMl|d0d-,[ϿE=^[uu}C'[vF>S{mA^wڿUM> +'yָ\Ha=qƌr! N|Rf`?IkÇ,b ]8 reV0ffjfL_qxV3Z.8mˮfy4(8UQ~aM_^.zhK%[7~ S|l :N+I_=/ a뚫ٌ_ϫ'Y^ҒfYC~d #x(fg3%;QŌq rT@̛ : +f5CÌsKa0ӄ&yG&꾱aR  ˣgkķm길ڦ'C~c͛9_ʮk^9=29́ |ؙNec87\aɥ ! 6?o~oZ<{/4L\~C/D?ܐe95OvRk7kxy混}^$߽mZaȌ 7=ُL೙Q̔S>3seytzy97-j=kf]Ͳӆ-=hpr}:}a}C b w5"gy%sm +A,cx"'~be.3/_ҘA$  d/Kfqcf7qg<ۆ9n-~on~fo.zm×6"_ !ޥK?]- {Wƫc+/K| +lI+HsP쏡W:1O`SUC{XL=*f%LU3SӘic<3~3a'3e媍楞5?ykaW5?^TKo~ Mrs RAP2&JK_]꣭//lUpmsJew6޺VVwF;7U=<#j 3l\f fW<•1|fa\k~jA4[borU?f~J_ + *_ : _^4x|2/+W}yʢէ A7oc_hHS_i T0bDXnvC^v.M'tO>6Pzh:w ^Sf)a6cZۦ ɳHg3f8/}ot)5+jH +! \M!@ξ5}kS4H M/*ˎ:(:sVGb, C*N8ߟw-|NRob&Wwn +z~p^5ռЇTg -n5NO>C\}wFq \?˒>&Ù;QÌLf\zѡ~۰'bs;,h|O6o_^1(6R$0UJ3Wo9+|\.=~<:GaòLc 76֖ 5̓}^|/.]}FF޷Kxp{qw!:S9U{F_B=H̘2eK +Y~A!!k]_J_ޕo3wL&0),+okEtd=i U]rસh f8凿y}%rb݇_b2>΄r/nwZ-8C^B6qUPVfE)̨!4Ft?ҌkBx6rPs) K|fг}Tuo7ޟYpu/ſkūn<-Nr9mQ-2j<_^ʖkUlnR_[ +B3]B 1͵_>Nb@2j(`0f2\鲳⃕o$Of^=ѱBx9XoR&pLUyc'3K&f1!pBaE}Ob|aw+G/ޗKNn>'7 +eb6 ?U[osq yBnuǣܾ[sT&*:mCx`~k9'}Rf k!({仞EJAO}b z]|^>G00؝Obqkm vBI?u< +DBh:݉a7BˋlWwv?Uu g& NV?\mS}e J-avcK̖.U0^J4Qoa3~:Շqx-^ޣE#'{z2n1>Z3}IhJmHIǘв ҕG?c_k?[eF]?2W'V[~w]~*}%^̭=0+>_>:l<{5ߘvn`O\At?tAWC?]CE[+w2?jz>[?}QO)GΝϸ,1')^6tUbM|pO#/KG +jakTe0@*h&2[5P;7!O%r~TRuO˄S q.cS +i\և.¹w*c=]jy"#GS +OZ +Ɓm}N5gjUflH_i̿R{uü X K2էWkk?-w;nw;y?|G;_Q(:X,^/Kkw:G-ɏw]\:PxDn[77"^:{i*mHE}ƞ\6D+#-vUunKѽ3H4np9`ৠbbbM.N򚾚83+_ nRVם=L_ 6}6S+_~x48cت0g:~_{cq;1`r)N#A~]%G:ZV?)5 zN#W^cG0_a5&Bqp+Q'<{'z[ ]ݸ=O/" 373k9$}n;/hW%w^*DU4wZu7OߖqVR2$[}tg o_'S!PQ/u43efHs8/2sdS.2\O3u],!sTK]o+$YB+ KmzՌɶM GJGZA\Ep>N`Bfu?9 끻̵gW!܍Ta/I%G 5'|B푉$OUvջ*;S{zy!h; PERWat{^b()@w?;`!`7G&kvޘ#z$| + ?yɟ~)+렎+P^`S6󻆂GR1w$[!G'з"t +-2k#$2P#kOLn.ڟ.|'Z΃ >N> "Tվ7B-=o?y!?UއM̷yz L8ꃱk3T-Pu:+0=~3~42f:p,f匏 ܛ6Iox{*o_% O4aA6X`|1"4q[/͕:,;%[ ? U{`btgZ +nޟ7ߝk(¯~Y8*H>"B}^ ؚS\:^/PW7Sƾl2,ӔKhZg> 3}8feR7 4"׮?AvS讁C^3A +zE Cn880B(4H(mu`۞,}4Ui$xbF+hJOgRf g FUǨw};}p;%'wn1'2TXmVYAA&9 +L=uH8{?+V v NLޟ #>|r-_d;R,;n4 }8|V;y;B>H?+Q` vE NDo"ׄK_.hNm˖t WUfb{Zb5 6˜rZnVw ;d.[,2YЛإ TZnF2i5kN1%eDdY3SU0P;8:B#E뢳,;G76_m_w4h~U}R 4#OT꽯T\Ɵ~-h)㺸s$أMbluEd>.)6}2oPyW}_-uv~6P:U|n>,# |Pe0W.3K\2lzkeo};U'ư[퐋z.fk0 FVG#{}CΦ`CW zxuעCa}DU~ ,:hqLw JH%H|i^<+&Q~BF;!Pb'$C!TT`,fۣzupC*1jՒ: Š]^h31$E8|V{N:x_hP`=.{ <}o}}b΀a&A&Z*iq@nX~ AAM jh(\BEJ` ;2Ü-ͯ7mWiͱpc$3\x\}8 VPHQGyILcDC`)`ɈۉTsM8k\}d2e;2|~&}.ObNK =yȇ(XKǟ+t/Xrz*+/z 즏a~1[xnJfy tq3zĺDceX"pF 2{*op唝[jVtKu|;yTzt`*qMg Gߪc?~Z u^&9eG3AH#zbA'YSUklj +,㉯Xg'740  1p'XaKġ12J9UeyA,nxH sad t(3*C>·K/@`xa6.3:|jk6$Ƅ mcJvybV{&8H$Q4{!MC>x!gA!+)g19JRfd3:e\T?Xa,NcI`F i5^j)r$ IUƐ})BRéyჩvv=6Ki> +>xKOِgcM叟W%O5<8BH(e+Mw Rj`65?nȟ?~4QEduI&>vuV\䐚dCRT35ṁ2jo>GwTk>Bمe-#ɧg*1xkqdMfJYCNkbNļ: -GƙI*} Pˌk*K/+T< z`i6J$Ò(tmrAJI4Nll\sp2=ڔ5} } ;r30Cnva`j{\2 g'O"#Ė[q+-4 \T9ka棐Hθ̟:p3Hgg kk*ΨA'>؏~,fRٶ (ܫP_4\ˋےaGj ] 'X[@AF=t4qZF-MщyRRfFJ-WZ/.7~8v-D&n:_l/v ͵]Ƿ: aڲ[t +X|ڽwUoUlX)9>Z{2t=|W~4w=q,cĘ +K1В#OE\iT" XqQØa- l"q^T1uX>O?s_xڟ ;\-DMVT1,Cp|cˁNN7_+}\|뼫麭~"4%#tEuC5'fחɭ7܉mМ6BŮQ$M<*[hbm{IݯnuR𠅊4?fXF k;.چ[mC/g-\P m-w]׈ԵJfШ֟whivTs\ m-qg-CRlzgvwj-ɟGP^ߑ ++^Gw!w= +Jztऋkov"RTYp6G}`|xJ!F1"n7x|dOzit}tY`|PjwKSzqt1sO)ŭ56d\B5]}_] t=X.7̧ڀ/B?B+X{Jw:xΘPo91};wܛOl6;n/@ ,u=``O-_."7t5#츹PP\Ih}D㡏rzwp95UfXsn<1SsG箠i7>R`IeU5]_+:^,*ǕNoBH)u}5֨uH uD)mS?HB3؝J6VS1U/U/5NJ[O&izoX6w BF|zj[hIhgq[I^vR{Aev(^x) LA8_]A]f 0P[E$ނO+ h niJ+l֝ThldZPltY=,nqcD}`q2|nA] )5e b[.V$ڋI^W9@aq|:kuxhJE5.+Gf#6kRK kз>V"P#qWj qY?A9uzCPJ~ڪ3Q=$1N1Cʹ}(s9{>zG\g;S]a`*j/qݱܮG37ϝKםBu;_wnt1Xh>zGb=e<_UI$ 1SoNTb+D%!.Rݬ~J;0ɳӦkHc\nEW/M=|➯}v@;kE:<ڲgŠqlu1V8Lٰ s2m)Sy12ЀȨ nw +6NWI(fm?Yܚ^עC_,?`3˕LNӶ,uS=lO}=DvTg(i(X]vwԇ\G;jPmBhSPmxʥ3lO,m˩9Yb[Кkjȩ|ZS>I."zA_^|o|4^g#~ 4ëĚ}ƳΚ;JϽxZ/%1r;?o=u'_P@!H P{$^oGkNL\pUd)X2usBI MDmZAs=^l]$W=TsMEЎ&Yhg58{a}PʀvQ*g :]0!m|YEyjgE;/7NK\mIurR uбorK_G|!WMLsz`VþDbtk ~'Mwɝןދ4hhjwd3ͱ&%!C7+ 3R!F,tJ;7!ϕsۇ"Aۏ1W !F*!vbJuXWZEtu=zmkM:/.$ t~,MԒֈ5 +kD@="%I[kɲD{GCS6os- .|uv[$'~`v͆S5Mm#]w<k`qKvyOIwܐb PSd+¾U9 +-TAH[ۇ O4ZN$fv֎;λXKS; V?Yέ^쿵Jz3E|ѧ#1w׮C+6l8rxv/E^a ^u|h0frM.Wɫ3]==+v,Ifq=)Jq+i0J)#ֳZb%J e(/0*FGrԗmtTqP-ʃcǞԬ܁} 7@암 +#@;8!G~ץM/eSjF9"\H4$ .媮1@];Y˭%Rwd6u8BBn<9SM ׶o!PJwo.es.G$ߺL_i.*$qHp홠%OLɃ|ۅjgQOvU,?EmTnMv/A?Y%=YJS;+c‰Ԓ9/㋶; $eT8 +CVp|t3PdKz8ǻhɵYQ}^modBr!&_Γj5Z'ħCM}4'5yd~/<'P1j}ScuDox_a@w O!RWmn?OabLCN/+IܖOKj9ja󉛼?#=.BO?,T;4MYgmje_5jm+6 \>&8[zv1-x.bz\5'f-8XׇŘB :RY+=wh ߻i6Ξj8ǭZխrU_]?pMŊG16B-Fƈ4?SdTaFbx..w`h'KLvdܫX>_JE,t&Ju{~HA.8+hAc$kTgkn4q6[5xzttٛS}fĝfǧkH + oh +P<3ߐPMh\&@  hzCy yq_EΏXj:MhX4Nn +:_z~_۲D'tO2:e;Fʫ*>DH̷})sH`w>wvD|H{GKT; Z!XZO쳲9_hghgɛ?#6]A[I69\}h uyW;FQ=cgH[/ΣZO_HhUuOn=jOLt>]wj.{!*L=8kwyEtm+\Dj,Zk;>EnDre$*Rh9|ɫmͲev|j D.z(HIuضcauc`]`T4RW4]6hnGDŽ3t;W_&XEè %MɸA5Vۈ4G8:^zmD4坣-m!\Wԣ/ljRe ʮ3r=ۤFCOul;z2GJjg9XזryߎZ BzxT Ry5x WA|r۠;EXCj;K5 ܵOM&ȇCO]]uLRϢo ]_.#3|Ձ%9rt^7)8Z37]Xn֯4HNM5Ww9BwK$gc &Q 1KKzQ-OY5Nз%IT}f9s ,֢83hmzjj?.|0kbLK62k*(^/4ZFBg k%;<&$mСpiӅܒΣ=t? +c&q4ڵyϗiRJk!i2`_xECfCtk!՝>1Cl|횽㩦94oIGs>@Su4gg󻞸sM]oqeV4&?8 ~&S7Օ:{BĿMpOO:y&TS*Ze< y2ŚDe6 DC^{&~ @ۿ\$n8sK\$x {= +b%g6DΊ>%^B h֫nth ^Xh=X NL +D2*8'VVȫ]s=zNӬ?0kQRbK5HF嶺i<73 +bi+M`O_`ɂ%yh'[aRnx4~ u0lz/4ȠWwd*[ NFw_NxZ/v^C=ԪnAo{~g!|f=%XT㱛v5tbpj}Wѽ'Xk +BrǝlMf{:$vv7nFb={6ɑG`͝=c,3Vts@NH̦A2?e췪;Vz0zləb%Ҧ.O/+F +v:r͟|bE0Ǥ.G~ \۵\ӕE,)gYhKᰇdJCW[i VZ$wzh2l\ָ)MRVQ_.$>u8]#57p/S1AjA.;$oJ Զ1Mm yZs@O li˻w +5[vFIu=GЂ`O={%>EnMg'T[:XͩOp&yŚ C1ǥzO:WЊ:;c rJyl@Əgb +ϕk{zb0yNYُ+%94;N$6Vv8hQ2ܧ]g: u:?l8>^w` B7/EsrmtE#h\>:jsMjdD1=7{i|؊N2N%_J_H{ AGn,tzHH#+mtx?x?x?x?x?ر CSCmDon>sUR#SlmOwKI]9 'fEVB>?mh$BR"DfM8M1HD +f&DǰbJ.4DJ4|L9B1oIy=U͆0#-yU&:F 9Hc$@ϽrX:lWAG(g~^T)=J*h*U؊u:{!w.V)К2;)&(:]L9Ow@>5chcA`VE)UF~J-ڒ] R%9k(x :G՚xN);(t*Ĕ[r1lX +K _ioA紸_Pxo,R0l 2jHe֬h u }y;!F{FN7tIBTve.ZV767R[7;Yr;k27k;b˨a?R\aiJ0 [k7];+HvL tP*II+4g'R2tA+4UmABW< c"'Q"dlR`NJ}SKl5QTy5r\b9\ >`*F&$^򺪃55Ǧ26 |K9:@"BjH;JaڢxyrӍE` +z(BrZGƂ:N%Ѡ(P%b"3Ȝ7RrzU|?Zň^+A( !ڈ9[YJo/EPRL RX[l"iJGblVo2+Vd?hYJq.e6>Je.T}v@)]p6.12%Oǒbqd*;tR^z:Ny5]Bd +U쟠pjnZJq9Nn"k%>LthG*+167:a3~Z*)9]=357ukS+iRl^1ϭ]{kc@D;:Fj [_ۉ~ɅrQptNt#qS7A+12.59n&muo]WAwd,a!> 2o.7!`W +_ŀĊy&_ϰ2y=I|x2ā^f6t og?BdSh->dyi._-tGюq~e֚ څ&D?^ 3֠c&ؒ I6PI #ٖ#hRJ72*̳@7r:`[r,O2p0yI+E䰌O_{&2'/hCЄkjߤ]s`|.c(XbT"R9hV'g @1@<ۄqZԕ[BG,: +7JX#04':R5Mݶ f cpB>C)9 TLF_:`kvYWw.a.IN*9vܖM0Wrs,ѥG}* 6pIdVP"0|tz|FPWcH\XY` j*eO"1FE1@`m7c@BPu#ƐSIU7ҘjӫS (S͠#>s(} TVd(mI#:wi# Lc %9tGE?SO1)qݱ bDϤݔB,p@)$jᠭ̈/'ׂW~_ŔRQ +:![ImYHm lz%w%6B8}բ~5c.{Km@bWµ\~/H|ՀiICtn7om >/nh: +WD;J9̓N,9K5 +t&~TttkWs:AC(OƓ2k_d>js՗_#0K 5smi%GA7lt IY\2?(1BrB:O)> +RRGI㕶GR1y?:Ca3"Dώ||ڵLrHUv@% ǧMUj6R_If? + ;2*̑RfK/eݣA?]\T24%m#ړSy,.1]PT[IJOA͋pmI݂-Y*a_X!O)H2RXВVxcXm!ف$Pyig뚣Xb 1r偱b͇A hĬ{pP(kwA^ r+R!qy"(Qy-Z28KL 09-Su-4R0HcIs;?~O*$ s +Y.oEIUw9 + 5#~>s eGaQLR3ǙfI㡨zC傓iGd +$J1 1I% 2˜'ZOiUu_F:k]ɒZ1ZB|7!u{J&uJcC aBL&$/f_u}(~Bh)dF$xe-*RL6!$Z!iJaz dz#Œ:] UԸdU 5": +6 vY$y`RR?l0qb$i&l,m#y5T (5Plۤ>-}$qӏ!Yf(3͠4+EKE)X 8 e K@VH]Dj: Í(QO\Al72H렼sP@㣲1ِUtuKNRG| u#Q_ +E9pjFRゾ  y՟o E cq +*B7aÇS>E|lQ:%kOEA &z/m2@ZA4ԇ-+@*U'A[}$Xjsj{uChӉor1U=X뙔P4JS{9|sw~˨cӵy @3c+kn"Ua5g)o`BLnoDžm_/4Uk [{r&[n̗?pۿtVHJqpԥZ%OJi~Lu'&Kg de.؞zU u?ǯ|COѹIC͕\>΁i)"]\ +"ȍK/ +&lФ!_85wg9(_?0N ]SsɸiD`R#LNŴMt +Kק@~>&s6^W1ǟ`Ҵu'IX 㝳FVn]R~(M<@$ԫr㑼7_[VEд&v~,u4u7A +7,L$='s9Fs!^i{˩D٫DfҖG9E d6Cs\ Sl@"La 92!p^ćm@MHz-O~ek\!eGb.GQ0lSy? H ԭvޣ nR`,FSX<8gWa"sMIrӫ`2O0} _1d?M[c^[n_ ^7)jZR̝Wri\8۫Ӌ|׀O$#>]'?GGqݒ>a_e /TErDi9s1S4%qL ftFc^c6LMJ*:(HI*c4@%0P +ܹqƱ+ +MM( +0>~J:`9U̟MVh5NY0U0(Lb5Hj3d ("30W)D|}+s'_gu#ȿN +hyqGSEy_7|!ryg%_(򺱂ly&N "c(BSQk,Diط;85XERrNqh)؇ل1 $l"k^VJ9u +C><|(àHe[<%0.:LӃ\s<Ԏ/$cw߬1ɐdpN q!5PCgS2!0W2?m@ B|fNi1'Ĥ!5qt(I!Җ߿=&+,BFa˹rg.#bv}A%Z&sUq ؆NvWJGMuːǛN 2N__^o +{OˡF\r-㔡LvoQWs2#$kPǮi- )tݾ7r 1ǾK};En?NF0(xZN Z? @ wh2?/5sQ&F2A&q~4N wc<!}w&L5ǡ17L}&f3R#`^.؆ʏ}=2~/ss +gMgȩo B\ jRdl,AMA=8L8T j6_jWFM|EvkcrA5}⓹ +TȨ]tPzP+߅njףUDD6Pt7U\p7P3%uLrS@܊(ALNcnMx&vO5 蕀<p~ \>|Xxr&9OP&ڎEK3OQ ǀ?jĘ-~S.E4e;IM{Am#m]N B2]@'[%-/rmr&a8 +rXb@ɳab*W+?9^MYM0chg ԁ )>ۥD. V0@z;s} N-gPH'URJAe9< { qW5!585l72+́یw9;n/D#J$tQ T\$2s?S?p~(XS>~q46PdR7"(ya? x#zOʡ:ڿSۣ@=r1{!o5,;_q̄|!(q ^ƀc&`ryaR%ngJN.>S[A  x0 t\AOsjz2'shn`'\ 5,N>ZJ|P=}ey"8ld8n/\'s?DR aO7PaO6 MQ]Mf Ȥ~PCYa_~-a8BG -"r; ykǩAP%;85̟BO+"(BSv^*+_ qa$evTS.Ԅq"Kw1E;;׭G8i$qNhϤv둉ꐗ\,Kwhya5%ym2a4x9䏀NCX^-=}mUA&R!2`;y>`2'_rTA[5 1Ǹz(N*Z&ra HP:;~6A(uHsiX$T*˽[N[=Lr.l՟jGK95;`)[DB ƚRRx(ȵ ).0B=n)KWr~&m9%(~BT#H<4ca f(IT8_U*(+3a*:= K&zejvAМD^QJ8Gfkj%P!؏Isݸ[y))(7Y'u wD׈ox]`8vp=^Q1EELBP g*8nĠl~gS *@_^F=_(+Am 0͡x\Ӗ0J5X] +ypȭH4{8;6qo#y)9eI=reo^V=-=^ĒҔ#QjSoly +LŇ)\=0Fl63.JD`nqVI(nI ɟ'J:ba;Q>i+ %q^A A7-c95SiL|:X&ub(ڑݼ*%ujBߏ.C_0=\MبMlՄ@Uj!s$U=p(uUL |;Ŏa:آ~NY/y)q2#z(s>\_5x7 jG[=?8.`WjSB ؠF +>(IκNrGLj/`}s8fہZ>ܔf]ڋ˫CDS"I >:Mz{K7=*9d_hMoBPO 6q| r;`(RImI72!*Y+95]+A>6+Wr]'Q%@Q<:8quyͩʻ`/5Џpw-r+l{R~W9N뙶y*UkhGhd_A03#ldxߛ+9'ԇ8$[G+r%}܌Nifo.n-h*t:i<r- Cل0jf"GE+BV~X}Z#sdƐdcx Cj\T?_>xx +`v3]e^ j\rSA}jC^k2W P0O.MNxP>8#\vQsA rRg|UKz#`f*=P>r= +\C!py芝3oN \}೾Z8F*(JdKКAIU{mK)1D攚rpAvJ05۠dīDXF.oQO<20p (K=ozz\L/J3?\׀rkK1g +.ܤ|W೸ w6 +xm\\O7uA!f.=G>1uh>~hA8l&uP1ҕE[wq/=S$8r~ W.;[!zrofTKuq `l{g59cB y~״E\^jr|aqsΏ:ԚFm u.Ŝ2[A7'F-x:Ԥ +ꍢ~S5c_E.N +l IOJUȫpkrį )`=\82 ҲHoр\(햺qMV>+1e;D6ryi|6_6s}nZ1nmxEX#v뗅y!8Th*[W[Ctw +iq땄"ԮἋ[!;Se "|ru 9h:J.s#zcu (G꩷8=rKӊ3&Nُ%[qP(aӁymp@y z7Sz})L@ơ{:b*:J^ Tc9>}+5􉚺/x)PX|=g5K@ K╳ )P0+0`%pqSO`/L`fP}]ʠj$.FqJ!(sFx 8<C%Uױܳ*NI@Г >/8rKq^I0uLB.n圹n&n~ҡn n~f0"_3%>r'}"~2m@%TN'őE22$cwC~Fr3eqջA3 }:Oou@sIͪ&6%"J4d=O2]| ׬ +v&񼳊˥rY+GR*z* +aԴJkg4Di HxDLNk2] zG=zf>D?޽֝Dw-yLaIVZt_wY5̽&^Z qv&K66 UNF44zt@ z 谢"^90o'@>x7߬jiV!*Pp^/qk1u\M5|!" +.|_dIun]AW,i>rpYdU)|CH&% +3Y +퀠Z\uF|*z2}'K]$51_"yYBj YJ _])ӡc߳ xkU6֑6-u\bIg15m(jǹR(xB#}G7a&E>'2y/ ysDy@|1$0Z$N> +O?SL¿/D$W^h)iVlHkc@, +GdG([$k1YlĵiT40'j_h%{Q~F|T+iK3ZIzJԬ'N6bҺ'A͊~9%/VD~_+;Α/$tN |+&|"* ]rAa>X2'lfkXKEÆ~{Kj> x49_q)Wo]C!_|{I]E?^#]M_3M< #zUvue-c"k" ]Jl%/kNH6&VIF8$bL":_UM1Q͂(W2X+Sws k͐nN +( +.qN9𛓤f咞_i{7Uq 3AK!a}DҀ4YAI1A~%o2k)6_i>).nYZTi--7ʿ*~|AmI^9U%$cIi~YIAYLMRv +.Gl&kB{Ⱥd}U뢾Fo悒?̈_,_R>^ 6xJ?UZv=|j*>^u[<"*z2VQ'~`i,,4T4ʜ~&a~ϻU8$iQv "!hO˼jTT\V;ɚ*w{f·Q^E=%-+u,\d6`on;1g.F] +;y5#t /qrwCH~&vL[@&|Pn 1z v ^,0NB 1k`і'7-><`pQU軹CV]bHC=SDQ +b %uNݍИ|Qi}'5n3)h"fuy^?37w[gFm"b޻yx j:$T`H~NdFSag¥nuem!7|F7 XsX2㻶0ktEYyӎD !U/Ѳ*/ˎQGڟFVZ\$t vV'LO~7>&w' šoG:'ؓOեWLTo6NaQoɖr +(15b-FHTI>>~scB0'GA“d8?-| ( ؕb7[Ӵ:4MKS!o|Ԗj=iZQ"-p=1OR/\˗_O<$G><9"ZQ+Pa:Xr16ֽ7έ769ң7QR|k;oNpzxpKxZ<>vC^or0^>.GyQ% 'H /I7e7S̭fF/ k|v93\sjq'K;% +k~%*~56exI][S?kx8`wns.R=.^ J9i'xxל5& +0+wx9=`0ioGw n v _e'/*h +|hX?YZ>]bsXrsX|KGOS?`EĒ]sf%Ůf E!ⷍgDJ$I(yvHZxQRc/ҚZw +Djyyk\z ~-iXr1>D8" C$_vg4:EFԺƞiI`:꜏vdE6Ƹ{GF:oWÕUiӫ+baY%Rc홡->&YuUQ~III} + yUla^^偑NUqn>ޱTgVodɯ3:#<-̮.W\\`coYCP,퓍|3}2ϴIgmDe]k`zUir>$)~͕Y/=Qg/?9? +]Vz24l}|crYOEs1@W%zAE/~9dTz)=tsFYz_y.~՚v;q7̢ܤ=nOzOOΗZ#^7z)01\5cz Tᵍ¯J[8|+ ~oF ?ND#&fYS֠u[U38n G5oZ54K(1EAn~=O6:dsGoKUpc7761@g5^H + xMӱMşj0nuan,{3Y\Rjy]e4a:zHϵɛЪcwGhf +YC-U&^tCbhMK:EN1M.Mcj_u +9,#LnTqg ۷߽#hn;S˅h%Y;lO;3kyεawss߸Dw)wI(-v{QSP./ ˥/wyx`RVMu yCE5+˞gcO)fѤa3>M>i. дKѤKDhʘ5hLJ wEĄPpRӁ\M)rcBa_طɛ.ƜӪ9$;{H+ò]zR4Kqeo_`MJY79wӶ#̩[ QqT|F4sp $4Ca=v=e=;k/ZVo?mq4w]DY4kPDsp?[Gj'h~`C)OyZ-kLqK oJ"*"kl#jbkJb` ^$M=<8{C?:9 +)M]}J4sZ|vSw#ii{[a-EOzh;e+*FÇnx}YQ.!\{}R[CB[KxiԲ{_ZϦ*FS1Cj>s-@N״Qьk6U}c4Bs6*g}h7ZDdhLaw3TKٍToKn^!1k{! qam]mvy竣C4߲fOo_ =DJHiB9O{!1+?{7n=[ܙ{тEZ8Z +-rFK5Uk4_iVCԞ+yQWy!A]z_=9/) 8gˮpyHWWKg^op)c߿) _l38ۜߧ+[f[[}#&B3cX)5G,'В=hQZZUQ;n]vFqIiKI}CtWvk+s ~Vb:Smtm|Y]l\kɏkV=_d 5xgstşҤ^)6`;i>]#ZHiZ9_xր ;oU+M6lqݳ;8:j16\eYc[gJ\wcBG=-,ok?x-}_RG u:W L|Y7{7;g/-)hQB&Gr6Z`ܩk1o37g;Ɣ

9٘Zdܛ둧nĘ:Ն$UXFFFrH8?1ϣ:.e4}Z4{>Zٗs]]xR |wfN5eSU>E=]U7Ğ3Ğ})0{1Z|89`'] ^)-.t1CN4g,d4j.7eZZ>VheZc3|ۑޏvOգlV[=o7Z*oN kղUogjd˵kYmzVhƊ X`|:"0 +cgʠet !!$"ynw稊. [|` +ܞ_,54s&RZa;h qo*n󫙸;_5O%u_%.)*G׋Y'oT:qg4?a7a| k1xR͊t|J60(ZAXT}^F俩30?nT;}n*L݇gǘV7ת؊R +pU Tֆ>gx-As窠M&hvvh׉;MQ-dWi`OϜ_+lMg?~gi}gyڟYwVj=Hr3bt| m7Ҡ2nxskBu;ou^Ƥb }:FIf=I?'}ڪu!vܒ8߽M6lkh#{Oܙ*aۮ% ~e`}g%؝efnVwum<1$ET"|9=l{Zmڏ-:e5wg\|c|fU32=y;D%1IjTb۔ySASD9E9dgmBm5 KK.?lS,&@K8~m̝ٓƿg>/e 20x׳֤=+cm[|^bA'G֊7C3[SYz<"J6/*q;y}i97FshW>2_^)KoE$yhhąGoE Whۉ;÷]+Ҩdwyqs2qC%׬Tn{íh`?=*1*`y YS%8_] oе1e6Rٱ9?aٖD9ag^+KN|Ǭ5kdر82?t4Dx7]R49E}!"d'9I0)a>ݰH񒬠 apq&VQS ΅mx-ъ\>}؟v9MVqmf|m S 랣]Q |BVe>?ōDvx.<%mt6;29&n>ݹA;?thG[Ͽ5Fm"2}aDVC,='W2&tiz%+,6+-r=%n2[Dz~"d"6}.wo3'D?@kϠg]R_t"vk`t4ȄoLq D%{Gk h9h璹h7\+ }Eo${O3 c( 0KVߡ)JҢr7Q]M ]uA\tHm?zW=`U{Ԫ`ULq8YqỏdP?9JIOd|]K @/cuP CV8kqɍ{M.EMٽ[ W6͝:c-mg:3EzvTRQa'x _p=_5> i{5Zcժh9r hVV W;_OYzgбFΩ3Mn,4Iorw /v FN_J6qy1bc795:=AGCiދz8^Dj +P.Qs1[ȈǛ1oi6|.*G/7ϪV8[1-)vU_Ԅ*+!ysMu=w^ 54c26CWl!uǂɚM~&|t!uai[Ct#^^t ztKpx)hnCFHDfǹd,6 +%  k<"t+ᗳZN_\ܬg^%eKw2Wg3ϝdR>*f7l04TȚʼMn]]0W]x6H6{ 3?XkENvn6]rd5-920a m#*D=χOٯ{F + +=m_]SGf ŃZwvd,2pԛ9|"6?kj|;s1u}[ߴmQnH)lCfvQ -~\V~ЛnV>([W1ngwjFzAP50xbMY?S6<8⌢ɁӊzhǪh߶mHgȃF M}_vs |oIϔ<ϤeR{o%x*|0v߲HUfSNaJsO(yA[$ J?\"yrPZY}wl{AB"BTϋ,bH[3c/eO:1^! |J)})T3kD;TѾf(LnRuk,!x&OxN5],uMT”&U8sͥ k_*xD`0OgRܳ.61J<3q"%f0O虵8nq _UMJ7RTn|al3ԤowSgæ|A&DL^ +b,:4LMeڶtڳAsGu ϲ,=%i0~?֊fJtQ@eRNs}\J4ѳk okfšģ|o􏪂_Qz((}^  ymG1Lgq.3zj1tQgtQg.M6fϖI*-lG.7&)To=Rx5Yީƈ133`n1}pEEI:db0j/Ң3 dwn j2>vK^z0Ǣݩg繈hk/Wԓ ugm"ېZNN'#gf{[ zg6MWvYA85L_b#/*&6sDuU#h44SB#>C #9]&AⵂD%=c@MWfa1KF蒒z͘DN_̿>8C|YEx~+}Vw̴}=+51k}D[pQZVOX?CZ4)8f3pمj4^?؈3 ++D/\:ӌ:j=8=\t `, :MyTArN*KR}: wEϜV=6{YzJ׆g(y6Jj2jwo% +7Êכ؆N;1],89EC-n S,'2zHAjQyM2:"nWNκ߽ԵDxVN U{K0[O(w'\ +ZR!̹?4<0KsHKQ<(ȟ`[rŗ_:xn1O{f-'N8!ιq<`LZ>#Wqe}ԭ}ѭ۾eD=s„kjpY^){!oOZOCy_.LP'RL2R~RN[U"j7mK֣]6#]#_6LgD!-3FVSa xFf-¬?P 87*Lb~2YfvgQKMF;b]3}19fbn +9uRQv#?j_02zCsl;u nYAߴV}K'n3I)cY=a.'L2rmhOЮ0^r +i;-qD^]9t0/!8=L?}y,Hq+QqMۃDG-2Z&J̋/Lɳᛐ\AD duGBAj^"Lj6&Rh/y|9EuDof=89tD?q3f#`$8Ҟ+Z/qO9IFe:P 6ŦIrXFG#idi4-cq1w !JrJ!ޔ۾~㾵֩3#Ygʌ8֜}Z{U^{-:֞O: cG6rEӟtyw+Jų ۶ț56t'[x5Nóx~-nM_3wwUج=0XâIQkHqZvT9tm_uSfͼe}B_kؐ۲]+O_}<栤/m&vM?u6-{آ89nظ{]L1f`}vc'7x5ũO0pK*_F3?,?9tߑ+Bkw5 <:^-{,]gƆ)/oL {o/oX9 dP6熚2㉹|>kJLԲ1f*KG^CYpS#8{C < +;}19+1F`ɡdhSL\^ZKrŮo+O=lT^YENXXc*'9iim=6ۄ2g7}^1NA1 +<Ű#_.dž_6ǰTwFÓWk^csu lyP?1?Ǝ`tspmПnz0&Ptk;alI1cR,m=C_qFR{?y[~ 3|^;M|Ow>" ~;ػHwn, Ƽ7:'vN#~%AsvBbbn&1Z |k"o}˟FѸvB1xijh~z +<.g07H >L3,gמ^ŨY\ywo[m28F1s 'LĘ w,;aɚp qºֲ7--Ro]:)rӗ`q\w*-%'-.C5>cɄ(~sWP,U-Dv-J~RNgenOd[?e"|y:?cO/r$!<l)m?'UW֕]R6lDr `>ĺk[oZNSmrk΢iK=cEa19pQn7~7M-ηK1僯{}{n| +a:#gBKws[,1uЦ@9vx﹯c{<׹xSCm=;&W>aѲI  ladʱ Xڣc r?#rזs0.#yP1wdKWc:aZg/XwbF->bC P.X̧<]7H紆ۓoOiy12ʆ?TB˞\x[xc01!ԾD!ջ,rF]su0m1l?x1QE-wNE`Y|̉:Vdu%AGж`V /8Eao<{%7a3<ÏR̻U9;1w_OZ~Mr׻<ц?PG~EKL9=1k8; .{'EuהW+eoZPV߶|y`VR4$߮kOb@kf/hbgR^y m +<F:]K/_6o{;l3j0,pV0w^}ESz@hkNܜDxI*a|aLZ_,زH7 u\ZBG^AqAtMxU9= +˅9B .gjdb|C-b1F`3B~[ k`F_,GwD_]( +aMZbT\`ǦSz'aG(&1u ;N\Bx=Nw7e;XG|3>QNW| #0V39rwql+S)sȡĞ.9GḌ?3Gy|Jh*r7tzd2i` ߾/}G/yBlۼYs`>ʂK6cɷE;'N&ͫ S]&_%RJ2.:.l_2tY'cRe[ΎݽskF0gT_e +c1o}A~x 9zt@?V7ܹ eR[=8NQ'o|˛bDWZ~?;b? ?{߭<8I,оo¸_K֓)04yal0@_{Sc\8ͧ7<盞|s]Ƶ;.| x*j2d7={ȺiOs3BG1?|o pI1tpKIw)>\% Xm+N 'T07B c~3%][W* $:&aɔwٔ9"vÚ?S"kȇ3N{kSk[|x2dMsv&I0w"ƥwNj^EMky>~1`,(k>1x[ wN}'3û߿bQ_A o9p}W ?v䓺nZr崁~Bÿ +i]\Xzo9e=sw(Tq!|37pYC4uq& keOTC"+]Haog͋9p]6 |O`/BOF}Y$뇹f}SOo8&B+{qyƸMwmfg{\s=&Fvv`/UFƺ ox +{[Ӣ2?rugkn ozm +o>zŷ{ڦ7}?>F3]m`b 19HbwEknQc̭ 4m|pgE]OnΟ b!|x=9z,[kjȝzT pMķqK&@mZe-yq)ފMgE{9O}tkt7S.;7޴ _̱dʃ jbq>:-ʧuw?L64Wykšsmg?Y?m lhc=Ywt+)"7qá+Qco\zu${0It E׽xը+`"#z!-_s"{M{?Pm'&OWrpdD21.'`^^ӟϏmyNjqq ?9) }k>Bq1ߑ0(/._36W1TO m}cnn|pi>X\Tv|须ŜsOgŸ0r=7ֲևA}ok$7mga\U'㼋e֝rcb\nʝGsG?|#1;kZe©BK&b<N'Se ai4|+#s@,t&<3M?]9V(qצ)o]ټt7n\s:TJ#wtsW~9_,wWḙʴ= sGҫNrg=rq2̝TVWLyp u,?Eϝu΢xFĪIt/\yFq۵rCqɘDzqݣc,U _{OGyxV~Vcjߡ4jU>5G n:w>`ĝq_qFߋ/(0f/;|iâ'Dw}l7keBm Gznd Pan@]'O89l8r/xVl7.ۡk9}> -yk7̾rg± A3weF2vaS _'|jL8qʓ}'G'FW<sqS~5.{b䟡QS m˘n==}Պ\C964GsJ1wxgNoQ%ꤸM=%Zs*Pn .𷦏\N 3IFT9-bBy}ϑ&c2̝6rgrg5{x/MKFm 8w1g٨!m\%Go#ҺDʽ囗=t.xh6cnS>5ڸ'͛_&j=S.^*DbX̍GAv@p^]>s%b\=HYm$1q/ZVn#g/0'vxFhOY%#j֗׸s6<}AGrT\;\_1ѧmVK~ǺN[rgQ'ʝ;kn̝u|g/Xݔ)wV#OYYYi#w!-c΋>}1`.ea~s+Py;"wMT3P,kDsܕ1ec,{"zcć^ ?/:m5:zO +[-MD|fa21rɸ700﴿ 8?[` +=NCy eoˀ3wr=wVM;eotXf󙾆l;ƽƕ{79[=(Ϡ| 0/ Ų||y+Qgw%OkaC~~ƒGؖo_yIc~0e oc)`]~Q -rg)w'7Fѣ_܆>hP.uWXh$rIF,\_œ_Sb_$ ?ҵG~ 4ny:]Z}uu<?(o̦<,ʎ(&xk<|>(oI0}xоOo5s\v}Xfd᜺r7ݳr +ܿHЦВ'B mQv1Wl<N ֻNh|:ڷ+>s O?ڵC.8ȸW燻7ߘ>2}~@.rqKHF\< vz2\]o3qcQ~5{.FӼiضoO9pp}S)l-PSw#`7(' +]wq| u}<"`x̋y~B;ߚNcsˬ"YtU_ߌ3|_oءOǏ6yj`zK#==}`Sp_*K;ς |y3 +=4<5/XAZs4ʝBp=N/κW˝ybhO +2qI9[P>=!|ƃQGv#xWtV0ߖS>n< u=浘|`ַ+RWf#P/zXǽz-wVh׏x}Qd[, {?R~oh_qB{\iz.N`_6rgmw6 +zc=vWhقzWwQcƘ#YhjiXއ6;s4l}@?`{쁷7ٷ+oڛT=TmF^[bKhߡ)^W"ygDWxGУ^^/zR̳kx,GYQC\qkb^yMk%ͣ#W5> +׸ބ׮#>7ܽ<\yr{Rto_O?-l|賿 p&5;&O}_M91-|n{u"{B́z{|?TwL]o Lzkϣ.p)w?vA{='=Ǣ#}0:hD_A9U@۴}XA7$#`އ<59-<aO:ۍg羸sp\z]ʯWV @zΊDK~$p +?DJ{qh$pSgYˉ0 +{c;_p}~y"zE畱?aܫ\_KM'ֈ>@[^z-7{OwG74x,فc昖xυ;5y%G3ҝ"\os +u Z0șG3V[ iﰡ}~]郭wPc1Ȳ x{~&c_s7{/9xx}E V1C}'G{f +C{htG= [0fÝ+N9f.A_HSb(w-DW+lG?q.ȿQ[&# &|S+v9)0+C9c`[\k9xwYk&?Ə0)8sbqXtn=3OP/16uHaP?x.mt0+}['pn?8UkLu-7N1Kό:*;tog*)F4uL*3:ƣ1}>nnδ/̴,B T`*39E5_Zf"?zxObNwDgSj1v xjEgg rOw7\;@4=F6IhpcVA"d lŮೣ>ʒNqIw%tc/ݚZluYNtGW')`D;Htڄ힙 X| (&k/ X&cRz&uҴۻ@tǵ]ŵ]еƝ XF%dV%&RqXLƭZ1S6QOmVbi2 *޳uZRHU{{zΪAq,FksL2us6 RL-ޓ >3Jw7{*v[7SqUȝB_ښNg,[<-jW +4Qǰ+Z;1{ Лw^ޕLt:-cY(ΞL|HZKRs >N1UV)J[l!2v"udgYL[ӎ>CC{ vJǚqū-qіCs, 8dGD(jD;;"ls◙dfqG"|cd܏I.tE([Jrx,Ht/J %KO%*t.vGãQI8UөDbm:?"_.`V[d*|Lmu-1"%u;ޖuNENww-Nҋ1[/ws~Vȹ۸i#i.OCu8vڤX'9R[L/U#rp,@"8F1Z[ &"%se$S)Gm& +;U2VIOwhiq7.+>)EJ;(Ջ㝝/JȖ_pԑw5T$ 8J[̂xW$cɻd.v+]97:G}vġ*[%sq\Bq&^o8DZTq>|8?3[^2wE w)Uc;ܧA:f?ڳ"|Y{z=R}qwdJg"pCk<պ AVxwr2iGz Lw:Јvgdg"[w{赵%3ɥ gBۓT!>ש1LɎQ)wxqiufs%$یc73+WǙq8S-v:|rLgZǍ8bb8:ΰLjqq\Bu)F:ng +E>UƵWj|`JR,`xBQ=J/Fcۆq2r^, +\ڟ#%t 4._;_q#&o%Umv7[IrnrZys[)q7Wwscf6j.qa19˾gII$'cIN"u#:vmЮ/Vb5%'Rux_saGi,mt8DK> 'n70 :;c< =~7.YRXlK';ͱl]xfVVbsٔd DGz~-*RH:"?%0I_ck~`4vn`4 +bEZ]Zk{ K/8^`[2?=9O[2,AvE }gG--%g;Yܑ8JIEꔒ$ASǐ=8vsυ,%4wv&Y;3 Q*{7K9<{sv-s-ξg)ݦd{{oO: wWnvLº/Ш#חH˜bJ.Z+Z1xcs: /yP"ԍpUl"tD]ѱJRndq٪nuZ cۆqժr^oq-;z},sPڇms!AXE>W{݉uqZke# sDZP,3:/WF+0SeɶGwD!>Y㣎ej!=ʨV\= L[,}[ +QOi/(e9"G Xȥw.֙xNOY];؝i_&idn Tgʌc7ގL|iًqxObNwDgs "Ԩ#gh"hij-lҡ*0LuO `~&ޝpãx +&5^/DqLڹN*n4%7Ҁȹє{hJ#қd80%pDp:]=ոB+B$F %9Tí$nJtU +Cxػ;>stream +TU喻Snn;2h#iæ՗W0^ަ* z:+X˪{Y<U^"D,{*T*.`,FԔcQYT(x0 @/x/HP=+{y K*i6+ +'qJVD p) 멀j*^xlI +k%hUYI)W0:YU"C0%Ydh-v1/Z h&{Ut40hO!~*̪pmOAXVÊQeC +r}LD@?V!2+(B֒,,9emMig\S f]SļPה~,Q\&|66yH 2pf +;lH49c_Qe9 zJe_Yx_ne$mx9c6Ҭyױ8C@gDQK8.ׇ<$ 1G/,J؍l=< 5p@aXu;{$l Bo%` 0T />5$zh69m p`׿c?LqBeu+#CyLA p86nUËmhJ54DmhЙu 9AC9ă=0dVqUTT\xRFp`[f*c  V]Lpm.<:}CTZ>UÊ:&8XMpv :O\u1c) +>4&8XMp)_E"8 I]T6E(F@p E!Ʊ N&-(dK<ʪxU9 S5HNeC"#`".fPqя mi]\ ۠DaINC҂#y4^-Ia?1[765T6 +QnSIADGnH9i_c/)rЀօmQtqel3xB?TNhHRA$ FNY=1GKs]c~91nj;U!z*hDN7WdkoLM9Aup xUcal +s,#^ +Ί飡yMtM0CJ7jP?-ztƢk|q?V-ngdc־]o0eǻU9reN#{kSx +JW .UÄC0`[(8cenxfkxX鮶c:Z[Y ΘY Dt0QGƄRp@nL'.fE ӵdѪۂHV]j@fUTMNKT9b`oԚK2wOEEKy$MaWUUvԧ3fA(l/陾0OۀicKAI<9*# ysu6О0bӠAۖnI4WSL_*l6t6\8%S̲Fb;mF xgT+&" M ˵Ba8T$Yڨq8VX;-˕c@ $J ,%s(8ƙ[1F駫*kۖn9!Z2QmF.sW( + +I"f{Cw0lm+0mxU_j.'\p"065mk]~2?fmM0 G.(cb2\cvn~a8[qAMdʐ<)me\$NN Y!Zs`٬mS<\8'usvbtYbt 2:N>Z ɪWoX(V5g1%_/F5A);G]%)<#s:.Ж+" +s 9˲Xr$lapV*X0h?s~ cgN%B~#G^"k~ +!9TDZM~ cؽey[B]NF>tTcYԘY  v0@( J^CbZ>Ä~gxeE`x=:#ʁ*P +'Jt^-̳LRb%S¼;F䄀8ɲIsY^EIJ4b`xJv#fbFYW +)xRADgNez'`#YTAY@S@,kkmEEx0H ?b=Qz`B{Е5si;hYN%g%{#On`Nif|bqC>Ca䚡m dK,MR>Oh!hG),U=xG7 Ab>֝9S- "\wJWh{Y1c>jd S@vvf2RzXAjӒ4#6uе֝QjK :*lΩJcfrԡH@\E*'NwR*TK!(O¸WyZ hS۩<]t+-Pe,kJ3\UZͣw?FE 5|tMiCӄh s ˈ{ Py΂LeEr +V,-gbc,|:򨄤J٤=j`0pWTlȤHf"$ˣLY""$;d/˼b2]J^MѪ)fLAYj`f,8 KZN<ڐe 6AZ[,Jx`qY7v&Mf6N1 dRBz wۦBd&f1O^۽N&fhI6YP̊ d&Ȣ fTlbo>/&'51 ͉ỉCvfь3)afh3cT,2+Z`/bsZ2p0 E>v+.vu̢12kCӱheC ɀQqoɄdAN_Y42n@Ll:m:ra׿,drcHW,*1T(J" 1Id:FK- +(X &z{B԰+\ 3Ne, + +E"C'2:Y~oȄ$AemN_㭶̢vQu(9q0lJX4bHQ|As_е_uXߪ2Xq~܏g ǡ~f+zyjz Omw1-߇7ei' }5gKDaSD+--`cKmhes?i)aƎ\o$| +m!nPɼ+wC@vh[+Pa{ndoRy,6K;~Ɂ`:M* uCmt(1Qo`JI tMI7j(֐jdfMv fy۰AT ߲jltsCby[:@:M޶ Z l-dowRO6Ⓑ·1ml!(5D7XP6aED7ֱdZ XMlp![H6Z:F`kUȦl%Cc6,`ɦfv(M66Cl,eéj9u;(E·,u6Cؒ)YdʶT?rf3!lX,eʦfQv(W66 az )[MuH[[6}N!moY*oqipN7,N7, N7,N73jd[dj'ddJj'd[ejce\Aӱ4K94K 5K+SM| LɁrӾ%ݑ8Kd"I ^jJ 9`Y̐zr8\ e//iA"FXM9^xg"\J$쵃 Rv3j ŮBҞ7(Y:o%04e[CaU{CvU$ONXx:02Nu7A.eDQB_0byé^*dЖNqHlAjLnΆ .K`~"@*6/I6粄QEmEdm7uhiRێܦ <|*k[;Nr^U; `(;Q'-'hɸ1o D-5m#;lU]noQE3x:aCjL'k;5ŴӿcBݨnۺcQgM[o3k+:X Κ/͹Wd"u^!cA}_{ +" +M, +'[]F7^@xȽXsjZ=L{pGPpMY +_;o>_>#en1 +0Gi^=$).<%"y\(I*KNAu d=>f%xQk A;WD`cZͶ5+Ũu5y<3iTA3NEsgt*:a^f'(ZNLVX p}5FB€p^TH8aL +2-@ 2NQ/8Z H B;bqK +*I[ASEܚWQ x@VD{P0'\4`ڀC?>>i-BcU F5ګ{myTCC0߮pyLN +F`i$$4x}{M*i}f00[^ZnYX9YèZ Ψr<1|FS^Lu<3,jHșL/1kB&ƾ\rmYQY[I =#-^LK)rvYZx`*PXɏY5C0$ VPm,soVy5鬿Bg'F</ff,Jh.x^~PW&#cs<``wzF#s)(,6Ġ3fw +[ƽ$dn#ĵh +qkm6 + nwp@Ud"Y2. 2,b\b!R[sXWi+~`G_iy:) gB]v1mV!-:S{ԨHZU-LOs@*bU4Urә;[cg~KjFt5Bcߦxsf m!:B8jq<Ѯ!9 @rYUǼYNwղ8. +JԶVɑKEW!G x#W5i>`˱灑ㅖ5;OieN8m>&h ԬY?y4A@]ʢ<ys\53Y[d>l] +ڿ0[5ZUIF96xՏOCCHAe%UBS0'YPǪxT + .a%Eai` ~jf7·X%ڵw0JuԊwJ^p8 %F +}fJ55vCby;onOks=0YND:'Ak]t1v8+inʬ'TٿXg# jZuSTw7u/z% +*e2: d7]~Xtu%<*0OaF֍axFm^$W4hFEErC.E~Kږ;r-ggA3(nCyT\dd6{2^O}mz<S 1Kd?,z +(0' |CuqTEb4 NYEǻ[SlM)"@@2#^ދӌ^+z4M@Gsў90  Lt_/)>3n@OLěy/ tz%K#ӫ]_s)(?Cӣ=A5kmuO${ |xj$[ŻKsC[/5ڕ @=OAVc:}Dk/~yL؋?~6&tvYޓL^`^*Kcc&0őNYzj df( {jmo,AĬdOW*ާNAn 5Sk#fTjsfװޱ(īJ6 `DN'bţr+Z\W h8+NH&y$%SR=DwpIJMIp̾`,:-gao]I闱(ŭ򺳱g#D /32/q"uxNdJ,:;A#]PV5%GZLD=;9SQN\u% +FLLuJJ(~qL*՛,L3ĸȊ,'rxJ2'<+`MV׏gluH9, |=%1ϓlk,'2"fP;)YxPv䘮H eו&!X ֖PSA5Ɏ'zJA&p*.ELdI6܍y;x]l@à|"1*z#qzCT<栣QH>L3\Z~dPyOYQ00yeEa)ωfplM_xLj w^yXI Β,q"O Gxm N^I|&gHd3MM3{xn g̵U]]]QY#IhWV0i7dН!:bOSA:(n! UU  KrDr+@8m`$Hja _O $KRV_g?5ݮ&W),K7hIl|;87&Qz~UW\zÏZtWP&>Z4ׂNj4O3n/jdNЙ7# g)(,CBX$tbz.%<\ZB&[ +{)-ޮ7ioU)\aoKp`ED3KcܮMoxs_?𻙻b+a75a+tKs-OGuxDwpbo3@&Gvtmw+j\dN)5$ϨAgӠH_-t/!MD#j@H*H|l E>'ڡ8(aH2;@1?Yz@'fvx=jZ)_lj38g`j/kؿDkc,N;d<6@r S Y >Y-F1Y_'J0~S\ڞ#o/M`PmG'8Ym}40!ht7/8t~7z>lGVh$Jl(?dsBn;~MCA޼1H)nH6~@K#G)A&}Lnϔ?*)e+գJo2Z{\`Y$flxKxIdkbcSx|M2>D0{/djDR]1Am. +$>qO{QZ- G $ 9ހ$Z?  :1*\P"p'SVIobE-rQ*Xy'2e*Ea +0B/Tx}~pP>剡"GHHQE } pV0.za% N!|=b(g.'h>@т>@!D~tm!+~%̓BR Ä (]r_hIP"`c^ ; +Q,H~PBXԠ4"RP^@lU/':CGY& –&٪%sAm8F fs;ESh<Zl%:I oFD"b!C@;oF`KzX0Gz lȢp 0w?&3~?~:lC˒`XypYJˏFJQUK>V⠱SQ>T@#&*>$Rf]籑^4@]A IDgVBܱk @sAw0u5"PHa%(.E |>vjILSGQ5 `!u "$89v((_u h/Ddc@瓬nQfo,<`;!2 E剬#̇YP , z5I@gM$aI$5$]){a Pҗ9I`rXyQ&~v~%t9 x>DXbu +"Pa]hJ'G'`!nF p2xdG`s kL֥BD|$^ + !Ų>^䌁KPddRay bfȣM4P`ASH惩Ьc.V!-0C/'{8N˺D#x`"+F恁0 k0)-A"Q 9RJR +nǷ/XieNz}X3'Ë5Ff8h:ou!itGz +!}.6 +.k_VB Xh%NX1ȋ5ʢ]ހL"H\l_+S +k +bO/%&,, +''] >0hO.X^A\{)I첸ע~Jh}D=.Hc CN;kЕ0`w/:HcФt{H"rJHy0TVؚ PG*U ҋ + ݠcGOXHLGHKZ Cl|[lܞ, /&A`]c4whe ڏ bn&xy!V@[990E~q)hPO`p{qi!G +p8 tX.^bZJ6\ud*À!P^Bν0x,QlXA]PJ!OqLF+-r޲%^ K*7KGcꫨv0 U_q=)zX.!\%l3.rpk~mQg[+t׈%)kUZsޟ\_Ld lO6%I!4$"G]gm3`̐<$!X/~ ĬZ@'FGaXI\d-?D3w x2#X:ebpxcY0N +g͂, v[4Z  e&1' B$5$9iOlet0 ?ߔG<*0} 9 0ؘi J|*gسNăUÉZQR0 (г`Pb_MC ЀwX~ N<|_`B[ŀ〢ulBZ| /~Kp1>_84֤ž sT`|->$ϕAe*"Z"|;@=kYAP/hM6A +QSdr/F_ p%Y!GoSz{ۮ2 V >""(e)rCiD^K((Fe9f.\Jq.#6ș h;0QvȸgB&@ha{eZCx[|0<` B +h9D^'! esHځoEȥ89R2 $ *Aps0%O{-k.+v{02oڟĩDG|w$ ʨ`t׫fm]^ekmyBz=XFl*׮eg0;[{8 4ƨ*"v4oS-&1]o6#hmzx6Ϳig_]|~߄7&{*p$0|krǮ~=-؟.~^+phŶ9f?eVjGo L7vٶL;T60 Rg^4fGC-9tVC@06`h>>~_RTZ_uVzK59 ~{%ٿCJpC:Vv? t76n1@M CII1 Xn Q+35{EqPu<wꀎ-LZ 7IOM lO-#)^n58hng?j;F~a;%Qϯnv|vٓ(|gbc[,NHOSQ6#\eΆu@yCJ]DAڏuPhZ?)9`+͂q:S5Jčxfӳݾct*!o` ֺxNj78.`G\͸cd?1B[D~S?dgJ۞9CA),W   +XDSeHd`Kd75Z,Rr }_h(y4*ϋQ2oM2b4ȵzvs;. A6D ~X@X|n;a _b{-yytyc<"QFLq4t}h,Xvwjୋ +h~~a.R[ NHv@jbۉD^aF;| S?xB!O)vQMzoZ$(H}`_ sIo`49_ +Mg5PgtG7p5 o7 x7[3o{G]1oN!v"Q uKfM'*5@ÒM,|5Ih^pKbw1.1?!/b0Uk#ҖX=|}|[p6ŎҟLTf-YQyPmٛvtV51}M3hXۮQ +Fi$fbAS%(%!9;ux /X3` +gՑ]w :1u?LMyd_6EP.*}DAC:?b6QPr?Ba +L|vaj8Ww.V_C.:(h<>n&L%)jZytZLL +mJ)a3iRmъOD,5p&\[pE.!Xwg>=WnBor^,Nz?Fr@v~a5^ߞ7WU}*mi=zSt{Q\/C*Aq{Z xҌfsCGm8)M.Uex3AViB6r݋X[zr{#ۖͲ(2fx6S)@@D3F$cZv8:5r +o{(ˏh7x#JI`U eȬxg q9ֽoNIWJ"%@dc|0QCz.cÉA}o 6d ) ĭ|\?.,Nqʩ81OyVa V q=]Ah7t)ྙ, +% )I]jw6 O/pyѬ*pԴ߻ %5A(8h +?=Bt!X+&uX̛TpgFqPumЙ}zQ#}=g{IvMu>H4mGՕ}Yч Դ_X_gS_3fMB |g&=Z3e3I;eKzlɛҥI1a;ݔJ*m| -_qlB7]g*s%=eqM㘛<ԸΎw# ?aR~ώm!4;wxRe0$w뽛%;D}xgNf<|u<1+Wgҹ*7yX'l$[tiJ͢#S)Ȕxg^,(* g|M|ӣygS;||aS vjO8R0t$]taXܘ›\M}vP Z"@.nHfq.An]xO\n}zzJL<\y/Ftu:0ӥ>mJ+znl7vvx^bo!k]klzB3淐L댔31tѪx"r:Eg4:{)9Im\OcClH|yP{}QcY oЫѤ4|RaUOM ;sbbWF351:>~|}2J1&-Ƽ72VmEmw(g|j^bWJScw0n9I>DoL&}8X^M}oe )roĸaMJ)j35wL4Ѧi3,ͺaġfy1}/,a1stdKZj;K{fXi2;˲\x;XTguOV:HޚɆaaqS.m'Kz%9<)GƔ1]w;[=ڞc+f&@}-~)尻/;}s{k랽uE>6ysP#bwviw&g%vu䴾N*[wF\gf9yyoƹ.9wL1 WUZ׮xmW\sw}:L #RqnԲݭrEqވU=I]O5z]0U 5!9qef Zt?x +|c"{6lS#*㚡>Z|Ef ZTlYPTC=9 +L^*,^[>ۇ7x֘d4ƽIoWϳO9|c;nFηw~ܾیޟKLGO&igh=yGȏ RI_9JϠԨT &ͭM!3?FFX3duB^2.ꌦЬMߘbrp:mمrx.ZюV?O}"^mGuIEc-zߛ-c))1gE}^?W`c|rw]uSv =՜'`xjŇ&,v&=N|ZzӤ%2LdiCldΐlS}oT9jԸkOaŪ۝~kOGcL2`e_u̾d-]6c_r-\vProA%mDʧkyiŤlbYx| +ܶh;P(6ӷ5՗])mSSSaIgFZ,4gT3;>Ag^~2򚮺+p5%~e5jC}?Ƈ@vAVOy?z+๫/ yFj4uWG3м4k!k9ُA|)m3?ڠxu,?7Bt4/V:w"y,:r?-cx\vlqe-|] yyq6u" -#Wg_*|Ė} ׹_Bbž4u}x}ʷna ΢$NAAMO%PcLD>s6:NKd㦗ihM1͔=Mݾ=Bj.֜vvtn1v&>VojTo {ܹ?ׇ^Zni˨gn. 9ovF] F3ջ]De0 8_)BJs7,[gj.hv+sqvQ &rfv[֪GTw%!ɋn*[@,7-ْOe# {9j<볧]y^O#8vajR|^Q8=>_<ʗ\~ɫDѪBU8K'SAyE!U3{TE;C{Te튩$TFReXF U&Esճ\)J`l +X :{׾iG-ޓ5W]6&+]1sZ-YϽ3ҏ ms.T~TÀfJD|إ?'an{<1eبONx7jf v[&cYSyӟoI߳˙X˻D:bMҾ1D/ٙ krۅy-ڹtZs +1=~k3hK)4NN)L +aNWΨU}"XI&Qv4~wLI?%'h5o~]2I6}39H4ӟSzc?>kD5ǍW-41%};Kgzy')Qͦ?' I65@ԇ3@kYJTR6mX >~~pܵz_bt7D?\MDYjʩ74>"wi"b|Diӑ\R?lj`9n;`*Xt*s"9&؄?qɝ "gfFXD(k5ol)]gew̩:k$a!8,f&̎  j砖_}s|TJݘ&L'˝%]Ìad(*emE8?SXuc: F5g3i?]8n2rbiƞŪN4OF8u8Tb^hgEPE"[7l$GZyni'IX_=N 7j\<|8…$OKQLqOx$rCH9I>Cw~ dfg>=]5b㉕w^ΘA<"sr}%F鴦Noܘ-k~e=ooIDĕetzdPnoL' +'"~B>vz# ljp&;`SW0 w!&e1P_rzF0Τ1z,WÉ Lh`%1ߘ4pPw}a`: ţ:ŝ $pK%>%,өG+ C-PiTgV$~|I(;Vr䈞njW*Z'FYivq'&u(n|^uj̵9DBbONƒeR[I/䘊gǤ ii^dž씜+-1u:@ڪK~4 <,4f-&Xc0I9Q鄲:W]F\x8Iͺ`efUϜk p;С'.k +׳x[!޲X.%G*]7>\B(;{]zpB[$i'd?IٶBR<^U7"5Emp]Y좃'Z{* ,%?qN4&Rs5ᬾXYvg%po<}TLb3LsW< Hk-O>_HT0t"ctɝ.|23@ȟX֢LTK:Y<#"TѴr=yl_":ssC5ɹm` -8) +ar"'_~W⏺ 6# ܧSsCj spݰwZ !ts*;?81;.GH}vlfuUIYN% C-ڢ=tLF O/G㵙`~(o܋ܮ:ڊ sR!ߘN,oG_Ҿ]yw)bLx}W7|wHqK7 \Mߗ{Xj@;ճwnFGU0 _9O:_0tqk`yct~i[0c 0j{:*^"h+f +:Q|D=jy>@sW̗7 Y[O:䐝h~.#YT*#Zs6z֎CSI.u:mqcb!?2] ?ſ]IcMO|R69L<0Xb`ߝ XJLiu7ےx3L$ؖ8ׄ|kGr܉{m7YL7&9'[*']x)ۑQM@:GRfc#n0wcWr0:E͊ǖ4ccc*J8\mޓx˸ߟ a':z_ցMKȢ# +/Ims<,0霉lK M1/JZv~G]B,\? dϾ{gOӹi&Ffcm8k- ȹ|E2)=tpp.npgJ&f_G3> =<*;3{e~W + +nfNg˫.MXӨda::1JCʗr]Xz"& iiZgEOwbJ] :[:|;ȠYFu qYhȣh䂉+mdhs\4R~b!rH,z#+a@5D.h)'C1gz <6 bc+_/cG9<^g[Hθd"8Rj2,r-tݎGYw֐_wh 3Ps,AZ!EP=3C(FI/$v{ZV|\£y cc_KlE"|7 !o&<C/ޚXi0fE`o7qhN/*>m +HX m_` w4 BZ^Vo>ҭǷVKSӗyXxMJiXcٞ9=<85)<;$ [ {OI.FJwkL1AT4=:c_ xAl$;$N(89㜊34DRLc51v]fh84CT^K.\d3y'<}~Flb߃W&њX'lإXr,ݶx)cDii?0e[DThbXIKqu$Џ՗ON”}gm:3mdf1ƒyhJl|θ:Ix?:'fzgJ(Vp"ީ{K?4fN`tR Cj҅E-RjZKKٹ_OaOϒQ!X#$ASA> +c.LәXjkg}wÅjN0Q38iQ$&0`+?VO oRwGªs8a 5wA,]=7cG(.&((+U\1T`~s\æϡl3.S/Ql[I^GvY7&7[{prxPMa5-v{ 5C{*~YSf/ OgV#G|O&;>_$ +1k=Qq9{/D"!~5Ir!&vzJ9uzyQcߘtvgϾVfLf6HVrL [bʻ6kg0Θ}fbh&1@߁#әn.Ÿ5;i![AhC[hx>(=+L@#:vG@kOOg73؝YqO=K'# g{uDHٮdx,Rc%R\(m0 NȎg86{Sh.O5eK=ǚ(b?#6p!VF`mjx_ T&К"/7ggNfoߦ xfFRh.4c2zWFto;tTž˲PGx¼Y~Ju:D6HKg'R@oLD-+3V`*)9="hI t0{qYEE9f^YQf\$j1њ\e6b-v倦-"n@aWhiR@ƻM/5֝ݿ<۬@[Dhc@oLc}&" 5=f㑨4MQ/d2z2~RZh%y>I} (/cYmpI,WEk%K୩<ٲ( 4po.c6%^4wD\1P#-Pu: +V t6G9;hދ tnw1@KTpB4 ,Dйݳk J(5n +%D@h}ߢNt q.W}4o8a{HuHi\Np~z!A2o#^#FMnXjz +Z!*4@OD <}Zv(8|~*mہ9yܱOǖGhI}bbc%ri|+m{tO_ӡb?L](s_e[Tby6֕{Pze)_Ib(˽m֙ v٧D?}Z<:h 2l>͆3PlzEEiW>X,Pl(E07Y@O(eޘ?kDVZ6(ZȦh +fu<ǟK-势eZ҆n橪?IkKúDԩ4a s#.ZhyXMYG2csъOE,{IedY4qcYYs |p-\t6E@%=QtS[\̬zjFPr~︘XȠ=؏avNFP3 xDo2?!B/I +y B[qR;G1AZ%5?3/1>Nv|7<_C>I +>k̟gX +gV-~$VugJYʽ~]OyIqq)O%EeK(zlQcka' eͬ葦]U+ ,3dp#WҴtb[nUx:bxp޻VF\&H"vFbQjn37b4PZ$%aw{ |a3rOiirnȞђ8qoӵ#z'㠎tgΤt/]/u):Е=Aq. t?/&[dfJR O(zD_$/ypB>'Y, 2N +rJ_q9%ÜUb`35UK7k7hzZÜPNK>+^wEY]Ysh1%y8u7&m3^af fpeR4,\mytXi UkLPuQvb$ߪ1޷H1D0l/}lMX䥜A9VRASɧNE lUڪL>}s؋̣-6:Yq-ԉNjY5 mEBArOSl8) m=,{"QQ< +]\ᓳ$SnymT@<LP,A #)>dHA1]@(-ђ{ۛղVP8 ,H~[A=!ϱkSdm;KA.#OZ۱R"%.`/ u^cp."pKz%ZR,(ɊQ +Ɋ"$ˢЂqC04Bf0I%T7N^A>pli+Nܘt"(Ȣx yrSiLAlJ9FT.Mkhc2>Z ޻G"/v",,ﭗЃMyh|^:+~F4zS=ݘ8xG#M9Fw Hٲ@SC|[ Oա* ? +~ )}]rO )m'ռ [g!oE]%X47oRYSVy7:a퉳ts/G|M?뽓/љ`:%*`iZ)+; )|zcR_ r_'cD\N&RЗ@%nnhxOD0JdzD;zX%ڍ$%iUZ_h0kR\/.bl??h~vIiscJV[6Y[sGt_Ɩ/y12K:3NBg-UB$+f \-K <(0k&9 ޏ6^~~{qE;75%vpgfu!ρ 6_]?yLw?ZY؅6l_e+`Qg?_tZ !K-} p?T/'UPYb cm(Ѕ}b ~t$$$8])H:a[)ҩa'jQ:%s(=$"\5sгQ\iH$8GuySPK)G_A1Qɧlz|暌QHaqwm ݜ=Z31\*F(\gb |wk,b-7&4r42t,毬$= npnsuV7s%]T7cOny _]VЉ*]C\Ae/tՂW)WRC\A'~PC\A'A rBU5ttZjq?X +g: +:l *j-/_ $Jvрd7mV/NM_HKZ:_Zm:v֊tUK1sR :S6>S<>Qrh'zd*U"WJ(I̡\U4IdD ܞ +Wc ׇdǫ:.n4 3! bN9iĘ-v۶zIjnOZfAU3*u&L"/wlԗZ6^U))WڷƪCu%}.Cgjy`# +Iرɚ]U`ȳDń_*q zJ.mE:-tR^NԅLsP#KtAuICqu58{'Oٛ5;{rsх(0ϧS5}k ur4i*qS2(QUwJ5r7*e<񀔏S>Iա>U&,w*R{w E+R{J߯T~NE*7*RQ+RQ/Qv % Dԫl.nPT +'-~+fF)z)B)W?(A%pQA)t|LQ2 ~RT6WUˉB{,Vq&z"Ȩ3a.vsWѸt:/r)w^,{=GQ p^8iljF-}eM=l5:˴!zbBPbRncŎEGT3G=CU5KJ}1ԒS{.:>4]Yj??Nzɍ/"Mwzr;tߋ\[M'j:N*&(^/?n|5T,^:'tpkVUIurB7龩ڧ9_SM'UK1j:X߫]j:)㆟;;tRt2ARn5qzcj:Ȇa5+;UM'g[n5vNԕxOEk~N:(\M'["ʁj:) ^Neg䗪oTIlV5Z%TIsuv]ut-^TX}u(.oW'o]haNg* 2!QMa +2UrHP* +4.'ܘJbU.+$H!+apDZLݑŝ#͕#s۲.5ws4߹NvZ%Uri+Ӕ |gsl2t͝jDq6Ew?掭}SNѦ \yII^gQMlriO]tAj2:<+F5ihQ0O\_PH"Cԑ 9Y [`CSe,u6~Ofa  +J%\s6t?9 +:Ӗѭ،e߯T>|+(p87tT/̮o@E%dz-;LSa결SQgr11V0.YR6Hz߫RrKU]fP+zr9ԣW*SN'_oI\vU> &Ey?^uQxۋRV)l??N8.WQC!U;62li(d wJ; %+{ou7)U>`WnS'vSON7|*p'KR{Ew]ÝSQ k_f:S7sn:t+W>?Brι|Cn^z +SGVTtv.v"&(΋eL7eLZ,Ѯi1-eLAN]E)dT趟VeȪeUj)bDWb~UELrDDM{aT~a(qXS7j\SnSŐrtW]I)ou~h}׎T0U=ܔf+o}04T=׸Jj\2# h|NFƍ)}h4 r5\ݗ}z)KLfb'A]TPwcZ?T%-z𶇏)ɢ2<.WGL&W* Ƣnc%rGYB=vz:x@i; c>#U9ڬw/ )7&D`s2OR&6|s V\4g R@oR t`%4y +2=w>qE{#}v!ێ__I|C =:B}&arڪZhU͕%Eumɴ-0}|dl!],)>+*>_.}$>ْ3{+9V(':5qN)"hMDIZBœؔB%I>Au>}my%M Z!KG՗1]Mݡ-n:>fV))Yy^Rlyɧѹ٣MRCլτ\~v65ƶ\J̥ɚ|XȔ,n]ߠ5^_ngoeqBVhՂ% +-V6] ,-𸾝T`֏)$Qs~'ִqWVe\v=h 7򇬪BZ]"ķV`Z*uY>ɰgyڋ>lHe"쨴 +== ^nb7;"J{vj]G~]6T-B|ד#( ?Z^[zY]&/kN >co,5@XNG-%HSPeC,ÎecNK'$N>)׺S'>$ylk$,tGT'=V%F|q%? -Oml`y`]qq< SZ8 I7S{a8XV/Fc,ð0stҫ001ٜ-m"3A6qԇA6Db#-lw}rvj 3&JB{Yss\X'L?[mZ?}c>1 $dz^g[☏ͭc>Y?99W J 6Wab߿dwi 5ޥ ۸8%b9Rh汐5&7- +dXn?ow](]zfq.(]}HV[VQ|1SMc+.[Giء=C>fѶi WyuX6-?=:WqRXWƆJDjy]I|${F{Yr_9>VI[| +;bW%2S/-Vy2/=o&>m0 Od5lUkv]'*{ 5Pj[Re=@ tRf´LCkYŕy*YV<[]}z]7S+y)2=˧k\9,=tW$VEN[tl2_*IY J5V*V3HwJ2"->yZfl[ -iGfmUJ&ӗ $f¸R㽤l}܃ftζijk.'|$0~V'Nݛ)+7)0lםm2N’q>esoMn붷+c yg~;Pݮl h u.}C7:f0XqKY|38u3\xW񒹌ij!,z&-qt11S–⩤KR>%5H͖T67|0IW}i q?1V(Iؔ8T^218?1VT0Ky5K ;.`d c,M(5olKyKJjWiFY_X7AUԧ=_:в 9;˦ه ť'rhEkQcM=$pTaDr/؇ q☴zٜFıeG#FжrՕLQ&}]{lEƆ#J͖U,[V' Ok[#0|--rCnۉ&pvYYcR-4-Rqz_M%1*T0g0ÜU7g}^qLS͑O,*JuZOiOʕJmEw=E%MM-g9ti:UCmM}³A#z.4&?*>ROџ +T>u|Kg.9zgˮ]7#kx9mQ~p仧ɇ*qՓOgq; 9wb$~>=2yhjzUqwU␔P5CLDC1RXQ[hhX RZ} I L)&G \O2I^_˃t'*K!*&M,7' += 0p1>XM2%iZ)cUb&XpMU=Ĥ))ɯiU{p6eJV7I̘DU'7SMAnk)I͐䍾-HJ'!//cUmIa#C'DG~:@~Z5Ր5̔1֍1q8TܜK!)rf""]5{ˤURk 0aW4W![I{x] X.&ӥ$DXss( )rfXa!VdBUoMa'rzf~$@Jws_ .Rʝ㻗[1" [&kRgfݴ#Y +.&;kEfa$ד(ʊS  `RiWQ3f2TފS2VżaZA DxQL!1 +B SLFǙ,dGҶ?W(+2baeô, T] Q4쎲t7%^]vV_nO|W qBpڋ'Ԑ+s$۱U?.%Â3sKR+\q&dU䑐zO1I$I#Ҩ&xe@$DۥU7I {0 D䐊s#;.V <`yJWכGUנ*lnu\4IA"&oTO_O#(I ݲ*&F/9S3$)Tu3!{FcÝD #T@<MxK_IRLJy-,haNفD/ŪdU%uXpItoCO* 7oǨxHI8hdI{<A!yqSU2y7h̝nx!2yFa _ NZft|?pC.lV@rpJ iqaU;qƜ2frv{ӽ+qHJL__kڟ* ikfuPwox.i(5#dbL㾾Wg0'r +JKfX&m^9I͡n1wG L+v957=0BJCZ$/"GidR1I_Qd{X) caDR4w=I^{5E19,̜ eƉR?P!kS+) fLI219?{Z=]J"f# Hɰ[Q:?,'MyDS3q=A-׺LFRFH* FHe5`_>S0i}^ذ]2|n4涃47Z;68$%,DN9HHiDͲo&HN#;_uȟ: #&1F`{ Ӡɪjr2dzZv'7@*7CRy}H"vSGM" 5yȪd?l>\'Wn,0'LW,[ נb矫z +aHKުf4V@],υC):[jHFo6IW T|fh0s/F/La؈Q!0I!rHujn*^?ԻtIiOjqfh},zC/*^b2htYH 4itlkO-UZei)nwN7ug22br21Iv?kNVar2hd psg&wwGG-PLpk_o.RT@y&܌GyN) 6EQLLFOzq{,jx""襘ԥ~Xa !otϵHy۟i)iz(&o_߿C4y)_CZ`3oB"wsI#@nY9q~ַ#!ުFٗ4diȄdߞj\4`rVOjq X$MFif2B 9ު$pK]uz8D^f7zw7>d&)/2ÀI냀*O80G`4De$sgcznz0B"G۲.~UcV:jԋ}+F"oU3brD}KTʕ'%kuBG2,mH`L$)$%n-Vf|D Gr9FIMA.70g'9|*29Ƭ`C&jFY?씩;Bo  i6V3E,`*m&!Y*7dR +ZNJÌ(#]rHF1[UMׇ͏YS3#19Tq/Ywշc +uv.0]S1?|TE{ I5 +cJ\L~ea"42a:u<;{Π'AլHIC 9^cn}O wE-C)8و4&){=3`rLCG/n`EBGbՊ#(џ yyȱ}~= _g!E7"PͨD 57d]e5߻.tY#hgpAogoDNiJO QpiɋGi7&% V54r\u+틯u_WÚgЌFoߎO19ɿh>}۩r 9o"by!&#!p@pZU8%y;1w-u͘U8ftMUe %L@zhb^ΜܜKQ kyyyPjn$Qc.%iL\yжQy4K䕈hX{C!IoňHXjxŷ{QŦ5ƍǻ=mr8̀ɟ>%Zr哑5q$Qisal&tRQsj&xŸ6:\WO D:ތ!zH$MNvaʕOAHrTa.gV{j+ɓ4ӽ7RäբtS桏DFD@Z{yfY LtWNմvnh0HȤ"1` ɑgp| oT,1"YzO;[1OQc$ϾyK~0 Twjfj1.&;՝IT]9C^CR8>5/U[%>^`Z$`j7z>$ȎOLɏtL&&gP;t$B/vp0hzUT7 aȺ<8J<[oFw7y7kW.V7E5=[1V3hHnɬ?0,`&pEpBcnp +RiqDf$q\<0-y:4շb4&GvNAẌ09tQr$5fDcnţ{TN#ra=BY8TVU]8'V578m9̺&|v0םsm}ceG f/hhbE^!E3jOd^R,QPߗ'p !@f^uN*0N3T#Om2~Ivt(a2i[U͘ljH"FwÚ]АA&&G4W-2,`J# W $:@!U砣ssx +3rvU֪HSwotŨ'hۭp#ϾMz*&#\/XVFE`fy[Z#zI!$R]y#R"~Vf\$ˬ&J%)}K=`2"i1v1fI+*^+$X# 5p1W" f$Vn-X߀tnL9|I-#$mkoxH8t<̀_wȟoI,&ͷS2Mel)c81Vvz tQ1)t"m,wL7 Ez@S$4;~_>(S֖А`j4&$%`r,]O70 ,bz Q^Qec8~= ;AX .C'$b{}/Sͫ!'yjݩ,VQ~Ƙuߺ,6ſ۔]-?zUvoD*9!ѪirV9āZOeߘ{,*nX&.&h/NCB譸?sU9?~9Lwf;='+eo$OqYzg춆[= 4`2"j?>!>5ʕ'Hkz0$G$w&xb9RjYzL{}|k !4&_>Ue}$'K`AOc$iAY(<&Ɗ?C@Rx2SvK0iL]!/YveyriGEELj`JKG=o_x@n8d1E%8{r壚'wgqQWxIAKUA0Uo]E'ouKГGΰ&#SfX+[CF@G  +'#O`V/`탞gC09rZv嘘TdI'L!8:xI'AL? |F"CIpv_<( +4=ؚZQ + .r eTg@3OI'@4+tJArf /6Z}{=wwP˛Г`J4˕Y|L3YncideaB>JA.vT$rv 0sxW= Nk,`jwIUqEѳ0AŚt8 :A孞_G$T8M=z?q)阗O&9<5Z} 8e DC.}l=JAF.&!)A''䟷˟!J2ʕ''U#j sלQlQ(ՌgQT5Ѭ06Cr\yRFT0thI } A +ϳ&}V \n +%8Iߕ4 QnA++ `7t1Y*.ުA͖8y 1`3DLjh8&ӣzvk8I!Ch'A0A^/!1Y RbW9I+YE=qX8}2qSj}sWBF)9uKIROolxn԰e{L!8|M)>m $3^'& jn)&$]&8$f{XLZ4X$Xƾ] 8h+dߌ#twW(c s>so@|&&*V5-JFoDqP8zcH rG"#yp| ߻"$B(G\>V=)B-8݀ 8i9JW"ϡ¹Ih zYfTNIɶ0YQL7Һ.sa(L&M+ܟkܿ,pEUs\!?_=v;3`21JA0 +=v` +na(4-|U5c)N5VaNG}]4IK S6n4IQv;0Q:|W5[J=`2*S'INjI(-o4dIH}|`c'rN%B(s|@w/J/r;8r UPD8o= /Asis;=j<`*8PqxdbSTWghqe'#?^܃2A 8sjaj0:w$ +u_>#3wL6x0N:q֫n+^oB(s]&]/QȾRcժ#!oI4Nn) 8;M]ހ LBB3rXUpLi%uu\0szS@p+ܟ}c@~ルnPi$l.o0ȁZZPГ' $&;(G<=yb2HͰ=\Oةf3qzt`9DR^t}퓰R:d1AOf+SҋYr&xu8"'>UN%ha~&g'y$zA|v^gTd6(!zv#bJpGtzք^ovviΔ,`oNy{ TS8G& QVJ8bQ~UX){ћ|êZFoD !K='쎅\y +^M(ΆV-X'w> vĜ?-PQBOf5e!\bTf6]O2!n' uB~0dGP^(=wg:i꿡_;qрI犬 ?K4Br=a;Ѱ"P<׻6a&$3x_]/SL)1o,*L=Wd OSUtg7*9>]&k¸ܿ,!1!|2-v&p"y͈&S'yD&փn n6Xja E(G $WDL~UP!Iev񓀳9.aPCFPBOfyaU9ߕBG攙Oe,rR d123M1@FE7 P$/z<%~z2&뗏{`U͇U$0. &:qzRГ$Mכٲl?|ҰʅUDLI#{X$o1enz`l^Ü&tz_G$R6GcГ (&٩&_#`2S/CTD : Q=" &+ݟ:y2 ==~7Tʯո:J'{Mpv;=`DqGBo5}(sc4^19ɯv~ħrLiw氠Qv`zR -V 5 +mG}z*߼,@*I#`gO˟!Ji' =Aq:xQBpti$$Q|$;DL~iXWv'n1 ň⎅&(G |ɡ4~V\!bS Q7XuB.sl>>A4IjPg[2$e>Wg=GQL'yRIc搰}d 8<޷;x@BJN:0KҧNM)8LgMָ@03٥I޹GrAB.1%19o?\'7onASDnB7NnTcAp2\J' W )s4z2b:Ld,yI՛=T葓AVkG&19UIܦB$Ĥca^z5EEx+ބ99W7 &$_K=(4>brv0.vf>|"&F^na7Qݶ0 zsIgC zwG&0^݂99Er)ov#%gbaU(Wp!vGhIIByDm H `T>"镮AOPLɿ9FB:Ae'Q)ȱqLFQyc.E )o=yU/ڮвDLkKeb=\T DZ^Sh&A:r?xsГ9<&i}'t(WԌh8z,:.  &?#CbN\ј=9$QߚwA<xC-I"Oo؍ &@Of?vPN)&!C~Nq%avTn" I^ja'SgP2I>5[ $q˳4D"4ŭiEX{^\醊&G427f\T ǙdND  W&-9Г$yh?&mٚ9z,'s k> NEQ^&A9,+j>VLaUY3[nVܦvw1 `Zs?} X)w{!&bz#_Ej{ۥ `2#mJ'9L]U\1'A0s>=#$)9<`rD&q&3ŒQaﱈnn\$_:}eݺ5oƜ'k7ې}qea Y=DvH9 = GބOyQ2y~]gR=I_w M"z$2NתVQLN-LkȚ0락|cI0ʯtH;Io(p{sw ʘ;Z^Ü&\fGP3޷;;e5ۮHI2j򏞔>sHz4VbҙЍ[ ΠNg띕$Wm1!0Gww `2s zMZ5NL뮅B0N`>ˬ%$eN=˕P寞Iop2`/8NyyZ'm .#,7M=Ĝ#Ys^O +v~ׅ &Dzs 8 znŭ[A (!Q7$e6'vKRLLF<%}VDd&S32qZ/0N:EZ U +g7:<#޷;H4Q^>Sij)II A8叨dDo&Y[ҵӓ<`Y_-&2J!1TpgwTYl .5V;Cun"$eUno +D$Q +੔1{%Vv2 +=ul!ġm-T6Ua -$j^o$dBJ3`{ 4L\Ǫh8ʘ;d$עL:G}*<`rXIzfʳTL*KqIGj8 '9YW;&b_>鋱rqg5VƼ3L#'L /GEtFE٭CO:FEI 7%R pej2+emLIђ;[ɍZ8쓎n )Ă hKqo!;K8m!4`2" k/n+2i&z,:6VaH30QfodL|Sf73Iu)82q 'C.vCO heGU`|pD۵4J(WjaogMr;O#a^tU`qԌ~<sed?)}|]S/z=qn += DDGn^'@ZFPrK^߁!aA@O:9 =iΠ1='#ov*g>_2SOq xqҡQDN 8Rc}AM!vXt"ISa>DQf8|FfPY!`2IC 89UhD'ox. ^ Lqy8#9FBg7ʘ; z r H*]9b9W'I^} 饦4=qT +rt=Yn'AL (3v19y(Wde̅(āqiH0x?qS$OyNa(FzCRf&_ +%/WNOpwS LG8LO* L%KGo=CMaLތ٣'idDV9/^ &_Bi8٭,6 +F? A.F'}>Ĝ#{l'}0-W4`Q3Pg7N_A<]dL#:TPvrI^arD!Y7I%x>Vf},,rV&2l f);9(OD޿,XJ0I䏟>4z,Y`TW˿ո~.r3# R\mߎr 슄8ĤsB'A 4^>2ޙ R՜lJ+rnEﱈL$bg6a-0fyt~G,!,c{UIDO9i!z?J^iHχ 5x\chdv4<ɋvn֍7a\M0k1UG5-v"`҉CO: zŰ1FsV-zgovwC^ Xha솧i`WAyv)yUp/NOɟ˟9! &0!#'{ȊRq8 (?_7 e&PQM?ͲoP4"쓎^v#rbY)Ḿ$LPoN[r>@%_FEA  @!"65Mv>"'y7=U4`f5v_71'vx 턥a7 H]\a"]V9o]!wR*A&_? `stǢ` @"Ham?`VM<`?)}ZOg;NcDnj 41;iFT8DϏD'Ŀ'b;,`e3=QQhULOzuݲʱ$fwr$$Y_o5]094I$-9viQsu"awo5 F6 )^o{$k,`˕CLS-Dq'~$'AGSt8MqZΔ$q C.&_ɓ҈6IwzE h:x8| *+;GE'Z,mAd !jdTUhDE[x@J8EuII.((N_YF/0=Y;Oh’ɁZ.J7)Y&z(m[cC(\^V!)HV%fV{qIɖ(> ’I=$PLr2mou ju|s.)mcBF2t; zk;4 DuAc&'-ODyǏ,~1v.)%{6y缧M_[ ےX\%JdbDZ-ﲭ}Ip_=mI:ik7i$y['qӦf'M#.qQINw`\`}p\o;s?{YB m {QƼ˰ \&ɗ<-e4d/VR\ cl LJR۳R +m#8Sx7WU~& I܄= NҗVfRS{(x<qRt 47*b'qZ`Ad=O6IF~`0\&J,yq18M{4R(+-wԔt"o JibX s`DOFGnvX{z>{GTQ2tR>WR~Jivm{^(e᠖Z|1 07b"I[ݒx*$u*nH`{Ձ[kmQݷFbkd1s߼hZhbv\LGŔR(Yu"o=c 89Z.Ko KY1^kMQ +B\K8L[ +;EKf[ҚWrQʿyȎnG6/sfX0+=XL-bm&Tu? ߦJYX|Xe_w 2G7ZvN1B&f4 #ʫu6d)M'bͻE=P}T&O2(3-ybgK#)OV^ MWJK#PJ > 0.?Eyp#sF[VqMQδĔtSєsX}C4J5}9U_rRʯ+m7(є ߦ yo(-ѳ3iIF}2{Qbo20kttwoj|2B^ .we؞;&66[ҼXCL)tsADs.܁zNd?2[F!,3!Rǯ 쒚LC{)M'⓸sC>h/2A{/Vn?h./·AҜI]0׻bJ~2܄S9fYN' W2(R0zwiq{-XnL/lOʢe&eӎ@) L-ma N勵N#Ey a@ rcV27J龕HJ]ڂb L1bJ>9V;[V@~ᦌ.}/I(eUJ%^Kyz]B$]-p蒯^}?ȁbE v̷cI/Wò eLUX~1 Z2b;_{0)kw1\hTy90wTRmQKWg.A#L}JM$D|)ݪAӼ+=,Rb;Cu伶Jd߷Kjɤ$M + +g'+]a>۠|V_v+eJo}2vVc0WeE0.XǾe)OVXkkRY<5RKmls9+W{1 xqrSJuԽEPvVI(fH7:.9ܜ:32F1A/:} qHU;$?Z_bRzH2->z-h Hl@&ދ(DEh ?}j(b<"ƘN 9 IL Kˤ"WZ=;YE_/X67!_qgUK`; RK-jqv<̉&8{PXB!l2NJ˽rkYJ3œ5&2Y늠wGd-jۑQYxr"'+9'ypP_ՠ|1*]j./Fdp ҒA`;Cum82e))~~!ڞ;e;aCμLJb(9Pc1d^oZQ0343Yb9^z;K{1ƋFzLo!\&UddIł3KLFtj8%c;O8>&%zr9ɼ v^}b),ZP,0b 8Ė'xE"wC XT)2 =L 2G/֡ bLEe4y|g&Z&O^ b;{u%iCwے9IrYR>"$ojLBRv d2"ZC&OBfons r=wD Q*puR &g>9)ZI%N:I&5ދЪA+eGJ0(DL8<ӈw[xJ,=`;ߨR&uɬ@RSR\)OVXkR Ks="fWE/~BG6 tR6Ljbָښ-dQ/`v5b{&o`;C*'Ah(ICe^l/ZX|1=:R<|a s *^k2i.vVHDe{&_\Ldr2re75TtWj|tf)[4`d>GQQQhJP5VIHf&=XGEˤz/f-/WJ2dKo`d~HrZצZX Q{k^d# bָ.i/k͕4A)#[ ' Fes9d-n%}xi|SD ?<UJw+6b&ATҌ(6J̢~5no$40#%ů?l'sk {r{q(fGx]SJwѶ\l_׎OHhd,I*6[8 /k0d :IHQDD~gᗛnr`JGc@#\f`; *4S L:ZDVd`bvr,7K>y[]YP}Y)Ld轘:sh e JGw XUmN{-XƢ"XeznOXFY2+Y{&+!HͳP8%WRr$cg%Лb%Лm܈hM!,TZɟ*scI/}pșv΂ދYJ=QlA)s~6UFpT'RKC <$Ho=@@,0MJ/R.5W3(gZ,u.}RK?+eD)ܷ<; +ƀ=a.uW.wZp1* UJ?2#ViWپJd)mSe['CX=yy +zcB&3s rQUFQxFɾؽÕLP܁+hn'3ZLjyn.7 +ɀmAd.Wb`&XՠɾXr8F/M7UrOffT%DDgi)_t@v2IG|Lf$~e8S9FI}rU }g ?}M^g/"D'jBIB;S_5OBs +xs_.|%o!ƞUeaR)ELlnK^QTTFml]n)9O2iE2%OtK[o(O ݳ/H[0I%T턐4,E]-w:JJLrKF2EK3q+r'3.s kL &X6:4cW7XouiU`2<֯&;Ȥ@—qP_iɧGʼnB&s),!ڨ?G<-d2_r;y +-D-!EiѪA3iwUy[X6in d2"nSeS-֖>ZBn#%Y9d&b O1Ĕ-t/%V v endstream endobj 30 0 obj <>stream +dI"![xi n/9Őנbvi΃# Wj+R?Gs{A.w-up;i{h[adv[e,yl#uVA)uCI۳Њ7@3èIq΀ދFC~B?WUo!gb(e/sC&+&ov6Jrr?6xſO- ~}\)@PyGǔRC#IXLcT|#%AX%B2Yb!2 p _a@&b[(RQ{1u2YaIȤ HT.moAL2en@>.3)[ՠܧ?|B)SxsAȤB;HE^z}yM d-uVY-\j/]f9_AB>xR&ctRuk2iig zi5VwYl.+.i(6f[& Й~cJ)a˨Zm4g?/BX +dO;..]&f]^$_(LbR_5/Warj,QJL@&s ^RtN&`@A'|;|u /IuPѷzjB}!d2gKA^4nrK'r1mPs6rS <9I}g N!E(~SeEe]uZ$Vx8+d2W(W/S2%_khfOk ʹkۡSwA /Bq Rkkі k| d$|B zh$5SL:[}e6w' .Ox=C!t ]Lu3|j2Q +Np<>R?$vM%,U`5* ?QʘL 4dŠ&SAe^4w}3SJtc C-Ehy+z "{Q*7jwȇBLdd2nދF2!¯Æe)_҅ovKկBIGNgS*yZU%> @jd5jbH ?WʷHU*2X-jzf< #27yh9H _hhLPv@KE_eCFNRz*Al %Jq#oDl=k+VGU +ypO@p<נ`;mM{;L*ee+Ƀq/xI]4EKoo`iU}N^flk 4LZEeV:^bڨÔ_qVʶ +[%r>W߼ֺG5Pz+rp8dS5&߆C *d2({Y; +zk20BWJ0l52 CEE3'.4yj,RnmHׯi@/ukg*L+t[EEjZ싥R6F9ϒ֊hIJ:x=UrB+~{:iJ)*:~砪|qU-̙IV,ɜ Zg {!~62k.H0)LQ`aKNaX5!ěa{ۭ +Le&ODPRhD̢zMI|VM2mC r6@&MGyMBE#`UL! + _CcJa^rP + MTFΐK2 8X_by~݃ty 2iz{8얮I/qJ!{5f%((A)*J龕mh @eGBfd/c8c JyABRJ.*ͬL%v %h[&7Iw3=m!z/PPj *Rz +e>ɖ->RjUF+7*3AȤI!pYw.z^%XJ٨|>[,|OXJ!&視7:EJ}eM)i2 4%%NyTKpPGW؎98D \ V\#VVpR]>qp x2Y\Œ1Q0{ՠx!tL!(e7< ̥ z*̛y HLzL~cUL^4ᦂ+³*OynͿG|c#drPLphx2%W[_b!<@~_鮶^)Y_qynGVڞ5ѦA$*Е<d +{QAas#6~=X*nWN[aGs>nWz?*:;&dɤ[/OILt; l b [ꪲ6xL{ Rl'$JTW *!΀)b[[EaNK/z/ɕQ!UϝRs9=g™G/ +½2ǀiGnmSYgFowK9 VH.2YH@@PVCE?t׻#Is;/. +Zj z!` +%o?PX-LA,yƋ/ƔRPGhC'oFfm]QC_iU1p(%8$ +"UAN|Zj^?(%0\&LS< +Qxa7^eGӱ y_8?Y'eˬ2 +@&p쨲t $/D9- w0A9WʷaEYy5zZJf`/%ker(DLb"IދqM~qs^<#ѓ7oE-d'Z=b9ݢ[Ƞ%I H~*g-y㬷uh8Ӑ#ydUR&)3J7" n TLTXɓy2 ҂ ^4^mȱQGYo=#ן G7$V )+o27C46ǀ +CGNlC'=t29c{)J=V ;4s74 2I^Ӹ2 2f)^X<’lPH㎆K@su*M^[' 8{K/a_"Z.| ݀8  Q +0qYO>r?ͭFOqN)e[7]Ɓ ޿[C36Q LwLz/ +031-]i+\)ŔRc6K:ˬ*kB E|wE̦zCW"0 VjU76g/K9mt=j{JpdC轨3WZ_ټJYc%JiS/>k2iкoV~C>'S لom`fbҡ(eH}RWJ-]dMچX?-DwSTؐ&Fe?A&A!3,x @8d T ґVASzm2e)CNڤ2Y Q+8*J LXc=BAEpSg0&@PyS'ԶMJ-fDB놣 C)PYf˽҃yuOfBT)UVz}k‹IޢB)I9]nRki蒯+dʷgf ]TY`t$QPQ`%(yv>@DeJdr(V ' Q ԋNyU}47:R(ޤ[듂(Ў簝PJ@<39$h"^L1KMXgdD)OR|YFL";eR|>IPB&uA'K\*Fb "7f C)A^Q6/Z9v Z5ug)OrTaj5zg^? ">M2 d dFE}J}砪J+ Y A$MJ HLA&A‹Hc[/&XELDK,3 ]uC܈"60Km h[`L! @Eݹ&/n6xqur8A(cuk gZKA$E!JJє锒akZe%M">Bj렔 G2)Vo=ދB/8Zs="JI>J)Z$ '7>o= /A轨7\l^a%2٢rDJiXzL$Shf,y4^4lZ;Nº#XFd|/ +B) L~>zuM +Q@'mxMzp~;17j!+e?ǻX]J(%2LB&A%oi ~? _ +; x+xhA}jd "_ L AE}$`~9,S؂$BnXAep^\|7@ qa;cْl4!I>}"^)F5cd$0轨 ]vV{}AHI~ށB%>/³DCGsA(bSD ø'V~[zfJYl)J;dP5z$f +`2Ȑ:{QKޗmd^mvI$Bf)+XV B=]UOc8{E2ug_Qa@Db]2Đ=’>uǁ16W_AIe)&?L^.c`0'm^DwX(Z:fh]J(%ȘD `h!O-I JAD){O"RzD)y!f.27B/y.\o>熞."O'_n*TYJ"4~gC&P8\kIFoCXPͻ >B +ɣhi S^2 +^T Z|2 .<#ҩK י 1TPT.\mѓX3AkYR"K wR&* +@v!uaCFE=ˣ{@d'x+RB) `{&c p!>9Ʃ ʍp7QPR"K d[)U!.$XΜ Z5HFG- Îy$x`@dhJ(Z "~oyIg$[Pk;]Lz/v&g_9nDD(N)$bNKW?'?)7 DMtpCJdLs=m҄h{:RfW_y>x ᓈM)*uUwovW[*kZaM|tt:ˁ!Ň7L H qf7o'tь1QG/C&rI?h ~z ]F1sD[-=!_>({?cO;{OS⟷ٿl_ǵ?K6Y[+mCL T9Ĝ9 vfZy*čmZg;۠@&rw{d(3*]rlj`kvu:#/9ۣ +yA(E?SãZP0HK|E;.T9N4PN2'n;S_pxsJGlyо$|O/Y_8Ĩۻ!UKf(VK2މ:p`̱؍ȝ`295F t9g,wE)A7uQ2B#q L>ڜ:?!1+m>U[n $Q!^4^Vy l +O"r&OxVW#%!8ΤTdՙ;gxL .ϳa4܄g;s{/b;sy +fN '}|6&f<%?\AA)^{Qc$m/h^JUJ$"ѣ= +&HQ (D c2fJIl{f&.v {/cfT J-fR,DHС8ޭD^r +(@(3dU 'mF>mDB6r< OQ +NBXNӍvAf^◯4Xn$"u:'ȋ9 `ne!FՠtIZ>ȍ6H]2v[ǯ 7aA'NtEF۔ ҕᐳ?Z|2DX"%sB轘ZؔaBnI'SA/\"HfQH!b#AȑD :>|~SeE?L@n%sB{/6[mR_LA2Aּ>'sRz/fX'P%ּ{wU$BPwXHL22NGDڴ6bPfMN'I%z/<9<'J7O"DEIWe.q.ALK!~ͮ>?-"{.8KQ/D9-a'=:Q5(9Xkͮޠ`c<>2-1r`v %Ʉf,y'bsQd|h[$H`vY!JL-]eU05[|+xh@d=q&:RyẌދ~ +] |2c"B+VSrFT =~'_z7"_qˁ$Ћ-x0=|fGg3g[:9"F-[vD cd"6_R[ ⓈX!@O.`%CߤyX'{]}srtᓈlA 2AW^L^ÒwZw. k-y_9>Z6EM@_& Yֆ#9i>['? '_oe3<| G$`&}ދI}2ڊKE5oA>?܍݈l/-!&z,#@ ŠB޹ߧXIQ,y*HLePu ~ֶ>R͓KI`8hDi0ƃ&C|a~QQ|d1?~-pF 7`a +C9>yaAQQ'擣I%&`:z\kp(2 U`Wh1wvQ Iث] Yc 2Ҍ"iщ$z/$2I9`qVCYa({QE!4nO:6zɺ"(%¸.z(ވAVA-JZW$/~zǯ']:ƻt$6ZLfSJ ++f>++=R)b!:& =$/(; jQ0~*g /3Ƌ?BF'IWÂI<pb9Ή''KNvDP)( +/D)/AxPhs|ȂE jkkc)J,y# tqD; +(Hf UC! n|'$B`#[F" 0-#NNKMX򦇻ɏOOnuvQ"E5^g=q7:nI +.(7v]'Kag%OmJ,E DDHN] '轘ە.OktwBw1E b)J$B`>GUsr +/D LZ5(,_D|aM%{֮-^)JҍnǺA${/TNLtK&A'S.}V9ةmhA (:,"_> r ZM"ÇC.]u{ӝwX^(n9*@rH/yTNݒ:%VչHw9奄keQJphAwh$hrԢ {q(S\C}Cvw-PG|(yys(?- cQ-@E @vދs#=JOtA>D>X:wX9틉F޹(.Jh!AqQ;^e'AN%a(`;Ui0O},șzѣQHTD8qjn;ݎwQe{?߳՛W >(.JJ`m\n'w7OnS1i:f LQNp/WIp|+= DNA'AnB^vP4=xѠ+2.rWis,Z_}{+֊D."ʘ(A;K2`}6pU՛]+/Dɱ.Gd;Y@-J7hԿ*d_D?١]IyM:oR 2ɍ0vot~O/YSu7/[U)Z'{sh-2|Rwrsv0VB읏!5/!=?`},OFR妍pg8C1C3N;G/tw++dx{[rIo}ѺMwx*$&Z_ v'D$l?m1A^v9iw(5lrszϯve_^i|.OB~o-am7v!:E#&W7^2P$B2܄S)@~JC\8I_J^3~R΁E nwњWoVܴv(|Uv& .rԢ4:/Z'V? =v9w(qhe_cqğVHaMO"%aޮ`)YؒǩY$ g[/VFaŵ.w˽/yrn>%"S<9_z:$݄,_E>^9yr<=UWVܹr*VB{wϧ$RقXJ7k&zy:KIcZeSr#:@>B.|ON^5(,_ﴝqC<82v`t۳uKrO.x|)JLv?oR"/#9 -Ǜfm7vMs#َ>A%aϤ$רXN^ww8~#KY9WhsspbjZ +Q.^UA|_F1m|^gw  .51UYop1 8C@/T!3ใ,3sߙDyRJ#ڴn-N$S9IltD~G!#t'*z8"z;0ɁkKvr[Jv׬>'z&. hxL!yuK%kK W%o$UZ%2$U"J9,#X 9k/p]<^'v$`g Gί?yb<[O]w(*~*$/D DLlHu9>~ThJIiapIi6 XN3rO.())%>YU3ݒJELIƵ׏.]f-cO]W΢uItC':e' JiTU6) ~"Azkm +Do}v:zK;%l<3{?a܂;q0qd1v'o+nj^\H|rŲ'){' Jߊ:$s.yRDL~ o{f:I 薆CRvŃ䒵sٽUغg)J2T߼/br eyؖuU5| E]kK'q*1m|Э$0{)=C*Ҡ;'s婭6)2BI>4Xk҉l<1tHS\SM _K=?yFYJzʓ= 4>-ˏ֎F9VE ݵ2z*'>vNrD^O33U)\%-.wՠp19ΎH8G#NfH:}qmj% %}O^xcgVm[donv\iEh>y1WntYjuެ֖y;o(%"GOr|G 0;+e,sDrr˻lڅ}47.޾i?=0%ӆ#%IX~Ľ/%ygqͫk.V֥dME.9;9`䊻7\^vὼ,ha@d=qɉoE)9VYhj{vK}bͫnsX2v' >yQŒwwF’ ;'w]z^'/U=#2"4F E,_G}r{vʛ|YXd,E ,`e'?`gp>IP0̡1ˬ2Z.=A${Kuh˪{Ui/>~m?3$%ow_Pg{/>ӧڲ;B/*uawjnqƋk\M߄7Y7.^$KQA) <ި'KIPr4Ѝn^*/=r֝KjM$Nt;yr%~[X"q7\pǽUM{!}޺ ?^|<6IIO/!>x[( OhA vg}߿dR YVװC:/km,L&T/yNnz}?=Wo_[GnϼcRskͮ3 Ƕ_pOBɫ/$_y %NyjvSDsPȤ1ay9i@IbP }2wO&;o(KX@Lw r*E=@T+HQ&,yg~]lE7 %Cgi k-c ?[n涒 +{|ɛɹy[( -xO9MN3d+UjdR>Q} L#ח6>OrU̖O'$fRtQ\1ٳC^gʋJ%|[(O78_z'qVҺ.%o' ŒJ/yʺ/b e!+;9 >fZُ:ـ(}V'}&)dڲw[v4$1'z2h|jK'^j W>=ms邞JKn1% W|]]]kI( ){nxy'JׁUxa-?q6sJs՛}\蘓dpE?і{u鲵U?$߹i ;Ҫ;<9WArCފ7.]_}r٪: W%ɁOVNZLG'MW'|{ڍ_~hY.IZ [(vխ]pOէח#' 'y  +dMNrOM RriD{LmnNi-^_ɞ9irǦ5KI1d +s]FW[뺅Owg|;GWJv՛w^'ɗ+O^[v]\zw _'(ĉ]H^P&d0B)h+p)^ŅRiIIfU:#e)wmZsr=Ut;ؠ ;c9ejx'o]ᓅp  <3W:U-юo_'XQY jtҘz@17).~]T4r_5|rݢLɥk Iz˔r8w˖Shix' & Mt>~^A1`Ԭgyw~7вTaM2>EIhw0V2M34_e9_VB}Z|τ_)Y͓wK!G葜-T,\U]rOz}?e'$cRjO*̃kܯ=|ÕL=t#$Rn(u0>Sd>]Rzy'D7Byjnh.ge4">w׬zc}ɹw̽ Wo]M|8$?ûxO瓨Biэ!'gGS +; =7]T[;sD;6!yj[tr46Oj0%v6<9߽,!8v;α}>4{G:l+xn\n$vNI(^mƻw%yG2]PϬrW%%)rY}k ¦N9 }mtBˇo/e;E˖a-ϫHQ&`Uu[#|㛏.k]E&H/,6ٙzᏎ]&>ɻpL 4l6&|x> @dC)cЦ*N38m{V[͛L_)YrsYϞUNز$n{OC݄2܄I!)O^ +Gd~8!-[dR.amߴG7O֧?H.79Yhuk=MHO#޼7|@F=vN>RA0{U.G~ۜm+O8} ݸ_^G/hvCګϒtwx'L'ݱd?}V8OHq'Mך]g]eeOuUuZ~2"|NNm{(:eVy9"j:~]zsjks#|B_OΟeQߍIw{R7'gɛX_GcdLG,+h+h'ᐳ|'^#>95B ,=HN#Vh!t嬉B@ qp5>΋d܄N?^{zxRgSy2')o7T0eAMkheay? 6E.9dp-wT\r٪ū*z7| %'[l7:e$z޸*i2 IwD#K(_w[L Ө'}m= s覸]lZD jPJǣN>_y |dysC |r5'yI[&mN3Ar0ՅI Х{:N햾,wkǷc{ZWۥ솮z8z yKz܌g)wоE+0BF3 ޵]}džr^2tg˫$R +snt$z,`20Y],s$u{n(z9UR29Ds`Fn-38%$UB<ޒOoup]ЊQˉO.>O8ƻ\UpvCjT=ϞQ:z6.ٕrOwU8-Z;ZҾvO~ yGOMw^Q6]J|>Z_}R+i4iKuKF x8<$b_zpyew2nZo_[.F(:yeisq;L1;=êAp;bqܘqի{|i/X|ɚ;hz>YA/5yc&Q>^~GPM''v|.-0?Yk't6זm Rht^oRu@@:lQV\jcc++ޅ#agO d@vnp[K|rC4ҵf:#Z\EƑsJ[7g:Y0IV׷.}rmO.Zᓱ/]k@hjLo@iqQ83m)pTiˠ(oXGyrcww`ѥf3VBpœ&>rH]E|I? ~ R&Xncs{i9lkB@f]ݞ >P>R+x. +3$=dR\ow֭D,}v35M?}io*>L)4emWmeO<?1D wD9[Bdr㧝|%ҺG/(“w4/&mj϶P_Kܛ{DIđJɋ ;,#}<}c-;˽^MIGLن_mx]|^AYgU(/7K's{7-JXϪWZy6 wGCu:&&ֻ9C ^w-gI]ADnđssfTss{+K=|3eI̔~ܧqc՛}>x+HQ}wt eK'{K;\tly'y& +$?x{Uq"⓷Ts\h'ᓦ +vny$id$יU,yIIb֥|%Z B;t0po(6=R0a_͓aY]LtKGY>~#x,+mD##/(29\?ڡ:T)>`Jzgu•uIF$j;RrUNBOИw>ZSY^mIΦs&R9qsLj8>IAŒV4Z 8_yEk$ndU9'LMh\JrA3#!zyO5]vW{ʻ?|ӻ@Hb,$BIYa=K:-rCȞ| F)pRZ $l e{;a{XdK:H|ر-{ށZkA}@&N`#")z* +K+}r!y1~jvT<\R3Nrʇ7(e\ab\ ]<+y=~E59N'#$ǢHiΎؕ-[K~yuLF͢~rE?bzr!_Гqxg+682udshcR_<ԾF@<~{׍W)Ma?X2ӓ9֟5zux{RÚ;/UOuEOLI~Q#9F8{H]vZz]{;'~A v%JƏv@ιAX}61rzjM' =V3>C oь} $One<ڷi ?LħCvˑ渕&fᚳ\u'g]~Г~Г^|mW$a&IӨ,ŷ,Z %8Dž%Xӵ{Oގ28䏝y֟,6G %R2pM4cɏoť擌fd$hLES>M]<7PmHazr›]xً|AO:~;x,E3YRͤnROɘ{~-m$/vRԀ!7kiݝW,o&/b-?Sl (+pfޯOsm]rAG aqR!I9%yUi9?cE?1=9g$-R%54$%{>].0nR3֘s_Z6/Xwwr9;XMo|۳W$qbs̰ҔN%, ֑5v63ThɉД]k1=k d\-i9}N\㕋;JN[HX2+g᜝w t8bR^&0?<(ۘC84o+֟26p +՜}`zr߽go[y'RS%rHAyg3=y_O + SOZ-&er"3qpΒj/Wbrә)g}v K.Ȼ_q4[M$mi-?W2*5rd[mA{INl CAr_| of$h_{:EGf(lю?ŋ) =?Kd=} +:qp $e$܆'3՞,4e?/䦴f더cxsSޓ_~ۢkqz.ϪKQEy͢VĜɵcyr'> s *[*tܭ];nLv8D:ӗR:οzhx7qamI:fp8F2C #)=\a:}=".l_q׌7Ef(L9!N1>H؟ k' } T=IC_*_n4NX΢4nV[ozœ]bM/_RIDեRCgam0<()zEc\xc"QE_jY!x.oQl,[h~ GTr,MO)KK{M_|0 +ԓk"I/>Y_&bsIFCc6'kˮB6's^]>?xW^uݍL}΅#k/Ĝ_|?t̾ $%+U#SN|ahs[<[c"iJNd-8Uz|YLO۠'lJUd%p4]Q-OFr3u- 3yݷ_~pͣi[-k^ +)w:+v[mXjv׬uM!]5k\tdb'od\3'S]xm&=I'/6l9"vU`s_`(,UޤK=a">7龋ux 7𽠾ˆ۲Rd#*(Ԍȝ~"Kb!Rލa'(e;vQ v6EڢkL6gi31 =vܩTvz#'l+))坩ƶ.Rs]=w/k.1Q{iP+jx!J&<ʖP"UcQG:=ec:#ВVĸ9\&&'EX]xRbi: p\~oW,xbzc8iqN-ƥ eÜiWb]6gL,ޭ}'E{…(t2ڴbuəl$%{% +Ye&$0-. ՙR(5=񇋖n~^reSՖ/ݿSbXʘ;JG~Rnn!*Wz +>ݲ-Wtk[y… 'x[F tUM7 տ-UH=3z\;/榠dz7_jY+Kv j[G-TjʓG=>LQOx4v0rf]~ Г^\O >- HR2i4/ iKH CuAks}`~=Ϋn麋Fi.Bk^J#D)'-l Mj,8>ޛURXR alϕd`GB_~J]zE'b8N\|mƎoyp2^/ l") hv|;D<ֿ!%Y9={.͝W_cLc6w*&rW}-l#1#zW /MRTGOsT!6'tSL;HJ P_I[>&I=1 3~qOnYtᕷ*/#i/6zf'.)saitG1Zapr:J[V2{._N]ӌNafY-nCR +!mYHyϸ:(g_i䤾~`O.+̳y=97۳Vʟ/qU-OvrxHyR9>A,adla's DfJT'.|/V 8 @b)oԵ-R귛T*w/ +/9;^P[|m7]pQxn_KerZf@I_Xцf]9>pQs3Qv=8Ɉt:-IbΓ\]J?SVaD*{Ba>L >x_Oޯ/Wzu|^Jv22p7gT$<;!sssewJI&eHO't1[$"$6K<7S0/yk=|)L/>u>d~/H9b/=Qb#;0yWs'ceZ?bz =%R?I|BX))[8? >Fy) Y嵲v:Cl8;!- !¸!Zvnn#=g^"$'žx`ҧk4[PB){ ,U;"0s9$Y8Mz Nc6>1HjGSHU^6m M&)Wb ax2;:ՆIN\OX-\zҩ'Oj` +CRVT?גPUtR&,r6M2]i +A4$¯&jޖM]~bv/|0`b/) >cr^Hㅲ<4SJTȍdZrJGN +{3q'xdAj?D4`Tb,)Rޱ@iΚKt8+BDD)C9bf2 aI]kϡd.eJzҹX:Uڤx@kIV"ȉX } g !F)2xcN^C' n5[Itkb0!rŠX?đ:% ] +ER!]y%|!ԓ-YLOj zҁ`= IJ3>!|IӤ*+)AȰ;{cz~UxGtќ$') =ŋ'7Hy⒘Izڞ k-6 +(|%lɌd21>ȦZa=Xⶓr$!A`|LQ}6(D2=XD1"9_74!뭹| 5'ܪhÁtM.1:.Pq|yr"zsY3q;C;ٹnʏ7j,'vﬓ%ࣰL$XN>{e6wIٝ/W`tz!S=I&&i2%gBL:j8'K>n;i1p(іJفs}8eht j)^%Dq{0$w#et⨘"Q;⃆̗-#pH!"f"VCԓM*;kKLOX =d'-lS&ڒ)oS0zMas|x94qK ag')V?zanQѓ"i&"m$$`*YWnGRV!?۸Փ5IG8FpH%IJڋ9{`6X>FrrȐƑG9L;HX⇾+p*==ԗl6!!)ocw7$ex?5M._HSf;%cdVN1U +-O` D×)oBctrEr0X~<!uŁTG*zKA2g@Tx4C&4}iX >^HS0yK<(%-[rG(KuVhK$bn@F3O<8Y0_nn$eXSJ&)VCIad?'})Fdz K8|`4 w9ֿ)0LεdV} II>箱|Ιl5DҎA ?s/,N˃['BDTR5R޶rsάZ%MsDڔ'2IONt +E@ !I iQVr; z +f )ÅI,y,S/]F xvyR,NI/ 6uLQRv3Jk߃vŔbcW^lӹeI',ccMRSOL+rߦDeO\vSJuaQvx&AbآOMJmMPW~ b{N%ct؟c:#SJ&|x˜ew&9'֧`3LT)N%]d?՜1ĶRlK:=RŊ(%'әbcՓ V?KI.5f-%ӖQQ]OdX =i;Ji?v8B1b&Jy< 8 6, IBRvJ~sn͚כQc[Idda8-o~#۹[Pp$F)w pg:b8F2Dy(S;;`d %ӓ n)绡'MPHE8B:-aҥ+_nY2Ҝ1)6f=w]M]C;ctIiJ_8qTKm\OR!NhŃ%W<  Y !)'snaMcadiľ(HmqP-Ƒ'LPiΒc&)C|Η!|vcDт}v ՗AfA0΅M KR"址xё*$x|z5E)k6o ;ٙIiy|NI,A_28xcIS`+$6ct )'DRv+wʗ}>iwl}1Q(8Q%e[mF|21:dCRb ._bz)S|qRRn@2RRzy)< eGd\$w ;&SI&D|.˟.Fg7 ER./: IV\]=LR SʕRO} b\p"3TavZxX:L7 1"-A<ir˟?jHK&͍ٖQe:Ob8n8X|G n3Q}(-)ezCbE+ =)ГvXv2N8C`;{ 4Q#$ec`׮VI?cp5׵TSI1@!$e[eH )4tKonʄћSTӇy{xحtyR T2XV.16hA|ԐvTEs$_׮;Dֻs:X ۣ5dh|N|I@"bZr\atѳÔ2WfђV:sS|:=lғaHP=dnM",J,I& Dy$ĻDȕZlw0=ٜ5ꪡg']\O +?L'EoݪҤR1/v@k%"JsMd Bz23ГV.cK"8F@\n@B䟝?fH+V/XBSjHɄ%og~*̛cG7$eH;/B+K$7Q:qGۺi@pcz4䱇2uSZ[F}[.O##~ +(с >~:w4ۭ4VHv!#ȕu~TB:WC0yY6ƍޯ- a),EQ}A\{u3X,cO3:qP&p.ECcDF[8׵fQUdWre3<!M׫빑 0­?(2s(rTvh"Pz31œ]t^lBg7yP:?V'ХڵrJ^94d2W}LB- 9ĄhšI9Qjя=iڱ?6A`4h4װ(P.W2d19,8fXy>J *,-P 6Fg$;z< =i3q +/v >Nȓ\FxGjTK+\cUE"&˖ӧbr˅b>l7$ _;D=i:/86 lM#ʖ5dk(ן +'X2 =tcHʘU)/t5Г,G3Q'/?#ȺuRJi*r +208 Zvcq;P{ܖhHNH|Q.P{$5ۡ'X|,wQd(U4|yU!RN grztGo!̾>FQQ6{Lmh/B$zLyԨʓ[dzkȣ<8ŌXR2 Wws"!HQa)xlR6zrI+0`-ovm*f Y6DVCASXrq"KmмBdķonZ!&!)chSc& fuMNtBKU%o4+#?$\`Xf;:S &)D%GmNgߝ  oHX,c#\yGPUxn9/MV 9$& N/,uUD,Otx_)4 _4®6B虐^O=2mo15~"bTEfu+RΚ$e䨪ɽDv$娜*%UN@%'%d~HUTٞý"26UIbҬ3.D5΅ё#7jQJHh-ݤX| !#|*rζ\C'WOf²1:plPrTx-9!{ARFaqk $a9/mYU֥' NNq%r;zw]crߐ#9ɢR>͉zmG5|\cjHϮ|UE'8#89X2aM,с7$e(sMt @' te$"WKe\Bgblq1%„eGNc?BRF-Uq*?V8ZȈ@U9Sn ڦؚ굲M<"*"Yr{.E1K)tHIʶ\ߑM LR҉$%1Ffg = ")UiTYΫ"m_L/q]TzNң}gAeԟȊdҤ9Yj )xd4 @4̓OR:rlD~㎕Ԭ#kG˅_tU?nt<JH@|9ߔCX]7=- Hd$2@iUK%pr42 +Nkaޜ wtӑG{*"JH ڨ=7]!~Q|E4ל2 NԒ){S] vkaX +w|@D)ྔ EXa/n_ww+ %7EsMOʽ"k\b|v\jH{k+m7]Ba)(b;1 rJhtH NH^~g"t@gtͺ"P`L,s(Z"/"2FrEʗ*{HBJ9r@1)I&r=-T:r%Yulir2rp"4]q c ,}Ƅe[&tEbj)WߘڱX$UtǘFUdWܚ%{7"H]CǗJui Z90%31:"J~71I)$PH6Al >6d~DsMwܖ#7ڵrٲēCzrj5K/,ѹcB2Uߖk©֖RP){ nVSL ”2g=کMp`W*2Ө,[FUS+1@aNˈIGtې5>CD5$$xp4@|77UGk֐*>?oY0wt3op*ct%%8)jϹ_J%⺺7 !#-#QU4㐉7q(8zM";z4љD[ph;@R%Q>tXFg7fW]۲"@:;zb=.M]O )H;Q SCҏS,$崄 z +]Œ,h3*4g*uʕM?|22zrr3nXAXF|yrct )3zKVRd@4$%PFtvOґ+5ilMR)D#c)&/Ӡ'h 2D1:}CR{OBy0M)SVݬbF'QadNԒ%{7H+TEZ'+WCL:!wtP/74vo$f蜄I񊐑=T٘]+/JgCCZ#&J>LOt*kpGGd̛H!HR ~D*1Ka;DXٕ'7TY "mk &Ē \%}d'I)TÞ?<VRHhGQ;#9B@@m"`9Pa6hcK!#mXb;G)B|(bup%JRDkR,:;+{ +c"P@mH:!'|dol44Q_pŹll$%fb|&LWBpCEB(BcNRe)(r4{R-??DoR +6XHb +7>psߐZ_[w7pot F.+$%3a)Z‡س?Ii&aϦz&JGN +RDtl9-ǗJui B W;zڞ'rbN(}ߦe[XT+>%)'>Ctv $zr5+1 ,(jpGOΝn!쀤,t)le^3VV7(V d2LWS +oɦb=2eGȹo!;=ֿf|D܂sڱ8$%G|['ћc Uk0]DCXf(MjKڑ&j +q$AE)icl.KR[93v:(09=N(a a5=A1:!)}|7]z`E)uMГD Ǟuwǘ)/,*TbCVB%"J)jxY<5t=)AM4ހ;'xKX&;zoHJX*,$ֹ&QUt &1Ise„ew<ݜ-CRžݤBxX\{u٧k9Dvy+%'a٨]=ct>ޖᏐ'y(|ƬJ Lc@zZ X;6ğ;zW>hHPzF"'EʻQwJ (*09&"8i^ 3΄esXct 5GIxo4/K]$CRFvhQmMA䱥u64-CjaYFbaܷ[*,蜾 '9ww&]uu*W7'zmU,b%ctIDep6Pk$AR[hY?)I2§{ L#,&J>TON(ѳ@8 IY,QHJRnw7d'2%$%QEטyWjIHM,l^Cz3%hN63sBt!ʦtڹD]]ؚz01ɔ9 sGRpG\L sߐZʮM3I9YR 5 ]9Ĉ5͉^ 1 +#[=JI痔Es˧+m<ٍ7ǛTT,[O9OLaIK*ZEB(os4%evoaD٦Emf +BO +N/k<8V;H;-box;cM9\R,{Cs*)rȈgbEL'b' @PKþ~"Kj)rD>U8JNi|ϭt)095ټôL 8AXW4pVBC竍Z?a9XROWr TyPpr+D z KX\GSQk&)^ҁҘ+9@LyԮ~`~~{G,;ƴ +RZ:@ʗ2i3'Rܢl'1.My(MTRQD6,,{ + J&>(ٝ<7 LΗ|"&m@7cᱯ;:s.k)~@Jg:s)iϲO7Ę5(q*WPq;zG5o[ĉvZk6>sJ*dǗIuiN6fKx3X-̄nhct )$>/Z%L#J=% AX@} +djx0yM,^C +Z51zlxMYjO2I0 nP5[/nsT$qXEzLXR}(f&@w,Y1:2Ιvx2̚Fj8ft尽֛j @"S&tE'$,O bNPF)F`b Kl%QEB !J,ibdz'"NٱsGSsq/)ꑬzy $o7)lvSS4[*y e9Ah>\49I7>`T<8u-9SE6=0"-G\@yp|D(sL~.g.]{~ig$ϓ$CSR_OWZՇ.K♜GRk7>`\M,~sKjEҖ}&A~^9* H_r,M`}Ҧ\$]7/y$'%f%͘4+INNv%%%''=Bқ-}J~wKi7'}i%Iuz3<4̙$# 'q CLUY9kR`D =H%Sp2 f*Dږtˑ5ctH*m&)}TUF&#WuJ^)6oHsA%s$9=$ ]$=&' IWvPq6WVًRų<LZ$g=0f-ct!)n>sA{d|4#?uE M>?eJ:팤{=$q&ӣPRh"r:6Jݜ1I #J@}\,{stE6 D*"ЅBX&# Fhd#S9t{]uys2i3$x3gq +[@*m,!!)k2)ɮty`=V^9z==$qׯ-s5DSS3Uo4k)@ЯL$e[pR׼ԤY3I1HdV@"9CPʑE]XUũ9{Se] pb2pGL19'!) B O-k\7Od&iIO!]"ldh( ȱ5*T:Oygna9O`*Wa!ч @ pGRle`IJQ#%&KR9#)ifpr1tU)%uAc! tzmSpr$@M,CQ\ +:XYy<(aTΚYUTC>]aamoL0l=WΗC,R:]uɹNb" XjkmT8 +x;З׌<^g +3-%'+bI Ocz7/z s" 8 +eCeܙOCBRX7'(47+AARR4)D QN\+"&˖>ωlݣv]|J|SC2b2PUx TFt:mg큱96&UEp)4BdC^LԐ.yӒ>Ywf&g']$OOp%PevGLQFoZ1{u+eTN)Co1ŘcrCZRÆ{¾[Lff]XWf˭UO>;.N3P$tlF$%Rؒtl}{sj֡`0*#"_4%s8÷no\4* x=]X_xY}Gn}⮚g~~mՎ]*?Yew?7>sf9]_U&%ERLӞvE/^:\!rDј[?a=ӍO2H&= .+]Tϫ꿯 jqŁ';?|^;ށOSߛ;g3'Bqƙ7o^_R&Tm54G&t тg,eiC4jR7<12Ÿ&٥ϯz1}8tez7roEү`8?㓯3=yG8HwJ3f`cJ*WRR|ECZ E?E)[\PWr%SV?jWK*wod?B/{?yS72XA>I(o<90*_Г#LΣ"&Ry?S/\\m38B7* 3)udC1F*eϳҋ5}j[⻯EO䦙n2E&&?@r 8P  D4B^] + 9{JOkQtp[{otS9QKѥ%Wmnc|a1}~I?.R(h|ӈ1qM T⨢qzr*zRmQdg>It`wt&KP9 @ %1-_ܒ7k.1/tm?}'cU[D4oDޠL4Qc,3A!''-&h'ڱvk06XkrDQ㊢?Z" mћsn}_+ן-?nR}Tx$| ]#|QՓwrct͗+c9MW\O LF:1Ec`7gN\&J|cw/;2I.xgЎw^);"Ȥch*bX# [rE]Lg* 7/r?i%&FpbQþ;/hHF3ȾxV<?/n"J?uOWToPR[ɾݝ+^EI1"Q8vE#X"DI|S +I ѵnrKIIt$A2}f.1~ϐ}?V?jkJybU-7Js:#Ì*'''&]Ib$& J}.;krSPE е굱.^6Hwwk~iH)i&)%}CT<@Yzy8?o>WK*1gRF뵜GOF|%R޾ )S5nT*tEpF䣇:_Fݦ}7͞[_ti]E_T?lxŁH1!Ҽ=^Z|+hӞ;=9m4ʢդ>ܚ2(e:/1n;Ⱦ{:=/z*y9ϛ;>o~}ɕ^<@+_ίܵz^|#|!n&ΗC"a-uFFhz2fzR@LzrB +uyZ]ZM//_!!8 ĈӢcJ'u֞EF/L.\L~r)?[+Tݤb}B/:F3;n 'CII̙VI!S9ҨHy &Qw׮mD!nehiӾ{ 1fCڇoOQ6wŕ{-凶3h +F}7׊12`r'Gד<8iԍw(ADڊUֿAG+qnҐ^=>bɾ٥U3^ʭܽr#\i=E'btГɤ$mtkd˧k)V_pa>|֛bKypm <z3"ơQVTzaߝ5duQ+Tqi*e|kk;/IQ}y(Q@OXV9.,v^+>p79gr 'XÌÊO/<%=빠>K>Ú'Ỹ4%beR/]R{^)fϗkݣ8x#%@Oɘ{Nx}$#J1 T"hrNՒDHކ+cwB݅mی_YʗWzurh>nz #;_.@O(.Zў= y$%'Ip)b:FKz۴烈\@jXK(+؝5o˫vdSˁ'i[[~J&$ڥH#OIΗ"FEU'O{zROZs$΃LΗM[LWOi}ƾ1nEsL^./*C1>gڮWO#Y4MӘ1qԎiU@OaL+9s+9)>(p~Iɚ21aV@#zJG~[Ӫ^̭hzWihӾh)f ldX2U&&aC9MW\.{rcLOa5["w‹#$_)ؽbxh;|ڐ}wn@O +=I[gi)R8[X9w:qp 3)ΚC 6./O)F`̮o{yJE0c"eGFaD#W'[K[wGZ)>r t9,s +Np6fǴ᫓L gOd41 ܌uȏbƚ':̗ݛ+Њd߽@Q2}T"ӍΛ?"t#D#H4' IxWNWK+p"V^9441i/BE3D#9xg(+[tQݖjq?ɾ{cc|{XRF|žc_0Ip=$'IlgK%(&hrOx50ׂ:_w0WafzsΣΗҫ6PiT[jq?Vx6(dC+ /ef Id=ެ%mŤX\Rzr_~ +Zrp4d_mY84%Ⱦ1?NuWm?}W4e&+woaruK!ďߡ"aݦMtYO*\O +fk=]Q|fdΤ>}s)(b |O+%{7|g~}Eu>zgyWF釩Kŷ_,{û@Q1q}7G^h $A5hPϮdtnr|4]qTfJimDڟ)ica\wƜeǸڇoyU/fS$TAn}hӥEq/! 'x}_o;Nǯch~ؕѓ⭩MvS 9D Ilyy5${I +N +2S9(!@FNMe9+{/?տ޵탽{˴e$Ax_ftܱXʙݐACHt'2S:rR ;/Yϫkzj~NmWT{ٽ=;{>:=hO_{oʾ{ %Dѓq4Ez3x%$Q+pW!OdRF{zz +:CH_sT%[dzwogb_W>O=>ܷ/~. .{=Qՠy +>β9K>]>]񺵆tͷn٨tv55mww]L[ncb?:^`û?{}q{=ɐ,8~ %vwc9on{nƔ$:%6rH=(^ +tas۶mzJ*xo^O$/?9"mvfƙ,L|xj$w,t0y(`<Sl*=i7mR5Q[6+|p]t|o5|=*L^nLv]K=LJkKדX%% 1 H4-WDGgYuY>tCA% JI9s`CA%I pi&#'/1~ZzEwbWm| }iûۻ?}KCaf%#-Qu @"zm8A]2޼T 5bvS_Í3J|i|_.VPΠvCGk/nP*|P8Ӗo*BG+U$')_rGM_OC:FWH`/6jsfsa_>| ?_5 {^`J|^h@XRd?ط/$ՓB:2t͚ۖ%;WqM]QA2 +#@'EbFFt pAVnjAzxꞭb0Qy@OO"m}駟~}/*s5_rǍW7XM>A|˟^d ~E:{k/=c5ײԳ{ks&8,/owk,{gN6w } g+] _EU3Qkmg4]i}Cθ_5 =sGv=oW{ {ÃlRݢ8ό KOU8g<(>>'[w[Հ @8 PB +%@Bc܀GI@dl5wclq{l|3wSI73=!}m#:eN}129!1wY\tW]=>k/Lzfx{KGl!\GL 7sǥkbny~D˨OoDr7D-V+_~gRD 0-$nXsH= ,zY,7.6)'^3'ŬƊynn, XX[5IY.+G:՛usG?᧿O?r޿Lsy\W!_̇\VUz9顱&%DBVaUOQ𧼇Uxѕ% +cT FJbSi }E3s F+?(\4T2i!nxKXXm$ E|rMƭxt<0μvʼnOg̷x*Z0}1#5v+1g2hNfeх;<5s%;zQ'qkJ.i՞ +#Uրtuy9{gQΛrLo ]Q틼id4_`^{tB¸ λ庫ͯS{Rud?Y4].2ﱞZǚcxN&S1K2g&W0>9,)Fv^?=ܿrVΛY7p}ˀnqwyḎo%tyok~uO=O:&s0.xНtֱMJ9oP'i`pPaƚ[;XQxLx yʋ$kzYFjNK򘿗.La\>>ǒ?ǦnA⹛0D +,i9n?&X^Ͷl1q~fu$Z{0EcOw]%?uN 6]!//^QHKsSk]`.a3%fn:?C QwC-6,7#pU: Q^ŜܬH} ʊ8+gSObJ!]]Iޤ<  ABJzk8vL7pF˛wv/Ʈ^ȇC̴Df b{Ug`ʒ{|l[*'G|=C{@XZ`7m^N27r[J _{>0s~@-DZeq2fsֳ)9qZ9oEsIDu QŠT϶`Ğ=P߲?D}Ƈ[F̌G(Y6 +V')'sj'ɶx7툅ҕϋrMޤ) 'fߌHL^ov^]#•yEtnƲ}3V/n1O-4^n {f+I$hMVzrqf& dʼnl+ݾܖHoo<9us>j QG%*+cÒܕ~LqO8 +4u}oOٞJOb5fC$ēݏ >aމ"hC!wV,q?y6Z%!,[/+c:Z+|n*S* w%Hp7,7D|mHycճB}Ia3śXG;hH@풧r_*(߲e"v*&>=AKŞO=nj/1Y?j4 +azy SXa_{ɉ܍ ww=3'K@2EuEiAb)*k Hok^-.n.u,l/'ֱ-RqVs]9XQx+MЇ sȸޡLJ粃P^.U›w2jH +QeUGO[ +޾4'Y*2JgNA림w2\(^]}Ըm>9,,@uXM OfGfOod%8 -+--%%K.?3YX]^M rҍN˝ &Nn&B;/LG&| ?J}"E_M=sO_7hF̠=๙U҃>^D}ˠggeCԱZi},na} g_校ۼ^nWAX /wOjմkDCVoQ 5@sΖ8+s#yad}ț 2-~䧕*W͟(g;Nz#^RT d=[|Sq!X/VfUnҗU(~YKx'#[&H~̺7#sR|5$Hd uByXg,\?j2&wtI RMqWQBk`[jv/F|z;x{+!5E9/NYܱ %Oz5pm,_p/U*Y&YUJJ?a> lTr$ybV]*{~_xU99y'77z97f ,E'.7,9D}Ni6ݬ[ď>ډ['$TLUz14Mc&ri\Dt1zVi`:jt,gl謺tOY8gEt7\tҸ<Ւ r*lOࠓ"kb$܉۽RvLNvIFM]җ|<|L/+'kZ]'_Նgq]F|Km2z$kfGD.egQBY͍ qLDm}vsLJ5[Cniy6YIȝx8c'B*'GTT K#6|˕f0O{✏n,w t Zpvit]6 $"JmOIZsEԑEa$#Dq~Mo^orIFeZ/WI+#|Lv5V!:&U]'ݰ M7MY+Q^lR ʝ^䳻Jyk"dn()ĿLsQ v+r33K}~\ H_Ƭu^u wz$'ugƃǎg&.ǙMX%04/cq&zpAlZ{ߖY9Fz&8&ś{ }orPǛwrN`@zv*kC2Wf.?%m>nLaM7X̙ddQ޻ΤZxalMIM+kFq)Q _x|G7yY452(0kQ_&ŃC? {\?͗.ZdZ8{ +o,g}Z>"ҟv #+Jo|uk#4ڝc^X1Kŀ(B!̽tLt IkżQ8[EDy!eN-f3rd^{Rb mU/3xs y{jvՀH,X)[t!Aig/p#+į'ę?{^W{ +#1 }Už̲ _cq? m+2}/I 7&}d!}y(Oeb7~kJ +#XZJ_ZYȀeXQ5 t&={19]ܖFwrN&=p=p#Lw2Lsˑz.ʫ_|A_1XYV&?kՂYe܏}iU{wwĎTQ )If#@#oY?E4@cNVR_ƀXuUޫ-|K%./fk|>kt-akÒM3o^^D>nKM:0%i]y{7nkJ?1qNmbe%inpՀt<*V%keynqY~˘X8-]s\Q8{ka_*wK[K/qw璼ikf+qpp˄EI$V-}BL/~ /ɗkΫr볬 ]g!5]49+`lן q ځ,a_3/NJE\1/gXViN3W_[ Bo,7@R{@ziwj HgN=!b+VC("w]xy03s_Y%yl3GXNZ/8' p 2]9?y}EEEMqW\kOefd +(}/L.^_7@[QY+-e2n)j )u+!-y<32޺![6Ӫ,a>g>//xEnKSQi20/1d<p2P +pH=e".e{ )vn,9ŀM^WvR.f_ծod88]ynj,?ԗYV7pFQrrZrQbU k5S@ACfBLb[ɞGJkQZ#8>u_J >ܨ@f.m16] aw +?aQP2=`ܸ঵+ +NaSǩ_OGb<݋r¡tᢞ~qϮtf[t(03%`SVxQ ߯ Mm +n_(0=GjyOfeQ}ZƮ5[U'6d,1[8߽~SZGbߏ{rf@pz/7y U6@USM=, fW 1{^)?j1#On޸ܜM\Ra ]KlV ~!_\6=s8]H;_/jC9L=0cPIc6bVw.Nm:r٤Y+ubӯyȀ9\VG/kM*}3ImYUEvd͝v]qCx탯ʹjM|(Ο&SԶ5\ubBo߯NC{a֣F6yVAXW_&Iwp +a=`.o:{k-;S[LփE?N ؘqMs*g1rHVvD0P +Iد%Oŝjn1EWƮۍhex.Tql/#SدWM1~*[>ycrS"hm2w7Bc-vwpՉM +ɗ9{{jn1,w-,u,'!$3>vS}-ši<$uW6,VB_c7n1Z7_kĦoN5=ӬͩlTZaB~S*#MRnha?οuk5o}$EdljA#/[7 [ޯ7EԁM[ZGOBØnĭװh0ɹk5#P{մo/ +~"DC}M=>:ȓm-}goM2L~~׿WhVYCJ!vq[+݈4{7<$?rf^:5c=o,!.{kĢ34+Wgwq)n5$?WW תP>n9[YXtqoXV0‘~cn[!2bFjeL*rpoy 0l_{#C*Hfo+#7Qؕ4CH]^U\Lf7C+2IP~{N7 k|ߗ~΃|&6ݠSCFPn?W t0cU1gs H_^H=db)5 +`.NZ:!Ups60YMt׷&5U٧򙜯+ɰBmZ.j{6^hC!B^o3w[4dc P؃r.0q:;C:vcvqh.{u&M^}h đ.q<1g(KxHcpdGT3>Fe}2 Eп0G=Ƽ2{XMgŢ {¥l`cv+֋DпXt(- ^J}^BO N0kA,-6D )8𼇲P6Ʀޠ!]_U]nn [=0mZW8l]ƽ/0:Z+?AZuث? <4CP:T#< c{e:_U(ׇcXH88w?# +GۯA{H|%uD <DϾ?C+56}TkxūeY ]T>HK~rWkbApѳOv{mX7MҘIUcٿ}d"ݵhf'{?٪rQItC4v2b[- !mycNGLb(r/7$k 'R^іaONS~wY7*>_&Jߘ&lw;hfTJ{&1On"Hq2n7~:v?E=ĘBu(T[CI/^^xj'¢)!!^TnD5x:JuܯUWvZbHL=jĦSp잣耸4H]yHWMJ&Ʊ4b&3oS[wǦk:({C=$nr$ݏkovggXZd=||mM%7ZwhKۨh{>ু/jԃj_j |(!8/1?d h3iO=ۥ4%7 o7 [8zaCCDI-ހv'7Bo@3nKLF:/f A}/K 7 B0 r.:s6 6)3߳z1ն6= .G٤)KQh h{D$p) W[[{. n$ǢtLpT!L[ϳZrJ]ý}(Sf;S@_Aƹy( +rGqMYxnkVm\l1Y=JթJ?}߳Ak d++on3L]o[سh*irg.  D33VOI*;C&B$J{vE^Ξchao~Bv Ozyt9< ٽƁ?_OMl2W܅Yb2F"wۨcí+a@b}?ks6gjQo :,zӛ.a`D%m78r>,zaҦr#㻑цEo;뭣Ɓ¢o̔o^[ +J!4|d\ǿDnD LnGJn~%ǁ@Ta3JX|)`\rZSI7@nĭt>J1tF4i.}!7y/%" ĢF,Px%RɭTr#6@q@7,F*G(#܈[|JCJ*7"&{>b/Lf:VnS9p*8t~?FsT ph^nub-q$#P-i.%o6=|Ï?;vq@ezs-j &@e*3*?W SI_;:>I?NaဢX4Mo a5Ŀb"a/1v9ŦY Zh (Cw/e5_nx%P~{tz3%>X箄րX6܃M~\Klh Ebƕ9^-mR`g"? 1)caJԀF'۩wTo˂Ħ"Pvs/_Xawma՛Aztj, $MpB nȶ 59?3 }#hvUobi89Lo8\qw+W }azqI>CߎAo\qe:i+AT@[@7$~Fq0I|xM Ǧ{K7wϫ:b#(q_HaSYoxzK(@3j#1cc8 +h{S#-:W>ǡoJhi N>tzsC'1?ytVo=Q{<<@bSYohM.W&"~Ƴ J66=pz+.iq{dW{I>HaOxFW7P(E恞*P*(%Ej4fxBەőn4~8H!126\JwZrqڀzb*MMn4ǦX&ܶu*^J æz(M2uqx(MJM}%@Ml/Tn"};EC:Il޸=(b]0oRy;0 E׍RBn<(91(MR[{7S%01bHyj-c%-y^urb.kW XtyˍY m:6#ܘھ7$Pf/* " 4n)@},ˍKw"RTǦ^+D2({ܛ7&!84M:`ވܕ@p@a, eZL mAp@YlzMJz/Ԅm^7ټXZrt9 +m@p@IZyj֍twp$P y#\p(E7&yџ*j8P ?)0~LbBp@+ 2!@Z6DaoR`3!8!JXIqg iqSԷo~cOG}>Z n.@nl:zcA+,ƢQ;V$@f,\EP/}@&FEZ&2P_p9q7?Jo$}sz鍽킅Rbӕ4I1ɥ.꧛ވ9hv{mo I%}M;&NkV ~nH?ita.M' 8 Ro|O;ȅC] 8 6XxaOF7~w@,fv Lp ! -/]E  JZ;I ȃMؼsz{ HC{cb2~wz$T_H1 3Q^$¦i7 HECR!7 6-3t՛I84[jCE2{_Uoy d]fSwü`ꍽN`zʍeNp}8¦p=1I 6>HOfa_#: ¦7y 6-誥 ·3dX-rA7 6y7 6n Mrfx-62vl7 \N 5V!MA HM7'i3H p'tt@ fހtXtfW$7 }ӣLr0o@>*; $W1i#8\s zdSA-ʻ۵Go&9K$eǘ81q7 -6=rn& =ia endstream endobj 31 0 obj <>stream +:yǑ bm:wlGyqhq.]ڷBo2jڥ& wHCӵ[ؘ "&0rΗIDL"-c 0H ۣ}{wڱCG&/@b~}wԡ)8I 1LoԿw.c+!w!d= اgfbTכAfc  ԯgNb)7 56zAwazS$ ȍMf<%оcM2xZG  \G^iѴ\3q'tCYm ¼ٱ釣F Ч3p1 + 2I!$ش#&ewpO-cӣōE(S#c֑t)1(M_:Mg8HH6瞒8t@޽DD yȥG mLo{S{2Ґjz)#Л%&&I # Eo?u!Lo;+7܀ % v?84clRܐ@R< )NC?<7ݣkXLԖIi9Էww=wH'}6zSF r+(6pI )Lo'5&~`7QA)=v+IyvRFߗ7^%Lr0o@VN|to짆wM_Orh ?ӛ !@ ѿO̾:$c2ճs8I6 hE?WN cF8\opCSz(L{.q@,?Л7/a߳8TB1wr.7'zVo܅=zt$ԛAb8t|nBԖpP"-:0ɿpH?vۮ) o o@#l:Ror'o@'l몮۷pf#cN;ws!y:a;0gޘ}{7]Po& +-AV]th/\bO:.b79}p'^8b< w{+ I KLrN"-Z?[TsCH7$whM_^Ɠ0o@32&UtæΏPor:qvtP 'O IJƃqL2d ~Oo܁-1+4plSF7CeLU Ja$SlAE ӐMo184h {No̻}c0]+܈!Lo7H7<Zc OV]tĦvh . b7%6Џȵ3I0BbVo&qeIzBo :qlp@ +C1x ZaІ3ygmXJi[Nٚ]eA; fmh+1w2 D56m.ReX6 wD7][x 辋 +C~N6mu0o[[p2vI8M L}%qhvN4ep'p]'8ij3HNajx +MLo`SgvEHE+ok!yr%N:6{-1[Ap&ycM_*i0oĢZN tC+:z,Z7iC2:pl?܂{9 +Iq:s7#o +Ya{^݂I#̂%Fj.æUp?<sW8K)c砞 zΟOpGDLib&& ;8Ǣ-IE`5ceLr`g2 tNX8{E8tnX8$8LpSq\JN/ }ax)e疗R%YRzHb@#`:2 "4ҔVvy'`h =~ogah=qG!^dh=pYp&9k8Ew\ҢA7EiL㈘H,:3H Xlv(i1XA=Xl%Y7_Ji_xyzl<$38OJ) "Rp4Zw7Wprax4A:-i4yH$,ǤY& q_b@Gfv1#B @`)9& p(hL2kc8t_ @qh|sSM< MƱ(-II1(Z0}QI-:}J7q_߽I&.܆ f&9& ̈́)qMT84JJ?}&d7 ͅ;ye-@2͆'ynT#J< %@ʏ~סQo +Wp^cL`hLp6qem_4hLu)JZT}!`'WAh9LF{o 7!cw8n-JxB {>!7&{'=_ 'Jo?$Ep#?$g킁 \0wC10pJ$ vDL6=p*L+8ƒE8!:4o7M\lApNk8tσC @顳:C~ao6xݧ7X)ܹK$YK3{9s9νz7 2O!@|JOpp;Q tEH98'ɷz@DrpOP{ 8'l P37Y{C |'_ҽs9&PVS㭹QJŗh@@x N0Ǜ$ߤ#@x e4 %'Wȧ.ڽ+-wr08P""wzxdXW!ހ +}MbFF|7vn%P<ܨ]98sp0߆Ș܍1y2ǧ$ಾES\_7ްzw7$#龻7@;c7 =w@_p3N dpGS}N$t+P{ Ԇ~Ñ/Tpaf4 g;wQ=yYP.X!c`@y鳨=tdgn]9ҙ~ΘAAn#9vp޼37S](JR _~>#P,tg%'i{K="8/SNͥdU Fp_{%̏yȏ lⴓC.(YPQ,|9w`b@m16t=9J|ppΐ_"I93i)?2Iڷ/b9@ëXto ciHGpq d+:.5 l PwU|y;r"Vsz1@gK57YGpv=ɡ%pnl4L|q$J|L'Jг78${g&z '!~x7N~f=]bsLO4(үΔ›Q)\ddo0?rf + ct,+@pf$yʀ/_9bGf|X +_l}8rr'_j-AUoif~tn=R],%_q"p)v՛nYN+y`#3L)CMئB&I;#”  r) 3 ZGސ44 GHANOprFYɠtp%m>hʄ”~'0DoN927KCMR `bt_V\D鷱=d+lҲ{6,Øu9/|/Ks<o1btft2 ȣɜPv}Ҳ{n]JIs奙 #T S})f{c#g_ B6LD鑙ES'@Q)[p]j-S_6C #C^65d89\*s յqnnUr@ɬ;[- :c0ӚcuDpΠn w=sp_`+ du{ 01rlB7VڟBC:dX +?gOBP涋mL=C) +~КVQGKf ytETmTdE8Y?^MՓ$8qJ\02&VQG_<26OGݴoX &9&Lz<9p(dl^K/r3"]0^2܋|% ?~ЛN-"ŖΟc#Ǣ#KS~69>QtG` oca+C o޾5olah/pnix %*O A^+cenGziYJS/ܨ _;8w}=M$>+gWTQ,7ҀHn d]޷ye< Rnp7S +G&ǀx47L:0{[ixgyrlKs4?yx9r4.|)6;08y it ei7 ba(s;SOms{sCE{]-.irms8ITx#L>7/ lHR졥±[y77`p+y3뎟릜%]!⽽^kahfտ>FYNp`koqɽ}'p(O\xMM2H;8fף/<XBB>L~ (fǬ=(x dl /"+D- s(Jf_3IbLl[i;8d馽P CdZ<}l7q}DX1R[i=pZeE\>5g&H0ZNB*AfcJ /#A S +WE>!pr@͍˿r׮1H^ҋ/P6dd2T\ 6#ܥ*#FƏI]ç/঍?/0iml3R +Mڐr#rM7AԱc}m߸᧫V2(&C@S +_Zv׿Xnt8<]>xG~~݆9_u!ԍd~Z@.]Zaz1M †8/xs3ݵ 1qK]17|s|@X +G#_o/-m5YQgM ңKf}]\{Ւ>X" Lƈ0y0psDB9z|(1/ bJS_JCqZ3V6 /+ՆgF/-)6*t5fMsRs"Kc(N4S_>U$P&)+mE GceQk\~']6Tgmm6Լ^Wc5g*cbE Ćx:)>n%ӶZפmЬ7g袲YzGsW`.#-ɘ7+Fkmj}|֛#W?k¦FCAQ]rCF˺ڼx}A`lN6}[Mb[fLQe5E&;Őܬ.]ї9یMٵm%re|v`-Ԣj-2R樾 +C2l27kʲB]_zuEkFV\L6ڠQw>j0#ݽ˴5EZr%}n\bj{RuD93rx G^r0&ϤꯏfΡ@ݗѮh rfENMݘST4 +mIT:VQ +}Vowö^5ݍ{cs"fjZL}wQc02c;ӝ>Rl[)qW6z]P"~`s+[EbZBDQnSMC(ΑXߝ:؝[d2csbm֖ ߥ,mj0QoӒX3@ggT*+ kkʛu9jg8^ng2R' )UUҟѕnHoo.p4$GMΠFh2s\^m1e^#<tNZ`TimYPG>t ~TP:-Jԓ+\e6ZU*SSuԷv)6Z,EI}v^ұ/МߝC40w#d*]Ѣn+ ڢWeO16UeFVk]Uݥ+Փf!r؃&ܙs͚5GPF[SRBY.ĤkY-Ime)U&92ͨn.6W-3[Zcu ZmkIF[\e'yP'+RB兆h&+ٸ^Zҥ5e)v}JSTȭv9QNhlNXƩ]ָ3Ra6%SJ][i[M9efBJwŕJ +"'9h,dRLQZJӔjcIAǪ|٘֘ڣcIi)-2IUV:*q[< = +p*EwmlعU{O쫀mش*0L1YU_{̻˳/T Vɭ +xTs4> +LL*\q3Q5 J,(I endstream endobj 15 0 obj [/ICCBased 19 0 R] endobj 6 0 obj [5 0 R] endobj 32 0 obj <> endobj xref 0 33 0000000000 65535 f +0000000016 00000 n +0000000144 00000 n +0000047649 00000 n +0000000000 00000 f +0000163121 00000 n +0000593503 00000 n +0000047700 00000 n +0000048109 00000 n +0000048283 00000 n +0000163420 00000 n +0000139682 00000 n +0000163307 00000 n +0000049181 00000 n +0000048344 00000 n +0000593468 00000 n +0000048620 00000 n +0000048668 00000 n +0000139717 00000 n +0000160473 00000 n +0000163191 00000 n +0000163222 00000 n +0000163494 00000 n +0000163800 00000 n +0000165099 00000 n +0000187851 00000 n +0000253439 00000 n +0000319027 00000 n +0000384615 00000 n +0000450203 00000 n +0000515791 00000 n +0000581379 00000 n +0000593526 00000 n +trailer <]>> startxref 593722 %%EOF \ No newline at end of file diff --git a/responsive-ui/design/chromeStorePics/promo1400560.png b/responsive-ui/design/chromeStorePics/promo1400560.png new file mode 100644 index 000000000..d3637ecc8 Binary files /dev/null and b/responsive-ui/design/chromeStorePics/promo1400560.png differ diff --git a/responsive-ui/design/chromeStorePics/promo440280.png b/responsive-ui/design/chromeStorePics/promo440280.png new file mode 100644 index 000000000..c1f92b1c0 Binary files /dev/null and b/responsive-ui/design/chromeStorePics/promo440280.png differ diff --git a/responsive-ui/design/chromeStorePics/promo920680.png b/responsive-ui/design/chromeStorePics/promo920680.png new file mode 100644 index 000000000..726bd810a Binary files /dev/null and b/responsive-ui/design/chromeStorePics/promo920680.png differ diff --git a/responsive-ui/design/chromeStorePics/screen_dao_accounts.png b/responsive-ui/design/chromeStorePics/screen_dao_accounts.png new file mode 100644 index 000000000..1a2e8052c Binary files /dev/null and b/responsive-ui/design/chromeStorePics/screen_dao_accounts.png differ diff --git a/responsive-ui/design/chromeStorePics/screen_dao_locked.png b/responsive-ui/design/chromeStorePics/screen_dao_locked.png new file mode 100644 index 000000000..6592c17e4 Binary files /dev/null and b/responsive-ui/design/chromeStorePics/screen_dao_locked.png differ diff --git a/responsive-ui/design/chromeStorePics/screen_dao_notification.png b/responsive-ui/design/chromeStorePics/screen_dao_notification.png new file mode 100644 index 000000000..baeb2ec39 Binary files /dev/null and b/responsive-ui/design/chromeStorePics/screen_dao_notification.png differ diff --git a/responsive-ui/design/chromeStorePics/screen_wei_account.png b/responsive-ui/design/chromeStorePics/screen_wei_account.png new file mode 100644 index 000000000..23301e4bf Binary files /dev/null and b/responsive-ui/design/chromeStorePics/screen_wei_account.png differ diff --git a/responsive-ui/design/chromeStorePics/screen_wei_notification.png b/responsive-ui/design/chromeStorePics/screen_wei_notification.png new file mode 100644 index 000000000..7a763e5df Binary files /dev/null and b/responsive-ui/design/chromeStorePics/screen_wei_notification.png differ diff --git a/responsive-ui/design/metamask-logo-eyes.png b/responsive-ui/design/metamask-logo-eyes.png new file mode 100644 index 000000000..c29331b28 Binary files /dev/null and b/responsive-ui/design/metamask-logo-eyes.png differ diff --git a/responsive-ui/design/wireframes/1st_time_use.png b/responsive-ui/design/wireframes/1st_time_use.png new file mode 100644 index 000000000..c18ced5e2 Binary files /dev/null and b/responsive-ui/design/wireframes/1st_time_use.png differ diff --git a/responsive-ui/design/wireframes/metamask_wfs_jan_13.pdf b/responsive-ui/design/wireframes/metamask_wfs_jan_13.pdf new file mode 100644 index 000000000..c77c9274a Binary files /dev/null and b/responsive-ui/design/wireframes/metamask_wfs_jan_13.pdf differ diff --git a/responsive-ui/design/wireframes/metamask_wfs_jan_13.png b/responsive-ui/design/wireframes/metamask_wfs_jan_13.png new file mode 100644 index 000000000..d71d7bdb4 Binary files /dev/null and b/responsive-ui/design/wireframes/metamask_wfs_jan_13.png differ diff --git a/responsive-ui/design/wireframes/metamask_wfs_jan_18.pdf b/responsive-ui/design/wireframes/metamask_wfs_jan_18.pdf new file mode 100644 index 000000000..592ba8532 Binary files /dev/null and b/responsive-ui/design/wireframes/metamask_wfs_jan_18.pdf differ diff --git a/responsive-ui/example.js b/responsive-ui/example.js new file mode 100644 index 000000000..4627c0e9c --- /dev/null +++ b/responsive-ui/example.js @@ -0,0 +1,123 @@ +const injectCss = require('inject-css') +const MetaMaskUi = require('./index.js') +const MetaMaskUiCss = require('./css.js') +const EventEmitter = require('events').EventEmitter + +// account management + +var identities = { + '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111': { + name: 'Walrus', + img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', + address: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', + balance: 220, + txCount: 4, + }, + '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222': { + name: 'Tardus', + img: 'QmQYaRdrf2EhRhJWaHnts8Meu1mZiXrNib5W1P6cYmXWRL', + address: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222', + balance: 10.005, + txCount: 16, + }, + '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333': { + name: 'Gambler', + img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', + address: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333', + balance: 0.000001, + txCount: 1, + }, +} + +var unapprovedTxs = {} +addUnconfTx({ + from: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222', + to: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', + value: '0x123', +}) +addUnconfTx({ + from: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', + to: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333', + value: '0x0000', + data: '0x000462427bcc9133bb46e88bcbe39cd7ef0e7000', +}) + +function addUnconfTx (txParams) { + var time = (new Date()).getTime() + var id = createRandomId() + unapprovedTxs[id] = { + id: id, + txParams: txParams, + time: time, + } +} + +var isUnlocked = false +var selectedAccount = null + +function getState () { + return { + isUnlocked: isUnlocked, + identities: isUnlocked ? identities : {}, + unapprovedTxs: isUnlocked ? unapprovedTxs : {}, + selectedAccount: selectedAccount, + } +} + +var accountManager = new EventEmitter() + +accountManager.getState = function (cb) { + cb(null, getState()) +} + +accountManager.setLocked = function () { + isUnlocked = false + this._didUpdate() +} + +accountManager.submitPassword = function (password, cb) { + if (password === 'test') { + isUnlocked = true + cb(null, getState()) + this._didUpdate() + } else { + cb(new Error('Bad password -- try "test"')) + } +} + +accountManager.setSelectedAccount = function (address, cb) { + selectedAccount = address + cb(null, getState()) + this._didUpdate() +} + +accountManager.signTransaction = function (txParams, cb) { + alert('signing tx....') +} + +accountManager._didUpdate = function () { + this.emit('update', getState()) +} + +// start app + +var container = document.getElementById('app-content') + +var css = MetaMaskUiCss() +injectCss(css) + +MetaMaskUi({ + container: container, + accountManager: accountManager, +}) + +// util + +function createRandomId () { + // 13 time digits + var datePart = new Date().getTime() * Math.pow(10, 3) + // 3 random digits + var extraPart = Math.floor(Math.random() * Math.pow(10, 3)) + // 16 digits + return datePart + extraPart +} diff --git a/responsive-ui/index.html b/responsive-ui/index.html new file mode 100644 index 000000000..9dfaefbb3 --- /dev/null +++ b/responsive-ui/index.html @@ -0,0 +1,20 @@ + + + + + MetaMask + + + + +

+ + + + +
+ +
+ + + diff --git a/responsive-ui/index.js b/responsive-ui/index.js new file mode 100644 index 000000000..a729138d3 --- /dev/null +++ b/responsive-ui/index.js @@ -0,0 +1,58 @@ +const render = require('react-dom').render +const h = require('react-hyperscript') +const Root = require('./app/root') +const actions = require('./app/actions') +const configureStore = require('./app/store') +const txHelper = require('./lib/tx-helper') +global.log = require('loglevel') + +module.exports = launchMetamaskUi + + +log.setLevel(global.METAMASK_DEBUG ? 'debug' : 'warn') + +function launchMetamaskUi (opts, cb) { + var accountManager = opts.accountManager + actions._setBackgroundConnection(accountManager) + // check if we are unlocked first + accountManager.getState(function (err, metamaskState) { + if (err) return cb(err) + const store = startApp(metamaskState, accountManager, opts) + cb(null, store) + }) +} + +function startApp (metamaskState, accountManager, opts) { + // parse opts + const store = configureStore({ + + // metamaskState represents the cross-tab state + metamask: metamaskState, + + // appState represents the current tab's popup state + appState: {}, + + // Which blockchain we are using: + networkVersion: opts.networkVersion, + }) + + // if unconfirmed txs, start on txConf page + const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.network) + if (unapprovedTxsAll.length > 0) { + store.dispatch(actions.showConfTxPage()) + } + + accountManager.on('update', function (metamaskState) { + store.dispatch(actions.updateMetamaskState(metamaskState)) + }) + + // start app + render( + h(Root, { + // inject initial state + store: store, + } + ), opts.container) + + return store +} diff --git a/responsive-ui/lib/account-link.js b/responsive-ui/lib/account-link.js new file mode 100644 index 000000000..d061d0ad1 --- /dev/null +++ b/responsive-ui/lib/account-link.js @@ -0,0 +1,26 @@ +module.exports = function (address, network) { + const net = parseInt(network) + let link + switch (net) { + case 1: // main net + link = `http://etherscan.io/address/${address}` + break + case 2: // morden test net + link = `http://morden.etherscan.io/address/${address}` + break + case 3: // ropsten test net + link = `http://ropsten.etherscan.io/address/${address}` + break + case 4: // rinkeby test net + link = `http://rinkeby.etherscan.io/address/${address}` + break + case 42: // kovan test net + link = `http://kovan.etherscan.io/address/${address}` + break + default: + link = '' + break + } + + return link +} diff --git a/responsive-ui/lib/contract-namer.js b/responsive-ui/lib/contract-namer.js new file mode 100644 index 000000000..f05e770cc --- /dev/null +++ b/responsive-ui/lib/contract-namer.js @@ -0,0 +1,33 @@ +/* CONTRACT NAMER + * + * Takes an address, + * Returns a nicname if we have one stored, + * otherwise returns null. + */ + +const contractMap = require('eth-contract-metadata') +const ethUtil = require('ethereumjs-util') + +module.exports = function (addr, identities = {}) { + const checksummed = ethUtil.toChecksumAddress(addr) + if (contractMap[checksummed] && contractMap[checksummed].name) { + return contractMap[checksummed].name + } + + const address = addr.toLowerCase() + const ids = hashFromIdentities(identities) + return addrFromHash(address, ids) +} + +function hashFromIdentities (identities) { + const result = {} + for (const key in identities) { + result[key] = identities[key].name + } + return result +} + +function addrFromHash (addr, hash) { + const address = addr.toLowerCase() + return hash[address] || null +} diff --git a/responsive-ui/lib/etherscan-prefix-for-network.js b/responsive-ui/lib/etherscan-prefix-for-network.js new file mode 100644 index 000000000..2c1904f1c --- /dev/null +++ b/responsive-ui/lib/etherscan-prefix-for-network.js @@ -0,0 +1,21 @@ +module.exports = function (network) { + const net = parseInt(network) + let prefix + switch (net) { + case 1: // main net + prefix = '' + break + case 3: // ropsten test net + prefix = 'ropsten.' + break + case 4: // rinkeby test net + prefix = 'rinkeby.' + break + case 42: // kovan test net + prefix = 'kovan.' + break + default: + prefix = '' + } + return prefix +} diff --git a/responsive-ui/lib/explorer-link.js b/responsive-ui/lib/explorer-link.js new file mode 100644 index 000000000..3b82ecd5f --- /dev/null +++ b/responsive-ui/lib/explorer-link.js @@ -0,0 +1,6 @@ +const prefixForNetwork = require('./etherscan-prefix-for-network') + +module.exports = function (hash, network) { + const prefix = prefixForNetwork(network) + return `http://${prefix}etherscan.io/tx/${hash}` +} diff --git a/responsive-ui/lib/icon-factory.js b/responsive-ui/lib/icon-factory.js new file mode 100644 index 000000000..27a74de66 --- /dev/null +++ b/responsive-ui/lib/icon-factory.js @@ -0,0 +1,65 @@ +var iconFactory +const isValidAddress = require('ethereumjs-util').isValidAddress +const toChecksumAddress = require('ethereumjs-util').toChecksumAddress +const contractMap = require('eth-contract-metadata') + +module.exports = function (jazzicon) { + if (!iconFactory) { + iconFactory = new IconFactory(jazzicon) + } + return iconFactory +} + +function IconFactory (jazzicon) { + this.jazzicon = jazzicon + this.cache = {} +} + +IconFactory.prototype.iconForAddress = function (address, diameter) { + const addr = toChecksumAddress(address) + if (iconExistsFor(addr)) { + return imageElFor(addr) + } + + return this.generateIdenticonSvg(address, diameter) +} + +// returns svg dom element +IconFactory.prototype.generateIdenticonSvg = function (address, diameter) { + var cacheId = `${address}:${diameter}` + // check cache, lazily generate and populate cache + var identicon = this.cache[cacheId] || (this.cache[cacheId] = this.generateNewIdenticon(address, diameter)) + // create a clean copy so you can modify it + var cleanCopy = identicon.cloneNode(true) + return cleanCopy +} + +// creates a new identicon +IconFactory.prototype.generateNewIdenticon = function (address, diameter) { + var numericRepresentation = jsNumberForAddress(address) + var identicon = this.jazzicon(diameter, numericRepresentation) + return identicon +} + +// util + +function iconExistsFor (address) { + return contractMap[address] && isValidAddress(address) && contractMap[address].logo +} + +function imageElFor (address) { + const contract = contractMap[address] + const fileName = contract.logo + const path = `images/contract/${fileName}` + const img = document.createElement('img') + img.src = path + img.style.width = '75%' + return img +} + +function jsNumberForAddress (address) { + var addr = address.slice(2, 10) + var seed = parseInt(addr, 16) + return seed +} + diff --git a/responsive-ui/lib/lost-accounts-notice.js b/responsive-ui/lib/lost-accounts-notice.js new file mode 100644 index 000000000..948b13db6 --- /dev/null +++ b/responsive-ui/lib/lost-accounts-notice.js @@ -0,0 +1,23 @@ +const summary = require('../app/util').addressSummary + +module.exports = function (lostAccounts) { + return { + date: new Date().toDateString(), + title: 'Account Problem Caught', + body: `MetaMask has fixed a bug where some accounts were previously mis-generated. This was a rare issue, but you were affected! + +We have successfully imported the accounts that were mis-generated, but they will no longer be recovered with your normal seed phrase. + +We have marked the affected accounts as "Loose", and recommend you transfer ether and tokens away from those accounts, or export & back them up elsewhere. + +Your affected accounts are: +${lostAccounts.map(acct => ` - ${summary(acct)}`).join('\n')} + +These accounts have been marked as "Loose" so they will be easy to recognize in the account list. + +For more information, please read [our blog post.][1] + +[1]: https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd#.7d8ktj4h3 + `, + } +} diff --git a/responsive-ui/lib/persistent-form.js b/responsive-ui/lib/persistent-form.js new file mode 100644 index 000000000..d4dc20b03 --- /dev/null +++ b/responsive-ui/lib/persistent-form.js @@ -0,0 +1,61 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const defaultKey = 'persistent-form-default' +const eventName = 'keyup' + +module.exports = PersistentForm + +function PersistentForm () { + Component.call(this) +} + +inherits(PersistentForm, Component) + +PersistentForm.prototype.componentDidMount = function () { + const fields = document.querySelectorAll('[data-persistent-formid]') + const store = this.getPersistentStore() + + for (var i = 0; i < fields.length; i++) { + const field = fields[i] + const key = field.getAttribute('data-persistent-formid') + const cached = store[key] + if (cached !== undefined) { + field.value = cached + } + + field.addEventListener(eventName, this.persistentFieldDidUpdate.bind(this)) + } +} + +PersistentForm.prototype.getPersistentStore = function () { + let store = window.localStorage[this.persistentFormParentId || defaultKey] + if (store && store !== 'null') { + store = JSON.parse(store) + } else { + store = {} + } + return store +} + +PersistentForm.prototype.setPersistentStore = function (newStore) { + window.localStorage[this.persistentFormParentId || defaultKey] = JSON.stringify(newStore) +} + +PersistentForm.prototype.persistentFieldDidUpdate = function (event) { + const field = event.target + const store = this.getPersistentStore() + const key = field.getAttribute('data-persistent-formid') + const val = field.value + store[key] = val + this.setPersistentStore(store) +} + +PersistentForm.prototype.componentWillUnmount = function () { + const fields = document.querySelectorAll('[data-persistent-formid]') + for (var i = 0; i < fields.length; i++) { + const field = fields[i] + field.removeEventListener(eventName, this.persistentFieldDidUpdate.bind(this)) + } + this.setPersistentStore({}) +} + diff --git a/responsive-ui/lib/tx-helper.js b/responsive-ui/lib/tx-helper.js new file mode 100644 index 000000000..ec19daf64 --- /dev/null +++ b/responsive-ui/lib/tx-helper.js @@ -0,0 +1,17 @@ +const valuesFor = require('../app/util').valuesFor + +module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, network) { + log.debug('tx-helper called with params:') + log.debug({ unapprovedTxs, unapprovedMsgs, personalMsgs, network }) + + const txValues = network ? valuesFor(unapprovedTxs).filter(txMeta => txMeta.metamaskNetworkId === network) : valuesFor(unapprovedTxs) + log.debug(`tx helper found ${txValues.length} unapproved txs`) + const msgValues = valuesFor(unapprovedMsgs) + log.debug(`tx helper found ${msgValues.length} unsigned messages`) + let allValues = txValues.concat(msgValues) + const personalValues = valuesFor(personalMsgs) + log.debug(`tx helper found ${personalValues.length} unsigned personal messages`) + allValues = allValues.concat(personalValues) + + return allValues.sort(txMeta => txMeta.time) +} diff --git a/test/unit/responsive/components/dropdown-test.js b/test/unit/responsive/components/dropdown-test.js index 4d417d394..0472c541b 100644 --- a/test/unit/responsive/components/dropdown-test.js +++ b/test/unit/responsive/components/dropdown-test.js @@ -5,8 +5,8 @@ const h = require('react-hyperscript'); const ReactTestUtils = require('react-addons-test-utils'); const sinon = require('sinon'); const path = require('path'); -const Dropdown = require(path.join(__dirname, '..', '..', '..', '..', 'ui', 'responsive', 'app', 'components', 'dropdown.js')).Dropdown; -const DropdownMenuItem = require(path.join(__dirname, '..', '..', '..', '..', 'ui', 'responsive', 'app', 'components', 'dropdown.js')).DropdownMenuItem; +const Dropdown = require(path.join(__dirname, '..', '..', '..', '..', 'responsive-ui', 'app', 'components', 'dropdown.js')).Dropdown; +const DropdownMenuItem = require(path.join(__dirname, '..', '..', '..', '..', 'responsive-ui', 'app', 'components', 'dropdown.js')).DropdownMenuItem; describe('Dropdown components', function () { let onClickOutside; @@ -112,4 +112,4 @@ describe('Dropdown components', function () { ReactTestUtils.Simulate.click(node); assert.equal(onClick.calledOnce, true); }); -}); \ No newline at end of file +}); diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js new file mode 100644 index 000000000..bed05a7fb --- /dev/null +++ b/ui/app/account-detail.js @@ -0,0 +1,311 @@ +const inherits = require('util').inherits +const extend = require('xtend') +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const CopyButton = require('./components/copyButton') +const AccountInfoLink = require('./components/account-info-link') +const actions = require('./actions') +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +const valuesFor = require('./util').valuesFor + +const Identicon = require('./components/identicon') +const EthBalance = require('./components/eth-balance') +const TransactionList = require('./components/transaction-list') +const ExportAccountView = require('./components/account-export') +const ethUtil = require('ethereumjs-util') +const EditableLabel = require('./components/editable-label') +const Tooltip = require('./components/tooltip') +const TabBar = require('./components/tab-bar') +const TokenList = require('./components/token-list') + +module.exports = connect(mapStateToProps)(AccountDetailScreen) + +function mapStateToProps (state) { + return { + metamask: state.metamask, + identities: state.metamask.identities, + accounts: state.metamask.accounts, + address: state.metamask.selectedAddress, + accountDetail: state.appState.accountDetail, + network: state.metamask.network, + unapprovedMsgs: valuesFor(state.metamask.unapprovedMsgs), + shapeShiftTxList: state.metamask.shapeShiftTxList, + transactions: state.metamask.selectedAddressTxList || [], + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + currentAccountTab: state.metamask.currentAccountTab, + tokens: state.metamask.tokens, + } +} + +inherits(AccountDetailScreen, Component) +function AccountDetailScreen () { + Component.call(this) +} + +AccountDetailScreen.prototype.render = function () { + var props = this.props + var selected = props.address || Object.keys(props.accounts)[0] + var checksumAddress = selected && ethUtil.toChecksumAddress(selected) + var identity = props.identities[selected] + var account = props.accounts[selected] + const { network, conversionRate, currentCurrency } = props + + return ( + + h('.account-detail-section', [ + + // identicon, label, balance, etc + h('.account-data-subsection', { + style: { + margin: '0 20px', + }, + }, [ + + // header - identicon + nav + h('div', { + style: { + paddingTop: '20px', + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'flex-start', + }, + }, [ + + // large identicon and addresses + h('.identicon-wrapper.select-none', [ + h(Identicon, { + diameter: 62, + address: selected, + }), + ]), + h('flex-column', { + style: { + lineHeight: '10px', + marginLeft: '15px', + }, + }, [ + h(EditableLabel, { + textValue: identity ? identity.name : '', + state: { + isEditingLabel: false, + }, + saveText: (text) => { + props.dispatch(actions.saveAccountLabel(selected, text)) + }, + }, [ + + // What is shown when not editing + edit text: + h('label.editing-label', [h('.edit-text', 'edit')]), + h('h2.font-medium.color-forest', {name: 'edit'}, identity && identity.name), + ]), + h('.flex-row', { + style: { + width: '15em', + justifyContent: 'space-between', + alignItems: 'baseline', + }, + }, [ + + // address + + h('div', { + style: { + overflow: 'hidden', + textOverflow: 'ellipsis', + paddingTop: '3px', + width: '5em', + fontSize: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + marginTop: '10px', + marginBottom: '15px', + color: '#AEAEAE', + }, + }, checksumAddress), + + // copy and export + + h('.flex-row', { + style: { + justifyContent: 'flex-end', + }, + }, [ + + h(AccountInfoLink, { selected, network }), + + h(CopyButton, { + value: checksumAddress, + }), + + h(Tooltip, { + title: 'QR Code', + }, [ + h('i.fa.fa-qrcode.pointer.pop-hover', { + onClick: () => props.dispatch(actions.showQrView(selected, identity ? identity.name : '')), + style: { + fontSize: '18px', + position: 'relative', + color: 'rgb(247, 134, 28)', + top: '5px', + marginLeft: '3px', + marginRight: '3px', + }, + }), + ]), + + h(Tooltip, { + title: 'Export Private Key', + }, [ + h('div', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + h('img.cursor-pointer.color-orange', { + src: 'images/key-32.png', + onClick: () => this.requestAccountExport(selected), + style: { + height: '19px', + }, + }), + ]), + ]), + ]), + ]), + + // account ballence + + ]), + ]), + h('.flex-row', { + style: { + justifyContent: 'space-between', + alignItems: 'flex-start', + }, + }, [ + + h(EthBalance, { + value: account && account.balance, + conversionRate, + currentCurrency, + style: { + lineHeight: '7px', + marginTop: '10px', + }, + }), + + h('button', { + onClick: () => props.dispatch(actions.buyEthView(selected)), + style: { + marginBottom: '20px', + marginRight: '8px', + position: 'absolute', + left: '219px', + }, + }, 'BUY'), + + h('button', { + onClick: () => props.dispatch(actions.showSendPage()), + style: { + marginBottom: '20px', + marginRight: '8px', + }, + }, 'SEND'), + + ]), + ]), + + // subview (tx history, pk export confirm, buy eth warning) + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + this.subview(), + ]), + + ]) + ) +} + +AccountDetailScreen.prototype.subview = function () { + var subview + try { + subview = this.props.accountDetail.subview + } catch (e) { + subview = null + } + + switch (subview) { + case 'transactions': + return this.tabSections() + case 'export': + var state = extend({key: 'export'}, this.props) + return h(ExportAccountView, state) + default: + return this.tabSections() + } +} + +AccountDetailScreen.prototype.tabSections = function () { + const { currentAccountTab } = this.props + + return h('section.tabSection', [ + + h(TabBar, { + tabs: [ + { content: 'Sent', key: 'history' }, + { content: 'Tokens', key: 'tokens' }, + ], + defaultTab: currentAccountTab || 'history', + tabSelected: (key) => { + this.props.dispatch(actions.setCurrentAccountTab(key)) + }, + }), + + this.tabSwitchView(), + ]) +} + +AccountDetailScreen.prototype.tabSwitchView = function () { + const props = this.props + const { address, network } = props + const { currentAccountTab, tokens } = this.props + + switch (currentAccountTab) { + case 'tokens': + return h(TokenList, { + userAddress: address, + network, + tokens, + addToken: () => this.props.dispatch(actions.showAddTokenPage()), + }) + default: + return this.transactionList() + } +} + +AccountDetailScreen.prototype.transactionList = function () { + const {transactions, unapprovedMsgs, address, + network, shapeShiftTxList, conversionRate } = this.props + + return h(TransactionList, { + transactions: transactions.sort((a, b) => b.time - a.time), + network, + unapprovedMsgs, + conversionRate, + address, + shapeShiftTxList, + viewPendingTx: (txId) => { + this.props.dispatch(actions.viewPendingTx(txId)) + }, + }) +} + +AccountDetailScreen.prototype.requestAccountExport = function () { + this.props.dispatch(actions.requestExportAccount()) +} diff --git a/ui/app/accounts/account-list-item.js b/ui/app/accounts/account-list-item.js new file mode 100644 index 000000000..10a0b6cc7 --- /dev/null +++ b/ui/app/accounts/account-list-item.js @@ -0,0 +1,91 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') + +const EthBalance = require('../components/eth-balance') +const CopyButton = require('../components/copyButton') +const Identicon = require('../components/identicon') + +module.exports = AccountListItem + +inherits(AccountListItem, Component) +function AccountListItem () { + Component.call(this) +} + +AccountListItem.prototype.render = function () { + const { identity, selectedAddress, accounts, onShowDetail, + conversionRate, currentCurrency } = this.props + + const checksumAddress = identity && identity.address && ethUtil.toChecksumAddress(identity.address) + const isSelected = selectedAddress === identity.address + const account = accounts[identity.address] + const selectedClass = isSelected ? '.selected' : '' + + return ( + h(`.accounts-list-option.flex-row.flex-space-between.pointer.hover-white${selectedClass}`, { + key: `account-panel-${identity.address}`, + onClick: (event) => onShowDetail(identity.address, event), + }, [ + + h('.identicon-wrapper.flex-column.flex-center.select-none', [ + this.pendingOrNot(), + this.indicateIfLoose(), + h(Identicon, { + address: identity.address, + imageify: true, + }), + ]), + + // account address, balance + h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', { + style: { + width: '200px', + }, + }, [ + h('span', identity.name), + h('span.font-small', { + style: { + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + }, checksumAddress), + h(EthBalance, { + value: account && account.balance, + currentCurrency, + conversionRate, + style: { + lineHeight: '7px', + marginTop: '10px', + }, + }), + ]), + + // copy button + h('.identity-copy.flex-column', { + style: { + margin: '0 20px', + }, + }, [ + h(CopyButton, { + value: checksumAddress, + }), + ]), + ]) + ) +} + +AccountListItem.prototype.indicateIfLoose = function () { + try { // Sometimes keyrings aren't loaded yet: + const type = this.props.keyring.type + const isLoose = type !== 'HD Key Tree' + return isLoose ? h('.keyring-label', 'LOOSE') : null + } catch (e) { return } +} + +AccountListItem.prototype.pendingOrNot = function () { + const pending = this.props.pending + if (pending.length === 0) return null + return h('.pending-dot', pending.length) +} diff --git a/ui/app/accounts/import/index.js b/ui/app/accounts/import/index.js new file mode 100644 index 000000000..97b387229 --- /dev/null +++ b/ui/app/accounts/import/index.js @@ -0,0 +1,100 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../../actions') +import Select from 'react-select' + +// Subviews +const JsonImportView = require('./json.js') +const PrivateKeyImportView = require('./private-key.js') + +const menuItems = [ + 'Private Key', + 'JSON File', +] + +module.exports = connect(mapStateToProps)(AccountImportSubview) + +function mapStateToProps (state) { + return { + menuItems, + } +} + +inherits(AccountImportSubview, Component) +function AccountImportSubview () { + Component.call(this) +} + +AccountImportSubview.prototype.render = function () { + const props = this.props + const state = this.state || {} + const { menuItems } = props + const { type } = state + + return ( + h('div', { + style: { + }, + }, [ + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + props.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Import Accounts'), + ]), + h('div', { + style: { + padding: '10px', + color: 'rgb(174, 174, 174)', + }, + }, [ + + h('h3', { style: { padding: '3px' } }, 'SELECT TYPE'), + + h('style', ` + .has-value.Select--single > .Select-control .Select-value .Select-value-label, .Select-value-label { + color: rgb(174,174,174); + } + `), + + h(Select, { + name: 'import-type-select', + clearable: false, + value: type || menuItems[0], + options: menuItems.map((type) => { + return { + value: type, + label: type, + } + }), + onChange: (opt) => { + this.setState({ type: opt.value }) + }, + }), + ]), + + this.renderImportView(), + ]) + ) +} + +AccountImportSubview.prototype.renderImportView = function () { + const props = this.props + const state = this.state || {} + const { type } = state + const { menuItems } = props + const current = type || menuItems[0] + + switch (current) { + case 'Private Key': + return h(PrivateKeyImportView) + case 'JSON File': + return h(JsonImportView) + default: + return h(JsonImportView) + } +} diff --git a/ui/app/accounts/import/json.js b/ui/app/accounts/import/json.js new file mode 100644 index 000000000..158a3c923 --- /dev/null +++ b/ui/app/accounts/import/json.js @@ -0,0 +1,100 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../../actions') +const FileInput = require('react-simple-file-input').default + +const HELP_LINK = 'https://github.com/MetaMask/faq/blob/master/README.md#q-i-cant-use-the-import-feature-for-uploading-a-json-file-the-window-keeps-closing-when-i-try-to-select-a-file' + +module.exports = connect(mapStateToProps)(JsonImportSubview) + +function mapStateToProps (state) { + return { + error: state.appState.warning, + } +} + +inherits(JsonImportSubview, Component) +function JsonImportSubview () { + Component.call(this) +} + +JsonImportSubview.prototype.render = function () { + const { error } = this.props + + return ( + h('div', { + style: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: '5px 15px 0px 15px', + }, + }, [ + + h('p', 'Used by a variety of different clients'), + h('a.warning', { href: HELP_LINK, target: '_blank' }, 'File import not working? Click here!'), + + h(FileInput, { + readAs: 'text', + onLoad: this.onLoad.bind(this), + style: { + margin: '20px 0px 12px 20px', + fontSize: '15px', + }, + }), + + h('input.large-input.letter-spacey', { + type: 'password', + placeholder: 'Enter password', + id: 'json-password-box', + onKeyPress: this.createKeyringOnEnter.bind(this), + style: { + width: 260, + marginTop: 12, + }, + }), + + h('button.primary', { + onClick: this.createNewKeychain.bind(this), + style: { + margin: 12, + }, + }, 'Import'), + + error ? h('span.error', error) : null, + ]) + ) +} + +JsonImportSubview.prototype.onLoad = function (event, file) { + this.setState({file: file, fileContents: event.target.result}) +} + +JsonImportSubview.prototype.createKeyringOnEnter = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.createNewKeychain() + } +} + +JsonImportSubview.prototype.createNewKeychain = function () { + const state = this.state + const { fileContents } = state + + if (!fileContents) { + const message = 'You must select a file to import.' + return this.props.dispatch(actions.displayWarning(message)) + } + + const passwordInput = document.getElementById('json-password-box') + const password = passwordInput.value + + if (!password) { + const message = 'You must enter a password for the selected file.' + return this.props.dispatch(actions.displayWarning(message)) + } + + this.props.dispatch(actions.importNewAccount('JSON File', [ fileContents, password ])) +} diff --git a/ui/app/accounts/import/private-key.js b/ui/app/accounts/import/private-key.js new file mode 100644 index 000000000..68ccee58e --- /dev/null +++ b/ui/app/accounts/import/private-key.js @@ -0,0 +1,67 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../../actions') + +module.exports = connect(mapStateToProps)(PrivateKeyImportView) + +function mapStateToProps (state) { + return { + error: state.appState.warning, + } +} + +inherits(PrivateKeyImportView, Component) +function PrivateKeyImportView () { + Component.call(this) +} + +PrivateKeyImportView.prototype.render = function () { + const { error } = this.props + + return ( + h('div', { + style: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: '5px 15px 0px 15px', + }, + }, [ + h('span', 'Paste your private key string here'), + + h('input.large-input.letter-spacey', { + type: 'password', + id: 'private-key-box', + onKeyPress: this.createKeyringOnEnter.bind(this), + style: { + width: 260, + marginTop: 12, + }, + }), + + h('button.primary', { + onClick: this.createNewKeychain.bind(this), + style: { + margin: 12, + }, + }, 'Import'), + + error ? h('span.error', error) : null, + ]) + ) +} + +PrivateKeyImportView.prototype.createKeyringOnEnter = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.createNewKeychain() + } +} + +PrivateKeyImportView.prototype.createNewKeychain = function () { + const input = document.getElementById('private-key-box') + const privateKey = input.value + this.props.dispatch(actions.importNewAccount('Private Key', [ privateKey ])) +} diff --git a/ui/app/accounts/import/seed.js b/ui/app/accounts/import/seed.js new file mode 100644 index 000000000..b4a7c0afa --- /dev/null +++ b/ui/app/accounts/import/seed.js @@ -0,0 +1,30 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect + +module.exports = connect(mapStateToProps)(SeedImportSubview) + +function mapStateToProps (state) { + return {} +} + +inherits(SeedImportSubview, Component) +function SeedImportSubview () { + Component.call(this) +} + +SeedImportSubview.prototype.render = function () { + return ( + h('div', { + style: { + }, + }, [ + `Paste your seed phrase here!`, + h('textarea'), + h('br'), + h('button', 'Submit'), + ]) + ) +} + diff --git a/ui/app/accounts/index.js b/ui/app/accounts/index.js new file mode 100644 index 000000000..ac2615cd7 --- /dev/null +++ b/ui/app/accounts/index.js @@ -0,0 +1,164 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../actions') +const valuesFor = require('../util').valuesFor +const findDOMNode = require('react-dom').findDOMNode +const AccountListItem = require('./account-list-item') + +module.exports = connect(mapStateToProps)(AccountsScreen) + +function mapStateToProps (state) { + const pendingTxs = valuesFor(state.metamask.unapprovedTxs) + .filter(txMeta => txMeta.metamaskNetworkId === state.metamask.network) + const pendingMsgs = valuesFor(state.metamask.unapprovedMsgs) + const pending = pendingTxs.concat(pendingMsgs) + + return { + accounts: state.metamask.accounts, + identities: state.metamask.identities, + unapprovedTxs: state.metamask.unapprovedTxs, + selectedAddress: state.metamask.selectedAddress, + scrollToBottom: state.appState.scrollToBottom, + pending, + keyrings: state.metamask.keyrings, + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + } +} + +inherits(AccountsScreen, Component) +function AccountsScreen () { + Component.call(this) +} + +AccountsScreen.prototype.render = function () { + const props = this.props + const { keyrings, conversionRate, currentCurrency } = props + const identityList = valuesFor(props.identities) + const unapprovedTxList = valuesFor(props.unapprovedTxs) + + return ( + + h('.accounts-section.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: this.goHome.bind(this), + }), + h('h2.page-subtitle', 'Select Account'), + ]), + + h('hr.horizontal-line'), + + // identity selection + h('section.identity-section', { + style: { + height: '418px', + overflowY: 'auto', + overflowX: 'hidden', + }, + }, + [ + identityList.map((identity) => { + const pending = this.props.pending.filter((txOrMsg) => { + if ('txParams' in txOrMsg) { + return txOrMsg.txParams.from === identity.address + } else if ('msgParams' in txOrMsg) { + return txOrMsg.msgParams.from === identity.address + } else { + return false + } + }) + + const simpleAddress = identity.address.substring(2).toLowerCase() + const keyring = keyrings.find((kr) => { + return kr.accounts.includes(simpleAddress) || + kr.accounts.includes(identity.address) + }) + + return h(AccountListItem, { + key: `acct-panel-${identity.address}`, + identity, + selectedAddress: this.props.selectedAddress, + conversionRate, + currentCurrency, + accounts: this.props.accounts, + onShowDetail: this.onShowDetail.bind(this), + pending, + keyring, + }) + }), + + h('hr.horizontal-line'), + h('div.footer.hover-white.pointer', { + key: 'reveal-account-bar', + onClick: () => { + this.addNewAccount() + }, + style: { + display: 'flex', + height: '40px', + padding: '10px', + justifyContent: 'center', + alignItems: 'center', + }, + }, [ + h('i.fa.fa-plus.fa-lg', {key: ''}), + ]), + h('hr.horizontal-line'), + ]), + + unapprovedTxList.length ? ( + + h('.unconftx-link.flex-row.flex-center', { + onClick: this.navigateToConfTx.bind(this), + }, [ + h('span', 'Unconfirmed Txs'), + h('i.fa.fa-arrow-right.fa-lg'), + ]) + + ) : ( + null + ), + ]) + ) +} + +// If a new account was revealed, scroll to the bottom +AccountsScreen.prototype.componentDidUpdate = function () { + const scrollToBottom = this.props.scrollToBottom + + if (scrollToBottom) { + var container = findDOMNode(this) + var scrollable = container.querySelector('.identity-section') + scrollable.scrollTop = scrollable.scrollHeight + } +} + +AccountsScreen.prototype.navigateToConfTx = function () { + event.stopPropagation() + this.props.dispatch(actions.showConfTxPage()) +} + +AccountsScreen.prototype.onShowDetail = function (address, event) { + event.stopPropagation() + this.props.dispatch(actions.showAccountDetail(address)) +} + +AccountsScreen.prototype.addNewAccount = function () { + this.props.dispatch(actions.addNewAccount(0)) +} + +/* An optional view proposed in this design: + * https://consensys.quip.com/zZVrAysM5znY +AccountsScreen.prototype.addNewAccount = function () { + this.props.dispatch(actions.navigateToNewAccountScreen()) +} +*/ + +AccountsScreen.prototype.goHome = function () { + this.props.dispatch(actions.goHome()) +} diff --git a/ui/app/actions.js b/ui/app/actions.js new file mode 100644 index 000000000..d99291e46 --- /dev/null +++ b/ui/app/actions.js @@ -0,0 +1,1031 @@ +const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url') + +var actions = { + _setBackgroundConnection: _setBackgroundConnection, + + GO_HOME: 'GO_HOME', + goHome: goHome, + // menu state + getNetworkStatus: 'getNetworkStatus', + // transition state + TRANSITION_FORWARD: 'TRANSITION_FORWARD', + TRANSITION_BACKWARD: 'TRANSITION_BACKWARD', + transitionForward, + transitionBackward, + // remote state + UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE', + updateMetamaskState: updateMetamaskState, + // notices + MARK_NOTICE_READ: 'MARK_NOTICE_READ', + markNoticeRead: markNoticeRead, + SHOW_NOTICE: 'SHOW_NOTICE', + showNotice: showNotice, + CLEAR_NOTICES: 'CLEAR_NOTICES', + clearNotices: clearNotices, + markAccountsFound, + // intialize screen + CREATE_NEW_VAULT_IN_PROGRESS: 'CREATE_NEW_VAULT_IN_PROGRESS', + SHOW_CREATE_VAULT: 'SHOW_CREATE_VAULT', + SHOW_RESTORE_VAULT: 'SHOW_RESTORE_VAULT', + FORGOT_PASSWORD: 'FORGOT_PASSWORD', + forgotPassword: forgotPassword, + SHOW_INIT_MENU: 'SHOW_INIT_MENU', + SHOW_NEW_VAULT_SEED: 'SHOW_NEW_VAULT_SEED', + SHOW_INFO_PAGE: 'SHOW_INFO_PAGE', + SHOW_IMPORT_PAGE: 'SHOW_IMPORT_PAGE', + unlockMetamask: unlockMetamask, + unlockFailed: unlockFailed, + showCreateVault: showCreateVault, + showRestoreVault: showRestoreVault, + showInitializeMenu: showInitializeMenu, + showImportPage, + createNewVaultAndKeychain: createNewVaultAndKeychain, + createNewVaultAndRestore: createNewVaultAndRestore, + createNewVaultInProgress: createNewVaultInProgress, + addNewKeyring, + importNewAccount, + addNewAccount, + NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN', + navigateToNewAccountScreen, + showNewVaultSeed: showNewVaultSeed, + showInfoPage: showInfoPage, + // seed recovery actions + REVEAL_SEED_CONFIRMATION: 'REVEAL_SEED_CONFIRMATION', + revealSeedConfirmation: revealSeedConfirmation, + requestRevealSeed: requestRevealSeed, + // unlock screen + UNLOCK_IN_PROGRESS: 'UNLOCK_IN_PROGRESS', + UNLOCK_FAILED: 'UNLOCK_FAILED', + UNLOCK_METAMASK: 'UNLOCK_METAMASK', + LOCK_METAMASK: 'LOCK_METAMASK', + tryUnlockMetamask: tryUnlockMetamask, + lockMetamask: lockMetamask, + unlockInProgress: unlockInProgress, + // error handling + displayWarning: displayWarning, + DISPLAY_WARNING: 'DISPLAY_WARNING', + HIDE_WARNING: 'HIDE_WARNING', + hideWarning: hideWarning, + // accounts screen + SET_SELECTED_ACCOUNT: 'SET_SELECTED_ACCOUNT', + SHOW_ACCOUNT_DETAIL: 'SHOW_ACCOUNT_DETAIL', + SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE', + SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE', + SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE', + SET_CURRENT_FIAT: 'SET_CURRENT_FIAT', + setCurrentCurrency: setCurrentCurrency, + setCurrentAccountTab, + // account detail screen + SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', + showSendPage: showSendPage, + ADD_TO_ADDRESS_BOOK: 'ADD_TO_ADDRESS_BOOK', + addToAddressBook: addToAddressBook, + REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT', + requestExportAccount: requestExportAccount, + EXPORT_ACCOUNT: 'EXPORT_ACCOUNT', + exportAccount: exportAccount, + SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY', + showPrivateKey: showPrivateKey, + SAVE_ACCOUNT_LABEL: 'SAVE_ACCOUNT_LABEL', + saveAccountLabel: saveAccountLabel, + // tx conf screen + COMPLETED_TX: 'COMPLETED_TX', + TRANSACTION_ERROR: 'TRANSACTION_ERROR', + NEXT_TX: 'NEXT_TX', + PREVIOUS_TX: 'PREV_TX', + signMsg: signMsg, + cancelMsg: cancelMsg, + signPersonalMsg, + cancelPersonalMsg, + sendTx: sendTx, + signTx: signTx, + updateAndApproveTx, + cancelTx: cancelTx, + completedTx: completedTx, + txError: txError, + nextTx: nextTx, + previousTx: previousTx, + viewPendingTx: viewPendingTx, + VIEW_PENDING_TX: 'VIEW_PENDING_TX', + // app messages + confirmSeedWords: confirmSeedWords, + showAccountDetail: showAccountDetail, + BACK_TO_ACCOUNT_DETAIL: 'BACK_TO_ACCOUNT_DETAIL', + backToAccountDetail: backToAccountDetail, + showAccountsPage: showAccountsPage, + showConfTxPage: showConfTxPage, + // config screen + SHOW_CONFIG_PAGE: 'SHOW_CONFIG_PAGE', + SET_RPC_TARGET: 'SET_RPC_TARGET', + SET_DEFAULT_RPC_TARGET: 'SET_DEFAULT_RPC_TARGET', + SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE', + USE_ETHERSCAN_PROVIDER: 'USE_ETHERSCAN_PROVIDER', + useEtherscanProvider: useEtherscanProvider, + showConfigPage, + SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', + showAddTokenPage, + addToken, + setRpcTarget: setRpcTarget, + setDefaultRpcTarget: setDefaultRpcTarget, + setProviderType: setProviderType, + // loading overlay + SHOW_LOADING: 'SHOW_LOADING_INDICATION', + HIDE_LOADING: 'HIDE_LOADING_INDICATION', + showLoadingIndication: showLoadingIndication, + hideLoadingIndication: hideLoadingIndication, + // buy Eth with coinbase + BUY_ETH: 'BUY_ETH', + buyEth: buyEth, + buyEthView: buyEthView, + BUY_ETH_VIEW: 'BUY_ETH_VIEW', + COINBASE_SUBVIEW: 'COINBASE_SUBVIEW', + coinBaseSubview: coinBaseSubview, + SHAPESHIFT_SUBVIEW: 'SHAPESHIFT_SUBVIEW', + shapeShiftSubview: shapeShiftSubview, + PAIR_UPDATE: 'PAIR_UPDATE', + pairUpdate: pairUpdate, + coinShiftRquest: coinShiftRquest, + SHOW_SUB_LOADING_INDICATION: 'SHOW_SUB_LOADING_INDICATION', + showSubLoadingIndication: showSubLoadingIndication, + HIDE_SUB_LOADING_INDICATION: 'HIDE_SUB_LOADING_INDICATION', + hideSubLoadingIndication: hideSubLoadingIndication, +// QR STUFF: + SHOW_QR: 'SHOW_QR', + showQrView: showQrView, + reshowQrCode: reshowQrCode, + SHOW_QR_VIEW: 'SHOW_QR_VIEW', +// FORGOT PASSWORD: + BACK_TO_INIT_MENU: 'BACK_TO_INIT_MENU', + goBackToInitView: goBackToInitView, + RECOVERY_IN_PROGRESS: 'RECOVERY_IN_PROGRESS', + BACK_TO_UNLOCK_VIEW: 'BACK_TO_UNLOCK_VIEW', + backToUnlockView: backToUnlockView, + // SHOWING KEYCHAIN + SHOW_NEW_KEYCHAIN: 'SHOW_NEW_KEYCHAIN', + showNewKeychain: showNewKeychain, + + callBackgroundThenUpdate, + forceUpdateMetamaskState, +} + +module.exports = actions + +var background = null +function _setBackgroundConnection (backgroundConnection) { + background = backgroundConnection +} + +function goHome () { + return { + type: actions.GO_HOME, + } +} + +// async actions + +function tryUnlockMetamask (password) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + dispatch(actions.unlockInProgress()) + log.debug(`background.submitPassword`) + background.submitPassword(password, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + dispatch(actions.unlockFailed(err.message)) + } else { + dispatch(actions.transitionForward()) + forceUpdateMetamaskState(dispatch) + } + }) + } +} + +function transitionForward () { + return { + type: this.TRANSITION_FORWARD, + } +} + +function transitionBackward () { + return { + type: this.TRANSITION_BACKWARD, + } +} + +function confirmSeedWords () { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.clearSeedWordCache`) + background.clearSeedWordCache((err, account) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + + log.info('Seed word cache cleared. ' + account) + dispatch(actions.showAccountDetail(account)) + }) + } +} + +function createNewVaultAndRestore (password, seed) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.createNewVaultAndRestore`) + background.createNewVaultAndRestore(password, seed, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.showAccountsPage()) + }) + } +} + +function createNewVaultAndKeychain (password) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.createNewVaultAndKeychain`) + background.createNewVaultAndKeychain(password, (err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + log.debug(`background.placeSeedWords`) + background.placeSeedWords((err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch(actions.hideLoadingIndication()) + forceUpdateMetamaskState(dispatch) + }) + }) + } +} + +function revealSeedConfirmation () { + return { + type: this.REVEAL_SEED_CONFIRMATION, + } +} + +function requestRevealSeed (password) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.submitPassword`) + background.submitPassword(password, (err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + log.debug(`background.placeSeedWords`) + background.placeSeedWords((err, result) => { + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.hideLoadingIndication()) + dispatch(actions.showNewVaultSeed(result)) + }) + }) + } +} + +function addNewKeyring (type, opts) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.addNewKeyring`) + background.addNewKeyring(type, opts, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.showAccountsPage()) + }) + } +} + +function importNewAccount (strategy, args) { + return (dispatch) => { + dispatch(actions.showLoadingIndication('This may take a while, be patient.')) + log.debug(`background.importAccountWithStrategy`) + background.importAccountWithStrategy(strategy, args, (err) => { + if (err) return dispatch(actions.displayWarning(err.message)) + log.debug(`background.getState`) + background.getState((err, newState) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch(actions.updateMetamaskState(newState)) + dispatch({ + type: actions.SHOW_ACCOUNT_DETAIL, + value: newState.selectedAddress, + }) + }) + }) + } +} + +function navigateToNewAccountScreen () { + return { + type: this.NEW_ACCOUNT_SCREEN, + } +} + +function addNewAccount () { + log.debug(`background.addNewAccount`) + return callBackgroundThenUpdate(background.addNewAccount) +} + +function showInfoPage () { + return { + type: actions.SHOW_INFO_PAGE, + } +} + +function setCurrentCurrency (currencyCode) { + return (dispatch) => { + dispatch(this.showLoadingIndication()) + log.debug(`background.setCurrentCurrency`) + background.setCurrentCurrency(currencyCode, (err, data) => { + dispatch(this.hideLoadingIndication()) + if (err) { + log.error(err.stack) + return dispatch(actions.displayWarning(err.message)) + } + dispatch({ + type: this.SET_CURRENT_FIAT, + value: { + currentCurrency: data.currentCurrency, + conversionRate: data.conversionRate, + conversionDate: data.conversionDate, + }, + }) + }) + } +} + +function signMsg (msgData) { + log.debug('action - signMsg') + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + + log.debug(`actions calling background.signMessage`) + background.signMessage(msgData, (err, newState) => { + log.debug('signMessage called back') + dispatch(actions.updateMetamaskState(newState)) + dispatch(actions.hideLoadingIndication()) + + if (err) log.error(err) + if (err) return dispatch(actions.displayWarning(err.message)) + + dispatch(actions.completedTx(msgData.metamaskId)) + }) + } +} + +function signPersonalMsg (msgData) { + log.debug('action - signPersonalMsg') + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + + log.debug(`actions calling background.signPersonalMessage`) + background.signPersonalMessage(msgData, (err, newState) => { + log.debug('signPersonalMessage called back') + dispatch(actions.updateMetamaskState(newState)) + dispatch(actions.hideLoadingIndication()) + + if (err) log.error(err) + if (err) return dispatch(actions.displayWarning(err.message)) + + dispatch(actions.completedTx(msgData.metamaskId)) + }) + } +} + +function signTx (txData) { + return (dispatch) => { + global.ethQuery.sendTransaction(txData, (err, data) => { + dispatch(actions.hideLoadingIndication()) + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.hideWarning()) + }) + dispatch(this.showConfTxPage()) + } +} + +function sendTx (txData) { + log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`) + return (dispatch) => { + log.debug(`actions calling background.approveTransaction`) + background.approveTransaction(txData.id, (err) => { + if (err) { + dispatch(actions.txError(err)) + return log.error(err.message) + } + dispatch(actions.completedTx(txData.id)) + }) + } +} + +function updateAndApproveTx (txData) { + log.info('actions: updateAndApproveTx: ' + JSON.stringify(txData)) + return (dispatch) => { + log.debug(`actions calling background.updateAndApproveTx`) + background.updateAndApproveTransaction(txData, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + dispatch(actions.txError(err)) + return log.error(err.message) + } + dispatch(actions.completedTx(txData.id)) + }) + } +} + +function completedTx (id) { + return { + type: actions.COMPLETED_TX, + value: id, + } +} + +function txError (err) { + return { + type: actions.TRANSACTION_ERROR, + message: err.message, + } +} + +function cancelMsg (msgData) { + log.debug(`background.cancelMessage`) + background.cancelMessage(msgData.id) + return actions.completedTx(msgData.id) +} + +function cancelPersonalMsg (msgData) { + const id = msgData.id + background.cancelPersonalMessage(id) + return actions.completedTx(id) +} + +function cancelTx (txData) { + log.debug(`background.cancelTransaction`) + background.cancelTransaction(txData.id) + return actions.completedTx(txData.id) +} + +// +// initialize screen +// + +function showCreateVault () { + return { + type: actions.SHOW_CREATE_VAULT, + } +} + +function showRestoreVault () { + return { + type: actions.SHOW_RESTORE_VAULT, + } +} + +function forgotPassword () { + return { + type: actions.FORGOT_PASSWORD, + } +} + +function showInitializeMenu () { + return { + type: actions.SHOW_INIT_MENU, + } +} + +function showImportPage () { + return { + type: actions.SHOW_IMPORT_PAGE, + } +} + +function createNewVaultInProgress () { + return { + type: actions.CREATE_NEW_VAULT_IN_PROGRESS, + } +} + +function showNewVaultSeed (seed) { + return { + type: actions.SHOW_NEW_VAULT_SEED, + value: seed, + } +} + +function backToUnlockView () { + return { + type: actions.BACK_TO_UNLOCK_VIEW, + } +} + +function showNewKeychain () { + return { + type: actions.SHOW_NEW_KEYCHAIN, + } +} + +// +// unlock screen +// + +function unlockInProgress () { + return { + type: actions.UNLOCK_IN_PROGRESS, + } +} + +function unlockFailed (message) { + return { + type: actions.UNLOCK_FAILED, + value: message, + } +} + +function unlockMetamask (account) { + return { + type: actions.UNLOCK_METAMASK, + value: account, + } +} + +function updateMetamaskState (newState) { + return { + type: actions.UPDATE_METAMASK_STATE, + value: newState, + } +} + +function lockMetamask () { + log.debug(`background.setLocked`) + return callBackgroundThenUpdate(background.setLocked) +} + +function setCurrentAccountTab (newTabName) { + log.debug(`background.setCurrentAccountTab: ${newTabName}`) + return callBackgroundThenUpdateNoSpinner(background.setCurrentAccountTab, newTabName) +} + +function showAccountDetail (address) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.setSelectedAddress`) + background.setSelectedAddress(address, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch({ + type: actions.SHOW_ACCOUNT_DETAIL, + value: address, + }) + }) + } +} + +function backToAccountDetail (address) { + return { + type: actions.BACK_TO_ACCOUNT_DETAIL, + value: address, + } +} + +function showAccountsPage () { + return { + type: actions.SHOW_ACCOUNTS_PAGE, + } +} + +function showConfTxPage (transForward = true) { + return { + type: actions.SHOW_CONF_TX_PAGE, + transForward: transForward, + } +} + +function nextTx () { + return { + type: actions.NEXT_TX, + } +} + +function viewPendingTx (txId) { + return { + type: actions.VIEW_PENDING_TX, + value: txId, + } +} + +function previousTx () { + return { + type: actions.PREVIOUS_TX, + } +} + +function showConfigPage (transitionForward = true) { + return { + type: actions.SHOW_CONFIG_PAGE, + value: transitionForward, + } +} + +function showAddTokenPage (transitionForward = true) { + return { + type: actions.SHOW_ADD_TOKEN_PAGE, + value: transitionForward, + } +} + +function addToken (address, symbol, decimals) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + background.addToken(address, symbol, decimals, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + setTimeout(() => { + dispatch(actions.goHome()) + }, 250) + }) + } +} + +function goBackToInitView () { + return { + type: actions.BACK_TO_INIT_MENU, + } +} + +// +// notice +// + +function markNoticeRead (notice) { + return (dispatch) => { + dispatch(this.showLoadingIndication()) + log.debug(`background.markNoticeRead`) + background.markNoticeRead(notice, (err, notice) => { + dispatch(this.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err)) + } + if (notice) { + return dispatch(actions.showNotice(notice)) + } else { + dispatch(this.clearNotices()) + return { + type: actions.SHOW_ACCOUNTS_PAGE, + } + } + }) + } +} + +function showNotice (notice) { + return { + type: actions.SHOW_NOTICE, + value: notice, + } +} + +function clearNotices () { + return { + type: actions.CLEAR_NOTICES, + } +} + +function markAccountsFound () { + log.debug(`background.markAccountsFound`) + return callBackgroundThenUpdate(background.markAccountsFound) +} + +// +// config +// + +// default rpc target refers to localhost:8545 in this instance. +function setDefaultRpcTarget (rpcList) { + log.debug(`background.setDefaultRpcTarget`) + return (dispatch) => { + background.setDefaultRpc((err, result) => { + if (err) { + log.error(err) + return dispatch(self.displayWarning('Had a problem changing networks.')) + } + }) + } +} + +function setRpcTarget (newRpc) { + log.debug(`background.setRpcTarget`) + return (dispatch) => { + background.setCustomRpc(newRpc, (err, result) => { + if (err) { + log.error(err) + return dispatch(self.displayWarning('Had a problem changing networks!')) + } + }) + } +} + +// Calls the addressBookController to add a new address. +function addToAddressBook (recipient, nickname) { + log.debug(`background.addToAddressBook`) + return (dispatch) => { + background.setAddressBook(recipient, nickname, (err, result) => { + if (err) { + log.error(err) + return dispatch(self.displayWarning('Address book failed to update')) + } + }) + } +} + +function setProviderType (type) { + log.debug(`background.setProviderType`) + background.setProviderType(type) + return { + type: actions.SET_PROVIDER_TYPE, + value: type, + } +} + +function useEtherscanProvider () { + log.debug(`background.useEtherscanProvider`) + background.useEtherscanProvider() + return { + type: actions.USE_ETHERSCAN_PROVIDER, + } +} + +function showLoadingIndication (message) { + return { + type: actions.SHOW_LOADING, + value: message, + } +} + +function hideLoadingIndication () { + return { + type: actions.HIDE_LOADING, + } +} + +function showSubLoadingIndication () { + return { + type: actions.SHOW_SUB_LOADING_INDICATION, + } +} + +function hideSubLoadingIndication () { + return { + type: actions.HIDE_SUB_LOADING_INDICATION, + } +} + +function displayWarning (text) { + return { + type: actions.DISPLAY_WARNING, + value: text, + } +} + +function hideWarning () { + return { + type: actions.HIDE_WARNING, + } +} + +function requestExportAccount () { + return { + type: actions.REQUEST_ACCOUNT_EXPORT, + } +} + +function exportAccount (password, address) { + var self = this + + return function (dispatch) { + dispatch(self.showLoadingIndication()) + + log.debug(`background.submitPassword`) + background.submitPassword(password, function (err) { + if (err) { + log.error('Error in submiting password.') + dispatch(self.hideLoadingIndication()) + return dispatch(self.displayWarning('Incorrect Password.')) + } + log.debug(`background.exportAccount`) + background.exportAccount(address, function (err, result) { + dispatch(self.hideLoadingIndication()) + + if (err) { + log.error(err) + return dispatch(self.displayWarning('Had a problem exporting the account.')) + } + + dispatch(self.showPrivateKey(result)) + }) + }) + } +} + +function showPrivateKey (key) { + return { + type: actions.SHOW_PRIVATE_KEY, + value: key, + } +} + +function saveAccountLabel (account, label) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.saveAccountLabel`) + background.saveAccountLabel(account, label, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch({ + type: actions.SAVE_ACCOUNT_LABEL, + value: { account, label }, + }) + }) + } +} + +function showSendPage () { + return { + type: actions.SHOW_SEND_PAGE, + } +} + +function buyEth (opts) { + return (dispatch) => { + const url = getBuyEthUrl(opts) + global.platform.openWindow({ url }) + dispatch({ + type: actions.BUY_ETH, + }) + } +} + +function buyEthView (address) { + return { + type: actions.BUY_ETH_VIEW, + value: address, + } +} + +function coinBaseSubview () { + return { + type: actions.COINBASE_SUBVIEW, + } +} + +function pairUpdate (coin) { + return (dispatch) => { + dispatch(actions.showSubLoadingIndication()) + dispatch(actions.hideWarning()) + shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { + dispatch(actions.hideSubLoadingIndication()) + dispatch({ + type: actions.PAIR_UPDATE, + value: { + marketinfo: mktResponse, + }, + }) + }) + } +} + +function shapeShiftSubview (network) { + var pair = 'btc_eth' + + return (dispatch) => { + dispatch(actions.showSubLoadingIndication()) + shapeShiftRequest('marketinfo', {pair}, (mktResponse) => { + shapeShiftRequest('getcoins', {}, (response) => { + dispatch(actions.hideSubLoadingIndication()) + if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) + dispatch({ + type: actions.SHAPESHIFT_SUBVIEW, + value: { + marketinfo: mktResponse, + coinOptions: response, + }, + }) + }) + }) + } +} + +function coinShiftRquest (data, marketData) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + shapeShiftRequest('shift', { method: 'POST', data}, (response) => { + dispatch(actions.hideLoadingIndication()) + if (response.error) return dispatch(actions.displayWarning(response.error)) + var message = ` + Deposit your ${response.depositType} to the address bellow:` + log.debug(`background.createShapeShiftTx`) + background.createShapeShiftTx(response.deposit, response.depositType) + dispatch(actions.showQrView(response.deposit, [message].concat(marketData))) + }) + } +} + +function showQrView (data, message) { + return { + type: actions.SHOW_QR_VIEW, + value: { + message: message, + data: data, + }, + } +} +function reshowQrCode (data, coin) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { + if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) + + var message = [ + `Deposit your ${coin} to the address bellow:`, + `Deposit Limit: ${mktResponse.limit}`, + `Deposit Minimum:${mktResponse.minimum}`, + ] + + dispatch(actions.hideLoadingIndication()) + return dispatch(actions.showQrView(data, message)) + }) + } +} + +function shapeShiftRequest (query, options, cb) { + var queryResponse, method + !options ? options = {} : null + options.method ? method = options.method : method = 'GET' + + var requestListner = function (request) { + queryResponse = JSON.parse(this.responseText) + cb ? cb(queryResponse) : null + return queryResponse + } + + var shapShiftReq = new XMLHttpRequest() + shapShiftReq.addEventListener('load', requestListner) + shapShiftReq.open(method, `https://shapeshift.io/${query}/${options.pair ? options.pair : ''}`, true) + + if (options.method === 'POST') { + var jsonObj = JSON.stringify(options.data) + shapShiftReq.setRequestHeader('Content-Type', 'application/json') + return shapShiftReq.send(jsonObj) + } else { + return shapShiftReq.send() + } +} + +// Call Background Then Update +// +// A function generator for a common pattern wherein: +// We show loading indication. +// We call a background method. +// We hide loading indication. +// If it errored, we show a warning. +// If it didn't, we update the state. +function callBackgroundThenUpdateNoSpinner (method, ...args) { + return (dispatch) => { + method.call(background, ...args, (err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + forceUpdateMetamaskState(dispatch) + }) + } +} + +function callBackgroundThenUpdate (method, ...args) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + method.call(background, ...args, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + forceUpdateMetamaskState(dispatch) + }) + } +} + +function forceUpdateMetamaskState (dispatch) { + log.debug(`background.getState`) + background.getState((err, newState) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch(actions.updateMetamaskState(newState)) + }) +} diff --git a/ui/app/add-token.js b/ui/app/add-token.js new file mode 100644 index 000000000..b303b5c0d --- /dev/null +++ b/ui/app/add-token.js @@ -0,0 +1,219 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') + +const ethUtil = require('ethereumjs-util') +const abi = require('human-standard-token-abi') +const Eth = require('ethjs-query') +const EthContract = require('ethjs-contract') + +const emptyAddr = '0x0000000000000000000000000000000000000000' + +module.exports = connect(mapStateToProps)(AddTokenScreen) + +function mapStateToProps (state) { + return { + } +} + +inherits(AddTokenScreen, Component) +function AddTokenScreen () { + this.state = { + warning: null, + address: null, + symbol: 'TOKEN', + decimals: 18, + } + Component.call(this) +} + +AddTokenScreen.prototype.render = function () { + const state = this.state + const props = this.props + const { warning, symbol, decimals } = state + + return ( + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + props.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Add Token'), + ]), + + h('.error', { + style: { + display: warning ? 'block' : 'none', + padding: '0 20px', + textAlign: 'center', + }, + }, warning), + + // conf view + h('.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-space-around', { + style: { + padding: '20px', + }, + }, [ + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Token Address'), + ]), + + h('section.flex-row.flex-center', [ + h('input#token-address', { + name: 'address', + placeholder: 'Token Address', + onChange: this.tokenAddressDidChange.bind(this), + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + }), + ]), + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Token Sybmol'), + ]), + + h('div', { style: {display: 'flex'} }, [ + h('input#token_symbol', { + placeholder: `Like "ETH"`, + value: symbol, + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onChange: (event) => { + var element = event.target + var symbol = element.value + this.setState({ symbol }) + }, + }), + ]), + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Decimals of Precision'), + ]), + + h('div', { style: {display: 'flex'} }, [ + h('input#token_decimals', { + value: decimals, + type: 'number', + min: 0, + max: 36, + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onChange: (event) => { + var element = event.target + var decimals = element.value.trim() + this.setState({ decimals }) + }, + }), + ]), + + h('button', { + style: { + alignSelf: 'center', + }, + onClick: (event) => { + const valid = this.validateInputs() + if (!valid) return + + const { address, symbol, decimals } = this.state + this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) + }, + }, 'Add'), + ]), + ]), + ]) + ) +} + +AddTokenScreen.prototype.componentWillMount = function () { + if (typeof global.ethereumProvider === 'undefined') return + + this.eth = new Eth(global.ethereumProvider) + this.contract = new EthContract(this.eth) + this.TokenContract = this.contract(abi) +} + +AddTokenScreen.prototype.tokenAddressDidChange = function (event) { + const el = event.target + const address = el.value.trim() + if (ethUtil.isValidAddress(address) && address !== emptyAddr) { + this.setState({ address }) + this.attemptToAutoFillTokenParams(address) + } +} + +AddTokenScreen.prototype.validateInputs = function () { + let msg = '' + const state = this.state + const { address, symbol, decimals } = state + + const validAddress = ethUtil.isValidAddress(address) + if (!validAddress) { + msg += 'Address is invalid. ' + } + + const validDecimals = decimals >= 0 && decimals < 36 + if (!validDecimals) { + msg += 'Decimals must be at least 0, and not over 36. ' + } + + const symbolLen = symbol.trim().length + const validSymbol = symbolLen > 0 && symbolLen < 10 + if (!validSymbol) { + msg += 'Symbol must be between 0 and 10 characters.' + } + + const isValid = validAddress && validDecimals + + if (!isValid) { + this.setState({ + warning: msg, + }) + } else { + this.setState({ warning: null }) + } + + return isValid +} + +AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) { + const contract = this.TokenContract.at(address) + + const results = await Promise.all([ + contract.symbol(), + contract.decimals(), + ]) + + const [ symbol, decimals ] = results + if (symbol && decimals) { + console.log('SETTING SYMBOL AND DECIMALS', { symbol, decimals }) + this.setState({ symbol: symbol[0], decimals: decimals[0].toString() }) + } +} + diff --git a/ui/app/app.js b/ui/app/app.js new file mode 100644 index 000000000..1a63002e1 --- /dev/null +++ b/ui/app/app.js @@ -0,0 +1,591 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('./actions') +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +// init +const InitializeMenuScreen = require('./first-time/init-menu') +const NewKeyChainScreen = require('./new-keychain') +// unlock +const UnlockScreen = require('./unlock') +// accounts +const AccountsScreen = require('./accounts') +const AccountDetailScreen = require('./account-detail') +const SendTransactionScreen = require('./send') +const ConfirmTxScreen = require('./conf-tx') +// notice +const NoticeScreen = require('./components/notice') +const generateLostAccountsNotice = require('../lib/lost-accounts-notice') +// other views +const ConfigScreen = require('./config') +const AddTokenScreen = require('./add-token') +const Import = require('./accounts/import') +const InfoScreen = require('./info') +const Loading = require('./components/loading') +const SandwichExpando = require('sandwich-expando') +const MenuDroppo = require('menu-droppo') +const DropMenuItem = require('./components/drop-menu-item') +const NetworkIndicator = require('./components/network') +const Tooltip = require('./components/tooltip') +const BuyView = require('./components/buy-button-subview') +const QrView = require('./components/qr-code') +const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete') +const HDRestoreVaultScreen = require('./keychains/hd/restore-vault') +const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation') + +module.exports = connect(mapStateToProps)(App) + +inherits(App, Component) +function App () { Component.call(this) } + +function mapStateToProps (state) { + return { + // state from plugin + isLoading: state.appState.isLoading, + loadingMessage: state.appState.loadingMessage, + noActiveNotices: state.metamask.noActiveNotices, + isInitialized: state.metamask.isInitialized, + isUnlocked: state.metamask.isUnlocked, + currentView: state.appState.currentView, + activeAddress: state.appState.activeAddress, + transForward: state.appState.transForward, + seedWords: state.metamask.seedWords, + unapprovedTxs: state.metamask.unapprovedTxs, + unapprovedMsgs: state.metamask.unapprovedMsgs, + menuOpen: state.appState.menuOpen, + network: state.metamask.network, + provider: state.metamask.provider, + forgottenPassword: state.appState.forgottenPassword, + lastUnreadNotice: state.metamask.lastUnreadNotice, + lostAccounts: state.metamask.lostAccounts, + frequentRpcList: state.metamask.frequentRpcList || [], + } +} + +App.prototype.render = function () { + var props = this.props + const { isLoading, loadingMessage, transForward, network } = props + const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config' + const loadMessage = loadingMessage || isLoadingNetwork ? + `Connecting to ${this.getNetworkName()}` : null + + log.debug('Main ui render function') + + return ( + + h('.flex-column.flex-grow.full-height', { + style: { + // Windows was showing a vertical scroll bar: + overflow: 'hidden', + position: 'relative', + }, + }, [ + + // app bar + this.renderAppBar(), + this.renderNetworkDropdown(), + this.renderDropdown(), + + h(Loading, { + isLoading: isLoading || isLoadingNetwork, + loadingMessage: loadMessage, + }), + + // panel content + h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), { + style: { + height: '380px', + width: '360px', + }, + }, [ + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + this.renderPrimary(), + ]), + ]), + ]) + ) +} + +App.prototype.renderAppBar = function () { + if (window.METAMASK_UI_TYPE === 'notification') { + return null + } + + const props = this.props + const state = this.state || {} + const isNetworkMenuOpen = state.isNetworkMenuOpen || false + + return ( + + h('div', [ + + h('.app-header.flex-row.flex-space-between', { + style: { + alignItems: 'center', + visibility: props.isUnlocked ? 'visible' : 'none', + background: props.isUnlocked ? 'white' : 'none', + height: '36px', + position: 'relative', + zIndex: 12, + }, + }, [ + + h('div.left-menu-section', { + style: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + }, + }, [ + + // mini logo + h('img', { + height: 24, + width: 24, + src: '/images/icon-128.png', + }), + + h(NetworkIndicator, { + network: this.props.network, + provider: this.props.provider, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen }) + }, + }), + ]), + + // metamask name + props.isUnlocked && h('h1', { + style: { + position: 'relative', + left: '9px', + }, + }, 'MetaMask'), + + props.isUnlocked && h('div', { + style: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + }, + }, [ + + // small accounts nav + props.isUnlocked && h(Tooltip, { title: 'Switch Accounts' }, [ + h('img.cursor-pointer.color-orange', { + src: 'images/switch_acc.svg', + style: { + width: '23.5px', + marginRight: '8px', + }, + onClick: (event) => { + event.stopPropagation() + this.props.dispatch(actions.showAccountsPage()) + }, + }), + ]), + + // hamburger + props.isUnlocked && h(SandwichExpando, { + width: 16, + barHeight: 2, + padding: 0, + isOpen: state.isMainMenuOpen, + color: 'rgb(247,146,30)', + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + this.setState({ isMainMenuOpen: !state.isMainMenuOpen }) + }, + }), + ]), + ]), + ]) + ) +} + +App.prototype.renderNetworkDropdown = function () { + const props = this.props + const rpcList = props.frequentRpcList + const state = this.state || {} + const isOpen = state.isNetworkMenuOpen + + return h(MenuDroppo, { + isOpen, + onClickOutside: (event) => { + this.setState({ isNetworkMenuOpen: !isOpen }) + }, + zIndex: 11, + style: { + position: 'absolute', + left: 0, + top: '36px', + }, + innerStyle: { + background: 'white', + boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', + }, + }, [ // DROP MENU ITEMS + h('style', ` + .drop-menu-item:hover { background:rgb(235, 235, 235); } + .drop-menu-item i { margin: 11px; } + `), + + h(DropMenuItem, { + label: 'Main Ethereum Network', + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setProviderType('mainnet')), + icon: h('.menu-icon.diamond'), + activeNetworkRender: props.network, + provider: props.provider, + }), + + h(DropMenuItem, { + label: 'Ropsten Test Network', + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setProviderType('ropsten')), + icon: h('.menu-icon.red-dot'), + activeNetworkRender: props.network, + provider: props.provider, + }), + + h(DropMenuItem, { + label: 'Kovan Test Network', + closeMenu: () => this.setState({ isNetworkMenuOpen: false}), + action: () => props.dispatch(actions.setProviderType('kovan')), + icon: h('.menu-icon.hollow-diamond'), + activeNetworkRender: props.network, + provider: props.provider, + }), + + h(DropMenuItem, { + label: 'Rinkeby Test Network', + closeMenu: () => this.setState({ isNetworkMenuOpen: false}), + action: () => props.dispatch(actions.setProviderType('rinkeby')), + icon: h('.menu-icon.golden-square'), + activeNetworkRender: props.network, + provider: props.provider, + }), + + h(DropMenuItem, { + label: 'Localhost 8545', + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setDefaultRpcTarget(rpcList)), + icon: h('i.fa.fa-question-circle.fa-lg'), + activeNetworkRender: props.provider.rpcTarget, + }), + + this.renderCustomOption(props.provider), + this.renderCommonRpc(rpcList, props.provider), + + h(DropMenuItem, { + label: 'Custom RPC', + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => this.props.dispatch(actions.showConfigPage()), + icon: h('i.fa.fa-question-circle.fa-lg'), + }), + + ]) +} + +App.prototype.renderDropdown = function () { + const state = this.state || {} + const isOpen = state.isMainMenuOpen + + return h(MenuDroppo, { + isOpen: isOpen, + zIndex: 11, + onClickOutside: (event) => { + this.setState({ isMainMenuOpen: !isOpen }) + }, + style: { + position: 'absolute', + right: 0, + top: '36px', + }, + innerStyle: { + background: 'white', + boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', + }, + }, [ // DROP MENU ITEMS + h('style', ` + .drop-menu-item:hover { background:rgb(235, 235, 235); } + .drop-menu-item i { margin: 11px; } + `), + + h(DropMenuItem, { + label: 'Settings', + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + action: () => this.props.dispatch(actions.showConfigPage()), + icon: h('i.fa.fa-gear.fa-lg'), + }), + + h(DropMenuItem, { + label: 'Import Account', + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + action: () => this.props.dispatch(actions.showImportPage()), + icon: h('i.fa.fa-arrow-circle-o-up.fa-lg'), + }), + + h(DropMenuItem, { + label: 'Lock', + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + action: () => this.props.dispatch(actions.lockMetamask()), + icon: h('i.fa.fa-lock.fa-lg'), + }), + + h(DropMenuItem, { + label: 'Info/Help', + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + action: () => this.props.dispatch(actions.showInfoPage()), + icon: h('i.fa.fa-question.fa-lg'), + }), + ]) +} + +App.prototype.renderBackButton = function (style, justArrow = false) { + var props = this.props + return ( + h('.flex-row', { + key: 'leftArrow', + style: style, + onClick: () => props.dispatch(actions.goBackToInitView()), + }, [ + h('i.fa.fa-arrow-left.cursor-pointer'), + justArrow ? null : h('div.cursor-pointer', { + style: { + marginLeft: '3px', + }, + onClick: () => props.dispatch(actions.goBackToInitView()), + }, 'BACK'), + ]) + ) +} + +App.prototype.renderPrimary = function () { + log.debug('rendering primary') + var props = this.props + + // notices + if (!props.noActiveNotices) { + log.debug('rendering notice screen for unread notices.') + return h(NoticeScreen, { + notice: props.lastUnreadNotice, + key: 'NoticeScreen', + onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)), + }) + } else if (props.lostAccounts && props.lostAccounts.length > 0) { + log.debug('rendering notice screen for lost accounts view.') + return h(NoticeScreen, { + notice: generateLostAccountsNotice(props.lostAccounts), + key: 'LostAccountsNotice', + onConfirm: () => props.dispatch(actions.markAccountsFound()), + }) + } + + if (props.seedWords) { + log.debug('rendering seed words') + return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'}) + } + + // show initialize screen + if (!props.isInitialized || props.forgottenPassword) { + // show current view + log.debug('rendering an initialize screen') + switch (props.currentView.name) { + + case 'restoreVault': + log.debug('rendering restore vault screen') + return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) + + default: + log.debug('rendering menu screen') + return h(InitializeMenuScreen, {key: 'menuScreenInit'}) + } + } + + // show unlock screen + if (!props.isUnlocked) { + switch (props.currentView.name) { + + case 'restoreVault': + log.debug('rendering restore vault screen') + return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) + + case 'config': + log.debug('rendering config screen from unlock screen.') + return h(ConfigScreen, {key: 'config'}) + + default: + log.debug('rendering locked screen') + return h(UnlockScreen, {key: 'locked'}) + } + } + + // show current view + switch (props.currentView.name) { + + case 'accounts': + log.debug('rendering accounts screen') + return h(AccountsScreen, {key: 'accounts'}) + + case 'accountDetail': + log.debug('rendering account detail screen') + return h(AccountDetailScreen, {key: 'account-detail'}) + + case 'sendTransaction': + log.debug('rendering send tx screen') + return h(SendTransactionScreen, {key: 'send-transaction'}) + + case 'newKeychain': + log.debug('rendering new keychain screen') + return h(NewKeyChainScreen, {key: 'new-keychain'}) + + case 'confTx': + log.debug('rendering confirm tx screen') + return h(ConfirmTxScreen, {key: 'confirm-tx'}) + + case 'add-token': + log.debug('rendering add-token screen from unlock screen.') + return h(AddTokenScreen, {key: 'add-token'}) + + case 'config': + log.debug('rendering config screen') + return h(ConfigScreen, {key: 'config'}) + + case 'import-menu': + log.debug('rendering import screen') + return h(Import, {key: 'import-menu'}) + + case 'reveal-seed-conf': + log.debug('rendering reveal seed confirmation screen') + return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'}) + + case 'info': + log.debug('rendering info screen') + return h(InfoScreen, {key: 'info'}) + + case 'buyEth': + log.debug('rendering buy ether screen') + return h(BuyView, {key: 'buyEthView'}) + + case 'qr': + log.debug('rendering show qr screen') + return h('div', { + style: { + position: 'absolute', + height: '100%', + top: '0px', + left: '0px', + }, + }, [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { + onClick: () => props.dispatch(actions.backToAccountDetail(props.activeAddress)), + style: { + marginLeft: '10px', + marginTop: '50px', + }, + }), + h('div', { + style: { + position: 'absolute', + left: '44px', + width: '285px', + }, + }, [ + h(QrView, {key: 'qr'}), + ]), + ]) + + default: + log.debug('rendering default, account detail screen') + return h(AccountDetailScreen, {key: 'account-detail'}) + } +} + +App.prototype.toggleMetamaskActive = function () { + if (!this.props.isUnlocked) { + // currently inactive: redirect to password box + var passwordBox = document.querySelector('input[type=password]') + if (!passwordBox) return + passwordBox.focus() + } else { + // currently active: deactivate + this.props.dispatch(actions.lockMetamask(false)) + } +} + +App.prototype.renderCustomOption = function (provider) { + const { rpcTarget, type } = provider + if (type !== 'rpc') return null + + // Concatenate long URLs + let label = rpcTarget + if (rpcTarget.length > 31) { + label = label.substr(0, 34) + '...' + } + + switch (rpcTarget) { + + case 'http://localhost:8545': + return null + + default: + return h(DropMenuItem, { + label, + key: rpcTarget, + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + icon: h('i.fa.fa-question-circle.fa-lg'), + activeNetworkRender: 'custom', + }) + } +} + +App.prototype.getNetworkName = function () { + const { provider } = this.props + const providerName = provider.type + + let name + + if (providerName === 'mainnet') { + name = 'Main Ethereum Network' + } else if (providerName === 'ropsten') { + name = 'Ropsten Test Network' + } else if (providerName === 'kovan') { + name = 'Kovan Test Network' + } else if (providerName === 'rinkeby') { + name = 'Rinkeby Test Network' + } else { + name = 'Unknown Private Network' + } + + return name +} + +App.prototype.renderCommonRpc = function (rpcList, provider) { + const { rpcTarget } = provider + const props = this.props + + return rpcList.map((rpc) => { + if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { + return null + } else { + return h(DropMenuItem, { + label: rpc, + key: rpc, + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setRpcTarget(rpc)), + icon: h('i.fa.fa-question-circle.fa-lg'), + activeNetworkRender: rpc, + }) + } + }) +} diff --git a/ui/app/components/account-export.js b/ui/app/components/account-export.js new file mode 100644 index 000000000..394d878f7 --- /dev/null +++ b/ui/app/components/account-export.js @@ -0,0 +1,122 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const copyToClipboard = require('copy-to-clipboard') +const actions = require('../actions') +const ethUtil = require('ethereumjs-util') +const connect = require('react-redux').connect + +module.exports = connect(mapStateToProps)(ExportAccountView) + +inherits(ExportAccountView, Component) +function ExportAccountView () { + Component.call(this) +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +ExportAccountView.prototype.render = function () { + var state = this.props + var accountDetail = state.accountDetail + + if (!accountDetail) return h('div') + var accountExport = accountDetail.accountExport + + var notExporting = accountExport === 'none' + var exportRequested = accountExport === 'requested' + var accountExported = accountExport === 'completed' + + if (notExporting) return h('div') + + if (exportRequested) { + var warning = `Export private keys at your own risk.` + return ( + h('div', { + style: { + display: 'inline-block', + textAlign: 'center', + }, + }, + [ + h('div', { + key: 'exporting', + style: { + margin: '0 20px', + }, + }, [ + h('p.error', warning), + h('input#exportAccount.sizing-input', { + type: 'password', + placeholder: 'confirm password', + onKeyPress: this.onExportKeyPress.bind(this), + style: { + position: 'relative', + top: '1.5px', + marginBottom: '7px', + }, + }), + ]), + h('div', { + key: 'buttons', + style: { + margin: '0 20px', + }, + }, + [ + h('button', { + onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }), + style: { + marginRight: '10px', + }, + }, 'Submit'), + h('button', { + onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), + }, 'Cancel'), + ]), + (this.props.warning) && ( + h('span.error', { + style: { + margin: '20px', + }, + }, this.props.warning.split('-')) + ), + ]) + ) + } + + if (accountExported) { + return h('div.privateKey', { + style: { + margin: '0 20px', + }, + }, [ + h('label', 'Your private key (click to copy):'), + h('p.error.cursor-pointer', { + style: { + textOverflow: 'ellipsis', + overflow: 'hidden', + webkitUserSelect: 'text', + width: '100%', + }, + onClick: function (event) { + copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey)) + }, + }, ethUtil.stripHexPrefix(accountDetail.privateKey)), + h('button', { + onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), + }, 'Done'), + ]) + } +} + +ExportAccountView.prototype.onExportKeyPress = function (event) { + if (event.key !== 'Enter') return + event.preventDefault() + + var input = document.getElementById('exportAccount').value + this.props.dispatch(actions.exportAccount(input, this.props.address)) +} diff --git a/ui/app/components/account-info-link.js b/ui/app/components/account-info-link.js new file mode 100644 index 000000000..6526ab502 --- /dev/null +++ b/ui/app/components/account-info-link.js @@ -0,0 +1,41 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Tooltip = require('./tooltip') +const genAccountLink = require('../../lib/account-link') + +module.exports = AccountInfoLink + +inherits(AccountInfoLink, Component) +function AccountInfoLink () { + Component.call(this) +} + +AccountInfoLink.prototype.render = function () { + const { selected, network } = this.props + const title = 'View account on Etherscan' + const url = genAccountLink(selected, network) + + if (!url) { + return null + } + + return h('.account-info-link', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + + h(Tooltip, { + title, + }, [ + h('i.fa.fa-info-circle.cursor-pointer.color-orange', { + style: { + margin: '5px', + }, + onClick () { global.platform.openWindow({ url }) }, + }), + ]), + ]) +} diff --git a/ui/app/components/account-panel.js b/ui/app/components/account-panel.js new file mode 100644 index 000000000..abaaf8163 --- /dev/null +++ b/ui/app/components/account-panel.js @@ -0,0 +1,86 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const Identicon = require('./identicon') +const formatBalance = require('../util').formatBalance +const addressSummary = require('../util').addressSummary + +module.exports = AccountPanel + + +inherits(AccountPanel, Component) +function AccountPanel () { + Component.call(this) +} + +AccountPanel.prototype.render = function () { + var state = this.props + var identity = state.identity || {} + var account = state.account || {} + var isFauceting = state.isFauceting + + var panelState = { + key: `accountPanel${identity.address}`, + identiconKey: identity.address, + identiconLabel: identity.name || '', + attributes: [ + { + key: 'ADDRESS', + value: addressSummary(identity.address), + }, + balanceOrFaucetingIndication(account, isFauceting), + ], + } + + return ( + + h('.identity-panel.flex-row.flex-space-between', { + style: { + flex: '1 0 auto', + cursor: panelState.onClick ? 'pointer' : undefined, + }, + onClick: panelState.onClick, + }, [ + + // account identicon + h('.identicon-wrapper.flex-column.select-none', [ + h(Identicon, { + address: panelState.identiconKey, + imageify: state.imageifyIdenticons, + }), + h('span.font-small', panelState.identiconLabel.substring(0, 7) + '...'), + ]), + + // account address, balance + h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [ + + panelState.attributes.map((attr) => { + return h('.flex-row.flex-space-between', { + key: '' + Math.round(Math.random() * 1000000), + }, [ + h('label.font-small.no-select', attr.key), + h('span.font-small', attr.value), + ]) + }), + ]), + + ]) + + ) +} + +function balanceOrFaucetingIndication (account, isFauceting) { + // Temporarily deactivating isFauceting indication + // because it shows fauceting for empty restored accounts. + if (/* isFauceting*/ false) { + return { + key: 'Account is auto-funding.', + value: 'Please wait.', + } + } else { + return { + key: 'BALANCE', + value: formatBalance(account.balance), + } + } +} diff --git a/ui/app/components/balance.js b/ui/app/components/balance.js new file mode 100644 index 000000000..57ca84564 --- /dev/null +++ b/ui/app/components/balance.js @@ -0,0 +1,89 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const formatBalance = require('../util').formatBalance +const generateBalanceObject = require('../util').generateBalanceObject +const Tooltip = require('./tooltip.js') +const FiatValue = require('./fiat-value.js') + +module.exports = EthBalanceComponent + +inherits(EthBalanceComponent, Component) +function EthBalanceComponent () { + Component.call(this) +} + +EthBalanceComponent.prototype.render = function () { + var props = this.props + let { value } = props + var style = props.style + var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true + value = value ? formatBalance(value, 6, needsParse) : '...' + var width = props.width + + return ( + + h('.ether-balance.ether-balance-amount', { + style: style, + }, [ + h('div', { + style: { + display: 'inline', + width: width, + }, + }, this.renderBalance(value)), + ]) + + ) +} +EthBalanceComponent.prototype.renderBalance = function (value) { + var props = this.props + if (value === 'None') return value + if (value === '...') return value + var balanceObj = generateBalanceObject(value, props.shorten ? 1 : 3) + var balance + var splitBalance = value.split(' ') + var ethNumber = splitBalance[0] + var ethSuffix = splitBalance[1] + const showFiat = 'showFiat' in props ? props.showFiat : true + + if (props.shorten) { + balance = balanceObj.shortBalance + } else { + balance = balanceObj.balance + } + + var label = balanceObj.label + + return ( + h(Tooltip, { + position: 'bottom', + title: `${ethNumber} ${ethSuffix}`, + }, h('div.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('div', { + style: { + width: '100%', + textAlign: 'right', + }, + }, this.props.incoming ? `+${balance}` : balance), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + }, + }, label), + ]), + + showFiat ? h(FiatValue, { value: props.value }) : null, + ])) + ) +} diff --git a/ui/app/components/binary-renderer.js b/ui/app/components/binary-renderer.js new file mode 100644 index 000000000..0b6a1f5c2 --- /dev/null +++ b/ui/app/components/binary-renderer.js @@ -0,0 +1,46 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const extend = require('xtend') + +module.exports = BinaryRenderer + +inherits(BinaryRenderer, Component) +function BinaryRenderer () { + Component.call(this) +} + +BinaryRenderer.prototype.render = function () { + const props = this.props + const { value, style } = props + const text = this.hexToText(value) + + const defaultStyle = extend({ + width: '315px', + maxHeight: '210px', + resize: 'none', + border: 'none', + background: 'white', + padding: '3px', + }, style) + + return ( + h('textarea.font-small', { + readOnly: true, + style: defaultStyle, + defaultValue: text, + }) + ) +} + +BinaryRenderer.prototype.hexToText = function (hex) { + try { + const stripped = ethUtil.stripHexPrefix(hex) + const buff = Buffer.from(stripped, 'hex') + return buff.toString('utf8') + } catch (e) { + return hex + } +} + diff --git a/ui/app/components/bn-as-decimal-input.js b/ui/app/components/bn-as-decimal-input.js new file mode 100644 index 000000000..f3ace4720 --- /dev/null +++ b/ui/app/components/bn-as-decimal-input.js @@ -0,0 +1,174 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const extend = require('xtend') + +module.exports = BnAsDecimalInput + +inherits(BnAsDecimalInput, Component) +function BnAsDecimalInput () { + this.state = { invalid: null } + Component.call(this) +} + +/* Bn as Decimal Input + * + * A component for allowing easy, decimal editing + * of a passed in bn string value. + * + * On change, calls back its `onChange` function parameter + * and passes it an updated bn string. + */ + +BnAsDecimalInput.prototype.render = function () { + const props = this.props + const state = this.state + + const { value, scale, precision, onChange, min, max } = props + + const suffix = props.suffix + const style = props.style + const valueString = value.toString(10) + const newValue = this.downsize(valueString, scale, precision) + + return ( + h('.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('input.hex-input', { + type: 'number', + step: 'any', + required: true, + min, + max, + style: extend({ + display: 'block', + textAlign: 'right', + backgroundColor: 'transparent', + border: '1px solid #bdbdbd', + + }, style), + value: newValue, + onBlur: (event) => { + this.updateValidity(event) + }, + onChange: (event) => { + this.updateValidity(event) + const value = (event.target.value === '') ? '' : event.target.value + + + const scaledNumber = this.upsize(value, scale, precision) + const precisionBN = new BN(scaledNumber, 10) + onChange(precisionBN, event.target.checkValidity()) + }, + onInvalid: (event) => { + const msg = this.constructWarning() + if (msg === state.invalid) { + return + } + this.setState({ invalid: msg }) + event.preventDefault() + return false + }, + }), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + marginRight: '6px', + width: '20px', + }, + }, suffix), + ]), + + state.invalid ? h('span.error', { + style: { + position: 'absolute', + right: '0px', + textAlign: 'right', + transform: 'translateY(26px)', + padding: '3px', + background: 'rgba(255,255,255,0.85)', + zIndex: '1', + textTransform: 'capitalize', + border: '2px solid #E20202', + }, + }, state.invalid) : null, + ]) + ) +} + +BnAsDecimalInput.prototype.setValid = function (message) { + this.setState({ invalid: null }) +} + +BnAsDecimalInput.prototype.updateValidity = function (event) { + const target = event.target + const value = this.props.value + const newValue = target.value + + if (value === newValue) { + return + } + + const valid = target.checkValidity() + + if (valid) { + this.setState({ invalid: null }) + } +} + +BnAsDecimalInput.prototype.constructWarning = function () { + const { name, min, max } = this.props + let message = name ? name + ' ' : '' + + if (min && max) { + message += `must be greater than or equal to ${min} and less than or equal to ${max}.` + } else if (min) { + message += `must be greater than or equal to ${min}.` + } else if (max) { + message += `must be less than or equal to ${max}.` + } else { + message += 'Invalid input.' + } + + return message +} + + +BnAsDecimalInput.prototype.downsize = function (number, scale, precision) { + // if there is no scaling, simply return the number + if (scale === 0) { + return Number(number) + } else { + // if the scale is the same as the precision, account for this edge case. + var decimals = (scale === precision) ? -1 : scale - precision + return Number(number.slice(0, -scale) + '.' + number.slice(-scale, decimals)) + } +} + +BnAsDecimalInput.prototype.upsize = function (number, scale, precision) { + var stringArray = number.toString().split('.') + var decimalLength = stringArray[1] ? stringArray[1].length : 0 + var newString = stringArray[0] + + // If there is scaling and decimal parts exist, integrate them in. + if ((scale !== 0) && (decimalLength !== 0)) { + newString += stringArray[1].slice(0, precision) + } + + // Add 0s to account for the upscaling. + for (var i = decimalLength; i < scale; i++) { + newString += '0' + } + return newString +} diff --git a/ui/app/components/buy-button-subview.js b/ui/app/components/buy-button-subview.js new file mode 100644 index 000000000..87084f92d --- /dev/null +++ b/ui/app/components/buy-button-subview.js @@ -0,0 +1,197 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const actions = require('../actions') +const CoinbaseForm = require('./coinbase-form') +const ShapeshiftForm = require('./shapeshift-form') +const Loading = require('./loading') +const AccountPanel = require('./account-panel') +const RadioList = require('./custom-radio-list') + +module.exports = connect(mapStateToProps)(BuyButtonSubview) + +function mapStateToProps (state) { + return { + identity: state.appState.identity, + account: state.metamask.accounts[state.appState.buyView.buyAddress], + warning: state.appState.warning, + buyView: state.appState.buyView, + network: state.metamask.network, + provider: state.metamask.provider, + context: state.appState.currentView.context, + isSubLoading: state.appState.isSubLoading, + } +} + +inherits(BuyButtonSubview, Component) +function BuyButtonSubview () { + Component.call(this) +} + +BuyButtonSubview.prototype.render = function () { + const props = this.props + const isLoading = props.isSubLoading + + return ( + h('.buy-eth-section.flex-column', { + style: { + alignItems: 'center', + }, + }, [ + // back button + h('.flex-row', { + style: { + alignItems: 'center', + justifyContent: 'center', + }, + }, [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { + onClick: this.backButtonContext.bind(this), + style: { + position: 'absolute', + left: '10px', + }, + }), + h('h2.text-transform-uppercase.flex-center', { + style: { + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', + }, + }, 'Buy Eth'), + ]), + h('div', { + style: { + position: 'absolute', + top: '57vh', + left: '49vw', + }, + }, [ + h(Loading, {isLoading}), + ]), + h('div', { + style: { + width: '80%', + }, + }, [ + h(AccountPanel, { + showFullAddress: true, + identity: props.identity, + account: props.account, + }), + ]), + h('h3.text-transform-uppercase', { + style: { + paddingLeft: '15px', + fontFamily: 'Montserrat Light', + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', + }, + }, 'Select Service'), + h('.flex-row.selected-exchange', { + style: { + position: 'relative', + right: '35px', + marginTop: '20px', + marginBottom: '20px', + }, + }, [ + h(RadioList, { + defaultFocus: props.buyView.subview, + labels: [ + 'Coinbase', + 'ShapeShift', + ], + subtext: { + 'Coinbase': 'Crypto/FIAT (USA only)', + 'ShapeShift': 'Crypto', + }, + onClick: this.radioHandler.bind(this), + }), + ]), + h('h3.text-transform-uppercase', { + style: { + paddingLeft: '15px', + fontFamily: 'Montserrat Light', + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', + }, + }, props.buyView.subview), + this.formVersionSubview(), + ]) + ) +} + +BuyButtonSubview.prototype.formVersionSubview = function () { + const network = this.props.network + if (network === '1') { + if (this.props.buyView.formView.coinbase) { + return h(CoinbaseForm, this.props) + } else if (this.props.buyView.formView.shapeshift) { + return h(ShapeshiftForm, this.props) + } + } else { + return h('div.flex-column', { + style: { + alignItems: 'center', + margin: '50px', + }, + }, [ + h('h3.text-transform-uppercase', { + style: { + width: '225px', + marginBottom: '15px', + }, + }, 'In order to access this feature, please switch to the Main Network'), + ((network === '3') || (network === '4') || (network === '42')) ? h('h3.text-transform-uppercase', 'or go to the') : null, + (network === '3') ? h('button.text-transform-uppercase', { + onClick: () => this.props.dispatch(actions.buyEth({ network })), + style: { + marginTop: '15px', + }, + }, 'Ropsten Test Faucet') : null, + (network === '4') ? h('button.text-transform-uppercase', { + onClick: () => this.props.dispatch(actions.buyEth({ network })), + style: { + marginTop: '15px', + }, + }, 'Rinkeby Test Faucet') : null, + (network === '42') ? h('button.text-transform-uppercase', { + onClick: () => this.props.dispatch(actions.buyEth({ network })), + style: { + marginTop: '15px', + }, + }, 'Kovan Test Faucet') : null, + ]) + } +} + +BuyButtonSubview.prototype.navigateTo = function (url) { + global.platform.openWindow({ url }) +} + +BuyButtonSubview.prototype.backButtonContext = function () { + if (this.props.context === 'confTx') { + this.props.dispatch(actions.showConfTxPage(false)) + } else { + this.props.dispatch(actions.goHome()) + } +} + +BuyButtonSubview.prototype.radioHandler = function (event) { + switch (event.target.title) { + case 'Coinbase': + return this.props.dispatch(actions.coinBaseSubview()) + case 'ShapeShift': + return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type)) + } +} diff --git a/ui/app/components/coinbase-form.js b/ui/app/components/coinbase-form.js new file mode 100644 index 000000000..f44d86045 --- /dev/null +++ b/ui/app/components/coinbase-form.js @@ -0,0 +1,63 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const actions = require('../actions') + +module.exports = connect(mapStateToProps)(CoinbaseForm) + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +inherits(CoinbaseForm, Component) + +function CoinbaseForm () { + Component.call(this) +} + +CoinbaseForm.prototype.render = function () { + var props = this.props + + return h('.flex-column', { + style: { + marginTop: '35px', + padding: '25px', + width: '100%', + }, + }, [ + h('.flex-row', { + style: { + justifyContent: 'space-around', + margin: '33px', + marginTop: '0px', + }, + }, [ + h('button.btn-green', { + onClick: this.toCoinbase.bind(this), + }, 'Continue to Coinbase'), + + h('button.btn-red', { + onClick: () => props.dispatch(actions.backTobuyView(props.accounts.address)), + }, 'Cancel'), + ]), + ]) +} + +CoinbaseForm.prototype.toCoinbase = function () { + const props = this.props + const address = props.buyView.buyAddress + props.dispatch(actions.buyEth({ network: '1', address, amount: 0 })) +} + +CoinbaseForm.prototype.renderLoading = function () { + return h('img', { + style: { + width: '27px', + marginRight: '-27px', + }, + src: 'images/loading.svg', + }) +} diff --git a/ui/app/components/copyButton.js b/ui/app/components/copyButton.js new file mode 100644 index 000000000..a25d0719c --- /dev/null +++ b/ui/app/components/copyButton.js @@ -0,0 +1,59 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const copyToClipboard = require('copy-to-clipboard') + +const Tooltip = require('./tooltip') + +module.exports = CopyButton + +inherits(CopyButton, Component) +function CopyButton () { + Component.call(this) +} + +// As parameters, accepts: +// "value", which is the value to copy (mandatory) +// "title", which is the text to show on hover (optional, defaults to 'Copy') +CopyButton.prototype.render = function () { + const props = this.props + const state = this.state || {} + + const value = props.value + const copied = state.copied + + const message = copied ? 'Copied' : props.title || ' Copy ' + + return h('.copy-button', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + + h(Tooltip, { + title: message, + }, [ + h('i.fa.fa-clipboard.cursor-pointer.color-orange', { + style: { + margin: '5px', + }, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(value) + this.debounceRestore() + }, + }), + ]), + + ]) +} + +CopyButton.prototype.debounceRestore = function () { + this.setState({ copied: true }) + clearTimeout(this.timeout) + this.timeout = setTimeout(() => { + this.setState({ copied: false }) + }, 850) +} diff --git a/ui/app/components/copyable.js b/ui/app/components/copyable.js new file mode 100644 index 000000000..a4f6f4bc6 --- /dev/null +++ b/ui/app/components/copyable.js @@ -0,0 +1,46 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const Tooltip = require('./tooltip') +const copyToClipboard = require('copy-to-clipboard') + +module.exports = Copyable + +inherits(Copyable, Component) +function Copyable () { + Component.call(this) + this.state = { + copied: false, + } +} + +Copyable.prototype.render = function () { + const props = this.props + const state = this.state + const { value, children } = props + const { copied } = state + + return h(Tooltip, { + title: copied ? 'Copied!' : 'Copy', + position: 'bottom', + }, h('span', { + style: { + cursor: 'pointer', + }, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(value) + this.debounceRestore() + }, + }, children)) +} + +Copyable.prototype.debounceRestore = function () { + this.setState({ copied: true }) + clearTimeout(this.timeout) + this.timeout = setTimeout(() => { + this.setState({ copied: false }) + }, 850) +} diff --git a/ui/app/components/custom-radio-list.js b/ui/app/components/custom-radio-list.js new file mode 100644 index 000000000..a4c525396 --- /dev/null +++ b/ui/app/components/custom-radio-list.js @@ -0,0 +1,60 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = RadioList + +inherits(RadioList, Component) +function RadioList () { + Component.call(this) +} + +RadioList.prototype.render = function () { + const props = this.props + const activeClass = '.custom-radio-selected' + const inactiveClass = '.custom-radio-inactive' + const { + labels, + defaultFocus, + } = props + + + return ( + h('.flex-row', { + style: { + fontSize: '12px', + }, + }, [ + h('.flex-column.custom-radios', { + style: { + marginRight: '5px', + }, + }, + labels.map((lable, i) => { + let isSelcted = (this.state !== null) + isSelcted = isSelcted ? (this.state.selected === lable) : (defaultFocus === lable) + return h(isSelcted ? activeClass : inactiveClass, { + title: lable, + onClick: (event) => { + this.setState({selected: event.target.title}) + props.onClick(event) + }, + }) + }) + ), + h('.text', {}, + labels.map((lable) => { + if (props.subtext) { + return h('.flex-row', {}, [ + h('.radio-titles', lable), + h('.radio-titles-subtext', `- ${props.subtext[lable]}`), + ]) + } else { + return h('.radio-titles', lable) + } + }) + ), + ]) + ) +} + diff --git a/ui/app/components/drop-menu-item.js b/ui/app/components/drop-menu-item.js new file mode 100644 index 000000000..e42948209 --- /dev/null +++ b/ui/app/components/drop-menu-item.js @@ -0,0 +1,59 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = DropMenuItem + +inherits(DropMenuItem, Component) +function DropMenuItem () { + Component.call(this) +} + +DropMenuItem.prototype.render = function () { + return h('li.drop-menu-item', { + onClick: () => { + this.props.closeMenu() + this.props.action() + }, + style: { + listStyle: 'none', + padding: '6px 16px 6px 5px', + fontFamily: 'Montserrat Regular', + color: 'rgb(125, 128, 130)', + cursor: 'pointer', + display: 'flex', + justifyContent: 'flex-start', + }, + }, [ + this.props.icon, + this.props.label, + this.activeNetworkRender(), + ]) +} + +DropMenuItem.prototype.activeNetworkRender = function () { + const activeNetwork = this.props.activeNetworkRender + const { provider } = this.props + const providerType = provider ? provider.type : null + if (activeNetwork === undefined) return + + switch (this.props.label) { + case 'Main Ethereum Network': + if (providerType === 'mainnet') return h('.check', '✓') + break + case 'Ropsten Test Network': + if (providerType === 'ropsten') return h('.check', '✓') + break + case 'Kovan Test Network': + if (providerType === 'kovan') return h('.check', '✓') + break + case 'Rinkeby Test Network': + if (providerType === 'rinkeby') return h('.check', '✓') + break + case 'Localhost 8545': + if (activeNetwork === 'http://localhost:8545') return h('.check', '✓') + break + default: + if (activeNetwork === 'custom') return h('.check', '✓') + } +} diff --git a/ui/app/components/editable-label.js b/ui/app/components/editable-label.js new file mode 100644 index 000000000..41936f5e0 --- /dev/null +++ b/ui/app/components/editable-label.js @@ -0,0 +1,51 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const findDOMNode = require('react-dom').findDOMNode + +module.exports = EditableLabel + +inherits(EditableLabel, Component) +function EditableLabel () { + Component.call(this) +} + +EditableLabel.prototype.render = function () { + const props = this.props + const state = this.state + + if (state && state.isEditingLabel) { + return h('div.editable-label', [ + h('input.sizing-input', { + defaultValue: props.textValue, + maxLength: '20', + onKeyPress: (event) => { + this.saveIfEnter(event) + }, + }), + h('button.editable-button', { + onClick: () => this.saveText(), + }, 'Save'), + ]) + } else { + return h('div.name-label', { + onClick: (event) => { + this.setState({ isEditingLabel: true }) + }, + }, this.props.children) + } +} + +EditableLabel.prototype.saveIfEnter = function (event) { + if (event.key === 'Enter') { + this.saveText() + } +} + +EditableLabel.prototype.saveText = function () { + var container = findDOMNode(this) + var text = container.querySelector('.editable-label input').value + var truncatedText = text.substring(0, 20) + this.props.saveText(truncatedText) + this.setState({ isEditingLabel: false, textLabel: truncatedText }) +} diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js new file mode 100644 index 000000000..3a33ebf74 --- /dev/null +++ b/ui/app/components/ens-input.js @@ -0,0 +1,170 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const extend = require('xtend') +const debounce = require('debounce') +const copyToClipboard = require('copy-to-clipboard') +const ENS = require('ethjs-ens') +const networkMap = require('ethjs-ens/lib/network-map.json') +const ensRE = /.+\.eth$/ +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + + +module.exports = EnsInput + +inherits(EnsInput, Component) +function EnsInput () { + Component.call(this) +} + +EnsInput.prototype.render = function () { + const props = this.props + const opts = extend(props, { + list: 'addresses', + onChange: () => { + const network = this.props.network + const networkHasEnsSupport = getNetworkEnsSupport(network) + if (!networkHasEnsSupport) return + + const recipient = document.querySelector('input[name="address"]').value + if (recipient.match(ensRE) === null) { + return this.setState({ + loadingEns: false, + ensResolution: null, + ensFailure: null, + }) + } + + this.setState({ + loadingEns: true, + }) + this.checkName() + }, + }) + return h('div', { + style: { width: '100%' }, + }, [ + h('input.large-input', opts), + // The address book functionality. + h('datalist#addresses', + [ + // Corresponds to the addresses owned. + Object.keys(props.identities).map((key) => { + const identity = props.identities[key] + return h('option', { + value: identity.address, + label: identity.name, + key: identity.address, + }) + }), + // Corresponds to previously sent-to addresses. + props.addressBook.map((identity) => { + return h('option', { + value: identity.address, + label: identity.name, + key: identity.address, + }) + }), + ]), + this.ensIcon(), + ]) +} + +EnsInput.prototype.componentDidMount = function () { + const network = this.props.network + const networkHasEnsSupport = getNetworkEnsSupport(network) + this.setState({ ensResolution: ZERO_ADDRESS }) + + if (networkHasEnsSupport) { + const provider = global.ethereumProvider + this.ens = new ENS({ provider, network }) + this.checkName = debounce(this.lookupEnsName.bind(this), 200) + } +} + +EnsInput.prototype.lookupEnsName = function () { + const recipient = document.querySelector('input[name="address"]').value + const { ensResolution } = this.state + + log.info(`ENS attempting to resolve name: ${recipient}`) + this.ens.lookup(recipient.trim()) + .then((address) => { + if (address === ZERO_ADDRESS) throw new Error('No address has been set for this name.') + if (address !== ensResolution) { + this.setState({ + loadingEns: false, + ensResolution: address, + nickname: recipient.trim(), + hoverText: address + '\nClick to Copy', + ensFailure: false, + }) + } + }) + .catch((reason) => { + log.error(reason) + return this.setState({ + loadingEns: false, + ensResolution: ZERO_ADDRESS, + ensFailure: true, + hoverText: reason.message, + }) + }) +} + +EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) { + const state = this.state || {} + const ensResolution = state.ensResolution + // If an address is sent without a nickname, meaning not from ENS or from + // the user's own accounts, a default of a one-space string is used. + const nickname = state.nickname || ' ' + if (prevState && ensResolution && this.props.onChange && + ensResolution !== prevState.ensResolution) { + this.props.onChange(ensResolution, nickname) + } +} + +EnsInput.prototype.ensIcon = function (recipient) { + const { hoverText } = this.state || {} + return h('span', { + title: hoverText, + style: { + position: 'absolute', + padding: '9px', + transform: 'translatex(-40px)', + }, + }, this.ensIconContents(recipient)) +} + +EnsInput.prototype.ensIconContents = function (recipient) { + const { loadingEns, ensFailure, ensResolution } = this.state || { ensResolution: ZERO_ADDRESS} + + if (loadingEns) { + return h('img', { + src: 'images/loading.svg', + style: { + width: '30px', + height: '30px', + transform: 'translateY(-6px)', + }, + }) + } + + if (ensFailure) { + return h('i.fa.fa-warning.fa-lg.warning') + } + + if (ensResolution && (ensResolution !== ZERO_ADDRESS)) { + return h('i.fa.fa-check-circle.fa-lg.cursor-pointer', { + style: { color: 'green' }, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(ensResolution) + }, + }) + } +} + +function getNetworkEnsSupport (network) { + return Boolean(networkMap[network]) +} diff --git a/ui/app/components/eth-balance.js b/ui/app/components/eth-balance.js new file mode 100644 index 000000000..4f538fd31 --- /dev/null +++ b/ui/app/components/eth-balance.js @@ -0,0 +1,89 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const formatBalance = require('../util').formatBalance +const generateBalanceObject = require('../util').generateBalanceObject +const Tooltip = require('./tooltip.js') +const FiatValue = require('./fiat-value.js') + +module.exports = EthBalanceComponent + +inherits(EthBalanceComponent, Component) +function EthBalanceComponent () { + Component.call(this) +} + +EthBalanceComponent.prototype.render = function () { + var props = this.props + let { value } = props + const { style, width } = props + var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true + value = value ? formatBalance(value, 6, needsParse) : '...' + + return ( + + h('.ether-balance.ether-balance-amount', { + style, + }, [ + h('div', { + style: { + display: 'inline', + width, + }, + }, this.renderBalance(value)), + ]) + + ) +} +EthBalanceComponent.prototype.renderBalance = function (value) { + var props = this.props + const { conversionRate, shorten, incoming, currentCurrency } = props + if (value === 'None') return value + if (value === '...') return value + var balanceObj = generateBalanceObject(value, shorten ? 1 : 3) + var balance + var splitBalance = value.split(' ') + var ethNumber = splitBalance[0] + var ethSuffix = splitBalance[1] + const showFiat = 'showFiat' in props ? props.showFiat : true + + if (shorten) { + balance = balanceObj.shortBalance + } else { + balance = balanceObj.balance + } + + var label = balanceObj.label + + return ( + h(Tooltip, { + position: 'bottom', + title: `${ethNumber} ${ethSuffix}`, + }, h('div.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('div', { + style: { + width: '100%', + textAlign: 'right', + }, + }, incoming ? `+${balance}` : balance), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + }, + }, label), + ]), + + showFiat ? h(FiatValue, { value: props.value, conversionRate, currentCurrency }) : null, + ])) + ) +} diff --git a/ui/app/components/fiat-value.js b/ui/app/components/fiat-value.js new file mode 100644 index 000000000..8a64a1cfc --- /dev/null +++ b/ui/app/components/fiat-value.js @@ -0,0 +1,63 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const formatBalance = require('../util').formatBalance + +module.exports = FiatValue + +inherits(FiatValue, Component) +function FiatValue () { + Component.call(this) +} + +FiatValue.prototype.render = function () { + const props = this.props + const { conversionRate, currentCurrency } = props + + const value = formatBalance(props.value, 6) + + if (value === 'None') return value + var fiatDisplayNumber, fiatTooltipNumber + var splitBalance = value.split(' ') + + if (conversionRate !== 0) { + fiatTooltipNumber = Number(splitBalance[0]) * conversionRate + fiatDisplayNumber = fiatTooltipNumber.toFixed(2) + } else { + fiatDisplayNumber = 'N/A' + fiatTooltipNumber = 'Unknown' + } + + return fiatDisplay(fiatDisplayNumber, currentCurrency) +} + +function fiatDisplay (fiatDisplayNumber, fiatSuffix) { + if (fiatDisplayNumber !== 'N/A') { + return h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('div', { + style: { + width: '100%', + textAlign: 'right', + fontSize: '12px', + color: '#333333', + }, + }, fiatDisplayNumber), + h('div', { + style: { + color: '#AEAEAE', + marginLeft: '5px', + fontSize: '12px', + }, + }, fiatSuffix), + ]) + } else { + return h('div') + } +} diff --git a/ui/app/components/hex-as-decimal-input.js b/ui/app/components/hex-as-decimal-input.js new file mode 100644 index 000000000..4a71e9585 --- /dev/null +++ b/ui/app/components/hex-as-decimal-input.js @@ -0,0 +1,154 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const extend = require('xtend') + +module.exports = HexAsDecimalInput + +inherits(HexAsDecimalInput, Component) +function HexAsDecimalInput () { + this.state = { invalid: null } + Component.call(this) +} + +/* Hex as Decimal Input + * + * A component for allowing easy, decimal editing + * of a passed in hex string value. + * + * On change, calls back its `onChange` function parameter + * and passes it an updated hex string. + */ + +HexAsDecimalInput.prototype.render = function () { + const props = this.props + const state = this.state + + const { value, onChange, min, max } = props + + const toEth = props.toEth + const suffix = props.suffix + const decimalValue = decimalize(value, toEth) + const style = props.style + + return ( + h('.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('input.hex-input', { + type: 'number', + required: true, + min: min, + max: max, + style: extend({ + display: 'block', + textAlign: 'right', + backgroundColor: 'transparent', + border: '1px solid #bdbdbd', + + }, style), + value: parseInt(decimalValue), + onBlur: (event) => { + this.updateValidity(event) + }, + onChange: (event) => { + this.updateValidity(event) + const hexString = (event.target.value === '') ? '' : hexify(event.target.value) + onChange(hexString) + }, + onInvalid: (event) => { + const msg = this.constructWarning() + if (msg === state.invalid) { + return + } + this.setState({ invalid: msg }) + event.preventDefault() + return false + }, + }), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + marginRight: '6px', + width: '20px', + }, + }, suffix), + ]), + + state.invalid ? h('span.error', { + style: { + position: 'absolute', + right: '0px', + textAlign: 'right', + transform: 'translateY(26px)', + padding: '3px', + background: 'rgba(255,255,255,0.85)', + zIndex: '1', + textTransform: 'capitalize', + border: '2px solid #E20202', + }, + }, state.invalid) : null, + ]) + ) +} + +HexAsDecimalInput.prototype.setValid = function (message) { + this.setState({ invalid: null }) +} + +HexAsDecimalInput.prototype.updateValidity = function (event) { + const target = event.target + const value = this.props.value + const newValue = target.value + + if (value === newValue) { + return + } + + const valid = target.checkValidity() + if (valid) { + this.setState({ invalid: null }) + } +} + +HexAsDecimalInput.prototype.constructWarning = function () { + const { name, min, max } = this.props + let message = name ? name + ' ' : '' + + if (min && max) { + message += `must be greater than or equal to ${min} and less than or equal to ${max}.` + } else if (min) { + message += `must be greater than or equal to ${min}.` + } else if (max) { + message += `must be less than or equal to ${max}.` + } else { + message += 'Invalid input.' + } + + return message +} + +function hexify (decimalString) { + const hexBN = new BN(parseInt(decimalString), 10) + return '0x' + hexBN.toString('hex') +} + +function decimalize (input, toEth) { + if (input === '') { + return '' + } else { + const strippedInput = ethUtil.stripHexPrefix(input) + const inputBN = new BN(strippedInput, 'hex') + return inputBN.toString(10) + } +} diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js new file mode 100644 index 000000000..c754bc6ba --- /dev/null +++ b/ui/app/components/identicon.js @@ -0,0 +1,72 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const isNode = require('detect-node') +const findDOMNode = require('react-dom').findDOMNode +const jazzicon = require('jazzicon') +const iconFactoryGen = require('../../lib/icon-factory') +const iconFactory = iconFactoryGen(jazzicon) + +module.exports = IdenticonComponent + +inherits(IdenticonComponent, Component) +function IdenticonComponent () { + Component.call(this) + + this.defaultDiameter = 46 +} + +IdenticonComponent.prototype.render = function () { + var props = this.props + var diameter = props.diameter || this.defaultDiameter + return ( + h('div', { + key: 'identicon-' + this.props.address, + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: diameter, + width: diameter, + borderRadius: diameter / 2, + overflow: 'hidden', + }, + }) + ) +} + +IdenticonComponent.prototype.componentDidMount = function () { + var props = this.props + const { address } = props + + if (!address) return + + var container = findDOMNode(this) + + var diameter = props.diameter || this.defaultDiameter + if (!isNode) { + var img = iconFactory.iconForAddress(address, diameter) + container.appendChild(img) + } +} + +IdenticonComponent.prototype.componentDidUpdate = function () { + var props = this.props + const { address } = props + + if (!address) return + + var container = findDOMNode(this) + + var children = container.children + for (var i = 0; i < children.length; i++) { + container.removeChild(children[i]) + } + + var diameter = props.diameter || this.defaultDiameter + if (!isNode) { + var img = iconFactory.iconForAddress(address, diameter) + container.appendChild(img) + } +} + diff --git a/ui/app/components/loading.js b/ui/app/components/loading.js new file mode 100644 index 000000000..87d6f5d20 --- /dev/null +++ b/ui/app/components/loading.js @@ -0,0 +1,53 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') + + +inherits(LoadingIndicator, Component) +module.exports = LoadingIndicator + +function LoadingIndicator () { + Component.call(this) +} + +LoadingIndicator.prototype.render = function () { + const { isLoading, loadingMessage } = this.props + + return ( + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'loader', + transitionEnterTimeout: 150, + transitionLeaveTimeout: 150, + }, [ + + isLoading ? h('div', { + style: { + zIndex: 10, + position: 'absolute', + flexDirection: 'column', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '100%', + width: '100%', + background: 'rgba(255, 255, 255, 0.8)', + }, + }, [ + h('img', { + src: 'images/loading.svg', + }), + + h('br'), + + showMessageIfAny(loadingMessage), + ]) : null, + ]) + ) +} + +function showMessageIfAny (loadingMessage) { + if (!loadingMessage) return null + return h('span', loadingMessage) +} diff --git a/ui/app/components/mascot.js b/ui/app/components/mascot.js new file mode 100644 index 000000000..973ec2cad --- /dev/null +++ b/ui/app/components/mascot.js @@ -0,0 +1,59 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const metamaskLogo = require('metamask-logo') +const debounce = require('debounce') + +module.exports = Mascot + +inherits(Mascot, Component) +function Mascot () { + Component.call(this) + this.logo = metamaskLogo({ + followMouse: true, + pxNotRatio: true, + width: 200, + height: 200, + }) + + this.refollowMouse = debounce(this.logo.setFollowMouse.bind(this.logo, true), 1000) + this.unfollowMouse = this.logo.setFollowMouse.bind(this.logo, false) +} + +Mascot.prototype.render = function () { + // this is a bit hacky + // the event emitter is on `this.props` + // and we dont get that until render + this.handleAnimationEvents() + + return h('#metamask-mascot-container', { + style: { zIndex: 0 }, + }) +} + +Mascot.prototype.componentDidMount = function () { + var targetDivId = 'metamask-mascot-container' + var container = document.getElementById(targetDivId) + container.appendChild(this.logo.container) +} + +Mascot.prototype.componentWillUnmount = function () { + this.animations = this.props.animationEventEmitter + this.animations.removeAllListeners() + this.logo.container.remove() + this.logo.stopAnimation() +} + +Mascot.prototype.handleAnimationEvents = function () { + // only setup listeners once + if (this.animations) return + this.animations = this.props.animationEventEmitter + this.animations.on('point', this.lookAt.bind(this)) + this.animations.on('setFollowMouse', this.logo.setFollowMouse.bind(this.logo)) +} + +Mascot.prototype.lookAt = function (target) { + this.unfollowMouse() + this.logo.lookAt(target) + this.refollowMouse() +} diff --git a/ui/app/components/mini-account-panel.js b/ui/app/components/mini-account-panel.js new file mode 100644 index 000000000..c09cf5b7a --- /dev/null +++ b/ui/app/components/mini-account-panel.js @@ -0,0 +1,74 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const Identicon = require('./identicon') + +module.exports = AccountPanel + + +inherits(AccountPanel, Component) +function AccountPanel () { + Component.call(this) +} + +AccountPanel.prototype.render = function () { + var props = this.props + var picOrder = props.picOrder || 'left' + const { imageSeed } = props + + return ( + + h('.identity-panel.flex-row.flex-left', { + style: { + cursor: props.onClick ? 'pointer' : undefined, + }, + onClick: props.onClick, + }, [ + + this.genIcon(imageSeed, picOrder), + + h('div.flex-column.flex-justify-center', { + style: { + lineHeight: '15px', + order: 2, + display: 'flex', + alignItems: picOrder === 'left' ? 'flex-begin' : 'flex-end', + }, + }, this.props.children), + ]) + ) +} + +AccountPanel.prototype.genIcon = function (seed, picOrder) { + const props = this.props + + // When there is no seed value, this is a contract creation. + // We then show the contract icon. + if (!seed) { + return h('.identicon-wrapper.flex-column.select-none', { + style: { + order: picOrder === 'left' ? 1 : 3, + }, + }, [ + h('i.fa.fa-file-text-o.fa-lg', { + style: { + fontSize: '42px', + transform: 'translate(0px, -16px)', + }, + }), + ]) + } + + // If there was a seed, we return an identicon for that address. + return h('.identicon-wrapper.flex-column.select-none', { + style: { + order: picOrder === 'left' ? 1 : 3, + }, + }, [ + h(Identicon, { + address: seed, + imageify: props.imageifyIdenticons, + }), + ]) +} + diff --git a/ui/app/components/network.js b/ui/app/components/network.js new file mode 100644 index 000000000..698a0bbb9 --- /dev/null +++ b/ui/app/components/network.js @@ -0,0 +1,124 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = Network + +inherits(Network, Component) + +function Network () { + Component.call(this) +} + +Network.prototype.render = function () { + const props = this.props + const networkNumber = props.network + let providerName + try { + providerName = props.provider.type + } catch (e) { + providerName = null + } + let iconName, hoverText + + if (networkNumber === 'loading') { + return h('span', { + style: { + display: 'flex', + alignItems: 'center', + flexDirection: 'row', + }, + onClick: (event) => this.props.onClick(event), + }, [ + h('img', { + title: 'Attempting to connect to blockchain.', + style: { + width: '27px', + }, + src: 'images/loading.svg', + }), + h('i.fa.fa-sort-desc'), + ]) + } else if (providerName === 'mainnet') { + hoverText = 'Main Ethereum Network' + iconName = 'ethereum-network' + } else if (providerName === 'ropsten') { + hoverText = 'Ropsten Test Network' + iconName = 'ropsten-test-network' + } else if (parseInt(networkNumber) === 3) { + hoverText = 'Ropsten Test Network' + iconName = 'ropsten-test-network' + } else if (providerName === 'kovan') { + hoverText = 'Kovan Test Network' + iconName = 'kovan-test-network' + } else if (providerName === 'rinkeby') { + hoverText = 'Rinkeby Test Network' + iconName = 'rinkeby-test-network' + } else { + hoverText = 'Unknown Private Network' + iconName = 'unknown-private-network' + } + + return ( + h('#network_component.pointer', { + title: hoverText, + onClick: (event) => this.props.onClick(event), + }, [ + (function () { + switch (iconName) { + case 'ethereum-network': + return h('.network-indicator', [ + h('.menu-icon.diamond'), + h('.network-name', { + style: { + color: '#039396', + }}, + 'Ethereum Main Net'), + ]) + case 'ropsten-test-network': + return h('.network-indicator', [ + h('.menu-icon.red-dot'), + h('.network-name', { + style: { + color: '#ff6666', + }}, + 'Ropsten Test Net'), + ]) + case 'kovan-test-network': + return h('.network-indicator', [ + h('.menu-icon.hollow-diamond'), + h('.network-name', { + style: { + color: '#690496', + }}, + 'Kovan Test Net'), + ]) + case 'rinkeby-test-network': + return h('.network-indicator', [ + h('.menu-icon.golden-square'), + h('.network-name', { + style: { + color: '#e7a218', + }}, + 'Rinkeby Test Net'), + ]) + default: + return h('.network-indicator', [ + h('i.fa.fa-question-circle.fa-lg', { + style: { + margin: '10px', + color: 'rgb(125, 128, 130)', + }, + }), + + h('.network-name', { + style: { + color: '#AEAEAE', + }}, + 'Private Network'), + ]) + } + })(), + ]) + ) +} diff --git a/ui/app/components/notice.js b/ui/app/components/notice.js new file mode 100644 index 000000000..d9f0067cd --- /dev/null +++ b/ui/app/components/notice.js @@ -0,0 +1,126 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const ReactMarkdown = require('react-markdown') +const linker = require('extension-link-enabler') +const findDOMNode = require('react-dom').findDOMNode + +module.exports = Notice + +inherits(Notice, Component) +function Notice () { + Component.call(this) +} + +Notice.prototype.render = function () { + const { notice, onConfirm } = this.props + const { title, date, body } = notice + const state = this.state || { disclaimerDisabled: true } + const disabled = state.disclaimerDisabled + + return ( + h('.flex-column.flex-center.flex-grow', [ + h('h3.flex-center.text-transform-uppercase.terms-header', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + width: '100%', + fontSize: '20px', + textAlign: 'center', + padding: 6, + }, + }, [ + title, + ]), + + h('h5.flex-center.text-transform-uppercase.terms-header', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginBottom: 24, + width: '100%', + fontSize: '20px', + textAlign: 'center', + padding: 6, + }, + }, [ + date, + ]), + + h('style', ` + + .markdown { + overflow-x: hidden; + } + + .markdown h1, .markdown h2, .markdown h3 { + margin: 10px 0; + font-weight: bold; + } + + .markdown strong { + font-weight: bold; + } + .markdown em { + font-style: italic; + } + + .markdown p { + margin: 10px 0; + } + + .markdown a { + color: #df6b0e; + } + + `), + + h('div.markdown', { + onScroll: (e) => { + var object = e.currentTarget + if (object.offsetHeight + object.scrollTop + 100 >= object.scrollHeight) { + this.setState({disclaimerDisabled: false}) + } + }, + style: { + background: 'rgb(235, 235, 235)', + height: '310px', + padding: '6px', + width: '90%', + overflowY: 'scroll', + scroll: 'auto', + }, + }, [ + h(ReactMarkdown, { + className: 'notice-box', + source: body, + skipHtml: true, + }), + ]), + + h('button', { + disabled, + onClick: () => { + this.setState({disclaimerDisabled: true}) + onConfirm() + }, + style: { + marginTop: '18px', + }, + }, 'Accept'), + ]) + ) +} + +Notice.prototype.componentDidMount = function () { + var node = findDOMNode(this) + linker.setupListener(node) + if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) { + this.setState({disclaimerDisabled: false}) + } +} + +Notice.prototype.componentWillUnmount = function () { + var node = findDOMNode(this) + linker.teardownListener(node) +} diff --git a/ui/app/components/pending-msg-details.js b/ui/app/components/pending-msg-details.js new file mode 100644 index 000000000..16308d121 --- /dev/null +++ b/ui/app/components/pending-msg-details.js @@ -0,0 +1,50 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const AccountPanel = require('./account-panel') + +module.exports = PendingMsgDetails + +inherits(PendingMsgDetails, Component) +function PendingMsgDetails () { + Component.call(this) +} + +PendingMsgDetails.prototype.render = function () { + var state = this.props + var msgData = state.txData + + var msgParams = msgData.msgParams || {} + var address = msgParams.from || state.selectedAddress + var identity = state.identities[address] || { address: address } + var account = state.accounts[address] || { address: address } + + return ( + h('div', { + key: msgData.id, + style: { + margin: '10px 20px', + }, + }, [ + + // account that will sign + h(AccountPanel, { + showFullAddress: true, + identity: identity, + account: account, + imageifyIdenticons: state.imageifyIdenticons, + }), + + // message data + h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-row.flex-space-between', [ + h('label.font-small', 'MESSAGE'), + h('span.font-small', msgParams.data), + ]), + ]), + + ]) + ) +} + diff --git a/ui/app/components/pending-msg.js b/ui/app/components/pending-msg.js new file mode 100644 index 000000000..b2cac164a --- /dev/null +++ b/ui/app/components/pending-msg.js @@ -0,0 +1,56 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const PendingTxDetails = require('./pending-msg-details') + +module.exports = PendingMsg + +inherits(PendingMsg, Component) +function PendingMsg () { + Component.call(this) +} + +PendingMsg.prototype.render = function () { + var state = this.props + var msgData = state.txData + + return ( + + h('div', { + key: msgData.id, + }, [ + + // header + h('h3', { + style: { + fontWeight: 'bold', + textAlign: 'center', + }, + }, 'Sign Message'), + + h('.error', { + style: { + margin: '10px', + }, + }, `Signing this message can have + dangerous side effects. Only sign messages from + sites you fully trust with your entire account. + This will be fixed in a future version.`), + + // message details + h(PendingTxDetails, state), + + // sign + cancel + h('.flex-row.flex-space-around', [ + h('button', { + onClick: state.cancelMessage, + }, 'Cancel'), + h('button', { + onClick: state.signMessage, + }, 'Sign'), + ]), + ]) + + ) +} + diff --git a/ui/app/components/pending-personal-msg-details.js b/ui/app/components/pending-personal-msg-details.js new file mode 100644 index 000000000..1050513f2 --- /dev/null +++ b/ui/app/components/pending-personal-msg-details.js @@ -0,0 +1,60 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const AccountPanel = require('./account-panel') +const BinaryRenderer = require('./binary-renderer') + +module.exports = PendingMsgDetails + +inherits(PendingMsgDetails, Component) +function PendingMsgDetails () { + Component.call(this) +} + +PendingMsgDetails.prototype.render = function () { + var state = this.props + var msgData = state.txData + + var msgParams = msgData.msgParams || {} + var address = msgParams.from || state.selectedAddress + var identity = state.identities[address] || { address: address } + var account = state.accounts[address] || { address: address } + + var { data } = msgParams + + return ( + h('div', { + key: msgData.id, + style: { + margin: '10px 20px', + }, + }, [ + + // account that will sign + h(AccountPanel, { + showFullAddress: true, + identity: identity, + account: account, + imageifyIdenticons: state.imageifyIdenticons, + }), + + // message data + h('div', { + style: { + height: '260px', + }, + }, [ + h('label.font-small', { style: { display: 'block' } }, 'MESSAGE'), + h(BinaryRenderer, { + value: data, + style: { + height: '215px', + }, + }), + ]), + + ]) + ) +} + diff --git a/ui/app/components/pending-personal-msg.js b/ui/app/components/pending-personal-msg.js new file mode 100644 index 000000000..4542adb28 --- /dev/null +++ b/ui/app/components/pending-personal-msg.js @@ -0,0 +1,47 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const PendingTxDetails = require('./pending-personal-msg-details') + +module.exports = PendingMsg + +inherits(PendingMsg, Component) +function PendingMsg () { + Component.call(this) +} + +PendingMsg.prototype.render = function () { + var state = this.props + var msgData = state.txData + + return ( + + h('div', { + key: msgData.id, + }, [ + + // header + h('h3', { + style: { + fontWeight: 'bold', + textAlign: 'center', + }, + }, 'Sign Message'), + + // message details + h(PendingTxDetails, state), + + // sign + cancel + h('.flex-row.flex-space-around', [ + h('button', { + onClick: state.cancelPersonalMessage, + }, 'Cancel'), + h('button', { + onClick: state.signPersonalMessage, + }, 'Sign'), + ]), + ]) + + ) +} + diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js new file mode 100644 index 000000000..d7d602f31 --- /dev/null +++ b/ui/app/components/pending-tx.js @@ -0,0 +1,480 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const actions = require('../actions') +const clone = require('clone') + +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const hexToBn = require('../../../app/scripts/lib/hex-to-bn') +const util = require('../util') +const MiniAccountPanel = require('./mini-account-panel') +const Copyable = require('./copyable') +const EthBalance = require('./eth-balance') +const addressSummary = util.addressSummary +const nameForAddress = require('../../lib/contract-namer') +const BNInput = require('./bn-as-decimal-input') + +const MIN_GAS_PRICE_GWEI_BN = new BN(2) +const GWEI_FACTOR = new BN(1e9) +const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) +const MIN_GAS_LIMIT_BN = new BN(21000) + +module.exports = PendingTx +inherits(PendingTx, Component) +function PendingTx () { + Component.call(this) + this.state = { + valid: true, + txData: null, + submitting: false, + } +} + +PendingTx.prototype.render = function () { + const props = this.props + const { currentCurrency, blockGasLimit } = props + + const conversionRate = props.conversionRate + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} + + // Account Details + const address = txParams.from || props.selectedAddress + const identity = props.identities[address] || { address: address } + const account = props.accounts[address] + const balance = account ? account.balance : '0x0' + + // recipient check + const isValidAddress = !txParams.to || util.isValidAddress(txParams.to) + + // Gas + const gas = txParams.gas + const gasBn = hexToBn(gas) + const gasLimit = new BN(parseInt(blockGasLimit)) + const safeGasLimit = this.bnMultiplyByFraction(gasLimit, 19, 20).toString(10) + + // Gas Price + const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) + const gasPriceBn = hexToBn(gasPrice) + + const txFeeBn = gasBn.mul(gasPriceBn) + const valueBn = hexToBn(txParams.value) + const maxCost = txFeeBn.add(valueBn) + + const dataLength = txParams.data ? (txParams.data.length - 2) / 2 : 0 + + const balanceBn = hexToBn(balance) + const insufficientBalance = balanceBn.lt(maxCost) + + this.inputs = [] + + return ( + + h('div', { + key: txMeta.id, + }, [ + + h('form#pending-tx-form', { + onSubmit: this.onSubmit.bind(this), + + }, [ + + // tx info + h('div', [ + + h('.flex-row.flex-center', { + style: { + maxWidth: '100%', + }, + }, [ + + h(MiniAccountPanel, { + imageSeed: address, + picOrder: 'right', + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, identity.name), + + h(Copyable, { + value: ethUtil.toChecksumAddress(address), + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, addressSummary(address, 6, 4, false)), + ]), + + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, [ + h(EthBalance, { + value: balance, + conversionRate, + currentCurrency, + inline: true, + labelColor: '#F7861C', + }), + ]), + ]), + + forwardCarrat(), + + this.miniAccountPanelForRecipient(), + ]), + + h('style', ` + .table-box { + margin: 7px 0px 0px 0px; + width: 100%; + } + .table-box .row { + margin: 0px; + background: rgb(236,236,236); + display: flex; + justify-content: space-between; + font-family: Montserrat Light, sans-serif; + font-size: 13px; + padding: 5px 25px; + } + .table-box .row .value { + font-family: Montserrat Regular; + } + `), + + h('.table-box', [ + + // Ether Value + // Currently not customizable, but easily modified + // in the way that gas and gasLimit currently are. + h('.row', [ + h('.cell.label', 'Amount'), + h(EthBalance, { value: txParams.value, currentCurrency, conversionRate }), + ]), + + // Gas Limit (customizable) + h('.cell.row', [ + h('.cell.label', 'Gas Limit'), + h('.cell.value', { + }, [ + h(BNInput, { + name: 'Gas Limit', + value: gasBn, + precision: 0, + scale: 0, + // The hard lower limit for gas. + min: MIN_GAS_LIMIT_BN.toString(10), + max: safeGasLimit, + suffix: 'UNITS', + style: { + position: 'relative', + top: '5px', + }, + onChange: this.gasLimitChanged.bind(this), + + ref: (hexInput) => { this.inputs.push(hexInput) }, + }), + ]), + ]), + + // Gas Price (customizable) + h('.cell.row', [ + h('.cell.label', 'Gas Price'), + h('.cell.value', { + }, [ + h(BNInput, { + name: 'Gas Price', + value: gasPriceBn, + precision: 9, + scale: 9, + suffix: 'GWEI', + min: MIN_GAS_PRICE_GWEI_BN.toString(10), + style: { + position: 'relative', + top: '5px', + }, + onChange: this.gasPriceChanged.bind(this), + ref: (hexInput) => { this.inputs.push(hexInput) }, + }), + ]), + ]), + + // Max Transaction Fee (calculated) + h('.cell.row', [ + h('.cell.label', 'Max Transaction Fee'), + h(EthBalance, { value: txFeeBn.toString(16), currentCurrency, conversionRate }), + ]), + + h('.cell.row', { + style: { + fontFamily: 'Montserrat Regular', + background: 'white', + padding: '10px 25px', + }, + }, [ + h('.cell.label', 'Max Total'), + h('.cell.value', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + h(EthBalance, { + value: maxCost.toString(16), + currentCurrency, + conversionRate, + inline: true, + labelColor: 'black', + fontSize: '16px', + }), + ]), + ]), + + // Data size row: + h('.cell.row', { + style: { + background: '#f7f7f7', + paddingBottom: '0px', + }, + }, [ + h('.cell.label'), + h('.cell.value', { + style: { + fontFamily: 'Montserrat Light', + fontSize: '11px', + }, + }, `Data included: ${dataLength} bytes`), + ]), + ]), // End of Table + + ]), + + h('style', ` + .conf-buttons button { + margin-left: 10px; + text-transform: uppercase; + } + `), + + txMeta.simulationFails ? + h('.error', { + style: { + marginLeft: 50, + fontSize: '0.9em', + }, + }, 'Transaction Error. Exception thrown in contract code.') + : null, + + !isValidAddress ? + h('.error', { + style: { + marginLeft: 50, + fontSize: '0.9em', + }, + }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.') + : null, + + insufficientBalance ? + h('span.error', { + style: { + marginLeft: 50, + fontSize: '0.9em', + }, + }, 'Insufficient balance for transaction') + : null, + + // send + cancel + h('.flex-row.flex-space-around.conf-buttons', { + style: { + display: 'flex', + justifyContent: 'flex-end', + margin: '14px 25px', + }, + }, [ + + + insufficientBalance ? + h('button.btn-green', { + onClick: props.buyEth, + }, 'Buy Ether') + : null, + + h('button', { + onClick: (event) => { + this.resetGasFields() + event.preventDefault() + }, + }, 'Reset'), + + // Accept Button + h('input.confirm.btn-green', { + type: 'submit', + value: 'SUBMIT', + style: { marginLeft: '10px' }, + disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, + }), + + h('button.cancel.btn-red', { + onClick: props.cancelTransaction, + }, 'Reject'), + ]), + ]), + ]) + ) +} + +PendingTx.prototype.miniAccountPanelForRecipient = function () { + const props = this.props + const txData = props.txData + const txParams = txData.txParams || {} + const isContractDeploy = !('to' in txParams) + + // If it's not a contract deploy, send to the account + if (!isContractDeploy) { + return h(MiniAccountPanel, { + imageSeed: txParams.to, + picOrder: 'left', + }, [ + + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, nameForAddress(txParams.to, props.identities)), + + h(Copyable, { + value: ethUtil.toChecksumAddress(txParams.to), + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, addressSummary(txParams.to, 6, 4, false)), + ]), + + ]) + } else { + return h(MiniAccountPanel, { + picOrder: 'left', + }, [ + + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, 'New Contract'), + + ]) + } +} + +PendingTx.prototype.gasPriceChanged = function (newBN, valid) { + log.info(`Gas price changed to: ${newBN.toString(10)}`) + const txMeta = this.gatherTxMeta() + txMeta.txParams.gasPrice = '0x' + newBN.toString('hex') + this.setState({ + txData: clone(txMeta), + valid, + }) +} + +PendingTx.prototype.gasLimitChanged = function (newBN, valid) { + log.info(`Gas limit changed to ${newBN.toString(10)}`) + const txMeta = this.gatherTxMeta() + txMeta.txParams.gas = '0x' + newBN.toString('hex') + this.setState({ + txData: clone(txMeta), + valid, + }) +} + +PendingTx.prototype.resetGasFields = function () { + log.debug(`pending-tx resetGasFields`) + + this.inputs.forEach((hexInput) => { + if (hexInput) { + hexInput.setValid() + } + }) + + this.setState({ + txData: null, + valid: true, + }) +} + +PendingTx.prototype.onSubmit = function (event) { + event.preventDefault() + const txMeta = this.gatherTxMeta() + const valid = this.checkValidity() + this.setState({ valid, submitting: true }) + if (valid && this.verifyGasParams()) { + this.props.sendTransaction(txMeta, event) + } else { + this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) + this.setState({ submitting: false }) + } +} + +PendingTx.prototype.checkValidity = function () { + const form = this.getFormEl() + const valid = form.checkValidity() + return valid +} + +PendingTx.prototype.getFormEl = function () { + const form = document.querySelector('form#pending-tx-form') + // Stub out form for unit tests: + if (!form) { + return { checkValidity () { return true } } + } + return form +} + +// After a customizable state value has been updated, +PendingTx.prototype.gatherTxMeta = function () { + log.debug(`pending-tx gatherTxMeta`) + const props = this.props + const state = this.state + const txData = clone(state.txData) || clone(props.txData) + + log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) + return txData +} + +PendingTx.prototype.verifyGasParams = function () { + // We call this in case the gas has not been modified at all + if (!this.state) { return true } + return ( + this._notZeroOrEmptyString(this.state.gas) && + this._notZeroOrEmptyString(this.state.gasPrice) + ) +} + +PendingTx.prototype._notZeroOrEmptyString = function (obj) { + return obj !== '' && obj !== '0x0' +} + +PendingTx.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) { + const numBN = new BN(numerator) + const denomBN = new BN(denominator) + return targetBN.mul(numBN).div(denomBN) +} + +function forwardCarrat () { + return ( + h('img', { + src: 'images/forward-carrat.svg', + style: { + padding: '5px 6px 0px 10px', + height: '37px', + }, + }) + ) +} diff --git a/ui/app/components/qr-code.js b/ui/app/components/qr-code.js new file mode 100644 index 000000000..06b9aed9b --- /dev/null +++ b/ui/app/components/qr-code.js @@ -0,0 +1,79 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const qrCode = require('qrcode-npm').qrcode +const inherits = require('util').inherits +const connect = require('react-redux').connect +const isHexPrefixed = require('ethereumjs-util').isHexPrefixed +const CopyButton = require('./copyButton') + +module.exports = connect(mapStateToProps)(QrCodeView) + +function mapStateToProps (state) { + return { + Qr: state.appState.Qr, + buyView: state.appState.buyView, + warning: state.appState.warning, + } +} + +inherits(QrCodeView, Component) + +function QrCodeView () { + Component.call(this) +} + +QrCodeView.prototype.render = function () { + const props = this.props + const Qr = props.Qr + const address = `${isHexPrefixed(Qr.data) ? 'ethereum:' : ''}${Qr.data}` + const qrImage = qrCode(4, 'M') + qrImage.addData(address) + qrImage.make() + return h('.main-container.flex-column', { + key: 'qr', + style: { + justifyContent: 'center', + paddingBottom: '45px', + paddingLeft: '45px', + paddingRight: '45px', + alignItems: 'center', + }, + }, [ + Array.isArray(Qr.message) ? h('.message-container', this.renderMultiMessage()) : h('.qr-header', Qr.message), + + this.props.warning ? this.props.warning && h('span.error.flex-center', { + style: { + textAlign: 'center', + width: '229px', + height: '82px', + }, + }, + this.props.warning) : null, + + h('#qr-container.flex-column', { + style: { + marginTop: '25px', + marginBottom: '15px', + }, + dangerouslySetInnerHTML: { + __html: qrImage.createTableTag(4), + }, + }), + h('.flex-row', [ + h('h3.ellip-address', { + style: { + width: '247px', + }, + }, Qr.data), + h(CopyButton, { + value: Qr.data, + }), + ]), + ]) +} + +QrCodeView.prototype.renderMultiMessage = function () { + var Qr = this.props.Qr + var multiMessage = Qr.message.map((message) => h('.qr-message', message)) + return multiMessage +} diff --git a/ui/app/components/range-slider.js b/ui/app/components/range-slider.js new file mode 100644 index 000000000..823f5eb01 --- /dev/null +++ b/ui/app/components/range-slider.js @@ -0,0 +1,58 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = RangeSlider + +inherits(RangeSlider, Component) +function RangeSlider () { + Component.call(this) +} + +RangeSlider.prototype.render = function () { + const state = this.state || {} + const props = this.props + const onInput = props.onInput || function () {} + const name = props.name + const { + min = 0, + max = 100, + increment = 1, + defaultValue = 50, + mirrorInput = false, + } = this.props.options + const {container, input, range} = props.style + + return ( + h('.flex-row', { + style: container, + }, [ + h('input', { + type: 'range', + name: name, + min: min, + max: max, + step: increment, + style: range, + value: state.value || defaultValue, + onChange: mirrorInput ? this.mirrorInputs.bind(this, event) : onInput, + }), + + // Mirrored input for range + mirrorInput ? h('input.large-input', { + type: 'number', + name: `${name}Mirror`, + min: min, + max: max, + value: state.value || defaultValue, + step: increment, + style: input, + onChange: this.mirrorInputs.bind(this, event), + }) : null, + ]) + ) +} + +RangeSlider.prototype.mirrorInputs = function (event) { + this.setState({value: event.target.value}) +} diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js new file mode 100644 index 000000000..e0a720426 --- /dev/null +++ b/ui/app/components/shapeshift-form.js @@ -0,0 +1,306 @@ +const PersistentForm = require('../../lib/persistent-form') +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +const actions = require('../actions') +const Qr = require('./qr-code') +const isValidAddress = require('../util').isValidAddress +module.exports = connect(mapStateToProps)(ShapeshiftForm) + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + isSubLoading: state.appState.isSubLoading, + qrRequested: state.appState.qrRequested, + } +} + +inherits(ShapeshiftForm, PersistentForm) + +function ShapeshiftForm () { + PersistentForm.call(this) + this.persistentFormParentId = 'shapeshift-buy-form' +} + +ShapeshiftForm.prototype.render = function () { + return h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + this.props.qrRequested ? h(Qr, {key: 'qr'}) : this.renderMain(), + ]) +} + +ShapeshiftForm.prototype.renderMain = function () { + const marketinfo = this.props.buyView.formView.marketinfo + const coinOptions = this.props.buyView.formView.coinOptions + var coin = marketinfo.pair.split('_')[0].toUpperCase() + + return h('.flex-column', { + style: { + // marginTop: '10px', + padding: '25px', + paddingTop: '5px', + width: '100%', + minHeight: '215px', + alignItems: 'center', + overflowY: 'auto', + }, + }, [ + h('.flex-row', { + style: { + justifyContent: 'center', + alignItems: 'baseline', + height: '42px', + }, + }, [ + h('img', { + src: coinOptions[coin].image, + width: '25px', + height: '25px', + style: { + marginRight: '5px', + }, + }), + + h('.input-container', [ + h('input#fromCoin.buy-inputs.ex-coins', { + type: 'text', + list: 'coinList', + autoFocus: true, + dataset: { + persistentFormId: 'input-coin', + }, + style: { + boxSizing: 'border-box', + }, + onChange: this.handleLiveInput.bind(this), + defaultValue: 'BTC', + }), + + this.renderCoinList(), + + h('i.fa.fa-pencil-square-o.edit-text', { + style: { + fontSize: '12px', + color: '#F7861C', + position: 'relative', + bottom: '48px', + left: '106px', + }, + }), + ]), + + h('.icon-control', [ + h('i.fa.fa-refresh.fa-4.orange', { + style: { + bottom: '5px', + left: '5px', + color: '#F7861C', + }, + onClick: this.updateCoin.bind(this), + }), + h('i.fa.fa-chevron-right.fa-4.orange', { + style: { + position: 'relative', + bottom: '26px', + left: '10px', + color: '#F7861C', + }, + onClick: this.updateCoin.bind(this), + }), + ]), + + h('#toCoin.ex-coins', marketinfo.pair.split('_')[1].toUpperCase()), + + h('img', { + src: coinOptions[marketinfo.pair.split('_')[1].toUpperCase()].image, + width: '25px', + height: '25px', + style: { + marginLeft: '5px', + }, + }), + ]), + h('.flex-column', { + style: { + alignItems: 'flex-start', + }, + }, [ + this.props.warning ? this.props.warning && h('span.error.flex-center', { + style: { + textAlign: 'center', + width: '229px', + height: '82px', + }, + }, + this.props.warning) : this.renderInfo(), + ]), + + h(this.activeToggle('.input-container'), { + style: { + padding: '10px', + paddingTop: '0px', + width: '100%', + }, + }, [ + + h('div', `${coin} Address:`), + + h('input#fromCoinAddress.buy-inputs', { + type: 'text', + placeholder: `Your ${coin} Refund Address`, + dataset: { + persistentFormId: 'refund-address', + }, + style: { + boxSizing: 'border-box', + width: '227px', + height: '30px', + padding: ' 5px ', + }, + }), + + h('i.fa.fa-pencil-square-o.edit-text', { + style: { + fontSize: '12px', + color: '#F7861C', + position: 'relative', + bottom: '10px', + right: '11px', + }, + }), + h('.flex-row', { + style: { + justifyContent: 'flex-end', + }, + }, [ + h('button', { + onClick: this.shift.bind(this), + style: { + marginTop: '10px', + position: 'relative', + bottom: '40px', + }, + }, + 'Submit'), + ]), + ]), + ]) +} + +ShapeshiftForm.prototype.shift = function () { + var props = this.props + var withdrawal = this.props.buyView.buyAddress + var returnAddress = document.getElementById('fromCoinAddress').value + var pair = this.props.buyView.formView.marketinfo.pair + var data = { + 'withdrawal': withdrawal, + 'pair': pair, + 'returnAddress': returnAddress, + // Public api key + 'apiKey': '803d1f5df2ed1b1476e4b9e6bcd089e34d8874595dda6a23b67d93c56ea9cc2445e98a6748b219b2b6ad654d9f075f1f1db139abfa93158c04e825db122c14b6', + } + var message = [ + `Deposit Limit: ${props.buyView.formView.marketinfo.limit}`, + `Deposit Minimum:${props.buyView.formView.marketinfo.minimum}`, + ] + if (isValidAddress(withdrawal)) { + this.props.dispatch(actions.coinShiftRquest(data, message)) + } +} + +ShapeshiftForm.prototype.renderCoinList = function () { + var list = Object.keys(this.props.buyView.formView.coinOptions).map((item) => { + return h('option', { + value: item, + }, item) + }) + + return h('datalist#coinList', { + onClick: (event) => { + event.preventDefault() + }, + }, list) +} + +ShapeshiftForm.prototype.updateCoin = function (event) { + event.preventDefault() + const props = this.props + var coinOptions = this.props.buyView.formView.coinOptions + var coin = document.getElementById('fromCoin').value + + if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { + var message = 'Not a valid coin' + return props.dispatch(actions.displayWarning(message)) + } else { + return props.dispatch(actions.pairUpdate(coin)) + } +} + +ShapeshiftForm.prototype.handleLiveInput = function () { + const props = this.props + var coinOptions = this.props.buyView.formView.coinOptions + var coin = document.getElementById('fromCoin').value + + if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { + return null + } else { + return props.dispatch(actions.pairUpdate(coin)) + } +} + +ShapeshiftForm.prototype.renderInfo = function () { + const marketinfo = this.props.buyView.formView.marketinfo + const coinOptions = this.props.buyView.formView.coinOptions + var coin = marketinfo.pair.split('_')[0].toUpperCase() + + return h('span', { + style: { + }, + }, [ + h('h3.flex-row.text-transform-uppercase', { + style: { + color: '#868686', + paddingTop: '4px', + justifyContent: 'space-around', + textAlign: 'center', + fontSize: '17px', + }, + }, `Market Info for ${marketinfo.pair.replace('_', ' to ').toUpperCase()}:`), + h('.marketinfo', ['Status : ', `${coinOptions[coin].status}`]), + h('.marketinfo', ['Exchange Rate: ', `${marketinfo.rate}`]), + h('.marketinfo', ['Limit: ', `${marketinfo.limit}`]), + h('.marketinfo', ['Minimum : ', `${marketinfo.minimum}`]), + ]) +} + +ShapeshiftForm.prototype.activeToggle = function (elementType) { + if (!this.props.buyView.formView.response || this.props.warning) return elementType + return `${elementType}.inactive` +} + +ShapeshiftForm.prototype.renderLoading = function () { + return h('span', { + style: { + position: 'absolute', + left: '70px', + bottom: '194px', + background: 'transparent', + width: '229px', + height: '82px', + display: 'flex', + justifyContent: 'center', + }, + }, [ + h('img', { + style: { + width: '60px', + }, + src: 'images/loading.svg', + }), + ]) +} diff --git a/ui/app/components/shift-list-item.js b/ui/app/components/shift-list-item.js new file mode 100644 index 000000000..32bfbeda4 --- /dev/null +++ b/ui/app/components/shift-list-item.js @@ -0,0 +1,204 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const vreme = new (require('vreme')) +const explorerLink = require('../../lib/explorer-link') +const actions = require('../actions') +const addressSummary = require('../util').addressSummary + +const CopyButton = require('./copyButton') +const EthBalance = require('./eth-balance') +const Tooltip = require('./tooltip') + + +module.exports = connect(mapStateToProps)(ShiftListItem) + +function mapStateToProps (state) { + return { + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + } +} + +inherits(ShiftListItem, Component) + +function ShiftListItem () { + Component.call(this) +} + +ShiftListItem.prototype.render = function () { + return ( + h('.transaction-list-item.flex-row', { + style: { + paddingTop: '20px', + paddingBottom: '20px', + justifyContent: 'space-around', + alignItems: 'center', + }, + }, [ + h('div', { + style: { + width: '0px', + position: 'relative', + bottom: '19px', + }, + }, [ + h('img', { + src: 'https://info.shapeshift.io/sites/default/files/logo.png', + style: { + height: '35px', + width: '132px', + position: 'absolute', + clip: 'rect(0px,23px,34px,0px)', + }, + }), + ]), + + this.renderInfo(), + this.renderUtilComponents(), + ]) + ) +} + +function formatDate (date) { + return vreme.format(new Date(date), 'March 16 2014 14:30') +} + +ShiftListItem.prototype.renderUtilComponents = function () { + var props = this.props + const { conversionRate, currentCurrency } = props + + switch (props.response.status) { + case 'no_deposits': + return h('.flex-row', [ + h(CopyButton, { + value: this.props.depositAddress, + }), + h(Tooltip, { + title: 'QR Code', + }, [ + h('i.fa.fa-qrcode.pointer.pop-hover', { + onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType)), + style: { + margin: '5px', + marginLeft: '23px', + marginRight: '12px', + fontSize: '20px', + color: '#F7861C', + }, + }), + ]), + ]) + case 'received': + return h('.flex-row') + + case 'complete': + return h('.flex-row', [ + h(CopyButton, { + value: this.props.response.transaction, + }), + h(EthBalance, { + value: `${props.response.outgoingCoin}`, + conversionRate, + currentCurrency, + width: '55px', + shorten: true, + needsParse: false, + incoming: true, + style: { + fontSize: '15px', + color: '#01888C', + }, + }), + ]) + + case 'failed': + return '' + default: + return '' + } +} + +ShiftListItem.prototype.renderInfo = function () { + var props = this.props + switch (props.response.status) { + case 'no_deposits': + return h('.flex-column', { + style: { + width: '200px', + overflow: 'hidden', + }, + }, [ + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, `${props.depositType} to ETH via ShapeShift`), + h('div', 'No deposits received'), + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, formatDate(props.time)), + ]) + case 'received': + return h('.flex-column', { + style: { + width: '200px', + overflow: 'hidden', + }, + }, [ + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, `${props.depositType} to ETH via ShapeShift`), + h('div', 'Conversion in progress'), + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, formatDate(props.time)), + ]) + case 'complete': + var url = explorerLink(props.response.transaction, parseInt('1')) + + return h('.flex-column.pointer', { + style: { + width: '200px', + overflow: 'hidden', + }, + onClick: () => global.platform.openWindow({ url }), + }, [ + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, 'From ShapeShift'), + h('div', formatDate(props.time)), + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, addressSummary(props.response.transaction)), + ]) + + case 'failed': + return h('span.error', '(Failed)') + default: + return '' + } +} diff --git a/ui/app/components/tab-bar.js b/ui/app/components/tab-bar.js new file mode 100644 index 000000000..6295e7dd9 --- /dev/null +++ b/ui/app/components/tab-bar.js @@ -0,0 +1,36 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = TabBar + +inherits(TabBar, Component) +function TabBar () { + Component.call(this) +} + +TabBar.prototype.render = function () { + const props = this.props + const state = this.state || {} + const { tabs = [], defaultTab, tabSelected } = props + const { subview = defaultTab } = state + + return ( + h('.flex-row.space-around.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + paddingTop: '4px', + }, + }, tabs.map((tab) => { + const { key, content } = tab + return h(subview === key ? '.activeForm' : '.inactiveForm.pointer', { + onClick: () => { + this.setState({ subview: key }) + tabSelected(key) + }, + }, content) + })) + ) +} + diff --git a/ui/app/components/template.js b/ui/app/components/template.js new file mode 100644 index 000000000..b6ed8eaa0 --- /dev/null +++ b/ui/app/components/template.js @@ -0,0 +1,18 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = NewComponent + +inherits(NewComponent, Component) +function NewComponent () { + Component.call(this) +} + +NewComponent.prototype.render = function () { + const props = this.props + + return ( + h('span', props.message) + ) +} diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js new file mode 100644 index 000000000..19d7139bb --- /dev/null +++ b/ui/app/components/token-cell.js @@ -0,0 +1,72 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Identicon = require('./identicon') +const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') + +module.exports = TokenCell + +inherits(TokenCell, Component) +function TokenCell () { + Component.call(this) +} + +TokenCell.prototype.render = function () { + const props = this.props + const { address, symbol, string, network, userAddress } = props + + return ( + h('li.token-cell', { + style: { cursor: network === '1' ? 'pointer' : 'default' }, + onClick: this.view.bind(this, address, userAddress, network), + }, [ + + h(Identicon, { + diameter: 50, + address, + network, + }), + + h('h3', `${string || 0} ${symbol}`), + + h('span', { style: { flex: '1 0 auto' } }), + + /* + h('button', { + onClick: this.send.bind(this, address), + }, 'SEND'), + */ + + ]) + ) +} + +TokenCell.prototype.send = function (address, event) { + event.preventDefault() + event.stopPropagation() + const url = tokenFactoryFor(address) + if (url) { + navigateTo(url) + } +} + +TokenCell.prototype.view = function (address, userAddress, network, event) { + const url = etherscanLinkFor(address, userAddress, network) + if (url) { + navigateTo(url) + } +} + +function navigateTo (url) { + global.platform.openWindow({ url }) +} + +function etherscanLinkFor (tokenAddress, address, network) { + const prefix = prefixForNetwork(network) + return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}` +} + +function tokenFactoryFor (tokenAddress) { + return `https://tokenfactory.surge.sh/#/token/${tokenAddress}` +} + diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js new file mode 100644 index 000000000..20cfa897e --- /dev/null +++ b/ui/app/components/token-list.js @@ -0,0 +1,192 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const TokenTracker = require('eth-token-tracker') +const TokenCell = require('./token-cell.js') +const normalizeAddress = require('eth-sig-util').normalize + +const defaultTokens = [] +const contracts = require('eth-contract-metadata') +for (const address in contracts) { + const contract = contracts[address] + if (contract.erc20) { + contract.address = address + defaultTokens.push(contract) + } +} + +module.exports = TokenList + +inherits(TokenList, Component) +function TokenList () { + this.state = { + tokens: [], + isLoading: true, + network: null, + } + Component.call(this) +} + +TokenList.prototype.render = function () { + const state = this.state + const { tokens, isLoading, error } = state + const { userAddress, network } = this.props + + if (isLoading) { + return this.message('Loading') + } + + if (error) { + log.error(error) + return this.message('There was a problem loading your token balances.') + } + + const tokenViews = tokens.map((tokenData) => { + tokenData.network = network + tokenData.userAddress = userAddress + return h(TokenCell, tokenData) + }) + + return h('div', [ + h('ol', { + style: { + height: '260px', + overflowY: 'auto', + display: 'flex', + flexDirection: 'column', + }, + }, [ + h('style', ` + + li.token-cell { + display: flex; + flex-direction: row; + align-items: center; + padding: 10px; + } + + li.token-cell > h3 { + margin-left: 12px; + } + + li.token-cell:hover { + background: white; + cursor: pointer; + } + + `), + ...tokenViews, + tokenViews.length ? null : this.message('No Tokens Found.'), + ]), + this.addTokenButtonElement(), + ]) +} + +TokenList.prototype.addTokenButtonElement = function () { + return h('div', [ + h('div.footer.hover-white.pointer', { + key: 'reveal-account-bar', + onClick: () => { + this.props.addToken() + }, + style: { + display: 'flex', + height: '40px', + padding: '10px', + justifyContent: 'center', + alignItems: 'center', + }, + }, [ + h('i.fa.fa-plus.fa-lg'), + ]), + ]) +} + +TokenList.prototype.message = function (body) { + return h('div', { + style: { + display: 'flex', + height: '250px', + alignItems: 'center', + justifyContent: 'center', + padding: '30px', + }, + }, body) +} + +TokenList.prototype.componentDidMount = function () { + this.createFreshTokenTracker() +} + +TokenList.prototype.createFreshTokenTracker = function () { + if (this.tracker) { + // Clean up old trackers when refreshing: + this.tracker.stop() + this.tracker.removeListener('update', this.balanceUpdater) + this.tracker.removeListener('error', this.showError) + } + + if (!global.ethereumProvider) return + const { userAddress } = this.props + this.tracker = new TokenTracker({ + userAddress, + provider: global.ethereumProvider, + tokens: uniqueMergeTokens(defaultTokens, this.props.tokens), + pollingInterval: 8000, + }) + + + // Set up listener instances for cleaning up + this.balanceUpdater = this.updateBalances.bind(this) + this.showError = (error) => { + this.setState({ error, isLoading: false }) + } + this.tracker.on('update', this.balanceUpdater) + this.tracker.on('error', this.showError) + + this.tracker.updateBalances() + .then(() => { + this.updateBalances(this.tracker.serialize()) + }) + .catch((reason) => { + log.error(`Problem updating balances`, reason) + this.setState({ isLoading: false }) + }) +} + +TokenList.prototype.componentWillUpdate = function (nextProps) { + if (nextProps.network === 'loading') return + const oldNet = this.props.network + const newNet = nextProps.network + + if (oldNet && newNet && newNet !== oldNet) { + this.setState({ isLoading: true }) + this.createFreshTokenTracker() + } +} + +TokenList.prototype.updateBalances = function (tokens) { + const heldTokens = tokens.filter(token => { + return token.balance !== '0' && token.string !== '0.000' + }) + this.setState({ tokens: heldTokens, isLoading: false }) +} + +TokenList.prototype.componentWillUnmount = function () { + if (!this.tracker) return + this.tracker.stop() +} + +function uniqueMergeTokens (tokensA, tokensB) { + const uniqueAddresses = [] + const result = [] + tokensA.concat(tokensB).forEach((token) => { + const normal = normalizeAddress(token.address) + if (!uniqueAddresses.includes(normal)) { + uniqueAddresses.push(normal) + result.push(token) + } + }) + return result +} + diff --git a/ui/app/components/tooltip.js b/ui/app/components/tooltip.js new file mode 100644 index 000000000..edbc074bb --- /dev/null +++ b/ui/app/components/tooltip.js @@ -0,0 +1,22 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ReactTooltip = require('react-tooltip-component') + +module.exports = Tooltip + +inherits(Tooltip, Component) +function Tooltip () { + Component.call(this) +} + +Tooltip.prototype.render = function () { + const props = this.props + const { position, title, children } = props + + return h(ReactTooltip, { + position: position || 'left', + title, + fixed: false, + }, children) +} diff --git a/ui/app/components/transaction-list-item-icon.js b/ui/app/components/transaction-list-item-icon.js new file mode 100644 index 000000000..431054340 --- /dev/null +++ b/ui/app/components/transaction-list-item-icon.js @@ -0,0 +1,68 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Tooltip = require('./tooltip') + +const Identicon = require('./identicon') + +module.exports = TransactionIcon + +inherits(TransactionIcon, Component) +function TransactionIcon () { + Component.call(this) +} + +TransactionIcon.prototype.render = function () { + const { transaction, txParams, isMsg } = this.props + switch (transaction.status) { + case 'unapproved': + return h(!isMsg ? '.unapproved-tx-icon' : 'i.fa.fa-certificate.fa-lg') + + case 'rejected': + return h('i.fa.fa-exclamation-triangle.fa-lg.warning', { + style: { + width: '24px', + }, + }) + + case 'failed': + return h('i.fa.fa-exclamation-triangle.fa-lg.error', { + style: { + width: '24px', + }, + }) + + case 'submitted': + return h(Tooltip, { + title: 'Pending', + position: 'bottom', + }, [ + h('i.fa.fa-ellipsis-h', { + style: { + fontSize: '27px', + }, + }), + ]) + } + + if (isMsg) { + return h('i.fa.fa-certificate.fa-lg', { + style: { + width: '24px', + }, + }) + } + + if (txParams.to) { + return h(Identicon, { + diameter: 24, + address: txParams.to || transaction.hash, + }) + } else { + return h('i.fa.fa-file-text-o.fa-lg', { + style: { + width: '24px', + }, + }) + } +} diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js new file mode 100644 index 000000000..dbda66a31 --- /dev/null +++ b/ui/app/components/transaction-list-item.js @@ -0,0 +1,165 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const EthBalance = require('./eth-balance') +const addressSummary = require('../util').addressSummary +const explorerLink = require('../../lib/explorer-link') +const CopyButton = require('./copyButton') +const vreme = new (require('vreme')) +const Tooltip = require('./tooltip') +const numberToBN = require('number-to-bn') + +const TransactionIcon = require('./transaction-list-item-icon') +const ShiftListItem = require('./shift-list-item') +module.exports = TransactionListItem + +inherits(TransactionListItem, Component) +function TransactionListItem () { + Component.call(this) +} + +TransactionListItem.prototype.render = function () { + const { transaction, network, conversionRate, currentCurrency } = this.props + if (transaction.key === 'shapeshift') { + if (network === '1') return h(ShiftListItem, transaction) + } + var date = formatDate(transaction.time) + + let isLinkable = false + const numericNet = parseInt(network) + isLinkable = numericNet === 1 || numericNet === 3 || numericNet === 4 || numericNet === 42 + + var isMsg = ('msgParams' in transaction) + var isTx = ('txParams' in transaction) + var isPending = transaction.status === 'unapproved' + let txParams + if (isTx) { + txParams = transaction.txParams + } else if (isMsg) { + txParams = transaction.msgParams + } + + const nonce = txParams.nonce ? numberToBN(txParams.nonce).toString(10) : '' + + const isClickable = ('hash' in transaction && isLinkable) || isPending + return ( + h(`.transaction-list-item.flex-row.flex-space-between${isClickable ? '.pointer' : ''}`, { + onClick: (event) => { + if (isPending) { + this.props.showTx(transaction.id) + } + event.stopPropagation() + if (!transaction.hash || !isLinkable) return + var url = explorerLink(transaction.hash, parseInt(network)) + global.platform.openWindow({ url }) + }, + style: { + padding: '20px 0', + }, + }, [ + + h('.identicon-wrapper.flex-column.flex-center.select-none', [ + h('.pop-hover', { + onClick: (event) => { + event.stopPropagation() + if (!isTx || isPending) return + var url = `https://metamask.github.io/eth-tx-viz/?tx=${transaction.hash}` + global.platform.openWindow({ url }) + }, + }, [ + h(TransactionIcon, { txParams, transaction, isTx, isMsg }), + ]), + ]), + + h(Tooltip, { + title: 'Transaction Number', + position: 'bottom', + }, [ + h('span', { + style: { + display: 'flex', + cursor: 'normal', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + padding: '10px', + }, + }, nonce), + ]), + + h('.flex-column', {style: {width: '200px', overflow: 'hidden'}}, [ + domainField(txParams), + h('div', date), + recipientField(txParams, transaction, isTx, isMsg), + ]), + + // Places a copy button if tx is successful, else places a placeholder empty div. + transaction.hash ? h(CopyButton, { value: transaction.hash }) : h('div', {style: { display: 'flex', alignItems: 'center', width: '26px' }}), + + isTx ? h(EthBalance, { + value: txParams.value, + conversionRate, + currentCurrency, + width: '55px', + shorten: true, + showFiat: false, + style: {fontSize: '15px'}, + }) : h('.flex-column'), + ]) + ) +} + +function domainField (txParams) { + return h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + overflow: 'hidden', + textOverflow: 'ellipsis', + width: '100%', + }, + }, [ + txParams.origin, + ]) +} + +function recipientField (txParams, transaction, isTx, isMsg) { + let message + + if (isMsg) { + message = 'Signature Requested' + } else if (txParams.to) { + message = addressSummary(txParams.to) + } else { + message = 'Contract Published' + } + + return h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + }, + }, [ + message, + failIfFailed(transaction), + ]) +} + +function formatDate (date) { + return vreme.format(new Date(date), 'March 16 2014 14:30') +} + +function failIfFailed (transaction) { + if (transaction.status === 'rejected') { + return h('span.error', ' (Rejected)') + } + if (transaction.err) { + return h(Tooltip, { + title: transaction.err.message, + position: 'bottom', + }, [ + h('span.error', ' (Failed)'), + ]) + } +} diff --git a/ui/app/components/transaction-list.js b/ui/app/components/transaction-list.js new file mode 100644 index 000000000..3b4ba741e --- /dev/null +++ b/ui/app/components/transaction-list.js @@ -0,0 +1,79 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const TransactionListItem = require('./transaction-list-item') + +module.exports = TransactionList + + +inherits(TransactionList, Component) +function TransactionList () { + Component.call(this) +} + +TransactionList.prototype.render = function () { + const { transactions, network, unapprovedMsgs, conversionRate } = this.props + + var shapeShiftTxList + if (network === '1') { + shapeShiftTxList = this.props.shapeShiftTxList + } + const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList) + .sort((a, b) => b.time - a.time) + + return ( + + h('section.transaction-list', [ + + h('style', ` + .transaction-list .transaction-list-item:not(:last-of-type) { + border-bottom: 1px solid #D4D4D4; + } + .transaction-list .transaction-list-item .ether-balance-label { + display: block !important; + font-size: small; + } + `), + + h('.tx-list', { + style: { + overflowY: 'auto', + height: '300px', + padding: '0 20px', + textAlign: 'center', + }, + }, [ + + txsToRender.length + ? txsToRender.map((transaction, i) => { + let key + switch (transaction.key) { + case 'shapeshift': + const { depositAddress, time } = transaction + key = `shift-tx-${depositAddress}-${time}-${i}` + break + default: + key = `tx-${transaction.id}-${i}` + } + return h(TransactionListItem, { + transaction, i, network, key, + conversionRate, + showTx: (txId) => { + this.props.viewPendingTx(txId) + }, + }) + }) + : h('.flex-center', { + style: { + flexDirection: 'column', + height: '100%', + }, + }, [ + 'No transaction history.', + ]), + ]), + ]) + ) +} + diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js new file mode 100644 index 000000000..63b77ef7f --- /dev/null +++ b/ui/app/conf-tx.js @@ -0,0 +1,213 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') +const NetworkIndicator = require('./components/network') +const txHelper = require('../lib/tx-helper') +const isPopupOrNotification = require('../../../app/scripts/lib/is-popup-or-notification') + +const PendingTx = require('./components/pending-tx') +const PendingMsg = require('./components/pending-msg') +const PendingPersonalMsg = require('./components/pending-personal-msg') +const Loading = require('./components/loading') + +module.exports = connect(mapStateToProps)(ConfirmTxScreen) + +function mapStateToProps (state) { + return { + identities: state.metamask.identities, + accounts: state.metamask.accounts, + selectedAddress: state.metamask.selectedAddress, + unapprovedTxs: state.metamask.unapprovedTxs, + unapprovedMsgs: state.metamask.unapprovedMsgs, + unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs, + index: state.appState.currentView.context, + warning: state.appState.warning, + network: state.metamask.network, + provider: state.metamask.provider, + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + blockGasLimit: state.metamask.currentBlockGasLimit, + } +} + +inherits(ConfirmTxScreen, Component) +function ConfirmTxScreen () { + Component.call(this) +} + +ConfirmTxScreen.prototype.render = function () { + const props = this.props + const { network, provider, unapprovedTxs, currentCurrency, + unapprovedMsgs, unapprovedPersonalMsgs, conversionRate, blockGasLimit } = props + + var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) + + var txData = unconfTxList[props.index] || {} + var txParams = txData.params || {} + var isNotification = isPopupOrNotification() === 'notification' + + + log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`) + if (unconfTxList.length === 0) return h(Loading, { isLoading: true }) + + return ( + + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + !isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: this.goHome.bind(this), + }) : null, + h('h2.page-subtitle', 'Confirm Transaction'), + isNotification ? h(NetworkIndicator, { + network: network, + provider: provider, + }) : null, + ]), + + h('h3', { + style: { + alignSelf: 'center', + display: unconfTxList.length > 1 ? 'block' : 'none', + }, + }, [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + style: { + display: props.index === 0 ? 'none' : 'inline-block', + }, + onClick: () => props.dispatch(actions.previousTx()), + }), + ` ${props.index + 1} of ${unconfTxList.length} `, + h('i.fa.fa-arrow-right.fa-lg.cursor-pointer', { + style: { + display: props.index + 1 === unconfTxList.length ? 'none' : 'inline-block', + }, + onClick: () => props.dispatch(actions.nextTx()), + }), + ]), + + warningIfExists(props.warning), + + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + + currentTxView({ + // Properties + txData: txData, + key: txData.id, + selectedAddress: props.selectedAddress, + accounts: props.accounts, + identities: props.identities, + conversionRate, + currentCurrency, + blockGasLimit, + // Actions + buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), + sendTransaction: this.sendTransaction.bind(this), + cancelTransaction: this.cancelTransaction.bind(this, txData), + signMessage: this.signMessage.bind(this, txData), + signPersonalMessage: this.signPersonalMessage.bind(this, txData), + cancelMessage: this.cancelMessage.bind(this, txData), + cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), + }), + + ]), + ]) + ) +} + +function currentTxView (opts) { + log.info('rendering current tx view') + const { txData } = opts + const { txParams, msgParams, type } = txData + + if (txParams) { + log.debug('txParams detected, rendering pending tx') + return h(PendingTx, opts) + } else if (msgParams) { + log.debug('msgParams detected, rendering pending msg') + + if (type === 'eth_sign') { + log.debug('rendering eth_sign message') + return h(PendingMsg, opts) + } else if (type === 'personal_sign') { + log.debug('rendering personal_sign message') + return h(PendingPersonalMsg, opts) + } + } +} + +ConfirmTxScreen.prototype.buyEth = function (address, event) { + event.preventDefault() + this.props.dispatch(actions.buyEthView(address)) +} + +ConfirmTxScreen.prototype.sendTransaction = function (txData, event) { + this.stopPropagation(event) + this.props.dispatch(actions.updateAndApproveTx(txData)) +} + +ConfirmTxScreen.prototype.cancelTransaction = function (txData, event) { + this.stopPropagation(event) + event.preventDefault() + this.props.dispatch(actions.cancelTx(txData)) +} + +ConfirmTxScreen.prototype.signMessage = function (msgData, event) { + log.info('conf-tx.js: signing message') + var params = msgData.msgParams + params.metamaskId = msgData.id + this.stopPropagation(event) + this.props.dispatch(actions.signMsg(params)) +} + +ConfirmTxScreen.prototype.stopPropagation = function (event) { + if (event.stopPropagation) { + event.stopPropagation() + } +} + +ConfirmTxScreen.prototype.signPersonalMessage = function (msgData, event) { + log.info('conf-tx.js: signing personal message') + var params = msgData.msgParams + params.metamaskId = msgData.id + this.stopPropagation(event) + this.props.dispatch(actions.signPersonalMsg(params)) +} + +ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) { + log.info('canceling message') + this.stopPropagation(event) + this.props.dispatch(actions.cancelMsg(msgData)) +} + +ConfirmTxScreen.prototype.cancelPersonalMessage = function (msgData, event) { + log.info('canceling personal message') + this.stopPropagation(event) + this.props.dispatch(actions.cancelPersonalMsg(msgData)) +} + +ConfirmTxScreen.prototype.goHome = function (event) { + this.stopPropagation(event) + this.props.dispatch(actions.goHome()) +} + +function warningIfExists (warning) { + if (warning && + // Do not display user rejections on this screen: + warning.indexOf('User denied transaction signature') === -1) { + return h('.error', { + style: { + margin: 'auto', + }, + }, warning) + } +} diff --git a/ui/app/config.js b/ui/app/config.js new file mode 100644 index 000000000..62785c49b --- /dev/null +++ b/ui/app/config.js @@ -0,0 +1,211 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') +const currencies = require('./conversion.json').rows +const validUrl = require('valid-url') +const copyToClipboard = require('copy-to-clipboard') + +module.exports = connect(mapStateToProps)(ConfigScreen) + +function mapStateToProps (state) { + return { + metamask: state.metamask, + warning: state.appState.warning, + } +} + +inherits(ConfigScreen, Component) +function ConfigScreen () { + Component.call(this) +} + +ConfigScreen.prototype.render = function () { + var state = this.props + var metamaskState = state.metamask + var warning = state.warning + + return ( + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + state.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Settings'), + ]), + + h('.error', { + style: { + display: warning ? 'block' : 'none', + padding: '0 20px', + textAlign: 'center', + }, + }, warning), + + // conf view + h('.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-space-around', { + style: { + padding: '20px', + }, + }, [ + + currentProviderDisplay(metamaskState), + + h('div', { style: {display: 'flex'} }, [ + h('input#new_rpc', { + placeholder: 'New RPC URL', + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onKeyPress (event) { + if (event.key === 'Enter') { + var element = event.target + var newRpc = element.value + rpcValidation(newRpc, state) + } + }, + }), + h('button', { + style: { + alignSelf: 'center', + }, + onClick (event) { + event.preventDefault() + var element = document.querySelector('input#new_rpc') + var newRpc = element.value + rpcValidation(newRpc, state) + }, + }, 'Save'), + ]), + + h('hr.horizontal-line'), + + currentConversionInformation(metamaskState, state), + + h('hr.horizontal-line'), + + h('div', { + style: { + marginTop: '20px', + }, + }, [ + h('p', { + style: { + fontFamily: 'Montserrat Light', + fontSize: '13px', + }, + }, `State logs contain your public account addresses and sent transactions.`), + h('br'), + h('button', { + style: { + alignSelf: 'center', + }, + onClick (event) { + copyToClipboard(window.logState()) + }, + }, 'Copy State Logs'), + ]), + + h('hr.horizontal-line'), + + h('div', { + style: { + marginTop: '20px', + }, + }, [ + h('button', { + style: { + alignSelf: 'center', + }, + onClick (event) { + event.preventDefault() + state.dispatch(actions.revealSeedConfirmation()) + }, + }, 'Reveal Seed Words'), + ]), + + ]), + ]), + ]) + ) +} + +function rpcValidation (newRpc, state) { + if (validUrl.isWebUri(newRpc)) { + state.dispatch(actions.setRpcTarget(newRpc)) + } else { + var appendedRpc = `http://${newRpc}` + if (validUrl.isWebUri(appendedRpc)) { + state.dispatch(actions.displayWarning('URIs require the appropriate HTTP/HTTPS prefix.')) + } else { + state.dispatch(actions.displayWarning('Invalid RPC URI')) + } + } +} + +function currentConversionInformation (metamaskState, state) { + var currentCurrency = metamaskState.currentCurrency + var conversionDate = metamaskState.conversionDate + return h('div', [ + h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, 'Current Conversion'), + h('span', {style: { fontWeight: 'bold', paddingRight: '10px', fontSize: '13px'}}, `Updated ${Date(conversionDate)}`), + h('select#currentCurrency', { + onChange (event) { + event.preventDefault() + var element = document.getElementById('currentCurrency') + var newCurrency = element.value + state.dispatch(actions.setCurrentCurrency(newCurrency)) + }, + defaultValue: currentCurrency, + }, currencies.map((currency) => { + return h('option', {key: currency.code, value: currency.code}, `${currency.code} - ${currency.name}`) + }) + ), + ]) +} + +function currentProviderDisplay (metamaskState) { + var provider = metamaskState.provider + var title, value + + switch (provider.type) { + + case 'mainnet': + title = 'Current Network' + value = 'Main Ethereum Network' + break + + case 'ropsten': + title = 'Current Network' + value = 'Ropsten Test Network' + break + + case 'kovan': + title = 'Current Network' + value = 'Kovan Test Network' + break + + case 'rinkeby': + title = 'Current Network' + value = 'Rinkeby Test Network' + break + + default: + title = 'Current RPC' + value = metamaskState.provider.rpcTarget + } + + return h('div', [ + h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, title), + h('span', value), + ]) +} diff --git a/ui/app/conversion.json b/ui/app/conversion.json new file mode 100644 index 000000000..155ffc4fc --- /dev/null +++ b/ui/app/conversion.json @@ -0,0 +1,207 @@ +{ + "rows": [ + { + "code": "REP", + "name": "Augur", + "statuses": [ + "primary" + ] + }, + { + "code": "BCN", + "name": "Bytecoin", + "statuses": [ + "primary" + ] + }, + { + "code": "BTC", + "name": "Bitcoin", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "BTS", + "name": "BitShares", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "BLK", + "name": "Blackcoin", + "statuses": [ + "primary" + ] + }, + { + "code": "GBP", + "name": "British Pound Sterling", + "statuses": [ + "secondary" + ] + }, + { + "code": "CAD", + "name": "Canadian Dollar", + "statuses": [ + "secondary" + ] + }, + { + "code": "CNY", + "name": "Chinese Yuan", + "statuses": [ + "secondary" + ] + }, + { + "code": "DSH", + "name": "Dashcoin", + "statuses": [ + "primary" + ] + }, + { + "code": "DOGE", + "name": "Dogecoin", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "ETC", + "name": "Ethereum Classic", + "statuses": [ + "primary" + ] + }, + { + "code": "EUR", + "name": "Euro", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "GNO", + "name": "GNO", + "statuses": [ + "primary" + ] + }, + { + "code": "GNT", + "name": "GNT", + "statuses": [ + "primary" + ] + }, + { + "code": "JPY", + "name": "Japanese Yen", + "statuses": [ + "secondary" + ] + }, + { + "code": "LTC", + "name": "Litecoin", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "MAID", + "name": "MaidSafeCoin", + "statuses": [ + "primary" + ] + }, + { + "code": "XEM", + "name": "NEM", + "statuses": [ + "primary" + ] + }, + { + "code": "XLM", + "name": "Stellar", + "statuses": [ + "primary" + ] + }, + { + "code": "XMR", + "name": "Monero", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "XRP", + "name": "Ripple", + "statuses": [ + "primary" + ] + }, + { + "code": "RUR", + "name": "Ruble", + "statuses": [ + "secondary" + ] + }, + { + "code": "STEEM", + "name": "Steem", + "statuses": [ + "primary" + ] + }, + { + "code": "STRAT", + "name": "STRAT", + "statuses": [ + "primary" + ] + }, + { + "code": "UAH", + "name": "Ukrainian Hryvnia", + "statuses": [ + "secondary" + ] + }, + { + "code": "USD", + "name": "US Dollar", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "WAVES", + "name": "WAVES", + "statuses": [ + "primary" + ] + }, + { + "code": "ZEC", + "name": "Zcash", + "statuses": [ + "primary" + ] + } + ] +} diff --git a/ui/app/css/debug.css b/ui/app/css/debug.css new file mode 100644 index 000000000..3e125bcd4 --- /dev/null +++ b/ui/app/css/debug.css @@ -0,0 +1,21 @@ +/* +debug / dev +*/ + +#app-content { + border: 2px solid green; +} + +#design-container { + position: absolute; + left: 360px; + top: -42px; + width: calc(100vw - 360px); + height: 100vh; + overflow: scroll; +} + +#design-container img { + width: 2000px; + margin-right: 600px; +} \ No newline at end of file diff --git a/ui/app/css/fonts.css b/ui/app/css/fonts.css new file mode 100644 index 000000000..3b9f581b9 --- /dev/null +++ b/ui/app/css/fonts.css @@ -0,0 +1,36 @@ +@import url(https://fonts.googleapis.com/css?family=Roboto:300,500); +@import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css); + +@font-face { + font-family: 'Montserrat Regular'; + src: url('/fonts/Montserrat/Montserrat-Regular.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; + font-size: 'small'; + +} + +@font-face { + font-family: 'Montserrat Bold'; + src: url('/fonts/Montserrat/Montserrat-Bold.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-Bold.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Montserrat Light'; + src: url('/fonts/Montserrat/Montserrat-Light.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-Light.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Montserrat UltraLight'; + src: url('/fonts/Montserrat/Montserrat-UltraLight.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-UltraLight.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} diff --git a/ui/app/css/index.css b/ui/app/css/index.css new file mode 100644 index 000000000..808aafb4c --- /dev/null +++ b/ui/app/css/index.css @@ -0,0 +1,667 @@ +/* +faint orange (textfield shades) #FAF6F0 +light orange (button shades): #F5C26D +dark orange (text): #F5A623 +borders/font/any gray: #4A4A4A +*/ + +/* +application specific styles +*/ + +* { + box-sizing: border-box; +} + +html, body { + font-family: 'Montserrat Regular', Arial; + color: #4D4D4D; + font-weight: 300; + line-height: 1.4em; + background: #F7F7F7; +} + +input:focus, textarea:focus { + outline: none; +} + +#app-content { + overflow-x: hidden; + min-width: 357px; + width: 360px; + height: 500px; +} + +button, input[type="submit"] { + font-family: 'Montserrat Bold'; + outline: none; + cursor: pointer; + padding: 8px 12px; + border: none; + color: white; + transform-origin: center center; + transition: transform 50ms ease-in; + /* default orange */ + background: rgba(247, 134, 28, 1); + box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); +} + +.btn-green, input[type="submit"].btn-green { + background: rgba(106, 195, 96, 1); + box-shadow: 0px 3px 6px rgba(106, 195, 96, 0.36); +} + +.btn-red { + background: rgba(254, 35, 17, 1); + box-shadow: 0px 3px 6px rgba(254, 35, 17, 0.36); +} + +button[disabled], input[type="submit"][disabled] { + cursor: not-allowed; + background: rgba(197, 197, 197, 1); + box-shadow: 0px 3px 6px rgba(197, 197, 197, 0.36); +} + +button.spaced { + margin: 2px; +} + +button:not([disabled]):hover, input[type="submit"]:not([disabled]):hover { + transform: scale(1.1); +} +button:not([disabled]):active, input[type="submit"]:not([disabled]):active { + transform: scale(0.95); +} + +a { + text-decoration: none; + color: inherit; +} + +a:hover{ + color: #df6b0e; +} + +/* +app +*/ + +.active { + color: #909090; +} + +button.primary { + padding: 8px 12px; + background: #F7861C; + box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); + color: white; + font-size: 1.1em; + font-family: 'Montserrat Regular'; + text-transform: uppercase; +} + +button.btn-thin { + border: 1px solid; + border-color: #4D4D4D; + color: #4D4D4D; + background: rgb(255, 174, 41); + border-radius: 4px; + min-width: 200px; + margin: 12px 0; + padding: 6px; + font-size: 13px; +} + +.app-header { + padding: 6px 8px; +} + +.app-header h1 { + font-family: 'Montserrat Regular'; + text-transform: uppercase; + color: #AEAEAE; +} + +h2.page-subtitle { + font-family: 'Montserrat Regular'; + text-transform: uppercase; + color: #AEAEAE; + font-size: 1em; + margin: 12px; +} + +.app-primary { + +} + +.app-footer { + padding-bottom: 10px; + align-items: center; +} + +.identicon { + height: 46px; + width: 46px; + background-size: cover; + border-radius: 100%; + border: 3px solid gray; +} + +textarea.twelve-word-phrase { + padding: 12px; + width: 300px; + height: 140px; + font-size: 16px; + background: white; + resize: none; +} + +.network-indicator { + display: flex; + align-items: center; + font-size: 0.6em; + +} + +.network-name { + width: 5.2em; + line-height: 9px; + text-rendering: geometricPrecision; +} + +.check { + margin-left: 7px; + color: #F7861C; + flex: 1 0 auto; + display: flex; + justify-content: flex-end; +} +/* +app sections +*/ + +/* initialize */ + +.initialize-screen hr { + width: 60px; + margin: 12px; + border-color: #F7861C; + border-style: solid; +} + +.initialize-screen label { + margin-top: 20px; +} + +.initialize-screen button.create-vault { + margin-top: 40px; +} + +.initialize-screen .warning { + font-size: 14px; + margin: 0 16px; +} + +/* unlock */ +.error { + color: #E20202; +} + +.warning { + color: #FFAE00; +} + +.lock { + width: 50px; + height: 50px; +} + +.lock.locked { + transform: scale(1.5); + opacity: 0.0; + transition: opacity 400ms ease-in, transform 400ms ease-in; +} +.lock.unlocked { + transform: scale(1); + opacity: 1; + transition: opacity 500ms ease-out, transform 500ms ease-out, background 200ms ease-in; +} + +.lock.locked .lock-top { + transform: scaleX(1) translateX(0); + transition: transform 250ms ease-in; +} +.lock.unlocked .lock-top { + transform: scaleX(-1) translateX(-12px); + transition: transform 250ms ease-in; +} +.lock.unlocked:hover { + border-radius: 4px; + background: #e5e5e5; + border: 1px solid #b1b1b1; +} +.lock.unlocked:active { + background: #c3c3c3; +} + +.section-title .fa-arrow-left { + margin: -2px 8px 0px -8px; +} + +.unlock-screen #metamask-mascot-container { + margin-top: 24px; +} + +.unlock-screen h1 { + margin-top: -28px; + margin-bottom: 42px; +} + +.unlock-screen input[type=password] { + width: 260px; + /*height: 36px; + margin-bottom: 24px; + padding: 8px;*/ +} + +.sizing-input{ + font-size: 14px; + height: 30px; + padding-left: 5px; +} +.editable-label{ + display: flex; +} +/* Webkit */ +.unlock-screen input::-webkit-input-placeholder { + text-align: center; + font-size: 1.2em; +} +/* Firefox 18- */ +.unlock-screen input:-moz-placeholder { + text-align: center; + font-size: 1.2em; +} +/* Firefox 19+ */ +.unlock-screen input::-moz-placeholder { + text-align: center; + font-size: 1.2em; +} +/* IE */ +.unlock-screen input:-ms-input-placeholder { + text-align: center; + font-size: 1.2em; +} + +input.large-input, textarea.large-input { + /*margin-bottom: 24px;*/ + padding: 8px; +} + +input.large-input { + height: 36px; +} + +.letter-spacey { + letter-spacing: 0.1em; +} + + + +/* accounts */ + +.accounts-section { + margin: 0 0px; +} + +.accounts-section .horizontal-line { + margin: 0px 18px; +} + +.accounts-list-option { + height: 120px; +} + +.accounts-list-option .identicon-wrapper { + width: 100px; +} + +.unconftx-link { + margin-top: 24px; + cursor: pointer; +} + +.unconftx-link .fa-arrow-right { + margin: 0px -8px 0px 8px; +} + +/* identity panel */ + +.identity-panel { + font-weight: 500; +} + +.identity-panel .identicon-wrapper { + margin: 4px; + margin-top: 8px; + display: flex; + align-items: center; +} + +.identity-panel .identicon-wrapper span { + margin: 0 auto; +} + +.identity-panel .identity-data { + margin: 8px 8px 8px 18px; +} + +.identity-panel i { + margin-top: 32px; + margin-right: 6px; + color: #B9B9B9; +} + +.identity-panel .arrow-right { + padding-left: 18px; + width: 42px; + min-width: 18px; + height: 100%; +} + +.identity-copy.flex-column { + flex: 0.25 0 auto; + justify-content: center; +} + +/* accounts screen */ + +.identity-section { + +} + +.identity-section .identity-panel { + background: #E9E9E9; + border-bottom: 1px solid #B1B1B1; + cursor: pointer; +} + +.identity-section .identity-panel.selected { + background: white; + color: #F3C83E; +} + +.identity-section .identity-panel.selected .identicon { + border-color: orange; +} + +.identity-section .accounts-list-option:hover, +.identity-section .accounts-list-option.selected { + background:white; +} + +/* account detail screen */ + +.account-detail-section { + +} +.name-label{ + +} + +.unapproved-tx-icon { + height: 16px; + width: 16px; + background: rgb(47, 174, 244); + border-color: #AEAEAE; + border-radius: 13px; +} + +.edit-text { + height: 100%; + visibility: hidden; +} +.editing-label { + display: flex; + justify-content: flex-start; + margin-left: 50px; + margin-bottom: 2px; + font-size: 11px; + text-rendering: geometricPrecision; + color: #F7861C; +} +.name-label:hover .edit-text { + visibility: visible; +} +/* tx confirm */ + +.unconftx-section input[type=password] { + height: 22px; + padding: 2px; + margin: 12px; + margin-bottom: 24px; + border-radius: 4px; + border: 2px solid #F3C83E; + background: #FAF6F0; +} + +/* Send Screen */ + +.send-screen { + +} + +.send-screen section { + margin: 8px 16px; +} + +.send-screen input { + width: 100%; + font-size: 12px; +} + +/* Ether Balance Widget */ + +.ether-balance-amount { + color: #F7861C; +} + +.ether-balance-label { + color: #ABA9AA; +} + +/* Info screen */ +.info-gray{ + font-family: 'Montserrat Regular'; + text-transform: uppercase; + color: #AEAEAE; +} + +.icon-size{ + width: 20px; +} + +.info{ + font-family: 'Montserrat Regular', Arial; + padding-bottom: 10px; + display: inline-block; + padding-left: 5px; +} + +/* buy eth warning screen */ +.custom-radios { + justify-content: space-around; + align-items: center; +} + + +.custom-radio-selected { + width: 17px; + height: 17px; + border: solid; + border-style: double; + border-radius: 15px; + border-width: 5px; + background: rgba(247, 134, 28, 1); + border-color: #F7F7F7; +} + +.custom-radio-inactive { + width: 14px; + height: 14px; + border: solid; + border-width: 1px; + border-radius: 24px; + border-color: #AEAEAE; +} + +.radio-titles { + color: rgba(247, 134, 28, 1); +} + +.radio-titles-subtext { + +} + +.selected-exchange { + +} + +.buy-radio { + +} + +.eth-warning{ + transition: opacity 400ms ease-in, transform 400ms ease-in; +} + +.buy-subview{ + transition: opacity 400ms ease-in, transform 400ms ease-in; +} + +.input-container:hover .edit-text{ + visibility: visible; +} + +.buy-inputs{ + font-family: 'Montserrat Light'; + font-size: 13px; + height: 20px; + background: transparent; + box-sizing: border-box; + border: solid; + border-color: transparent; + border-width: 0.5px; + border-radius: 2px; + +} +.input-container:hover .buy-inputs{ + box-sizing: inherit; + border: solid; + border-color: #F7861C; + border-width: 0.5px; + border-radius: 2px; +} + +.buy-inputs:focus{ + border: solid; + border-color: #F7861C; + border-width: 0.5px; + border-radius: 2px; +} + +.activeForm { + background: #F7F7F7; + border: none; + border-radius: 8px 8px 0px 0px; + width: 50%; + text-align: center; + padding-bottom: 4px; + +} + +.inactiveForm { + border: none; + border-radius: 8px 8px 0px 0px; + width: 50%; + text-align: center; + padding-bottom: 4px; +} + +.ex-coins { + font-family: 'Montserrat Regular'; + text-transform: uppercase; + text-align: center; + font-size: 33px; + width: 118px; + height: 42px; + padding: 1px; + color: #4D4D4D; +} + +.marketinfo{ + font-family: 'Montserrat light'; + color: #AEAEAE; + font-size: 15px; + line-height: 17px; +} + +#fromCoin::-webkit-calendar-picker-indicator { + display: none; +} + +#coinList { + width: 400px; + height: 500px; + overflow: scroll; +} + +.icon-control .fa-refresh{ + visibility: hidden; +} + +.icon-control:hover .fa-refresh{ + visibility: visible; +} + +.icon-control:hover .fa-chevron-right{ + visibility: hidden; +} + +.inactive { + color: #AEAEAE; +} + +.inactive button{ + background: #AEAEAE; + color: white; +} + +.ellip-address { + overflow: hidden; + text-overflow: ellipsis; + width: 5em; + font-size: 14px; + font-family: "Montserrat Light"; + margin-left: 5px; +} + +.qr-header { + font-size: 25px; + margin-top: 40px; +} + +.qr-message { + font-size: 12px; + color: #F7861C; +} + +div.message-container > div:first-child { + margin-top: 18px; + font-size: 15px; + color: #4D4D4D; +} + +.pop-hover:hover { + transform: scale(1.1); +} diff --git a/ui/app/css/lib.css b/ui/app/css/lib.css new file mode 100644 index 000000000..910a24ee2 --- /dev/null +++ b/ui/app/css/lib.css @@ -0,0 +1,268 @@ +/* color */ + +.color-orange { + color: #F7861C; +} + +.color-forest { + color: #0A5448; +} + +/* lib */ + +.full-width { + width: 100%; +} + +.full-height { + height: 100%; +} + +.flex-column { + display: flex; + flex-direction: column; +} + +.space-between { + justify-content: space-between; +} + +.space-around { + justify-content: space-around; +} + +.flex-column-bottom { + display: flex; + flex-direction: column-reverse; +} + +.flex-row { + display: flex; + flex-direction: row; +} + +.flex-space-between { + justify-content: space-between; +} + +.flex-space-around { + justify-content: space-around; +} + +.flex-right { + display: flex; + flex-direction: row; + justify-content: flex-end; +} + +.flex-left { + display: flex; + flex-direction: row; + justify-content: flex-start; +} + +.flex-fixed { + flex: none; +} + +.flex-basis-auto { + flex-basis: auto; +} + +.flex-grow { + flex: 1 1 auto; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.flex-center { + display: flex; + justify-content: center; + align-items: center; +} + +.flex-justify-center { + justify-content: center; +} + +.flex-align-center { + align-items: center; +} + +.flex-self-end { + align-self: flex-end; +} + +.flex-self-stretch { + align-self: stretch; +} + +.flex-vertical { + flex-direction: column; +} + +.z-bump { + z-index: 1; +} + +.select-none { + cursor: inherit; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.pointer { + cursor: pointer; +} +.cursor-pointer { + cursor: pointer; + transform-origin: center center; + transition: transform 50ms ease-in-out; +} +.cursor-pointer:hover { + transform: scale(1.1); +} +.cursor-pointer:active { + transform: scale(0.95); +} + +.cursor-disabled { + cursor: not-allowed; +} + +.margin-bottom-sml { + margin-bottom: 20px; +} + +.margin-bottom-med { + margin-bottom: 40px; +} + +.margin-right-left { + margin: 0 20px; +} + +.bold { + font-weight: bold; +} + +.text-transform-uppercase { + text-transform: uppercase; +} + +.font-small { + font-size: 12px; +} + +.font-medium { + font-size: 1.2em; +} + +hr.horizontal-line { + display: block; + height: 1px; + border: 0; + border-top: 1px solid #ccc; + margin: 1em 0; + padding: 0; +} + +.hover-white:hover { + background: white; +} + +.red-dot { + background: #E91550; + color: white; + border-radius: 10px; +} + +.diamond { + transform: rotate(45deg); + background: #038789; +} + +.hollow-diamond { + transform: rotate(45deg); + border: 3px solid #690496; +} + +.golden-square { + background: #EBB33F; +} + +.pending-dot { + background: red; + left: 14px; + top: 14px; + color: white; + border-radius: 10px; + height: 20px; + min-width: 20px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + z-index: 1; +} + +.keyring-label { + z-index: 1; + font-size: 11px; + background: rgba(255,0,0,0.8); + bottom: -47px; + color: white; + border-radius: 10px; + height: 20px; + min-width: 20px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding: 4px; +} + +.ether-balance { + display: flex; + align-items: center; +} + +.menu-icon { + display: inline-block; + height: 9px; + min-width: 9px; + margin: 13px; +} +.ether-icon { + background: rgb(0, 163, 68); + border-radius: 20px; +} +.testnet-icon { + background: #2465E1; +} + +.drop-menu-item { + display: flex; + align-items: center; +} + +.invisible { + visibility: hidden; +} + +.one-line-concat { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.critical-error { + text-align: center; + margin-top: 20px; + color: red; +} diff --git a/ui/app/css/reset.css b/ui/app/css/reset.css new file mode 100644 index 000000000..9ce89e8bc --- /dev/null +++ b/ui/app/css/reset.css @@ -0,0 +1,48 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +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; +} \ No newline at end of file diff --git a/ui/app/css/transitions.css b/ui/app/css/transitions.css new file mode 100644 index 000000000..393a944f9 --- /dev/null +++ b/ui/app/css/transitions.css @@ -0,0 +1,42 @@ +/* universal */ +.app-primary .main-enter { + position: absolute; + width: 100%; +} + +/* center position */ +.app-primary.from-right .main-enter-active, +.app-primary.from-left .main-enter-active { + overflow-x: hidden; + transform: translateX(0px); + transition: transform 300ms ease-in; +} + +/* exited positions */ +.app-primary.from-left .main-leave-active { + transform: translateX(360px); + transition: transform 300ms ease-in; +} +.app-primary.from-right .main-leave-active { + transform: translateX(-360px); + transition: transform 300ms ease-in; +} + +/* loader transitions */ +.loader-enter, .loader-leave-active { + opacity: 0.0; + transition: opacity 150 ease-in; +} +.loader-enter-active, .loader-leave { + opacity: 1.0; + transition: opacity 150 ease-in; +} + +/* entering positions */ +.app-primary.from-right .main-enter:not(.main-enter-active) { + transform: translateX(360px); +} +.app-primary.from-left .main-enter:not(.main-enter-active) { + transform: translateX(-360px); +} + diff --git a/ui/app/first-time/init-menu.js b/ui/app/first-time/init-menu.js new file mode 100644 index 000000000..cc7c51bd3 --- /dev/null +++ b/ui/app/first-time/init-menu.js @@ -0,0 +1,179 @@ +const inherits = require('util').inherits +const EventEmitter = require('events').EventEmitter +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const Mascot = require('../components/mascot') +const actions = require('../actions') +const Tooltip = require('../components/tooltip') +const getCaretCoordinates = require('textarea-caret') + +module.exports = connect(mapStateToProps)(InitializeMenuScreen) + +inherits(InitializeMenuScreen, Component) +function InitializeMenuScreen () { + Component.call(this) + this.animationEventEmitter = new EventEmitter() +} + +function mapStateToProps (state) { + return { + // state from plugin + currentView: state.appState.currentView, + warning: state.appState.warning, + } +} + +InitializeMenuScreen.prototype.render = function () { + var state = this.props + + switch (state.currentView.name) { + + default: + return this.renderMenu(state) + + } +} + +// InitializeMenuScreen.prototype.componentDidMount = function(){ +// document.getElementById('password-box').focus() +// } + +InitializeMenuScreen.prototype.renderMenu = function (state) { + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + h(Mascot, { + animationEventEmitter: this.animationEventEmitter, + }), + + h('h1', { + style: { + fontSize: '1.3em', + textTransform: 'uppercase', + color: '#7F8082', + marginBottom: 10, + }, + }, 'MetaMask'), + + + h('div', [ + h('h3', { + style: { + fontSize: '0.8em', + color: '#7F8082', + display: 'inline', + }, + }, 'Encrypt your new DEN'), + + h(Tooltip, { + title: 'Your DEN is your password-encrypted storage within MetaMask.', + }, [ + h('i.fa.fa-question-circle.pointer', { + style: { + fontSize: '18px', + position: 'relative', + color: 'rgb(247, 134, 28)', + top: '2px', + marginLeft: '4px', + }, + }), + ]), + ]), + + h('span.in-progress-notification', state.warning), + + // password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: 'New Password (min 8 chars)', + onInput: this.inputChanged.bind(this), + style: { + width: 260, + marginTop: 12, + }, + }), + + // confirm password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box-confirm', + placeholder: 'Confirm Password', + onKeyPress: this.createVaultOnEnter.bind(this), + onInput: this.inputChanged.bind(this), + style: { + width: 260, + marginTop: 16, + }, + }), + + + h('button.primary', { + onClick: this.createNewVaultAndKeychain.bind(this), + style: { + margin: 12, + }, + }, 'Create'), + + h('.flex-row.flex-center.flex-grow', [ + h('p.pointer', { + onClick: this.showRestoreVault.bind(this), + style: { + fontSize: '0.8em', + color: 'rgb(247, 134, 28)', + textDecoration: 'underline', + }, + }, 'Import Existing DEN'), + ]), + + ]) + ) +} + +InitializeMenuScreen.prototype.createVaultOnEnter = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.createNewVaultAndKeychain() + } +} + +InitializeMenuScreen.prototype.componentDidMount = function () { + document.getElementById('password-box').focus() +} + +InitializeMenuScreen.prototype.showRestoreVault = function () { + this.props.dispatch(actions.showRestoreVault()) +} + +InitializeMenuScreen.prototype.createNewVaultAndKeychain = function () { + var passwordBox = document.getElementById('password-box') + var password = passwordBox.value + var passwordConfirmBox = document.getElementById('password-box-confirm') + var passwordConfirm = passwordConfirmBox.value + + if (password.length < 8) { + this.warning = 'password not long enough' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + if (password !== passwordConfirm) { + this.warning = 'passwords don\'t match' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + + this.props.dispatch(actions.createNewVaultAndKeychain(password)) +} + +InitializeMenuScreen.prototype.inputChanged = function (event) { + // tell mascot to look at page action + var element = event.target + var boundingRect = element.getBoundingClientRect() + var coordinates = getCaretCoordinates(element, element.selectionEnd) + this.animationEventEmitter.emit('point', { + x: boundingRect.left + coordinates.left - element.scrollLeft, + y: boundingRect.top + coordinates.top - element.scrollTop, + }) +} diff --git a/ui/app/img/identicon-tardigrade.png b/ui/app/img/identicon-tardigrade.png new file mode 100644 index 000000000..1742a32b8 Binary files /dev/null and b/ui/app/img/identicon-tardigrade.png differ diff --git a/ui/app/img/identicon-walrus.png b/ui/app/img/identicon-walrus.png new file mode 100644 index 000000000..d58fae912 Binary files /dev/null and b/ui/app/img/identicon-walrus.png differ diff --git a/ui/app/info.js b/ui/app/info.js new file mode 100644 index 000000000..e8470de97 --- /dev/null +++ b/ui/app/info.js @@ -0,0 +1,154 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') + +module.exports = connect(mapStateToProps)(InfoScreen) + +function mapStateToProps (state) { + return {} +} + +inherits(InfoScreen, Component) +function InfoScreen () { + Component.call(this) +} + +InfoScreen.prototype.render = function () { + const state = this.props + const version = global.platform.getVersion() + + return ( + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + state.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Info'), + ]), + + // main view + h('.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-space-around', { + style: { + padding: '20px', + }, + }, [ + // current version number + + h('.info.info-gray', [ + h('div', 'Metamask'), + h('div', { + style: { + marginBottom: '10px', + }, + }, `Version: ${version}`), + ]), + + h('div', { + style: { + marginBottom: '5px', + }}, + [ + h('div', [ + h('a', { + href: 'https://metamask.io/privacy.html', + target: '_blank', + onClick (event) { this.navigateTo(event.target.href) }, + }, [ + h('div.info', 'Privacy Policy'), + ]), + ]), + h('div', [ + h('a', { + href: 'https://metamask.io/terms.html', + target: '_blank', + onClick (event) { this.navigateTo(event.target.href) }, + }, [ + h('div.info', 'Terms of Use'), + ]), + ]), + h('div', [ + h('a', { + href: 'https://metamask.io/attributions.html', + target: '_blank', + onClick (event) { this.navigateTo(event.target.href) }, + }, [ + h('div.info', 'Attributions'), + ]), + ]), + ] + ), + + h('hr', { + style: { + margin: '10px 0 ', + width: '7em', + }, + }), + + h('div', { + style: { + paddingLeft: '30px', + }}, + [ + h('div.fa.fa-github', [ + h('a.info', { + href: 'https://github.com/MetaMask/faq', + target: '_blank', + }, 'Need Help? Read our FAQ!'), + ]), + h('div', [ + h('a', { + href: 'https://metamask.io/', + target: '_blank', + }, [ + h('img.icon-size', { + src: 'images/icon-128.png', + style: { + // IE6-9 + filter: 'grayscale(100%)', + // Microsoft Edge and Firefox 35+ + WebkitFilter: 'grayscale(100%)', + }, + }), + h('div.info', 'Visit our web site'), + ]), + ]), + h('div.fa.fa-slack', [ + h('a.info', { + href: 'http://slack.metamask.io', + target: '_blank', + }, 'Join the conversation on Slack'), + ]), + + h('div.fa.fa-twitter', [ + h('a.info', { + href: 'https://twitter.com/metamask_io', + target: '_blank', + }, 'Follow us on Twitter'), + ]), + + h('div.fa.fa-envelope', [ + h('a.info', { + target: '_blank', + style: { width: '85vw' }, + href: 'mailto:help@metamask.io?subject=Feedback', + }, 'Email us!'), + ]), + ]), + ]), + ]), + ]) + ) +} + +InfoScreen.prototype.navigateTo = function (url) { + global.platform.openWindow({ url }) +} + diff --git a/ui/app/keychains/hd/create-vault-complete.js b/ui/app/keychains/hd/create-vault-complete.js new file mode 100644 index 000000000..a318a9b50 --- /dev/null +++ b/ui/app/keychains/hd/create-vault-complete.js @@ -0,0 +1,78 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('../../actions') + +module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen) + +inherits(CreateVaultCompleteScreen, Component) +function CreateVaultCompleteScreen () { + Component.call(this) +} + +function mapStateToProps (state) { + return { + seed: state.appState.currentView.seedWords, + cachedSeed: state.metamask.seedWords, + } +} + +CreateVaultCompleteScreen.prototype.render = function () { + var state = this.props + var seed = state.seed || state.cachedSeed || '' + + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + // // subtitle and nav + // h('.section-title.flex-row.flex-center', [ + // h('h2.page-subtitle', 'Vault Created'), + // ]), + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginTop: 36, + marginBottom: 8, + width: '100%', + fontSize: '20px', + padding: 6, + }, + }, [ + 'Vault Created', + ]), + + h('div', { + style: { + width: '360px', + height: '78px', + fontSize: '1em', + marginTop: '10px', + textAlign: 'center', + }, + }, [ + h('span.error', 'These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret.'), + ]), + + h('textarea.twelve-word-phrase', { + readOnly: true, + value: seed, + }), + + h('button.primary', { + onClick: () => this.confirmSeedWords(), + style: { + margin: '24px', + fontSize: '0.9em', + }, + }, 'I\'ve copied it somewhere safe'), + ]) + ) +} + +CreateVaultCompleteScreen.prototype.confirmSeedWords = function () { + this.props.dispatch(actions.confirmSeedWords()) +} diff --git a/ui/app/keychains/hd/recover-seed/confirmation.js b/ui/app/keychains/hd/recover-seed/confirmation.js new file mode 100644 index 000000000..4ccbec9fc --- /dev/null +++ b/ui/app/keychains/hd/recover-seed/confirmation.js @@ -0,0 +1,118 @@ +const inherits = require('util').inherits + +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('../../../actions') + +module.exports = connect(mapStateToProps)(RevealSeedConfirmation) + +inherits(RevealSeedConfirmation, Component) +function RevealSeedConfirmation () { + Component.call(this) +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +RevealSeedConfirmation.prototype.render = function () { + const props = this.props + + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginBottom: 24, + width: '100%', + fontSize: '20px', + padding: 6, + }, + }, [ + 'Reveal Seed Words', + ]), + + h('.div', { + style: { + display: 'flex', + flexDirection: 'column', + padding: '20px', + justifyContent: 'center', + }, + }, [ + + h('h4', 'Do not recover your seed words in a public place! These words can be used to steal all your accounts.'), + + // confirmation + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: 'Enter your password to confirm', + onKeyPress: this.checkConfirmation.bind(this), + style: { + width: 260, + marginTop: '12px', + }, + }), + + h('.flex-row.flex-space-between', { + style: { + marginTop: 30, + width: '50%', + }, + }, [ + // cancel + h('button.primary', { + onClick: this.goHome.bind(this), + }, 'CANCEL'), + + // submit + h('button.primary', { + onClick: this.revealSeedWords.bind(this), + }, 'OK'), + + ]), + + (props.warning) && ( + h('span.error', { + style: { + margin: '20px', + }, + }, props.warning.split('-')) + ), + + props.inProgress && ( + h('span.in-progress-notification', 'Generating Seed...') + ), + ]), + ]) + ) +} + +RevealSeedConfirmation.prototype.componentDidMount = function () { + document.getElementById('password-box').focus() +} + +RevealSeedConfirmation.prototype.goHome = function () { + this.props.dispatch(actions.showConfigPage(false)) +} + +// create vault + +RevealSeedConfirmation.prototype.checkConfirmation = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.revealSeedWords() + } +} + +RevealSeedConfirmation.prototype.revealSeedWords = function () { + var password = document.getElementById('password-box').value + this.props.dispatch(actions.requestRevealSeed(password)) +} diff --git a/ui/app/keychains/hd/restore-vault.js b/ui/app/keychains/hd/restore-vault.js new file mode 100644 index 000000000..06e51d9b3 --- /dev/null +++ b/ui/app/keychains/hd/restore-vault.js @@ -0,0 +1,152 @@ +const inherits = require('util').inherits +const PersistentForm = require('../../../lib/persistent-form') +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('../../actions') + +module.exports = connect(mapStateToProps)(RestoreVaultScreen) + +inherits(RestoreVaultScreen, PersistentForm) +function RestoreVaultScreen () { + PersistentForm.call(this) +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + forgottenPassword: state.appState.forgottenPassword, + } +} + +RestoreVaultScreen.prototype.render = function () { + var state = this.props + this.persistentFormParentId = 'restore-vault-form' + + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginBottom: 24, + width: '100%', + fontSize: '20px', + padding: 6, + }, + }, [ + 'Restore Vault', + ]), + + // wallet seed entry + h('h3', 'Wallet Seed'), + h('textarea.twelve-word-phrase.letter-spacey', { + dataset: { + persistentFormId: 'wallet-seed', + }, + placeholder: 'Enter your secret twelve word phrase here to restore your vault.', + }), + + // password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: 'New Password (min 8 chars)', + dataset: { + persistentFormId: 'password', + }, + style: { + width: 260, + marginTop: 12, + }, + }), + + // confirm password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box-confirm', + placeholder: 'Confirm Password', + onKeyPress: this.createOnEnter.bind(this), + dataset: { + persistentFormId: 'password-confirmation', + }, + style: { + width: 260, + marginTop: 16, + }, + }), + + (state.warning) && ( + h('span.error.in-progress-notification', state.warning) + ), + + // submit + + h('.flex-row.flex-space-between', { + style: { + marginTop: 30, + width: '50%', + }, + }, [ + + // cancel + h('button.primary', { + onClick: this.showInitializeMenu.bind(this), + }, 'CANCEL'), + + // submit + h('button.primary', { + onClick: this.createNewVaultAndRestore.bind(this), + }, 'OK'), + + ]), + ]) + + ) +} + +RestoreVaultScreen.prototype.showInitializeMenu = function () { + if (this.props.forgottenPassword) { + this.props.dispatch(actions.backToUnlockView()) + } else { + this.props.dispatch(actions.showInitializeMenu()) + } +} + +RestoreVaultScreen.prototype.createOnEnter = function (event) { + if (event.key === 'Enter') { + this.createNewVaultAndRestore() + } +} + +RestoreVaultScreen.prototype.createNewVaultAndRestore = function () { + // check password + var passwordBox = document.getElementById('password-box') + var password = passwordBox.value + var passwordConfirmBox = document.getElementById('password-box-confirm') + var passwordConfirm = passwordConfirmBox.value + if (password.length < 8) { + this.warning = 'Password not long enough' + + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + if (password !== passwordConfirm) { + this.warning = 'Passwords don\'t match' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + // check seed + var seedBox = document.querySelector('textarea.twelve-word-phrase') + var seed = seedBox.value.trim() + if (seed.split(' ').length !== 12) { + this.warning = 'seed phrases are 12 words long' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + // submit + this.warning = null + this.props.dispatch(actions.displayWarning(this.warning)) + this.props.dispatch(actions.createNewVaultAndRestore(password, seed)) +} diff --git a/ui/app/new-keychain.js b/ui/app/new-keychain.js new file mode 100644 index 000000000..cc9633166 --- /dev/null +++ b/ui/app/new-keychain.js @@ -0,0 +1,29 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect + +module.exports = connect(mapStateToProps)(NewKeychain) + +function mapStateToProps (state) { + return {} +} + +inherits(NewKeychain, Component) +function NewKeychain () { + Component.call(this) +} + +NewKeychain.prototype.render = function () { + // const props = this.props + + return ( + h('div', { + style: { + background: 'blue', + }, + }, [ + h('h1', `Here's a list!!!!`), + ]) + ) +} diff --git a/ui/app/reducers.js b/ui/app/reducers.js new file mode 100644 index 000000000..11efca529 --- /dev/null +++ b/ui/app/reducers.js @@ -0,0 +1,52 @@ +const extend = require('xtend') + +// +// Sub-Reducers take in the complete state and return their sub-state +// +const reduceIdentities = require('./reducers/identities') +const reduceMetamask = require('./reducers/metamask') +const reduceApp = require('./reducers/app') + +window.METAMASK_CACHED_LOG_STATE = null + +module.exports = rootReducer + +function rootReducer (state, action) { + // clone + state = extend(state) + + if (action.type === 'GLOBAL_FORCE_UPDATE') { + return action.value + } + + // + // Identities + // + + state.identities = reduceIdentities(state, action) + + // + // MetaMask + // + + state.metamask = reduceMetamask(state, action) + + // + // AppState + // + + state.appState = reduceApp(state, action) + + window.METAMASK_CACHED_LOG_STATE = state + return state +} + +window.logState = function () { + var stateString = JSON.stringify(window.METAMASK_CACHED_LOG_STATE, removeSeedWords, 2) + console.log(stateString) + return stateString +} + +function removeSeedWords (key, value) { + return key === 'seedWords' ? undefined : value +} diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js new file mode 100644 index 000000000..2fcc9bfe0 --- /dev/null +++ b/ui/app/reducers/app.js @@ -0,0 +1,585 @@ +const extend = require('xtend') +const actions = require('../actions') +const txHelper = require('../../lib/tx-helper') + +module.exports = reduceApp + + +function reduceApp (state, action) { + log.debug('App Reducer got ' + action.type) + // clone and defaults + const selectedAddress = state.metamask.selectedAddress + const hasUnconfActions = checkUnconfActions(state) + let name = 'accounts' + if (selectedAddress) { + name = 'accountDetail' + } + if (hasUnconfActions) { + log.debug('pending txs detected, defaulting to conf-tx view.') + name = 'confTx' + } + + var defaultView = { + name, + detailView: null, + context: selectedAddress, + } + + // confirm seed words + var seedWords = state.metamask.seedWords + var seedConfView = { + name: 'createVaultComplete', + seedWords, + } + + // default state + var appState = extend({ + shouldClose: false, + menuOpen: false, + currentView: seedWords ? seedConfView : defaultView, + accountDetail: { + subview: 'transactions', + }, + transForward: true, // Used to render transition direction + isLoading: false, // Used to display loading indicator + warning: null, // Used to display error text + }, state.appState) + + switch (action.type) { + + // transition methods + + case actions.TRANSITION_FORWARD: + return extend(appState, { + transForward: true, + }) + + case actions.TRANSITION_BACKWARD: + return extend(appState, { + transForward: false, + }) + + // intialize + + case actions.SHOW_CREATE_VAULT: + return extend(appState, { + currentView: { + name: 'createVault', + }, + transForward: true, + warning: null, + }) + + case actions.SHOW_RESTORE_VAULT: + return extend(appState, { + currentView: { + name: 'restoreVault', + }, + transForward: true, + forgottenPassword: true, + }) + + case actions.FORGOT_PASSWORD: + return extend(appState, { + currentView: { + name: 'restoreVault', + }, + transForward: false, + forgottenPassword: true, + }) + + case actions.SHOW_INIT_MENU: + return extend(appState, { + currentView: defaultView, + transForward: false, + }) + + case actions.SHOW_CONFIG_PAGE: + return extend(appState, { + currentView: { + name: 'config', + context: appState.currentView.context, + }, + transForward: action.value, + }) + + case actions.SHOW_ADD_TOKEN_PAGE: + return extend(appState, { + currentView: { + name: 'add-token', + context: appState.currentView.context, + }, + transForward: action.value, + }) + + case actions.SHOW_IMPORT_PAGE: + + return extend(appState, { + currentView: { + name: 'import-menu', + }, + transForward: true, + }) + + case actions.SHOW_INFO_PAGE: + return extend(appState, { + currentView: { + name: 'info', + context: appState.currentView.context, + }, + transForward: true, + }) + + case actions.CREATE_NEW_VAULT_IN_PROGRESS: + return extend(appState, { + currentView: { + name: 'createVault', + inProgress: true, + }, + transForward: true, + isLoading: true, + }) + + case actions.SHOW_NEW_VAULT_SEED: + return extend(appState, { + currentView: { + name: 'createVaultComplete', + seedWords: action.value, + }, + transForward: true, + isLoading: false, + }) + + case actions.NEW_ACCOUNT_SCREEN: + return extend(appState, { + currentView: { + name: 'new-account', + context: appState.currentView.context, + }, + transForward: true, + }) + + case actions.SHOW_SEND_PAGE: + return extend(appState, { + currentView: { + name: 'sendTransaction', + context: appState.currentView.context, + }, + transForward: true, + warning: null, + }) + + case actions.SHOW_NEW_KEYCHAIN: + return extend(appState, { + currentView: { + name: 'newKeychain', + context: appState.currentView.context, + }, + transForward: true, + }) + + // unlock + + case actions.UNLOCK_METAMASK: + return extend(appState, { + forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null, + detailView: {}, + transForward: true, + isLoading: false, + warning: null, + }) + + case actions.LOCK_METAMASK: + return extend(appState, { + currentView: defaultView, + transForward: false, + warning: null, + }) + + case actions.BACK_TO_INIT_MENU: + return extend(appState, { + warning: null, + transForward: false, + forgottenPassword: true, + currentView: { + name: 'InitMenu', + }, + }) + + case actions.BACK_TO_UNLOCK_VIEW: + return extend(appState, { + warning: null, + transForward: true, + forgottenPassword: false, + currentView: { + name: 'UnlockScreen', + }, + }) + // reveal seed words + + case actions.REVEAL_SEED_CONFIRMATION: + return extend(appState, { + currentView: { + name: 'reveal-seed-conf', + }, + transForward: true, + warning: null, + }) + + // accounts + + case actions.SET_SELECTED_ACCOUNT: + return extend(appState, { + activeAddress: action.value, + }) + + case actions.GO_HOME: + return extend(appState, { + currentView: extend(appState.currentView, { + name: 'accountDetail', + }), + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + transForward: false, + warning: null, + }) + + case actions.SHOW_ACCOUNT_DETAIL: + return extend(appState, { + forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null, + currentView: { + name: 'accountDetail', + context: action.value, + }, + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + transForward: false, + }) + + case actions.BACK_TO_ACCOUNT_DETAIL: + return extend(appState, { + currentView: { + name: 'accountDetail', + context: action.value, + }, + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + transForward: false, + }) + + case actions.SHOW_ACCOUNTS_PAGE: + return extend(appState, { + currentView: { + name: seedWords ? 'createVaultComplete' : 'accounts', + seedWords, + }, + transForward: true, + isLoading: false, + warning: null, + scrollToBottom: false, + forgottenPassword: false, + }) + + case actions.SHOW_NOTICE: + return extend(appState, { + transForward: true, + isLoading: false, + }) + + case actions.REVEAL_ACCOUNT: + return extend(appState, { + scrollToBottom: true, + }) + + case actions.SHOW_CONF_TX_PAGE: + return extend(appState, { + currentView: { + name: 'confTx', + context: 0, + }, + transForward: action.transForward, + warning: null, + isLoading: false, + }) + + case actions.SHOW_CONF_MSG_PAGE: + return extend(appState, { + currentView: { + name: hasUnconfActions ? 'confTx' : 'account-detail', + context: 0, + }, + transForward: true, + warning: null, + isLoading: false, + }) + + case actions.COMPLETED_TX: + log.debug('reducing COMPLETED_TX for tx ' + action.value) + const otherUnconfActions = getUnconfActionList(state) + .filter(tx => tx.id !== action.value) + const hasOtherUnconfActions = otherUnconfActions.length > 0 + + if (hasOtherUnconfActions) { + log.debug('reducer detected txs - rendering confTx view') + return extend(appState, { + transForward: false, + currentView: { + name: 'confTx', + context: 0, + }, + warning: null, + }) + } else { + log.debug('attempting to close popup') + return extend(appState, { + // indicate notification should close + shouldClose: true, + transForward: false, + warning: null, + currentView: { + name: 'accountDetail', + context: state.metamask.selectedAddress, + }, + accountDetail: { + subview: 'transactions', + }, + }) + } + + case actions.NEXT_TX: + return extend(appState, { + transForward: true, + currentView: { + name: 'confTx', + context: ++appState.currentView.context, + warning: null, + }, + }) + + case actions.VIEW_PENDING_TX: + const context = indexForPending(state, action.value) + return extend(appState, { + transForward: true, + currentView: { + name: 'confTx', + context, + warning: null, + }, + }) + + case actions.PREVIOUS_TX: + return extend(appState, { + transForward: false, + currentView: { + name: 'confTx', + context: --appState.currentView.context, + warning: null, + }, + }) + + case actions.TRANSACTION_ERROR: + return extend(appState, { + currentView: { + name: 'confTx', + errorMessage: 'There was a problem submitting this transaction.', + }, + }) + + case actions.UNLOCK_FAILED: + return extend(appState, { + warning: action.value || 'Incorrect password. Try again.', + }) + + case actions.SHOW_LOADING: + return extend(appState, { + isLoading: true, + loadingMessage: action.value, + }) + + case actions.HIDE_LOADING: + return extend(appState, { + isLoading: false, + }) + + case actions.SHOW_SUB_LOADING_INDICATION: + return extend(appState, { + isSubLoading: true, + }) + + case actions.HIDE_SUB_LOADING_INDICATION: + return extend(appState, { + isSubLoading: false, + }) + case actions.CLEAR_SEED_WORD_CACHE: + return extend(appState, { + transForward: true, + currentView: {}, + isLoading: false, + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + }) + + case actions.DISPLAY_WARNING: + return extend(appState, { + warning: action.value, + isLoading: false, + }) + + case actions.HIDE_WARNING: + return extend(appState, { + warning: undefined, + }) + + case actions.REQUEST_ACCOUNT_EXPORT: + return extend(appState, { + transForward: true, + currentView: { + name: 'accountDetail', + context: appState.currentView.context, + }, + accountDetail: { + subview: 'export', + accountExport: 'requested', + }, + }) + + case actions.EXPORT_ACCOUNT: + return extend(appState, { + accountDetail: { + subview: 'export', + accountExport: 'completed', + }, + }) + + case actions.SHOW_PRIVATE_KEY: + return extend(appState, { + accountDetail: { + subview: 'export', + accountExport: 'completed', + privateKey: action.value, + }, + }) + + case actions.BUY_ETH_VIEW: + return extend(appState, { + transForward: true, + currentView: { + name: 'buyEth', + context: appState.currentView.name, + }, + identity: state.metamask.identities[action.value], + buyView: { + subview: 'Coinbase', + amount: '15.00', + buyAddress: action.value, + formView: { + coinbase: true, + shapeshift: false, + }, + }, + }) + + case actions.COINBASE_SUBVIEW: + return extend(appState, { + buyView: { + subview: 'Coinbase', + formView: { + coinbase: true, + shapeshift: false, + }, + buyAddress: appState.buyView.buyAddress, + amount: appState.buyView.amount, + }, + }) + + case actions.SHAPESHIFT_SUBVIEW: + return extend(appState, { + buyView: { + subview: 'ShapeShift', + formView: { + coinbase: false, + shapeshift: true, + marketinfo: action.value.marketinfo, + coinOptions: action.value.coinOptions, + }, + buyAddress: appState.buyView.buyAddress, + amount: appState.buyView.amount, + }, + }) + + case actions.PAIR_UPDATE: + return extend(appState, { + buyView: { + subview: 'ShapeShift', + formView: { + coinbase: false, + shapeshift: true, + marketinfo: action.value.marketinfo, + coinOptions: appState.buyView.formView.coinOptions, + }, + buyAddress: appState.buyView.buyAddress, + amount: appState.buyView.amount, + warning: null, + }, + }) + + case actions.SHOW_QR: + return extend(appState, { + qrRequested: true, + transForward: true, + + Qr: { + message: action.value.message, + data: action.value.data, + }, + }) + + case actions.SHOW_QR_VIEW: + return extend(appState, { + currentView: { + name: 'qr', + context: appState.currentView.context, + }, + transForward: true, + Qr: { + message: action.value.message, + data: action.value.data, + }, + }) + default: + return appState + } +} + +function checkUnconfActions (state) { + const unconfActionList = getUnconfActionList(state) + const hasUnconfActions = unconfActionList.length > 0 + return hasUnconfActions +} + +function getUnconfActionList (state) { + const { unapprovedTxs, unapprovedMsgs, + unapprovedPersonalMsgs, network } = state.metamask + + const unconfActionList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) + return unconfActionList +} + +function indexForPending (state, txId) { + const unconfTxList = getUnconfActionList(state) + const match = unconfTxList.find((tx) => tx.id === txId) + const index = unconfTxList.indexOf(match) + return index +} diff --git a/ui/app/reducers/identities.js b/ui/app/reducers/identities.js new file mode 100644 index 000000000..341a404e7 --- /dev/null +++ b/ui/app/reducers/identities.js @@ -0,0 +1,15 @@ +const extend = require('xtend') + +module.exports = reduceIdentities + +function reduceIdentities (state, action) { + // clone + defaults + var idState = extend({ + + }, state.identities) + + switch (action.type) { + default: + return idState + } +} diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js new file mode 100644 index 000000000..e0c416c2d --- /dev/null +++ b/ui/app/reducers/metamask.js @@ -0,0 +1,137 @@ +const extend = require('xtend') +const actions = require('../actions') + +module.exports = reduceMetamask + +function reduceMetamask (state, action) { + let newState + + // clone + defaults + var metamaskState = extend({ + isInitialized: false, + isUnlocked: false, + rpcTarget: 'https://rawtestrpc.metamask.io/', + identities: {}, + unapprovedTxs: {}, + noActiveNotices: true, + lastUnreadNotice: undefined, + frequentRpcList: [], + addressBook: [], + }, state.metamask) + + switch (action.type) { + + case actions.SHOW_ACCOUNTS_PAGE: + newState = extend(metamaskState) + delete newState.seedWords + return newState + + case actions.SHOW_NOTICE: + return extend(metamaskState, { + noActiveNotices: false, + lastUnreadNotice: action.value, + }) + + case actions.CLEAR_NOTICES: + return extend(metamaskState, { + noActiveNotices: true, + }) + + case actions.UPDATE_METAMASK_STATE: + return extend(metamaskState, action.value) + + case actions.UNLOCK_METAMASK: + return extend(metamaskState, { + isUnlocked: true, + isInitialized: true, + selectedAddress: action.value, + }) + + case actions.LOCK_METAMASK: + return extend(metamaskState, { + isUnlocked: false, + }) + + case actions.SET_RPC_LIST: + return extend(metamaskState, { + frequentRpcList: action.value, + }) + + case actions.SET_RPC_TARGET: + return extend(metamaskState, { + provider: { + type: 'rpc', + rpcTarget: action.value, + }, + }) + + case actions.SET_PROVIDER_TYPE: + return extend(metamaskState, { + provider: { + type: action.value, + }, + }) + + case actions.COMPLETED_TX: + var stringId = String(action.id) + newState = extend(metamaskState, { + unapprovedTxs: {}, + unapprovedMsgs: {}, + }) + for (const id in metamaskState.unapprovedTxs) { + if (id !== stringId) { + newState.unapprovedTxs[id] = metamaskState.unapprovedTxs[id] + } + } + for (const id in metamaskState.unapprovedMsgs) { + if (id !== stringId) { + newState.unapprovedMsgs[id] = metamaskState.unapprovedMsgs[id] + } + } + return newState + + case actions.SHOW_NEW_VAULT_SEED: + return extend(metamaskState, { + isUnlocked: true, + isInitialized: false, + seedWords: action.value, + }) + + case actions.CLEAR_SEED_WORD_CACHE: + newState = extend(metamaskState, { + isUnlocked: true, + isInitialized: true, + selectedAddress: action.value, + }) + delete newState.seedWords + return newState + + case actions.SHOW_ACCOUNT_DETAIL: + newState = extend(metamaskState, { + isUnlocked: true, + isInitialized: true, + selectedAddress: action.value, + }) + delete newState.seedWords + return newState + + case actions.SAVE_ACCOUNT_LABEL: + const account = action.value.account + const name = action.value.label + var id = {} + id[account] = extend(metamaskState.identities[account], { name }) + var identities = extend(metamaskState.identities, id) + return extend(metamaskState, { identities }) + + case actions.SET_CURRENT_FIAT: + return extend(metamaskState, { + currentCurrency: action.value.currentCurrency, + conversionRate: action.value.conversionRate, + conversionDate: action.value.conversionDate, + }) + + default: + return metamaskState + + } +} diff --git a/ui/app/root.js b/ui/app/root.js new file mode 100644 index 000000000..9e7314b20 --- /dev/null +++ b/ui/app/root.js @@ -0,0 +1,22 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const Provider = require('react-redux').Provider +const h = require('react-hyperscript') +const App = require('./app') + +module.exports = Root + +inherits(Root, Component) +function Root () { Component.call(this) } + +Root.prototype.render = function () { + return ( + + h(Provider, { + store: this.props.store, + }, [ + h(App), + ]) + + ) +} diff --git a/ui/app/send.js b/ui/app/send.js new file mode 100644 index 000000000..a21a219eb --- /dev/null +++ b/ui/app/send.js @@ -0,0 +1,288 @@ +const inherits = require('util').inherits +const PersistentForm = require('../lib/persistent-form') +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const Identicon = require('./components/identicon') +const actions = require('./actions') +const util = require('./util') +const numericBalance = require('./util').numericBalance +const addressSummary = require('./util').addressSummary +const isHex = require('./util').isHex +const EthBalance = require('./components/eth-balance') +const EnsInput = require('./components/ens-input') +const ethUtil = require('ethereumjs-util') +module.exports = connect(mapStateToProps)(SendTransactionScreen) + +function mapStateToProps (state) { + var result = { + address: state.metamask.selectedAddress, + accounts: state.metamask.accounts, + identities: state.metamask.identities, + warning: state.appState.warning, + network: state.metamask.network, + addressBook: state.metamask.addressBook, + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + } + + result.error = result.warning && result.warning.split('.')[0] + + result.account = result.accounts[result.address] + result.identity = result.identities[result.address] + result.balance = result.account ? numericBalance(result.account.balance) : null + + return result +} + +inherits(SendTransactionScreen, PersistentForm) +function SendTransactionScreen () { + PersistentForm.call(this) +} + +SendTransactionScreen.prototype.render = function () { + this.persistentFormParentId = 'send-tx-form' + + const props = this.props + const { + address, + account, + identity, + network, + identities, + addressBook, + conversionRate, + currentCurrency, + } = props + + return ( + + h('.send-screen.flex-column.flex-grow', [ + + // + // Sender Profile + // + + h('.account-data-subsection.flex-row.flex-grow', { + style: { + margin: '0 20px', + }, + }, [ + + // header - identicon + nav + h('.flex-row.flex-space-between', { + style: { + marginTop: '15px', + }, + }, [ + // back button + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { + onClick: this.back.bind(this), + }), + + // large identicon + h('.identicon-wrapper.flex-column.flex-center.select-none', [ + h(Identicon, { + diameter: 62, + address: address, + }), + ]), + + // invisible place holder + h('i.fa.fa-users.fa-lg.invisible', { + style: { + marginTop: '28px', + }, + }), + + ]), + + // account label + + h('.flex-column', { + style: { + marginTop: '10px', + alignItems: 'flex-start', + }, + }, [ + h('h2.font-medium.color-forest.flex-center', { + style: { + paddingTop: '8px', + marginBottom: '8px', + }, + }, identity && identity.name), + + // address and getter actions + h('.flex-row.flex-center', { + style: { + marginBottom: '8px', + }, + }, [ + + h('div', { + style: { + lineHeight: '16px', + }, + }, addressSummary(address)), + + ]), + + // balance + h('.flex-row.flex-center', [ + + h(EthBalance, { + value: account && account.balance, + conversionRate, + currentCurrency, + }), + + ]), + ]), + ]), + + // + // Required Fields + // + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginTop: '15px', + marginBottom: '16px', + }, + }, [ + 'Send Transaction', + ]), + + // error message + props.error && h('span.error.flex-center', props.error), + + // 'to' field + h('section.flex-row.flex-center', [ + h(EnsInput, { + name: 'address', + placeholder: 'Recipient Address', + onChange: this.recipientDidChange.bind(this), + network, + identities, + addressBook, + }), + ]), + + // 'amount' and send button + h('section.flex-row.flex-center', [ + + h('input.large-input', { + name: 'amount', + placeholder: 'Amount', + type: 'number', + style: { + marginRight: '6px', + }, + dataset: { + persistentFormId: 'tx-amount', + }, + }), + + h('button.primary', { + onClick: this.onSubmit.bind(this), + style: { + textTransform: 'uppercase', + }, + }, 'Next'), + + ]), + + // + // Optional Fields + // + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginTop: '16px', + marginBottom: '16px', + }, + }, [ + 'Transaction Data (optional)', + ]), + + // 'data' field + h('section.flex-column.flex-center', [ + h('input.large-input', { + name: 'txData', + placeholder: '0x01234', + style: { + width: '100%', + resize: 'none', + }, + dataset: { + persistentFormId: 'tx-data', + }, + }), + ]), + ]) + ) +} + +SendTransactionScreen.prototype.navigateToAccounts = function (event) { + event.stopPropagation() + this.props.dispatch(actions.showAccountsPage()) +} + +SendTransactionScreen.prototype.back = function () { + var address = this.props.address + this.props.dispatch(actions.backToAccountDetail(address)) +} + +SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickname) { + this.setState({ + recipient: recipient, + nickname: nickname, + }) +} + +SendTransactionScreen.prototype.onSubmit = function () { + const state = this.state || {} + const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '') + const nickname = state.nickname || ' ' + const input = document.querySelector('input[name="amount"]').value + const value = util.normalizeEthStringToWei(input) + const txData = document.querySelector('input[name="txData"]').value + const balance = this.props.balance + let message + + if (value.gt(balance)) { + message = 'Insufficient funds.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if (input < 0) { + message = 'Can not send negative amounts of ETH.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) { + message = 'Recipient address is invalid.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if (!isHex(ethUtil.stripHexPrefix(txData)) && txData) { + message = 'Transaction data must be hex string.' + return this.props.dispatch(actions.displayWarning(message)) + } + + this.props.dispatch(actions.hideWarning()) + + this.props.dispatch(actions.addToAddressBook(recipient, nickname)) + + var txParams = { + from: this.props.address, + value: '0x' + value.toString(16), + } + + if (recipient) txParams.to = ethUtil.addHexPrefix(recipient) + if (txData) txParams.data = txData + + this.props.dispatch(actions.signTx(txParams)) +} diff --git a/ui/app/settings.js b/ui/app/settings.js new file mode 100644 index 000000000..454cc95e0 --- /dev/null +++ b/ui/app/settings.js @@ -0,0 +1,59 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') + +module.exports = connect(mapStateToProps)(AppSettingsPage) + +function mapStateToProps (state) { + return {} +} + +inherits(AppSettingsPage, Component) +function AppSettingsPage () { + Component.call(this) +} + +AppSettingsPage.prototype.render = function () { + return ( + + h('.account-detail-section.flex-column.flex-grow', [ + + // subtitle and nav + h('.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: this.navigateToAccounts.bind(this), + }), + h('h2.page-subtitle', 'Settings'), + ]), + + h('label', { + htmlFor: 'settings-rpc-endpoint', + }, 'RPC Endpoint:'), + h('input', { + type: 'url', + id: 'settings-rpc-endpoint', + onKeyPress: this.onKeyPress.bind(this), + }), + + ]) + + ) +} + +AppSettingsPage.prototype.componentDidMount = function () { + document.querySelector('input').focus() +} + +AppSettingsPage.prototype.onKeyPress = function (event) { + // get submit event + if (event.key === 'Enter') { + // this.submitPassword(event) + } +} + +AppSettingsPage.prototype.navigateToAccounts = function (event) { + event.stopPropagation() + this.props.dispatch(actions.showAccountsPage()) +} diff --git a/ui/app/store.js b/ui/app/store.js new file mode 100644 index 000000000..ba9e58b49 --- /dev/null +++ b/ui/app/store.js @@ -0,0 +1,21 @@ +const createStore = require('redux').createStore +const applyMiddleware = require('redux').applyMiddleware +const thunkMiddleware = require('redux-thunk') +const rootReducer = require('./reducers') +const createLogger = require('redux-logger') + +global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' + +module.exports = configureStore + +const loggerMiddleware = createLogger({ + predicate: () => global.METAMASK_DEBUG, +}) + +const middlewares = [thunkMiddleware, loggerMiddleware] + +const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore) + +function configureStore (initialState) { + return createStoreWithMiddleware(rootReducer, initialState) +} diff --git a/ui/app/template.js b/ui/app/template.js new file mode 100644 index 000000000..d15b30fd2 --- /dev/null +++ b/ui/app/template.js @@ -0,0 +1,30 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect + +module.exports = connect(mapStateToProps)(COMPONENTNAME) + +function mapStateToProps (state) { + return {} +} + +inherits(COMPONENTNAME, Component) +function COMPONENTNAME () { + Component.call(this) +} + +COMPONENTNAME.prototype.render = function () { + const props = this.props + + return ( + h('div', { + style: { + background: 'blue', + }, + }, [ + `Hello, ${props.sender}`, + ]) + ) +} + diff --git a/ui/app/unlock.js b/ui/app/unlock.js new file mode 100644 index 000000000..1aee3c5d0 --- /dev/null +++ b/ui/app/unlock.js @@ -0,0 +1,118 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') +const getCaretCoordinates = require('textarea-caret') +const EventEmitter = require('events').EventEmitter + +const Mascot = require('./components/mascot') + +module.exports = connect(mapStateToProps)(UnlockScreen) + +inherits(UnlockScreen, Component) +function UnlockScreen () { + Component.call(this) + this.animationEventEmitter = new EventEmitter() +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +UnlockScreen.prototype.render = function () { + const state = this.props + const warning = state.warning + return ( + h('.flex-column', [ + h('.unlock-screen.flex-column.flex-center.flex-grow', [ + + h(Mascot, { + animationEventEmitter: this.animationEventEmitter, + }), + + h('h1', { + style: { + fontSize: '1.4em', + textTransform: 'uppercase', + color: '#7F8082', + }, + }, 'MetaMask'), + + h('input.large-input', { + type: 'password', + id: 'password-box', + placeholder: 'enter password', + style: { + + }, + onKeyPress: this.onKeyPress.bind(this), + onInput: this.inputChanged.bind(this), + }), + + h('.error', { + style: { + display: warning ? 'block' : 'none', + padding: '0 20px', + textAlign: 'center', + }, + }, warning), + + h('button.primary.cursor-pointer', { + onClick: this.onSubmit.bind(this), + style: { + margin: 10, + }, + }, 'Unlock'), + ]), + + h('.flex-row.flex-center.flex-grow', [ + h('p.pointer', { + onClick: () => this.props.dispatch(actions.forgotPassword()), + style: { + fontSize: '0.8em', + color: 'rgb(247, 134, 28)', + textDecoration: 'underline', + }, + }, 'I forgot my password.'), + ]), + ]) + ) +} + +UnlockScreen.prototype.componentDidMount = function () { + document.getElementById('password-box').focus() +} + +UnlockScreen.prototype.onSubmit = function (event) { + const input = document.getElementById('password-box') + const password = input.value + this.props.dispatch(actions.tryUnlockMetamask(password)) +} + +UnlockScreen.prototype.onKeyPress = function (event) { + if (event.key === 'Enter') { + this.submitPassword(event) + } +} + +UnlockScreen.prototype.submitPassword = function (event) { + var element = event.target + var password = element.value + // reset input + element.value = '' + this.props.dispatch(actions.tryUnlockMetamask(password)) +} + +UnlockScreen.prototype.inputChanged = function (event) { + // tell mascot to look at page action + var element = event.target + var boundingRect = element.getBoundingClientRect() + var coordinates = getCaretCoordinates(element, element.selectionEnd) + this.animationEventEmitter.emit('point', { + x: boundingRect.left + coordinates.left - element.scrollLeft, + y: boundingRect.top + coordinates.top - element.scrollTop, + }) +} diff --git a/ui/app/util.js b/ui/app/util.js new file mode 100644 index 000000000..ac3f42c6b --- /dev/null +++ b/ui/app/util.js @@ -0,0 +1,217 @@ +const ethUtil = require('ethereumjs-util') + +var valueTable = { + wei: '1000000000000000000', + kwei: '1000000000000000', + mwei: '1000000000000', + gwei: '1000000000', + szabo: '1000000', + finney: '1000', + ether: '1', + kether: '0.001', + mether: '0.000001', + gether: '0.000000001', + tether: '0.000000000001', +} +var bnTable = {} +for (var currency in valueTable) { + bnTable[currency] = new ethUtil.BN(valueTable[currency], 10) +} + +module.exports = { + valuesFor: valuesFor, + addressSummary: addressSummary, + miniAddressSummary: miniAddressSummary, + isAllOneCase: isAllOneCase, + isValidAddress: isValidAddress, + numericBalance: numericBalance, + parseBalance: parseBalance, + formatBalance: formatBalance, + generateBalanceObject: generateBalanceObject, + dataSize: dataSize, + readableDate: readableDate, + normalizeToWei: normalizeToWei, + normalizeEthStringToWei: normalizeEthStringToWei, + normalizeNumberToWei: normalizeNumberToWei, + valueTable: valueTable, + bnTable: bnTable, + isHex: isHex, +} + +function valuesFor (obj) { + if (!obj) return [] + return Object.keys(obj) + .map(function (key) { return obj[key] }) +} + +function addressSummary (address, firstSegLength = 10, lastSegLength = 4, includeHex = true) { + if (!address) return '' + let checked = ethUtil.toChecksumAddress(address) + if (!includeHex) { + checked = ethUtil.stripHexPrefix(checked) + } + return checked ? checked.slice(0, firstSegLength) + '...' + checked.slice(checked.length - lastSegLength) : '...' +} + +function miniAddressSummary (address) { + if (!address) return '' + var checked = ethUtil.toChecksumAddress(address) + return checked ? checked.slice(0, 4) + '...' + checked.slice(-4) : '...' +} + +function isValidAddress (address) { + var prefixed = ethUtil.addHexPrefix(address) + if (address === '0x0000000000000000000000000000000000000000') return false + return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed) +} + +function isAllOneCase (address) { + if (!address) return true + var lower = address.toLowerCase() + var upper = address.toUpperCase() + return address === lower || address === upper +} + +// Takes wei Hex, returns wei BN, even if input is null +function numericBalance (balance) { + if (!balance) return new ethUtil.BN(0, 16) + var stripped = ethUtil.stripHexPrefix(balance) + return new ethUtil.BN(stripped, 16) +} + +// Takes hex, returns [beforeDecimal, afterDecimal] +function parseBalance (balance) { + var beforeDecimal, afterDecimal + const wei = numericBalance(balance) + var weiString = wei.toString() + const trailingZeros = /0+$/ + + beforeDecimal = weiString.length > 18 ? weiString.slice(0, weiString.length - 18) : '0' + afterDecimal = ('000000000000000000' + wei).slice(-18).replace(trailingZeros, '') + if (afterDecimal === '') { afterDecimal = '0' } + return [beforeDecimal, afterDecimal] +} + +// Takes wei hex, returns an object with three properties. +// Its "formatted" property is what we generally use to render values. +function formatBalance (balance, decimalsToKeep, needsParse = true) { + var parsed = needsParse ? parseBalance(balance) : balance.split('.') + var beforeDecimal = parsed[0] + var afterDecimal = parsed[1] + var formatted = 'None' + if (decimalsToKeep === undefined) { + if (beforeDecimal === '0') { + if (afterDecimal !== '0') { + var sigFigs = afterDecimal.match(/^0*(.{2})/) // default: grabs 2 most significant digits + if (sigFigs) { afterDecimal = sigFigs[0] } + formatted = '0.' + afterDecimal + ' ETH' + } + } else { + formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ' ETH' + } + } else { + afterDecimal += Array(decimalsToKeep).join('0') + formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ' ETH' + } + return formatted +} + + +function generateBalanceObject (formattedBalance, decimalsToKeep = 1) { + var balance = formattedBalance.split(' ')[0] + var label = formattedBalance.split(' ')[1] + var beforeDecimal = balance.split('.')[0] + var afterDecimal = balance.split('.')[1] + var shortBalance = shortenBalance(balance, decimalsToKeep) + + if (beforeDecimal === '0' && afterDecimal.substr(0, 5) === '00000') { + // eslint-disable-next-line eqeqeq + if (afterDecimal == 0) { + balance = '0' + } else { + balance = '<1.0e-5' + } + } else if (beforeDecimal !== '0') { + balance = `${beforeDecimal}.${afterDecimal.slice(0, decimalsToKeep)}` + } + + return { balance, label, shortBalance } +} + +function shortenBalance (balance, decimalsToKeep = 1) { + var truncatedValue + var convertedBalance = parseFloat(balance) + if (convertedBalance > 1000000) { + truncatedValue = (balance / 1000000).toFixed(decimalsToKeep) + return `${truncatedValue}m` + } else if (convertedBalance > 1000) { + truncatedValue = (balance / 1000).toFixed(decimalsToKeep) + return `${truncatedValue}k` + } else if (convertedBalance === 0) { + return '0' + } else if (convertedBalance < 0.001) { + return '<0.001' + } else if (convertedBalance < 1) { + var stringBalance = convertedBalance.toString() + if (stringBalance.split('.')[1].length > 3) { + return convertedBalance.toFixed(3) + } else { + return stringBalance + } + } else { + return convertedBalance.toFixed(decimalsToKeep) + } +} + +function dataSize (data) { + var size = data ? ethUtil.stripHexPrefix(data).length : 0 + return size + ' bytes' +} + +// Takes a BN and an ethereum currency name, +// returns a BN in wei +function normalizeToWei (amount, currency) { + try { + return amount.mul(bnTable.wei).div(bnTable[currency]) + } catch (e) {} + return amount +} + +function normalizeEthStringToWei (str) { + const parts = str.split('.') + let eth = new ethUtil.BN(parts[0], 10).mul(bnTable.wei) + if (parts[1]) { + var decimal = parts[1] + while (decimal.length < 18) { + decimal += '0' + } + const decimalBN = new ethUtil.BN(decimal, 10) + eth = eth.add(decimalBN) + } + return eth +} + +var multiple = new ethUtil.BN('10000', 10) +function normalizeNumberToWei (n, currency) { + var enlarged = n * 10000 + var amount = new ethUtil.BN(String(enlarged), 10) + return normalizeToWei(amount, currency).div(multiple) +} + +function readableDate (ms) { + var date = new Date(ms) + var month = date.getMonth() + var day = date.getDate() + var year = date.getFullYear() + var hours = date.getHours() + var minutes = '0' + date.getMinutes() + var seconds = '0' + date.getSeconds() + + var dateStr = `${month}/${day}/${year}` + var time = `${hours}:${minutes.substr(-2)}:${seconds.substr(-2)}` + return `${dateStr} ${time}` +} + +function isHex (str) { + return Boolean(str.match(/^(0x)?[0-9a-fA-F]+$/)) +} diff --git a/ui/classic/.gitignore b/ui/classic/.gitignore deleted file mode 100644 index c6b1254b5..000000000 --- a/ui/classic/.gitignore +++ /dev/null @@ -1,66 +0,0 @@ - -# Created by https://www.gitignore.io/api/osx,node - -### OSX ### -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - - -### Node ### -# Logs -logs -*.log -npm-debug.log* - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directory -# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git -node_modules - -# Optional npm cache directory -.npm - -# Optional REPL history -.node_repl_history - diff --git a/ui/classic/app/account-detail.js b/ui/classic/app/account-detail.js deleted file mode 100644 index bed05a7fb..000000000 --- a/ui/classic/app/account-detail.js +++ /dev/null @@ -1,311 +0,0 @@ -const inherits = require('util').inherits -const extend = require('xtend') -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const CopyButton = require('./components/copyButton') -const AccountInfoLink = require('./components/account-info-link') -const actions = require('./actions') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') -const valuesFor = require('./util').valuesFor - -const Identicon = require('./components/identicon') -const EthBalance = require('./components/eth-balance') -const TransactionList = require('./components/transaction-list') -const ExportAccountView = require('./components/account-export') -const ethUtil = require('ethereumjs-util') -const EditableLabel = require('./components/editable-label') -const Tooltip = require('./components/tooltip') -const TabBar = require('./components/tab-bar') -const TokenList = require('./components/token-list') - -module.exports = connect(mapStateToProps)(AccountDetailScreen) - -function mapStateToProps (state) { - return { - metamask: state.metamask, - identities: state.metamask.identities, - accounts: state.metamask.accounts, - address: state.metamask.selectedAddress, - accountDetail: state.appState.accountDetail, - network: state.metamask.network, - unapprovedMsgs: valuesFor(state.metamask.unapprovedMsgs), - shapeShiftTxList: state.metamask.shapeShiftTxList, - transactions: state.metamask.selectedAddressTxList || [], - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - currentAccountTab: state.metamask.currentAccountTab, - tokens: state.metamask.tokens, - } -} - -inherits(AccountDetailScreen, Component) -function AccountDetailScreen () { - Component.call(this) -} - -AccountDetailScreen.prototype.render = function () { - var props = this.props - var selected = props.address || Object.keys(props.accounts)[0] - var checksumAddress = selected && ethUtil.toChecksumAddress(selected) - var identity = props.identities[selected] - var account = props.accounts[selected] - const { network, conversionRate, currentCurrency } = props - - return ( - - h('.account-detail-section', [ - - // identicon, label, balance, etc - h('.account-data-subsection', { - style: { - margin: '0 20px', - }, - }, [ - - // header - identicon + nav - h('div', { - style: { - paddingTop: '20px', - display: 'flex', - justifyContent: 'flex-start', - alignItems: 'flex-start', - }, - }, [ - - // large identicon and addresses - h('.identicon-wrapper.select-none', [ - h(Identicon, { - diameter: 62, - address: selected, - }), - ]), - h('flex-column', { - style: { - lineHeight: '10px', - marginLeft: '15px', - }, - }, [ - h(EditableLabel, { - textValue: identity ? identity.name : '', - state: { - isEditingLabel: false, - }, - saveText: (text) => { - props.dispatch(actions.saveAccountLabel(selected, text)) - }, - }, [ - - // What is shown when not editing + edit text: - h('label.editing-label', [h('.edit-text', 'edit')]), - h('h2.font-medium.color-forest', {name: 'edit'}, identity && identity.name), - ]), - h('.flex-row', { - style: { - width: '15em', - justifyContent: 'space-between', - alignItems: 'baseline', - }, - }, [ - - // address - - h('div', { - style: { - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingTop: '3px', - width: '5em', - fontSize: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - marginTop: '10px', - marginBottom: '15px', - color: '#AEAEAE', - }, - }, checksumAddress), - - // copy and export - - h('.flex-row', { - style: { - justifyContent: 'flex-end', - }, - }, [ - - h(AccountInfoLink, { selected, network }), - - h(CopyButton, { - value: checksumAddress, - }), - - h(Tooltip, { - title: 'QR Code', - }, [ - h('i.fa.fa-qrcode.pointer.pop-hover', { - onClick: () => props.dispatch(actions.showQrView(selected, identity ? identity.name : '')), - style: { - fontSize: '18px', - position: 'relative', - color: 'rgb(247, 134, 28)', - top: '5px', - marginLeft: '3px', - marginRight: '3px', - }, - }), - ]), - - h(Tooltip, { - title: 'Export Private Key', - }, [ - h('div', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - h('img.cursor-pointer.color-orange', { - src: 'images/key-32.png', - onClick: () => this.requestAccountExport(selected), - style: { - height: '19px', - }, - }), - ]), - ]), - ]), - ]), - - // account ballence - - ]), - ]), - h('.flex-row', { - style: { - justifyContent: 'space-between', - alignItems: 'flex-start', - }, - }, [ - - h(EthBalance, { - value: account && account.balance, - conversionRate, - currentCurrency, - style: { - lineHeight: '7px', - marginTop: '10px', - }, - }), - - h('button', { - onClick: () => props.dispatch(actions.buyEthView(selected)), - style: { - marginBottom: '20px', - marginRight: '8px', - position: 'absolute', - left: '219px', - }, - }, 'BUY'), - - h('button', { - onClick: () => props.dispatch(actions.showSendPage()), - style: { - marginBottom: '20px', - marginRight: '8px', - }, - }, 'SEND'), - - ]), - ]), - - // subview (tx history, pk export confirm, buy eth warning) - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.subview(), - ]), - - ]) - ) -} - -AccountDetailScreen.prototype.subview = function () { - var subview - try { - subview = this.props.accountDetail.subview - } catch (e) { - subview = null - } - - switch (subview) { - case 'transactions': - return this.tabSections() - case 'export': - var state = extend({key: 'export'}, this.props) - return h(ExportAccountView, state) - default: - return this.tabSections() - } -} - -AccountDetailScreen.prototype.tabSections = function () { - const { currentAccountTab } = this.props - - return h('section.tabSection', [ - - h(TabBar, { - tabs: [ - { content: 'Sent', key: 'history' }, - { content: 'Tokens', key: 'tokens' }, - ], - defaultTab: currentAccountTab || 'history', - tabSelected: (key) => { - this.props.dispatch(actions.setCurrentAccountTab(key)) - }, - }), - - this.tabSwitchView(), - ]) -} - -AccountDetailScreen.prototype.tabSwitchView = function () { - const props = this.props - const { address, network } = props - const { currentAccountTab, tokens } = this.props - - switch (currentAccountTab) { - case 'tokens': - return h(TokenList, { - userAddress: address, - network, - tokens, - addToken: () => this.props.dispatch(actions.showAddTokenPage()), - }) - default: - return this.transactionList() - } -} - -AccountDetailScreen.prototype.transactionList = function () { - const {transactions, unapprovedMsgs, address, - network, shapeShiftTxList, conversionRate } = this.props - - return h(TransactionList, { - transactions: transactions.sort((a, b) => b.time - a.time), - network, - unapprovedMsgs, - conversionRate, - address, - shapeShiftTxList, - viewPendingTx: (txId) => { - this.props.dispatch(actions.viewPendingTx(txId)) - }, - }) -} - -AccountDetailScreen.prototype.requestAccountExport = function () { - this.props.dispatch(actions.requestExportAccount()) -} diff --git a/ui/classic/app/accounts/account-list-item.js b/ui/classic/app/accounts/account-list-item.js deleted file mode 100644 index 10a0b6cc7..000000000 --- a/ui/classic/app/accounts/account-list-item.js +++ /dev/null @@ -1,91 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ethUtil = require('ethereumjs-util') - -const EthBalance = require('../components/eth-balance') -const CopyButton = require('../components/copyButton') -const Identicon = require('../components/identicon') - -module.exports = AccountListItem - -inherits(AccountListItem, Component) -function AccountListItem () { - Component.call(this) -} - -AccountListItem.prototype.render = function () { - const { identity, selectedAddress, accounts, onShowDetail, - conversionRate, currentCurrency } = this.props - - const checksumAddress = identity && identity.address && ethUtil.toChecksumAddress(identity.address) - const isSelected = selectedAddress === identity.address - const account = accounts[identity.address] - const selectedClass = isSelected ? '.selected' : '' - - return ( - h(`.accounts-list-option.flex-row.flex-space-between.pointer.hover-white${selectedClass}`, { - key: `account-panel-${identity.address}`, - onClick: (event) => onShowDetail(identity.address, event), - }, [ - - h('.identicon-wrapper.flex-column.flex-center.select-none', [ - this.pendingOrNot(), - this.indicateIfLoose(), - h(Identicon, { - address: identity.address, - imageify: true, - }), - ]), - - // account address, balance - h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', { - style: { - width: '200px', - }, - }, [ - h('span', identity.name), - h('span.font-small', { - style: { - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - }, checksumAddress), - h(EthBalance, { - value: account && account.balance, - currentCurrency, - conversionRate, - style: { - lineHeight: '7px', - marginTop: '10px', - }, - }), - ]), - - // copy button - h('.identity-copy.flex-column', { - style: { - margin: '0 20px', - }, - }, [ - h(CopyButton, { - value: checksumAddress, - }), - ]), - ]) - ) -} - -AccountListItem.prototype.indicateIfLoose = function () { - try { // Sometimes keyrings aren't loaded yet: - const type = this.props.keyring.type - const isLoose = type !== 'HD Key Tree' - return isLoose ? h('.keyring-label', 'LOOSE') : null - } catch (e) { return } -} - -AccountListItem.prototype.pendingOrNot = function () { - const pending = this.props.pending - if (pending.length === 0) return null - return h('.pending-dot', pending.length) -} diff --git a/ui/classic/app/accounts/import/index.js b/ui/classic/app/accounts/import/index.js deleted file mode 100644 index 97b387229..000000000 --- a/ui/classic/app/accounts/import/index.js +++ /dev/null @@ -1,100 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../../actions') -import Select from 'react-select' - -// Subviews -const JsonImportView = require('./json.js') -const PrivateKeyImportView = require('./private-key.js') - -const menuItems = [ - 'Private Key', - 'JSON File', -] - -module.exports = connect(mapStateToProps)(AccountImportSubview) - -function mapStateToProps (state) { - return { - menuItems, - } -} - -inherits(AccountImportSubview, Component) -function AccountImportSubview () { - Component.call(this) -} - -AccountImportSubview.prototype.render = function () { - const props = this.props - const state = this.state || {} - const { menuItems } = props - const { type } = state - - return ( - h('div', { - style: { - }, - }, [ - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: (event) => { - props.dispatch(actions.goHome()) - }, - }), - h('h2.page-subtitle', 'Import Accounts'), - ]), - h('div', { - style: { - padding: '10px', - color: 'rgb(174, 174, 174)', - }, - }, [ - - h('h3', { style: { padding: '3px' } }, 'SELECT TYPE'), - - h('style', ` - .has-value.Select--single > .Select-control .Select-value .Select-value-label, .Select-value-label { - color: rgb(174,174,174); - } - `), - - h(Select, { - name: 'import-type-select', - clearable: false, - value: type || menuItems[0], - options: menuItems.map((type) => { - return { - value: type, - label: type, - } - }), - onChange: (opt) => { - this.setState({ type: opt.value }) - }, - }), - ]), - - this.renderImportView(), - ]) - ) -} - -AccountImportSubview.prototype.renderImportView = function () { - const props = this.props - const state = this.state || {} - const { type } = state - const { menuItems } = props - const current = type || menuItems[0] - - switch (current) { - case 'Private Key': - return h(PrivateKeyImportView) - case 'JSON File': - return h(JsonImportView) - default: - return h(JsonImportView) - } -} diff --git a/ui/classic/app/accounts/import/json.js b/ui/classic/app/accounts/import/json.js deleted file mode 100644 index 158a3c923..000000000 --- a/ui/classic/app/accounts/import/json.js +++ /dev/null @@ -1,100 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../../actions') -const FileInput = require('react-simple-file-input').default - -const HELP_LINK = 'https://github.com/MetaMask/faq/blob/master/README.md#q-i-cant-use-the-import-feature-for-uploading-a-json-file-the-window-keeps-closing-when-i-try-to-select-a-file' - -module.exports = connect(mapStateToProps)(JsonImportSubview) - -function mapStateToProps (state) { - return { - error: state.appState.warning, - } -} - -inherits(JsonImportSubview, Component) -function JsonImportSubview () { - Component.call(this) -} - -JsonImportSubview.prototype.render = function () { - const { error } = this.props - - return ( - h('div', { - style: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - padding: '5px 15px 0px 15px', - }, - }, [ - - h('p', 'Used by a variety of different clients'), - h('a.warning', { href: HELP_LINK, target: '_blank' }, 'File import not working? Click here!'), - - h(FileInput, { - readAs: 'text', - onLoad: this.onLoad.bind(this), - style: { - margin: '20px 0px 12px 20px', - fontSize: '15px', - }, - }), - - h('input.large-input.letter-spacey', { - type: 'password', - placeholder: 'Enter password', - id: 'json-password-box', - onKeyPress: this.createKeyringOnEnter.bind(this), - style: { - width: 260, - marginTop: 12, - }, - }), - - h('button.primary', { - onClick: this.createNewKeychain.bind(this), - style: { - margin: 12, - }, - }, 'Import'), - - error ? h('span.error', error) : null, - ]) - ) -} - -JsonImportSubview.prototype.onLoad = function (event, file) { - this.setState({file: file, fileContents: event.target.result}) -} - -JsonImportSubview.prototype.createKeyringOnEnter = function (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.createNewKeychain() - } -} - -JsonImportSubview.prototype.createNewKeychain = function () { - const state = this.state - const { fileContents } = state - - if (!fileContents) { - const message = 'You must select a file to import.' - return this.props.dispatch(actions.displayWarning(message)) - } - - const passwordInput = document.getElementById('json-password-box') - const password = passwordInput.value - - if (!password) { - const message = 'You must enter a password for the selected file.' - return this.props.dispatch(actions.displayWarning(message)) - } - - this.props.dispatch(actions.importNewAccount('JSON File', [ fileContents, password ])) -} diff --git a/ui/classic/app/accounts/import/private-key.js b/ui/classic/app/accounts/import/private-key.js deleted file mode 100644 index 68ccee58e..000000000 --- a/ui/classic/app/accounts/import/private-key.js +++ /dev/null @@ -1,67 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../../actions') - -module.exports = connect(mapStateToProps)(PrivateKeyImportView) - -function mapStateToProps (state) { - return { - error: state.appState.warning, - } -} - -inherits(PrivateKeyImportView, Component) -function PrivateKeyImportView () { - Component.call(this) -} - -PrivateKeyImportView.prototype.render = function () { - const { error } = this.props - - return ( - h('div', { - style: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - padding: '5px 15px 0px 15px', - }, - }, [ - h('span', 'Paste your private key string here'), - - h('input.large-input.letter-spacey', { - type: 'password', - id: 'private-key-box', - onKeyPress: this.createKeyringOnEnter.bind(this), - style: { - width: 260, - marginTop: 12, - }, - }), - - h('button.primary', { - onClick: this.createNewKeychain.bind(this), - style: { - margin: 12, - }, - }, 'Import'), - - error ? h('span.error', error) : null, - ]) - ) -} - -PrivateKeyImportView.prototype.createKeyringOnEnter = function (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.createNewKeychain() - } -} - -PrivateKeyImportView.prototype.createNewKeychain = function () { - const input = document.getElementById('private-key-box') - const privateKey = input.value - this.props.dispatch(actions.importNewAccount('Private Key', [ privateKey ])) -} diff --git a/ui/classic/app/accounts/import/seed.js b/ui/classic/app/accounts/import/seed.js deleted file mode 100644 index b4a7c0afa..000000000 --- a/ui/classic/app/accounts/import/seed.js +++ /dev/null @@ -1,30 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect - -module.exports = connect(mapStateToProps)(SeedImportSubview) - -function mapStateToProps (state) { - return {} -} - -inherits(SeedImportSubview, Component) -function SeedImportSubview () { - Component.call(this) -} - -SeedImportSubview.prototype.render = function () { - return ( - h('div', { - style: { - }, - }, [ - `Paste your seed phrase here!`, - h('textarea'), - h('br'), - h('button', 'Submit'), - ]) - ) -} - diff --git a/ui/classic/app/accounts/index.js b/ui/classic/app/accounts/index.js deleted file mode 100644 index ac2615cd7..000000000 --- a/ui/classic/app/accounts/index.js +++ /dev/null @@ -1,164 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../actions') -const valuesFor = require('../util').valuesFor -const findDOMNode = require('react-dom').findDOMNode -const AccountListItem = require('./account-list-item') - -module.exports = connect(mapStateToProps)(AccountsScreen) - -function mapStateToProps (state) { - const pendingTxs = valuesFor(state.metamask.unapprovedTxs) - .filter(txMeta => txMeta.metamaskNetworkId === state.metamask.network) - const pendingMsgs = valuesFor(state.metamask.unapprovedMsgs) - const pending = pendingTxs.concat(pendingMsgs) - - return { - accounts: state.metamask.accounts, - identities: state.metamask.identities, - unapprovedTxs: state.metamask.unapprovedTxs, - selectedAddress: state.metamask.selectedAddress, - scrollToBottom: state.appState.scrollToBottom, - pending, - keyrings: state.metamask.keyrings, - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - } -} - -inherits(AccountsScreen, Component) -function AccountsScreen () { - Component.call(this) -} - -AccountsScreen.prototype.render = function () { - const props = this.props - const { keyrings, conversionRate, currentCurrency } = props - const identityList = valuesFor(props.identities) - const unapprovedTxList = valuesFor(props.unapprovedTxs) - - return ( - - h('.accounts-section.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: this.goHome.bind(this), - }), - h('h2.page-subtitle', 'Select Account'), - ]), - - h('hr.horizontal-line'), - - // identity selection - h('section.identity-section', { - style: { - height: '418px', - overflowY: 'auto', - overflowX: 'hidden', - }, - }, - [ - identityList.map((identity) => { - const pending = this.props.pending.filter((txOrMsg) => { - if ('txParams' in txOrMsg) { - return txOrMsg.txParams.from === identity.address - } else if ('msgParams' in txOrMsg) { - return txOrMsg.msgParams.from === identity.address - } else { - return false - } - }) - - const simpleAddress = identity.address.substring(2).toLowerCase() - const keyring = keyrings.find((kr) => { - return kr.accounts.includes(simpleAddress) || - kr.accounts.includes(identity.address) - }) - - return h(AccountListItem, { - key: `acct-panel-${identity.address}`, - identity, - selectedAddress: this.props.selectedAddress, - conversionRate, - currentCurrency, - accounts: this.props.accounts, - onShowDetail: this.onShowDetail.bind(this), - pending, - keyring, - }) - }), - - h('hr.horizontal-line'), - h('div.footer.hover-white.pointer', { - key: 'reveal-account-bar', - onClick: () => { - this.addNewAccount() - }, - style: { - display: 'flex', - height: '40px', - padding: '10px', - justifyContent: 'center', - alignItems: 'center', - }, - }, [ - h('i.fa.fa-plus.fa-lg', {key: ''}), - ]), - h('hr.horizontal-line'), - ]), - - unapprovedTxList.length ? ( - - h('.unconftx-link.flex-row.flex-center', { - onClick: this.navigateToConfTx.bind(this), - }, [ - h('span', 'Unconfirmed Txs'), - h('i.fa.fa-arrow-right.fa-lg'), - ]) - - ) : ( - null - ), - ]) - ) -} - -// If a new account was revealed, scroll to the bottom -AccountsScreen.prototype.componentDidUpdate = function () { - const scrollToBottom = this.props.scrollToBottom - - if (scrollToBottom) { - var container = findDOMNode(this) - var scrollable = container.querySelector('.identity-section') - scrollable.scrollTop = scrollable.scrollHeight - } -} - -AccountsScreen.prototype.navigateToConfTx = function () { - event.stopPropagation() - this.props.dispatch(actions.showConfTxPage()) -} - -AccountsScreen.prototype.onShowDetail = function (address, event) { - event.stopPropagation() - this.props.dispatch(actions.showAccountDetail(address)) -} - -AccountsScreen.prototype.addNewAccount = function () { - this.props.dispatch(actions.addNewAccount(0)) -} - -/* An optional view proposed in this design: - * https://consensys.quip.com/zZVrAysM5znY -AccountsScreen.prototype.addNewAccount = function () { - this.props.dispatch(actions.navigateToNewAccountScreen()) -} -*/ - -AccountsScreen.prototype.goHome = function () { - this.props.dispatch(actions.goHome()) -} diff --git a/ui/classic/app/actions.js b/ui/classic/app/actions.js deleted file mode 100644 index 2c60448dd..000000000 --- a/ui/classic/app/actions.js +++ /dev/null @@ -1,1031 +0,0 @@ -const getBuyEthUrl = require('../../../app/scripts/lib/buy-eth-url') - -var actions = { - _setBackgroundConnection: _setBackgroundConnection, - - GO_HOME: 'GO_HOME', - goHome: goHome, - // menu state - getNetworkStatus: 'getNetworkStatus', - // transition state - TRANSITION_FORWARD: 'TRANSITION_FORWARD', - TRANSITION_BACKWARD: 'TRANSITION_BACKWARD', - transitionForward, - transitionBackward, - // remote state - UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE', - updateMetamaskState: updateMetamaskState, - // notices - MARK_NOTICE_READ: 'MARK_NOTICE_READ', - markNoticeRead: markNoticeRead, - SHOW_NOTICE: 'SHOW_NOTICE', - showNotice: showNotice, - CLEAR_NOTICES: 'CLEAR_NOTICES', - clearNotices: clearNotices, - markAccountsFound, - // intialize screen - CREATE_NEW_VAULT_IN_PROGRESS: 'CREATE_NEW_VAULT_IN_PROGRESS', - SHOW_CREATE_VAULT: 'SHOW_CREATE_VAULT', - SHOW_RESTORE_VAULT: 'SHOW_RESTORE_VAULT', - FORGOT_PASSWORD: 'FORGOT_PASSWORD', - forgotPassword: forgotPassword, - SHOW_INIT_MENU: 'SHOW_INIT_MENU', - SHOW_NEW_VAULT_SEED: 'SHOW_NEW_VAULT_SEED', - SHOW_INFO_PAGE: 'SHOW_INFO_PAGE', - SHOW_IMPORT_PAGE: 'SHOW_IMPORT_PAGE', - unlockMetamask: unlockMetamask, - unlockFailed: unlockFailed, - showCreateVault: showCreateVault, - showRestoreVault: showRestoreVault, - showInitializeMenu: showInitializeMenu, - showImportPage, - createNewVaultAndKeychain: createNewVaultAndKeychain, - createNewVaultAndRestore: createNewVaultAndRestore, - createNewVaultInProgress: createNewVaultInProgress, - addNewKeyring, - importNewAccount, - addNewAccount, - NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN', - navigateToNewAccountScreen, - showNewVaultSeed: showNewVaultSeed, - showInfoPage: showInfoPage, - // seed recovery actions - REVEAL_SEED_CONFIRMATION: 'REVEAL_SEED_CONFIRMATION', - revealSeedConfirmation: revealSeedConfirmation, - requestRevealSeed: requestRevealSeed, - // unlock screen - UNLOCK_IN_PROGRESS: 'UNLOCK_IN_PROGRESS', - UNLOCK_FAILED: 'UNLOCK_FAILED', - UNLOCK_METAMASK: 'UNLOCK_METAMASK', - LOCK_METAMASK: 'LOCK_METAMASK', - tryUnlockMetamask: tryUnlockMetamask, - lockMetamask: lockMetamask, - unlockInProgress: unlockInProgress, - // error handling - displayWarning: displayWarning, - DISPLAY_WARNING: 'DISPLAY_WARNING', - HIDE_WARNING: 'HIDE_WARNING', - hideWarning: hideWarning, - // accounts screen - SET_SELECTED_ACCOUNT: 'SET_SELECTED_ACCOUNT', - SHOW_ACCOUNT_DETAIL: 'SHOW_ACCOUNT_DETAIL', - SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE', - SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE', - SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE', - SET_CURRENT_FIAT: 'SET_CURRENT_FIAT', - setCurrentCurrency: setCurrentCurrency, - setCurrentAccountTab, - // account detail screen - SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', - showSendPage: showSendPage, - ADD_TO_ADDRESS_BOOK: 'ADD_TO_ADDRESS_BOOK', - addToAddressBook: addToAddressBook, - REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT', - requestExportAccount: requestExportAccount, - EXPORT_ACCOUNT: 'EXPORT_ACCOUNT', - exportAccount: exportAccount, - SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY', - showPrivateKey: showPrivateKey, - SAVE_ACCOUNT_LABEL: 'SAVE_ACCOUNT_LABEL', - saveAccountLabel: saveAccountLabel, - // tx conf screen - COMPLETED_TX: 'COMPLETED_TX', - TRANSACTION_ERROR: 'TRANSACTION_ERROR', - NEXT_TX: 'NEXT_TX', - PREVIOUS_TX: 'PREV_TX', - signMsg: signMsg, - cancelMsg: cancelMsg, - signPersonalMsg, - cancelPersonalMsg, - sendTx: sendTx, - signTx: signTx, - updateAndApproveTx, - cancelTx: cancelTx, - completedTx: completedTx, - txError: txError, - nextTx: nextTx, - previousTx: previousTx, - viewPendingTx: viewPendingTx, - VIEW_PENDING_TX: 'VIEW_PENDING_TX', - // app messages - confirmSeedWords: confirmSeedWords, - showAccountDetail: showAccountDetail, - BACK_TO_ACCOUNT_DETAIL: 'BACK_TO_ACCOUNT_DETAIL', - backToAccountDetail: backToAccountDetail, - showAccountsPage: showAccountsPage, - showConfTxPage: showConfTxPage, - // config screen - SHOW_CONFIG_PAGE: 'SHOW_CONFIG_PAGE', - SET_RPC_TARGET: 'SET_RPC_TARGET', - SET_DEFAULT_RPC_TARGET: 'SET_DEFAULT_RPC_TARGET', - SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE', - USE_ETHERSCAN_PROVIDER: 'USE_ETHERSCAN_PROVIDER', - useEtherscanProvider: useEtherscanProvider, - showConfigPage, - SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', - showAddTokenPage, - addToken, - setRpcTarget: setRpcTarget, - setDefaultRpcTarget: setDefaultRpcTarget, - setProviderType: setProviderType, - // loading overlay - SHOW_LOADING: 'SHOW_LOADING_INDICATION', - HIDE_LOADING: 'HIDE_LOADING_INDICATION', - showLoadingIndication: showLoadingIndication, - hideLoadingIndication: hideLoadingIndication, - // buy Eth with coinbase - BUY_ETH: 'BUY_ETH', - buyEth: buyEth, - buyEthView: buyEthView, - BUY_ETH_VIEW: 'BUY_ETH_VIEW', - COINBASE_SUBVIEW: 'COINBASE_SUBVIEW', - coinBaseSubview: coinBaseSubview, - SHAPESHIFT_SUBVIEW: 'SHAPESHIFT_SUBVIEW', - shapeShiftSubview: shapeShiftSubview, - PAIR_UPDATE: 'PAIR_UPDATE', - pairUpdate: pairUpdate, - coinShiftRquest: coinShiftRquest, - SHOW_SUB_LOADING_INDICATION: 'SHOW_SUB_LOADING_INDICATION', - showSubLoadingIndication: showSubLoadingIndication, - HIDE_SUB_LOADING_INDICATION: 'HIDE_SUB_LOADING_INDICATION', - hideSubLoadingIndication: hideSubLoadingIndication, -// QR STUFF: - SHOW_QR: 'SHOW_QR', - showQrView: showQrView, - reshowQrCode: reshowQrCode, - SHOW_QR_VIEW: 'SHOW_QR_VIEW', -// FORGOT PASSWORD: - BACK_TO_INIT_MENU: 'BACK_TO_INIT_MENU', - goBackToInitView: goBackToInitView, - RECOVERY_IN_PROGRESS: 'RECOVERY_IN_PROGRESS', - BACK_TO_UNLOCK_VIEW: 'BACK_TO_UNLOCK_VIEW', - backToUnlockView: backToUnlockView, - // SHOWING KEYCHAIN - SHOW_NEW_KEYCHAIN: 'SHOW_NEW_KEYCHAIN', - showNewKeychain: showNewKeychain, - - callBackgroundThenUpdate, - forceUpdateMetamaskState, -} - -module.exports = actions - -var background = null -function _setBackgroundConnection (backgroundConnection) { - background = backgroundConnection -} - -function goHome () { - return { - type: actions.GO_HOME, - } -} - -// async actions - -function tryUnlockMetamask (password) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - dispatch(actions.unlockInProgress()) - log.debug(`background.submitPassword`) - background.submitPassword(password, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - dispatch(actions.unlockFailed(err.message)) - } else { - dispatch(actions.transitionForward()) - forceUpdateMetamaskState(dispatch) - } - }) - } -} - -function transitionForward () { - return { - type: this.TRANSITION_FORWARD, - } -} - -function transitionBackward () { - return { - type: this.TRANSITION_BACKWARD, - } -} - -function confirmSeedWords () { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.clearSeedWordCache`) - background.clearSeedWordCache((err, account) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - - log.info('Seed word cache cleared. ' + account) - dispatch(actions.showAccountDetail(account)) - }) - } -} - -function createNewVaultAndRestore (password, seed) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.createNewVaultAndRestore`) - background.createNewVaultAndRestore(password, seed, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.showAccountsPage()) - }) - } -} - -function createNewVaultAndKeychain (password) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.createNewVaultAndKeychain`) - background.createNewVaultAndKeychain(password, (err) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - log.debug(`background.placeSeedWords`) - background.placeSeedWords((err) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch(actions.hideLoadingIndication()) - forceUpdateMetamaskState(dispatch) - }) - }) - } -} - -function revealSeedConfirmation () { - return { - type: this.REVEAL_SEED_CONFIRMATION, - } -} - -function requestRevealSeed (password) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.submitPassword`) - background.submitPassword(password, (err) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - log.debug(`background.placeSeedWords`) - background.placeSeedWords((err, result) => { - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.hideLoadingIndication()) - dispatch(actions.showNewVaultSeed(result)) - }) - }) - } -} - -function addNewKeyring (type, opts) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.addNewKeyring`) - background.addNewKeyring(type, opts, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.showAccountsPage()) - }) - } -} - -function importNewAccount (strategy, args) { - return (dispatch) => { - dispatch(actions.showLoadingIndication('This may take a while, be patient.')) - log.debug(`background.importAccountWithStrategy`) - background.importAccountWithStrategy(strategy, args, (err) => { - if (err) return dispatch(actions.displayWarning(err.message)) - log.debug(`background.getState`) - background.getState((err, newState) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch(actions.updateMetamaskState(newState)) - dispatch({ - type: actions.SHOW_ACCOUNT_DETAIL, - value: newState.selectedAddress, - }) - }) - }) - } -} - -function navigateToNewAccountScreen () { - return { - type: this.NEW_ACCOUNT_SCREEN, - } -} - -function addNewAccount () { - log.debug(`background.addNewAccount`) - return callBackgroundThenUpdate(background.addNewAccount) -} - -function showInfoPage () { - return { - type: actions.SHOW_INFO_PAGE, - } -} - -function setCurrentCurrency (currencyCode) { - return (dispatch) => { - dispatch(this.showLoadingIndication()) - log.debug(`background.setCurrentCurrency`) - background.setCurrentCurrency(currencyCode, (err, data) => { - dispatch(this.hideLoadingIndication()) - if (err) { - log.error(err.stack) - return dispatch(actions.displayWarning(err.message)) - } - dispatch({ - type: this.SET_CURRENT_FIAT, - value: { - currentCurrency: data.currentCurrency, - conversionRate: data.conversionRate, - conversionDate: data.conversionDate, - }, - }) - }) - } -} - -function signMsg (msgData) { - log.debug('action - signMsg') - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - - log.debug(`actions calling background.signMessage`) - background.signMessage(msgData, (err, newState) => { - log.debug('signMessage called back') - dispatch(actions.updateMetamaskState(newState)) - dispatch(actions.hideLoadingIndication()) - - if (err) log.error(err) - if (err) return dispatch(actions.displayWarning(err.message)) - - dispatch(actions.completedTx(msgData.metamaskId)) - }) - } -} - -function signPersonalMsg (msgData) { - log.debug('action - signPersonalMsg') - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - - log.debug(`actions calling background.signPersonalMessage`) - background.signPersonalMessage(msgData, (err, newState) => { - log.debug('signPersonalMessage called back') - dispatch(actions.updateMetamaskState(newState)) - dispatch(actions.hideLoadingIndication()) - - if (err) log.error(err) - if (err) return dispatch(actions.displayWarning(err.message)) - - dispatch(actions.completedTx(msgData.metamaskId)) - }) - } -} - -function signTx (txData) { - return (dispatch) => { - global.ethQuery.sendTransaction(txData, (err, data) => { - dispatch(actions.hideLoadingIndication()) - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.hideWarning()) - }) - dispatch(this.showConfTxPage()) - } -} - -function sendTx (txData) { - log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`) - return (dispatch) => { - log.debug(`actions calling background.approveTransaction`) - background.approveTransaction(txData.id, (err) => { - if (err) { - dispatch(actions.txError(err)) - return log.error(err.message) - } - dispatch(actions.completedTx(txData.id)) - }) - } -} - -function updateAndApproveTx (txData) { - log.info('actions: updateAndApproveTx: ' + JSON.stringify(txData)) - return (dispatch) => { - log.debug(`actions calling background.updateAndApproveTx`) - background.updateAndApproveTransaction(txData, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - dispatch(actions.txError(err)) - return log.error(err.message) - } - dispatch(actions.completedTx(txData.id)) - }) - } -} - -function completedTx (id) { - return { - type: actions.COMPLETED_TX, - value: id, - } -} - -function txError (err) { - return { - type: actions.TRANSACTION_ERROR, - message: err.message, - } -} - -function cancelMsg (msgData) { - log.debug(`background.cancelMessage`) - background.cancelMessage(msgData.id) - return actions.completedTx(msgData.id) -} - -function cancelPersonalMsg (msgData) { - const id = msgData.id - background.cancelPersonalMessage(id) - return actions.completedTx(id) -} - -function cancelTx (txData) { - log.debug(`background.cancelTransaction`) - background.cancelTransaction(txData.id) - return actions.completedTx(txData.id) -} - -// -// initialize screen -// - -function showCreateVault () { - return { - type: actions.SHOW_CREATE_VAULT, - } -} - -function showRestoreVault () { - return { - type: actions.SHOW_RESTORE_VAULT, - } -} - -function forgotPassword () { - return { - type: actions.FORGOT_PASSWORD, - } -} - -function showInitializeMenu () { - return { - type: actions.SHOW_INIT_MENU, - } -} - -function showImportPage () { - return { - type: actions.SHOW_IMPORT_PAGE, - } -} - -function createNewVaultInProgress () { - return { - type: actions.CREATE_NEW_VAULT_IN_PROGRESS, - } -} - -function showNewVaultSeed (seed) { - return { - type: actions.SHOW_NEW_VAULT_SEED, - value: seed, - } -} - -function backToUnlockView () { - return { - type: actions.BACK_TO_UNLOCK_VIEW, - } -} - -function showNewKeychain () { - return { - type: actions.SHOW_NEW_KEYCHAIN, - } -} - -// -// unlock screen -// - -function unlockInProgress () { - return { - type: actions.UNLOCK_IN_PROGRESS, - } -} - -function unlockFailed (message) { - return { - type: actions.UNLOCK_FAILED, - value: message, - } -} - -function unlockMetamask (account) { - return { - type: actions.UNLOCK_METAMASK, - value: account, - } -} - -function updateMetamaskState (newState) { - return { - type: actions.UPDATE_METAMASK_STATE, - value: newState, - } -} - -function lockMetamask () { - log.debug(`background.setLocked`) - return callBackgroundThenUpdate(background.setLocked) -} - -function setCurrentAccountTab (newTabName) { - log.debug(`background.setCurrentAccountTab: ${newTabName}`) - return callBackgroundThenUpdateNoSpinner(background.setCurrentAccountTab, newTabName) -} - -function showAccountDetail (address) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.setSelectedAddress`) - background.setSelectedAddress(address, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch({ - type: actions.SHOW_ACCOUNT_DETAIL, - value: address, - }) - }) - } -} - -function backToAccountDetail (address) { - return { - type: actions.BACK_TO_ACCOUNT_DETAIL, - value: address, - } -} - -function showAccountsPage () { - return { - type: actions.SHOW_ACCOUNTS_PAGE, - } -} - -function showConfTxPage (transForward = true) { - return { - type: actions.SHOW_CONF_TX_PAGE, - transForward: transForward, - } -} - -function nextTx () { - return { - type: actions.NEXT_TX, - } -} - -function viewPendingTx (txId) { - return { - type: actions.VIEW_PENDING_TX, - value: txId, - } -} - -function previousTx () { - return { - type: actions.PREVIOUS_TX, - } -} - -function showConfigPage (transitionForward = true) { - return { - type: actions.SHOW_CONFIG_PAGE, - value: transitionForward, - } -} - -function showAddTokenPage (transitionForward = true) { - return { - type: actions.SHOW_ADD_TOKEN_PAGE, - value: transitionForward, - } -} - -function addToken (address, symbol, decimals) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - background.addToken(address, symbol, decimals, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - setTimeout(() => { - dispatch(actions.goHome()) - }, 250) - }) - } -} - -function goBackToInitView () { - return { - type: actions.BACK_TO_INIT_MENU, - } -} - -// -// notice -// - -function markNoticeRead (notice) { - return (dispatch) => { - dispatch(this.showLoadingIndication()) - log.debug(`background.markNoticeRead`) - background.markNoticeRead(notice, (err, notice) => { - dispatch(this.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err)) - } - if (notice) { - return dispatch(actions.showNotice(notice)) - } else { - dispatch(this.clearNotices()) - return { - type: actions.SHOW_ACCOUNTS_PAGE, - } - } - }) - } -} - -function showNotice (notice) { - return { - type: actions.SHOW_NOTICE, - value: notice, - } -} - -function clearNotices () { - return { - type: actions.CLEAR_NOTICES, - } -} - -function markAccountsFound () { - log.debug(`background.markAccountsFound`) - return callBackgroundThenUpdate(background.markAccountsFound) -} - -// -// config -// - -// default rpc target refers to localhost:8545 in this instance. -function setDefaultRpcTarget (rpcList) { - log.debug(`background.setDefaultRpcTarget`) - return (dispatch) => { - background.setDefaultRpc((err, result) => { - if (err) { - log.error(err) - return dispatch(self.displayWarning('Had a problem changing networks.')) - } - }) - } -} - -function setRpcTarget (newRpc) { - log.debug(`background.setRpcTarget`) - return (dispatch) => { - background.setCustomRpc(newRpc, (err, result) => { - if (err) { - log.error(err) - return dispatch(self.displayWarning('Had a problem changing networks!')) - } - }) - } -} - -// Calls the addressBookController to add a new address. -function addToAddressBook (recipient, nickname) { - log.debug(`background.addToAddressBook`) - return (dispatch) => { - background.setAddressBook(recipient, nickname, (err, result) => { - if (err) { - log.error(err) - return dispatch(self.displayWarning('Address book failed to update')) - } - }) - } -} - -function setProviderType (type) { - log.debug(`background.setProviderType`) - background.setProviderType(type) - return { - type: actions.SET_PROVIDER_TYPE, - value: type, - } -} - -function useEtherscanProvider () { - log.debug(`background.useEtherscanProvider`) - background.useEtherscanProvider() - return { - type: actions.USE_ETHERSCAN_PROVIDER, - } -} - -function showLoadingIndication (message) { - return { - type: actions.SHOW_LOADING, - value: message, - } -} - -function hideLoadingIndication () { - return { - type: actions.HIDE_LOADING, - } -} - -function showSubLoadingIndication () { - return { - type: actions.SHOW_SUB_LOADING_INDICATION, - } -} - -function hideSubLoadingIndication () { - return { - type: actions.HIDE_SUB_LOADING_INDICATION, - } -} - -function displayWarning (text) { - return { - type: actions.DISPLAY_WARNING, - value: text, - } -} - -function hideWarning () { - return { - type: actions.HIDE_WARNING, - } -} - -function requestExportAccount () { - return { - type: actions.REQUEST_ACCOUNT_EXPORT, - } -} - -function exportAccount (password, address) { - var self = this - - return function (dispatch) { - dispatch(self.showLoadingIndication()) - - log.debug(`background.submitPassword`) - background.submitPassword(password, function (err) { - if (err) { - log.error('Error in submiting password.') - dispatch(self.hideLoadingIndication()) - return dispatch(self.displayWarning('Incorrect Password.')) - } - log.debug(`background.exportAccount`) - background.exportAccount(address, function (err, result) { - dispatch(self.hideLoadingIndication()) - - if (err) { - log.error(err) - return dispatch(self.displayWarning('Had a problem exporting the account.')) - } - - dispatch(self.showPrivateKey(result)) - }) - }) - } -} - -function showPrivateKey (key) { - return { - type: actions.SHOW_PRIVATE_KEY, - value: key, - } -} - -function saveAccountLabel (account, label) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.saveAccountLabel`) - background.saveAccountLabel(account, label, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch({ - type: actions.SAVE_ACCOUNT_LABEL, - value: { account, label }, - }) - }) - } -} - -function showSendPage () { - return { - type: actions.SHOW_SEND_PAGE, - } -} - -function buyEth (opts) { - return (dispatch) => { - const url = getBuyEthUrl(opts) - global.platform.openWindow({ url }) - dispatch({ - type: actions.BUY_ETH, - }) - } -} - -function buyEthView (address) { - return { - type: actions.BUY_ETH_VIEW, - value: address, - } -} - -function coinBaseSubview () { - return { - type: actions.COINBASE_SUBVIEW, - } -} - -function pairUpdate (coin) { - return (dispatch) => { - dispatch(actions.showSubLoadingIndication()) - dispatch(actions.hideWarning()) - shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { - dispatch(actions.hideSubLoadingIndication()) - dispatch({ - type: actions.PAIR_UPDATE, - value: { - marketinfo: mktResponse, - }, - }) - }) - } -} - -function shapeShiftSubview (network) { - var pair = 'btc_eth' - - return (dispatch) => { - dispatch(actions.showSubLoadingIndication()) - shapeShiftRequest('marketinfo', {pair}, (mktResponse) => { - shapeShiftRequest('getcoins', {}, (response) => { - dispatch(actions.hideSubLoadingIndication()) - if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) - dispatch({ - type: actions.SHAPESHIFT_SUBVIEW, - value: { - marketinfo: mktResponse, - coinOptions: response, - }, - }) - }) - }) - } -} - -function coinShiftRquest (data, marketData) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - shapeShiftRequest('shift', { method: 'POST', data}, (response) => { - dispatch(actions.hideLoadingIndication()) - if (response.error) return dispatch(actions.displayWarning(response.error)) - var message = ` - Deposit your ${response.depositType} to the address bellow:` - log.debug(`background.createShapeShiftTx`) - background.createShapeShiftTx(response.deposit, response.depositType) - dispatch(actions.showQrView(response.deposit, [message].concat(marketData))) - }) - } -} - -function showQrView (data, message) { - return { - type: actions.SHOW_QR_VIEW, - value: { - message: message, - data: data, - }, - } -} -function reshowQrCode (data, coin) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { - if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) - - var message = [ - `Deposit your ${coin} to the address bellow:`, - `Deposit Limit: ${mktResponse.limit}`, - `Deposit Minimum:${mktResponse.minimum}`, - ] - - dispatch(actions.hideLoadingIndication()) - return dispatch(actions.showQrView(data, message)) - }) - } -} - -function shapeShiftRequest (query, options, cb) { - var queryResponse, method - !options ? options = {} : null - options.method ? method = options.method : method = 'GET' - - var requestListner = function (request) { - queryResponse = JSON.parse(this.responseText) - cb ? cb(queryResponse) : null - return queryResponse - } - - var shapShiftReq = new XMLHttpRequest() - shapShiftReq.addEventListener('load', requestListner) - shapShiftReq.open(method, `https://shapeshift.io/${query}/${options.pair ? options.pair : ''}`, true) - - if (options.method === 'POST') { - var jsonObj = JSON.stringify(options.data) - shapShiftReq.setRequestHeader('Content-Type', 'application/json') - return shapShiftReq.send(jsonObj) - } else { - return shapShiftReq.send() - } -} - -// Call Background Then Update -// -// A function generator for a common pattern wherein: -// We show loading indication. -// We call a background method. -// We hide loading indication. -// If it errored, we show a warning. -// If it didn't, we update the state. -function callBackgroundThenUpdateNoSpinner (method, ...args) { - return (dispatch) => { - method.call(background, ...args, (err) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - forceUpdateMetamaskState(dispatch) - }) - } -} - -function callBackgroundThenUpdate (method, ...args) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - method.call(background, ...args, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - forceUpdateMetamaskState(dispatch) - }) - } -} - -function forceUpdateMetamaskState (dispatch) { - log.debug(`background.getState`) - background.getState((err, newState) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch(actions.updateMetamaskState(newState)) - }) -} diff --git a/ui/classic/app/add-token.js b/ui/classic/app/add-token.js deleted file mode 100644 index b303b5c0d..000000000 --- a/ui/classic/app/add-token.js +++ /dev/null @@ -1,219 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') - -const ethUtil = require('ethereumjs-util') -const abi = require('human-standard-token-abi') -const Eth = require('ethjs-query') -const EthContract = require('ethjs-contract') - -const emptyAddr = '0x0000000000000000000000000000000000000000' - -module.exports = connect(mapStateToProps)(AddTokenScreen) - -function mapStateToProps (state) { - return { - } -} - -inherits(AddTokenScreen, Component) -function AddTokenScreen () { - this.state = { - warning: null, - address: null, - symbol: 'TOKEN', - decimals: 18, - } - Component.call(this) -} - -AddTokenScreen.prototype.render = function () { - const state = this.state - const props = this.props - const { warning, symbol, decimals } = state - - return ( - h('.flex-column.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: (event) => { - props.dispatch(actions.goHome()) - }, - }), - h('h2.page-subtitle', 'Add Token'), - ]), - - h('.error', { - style: { - display: warning ? 'block' : 'none', - padding: '0 20px', - textAlign: 'center', - }, - }, warning), - - // conf view - h('.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-space-around', { - style: { - padding: '20px', - }, - }, [ - - h('div', [ - h('span', { - style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Token Address'), - ]), - - h('section.flex-row.flex-center', [ - h('input#token-address', { - name: 'address', - placeholder: 'Token Address', - onChange: this.tokenAddressDidChange.bind(this), - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - }), - ]), - - h('div', [ - h('span', { - style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Token Sybmol'), - ]), - - h('div', { style: {display: 'flex'} }, [ - h('input#token_symbol', { - placeholder: `Like "ETH"`, - value: symbol, - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - onChange: (event) => { - var element = event.target - var symbol = element.value - this.setState({ symbol }) - }, - }), - ]), - - h('div', [ - h('span', { - style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Decimals of Precision'), - ]), - - h('div', { style: {display: 'flex'} }, [ - h('input#token_decimals', { - value: decimals, - type: 'number', - min: 0, - max: 36, - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - onChange: (event) => { - var element = event.target - var decimals = element.value.trim() - this.setState({ decimals }) - }, - }), - ]), - - h('button', { - style: { - alignSelf: 'center', - }, - onClick: (event) => { - const valid = this.validateInputs() - if (!valid) return - - const { address, symbol, decimals } = this.state - this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) - }, - }, 'Add'), - ]), - ]), - ]) - ) -} - -AddTokenScreen.prototype.componentWillMount = function () { - if (typeof global.ethereumProvider === 'undefined') return - - this.eth = new Eth(global.ethereumProvider) - this.contract = new EthContract(this.eth) - this.TokenContract = this.contract(abi) -} - -AddTokenScreen.prototype.tokenAddressDidChange = function (event) { - const el = event.target - const address = el.value.trim() - if (ethUtil.isValidAddress(address) && address !== emptyAddr) { - this.setState({ address }) - this.attemptToAutoFillTokenParams(address) - } -} - -AddTokenScreen.prototype.validateInputs = function () { - let msg = '' - const state = this.state - const { address, symbol, decimals } = state - - const validAddress = ethUtil.isValidAddress(address) - if (!validAddress) { - msg += 'Address is invalid. ' - } - - const validDecimals = decimals >= 0 && decimals < 36 - if (!validDecimals) { - msg += 'Decimals must be at least 0, and not over 36. ' - } - - const symbolLen = symbol.trim().length - const validSymbol = symbolLen > 0 && symbolLen < 10 - if (!validSymbol) { - msg += 'Symbol must be between 0 and 10 characters.' - } - - const isValid = validAddress && validDecimals - - if (!isValid) { - this.setState({ - warning: msg, - }) - } else { - this.setState({ warning: null }) - } - - return isValid -} - -AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) { - const contract = this.TokenContract.at(address) - - const results = await Promise.all([ - contract.symbol(), - contract.decimals(), - ]) - - const [ symbol, decimals ] = results - if (symbol && decimals) { - console.log('SETTING SYMBOL AND DECIMALS', { symbol, decimals }) - this.setState({ symbol: symbol[0], decimals: decimals[0].toString() }) - } -} - diff --git a/ui/classic/app/app.js b/ui/classic/app/app.js deleted file mode 100644 index 1a63002e1..000000000 --- a/ui/classic/app/app.js +++ /dev/null @@ -1,591 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('./actions') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') -// init -const InitializeMenuScreen = require('./first-time/init-menu') -const NewKeyChainScreen = require('./new-keychain') -// unlock -const UnlockScreen = require('./unlock') -// accounts -const AccountsScreen = require('./accounts') -const AccountDetailScreen = require('./account-detail') -const SendTransactionScreen = require('./send') -const ConfirmTxScreen = require('./conf-tx') -// notice -const NoticeScreen = require('./components/notice') -const generateLostAccountsNotice = require('../lib/lost-accounts-notice') -// other views -const ConfigScreen = require('./config') -const AddTokenScreen = require('./add-token') -const Import = require('./accounts/import') -const InfoScreen = require('./info') -const Loading = require('./components/loading') -const SandwichExpando = require('sandwich-expando') -const MenuDroppo = require('menu-droppo') -const DropMenuItem = require('./components/drop-menu-item') -const NetworkIndicator = require('./components/network') -const Tooltip = require('./components/tooltip') -const BuyView = require('./components/buy-button-subview') -const QrView = require('./components/qr-code') -const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete') -const HDRestoreVaultScreen = require('./keychains/hd/restore-vault') -const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation') - -module.exports = connect(mapStateToProps)(App) - -inherits(App, Component) -function App () { Component.call(this) } - -function mapStateToProps (state) { - return { - // state from plugin - isLoading: state.appState.isLoading, - loadingMessage: state.appState.loadingMessage, - noActiveNotices: state.metamask.noActiveNotices, - isInitialized: state.metamask.isInitialized, - isUnlocked: state.metamask.isUnlocked, - currentView: state.appState.currentView, - activeAddress: state.appState.activeAddress, - transForward: state.appState.transForward, - seedWords: state.metamask.seedWords, - unapprovedTxs: state.metamask.unapprovedTxs, - unapprovedMsgs: state.metamask.unapprovedMsgs, - menuOpen: state.appState.menuOpen, - network: state.metamask.network, - provider: state.metamask.provider, - forgottenPassword: state.appState.forgottenPassword, - lastUnreadNotice: state.metamask.lastUnreadNotice, - lostAccounts: state.metamask.lostAccounts, - frequentRpcList: state.metamask.frequentRpcList || [], - } -} - -App.prototype.render = function () { - var props = this.props - const { isLoading, loadingMessage, transForward, network } = props - const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config' - const loadMessage = loadingMessage || isLoadingNetwork ? - `Connecting to ${this.getNetworkName()}` : null - - log.debug('Main ui render function') - - return ( - - h('.flex-column.flex-grow.full-height', { - style: { - // Windows was showing a vertical scroll bar: - overflow: 'hidden', - position: 'relative', - }, - }, [ - - // app bar - this.renderAppBar(), - this.renderNetworkDropdown(), - this.renderDropdown(), - - h(Loading, { - isLoading: isLoading || isLoadingNetwork, - loadingMessage: loadMessage, - }), - - // panel content - h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), { - style: { - height: '380px', - width: '360px', - }, - }, [ - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.renderPrimary(), - ]), - ]), - ]) - ) -} - -App.prototype.renderAppBar = function () { - if (window.METAMASK_UI_TYPE === 'notification') { - return null - } - - const props = this.props - const state = this.state || {} - const isNetworkMenuOpen = state.isNetworkMenuOpen || false - - return ( - - h('div', [ - - h('.app-header.flex-row.flex-space-between', { - style: { - alignItems: 'center', - visibility: props.isUnlocked ? 'visible' : 'none', - background: props.isUnlocked ? 'white' : 'none', - height: '36px', - position: 'relative', - zIndex: 12, - }, - }, [ - - h('div.left-menu-section', { - style: { - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - }, - }, [ - - // mini logo - h('img', { - height: 24, - width: 24, - src: '/images/icon-128.png', - }), - - h(NetworkIndicator, { - network: this.props.network, - provider: this.props.provider, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen }) - }, - }), - ]), - - // metamask name - props.isUnlocked && h('h1', { - style: { - position: 'relative', - left: '9px', - }, - }, 'MetaMask'), - - props.isUnlocked && h('div', { - style: { - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - }, - }, [ - - // small accounts nav - props.isUnlocked && h(Tooltip, { title: 'Switch Accounts' }, [ - h('img.cursor-pointer.color-orange', { - src: 'images/switch_acc.svg', - style: { - width: '23.5px', - marginRight: '8px', - }, - onClick: (event) => { - event.stopPropagation() - this.props.dispatch(actions.showAccountsPage()) - }, - }), - ]), - - // hamburger - props.isUnlocked && h(SandwichExpando, { - width: 16, - barHeight: 2, - padding: 0, - isOpen: state.isMainMenuOpen, - color: 'rgb(247,146,30)', - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - this.setState({ isMainMenuOpen: !state.isMainMenuOpen }) - }, - }), - ]), - ]), - ]) - ) -} - -App.prototype.renderNetworkDropdown = function () { - const props = this.props - const rpcList = props.frequentRpcList - const state = this.state || {} - const isOpen = state.isNetworkMenuOpen - - return h(MenuDroppo, { - isOpen, - onClickOutside: (event) => { - this.setState({ isNetworkMenuOpen: !isOpen }) - }, - zIndex: 11, - style: { - position: 'absolute', - left: 0, - top: '36px', - }, - innerStyle: { - background: 'white', - boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', - }, - }, [ // DROP MENU ITEMS - h('style', ` - .drop-menu-item:hover { background:rgb(235, 235, 235); } - .drop-menu-item i { margin: 11px; } - `), - - h(DropMenuItem, { - label: 'Main Ethereum Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setProviderType('mainnet')), - icon: h('.menu-icon.diamond'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Ropsten Test Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setProviderType('ropsten')), - icon: h('.menu-icon.red-dot'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Kovan Test Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false}), - action: () => props.dispatch(actions.setProviderType('kovan')), - icon: h('.menu-icon.hollow-diamond'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Rinkeby Test Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false}), - action: () => props.dispatch(actions.setProviderType('rinkeby')), - icon: h('.menu-icon.golden-square'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Localhost 8545', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setDefaultRpcTarget(rpcList)), - icon: h('i.fa.fa-question-circle.fa-lg'), - activeNetworkRender: props.provider.rpcTarget, - }), - - this.renderCustomOption(props.provider), - this.renderCommonRpc(rpcList, props.provider), - - h(DropMenuItem, { - label: 'Custom RPC', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => this.props.dispatch(actions.showConfigPage()), - icon: h('i.fa.fa-question-circle.fa-lg'), - }), - - ]) -} - -App.prototype.renderDropdown = function () { - const state = this.state || {} - const isOpen = state.isMainMenuOpen - - return h(MenuDroppo, { - isOpen: isOpen, - zIndex: 11, - onClickOutside: (event) => { - this.setState({ isMainMenuOpen: !isOpen }) - }, - style: { - position: 'absolute', - right: 0, - top: '36px', - }, - innerStyle: { - background: 'white', - boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', - }, - }, [ // DROP MENU ITEMS - h('style', ` - .drop-menu-item:hover { background:rgb(235, 235, 235); } - .drop-menu-item i { margin: 11px; } - `), - - h(DropMenuItem, { - label: 'Settings', - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.showConfigPage()), - icon: h('i.fa.fa-gear.fa-lg'), - }), - - h(DropMenuItem, { - label: 'Import Account', - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.showImportPage()), - icon: h('i.fa.fa-arrow-circle-o-up.fa-lg'), - }), - - h(DropMenuItem, { - label: 'Lock', - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.lockMetamask()), - icon: h('i.fa.fa-lock.fa-lg'), - }), - - h(DropMenuItem, { - label: 'Info/Help', - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.showInfoPage()), - icon: h('i.fa.fa-question.fa-lg'), - }), - ]) -} - -App.prototype.renderBackButton = function (style, justArrow = false) { - var props = this.props - return ( - h('.flex-row', { - key: 'leftArrow', - style: style, - onClick: () => props.dispatch(actions.goBackToInitView()), - }, [ - h('i.fa.fa-arrow-left.cursor-pointer'), - justArrow ? null : h('div.cursor-pointer', { - style: { - marginLeft: '3px', - }, - onClick: () => props.dispatch(actions.goBackToInitView()), - }, 'BACK'), - ]) - ) -} - -App.prototype.renderPrimary = function () { - log.debug('rendering primary') - var props = this.props - - // notices - if (!props.noActiveNotices) { - log.debug('rendering notice screen for unread notices.') - return h(NoticeScreen, { - notice: props.lastUnreadNotice, - key: 'NoticeScreen', - onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)), - }) - } else if (props.lostAccounts && props.lostAccounts.length > 0) { - log.debug('rendering notice screen for lost accounts view.') - return h(NoticeScreen, { - notice: generateLostAccountsNotice(props.lostAccounts), - key: 'LostAccountsNotice', - onConfirm: () => props.dispatch(actions.markAccountsFound()), - }) - } - - if (props.seedWords) { - log.debug('rendering seed words') - return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'}) - } - - // show initialize screen - if (!props.isInitialized || props.forgottenPassword) { - // show current view - log.debug('rendering an initialize screen') - switch (props.currentView.name) { - - case 'restoreVault': - log.debug('rendering restore vault screen') - return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) - - default: - log.debug('rendering menu screen') - return h(InitializeMenuScreen, {key: 'menuScreenInit'}) - } - } - - // show unlock screen - if (!props.isUnlocked) { - switch (props.currentView.name) { - - case 'restoreVault': - log.debug('rendering restore vault screen') - return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) - - case 'config': - log.debug('rendering config screen from unlock screen.') - return h(ConfigScreen, {key: 'config'}) - - default: - log.debug('rendering locked screen') - return h(UnlockScreen, {key: 'locked'}) - } - } - - // show current view - switch (props.currentView.name) { - - case 'accounts': - log.debug('rendering accounts screen') - return h(AccountsScreen, {key: 'accounts'}) - - case 'accountDetail': - log.debug('rendering account detail screen') - return h(AccountDetailScreen, {key: 'account-detail'}) - - case 'sendTransaction': - log.debug('rendering send tx screen') - return h(SendTransactionScreen, {key: 'send-transaction'}) - - case 'newKeychain': - log.debug('rendering new keychain screen') - return h(NewKeyChainScreen, {key: 'new-keychain'}) - - case 'confTx': - log.debug('rendering confirm tx screen') - return h(ConfirmTxScreen, {key: 'confirm-tx'}) - - case 'add-token': - log.debug('rendering add-token screen from unlock screen.') - return h(AddTokenScreen, {key: 'add-token'}) - - case 'config': - log.debug('rendering config screen') - return h(ConfigScreen, {key: 'config'}) - - case 'import-menu': - log.debug('rendering import screen') - return h(Import, {key: 'import-menu'}) - - case 'reveal-seed-conf': - log.debug('rendering reveal seed confirmation screen') - return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'}) - - case 'info': - log.debug('rendering info screen') - return h(InfoScreen, {key: 'info'}) - - case 'buyEth': - log.debug('rendering buy ether screen') - return h(BuyView, {key: 'buyEthView'}) - - case 'qr': - log.debug('rendering show qr screen') - return h('div', { - style: { - position: 'absolute', - height: '100%', - top: '0px', - left: '0px', - }, - }, [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { - onClick: () => props.dispatch(actions.backToAccountDetail(props.activeAddress)), - style: { - marginLeft: '10px', - marginTop: '50px', - }, - }), - h('div', { - style: { - position: 'absolute', - left: '44px', - width: '285px', - }, - }, [ - h(QrView, {key: 'qr'}), - ]), - ]) - - default: - log.debug('rendering default, account detail screen') - return h(AccountDetailScreen, {key: 'account-detail'}) - } -} - -App.prototype.toggleMetamaskActive = function () { - if (!this.props.isUnlocked) { - // currently inactive: redirect to password box - var passwordBox = document.querySelector('input[type=password]') - if (!passwordBox) return - passwordBox.focus() - } else { - // currently active: deactivate - this.props.dispatch(actions.lockMetamask(false)) - } -} - -App.prototype.renderCustomOption = function (provider) { - const { rpcTarget, type } = provider - if (type !== 'rpc') return null - - // Concatenate long URLs - let label = rpcTarget - if (rpcTarget.length > 31) { - label = label.substr(0, 34) + '...' - } - - switch (rpcTarget) { - - case 'http://localhost:8545': - return null - - default: - return h(DropMenuItem, { - label, - key: rpcTarget, - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - icon: h('i.fa.fa-question-circle.fa-lg'), - activeNetworkRender: 'custom', - }) - } -} - -App.prototype.getNetworkName = function () { - const { provider } = this.props - const providerName = provider.type - - let name - - if (providerName === 'mainnet') { - name = 'Main Ethereum Network' - } else if (providerName === 'ropsten') { - name = 'Ropsten Test Network' - } else if (providerName === 'kovan') { - name = 'Kovan Test Network' - } else if (providerName === 'rinkeby') { - name = 'Rinkeby Test Network' - } else { - name = 'Unknown Private Network' - } - - return name -} - -App.prototype.renderCommonRpc = function (rpcList, provider) { - const { rpcTarget } = provider - const props = this.props - - return rpcList.map((rpc) => { - if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { - return null - } else { - return h(DropMenuItem, { - label: rpc, - key: rpc, - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setRpcTarget(rpc)), - icon: h('i.fa.fa-question-circle.fa-lg'), - activeNetworkRender: rpc, - }) - } - }) -} diff --git a/ui/classic/app/components/account-export.js b/ui/classic/app/components/account-export.js deleted file mode 100644 index 394d878f7..000000000 --- a/ui/classic/app/components/account-export.js +++ /dev/null @@ -1,122 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const copyToClipboard = require('copy-to-clipboard') -const actions = require('../actions') -const ethUtil = require('ethereumjs-util') -const connect = require('react-redux').connect - -module.exports = connect(mapStateToProps)(ExportAccountView) - -inherits(ExportAccountView, Component) -function ExportAccountView () { - Component.call(this) -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -ExportAccountView.prototype.render = function () { - var state = this.props - var accountDetail = state.accountDetail - - if (!accountDetail) return h('div') - var accountExport = accountDetail.accountExport - - var notExporting = accountExport === 'none' - var exportRequested = accountExport === 'requested' - var accountExported = accountExport === 'completed' - - if (notExporting) return h('div') - - if (exportRequested) { - var warning = `Export private keys at your own risk.` - return ( - h('div', { - style: { - display: 'inline-block', - textAlign: 'center', - }, - }, - [ - h('div', { - key: 'exporting', - style: { - margin: '0 20px', - }, - }, [ - h('p.error', warning), - h('input#exportAccount.sizing-input', { - type: 'password', - placeholder: 'confirm password', - onKeyPress: this.onExportKeyPress.bind(this), - style: { - position: 'relative', - top: '1.5px', - marginBottom: '7px', - }, - }), - ]), - h('div', { - key: 'buttons', - style: { - margin: '0 20px', - }, - }, - [ - h('button', { - onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }), - style: { - marginRight: '10px', - }, - }, 'Submit'), - h('button', { - onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), - }, 'Cancel'), - ]), - (this.props.warning) && ( - h('span.error', { - style: { - margin: '20px', - }, - }, this.props.warning.split('-')) - ), - ]) - ) - } - - if (accountExported) { - return h('div.privateKey', { - style: { - margin: '0 20px', - }, - }, [ - h('label', 'Your private key (click to copy):'), - h('p.error.cursor-pointer', { - style: { - textOverflow: 'ellipsis', - overflow: 'hidden', - webkitUserSelect: 'text', - width: '100%', - }, - onClick: function (event) { - copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey)) - }, - }, ethUtil.stripHexPrefix(accountDetail.privateKey)), - h('button', { - onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), - }, 'Done'), - ]) - } -} - -ExportAccountView.prototype.onExportKeyPress = function (event) { - if (event.key !== 'Enter') return - event.preventDefault() - - var input = document.getElementById('exportAccount').value - this.props.dispatch(actions.exportAccount(input, this.props.address)) -} diff --git a/ui/classic/app/components/account-info-link.js b/ui/classic/app/components/account-info-link.js deleted file mode 100644 index 6526ab502..000000000 --- a/ui/classic/app/components/account-info-link.js +++ /dev/null @@ -1,41 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const Tooltip = require('./tooltip') -const genAccountLink = require('../../lib/account-link') - -module.exports = AccountInfoLink - -inherits(AccountInfoLink, Component) -function AccountInfoLink () { - Component.call(this) -} - -AccountInfoLink.prototype.render = function () { - const { selected, network } = this.props - const title = 'View account on Etherscan' - const url = genAccountLink(selected, network) - - if (!url) { - return null - } - - return h('.account-info-link', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - - h(Tooltip, { - title, - }, [ - h('i.fa.fa-info-circle.cursor-pointer.color-orange', { - style: { - margin: '5px', - }, - onClick () { global.platform.openWindow({ url }) }, - }), - ]), - ]) -} diff --git a/ui/classic/app/components/account-panel.js b/ui/classic/app/components/account-panel.js deleted file mode 100644 index abaaf8163..000000000 --- a/ui/classic/app/components/account-panel.js +++ /dev/null @@ -1,86 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const Identicon = require('./identicon') -const formatBalance = require('../util').formatBalance -const addressSummary = require('../util').addressSummary - -module.exports = AccountPanel - - -inherits(AccountPanel, Component) -function AccountPanel () { - Component.call(this) -} - -AccountPanel.prototype.render = function () { - var state = this.props - var identity = state.identity || {} - var account = state.account || {} - var isFauceting = state.isFauceting - - var panelState = { - key: `accountPanel${identity.address}`, - identiconKey: identity.address, - identiconLabel: identity.name || '', - attributes: [ - { - key: 'ADDRESS', - value: addressSummary(identity.address), - }, - balanceOrFaucetingIndication(account, isFauceting), - ], - } - - return ( - - h('.identity-panel.flex-row.flex-space-between', { - style: { - flex: '1 0 auto', - cursor: panelState.onClick ? 'pointer' : undefined, - }, - onClick: panelState.onClick, - }, [ - - // account identicon - h('.identicon-wrapper.flex-column.select-none', [ - h(Identicon, { - address: panelState.identiconKey, - imageify: state.imageifyIdenticons, - }), - h('span.font-small', panelState.identiconLabel.substring(0, 7) + '...'), - ]), - - // account address, balance - h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [ - - panelState.attributes.map((attr) => { - return h('.flex-row.flex-space-between', { - key: '' + Math.round(Math.random() * 1000000), - }, [ - h('label.font-small.no-select', attr.key), - h('span.font-small', attr.value), - ]) - }), - ]), - - ]) - - ) -} - -function balanceOrFaucetingIndication (account, isFauceting) { - // Temporarily deactivating isFauceting indication - // because it shows fauceting for empty restored accounts. - if (/* isFauceting*/ false) { - return { - key: 'Account is auto-funding.', - value: 'Please wait.', - } - } else { - return { - key: 'BALANCE', - value: formatBalance(account.balance), - } - } -} diff --git a/ui/classic/app/components/balance.js b/ui/classic/app/components/balance.js deleted file mode 100644 index 57ca84564..000000000 --- a/ui/classic/app/components/balance.js +++ /dev/null @@ -1,89 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const formatBalance = require('../util').formatBalance -const generateBalanceObject = require('../util').generateBalanceObject -const Tooltip = require('./tooltip.js') -const FiatValue = require('./fiat-value.js') - -module.exports = EthBalanceComponent - -inherits(EthBalanceComponent, Component) -function EthBalanceComponent () { - Component.call(this) -} - -EthBalanceComponent.prototype.render = function () { - var props = this.props - let { value } = props - var style = props.style - var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true - value = value ? formatBalance(value, 6, needsParse) : '...' - var width = props.width - - return ( - - h('.ether-balance.ether-balance-amount', { - style: style, - }, [ - h('div', { - style: { - display: 'inline', - width: width, - }, - }, this.renderBalance(value)), - ]) - - ) -} -EthBalanceComponent.prototype.renderBalance = function (value) { - var props = this.props - if (value === 'None') return value - if (value === '...') return value - var balanceObj = generateBalanceObject(value, props.shorten ? 1 : 3) - var balance - var splitBalance = value.split(' ') - var ethNumber = splitBalance[0] - var ethSuffix = splitBalance[1] - const showFiat = 'showFiat' in props ? props.showFiat : true - - if (props.shorten) { - balance = balanceObj.shortBalance - } else { - balance = balanceObj.balance - } - - var label = balanceObj.label - - return ( - h(Tooltip, { - position: 'bottom', - title: `${ethNumber} ${ethSuffix}`, - }, h('div.flex-column', [ - h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('div', { - style: { - width: '100%', - textAlign: 'right', - }, - }, this.props.incoming ? `+${balance}` : balance), - h('div', { - style: { - color: ' #AEAEAE', - fontSize: '12px', - marginLeft: '5px', - }, - }, label), - ]), - - showFiat ? h(FiatValue, { value: props.value }) : null, - ])) - ) -} diff --git a/ui/classic/app/components/binary-renderer.js b/ui/classic/app/components/binary-renderer.js deleted file mode 100644 index 0b6a1f5c2..000000000 --- a/ui/classic/app/components/binary-renderer.js +++ /dev/null @@ -1,46 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ethUtil = require('ethereumjs-util') -const extend = require('xtend') - -module.exports = BinaryRenderer - -inherits(BinaryRenderer, Component) -function BinaryRenderer () { - Component.call(this) -} - -BinaryRenderer.prototype.render = function () { - const props = this.props - const { value, style } = props - const text = this.hexToText(value) - - const defaultStyle = extend({ - width: '315px', - maxHeight: '210px', - resize: 'none', - border: 'none', - background: 'white', - padding: '3px', - }, style) - - return ( - h('textarea.font-small', { - readOnly: true, - style: defaultStyle, - defaultValue: text, - }) - ) -} - -BinaryRenderer.prototype.hexToText = function (hex) { - try { - const stripped = ethUtil.stripHexPrefix(hex) - const buff = Buffer.from(stripped, 'hex') - return buff.toString('utf8') - } catch (e) { - return hex - } -} - diff --git a/ui/classic/app/components/bn-as-decimal-input.js b/ui/classic/app/components/bn-as-decimal-input.js deleted file mode 100644 index f3ace4720..000000000 --- a/ui/classic/app/components/bn-as-decimal-input.js +++ /dev/null @@ -1,174 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const extend = require('xtend') - -module.exports = BnAsDecimalInput - -inherits(BnAsDecimalInput, Component) -function BnAsDecimalInput () { - this.state = { invalid: null } - Component.call(this) -} - -/* Bn as Decimal Input - * - * A component for allowing easy, decimal editing - * of a passed in bn string value. - * - * On change, calls back its `onChange` function parameter - * and passes it an updated bn string. - */ - -BnAsDecimalInput.prototype.render = function () { - const props = this.props - const state = this.state - - const { value, scale, precision, onChange, min, max } = props - - const suffix = props.suffix - const style = props.style - const valueString = value.toString(10) - const newValue = this.downsize(valueString, scale, precision) - - return ( - h('.flex-column', [ - h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('input.hex-input', { - type: 'number', - step: 'any', - required: true, - min, - max, - style: extend({ - display: 'block', - textAlign: 'right', - backgroundColor: 'transparent', - border: '1px solid #bdbdbd', - - }, style), - value: newValue, - onBlur: (event) => { - this.updateValidity(event) - }, - onChange: (event) => { - this.updateValidity(event) - const value = (event.target.value === '') ? '' : event.target.value - - - const scaledNumber = this.upsize(value, scale, precision) - const precisionBN = new BN(scaledNumber, 10) - onChange(precisionBN, event.target.checkValidity()) - }, - onInvalid: (event) => { - const msg = this.constructWarning() - if (msg === state.invalid) { - return - } - this.setState({ invalid: msg }) - event.preventDefault() - return false - }, - }), - h('div', { - style: { - color: ' #AEAEAE', - fontSize: '12px', - marginLeft: '5px', - marginRight: '6px', - width: '20px', - }, - }, suffix), - ]), - - state.invalid ? h('span.error', { - style: { - position: 'absolute', - right: '0px', - textAlign: 'right', - transform: 'translateY(26px)', - padding: '3px', - background: 'rgba(255,255,255,0.85)', - zIndex: '1', - textTransform: 'capitalize', - border: '2px solid #E20202', - }, - }, state.invalid) : null, - ]) - ) -} - -BnAsDecimalInput.prototype.setValid = function (message) { - this.setState({ invalid: null }) -} - -BnAsDecimalInput.prototype.updateValidity = function (event) { - const target = event.target - const value = this.props.value - const newValue = target.value - - if (value === newValue) { - return - } - - const valid = target.checkValidity() - - if (valid) { - this.setState({ invalid: null }) - } -} - -BnAsDecimalInput.prototype.constructWarning = function () { - const { name, min, max } = this.props - let message = name ? name + ' ' : '' - - if (min && max) { - message += `must be greater than or equal to ${min} and less than or equal to ${max}.` - } else if (min) { - message += `must be greater than or equal to ${min}.` - } else if (max) { - message += `must be less than or equal to ${max}.` - } else { - message += 'Invalid input.' - } - - return message -} - - -BnAsDecimalInput.prototype.downsize = function (number, scale, precision) { - // if there is no scaling, simply return the number - if (scale === 0) { - return Number(number) - } else { - // if the scale is the same as the precision, account for this edge case. - var decimals = (scale === precision) ? -1 : scale - precision - return Number(number.slice(0, -scale) + '.' + number.slice(-scale, decimals)) - } -} - -BnAsDecimalInput.prototype.upsize = function (number, scale, precision) { - var stringArray = number.toString().split('.') - var decimalLength = stringArray[1] ? stringArray[1].length : 0 - var newString = stringArray[0] - - // If there is scaling and decimal parts exist, integrate them in. - if ((scale !== 0) && (decimalLength !== 0)) { - newString += stringArray[1].slice(0, precision) - } - - // Add 0s to account for the upscaling. - for (var i = decimalLength; i < scale; i++) { - newString += '0' - } - return newString -} diff --git a/ui/classic/app/components/buy-button-subview.js b/ui/classic/app/components/buy-button-subview.js deleted file mode 100644 index 87084f92d..000000000 --- a/ui/classic/app/components/buy-button-subview.js +++ /dev/null @@ -1,197 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const connect = require('react-redux').connect -const actions = require('../actions') -const CoinbaseForm = require('./coinbase-form') -const ShapeshiftForm = require('./shapeshift-form') -const Loading = require('./loading') -const AccountPanel = require('./account-panel') -const RadioList = require('./custom-radio-list') - -module.exports = connect(mapStateToProps)(BuyButtonSubview) - -function mapStateToProps (state) { - return { - identity: state.appState.identity, - account: state.metamask.accounts[state.appState.buyView.buyAddress], - warning: state.appState.warning, - buyView: state.appState.buyView, - network: state.metamask.network, - provider: state.metamask.provider, - context: state.appState.currentView.context, - isSubLoading: state.appState.isSubLoading, - } -} - -inherits(BuyButtonSubview, Component) -function BuyButtonSubview () { - Component.call(this) -} - -BuyButtonSubview.prototype.render = function () { - const props = this.props - const isLoading = props.isSubLoading - - return ( - h('.buy-eth-section.flex-column', { - style: { - alignItems: 'center', - }, - }, [ - // back button - h('.flex-row', { - style: { - alignItems: 'center', - justifyContent: 'center', - }, - }, [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { - onClick: this.backButtonContext.bind(this), - style: { - position: 'absolute', - left: '10px', - }, - }), - h('h2.text-transform-uppercase.flex-center', { - style: { - width: '100vw', - background: 'rgb(235, 235, 235)', - color: 'rgb(174, 174, 174)', - paddingTop: '4px', - paddingBottom: '4px', - }, - }, 'Buy Eth'), - ]), - h('div', { - style: { - position: 'absolute', - top: '57vh', - left: '49vw', - }, - }, [ - h(Loading, {isLoading}), - ]), - h('div', { - style: { - width: '80%', - }, - }, [ - h(AccountPanel, { - showFullAddress: true, - identity: props.identity, - account: props.account, - }), - ]), - h('h3.text-transform-uppercase', { - style: { - paddingLeft: '15px', - fontFamily: 'Montserrat Light', - width: '100vw', - background: 'rgb(235, 235, 235)', - color: 'rgb(174, 174, 174)', - paddingTop: '4px', - paddingBottom: '4px', - }, - }, 'Select Service'), - h('.flex-row.selected-exchange', { - style: { - position: 'relative', - right: '35px', - marginTop: '20px', - marginBottom: '20px', - }, - }, [ - h(RadioList, { - defaultFocus: props.buyView.subview, - labels: [ - 'Coinbase', - 'ShapeShift', - ], - subtext: { - 'Coinbase': 'Crypto/FIAT (USA only)', - 'ShapeShift': 'Crypto', - }, - onClick: this.radioHandler.bind(this), - }), - ]), - h('h3.text-transform-uppercase', { - style: { - paddingLeft: '15px', - fontFamily: 'Montserrat Light', - width: '100vw', - background: 'rgb(235, 235, 235)', - color: 'rgb(174, 174, 174)', - paddingTop: '4px', - paddingBottom: '4px', - }, - }, props.buyView.subview), - this.formVersionSubview(), - ]) - ) -} - -BuyButtonSubview.prototype.formVersionSubview = function () { - const network = this.props.network - if (network === '1') { - if (this.props.buyView.formView.coinbase) { - return h(CoinbaseForm, this.props) - } else if (this.props.buyView.formView.shapeshift) { - return h(ShapeshiftForm, this.props) - } - } else { - return h('div.flex-column', { - style: { - alignItems: 'center', - margin: '50px', - }, - }, [ - h('h3.text-transform-uppercase', { - style: { - width: '225px', - marginBottom: '15px', - }, - }, 'In order to access this feature, please switch to the Main Network'), - ((network === '3') || (network === '4') || (network === '42')) ? h('h3.text-transform-uppercase', 'or go to the') : null, - (network === '3') ? h('button.text-transform-uppercase', { - onClick: () => this.props.dispatch(actions.buyEth({ network })), - style: { - marginTop: '15px', - }, - }, 'Ropsten Test Faucet') : null, - (network === '4') ? h('button.text-transform-uppercase', { - onClick: () => this.props.dispatch(actions.buyEth({ network })), - style: { - marginTop: '15px', - }, - }, 'Rinkeby Test Faucet') : null, - (network === '42') ? h('button.text-transform-uppercase', { - onClick: () => this.props.dispatch(actions.buyEth({ network })), - style: { - marginTop: '15px', - }, - }, 'Kovan Test Faucet') : null, - ]) - } -} - -BuyButtonSubview.prototype.navigateTo = function (url) { - global.platform.openWindow({ url }) -} - -BuyButtonSubview.prototype.backButtonContext = function () { - if (this.props.context === 'confTx') { - this.props.dispatch(actions.showConfTxPage(false)) - } else { - this.props.dispatch(actions.goHome()) - } -} - -BuyButtonSubview.prototype.radioHandler = function (event) { - switch (event.target.title) { - case 'Coinbase': - return this.props.dispatch(actions.coinBaseSubview()) - case 'ShapeShift': - return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type)) - } -} diff --git a/ui/classic/app/components/coinbase-form.js b/ui/classic/app/components/coinbase-form.js deleted file mode 100644 index f44d86045..000000000 --- a/ui/classic/app/components/coinbase-form.js +++ /dev/null @@ -1,63 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const connect = require('react-redux').connect -const actions = require('../actions') - -module.exports = connect(mapStateToProps)(CoinbaseForm) - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -inherits(CoinbaseForm, Component) - -function CoinbaseForm () { - Component.call(this) -} - -CoinbaseForm.prototype.render = function () { - var props = this.props - - return h('.flex-column', { - style: { - marginTop: '35px', - padding: '25px', - width: '100%', - }, - }, [ - h('.flex-row', { - style: { - justifyContent: 'space-around', - margin: '33px', - marginTop: '0px', - }, - }, [ - h('button.btn-green', { - onClick: this.toCoinbase.bind(this), - }, 'Continue to Coinbase'), - - h('button.btn-red', { - onClick: () => props.dispatch(actions.backTobuyView(props.accounts.address)), - }, 'Cancel'), - ]), - ]) -} - -CoinbaseForm.prototype.toCoinbase = function () { - const props = this.props - const address = props.buyView.buyAddress - props.dispatch(actions.buyEth({ network: '1', address, amount: 0 })) -} - -CoinbaseForm.prototype.renderLoading = function () { - return h('img', { - style: { - width: '27px', - marginRight: '-27px', - }, - src: 'images/loading.svg', - }) -} diff --git a/ui/classic/app/components/copyButton.js b/ui/classic/app/components/copyButton.js deleted file mode 100644 index a25d0719c..000000000 --- a/ui/classic/app/components/copyButton.js +++ /dev/null @@ -1,59 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const copyToClipboard = require('copy-to-clipboard') - -const Tooltip = require('./tooltip') - -module.exports = CopyButton - -inherits(CopyButton, Component) -function CopyButton () { - Component.call(this) -} - -// As parameters, accepts: -// "value", which is the value to copy (mandatory) -// "title", which is the text to show on hover (optional, defaults to 'Copy') -CopyButton.prototype.render = function () { - const props = this.props - const state = this.state || {} - - const value = props.value - const copied = state.copied - - const message = copied ? 'Copied' : props.title || ' Copy ' - - return h('.copy-button', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - - h(Tooltip, { - title: message, - }, [ - h('i.fa.fa-clipboard.cursor-pointer.color-orange', { - style: { - margin: '5px', - }, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - copyToClipboard(value) - this.debounceRestore() - }, - }), - ]), - - ]) -} - -CopyButton.prototype.debounceRestore = function () { - this.setState({ copied: true }) - clearTimeout(this.timeout) - this.timeout = setTimeout(() => { - this.setState({ copied: false }) - }, 850) -} diff --git a/ui/classic/app/components/copyable.js b/ui/classic/app/components/copyable.js deleted file mode 100644 index a4f6f4bc6..000000000 --- a/ui/classic/app/components/copyable.js +++ /dev/null @@ -1,46 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const Tooltip = require('./tooltip') -const copyToClipboard = require('copy-to-clipboard') - -module.exports = Copyable - -inherits(Copyable, Component) -function Copyable () { - Component.call(this) - this.state = { - copied: false, - } -} - -Copyable.prototype.render = function () { - const props = this.props - const state = this.state - const { value, children } = props - const { copied } = state - - return h(Tooltip, { - title: copied ? 'Copied!' : 'Copy', - position: 'bottom', - }, h('span', { - style: { - cursor: 'pointer', - }, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - copyToClipboard(value) - this.debounceRestore() - }, - }, children)) -} - -Copyable.prototype.debounceRestore = function () { - this.setState({ copied: true }) - clearTimeout(this.timeout) - this.timeout = setTimeout(() => { - this.setState({ copied: false }) - }, 850) -} diff --git a/ui/classic/app/components/custom-radio-list.js b/ui/classic/app/components/custom-radio-list.js deleted file mode 100644 index a4c525396..000000000 --- a/ui/classic/app/components/custom-radio-list.js +++ /dev/null @@ -1,60 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = RadioList - -inherits(RadioList, Component) -function RadioList () { - Component.call(this) -} - -RadioList.prototype.render = function () { - const props = this.props - const activeClass = '.custom-radio-selected' - const inactiveClass = '.custom-radio-inactive' - const { - labels, - defaultFocus, - } = props - - - return ( - h('.flex-row', { - style: { - fontSize: '12px', - }, - }, [ - h('.flex-column.custom-radios', { - style: { - marginRight: '5px', - }, - }, - labels.map((lable, i) => { - let isSelcted = (this.state !== null) - isSelcted = isSelcted ? (this.state.selected === lable) : (defaultFocus === lable) - return h(isSelcted ? activeClass : inactiveClass, { - title: lable, - onClick: (event) => { - this.setState({selected: event.target.title}) - props.onClick(event) - }, - }) - }) - ), - h('.text', {}, - labels.map((lable) => { - if (props.subtext) { - return h('.flex-row', {}, [ - h('.radio-titles', lable), - h('.radio-titles-subtext', `- ${props.subtext[lable]}`), - ]) - } else { - return h('.radio-titles', lable) - } - }) - ), - ]) - ) -} - diff --git a/ui/classic/app/components/drop-menu-item.js b/ui/classic/app/components/drop-menu-item.js deleted file mode 100644 index e42948209..000000000 --- a/ui/classic/app/components/drop-menu-item.js +++ /dev/null @@ -1,59 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = DropMenuItem - -inherits(DropMenuItem, Component) -function DropMenuItem () { - Component.call(this) -} - -DropMenuItem.prototype.render = function () { - return h('li.drop-menu-item', { - onClick: () => { - this.props.closeMenu() - this.props.action() - }, - style: { - listStyle: 'none', - padding: '6px 16px 6px 5px', - fontFamily: 'Montserrat Regular', - color: 'rgb(125, 128, 130)', - cursor: 'pointer', - display: 'flex', - justifyContent: 'flex-start', - }, - }, [ - this.props.icon, - this.props.label, - this.activeNetworkRender(), - ]) -} - -DropMenuItem.prototype.activeNetworkRender = function () { - const activeNetwork = this.props.activeNetworkRender - const { provider } = this.props - const providerType = provider ? provider.type : null - if (activeNetwork === undefined) return - - switch (this.props.label) { - case 'Main Ethereum Network': - if (providerType === 'mainnet') return h('.check', '✓') - break - case 'Ropsten Test Network': - if (providerType === 'ropsten') return h('.check', '✓') - break - case 'Kovan Test Network': - if (providerType === 'kovan') return h('.check', '✓') - break - case 'Rinkeby Test Network': - if (providerType === 'rinkeby') return h('.check', '✓') - break - case 'Localhost 8545': - if (activeNetwork === 'http://localhost:8545') return h('.check', '✓') - break - default: - if (activeNetwork === 'custom') return h('.check', '✓') - } -} diff --git a/ui/classic/app/components/editable-label.js b/ui/classic/app/components/editable-label.js deleted file mode 100644 index 41936f5e0..000000000 --- a/ui/classic/app/components/editable-label.js +++ /dev/null @@ -1,51 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const findDOMNode = require('react-dom').findDOMNode - -module.exports = EditableLabel - -inherits(EditableLabel, Component) -function EditableLabel () { - Component.call(this) -} - -EditableLabel.prototype.render = function () { - const props = this.props - const state = this.state - - if (state && state.isEditingLabel) { - return h('div.editable-label', [ - h('input.sizing-input', { - defaultValue: props.textValue, - maxLength: '20', - onKeyPress: (event) => { - this.saveIfEnter(event) - }, - }), - h('button.editable-button', { - onClick: () => this.saveText(), - }, 'Save'), - ]) - } else { - return h('div.name-label', { - onClick: (event) => { - this.setState({ isEditingLabel: true }) - }, - }, this.props.children) - } -} - -EditableLabel.prototype.saveIfEnter = function (event) { - if (event.key === 'Enter') { - this.saveText() - } -} - -EditableLabel.prototype.saveText = function () { - var container = findDOMNode(this) - var text = container.querySelector('.editable-label input').value - var truncatedText = text.substring(0, 20) - this.props.saveText(truncatedText) - this.setState({ isEditingLabel: false, textLabel: truncatedText }) -} diff --git a/ui/classic/app/components/ens-input.js b/ui/classic/app/components/ens-input.js deleted file mode 100644 index 3a33ebf74..000000000 --- a/ui/classic/app/components/ens-input.js +++ /dev/null @@ -1,170 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const extend = require('xtend') -const debounce = require('debounce') -const copyToClipboard = require('copy-to-clipboard') -const ENS = require('ethjs-ens') -const networkMap = require('ethjs-ens/lib/network-map.json') -const ensRE = /.+\.eth$/ -const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' - - -module.exports = EnsInput - -inherits(EnsInput, Component) -function EnsInput () { - Component.call(this) -} - -EnsInput.prototype.render = function () { - const props = this.props - const opts = extend(props, { - list: 'addresses', - onChange: () => { - const network = this.props.network - const networkHasEnsSupport = getNetworkEnsSupport(network) - if (!networkHasEnsSupport) return - - const recipient = document.querySelector('input[name="address"]').value - if (recipient.match(ensRE) === null) { - return this.setState({ - loadingEns: false, - ensResolution: null, - ensFailure: null, - }) - } - - this.setState({ - loadingEns: true, - }) - this.checkName() - }, - }) - return h('div', { - style: { width: '100%' }, - }, [ - h('input.large-input', opts), - // The address book functionality. - h('datalist#addresses', - [ - // Corresponds to the addresses owned. - Object.keys(props.identities).map((key) => { - const identity = props.identities[key] - return h('option', { - value: identity.address, - label: identity.name, - key: identity.address, - }) - }), - // Corresponds to previously sent-to addresses. - props.addressBook.map((identity) => { - return h('option', { - value: identity.address, - label: identity.name, - key: identity.address, - }) - }), - ]), - this.ensIcon(), - ]) -} - -EnsInput.prototype.componentDidMount = function () { - const network = this.props.network - const networkHasEnsSupport = getNetworkEnsSupport(network) - this.setState({ ensResolution: ZERO_ADDRESS }) - - if (networkHasEnsSupport) { - const provider = global.ethereumProvider - this.ens = new ENS({ provider, network }) - this.checkName = debounce(this.lookupEnsName.bind(this), 200) - } -} - -EnsInput.prototype.lookupEnsName = function () { - const recipient = document.querySelector('input[name="address"]').value - const { ensResolution } = this.state - - log.info(`ENS attempting to resolve name: ${recipient}`) - this.ens.lookup(recipient.trim()) - .then((address) => { - if (address === ZERO_ADDRESS) throw new Error('No address has been set for this name.') - if (address !== ensResolution) { - this.setState({ - loadingEns: false, - ensResolution: address, - nickname: recipient.trim(), - hoverText: address + '\nClick to Copy', - ensFailure: false, - }) - } - }) - .catch((reason) => { - log.error(reason) - return this.setState({ - loadingEns: false, - ensResolution: ZERO_ADDRESS, - ensFailure: true, - hoverText: reason.message, - }) - }) -} - -EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) { - const state = this.state || {} - const ensResolution = state.ensResolution - // If an address is sent without a nickname, meaning not from ENS or from - // the user's own accounts, a default of a one-space string is used. - const nickname = state.nickname || ' ' - if (prevState && ensResolution && this.props.onChange && - ensResolution !== prevState.ensResolution) { - this.props.onChange(ensResolution, nickname) - } -} - -EnsInput.prototype.ensIcon = function (recipient) { - const { hoverText } = this.state || {} - return h('span', { - title: hoverText, - style: { - position: 'absolute', - padding: '9px', - transform: 'translatex(-40px)', - }, - }, this.ensIconContents(recipient)) -} - -EnsInput.prototype.ensIconContents = function (recipient) { - const { loadingEns, ensFailure, ensResolution } = this.state || { ensResolution: ZERO_ADDRESS} - - if (loadingEns) { - return h('img', { - src: 'images/loading.svg', - style: { - width: '30px', - height: '30px', - transform: 'translateY(-6px)', - }, - }) - } - - if (ensFailure) { - return h('i.fa.fa-warning.fa-lg.warning') - } - - if (ensResolution && (ensResolution !== ZERO_ADDRESS)) { - return h('i.fa.fa-check-circle.fa-lg.cursor-pointer', { - style: { color: 'green' }, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - copyToClipboard(ensResolution) - }, - }) - } -} - -function getNetworkEnsSupport (network) { - return Boolean(networkMap[network]) -} diff --git a/ui/classic/app/components/eth-balance.js b/ui/classic/app/components/eth-balance.js deleted file mode 100644 index 4f538fd31..000000000 --- a/ui/classic/app/components/eth-balance.js +++ /dev/null @@ -1,89 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const formatBalance = require('../util').formatBalance -const generateBalanceObject = require('../util').generateBalanceObject -const Tooltip = require('./tooltip.js') -const FiatValue = require('./fiat-value.js') - -module.exports = EthBalanceComponent - -inherits(EthBalanceComponent, Component) -function EthBalanceComponent () { - Component.call(this) -} - -EthBalanceComponent.prototype.render = function () { - var props = this.props - let { value } = props - const { style, width } = props - var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true - value = value ? formatBalance(value, 6, needsParse) : '...' - - return ( - - h('.ether-balance.ether-balance-amount', { - style, - }, [ - h('div', { - style: { - display: 'inline', - width, - }, - }, this.renderBalance(value)), - ]) - - ) -} -EthBalanceComponent.prototype.renderBalance = function (value) { - var props = this.props - const { conversionRate, shorten, incoming, currentCurrency } = props - if (value === 'None') return value - if (value === '...') return value - var balanceObj = generateBalanceObject(value, shorten ? 1 : 3) - var balance - var splitBalance = value.split(' ') - var ethNumber = splitBalance[0] - var ethSuffix = splitBalance[1] - const showFiat = 'showFiat' in props ? props.showFiat : true - - if (shorten) { - balance = balanceObj.shortBalance - } else { - balance = balanceObj.balance - } - - var label = balanceObj.label - - return ( - h(Tooltip, { - position: 'bottom', - title: `${ethNumber} ${ethSuffix}`, - }, h('div.flex-column', [ - h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('div', { - style: { - width: '100%', - textAlign: 'right', - }, - }, incoming ? `+${balance}` : balance), - h('div', { - style: { - color: ' #AEAEAE', - fontSize: '12px', - marginLeft: '5px', - }, - }, label), - ]), - - showFiat ? h(FiatValue, { value: props.value, conversionRate, currentCurrency }) : null, - ])) - ) -} diff --git a/ui/classic/app/components/fiat-value.js b/ui/classic/app/components/fiat-value.js deleted file mode 100644 index 8a64a1cfc..000000000 --- a/ui/classic/app/components/fiat-value.js +++ /dev/null @@ -1,63 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const formatBalance = require('../util').formatBalance - -module.exports = FiatValue - -inherits(FiatValue, Component) -function FiatValue () { - Component.call(this) -} - -FiatValue.prototype.render = function () { - const props = this.props - const { conversionRate, currentCurrency } = props - - const value = formatBalance(props.value, 6) - - if (value === 'None') return value - var fiatDisplayNumber, fiatTooltipNumber - var splitBalance = value.split(' ') - - if (conversionRate !== 0) { - fiatTooltipNumber = Number(splitBalance[0]) * conversionRate - fiatDisplayNumber = fiatTooltipNumber.toFixed(2) - } else { - fiatDisplayNumber = 'N/A' - fiatTooltipNumber = 'Unknown' - } - - return fiatDisplay(fiatDisplayNumber, currentCurrency) -} - -function fiatDisplay (fiatDisplayNumber, fiatSuffix) { - if (fiatDisplayNumber !== 'N/A') { - return h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('div', { - style: { - width: '100%', - textAlign: 'right', - fontSize: '12px', - color: '#333333', - }, - }, fiatDisplayNumber), - h('div', { - style: { - color: '#AEAEAE', - marginLeft: '5px', - fontSize: '12px', - }, - }, fiatSuffix), - ]) - } else { - return h('div') - } -} diff --git a/ui/classic/app/components/hex-as-decimal-input.js b/ui/classic/app/components/hex-as-decimal-input.js deleted file mode 100644 index 4a71e9585..000000000 --- a/ui/classic/app/components/hex-as-decimal-input.js +++ /dev/null @@ -1,154 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const extend = require('xtend') - -module.exports = HexAsDecimalInput - -inherits(HexAsDecimalInput, Component) -function HexAsDecimalInput () { - this.state = { invalid: null } - Component.call(this) -} - -/* Hex as Decimal Input - * - * A component for allowing easy, decimal editing - * of a passed in hex string value. - * - * On change, calls back its `onChange` function parameter - * and passes it an updated hex string. - */ - -HexAsDecimalInput.prototype.render = function () { - const props = this.props - const state = this.state - - const { value, onChange, min, max } = props - - const toEth = props.toEth - const suffix = props.suffix - const decimalValue = decimalize(value, toEth) - const style = props.style - - return ( - h('.flex-column', [ - h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('input.hex-input', { - type: 'number', - required: true, - min: min, - max: max, - style: extend({ - display: 'block', - textAlign: 'right', - backgroundColor: 'transparent', - border: '1px solid #bdbdbd', - - }, style), - value: parseInt(decimalValue), - onBlur: (event) => { - this.updateValidity(event) - }, - onChange: (event) => { - this.updateValidity(event) - const hexString = (event.target.value === '') ? '' : hexify(event.target.value) - onChange(hexString) - }, - onInvalid: (event) => { - const msg = this.constructWarning() - if (msg === state.invalid) { - return - } - this.setState({ invalid: msg }) - event.preventDefault() - return false - }, - }), - h('div', { - style: { - color: ' #AEAEAE', - fontSize: '12px', - marginLeft: '5px', - marginRight: '6px', - width: '20px', - }, - }, suffix), - ]), - - state.invalid ? h('span.error', { - style: { - position: 'absolute', - right: '0px', - textAlign: 'right', - transform: 'translateY(26px)', - padding: '3px', - background: 'rgba(255,255,255,0.85)', - zIndex: '1', - textTransform: 'capitalize', - border: '2px solid #E20202', - }, - }, state.invalid) : null, - ]) - ) -} - -HexAsDecimalInput.prototype.setValid = function (message) { - this.setState({ invalid: null }) -} - -HexAsDecimalInput.prototype.updateValidity = function (event) { - const target = event.target - const value = this.props.value - const newValue = target.value - - if (value === newValue) { - return - } - - const valid = target.checkValidity() - if (valid) { - this.setState({ invalid: null }) - } -} - -HexAsDecimalInput.prototype.constructWarning = function () { - const { name, min, max } = this.props - let message = name ? name + ' ' : '' - - if (min && max) { - message += `must be greater than or equal to ${min} and less than or equal to ${max}.` - } else if (min) { - message += `must be greater than or equal to ${min}.` - } else if (max) { - message += `must be less than or equal to ${max}.` - } else { - message += 'Invalid input.' - } - - return message -} - -function hexify (decimalString) { - const hexBN = new BN(parseInt(decimalString), 10) - return '0x' + hexBN.toString('hex') -} - -function decimalize (input, toEth) { - if (input === '') { - return '' - } else { - const strippedInput = ethUtil.stripHexPrefix(input) - const inputBN = new BN(strippedInput, 'hex') - return inputBN.toString(10) - } -} diff --git a/ui/classic/app/components/identicon.js b/ui/classic/app/components/identicon.js deleted file mode 100644 index c754bc6ba..000000000 --- a/ui/classic/app/components/identicon.js +++ /dev/null @@ -1,72 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const isNode = require('detect-node') -const findDOMNode = require('react-dom').findDOMNode -const jazzicon = require('jazzicon') -const iconFactoryGen = require('../../lib/icon-factory') -const iconFactory = iconFactoryGen(jazzicon) - -module.exports = IdenticonComponent - -inherits(IdenticonComponent, Component) -function IdenticonComponent () { - Component.call(this) - - this.defaultDiameter = 46 -} - -IdenticonComponent.prototype.render = function () { - var props = this.props - var diameter = props.diameter || this.defaultDiameter - return ( - h('div', { - key: 'identicon-' + this.props.address, - style: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - height: diameter, - width: diameter, - borderRadius: diameter / 2, - overflow: 'hidden', - }, - }) - ) -} - -IdenticonComponent.prototype.componentDidMount = function () { - var props = this.props - const { address } = props - - if (!address) return - - var container = findDOMNode(this) - - var diameter = props.diameter || this.defaultDiameter - if (!isNode) { - var img = iconFactory.iconForAddress(address, diameter) - container.appendChild(img) - } -} - -IdenticonComponent.prototype.componentDidUpdate = function () { - var props = this.props - const { address } = props - - if (!address) return - - var container = findDOMNode(this) - - var children = container.children - for (var i = 0; i < children.length; i++) { - container.removeChild(children[i]) - } - - var diameter = props.diameter || this.defaultDiameter - if (!isNode) { - var img = iconFactory.iconForAddress(address, diameter) - container.appendChild(img) - } -} - diff --git a/ui/classic/app/components/loading.js b/ui/classic/app/components/loading.js deleted file mode 100644 index 87d6f5d20..000000000 --- a/ui/classic/app/components/loading.js +++ /dev/null @@ -1,53 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') - - -inherits(LoadingIndicator, Component) -module.exports = LoadingIndicator - -function LoadingIndicator () { - Component.call(this) -} - -LoadingIndicator.prototype.render = function () { - const { isLoading, loadingMessage } = this.props - - return ( - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'loader', - transitionEnterTimeout: 150, - transitionLeaveTimeout: 150, - }, [ - - isLoading ? h('div', { - style: { - zIndex: 10, - position: 'absolute', - flexDirection: 'column', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - height: '100%', - width: '100%', - background: 'rgba(255, 255, 255, 0.8)', - }, - }, [ - h('img', { - src: 'images/loading.svg', - }), - - h('br'), - - showMessageIfAny(loadingMessage), - ]) : null, - ]) - ) -} - -function showMessageIfAny (loadingMessage) { - if (!loadingMessage) return null - return h('span', loadingMessage) -} diff --git a/ui/classic/app/components/mascot.js b/ui/classic/app/components/mascot.js deleted file mode 100644 index 973ec2cad..000000000 --- a/ui/classic/app/components/mascot.js +++ /dev/null @@ -1,59 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const metamaskLogo = require('metamask-logo') -const debounce = require('debounce') - -module.exports = Mascot - -inherits(Mascot, Component) -function Mascot () { - Component.call(this) - this.logo = metamaskLogo({ - followMouse: true, - pxNotRatio: true, - width: 200, - height: 200, - }) - - this.refollowMouse = debounce(this.logo.setFollowMouse.bind(this.logo, true), 1000) - this.unfollowMouse = this.logo.setFollowMouse.bind(this.logo, false) -} - -Mascot.prototype.render = function () { - // this is a bit hacky - // the event emitter is on `this.props` - // and we dont get that until render - this.handleAnimationEvents() - - return h('#metamask-mascot-container', { - style: { zIndex: 0 }, - }) -} - -Mascot.prototype.componentDidMount = function () { - var targetDivId = 'metamask-mascot-container' - var container = document.getElementById(targetDivId) - container.appendChild(this.logo.container) -} - -Mascot.prototype.componentWillUnmount = function () { - this.animations = this.props.animationEventEmitter - this.animations.removeAllListeners() - this.logo.container.remove() - this.logo.stopAnimation() -} - -Mascot.prototype.handleAnimationEvents = function () { - // only setup listeners once - if (this.animations) return - this.animations = this.props.animationEventEmitter - this.animations.on('point', this.lookAt.bind(this)) - this.animations.on('setFollowMouse', this.logo.setFollowMouse.bind(this.logo)) -} - -Mascot.prototype.lookAt = function (target) { - this.unfollowMouse() - this.logo.lookAt(target) - this.refollowMouse() -} diff --git a/ui/classic/app/components/mini-account-panel.js b/ui/classic/app/components/mini-account-panel.js deleted file mode 100644 index c09cf5b7a..000000000 --- a/ui/classic/app/components/mini-account-panel.js +++ /dev/null @@ -1,74 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const Identicon = require('./identicon') - -module.exports = AccountPanel - - -inherits(AccountPanel, Component) -function AccountPanel () { - Component.call(this) -} - -AccountPanel.prototype.render = function () { - var props = this.props - var picOrder = props.picOrder || 'left' - const { imageSeed } = props - - return ( - - h('.identity-panel.flex-row.flex-left', { - style: { - cursor: props.onClick ? 'pointer' : undefined, - }, - onClick: props.onClick, - }, [ - - this.genIcon(imageSeed, picOrder), - - h('div.flex-column.flex-justify-center', { - style: { - lineHeight: '15px', - order: 2, - display: 'flex', - alignItems: picOrder === 'left' ? 'flex-begin' : 'flex-end', - }, - }, this.props.children), - ]) - ) -} - -AccountPanel.prototype.genIcon = function (seed, picOrder) { - const props = this.props - - // When there is no seed value, this is a contract creation. - // We then show the contract icon. - if (!seed) { - return h('.identicon-wrapper.flex-column.select-none', { - style: { - order: picOrder === 'left' ? 1 : 3, - }, - }, [ - h('i.fa.fa-file-text-o.fa-lg', { - style: { - fontSize: '42px', - transform: 'translate(0px, -16px)', - }, - }), - ]) - } - - // If there was a seed, we return an identicon for that address. - return h('.identicon-wrapper.flex-column.select-none', { - style: { - order: picOrder === 'left' ? 1 : 3, - }, - }, [ - h(Identicon, { - address: seed, - imageify: props.imageifyIdenticons, - }), - ]) -} - diff --git a/ui/classic/app/components/network.js b/ui/classic/app/components/network.js deleted file mode 100644 index 698a0bbb9..000000000 --- a/ui/classic/app/components/network.js +++ /dev/null @@ -1,124 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = Network - -inherits(Network, Component) - -function Network () { - Component.call(this) -} - -Network.prototype.render = function () { - const props = this.props - const networkNumber = props.network - let providerName - try { - providerName = props.provider.type - } catch (e) { - providerName = null - } - let iconName, hoverText - - if (networkNumber === 'loading') { - return h('span', { - style: { - display: 'flex', - alignItems: 'center', - flexDirection: 'row', - }, - onClick: (event) => this.props.onClick(event), - }, [ - h('img', { - title: 'Attempting to connect to blockchain.', - style: { - width: '27px', - }, - src: 'images/loading.svg', - }), - h('i.fa.fa-sort-desc'), - ]) - } else if (providerName === 'mainnet') { - hoverText = 'Main Ethereum Network' - iconName = 'ethereum-network' - } else if (providerName === 'ropsten') { - hoverText = 'Ropsten Test Network' - iconName = 'ropsten-test-network' - } else if (parseInt(networkNumber) === 3) { - hoverText = 'Ropsten Test Network' - iconName = 'ropsten-test-network' - } else if (providerName === 'kovan') { - hoverText = 'Kovan Test Network' - iconName = 'kovan-test-network' - } else if (providerName === 'rinkeby') { - hoverText = 'Rinkeby Test Network' - iconName = 'rinkeby-test-network' - } else { - hoverText = 'Unknown Private Network' - iconName = 'unknown-private-network' - } - - return ( - h('#network_component.pointer', { - title: hoverText, - onClick: (event) => this.props.onClick(event), - }, [ - (function () { - switch (iconName) { - case 'ethereum-network': - return h('.network-indicator', [ - h('.menu-icon.diamond'), - h('.network-name', { - style: { - color: '#039396', - }}, - 'Ethereum Main Net'), - ]) - case 'ropsten-test-network': - return h('.network-indicator', [ - h('.menu-icon.red-dot'), - h('.network-name', { - style: { - color: '#ff6666', - }}, - 'Ropsten Test Net'), - ]) - case 'kovan-test-network': - return h('.network-indicator', [ - h('.menu-icon.hollow-diamond'), - h('.network-name', { - style: { - color: '#690496', - }}, - 'Kovan Test Net'), - ]) - case 'rinkeby-test-network': - return h('.network-indicator', [ - h('.menu-icon.golden-square'), - h('.network-name', { - style: { - color: '#e7a218', - }}, - 'Rinkeby Test Net'), - ]) - default: - return h('.network-indicator', [ - h('i.fa.fa-question-circle.fa-lg', { - style: { - margin: '10px', - color: 'rgb(125, 128, 130)', - }, - }), - - h('.network-name', { - style: { - color: '#AEAEAE', - }}, - 'Private Network'), - ]) - } - })(), - ]) - ) -} diff --git a/ui/classic/app/components/notice.js b/ui/classic/app/components/notice.js deleted file mode 100644 index d9f0067cd..000000000 --- a/ui/classic/app/components/notice.js +++ /dev/null @@ -1,126 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const ReactMarkdown = require('react-markdown') -const linker = require('extension-link-enabler') -const findDOMNode = require('react-dom').findDOMNode - -module.exports = Notice - -inherits(Notice, Component) -function Notice () { - Component.call(this) -} - -Notice.prototype.render = function () { - const { notice, onConfirm } = this.props - const { title, date, body } = notice - const state = this.state || { disclaimerDisabled: true } - const disabled = state.disclaimerDisabled - - return ( - h('.flex-column.flex-center.flex-grow', [ - h('h3.flex-center.text-transform-uppercase.terms-header', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - width: '100%', - fontSize: '20px', - textAlign: 'center', - padding: 6, - }, - }, [ - title, - ]), - - h('h5.flex-center.text-transform-uppercase.terms-header', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginBottom: 24, - width: '100%', - fontSize: '20px', - textAlign: 'center', - padding: 6, - }, - }, [ - date, - ]), - - h('style', ` - - .markdown { - overflow-x: hidden; - } - - .markdown h1, .markdown h2, .markdown h3 { - margin: 10px 0; - font-weight: bold; - } - - .markdown strong { - font-weight: bold; - } - .markdown em { - font-style: italic; - } - - .markdown p { - margin: 10px 0; - } - - .markdown a { - color: #df6b0e; - } - - `), - - h('div.markdown', { - onScroll: (e) => { - var object = e.currentTarget - if (object.offsetHeight + object.scrollTop + 100 >= object.scrollHeight) { - this.setState({disclaimerDisabled: false}) - } - }, - style: { - background: 'rgb(235, 235, 235)', - height: '310px', - padding: '6px', - width: '90%', - overflowY: 'scroll', - scroll: 'auto', - }, - }, [ - h(ReactMarkdown, { - className: 'notice-box', - source: body, - skipHtml: true, - }), - ]), - - h('button', { - disabled, - onClick: () => { - this.setState({disclaimerDisabled: true}) - onConfirm() - }, - style: { - marginTop: '18px', - }, - }, 'Accept'), - ]) - ) -} - -Notice.prototype.componentDidMount = function () { - var node = findDOMNode(this) - linker.setupListener(node) - if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) { - this.setState({disclaimerDisabled: false}) - } -} - -Notice.prototype.componentWillUnmount = function () { - var node = findDOMNode(this) - linker.teardownListener(node) -} diff --git a/ui/classic/app/components/pending-msg-details.js b/ui/classic/app/components/pending-msg-details.js deleted file mode 100644 index 16308d121..000000000 --- a/ui/classic/app/components/pending-msg-details.js +++ /dev/null @@ -1,50 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const AccountPanel = require('./account-panel') - -module.exports = PendingMsgDetails - -inherits(PendingMsgDetails, Component) -function PendingMsgDetails () { - Component.call(this) -} - -PendingMsgDetails.prototype.render = function () { - var state = this.props - var msgData = state.txData - - var msgParams = msgData.msgParams || {} - var address = msgParams.from || state.selectedAddress - var identity = state.identities[address] || { address: address } - var account = state.accounts[address] || { address: address } - - return ( - h('div', { - key: msgData.id, - style: { - margin: '10px 20px', - }, - }, [ - - // account that will sign - h(AccountPanel, { - showFullAddress: true, - identity: identity, - account: account, - imageifyIdenticons: state.imageifyIdenticons, - }), - - // message data - h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-row.flex-space-between', [ - h('label.font-small', 'MESSAGE'), - h('span.font-small', msgParams.data), - ]), - ]), - - ]) - ) -} - diff --git a/ui/classic/app/components/pending-msg.js b/ui/classic/app/components/pending-msg.js deleted file mode 100644 index b2cac164a..000000000 --- a/ui/classic/app/components/pending-msg.js +++ /dev/null @@ -1,56 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const PendingTxDetails = require('./pending-msg-details') - -module.exports = PendingMsg - -inherits(PendingMsg, Component) -function PendingMsg () { - Component.call(this) -} - -PendingMsg.prototype.render = function () { - var state = this.props - var msgData = state.txData - - return ( - - h('div', { - key: msgData.id, - }, [ - - // header - h('h3', { - style: { - fontWeight: 'bold', - textAlign: 'center', - }, - }, 'Sign Message'), - - h('.error', { - style: { - margin: '10px', - }, - }, `Signing this message can have - dangerous side effects. Only sign messages from - sites you fully trust with your entire account. - This will be fixed in a future version.`), - - // message details - h(PendingTxDetails, state), - - // sign + cancel - h('.flex-row.flex-space-around', [ - h('button', { - onClick: state.cancelMessage, - }, 'Cancel'), - h('button', { - onClick: state.signMessage, - }, 'Sign'), - ]), - ]) - - ) -} - diff --git a/ui/classic/app/components/pending-personal-msg-details.js b/ui/classic/app/components/pending-personal-msg-details.js deleted file mode 100644 index 1050513f2..000000000 --- a/ui/classic/app/components/pending-personal-msg-details.js +++ /dev/null @@ -1,60 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const AccountPanel = require('./account-panel') -const BinaryRenderer = require('./binary-renderer') - -module.exports = PendingMsgDetails - -inherits(PendingMsgDetails, Component) -function PendingMsgDetails () { - Component.call(this) -} - -PendingMsgDetails.prototype.render = function () { - var state = this.props - var msgData = state.txData - - var msgParams = msgData.msgParams || {} - var address = msgParams.from || state.selectedAddress - var identity = state.identities[address] || { address: address } - var account = state.accounts[address] || { address: address } - - var { data } = msgParams - - return ( - h('div', { - key: msgData.id, - style: { - margin: '10px 20px', - }, - }, [ - - // account that will sign - h(AccountPanel, { - showFullAddress: true, - identity: identity, - account: account, - imageifyIdenticons: state.imageifyIdenticons, - }), - - // message data - h('div', { - style: { - height: '260px', - }, - }, [ - h('label.font-small', { style: { display: 'block' } }, 'MESSAGE'), - h(BinaryRenderer, { - value: data, - style: { - height: '215px', - }, - }), - ]), - - ]) - ) -} - diff --git a/ui/classic/app/components/pending-personal-msg.js b/ui/classic/app/components/pending-personal-msg.js deleted file mode 100644 index 4542adb28..000000000 --- a/ui/classic/app/components/pending-personal-msg.js +++ /dev/null @@ -1,47 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const PendingTxDetails = require('./pending-personal-msg-details') - -module.exports = PendingMsg - -inherits(PendingMsg, Component) -function PendingMsg () { - Component.call(this) -} - -PendingMsg.prototype.render = function () { - var state = this.props - var msgData = state.txData - - return ( - - h('div', { - key: msgData.id, - }, [ - - // header - h('h3', { - style: { - fontWeight: 'bold', - textAlign: 'center', - }, - }, 'Sign Message'), - - // message details - h(PendingTxDetails, state), - - // sign + cancel - h('.flex-row.flex-space-around', [ - h('button', { - onClick: state.cancelPersonalMessage, - }, 'Cancel'), - h('button', { - onClick: state.signPersonalMessage, - }, 'Sign'), - ]), - ]) - - ) -} - diff --git a/ui/classic/app/components/pending-tx.js b/ui/classic/app/components/pending-tx.js deleted file mode 100644 index 962680d30..000000000 --- a/ui/classic/app/components/pending-tx.js +++ /dev/null @@ -1,480 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const actions = require('../actions') -const clone = require('clone') - -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const hexToBn = require('../../../../app/scripts/lib/hex-to-bn') -const util = require('../util') -const MiniAccountPanel = require('./mini-account-panel') -const Copyable = require('./copyable') -const EthBalance = require('./eth-balance') -const addressSummary = util.addressSummary -const nameForAddress = require('../../lib/contract-namer') -const BNInput = require('./bn-as-decimal-input') - -const MIN_GAS_PRICE_GWEI_BN = new BN(2) -const GWEI_FACTOR = new BN(1e9) -const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) -const MIN_GAS_LIMIT_BN = new BN(21000) - -module.exports = PendingTx -inherits(PendingTx, Component) -function PendingTx () { - Component.call(this) - this.state = { - valid: true, - txData: null, - submitting: false, - } -} - -PendingTx.prototype.render = function () { - const props = this.props - const { currentCurrency, blockGasLimit } = props - - const conversionRate = props.conversionRate - const txMeta = this.gatherTxMeta() - const txParams = txMeta.txParams || {} - - // Account Details - const address = txParams.from || props.selectedAddress - const identity = props.identities[address] || { address: address } - const account = props.accounts[address] - const balance = account ? account.balance : '0x0' - - // recipient check - const isValidAddress = !txParams.to || util.isValidAddress(txParams.to) - - // Gas - const gas = txParams.gas - const gasBn = hexToBn(gas) - const gasLimit = new BN(parseInt(blockGasLimit)) - const safeGasLimit = this.bnMultiplyByFraction(gasLimit, 19, 20).toString(10) - - // Gas Price - const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) - const gasPriceBn = hexToBn(gasPrice) - - const txFeeBn = gasBn.mul(gasPriceBn) - const valueBn = hexToBn(txParams.value) - const maxCost = txFeeBn.add(valueBn) - - const dataLength = txParams.data ? (txParams.data.length - 2) / 2 : 0 - - const balanceBn = hexToBn(balance) - const insufficientBalance = balanceBn.lt(maxCost) - - this.inputs = [] - - return ( - - h('div', { - key: txMeta.id, - }, [ - - h('form#pending-tx-form', { - onSubmit: this.onSubmit.bind(this), - - }, [ - - // tx info - h('div', [ - - h('.flex-row.flex-center', { - style: { - maxWidth: '100%', - }, - }, [ - - h(MiniAccountPanel, { - imageSeed: address, - picOrder: 'right', - }, [ - h('span.font-small', { - style: { - fontFamily: 'Montserrat Bold, Montserrat, sans-serif', - }, - }, identity.name), - - h(Copyable, { - value: ethUtil.toChecksumAddress(address), - }, [ - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, addressSummary(address, 6, 4, false)), - ]), - - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, [ - h(EthBalance, { - value: balance, - conversionRate, - currentCurrency, - inline: true, - labelColor: '#F7861C', - }), - ]), - ]), - - forwardCarrat(), - - this.miniAccountPanelForRecipient(), - ]), - - h('style', ` - .table-box { - margin: 7px 0px 0px 0px; - width: 100%; - } - .table-box .row { - margin: 0px; - background: rgb(236,236,236); - display: flex; - justify-content: space-between; - font-family: Montserrat Light, sans-serif; - font-size: 13px; - padding: 5px 25px; - } - .table-box .row .value { - font-family: Montserrat Regular; - } - `), - - h('.table-box', [ - - // Ether Value - // Currently not customizable, but easily modified - // in the way that gas and gasLimit currently are. - h('.row', [ - h('.cell.label', 'Amount'), - h(EthBalance, { value: txParams.value, currentCurrency, conversionRate }), - ]), - - // Gas Limit (customizable) - h('.cell.row', [ - h('.cell.label', 'Gas Limit'), - h('.cell.value', { - }, [ - h(BNInput, { - name: 'Gas Limit', - value: gasBn, - precision: 0, - scale: 0, - // The hard lower limit for gas. - min: MIN_GAS_LIMIT_BN.toString(10), - max: safeGasLimit, - suffix: 'UNITS', - style: { - position: 'relative', - top: '5px', - }, - onChange: this.gasLimitChanged.bind(this), - - ref: (hexInput) => { this.inputs.push(hexInput) }, - }), - ]), - ]), - - // Gas Price (customizable) - h('.cell.row', [ - h('.cell.label', 'Gas Price'), - h('.cell.value', { - }, [ - h(BNInput, { - name: 'Gas Price', - value: gasPriceBn, - precision: 9, - scale: 9, - suffix: 'GWEI', - min: MIN_GAS_PRICE_GWEI_BN.toString(10), - style: { - position: 'relative', - top: '5px', - }, - onChange: this.gasPriceChanged.bind(this), - ref: (hexInput) => { this.inputs.push(hexInput) }, - }), - ]), - ]), - - // Max Transaction Fee (calculated) - h('.cell.row', [ - h('.cell.label', 'Max Transaction Fee'), - h(EthBalance, { value: txFeeBn.toString(16), currentCurrency, conversionRate }), - ]), - - h('.cell.row', { - style: { - fontFamily: 'Montserrat Regular', - background: 'white', - padding: '10px 25px', - }, - }, [ - h('.cell.label', 'Max Total'), - h('.cell.value', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - h(EthBalance, { - value: maxCost.toString(16), - currentCurrency, - conversionRate, - inline: true, - labelColor: 'black', - fontSize: '16px', - }), - ]), - ]), - - // Data size row: - h('.cell.row', { - style: { - background: '#f7f7f7', - paddingBottom: '0px', - }, - }, [ - h('.cell.label'), - h('.cell.value', { - style: { - fontFamily: 'Montserrat Light', - fontSize: '11px', - }, - }, `Data included: ${dataLength} bytes`), - ]), - ]), // End of Table - - ]), - - h('style', ` - .conf-buttons button { - margin-left: 10px; - text-transform: uppercase; - } - `), - - txMeta.simulationFails ? - h('.error', { - style: { - marginLeft: 50, - fontSize: '0.9em', - }, - }, 'Transaction Error. Exception thrown in contract code.') - : null, - - !isValidAddress ? - h('.error', { - style: { - marginLeft: 50, - fontSize: '0.9em', - }, - }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.') - : null, - - insufficientBalance ? - h('span.error', { - style: { - marginLeft: 50, - fontSize: '0.9em', - }, - }, 'Insufficient balance for transaction') - : null, - - // send + cancel - h('.flex-row.flex-space-around.conf-buttons', { - style: { - display: 'flex', - justifyContent: 'flex-end', - margin: '14px 25px', - }, - }, [ - - - insufficientBalance ? - h('button.btn-green', { - onClick: props.buyEth, - }, 'Buy Ether') - : null, - - h('button', { - onClick: (event) => { - this.resetGasFields() - event.preventDefault() - }, - }, 'Reset'), - - // Accept Button - h('input.confirm.btn-green', { - type: 'submit', - value: 'SUBMIT', - style: { marginLeft: '10px' }, - disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, - }), - - h('button.cancel.btn-red', { - onClick: props.cancelTransaction, - }, 'Reject'), - ]), - ]), - ]) - ) -} - -PendingTx.prototype.miniAccountPanelForRecipient = function () { - const props = this.props - const txData = props.txData - const txParams = txData.txParams || {} - const isContractDeploy = !('to' in txParams) - - // If it's not a contract deploy, send to the account - if (!isContractDeploy) { - return h(MiniAccountPanel, { - imageSeed: txParams.to, - picOrder: 'left', - }, [ - - h('span.font-small', { - style: { - fontFamily: 'Montserrat Bold, Montserrat, sans-serif', - }, - }, nameForAddress(txParams.to, props.identities)), - - h(Copyable, { - value: ethUtil.toChecksumAddress(txParams.to), - }, [ - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, addressSummary(txParams.to, 6, 4, false)), - ]), - - ]) - } else { - return h(MiniAccountPanel, { - picOrder: 'left', - }, [ - - h('span.font-small', { - style: { - fontFamily: 'Montserrat Bold, Montserrat, sans-serif', - }, - }, 'New Contract'), - - ]) - } -} - -PendingTx.prototype.gasPriceChanged = function (newBN, valid) { - log.info(`Gas price changed to: ${newBN.toString(10)}`) - const txMeta = this.gatherTxMeta() - txMeta.txParams.gasPrice = '0x' + newBN.toString('hex') - this.setState({ - txData: clone(txMeta), - valid, - }) -} - -PendingTx.prototype.gasLimitChanged = function (newBN, valid) { - log.info(`Gas limit changed to ${newBN.toString(10)}`) - const txMeta = this.gatherTxMeta() - txMeta.txParams.gas = '0x' + newBN.toString('hex') - this.setState({ - txData: clone(txMeta), - valid, - }) -} - -PendingTx.prototype.resetGasFields = function () { - log.debug(`pending-tx resetGasFields`) - - this.inputs.forEach((hexInput) => { - if (hexInput) { - hexInput.setValid() - } - }) - - this.setState({ - txData: null, - valid: true, - }) -} - -PendingTx.prototype.onSubmit = function (event) { - event.preventDefault() - const txMeta = this.gatherTxMeta() - const valid = this.checkValidity() - this.setState({ valid, submitting: true }) - if (valid && this.verifyGasParams()) { - this.props.sendTransaction(txMeta, event) - } else { - this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) - this.setState({ submitting: false }) - } -} - -PendingTx.prototype.checkValidity = function () { - const form = this.getFormEl() - const valid = form.checkValidity() - return valid -} - -PendingTx.prototype.getFormEl = function () { - const form = document.querySelector('form#pending-tx-form') - // Stub out form for unit tests: - if (!form) { - return { checkValidity () { return true } } - } - return form -} - -// After a customizable state value has been updated, -PendingTx.prototype.gatherTxMeta = function () { - log.debug(`pending-tx gatherTxMeta`) - const props = this.props - const state = this.state - const txData = clone(state.txData) || clone(props.txData) - - log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) - return txData -} - -PendingTx.prototype.verifyGasParams = function () { - // We call this in case the gas has not been modified at all - if (!this.state) { return true } - return ( - this._notZeroOrEmptyString(this.state.gas) && - this._notZeroOrEmptyString(this.state.gasPrice) - ) -} - -PendingTx.prototype._notZeroOrEmptyString = function (obj) { - return obj !== '' && obj !== '0x0' -} - -PendingTx.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) { - const numBN = new BN(numerator) - const denomBN = new BN(denominator) - return targetBN.mul(numBN).div(denomBN) -} - -function forwardCarrat () { - return ( - h('img', { - src: 'images/forward-carrat.svg', - style: { - padding: '5px 6px 0px 10px', - height: '37px', - }, - }) - ) -} diff --git a/ui/classic/app/components/qr-code.js b/ui/classic/app/components/qr-code.js deleted file mode 100644 index 06b9aed9b..000000000 --- a/ui/classic/app/components/qr-code.js +++ /dev/null @@ -1,79 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const qrCode = require('qrcode-npm').qrcode -const inherits = require('util').inherits -const connect = require('react-redux').connect -const isHexPrefixed = require('ethereumjs-util').isHexPrefixed -const CopyButton = require('./copyButton') - -module.exports = connect(mapStateToProps)(QrCodeView) - -function mapStateToProps (state) { - return { - Qr: state.appState.Qr, - buyView: state.appState.buyView, - warning: state.appState.warning, - } -} - -inherits(QrCodeView, Component) - -function QrCodeView () { - Component.call(this) -} - -QrCodeView.prototype.render = function () { - const props = this.props - const Qr = props.Qr - const address = `${isHexPrefixed(Qr.data) ? 'ethereum:' : ''}${Qr.data}` - const qrImage = qrCode(4, 'M') - qrImage.addData(address) - qrImage.make() - return h('.main-container.flex-column', { - key: 'qr', - style: { - justifyContent: 'center', - paddingBottom: '45px', - paddingLeft: '45px', - paddingRight: '45px', - alignItems: 'center', - }, - }, [ - Array.isArray(Qr.message) ? h('.message-container', this.renderMultiMessage()) : h('.qr-header', Qr.message), - - this.props.warning ? this.props.warning && h('span.error.flex-center', { - style: { - textAlign: 'center', - width: '229px', - height: '82px', - }, - }, - this.props.warning) : null, - - h('#qr-container.flex-column', { - style: { - marginTop: '25px', - marginBottom: '15px', - }, - dangerouslySetInnerHTML: { - __html: qrImage.createTableTag(4), - }, - }), - h('.flex-row', [ - h('h3.ellip-address', { - style: { - width: '247px', - }, - }, Qr.data), - h(CopyButton, { - value: Qr.data, - }), - ]), - ]) -} - -QrCodeView.prototype.renderMultiMessage = function () { - var Qr = this.props.Qr - var multiMessage = Qr.message.map((message) => h('.qr-message', message)) - return multiMessage -} diff --git a/ui/classic/app/components/range-slider.js b/ui/classic/app/components/range-slider.js deleted file mode 100644 index 823f5eb01..000000000 --- a/ui/classic/app/components/range-slider.js +++ /dev/null @@ -1,58 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = RangeSlider - -inherits(RangeSlider, Component) -function RangeSlider () { - Component.call(this) -} - -RangeSlider.prototype.render = function () { - const state = this.state || {} - const props = this.props - const onInput = props.onInput || function () {} - const name = props.name - const { - min = 0, - max = 100, - increment = 1, - defaultValue = 50, - mirrorInput = false, - } = this.props.options - const {container, input, range} = props.style - - return ( - h('.flex-row', { - style: container, - }, [ - h('input', { - type: 'range', - name: name, - min: min, - max: max, - step: increment, - style: range, - value: state.value || defaultValue, - onChange: mirrorInput ? this.mirrorInputs.bind(this, event) : onInput, - }), - - // Mirrored input for range - mirrorInput ? h('input.large-input', { - type: 'number', - name: `${name}Mirror`, - min: min, - max: max, - value: state.value || defaultValue, - step: increment, - style: input, - onChange: this.mirrorInputs.bind(this, event), - }) : null, - ]) - ) -} - -RangeSlider.prototype.mirrorInputs = function (event) { - this.setState({value: event.target.value}) -} diff --git a/ui/classic/app/components/shapeshift-form.js b/ui/classic/app/components/shapeshift-form.js deleted file mode 100644 index e0a720426..000000000 --- a/ui/classic/app/components/shapeshift-form.js +++ /dev/null @@ -1,306 +0,0 @@ -const PersistentForm = require('../../lib/persistent-form') -const h = require('react-hyperscript') -const inherits = require('util').inherits -const connect = require('react-redux').connect -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') -const actions = require('../actions') -const Qr = require('./qr-code') -const isValidAddress = require('../util').isValidAddress -module.exports = connect(mapStateToProps)(ShapeshiftForm) - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - isSubLoading: state.appState.isSubLoading, - qrRequested: state.appState.qrRequested, - } -} - -inherits(ShapeshiftForm, PersistentForm) - -function ShapeshiftForm () { - PersistentForm.call(this) - this.persistentFormParentId = 'shapeshift-buy-form' -} - -ShapeshiftForm.prototype.render = function () { - return h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.props.qrRequested ? h(Qr, {key: 'qr'}) : this.renderMain(), - ]) -} - -ShapeshiftForm.prototype.renderMain = function () { - const marketinfo = this.props.buyView.formView.marketinfo - const coinOptions = this.props.buyView.formView.coinOptions - var coin = marketinfo.pair.split('_')[0].toUpperCase() - - return h('.flex-column', { - style: { - // marginTop: '10px', - padding: '25px', - paddingTop: '5px', - width: '100%', - minHeight: '215px', - alignItems: 'center', - overflowY: 'auto', - }, - }, [ - h('.flex-row', { - style: { - justifyContent: 'center', - alignItems: 'baseline', - height: '42px', - }, - }, [ - h('img', { - src: coinOptions[coin].image, - width: '25px', - height: '25px', - style: { - marginRight: '5px', - }, - }), - - h('.input-container', [ - h('input#fromCoin.buy-inputs.ex-coins', { - type: 'text', - list: 'coinList', - autoFocus: true, - dataset: { - persistentFormId: 'input-coin', - }, - style: { - boxSizing: 'border-box', - }, - onChange: this.handleLiveInput.bind(this), - defaultValue: 'BTC', - }), - - this.renderCoinList(), - - h('i.fa.fa-pencil-square-o.edit-text', { - style: { - fontSize: '12px', - color: '#F7861C', - position: 'relative', - bottom: '48px', - left: '106px', - }, - }), - ]), - - h('.icon-control', [ - h('i.fa.fa-refresh.fa-4.orange', { - style: { - bottom: '5px', - left: '5px', - color: '#F7861C', - }, - onClick: this.updateCoin.bind(this), - }), - h('i.fa.fa-chevron-right.fa-4.orange', { - style: { - position: 'relative', - bottom: '26px', - left: '10px', - color: '#F7861C', - }, - onClick: this.updateCoin.bind(this), - }), - ]), - - h('#toCoin.ex-coins', marketinfo.pair.split('_')[1].toUpperCase()), - - h('img', { - src: coinOptions[marketinfo.pair.split('_')[1].toUpperCase()].image, - width: '25px', - height: '25px', - style: { - marginLeft: '5px', - }, - }), - ]), - h('.flex-column', { - style: { - alignItems: 'flex-start', - }, - }, [ - this.props.warning ? this.props.warning && h('span.error.flex-center', { - style: { - textAlign: 'center', - width: '229px', - height: '82px', - }, - }, - this.props.warning) : this.renderInfo(), - ]), - - h(this.activeToggle('.input-container'), { - style: { - padding: '10px', - paddingTop: '0px', - width: '100%', - }, - }, [ - - h('div', `${coin} Address:`), - - h('input#fromCoinAddress.buy-inputs', { - type: 'text', - placeholder: `Your ${coin} Refund Address`, - dataset: { - persistentFormId: 'refund-address', - }, - style: { - boxSizing: 'border-box', - width: '227px', - height: '30px', - padding: ' 5px ', - }, - }), - - h('i.fa.fa-pencil-square-o.edit-text', { - style: { - fontSize: '12px', - color: '#F7861C', - position: 'relative', - bottom: '10px', - right: '11px', - }, - }), - h('.flex-row', { - style: { - justifyContent: 'flex-end', - }, - }, [ - h('button', { - onClick: this.shift.bind(this), - style: { - marginTop: '10px', - position: 'relative', - bottom: '40px', - }, - }, - 'Submit'), - ]), - ]), - ]) -} - -ShapeshiftForm.prototype.shift = function () { - var props = this.props - var withdrawal = this.props.buyView.buyAddress - var returnAddress = document.getElementById('fromCoinAddress').value - var pair = this.props.buyView.formView.marketinfo.pair - var data = { - 'withdrawal': withdrawal, - 'pair': pair, - 'returnAddress': returnAddress, - // Public api key - 'apiKey': '803d1f5df2ed1b1476e4b9e6bcd089e34d8874595dda6a23b67d93c56ea9cc2445e98a6748b219b2b6ad654d9f075f1f1db139abfa93158c04e825db122c14b6', - } - var message = [ - `Deposit Limit: ${props.buyView.formView.marketinfo.limit}`, - `Deposit Minimum:${props.buyView.formView.marketinfo.minimum}`, - ] - if (isValidAddress(withdrawal)) { - this.props.dispatch(actions.coinShiftRquest(data, message)) - } -} - -ShapeshiftForm.prototype.renderCoinList = function () { - var list = Object.keys(this.props.buyView.formView.coinOptions).map((item) => { - return h('option', { - value: item, - }, item) - }) - - return h('datalist#coinList', { - onClick: (event) => { - event.preventDefault() - }, - }, list) -} - -ShapeshiftForm.prototype.updateCoin = function (event) { - event.preventDefault() - const props = this.props - var coinOptions = this.props.buyView.formView.coinOptions - var coin = document.getElementById('fromCoin').value - - if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { - var message = 'Not a valid coin' - return props.dispatch(actions.displayWarning(message)) - } else { - return props.dispatch(actions.pairUpdate(coin)) - } -} - -ShapeshiftForm.prototype.handleLiveInput = function () { - const props = this.props - var coinOptions = this.props.buyView.formView.coinOptions - var coin = document.getElementById('fromCoin').value - - if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { - return null - } else { - return props.dispatch(actions.pairUpdate(coin)) - } -} - -ShapeshiftForm.prototype.renderInfo = function () { - const marketinfo = this.props.buyView.formView.marketinfo - const coinOptions = this.props.buyView.formView.coinOptions - var coin = marketinfo.pair.split('_')[0].toUpperCase() - - return h('span', { - style: { - }, - }, [ - h('h3.flex-row.text-transform-uppercase', { - style: { - color: '#868686', - paddingTop: '4px', - justifyContent: 'space-around', - textAlign: 'center', - fontSize: '17px', - }, - }, `Market Info for ${marketinfo.pair.replace('_', ' to ').toUpperCase()}:`), - h('.marketinfo', ['Status : ', `${coinOptions[coin].status}`]), - h('.marketinfo', ['Exchange Rate: ', `${marketinfo.rate}`]), - h('.marketinfo', ['Limit: ', `${marketinfo.limit}`]), - h('.marketinfo', ['Minimum : ', `${marketinfo.minimum}`]), - ]) -} - -ShapeshiftForm.prototype.activeToggle = function (elementType) { - if (!this.props.buyView.formView.response || this.props.warning) return elementType - return `${elementType}.inactive` -} - -ShapeshiftForm.prototype.renderLoading = function () { - return h('span', { - style: { - position: 'absolute', - left: '70px', - bottom: '194px', - background: 'transparent', - width: '229px', - height: '82px', - display: 'flex', - justifyContent: 'center', - }, - }, [ - h('img', { - style: { - width: '60px', - }, - src: 'images/loading.svg', - }), - ]) -} diff --git a/ui/classic/app/components/shift-list-item.js b/ui/classic/app/components/shift-list-item.js deleted file mode 100644 index 32bfbeda4..000000000 --- a/ui/classic/app/components/shift-list-item.js +++ /dev/null @@ -1,204 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const vreme = new (require('vreme')) -const explorerLink = require('../../lib/explorer-link') -const actions = require('../actions') -const addressSummary = require('../util').addressSummary - -const CopyButton = require('./copyButton') -const EthBalance = require('./eth-balance') -const Tooltip = require('./tooltip') - - -module.exports = connect(mapStateToProps)(ShiftListItem) - -function mapStateToProps (state) { - return { - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - } -} - -inherits(ShiftListItem, Component) - -function ShiftListItem () { - Component.call(this) -} - -ShiftListItem.prototype.render = function () { - return ( - h('.transaction-list-item.flex-row', { - style: { - paddingTop: '20px', - paddingBottom: '20px', - justifyContent: 'space-around', - alignItems: 'center', - }, - }, [ - h('div', { - style: { - width: '0px', - position: 'relative', - bottom: '19px', - }, - }, [ - h('img', { - src: 'https://info.shapeshift.io/sites/default/files/logo.png', - style: { - height: '35px', - width: '132px', - position: 'absolute', - clip: 'rect(0px,23px,34px,0px)', - }, - }), - ]), - - this.renderInfo(), - this.renderUtilComponents(), - ]) - ) -} - -function formatDate (date) { - return vreme.format(new Date(date), 'March 16 2014 14:30') -} - -ShiftListItem.prototype.renderUtilComponents = function () { - var props = this.props - const { conversionRate, currentCurrency } = props - - switch (props.response.status) { - case 'no_deposits': - return h('.flex-row', [ - h(CopyButton, { - value: this.props.depositAddress, - }), - h(Tooltip, { - title: 'QR Code', - }, [ - h('i.fa.fa-qrcode.pointer.pop-hover', { - onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType)), - style: { - margin: '5px', - marginLeft: '23px', - marginRight: '12px', - fontSize: '20px', - color: '#F7861C', - }, - }), - ]), - ]) - case 'received': - return h('.flex-row') - - case 'complete': - return h('.flex-row', [ - h(CopyButton, { - value: this.props.response.transaction, - }), - h(EthBalance, { - value: `${props.response.outgoingCoin}`, - conversionRate, - currentCurrency, - width: '55px', - shorten: true, - needsParse: false, - incoming: true, - style: { - fontSize: '15px', - color: '#01888C', - }, - }), - ]) - - case 'failed': - return '' - default: - return '' - } -} - -ShiftListItem.prototype.renderInfo = function () { - var props = this.props - switch (props.response.status) { - case 'no_deposits': - return h('.flex-column', { - style: { - width: '200px', - overflow: 'hidden', - }, - }, [ - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, `${props.depositType} to ETH via ShapeShift`), - h('div', 'No deposits received'), - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, formatDate(props.time)), - ]) - case 'received': - return h('.flex-column', { - style: { - width: '200px', - overflow: 'hidden', - }, - }, [ - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, `${props.depositType} to ETH via ShapeShift`), - h('div', 'Conversion in progress'), - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, formatDate(props.time)), - ]) - case 'complete': - var url = explorerLink(props.response.transaction, parseInt('1')) - - return h('.flex-column.pointer', { - style: { - width: '200px', - overflow: 'hidden', - }, - onClick: () => global.platform.openWindow({ url }), - }, [ - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, 'From ShapeShift'), - h('div', formatDate(props.time)), - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, addressSummary(props.response.transaction)), - ]) - - case 'failed': - return h('span.error', '(Failed)') - default: - return '' - } -} diff --git a/ui/classic/app/components/tab-bar.js b/ui/classic/app/components/tab-bar.js deleted file mode 100644 index 6295e7dd9..000000000 --- a/ui/classic/app/components/tab-bar.js +++ /dev/null @@ -1,36 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = TabBar - -inherits(TabBar, Component) -function TabBar () { - Component.call(this) -} - -TabBar.prototype.render = function () { - const props = this.props - const state = this.state || {} - const { tabs = [], defaultTab, tabSelected } = props - const { subview = defaultTab } = state - - return ( - h('.flex-row.space-around.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - paddingTop: '4px', - }, - }, tabs.map((tab) => { - const { key, content } = tab - return h(subview === key ? '.activeForm' : '.inactiveForm.pointer', { - onClick: () => { - this.setState({ subview: key }) - tabSelected(key) - }, - }, content) - })) - ) -} - diff --git a/ui/classic/app/components/template.js b/ui/classic/app/components/template.js deleted file mode 100644 index b6ed8eaa0..000000000 --- a/ui/classic/app/components/template.js +++ /dev/null @@ -1,18 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = NewComponent - -inherits(NewComponent, Component) -function NewComponent () { - Component.call(this) -} - -NewComponent.prototype.render = function () { - const props = this.props - - return ( - h('span', props.message) - ) -} diff --git a/ui/classic/app/components/token-cell.js b/ui/classic/app/components/token-cell.js deleted file mode 100644 index 19d7139bb..000000000 --- a/ui/classic/app/components/token-cell.js +++ /dev/null @@ -1,72 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const Identicon = require('./identicon') -const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') - -module.exports = TokenCell - -inherits(TokenCell, Component) -function TokenCell () { - Component.call(this) -} - -TokenCell.prototype.render = function () { - const props = this.props - const { address, symbol, string, network, userAddress } = props - - return ( - h('li.token-cell', { - style: { cursor: network === '1' ? 'pointer' : 'default' }, - onClick: this.view.bind(this, address, userAddress, network), - }, [ - - h(Identicon, { - diameter: 50, - address, - network, - }), - - h('h3', `${string || 0} ${symbol}`), - - h('span', { style: { flex: '1 0 auto' } }), - - /* - h('button', { - onClick: this.send.bind(this, address), - }, 'SEND'), - */ - - ]) - ) -} - -TokenCell.prototype.send = function (address, event) { - event.preventDefault() - event.stopPropagation() - const url = tokenFactoryFor(address) - if (url) { - navigateTo(url) - } -} - -TokenCell.prototype.view = function (address, userAddress, network, event) { - const url = etherscanLinkFor(address, userAddress, network) - if (url) { - navigateTo(url) - } -} - -function navigateTo (url) { - global.platform.openWindow({ url }) -} - -function etherscanLinkFor (tokenAddress, address, network) { - const prefix = prefixForNetwork(network) - return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}` -} - -function tokenFactoryFor (tokenAddress) { - return `https://tokenfactory.surge.sh/#/token/${tokenAddress}` -} - diff --git a/ui/classic/app/components/token-list.js b/ui/classic/app/components/token-list.js deleted file mode 100644 index 20cfa897e..000000000 --- a/ui/classic/app/components/token-list.js +++ /dev/null @@ -1,192 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const TokenTracker = require('eth-token-tracker') -const TokenCell = require('./token-cell.js') -const normalizeAddress = require('eth-sig-util').normalize - -const defaultTokens = [] -const contracts = require('eth-contract-metadata') -for (const address in contracts) { - const contract = contracts[address] - if (contract.erc20) { - contract.address = address - defaultTokens.push(contract) - } -} - -module.exports = TokenList - -inherits(TokenList, Component) -function TokenList () { - this.state = { - tokens: [], - isLoading: true, - network: null, - } - Component.call(this) -} - -TokenList.prototype.render = function () { - const state = this.state - const { tokens, isLoading, error } = state - const { userAddress, network } = this.props - - if (isLoading) { - return this.message('Loading') - } - - if (error) { - log.error(error) - return this.message('There was a problem loading your token balances.') - } - - const tokenViews = tokens.map((tokenData) => { - tokenData.network = network - tokenData.userAddress = userAddress - return h(TokenCell, tokenData) - }) - - return h('div', [ - h('ol', { - style: { - height: '260px', - overflowY: 'auto', - display: 'flex', - flexDirection: 'column', - }, - }, [ - h('style', ` - - li.token-cell { - display: flex; - flex-direction: row; - align-items: center; - padding: 10px; - } - - li.token-cell > h3 { - margin-left: 12px; - } - - li.token-cell:hover { - background: white; - cursor: pointer; - } - - `), - ...tokenViews, - tokenViews.length ? null : this.message('No Tokens Found.'), - ]), - this.addTokenButtonElement(), - ]) -} - -TokenList.prototype.addTokenButtonElement = function () { - return h('div', [ - h('div.footer.hover-white.pointer', { - key: 'reveal-account-bar', - onClick: () => { - this.props.addToken() - }, - style: { - display: 'flex', - height: '40px', - padding: '10px', - justifyContent: 'center', - alignItems: 'center', - }, - }, [ - h('i.fa.fa-plus.fa-lg'), - ]), - ]) -} - -TokenList.prototype.message = function (body) { - return h('div', { - style: { - display: 'flex', - height: '250px', - alignItems: 'center', - justifyContent: 'center', - padding: '30px', - }, - }, body) -} - -TokenList.prototype.componentDidMount = function () { - this.createFreshTokenTracker() -} - -TokenList.prototype.createFreshTokenTracker = function () { - if (this.tracker) { - // Clean up old trackers when refreshing: - this.tracker.stop() - this.tracker.removeListener('update', this.balanceUpdater) - this.tracker.removeListener('error', this.showError) - } - - if (!global.ethereumProvider) return - const { userAddress } = this.props - this.tracker = new TokenTracker({ - userAddress, - provider: global.ethereumProvider, - tokens: uniqueMergeTokens(defaultTokens, this.props.tokens), - pollingInterval: 8000, - }) - - - // Set up listener instances for cleaning up - this.balanceUpdater = this.updateBalances.bind(this) - this.showError = (error) => { - this.setState({ error, isLoading: false }) - } - this.tracker.on('update', this.balanceUpdater) - this.tracker.on('error', this.showError) - - this.tracker.updateBalances() - .then(() => { - this.updateBalances(this.tracker.serialize()) - }) - .catch((reason) => { - log.error(`Problem updating balances`, reason) - this.setState({ isLoading: false }) - }) -} - -TokenList.prototype.componentWillUpdate = function (nextProps) { - if (nextProps.network === 'loading') return - const oldNet = this.props.network - const newNet = nextProps.network - - if (oldNet && newNet && newNet !== oldNet) { - this.setState({ isLoading: true }) - this.createFreshTokenTracker() - } -} - -TokenList.prototype.updateBalances = function (tokens) { - const heldTokens = tokens.filter(token => { - return token.balance !== '0' && token.string !== '0.000' - }) - this.setState({ tokens: heldTokens, isLoading: false }) -} - -TokenList.prototype.componentWillUnmount = function () { - if (!this.tracker) return - this.tracker.stop() -} - -function uniqueMergeTokens (tokensA, tokensB) { - const uniqueAddresses = [] - const result = [] - tokensA.concat(tokensB).forEach((token) => { - const normal = normalizeAddress(token.address) - if (!uniqueAddresses.includes(normal)) { - uniqueAddresses.push(normal) - result.push(token) - } - }) - return result -} - diff --git a/ui/classic/app/components/tooltip.js b/ui/classic/app/components/tooltip.js deleted file mode 100644 index edbc074bb..000000000 --- a/ui/classic/app/components/tooltip.js +++ /dev/null @@ -1,22 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ReactTooltip = require('react-tooltip-component') - -module.exports = Tooltip - -inherits(Tooltip, Component) -function Tooltip () { - Component.call(this) -} - -Tooltip.prototype.render = function () { - const props = this.props - const { position, title, children } = props - - return h(ReactTooltip, { - position: position || 'left', - title, - fixed: false, - }, children) -} diff --git a/ui/classic/app/components/transaction-list-item-icon.js b/ui/classic/app/components/transaction-list-item-icon.js deleted file mode 100644 index 431054340..000000000 --- a/ui/classic/app/components/transaction-list-item-icon.js +++ /dev/null @@ -1,68 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const Tooltip = require('./tooltip') - -const Identicon = require('./identicon') - -module.exports = TransactionIcon - -inherits(TransactionIcon, Component) -function TransactionIcon () { - Component.call(this) -} - -TransactionIcon.prototype.render = function () { - const { transaction, txParams, isMsg } = this.props - switch (transaction.status) { - case 'unapproved': - return h(!isMsg ? '.unapproved-tx-icon' : 'i.fa.fa-certificate.fa-lg') - - case 'rejected': - return h('i.fa.fa-exclamation-triangle.fa-lg.warning', { - style: { - width: '24px', - }, - }) - - case 'failed': - return h('i.fa.fa-exclamation-triangle.fa-lg.error', { - style: { - width: '24px', - }, - }) - - case 'submitted': - return h(Tooltip, { - title: 'Pending', - position: 'bottom', - }, [ - h('i.fa.fa-ellipsis-h', { - style: { - fontSize: '27px', - }, - }), - ]) - } - - if (isMsg) { - return h('i.fa.fa-certificate.fa-lg', { - style: { - width: '24px', - }, - }) - } - - if (txParams.to) { - return h(Identicon, { - diameter: 24, - address: txParams.to || transaction.hash, - }) - } else { - return h('i.fa.fa-file-text-o.fa-lg', { - style: { - width: '24px', - }, - }) - } -} diff --git a/ui/classic/app/components/transaction-list-item.js b/ui/classic/app/components/transaction-list-item.js deleted file mode 100644 index dbda66a31..000000000 --- a/ui/classic/app/components/transaction-list-item.js +++ /dev/null @@ -1,165 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const EthBalance = require('./eth-balance') -const addressSummary = require('../util').addressSummary -const explorerLink = require('../../lib/explorer-link') -const CopyButton = require('./copyButton') -const vreme = new (require('vreme')) -const Tooltip = require('./tooltip') -const numberToBN = require('number-to-bn') - -const TransactionIcon = require('./transaction-list-item-icon') -const ShiftListItem = require('./shift-list-item') -module.exports = TransactionListItem - -inherits(TransactionListItem, Component) -function TransactionListItem () { - Component.call(this) -} - -TransactionListItem.prototype.render = function () { - const { transaction, network, conversionRate, currentCurrency } = this.props - if (transaction.key === 'shapeshift') { - if (network === '1') return h(ShiftListItem, transaction) - } - var date = formatDate(transaction.time) - - let isLinkable = false - const numericNet = parseInt(network) - isLinkable = numericNet === 1 || numericNet === 3 || numericNet === 4 || numericNet === 42 - - var isMsg = ('msgParams' in transaction) - var isTx = ('txParams' in transaction) - var isPending = transaction.status === 'unapproved' - let txParams - if (isTx) { - txParams = transaction.txParams - } else if (isMsg) { - txParams = transaction.msgParams - } - - const nonce = txParams.nonce ? numberToBN(txParams.nonce).toString(10) : '' - - const isClickable = ('hash' in transaction && isLinkable) || isPending - return ( - h(`.transaction-list-item.flex-row.flex-space-between${isClickable ? '.pointer' : ''}`, { - onClick: (event) => { - if (isPending) { - this.props.showTx(transaction.id) - } - event.stopPropagation() - if (!transaction.hash || !isLinkable) return - var url = explorerLink(transaction.hash, parseInt(network)) - global.platform.openWindow({ url }) - }, - style: { - padding: '20px 0', - }, - }, [ - - h('.identicon-wrapper.flex-column.flex-center.select-none', [ - h('.pop-hover', { - onClick: (event) => { - event.stopPropagation() - if (!isTx || isPending) return - var url = `https://metamask.github.io/eth-tx-viz/?tx=${transaction.hash}` - global.platform.openWindow({ url }) - }, - }, [ - h(TransactionIcon, { txParams, transaction, isTx, isMsg }), - ]), - ]), - - h(Tooltip, { - title: 'Transaction Number', - position: 'bottom', - }, [ - h('span', { - style: { - display: 'flex', - cursor: 'normal', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - padding: '10px', - }, - }, nonce), - ]), - - h('.flex-column', {style: {width: '200px', overflow: 'hidden'}}, [ - domainField(txParams), - h('div', date), - recipientField(txParams, transaction, isTx, isMsg), - ]), - - // Places a copy button if tx is successful, else places a placeholder empty div. - transaction.hash ? h(CopyButton, { value: transaction.hash }) : h('div', {style: { display: 'flex', alignItems: 'center', width: '26px' }}), - - isTx ? h(EthBalance, { - value: txParams.value, - conversionRate, - currentCurrency, - width: '55px', - shorten: true, - showFiat: false, - style: {fontSize: '15px'}, - }) : h('.flex-column'), - ]) - ) -} - -function domainField (txParams) { - return h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - overflow: 'hidden', - textOverflow: 'ellipsis', - width: '100%', - }, - }, [ - txParams.origin, - ]) -} - -function recipientField (txParams, transaction, isTx, isMsg) { - let message - - if (isMsg) { - message = 'Signature Requested' - } else if (txParams.to) { - message = addressSummary(txParams.to) - } else { - message = 'Contract Published' - } - - return h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - }, - }, [ - message, - failIfFailed(transaction), - ]) -} - -function formatDate (date) { - return vreme.format(new Date(date), 'March 16 2014 14:30') -} - -function failIfFailed (transaction) { - if (transaction.status === 'rejected') { - return h('span.error', ' (Rejected)') - } - if (transaction.err) { - return h(Tooltip, { - title: transaction.err.message, - position: 'bottom', - }, [ - h('span.error', ' (Failed)'), - ]) - } -} diff --git a/ui/classic/app/components/transaction-list.js b/ui/classic/app/components/transaction-list.js deleted file mode 100644 index 3b4ba741e..000000000 --- a/ui/classic/app/components/transaction-list.js +++ /dev/null @@ -1,79 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const TransactionListItem = require('./transaction-list-item') - -module.exports = TransactionList - - -inherits(TransactionList, Component) -function TransactionList () { - Component.call(this) -} - -TransactionList.prototype.render = function () { - const { transactions, network, unapprovedMsgs, conversionRate } = this.props - - var shapeShiftTxList - if (network === '1') { - shapeShiftTxList = this.props.shapeShiftTxList - } - const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList) - .sort((a, b) => b.time - a.time) - - return ( - - h('section.transaction-list', [ - - h('style', ` - .transaction-list .transaction-list-item:not(:last-of-type) { - border-bottom: 1px solid #D4D4D4; - } - .transaction-list .transaction-list-item .ether-balance-label { - display: block !important; - font-size: small; - } - `), - - h('.tx-list', { - style: { - overflowY: 'auto', - height: '300px', - padding: '0 20px', - textAlign: 'center', - }, - }, [ - - txsToRender.length - ? txsToRender.map((transaction, i) => { - let key - switch (transaction.key) { - case 'shapeshift': - const { depositAddress, time } = transaction - key = `shift-tx-${depositAddress}-${time}-${i}` - break - default: - key = `tx-${transaction.id}-${i}` - } - return h(TransactionListItem, { - transaction, i, network, key, - conversionRate, - showTx: (txId) => { - this.props.viewPendingTx(txId) - }, - }) - }) - : h('.flex-center', { - style: { - flexDirection: 'column', - height: '100%', - }, - }, [ - 'No transaction history.', - ]), - ]), - ]) - ) -} - diff --git a/ui/classic/app/conf-tx.js b/ui/classic/app/conf-tx.js deleted file mode 100644 index 63b77ef7f..000000000 --- a/ui/classic/app/conf-tx.js +++ /dev/null @@ -1,213 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') -const NetworkIndicator = require('./components/network') -const txHelper = require('../lib/tx-helper') -const isPopupOrNotification = require('../../../app/scripts/lib/is-popup-or-notification') - -const PendingTx = require('./components/pending-tx') -const PendingMsg = require('./components/pending-msg') -const PendingPersonalMsg = require('./components/pending-personal-msg') -const Loading = require('./components/loading') - -module.exports = connect(mapStateToProps)(ConfirmTxScreen) - -function mapStateToProps (state) { - return { - identities: state.metamask.identities, - accounts: state.metamask.accounts, - selectedAddress: state.metamask.selectedAddress, - unapprovedTxs: state.metamask.unapprovedTxs, - unapprovedMsgs: state.metamask.unapprovedMsgs, - unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs, - index: state.appState.currentView.context, - warning: state.appState.warning, - network: state.metamask.network, - provider: state.metamask.provider, - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - blockGasLimit: state.metamask.currentBlockGasLimit, - } -} - -inherits(ConfirmTxScreen, Component) -function ConfirmTxScreen () { - Component.call(this) -} - -ConfirmTxScreen.prototype.render = function () { - const props = this.props - const { network, provider, unapprovedTxs, currentCurrency, - unapprovedMsgs, unapprovedPersonalMsgs, conversionRate, blockGasLimit } = props - - var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) - - var txData = unconfTxList[props.index] || {} - var txParams = txData.params || {} - var isNotification = isPopupOrNotification() === 'notification' - - - log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`) - if (unconfTxList.length === 0) return h(Loading, { isLoading: true }) - - return ( - - h('.flex-column.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - !isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: this.goHome.bind(this), - }) : null, - h('h2.page-subtitle', 'Confirm Transaction'), - isNotification ? h(NetworkIndicator, { - network: network, - provider: provider, - }) : null, - ]), - - h('h3', { - style: { - alignSelf: 'center', - display: unconfTxList.length > 1 ? 'block' : 'none', - }, - }, [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - style: { - display: props.index === 0 ? 'none' : 'inline-block', - }, - onClick: () => props.dispatch(actions.previousTx()), - }), - ` ${props.index + 1} of ${unconfTxList.length} `, - h('i.fa.fa-arrow-right.fa-lg.cursor-pointer', { - style: { - display: props.index + 1 === unconfTxList.length ? 'none' : 'inline-block', - }, - onClick: () => props.dispatch(actions.nextTx()), - }), - ]), - - warningIfExists(props.warning), - - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - - currentTxView({ - // Properties - txData: txData, - key: txData.id, - selectedAddress: props.selectedAddress, - accounts: props.accounts, - identities: props.identities, - conversionRate, - currentCurrency, - blockGasLimit, - // Actions - buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), - sendTransaction: this.sendTransaction.bind(this), - cancelTransaction: this.cancelTransaction.bind(this, txData), - signMessage: this.signMessage.bind(this, txData), - signPersonalMessage: this.signPersonalMessage.bind(this, txData), - cancelMessage: this.cancelMessage.bind(this, txData), - cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), - }), - - ]), - ]) - ) -} - -function currentTxView (opts) { - log.info('rendering current tx view') - const { txData } = opts - const { txParams, msgParams, type } = txData - - if (txParams) { - log.debug('txParams detected, rendering pending tx') - return h(PendingTx, opts) - } else if (msgParams) { - log.debug('msgParams detected, rendering pending msg') - - if (type === 'eth_sign') { - log.debug('rendering eth_sign message') - return h(PendingMsg, opts) - } else if (type === 'personal_sign') { - log.debug('rendering personal_sign message') - return h(PendingPersonalMsg, opts) - } - } -} - -ConfirmTxScreen.prototype.buyEth = function (address, event) { - event.preventDefault() - this.props.dispatch(actions.buyEthView(address)) -} - -ConfirmTxScreen.prototype.sendTransaction = function (txData, event) { - this.stopPropagation(event) - this.props.dispatch(actions.updateAndApproveTx(txData)) -} - -ConfirmTxScreen.prototype.cancelTransaction = function (txData, event) { - this.stopPropagation(event) - event.preventDefault() - this.props.dispatch(actions.cancelTx(txData)) -} - -ConfirmTxScreen.prototype.signMessage = function (msgData, event) { - log.info('conf-tx.js: signing message') - var params = msgData.msgParams - params.metamaskId = msgData.id - this.stopPropagation(event) - this.props.dispatch(actions.signMsg(params)) -} - -ConfirmTxScreen.prototype.stopPropagation = function (event) { - if (event.stopPropagation) { - event.stopPropagation() - } -} - -ConfirmTxScreen.prototype.signPersonalMessage = function (msgData, event) { - log.info('conf-tx.js: signing personal message') - var params = msgData.msgParams - params.metamaskId = msgData.id - this.stopPropagation(event) - this.props.dispatch(actions.signPersonalMsg(params)) -} - -ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) { - log.info('canceling message') - this.stopPropagation(event) - this.props.dispatch(actions.cancelMsg(msgData)) -} - -ConfirmTxScreen.prototype.cancelPersonalMessage = function (msgData, event) { - log.info('canceling personal message') - this.stopPropagation(event) - this.props.dispatch(actions.cancelPersonalMsg(msgData)) -} - -ConfirmTxScreen.prototype.goHome = function (event) { - this.stopPropagation(event) - this.props.dispatch(actions.goHome()) -} - -function warningIfExists (warning) { - if (warning && - // Do not display user rejections on this screen: - warning.indexOf('User denied transaction signature') === -1) { - return h('.error', { - style: { - margin: 'auto', - }, - }, warning) - } -} diff --git a/ui/classic/app/config.js b/ui/classic/app/config.js deleted file mode 100644 index 62785c49b..000000000 --- a/ui/classic/app/config.js +++ /dev/null @@ -1,211 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') -const currencies = require('./conversion.json').rows -const validUrl = require('valid-url') -const copyToClipboard = require('copy-to-clipboard') - -module.exports = connect(mapStateToProps)(ConfigScreen) - -function mapStateToProps (state) { - return { - metamask: state.metamask, - warning: state.appState.warning, - } -} - -inherits(ConfigScreen, Component) -function ConfigScreen () { - Component.call(this) -} - -ConfigScreen.prototype.render = function () { - var state = this.props - var metamaskState = state.metamask - var warning = state.warning - - return ( - h('.flex-column.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: (event) => { - state.dispatch(actions.goHome()) - }, - }), - h('h2.page-subtitle', 'Settings'), - ]), - - h('.error', { - style: { - display: warning ? 'block' : 'none', - padding: '0 20px', - textAlign: 'center', - }, - }, warning), - - // conf view - h('.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-space-around', { - style: { - padding: '20px', - }, - }, [ - - currentProviderDisplay(metamaskState), - - h('div', { style: {display: 'flex'} }, [ - h('input#new_rpc', { - placeholder: 'New RPC URL', - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - onKeyPress (event) { - if (event.key === 'Enter') { - var element = event.target - var newRpc = element.value - rpcValidation(newRpc, state) - } - }, - }), - h('button', { - style: { - alignSelf: 'center', - }, - onClick (event) { - event.preventDefault() - var element = document.querySelector('input#new_rpc') - var newRpc = element.value - rpcValidation(newRpc, state) - }, - }, 'Save'), - ]), - - h('hr.horizontal-line'), - - currentConversionInformation(metamaskState, state), - - h('hr.horizontal-line'), - - h('div', { - style: { - marginTop: '20px', - }, - }, [ - h('p', { - style: { - fontFamily: 'Montserrat Light', - fontSize: '13px', - }, - }, `State logs contain your public account addresses and sent transactions.`), - h('br'), - h('button', { - style: { - alignSelf: 'center', - }, - onClick (event) { - copyToClipboard(window.logState()) - }, - }, 'Copy State Logs'), - ]), - - h('hr.horizontal-line'), - - h('div', { - style: { - marginTop: '20px', - }, - }, [ - h('button', { - style: { - alignSelf: 'center', - }, - onClick (event) { - event.preventDefault() - state.dispatch(actions.revealSeedConfirmation()) - }, - }, 'Reveal Seed Words'), - ]), - - ]), - ]), - ]) - ) -} - -function rpcValidation (newRpc, state) { - if (validUrl.isWebUri(newRpc)) { - state.dispatch(actions.setRpcTarget(newRpc)) - } else { - var appendedRpc = `http://${newRpc}` - if (validUrl.isWebUri(appendedRpc)) { - state.dispatch(actions.displayWarning('URIs require the appropriate HTTP/HTTPS prefix.')) - } else { - state.dispatch(actions.displayWarning('Invalid RPC URI')) - } - } -} - -function currentConversionInformation (metamaskState, state) { - var currentCurrency = metamaskState.currentCurrency - var conversionDate = metamaskState.conversionDate - return h('div', [ - h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, 'Current Conversion'), - h('span', {style: { fontWeight: 'bold', paddingRight: '10px', fontSize: '13px'}}, `Updated ${Date(conversionDate)}`), - h('select#currentCurrency', { - onChange (event) { - event.preventDefault() - var element = document.getElementById('currentCurrency') - var newCurrency = element.value - state.dispatch(actions.setCurrentCurrency(newCurrency)) - }, - defaultValue: currentCurrency, - }, currencies.map((currency) => { - return h('option', {key: currency.code, value: currency.code}, `${currency.code} - ${currency.name}`) - }) - ), - ]) -} - -function currentProviderDisplay (metamaskState) { - var provider = metamaskState.provider - var title, value - - switch (provider.type) { - - case 'mainnet': - title = 'Current Network' - value = 'Main Ethereum Network' - break - - case 'ropsten': - title = 'Current Network' - value = 'Ropsten Test Network' - break - - case 'kovan': - title = 'Current Network' - value = 'Kovan Test Network' - break - - case 'rinkeby': - title = 'Current Network' - value = 'Rinkeby Test Network' - break - - default: - title = 'Current RPC' - value = metamaskState.provider.rpcTarget - } - - return h('div', [ - h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, title), - h('span', value), - ]) -} diff --git a/ui/classic/app/conversion.json b/ui/classic/app/conversion.json deleted file mode 100644 index 155ffc4fc..000000000 --- a/ui/classic/app/conversion.json +++ /dev/null @@ -1,207 +0,0 @@ -{ - "rows": [ - { - "code": "REP", - "name": "Augur", - "statuses": [ - "primary" - ] - }, - { - "code": "BCN", - "name": "Bytecoin", - "statuses": [ - "primary" - ] - }, - { - "code": "BTC", - "name": "Bitcoin", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "BTS", - "name": "BitShares", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "BLK", - "name": "Blackcoin", - "statuses": [ - "primary" - ] - }, - { - "code": "GBP", - "name": "British Pound Sterling", - "statuses": [ - "secondary" - ] - }, - { - "code": "CAD", - "name": "Canadian Dollar", - "statuses": [ - "secondary" - ] - }, - { - "code": "CNY", - "name": "Chinese Yuan", - "statuses": [ - "secondary" - ] - }, - { - "code": "DSH", - "name": "Dashcoin", - "statuses": [ - "primary" - ] - }, - { - "code": "DOGE", - "name": "Dogecoin", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "ETC", - "name": "Ethereum Classic", - "statuses": [ - "primary" - ] - }, - { - "code": "EUR", - "name": "Euro", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "GNO", - "name": "GNO", - "statuses": [ - "primary" - ] - }, - { - "code": "GNT", - "name": "GNT", - "statuses": [ - "primary" - ] - }, - { - "code": "JPY", - "name": "Japanese Yen", - "statuses": [ - "secondary" - ] - }, - { - "code": "LTC", - "name": "Litecoin", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "MAID", - "name": "MaidSafeCoin", - "statuses": [ - "primary" - ] - }, - { - "code": "XEM", - "name": "NEM", - "statuses": [ - "primary" - ] - }, - { - "code": "XLM", - "name": "Stellar", - "statuses": [ - "primary" - ] - }, - { - "code": "XMR", - "name": "Monero", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "XRP", - "name": "Ripple", - "statuses": [ - "primary" - ] - }, - { - "code": "RUR", - "name": "Ruble", - "statuses": [ - "secondary" - ] - }, - { - "code": "STEEM", - "name": "Steem", - "statuses": [ - "primary" - ] - }, - { - "code": "STRAT", - "name": "STRAT", - "statuses": [ - "primary" - ] - }, - { - "code": "UAH", - "name": "Ukrainian Hryvnia", - "statuses": [ - "secondary" - ] - }, - { - "code": "USD", - "name": "US Dollar", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "WAVES", - "name": "WAVES", - "statuses": [ - "primary" - ] - }, - { - "code": "ZEC", - "name": "Zcash", - "statuses": [ - "primary" - ] - } - ] -} diff --git a/ui/classic/app/css/debug.css b/ui/classic/app/css/debug.css deleted file mode 100644 index 3e125bcd4..000000000 --- a/ui/classic/app/css/debug.css +++ /dev/null @@ -1,21 +0,0 @@ -/* -debug / dev -*/ - -#app-content { - border: 2px solid green; -} - -#design-container { - position: absolute; - left: 360px; - top: -42px; - width: calc(100vw - 360px); - height: 100vh; - overflow: scroll; -} - -#design-container img { - width: 2000px; - margin-right: 600px; -} \ No newline at end of file diff --git a/ui/classic/app/css/fonts.css b/ui/classic/app/css/fonts.css deleted file mode 100644 index 3b9f581b9..000000000 --- a/ui/classic/app/css/fonts.css +++ /dev/null @@ -1,36 +0,0 @@ -@import url(https://fonts.googleapis.com/css?family=Roboto:300,500); -@import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css); - -@font-face { - font-family: 'Montserrat Regular'; - src: url('/fonts/Montserrat/Montserrat-Regular.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-Regular.ttf') format('truetype'); - font-weight: normal; - font-style: normal; - font-size: 'small'; - -} - -@font-face { - font-family: 'Montserrat Bold'; - src: url('/fonts/Montserrat/Montserrat-Bold.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-Bold.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'Montserrat Light'; - src: url('/fonts/Montserrat/Montserrat-Light.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-Light.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'Montserrat UltraLight'; - src: url('/fonts/Montserrat/Montserrat-UltraLight.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-UltraLight.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} diff --git a/ui/classic/app/css/index.css b/ui/classic/app/css/index.css deleted file mode 100644 index 808aafb4c..000000000 --- a/ui/classic/app/css/index.css +++ /dev/null @@ -1,667 +0,0 @@ -/* -faint orange (textfield shades) #FAF6F0 -light orange (button shades): #F5C26D -dark orange (text): #F5A623 -borders/font/any gray: #4A4A4A -*/ - -/* -application specific styles -*/ - -* { - box-sizing: border-box; -} - -html, body { - font-family: 'Montserrat Regular', Arial; - color: #4D4D4D; - font-weight: 300; - line-height: 1.4em; - background: #F7F7F7; -} - -input:focus, textarea:focus { - outline: none; -} - -#app-content { - overflow-x: hidden; - min-width: 357px; - width: 360px; - height: 500px; -} - -button, input[type="submit"] { - font-family: 'Montserrat Bold'; - outline: none; - cursor: pointer; - padding: 8px 12px; - border: none; - color: white; - transform-origin: center center; - transition: transform 50ms ease-in; - /* default orange */ - background: rgba(247, 134, 28, 1); - box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); -} - -.btn-green, input[type="submit"].btn-green { - background: rgba(106, 195, 96, 1); - box-shadow: 0px 3px 6px rgba(106, 195, 96, 0.36); -} - -.btn-red { - background: rgba(254, 35, 17, 1); - box-shadow: 0px 3px 6px rgba(254, 35, 17, 0.36); -} - -button[disabled], input[type="submit"][disabled] { - cursor: not-allowed; - background: rgba(197, 197, 197, 1); - box-shadow: 0px 3px 6px rgba(197, 197, 197, 0.36); -} - -button.spaced { - margin: 2px; -} - -button:not([disabled]):hover, input[type="submit"]:not([disabled]):hover { - transform: scale(1.1); -} -button:not([disabled]):active, input[type="submit"]:not([disabled]):active { - transform: scale(0.95); -} - -a { - text-decoration: none; - color: inherit; -} - -a:hover{ - color: #df6b0e; -} - -/* -app -*/ - -.active { - color: #909090; -} - -button.primary { - padding: 8px 12px; - background: #F7861C; - box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); - color: white; - font-size: 1.1em; - font-family: 'Montserrat Regular'; - text-transform: uppercase; -} - -button.btn-thin { - border: 1px solid; - border-color: #4D4D4D; - color: #4D4D4D; - background: rgb(255, 174, 41); - border-radius: 4px; - min-width: 200px; - margin: 12px 0; - padding: 6px; - font-size: 13px; -} - -.app-header { - padding: 6px 8px; -} - -.app-header h1 { - font-family: 'Montserrat Regular'; - text-transform: uppercase; - color: #AEAEAE; -} - -h2.page-subtitle { - font-family: 'Montserrat Regular'; - text-transform: uppercase; - color: #AEAEAE; - font-size: 1em; - margin: 12px; -} - -.app-primary { - -} - -.app-footer { - padding-bottom: 10px; - align-items: center; -} - -.identicon { - height: 46px; - width: 46px; - background-size: cover; - border-radius: 100%; - border: 3px solid gray; -} - -textarea.twelve-word-phrase { - padding: 12px; - width: 300px; - height: 140px; - font-size: 16px; - background: white; - resize: none; -} - -.network-indicator { - display: flex; - align-items: center; - font-size: 0.6em; - -} - -.network-name { - width: 5.2em; - line-height: 9px; - text-rendering: geometricPrecision; -} - -.check { - margin-left: 7px; - color: #F7861C; - flex: 1 0 auto; - display: flex; - justify-content: flex-end; -} -/* -app sections -*/ - -/* initialize */ - -.initialize-screen hr { - width: 60px; - margin: 12px; - border-color: #F7861C; - border-style: solid; -} - -.initialize-screen label { - margin-top: 20px; -} - -.initialize-screen button.create-vault { - margin-top: 40px; -} - -.initialize-screen .warning { - font-size: 14px; - margin: 0 16px; -} - -/* unlock */ -.error { - color: #E20202; -} - -.warning { - color: #FFAE00; -} - -.lock { - width: 50px; - height: 50px; -} - -.lock.locked { - transform: scale(1.5); - opacity: 0.0; - transition: opacity 400ms ease-in, transform 400ms ease-in; -} -.lock.unlocked { - transform: scale(1); - opacity: 1; - transition: opacity 500ms ease-out, transform 500ms ease-out, background 200ms ease-in; -} - -.lock.locked .lock-top { - transform: scaleX(1) translateX(0); - transition: transform 250ms ease-in; -} -.lock.unlocked .lock-top { - transform: scaleX(-1) translateX(-12px); - transition: transform 250ms ease-in; -} -.lock.unlocked:hover { - border-radius: 4px; - background: #e5e5e5; - border: 1px solid #b1b1b1; -} -.lock.unlocked:active { - background: #c3c3c3; -} - -.section-title .fa-arrow-left { - margin: -2px 8px 0px -8px; -} - -.unlock-screen #metamask-mascot-container { - margin-top: 24px; -} - -.unlock-screen h1 { - margin-top: -28px; - margin-bottom: 42px; -} - -.unlock-screen input[type=password] { - width: 260px; - /*height: 36px; - margin-bottom: 24px; - padding: 8px;*/ -} - -.sizing-input{ - font-size: 14px; - height: 30px; - padding-left: 5px; -} -.editable-label{ - display: flex; -} -/* Webkit */ -.unlock-screen input::-webkit-input-placeholder { - text-align: center; - font-size: 1.2em; -} -/* Firefox 18- */ -.unlock-screen input:-moz-placeholder { - text-align: center; - font-size: 1.2em; -} -/* Firefox 19+ */ -.unlock-screen input::-moz-placeholder { - text-align: center; - font-size: 1.2em; -} -/* IE */ -.unlock-screen input:-ms-input-placeholder { - text-align: center; - font-size: 1.2em; -} - -input.large-input, textarea.large-input { - /*margin-bottom: 24px;*/ - padding: 8px; -} - -input.large-input { - height: 36px; -} - -.letter-spacey { - letter-spacing: 0.1em; -} - - - -/* accounts */ - -.accounts-section { - margin: 0 0px; -} - -.accounts-section .horizontal-line { - margin: 0px 18px; -} - -.accounts-list-option { - height: 120px; -} - -.accounts-list-option .identicon-wrapper { - width: 100px; -} - -.unconftx-link { - margin-top: 24px; - cursor: pointer; -} - -.unconftx-link .fa-arrow-right { - margin: 0px -8px 0px 8px; -} - -/* identity panel */ - -.identity-panel { - font-weight: 500; -} - -.identity-panel .identicon-wrapper { - margin: 4px; - margin-top: 8px; - display: flex; - align-items: center; -} - -.identity-panel .identicon-wrapper span { - margin: 0 auto; -} - -.identity-panel .identity-data { - margin: 8px 8px 8px 18px; -} - -.identity-panel i { - margin-top: 32px; - margin-right: 6px; - color: #B9B9B9; -} - -.identity-panel .arrow-right { - padding-left: 18px; - width: 42px; - min-width: 18px; - height: 100%; -} - -.identity-copy.flex-column { - flex: 0.25 0 auto; - justify-content: center; -} - -/* accounts screen */ - -.identity-section { - -} - -.identity-section .identity-panel { - background: #E9E9E9; - border-bottom: 1px solid #B1B1B1; - cursor: pointer; -} - -.identity-section .identity-panel.selected { - background: white; - color: #F3C83E; -} - -.identity-section .identity-panel.selected .identicon { - border-color: orange; -} - -.identity-section .accounts-list-option:hover, -.identity-section .accounts-list-option.selected { - background:white; -} - -/* account detail screen */ - -.account-detail-section { - -} -.name-label{ - -} - -.unapproved-tx-icon { - height: 16px; - width: 16px; - background: rgb(47, 174, 244); - border-color: #AEAEAE; - border-radius: 13px; -} - -.edit-text { - height: 100%; - visibility: hidden; -} -.editing-label { - display: flex; - justify-content: flex-start; - margin-left: 50px; - margin-bottom: 2px; - font-size: 11px; - text-rendering: geometricPrecision; - color: #F7861C; -} -.name-label:hover .edit-text { - visibility: visible; -} -/* tx confirm */ - -.unconftx-section input[type=password] { - height: 22px; - padding: 2px; - margin: 12px; - margin-bottom: 24px; - border-radius: 4px; - border: 2px solid #F3C83E; - background: #FAF6F0; -} - -/* Send Screen */ - -.send-screen { - -} - -.send-screen section { - margin: 8px 16px; -} - -.send-screen input { - width: 100%; - font-size: 12px; -} - -/* Ether Balance Widget */ - -.ether-balance-amount { - color: #F7861C; -} - -.ether-balance-label { - color: #ABA9AA; -} - -/* Info screen */ -.info-gray{ - font-family: 'Montserrat Regular'; - text-transform: uppercase; - color: #AEAEAE; -} - -.icon-size{ - width: 20px; -} - -.info{ - font-family: 'Montserrat Regular', Arial; - padding-bottom: 10px; - display: inline-block; - padding-left: 5px; -} - -/* buy eth warning screen */ -.custom-radios { - justify-content: space-around; - align-items: center; -} - - -.custom-radio-selected { - width: 17px; - height: 17px; - border: solid; - border-style: double; - border-radius: 15px; - border-width: 5px; - background: rgba(247, 134, 28, 1); - border-color: #F7F7F7; -} - -.custom-radio-inactive { - width: 14px; - height: 14px; - border: solid; - border-width: 1px; - border-radius: 24px; - border-color: #AEAEAE; -} - -.radio-titles { - color: rgba(247, 134, 28, 1); -} - -.radio-titles-subtext { - -} - -.selected-exchange { - -} - -.buy-radio { - -} - -.eth-warning{ - transition: opacity 400ms ease-in, transform 400ms ease-in; -} - -.buy-subview{ - transition: opacity 400ms ease-in, transform 400ms ease-in; -} - -.input-container:hover .edit-text{ - visibility: visible; -} - -.buy-inputs{ - font-family: 'Montserrat Light'; - font-size: 13px; - height: 20px; - background: transparent; - box-sizing: border-box; - border: solid; - border-color: transparent; - border-width: 0.5px; - border-radius: 2px; - -} -.input-container:hover .buy-inputs{ - box-sizing: inherit; - border: solid; - border-color: #F7861C; - border-width: 0.5px; - border-radius: 2px; -} - -.buy-inputs:focus{ - border: solid; - border-color: #F7861C; - border-width: 0.5px; - border-radius: 2px; -} - -.activeForm { - background: #F7F7F7; - border: none; - border-radius: 8px 8px 0px 0px; - width: 50%; - text-align: center; - padding-bottom: 4px; - -} - -.inactiveForm { - border: none; - border-radius: 8px 8px 0px 0px; - width: 50%; - text-align: center; - padding-bottom: 4px; -} - -.ex-coins { - font-family: 'Montserrat Regular'; - text-transform: uppercase; - text-align: center; - font-size: 33px; - width: 118px; - height: 42px; - padding: 1px; - color: #4D4D4D; -} - -.marketinfo{ - font-family: 'Montserrat light'; - color: #AEAEAE; - font-size: 15px; - line-height: 17px; -} - -#fromCoin::-webkit-calendar-picker-indicator { - display: none; -} - -#coinList { - width: 400px; - height: 500px; - overflow: scroll; -} - -.icon-control .fa-refresh{ - visibility: hidden; -} - -.icon-control:hover .fa-refresh{ - visibility: visible; -} - -.icon-control:hover .fa-chevron-right{ - visibility: hidden; -} - -.inactive { - color: #AEAEAE; -} - -.inactive button{ - background: #AEAEAE; - color: white; -} - -.ellip-address { - overflow: hidden; - text-overflow: ellipsis; - width: 5em; - font-size: 14px; - font-family: "Montserrat Light"; - margin-left: 5px; -} - -.qr-header { - font-size: 25px; - margin-top: 40px; -} - -.qr-message { - font-size: 12px; - color: #F7861C; -} - -div.message-container > div:first-child { - margin-top: 18px; - font-size: 15px; - color: #4D4D4D; -} - -.pop-hover:hover { - transform: scale(1.1); -} diff --git a/ui/classic/app/css/lib.css b/ui/classic/app/css/lib.css deleted file mode 100644 index 910a24ee2..000000000 --- a/ui/classic/app/css/lib.css +++ /dev/null @@ -1,268 +0,0 @@ -/* color */ - -.color-orange { - color: #F7861C; -} - -.color-forest { - color: #0A5448; -} - -/* lib */ - -.full-width { - width: 100%; -} - -.full-height { - height: 100%; -} - -.flex-column { - display: flex; - flex-direction: column; -} - -.space-between { - justify-content: space-between; -} - -.space-around { - justify-content: space-around; -} - -.flex-column-bottom { - display: flex; - flex-direction: column-reverse; -} - -.flex-row { - display: flex; - flex-direction: row; -} - -.flex-space-between { - justify-content: space-between; -} - -.flex-space-around { - justify-content: space-around; -} - -.flex-right { - display: flex; - flex-direction: row; - justify-content: flex-end; -} - -.flex-left { - display: flex; - flex-direction: row; - justify-content: flex-start; -} - -.flex-fixed { - flex: none; -} - -.flex-basis-auto { - flex-basis: auto; -} - -.flex-grow { - flex: 1 1 auto; -} - -.flex-wrap { - flex-wrap: wrap; -} - -.flex-center { - display: flex; - justify-content: center; - align-items: center; -} - -.flex-justify-center { - justify-content: center; -} - -.flex-align-center { - align-items: center; -} - -.flex-self-end { - align-self: flex-end; -} - -.flex-self-stretch { - align-self: stretch; -} - -.flex-vertical { - flex-direction: column; -} - -.z-bump { - z-index: 1; -} - -.select-none { - cursor: inherit; - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; -} - -.pointer { - cursor: pointer; -} -.cursor-pointer { - cursor: pointer; - transform-origin: center center; - transition: transform 50ms ease-in-out; -} -.cursor-pointer:hover { - transform: scale(1.1); -} -.cursor-pointer:active { - transform: scale(0.95); -} - -.cursor-disabled { - cursor: not-allowed; -} - -.margin-bottom-sml { - margin-bottom: 20px; -} - -.margin-bottom-med { - margin-bottom: 40px; -} - -.margin-right-left { - margin: 0 20px; -} - -.bold { - font-weight: bold; -} - -.text-transform-uppercase { - text-transform: uppercase; -} - -.font-small { - font-size: 12px; -} - -.font-medium { - font-size: 1.2em; -} - -hr.horizontal-line { - display: block; - height: 1px; - border: 0; - border-top: 1px solid #ccc; - margin: 1em 0; - padding: 0; -} - -.hover-white:hover { - background: white; -} - -.red-dot { - background: #E91550; - color: white; - border-radius: 10px; -} - -.diamond { - transform: rotate(45deg); - background: #038789; -} - -.hollow-diamond { - transform: rotate(45deg); - border: 3px solid #690496; -} - -.golden-square { - background: #EBB33F; -} - -.pending-dot { - background: red; - left: 14px; - top: 14px; - color: white; - border-radius: 10px; - height: 20px; - min-width: 20px; - position: relative; - display: flex; - align-items: center; - justify-content: center; - padding: 4px; - z-index: 1; -} - -.keyring-label { - z-index: 1; - font-size: 11px; - background: rgba(255,0,0,0.8); - bottom: -47px; - color: white; - border-radius: 10px; - height: 20px; - min-width: 20px; - position: relative; - display: flex; - align-items: center; - justify-content: center; - padding: 4px; -} - -.ether-balance { - display: flex; - align-items: center; -} - -.menu-icon { - display: inline-block; - height: 9px; - min-width: 9px; - margin: 13px; -} -.ether-icon { - background: rgb(0, 163, 68); - border-radius: 20px; -} -.testnet-icon { - background: #2465E1; -} - -.drop-menu-item { - display: flex; - align-items: center; -} - -.invisible { - visibility: hidden; -} - -.one-line-concat { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.critical-error { - text-align: center; - margin-top: 20px; - color: red; -} diff --git a/ui/classic/app/css/reset.css b/ui/classic/app/css/reset.css deleted file mode 100644 index 9ce89e8bc..000000000 --- a/ui/classic/app/css/reset.css +++ /dev/null @@ -1,48 +0,0 @@ -/* http://meyerweb.com/eric/tools/css/reset/ - v2.0 | 20110126 - License: none (public domain) -*/ - -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; -} \ No newline at end of file diff --git a/ui/classic/app/css/transitions.css b/ui/classic/app/css/transitions.css deleted file mode 100644 index 393a944f9..000000000 --- a/ui/classic/app/css/transitions.css +++ /dev/null @@ -1,42 +0,0 @@ -/* universal */ -.app-primary .main-enter { - position: absolute; - width: 100%; -} - -/* center position */ -.app-primary.from-right .main-enter-active, -.app-primary.from-left .main-enter-active { - overflow-x: hidden; - transform: translateX(0px); - transition: transform 300ms ease-in; -} - -/* exited positions */ -.app-primary.from-left .main-leave-active { - transform: translateX(360px); - transition: transform 300ms ease-in; -} -.app-primary.from-right .main-leave-active { - transform: translateX(-360px); - transition: transform 300ms ease-in; -} - -/* loader transitions */ -.loader-enter, .loader-leave-active { - opacity: 0.0; - transition: opacity 150 ease-in; -} -.loader-enter-active, .loader-leave { - opacity: 1.0; - transition: opacity 150 ease-in; -} - -/* entering positions */ -.app-primary.from-right .main-enter:not(.main-enter-active) { - transform: translateX(360px); -} -.app-primary.from-left .main-enter:not(.main-enter-active) { - transform: translateX(-360px); -} - diff --git a/ui/classic/app/first-time/init-menu.js b/ui/classic/app/first-time/init-menu.js deleted file mode 100644 index cc7c51bd3..000000000 --- a/ui/classic/app/first-time/init-menu.js +++ /dev/null @@ -1,179 +0,0 @@ -const inherits = require('util').inherits -const EventEmitter = require('events').EventEmitter -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const Mascot = require('../components/mascot') -const actions = require('../actions') -const Tooltip = require('../components/tooltip') -const getCaretCoordinates = require('textarea-caret') - -module.exports = connect(mapStateToProps)(InitializeMenuScreen) - -inherits(InitializeMenuScreen, Component) -function InitializeMenuScreen () { - Component.call(this) - this.animationEventEmitter = new EventEmitter() -} - -function mapStateToProps (state) { - return { - // state from plugin - currentView: state.appState.currentView, - warning: state.appState.warning, - } -} - -InitializeMenuScreen.prototype.render = function () { - var state = this.props - - switch (state.currentView.name) { - - default: - return this.renderMenu(state) - - } -} - -// InitializeMenuScreen.prototype.componentDidMount = function(){ -// document.getElementById('password-box').focus() -// } - -InitializeMenuScreen.prototype.renderMenu = function (state) { - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - h(Mascot, { - animationEventEmitter: this.animationEventEmitter, - }), - - h('h1', { - style: { - fontSize: '1.3em', - textTransform: 'uppercase', - color: '#7F8082', - marginBottom: 10, - }, - }, 'MetaMask'), - - - h('div', [ - h('h3', { - style: { - fontSize: '0.8em', - color: '#7F8082', - display: 'inline', - }, - }, 'Encrypt your new DEN'), - - h(Tooltip, { - title: 'Your DEN is your password-encrypted storage within MetaMask.', - }, [ - h('i.fa.fa-question-circle.pointer', { - style: { - fontSize: '18px', - position: 'relative', - color: 'rgb(247, 134, 28)', - top: '2px', - marginLeft: '4px', - }, - }), - ]), - ]), - - h('span.in-progress-notification', state.warning), - - // password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box', - placeholder: 'New Password (min 8 chars)', - onInput: this.inputChanged.bind(this), - style: { - width: 260, - marginTop: 12, - }, - }), - - // confirm password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box-confirm', - placeholder: 'Confirm Password', - onKeyPress: this.createVaultOnEnter.bind(this), - onInput: this.inputChanged.bind(this), - style: { - width: 260, - marginTop: 16, - }, - }), - - - h('button.primary', { - onClick: this.createNewVaultAndKeychain.bind(this), - style: { - margin: 12, - }, - }, 'Create'), - - h('.flex-row.flex-center.flex-grow', [ - h('p.pointer', { - onClick: this.showRestoreVault.bind(this), - style: { - fontSize: '0.8em', - color: 'rgb(247, 134, 28)', - textDecoration: 'underline', - }, - }, 'Import Existing DEN'), - ]), - - ]) - ) -} - -InitializeMenuScreen.prototype.createVaultOnEnter = function (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.createNewVaultAndKeychain() - } -} - -InitializeMenuScreen.prototype.componentDidMount = function () { - document.getElementById('password-box').focus() -} - -InitializeMenuScreen.prototype.showRestoreVault = function () { - this.props.dispatch(actions.showRestoreVault()) -} - -InitializeMenuScreen.prototype.createNewVaultAndKeychain = function () { - var passwordBox = document.getElementById('password-box') - var password = passwordBox.value - var passwordConfirmBox = document.getElementById('password-box-confirm') - var passwordConfirm = passwordConfirmBox.value - - if (password.length < 8) { - this.warning = 'password not long enough' - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - if (password !== passwordConfirm) { - this.warning = 'passwords don\'t match' - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - - this.props.dispatch(actions.createNewVaultAndKeychain(password)) -} - -InitializeMenuScreen.prototype.inputChanged = function (event) { - // tell mascot to look at page action - var element = event.target - var boundingRect = element.getBoundingClientRect() - var coordinates = getCaretCoordinates(element, element.selectionEnd) - this.animationEventEmitter.emit('point', { - x: boundingRect.left + coordinates.left - element.scrollLeft, - y: boundingRect.top + coordinates.top - element.scrollTop, - }) -} diff --git a/ui/classic/app/img/identicon-tardigrade.png b/ui/classic/app/img/identicon-tardigrade.png deleted file mode 100644 index 1742a32b8..000000000 Binary files a/ui/classic/app/img/identicon-tardigrade.png and /dev/null differ diff --git a/ui/classic/app/img/identicon-walrus.png b/ui/classic/app/img/identicon-walrus.png deleted file mode 100644 index d58fae912..000000000 Binary files a/ui/classic/app/img/identicon-walrus.png and /dev/null differ diff --git a/ui/classic/app/info.js b/ui/classic/app/info.js deleted file mode 100644 index e8470de97..000000000 --- a/ui/classic/app/info.js +++ /dev/null @@ -1,154 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') - -module.exports = connect(mapStateToProps)(InfoScreen) - -function mapStateToProps (state) { - return {} -} - -inherits(InfoScreen, Component) -function InfoScreen () { - Component.call(this) -} - -InfoScreen.prototype.render = function () { - const state = this.props - const version = global.platform.getVersion() - - return ( - h('.flex-column.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: (event) => { - state.dispatch(actions.goHome()) - }, - }), - h('h2.page-subtitle', 'Info'), - ]), - - // main view - h('.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-space-around', { - style: { - padding: '20px', - }, - }, [ - // current version number - - h('.info.info-gray', [ - h('div', 'Metamask'), - h('div', { - style: { - marginBottom: '10px', - }, - }, `Version: ${version}`), - ]), - - h('div', { - style: { - marginBottom: '5px', - }}, - [ - h('div', [ - h('a', { - href: 'https://metamask.io/privacy.html', - target: '_blank', - onClick (event) { this.navigateTo(event.target.href) }, - }, [ - h('div.info', 'Privacy Policy'), - ]), - ]), - h('div', [ - h('a', { - href: 'https://metamask.io/terms.html', - target: '_blank', - onClick (event) { this.navigateTo(event.target.href) }, - }, [ - h('div.info', 'Terms of Use'), - ]), - ]), - h('div', [ - h('a', { - href: 'https://metamask.io/attributions.html', - target: '_blank', - onClick (event) { this.navigateTo(event.target.href) }, - }, [ - h('div.info', 'Attributions'), - ]), - ]), - ] - ), - - h('hr', { - style: { - margin: '10px 0 ', - width: '7em', - }, - }), - - h('div', { - style: { - paddingLeft: '30px', - }}, - [ - h('div.fa.fa-github', [ - h('a.info', { - href: 'https://github.com/MetaMask/faq', - target: '_blank', - }, 'Need Help? Read our FAQ!'), - ]), - h('div', [ - h('a', { - href: 'https://metamask.io/', - target: '_blank', - }, [ - h('img.icon-size', { - src: 'images/icon-128.png', - style: { - // IE6-9 - filter: 'grayscale(100%)', - // Microsoft Edge and Firefox 35+ - WebkitFilter: 'grayscale(100%)', - }, - }), - h('div.info', 'Visit our web site'), - ]), - ]), - h('div.fa.fa-slack', [ - h('a.info', { - href: 'http://slack.metamask.io', - target: '_blank', - }, 'Join the conversation on Slack'), - ]), - - h('div.fa.fa-twitter', [ - h('a.info', { - href: 'https://twitter.com/metamask_io', - target: '_blank', - }, 'Follow us on Twitter'), - ]), - - h('div.fa.fa-envelope', [ - h('a.info', { - target: '_blank', - style: { width: '85vw' }, - href: 'mailto:help@metamask.io?subject=Feedback', - }, 'Email us!'), - ]), - ]), - ]), - ]), - ]) - ) -} - -InfoScreen.prototype.navigateTo = function (url) { - global.platform.openWindow({ url }) -} - diff --git a/ui/classic/app/keychains/hd/create-vault-complete.js b/ui/classic/app/keychains/hd/create-vault-complete.js deleted file mode 100644 index a318a9b50..000000000 --- a/ui/classic/app/keychains/hd/create-vault-complete.js +++ /dev/null @@ -1,78 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('../../actions') - -module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen) - -inherits(CreateVaultCompleteScreen, Component) -function CreateVaultCompleteScreen () { - Component.call(this) -} - -function mapStateToProps (state) { - return { - seed: state.appState.currentView.seedWords, - cachedSeed: state.metamask.seedWords, - } -} - -CreateVaultCompleteScreen.prototype.render = function () { - var state = this.props - var seed = state.seed || state.cachedSeed || '' - - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - // // subtitle and nav - // h('.section-title.flex-row.flex-center', [ - // h('h2.page-subtitle', 'Vault Created'), - // ]), - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginTop: 36, - marginBottom: 8, - width: '100%', - fontSize: '20px', - padding: 6, - }, - }, [ - 'Vault Created', - ]), - - h('div', { - style: { - width: '360px', - height: '78px', - fontSize: '1em', - marginTop: '10px', - textAlign: 'center', - }, - }, [ - h('span.error', 'These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret.'), - ]), - - h('textarea.twelve-word-phrase', { - readOnly: true, - value: seed, - }), - - h('button.primary', { - onClick: () => this.confirmSeedWords(), - style: { - margin: '24px', - fontSize: '0.9em', - }, - }, 'I\'ve copied it somewhere safe'), - ]) - ) -} - -CreateVaultCompleteScreen.prototype.confirmSeedWords = function () { - this.props.dispatch(actions.confirmSeedWords()) -} diff --git a/ui/classic/app/keychains/hd/recover-seed/confirmation.js b/ui/classic/app/keychains/hd/recover-seed/confirmation.js deleted file mode 100644 index 4ccbec9fc..000000000 --- a/ui/classic/app/keychains/hd/recover-seed/confirmation.js +++ /dev/null @@ -1,118 +0,0 @@ -const inherits = require('util').inherits - -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('../../../actions') - -module.exports = connect(mapStateToProps)(RevealSeedConfirmation) - -inherits(RevealSeedConfirmation, Component) -function RevealSeedConfirmation () { - Component.call(this) -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -RevealSeedConfirmation.prototype.render = function () { - const props = this.props - - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginBottom: 24, - width: '100%', - fontSize: '20px', - padding: 6, - }, - }, [ - 'Reveal Seed Words', - ]), - - h('.div', { - style: { - display: 'flex', - flexDirection: 'column', - padding: '20px', - justifyContent: 'center', - }, - }, [ - - h('h4', 'Do not recover your seed words in a public place! These words can be used to steal all your accounts.'), - - // confirmation - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box', - placeholder: 'Enter your password to confirm', - onKeyPress: this.checkConfirmation.bind(this), - style: { - width: 260, - marginTop: '12px', - }, - }), - - h('.flex-row.flex-space-between', { - style: { - marginTop: 30, - width: '50%', - }, - }, [ - // cancel - h('button.primary', { - onClick: this.goHome.bind(this), - }, 'CANCEL'), - - // submit - h('button.primary', { - onClick: this.revealSeedWords.bind(this), - }, 'OK'), - - ]), - - (props.warning) && ( - h('span.error', { - style: { - margin: '20px', - }, - }, props.warning.split('-')) - ), - - props.inProgress && ( - h('span.in-progress-notification', 'Generating Seed...') - ), - ]), - ]) - ) -} - -RevealSeedConfirmation.prototype.componentDidMount = function () { - document.getElementById('password-box').focus() -} - -RevealSeedConfirmation.prototype.goHome = function () { - this.props.dispatch(actions.showConfigPage(false)) -} - -// create vault - -RevealSeedConfirmation.prototype.checkConfirmation = function (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.revealSeedWords() - } -} - -RevealSeedConfirmation.prototype.revealSeedWords = function () { - var password = document.getElementById('password-box').value - this.props.dispatch(actions.requestRevealSeed(password)) -} diff --git a/ui/classic/app/keychains/hd/restore-vault.js b/ui/classic/app/keychains/hd/restore-vault.js deleted file mode 100644 index 06e51d9b3..000000000 --- a/ui/classic/app/keychains/hd/restore-vault.js +++ /dev/null @@ -1,152 +0,0 @@ -const inherits = require('util').inherits -const PersistentForm = require('../../../lib/persistent-form') -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('../../actions') - -module.exports = connect(mapStateToProps)(RestoreVaultScreen) - -inherits(RestoreVaultScreen, PersistentForm) -function RestoreVaultScreen () { - PersistentForm.call(this) -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - forgottenPassword: state.appState.forgottenPassword, - } -} - -RestoreVaultScreen.prototype.render = function () { - var state = this.props - this.persistentFormParentId = 'restore-vault-form' - - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginBottom: 24, - width: '100%', - fontSize: '20px', - padding: 6, - }, - }, [ - 'Restore Vault', - ]), - - // wallet seed entry - h('h3', 'Wallet Seed'), - h('textarea.twelve-word-phrase.letter-spacey', { - dataset: { - persistentFormId: 'wallet-seed', - }, - placeholder: 'Enter your secret twelve word phrase here to restore your vault.', - }), - - // password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box', - placeholder: 'New Password (min 8 chars)', - dataset: { - persistentFormId: 'password', - }, - style: { - width: 260, - marginTop: 12, - }, - }), - - // confirm password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box-confirm', - placeholder: 'Confirm Password', - onKeyPress: this.createOnEnter.bind(this), - dataset: { - persistentFormId: 'password-confirmation', - }, - style: { - width: 260, - marginTop: 16, - }, - }), - - (state.warning) && ( - h('span.error.in-progress-notification', state.warning) - ), - - // submit - - h('.flex-row.flex-space-between', { - style: { - marginTop: 30, - width: '50%', - }, - }, [ - - // cancel - h('button.primary', { - onClick: this.showInitializeMenu.bind(this), - }, 'CANCEL'), - - // submit - h('button.primary', { - onClick: this.createNewVaultAndRestore.bind(this), - }, 'OK'), - - ]), - ]) - - ) -} - -RestoreVaultScreen.prototype.showInitializeMenu = function () { - if (this.props.forgottenPassword) { - this.props.dispatch(actions.backToUnlockView()) - } else { - this.props.dispatch(actions.showInitializeMenu()) - } -} - -RestoreVaultScreen.prototype.createOnEnter = function (event) { - if (event.key === 'Enter') { - this.createNewVaultAndRestore() - } -} - -RestoreVaultScreen.prototype.createNewVaultAndRestore = function () { - // check password - var passwordBox = document.getElementById('password-box') - var password = passwordBox.value - var passwordConfirmBox = document.getElementById('password-box-confirm') - var passwordConfirm = passwordConfirmBox.value - if (password.length < 8) { - this.warning = 'Password not long enough' - - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - if (password !== passwordConfirm) { - this.warning = 'Passwords don\'t match' - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - // check seed - var seedBox = document.querySelector('textarea.twelve-word-phrase') - var seed = seedBox.value.trim() - if (seed.split(' ').length !== 12) { - this.warning = 'seed phrases are 12 words long' - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - // submit - this.warning = null - this.props.dispatch(actions.displayWarning(this.warning)) - this.props.dispatch(actions.createNewVaultAndRestore(password, seed)) -} diff --git a/ui/classic/app/new-keychain.js b/ui/classic/app/new-keychain.js deleted file mode 100644 index cc9633166..000000000 --- a/ui/classic/app/new-keychain.js +++ /dev/null @@ -1,29 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect - -module.exports = connect(mapStateToProps)(NewKeychain) - -function mapStateToProps (state) { - return {} -} - -inherits(NewKeychain, Component) -function NewKeychain () { - Component.call(this) -} - -NewKeychain.prototype.render = function () { - // const props = this.props - - return ( - h('div', { - style: { - background: 'blue', - }, - }, [ - h('h1', `Here's a list!!!!`), - ]) - ) -} diff --git a/ui/classic/app/reducers.js b/ui/classic/app/reducers.js deleted file mode 100644 index 11efca529..000000000 --- a/ui/classic/app/reducers.js +++ /dev/null @@ -1,52 +0,0 @@ -const extend = require('xtend') - -// -// Sub-Reducers take in the complete state and return their sub-state -// -const reduceIdentities = require('./reducers/identities') -const reduceMetamask = require('./reducers/metamask') -const reduceApp = require('./reducers/app') - -window.METAMASK_CACHED_LOG_STATE = null - -module.exports = rootReducer - -function rootReducer (state, action) { - // clone - state = extend(state) - - if (action.type === 'GLOBAL_FORCE_UPDATE') { - return action.value - } - - // - // Identities - // - - state.identities = reduceIdentities(state, action) - - // - // MetaMask - // - - state.metamask = reduceMetamask(state, action) - - // - // AppState - // - - state.appState = reduceApp(state, action) - - window.METAMASK_CACHED_LOG_STATE = state - return state -} - -window.logState = function () { - var stateString = JSON.stringify(window.METAMASK_CACHED_LOG_STATE, removeSeedWords, 2) - console.log(stateString) - return stateString -} - -function removeSeedWords (key, value) { - return key === 'seedWords' ? undefined : value -} diff --git a/ui/classic/app/reducers/app.js b/ui/classic/app/reducers/app.js deleted file mode 100644 index 2fcc9bfe0..000000000 --- a/ui/classic/app/reducers/app.js +++ /dev/null @@ -1,585 +0,0 @@ -const extend = require('xtend') -const actions = require('../actions') -const txHelper = require('../../lib/tx-helper') - -module.exports = reduceApp - - -function reduceApp (state, action) { - log.debug('App Reducer got ' + action.type) - // clone and defaults - const selectedAddress = state.metamask.selectedAddress - const hasUnconfActions = checkUnconfActions(state) - let name = 'accounts' - if (selectedAddress) { - name = 'accountDetail' - } - if (hasUnconfActions) { - log.debug('pending txs detected, defaulting to conf-tx view.') - name = 'confTx' - } - - var defaultView = { - name, - detailView: null, - context: selectedAddress, - } - - // confirm seed words - var seedWords = state.metamask.seedWords - var seedConfView = { - name: 'createVaultComplete', - seedWords, - } - - // default state - var appState = extend({ - shouldClose: false, - menuOpen: false, - currentView: seedWords ? seedConfView : defaultView, - accountDetail: { - subview: 'transactions', - }, - transForward: true, // Used to render transition direction - isLoading: false, // Used to display loading indicator - warning: null, // Used to display error text - }, state.appState) - - switch (action.type) { - - // transition methods - - case actions.TRANSITION_FORWARD: - return extend(appState, { - transForward: true, - }) - - case actions.TRANSITION_BACKWARD: - return extend(appState, { - transForward: false, - }) - - // intialize - - case actions.SHOW_CREATE_VAULT: - return extend(appState, { - currentView: { - name: 'createVault', - }, - transForward: true, - warning: null, - }) - - case actions.SHOW_RESTORE_VAULT: - return extend(appState, { - currentView: { - name: 'restoreVault', - }, - transForward: true, - forgottenPassword: true, - }) - - case actions.FORGOT_PASSWORD: - return extend(appState, { - currentView: { - name: 'restoreVault', - }, - transForward: false, - forgottenPassword: true, - }) - - case actions.SHOW_INIT_MENU: - return extend(appState, { - currentView: defaultView, - transForward: false, - }) - - case actions.SHOW_CONFIG_PAGE: - return extend(appState, { - currentView: { - name: 'config', - context: appState.currentView.context, - }, - transForward: action.value, - }) - - case actions.SHOW_ADD_TOKEN_PAGE: - return extend(appState, { - currentView: { - name: 'add-token', - context: appState.currentView.context, - }, - transForward: action.value, - }) - - case actions.SHOW_IMPORT_PAGE: - - return extend(appState, { - currentView: { - name: 'import-menu', - }, - transForward: true, - }) - - case actions.SHOW_INFO_PAGE: - return extend(appState, { - currentView: { - name: 'info', - context: appState.currentView.context, - }, - transForward: true, - }) - - case actions.CREATE_NEW_VAULT_IN_PROGRESS: - return extend(appState, { - currentView: { - name: 'createVault', - inProgress: true, - }, - transForward: true, - isLoading: true, - }) - - case actions.SHOW_NEW_VAULT_SEED: - return extend(appState, { - currentView: { - name: 'createVaultComplete', - seedWords: action.value, - }, - transForward: true, - isLoading: false, - }) - - case actions.NEW_ACCOUNT_SCREEN: - return extend(appState, { - currentView: { - name: 'new-account', - context: appState.currentView.context, - }, - transForward: true, - }) - - case actions.SHOW_SEND_PAGE: - return extend(appState, { - currentView: { - name: 'sendTransaction', - context: appState.currentView.context, - }, - transForward: true, - warning: null, - }) - - case actions.SHOW_NEW_KEYCHAIN: - return extend(appState, { - currentView: { - name: 'newKeychain', - context: appState.currentView.context, - }, - transForward: true, - }) - - // unlock - - case actions.UNLOCK_METAMASK: - return extend(appState, { - forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null, - detailView: {}, - transForward: true, - isLoading: false, - warning: null, - }) - - case actions.LOCK_METAMASK: - return extend(appState, { - currentView: defaultView, - transForward: false, - warning: null, - }) - - case actions.BACK_TO_INIT_MENU: - return extend(appState, { - warning: null, - transForward: false, - forgottenPassword: true, - currentView: { - name: 'InitMenu', - }, - }) - - case actions.BACK_TO_UNLOCK_VIEW: - return extend(appState, { - warning: null, - transForward: true, - forgottenPassword: false, - currentView: { - name: 'UnlockScreen', - }, - }) - // reveal seed words - - case actions.REVEAL_SEED_CONFIRMATION: - return extend(appState, { - currentView: { - name: 'reveal-seed-conf', - }, - transForward: true, - warning: null, - }) - - // accounts - - case actions.SET_SELECTED_ACCOUNT: - return extend(appState, { - activeAddress: action.value, - }) - - case actions.GO_HOME: - return extend(appState, { - currentView: extend(appState.currentView, { - name: 'accountDetail', - }), - accountDetail: { - subview: 'transactions', - accountExport: 'none', - privateKey: '', - }, - transForward: false, - warning: null, - }) - - case actions.SHOW_ACCOUNT_DETAIL: - return extend(appState, { - forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null, - currentView: { - name: 'accountDetail', - context: action.value, - }, - accountDetail: { - subview: 'transactions', - accountExport: 'none', - privateKey: '', - }, - transForward: false, - }) - - case actions.BACK_TO_ACCOUNT_DETAIL: - return extend(appState, { - currentView: { - name: 'accountDetail', - context: action.value, - }, - accountDetail: { - subview: 'transactions', - accountExport: 'none', - privateKey: '', - }, - transForward: false, - }) - - case actions.SHOW_ACCOUNTS_PAGE: - return extend(appState, { - currentView: { - name: seedWords ? 'createVaultComplete' : 'accounts', - seedWords, - }, - transForward: true, - isLoading: false, - warning: null, - scrollToBottom: false, - forgottenPassword: false, - }) - - case actions.SHOW_NOTICE: - return extend(appState, { - transForward: true, - isLoading: false, - }) - - case actions.REVEAL_ACCOUNT: - return extend(appState, { - scrollToBottom: true, - }) - - case actions.SHOW_CONF_TX_PAGE: - return extend(appState, { - currentView: { - name: 'confTx', - context: 0, - }, - transForward: action.transForward, - warning: null, - isLoading: false, - }) - - case actions.SHOW_CONF_MSG_PAGE: - return extend(appState, { - currentView: { - name: hasUnconfActions ? 'confTx' : 'account-detail', - context: 0, - }, - transForward: true, - warning: null, - isLoading: false, - }) - - case actions.COMPLETED_TX: - log.debug('reducing COMPLETED_TX for tx ' + action.value) - const otherUnconfActions = getUnconfActionList(state) - .filter(tx => tx.id !== action.value) - const hasOtherUnconfActions = otherUnconfActions.length > 0 - - if (hasOtherUnconfActions) { - log.debug('reducer detected txs - rendering confTx view') - return extend(appState, { - transForward: false, - currentView: { - name: 'confTx', - context: 0, - }, - warning: null, - }) - } else { - log.debug('attempting to close popup') - return extend(appState, { - // indicate notification should close - shouldClose: true, - transForward: false, - warning: null, - currentView: { - name: 'accountDetail', - context: state.metamask.selectedAddress, - }, - accountDetail: { - subview: 'transactions', - }, - }) - } - - case actions.NEXT_TX: - return extend(appState, { - transForward: true, - currentView: { - name: 'confTx', - context: ++appState.currentView.context, - warning: null, - }, - }) - - case actions.VIEW_PENDING_TX: - const context = indexForPending(state, action.value) - return extend(appState, { - transForward: true, - currentView: { - name: 'confTx', - context, - warning: null, - }, - }) - - case actions.PREVIOUS_TX: - return extend(appState, { - transForward: false, - currentView: { - name: 'confTx', - context: --appState.currentView.context, - warning: null, - }, - }) - - case actions.TRANSACTION_ERROR: - return extend(appState, { - currentView: { - name: 'confTx', - errorMessage: 'There was a problem submitting this transaction.', - }, - }) - - case actions.UNLOCK_FAILED: - return extend(appState, { - warning: action.value || 'Incorrect password. Try again.', - }) - - case actions.SHOW_LOADING: - return extend(appState, { - isLoading: true, - loadingMessage: action.value, - }) - - case actions.HIDE_LOADING: - return extend(appState, { - isLoading: false, - }) - - case actions.SHOW_SUB_LOADING_INDICATION: - return extend(appState, { - isSubLoading: true, - }) - - case actions.HIDE_SUB_LOADING_INDICATION: - return extend(appState, { - isSubLoading: false, - }) - case actions.CLEAR_SEED_WORD_CACHE: - return extend(appState, { - transForward: true, - currentView: {}, - isLoading: false, - accountDetail: { - subview: 'transactions', - accountExport: 'none', - privateKey: '', - }, - }) - - case actions.DISPLAY_WARNING: - return extend(appState, { - warning: action.value, - isLoading: false, - }) - - case actions.HIDE_WARNING: - return extend(appState, { - warning: undefined, - }) - - case actions.REQUEST_ACCOUNT_EXPORT: - return extend(appState, { - transForward: true, - currentView: { - name: 'accountDetail', - context: appState.currentView.context, - }, - accountDetail: { - subview: 'export', - accountExport: 'requested', - }, - }) - - case actions.EXPORT_ACCOUNT: - return extend(appState, { - accountDetail: { - subview: 'export', - accountExport: 'completed', - }, - }) - - case actions.SHOW_PRIVATE_KEY: - return extend(appState, { - accountDetail: { - subview: 'export', - accountExport: 'completed', - privateKey: action.value, - }, - }) - - case actions.BUY_ETH_VIEW: - return extend(appState, { - transForward: true, - currentView: { - name: 'buyEth', - context: appState.currentView.name, - }, - identity: state.metamask.identities[action.value], - buyView: { - subview: 'Coinbase', - amount: '15.00', - buyAddress: action.value, - formView: { - coinbase: true, - shapeshift: false, - }, - }, - }) - - case actions.COINBASE_SUBVIEW: - return extend(appState, { - buyView: { - subview: 'Coinbase', - formView: { - coinbase: true, - shapeshift: false, - }, - buyAddress: appState.buyView.buyAddress, - amount: appState.buyView.amount, - }, - }) - - case actions.SHAPESHIFT_SUBVIEW: - return extend(appState, { - buyView: { - subview: 'ShapeShift', - formView: { - coinbase: false, - shapeshift: true, - marketinfo: action.value.marketinfo, - coinOptions: action.value.coinOptions, - }, - buyAddress: appState.buyView.buyAddress, - amount: appState.buyView.amount, - }, - }) - - case actions.PAIR_UPDATE: - return extend(appState, { - buyView: { - subview: 'ShapeShift', - formView: { - coinbase: false, - shapeshift: true, - marketinfo: action.value.marketinfo, - coinOptions: appState.buyView.formView.coinOptions, - }, - buyAddress: appState.buyView.buyAddress, - amount: appState.buyView.amount, - warning: null, - }, - }) - - case actions.SHOW_QR: - return extend(appState, { - qrRequested: true, - transForward: true, - - Qr: { - message: action.value.message, - data: action.value.data, - }, - }) - - case actions.SHOW_QR_VIEW: - return extend(appState, { - currentView: { - name: 'qr', - context: appState.currentView.context, - }, - transForward: true, - Qr: { - message: action.value.message, - data: action.value.data, - }, - }) - default: - return appState - } -} - -function checkUnconfActions (state) { - const unconfActionList = getUnconfActionList(state) - const hasUnconfActions = unconfActionList.length > 0 - return hasUnconfActions -} - -function getUnconfActionList (state) { - const { unapprovedTxs, unapprovedMsgs, - unapprovedPersonalMsgs, network } = state.metamask - - const unconfActionList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) - return unconfActionList -} - -function indexForPending (state, txId) { - const unconfTxList = getUnconfActionList(state) - const match = unconfTxList.find((tx) => tx.id === txId) - const index = unconfTxList.indexOf(match) - return index -} diff --git a/ui/classic/app/reducers/identities.js b/ui/classic/app/reducers/identities.js deleted file mode 100644 index 341a404e7..000000000 --- a/ui/classic/app/reducers/identities.js +++ /dev/null @@ -1,15 +0,0 @@ -const extend = require('xtend') - -module.exports = reduceIdentities - -function reduceIdentities (state, action) { - // clone + defaults - var idState = extend({ - - }, state.identities) - - switch (action.type) { - default: - return idState - } -} diff --git a/ui/classic/app/reducers/metamask.js b/ui/classic/app/reducers/metamask.js deleted file mode 100644 index e0c416c2d..000000000 --- a/ui/classic/app/reducers/metamask.js +++ /dev/null @@ -1,137 +0,0 @@ -const extend = require('xtend') -const actions = require('../actions') - -module.exports = reduceMetamask - -function reduceMetamask (state, action) { - let newState - - // clone + defaults - var metamaskState = extend({ - isInitialized: false, - isUnlocked: false, - rpcTarget: 'https://rawtestrpc.metamask.io/', - identities: {}, - unapprovedTxs: {}, - noActiveNotices: true, - lastUnreadNotice: undefined, - frequentRpcList: [], - addressBook: [], - }, state.metamask) - - switch (action.type) { - - case actions.SHOW_ACCOUNTS_PAGE: - newState = extend(metamaskState) - delete newState.seedWords - return newState - - case actions.SHOW_NOTICE: - return extend(metamaskState, { - noActiveNotices: false, - lastUnreadNotice: action.value, - }) - - case actions.CLEAR_NOTICES: - return extend(metamaskState, { - noActiveNotices: true, - }) - - case actions.UPDATE_METAMASK_STATE: - return extend(metamaskState, action.value) - - case actions.UNLOCK_METAMASK: - return extend(metamaskState, { - isUnlocked: true, - isInitialized: true, - selectedAddress: action.value, - }) - - case actions.LOCK_METAMASK: - return extend(metamaskState, { - isUnlocked: false, - }) - - case actions.SET_RPC_LIST: - return extend(metamaskState, { - frequentRpcList: action.value, - }) - - case actions.SET_RPC_TARGET: - return extend(metamaskState, { - provider: { - type: 'rpc', - rpcTarget: action.value, - }, - }) - - case actions.SET_PROVIDER_TYPE: - return extend(metamaskState, { - provider: { - type: action.value, - }, - }) - - case actions.COMPLETED_TX: - var stringId = String(action.id) - newState = extend(metamaskState, { - unapprovedTxs: {}, - unapprovedMsgs: {}, - }) - for (const id in metamaskState.unapprovedTxs) { - if (id !== stringId) { - newState.unapprovedTxs[id] = metamaskState.unapprovedTxs[id] - } - } - for (const id in metamaskState.unapprovedMsgs) { - if (id !== stringId) { - newState.unapprovedMsgs[id] = metamaskState.unapprovedMsgs[id] - } - } - return newState - - case actions.SHOW_NEW_VAULT_SEED: - return extend(metamaskState, { - isUnlocked: true, - isInitialized: false, - seedWords: action.value, - }) - - case actions.CLEAR_SEED_WORD_CACHE: - newState = extend(metamaskState, { - isUnlocked: true, - isInitialized: true, - selectedAddress: action.value, - }) - delete newState.seedWords - return newState - - case actions.SHOW_ACCOUNT_DETAIL: - newState = extend(metamaskState, { - isUnlocked: true, - isInitialized: true, - selectedAddress: action.value, - }) - delete newState.seedWords - return newState - - case actions.SAVE_ACCOUNT_LABEL: - const account = action.value.account - const name = action.value.label - var id = {} - id[account] = extend(metamaskState.identities[account], { name }) - var identities = extend(metamaskState.identities, id) - return extend(metamaskState, { identities }) - - case actions.SET_CURRENT_FIAT: - return extend(metamaskState, { - currentCurrency: action.value.currentCurrency, - conversionRate: action.value.conversionRate, - conversionDate: action.value.conversionDate, - }) - - default: - return metamaskState - - } -} diff --git a/ui/classic/app/root.js b/ui/classic/app/root.js deleted file mode 100644 index 9e7314b20..000000000 --- a/ui/classic/app/root.js +++ /dev/null @@ -1,22 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const Provider = require('react-redux').Provider -const h = require('react-hyperscript') -const App = require('./app') - -module.exports = Root - -inherits(Root, Component) -function Root () { Component.call(this) } - -Root.prototype.render = function () { - return ( - - h(Provider, { - store: this.props.store, - }, [ - h(App), - ]) - - ) -} diff --git a/ui/classic/app/send.js b/ui/classic/app/send.js deleted file mode 100644 index a21a219eb..000000000 --- a/ui/classic/app/send.js +++ /dev/null @@ -1,288 +0,0 @@ -const inherits = require('util').inherits -const PersistentForm = require('../lib/persistent-form') -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const Identicon = require('./components/identicon') -const actions = require('./actions') -const util = require('./util') -const numericBalance = require('./util').numericBalance -const addressSummary = require('./util').addressSummary -const isHex = require('./util').isHex -const EthBalance = require('./components/eth-balance') -const EnsInput = require('./components/ens-input') -const ethUtil = require('ethereumjs-util') -module.exports = connect(mapStateToProps)(SendTransactionScreen) - -function mapStateToProps (state) { - var result = { - address: state.metamask.selectedAddress, - accounts: state.metamask.accounts, - identities: state.metamask.identities, - warning: state.appState.warning, - network: state.metamask.network, - addressBook: state.metamask.addressBook, - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - } - - result.error = result.warning && result.warning.split('.')[0] - - result.account = result.accounts[result.address] - result.identity = result.identities[result.address] - result.balance = result.account ? numericBalance(result.account.balance) : null - - return result -} - -inherits(SendTransactionScreen, PersistentForm) -function SendTransactionScreen () { - PersistentForm.call(this) -} - -SendTransactionScreen.prototype.render = function () { - this.persistentFormParentId = 'send-tx-form' - - const props = this.props - const { - address, - account, - identity, - network, - identities, - addressBook, - conversionRate, - currentCurrency, - } = props - - return ( - - h('.send-screen.flex-column.flex-grow', [ - - // - // Sender Profile - // - - h('.account-data-subsection.flex-row.flex-grow', { - style: { - margin: '0 20px', - }, - }, [ - - // header - identicon + nav - h('.flex-row.flex-space-between', { - style: { - marginTop: '15px', - }, - }, [ - // back button - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { - onClick: this.back.bind(this), - }), - - // large identicon - h('.identicon-wrapper.flex-column.flex-center.select-none', [ - h(Identicon, { - diameter: 62, - address: address, - }), - ]), - - // invisible place holder - h('i.fa.fa-users.fa-lg.invisible', { - style: { - marginTop: '28px', - }, - }), - - ]), - - // account label - - h('.flex-column', { - style: { - marginTop: '10px', - alignItems: 'flex-start', - }, - }, [ - h('h2.font-medium.color-forest.flex-center', { - style: { - paddingTop: '8px', - marginBottom: '8px', - }, - }, identity && identity.name), - - // address and getter actions - h('.flex-row.flex-center', { - style: { - marginBottom: '8px', - }, - }, [ - - h('div', { - style: { - lineHeight: '16px', - }, - }, addressSummary(address)), - - ]), - - // balance - h('.flex-row.flex-center', [ - - h(EthBalance, { - value: account && account.balance, - conversionRate, - currentCurrency, - }), - - ]), - ]), - ]), - - // - // Required Fields - // - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginTop: '15px', - marginBottom: '16px', - }, - }, [ - 'Send Transaction', - ]), - - // error message - props.error && h('span.error.flex-center', props.error), - - // 'to' field - h('section.flex-row.flex-center', [ - h(EnsInput, { - name: 'address', - placeholder: 'Recipient Address', - onChange: this.recipientDidChange.bind(this), - network, - identities, - addressBook, - }), - ]), - - // 'amount' and send button - h('section.flex-row.flex-center', [ - - h('input.large-input', { - name: 'amount', - placeholder: 'Amount', - type: 'number', - style: { - marginRight: '6px', - }, - dataset: { - persistentFormId: 'tx-amount', - }, - }), - - h('button.primary', { - onClick: this.onSubmit.bind(this), - style: { - textTransform: 'uppercase', - }, - }, 'Next'), - - ]), - - // - // Optional Fields - // - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginTop: '16px', - marginBottom: '16px', - }, - }, [ - 'Transaction Data (optional)', - ]), - - // 'data' field - h('section.flex-column.flex-center', [ - h('input.large-input', { - name: 'txData', - placeholder: '0x01234', - style: { - width: '100%', - resize: 'none', - }, - dataset: { - persistentFormId: 'tx-data', - }, - }), - ]), - ]) - ) -} - -SendTransactionScreen.prototype.navigateToAccounts = function (event) { - event.stopPropagation() - this.props.dispatch(actions.showAccountsPage()) -} - -SendTransactionScreen.prototype.back = function () { - var address = this.props.address - this.props.dispatch(actions.backToAccountDetail(address)) -} - -SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickname) { - this.setState({ - recipient: recipient, - nickname: nickname, - }) -} - -SendTransactionScreen.prototype.onSubmit = function () { - const state = this.state || {} - const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '') - const nickname = state.nickname || ' ' - const input = document.querySelector('input[name="amount"]').value - const value = util.normalizeEthStringToWei(input) - const txData = document.querySelector('input[name="txData"]').value - const balance = this.props.balance - let message - - if (value.gt(balance)) { - message = 'Insufficient funds.' - return this.props.dispatch(actions.displayWarning(message)) - } - - if (input < 0) { - message = 'Can not send negative amounts of ETH.' - return this.props.dispatch(actions.displayWarning(message)) - } - - if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) { - message = 'Recipient address is invalid.' - return this.props.dispatch(actions.displayWarning(message)) - } - - if (!isHex(ethUtil.stripHexPrefix(txData)) && txData) { - message = 'Transaction data must be hex string.' - return this.props.dispatch(actions.displayWarning(message)) - } - - this.props.dispatch(actions.hideWarning()) - - this.props.dispatch(actions.addToAddressBook(recipient, nickname)) - - var txParams = { - from: this.props.address, - value: '0x' + value.toString(16), - } - - if (recipient) txParams.to = ethUtil.addHexPrefix(recipient) - if (txData) txParams.data = txData - - this.props.dispatch(actions.signTx(txParams)) -} diff --git a/ui/classic/app/settings.js b/ui/classic/app/settings.js deleted file mode 100644 index 454cc95e0..000000000 --- a/ui/classic/app/settings.js +++ /dev/null @@ -1,59 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') - -module.exports = connect(mapStateToProps)(AppSettingsPage) - -function mapStateToProps (state) { - return {} -} - -inherits(AppSettingsPage, Component) -function AppSettingsPage () { - Component.call(this) -} - -AppSettingsPage.prototype.render = function () { - return ( - - h('.account-detail-section.flex-column.flex-grow', [ - - // subtitle and nav - h('.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: this.navigateToAccounts.bind(this), - }), - h('h2.page-subtitle', 'Settings'), - ]), - - h('label', { - htmlFor: 'settings-rpc-endpoint', - }, 'RPC Endpoint:'), - h('input', { - type: 'url', - id: 'settings-rpc-endpoint', - onKeyPress: this.onKeyPress.bind(this), - }), - - ]) - - ) -} - -AppSettingsPage.prototype.componentDidMount = function () { - document.querySelector('input').focus() -} - -AppSettingsPage.prototype.onKeyPress = function (event) { - // get submit event - if (event.key === 'Enter') { - // this.submitPassword(event) - } -} - -AppSettingsPage.prototype.navigateToAccounts = function (event) { - event.stopPropagation() - this.props.dispatch(actions.showAccountsPage()) -} diff --git a/ui/classic/app/store.js b/ui/classic/app/store.js deleted file mode 100644 index ba9e58b49..000000000 --- a/ui/classic/app/store.js +++ /dev/null @@ -1,21 +0,0 @@ -const createStore = require('redux').createStore -const applyMiddleware = require('redux').applyMiddleware -const thunkMiddleware = require('redux-thunk') -const rootReducer = require('./reducers') -const createLogger = require('redux-logger') - -global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' - -module.exports = configureStore - -const loggerMiddleware = createLogger({ - predicate: () => global.METAMASK_DEBUG, -}) - -const middlewares = [thunkMiddleware, loggerMiddleware] - -const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore) - -function configureStore (initialState) { - return createStoreWithMiddleware(rootReducer, initialState) -} diff --git a/ui/classic/app/template.js b/ui/classic/app/template.js deleted file mode 100644 index d15b30fd2..000000000 --- a/ui/classic/app/template.js +++ /dev/null @@ -1,30 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect - -module.exports = connect(mapStateToProps)(COMPONENTNAME) - -function mapStateToProps (state) { - return {} -} - -inherits(COMPONENTNAME, Component) -function COMPONENTNAME () { - Component.call(this) -} - -COMPONENTNAME.prototype.render = function () { - const props = this.props - - return ( - h('div', { - style: { - background: 'blue', - }, - }, [ - `Hello, ${props.sender}`, - ]) - ) -} - diff --git a/ui/classic/app/unlock.js b/ui/classic/app/unlock.js deleted file mode 100644 index 1aee3c5d0..000000000 --- a/ui/classic/app/unlock.js +++ /dev/null @@ -1,118 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') -const getCaretCoordinates = require('textarea-caret') -const EventEmitter = require('events').EventEmitter - -const Mascot = require('./components/mascot') - -module.exports = connect(mapStateToProps)(UnlockScreen) - -inherits(UnlockScreen, Component) -function UnlockScreen () { - Component.call(this) - this.animationEventEmitter = new EventEmitter() -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -UnlockScreen.prototype.render = function () { - const state = this.props - const warning = state.warning - return ( - h('.flex-column', [ - h('.unlock-screen.flex-column.flex-center.flex-grow', [ - - h(Mascot, { - animationEventEmitter: this.animationEventEmitter, - }), - - h('h1', { - style: { - fontSize: '1.4em', - textTransform: 'uppercase', - color: '#7F8082', - }, - }, 'MetaMask'), - - h('input.large-input', { - type: 'password', - id: 'password-box', - placeholder: 'enter password', - style: { - - }, - onKeyPress: this.onKeyPress.bind(this), - onInput: this.inputChanged.bind(this), - }), - - h('.error', { - style: { - display: warning ? 'block' : 'none', - padding: '0 20px', - textAlign: 'center', - }, - }, warning), - - h('button.primary.cursor-pointer', { - onClick: this.onSubmit.bind(this), - style: { - margin: 10, - }, - }, 'Unlock'), - ]), - - h('.flex-row.flex-center.flex-grow', [ - h('p.pointer', { - onClick: () => this.props.dispatch(actions.forgotPassword()), - style: { - fontSize: '0.8em', - color: 'rgb(247, 134, 28)', - textDecoration: 'underline', - }, - }, 'I forgot my password.'), - ]), - ]) - ) -} - -UnlockScreen.prototype.componentDidMount = function () { - document.getElementById('password-box').focus() -} - -UnlockScreen.prototype.onSubmit = function (event) { - const input = document.getElementById('password-box') - const password = input.value - this.props.dispatch(actions.tryUnlockMetamask(password)) -} - -UnlockScreen.prototype.onKeyPress = function (event) { - if (event.key === 'Enter') { - this.submitPassword(event) - } -} - -UnlockScreen.prototype.submitPassword = function (event) { - var element = event.target - var password = element.value - // reset input - element.value = '' - this.props.dispatch(actions.tryUnlockMetamask(password)) -} - -UnlockScreen.prototype.inputChanged = function (event) { - // tell mascot to look at page action - var element = event.target - var boundingRect = element.getBoundingClientRect() - var coordinates = getCaretCoordinates(element, element.selectionEnd) - this.animationEventEmitter.emit('point', { - x: boundingRect.left + coordinates.left - element.scrollLeft, - y: boundingRect.top + coordinates.top - element.scrollTop, - }) -} diff --git a/ui/classic/app/util.js b/ui/classic/app/util.js deleted file mode 100644 index ac3f42c6b..000000000 --- a/ui/classic/app/util.js +++ /dev/null @@ -1,217 +0,0 @@ -const ethUtil = require('ethereumjs-util') - -var valueTable = { - wei: '1000000000000000000', - kwei: '1000000000000000', - mwei: '1000000000000', - gwei: '1000000000', - szabo: '1000000', - finney: '1000', - ether: '1', - kether: '0.001', - mether: '0.000001', - gether: '0.000000001', - tether: '0.000000000001', -} -var bnTable = {} -for (var currency in valueTable) { - bnTable[currency] = new ethUtil.BN(valueTable[currency], 10) -} - -module.exports = { - valuesFor: valuesFor, - addressSummary: addressSummary, - miniAddressSummary: miniAddressSummary, - isAllOneCase: isAllOneCase, - isValidAddress: isValidAddress, - numericBalance: numericBalance, - parseBalance: parseBalance, - formatBalance: formatBalance, - generateBalanceObject: generateBalanceObject, - dataSize: dataSize, - readableDate: readableDate, - normalizeToWei: normalizeToWei, - normalizeEthStringToWei: normalizeEthStringToWei, - normalizeNumberToWei: normalizeNumberToWei, - valueTable: valueTable, - bnTable: bnTable, - isHex: isHex, -} - -function valuesFor (obj) { - if (!obj) return [] - return Object.keys(obj) - .map(function (key) { return obj[key] }) -} - -function addressSummary (address, firstSegLength = 10, lastSegLength = 4, includeHex = true) { - if (!address) return '' - let checked = ethUtil.toChecksumAddress(address) - if (!includeHex) { - checked = ethUtil.stripHexPrefix(checked) - } - return checked ? checked.slice(0, firstSegLength) + '...' + checked.slice(checked.length - lastSegLength) : '...' -} - -function miniAddressSummary (address) { - if (!address) return '' - var checked = ethUtil.toChecksumAddress(address) - return checked ? checked.slice(0, 4) + '...' + checked.slice(-4) : '...' -} - -function isValidAddress (address) { - var prefixed = ethUtil.addHexPrefix(address) - if (address === '0x0000000000000000000000000000000000000000') return false - return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed) -} - -function isAllOneCase (address) { - if (!address) return true - var lower = address.toLowerCase() - var upper = address.toUpperCase() - return address === lower || address === upper -} - -// Takes wei Hex, returns wei BN, even if input is null -function numericBalance (balance) { - if (!balance) return new ethUtil.BN(0, 16) - var stripped = ethUtil.stripHexPrefix(balance) - return new ethUtil.BN(stripped, 16) -} - -// Takes hex, returns [beforeDecimal, afterDecimal] -function parseBalance (balance) { - var beforeDecimal, afterDecimal - const wei = numericBalance(balance) - var weiString = wei.toString() - const trailingZeros = /0+$/ - - beforeDecimal = weiString.length > 18 ? weiString.slice(0, weiString.length - 18) : '0' - afterDecimal = ('000000000000000000' + wei).slice(-18).replace(trailingZeros, '') - if (afterDecimal === '') { afterDecimal = '0' } - return [beforeDecimal, afterDecimal] -} - -// Takes wei hex, returns an object with three properties. -// Its "formatted" property is what we generally use to render values. -function formatBalance (balance, decimalsToKeep, needsParse = true) { - var parsed = needsParse ? parseBalance(balance) : balance.split('.') - var beforeDecimal = parsed[0] - var afterDecimal = parsed[1] - var formatted = 'None' - if (decimalsToKeep === undefined) { - if (beforeDecimal === '0') { - if (afterDecimal !== '0') { - var sigFigs = afterDecimal.match(/^0*(.{2})/) // default: grabs 2 most significant digits - if (sigFigs) { afterDecimal = sigFigs[0] } - formatted = '0.' + afterDecimal + ' ETH' - } - } else { - formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ' ETH' - } - } else { - afterDecimal += Array(decimalsToKeep).join('0') - formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ' ETH' - } - return formatted -} - - -function generateBalanceObject (formattedBalance, decimalsToKeep = 1) { - var balance = formattedBalance.split(' ')[0] - var label = formattedBalance.split(' ')[1] - var beforeDecimal = balance.split('.')[0] - var afterDecimal = balance.split('.')[1] - var shortBalance = shortenBalance(balance, decimalsToKeep) - - if (beforeDecimal === '0' && afterDecimal.substr(0, 5) === '00000') { - // eslint-disable-next-line eqeqeq - if (afterDecimal == 0) { - balance = '0' - } else { - balance = '<1.0e-5' - } - } else if (beforeDecimal !== '0') { - balance = `${beforeDecimal}.${afterDecimal.slice(0, decimalsToKeep)}` - } - - return { balance, label, shortBalance } -} - -function shortenBalance (balance, decimalsToKeep = 1) { - var truncatedValue - var convertedBalance = parseFloat(balance) - if (convertedBalance > 1000000) { - truncatedValue = (balance / 1000000).toFixed(decimalsToKeep) - return `${truncatedValue}m` - } else if (convertedBalance > 1000) { - truncatedValue = (balance / 1000).toFixed(decimalsToKeep) - return `${truncatedValue}k` - } else if (convertedBalance === 0) { - return '0' - } else if (convertedBalance < 0.001) { - return '<0.001' - } else if (convertedBalance < 1) { - var stringBalance = convertedBalance.toString() - if (stringBalance.split('.')[1].length > 3) { - return convertedBalance.toFixed(3) - } else { - return stringBalance - } - } else { - return convertedBalance.toFixed(decimalsToKeep) - } -} - -function dataSize (data) { - var size = data ? ethUtil.stripHexPrefix(data).length : 0 - return size + ' bytes' -} - -// Takes a BN and an ethereum currency name, -// returns a BN in wei -function normalizeToWei (amount, currency) { - try { - return amount.mul(bnTable.wei).div(bnTable[currency]) - } catch (e) {} - return amount -} - -function normalizeEthStringToWei (str) { - const parts = str.split('.') - let eth = new ethUtil.BN(parts[0], 10).mul(bnTable.wei) - if (parts[1]) { - var decimal = parts[1] - while (decimal.length < 18) { - decimal += '0' - } - const decimalBN = new ethUtil.BN(decimal, 10) - eth = eth.add(decimalBN) - } - return eth -} - -var multiple = new ethUtil.BN('10000', 10) -function normalizeNumberToWei (n, currency) { - var enlarged = n * 10000 - var amount = new ethUtil.BN(String(enlarged), 10) - return normalizeToWei(amount, currency).div(multiple) -} - -function readableDate (ms) { - var date = new Date(ms) - var month = date.getMonth() - var day = date.getDate() - var year = date.getFullYear() - var hours = date.getHours() - var minutes = '0' + date.getMinutes() - var seconds = '0' + date.getSeconds() - - var dateStr = `${month}/${day}/${year}` - var time = `${hours}:${minutes.substr(-2)}:${seconds.substr(-2)}` - return `${dateStr} ${time}` -} - -function isHex (str) { - return Boolean(str.match(/^(0x)?[0-9a-fA-F]+$/)) -} diff --git a/ui/classic/css.js b/ui/classic/css.js deleted file mode 100644 index 7c394a87b..000000000 --- a/ui/classic/css.js +++ /dev/null @@ -1,29 +0,0 @@ -const fs = require('fs') -const path = require('path') - -module.exports = bundleCss - -var cssFiles = { - 'fonts.css': fs.readFileSync(path.join(__dirname, '/app/css/fonts.css'), 'utf8'), - 'reset.css': fs.readFileSync(path.join(__dirname, '/app/css/reset.css'), 'utf8'), - 'lib.css': fs.readFileSync(path.join(__dirname, '/app/css/lib.css'), 'utf8'), - 'index.css': fs.readFileSync(path.join(__dirname, '/app/css/index.css'), 'utf8'), - 'transitions.css': fs.readFileSync(path.join(__dirname, '/app/css/transitions.css'), 'utf8'), - 'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'), - 'react-css': fs.readFileSync(path.join(__dirname, '..', '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'), -} - -function bundleCss () { - var cssBundle = Object.keys(cssFiles).reduce(function (bundle, fileName) { - var fileContent = cssFiles[fileName] - var output = String() - - output += '/*========== ' + fileName + ' ==========*/\n\n' - output += fileContent - output += '\n\n' - - return bundle + output - }, String()) - - return cssBundle -} diff --git a/ui/classic/design/00-metamask-SignIn.jpg b/ui/classic/design/00-metamask-SignIn.jpg deleted file mode 100644 index 2becdb032..000000000 Binary files a/ui/classic/design/00-metamask-SignIn.jpg and /dev/null differ diff --git a/ui/classic/design/01-metamask-SelectAcc.jpg b/ui/classic/design/01-metamask-SelectAcc.jpg deleted file mode 100644 index 239091a98..000000000 Binary files a/ui/classic/design/01-metamask-SelectAcc.jpg and /dev/null differ diff --git a/ui/classic/design/02-metamask-AccDetails.jpg b/ui/classic/design/02-metamask-AccDetails.jpg deleted file mode 100644 index d7d408ffc..000000000 Binary files a/ui/classic/design/02-metamask-AccDetails.jpg and /dev/null differ diff --git a/ui/classic/design/02a-metamask-AccDetails-OverToken.jpg b/ui/classic/design/02a-metamask-AccDetails-OverToken.jpg deleted file mode 100644 index f26ff31e8..000000000 Binary files a/ui/classic/design/02a-metamask-AccDetails-OverToken.jpg and /dev/null differ diff --git a/ui/classic/design/02a-metamask-AccDetails-OverTransaction.jpg b/ui/classic/design/02a-metamask-AccDetails-OverTransaction.jpg deleted file mode 100644 index 8a06be6b9..000000000 Binary files a/ui/classic/design/02a-metamask-AccDetails-OverTransaction.jpg and /dev/null differ diff --git a/ui/classic/design/02a-metamask-AccDetails.jpg b/ui/classic/design/02a-metamask-AccDetails.jpg deleted file mode 100644 index c37e0f539..000000000 Binary files a/ui/classic/design/02a-metamask-AccDetails.jpg and /dev/null differ diff --git a/ui/classic/design/02b-metamask-AccDetails-Send.jpg b/ui/classic/design/02b-metamask-AccDetails-Send.jpg deleted file mode 100644 index 10f2d27fd..000000000 Binary files a/ui/classic/design/02b-metamask-AccDetails-Send.jpg and /dev/null differ diff --git a/ui/classic/design/03-metamask-Qr.jpg b/ui/classic/design/03-metamask-Qr.jpg deleted file mode 100644 index 9c09de42f..000000000 Binary files a/ui/classic/design/03-metamask-Qr.jpg and /dev/null differ diff --git a/ui/classic/design/05-metamask-Menu.jpg b/ui/classic/design/05-metamask-Menu.jpg deleted file mode 100644 index 0a43d7b2a..000000000 Binary files a/ui/classic/design/05-metamask-Menu.jpg and /dev/null differ diff --git a/ui/classic/design/chromeStorePics/final_screen_dao_accounts.png b/ui/classic/design/chromeStorePics/final_screen_dao_accounts.png deleted file mode 100644 index 805cc96b6..000000000 Binary files a/ui/classic/design/chromeStorePics/final_screen_dao_accounts.png and /dev/null differ diff --git a/ui/classic/design/chromeStorePics/final_screen_dao_locked.png b/ui/classic/design/chromeStorePics/final_screen_dao_locked.png deleted file mode 100644 index 9d9e33930..000000000 Binary files a/ui/classic/design/chromeStorePics/final_screen_dao_locked.png and /dev/null differ diff --git a/ui/classic/design/chromeStorePics/final_screen_dao_notification.png b/ui/classic/design/chromeStorePics/final_screen_dao_notification.png deleted file mode 100644 index d56a5ce62..000000000 Binary files a/ui/classic/design/chromeStorePics/final_screen_dao_notification.png and /dev/null differ diff --git a/ui/classic/design/chromeStorePics/final_screen_wei_account.png b/ui/classic/design/chromeStorePics/final_screen_wei_account.png deleted file mode 100644 index d503ff301..000000000 Binary files a/ui/classic/design/chromeStorePics/final_screen_wei_account.png and /dev/null differ diff --git a/ui/classic/design/chromeStorePics/final_screen_wei_notification.png b/ui/classic/design/chromeStorePics/final_screen_wei_notification.png deleted file mode 100644 index 3560c51ff..000000000 Binary files a/ui/classic/design/chromeStorePics/final_screen_wei_notification.png and /dev/null differ diff --git a/ui/classic/design/chromeStorePics/icon-128.png b/ui/classic/design/chromeStorePics/icon-128.png deleted file mode 100644 index ae687147d..000000000 Binary files a/ui/classic/design/chromeStorePics/icon-128.png and /dev/null differ diff --git a/ui/classic/design/chromeStorePics/icon-64.png b/ui/classic/design/chromeStorePics/icon-64.png deleted file mode 100644 index 7062cf4f1..000000000 Binary files a/ui/classic/design/chromeStorePics/icon-64.png and /dev/null differ diff --git a/ui/classic/design/chromeStorePics/metamask_icon.ai b/ui/classic/design/chromeStorePics/metamask_icon.ai deleted file mode 100644 index 27400c5a4..000000000 --- a/ui/classic/design/chromeStorePics/metamask_icon.ai +++ /dev/null @@ -1,2383 +0,0 @@ -%PDF-1.5 % -1 0 obj <>/OCGs[5 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream - - - - - application/pdf - - - metamask_icon - - - Adobe Illustrator CC 2015 (Macintosh) - 2016-06-15T14:23:12-04:00 - 2016-06-15T14:23:12-04:00 - 2016-06-15T14:23:12-04:00 - - - - 240 - 256 - JPEG - /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAADwAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXnP5r/mvB5Tg/RmnAT6/cJyUMKx28bVAkf+ZjT4U+k7UDYuo1HBsObl6bTce5+l5X+Wf5t6jonm KZtfu5rzTNVcG+lkZpHilACLOAamgUBWC/sgUrxAzEwakxl6uRczUaYSj6eYfS9vcQXEEdxA6ywT KskUimqsjCqsCOoIObUG3UkUvxQ7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXln5rfnHb+X1l0bQnWfXCCs0+zR2tfEGoaTwXoO/hmJqNTw7Dm5mm0plvL6fvfO U889xPJcXEjTTzM0ksshLO7saszMdySTUk5qybdsBSzAl7R+Rv5ni0dPKutTqto5ppNw/KqyOwH1 c0BHFuVVJpTpvUUz9Jnr0n4Ov1mnv1D4ve82LrHYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FUn1Pzl5W0yF5r3VLeNI9no4kYfNU5N+GY89XijsZC/mfkHIhpMstxE18h8ynCmqg7iorQ7HMhx 3Yq7FXYq7FXYq7FXjf5v/nG2nPL5e8tXAN9Ro9Rv039A7D04WBp6nUM1Ph7fFXjg6nU16Y83P0ul v1S5PAWZmYsxJYmpJ3JJzWu0axV2KuxV9Ifkr+Zq67py6FrF1y121+G3eUjlcwKtQeRPxyIAeXci jbnkc2mlz8Qo83U6vT8J4h9L1PMxwnYq7FXYq7FXYq7FXYq7FXYq7FXYqlN95s8t2I/0jUYQQaFE b1HHzWPk34Zi5Nbhh9Uh9/3OVi0Waf0xP3fex7UfzX0WEOtjBNdSCoR2AjjPgak8/wDhcwcvbWIf SDL7B+v7HOxdi5T9REftP6vtYre/mf5ouD+5eK0UdoowxPzMnqfhmsydsZpcqj7h+u3aY+x8Eedy 95/VTFdc803n1dptVv5powSVjeRmqx7IhNMxPEy5jRJPx2cwY8WEWAB8N0l8gRXnm/z/AKXazpXT 7WX65NCo5II4PjHqVrXk3FDX+btXNtodLETDqddqiYH7H1LnQPOuxV2KuxV2KuZlVSzEBQKknYAD FXhX5t/nOsyzaB5XuCEDBbzVoXZSSrA8Ld0I2qKM/foNt81+o1X8Mfm7LTaT+KXyeI5r3YuxV2Ku xV2KqtpdXNpdQ3VrI0NzA6yQyoaMroaqwPiCMINboIsUX1j+XH5g6f5w0WOcNHDq0I439irbqwoD IiklvTaux7dK1GbnBmEx5ukz4DjPky3Lmh2KuxV2KuxV2KuxVKb/AM2eW7CoudQhDA0KI3qMD7rH yYZjZdbhh9Uh9/3OVi0Waf0xP3fex6//ADY0OHktnbzXTKaKxpFGw8QTyb/hcwMnbWIfSDL7B+Pg 5+PsXKa4iI/afx8WO3/5ra9OJEtYIbVG2RqNJIo/1iQv/C5gZe2sp+kCP2n8fBz8XYuIVxEy+wfj 4sVvdY1a+FLy8muFBLBZJGZQT4KTQZrMmfJP6pE/F2ePBjh9MQPghMqbXYqler+YLPTgUJ9W57Qq en+se2X4dPKfuaMueMPewW+vrm9uGnuH5Ox2G/FR/KoPQZtYQERQdZOZkbL3L/nG7y8Y7PVPMEqj lOy2VqxBDBEpJKQSPsszINu6nNpoYbGTqdfPcRe1ZnuvdirsVdirsVeF/n1+Y96l1N5O04+lCERt Vn35uXAkWBfBOJVmI+1XjsAeWv1ec3wD4uy0eAVxn4PEM17sXYq7FXYq7FXYq7FU18seZNU8uaxD qumymOeKqsBQh0YUZCGDDceI2O+ESlH6TRYmEZbSFh7bbfmL5mubeO4iv6xSqHQ+lD0YV/kzVy7U 1MTRl9kf1Owj2XpiLEftP61T/Hvmv/lt/wCSUP8AzRkf5W1P877I/qZfyTp/5v2n9bv8e+a/+W3/ AJJQ/wDNGP8AK2p/nfZH9S/yTp/5v2n9bv8AHvmv/lt/5JQ/80Y/ytqf532R/Uv8k6f+b9p/W7/H vmv/AJbf+SUP/NGP8ran+d9kf1L/ACTp/wCb9p/W7/Hvmv8A5bf+SUP/ADRj/K2p/nfZH9S/yTp/ 5v2n9bHtW1/WtUeuoXTz8eiGioCNtkUKv4ZDNqsmX6zbdh0uPF9ApL8ob3Yq7FXYq7FWO695pW0d rWyo9wtRJKd1Q+A8WH3ZmYNLxby5OJn1PDtHmw6SR5JGkclnclmY9STuTmzArZ1xN7uhhlmmSGJS 8sjBI0HUsxoAPpwhiS+zvLGhxaF5e0/SIiGFlAkTOoIDuB8b0JNOb1bN5jhwxAdBknxSJ70zybB2 KuxV2KpX5o12HQfL2oaxNxK2UDyKjHiHkpSOOu9ObkL9OQyT4Yks8cOKQHe+Nr6+u7+8mvLyVp7q 4cyTTOaszNuSc0ZJJsu/AAFBRwJdirsVdirsVdirsVdirKvI2tmC6OmzN+5uDWEmlFkp03/mp9/z zX67BY4hzDm6PNR4T1Z5mpdo7FXYq7FXYqg7laSn33y2PJgVLJIdirsVWu6IjO7BUUEsxNAAOpJO IFqTTD9c81yT1gsGaKIH4px8LtT+Xuo/HNlg0gG8ubrs2qJ2ixzM1w3Yqzz8k/L66z5/s2kAMGmK 2oSAkgkwkCKlO4ldDTwBzJ0sOKfucbVz4YHz2fU+bd0rsVdirsVdirxT/nIzzWi2ll5ZtZ1Mkj/W dRjQnkqoB6KPTajli9D/ACqcwNbk2EQ7DQ49zIvB81zs3Yq7FXYq7FXYq7FXYq7FW0d0dXRirqQV YGhBG4IIxItQXp3ljWjqunc5Cv1qI8J1Xb/Van+UPxrmh1WDw5bcnc6fNxx35pvmO5DsVdirsVQ1 2v2W+gnJwYlD5YxdiqheXttZwGe4cRxjap6k+AHc5KEDI0GM5iIssE1fzBeakeB/dWw6Qqevux7n Nth08Ye91eXPKfuSzL2h2KuxV9If84/eVk03ys+tyEm51lqhSKcIYHdEArv8Zq3uKZtdHjqN97qN bkuVdz1PMtw3Yq7FXYqp3V1b2ltNdXMgit4EaWaVjRVRByZifAAYCaSBez418169Nr/mPUdYl5Vv Z2kjVyCyRVpEhIp9iMKv0Zo8k+KRLv8AHDhiB3JVkGbsVdirsVdirsVdirsVdirsVTPy7q50vU45 2J9B/guFHdD36H7J3/DKNTh8SFdejdgy8Er6PUYpY5okljblHIoZGHQqwqDmhIINF3QNiwuwJdir sVUrlaxH23yUeaCg8tYJfq2t2Wmx/vW5TleUcC/abtv/ACj3OXYsEp8uTVlzRhz5sD1DULm+uGnu GLE/ZX9lR/KozbY8YgKDqsmQyNlDZNg7FXYqiNN0+51HUbXT7UBrm8mjggUmgLysEWp7bnDEWaRK VCy+0tM0+307TbXT7YEW9nDHbwgmp4RKEWp+QzfRFCnnpSs2UThQ7FXYq7FXmX59ebIdL8ovpEMw Goauyx+mrFXW2U8pH2/Zbj6dD15HwOYmryVGupczR4uKd9A+ac1Tt3Yq7FXYq7FXYq7FXYq7FXYq 7FXYqzXyJrSem2lztRgS9sSQAQT8SD3ruPpzV6/Bvxj4ux0Wb+EsxzWuwdirsVadeSlfEEYhDE9b 8zwWLNb24E10pKuK/AhHjTqa9s2ODSme52Dh5tSI7DcsJmmlmkaWVy8jmrOxqTm0AAFB1hJJsrMK HYq7FXYq9S/5x+8qvqXmp9bkIFtoq1CEV5zTo6IBXb4RVq9jTMzR47lfc4WtyVHh730jm0dS7FXY q7FXYq+X/wA+dQmuvzGu4JPsWMFvBF/qtGJ/+JTHNTq5Xk9zudHGsY83nmYrlOxV2KuxV2KuxV2K uxV2KuxV2KtqrMwVQSxNABuSTirN/Lfl9LBVurhQ1626g7iMeA/yvE/R89VqdRx7D6XZ6fT8O55s tUggEdDuM1zmt4pdirsVYZ5s8s+pLJfWS0lNXmhH7fcsv+V4jv8APrs9JqaHDJ1+p01+qLDM2brn Yq7FXYq7FX0n/wA48to48kyR2cwfUPrLyanEdmjZvhiA2rwMaAj35ZtdHXBtzdRrr49+XR6hmW4b sVdirsVdir5I/Ne/+vfmJrs1QeFx6G3/AC7osP8AzLzTag3Mu800axhieUN7sVdirsVdirsVdirs VdirsVdirMPLHl8wAXt4hE5/uYmFCg/mI8f1ZrdVqL9MeTsdNgr1HmyXMJzEXatWOndf1ZVMbswr ZFLsVdiqGu13VvoOTgWJYV5o8vFS9/aL8O7XEQ7eLj28c2ml1H8MnXanT/xBi+Z7guxV2KuxVP8A yLf+abLzPZP5ZDyarI4SO3XdZVO7JKKgenQVYkjiPiqKVFuKUhIcPNqzRiYni5PsKIymJDKFWXiP UCElQ1N6EgEivtm7dCuxV2KuxV4P+Zn5i/m7o189tNbRaNYszi2urVBOsqEkD/SJAw5bV2VG8QNs 12fNlie4Oy0+DFId5eL3FxPczyXFxI01xMzSTSuSzu7GrMzHckk1JzBJt2AFLMCXYq7FXYq7FXYq 7FXYq7FXYqyvyv5fZWF9ex0IobaNvv5kfq/2s1+q1H8Mfi5+mwfxS+DKswHOdiqrbuVkA7Nsf4ZG Q2SEZlTN2KuxVTnTlEfbcfRhid0FBZcwYd5k8uNAz3tkg+rdZYl6oe7KKfZ/V8umy02pv0y5uu1G nr1R5MbzNcN2KuxVnH5Z/mdP5MuXjayhudPu5Fa9cJS6CAEUjkqoIFa8W2/1ak5kYM/B02cbUafx Ou76X8s+ZdL8x6RDqumGU2s32TLG8R5DZl+IUbi1VJQlajrm1hMSFh1GTGYGimmTYOxV2KqN9HZS Wk0d8sT2boVuEnCmIodiHDfDT54DVbpF3s+b/wAyfI/kCy9fU/LvmWzJZix0f1BOQSWJWF4OZXsq q6/N81efFAbxkPc7bBmmdpRPveZZiOY7FXYq7FXYq7FXYq7FXYqybyz5cMhjv7xaRD4oYSPteDN/ k+Hj8uuDqdTXpi5um09+osvzXOwdirsVdiqYIwZA3iMoIZt4pdirsVS9l4sV8DTLg1tEAih6YVYT 5k8vGzY3dsC1qxJdf99knpt+z4ZtNNqOLY83W6jT8O45JBmW4jsVZP8Alp5c07zH5z0/SNQd1tZz I7rH1f0o2l4V/ZDcNzl2CAlMAtOomYQJD65gggt4I7e3jWGCFRHFFGAqIiiiqqjYADYAZugKdGTa /FDsVdirHfNvkDyv5rjUava87iNGSC7iYxzRhvBhs1DuA4I9sqyYYz5tuLNKHJ4v5q/5x58xWBlu NAuE1S1G6270iuQCTtv+7fitN+QJ7LmDk0Uh9O7sMeuifq2eUzQzQTPDMjRTRMUkjcFWVlNCrA7g g5hkU5oNrMCXYq7FXYq7FXYq7FWR+XfLJuAl5eDjBUGKEjdx4nwX9fy64Wo1NemPNzNPpr9UuTMs 1rsXYq7FXYq7FUVavVSh6jcfLK5hkFfIMnYq7FUHdLSWviK/wyyHJgVLJoWuiOjI4DIwKsp3BB2I OINKRbB/MPl19Pb6xb1ezY713MZPY+3gfo+e10+o49j9Tq9Rp+DcckkzKcZmP5P3EsH5k6G8cTTM ZZIyqgkhZIXRm27IrFj7DL9Mf3gcfVC8ZfWWbl0jsVdirsVdirwr87POX5jaTqj6dFIdP0O4VTaX dorK8o6lXuDusgZTVUI+HrUHfX6rLOJrkHZaTFjkL5l4jmvdi7FXYq7FXYq7FXYqn/lzy6t8purr kLZTREG3Mg77/wAvbbMTU6jg2HNy9Pp+Lc8maqqooVQFVRRVGwAHYZqyXZAN4q7FXYq7FXYqvhfj Ip7dD9ORkNkhHZUzdirsVULpKoG/l/jkoFiULlrF2KrJYYpozHKiyRt9pGAIP0HCCQbCCAdiwTzD obabcB4qm0lJ9M7nif5Sf1ZttPn4xvzdXqMPAduT6E/Jz8tofLejx6pqMStrt+iyNzSj20bLtCOY DK9G/edN/h7VO+02DhFnmXn9Vn4zQ+kPSMynEdirsVdirsVS7zBoGl6/pM+l6nCJrScUI6MrD7Lo ezKehyM4CQos4TMTYfKfn3yJq3lDWHs7pWkspCTYX1KJMgp4Vo61oy9vlQ5p82EwNF3WHMMgsMYq MpbnVGKuxVvFXYqnnlvQDfSfWbhSLRDsP9+MOw9h3zF1Oo4BQ5uVp8HEbPJm6IiIqIoVFACqBQAD YADNUTbswKXYq7FXYq7FXYq7FXYqjoX5xg9+h+eUyFFmF+BLsVWyLyRl8RtiChAZewdirsVTjyfZ JeeZ9NidOarOkpUio/dH1Afo45mdnx4s8R5/du4faE+HBI+X37Pds7N4t2KuxV2KuxV2KuxV5Z+Y esC91gWcZBhsKpUb1kahf7qcfozke2dTx5eEcoff1/U9b2PpuDFxHnP7un62K5p3buxV2KuxV1Bi qFukowcdDsfnlkCxKhk2LsVdirsVdirsVdirsVRFo/xFfHcZCYZBE5WydirsVQEq8ZGHv+vLgdmB W4UOxVmH5WQCTzOzn/dFvI4+kqn/ABvm27GiDm90T+h1PbUiMI85D9L17OpeVdirsVdirsVdiqG1 K/i0+wuL2X7ECFyK0qR0Ue7HbKs+UY4GZ6Btw4jkmIjqXh000k0zzSsWkkYu7HqWY1Jzz+UjIknm XvYxEQAOQWYGTsVdirsVdiq2ROaFfHp88INIKAIIJB6jrlrB2FXYq7FXYq7FXYq7FV0bcXDeBwEJ CPylm7FXYqhbtTyDdiKfTlkCxKhk2LsVZ7+UduW1S+uO0cCx/TI4P/MvN32HC5yl3Cvn/Y6PtydQ jHvN/L+16jnSPNuxV2KuxV2KuxVhP5l60IrOPSY6+rccZZj29NSeI+l1r9GaHtzUgQGMc5bn3f2/ c73sTTEzOQ8o7D3/ANn3vOM5d6d2KuxV2KuxV2KuxVCXiFT6iqWB+1Sn8csgejEhBfW4/Bvw/rlv Chr64n8px4Va+uD+T8ceBWvrv+R+P9mHgVv67/kfj/ZjwK19cP8AL+OPArX1yT+UY8KtfXJfBfx/ rjwhU2s5Ge3Ut9rv/DMeYosgr5FLsVUL3l9WZlFWX4hX26/hkoc0FKDdSnwHyGZPCGKhZ6oLy3We CTlGxIBoBupKnt4jLMuA45cMhu1Yc0ckeKPJ6F+UmpSxapdQMapPGrGvjG1BT/kYc2vYs6nKPeL+ X9rqO3IeiMu418/7HsA3GdE807FXYq7FXYq7FXi/mfVP0nrl1dA1i5cIaGo9NPhUj/Wpy+nOF7Qz +LmlLpyHuH4t7jQYPCwxj15n3n8UlWYbmOxV2KuxV2KuxV2KuIB2PTFUDdWaV5caqe/cZbCbAhAy WjCpQ8h4d8tElUCCDQ9ckrWKpZrHmHTtKUeuxeY9II6F6eJBIoMztJoMmf6dh3nk4Os7Rxaceo2e 4c2Far5x1a+5JE31W3bb04z8RHu/X7qZ0ul7IxYtz6pef6v7XltX2zmy7A8EfL9f9id+R9d9WP8A Rc5q8YLW7kjdR1Tfeo6j2+WaztrRcJ8WPI8/1/jr73adh6/iHgy5jl7u78dPcy5RyYL4mmc+9GnN o1HK9iP1ZjzCQisrZOxVp1DoynowIPyOIKscl/dc+ewSvL2p1zNiL5NZNCy888na79QvDa3DgWlw d2Y0CPTZvDfofo8M67tfQ+LDiiPXH7Q8b2Nr/CnwSPol9h7/ANb2PytcvZ6rYSqwX96odu3FzRq/ Qc5fRZTDPEjvr57PT6/GJ4ZA91/J79A3KJT7Z2bxS/FXYq7FXYqk/m7VRpug3UyvwnkX0oN6Hm+1 V91FWzC7Rz+FhkevIe8/i3N7PweLmiOnM/D8U8azhnt3Yq7FXYq7FXYq7FXYq7FXEAih6Yqg54Ch 5L9j9WWRlbAhQeNHFGFcmChJ9b0DXL6ALpN8lt2kVwysfcSLyI+hfpzP0WqwY5XliZfju/b8HB12 DPkjWKQj+O/9nxYTe/l55tilc+gt11Zpo5VPInc7OUcn6M6XD23pSBvw+RH6rDzGXsXUgnbi8wf1 0Uiu9K1SzAa7s5rdTsGljZAfkWAzZYtTjyfTKMvcQ67Jp8kPqiY+8KNrczWtxHcQNwliYMje4yeX HGcTGXIscWWWOQlHmHrei39tqUMVzAQyMKuvdGAqVb3GcDqsEsMjGX9vm+g6bUxzQE4/2eSco3Fw 3gcwyHITAEEAjodxlLJ2KXYqx/X7J5UubeJgj3MThHaoVWcFakgHau+Z2kyiMoyPKJH2OPqMZnjl EcyCEB5f/LjSLBEm1AC+ux1Dbwqd+iftf7KvyGZ2t7dy5DUPRH7fn+p1ej7DxYxc/XL7Pl+tkDoI 3KqAoX7IGwA7UzUg3u7iq2fQWlzGfTbadl4mWJHK+HJQaZ30JcUQe8PAzjwyI7iiskxdirsVdirz L8y9S9fV4rFSeFmnxj/iySjH/heOcp25n4sogP4R9p/ZT1PYmDhxmZ/iP2D9tsPzSO7dirsVdirs VdirsVdirsVdiriARQ9MVQc8BT4hun6ssjK2BDdq1JCP5h+IxmNkhF5WydiqAu9A0O8Ltc2FvLJJ 9uQxrzP+zpy/HMnHrc0KEZyAHnt8nGyaPDO+KEST5b/NRsPLOlaYH/R0RgEn205u6kjvRy1D8snn 12TNXiG68h+hjp9Hjw2MYoHzP6VVlZTRhQ5SC5CJt5l4hCaEdK98hKKQVfIMnYqo3dstxEV2DjdG 8DkoSooKhp8jrW2mqJE3UH+XJZB1ChddLSWviMYcmJfQsESwwRxL9mNQo+QFM9BAfPyV+FDsVdiq jeXdvZ2st1cOEhhUs7HwGQyZIwiZS2AZ48cpyEY7kvD7+7kvL2e7kFHuJGkYDoORrQfLOAzZDOZk ept73FjEICI6ClDK2x2KuxV2KuxV2KuxV2KuxV2KuxVxAIoeh64qhZIjE4dd0B+7LAbY1SKBBAI6 HplbJ2KuxV2KrJI1kWh69jhBpBCElhaM77jscsErYkK8NwGor/a7HxyEopBV8iydiqhcW5kKyRkL Mn2GPT3ByUZVz5IbVDcyQLTizuI2U9mYgZZijcuEdWGSXDEnufQeegPn7sVdirsVYb+ZmqGDS4dP Q/Fdvyk6f3cRBp47tT7s0fbmfhxiA/iP2D9tO67EwcWQzP8ACPtP7LeaZyr1TsVdirsVdirsVdir sVdirsVdirsVWvJGgq7BR2qaYgEoQc2qW4BVVMn4A/x/DLRiKLVIbscAGQjbpWtPbtgMFtEJIj/Z NfbvlZFJtdil2KuxVogEUIqMUIWa3K1ZN18O4yyMkEKkFxyoj9ex8cEoqCr5Bk7FUTpEdv8Apmxe YqkQuYWmZjReIcVJJ2+z3zI0kgMsCeXEPvcfVRJxSA58J+57pnfPBuxV2KuxV5B531M3/mK4INYr b/R46eEZPL/hy2cV2rn8TOe6O3y/bb2fZeHw8A75b/P9lJDmudi7FXYq7FXYq7FXYq7FXYqoSXtq nWQE+A3/AFZIQJRaEk1c7iOP5Fj/AAH9csGHvRaFkv7t+shA8F2/VvlgxgLagSSanqckhVtY+UnI 9F3+nBIqjcrQ7FVRZ5V/aqPA75ExCbVUuwdnFPcZEwTauro32SDkCEt4pdiqhNbhqsmzeHY5KMmJ DoJzXhJs3YnDKPUKCr5Bk7FWd+RvOPp+npOoyfu9ltJ2/Z7CNj4fy+HTpnQ9k9pVWKZ/qn9H6vk8 92r2dd5YD3j9P6/m9CzpXnHYq7FXhnnLS30vzHeW4BWF3M1vQED05PiAX2U1X6M4vX4PDzSHTmPj +Ke00GfxMMT15H4fi0l5N4nMOnMdybxONK7kfHFXVPjirVT44q6p8cVdU+OKuqcKrZEEiFT9B8Di DSoB0ZGKt1GWgpW4q7FWwCTQdT0xVHxR+mgXv1Jysm0L8CuxV2KuxV2KqiXEq96jwORMQm1dbpD9 oFfxGQMCm1VWVhVSD8sjSVssSyDfY9jhBpSFkbsh4S9f2W7HCRfJVbIpdir0fyR5zW4WPStRci5A 421wxr6ngjH+bw8fn16jsvtTjrHk+roe/wAvf9/v58x2n2Zw3kx/T1Hd5+77vdym2b50TsVef/m1 pJktbTVY1FYCYJyAa8X3Qk+CtUf7LNH21guImOmx/H45u97Ez1IwPXcfj8cnmOc49G7FXYq7FXYq 7FXYq7FXYqpTw+otR9odMINKgiCDQ9csS1iqItI6vzPRenzyMiqLyCHYq7FXYq7FXYq7FXYq4Eg1 HXFVVLmRdj8Q9+uRMQm1UXETijinz3GR4SE2vRgopXkg6N1p88iUoTWNf0bRoBNqd3Hao1eAc1Zq UrxQVZqV3oMtw6fJlNQFtWbUQxC5mmMXn5w+TbYqYJbi8J7wRFeP/I4xfhmwx9i6g86j7z+q3Ayd sYByuXuH66ehfl//AM5Q+UtVuk0rzAH0mT4Y7XUp6GGToo9dgW9JjWpY/B1qV79Tp4zEAJkGQeX1 BgZkwFRe3wzRTRJNC6yRSANHIhDKyncEEbEHL2hC6xpcGq6ZcafOSIp1oWHUEEMp+hgDlWfCMkDA 8i24MxxTExzDwG5t5ra5ltpl4zQO0ci9aMh4kfeM4ecDEkHmHuYTEoiQ5FTyLJ2KuxV2KuxV2Kux V2KuxVDXMNayL/shkolKGAJIA6nYZNUwRAihR2yolC7FXYq7FXYq7FW1VmPFQWJ6AbnEC+Skgc0V DpGpzbpbPTxYcR/w1MyIaTLLlEuPPV4o85BEDy3rJNDBT3Lp/A5aOzs3837Q1HtHD/O+wq48pamR UvCPYs38Fy4dk5e+P4+DSe1sXdL7P1q8Xk+cj97cqp/yVLfrK5ZHsiXWX4+xql2vHpH8farR+Tog f3l0zDwVAv6y2Wx7IHWX2Ncu1z0j9quvlLTlIPqzVH+Uv/NOWfyTi75fZ+pq/lbL3R+39b5x/OyS VfzBvrLmWt7JII7ZDT4VeFJW6UrV5Cc2uk00MUKiHV6rUTyyuRYJmS4zsVfU/wDziJp/mFdA1bUJ 72QaC8/oWOnHiUNwqq00+68h8JRBxeh+LkKgHCgvoLFDx/8AM3SBZeYfrMakQ36erWlF9RfhkA8e zH55yna+Dgy8Q5S+/r+v4vV9kZ+PFwnnHb4dP1fBiOat2rsVdirsVdirsVdirsVdiqvaWF9euUtL eW4cdViRnI+fEHJ48Up/SCfcwyZYw+oge9PY/wArfMqQrdskahhX0ORaVK+KqD+GbQdkZjGzQdbL tnCDQstDybIrFJboJKv2k4EkfOrKckOxz1l9jWe2B0j9v7FaLyfbj++uHf8A1AF/XyyyPZEesj+P m1y7Xl0iPv8A1Ky+U9MBqXlYeBZf4KMsHZWLvl+Pg1HtXKekfx8VceW9G/5Z6+/N/wDmrLv5Ow/z ftP62r+Uc3877B+pXTSNLQUFrER/lKGP3tXLY6TEP4R8mqWryn+I/NWis7SI1igjjPiqgfqGWRww jyAHwapZpy5kn4q2WNbsVdirsVdirsVdir5U/O7/AMmdrP8A0bf9QsWZEOTRPmwbJMU28p+XL3zL 5l03QbIH6xqNwkAcKX9NWPxysq78Y0q7ewOKv0B8teXNJ8t6FZ6HpEPoafYpwgjJLHclmZierMzF ifE4WKZYqxn8wtFj1Hy7PMIw11YqZoHrSiggyj6UB28QM13amAZMJPWO/wCv7HY9l6g48wH8Mtv1 fa8XzkXr3Yq7FXYq7FXYqmOm+Xdc1Ir9SspZkbYS8eMe3/FjUT8cvxaXLk+mJP3fNx82qxY/qkB9 /wAubK9I/KjUpZEfVJ0t4OrRRHnL8q04D51ObTB2LMm8hoeXP9X3usz9tQArGLPny/X9zL9N/L3y tYgH6r9akH+7Lk+pX5rtH/wubTF2Zgh0s+e/7PsdVm7Tzz60PLb9v2sghhhhiWKFFjiQUSNAFUD2 A2zPjEAUOTgSkSbPNfhQo3NnaXScLmFJV3oHANK+HhjSQUovPKNhIpNo720gHwgMXSvuG5fgcrOM MxkKQ3fl7zBa1IjW6Qb8o9zT/V+E/cMgcZbBkCXNdCNzHNG8Ug2ZWFCCPHvkKZ2vW4gbo4+nb9eB VTFLsUOxV2KuxV2KuxVpnVBViAPE4q8U89flBq3mfzvf6yt/b2un3Xo8Kh3mAjhSNqpRF6pt8eWx nQa5Qsr7b/nH3yssai51C+llH2mjaKNT/sTHIR/wWPiFfDD178qfyf8AKnli6/Ttrp3p35jMVrcT SPI4jcDm4ViVUsNgwANKjocnC+rXOuj1DJsHYq0yqylWAZWFGU7gg9sVeBa/pbaXrN3YN0gkIQ+K H4kP0qQc4fVYfDySj3H+x7nS5vExxl3j+1L8ob21VmYKoJYmgA3JOICkp5p3kjzRfjlFYPHHt8c9 IhuKggPQn6Bmbi7OzT5Rr37OFl7RwQ5yv3bsp0z8o3qj6nfLQN8cNupNV9pG40/4DNli7E6zl8B+ v9jrM3bnSEfif1ftZlp3lLy5p9Da2EQdSGWRx6jgjuGfkR9GbbFosOP6Yj7/AL3U5dbmyfVI/d9y bZlOK7FXYq7FXYq7FXYq7FVKe1tbheM8KSgdA6huvz+WNKCkWoeSdNnFbVjav4CrqfoJr+OQOMNg yFILvylrlpVolE6AVLQtv16cTRvuyswLYMgSx5b23k9OZWRx1SRSp/GhyBDMFUXUF/aQj5Gv9MaV VW7ganxUJ7EYFVVZWFVII8RviriQBUmgHUnFUNNfINo/iPiemGlQTu7mrmpxVrFWReVfLr3cyXt0 g+poaorb+ow26fyg9a/LxyyEba5zrZneXNDsVdirsVYV538iXeuajBe2Uscb8PSnEpIFFJKsOIap 3pmo7Q7OlmmJRIG1G3cdndpRwwMZAnexShp35S6ZEQ1/eS3J2PCICJfcGvMn6KZDF2JAfVIy+xnl 7bmfpiI/b+plmk+X9H0lWXT7VYOf22BLMfYsxZqfTmzwabHi+gU6vPqcmX6zaYZe0OxV2KuxV2Ku xV2KuxV2KuxV2KuxV2KuxVRu7K1vITDcxiSM9j/AjcYCLSDSQ3vkbTpSWtZHtif2f7xfuJDf8NkD jDMZCkV55O1m3HJEW4UVJMR3FP8AJbifurkDAsxkCXjRtX/5Yrjb/ip/6YOEsuIK40PX5lANpKQO nIcev+tTHgK8YVB5S8wEf7y/8lI/+asPAUcYVU8ma4w3RE9mcfwrj4ZR4gRlr5EvfXT61NGIK/vP TLF6eAqoGSGNByBmccaRRrHGOKIAqqOwAoBlrSuxV2Kv/9k= - - - - proof:pdf - uuid:65E6390686CF11DBA6E2D887CEACB407 - xmp.did:d4d07395-aa96-47c2-a9e5-d0351947bb0c - uuid:c63c1031-e157-9748-9c58-86481308e954 - - uuid:1abccb90-0c26-4942-b156-fd2eb962e3e1 - xmp.did:58fdc1b8-1448-3a44-9e20-282d8ec1cf95 - uuid:65E6390686CF11DBA6E2D887CEACB407 - proof:pdf - - - - - saved - xmp.iid:d4d07395-aa96-47c2-a9e5-d0351947bb0c - 2016-06-15T14:23:10-04:00 - Adobe Illustrator CC 2015 (Macintosh) - / - - - - Web - Document - 1 - True - False - - 128.000000 - 128.000000 - Pixels - - - - Cyan - Magenta - Yellow - Black - - - - - - Default Swatch Group - 0 - - - - White - RGB - PROCESS - 255 - 255 - 255 - - - Black - RGB - PROCESS - 0 - 0 - 0 - - - RGB Red - RGB - PROCESS - 255 - 0 - 0 - - - RGB Yellow - RGB - PROCESS - 255 - 255 - 0 - - - RGB Green - RGB - PROCESS - 0 - 255 - 0 - - - RGB Cyan - RGB - PROCESS - 0 - 255 - 255 - - - RGB Blue - RGB - PROCESS - 0 - 0 - 255 - - - RGB Magenta - RGB - PROCESS - 255 - 0 - 255 - - - R=193 G=39 B=45 - RGB - PROCESS - 193 - 39 - 45 - - - R=237 G=28 B=36 - RGB - PROCESS - 237 - 28 - 36 - - - R=241 G=90 B=36 - RGB - PROCESS - 241 - 90 - 36 - - - R=247 G=147 B=30 - RGB - PROCESS - 247 - 147 - 30 - - - R=251 G=176 B=59 - RGB - PROCESS - 251 - 176 - 59 - - - R=252 G=238 B=33 - RGB - PROCESS - 252 - 238 - 33 - - - R=217 G=224 B=33 - RGB - PROCESS - 217 - 224 - 33 - - - R=140 G=198 B=63 - RGB - PROCESS - 140 - 198 - 63 - - - R=57 G=181 B=74 - RGB - PROCESS - 57 - 181 - 74 - - - R=0 G=146 B=69 - RGB - PROCESS - 0 - 146 - 69 - - - R=0 G=104 B=55 - RGB - PROCESS - 0 - 104 - 55 - - - R=34 G=181 B=115 - RGB - PROCESS - 34 - 181 - 115 - - - R=0 G=169 B=157 - RGB - PROCESS - 0 - 169 - 157 - - - R=41 G=171 B=226 - RGB - PROCESS - 41 - 171 - 226 - - - R=0 G=113 B=188 - RGB - PROCESS - 0 - 113 - 188 - - - R=46 G=49 B=146 - RGB - PROCESS - 46 - 49 - 146 - - - R=27 G=20 B=100 - RGB - PROCESS - 27 - 20 - 100 - - - R=102 G=45 B=145 - RGB - PROCESS - 102 - 45 - 145 - - - R=147 G=39 B=143 - RGB - PROCESS - 147 - 39 - 143 - - - R=158 G=0 B=93 - RGB - PROCESS - 158 - 0 - 93 - - - R=212 G=20 B=90 - RGB - PROCESS - 212 - 20 - 90 - - - R=237 G=30 B=121 - RGB - PROCESS - 237 - 30 - 121 - - - R=199 G=178 B=153 - RGB - PROCESS - 199 - 178 - 153 - - - R=153 G=134 B=117 - RGB - PROCESS - 153 - 134 - 117 - - - R=115 G=99 B=87 - RGB - PROCESS - 115 - 99 - 87 - - - R=83 G=71 B=65 - RGB - PROCESS - 83 - 71 - 65 - - - R=198 G=156 B=109 - RGB - PROCESS - 198 - 156 - 109 - - - R=166 G=124 B=82 - RGB - PROCESS - 166 - 124 - 82 - - - R=140 G=98 B=57 - RGB - PROCESS - 140 - 98 - 57 - - - R=117 G=76 B=36 - RGB - PROCESS - 117 - 76 - 36 - - - R=96 G=56 B=19 - RGB - PROCESS - 96 - 56 - 19 - - - R=66 G=33 B=11 - RGB - PROCESS - 66 - 33 - 11 - - - - - - Grays - 1 - - - - R=0 G=0 B=0 - RGB - PROCESS - 0 - 0 - 0 - - - R=26 G=26 B=26 - RGB - PROCESS - 26 - 26 - 26 - - - R=51 G=51 B=51 - RGB - PROCESS - 51 - 51 - 51 - - - R=77 G=77 B=77 - RGB - PROCESS - 77 - 77 - 77 - - - R=102 G=102 B=102 - RGB - PROCESS - 102 - 102 - 102 - - - R=128 G=128 B=128 - RGB - PROCESS - 128 - 128 - 128 - - - R=153 G=153 B=153 - RGB - PROCESS - 153 - 153 - 153 - - - R=179 G=179 B=179 - RGB - PROCESS - 179 - 179 - 179 - - - R=204 G=204 B=204 - RGB - PROCESS - 204 - 204 - 204 - - - R=230 G=230 B=230 - RGB - PROCESS - 230 - 230 - 230 - - - R=242 G=242 B=242 - RGB - PROCESS - 242 - 242 - 242 - - - - - - Web Color Group - 1 - - - - R=63 G=169 B=245 - RGB - PROCESS - 63 - 169 - 245 - - - R=122 G=201 B=67 - RGB - PROCESS - 122 - 201 - 67 - - - R=255 G=147 B=30 - RGB - PROCESS - 255 - 147 - 30 - - - R=255 G=29 B=37 - RGB - PROCESS - 255 - 29 - 37 - - - R=255 G=123 B=172 - RGB - PROCESS - 255 - 123 - 172 - - - R=189 G=204 B=212 - RGB - PROCESS - 189 - 204 - 212 - - - - - - - Adobe PDF library 15.00 - - - - - - - - - - - - - - - - - - - - - - - - - endstream endobj 3 0 obj <> endobj 7 0 obj <>/Resources<>/ExtGState<>/ProcSet[/PDF/ImageC]/Properties<>/XObject<>>>/Thumb 14 0 R/TrimBox[0.0 0.0 128.0 128.0]/Type/Page>> endobj 8 0 obj <>stream -HwVu6PprqV*234R04S32P4ճT(J -W*w6PH/H+X)Hwr.gK>W /@.ӊ endstream endobj 9 0 obj <> endobj 14 0 obj <>stream -8;W:dYmnJk$j=`^PKX*GV"-/6MPPhMW4o*jFegTA5n:ROqi. -8M?-(/t#IN>re.=TbIMqYWQK1D%b&pOLGa]H?hKs'8Gqa4A/k;[i&\e-=4:h!/H6BW;~> endstream endobj 16 0 obj [/Indexed/DeviceRGB 255 17 0 R] endobj 17 0 obj <>stream -8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 -b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` -E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn -6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( -l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 13 0 obj <>/Filter/FlateDecode/Height 947/Intent/RelativeColorimetric/Length 90241/Name/X/SMask 18 0 R/Subtype/Image/Type/XObject/Width 880>>stream -Hoi@Hy&8_nyA'?6G3+ZHҥYakOj6gיoU GHII_AgK/EcF6LrchI 2$҆ԘU4w$5_7BQUm"Ť>&k2W$%Nib;Iߓuavտ,HJ \u.&1ٌ^₞@Ǥl_Lrs:#ј,32] IJ7d+65i1$Lb#d]G>&Y=g답*_/*:p.uʙcRIf") ˬ#q4Ό=sL&=(P{ HJ+b~n+cSFsm0'&&cܼXI=3zER,D#0)2=r -I Ә}즟 (9?l?ݳ;݃Q~twoo `41)"g476WxzGMݞx7hpqh{ƃn\ w Zᶂ37{M23>)25Eܩo|+>8q/8m y3=??~wL#I\dΕ/doޱ=Hh|d]ү$*wOc} yz<*\@~R/}}FRHxw G]as &lu9x")`m;=-Ƀ -O8Cmȑ{mG.&?){3];,V01o`it4)'ѭU, ]?b<ݳN=;.]Lں*_w6}hL[I$np+bdjlb46[ܩ0k`I{-ɯG_>]zt8rI_K}4јغOinӭng`HUN4ݛ|yRr #+x/>骞SyXAQȃękfUXDvE#&sBe< HQ%\Ωdg'sǟá:>xQ -!K -W<* '%Y%Vmao!ǩkv>w u{=Q<\ȃ*fƸmqY%ŏRpV{ ueW&)!)sE2Jݓ?ҋӆgohԎeɝiFGb}/g. -,%m.7'FX!TՀITV $y9Iפ?_ȼ0;Wi;h9:FQ]itB)`5yIe[j*lraպS"u 3$hۯNT´A}ٷO.ϡqˤ3!"Iڻkha˳J)@) iq1J٦oչrEIWt+>]hlrW9,-nr_}i#eR=椔 5 ](?"aIK9;z>g9d68 =FGY/Հ@@ 9ۋFJT2[̟~>:ekG<Q2B&M)}YƢ}\ekfeJ#.-3 -iHF'>hd,I#_ыTj~Q5cR`n:s e8 P/di]Ҩm +!g֝V=@nI${%3Tj[ԣ`Į;m$XOT4==Aŵ͆ޭ|hˑ"6XWvZY,{&y` wɵs/NٮMDz3z2 F^ęA r۝gB7hu ȲhI})CF -WWzٶ:lgl7ɃHiJ&/ Ӻg.}C'dD|V֪'9TL*4I]6 x74MVK%X T8ENZ!f8ah@&͚-AsׄOQ"2sO-ʃ#db-tgHIFHVj.Y!х@Cdҕ@2ǯeqyJΎC43nw0"D2ȥXiQϖJ'&:?Ed!ªGIKSيb$utõǭ2'}~dbNct`d K񕨈\29juC ڣy 5,ҧ9.~&g(r\&$zkddjd_&n,dk딝]|ڷт$lw)-H](H&, tHU4ѐxIE$\+Kl֓ȁI*?^^O/N*)iit<~O&=۠SZ0LK578hsZ?5ĬeV"k K ->#gV-?}= TjOK<>Nh auOBnY#qkB)fiQ@ 7@Olo-n 9 =)~erŗzC9z9Ilr&JO-NbW3Ӳh adR<+}D?NJiPJG%:?5pn2TI2v֋rBk &l f'<[wm}2I4KtwH r"!hͣ .:17f͝$Wq`Z Z 'R\%n~2/f|i|a*J&r>-fj@eRc}'yGBvr*'r ->|.WB~0ɱ{H ܌232ɤMRe7r!ic/_Ȗm͇OJ!dfH)OtE9#jԝj'C]aպWf+>KctȁL&rK%SͧH&1'ejuQA2p!Ϟ* UKW?-02hZ!nKO.? ZdzѤ٣wrLI*΍Sі2+TI,5N$6ぺ G7 whQXI4:?5ƫhq-Ǿ(v,vHz&.aKiݵdTOph2E ɤ0J>-zBb8,A6Mgd͝$K I,E[ 8ƶ0yTS rlS]|ѩ+&_9Ezb(Jr2h+ɓ)⇻A!_:۪.%ٱ4E3w~ sOq9F$~EH(M"0>"C|)4 {edUŭ߿/|}e.tD _(^u)TJ 8([E;ZgbDR!TY;g$÷=W__h8pü8SqO㠸*5:Mb)r2`Ny@:iXg*-X=zb-osW̟Y[V$J|!h|$p6V2LRwsU""YA(\A[u0#0j>k6NZ *P$Idv`;K?29G3@/)hqGaLH&)#f2ݥ:"@ -c1BuUU!hB -m?IXqBf=O-uS]*pb Lp=a d0 '%}QJ1kv-E&Y%͇ѓ!L6y֯-ZNſw@ME<V -+Qf1XGbu.AL}{;j:1XM;`m)ݒr2??bӥ^"T.4{7V:7cO n]&IIʴ׭]׳L&ټ~e?618qW 6$уS-J+j &HR#Y(u3C"vaVO qˤg/{Nd* w4~8`ਡODT -( ƃ(rVu6z0F|iU6_Up_ |7//y e26kaE9JTh<'| e\xy(7QQ1Z)7#5eoӈ0g+ۨwCxc XSg")-nkEJN[* FAKLLR7R.LBME#@* -~U݊tEEVOt7U콊ؾxԜ'isjf=O[ZO ((A>&]r"т|f0`A|0/2}+58{:!ELTǝuB1HzGQ0g[|Q_[VSor^Gy?lD$g=?ȩՕLN9 -K*RYģpu0%K'*- lpD ID2MnݾbL)OYׯR3OF"R j"iO8%{Pqs ?Hee}-XJNm\-H/}GϩZ&L/u>7&I wQ) /+P.bP"$N55B]={2`[Hnk?-s\粓y*dqP9I,1k[`^A/n3ՕcVna-_s%YSM{b FǠoZnkE%yt$mAs%Ev]JW^xA Xk0v_KӉ i$Fߪ/u( jLIO%k/Sr7B>Y,770ݙs)]ubQ9OΈL12$jn*2*쾊O&iV"sr+ 2LjȞCxҜJ 9:Jʌ H(hxA&xk i&Iu$6ԕj:.E Q\-^*%V@V;RM]dLW<}*w&Kߊ{-7@-&;. -C66 @9TBUfI[#v1r`,f/5n TLֹޤosIwT&\ߍ#UBXJ&uo6TE-EHLu[FUf -x謖Xz{FEr6qiVd>սl -\Uv^dKCR&p6kڄo@)ɛzxZZfBv5nFC `r{Lŷy7g2H&;x@kASYQC,29Wp -c!{)r*Rj!&#8ˁScM}Zi*H&Mf$\P -Ŵ\Id eDҐIЍF1|CeH ldԬ6i.2K8t׎&t(Q+ZfB*R&~?g4|W~ !$1[NIkqS(T['iͧ4*m~@?>KT)ΕJ3t -dEÀ."!|g_F>";,o)%OQ~Z2FBt-Ŵ=ݗJ)Rbd/}0i -3%f_,%.u;}oZ_`>19)ۂ֙Ĥ b- 2z[&;BEz1i6Cӯ!G9htj9'I#8ˁB!=*t-T:zTG\2z;F3}ZgLhHӍָ!fiVL:,N0EwHVsR I !-U֐L1#4zvB`V|u 4i$\"&^Xjbޟ mR q6H JER/-N&w',͌ƹӿ8!fI|1TBS?D%}35fW̊CȊ\!/5n:VC1@#&R&2rÚ!"H OS&c1[pUyuN'v96c9)Ӿ/I W%f<}*Gz{-/0D֧)T!LSXva?:n[ʜUX% *R&~o㹤w-dr>و'r*+*1=9)zKy$Sp$"ӔIWcQ@&~!AZۑ[W *'W|쌰95n}ăF.i(xyB [(58|i+&Yjhz>˜2m^?fA)\('N,1^{,gI&}<Ǿ dTVԀaI$7XUkt sU dl:OOٯ<2w)6E =Lq<{ hK|7%FE=ikA(#cia78Hw:;i'}h%Z^N^7VҺuQIHNcMft+ -0 '0$:HJ\ ;``pPL=NM.H1Eb~ԓvsK`TO=hwѓ7|GOYZaȎL7e Ջ[녤&Ɍ;b1lx"Iw!}9pXfv`7xgV~π\\zπD-/؁_~ɬebJz!QyrTS蕴.}?]$i;o"):F)(&ۂS^/Ǧ1IG )!bZ1 p8ĕT-@Kp -m crE?m}F!e_JRPF -7b1T}<x4zV,&읲yeTJ=q#cz>)Rv:5[QГO"5o) c^mXyTعT=%o-oK2U~c͠B>(h1*h|:6Ll' ޑ4 =ui7eGo{s͈KjDE1!3e00R,I %y)1]5ά>#Vgr|%vVV>&I5lS<v Z ;RLj/1'{uO -ؐsz( o?;I&mT@L>`|wdF[pY!=;Yk AR;o^2Lh ~_ٛ|GdO q/zRqcw_}9~eiq$i[?~$N%Y72zىSEx1,HDlEg3JJy}F}7)?'|(GoŤ*isFGO=`r3R8)p/MM$j٪u=9(&˜|m5(t75zM'O \]]ITٱ3u0Ǜe/$iF1Da*b1Tzz_W8/wzg'=srV~@?];(&0H1ʿ[P=kW˻zBf`d.d*XJZC^mt.'h V QLqr9wTҺ辣QEx=D19-d!}?d!}3Z#)nmDzly_|1^Nd`Q0l9'0NnbX9T0ZdWf˂8d=pJl#&BsԨA7FDqڇJZ*レT=eSH'YTo4>doE|~ $#&1¾W` ڑ1)د6:'P}O࿠ne*Fرs|q -(iC4P+ $ -cT6^b-4je˷O|zS~?_qCjRr̖E˓>jEk.C-n#E<IO{vE5ӄ&EN& ݆)-:< I'%}8HwяV4d=Yv 1|Dh*; <Okr(Ny%*~*Yy&WA4!x2zsY-L"=\Le5ƔRFI%IF\3_87 0>hq x2`+IǙBh#R8-w*Ó1YeVO[x!mh?[ <$|`2s2l嚽O EҌX)#Z!Sр4Yoy?ªf8jO1O_9O"%z dy.nNY2u5UV\Q~ɲ|kxrd'?apK tE7s!m`jqZv[>hZ-%6}az,ڜdKɷGM)،xd"6T\l,&c<'Uf; -w&B gw)#„S]\QVQ>$I_jJX,\^YSd|'zD%{o1!䅇qx';qڈ>
khYӷ@mwGyxbr ~=ͱ{9hsۈ!x2< !f!mf" e!ONYW,B-%'>,;} ^&CE"OtzK68]dGRfɈt}B)ILN-^3d[ɿ/3GlV&77ug#K1P)^I\D}&/o>߭SHh+<1Iy_uA^ -sMzC*d\'\z1zADd& -9$Y"?LtzK_14*Y|!ԯ)7$URyuf۲/ɖ$yhs:ڏa<#<(){;qSdLt&}ZHy$yyLܘ: ew}\yYj|aIQ%#?xCE"Oq1Nb5˵I~t֌ZcEb-%Շ8}@i?qqN~ Ndl'\z¤.o !جlݜ(B],YoSO&w"0fr -L&\Pby?ޓur!m FZKGbrΓͲc)eE+WqytT?w ]]}["֫K5Ez~J+T3YnO26hSEH'㷢MO$ta0?j9VuK'/K~CcζI-OV/KѸmkҡuΦ)"&㸔]LyĂX)cML=yn\Ω۬ crї'ma5r(E=pu7< - [rd{d7.`w(d;wr(M=zRy -7]=!Ij9Cidy!NmSiǯƆX9r R:wP<+y^{᬴$eYn;ﷂ2^%)uũ Kw Eߊ. UxlidW)I5Ip΍y%gMGƔd Z9"~t]utڵɲ2E#{Xtp7 #i4i>f-2x jL?bGœ{y9k -AQש'=FE4b2&al6>` -hB");Is*QY9c"1鲒z2klvy0 7>`%JN dXn; ŘWO8@g,צ)_cvH$q\ѾM_@%Ƈ؛XBRcΜJcRΆ1xZU]è-9NEw'c뜠I=]c fi~>?!NI&ļfb2 Z8,W䥌e|a(!me?MQH'cMsY*+!\VuSLQ5Kp#}֓mj:SHú5\bØC)I0> jYn>_+c t57*pT̛6=nW%4EQ4z2~+ɜEO1$|T9c^Jt,Ύ9AŵK2Ɏ'+{,uCL/ڻAZ" -d֕MW.oBgDtʿv(uX4Kp}Gߓ-8,]o^ѓ[J^c(NY]$eo h9[ƣ:1vt᥌e| q'Kp4 b>HAB t2n{'ͩ(*rGOӡz>(-ɘ-d6=г.k؉NO&37{UGɓ*Ysgzҋ>XA[ &f.oqC󢔱gs.$e/;?n>.0&cT51}<;(*FOkE;a\.S+S=˖&EǗo3rLdA˿}yb/IE\E"*;pIыeCbuZ ){ٻ #=I_cN|k)rd@Q?h.3Ed^ts*g5 xQX욮&VO䩪(Idt1>ðh[[AEX̕ϺܓyK{./T˱^>xL,Mo'svy[/*|OJgC{::EQ1SDLk%>>Ѕ<I t -eo$7-gHIΆ(&-^^rs *BadVؓ-W*j͉IvHʞNLO:W%_31dӨXܸvH^|Tݓ+9/Hrdz_wq\e?@MQ̙ݙ"NuhEA(iK̙}v(AHQ"@D(WDUlMĎ-'Eyf&٦Q|~GV ?|mzR3|}pӬ3 QJoJpd\nc\7n,Aײmɏzs ޡ22JywoK2/X'& ځNJ* pbR3|w) A7ɤbrc )#f dp[M_&g][ه?@O*v'd5Ä-`˨[ GOFntLλ=95fPL -&!&;=@OK1Ŏ=5:2J.778&$k4RJGL*sͽ֨+IN,Iij&}`K闍sEvDRϿd}OIb_93Da`(r\|@O@Moߎ$gi)R~cb]_+$H!n~F]1GL*vZFi2T'{ z\ARFMbz~wb1Qe5+zRb25em-3_~Ȯ) ֧I/_Ox^JIQOyT=F9CW鉣)fʴRڴ1[Ig - &NA@Ot\ᏻNoV0[%dRїbۤARJwu oX7h4b0$m~a'U:냰6G8XГOГWuNVL1҅ Qp00bIe΍{֠ 6 =q(B:w6Ñrޯ[?^ORLRIv+/gRJ:A{MAOzIN0n/{Nac5H$,+ԓr~ ~OWllfC+0+ГwځZWBA9%gѓ-y唋w-GF)Uk(5?C%;=GLj/bIIzYw~<HיմFi1GL*t\۵ŤH5$gP!||5Y,_;$8hdẂHh C~aГʜ` 2$R 2-D=yq{IeMИJ)1 wT2#&:=FI'@L&ƥfZDRʲ2n%%AL)~(_"H1IW0J W'91S;nZk PJk3=) {=^)&/75fPOG3-#&7@.d{LO\ ~OywtALL%h//8IeN@7sbHbۼ1W2\jn} bRn@z4M6 -'?Ztw -٫0T?^Г" [od% I'{}q{LII6WeVgROS8 K@D\>&;sp6"l\Z= SԞ89c-|~ۘlr\iƌU?D'3&Haaőom“ߓ?wP9Ô"BH$&;m5wI'6WvyCi~tw g#̵͎.p9 FЧziۈ$)&$#})r%R+ [=ړ~t; ՛!Qײ^Gzr}\10Ouc+#C͂&(_HP(D -d!թXKtkcZ*yͅ (  w¯<\*Db,$=OVp'1K.:|/lӥ+i&o练Ɏ[UlL=t _wHY{D'C%A)Cyt)8tDzPˠ|!B!DDEg ̓~WF/Vs'A\U/! -.a{0Ç)zfnڛ>< -.ĕ#_uMLzb)ZOVfc+UA)" -4D')58=26L">^&Ư~nc#{Uҭ' T Z; $U:ri -_͒K 쳷x#LJ4K\4^mΔX][XVBf@)5:'7}OV2L=Piϲgc0Yh-8iҧVk6\o'Nq|$T($)y6Aߓg O"Hb1flsarEtku*F?L$ |)> R ѸoB(L7H>IwUhc}[3;/)go2qCJ= RHOY$BkѧJ6)b Fl{h-8թrbVyZgcF;HҢt@ʰߓŤA#v齌3BILODxRzI;e5 Rkw'w9OD)Ý$)Cy|O[X)7PG$E,̿6D\GLJ_VO,Zhֻ\/of>!Ç6A0ߓN(+MC d.ia1^j&VmXuw}q:ZLa\RM24Iaw ! -yš|%0KeX\vIِ)H{fZ;qRC{ /Dny]&OkꉥUS=l'凯Gw%)H3ct:BxO 3$0.e#PɶO -|XZE_\(ZODhғŔA-]%f.>nEѫpE/zT(ЄOJs-M*_*PEJ}{ Pm3\)W>[w*]d:@,wZ$IQ@97,aNEd$KeR,}jV]RDP($)]ߎccC$BhGlkFbRz@>ZVN|H[l$R=y:QE>HiϯFxbJjVV5ܮyR^ -rk'eG!% :W!G{DNhJ\9\wACl -wϱR>"j'3J)_PKwG&) wZtݠVwgc)ßHaO&nr#󬦲l'3yxY}fdKJdARH9E}%TTҌS4<zbtXG٧WgB'WVD1T}gLbi[wǚS$B Lٹ#VLXC+;ݪd #+oIA{0]ceR(d֙F3$Bxt@V IғVm̴dE!)yJ>D*:!Zy1Mz/PBIf0 Ҏɸ; Gځ*z͹6HOI`)\NW~㠧£aITVߓMӬ$B'ctL&ݪ\Ylik>Wccyh)GՋ`Ji\1W݅JHrmL*w@ʡxѲHzCx /+΅cf%&B_$Gc&/ ɴ.>)=yV`pB_5B#:ʹv't,;fs8KUeD 0p1>$Bhg91jILJup6ꭕ7k6$?:L2zקb`};=R=fyq($&jkeMkoxs9["L -UB.t/MO0tx!Dn}~yLҿV]=2f^CQ_uyp%(I͹䈬!I-yBs95kIAQnԩ{Sѯp篧Gm2=d7R&Ӻ4M 54;=NGc2ncRt`AJxw&ӷ4p#BΣ)Óe6y0)%L^_׫rhe{-G^O9&%4j2Q/ -LAHiL"CɇvMå)30PfۿՂXa0)QqVgsIV^UB~l׋ Gސp3 L4RI9&M?V !$)n+Ye6\V0YO'j Sm,u^)dw>R CҠ/eO 5`2 j'#=%JXHPe9zTw?pf}iTnQntwR)OX"pO%tT&Ϗ]3)$7ePf[pr޸||l5pndғ+ĽH9zK6]mŤ zͿR8!>u=oD!h`EE}^:zO)vNVB%Ϩtg13nՅvkj,2Y'@]>';qQ)dzٳbT O? :wu\hvߟ#zٜl]CV rneu&w{LJw'R-FE?!R/DJrI$d<12,REV%U<7g:fg^d`bcꩶ[:+7>VΫq ҴP+}coVD0+ [uBKZ~ RyUs.++3UgD0:=/mCԁ*#iU}$]9e88 ?N!"::-_:kúXQG9zչ'Iy\8R*KƸ/!~Tk4NFIʞ.9])3#ByoL>{cRnn[n7V>)WKRQY#UzO?'s=Рg]duC. R> -'}nMty!׸/0y([v7t%OZ`bsI=P'L'ol3{%RJ }&|DQ5M,$)KBcuq\B銞SV'Ofw57'H#RΘs9'R/)Ȗ1BrTk,pY -}+;B(Ř ɯGE'ts+ mF\wO;KlTȒ_SOcf@=-M//:GnW?qPgE9oO%`^ xm{5Hܞ^Ĕ™M:'Z!2Wƶ4ro+nopEfDjtKГAbk&V=Bj%ܡHIc8gRchJ\#YYZi\\h<]R]Q_^vЮPv7>>i <]NlskT?'P5mIF@OU)xUaT}#F;B@ =~_ `rm,Z="]H ?jjR*~yT TI*{zԢ,HF -W!DdUb;<޵s[#Vۦ-Q|_緍Thwr+PKR*B@E` kR.Itiai^ArȥkP_ڃbDJl2ˣfgIrlX?gw>X<}"W -*e Oh)5ЋPŅ]lxh7&\B{ԭxhvRzE,Y0C>yF UJA)_~D7DAA$;)Q&%AGt)K^y4ν* o{MO8pr\r@x"Bqبrۑ]JƾИk7lJ){'@oJMNJ"\ Fc}IJӴq%M d* ,l(:KU+͌'ᏏGJ)23,I#kLZ 9:F-G'\Aθnqޒ`w.kY=ʆ7 ?>:ϕJ1+4V(*il"K!ʓ2G9.]*ӱU@)[r7]>cےuLDMbV [FQ )zeK W2|2& %n0I]4ukǢYNfh;Afbke2$op푮^J2\2޴ )HR>}yRE*oyd } iAbI_ :KUli -d4<@{d΍b}rJ4E7l;|@*w&$!ϧUke2t-:] -,!߼l]}~ =oz{֤X5Q$>eL)Lҹא/] -Og6*beC>7WH+#bX&8)rXu$|RGmkÛVlR޼lURՙ+ڑB#0UIEKأ9~,bԲ surX`,Pvx#Er TUUnMt)NOsT,pa]~C@0 蓉-#$Z|f—)E\%.rolϛ͂ / ߍbE2yL{L& "t{~@C"a(R -tRuf:NReؚ3CQJXkl cfS,hICc=u0_Wfk>knL1ז^O> ~Q'tz`'#W xV -t`O=?7F{Nvfowvv*QJ*0 -D?ޙa B J_$<z;i{wF#e={\&C[r!7&'kn¼~Ѻ{]2 @ *n{Q^Qw+eǔwT>~',U)+DBGbe!z/E"-|tʌWXbvF<6NHP&?pdrA[_Wm_ -5?&PF1J'3p|R]]9M]9LL2 Q -LrHP<ɤv4ΒV^ZYv?`vFRB(M(  -H4JoէX)Ϣ G)<Ʈ@C*p&̟\q7H&5UQ^Z^u-R)E7?A|^u60H%LϐORКr{$$A@$n|^v$zn₰WSo_Z[sSrdRޛ>||R -% -X3J*%0|,ϙ"g,39!+\JdR"NtgQ^ҊRlr?R)i'a,P * Jycq?DVI1? IM<(.-i[-gb\~{ ֟!ɥOZ:,Ø9{ٵJ:36pVݕII- o޾Ѳcݷ85kk,K(;9g' 8r[a/#<4+, -:VInI(o d^r@ԛ/{w?p_&4(eDRcёD>]+Xkdqj22y{6pdRw S^yK RE)10Kҟ(. E6L, bT!ЕLnTνe%U-V* [}iIX+ٯUBT&C86OʳDP1[]\/&ְUڪKKjn#2LvɩO%JzQΎ~.' τ9+RTDL.tdR>"V+[Wo__w7B/W3 O.9+Sl>t]ӉO"oOB|r -VNͪ^32 X)mP ?sbֺVf{D0/#o`7ΒVm_Z_~ BpSETTzaLtZv2Z)8Jq%S&I?IHd:A7.ɲG=Pkɳ)?(_;YDlgO_!;FJ**e' )2H& zӏz,T=Y]=+eQ/0g"cb|蛲IkF $!YrڪKK4.»5Jj;>i:':nA){;,nXepx}P<4MXyd@=K?IFmr[ŬUT=Nr I!ԡ +b(9qarJans=Iu f'-'Lu,QJ RtRJHEx<'*\aOpw@ӣe nhzuFa·-T_~M2I<L3+vm n a`Vx|€q*&L:t+/,C&MXeUH8mL^UI2IV9{5)uR -ƏA)(#nao$<2cIGG&/Zw$Q ھފ}!iD_~ϾzdÈ40ɴb)+2 dtd}wCxkW 1  >U ~lUH&6(^q10Po=&L_v|ș$+Aer@B6.䉳:/ Jy'Qʹ sx7o}'oLO sSao^<I) -2 -$lWS/`_wt7U6?J)p0g%z*u#"#eDy ڔן8ȈggnW4c[4R~zŰ:,,G L}ʳAHLBoHxVۗ~,9ʛ;`:A)10C|E"Edxvۭ -tbX:OZ` RFyxQh$TIcz78'a0y&'2Yd̟L/b]U/`_w[Q|$wYKRwEWp =NdcTWd@gBDHvrPXx^`aL?$I&Q`OnfY)*͎LL cz$GD<Rѕ۳dIⅉvkLn{yL2U+»=J$FwB! LouM_9[ƵR`#JA }IlVk^ݍ[[$<9Z; z>I)E߆Eܘ&LiÐ/Q/|e0A EߊH&~ȑq?, TUt<d`_3MG*&􏩧w -H\A=xraOjVDEI`NI&Ό8隧ϗ7E69t}y^&+tz“/޽%vp\ԧ">AnB.WC(yS&rt+YHJ W"U|C~KJǬwRŔ!3c[1 FdI2qǐxCo)Bi)LN0&%6$5KL#xG;n䟴T%m8<9%S4o6 I@Kq=ow;Gnۊhх|!`Jd}<v,Yl6I&ozIki[іp뺤u13O*QL#dI'LH;, o+R1 ;ϝ DDX| R&K!R]?y;i'|WKCr0X,>{;P7`Hdt~ìsVBF *`V7'98QN;&Uqk¤Th:0l.0Ϲm>h7JmTEHy R}ӿ_J9pIcT~B{Lh"axov% L"%˛:0Nܮ7Jm7XۊdJY>;)E{d ;L*;[0&|X$Hl٘LD'qYTjd]#mcw%R/oSa~/؉0+ ^&? -\CD}#IgJE>Bm'Rӆ"ixؐcχn' =B3]LItf-"`*E'GI)\R&'vؒNK)¤V3,L*> E}o|| -Dң[+lvu,ޒfZ˭+G_2T$fvS3DJUzDJF+7$; S2[+K}t]aBz1e m л~> R"eΙFc!έ|F W"%FI O)KHrڰ8:3Rƿ$ %R$Z㼩{W"=q t)pmyۊd@}zY:3JJ[F,qB&$Haos\7h=$%L&8ͤj߹4n1ȡ3)틤 % -n^_G,hZm|R0e"<9XiI$O.>j<3!R i^L LrД/15D6ĖmEl6uA]W6<=H"Hk!.I4yIܬEKLj6#k[F|r1 [C YI2ܟ!M]t-u:~lDAVtĥ3LMU߬JI瑒:vT -Q$EmHfDSɼ߽4Z&Q0"v%Ls|'瑒PJV4 h0yd…F e3LV[җZ.)Hm^sH=10 H^;ly,pg/B~\:Ôi| ٘F,& z BF -&H㑒#RʆBl, m+ -L`ڪlѠ6~TK''W"y0i%#gL襔AOI,g~et)AAII(kWu%2?X[E"\NHZ[Ѯ&QPs/Ƞ)_bɆ+Z%\yѿ^% cG_ I҆)щ7dDo·=D >V`%^n_&|l!#Of!!e -D'a?t1<|)zXZgz9x;$"%v)i)юec^$RӔ/7hJd]kUҳg}qOb&3IYJD~Fە30k1.zmE[_=.FZA<#VNɁ.g*|Rܨ=ާ&Ir}dEGwL~F4ƤD&sQ> &nl.]bj` Džؠvuf|c0~̈ Dh_ne6v/r+.& -BcO"N*HsVpIzQ.h% P@ Oq-ko&zcǛwy4;k5$wxPѰ@cl.7x[:qΙPJ0${p!YKVb1`= 5Z-0~),ȸgG)OEI)[K@#$|=+qPODo[kVf'g1sg-gf"p]N@w 3߈%-Y,p|Zyǂiy1ո*DIOu=tNZací( (8s '%$!@6/cAѲ*e}Y>Qc33gC)CB2de 2 ?eGneel LY(Q4:]rD *l= aϼ/_-t쯥,vAh,XM}ow#$D筫3y(% t0b3F+d/O8 &d3cAjBtl/hA;];ICJQJJ d©o-Tz*iʉXF:72'zڬvaf@eJ;}3R=L3/NFL>tZyZb*UVf/9!l{JL@). d]h -V$*#%RJsci$—0R.dL@&@@R+Q,8+δqh_=DXq(%8wr⧟L ڼj,zIQ6ǃj\kNA[fk$^rθ%rď?;s -2 h"V <44^WGúZU6v=JIF. -ẅ́c=M~_ghf]Sɷϩ`6SVOVd-6秋1}ᓈ/U# ??I}G> ;9G'#~,CڹI9=3 z+{ak?qz8gd%$}Ye喱CBN=sXlQOO"~ɫ#旗Y[>}OM ʫߌON 6L_=>~|'ޙOFLYO9ϙ`hg&W$m=4óI@A3+YLYyrAg;5MWځL"~6r+WԻEI0 KVs[ dE-irB ʿy?oGxzt/DhG╯O]Ͼ0&ѽsg#>|Ȩɿ?+V / cAeò1?7|M<\ݷ.I[GK3Sԭ7.J|7ux纇>?<{_}d)*n?&LC+YСLmp$x>}QҘe1LWe^9RgΈ7QdDGp#oU]t;w%ǂF 09IJO"~Jh(^_^Tfm34{ɞ~{xyiIR'k$4; $hY4J D|suIng=UW'#4&+qDO8YuH&UY.q|2ho,J,4҂ک]zTe{lڼOM5ZI'1G+a#5!aa@ʉ͔*ڢGhs'io K0Hr$>,ffQֲm[lE:>"KVF={jٕm (hql!!&$ -g8& Dỷ^/Z,iUJ|β-d@[I$ٿ̀Uո*bw'C7|B4<});B/R^`|2&V溳CI7Rr}Ew `]iiJ @_hF65'7leDőh|[)v9|a6'E̊X @hF I>l'(kYӝeO"=~͌h_VDK#-JZ $zf@lO&F[uΨ \|]T-V~ -$HOkidm'W'sY37%{HVЀT)R04+q`8AW'v_+owQ87+PUOKi 4k ybk:jO"1ƥh_FiEIwsgh@jh)+ T㪨UEKc rI`KЀTOkpʊSK-*|aJȜ7xLO}iz,iU.O|ƂmXmSuF>Er$SMQ\M&> -<}!jqj!!GuW'g!$”P'/\FϩeI+T=FZH3'H~6aF50t -J)H ®A]T*Cp&NlLnfUYT*V%6a50C00D?Փ+os9ؿAtjJ|LGq'l/-~zG7 0OhAW\m5-2V*Z<!Q=dF-V"`R!ZZͿ |;?E*80K2HGܲ,K̡x9Ulu,hOҰUEĵ.d쑭J (h5iI5˾:b-#0o#%E?+IFxl'Qw`4smH*3Ͽ9b#|9.Ī:Id .2}'lY; {oCEM+>#XJ=5k vi-:ӿl-ŗ^ao}^ty`$u-ž+fg+jZб>mHȢ+[wQ>j`"!jU.ZF'GeXM)p_0D[߉+vhh5ֳҵPZyhRUhs|_Wm(ًz%QOC)MMrЫcK3;>;|z2 vA' F;Ͽҳ*G@ΩV,|oakh9> nI4E##ɋD\[4s"%6 *Ap'6Mrg> ^L* uKg>9ڜOя>5,)^I^4sKj[EYӸJSՔ$2;A9+[:hZyt>#kIw.=5-3(zv||Wasrx8qYOIM$Uwъ -k)>%`tsg^. -{0y=k=+78\ɲE*'k_k>1+m;QOD= `7twKKj"T,)'t_S>)yvtp2 v*(lKj"Y+ldTmNwv*'q$me -znh)2ҵ8'VfE">|ڐI0&`@6,{IMd(X\_IO"`ƾa߉'D# `7) ^*R[US$qrأsm`?opW.I]D6rh*s'dJ\^LEgoĬQ솖bsB!΂& -=Sb#VS2H'?]/},6P. -w0iO6si"=[Դ-=ұ7'#_Gp[rHsē%^ lJR -$EFj-3>YM#D?Vx

`t ~9:X%I$i(%@A-#kY>?v|:H$'GG߉ eZ$@QӪ[_O٢+X(z uȅ333dC%H#{0C&iǽ& F,N>y=?})'~fso l?3tb]L$kI;:֙OfVTN|I"qn蓉D>g^mboz%HKnX9`yi[EY2WԔȨc~xLtrId?Βw;}lUנV3'kiJ4'g~/W.$.p}蓉DJ[A`Y/>Cz%QOP -C#-%4 l0VE>љxH$D#=>D += $AYH\4:襑SO|#܊⟞={G.\?}|ٿS+Kڸ'Kz%QOC)JJ6sµ,LT&)Ъ?n8dU%璤䓉D[EJb_yv.`ti- {7͂^z6eBC4G?㙙SHO>u|tr/}A`~ -s}tHOFBx7q!D5z[Z_ϊGG77?EI#^x:{i:<.! |2뭧 oc4Ό lf`̈'苪RJmݒ؀9Ćv~JժR/zҶ `!Y`se睱6Cד< 3 e+vPa>z^dPϦ5%JiB(.vnBn=4f]WyFd~Usd/0_OUOJu"aǰm$%[/MJ(1M*%H Z {X1Ԯ(e4}K1%.ghjR^6'Kj'!f+-ubY?GOb< -8TSsm֕$+F".P(. -Źڬ6:TQߵO"4TJ͕Wr'x(9$ IO= XN=? -+38B0 gS[=%;ˋ/qUb'D}$C*,=\C8/C1ԮԊ@;mx""5r tHoN ekk+k1r#@`>vt[#™ƨ'KfM d'S|%3YBWR/YCDk'(κh -@S8e1[ gY4UrgI(9+ʝ'%ItuKkK8ӤDtT0|vƐ8{bRI6eGX(Z9-A:E1/'|Ŵɲnw4e驒/tDyC=nzu ^<CO / QL#8K&<'A˰ßɋ$;)rOt:D姻e3}lߛDObh{@Rbxi"/žI)(*~]xAp=q S͸CfT>|{1~05$Ia6e?*/W5;glkJ,h,v(uP}J>0:x)[KUW:Rc}?)% - JZ$O|v؟ _ -P 3>o tC, U͂d7; V %gI${r5Tpi`ԓNߛDObZjwW,[\{S󥐏D|~H՗(/)K$ 0"'?nLv$Nv;U 5K$tpvx~Oe=)N#|=!%0#\ vF -sS0Hb<)V:o(Ic\&zb2|1$m$;āko`\}|0O%_߁RȧEr |'Puqn9dԜ;x@߇uZH?Jm K]T{I.;aCk(9 -ji4.;Nꌒi2:dm\xLd>v`n+̿>.ҟTQy$K!߼>^U+qGp)gQ9ݔw6' $惩ڝ ^f{w>Ki8` _~\j07Yf;0,u8l'u:kn 7)0k ;]cwՁEiUQS7b`ޒ*0{򹌽v.ԮOUK)6tۉ-XnF+6bTj&ٓC5S6{;/$_"U2lh/qsH}_  --vY`+Iѩ"[pi4agi.uR1Nɬ[x_zBRamOv KjbgHvPJQImHoT'iWB?ZF|2.u/S(rc*'}JJvfT"xL_?;Hɂ6eEk nU[_NdQaUJZkшvw1qR -5mYlQlDne6$@ڡO?wd[:(Ԛyo5bxpmZ >ū -VkkWX 2.$<y =VCyY_)*=)$OwJRozj?D?@h|8և77_!xK}rBv6!f'up-0mA J~̀|%G||RWqTmήtkC%n'OJɕXB"ÉMRd|Or i/@#5<֊@/wy |rayxU6E)|/Od^msN̸RvIٙ^pN}I-נ nvTSST>rOZq ,|2J}WB)mVJ`ٞ)ia K c=>r 6qvHBgzf;&_%\ai/^3# {a5U{-铚d; $څu&(ڻ(\'u'Q5ݪ7j}($TPR -)w.G#ʛT&){xf LZeG>ӁVͱBY*kR 3]+U73k[)g]'{0v,6~ ASyZɺAF̞{ cmcS°/f@g~R*ӖZ]I]|ׂO*q|rQd*I7ʞr3\mV""2)vY3Jqq Vs~k}b9EM -dKz9A|X|/㬶#/ÀR*b}LԦVӄd.-uךxge#V϶# &O -.KwfZiS痧2.&>)=bxIǫv|'QMMJ)vZRp_cVn-" aLJ pZe&9 -B/Gne^;͓SufuG%A}C<*xKVߜ('5froo? b,*^uZ vš\>k'_27ɼ<ņ$xt{]Y)V“>ʜ D 8Ҏ<'gy'G&zʃp}0c7ӳDo]BGr "$\x7533> -olMze[nw hyɞI>j[IJ)J"`>enX -EZU%RܨCRe]`&Q0,Oo2L~r ?L8vVS>'"+=r!cTVPv D)/n_) -YʙJ* Vلfتy&R'=KWnH'EUvHD7YIpB0/ZpmU-)BǙг[I_xQAvX Sٵ&($U%cں8$Ϲcشݾ`M% &Iv/ɦj*R2MUjkތ1yH3̐tUsJB˵WGWߗs~x < "<{`8LVѢ)QV)U<}BSTG{XØ~!7J~pOsW֗dy%#Qqdd=a딈('lHɟpL/8t y7>S{&JMa$ )38qf R$~,]r@#,O>LM%~[Mp ~a'N -,gGCO֗$Ħ223؍{UQ0!"z^"eT*'TмM9%Tkժ $e:;__r'8j)Iԫ.]k#8O -ϓpC`:Tjϓu4-ZCIOKÅV7~fyuJoyR]eJsDx2O-vGض^DDY? pUΞ*b6IYq oe Ӳ|x9 7}tp΅ƻDJҴ%-4BDe<7JZsOټጭҵ8q $DAiB<8DϜ9lJ.fO'AP `h);ă\A%vy^;ʀ'J RUa2yr ^njtH_@JÃ8؏'{$~rH\3NH?)4ij,2 Y7Os%Ӌ A1d]-k|p"hQ6ɣTaQYVeUjNo2&I <]HIx2dZ$"]E9H fN-OişbJ|NW~1ӷbS.J_rBP.Def>]0ICkަ1td'h4Xzl)<OR#dy xD}V^v[ZE?MP""arO:%[*/bIr΅~Js}},(:A1@7}| ؃3nhҩ"jeU -cA - 4"E(@cC㝣2H!:ovj'+j' *'nb`rZb$"24RrqpUwL%@`_FJYAZG̺>- Iy *Waq;zGh9 @^ ;[qPAC`5OjZU6EU3]i&IW -PJPpL>L:_HIWi͊ -5U -{2-nt IHR2{r,҉B܀1`u s% L^IJwM./?O¦x6yp8"SgQw%aTB6P!J.ԘsFbJ'\ :b҅E&VR]z94x I(3 <)1 )[*nE u$Eg`CTȠMz1+b;jAeF]/~\0B^j-ڃqK)Td9; KյIFCaH*J?!*@v<·=˄ǮjsFӍڬ Y8|vG=EO@b/FѵL~"a2?h.+ ؕt~ }A)4N=05@vI x2[[M<&^9ӳ^:$u두l$,) \ijāa&F=64Մ>?A_xc$L)vK2bs7u x́'SQ?qoLՅEqD;p -4OҋcHJ{2cr2l'5)Ry<8ϡ"dAqx-,On!LPP` ɦTO\RFr!~F 'cA￙c. ݆cʇ_{;:1kڸ9G(j2fV-9iL7j. ޓو[[E '-D ePv|&oo8>3}=y/<2!/#ړgme_ -./g MA~5xR/Ynne@=c$5!“?iHfUφ8Ucl3]\cZ$<%cat3jؙx2b^HHH"dPejHkX5!\;hGЅ(jO45qd=sQ"y$~zfp3kVyD7%s3/iȜLn -B~ri~?5c2 $HCDaAř$""WW$uwjfvvڭڣfv-P rprk췻!NBw߫OP <}- O[|'O#e#g`RJ13C_^SlFI?}I8nf`N$|@ e,ydMUn$9K1|N=@~{ 太7$ŻI_2FB"l3~n@♨z2|Rا:Dx|r])O03[<+$xa0.uN3zގZk`QS}m>@,5:,kQ0P~"@#!񰊿 ^)\3g%݋N0O|?Z}1I -DPÁ2$BGFťs>7gO` 伍r_`bc RJX䨙m^ CWۘZ=u[͂\ mRJݦֶ̗́{CG>0P KqQ>UD .ҐstӢSV6 &a!0ZtЄ~wQ>$bUI`5`SJ`qmN(`فX{VF)y}U|RWT"< b?<ɾRꑙ-O,_2"ƼZYk>sa8 o -r+9g[9mj6FO&@FZ{->9_b uR -'TYXSpmx5t1۪Od%N?`jb9nyƎDwOe$o>9lBރT1S G%įNL&6'$;ۘXMY L+`" |2;[2}r 3ye -/1 1JY+v"X꫟vC0d-1K$0(\.Uiiڗt ĒeBߎ(i&©Y) UjL6E+Ep%L \!@}co1q쳢VLѥϸy-e>La 9;\  :fYJYC=i[IqK=&\CkZn%a|Ju>~W-m(SJTӼ غdÄDl.탄<' ί".2rA Quj2&jBWb̌}2d.p! vGZb0#~6z`^[<3-;iP0Gne䝒_D*(ֻ)2Rh-܆Of ۳ådWዄ7<r7x9KrPTY'~a\ުPY\zllfLt'v"<:Rn|BrKbƊ%3˔[_Dr*#B}ĩR_!/ -]bfi"p~}SL<'(%Dp)"`G~) MĬ5lkz9o'hHpoW|>"WyxbjZꁣڍ&X?B{O¬V'u~` zb3Ta0AC.B@.9ȱjIdªH bAJ' -|;d!I쓻b0[K家т>Uʑjۀ͚Khw+VN-=ƨ_SgM 23Le0/!׽ѫx$#}k.b+9@BRJ.O-cv{e ߛI3㓐-—>UJ`J -Z\z服`/~κMTQ)=p HoJ b!Orw?tTŇC"b3L-E!r[FCWI_g4d`}]yyjIIddV&m?Ttwb`vTJ،96!=i,Ҟ;ƨqi T/8L\KJ`s:=ީ'z PG>9PF -tC9O皇WI=f~"Xu>:63;;n3>Њ<"*%,Z-.;гv`Vrʈ__:\?HO9S[sKf^p)UDxɡ -ꑙ&5Ԩ]U.D*K&NWl3|M7呜OTTO-Fx?֢hG bm f;M)a%5̮$y-5{.@&A)W8üܝj=P+?vu܉pHʷ)Sdœ t ԿE{RV \ܧw(xXEX5 ~OBHO鑚/دۧ;Й1sZiW*AY+,i)jʃ" -< ‹ng:w7}v:ӝo;l _');-T[L)AGuD5KO   wQ@L0Rj$vϜ$ 긣dV_𹒚- #l5VOJܗhr*-:*LW\VA_x#?(fouʊglkԖVRp0 [1Hv}#eQF5 òGRf!C1 HvY CMƜoG-4ƿR9үx'MwL?! -veGT -^txZ`vIr@ 1P^A]t3snZ9zO*O>q*eɍOB,0LfZmq'SVuǖ֡&Rj= =aٳәqG}'O>w`&mI6d`nāRid!`KaS^x{x/c瀵_]=ٲŨpqÙ_pN:XG`z·uIwEr?0OadmjV2D炝CnE:r\IMO"&udV01wJfrc"<Ò6ZDT ĔW1eqN8{մW~~'U'ږ-/xA*hxKrӓ6UGR;@!dFg0 CK-w@5%&ГlJբ>aՏD f7&'Hi NF-iWI77]>RYiyEEqYM -s_Iǘ'JO⾑[q#%O#V\/HRDa)dfhjoeN[CBy9zRM)`w>/ %I LwzÖ ⪟9p_x;ieeHuJni]tEJ Яxő&4'E {BvhZWyڌWM.ozil2rw#> ;YpQuϪ+>&ܿۗM[1gt,1湐Tjג"ela`F-xJI$-d2'1OuHd(0LNeEnrow"jS<$e:K ? (1hNpxI2i)̥]oU+Mdoa 񻸄16>)a?R%]E8)€TI=XVd) %EJVXp.idڌЛL&^ɇ9Fx 2)Fv_|d#΀xcrs̵sXd`6ҟ)fMÊWl!g۴{RۤQ (H=߶:(m/ 6)-DS9H`OJ SĤ -)AdH LXZwyøEKЛL'jjE/ &)6*sę|,$CJ`v1Rk݄'%$zRK='1L2)+wO"JSVs$'IO҆I65Gd 2cnx'udV/8<4_ &5RZCDOrJ8kcY)tFlEA hT9mr^9MO6MM{O -'?K6H2$li0gmN:Bk"%& -X8rKfãÒ2-wsh9Ȓ U6!KR<<^B>aBIk  >Ʀ%W*aKkջ)h7'k'G x>2Â{f*v@ReK쮨L@43Lzj Jyd/nj[C-=/$~$+1N>ۯ+u>?1Է: Z)g,'S(XJYO7!JI_s6R:L\,I9n?'CVOMN)iVK` d3ZA@)gY7QʷtۓIԢa仇>Pܟ&\f4+ѝtj>iyIJrcH>' vvƨb|#~z"!)7O(5SycPJjteO9C5n@)CɢH䓞j5-8 -oH\6_?৖ -AEdR r+TOnגRTY%rwͅJI_O,^cId(ʕɞNM[0r3 v;wŁJI⌎<*D --QO^g*J= \gyhTԕh$y(VC)C;V7wͅJIΰEg|䓥m$|2 eS$`LW)w~RC!c)eJO)[i_)I554<|2䧂!zIݭ3UY3`CR̒$igC(𔲙/oi}rkQ{~C'FOpj|OR4SsՆGk}ROSIVƝT?N+ʱ"Td/ojd畒<]}u(k _m@>YI@&CPnF:zL= nJ9 ؉~82Rc\ʏ5:fe _Nz߶bKK>ڐ}^ʻ=`LE) -ոͩ.;'sRrO^)s2t"CwUuŲ^cN譛g^p9H*XxhyILa55GO ڻZwE6УS(a Ԝ瓿jT 'Hrf= Pŕ&KwR1rعW)ê"ƽr%>'7h$4)*DjDEd@v,7L *%MLո} ,8'AT)ͅo|-fR)],E|2 u'|Hꁻd?F_P)Տ|lwɲw6UJ}Э&mK'i*>83.āe(0 ČWJFm3;ǝ#~}G\J)m-:Yt%8'O_ pLW1Aabx -%RqY(%m~ apink_)%II_)9N2?'Kl[* |2%i/ytTw)2R)eatV?ͻ$tPJ1֓Mm\Њt!܋f˚+ ^\өX2e -LyY&SJ27.H+*1; ܏7JIAѷ~3lGy'=O;kd $JĻ쓝 %f -K@) IcUR#O|\);i(d= pm\ ?5Sy[@_R铕:AbR -۩D֢vV)rmjhg%dKJ *ھ{ @3RTThyYi(/H wg}Ma餔9ZcF^槕R$]?~RM~d2}23RD{S+q.4, _k#e;l˚HX~?+'Tr}0}\`JIP%ftW3"$LD $jlN9~O"2{U7!TQ.AsS%ftŝ -% opV 􁧔RnǙ3T6(ja?!%QO\m&@*(m;Uaq(e |qC?VEuN?,Fc.>rs3LcaH*tԥa ^{2mέ L͕+/ɀՆLSftq_o'ϻ+J ekO>ד>^~oGy2wg |2H%dVy[kzhr)~_f;wՇ,:J -X\H)iN/wp'*>'0+O6d݂5sP"H$jIcR.fC3˱Lh`z֢݇TJV)AWed~/\qHRepU?}m F+`y C_ycc.#1r[2iN}5ՌeszِŃLTr8Fk?#d kn'@R[<04.o)|rS2FOvqs[[ .ᓣ器+5rRJ$ApRH(uВ_MYro_bK;z'G&:DGz`F)iNPm?!R\KJ0}8ߙQJ\d%sSҕ*I>92k`͔8p^|{\!y"?jR1JYgsy5TRS"繝zi<\H|rtBբw].;7E,'I-?^?d AYJ)3إ;Hm[O(#B)vn-(eHbi@t9o (e<^/ﰭ)xGE.߅]/Q U[>>֛ -9qD`dIyPJR%)?cY> ePЅh5n 2$򞬯*`j?(9_dXcyf=7jO@*d7HOv!kRd9n-ҷG^/Fxs:91@~6mwQ$03 =-)AJy%kY~ӺOŘ^6$* N lsUU8p /#_ 'Q~PJK'9v:>үuNj#<2rǷH#-Y~65 ϺF,!?<A$~R۹t#̆\.hǔOӌ -Z֛HRzbۙa[]{Tх$5myS:~ I)3 jRg<$'IK4@Cxg9մ9 |=Y9~?$[PbXh IuŨkZbJKj-5S$OyaadƦPT+j_ȏߋ^4w*q^<}yy]=ܾ/pDΈḙRRkA){)&3rϛN?O'$HFW#ZRfG?-XĒps(eƯ&[ZjBe{#~FF/aX?d;3J igQ~h$8ZR܆z Nn{RC3?>i'~A׆~-o Hy}ٜt6Ccwg=730nyOEd).{7?*:20>쟕'JFZ[SH3I+q~w؛{4r@"= zb-<r{ojڪ(,E)P nդřIJFJkH(-Yj!h"~0qDT|ӞkJ=U -lÆHFO=Ac7RNk%7F֕FWE%Ώy!EL)Cwa)K>˓&e= Q 2@(!$xPCgۏ)v؄4& ~!vپtR3XY0vэQ'gv4 Ő<%{kJ|H; Α{F03ΈԾEiM -hT`HN90/,9cka َqvЅEqH#uۀ,X;: R>R嬢p?|,c ד02󖢨T ?/: IF{rgkazX,E^-)Ja"ŜHF}):^W{)zZ\i|wmL -ifɍIp q!,iT*X6},I[G"c59G6@BOvV E#)qeȿb;$[/p'g"]8WvJ:HJ;5)z֧b3C"Ͽb[S -ӭ?2g+XiT\$$lR_0(LʠRu_Nt)Hȓw8Qw0uL ӾEu=x43ȋ8Dq #9䘱3˵_ʷl ,qe}>l)+K  -JEFYxGðZڼp6//g}C՘(Y0vf8'ILp<B*ZQYK×{A "HILeOw8?^:'Ӿ|EuPG;'Iq;6'QdvTɝ9~dmֿ,,&.I#:YvRRiJQ@1)1ɹYIJ GQ}SC6`ȋ8//ۚ2Gɍ # l0BUq2,qܐ?u&<N";^RHyDR&MTs"巆aΚ}({u12< ߟh1Oz98L - ՘ X>egQQLeNVH Po$dzHa#f#YdEB- ߛCC*ƈi/ ,S;3KuiQFqo(u/ '%h԰՗ۛ()F+eȿb;3 9tl`ܚo*KERXv~KRB^((Cօ]ey R1^lfj_RШ 0/,9ckȵ^|~@,S,DXF@A ʒ7,MH2©VL59^{-iiǮbEB%۞fhHb{r؞DVx,1e79L,]g%_' bO2O"<,}u}<8_"ߗiE޿VO:ug몴S2ރ(+/+=m(x&P=K쳔| TqtDܹZ)Uw3ĤLIkAQbd-!*a^\0ٯ64Πg眝G[1vplK*ef]#!گ -F|Oo6riwȐhsv{ɓN-߳e9,1 Kp5$Lh@֤l ?ehZ)_$񗃺Z'ъ3T`0醴*,޿6툢,1rj]dQnpW?R g=r`0E$+HĞ0og NߵACFؙc}4sȏ;{l[D] -7DH;~аLf -Sf 6D~^#eAqnuMx!ruA<(`W8[eWha dڂƤ=uN2,.ۙ@JH3s@_n#HŐ8*kZd:B0("(.3Fp@*))߃eނe5I K0A)^)fgk|1LC0R3Kl[A9,1^_e4YZi^I7QL)'e+c4"Lwƥe*YT$(77׏ "%z{ 끚KPAI%!&H9j R*wzR*E};S΢Hy97D/oN8Xx˒MOlkx76)O ָ/wҦr.8=)ʅ))=_ j(DD-I N+5qT{{xRֈ@M~e/҃*)OJX_raJ,ϼA>0e@9|ϖ%# `paXa6`N=x?3z6|IHiH -}!ORԤ{6XrK H~P.A^ -㨨%Dx`U@4nrEʙrh߳஻ Re0; F -sr KU+m)7{biƬw"X,wrI 3 ak')jB= D;`)T (?et@T +Hhe/ 斗5~,(YΡoOrkW8:<}g7GQ_ކuC4,AtI0RH)úlgŅ\DWXAIIQL%A8>2IXbe)^0"3iC4f -<&)j#H9`za>(jrٰ,*QjL6t4.~XDʷYѓf(*RJ6N(Er>pCC_k٪AX⼙l0lzUh"0}\@vZhz@|?M&oyH9`V_?2WȆig HiQt ]5^#Ð$=ا 3~ͧ)RCx;< >rw ^K}~F;S-ϰĆFљ`oLJ²)j){^(̐ RRԤ{̴ `>aVWUكqT,[Uo 8/(9)ZykUjzOI֢sJFQn "ay#_? "t94_s]0R4R"BnqD%H=NJ%;dʩ|@yUМGr~#R23D_G\*ᠨD9"j 3i>ib8 qRͲ&kaҡ8m3ug=r`vu&r_}X<o8 pΠHY [-SE][5 -Rv! 6&uW,3t9Fw*ʃ{ٰuK8C0p%<'[i>vw& -s.}93e(;=aÇ.4s@_5 ``V -Y\e0I:T'%ybH͌HٽTۄ<BHD{(jJTPR2ϓ†eF7TKp8I9?ɌO3LL9Г9zi#8οvwIxΜːV6j+5UWizjWEj6UwەY !!`\CUO"c}Z. !m`n1$ߙ, ig`~W3g,j.,Xh#&HE׽^,eD8٤>fugy5sR宺_y(iMF2lnL^UKa+;*[.2cڙ%j>TyYI SKcJg)exJ_7HJ +sZG~58cL2a~ɁeRZXa PҖՄ _"!1aRDgT.c 5!`f#Mt'$>0r`9-E* 9 M;6НH')ZL>oVs* -MȺ6v1zDR>Փ.1|(aKvX.(Xfj"C>1L@d"'Fփ(W+ -J|=jR ? ~cU>H[09Dڄ°fX·82ӫ蒋1vJw[I{-NKnp6D`6KNsKv9g{,Ťu -N8W5@/32WP-;E/jRF- ,RFŃ/Zmҙ _lox `cGvȹ!#M4.cg)p31R'c&SA_ Z>&)Oü<<^HJǓ/3+a^<@߲Q |MwuR߹@7`X˅n(i0")K=S'Zs떚po!)1LWtxC 0V-T-vseGPq)]Z@z&3_x3Rj3yYO‰߽H7`+Ii -Mn-MHx .b[k`&]Oz5^B뿓1̳Tu1̻Ik*i!%)/a3Sw@w!f6rݗy.(JlI-e1$O7LpEsߣ?8I^3 g`A)ڭ•T#ud3"0LKWvӓcL50m fHwq`H)[?f l&}x?gr97 Ai3giۏSwOAWUFu2dmb$xj55LS 3R XoT .1v9&H#l1 {*mD:Wn[3Pk.#eI^{?/w%kI[[f(@r8\ td`%}sO:*+++ŹI~ڞJ <<\pZas``]j/OBlQ+jgc~mc?Go;cֿI5F2Ɨ+VRDm=7M -^jRV͉ao6pj#MNb(ޟ ,sT;T\K=('HJ!!8JngDoi rǘ_u* > r;U:;Pg^\Kô'V>ܨ~{{-Lu0lm JDr!pOw -{DJУj1 o - 娟C_+gO'Z%;0$l$SㄘQJ6)N5@A˹ߐwi^`r9._d 24-g"/=pn6 c:) {7lC+?WYz7@W*CUFUu <@#K@>#sDYR}t*xB;0fzH9@[o'#FM|*7ѩ z.njWMJX:|cKjg̓DJ8;|h9JoSBb)X^K=0j{6p;{mo%Ga kki!5q! ;>KI)s$#iM^# ?Z1̳ſt_X,W"b&)T=ej뱺0j ̀Nj'%R[ŏKJ}aVHZ㗙vJ;=aVe)2}.Ul -΅s#%a-ƲJdeJY>UJYIɁIiVaӽK 0;oU0m)Uc6VcΟ8W4վZ`(LiKmɴIai0iPsؒm|M(%@6`1i2+r8@6͸ϻ+ɒ6vw֫]iw0aDJ*W3e09StM+}E[%djj+bilLY=<&>!Ϭ*cF%>X7ɃaXrj]YORu6fat -`鰼!o@U-@ʁLFV{[De%:BgO<><9P>=aҦ)"0lZw6{`U -+ԗ֒oxػ*b R*ŰJId>a0 Q00`RYç=0l'jY"o߃*j2Mhm`)Z !)؍d~q3$ɟ#ؘqtO6  þ3$Aw]`q"6AQ7Soq%jo~<˚e%ɇHiTr|n=@K(aX4ݻ6iI Y'vaؔ*Fp.c+mnG֊j%˂zWߛi"o338n='/;@JI̜0R e-?iI#0,M 4GheEfh$9Z|%|Z#zihǛ= z/ptDLq9iY!Z9s-誴ZZ!/ů Ib(\fOq=m~0 ıJB7E3YW4-V޺,ݿ z?pd=jo^O&wݭjQ]ptysYГ< ΢EٓqR{d OQ zY2 e>8wI2m$G#i lZ| -bFoUpvhRJ_ß<ǕmNyjȸ+M5*+rgC<=bb?&&ޔ,/ iķQ]3d̓kxa0J1+Ŋ:A]՟&jK@]+ |mm>9s3^,_(yYHĨt5}ؐD -+e]iEOyXfA0Ko0,}jݳnҐ2 9ُ ace(Սbt#h)EZ|tQ-_zh|w۰zsrIjZ|]GcW(;Gq(>f13ƄdΓpa5IJ$*/ &ia'mI aXGK1h^b,]~ּYx8(6T.f\ m:X-=FPbZ4>ѓI({ndaبIb0$- Ű za 4ԍNc̈ۦS˗Kõ? }ЮsQ14z4]rO%pZ ĸlPbИ)7'$Vkf=94tb=x-/#n ȟ#YaNMJ4n#݊q\Kj 7C{9'n6\:Kwe'cυu1&)#z=i/ZN)t?|0 Linw )Z^u 8`ؔ*FG8 ( ]ֈqu%ڌgi Zx1\jxC{|7IDb^ϹC#JI J|蘄FA\/%L`cRIEZ8P>;Ma.;nI g"&1,mb:4:.11hLF(9juW. Zط*pd};]9_t|\<\:`9]&a46q=ԞV{ɡ":HJ Ib0\&p.e} ıs H4R74mk_׹|_|9w~\Y.Уբlp$iFaS\%2mg;wתk8wkQaDQgT ->BxFgydI=i{?~>IEؠud~iZ#L˄>IR@y-= rVrzl/fa1+xr[/|/PHݗEo8x45OD??чD(<<|4&cEGNk(j|٦8)$(ta}i\TŨQb"auTIFA) x;T xl_q_[yN꾄bo[N(`^a!q8ч-nwkQ?ߑFF򤮘LKI<$,/rV|Ơ(j86gA'!ٮ0W% ܞhKŕxr|f\AO*s]oDZrPVԸ莡KɍyraO2L wkI%~bUYg͟c~?}5$ ?+e?@%*Eaq41sCC*-\V|kW? fwJ|mFZv(`(Gt#p("1&0 `R=cSb{¡(jxȴ|,F[֯uUٴ] p:?en\ʄ!W _soR&uWb-qY2"Flo.;XPBԋG &W:GE'%pU8_euјF8,c$oô݀hgbqrts[/)p9;Ǥ*V+ -#*T4{U1^\F@Ejݨ?xU<6|Ѩ'gЏ8+=JulY+IP]* Y4Ba]l FbُC U/}G9RK}Yې1btrЈ9]MPDT1Ӫ>rziE-@Rt:%ȐS}l2GňhdЍU=ǔe[B,t5{uo}w.J=WŁ&a DbD+@c܍oIb'YI -Orx_GȓR, %.4>"Jc,mZ -Ew~=|ts||f|~7>򵞖ګˢ[lFF0Уq0w87OjEL <^*)a2FuⰊ_ua(Ƹ3-* r1?%b7Lb2&;ʑ \Prտ~Xk)/z0$ Fc!DD^6w9t$(RyRK`I6Or}"OR+T`E`zЪ@X\7XG##uEqf(%o O$|Q1^KsFi[끐Wցح~xA4)"O03Ƀ<X<^8,>bU=R86gy>Lj 暧_؅dRܘnۀhteOn}sOY[_{uI)?SWsfHa̭rѭޣ4%&H%gIH`I*ODIqZIS.EQΟc>&ŃfЪ,Ŕs)gShZcV[0ä.!W -^iFrLj.ub0 -2FC1=tGHȓ'SSg 33GL0>v9RZ)9H̳̚zhPhԔXS[`/gSk4N:S1T,uTKݗEOD̍{~0!vƚgE'!3 3|3 }R|?]S…f@>)2HBn.Y%"V"yЍX.}3\%\5T#x+{ B:p0%n8=XO@ąHh^ȓXbR}qxVaS-9VDZgä:.U tAt1b7#aܤݬ+tP{r.5{u -eRG'ychcg\R8E%C'- +&Lr{1Sb$E8oS\>N)BBU~PJ4h[WuhҤp~iRPB;qqbqǹlv9>vHy$&  y?z~>me)+qe~j$RZFJѤSkL=x@)v*bhbbLCI@W+6"~رN6;\ -\8ꁋ3!OSk?'険QI}VBa&5{R<)mM{F-E)[JO - D!H _E*߮h$l4.4/Y*ɥpEKXQX%QLȎ'/~7"m(. -V4 -^~q|zh=ԊtT|bzR'7RJk}>z&[`߾]ѽZ9aExS*)&|/?SQi]=µI45 O$E@cfg`{G=7fJf.VFai%@B͔O(1f]7NŮňnİ5=0)}OJl3 MIaf+1){rf{y nV B1/ױ㿛=ٿP1X"G#]B} 1<VLdP\YM VM̓k)hߪ6ʓa#pn2L -oiГL&chbP\bf»ӈ?b|eoϴTDZFaP0h'ꑹ$Iؓ)bİM_s۶mRxR"0 Up0PYՉ&C© R~wh\JJfa&YV,ͣ14%'ErX9qx[英 {glHu:{64|mN>*F>hhYa*JIكIvGRiaC(7oieR=~̭F32icInTIFYӉڭTGvĎvkцweÑΏC&aN;0(n>ab~ wht24.w#T -=j&Zy 7*ѓ.Vޣf3.W$dɏR~,׈"*&=?BD?o gދtԅ]gB?σ YE[ҟdJK hD7bJjF2L.)~bL6e=pl Ё'C{򓥚T&Yn(Z65?Mz.~wΞ=1:=BsK Ђ!(.\Tbf(F ,zR$ I)ѓh'1\$yzTݎ'j_"j"pt! ~;U(qb2@/Qh%Q=TIYI=3F"a>`$Rf*ɛU[F;sf]z봽n*x[c=d8}؀0'=]Qa:S' -!%Ub#$FOI P0E)yٚ0O -wՀǕ/}e_t8C7#F Vj휜@O$`$ݪ6ГEC^ݩ5-SrcRZ7dSI4`x]}Hi#oV)#1II1^mXX[vRF>C8vXp3AmeO;j vx%0l}Oj -uI >hߪ6'1%rbQuwkV("͑Lpɦ 09nt:),G:ݶqɾ+^;RCðuٺQ~~IZ|_ 1j[H!F^P6.u֬lRKֵ>12m{Ntuϱ%qr#ݝ=T'%X-Ap|{>"nyIꪢP(IQ&u:K`5ׄy)goX#!, n4O3FJaF؀1*|L`lq7v !GolN/[Xƈ[v#c}-) -eYJ'P2)v~MMsc5 sI,b>De?(lH\v8oq42֡.pW$VJޚ˼[K].pˉN.-Jp:.}Q(T}ZT -%쓂Ї5c*%R FF3I"LjC悈@2%%zZS-Y$+㕜az[Eǔ6v'"ީ-CBY{J'5*P2IYϚ[jRw7.]$g+#Y?1 LҀۦ_ݹd+F.;EVI&fE.c}|*`JR$࿌ty&"ɘ:)GPֆRI_a}RC{A\#PhRۜBg -_33̭f"&l_H&MwBhJo{^+HOPؕ>N\Q薶cu}? PP6'Eut Q*UBYP("lă|R1n41')wobC*UmȎ-CrS -)8[\ب+&|l  M!hO'qK]jH*P(I:g:FFTj~mpHR%z掯}h/̚쓒;du|R;kgz+eikwL,p/s d2pcn6klx=J-Lcc3MP@t2$yR({d=!5*B^UUQYRR1mٽ~G^<{̟|mgY,^AffQ5*yS(_FEtn%(&Lv!l2EgUq`j(\Iۢkbٞ.d- l:hr'"ީe%A'Eu $qHryEeٖe堔ۿ\g=z虧BˉglhKӖco*` /F=nW]9szv) *z 7A#L2۠Q_sL&+Kqo[_ّўNH:R/zt>vt%B5Bi,|PGRV(UUe[*|`cowO - r ADPoK8 I(φRedЭ̤yR rŗ^.F&k6|nq#R&ȨF~Q*|LA XX8o]Uet-S~|pkC2lsMǥ/P -(:F4BU] ƀF* ޯ?xgק;p} -8ǃ@B=d9a®36&w֬>Iٸd5Ks̭fuMm!X4>!pprl"&C6l|!:>?[tKNuOT6:랈xh$&b7)BG٣F#Q]UrR Vco{/'^=fwIm( b!S[ωWsn]*JhOf*Rr7oi/p3!!܀B -$$.VH+jsݴ$!^ ,4cO$>Ӻ)Hۍ$mzb+ϙuv$c;Gq{k_w9{*ֹsm6.e۲F6{1N&CxLjƬ1R+Dp GD _fS0%A,O ᓐɎ';N8ucǎ=zȑc>j~ ׿~zgu/5HHbih{hRY#caÝJ@&=頺(;\e(eil3DLUC#7|YgS>Ҋҵ>m![5UUu 녈 -,d"0 0&y9`-Ǽ!ˆ&Rp{GYNԯ3|䓟z)|{?zo]u:Oކ.!샓q1-Fuo|:q>ol3t 7\Ds$%+]UX%*̾zs'8NFWR=np\ZJ -PsmA?!3Ҙ~3C!۵$$ŸG'vr{_k (%& {v9 H0r?aL1ƬQ"65OmE8!En?0H&wK赑Fw?{FP?G"'3thq4RH@%< %mZp<aUPQVַ}1'fsLq\Qq0sܻ 44R\ر4Yu8d쳩Р\aFwSl*]#$ggq4< ?7 -uwcݽ\wқxZ|J^:/A0I8+0IfPC H#}1̵k\,#}9F*&TI[iwQ^?6@W[ɤ"U8_jhCoE$.ؓ=ǾʇAͦBŨWBaRPՉzxp>Z<Ơ"ki2YKQ"|Jٶ|9\2|Uτ›Wᒱ<2i߹`oh>FR=Qīy9 YW -pp*`z<&9Lj-}dr0\C d&)d"lc݄k^^sJ.YX#J l%DXיcu}mf9$~VTU52i\+FP;eF^9raKXe[hbO7.KG1ʂfzzDp4cA3 -M Zx2%d?3<]vK r>ak3Xrf%êG?$%r!!/J VEN\6c+PQHչ?H^@-yL%`2/i "ZDWJfS\r 96ܑ3Í`!0(qت2nRCRc%?ﻥnʹfضT^g3i~#l"RJnS'Tpa$ ,cۈb.WuxRi5!~5/M- -(,⠨8 dk{Wp^Uk}eiH6H,G:g"Ckg[9n9<\Ӆ^B,M$$Y}Э&l'DmP-XgR6ǰǪ0IC#_k?rvw؋;ZGmFRs1USG]XΦP1.gu%JCsh!nIr1vI biẆ=1z%ç{;WMdwE{O&!ﺤn & Q{ ᐤT 'E[o؍aV+UZR"" i6 NHJJp`sN>b'Ƙd}0vubh;{#UbgVV$ò?닳[9gK2ެRIҰG}d%^awXgڌT.yNG!b',*!㉓AR ,eY_dl5:*Yq&:c4/][J5[[H6Bp%^2c,I]uIM_B q񋠎6R~p' 8M/:xEnߝ<H2\RW2aFanfj lC|bR^4]^-hi^} ^W0$ |M#ߘ -s' w a/f8 -?|"tABeQF#m4^D8oMӋ 8A #1 ab/3.,rj}>Q6Y6qx fgan`˽2%w`лc}q<#I[[HښTqhN%y-'(v s0)EXVP<o9H"3`.'yoS)-lkT5+QL;(޸OڲFX6j&Xk,k`K IX3Q'd,_H M:ܽ=y|M÷'˪hH -"Щ$KyǒP=ԖeSլ{j{R2L|}qRO~V -XF6UMNJ$Iӭ-‹Yθo=^?=$^z›OnHAO',yr{Ԑw#2lE3R2dw'YC6JcL VNLD[ޠbQI.kL c$IqSn0B_ѓKK(Ey@vsLxf$uH/k)zw|f6sZ𲚎7)ɳg}u>QL#tu^F% wV[y;턩!<dsW !#1kA V( pT2Źp'] F͙., jMjng;- -/`n3vYZTu+jgwya6WSbF fHJ Z|_2 FE8`nT-e1> -S%̻Յd͊3*eϖeSclj'-U]VcEscqpZx<<%]9y1CM'$ez0OOmmà?L#cwπ*!Z2.*aUұFVf,y W\nVbcin{Eظ+[PW;%{ʥ*2tM491bB$v Ꭾ+5d!"0EXh(kfCrZD8#K`F&t%6E}6d:R2;UƴR}[U닳em'/{)+SI6aWgIhR fqy7'-]_$;aݴ ;2C<-rA ,RѐlDZOM# G3[fk lj{E- '<0 <7] ,?fziWSbCP_H$ rJC^^{9uw)IR8NF8֬DJT3ļZ(jT;5ڲWXVIv  c.IZ- -H1WI+=7 +"dGoO8($y4`0c?Y^&jdђFgeT6jV܅e2l Nfbk ɭ`P%kg]>/qWIضh6) t MRR^'#$u``>lGpf1)h2 |r3*2#C([N^-`˽-p@9b$L)KZ^BsWz着ՔؕS EXM <{𭳷xc0 +0"ꌭSIںsgtЊ Ll,5liA$oZ&<ZIYxIV@tI=A||"8򝺥T !#1DgU*` ېZLF*pOK J,|ǚ_[HnfS;ypxWSkV%qVGs%ɾV/Hy[Dl'AO;i=:ACb0al#KUrkmasCjϖ8/r%78_U7ڶ3hՋJ[cje怱f7؎RloY{osƬc_(@8o}m \;3 0h]34_ +뻉9'+||Ig5ȓr6I`Ħd* ճ뽳_wO^w@`0 w?[H$H#`J&R2%xYVB^X%,z -&\eb׀&`ͿX_Ƙo#b\CގJN'T 8Rҹjq0RnE\.$۳ZV.x%גߠ+ڶ젫To.[~Tg I+|kKpBL^(wѮ:A%TϏ{zM.L´Ur4d f`B)1pH 0e [0͵B*^DWfkIzt&]od^ʬQoL0YA h -X%_z7HJC }H{_|raf8! f0#~-5ogj˰2X9˹R6*%%d+z>ԼTtR Q53 9ҜAL[J_wݧr UvUUv鯺g$O;`0gv.,=Z Zv`Y2t$"–p&[DWf ϟR -.-mSc9sNRD nx[<@Fx^@=֒.k0RN'7?%nҋhzG0 fC)mpH6euA'-LZ>R0[H9ğ-$7z*vlsW1[(Z wZPa/y@OGoH8#OʷF#dCb˧.`0 YӼZ*wgCHm.߲e%lObz}ް^HçL|yMJMR*y3I0Rs۔#~b~!S+]Wj"#1-Hk A j6VIGZjQe!ؒlf7uaK`Y/$KJ&|K.%#X_`2d}0W}i}`&]W8rard?{~} `0:ҬXNGVf@S!!m[CJ& -n \r^¾SxdVZh^ _}H<)*$89C?## 'gwϛx.d$\/⇔۲k$$2{}9@˹R6M%SKaK fd5ՐUԥՈp5_N -#mq<  p`-$[7vL0`07XgAga U'HQA*JGG$RǼo@tǁ>y%F?$i;/Q!}B}fH [(MR=jAy}u.)T,,i-Us9_uፐێQ~FUݖs:@mudW#F^Ģ\ l+W`Qg I y (!ܔeZ 2 _(OA}G~mqܧC?z8#sdϫ|Ey~2! 3@R_,#0XWfr:Pm}[⸞O"3bCHKnƵ鼃-< <[tx~O!ECb0P4Eq:\FLAvu. Bey>kx={ɧ^ lc}yGMz\H PJ,=RƸtfPnidb0Tm쀴6({P.:HqNzl8lyh8@l۠#t§ |u!^1B6,7 6*t`%TUsqqsZ#;*9P$IzNp㡏N(L(ޡ gGoEFb0̇3PYf>[e"fQp}d"t`>q=#?Fnp㡟'py`]vOX koVqI( &&u ^K0@l b^۹uhc;׶4J+lC$Mz5Z'- Ǒ EQW56T@R]v*9lDʏd~VpK%U|nRlrg^2uL#)'(BNrv04gƱl %JL[[,)+n,E;B(/pc긿cl}mlss,1j|Iu)^75iN2$EQu~qΣTr۳`rtKg)1~ab%*.j`>Rkթ0Xr lWb=9Bt}l㞎 ȐBE8FÕD&&V.׵S;#>4_ EQԅ9|$= @聭 -"rͼs1dE3bD v*n5!̳?:5 ݻ$v{i,=F2’+d6N >nzw+v=WqhJo꣎i[H(Ҏ7H wI ASj.S1N7`M7TsbJ_ƕ6# -!Ü.Vimt«~P"Sg]C(]*yg?'Ckr晷,eB%e_ԩii@UʴY &X.n,/l0PP0Ca+.;zgu-,:b߄({gE؞((Tv&tMߑ̓~d;8A#D:GRUbpn5ەv긜RapI0&|hs}G-*"zƝ= JqnL^An5ԏ((TҷgW.&\[,/߄btb1>y@g-65ki8<DT =%; b h\1떹R+bkYJO)=*:REQEQ,Ym=z ms&< /.\2”۱>mJʱ:򆣇;:\ns3>4nһVL%cS3 -EQEQuFLXBʊ0&;|UnL2|TOH*1RbiQEQEQ0GOvcu)((9C7y(JREQEQE` endstream endobj 11 0 obj [/ICCBased 19 0 R] endobj 18 0 obj <>/Filter/FlateDecode/Height 947/Intent/RelativeColorimetric/Length 20505/Name/X/Subtype/Image/Type/XObject/Width 880>>stream -H pT/!&_aBA+ʄbJZDhjhb"SlSF[qvBP -P)-7Bd I&;=&_{{|!;Lsw9W=!ግPڇ#=^v^fV$Ax 6G<=\ k˰n3,- gGX+)G`{[-6t 8W{17# 8nz GJyH&AwB tTV \D06ns'Ab G.⬊FI8 dw AZ-hV7%\( "~,z2YlKA bYqM V&u8minO1"`y;A^[7`:58vԆw^Z"VPQ $A.c'nnJm4RD,8>F75RI8; pћ"AрMV7Q#(Ag^PD ,=A$ۂXtSADXtSZM#%A} '&3>A$ u]NgO Z,;ȘuS+\$Aǥ|Z⚠nƩnc; 6,n>DACƔaw 0CKDSDr * FVFw w? GM!Unٕ#A݊a2, CŮ~ :vW74g58hr׹Z܉&j,v&pu$A\ t_7%܆4R8g ?#n#N{1L9# KM?)Txԯm4Q eS/1uC&n>v3>'$x i} RBsNOuSnk GH1>pRfDݦJuS­LOEne9< -]J{X/-0GOKK;ֺW̷Baפ =R2XG8ciLaԾG\},Rԗ)2NZʄs$\Ed\1f9t]RE?%H7%$\QO>KUsj Á]Sb}X & q kR"D-JS2sO}Uj9(%INtSsR"=Hla|9.X@S0 ~}}?X;8bcG[ʇ +|f|k|njMV Ϙ0"# F‘~$K֦F`__C KsM]f[:pfMeVtmdkP!(2K g\^t4饝T7HAھHL%}eu]EȨ;\߸`@7s%\X:]{x~'a,Z=lcx⼑\†n Nd{pnqY8ҕ2rr(Km{i]ذ xx;?GuGƼ#Mo' _qScm5k?bަTpt57mbtSLg ("dw2{J0ۅ}.6DŽ'<20äFFD@pa=x]aO) 've_ pPY+EЦI7H8cdo x".@״lᛇUrB7mDy=D yrEcU'tf<'_21uMq!~pz7Gqc:{loP%ZH:kծJp_ky6\ME}ߍrAp3j Nk˃iqV(8dݔp$\Bk9~{6& %dngZ8A(S7WSdZg AZ=$7`&&X护M5*~Z${S:ƎVBLJ`Y+TH$\gR|%1닶43hXz4J w @(_R `_ g,]ƜrlY.8.lg;jȾX8w޸`oO:ݔpZDE-#*5D^A;LI56(VX 2"sAVãTl1qVx`L2% 4MTદpuwVUn2"`Cpٯ+ sAGETdTQXRP#m5)hՖg}Ԉ $!)f,Q*>ZEB ַG0ujEq@{usIg0ufs_7ZY_z_W9.Or?KSy/?X?'[Ư -q'+l魌~_$ۘ\zFܠ=F_.LkQG+Y I1ӠMå;o]syk.Z5׽FĖ .?Ǐ}Q$^k0p2E~L4Z ^b7k'7-Xx]#]Z\)߽+k"IJj~ނU7:>"p3{+P֬&Y}4cI,ҝ~eݣV_y{&!DqK;]F>pqO׳UQ+fRn r[mEeZv7a񺷛p^k͖wpR{YP^:y-[W!=AvZk^w7nquEٍ+y*+E/lͼn=XN|qڏۏ}kO*lXZu^{Nj7hk k!9n>JTO2A: 8A ߆xm8 Wgqys޾;[Y5kmx] ~G7K~ t"\ZjU\^y[sΜ ~lm}r~̜~=oT*ZKc2 J -에T{aku)\`f+Qg>q,^yײjv}5}_CC9?׍5py¸<{{n9ZFp699%D@7˚qDuX7MA -0PGDrz-oD]][,Sghۃ,o)ZOXSK[j_Md,o) =doٽ]w:I߭Vym: NQ)#5Pya:1Mda -0D3ܩIF) rv7u2Vysr5NZ_kWG,7.W1n*I'cT/#^ "DA7Yy#H^GN(JPysRn@ͽZWm$Bd8bnPy!X,o*%`b ͵vaDm9*k`U74\U䷌*G`R 7{~oỲIFAJYAj'c*ay#Pvs$^e#'$;#%#7Btu /]PY(n6 }j2%5JoN3o*(SQZ@{`%(n~̼*8_$75vQ89m}3e>F7 ȼ2v3NzeVoNƔ7?NFbW6]l' ׍H)Jt!N7i_o}2M\FtIXV:}w7Q{ER*2(D8d,#YV0 'n u vqﰼ8&2M7ew"ER"IuSQ{9'Jt շ틉@;7•#%,o,Hm7as^N~FNzp┷H~2(k'ÔH5 N;Ɉ&ˊD,o,0[$'o$-#4i'c`ck9-#4O N27BVֹ0qs!NokHv2Q{p.$Zb*7'?`y#`y -Nyʼ0 Nz$ 7k'{7uv!D P´vYsv.Jy3wh9#5P%(-+y#d^oɡ7B"Cd\¼2k'qIYiT!J}q#d!7'a/:$nVަ4A7'FٯX2>c:$Ok -a "TZKX3@-DL~sCBؚJRa#jdSaSB=Pj~>?7}uݼH:ӕ8Ps%!kQ}լ'0@5s7y1zޚ: ym184hR3 ̷݃]0@5O u |d TsZB tS%dN=ycՌyh :z˨oy:I9jJym TFM~V /ɡ@N#bnױMJo St>Ղ9G|hFqK>IEou_yvX' ]0@7[Trߦc44 Is(C= T79-fo1rq},*Z Я ଔIۤ t|b- A|i{o畤79ϑ(rq@3;z(e7M>)Y'QynAy: kN`rb$z - 6 ؿɨEqWMz -AYtQNÎz:wEny gI/#Mɯ7Hq|qAJn$ʅ "o47e !en&3_$Cr[Zd3 RQVtJş y8no ~IئɋݧA"<UnƘ8MsљGm gSD;[߳8s GUd'.z$H>#+?G&/j|_KbyBg*y3]p8r T6mܭϝ8st9 skw?"cb4G1k[[[i-rgClPN^P}ŝ A-)Kt[yS2Dl;/#7\QW{!cA2fSߠ*'<A)QB^AOʓj{_l*9u3Z0694zKɈ=kԦkgX5œC/Duj+du#j{\mT W5,\BBfTuնr5Uˮ&,ZcjnEqD5򑩕\F>>qPS,*@n[1⠆+Uyk8*;傥's$d#RS.[T 9Oᅡ 56܈iOEqin5n:9 :b .lq6rX*! 75F0h3X*!/eu89~2*#6}ن~agoqԼ 8LޛUF;2ʼX>Y0R\£*C# ~}1@)kȄARl)U8t8ڏ(oEi`9sH ay|AS#3'yuwѼRR8^5xo.}?Y9GH }C/>?f69^ȣ5HWPNZ,2χP7 Uk]R'*_U|>{r Ns7RUVT J1Em(-'P6Κ#7%6FO%ɏrsλre֐e}^ݿŵQ,rm[%Hogqn 9U$Mw X~LI&M{.@;*BR"J _-e Ax!mX렯{.z?Xo˛t>g\q WIge9frBnI  Drpx+p77A,w41ތ Kc-6{3 dr+nY1@bUJ?ފYf9M2Vj_}*oR[vz[ `9N}n2}ox,7Ie;K{nR=θJBd^`rΫ"8<v{u.0me`BkX!8}x H3Cn  wzop e#8K!7*K%\X~@[AH}xe 8scs nyv6Pu fJ}WZ;I R@?G?| WJPѡ?z`A,oEoEps8zWhPyEFTj#wŕ!|o){n9@?O%ߙiͷr -pNUo1cpMCp?!;24 J9o[o 8)ur> 8ЩʏG7$8\)An^F=3u|lRbz=,p%s[ p}h{3t~7P$翛h{Ki1rM,oMbqEkoM/gi\ko e+1@[fha7Dݑ&5IPE8sְ x91JhiXGQ;AM.Wo ˹x7[J2byCs%Tz?X^]_417CcN9>$%C3W)B9fbo xD՛+N`R9`LυRd7P4w: J AIz&8CN7PJzMo =@+٥xz3t&PuuB0@-'-gi$.1@-'F˥X\JS7)JU54xoTWʢ-11@/ mn*G2 r斆L^pk鳄%s-2Ny(%-PR;: 7tHr]Ku'羷=Io*Z[L.;0@19Ul[B\xèЁK7P󰼷}DeP&-gh1fsK h e)L_o1@7#d5ԕw4}`~Yq?3;33n(Mj6J1zAŠ5P\`$1i0FBQ"4 mѴTUD:ҨW -5|k҆@1J NPlgƶfҟMTo8NV>P'z  y(ɘos|l]k^.dRgsНӟl Ut*P9bf p\&Q߰c3K"^ʉZK%ԛGk^T "zo>8nT2}Ǡ<7,$پ|Ypɸ͠RƛppgH}{%Uo9y/YkZ9T^gXaYqdQ+5e>(HOnorT,8CYR^LHꅽkzϦP: I0Mȏ\552\)uIheCkW5ToL2.fp=@3 '&r!M)4fQE655[\-G\Ћ{ZѪW -0N-8$_])yts\YOlӉX4bBoޅ tPp ˛…\sZ#N˚5[6iHom^=vh\gSr#7Г\`|tPߗQ߄VMc=-4$aykh؟uP څu4%ݔ|`Yͷě=J=>KzwR1j5L҄~[V\rg ;pmU -tR͡Ƴjlor7|HR̦z 7#ۍ@1J"9!wc,w{{j6PjޛGkNcQ[SԪloOu'70jb.7}{s~L4NZ)͡1IB>5[˺(Ǒ@sB.|m{si٫X'_yoDwb+H>Ų-9 -2/jyi$!C|.M{{ , }{si 7-"zt+XC|>mo.5:h.E<ձ7օj!'3x9܃ Grxu8{r}Ft#7G닩t<u!%ߑi,eAT.¥=_Kn$Hհ7J9#l>״7>2CIǣ_!q \>d\ͣx|ojV QuG1]{NU:3u]oy"7K7=i`bxSmIzhٛCu ` ]y$E򹛂}-I#B٭ш_o.M$XFoڝo݌|I06O5ͣ![$źiNϳ $X'Ro`FXGt!' #ycoN|HvK_~4w^62[o>8>ަ]oj˝^?NQȣ`Gp٩6kכKzIv^\* l$X4m^֬7Do`!ϔHͥI`%: V^9 @qi+X'Nt>7ݤUo.Eo`)՛GeMJx[ ެF>_1[uǿ A5lMeH:f` C.50ӦJij>d+ְrRqfP@&E~߷﹵\}=:sޯk'ΖZޑͦRR -X2q etӴ"ݓ -H9{I5+H7*Po|s -=z +N욾!yۙ7G~2o(1Ņ-ۅ U]Ϸ_tPZ;2"˻dPf^OZޡ%#UC3AW{2ZrC}آqN̼nY"9so۰A$B-Y(5Oe)D1o(7)FoLZMo(7w.Fo!詺ϷHF.aPr;[Ho(9?,Dov^>o) -qOƲJ~/(ȾUzC~R_9PvNɿXXμBkG ׬r -My9 -䝛W -꿖9-gPAwo7Tg["W*k3r-v&_HZ4&"Z{+Dʼ".38{;m`˲E*#aT~E2'pN:*+8;'傼}8'Q)^9CS|K7Tӭ%HF?d;P)N%rk*[fqtJd'Tw[,]XIo } .C?7 n>\PQ($ rNlkI߂d>$~g7TWP -ˇ6ͩߟB=$1(#ѴnҜtO*i=.71o'upL{y4]'|βhzĞG .Y=:$&KaLn٭0YpL9 pB,+kUpW](y"D?-qet@x<({aހt^erӳn2zށ{+[3А -(x@-Sz506{xgF?PP9"Q].Lpe۵g -ƣ .3ug[,< ӧ -V08]55刭4O镅Hj+ h x],ݥg0OXl\6 ˔AzK& Ɉ8(lf\"s8(Ƭs3 .3p@c^w? Pp|.<M8|DE88+'\"'=>}`E24tۧn{M9}dB}|? -PxqK~ h.][ '_Z` 8n$2yzZ`\u\$О5pe} ;]p|;I92'\,=qaT"YJo@>;b9q?prrEj^p@R7\8UNCL{ހV8XFhEpc߀<]w\"fY.D UNIȜW(681_⎂d}Z;[7\"(?&.u7Ʀk~o46X2Ercɸk_\,cW2p@E҂79[8]m U\"S_7}ܶ+RkkF,ڔNܺSǶ5q5舷py[Ge,$83VܫWgE-w%ЩwIZmfTzT1ӂki_Z6 -#Q˙AC?3 -"{QiL- s@7V&7;WǞq̓;Np@wgܚkM'.1孢k\\$=KlT\"6qP uFWc}KnL{^pjYV܋osd#XKȰ v@մ@ .8 +6qs-aJR_E!([>)ɰwȆAy9d 39p96J ^_ޤ7{}ݮNi еjz{ZyNM-[8K^׾jj&WAyIz{ӵSZ-)9(_QUw?啘 -$AQ#+X ->x4 "2h;NA* -% a)Pa HA) ;gY{w?;ݻ;{o4Hz%"ADo)E$8P"HH $9Lo8T98I"/!S9R{K[# #xVVX)Aȏ| NUl@p Po&vPfKo|& ]`M~.48qmX͡BQreI(6Lli-c%ob)IJ)70qDPQvm{uJ:7\* 6b 1X_5؁ L4X vb3{#$@D9Z}hp9L -8'%& {PnE$"4)m778&Lwnoepxo%?L,pn\㨷1Ln+5qtA7X]6%xPrɋ `(9gwre';ZvQ.Yt^T$ m7 -O!_k#5ݯ4cH $JIz j{.i&-y1%L.Vbp=ymAlzov>BQrEP̌"Eme^M?#K\ `4`z8YtЪ2:ʑn/qHw7&yU]T]5)ly=5HN\o'`9rsGn&=/l+洄nYMjJc!YC{쒀,J(`b__4߀vMdd`U,{7aZ~ҏ1r~En$7Ħ7ʯ<൦!x\oou~lQTlaboқ|La(L(pݳ2EFӑ-z]\ш`b;$B^4yV}["l{ e+Oã1 *-{#$@@Szj.l 6a䬮TΔa}ܫ)rQ i"eͿ?Ckĭ4z\*ʡۻgo`pSEo>gz>iJbӄzJ|1.ڛGS:qdT.D64b 6lκpy $Ey&ZI,OMUXjL6rw&ܖͻY{tL..]IyJ -sN/ߗs) K.|"T= Kz3H=7t}wjjC,"[_)ZzD%E_/bL!8Ӂ)R&x-wޏ/0rރn ʜT$2fRUWY.{j0y *674[*%gP[qlo.y &}XIFqcҡ53u tFZYE?W{>,Ɇ&q h!7!6Mh1/ԋE%&*z?M&yI]^ TUoeAP6g.$HlΉEy:i NF6{jwb[B?+fA4D{7bk}RLlN?TEgkaad \` Fr+b"ٶ!k=UŦ euɶ/@6UqM8`} ?Mcp̟@Qr3:-f:^$gvcVL.5`,;q(%Dr)g `yٔPZ fTY.3mqYH\[c)7-w'8W&?,Me"{7c Uqb*7(BT_|fUo)d"SVJ.0q!c~^6h D62t!Z4L $z3HE&Tp -Gmd aҲrJVʂ>l̘ n/Jj{Sư5B҇d[$g$;|gp\-AW-<>nT="8 ydt|C}aLÀkA܄3Q1.r?)xұ[V -h3 d t"=T͖ '[wFeK!) R6V -49{Yx} -m?zΛPQ;Fvw(97uUK6S7M^uJ.*cGׄ .$SF\ˌ_kD0;$c .gPBTs1h3NrMACD2<'jp!eOd(AA7cM(/Va*g_i)Wc֜c(k'ە(KԮzy$ 򏭢3C@$0?!vi! -%QSE@EXݒ?lVC]A Eإ -*hE`/ymiiݖ7ܙs?sWO.X}[2t6BmE`6NBnn l@) '?X>Y e ^#S?YB\ܑnƒskl_m^FPB-/+?faP!E2 .dup}R-Ԩg - Y1T.k'Ql o܅φoKll}aW%rɳ&$Rxؾb11ԁ'lO\)d9ҭ)>Mp]7Z‚G':u΀(q) -u$dlM -'wk S-| O;y] -1ԍ]7T5ھGOݸ$kTF`OV|PčM]tMcY }v,n \fcj{.)ٚUŠrLV$B@u*B7F6y2|rr;+\[ιrptrCx!r](P -g=c(1 fB8P -G]d i9++m'AUdQn3%ilrO8Ӫo+9ETOiSWhrFdw3:{ϣlW .6lY{ex!8t_xW>mΈȀ5{J=WFsLhPv$}_RMפ} -˴{Bzr5x.UI&${C[#Eqx(կa9EP\%wEOwS8q'^ӘD٢#AJSTY+v0ZGM0٪]lQm>Pvy$+^D & H}Bp8o/wo$_u.7Zհ 7D-z VwԼuYa0N@Ye囍 'cmt-ƗYkC#01*SuS,٩p܅[C_qbhjF Y.&L0W7O.(Uf?UʍlE0%ݹ@"qy*`@vyIy%YqpF$Xu{9"yFmNp*{BeQ-f^}szs>^D lZu4']G67TN@]6dA3$6(OY99Q#a9q 9\=Mx$U /> o$Co0:')8\٣!=zЎˋ5~Q6#]1rZ1O/h_ ~]e7yasrvlRB$@Ifs3FJ1!]Cmhykr-!إ?ns෣Ξr ONۯ0BBʆt/6ds;t>u>-.3oJM:h$c۶2kEȫ`KE9 -~&enc*wnjIcw5kΗ@,'k;}Y.Z.\\)+;=V~M+\`I)^*-O0 ! AB\u߃7%˵ .}w[<Ċsdr#Go?"Z-6K1 -$d/:0\}]7> -vTUC:ˉA€e>Ś>stream -HyTSwoɞc [5laQIBHADED2mtFOE.c}08׎8GNg9w߽'0 ֠Jb  - 2y.-;!KZ ^i"L0- @8(r;q7Ly&Qq4j|9 -V)gB0iW8#8wթ8_٥ʨQQj@&A)/g>'Kt;\ ӥ$պFZUn(4T%)뫔0C&Zi8bxEB;Pӓ̹A om?W= -x-[0}y)7ta>jT7@tܛ`q2ʀ&6ZLĄ?_yxg)˔zçLU*uSkSeO4?׸c. R ߁-25 S>ӣVd`rn~Y&+`;A4 A9=-tl`;~p Gp| [`L`< "A YA+Cb(R,*T2B- -ꇆnQt}MA0alSx k&^>0|>_',G!"F$H:R!zFQd?r 9\A&G rQ hE]a4zBgE#H *B=0HIpp0MxJ$D1D, VĭKĻYdE"EI2EBGt4MzNr!YK ?%_&#(0J:EAiQ(()ӔWT6U@P+!~mD eԴ!hӦh/']B/ҏӿ?a0nhF!X8܌kc&5S6lIa2cKMA!E#ƒdV(kel }}Cq9 -N')].uJr - wG xR^[oƜchg`>b$*~ :Eb~,m,-ݖ,Y¬*6X[ݱF=3뭷Y~dó ti zf6~`{v.Ng#{}}jc1X6fm;'_9 r:8q:˜O:ϸ8uJqnv=MmR 4 -n3ܣkGݯz=[==<=GTB(/S,]6*-W:#7*e^YDY}UjAyT`#D="b{ų+ʯ:!kJ4Gmt}uC%K7YVfFY .=b?SƕƩȺy چ k5%4m7lqlioZlG+Zz͹mzy]?uuw|"űNwW&e֥ﺱ*|j5kyݭǯg^ykEklD_p߶7Dmo꿻1ml{Mś nLl<9O[$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! -zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)Km endstream endobj 5 0 obj <> endobj 20 0 obj [/View/Design] endobj 21 0 obj <>>> endobj 12 0 obj <> endobj 10 0 obj <> endobj 22 0 obj <> endobj 23 0 obj <>stream -%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 17.0 %%AI8_CreatorVersion: 19.2.1 %%For: (Zachary Mitton) () %%Title: (metamask_icon) %%CreationDate: 6/15/16 2:23 PM %%Canvassize: 16383 %%BoundingBox: 98 -140 188 -44 %%HiResBoundingBox: 98.7919746568114 -140 188 -44 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 13.0 %AI12_BuildNumber: 147 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBProcessColor: 0 0 0 ([Registration]) %AI3_Cropmarks: 79 -156 207 -28 %AI3_TemplateBox: 180.5 -120.5 180.5 -120.5 %AI3_TileBox: -163 -488 449 304 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 6 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 1 %AI17_Begin_Content_if_version_gt:17 1 %AI9_OpenToView: -39.6666666666679 23.666666666667 3 1419 866 18 0 0 -5 38 0 0 0 1 1 0 1 1 0 1 %AI17_Alternate_Content %AI9_OpenToView: -39.6666666666679 23.666666666667 3 1419 866 18 0 0 -5 38 0 0 0 1 1 0 1 1 0 1 %AI17_End_Versioned_Content %AI5_OpenViewLayers: 7 %%PageOrigin:-220 -420 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 24 0 obj <>stream -%%BoundingBox: 98 -140 188 -44 %%HiResBoundingBox: 98.7919746568114 -140 188 -44 %AI7_Thumbnail: 120 128 8 %%BeginData: 22554 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C45FD24FFA776FD75FFA04A4AA1FD73FFA04A754A75A8FD71FF7C4475 %4A6F4A6FA8FD6FFFA04A754B754B754A75FD6EFF764A6F4A754A6F4A754A %76FD6CFF764A754B754A754B754A754AA1FD69FFA8754A6F4A754A6F4A75 %4A6F4A6F4AA1FD44FFA7C9A075A8FD1EFFA8754A754B754B754B754B754B %754B754ACAFD3FFFCFC9C299C1997476FD1EFFA76F4A754A6F4A754A6F4A %754A6F4A754A4B4AFD3BFFA7C99FC198BB98C198754AA8FD1DFFA8754A75 %4A754B754A754B754A754B754A754B6F76FD37FFC9C99FC198C199C199C1 %99754A75FD1DFFA74B4A754A6F4A754A6F4A754A6F4A754A6F4A754A4A76 %FD31FFA8C9A0C1999998C1999F99C199C1746F4A4A76FD1CFFA1754A754B %754B754B754B754B754B754B754B754B754B6F7CFD2CFFCAC9C89FC198C1 %99C199C199C199C1C1C175754B754ACAFD1BFF7D4A4A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A6FA8FD27FFCAC9A1C2989998C199C199 %C199C199C199C199C16E4B4A754A75A8FD1AFF7C6F4A754B754A754B754A %754B754A754B754A754B754A754B754A75FD24FFC9C99FC199C199C199C1 %99C199C199C199C199C1C1994A754B754A6F76FD1AFF764A4A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A76A8FD1DFFA7C9A0C1 %99BB989998C1999F99C1999F99C1999F99C199C199994A4B4A754A6F4AA8 %FD19FF756F4B754B754B754B754B754B754B754B754B754B754B754B754B %754B754A76FD1AFFCAC299C198C199C199C199C199C199C199C199C199C1 %99C199C199994B754B754B754A7CFD19FF764A4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A99FD04C199C1C1C199C1 %C1C199C1C1C199C1C1C199C1C1C199C198C199C199C199C199C199C199C1 %99C199C199C199C199C199754A754A6F4A754A4A7DFD18FF7C6E4B754A75 %4B754A754B754A754B754A754B754A754B754A754B754A754B754A754BFD %05C1BBC1C1C1BBC1C1C1BBC1C1C1BBC1C1C1BBC1C1C199C199C199C199C1 %99C199C199C199C199C199C199C199C199754B754A754B754A754BFD19FF %754A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A4B74C199C199C199C199C199C199C199C199C199C199C199C199 %9F99C1999F99C1999F99C1999F99C1999F99C1999F99C1994B4A754A6F4A %754A6F4A76FD18FFA14A754B754B754B754B754B754B754B754B754B754B %754B754B754B754B754B754B754B7599C2FD16C199C199C199C199C199C1 %99C199C199C199C199C199C175754B754B754B754B754B75A1FD18FF4A4B %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1 %99C199C199C199C199C199C199C199C199C199C16E4B4A6F4A754A6F4A75 %4A6F4AFD18FFA16F4B754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B75FD16C199C199C199C199C199C199 %C199C199C199C1999F6F754A754B754A754B754A754A7CFD18FF764A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A9999C199C199C199C199C199C199C199C199C199C199C199 %9F99C1999F99C1999F99C1999F99C199994A6F4A6F4A754A6F4A754A6F4A %4A7DFD17FFCA4A754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B7575FD17C199C199C199C199C199C1 %99C199C1C1994B754B754B754B754B754B754B754BFD18FF764A4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A4B74C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1 %99C199C199C199C199C199C199754A6F4A754A6F4A754A6F4A754A6F4A7C %FD18FF754B754A754B754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B7599FD13C1BBC199C199C199C199C1 %99C199C199754A754B754A754B754A754B754A754B75A8FD17FFA04A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A7599C199C199C199C199C199C199C199C199C199 %C199C1999F99C1999F99C199C174754A6F4A754A6F4A754A6F4A754A6F4A %6F75FD18FF4A754B754B754B754B754B754B754B754B754B754B754B754B %754B754B754B754B754B754B754B754B754B754A9FFD14C199C199C199C1 %99C199C175754B754B754B754B754B754B754B754B754AA7FD17FF7D4A4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A4B4AC1C1C199C1BBC199C1BBC199C1BBC199 %C1BBC199C199C199C199C199C16F4B4A754A6F4A754A6F4A754A6F4A754A %6F4A75A8FD17FF764A754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B7575FD13C199C1 %99C199C1BBC16F754B754A754B754A754B754A754B754A754B6F75FD17FF %A84A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A4B74C199C199C199C199C199 %C199C199C199C199C199C199C199994A4B4A754A6F4A754A6F4A754A6F4A %754A6F4A754AA1FD17FF76754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %9FFD11C199C199C199994B754B754B754B754B754B754B754B754B754B75 %4B75A8FD16FFA86F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A99C1C1 %99C1BBC199C1BBC199C1BBC199C1BBC199C199994A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A6F4A6F75FD17FFA74A754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A754B754A754B754A754B754A %754B754A754B754AFD13C199754B754A754B754A754B754A754B754A754B %754A754B754ACAFD16FFCA4A6F4A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A4B4AC1BBC199C199C199C199C199C199C199C1996F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A75FD17FF7D6F4B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B7599FD10C19F4A754B754B754B754B754B %754B754B754B754B754B754B6F7CFD17FF764A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A7599C1BBC199C1BBC199C1BBC199C1BBC199C1C199 %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754AA8FD17FF75754A %754B754A754B754A754B754A754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A7599C199FD12C1994A754B75 %4A754B754A754B754A754B754A754B754A75FD17FFA8754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A7599C1999999C199C199C199C199C199C199 %C199C199C199994A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A7CFD %17FF75754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B7599C199C199FD13C1 %99994B754B754B754B754B754B754B754B754B754B754A7CFD15FFA8754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A7599C199C199C199C1BBC199C1BB %C199C1BBC199C1BBC199C199C199994A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A76A8FD13FFA84A754B754A754B754A754B754A754B754A75 %4B754A754B754A754B754A754B754A754B754A754B754A754B754A754B75 %99C199C199C199C199FD0FC1BBC199C199994B754A754B754A754B754A75 %4B754A754B754A754AFD14FFA14A4A754A6F4A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %75C199C1999F99C199C199C199C199C199C199C199C199C199C199C199C1 %99754A6F4A754A6F4A754A6F4A754A6F4A754A4B4AA8FD14FF7C4A754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B7599C199C199C199C199C199FD11C199C199C1 %C1754A754B754B754B754B754B754B754B7576FD12FFA8A151754A6F4A75 %4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A4B74C199C199C199C199C199C199C1BBC199C1 %BBC199C1BBC199C1BBC199C199C199C199754A754A6F4A754A6F4A754A6F %4A754A757DFD11FFA14A4A4A754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A754B754A754B754A7575C199 %C199C199C199C199C199C1BBFD0FC199C199C199C199754A754B754A754B %754A754B754A754A4A75CFFD10FFA8754A4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %4B6EC1999F99C1999F99C1999F99C199C199C199C199C199C199C199C199 %C199C199C1999F99C199754A754A6F4A754A6F4A754A6F4A754A4A4ACAFD %11FFA8754A754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B7575C199C199C199C199C199C199C1 %99C199FD11C199C199C199C199754B754B754B754B754B754B754B754ACA %FD13FFA87C4A4B4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A756EC199C199C199C199C199C199C199 %C199C199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C199754A %6F4A754A6F4A754A6F4A754AA7FD17FF75754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B756FC199C199C1 %99C199C199C199C199C199C199FD11C199C199C199C199C199754B754A75 %4B754A754B754A76FD13FFA8CA7DA176754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A4B6EC199C1999F99 %C1999F99C1999F99C1999F99C199C199C199C199C199C199C199C199C199 %9F99C1999F99C199C198754A6F4A754A6F4A754A4B4AFD12FFA87C4A4A4A %754B754B754B754B754B754B754B754B754B754B754B754B754B754B754B %754B754B754B7575C199C199C199C199C199C199C199C199C199C199FD11 %C199C199C199C199C199C199754B754B754B754B754A75FD14FFA8754A4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A4B4A9F99C199C199C199C199C199C199C199C199C199C199C199 %C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C199C1994B4A754A %6F4A754A6F4AFD16FFA8A14B754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A7575C199C199C199C199C199C199C199 %C199C199C199C199C199FD11C199C199C199C199C199C199754A754B754A %754B6FA7FD18FF516F4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A4B4AC1999F99C1999F99C1999F99C1999F99C1999F99 %C1999F99C199C199C199C199C199C199C199C199C1999F99C1999F99C199 %9F99C1754B4A754A6F4A6F4AA8FD17FFA1754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754BC1C1C199C199C199C199C1 %99C199C199C199C199C199C199C199FD11C199C199C199C199C199C199C1 %75754B754B754AA7FD15FFA8A14B4A4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A756E9999C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C1BBC199C1BBC199C1BBC199C199 %C199C199C199C199C199C199C174754A6F4AA1FD17FF4B6F4B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B754AC1C1C199C1 %99C199C199C199C199C199C199C199C199C199C199C199FD11C199C199C1 %99C199C199C199C199C175754A76FD18FFCA4B4B4A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A6F4A9999C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C199C199C199C199C199C1 %99C199C199C1999F99C1999F99C1999F99C199C16E4BA8FD1AFF756F4B75 %4B754B754B754B754B754B754B754B754B754B754B754B756F9F99C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199FD0FC1 %99C199C199C199C199C199C199C199C1A1FD1CFF4B4B4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A4B4A9F99C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C1BBC199C1BBC1 %99C1BBC199C199C199C199C199C199C199C199C198CAFD1CFFCF4A754A75 %4B754A754B754A754B754A754B754A754B754A754B9999C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199FD0FC199C1 %99C199C199C199C199C199C199C1CAFD1DFFA84A6F4A754A6F4A754A6F4A %754A754A754A754A754A6F4A99999F99C1999F99C1999F99C1999F99C199 %9F99C1999F99C1999F99C1999F99C1999F99C199C199C199C199C199C199 %C199C199C1999F99C1999F99C1999F99C199FD1FFFC299C1C1C19FFD0FC1 %99C1C1C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199FD11C199C199C199C199C199C199C199C2FD1EFFCABBC1 %99C1C1C199C1C1C199C1C1C199C1C1C199C1C1C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1BBC199C1BBC199C1BBC199C199C199C199C199C199C199C1A0FD1EFF %FD18C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199FD0FC199C199C199C199C199C199C198C9FD1DFF %C9C199C199C199C199C199C199C199C199C199C199C199C199C199C1999F %99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1 %999F99C199C199C199C199C199C199C199C199C1999F99C1999F99C19999 %A1FD1DFFC9BBFD19C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199FD0FC199C199C199C199C199C199C198 %C9FD1DFFC1C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C1 %99C199BBA7FD1CFFC9FD1BC199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199FD0FC199C199C199C199C1 %99C199CFFD1CFFC298C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C1999F99C1999F99C1999F99C1999F99C1999F99C1999F %99C1999F99C1999F99C1999F99C199C199C199C199C199C199C1999F99C1 %999F99C1999F98C1A8FD1CFFFD1EC199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199FD0FC199C199C199C199 %C199C199FD1CFFC9C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C199C199C199C199C199C199C199C199C199C199C199 %C199C1999999C1999999C1999999C1BBC199C1BBC199C1BBC199C1999999 %C19999989999999899A8FD1BFFC2FD1EC1BBC199C199C199C199C199C199 %C1999999C1999999C1999999C199BB99C1999999C1999999FD0DC1999999 %C1999999C1999998C9FD1AFFC998C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199999899999998999999989999 %99989999999899999998BB999998999999989999C199C199C199C199C199 %C199C199C199999899279998999999A0FD1AFFC2FD23C199C199C199C199 %C199C199C199C199C199C199C1999F515299C199C199C199C199FD0DC199 %9999C1992E4BC199C199C2A8FD18FFCAC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C19999989999 %99989999999899999998C175510528057598999999989999C199C1BBC199 %C1BBC199C1BBC199C19999989927286FBB99C198C9FD18FFC9BBFD25C199 %C199C199C1999999C1999999C1BB994B2E0628272E279999C1999999C199 %FD0DC1BBC199BB992E282E99C199C1A0FD18FF9FC199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C1999F99 %C1999998999999989999BB98752706052827280528279998FD0499C199C1 %99C199C199C199C199C199C1989998990528054B98C198A0A9FD16FFCAFD %29C199C199C199C199C1999F7552282E272E272E272E272875C199C199C1 %99FD0FC199C1752E062E51C1BBC199FD17FFC998C1BBC199C1BBC199C1BB %C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C199C199C199C199992706052805280528272805280528989999C199C199 %C199C1BBC199C1BBC199C1BBC199999951057699C199C1999FA8FD16FFFD %2AC199C199C199C199C199A07576272E2728052E2728272E277599C199C1 %99C199FD0DC199C1759FFD04C199C199CAFD15FFA7C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C1999F99C199C1BBC1C1C1999F7575272827280528057598C1 %999F99C199C199C199C199C199C199C199C198BBC1C199C1999F98BBA7FD %15FFC2FD2EC199C199C199FD0BC17576512E27C19FC199C199FD0FC199FD %05C199C199CFFD14FFCA98C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1999F99C1 %999F99C1BBC199C1BBC199FD05C1999F99C199C199C199C199C1BBC199C1 %BBC199C1BBC1999999C199C1BBC198C1CAFD14FFC2FD31C199C199FD11C1 %99C199C199C199FD0DC199C199FD05C199FD14FFA8C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C1999F99C199C199C199C199C199C199C199C199C1 %99C199C1999F99C199C199C199C199C199C199C199C1999999C199C199C1 %CAFD13FFCFFD32C199C199FD15C199C199C199FD0FC1BBC1C1C199FD14FF %A0C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1 %BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C199C199C1BBC199C1BBC199C1 %BBC199C1999999C199C1CAFD13FFC1BBFD33C199C199FD15C199C199C199 %FD0DC199C1C1C1C2FD13FFC998C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1999F99C199C199C199C199C199C199C199C198C2FD13FFFD52C199C1 %99FD0DC199C1A1FD12FFA7C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1 %BBC199C1BBC199C199C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C199C199C199C199C1BBC199C1BBC199C1BBC198C9FD12FFC2BBFD35C1 %99C199C199C199FD17C199C199FD0DC1C9FD12FF99C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199999899999998C1999999C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %98C2FD11FFC9FD31C199C199BB99C199BB99C199C199C199C199FD15C199 %C199FD0BC1BAC9FD10FFC2BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1FD0499 %98999999989999C199C199C199C199C199C199C199C1BBC199C1BBC199C1 %BBC199C1BBC199C1999F99C1BBC199C1BBC199C1BBC198C9FD0EFFCFBBFD %2BC199C1999999C1999999C1999999C199C199C199C199C199C199C199C1 %99FD11C199C199FD0BC1BAC9FD0DFFA0C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C19999989999 %99989999999899999998FD0499C1999F99C1999F99C1999F99C1999F99C1 %99C199C199C199C199C199C199C199C1999F99C199C199C199C199C199C1 %98C9FD0CFFC299C19FC199C19FC199C19FC199C199C199C199C199C199C1 %99C199C199C199C199C199C199C1999999C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C19FFD0FC199FD %0CC1CFFD0BFFA79999C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C19999989999999899999998999999 %989999C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %BBC199C1BBC199C1BBC199C199C199C1BBC199C1BBC199C199CFFD0BFF99 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C1999999C1999999C1999999C1999999C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C19FC1BBFD09C199 %FD0CC1CFFD0AFFC2989F99C1999F99C1999F99C1999F99C1999F99C1999F %99C1999F99C1999F99C1999F99C199C1FD0499989999999899999998FD04 %99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1 %999F99C199C199C199C199C199C199C199C199C199C199C199CFFD09FFCA %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %FD07C199FD0CC1FD0AFF99C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199999899999998999999 %989999C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C1C1C199C1BBC199C1BBC199FD04C1 %FD09FFC999C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C1999999C1999999C1999999C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C1C1C199FD07C19975754B27A8FD07FFA8C1999F99 %C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C199 %9F99C1999F99C199999899999998FD0499C1999F99C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F %99C199C199C199C14A27F827F805F8F8F852A8FD06FF9FC199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1BBC175270027F82727272027F82752FD05FFCA98C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C1999998999999989999C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C198BB98C198C199C199C199C2A0A0A0 %C9A127F827F827F827F827F827F8F87DFD05FFC299C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C1999999C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C299C199C2A0C3A0C9A1CAA7CAA7CAA8CAA8CAA8A8 %2727F8272727F8272727F8274BFD06FFA0BB999F99C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C19999 %98FD0499C1999F99C1999F99C1999F98C1999998C198BB98C1999F99C199 %A09FA1A1A7A1A8A1A8A1A8A8A8A7A8A7A8A1A8A7A8A1A8A7A8A127F827F8 %27F827F827F827F8A8FD06FFCF99C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C29FC199C2A0C9A0C9A0C9A7CAA7CAA7CAA8 %CAA8CAA8CAA8CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA8CA27272027272720 %272727F852FD08FFC299C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C1989998BB99C199C199 %C2A0A0A0C3A0A7A1A8A7A8A1FD07A8A7A8A7A8A1A8A7A8A1A8A7A8A1A8A7 %A8A1A8A7A8A1A8A7A8A1A8A7A8A127F827F827F827F827F827A8FD08FFA1 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C198C2A1C9A0C9A1CAA7CAA7CAA8CAA8CAA8CAA7 %CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8 %CAA7CAA8CAA7CAA8A8FD0427F8272727F82752FD09FFCF98C1999F99C199 %9F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999998 %BB98C1A0C9CAFFAFFFA8A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7 %A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1 %CAA127F827F827F827F827F8A8FD0AFFC299C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C2C9CFFD0AFFA8A8 %A7A8A7CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CA %A8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8A8FD0427F827F827F87DFD0BFF %A8C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %989999C9A7CFFD10FFA8A87DA7A1A8A7A8A7CAA7A8A1A8A7A8A1A8A7A8A1 %A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1CAA127F82727 %5252767CA1A8FD0CFF9FC199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C2C9CFFD16FFA8A8A1A8A7A8A7CAA8CAA7CAA8CAA7CA %A8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7A8527D %7DA8A7CAA7A8A8FD0DFFC998C1999F99C1999F99C1999F99C1999F99C199 %9F98BB989999C9A7FD1CFFCFA7A87DA17DA8A1A8A1A8A7A8A1A8A7A8A1A8 %A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A1A77DA8A1A77DA7A1A1 %A7FD0EFFCAC199C199C199C199C199C199C199C199C199C199C2A0C9CAFD %23FFA8CAA1A8A1A8A7A8A7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CA %A7CAA8CAA7CAA7A8A1A8A1A8A1A8A1A8A8FD10FFA0C199C199C199C199C1 %99C199C199BB98C199C9CAFD29FFA8A77DA7A1A77DA8A1A8A1A8A7A8A7CA %A7A8A1A8A7A8A1A8A7A8A1CAA7A87DA8A1A77DA8A1A77DA7A1FD11FFCA98 %C199C199C199C199C199C198C2A0C9CAFD2FFFA8A8A1A7A1A8A1A8A1A8A7 %A8A7CAA8CAA7CAA8CAA7CAA8CAA7A8A1A8A1A8A1A8A1A8A1A8A1FD12FFCA %C198C1999F99C1999998C2A0CAA8FD33FFA8FFA8A87DA77DA77DA77DA77D %A8A1A8A1A8A7A8A1A8A1A77DA7A1A77DA7A1A77DA7A1FD14FFA0C199C199 %C199C1A0FD3DFFA8A8A1A8A1A8A1A8A1A8A1A8A7A8A7A8A1A8A1A8A1A8A1 %A8A1A8A1A8A1FD15FFCA98BB99C2A0CFFD41FFCFA7A8A1A17DA8A1A77DA8 %A1A77DA8A1A77DA8A1A77DA77DA17DCAFD16FFC9A7FD49FFA8A8A1A8A1A7 %A1A8A1A8A1A8A7A8A1FD04A8FFA8FD66FFA8CAA8A8A8FFA8FFA8FFFFFFA8 %FD12FFFF %%EndData endstream endobj 25 0 obj <>stream -%AI12_CompressedDatax$&?d& C;\;fJ-n[[YUZ(xd&,f #+3q98OxOq< ?wo ~7_{ˏ~/&G4M~Vۯ߼LG{4?oqwo^_^ޡݻ篞g/PWnC} 4`e߱=&7Ô?L/ޣvu&3%Co^|O߾yq7/߼W?>y~^|0;qv~c7/o^a|t/_/tqzWw0R<3ٯOaC)?l/Jo|ۿi[Lݘج{K̬̂1??J[x?~W~NguG|˻FѤS7_ܽD/ H1oɦ >Kz3 3y}vgӭw2IM~?WyOtҺ2!Lj}.6(^{_G<Ѽb |aoSl߿DŽkѽ_bٚO1';=I~R4!o;H]cձW?y=5w7_j|ӷR}To?~_^bqQ>mŃߔ?뿏 Bӛ{U>}|CsL!޽zn -!}Ho_wg߾܎Ͽzz_~˧ߩO n_|=w.̙/|ܿ8~_xNuTOX[˷Zߥfi>$_>ksP3|q-y=?F?!]϶j~xx7/~tS/>\?߿cwwI|+r;鳶|0e> loq\߼3L/pba,H=;0?)vU\) ߀%%Ӧ\ \܌x[f`*nU-LDIo^iSi.繜5Jz7ѵ][*FAiUU*'-VmӯVuY[a^^Zd]fU͛V+ebe7g]k;la]/WkdYԬU)۵Z)*և:YeXtMe@CY#չk)7ܲԓŗYUeLIɭ̍zʵؔF2 ssλɝP-Vx>'O[L .C -S -p7v v)esUsSʧ|o-&7 LNyni̕W*^rdNONtemwp|:[l6#ut}>_\ތXwoM7 usnnnn#n1aoz^m7r*U9զL _^*qSªUq 8R$l!z7M9kӪ\ʴ*ySҪU M*vU̪KS>_֣_WENf]Z%. bXv 2ʌd)v64o򬡙R&)$%JR\)vWL%Ϫ?')WLRr)8K R|)%Ѓֵ;zeY eeگed&G/fdndbN2yw|Q^Z^Jd^Fq``2Ϡ[W_t,yP5 j>H73_J F1aoטt@H tr@x#HuN D;x;p{{@}w & -D5CpqnX. K׌ucq7g\DW);2UnC|Ey x;Rٙ-خv8Pz? gx'H˗v؅0ܮH *`Cld!2r.y 9&*w"6`ټ.b/+>P,"eXalX~PdS }bٛ2<duXE3Ϩr;.E9J\d('}(bs=U-l4W=9"}ZӬYAL6%Ts끼VJ{OX 2=ye[3UCwD翇d?Sԇos<;XԵ?ʏOBF7mLE߰s8҆+ H$" (" tAy\( !DʢJBFǭH|! ,DiȪ4$uN"e(rE"R,RQш‘Q ,e$JI OeSB%'ЈjFĥkK(2Qhؔ|J5t[듖|97nI?FյX4̲P,~)uuxIx" ?4 Qs E6< KCvD1l4맕aýE|r.VOoGEq3- -U -ͪLTTJэEUZ*mXM]JY\9UDi%%2r5=Ҵъ %s(It8i4fCT -a);12y?Ӌ+[ @c&*PV5m\86 ŸV+-7bU[#w)Zf{% 0<@jXp1frb=]9It=G9 [>EXy+3KoJO+ï~QQ0:1L<98Ilj> -Ѿ8Jl+u!3)]Hh% \hB#ڸLo{[Z&qk"fÊmIWCv8x}i؎u'^{߇qmCIJϞ=c@09Bk۱iccwt6sM&;XݸCI1TQoZ@,qx)G7?}HH|5FPH!H58RZ$n҇U}|OV#pDnyu:q/#7I0c (0 -+tǵl⟿Z +R>qV#6Cxji7~zQfd @,zv4./_bS;;c4`&# 4؜6%0ySIRм m{=jP]||s|<:@> lm/zr._S  -_rUXBa3|!<4S<0< Sy+De&W@AAr-^uUjKTˈXōuXᒗe?#uFnfVŜzQ27sPf'z3 mdq<9+r>3_;ug{YʪHNEa십]ԕd1U w]<[ )8c(bb<;]=),IȥId,v+POD QuVoʬ*rk$V_ba_U[X.}7IʲC#rOL,lEkY, En%ʦ"ƞD1V] ֯$XJ:vl`{6VY=+2ҡYAnVط6{;,Y ugeV^am|O;>ͩ3/Յ8/+w J.zꓨW|٘X \bU鵧_o(r = wxw<}e|m>rz|/GTy/Ҡ `+0{ط>WG|/\,O_{ُF-I}34Yݵuw}\&vřTi: }S͍2.",W(^QFl~34mٷ!,ԅl#AiEqڐ9,%#s֡%bn#'g=<r9?3:Iq?:?'3{T|ʞ:/yE X0=QnEk Zi:EBm9Ɍ \:H͕}g02x˩yj4>/=?S=w טiąYpo8&joEI'Oo>sQcb J"cTՎb&)xEUܔ&`dgFzv%ś9rJAKqj6Myt a\~ا~+7^ݽJhgyus]j/iR:SnBL%4#Kq _ KX*8^Kz\e>l/ư7TVrǩ0 7U/ޅA[lH8$&qOTqʅZ%.B5!% KJ)@(|GY:>LBaK\NKyUoItfm6ŭݐ\¦?)NUWe6>%BE6Q%J>q唚mWCyZy"obݮuc*qj+}ZX9>p,JͫoD Dޤ$;;mXGƕl_f#2-dpeQSEN[cnm4;h\; Vymff g,Ju}o14Q$:i\-.ns8ič+9m6VkϚ|['hZzvvAuaG`}t`}YhG_:m8GN84hu*, _ CkA|,|aGWuw#my{"Vފݹ(|مavWJů9jZ~,wr Ez_dxZ_KY˻JR EV 襠qwU)y@R޸0fwnxQ4;9LAǦ6 -wz·2_}q|t0>\v,нe| -(Cq7o]ͷ`['sثj[d˧1rQNت8 ETԑ<}>S|efk Hʾ_>Ctu;,0D9,'%CS9 .N[ZcqaO΃q5Ovi7bE@Om>nzw)6>FaCmq$8I?I7 ,BԽMٻ -M^A*V*#9Փi]nLʼnigLìQ&[,_?5@'Q -oDT7 F\'uY6@  ,[eh>O*r.V+÷h#exZ:i0$3QN -ߑ6V<ϒ^XEH92kV'jX\+KdP8SGYр3ɡeNqV1 e'%:U9J1џP:vlu{L[5*F6Ԯ/+B ƪ-; eфaǓJږ߶<' Oo_KM ]"\EkF0EDϤE Ȕθϔog|VUu^v)H! {'xT:UjCITҒآ(ZJ(Z4KY薮lZJa\"6Bi(D9P{i{::6KOyíO0i5*alX!X%RK努Q>E(c4,Zۙ;^o;r%( />1]3HJW.=Cq Q; JliJB%۵C^ײҫjL[ExDZ䤵 nI˴~ԻzMj,v8'2<9> Oo_êD Oس&F$(0SNeb - -0jhMMop-~v*fPj0<м4A͗ƪ]\ /0)zdp[_bݫjZL]kmrkX6ָ՚.XƬuɨ1i=d.LYO^IS)exZ 2< NO' -O' -xm7?a>Ykpi>d8$\09 f‡B'zgFxqO/e⎭cT1;ɸC0tmojj{ (qp͂C@z Nim)x?NvU!qnܺ//rvEi]ŧMi|T3ٷO'ؤ\L+u6u&٦|t,JtѲK]j=?Q`%֜k,pHyTak5u1'@IQAl P8MJ&7nJiQ<YOdF0DG3yvhCx/ew9ˣ/h-UPhnu&rSwOfc[x^$؝lxqq}csnٖIİX#!uDmw2<@7[I%~d<>WMeuv]a v2K%ѳ.ڟsq\\40Ž[Ѭ 7A 7_,~bN|v#n`^E.'[dՊ>R: ^G=,>($&K}id5VZڨpk ,ٴ 0 JJTgb60rOd`WIj>Q)3G^<-g@GpAYj,D9&# n-ƄA2 m2v'}2(W]c~TQ9oXVͨt?{<% rv 3nl=ثcvL !crU8+}Nߓ%@q+%ǭ$"~(2^ -Ma\Qݙ>Y+|Z[1z[gz[1Ԙ۴mJUr5Y|Vnodw`xNRKisl\7P΍||TGYugnKE$%V`t -6>+j::T\Phel銻PnC%oS5 -YSh -fWX1V *:I2SOBI44!eVk?xܓ'cOLj4-oKYuUh% JLfd-ytu<+qeYmˇ_CUVN`7(<ˌpaN ƍ]崂v - 6<žr[D\,-9n^$ D8aw2\0[ԡz*--ZLTFh7@K`nQ@밆#^MnRD_yrwQvpѹჲpb?McCa7Mt;HsM8)4y%qi$!wb-jqo1Pm+?:W]:HC'tJ!v\Y"fzw)n.(RD -K gѢ_ \߹D!˔] bjQ"#ΐSm2a+|;rudȼ]A>Q?ulR0Ԝn`uEss9 +Q37fQ;NLthp) n)n6WZnE*:]yu}; ӕdqYJZ,=*SZ< JӐH~[͎csߥ ÅR$Azae+̬䜽)Ò)SA$ZܬKV׬q:k)=As<2mS/LtMo@] ?}S>wihx9{JgU|>i?)D=:Kb%5ީ xyN$ȫik. ~s>4P}h/=vY@M~ ƽ#j&btan"k9hj/$s.!OE8]O$opLs*~$neqb:<7BB.oN4;rN/+"V}ZC^2TX5wb!xS`*ffs9 : i.!JUfQ?H*F!uV[6Nա$ͲWa⇰0IRnJ:'hp!bBY -`E;p8O -n2 ;aN(FXg `ᡭXbmZbw k7ax-(ÅS[/|um*][Wm!n;2._=11i8cT]#w-ՇI~ݔ÷Q~^/CQJ Rz$-Hi[Ȥ"k?hR{W5qn UVi+~ - -whpC=C~ӷݿOVrbXݽ} ?9DaSt~;X43a{GOl6O2+o:O?1'?O}t+T>:oZv{{~ywo^?ïD{ӛ7/1y)wo>;=WL?ܿ{݋w8w|ȯZ>@ȒgEYkpZtSacBM~˵)\Y$gq81A`EtY5$fL!Lzv#`!1<5ق6$CkG9`g9A&0=޲;z|ŏM?!0}<&|;t> C0/6R[¾3>8bx 7鈹3]Gg #.6ÄJ*1*ɁDݡ ؚ#{6+#Xu<*ln[T8lǀ`!k_&.GN΄bia&P,. G_;3GxbcY16TCG)D[^6Wc,d:ɓGd1IN'LH0%@;K'Y(G(MO#6%] @PbsO' d$S߆d-J4Rwg4udo+qo5!|~lb>Y Vg= !9P`W^éȝGV` jTZYBؓuZuvd1elYSg#n -}[Ϲ0qufۿdf7_/ ʃs7_ٛ? ojõL{آ!TfEbѝ(xL(2f㴸˸$n -,L"C"zJЄ \7T[ʤd + 6vBoeW]!DW1oHwm%H2kdRq'CՎXZ[i;Fx#B޵] yoeNl3_cD9D/*m% -dބSy]- u•HJbcx[ w?* rYrBʟ:3̑nsS.eUdSik3G>fT9i\h?qsiC;+x#@E:PlXJŽKun|ƛy$ 8C(A]Z0Oׄm x{\F0TOEȓHfwaaJ@Cr`%@|R%RU#>Lo;yp"MfP"0=Xpn |9APr- -23A(LOř\'"Dӂ3 -|=g>/ݡR҉Ѩ#3'7=.{&a1[f^qɷb!qVxT,@m gS+ + ~ -gZh+6,QYޚYռ9>sǹ %ґ?l mm]㽃)Lp轑ChM - SI8\Յ<%44.i+഻2>=ρo?Rݵ41?U58?!Yި&2qňLeLf9m#p\ Kdve~e K¶#}0UOe$|xA*4>:Q[# - LTU$ ֎&&4yr($bx(5M0Ɉprwݗ#-F6<_fn.(`wy|nME&ڑFȄ/;b)q&L{30D>fL4Z$.H=%7}+.Xl Vnp4s. .N:іDkP'_C:QӍŪNT)/#*tNN."PtHݼBsEn>nlG؊ TV7SBH`dp,d kEڴqƑ-@#v∣ؔfAR !D^Kh׊309Pd.WjZQJ`t-vߩkNM7nv[y=Anu =U^d*NIq<4Q) -4֣VԻs9)߲zNWQFm;-:ϱ?3RONYϹ4wJ?Y1F7lֵ, F8J\oY}68j @)7TەTEਟ -ic c&N-pdd?ѭLc̍w0SkY~x;(&68`d]@i U}q@3ŭOʨ4|nނ, xʀ1MO#iڸ&H-nqdq]$v0qTɣR;{#OR?WjJ'$q+M[1MFU7$#C tm!NLa\h{:ldneMq @G׾QKr>2k;AMx_}puB^C?1*!jT-tC"GӰ׏-8`Ǹ;EN/ik0C?* RFnvn~Uh:}|KCpބTmOM4{ޭ8Ix!@cnY`LXV@--4%"Epj|E$GΈdɄx%hkQQomh=mCQoOwN $. -4"Fj0Z̝eHӐKaDcJj{ eY[El]lB8y@A;^|Dڋ(@\▃@:usFtΐ"hνIB/>RzH#wm=Y sΈr ̎\DK墿8 W&:nХb%uV7K$3)ގI^c_SroT#xCAa#4[So+8ո{|*&D -l0;ut0ۙ\L2(D/qpoTWF\ n6!XH/@]/"#RBեx9D -1 fEh5X1FHXPIX X|5^ɓIr=t]F3}7Q0Yuoe%D>9tɆ/Ge#* BBu Vѫ!-W'0rǺ~,!^iTvtItu:+! H{D>GYƕn\/ iǩ'+z&;vʈ!5p O| ߩw{$${b[BPO;y,Ͼ YKfa5;-gLP>]yl?w"CuQ*v>ͫ֋БGqE{'{: -豛ɗWi+vwf7V'˒Qv]j.gT쒑hL~^eY݄6:oCت(^@Úd7^_y“-]- T?Q{jԿYgOcV/ɂ3xo]:y_~lukjȣ^}V%/VQ92RඈPūV Xԓ;Nev=ϻJbOӪwCi֠cû3P.v^z:p}:5I /P'U`fxX;Eu} e?HS߼N"7m&E&<0A4۴-fRny|WF$C2KaR`/v&ӋKD?:\ - DLsL^:~"r|ws5mn%n!#\ -얍y09ġxJxI&0!Sl <ཽAz#௑(k$2JS` L%NO;/< &*0yf0 -XOV:GKoe'o/^wDFFWfn -8ݱ ɉ)Z\ɹxf#~2`gWuSq!nH "74w`>`ő<`z*Po1գguΐ!?穋H#o1)g 5k78'*%PbNHj2pWFpJO^6GA0SXb;VF h=Yt՘‡Ob4Q4d1VTGKN,"6ΜF %3a-W3e^\ zr] Ki -/ƺ7EN)(ꦑ6~,VE VSӢRfn~:}t0%GQ Dj[1"FQQb3Q]?h+&@zLYB -,%Mw'Ia$ Q=uYsTB -ݓU g&TQLQLgyiP6Ǝ}]7iR^!FKBᦢ5Jd2Ɲ:qu#U -H)4v-@BN ifSeO>I"Ntl?Us*Oi4K;!ql%1- "%(ѥܹLoSSᕝm( +֌ܪ+sFFWod,QbdB(*dtI$  F`3fP"0 }̼aiටkp=YS‰7-b$'186h^H6 -Gbgy@h <):o^i&망n( -"deA53cK^3C"xfB+DuŅ*MfHV@ cX=dԥ bkug(]ꓚVI`4f !m :Ez8ӿR@LM7P[y=A - D T C׊Vc}jxɠj)BН+e <*%2.Aܖnb;_G韬蓺VɕVv,]ߠwjڵm?cDtp4Gb@ +(•<j"y/R;θ:Q4rS%f6tw"zSv[NC*FJ""9XvZ4OZYե洣ԏ8^/\NMe4c ݲ -X dp> .hۈ~$t$kUeGʀ<K^ԷxQ$d B(^먭ŞBע}*M694O -XΛ -u,_w-/ߢZ {[;=qUZ*Qu஺/[etl mѾQZ7nv`܆EU vY};ڏ %^VDoɻ^taecDX)pÉꮅm3׭8mP d\K 6oѼЋaNt43ҌGS!xzFж^pݴt3zT9BET"C ":] È@:~$@":$:Jz^.8DׁCt|8D8D5lסD0};-^_  $C@"*_ 1CAB~-D *.D*`Pwa*&`P;P P6Cp~` U-.C:Eoha;px( .N8ZH] P"ҷ~~۱tk(M+%X8ag$ ٿQBT'z7[e 'bh3PW$PfOVtE9JF;z␙q@2\!"y҆Dp-T>jhGV,J;#[4oajRAq膂i_-􍚉ick4>ْ`86:~rѴcu{BXX,VR0HP]0OθN*E >eat <~*6Gۑ"ok ԯLpǖǻ*L{U8HJtRFmKr }#B;Ӷ > -|\oa\=y)t3!-m߈7p@y7E:B޵Ҕ=׼{R?G|?Va$T1Oب*Ed\5!soD 4@.3b$nJ{Us8^}]U~9)-ר>M!:4iR#e6ݏiXeiig#[%.hb7Ϡ=[/abrQkG0^3Y'{: Ė: virwb+VUcն$4M?ʼӽP짠<-1$[&mI1_VbFVcF2i@TkʸLŰA[#4S~^Ʀ5u<4O>NJE(پv -s.MCoELIənܶx`;(VLG+qv^BdIkQdX佗ak4Y|X;;iӍ4h|^3\ĺ&qeߩiQfn~:\l(neGQj~ZߊU!n+*Z3Vbk%Iu3f퀴$݇ϢzYTW$Kn_ $\VbE Xnfa}\":*K5*Z z*[^ŕb:Qw3H`P=)I`M]aU +3ryDgQMtz8EECẠ~ -E{,6A2 xA @ɹkAv*«;- dCBDn}'dхt_"1chFZ@*̓dZި\yřLb ----8 "d/]T̴t>PI(v u 4O8oub){4W`Chr)8E h`hK}LZ*hE4i[4gcj#;DKeuk#i[Ni9sR ,i@`-~k9N.mm]+[Tɹ@tWiߣķϩ{@:Kg~LAǴB_y22O8:}UaFE#b)#N( - ,0+\10WsZpyt{˵ jD$}4E} o~߼}wwᗿynovw_^߿~ֿgx۷o^_:7_m]iFϻ/ٛw n޽}qR0Y߾y|YڛgWqn^QпOw_޿./GEQ ɺQ1FF>gg|p4smYn^۬Ù߉|@c5eD!'1ծ뀑V+Fky>٩* ~ y#ogclJ)+J&H4 -f`E -ZT:УRS9@3%O,#/hF1CvU@uyPk%dK0^;:#x#vbΜ%gǤ_YR'$MhAܖFJÒI_G5@T0{7+\Τ7710 0@,n&V 0RUBn>GRW9E&?Y#Њ -lJFIVE [VW}-^pG|L8Mo F&ЃP=c0a`oM }8&\&)x,l'PX&8gc`RIM$h8t8*sȘV<$XzUAtDTTK{K(f@Z`@Nb}Koiٗ.F|ȀGԈ4тk ɳ`KZ9M5XXI3m"3'!Dk:$$'5A:ы;I0ђ#Hs9虙x3$f -TىVl K+nKv b@LjHE# -&4240=#%sM9I $Q,*WNXYI*J GFO%]POH(ߢTseѲWT<0ƬF&v -FB&?iRa}4J[1Ez.GLLĞ=ܻͻqt[ C5>N]d`Q8nvr-[2o5ȓo";fQlk=V1s!&imI*VpdJ11GA[.T¨heg4UȔ(1qK2g ] D`~K+U։SSZcIӚ\ܬu>B|59F$x8X3=,'(SOHc\|(6sZ3ʕqpǒ:Uh!v%,z8)Js [1I9(zdŅvj>i"eې4NRcy ;j@6WIF})Gbc-I * ̜!+(+oe#BC]Ѧ K*`R/}Wq 9W|.<(8"d譙PɕLU+Y/|d^+¬[I,$ےB L -W aҏe - -/AxC!A]U8VwC !oOsᓗCj%\B[.|&HpxQ3t+d`X)dVǩ -4+GNIeΥ3I bDI `xV4HE=sJeg %h>g8H\;nZne3ٙYL4tR9%\UصءIT|>T~>T*YIxYq;gn0+)["|=8:W4nDL_BQI>lC$;w$tд;#/%~ԥbœoG_0;hAr^9\o'“ -QWT &?H8 5`RoF9Hl Iev#Y B\j'ADUÒCTG|z!VXV."=X>w 8Fi$RJ[JW|_poe-v8GO| !v6*,W`-eIMu_HBRg_T]$:߆6P8|!= -IEmla˴Rvg *t-#dD| n1w81ge/$R`i qE8+e^2e[5ע>)~-~(i"^Yш*W#CJ '~L#?ϸKOՀhJu38IL/EQYɭ@E'R]3D"8Y!.=cVL7I/n[s .O99s}?B8AMQ2z݋RYajQ2u?Oύ}$)%M*CE[Br3:[!Q䗩rd(0-.J9*&rcDC +R1cţߑt0w7E%IKMj7؄^hqIj!HBT (M)j@%$q)dFF! TSF&7DaSGT'S= k -!#.ZUzZi)Vy ~TD܄tYx w &:GX~i+;$2d9&Ee'i! VAY)VQAEṑHTjŭQaW]2Ĭ HdffT7G2ydVgVZ*=IHyt 뢹rq=*7 ~&\jYP0ⓖE -j/P݉2L8zt?PsiFOIqK&W'5!4ĥj(YQ( -XվUPdlV@Dw<%ߜbF=EQ30YUJY9A}7 -jO*ijI[9p>;2U%Dc&ƴ0'NˎcyB *¥$ @k[ _K@(w˚fIP},PǼ(gn$:PIc㘊O0gT@t;)-",$U 1=ȗN/m3x6i^dMf., &b HdIЊQHğ5\j"Gs`c~\|P$=Dz8N4jsM$PJq^GY׈Ҡ4ptkO -} -%EEWg8չƩA]xnY!gŐIYW)¨{D*$q F'wc2xL=h+)WXQ/ Gf[4%XCJ損Pa^04yB -3ٙĊL$-8hRdwrzjsKlWMxI)v3d>J3CnYvܼhv 9z|`1m -`N_7y:x5uQO`̚($C#Q&fMΆuEXRQ`L{Y2gI@a!cհѧK-Kdr08OOQ[ptNL]6,J`I#ĕu=ADQL/@^I|ſGc!qʦL{>ggf0.JR]rOԸR]w 5{i, X]ź G+)%k\i"5(&)TJ' P^f/E Qԍ:5Kk5gPﲑFL55[!_DhXW] ă'$^υyt!D8 -YOlOEa)J2jBޘgA^($p$轕dpeՊłd ec9d_ -PHEw21hH#u;(DMv,ٓ-  ;IdgzW{8C@_al=0` eC<+ܟkR<0pg3"L2bgCh49r0GGu'x,Z0y2jLղ=;(fnzӸWl88@-aF`"Z̵I6$py30ܚ8oDXr82Ռ~Zq&nMDt 0ng=ܞq) l  {Fn^ -4Zc[,kl\bI+(5Sgw!9~qpT40c/[FbʞaRE7kg~D8ࡉ1`V aKb%J*c9dм(=L N7"Lbu9%6W`_ -2Ar+Pj#؂\͍i&YS~IY6e mCۨjk!atZ=x9[m5{z`fyv>C',|du2M JOjߑfI b಴^˥ǐ%0~VjA`Xl_nZCu$ (f_Ŝr}A'V e쳄mgB+ |%v1_#NjJم10tfW -'-L#!<؍IMMΪn0ǟ` cu - n-K#!!?FT 4ISiAKXZ^!TztAwJ6:7~:*GCb!1; 8[]>mS*|)겍@ .(W <:; :զ3 -h8qML(=\2)@xYȫ3{!n ؿ? -mD=ߞ+#QZ)N,czA-\7t6lN B{7qes P)|ïMCcK-8>4 34Lh=^Q HW,7)ӣ3?P8 -!FRd% Hi&v * r*Y6zELS "XG}v `ͿMA7R-̫{f4-?BEf/3~bpGhvcPS|]rߘd 2J9|J6 -m!Dr< K9o)ζ !~H㓃6-$ж|PN *>q<QRpQU?9/amK,?hca&ť6htKwO- G -U!|j l_p$7G:j'Rdq! U~~־ Em;np A*&<8'cQiTr[LmG@^J).bf_yXz|+ǞaV'%Sf'\<]1)fv=%LVe%wb$T*돐Cʉ뱉|^@r|,9Ff g*;7;v-n9,Vl(Z5420 2( νD͗8Q)XE4}<ZM7Jh]5y曲71RddYl=yEpw_L!;!N?P Gk6H~)H$(Ңa~=ЋorUO _7t~o }\vS)uIMaSw }҆߇ORޞ @1Bʰ'p PےZ(<A[lg ym <FumI ! ~mm.?pٓ~4袽H 22w΍kDSRꢺP)a戀~>$4B;>P_]{|WG(tϐf(r>Rǜgˎڵko -nSVfNMԴG<qҘ<35\Ҏ]5ۖt0ɰB;/1'YV4%AN$ErrR:u`+R_.5luG&Bv.̯iUsh_jә!f?PzE2U|\WFU:wX#= -ψ5vW=JEאd;3]助Q2ztN+_`}{"}4*T*QГB~_d)봳4Fړ{f) $yضi9ؿ`W@hMZ,[P B0@Z,a$|3Y4st(?~N!$s0 ͵ -ku{aR9'tv5e -K'S>!1=@tPWfl;Mu8Z]oTXó.?@i d30P^6@(AG`Se'.`+V]ˠ^B|,r:ҢcI]^_T6 tzU{9S5L-H AFce5GiH x7)G~9ї{>&0 -?:2˴elX,&6IGt&s۠qJr6:Z$Q6D0N{+X POM#; -g +2㧢Ѓ^Nω*0sʤ>G VrljIfmP%YU, yB h;+bPi,hvC%5x,=V_Kdt5첷f0L6OL:l[Wk"qƼre̋899C!)Bqpu»!^ҵ HAbB?Ww7 lgHH BW3|[(@"UZK!餍m=V̥Wx`p}{]$B~EjQf9!jBt<ފI \=)GW(fKQŲg.+sC?#Du>zoh.0C0i˔ē&鈀¤|3nJ `.GVk>nX9l -@0*'7v ?b՘ᒏZa6`P"̎垔]Ur'2Q ΨWDH B%}C[7c"))C_Fgl9B s^op"T6 XEoIdtĄ N"'L|[R=8;by!MRZLk&}9EoB1ws44&䩡4"t}n U (as!Sf6CeU3<  3ޖ,8aEv~@3ڑ*BDzL%gƟS mA92@E h1tzE-h 6= |^qD*bT`[BDˋM9W._ Cgz#_iYn!T{Z^ =xYahT<RQ{%"'M86P,vKnr-XJTi2sh2Gaɳj͇Gy6N -]l뫜%[U>C 8L޷8d8(s7QF (a6Ķ)2Li(X -G8x^+g+)}ǯxeQ@!= - X{3Y=aYLRIN+v\)3a +i, -MH^ƒd_w)sC~IPLzfW$dm&*n뫜ThN*m6RT)RŠc U>1n=PKG{ah̼r #moaN#Yl粄~ 术I_3gX Xus]|fAJ̗վ -8bʋң9rK֚FHU[O]Ad xހ?7L|' 8e%1`KQ,S -JytwFWr-ԼgT9ǁ77 MVDFO%a=WV8s{9DUpfB)R|N9E.6݊'5&%5*G*عJ pm}jHg/!I̼޸rqޕ9TVr(zD+="F$qG9mFpU,T·T3s1He>w#,fY\5Whߘ ˆYȽ ^(T=z sUU(K5ڟ 0F9RՆ ă蜉p{nakS-V'q˂Iɧ] -o}un`׵\YZHiQסostqRj{6aΟȡ1K mFvʫRBP%D-Ά9 xg - &\[SYg?Q] {I>xu/e|ڱOBɪXq*3o-D:ET]p?BtuG41E,4 Ż}d()#գKv66o -OX@(X8bZgw@C!'AQ{`w+9qVr6%}L -u I)#Eq&GUӒJCxzC>s4fYHx{DdǒԱ p\lwMgn0.PQV<S72=rj륯pTխQd:35k3;wPgRlx _lBGR׼:TbOqZno6A&F?FyP+Ytg3dk5^l4xSy`áͤWbw}5m:OX:If\i,o%6H2_57b;s7Z~C 8 Or;UMg,{2rfc:Sm?VXY0;ܾOX@u2M,6ª3e3xc 9YxAEIA`YZh`aQ+I?exBMZ0d 8ܾ !I{D>2@T:sYvpDvF>"'oz3ι0\2;zc@fC!!.(zUsF)WcќpCG`GRs^9(-J\s -7\%`7Ľ?#O MMV>żyH|+D3Sry,$GyЀ@@ceJ% =5tn+C'P|oUa$4{,g0 t _}8PHG PbΛS$@Ǣ*_A%$I)rmD1׎ʶFe^;^F‘|ȫ|7B<쉀.-- -AQe|(UXjno*I!CuԐEVAd\d[V%$M-V;C <36لdi$s̳8ȅ(ߧ5y- dnѽW "^m1$d,_zDD9Ƕ>4#XhD;uܦ$Ks9MGjToRn_Ds??O?ӿOO?OOO޾ͭ0'Ϸ`<១6`V1g ܪn17Zr7ŭa~|f{ 1S=V!osT^!<!# +I#*6(g}|щ|BȺb+[)>$;fqZvBIUsB0W0HQLslT Rg/?/zKԾ{#'A =fy_ݧo(?[.&ʧ}! 틐09 -a__$Z_ )Y=LٞG}okzJWGz)o#z>HBd|垦2oC?W-O˷<#Iʀ~6Ĵxm]<4O.)s6Wl[KG'xoξH}'Wݻp+Avos.~tqs6=| s~wB~Xryw6gj˓rmŎ"0ۂ} xf;'BOu>=hKPE:t{o/u1#D.;q2]N jendYG!,aq[m L1}QRP.C̲|>-tyahJG8402~~'woy| DQ?% 1QSnduh5OhZRQ9 -+t/ksG55x/FQ6J7lq((Π4~8/moct: .? /d`!_>+/Bnz1uNi[s!˰!Afy[ _~V۶ܱ[Y9nQ2L^ї!#W~G/ݮ5(}O%0"(߲oyX3Đ|C7!Dn4\m&ᔿ NJa !X?YEŒf]"j7Fp;,=/u[{7.OG<[|0.Sy۶jpaEnZOnS -mҝZ<'Y yF~FIDpFżvp<})?m-)e 4fZjOmIm]SlScⴎne$FV厍 +m 4uΧ6~gq!?k,ݚ=Mˈ}?\]T8o/ND۰H|7QA1n{j+5[+53 یfӃKmip7bt-P>Z~rf /jk8Lp҄\vYExI4$WmhNsy fDa _3J.oh B}l2{噿A=͝9E d;9smsFn,ݤDK:fIJ+@mD͉2y!кn2xOX[S2k\<#c[ݕ]Se6JGy)'Pd+S^~ɇ% e 6G.nrx.ܞȔK{Y!_a 2 -(=NZK*#69ON/scu/scdiS^V遼>H,a Q{HlpEJ|0#Ƕ1R.^8|)YnIak4OgA*&-yY v'v>T 7{Aέ.,̂r=*f *Qxdw'`V@ Gƿ棞Km{V.d9+`b|:g2=%Ǜ/BmVC.L{w>zYQ0wŲlg^\ko>"ۇVx:?Qo -c؋1.qZ{Ɓ6C&im\#f>o2/r/l6kqiO{[rp҇R}brE -1mu0~[2Xtч-TbW~ǖ;meq3d-yɟ?R.TIb%ݦgY+ Du}ڻ=5S$Vk&zʴ=]=htoGX,2_޳Oωg*QYJT=oWWGjCUw.iWb Neۻf4>we8&]8MH,W?e9?e5iQ쀄˛+ TtU~1{`_y1h>q žËm^="㵔7Wb/UCl4,U39\UiT 9\}w%Ai:yȵtܭy{k4Frկ*`r0nW,ίle6'=25R(1\ί=`˄bRsˣA|<`amg_֯4E=C/XzHgH hl^HG4ڗW|r􂅜-kg/X@8Yx?` B4Zxsei+?&@L`9윆)96G?{Bd1@x.;}ZVlWe9>+=1}8R@=9u 7Lvί/|< k畈A,l?6zw  Fv]t;AWͨн_ikwY -v| p5!)ެ_(cթm8iƑOW!)Zcێ"Ηe+ۈʲInåi6V$JyWQ\aTBv|WL2'(k#_{ۆӒNEMaU!<`zZÈtz}:ӻ{2N?RC&u^;: #Ӎz ~{֙s>3fX@t:TIo۾B:)"Mڛ i3=oR^ádlz2}4=m:m)sQt;.(^^x~ۈ9ú9)ܛkԏnP0IZGn'6Ed6NқRnflV|->Ufa$.kOe\@ -G/϶-۾/qa։/`<:NBd,#'xrv+R$SwMf[l{lX ;ŶyMeU32l->L2 @1I)ļTn'L#-@n'LP`敍y8acNB&pGSj+Lo|`N8 O4 %ˉd:e9r%I W٥W{s1. wTbP4^xذe9 -G78oKBt^'EmzIF&KXe!II"M4֥"Drm/Me3,ߍ7_3 -=U YD[".s^Z)& 4rS0(50x&6T0)o6JlL('F 0%׷ ﺝ0mcu\ }ZRUn(Pb! ezү oڑN+Ey(4+΀2yxb~E2I3.ٺfSs.3SuVggOOLɟ!cqeC\R)paĔVM o -$ϮgH( ?ζD:oLF@ΗñIb+d64M /ME+P2 RAV49_\2P6ΧexT7CgDɥsiѦ -z>&jkҷϥY}^A -lWKl3K)᩼yXAA a[Wvݎ]fTɱS2:*;-%+]ێvқdDZmsZN$I= &;e2e0˪ƒm7~HS6-&"֛"wɷsmE=MD+ vƞ&މtܖ\GҍJ.ȔN\-"ZULe9%އnC9j=!QZBf%_ml!,Ů o1zಷ}!oٮG+jl]^KxZba9mHx?OBJgߙ ڮ-6< {yeslvO`6"<59K63+.!I͏O#B/%4o#B#FfWq[! B0A)4 $$ +tֈ*H̨k 6/] v E(YUm@{θ/ ׷Dc/6HugSj&-|y ^aDRf_NO -6S/R¿cT ?0hpj4 M͂//>3ı,Wr[y42i!:޼ec/h%ؠoAb7\~}Y2Pg-N:IG!U*@ܯUFg N9nS@!n h?[(voK@Rbv{J6Vy4N/G@j,#@@L:6}s8ZFrT0EX`Cɂ2{8;# Pn'@`բ\ޜgo'@ I/Hhu9N`u(*< r: ]oF|Vumu'o6J5g/@Q9yWmHr rU9&4QQx31&01ڪ Г) -9<t4)}gJ A+y6q2^~@E6䜁J})"ڰzO?(Z[h05WEWRX|xsӇĐv@vP@;|y\S9( -v3}hw!\y;y:RpiwG-FQ$>AiO; f|8@iy -6QdDZ$]w']ZsIߑ{Q j - ݟ&xv~ q(/jESSyQrlx}7.?."R7Z}EsD}iI҆@`?w7ට׋93t{GMmO?qJ=nj㯚E7Zy_(۝gYj۪f+}T $J,cۏoޛ MpWX) oqs]p| -TR͎%^=M]oӷjo U.7e樟ooTŀYl_pܺ6o&n@nﶞBr[O6аsCZicWm6~UDF6[=BhZ=՚^vTXH-g٦ZG.-u3 ,Y=TDht7|V%73sdKY[|7+Wnqq --j~0>f{ːYe=O2U5[ɲu͒l˹n'XdU1a2C/Tebʝ0@N|l+14(=djzui%,`}v{ ߬EEeU']:#ܼBb4꛻WE( k#"#Miu)vBC/.F/O׹eP()#pcqAW͸X.@)%C!&.,4@X@֗-BZs`xi/-c%gemLlY2-i\Klr/*y^f(zx]lvbwU,Р8C+xcj∐J۪|UQe"}}2@x.8D3m? :{[yOd!0"b{MEvh[L)W7g)f!5Mּ}G4 -uh&5ET!KT,~_O\.m51ށAhcmtiH/z;;QKcۆN^}@Q/x7 vX˵)̄GT&Md/6-O'}l0n+jmT5(l>=mԗrA z8 Qu58IuP]hI5_&H k28tFw[ roH*1;Lo~Gcin#SFdRK{352)_2Ůuys.b۱h@2*%џhE R yTߨ>i:@n!͆d 7!Tr+ng! /C!1~&okj>]Igz8 Yѐ,H0bϗO?Yv?Âm|4Rz=}ˋGOKZ%6={d,_~(K Ӟ/ӕCǻʷ$H/?x4r4inx켎N[r_%,O|,Lto wNQN 4,s/xѯq((۝S6))#oޗ Zownb@6WgDyXp=/7e[S`ӭ8c!LjJ -7MA'K( ۉ3_1 h_2) :Y%,iC:Oq7D/G]_Nb=@ $k5sכ"D"ߌ\EK /&46v'[xfc䑊chsi2JLI:5e99K)`9ˁS vOj3yӰ1Cx\DB( <ޗ`zb^<ʄc*fZGѹ@P0!Ӛ+5+ v:{_P;>whK90%ud`]թO.ѩa"ۊJ - LHxNb0,sjnWEaV֜9)DtM(6gQ{*hK4{U6^DI-1JصME˯sN -V4C}H~s#W0h.sZD&?B:?UXiZ m0!NBwcPv@ -TbOd[3FՊ+=DRי+u3Ϝ[)w@3x8JhL-hZG(uA) x%f }ZQKѹa7 Cjb.uv4v[XVb:R X| 8Eu˅WS͜7Mm:B(:7RfYYWG:gxsc¢Y@vM}*0^Fo -# ܄zMъYV!:{ac=ؗ5}y9eS,v"88@flf"jQ̕ED,Cp0dZђmM} K\qs:A"LeY5̶ׁs8ey+¥oq$Ґl;AdMwU~j+l.фeuVQ1ΐ–*1Ѓ,: 5$M̼ayhSZA)ex$@V9PGCc.huzE^8P0L8Or7A|ʯ͸ (/z7=m+J8U Ĥ@:|=20UNXD;e)+SkPg/ΡUE³Nyuy(dqI#z 7Efb'ZKN}K\j*h@>7,-k -.E[Afx^nFVQIh86|ƟcyϲR6icTPqtaijI#zkVRsa=?Cʤ%8L\{47T6^qj}h"JM`V%qmN=$@F!DFK%hT?$x!P1W^Sy/t b -BZB6 36tB(qj_JϑG/B'e~5~+եL%NglfHtng[̙6h>8ה)3a'ё0#z!'Luѷ)';f[4 uTʪL -&E"1@Ys'8˹B䮴jk_]%W_6J IRZ,u B&9 V5Wp9K[[[ZFVs ۩]M{mP?4|yI"lCN~|Jo}Q|S}hb(?y,1h]aŋ M!wH:PJq'!He1vumel~MO!=!?ΑqKGK -3 zOPBpc'Oj%`E6!h2&Q&ufv"b;ח]*\HhEi6v;z=[z-9#v-" -%MGZ`v;)T <k"AZT)}R5%gD (T!!=c_=fdpWBFD-݅4R0;J3J``\IV3km{e?Xu6WĄi TLQ-?SH_ˢDmEW2&,ނA.clӌӹ]p=Ѱ1Bz&AΑ ]!2yg];\Hy> s/ ø%46©hbj2.޾9l5-tZ7g:(q*m:VBYw}.C,'3D u6kSaT<(Y~2RC&ZI!s* 3G%Vi dԔC_5=z?ap*:=b_ PH& c:NmɽT`$ǖ *3maWBѰ &f)W̯F nyh8$Ա!k)la iFMkG#$H4VpTL{׼RRlPA ӎe8ڭ;ؼĚ'5ch5WL^{̢3|XD: X {bY-[|ܱm9.3 }cFԂ5$[bO8J 2챾 6DZsrQFNa)KfeN>SLLV6^9Q4"9Kf$-ܑkxaWk,uv e%Ѳm#9sY229dd< 5va͹#d@;זfe4{ uWIk_!}E)oV3!ORfFx \S) S/"yORRcjY;0*fDB (&6:%!G~/۹ 2g~%LIۿ ,1 Y^.Y:A+{/$HvX>ƕ9^AR,(Yβ! =5*݈vrɫ5i1nmeц̝Cns  ➿|ϯVZ`2~ o^LjQl"  Z+PG;6{F{ߔy=n,ߔs(_,uك,% 2as+P54$1Odkc#iIwntLىTsnCr!+>-,]r@!)\ -^C19+lIoy -4FDbe =;~[pp5WaBqrd (0]e/~1vm^젅@'U G8fSpud< tC- xm~l\E_#@ 8tSFaD/3vqaȸBp%36 J(m瘘q̰G6%|o60RkFJX,wfڢ /x N#;ٗ>qz<=J Kݒ9ߏoiA8CMuW.^{Q/YMsޅl~Y/˒# $Jit65Sև M(F:-1z9 EQ(cH&4Cl~\; -bP~lhґ Z#;[ ͊pm/,:%f'4h5H͋G,NC:l؇(5ۙFh -Bj hP3N -dM#/P\p7DHq F +4| gJyk52=c -{n=qXF$_q[V(Ʀ@e}CUz =PitSAfɴ ]MzT4=ۻvMM|)i`XXIc9!r;Iٱ\RfWt*_Q#-`m` 6ъ]W?.HNF:='K,M{=1b|FiLzhRSئxm`uyΦ~#d8֍yhi)Z2(.kTˏ/IcXU]pz:uiP/\b8WerP\bs:L!;ju?֒Qb2~),h|K:-IoX6Vu @)YhjXX, e8 8;Nbߊj"^pCH4\案,Z%:/dXu몐C.:G2J/BlQ GhXbAE@ OD\?`FsK_` ̀<"v=pzQKᕗ/BI4Vt6qL5?P`[SwҔ~PͰ-]%xLQ.-]UBҘq*SE I& -q IqԍzCPt+eBSABOz!yI/D"]R)pQ;Y AFRW  @mHGcc5RH|hv }9KIN0nA-دVH K },x8y)\&I]3\.g& wDD`;aiL; -mR!gotF:U DZؼ:;˾#[_sgb@.L?DCbK43o/]jU&ӒHzﲂ>?=Y+#dK/rx+ ,^k=_,JP4g%hS_%HMxU ഗgfIPIЃJ8\x̀?mq?̺۫x[ul7:2߯T -Bn/\qgXSpҁ ܄[ 4{|?M0"q^F 抌8{^%f'}QiXom=0缊ױ,o=b%zr̀ܶC-S "=ܨ5yhaJ/ҕEbd>A5{tC.rudp 뛨:Q.]ak]ݤU^Wh/Iy`^#(8yr-X06񎖾[`YK"LW*X7k/-%A(` -3@ pa3a.@${.0N {W6Q:4{B8Gyd&.Z!hEdE "<-5)D%jAL/rU ^b9" ߏb`T)o,/r:uDƽvp`0-9 elO%=Q)@DQ9o]2ywz)1q'9I`CX#C45JFklm]N$:'^W ]G#&p6zJl*#??Jwp! wHA ¹sZ'B8x}=HkrDԋpo) '6'Q*J@'r2X - -g{BHkKi&-Ez\ %@O9GӤ}5>>}C=qu0ˁ_wwr6EqTjD;=:7nmai_uVi9Ic|ǜH0QF~T&BqW丹KCCȏ1ѵ6<%;>6Sb#B pf:IΜp}A⇸"#X=9/9Rt D19M=Q\+,w(M@mr՜דșRS$\;I-zkpTqiVZ?|+{ ڱv"@`ZPߤ"[yAt8#H¤HU\{}G\R&aaVYRBNN_jо5KKUo?k) 3a;֩6{"ͽZեo9X}N+)$gϺ:(>ǽWJ odQDe-!ʽ(g&8mW@2zIǸkQemG* {=E8k\a9L J 4JZCf_xxHbפpQl8Kqe@}eO&lc= -fsii1v)\ 'ÿ^KSD'g,pD%CP A:"DRo`g{QuJzj*9]05ԃ i.2 "|\ 6+s=<sT3cM4rHc+Ňnؕ;jdRZnn B[a$ʪTwJToo~cZ;|Qfp998+Cru/F|"P 4lt+ 5Ab|Mj7RRhg!/7|]dr\AQSjo<?mޚXGdUd~N~]rpQb?¬M5ucIw)7f^1yga\oq {Ѵi{c _"[Kw=PC6* -x=1F_CvcRhd-gT'jm$+9/SH_W9ToĂVB -2 /`0@xTJxgTH/ϠLC4}V0 $$kX0\,ZXWk %I!@LwEHP\η>LK٫uv5x|B05Nޏ[4`BDIL9^0t -?dd4d|n:DtN߱q-GZN)HTk) W04JU* fZ 﫻Mֱ8sVu5O5-Dzᣌ(vP9)T6Tx.'/-/w$`ok՝Pv0(0^!uƂi -zWi+&## mQ2  S8=b8 m؅3@r^ŝ]=䒣$:Q׾@^014~]ԃDi l]%'U`0I,#;uG=r%w|0[t@'G*w63OuZpʁhQCW -> ԡ3˭l7I|m -JT ) )F^dEIRU=b9_EJ#C1#EzөqB(Hs9ԢMΔ(%7nzF+qbr6b؂Mλ0 !܊@ɍT:/M -ra5qQ9!J)7ԫHwSe{\g* gIᠺK`f(MH~ KT* e~ ı FӔ@ !Bg^& -ux5x EZ xY#4!A,# |"u5n#7 JAU}r)^[xbU=13e -OJCDA BG8Z;0J(N5䝖9b-'n;Jj׫# ^Fw"ʾ^%SsX)HW=lTS[D9Pt2 D2Ca"јiK&Fg`])qLKVa!%u l){$9.`1b sߥPx!(=ٗ#*`זej1&0Շ"PA:ɘOjgTϱ%BSEےװc#PΌSdlb|?eV&NƣC̽S#(F7YyQ3be={(0JmQ9+_\,UkZptw2l#NNXPcՄ72.Nw;[;)?+aQKHAWO=.fJ Xcy]2)/e !$2֯$gX4P/28 +\Ӎ{U0]n8Sݏ *IT'bS,C#ߢ =lm5t=RO5 [=8xK n\F[Z`(o%o+=|q,Ӏ7~1n:Y 3ڵ;@(݋~U0Fc.6@1UXfa9a(@>p w^oD&bp2ɋ.qc&ˢkZ)INV#MuU7(XÅ#NтH~D{Rr2֠ghjկd(qP 1@N1:m6U[2=닿c)s!ș(rw -4yeMrj5@qhcP/Pj(!C.SI*\d 4oe.PسW,݆ɘ%V=epnWg"2+p 9j\eD!i5ӞVP#z JT -5vE4Lft{V,cnwE* 6l9#SSh9$VXC\b) ۪`Y[/Yu 9YA\šnb'jI h/i/6D@iJC@%Z=VĉæK?{CWC` W\=(G%B[JI;9UzٰH҈[bInh`Mw;ȬyUK͓ ۸p^-.+D|JINw !жSʝuZ\1@yc!M2 -xطh^wCe [= -ɏW)YMDRyD$Ujsn$y.f߱Y/f&`xq-dӫ|sM`\ u +P{''"@l'D m -L"ќ mاEm=NFI -w(!Mj챙}ǜc-~SX@ܯrN9f;ЃPF/[vHlzmj܉`v<´B'Tdz s{b/K~'qwsT!]OL~2#eȓ v"Kb^LOF(wF H-Tԋ<$F؂ĽĴ\Pn)e}S jX2|()F'`2B{ /&O*k\6cͻ-Rpa+6K*lIQ\"2BSV^bi(aϳ% -M\V)!d!B'h|ԍ(B -,MQִ*R4!M,K x !rH<$@ H Li"S4zc9);{ړ]\0?])%{}ZIa9w@j 잤"v* Xl[B}!֦^uGT77hœޙ%ǜ2n QW7)7Ȏ,5mpa\~EOxzOM1gfFL]b$d`ثN[|[3LA5Pkcdh SDʂ6L]PGp rZu"9@^M A!Wuu1EPI7" Hc`Tv2X6&c¸ė-5Gf{%S=AUrKҰ5X-vlrRu*zH -e_iZ0{ -;kAje_) 'E\3P3q7BzJo4K[k)hk3$':oIQ(!r\!:!L[1^x\1@ -M!"(cT|˱H {#K=?Cz~I%Yy„f+K<lyC< Z쁎M+oLjFSWU~!+].TrZaՇJ79+A-Rryf4?!R)S:c'"fllxmBb!بÉe4("rRt.D1rG}$?_uYgKH:sSIpfBV҄5[e!d^L1`6^J̛,n->.=X=e10&Rg>b--`|;`>40VIA(KHݣ+1iڥWWy -+Ib˩;B}Jh;O/ i~eRBdrR8cHA=X$4Y\L>("D7y+.mT>,Hނ<$#ju{"eɷp`a%׌ƜAOD7=X&޹n$O.qb{؃D iRoRl6Cկ @YU.`Qd6`{e""R>AiWsXAm2^D!9jrZ@&QHx|`7%_Jx(z)~,ȭ>vw5{Vv|s8`Q4h[lO[A( dO_ijFA^΃+2݋Mu5]D endstream endobj 26 0 obj <>stream -hFux(cŻ,ыqyh -.GQSC -ѫ!dPPfD2ӍWUЈw[H8K{hzH#0'V%s_DnQ|?$D *#U..yr(mȺּqmvU~WMfd)~u[%ggKe$pKN!p1Wﲩu͎>?ţD^T_ߟߨ>_#Mn1أ#ܤ#0sJ\ -Tct9\\"$H"=lRW| 9 iŃS] lB*.=R|>U=h \<pᤗLxt!\>GF$ H/$b51h3Ov~?`Co9K7-,K"j5w])r#[IC ~DS[Esx#U1tuEg:ԁsEzv` -d] n*T_:Je)CI[uŅeZ5KZ"@R]ZVw{NhA:tW?(r9$ӂ7y^MӖKboMh -v9ٛ+?M)dD_uO瑒90{q8v_m#DdӤf"qdTPv\ud4|*b/K=O769X7+!m)M0%)$Ԭ~xuٮPOz ~c"8h; Y%l&f;G6lI{~hb iyCuKۖ"`{u c:6|*PK!tʧ !2O&tUwa7Cߣ n &QUa*3w$W߯XlmA -i;=&GaށRµ%܊%ރfGjǯ5} Xa@vuS +ᐰ1PWy-_C}7t`TdҘn6#-#*2^Z=qlȿi3砺L!OfA -͍M-8ŐKƍ)I;JΑ}\.=]տE~+4PKUWmpqKAJD'Cu86 ^J'Vq}bH0RVXGJy -{L-,;B/;@=W3^RMKy1Mצ* ۏgGoT/9lFNR E]RWWЯ-S@}Џ(;b!g)YéԜ)t&5?o7ﳐ̘jL%I-,űw,>ʗF+@s-I3iLŎ܂Xmܾpąo<(ry>v>gqwv2P:z,)v\Cn%n&܏A@Ra;6qSu Bo6 _ʤp5}NnpGaYInvA@Φ9_hh7!Hmz($*PP|) MޏHua]o/NS@BmdvK/E) -yu^඗覙 .p0ir0dUzQ`(lY; 1x#p63Y4]0ʤnId̃+23ҟ)*e~j6꯳k)?s3Ik2ih{5~'Z;F#Ě:/ɭ2ЇkAɟ!g'wG[/d[^ 23[-%} ȗHKݰ_ -~)>Ct<8 jΰ(H*ֶv͎vP$G.d~UWKc -|? -oHDiQ`)Y_awxupKTff>">B5 8Ԝ.ݗFcQ>`EjR; Bg1|RmCe!Vodz=$.a)lm#(RRA7s^vۄi/9X -)׋ͬ`MA5}8=  [/4`C* )_K)ч4^a@7Wvܱg\J䙺@a=[m 6Z|pE0X,k2YԞ&Gi\?.;|vX[x= -E1_ :Pv:k;W/$M;~ZF"E K嶄5;vaCTn5WXA] m$hv 95B@\"pm{ldEA{0-%C6GwĞՕ؏:\-Q!P~`x6<QRaD6AηgZ$x0Ym]>p :X2X+y5*Uռb9IcR B&'QtbTj{PQi0 CH]I+Ps= ˫!VYby˓{\7Ce )!9P?ڟ&ܾv4&i.&'aY5*,o2cBC,ޛ_ғ<Òk\хԀّV H6+Lb{*S}G-I"SjztSM2yR=Xa?cbg#/l$ow\crb!I5޽%xL^e(DBJsUGoF2ILn]譒6%'݃\}q$UCυ(RYdKMU"!s|PfBC+UQJi#_7T 'ЎIV :Rm^\ujiKCV+|q$e4aaAY#;pipR=Hh3z-;=YZ+I YK(uڊHd¼١o"&aQ ڑk!@ҟMCT "qpgG.7G2Zz`o ;OK9Apj?19-iP={ 0+|HQwP9o3y`Ҏ wz *xWS;]w(ˠr.zڦ ^+&} OP%q9@IPE,Nרw4硦I1S@v`BbRn%Z@0'эHCaf`XƢ_D*?x{(j~27 121X$3 O*zQBHrPL(Ӑm p |Zo2i(-qgd`=r$&E,ɉQS{%P[(#N7l F!գ bbJ!F携ZW* VŒ#k80\H!Q%8L4ນ\ 47K+헓:P%_ZPLg)YVݸ}o#YN@O5U"w(9=RAdO5zF1n>I-,T!3B76cp<@!5Y{/H4/z}I&b鹯jI-Q2*ôH8KJ}B8= k, Yl"ZCa2Lw[r~"!2nfj)-5HcJg]m{u&恹;Q,fڹ`-~gb+/C&L]r~‘ʕP M܄eSU=CkC{oT\J%U(d!aIn;W/9(W{Kh;x!# #\W(C0Kþ#sYSp@hc"/P۪8.;)&W\CE(l%9 -*sEKV3Q).I/i -|ĥdlVFf6\n=|Gy'45%tX;wnb$(.&:n *r1U"K%!H.^*ݳac?]1N#29NҼ‰"/!ds4unxr A T:@mAv"{.tkuMUP(2~oq؂ 5J:jVgH4xDlSĞQ۸Uسt>%(SIJÍ6O\(&BZaE @3RzĮ")CbJS6ݸ?`*jJJ#䀷 -̻t}7݁ᑯ-xɺsUlw  HX a?=7z-#jnFC,0 -8A`b0G`K/R1)w\Sy>K -bwd~k[ Pq VyAt1$DHR}Pcn⒟j@[3w,)S3-۪ʑ!?qNh@=zP̶ |OgVKa/G-D^ 9~8?ؕO x*L+(&q5X'wU_R:(G(5NJ9Yt@?~il?ǵj`I2XZ>YLoaKPpo&EK{\$鶷Q;<4!^OEF?_)0ՒK"XU~HsEgnpv! Gq nlWµK`n ܠ|Yg[J+0JkP 5{lDM_%]7<صʡ8R߃b #+h\Sbc}ΕN7،Y%n({s.Bݷ2L@}v0 `J%$1fACfP1 Qo$ofk9KR ̠ab'i>eiq>l04k7GȎ~pwh/_.,{sʛ4Y 鰽~vAq~9'`wn%U E%$#mw!q>V вO8WLE (\,?!KdƁ/Y+A OZͨ~([ -XͣJF ePlIHC()PV>}ciuT -ʉ.1&t 'Lɴ-Ety/$Y@`k?R=T~( ,TmѪʯF{Ԭk&5T~+*$upqX+|G-CY"G3w/_d!/PɄr9m'2G -B)1aj^($ !@*%(OGWFL[RM-;=J TD2zh,|y -/P+Hx!v`^cAЀ%P[#Ģg 36:S9Fpa7Xo ' 0ako%m<\C=;ƃ6krƬ 񊬪0C301N<-}|<(RR+~!Ȇ렰"o:5K<$K:0FEBAA}^xÍW|@L`3RÓBG8_v-> ^ H4=3(S$D롪fHI6Nľ_^OOWDvA2ܑlх9!A'*UGqF^{C!b{ϩnRlCI9$րfy3Bl!E)M;@iiD$`S0vr-I@ek2lBs4^%xUBRd,VjP·\$N.K-2Oe-VB (XHBH9_ag8_[5M՞ȤP6ܞe-!xjI m^uMN52Q΋%NB9$>㼬+*jCN/K^IW g( - lO͌(e \l<}K.&e' wqx{`uJh|5ǶTK?6/0]KMrv$1` 7idx -Ri/}R fA噀.sDX0(&uґwMhb,F̻yF s4A:ɥ 1GJC6{ 2ʜ!Ez6Kꏑ:v -͚d*fk/p#0m *GdjF*%({Q4 8Ƶ[k,v* t٦â̧ol^` CBTc|Wā:`ԉ^s0fߗFj(0j!дD420PR&YxOC@jIz4@"i;Iդg-7R(HrI逮 N QOSULlj<%!t_@N$@('7d_;=f&T*%Ca%E%SXE匨LSvn_ImkC?.m. ~X?`Z\O -^t|v%ugK*k8#s tt] -Rl䰊 _CV8Cᤴ:h%qu?__0CQZ$MwV?љւE{їn@ޥyb݈.rDPc3mwܢfT>A)dk0/G{29Wܷ&n= -ZhT"ہLOszqe_Y/  *ɰ-GPQwݾ;9HĪ7 X9DjՔ훹2̓¨I\sl<.Y,w"{ΗS -ؿ\/r] XROjd:4*pxu}TQϢ@א\-G^ bCXBô8L) ;(,\5вw!`o^i,u(eB +ZԂ'"Ԙd n g$̂ȄDP;17-RJYz$1D,~%Fi⑭A]3ܡ?7T/_1$"98*⮴_r{_WK88D@J`+x?gWs\߯YPbCYv~l'mˢ$D]JCR\RN -xmsG"IC%v7-Edဧd$ ʟO|eH%(g9";A}$f;dogDo"{ߐ$ -T Lq?DG६׶(#%}î_UF=jo?K#kELlL@>T@# -1{Ű{I|ҫ1͆Uң J0jΣF!Tk|(Š$RLg͈7 zS=*$u*@ %էiu䡁]ڋ I6(SY)b;w<|3pHW cU0&9~H2FLTLFTZ)c'M{HHm=+u!? $J$"f&G 6>[w? !Q\QE')TX^7pHz;@w[$4Jr{ŷ6wrMk-HE'- )Z+)nRf^Zj5ɠ%bFr:L :if=-Xޛ$NQ`?uu?j֎ $Z,42t`M@zz}U]BeNG1vmVU=ˣoI,4?F"uW?5@ ],='8zcbN'}}YM!u]8SVqF\Z21DQ a^IP; WfX9SʋI1ō`ygǐ=U|{x;gx9铧- -)A&Uv4;+I2\a9N|q I@de]y]VnWI ' ^쾉gtfDZ)Vċ!Lwzln -[gѐT QAf@I(E_+,9=q3K΀87zU_ C1א%9i ʁX+/2뼔'cO]xŘM,*)tqV%&0ij 2qd4?3Ե kPޯL}~34+S`v -ȝYRl4w4% ySG%uz+߁҆W'1 q>n\Qaر'E8◙,G2=x5C"`_qeD~IK"fm)'5}n'5kl#e^tˏJ0ebŧG<.F8(]!3u<yF Ug.FN܆]16RV -@V빃.+H({T;f$[Pi/]&$U׮eU-#c J'v@T2XPviQ4z &=B} ʋ Q﷟M-Il?$A޹.C{T>85^F//Vvl&Tgs&E6 -!]Ԁ!Kn1%>?^.O4ChcƙtOaYÁɦ6et,S.%Bd剅ap~$,ݻdf"aGIP"w-< -Vk/MKBN@"s:T}ܕLS-faG~IGl8д$yi~~r -ݓVN6c{om~اzXf?dT&/R.b_%~5EZ`FSUbp;'q(uɲ,~TTo2MA-$DrX{,\Ҝ8-9}%lCfڙ%}z8:OboG$2]ΐd'A>Ȅjp`aT8b[6|`َMA;> -ET^W?юs]g'P~0qoaGꦀlRA3`IV|Fp,N<J!HshNDjͰRWj:cJEyHt$Ԋ"?ïMe ~3Lb PM{ -E]<5R幈ZSQՖwJXsИe|[cI(X tJ5'CAfPP [l, 3a,C Q| A9'%6-C0lM!YQ}QM7vU׳xcX}u9IbKNtոTW+A}áA2TA* ,Pbf`w/r֪̒\/B++ꥃځ$,[rR#xlIk)gmӞ |6 O#~ 8CGf&X*i-Hb4{fLM3H|zij3ّ1CaT-'DnǛ|c"][+?2N`HOt ץ3ќʆk4%MB*7֛} Jpy'jyn$sf*JQ a!jz1n r@3 &H q@$@3ULiS3ڗ/um_K-ݟXi{ -]+nah2A8t$[en2 BK%.kg(PIӁn*_xEұ W )?uɶCZdJu|^(8fQwGO%bR 2Qe'4)Chv,ě/C( C mbp @"~J%EMHctKTxhxN`dC -Hpٽ QW`Kع~¥ƾj!=fZDKOLY88ntqZ9m-knj^%L_vH) Rv$~_ʗ ׀ޖT@KL_ho/<%XE> f埭[M)b]LnC@ 7"AdžbqXjv|['6i1"qvQK+2˦ -BV -40[ J# 0 .t:ܫ)s Jt1&}ZuIfD*K)ME4>b&/*'☣lX
s.5pu;Y1R[y\{dV\0- 1:MPma~{Mo Y_`V$mɇp -+f]uײc ,"tPYM]Yʬn@y_:ph{]1J`cC!ֽ+J5ʒ{62@+`F;x@ۙ+6"!U@eԲdw -.;m^K:VJev|8{iA=_̣jxD,j)[KeIG67L-L3nޑBr`&SRW>`bnb'ThZq:cJ])(-PtIqn_Ot:DX)pMLF83iVj(oj}"qBL"6(+)V.aDW(h^TIB JډO~bcng#ܔrhT~]j|A({ڭ #Nb*.w2ETͨy<ɳnNbĊNmOVj=w֞9KUu+hjʅ`J⬆yUR@E$V<|nV/•Ϋ\Xu2[O8=@uiɆ}Vjp'RCRFXkﰠ"m%IQ-W("Qa -=G}:bEWa"n#2+5x!Kxfs?r9#:ň*Bʴ~0Ywlazx d<f[k9YV!F 8nq:@BogwT/ovsAK ʣ*mH& T>/FR|0bH{:Wߵry9S"s%/:()uK*dtg-W&=W`}̭r ܦҥ| 8J( ̏$R -$u,u^]yB}jB(b!Tn[} xBz5zX.ϸ* -CNőudY~"A܎[]iGدs:Dwأ &[eͶ@|=>h<;8^W0]ቼ9h(Qm()9 BEut&OCrDp_kF&L/I6S4eDz-+nK;o[)N3)o?Oo7o?7?~W~?wo_ =~'w?t}O_?n7ww?[j~|lS~ x͇WW귿Sο?ovʻ̑#㷿m_\oRq+/u{ko_W_6Ibq"~{!yYb 퇓L9o_$pw rIXZEYu%d;D!KG Zį Qû{^_}! ;=SE#*j|э7q|ظ||~su`HsXq*dyr#d ~ -wGJ?uBqʛ~pRqNtsx`3wD9~FWϗ|X%H )8,*eZ=/'` ^ |WC+C4ƙ 58A8-?=eh pv{jJ4x~'􍞍D<_/b9وN4ӈ[ st:<&3>Ύ+ GZ< -2ʖ?O+h\IiΩ|D4q ĝz En. Fˢq8X7y_ Qx^xb+U9+=#-uij^ -NPO5Ϗw'xyibYqiTUNSW [6^> k|ncw^A=;~f<8Wk'1{`jqĸrN %Kgh1C[ _ @U>0٫zvdU/55daL373Zk;ss;I?}ICco|>b 9<+9 {QU+LDi1֕8o~ξ1()w^A`ިE^k\"ƙOA I:o=!'1 5rN< -HٯUO*P\A1&12\3H=X8i+x)ΉFE[#*M& '8sŁ3h( _7`+3L76s>s\?)zXqa ~W]^}bm6_m{%q8CWϛw(;_w!T4kFعz7tsyi U||^_1c8|e79yL%zs#'qS4"2v(_ -ԝ<'ڮ◰;F8"xHD#hl2cѲohDcobfSkzij'#P_~- j)Y{Uc_?.׻95]9pz/aM4fј>l'YZNWl n9TpǷ &kjmB9mnbcl7;UF9|PhuA#)ϙJ?gs=ƹvD}'%WnV$[h9.7K73iO+4W+rNGNHŧ%z\ֽ\g >}DJ!oaBܾeEh^ y)϶ʟKcȠdg}|%ϔZ(4휠?DdPN^s;>2N$˒["ª -p=k}+ɸ|\@zyظgA)9a;)ۻ:2=zyDL]2,vO(R;A:Cɷ6X4^H_2|1;J j>ypc.o$lONPy邲3crx3{Oc ly/WS (@dڟ/R&V5VFDU+bIgX=x_9Zjفb~LNPW_$lFd/ +˵w<Ӳxv}ˍٜ#yVmą P"n#{yEL5q~yYk[Wl'Z։FLJAԒnrc;  LJ}/ o :^FMgcN4QŤu Z8YryLEWR6"쨈O{u裝v{sWzYcjGXep4SF;\Nl (l|V;.kn,R\h#$(|HgW}G'oyZ)g:kW{[FI)_"N ]9 u-f|6.,ˏ##kq.9/,CS̬S`؞}0jMAK)Ol<)೾ )O}`F㨙.( Y#f>1`?_؅Iگ^Y9аcOQߥl_!+O سc|2Tc\]40Yj%'w;6~4`AP':nZ7's4 oF{k kE.@Qys^9zoyhfSoQŬ}YKɾbFEۈ)NrSvO3hgnH0/|x9@try9g/4ڍ̜j PU8H3 -"w*iiVk.0x^iyȭ-?+<IW ֿ udew]q[/йfc;1o}v6%>69dN(<V{'n7{?gsԅGE˷<$fGg3}Ŵg@9끷wqs20%{w ެZb09cV-.(?tVghgKT!l}i9`PxR,w*;:+#y fF+y#X NNsͧp+Ϲ9|Z<\$UN4A -E!jYES̩2} |FvL4=b :Hrn<]½+u/ZS0YFZDQ$i8MMjq_Q}QO3S O ߍQB30\R n(23 W9\!ٕ-͑''Fz]}9^տSA6'"#|2EĉM Oj'gE*S/AK$ s-u]gA3O\IchHzWc@X,, u?_1\jL +I뙪{}q%ڕtu2 d}ؕ29ۙ E=\{բmmfX=AC_jG^W~r'/F=MOLyb1Ȭ+skM*CaO姜p9}(g5WR͚+Y65nq"QQ x61Ո|JN=*U3{]~K&֥ḽTc04#y۽1+* f>r\×8՜F>RǞC^{ԨOw{yaRȑ?@9hZjvqW't[w#ժ'rC7.~k;f6g:A ΓBVoD=d:6sF98"%D.p?D\ +]Ɖ#e[3.C|I:Ʈ+{*h泍ƽQ8th;״6E|PYON䏽ڕC~y+ZY]m4WΗyϼvzwkƔ됕ƉhȀI 2xGh\'i9UvrOj^4]zw8uZyH}hHa_sU6aq'+\'x?^l_'ONWlɵ5#XrB9ih~h;Nq1HcɞP^+dv6$m}ngD~W"l/0'Oczʅ|$5?4+! :)I,#",s=n\+aW,ܫHcgOeZ>=PD5j)ܠOԺѳא~=TCKz -}y·8A + P܋EΠo=_ש-@ -ٶg˙ IS,9U;(OkYN)2w,lA9ܘ;1NPj\s'fAcs̼HuhM:9oL)A=._g}sIm3K$lLSBi<2=|&~>|6TI_˾9U3D3xwBA8T!c 8HrYgx'ȒCwhl9fkJn+?8T!dt G'xmLJ߄un5+$I.! JN|sإ;'י<",d*?nwוdW䥮@IY=SW*~V3U*V ^~™2E. '*]#V+N0F6gV&g Ȱd]\tN%#2ΚJ~I -/bH렽ƻhH o 'ȓH}}MۜąF` rRdJ4bCWT8 n|YpvE/p, GCu͚[Yя9Uצy]OQPߣ,_29I9=iNk Npbe r"^'Z/vئ Q, -\g'H(t'yo -/z_ -A{LXQ'xr^{E?6f}*nqZJ^؍xa  |r'YVߣ3G}#>gBIh*}۩(KK!U5qGWő4o{.FrqEF].~nwg=@ٞ߯@RZjU,|UBL^:xU[qH__xc9id"dMBF)F}=*A@?(Sc- "*S}U5jQI25ā*&f"kRn,lӇb6P0fj`>@(5 * -~Wf*Oz@fߧ -O IH _7pZpZh) G_JW7NЫfd~:8;X;L4d2+ +LK^„_VPL;XU$Fը)bս 4M=8D|XK*v fP( J&0cUQdkZDb*fPPGG |jQTi5UL#`HhBBx*2x_}ʄ #U23o /w"/f&%sՍYCxS58F(Xg*KU6 (lõTTW . n6|@M2vBTLQ4zv -TW9a&bh( -3&S/㭎Û75$DfVi FJeB 蘚è5jd-Jn͸QFFRME]Z -ex U9 J -h'PUɍLj,rtu5Hkh F 5h5D苾T%-S3S5k`5ve*mIlR9Iwv3hоf q/՗]ƒT`WyMՁpLP_k*bGa]8AZ6iz&Qo&̔ %UFPGIQo ʽp/!/S5LT(' e* 2T2S~Ѓ$Tc\Bi -EhJ ! -,[+z.*k[Ruؾ-̭>T:a+YpH d - F}K-?=q]jR`5)tF33C͔n ʣ(XQjl,\+SF_ƚ1|Ш+o'eve`"RTsvezFaߪۨ:0cpMf+L]E>*؎u[eb(rYF!,E3]δj/Q|#Fd⪌j8b)^UVt1& -jjbҗo~0R\Hc.M|^hNdF8\}3HbY$j P4ZC3bQ1M57>AmJE$K4ePUK#-S5 -)@ß;Дǀfqqje :T[̠Q8}@]P[MkMRoi nXP*Ə؁NDk w"a,kjG*pPz}}B.<+B,RS+ XLjNJdh;1D(Ǘd*K' Ȍ 4Lq1*:h" w6C#Z31L_(R(3!V4GakM,gXtԆ7Im F]4&}MT oZ*dh,V."JPLT¬oTfFMe7 獪53AOV6*qҾB؀f 2Uh43KUs5jeB'hD.Dt|h:MdgUzP^ #ԵXEoYZHKM?TqbJkuڨSO#@su)]h4ƌBH 8!.ŝWxUeGv&}Pc!j U_j"W׫˪kM]KbyOo(ȝ-7Ԝڢbth>(T!IOJbj;)5]"WL 5S#"p@?fX .;\bR"4DEjMJMl c.13c?(ri&⻙諪Iy$lH•BBJ -܁N-y{+5ybۯn4EɮrPTO`/^+s0 J=Vb5Չ:xMZ ◉F7c1rPW[6S: oX_fj`*l+>0եLUUDui,V3PRj7i XD#7r,W>| Raȵ,TIdLF#S#qbwQ_R3d* D:|o)DC0cB'QK)˚bmj3[%VxR>e8S h6GHŚ3&j+5*1U:OۧvPTb,͚R6ҭZ柺$n,VѓKY}Xoa VF+fm45F3 /lV'^.>mЁz}^A]3FA=/uiH]D@1QCՈC+5\Z_VZȋثQ,m R4QR?UVjd"tWMl&aϫ:Eoz.nvt.`7XdY曑H5JHSoÍFu<ʸQ@¾zu+3ygbZB9f1TepU+F?dMs\eo&d )ԁ!k5447)LS/>O* S;5vS_7WhYlgNJ/U 妿cx۟NqfuNJFO EDXEu^'`pLR'j֔YA]*0B G,, -<";U.'UE6U vڤ9鏵܄󐄍,Z5xZLфXPXRG j*U,Uo&$jND|CaCDn \PPal"f5RO}Q2SUVH':kՇbu }\cm]5 -%!j%mi ͌LjPa:UQ)̇E<̇e=Cn!/Nf="1Ssfl #>&w\C#١Cў9ĺ|JpoyIJa>4k,=$ W8 -G_]Uu&u/$)$3SIL`p9\\d>~;L#2#A oOIΆ"NŇN3,ds.C3^1C( \B4.n2KmÅgRG2ICA6lJtS7TtLD6(g48ԆGsrFπ9a<'S^a||t:(m$2 "y#XǚՖ3h.uc͸kmHl$Vr~r61p -AkޡB)fnZ:cC.:H X*Y.~{ :x 0Ȇ׆{ t@#~=Ϗ{  o!OhSaY!rWۆCzGuO…Gs^QC8A|h8exx!?ޟLGg2e3Q"yC3Cr){6d҉LK|b4>,{5RLlh^?L`t(< Cxx&8k4%.yL6"&g,^MjB}M<sSo*b&n*?dEr6 bz,:R>}6U?U4k%%?2à6HmzXX+7{g S}*}j+l 8 ]P~֤}? Q6P{@-l s曁8@GuH~}S1ѥQ4W)TNoNg˦dž#uiKa\oPxɇ5;t I e.y6K3@+v9F1ly „qS<#܃׆,D:*b?7`;C>x"p\Xx.,c < 6dtHf.:Pt$0lXh>aX +K&$k4 tUJ .f<'-Q6䐏>I3l<:s<:TytT$:at&\ǦM7MerF.YHɒ3\ڮ9l$h,|O#4*@IkDMlzM2d68g4M8L=t@eLZcHn"dz>IHH`l(7Ⱦh33\zC]؆~ Yy݌*8,$r.`Dz>tTo«FWp klxx6bR6éȢq Z\ropЩ; Lm& 4-n9Dt% -,۾Y#[nV5_r}lꯋ)؏ +2WLyt@2yX:c{&ml*B:}]9.@EŠR_Dd;ZRzjɃ1_0kXk`R!'S"10; , -X_dc0yc{V`>D4{_)j{& -N,b맂|bܨ:p5!'٭ Gxv`~ |LȾ0ȾƘ2<]48,p CCg<oa{]DdCXSl]7l-35&rM9ÄeϠa;gC::q{)hgH!{G9"WIe~ڂ dž}J#K!΁lցSXc=$t@H'g6(kq®³=iCܑMG\` 6X_`P$YhS|H{(}u~:dP;Zrh- ^n!l ;iac |ֶ qjӮhm{Oݢ2ap6q$4GRw&k{ ^_b'*:ޙwcM33* ]_qr'+Gw!K4 Aa|`b|8|(KW nQ6U0oP't0O."} &8i; -k#ԟDؔʇ l3E1g#^<Zd}=Ekn]pt)5A=裢sFT'Vǫ)7!Q!y#51鉮] ~!kCGb\]x`&03pt $w`Xk XǢ߇.lXQ. j{!UKN;kqAoHCκjä[=Il΄Gki=t@ -qK[O#NAQHv"#yn#c,Z atC:;a\`hFށl4hXW%OK蒳23"KlQ}Q|ˈ)x "\){ߑQi#?a -ߨǃ[=)jMb>Cx yH@-:Xe,멣illyLk!7G .p_%!Y ̧V`3[璥eĶ'+_  Μx[KE PE `'^%+ЅʉIȁlv=Y&yS?Sac ftL}* -4F]*=)Ej>K_0I0>F6B:슱| 7U|)wO`s"2v$)L ~-? :0 ||#AF"=9L^1OF>it%ljLN7(K!1 -THH KttRt̟wcK6=& 0_`' Ɇ˩Tb4Ld$举2yc:u *8o4l^9)z;+ooҋ&>sK£K ii$Z-/a$ox53 |v&wuTX805ȒLjt. *sɂSK# s;Q1֔2 -|z`l |X/$H߂L8<%bĄmj55Wt%ws d81[2(=O~=HIYGN"w@;$qNG!?9Ħ8$\ph> +|b y> {4x%N~p} A5_ | hL2wys ?tM陜#@'UWLR9AF?P >'KD%cEM3*\7w84]ވ{';$\Z,)x%ٔ'M Et:W}w%[`vh>` #r5kT1]?SemD3Ll|-Ͼ:`%S3b=К A)<&q I( 5Im_%T ܣ 1oC\>;{2'>"_ -a.tXT|K,ɣp| #ȉm\\_G82;|3Y;1iۿ~&oga[2T fd9sGŝa} _Hڄg|0_Ll0:'!y`r/fvʧaΤψ:O0Uxl)Y~Έ,8|\ Rc+'CLIBeźԽhY0b{`bgI x 1)&6;>OR l~:{c,8z߈!v ~5OLL؁^ c5+U~g3Ph٦3y^;VՈc>Wgl9omVwF^uۘLc{2NM ؆-8O_6o/~ :'tq)Dٔz=L\C+#,{B؇>PgzOP+*ܴ@r z0`AG <(u!@;!ǻsgC HwqH^<]trF-z.zlR46񈬝HbKd L`XqQV[BܓFv֓cp{^O؇W5N=>@8b; I ̡7LNA,֥;F@\ o8X3*=aHY^CσG*=pż-4u4Z2j ۏx%'2W )g Cy) ݔcI稁ce Nɻ#E|Im3!V\@V^7%ko|M]FU}rf[0!oyRS_c ^(>o37 ;s#~gZm  x).J8?1 wćN6א"Sn9O`.!=m-r~re6CF|ŏ9&%̰ΛJ!Gy(rH9 &'s8ँr -JAl)? O~9'=Jyk@)|o9> p!UBe~s >|=릵~)YFoK6n$kqC<,riH3IGDv{C쳏ΟI5zG=L5[+ 1e鍳;G6gdˋdRtX't91"nQ5 J>ZT\TRC;)53 kHPhIm}FP.h.vn뮅.Ą'n~MW\1>@`Q6K7j|ڜhl/dۋDӣ"| H_yE〗:_>?qΡytCI>,.cOQx!CF#A>97uk,$+VlXg%q<p'poQxhh8aָ|"!|Aiĥݹƛ 26{W1x[z۳ duTE 3 -fy \|蚩\%L\`(٠D@ š|tdr7_P[=uqK@Er,U XuV5cϛ(`>B)\jLC+OS{,a~){/ko.o[3 wVgA7PswArB [3DWl :tZ,1aI#s !k/=g4. `lSgStʈڷ`va+0uwVэ7W,`6~ -wDE}*2"ͧ -PY @ -]yr@Α2r<9r:Ta*(8}G|02G5=xG|+|uڷ*Ύww_n >{/j*=|tx(:o|jWfLM:6I,9 ekolg?o -:j^G^1fZ3}U: 0q<)T!.Dpn#B -y㍯ĶD@H2t,yz`:|^\RtS1U~l  Oe -醻+54v UxZJ?(>42O,cVT\1_-֤l*Ϡn: y p#Kت٦udErGbq$vo'ax7+`-gPaa5/8p@H*jϛMW#%c(>:'yfGT]2!PrGW0av^IiCLRfr.B'"R1 l>Z`' `b l6>_Ŝ6uU{}Www-[w5>_trfgZ?"aP V(k GHn[rw!.:i@\@r1tT,0 A.@7Zzٻ2uS3.0dD8 ]1ȖbumdVߓqт|X} 4s7DϤ=%+pΑAx C3'z2+jҩ\֮K2˜du[9<[sB!`c͝g$aW8>Y$S{@J\V8Upt1ؤzm3K)jn{fe ')KW^2񟄚i%nFdb@D9ς|#0هQ53w/9}v&^ʹ̤kو3)zp@kQ Y1>8H?1! 1?%}c3t mi1Z>D`0Kfm Flaq*bts%\ܽN;0 M gF8[k6-p,~Wx @vAo@TxwU;šg7GS|/BmA~Y1x*RۧI -|=[fL9FkocgĊ w&jnن/XĤ_8/4 ,hD 4csGn8W|ΤFMfGs%Wl.Fp?bS s;cۏGw!{ -u~4*G|K6>3Vx VW@.OP:q*UxސekAax ұ!7pE叧c&A^ -]p@5egK&DnƴfC'>/[Qi3Z{YpÌi}l}{ cKȊD2= qTďͦlyh!]tl)6=YEzdIh yw!ܽuCh~hz*!q9Te)Z6HlzmLu| 8< q)פxJ\CqwdۗdyG81Gn=:iL*|E9/`񊠢QDtD2atEMbw[lvF~[~%J}O;>oT}^M~XC{eÝvxMQ-ѕWM~ ƌޏd}NӍ];٭ͷM7٪+@ʘZ<+{߀?# 7HM0D؞n{jj~jx&!ko\s2ySwʛ/M'l g9ٶ[@θ̽ /z ~ҩgE'׍-W+l{hԳR[xF+j~rrM4=[#7k#:\a;y͓Ne</NPP^A:mrv7{ˎ>" -oWLZF}I:=;;d '++SW̸;Ozk9Uu+6jxR^zYlYf{讣{Nd/kɂKlʑyl%pL+E`)?A1PCo'C%zCsHƷM%wLWiYPˉsK'/,[mn:O~b^?O!/S޾aμu&O|`S9CO^9([:5|Tcj{y% -N.,Xm@Oײ'WKGO Hm:)Y`3$oA#})MyЧmeO>pa>ufw=#k_[^.w?>(4,~BdE`ExF_?hwZ{Zyo^@ sT ck'eڟlnQ${0TKZ{9>eP4?_h}fw=6+Dɂ>4qOƕ=r+でs;ֳ>#_6ˏEq'>E~g~aOw~lw7ஒѳj{.>oxknzF`kvtU[t9!j{Rm_k[[%˷?!Iǜm{n}mNg}(OgK^3 y59|zM+5ͬuY^UXwzW(~.eg0ݧ$ݯ ko?S}xǾ~|0RקgkwS8r䏿leοv=P{x+w0C|zinP=VpW^n{J뗒߈tzY*߮F";Go{aBc.jTyuk:o>_>e>?Ϣ|~E..p3I{OOsy[i/Ͻwy~R\Yw6yEO_ǏNV*?,v];G:w=b~b= otu\Hcrs/_n>DxNjpÏx<[W_dm]ihX"sa>LUU|Q`dpr'#mn͍SY~g7(LsGcYR1FhcO?r=w;Xy'#[}RqM{^xÝ}͝x~Aq͊c)dsO;ww3_])q~ɮ:חޝd?Ieov??{۽\lӽ=A몃Ww5Ty_F`Noo l~n]CGBU&t@WK6?̨(~XWSNl z߲Y9׳rd>X M2yu='/^w}wXl>ӿLY߳gGWŽ,O7wMq-.Zn>ۻ֢rثOVl9Mu7un\K%&pzяYnOW;>;Qp]PwȮҪ{eKގ/?›]!W:".މ,Vp^XMwËn܈*>z3fBY*G͕(`?tf;:Qx_~6'zkCՆ:edI5J27_o󘗏Sϓ Ϸ oe)({{z2?m+ K;rf4mw|y{%JW]^\RYp/`GtEVx < SQ%GкkPVByԻ9-n2E7]rGor.ݸdJk%.?yUwn9w+l)9w6?RʜOs&?NeĠF'{^A|ݻk%/ _)._Uy?q[zAۋ5IݹǗ.ρ%ߣy:w+쵸Sb܈-\lgq;Uy#ԋq?(}/ -F4$Y*nVwV_K̹]G|ocuW+Oy2kVl\ W}}ᝌzwlćq/nw7?۫kSAG{y;}mq~y~t;}+0~a+|ݍ,s/dQ%W5=py}u^7we[oqvOtTͨTz%,nYÆ2=7^*tϻr'Y=9C2&Z*=STY?©_;\^+wxwYUGH}p>ލ[~m>1}Kxh[ n%޾x+^b󫽥V vӛ^n^~^}/c/V HHQsǽܦ{]뉰ב`]̽Ʋu"ws]g샻 GZ:J"VT~r\IjOW).vx}ՑYMuO*J#_a^w!yJ=UURd2{W?E/~J>m*1 zWX͛n^kXz3īdNȎ]/bݞIWwR|%G.%ᅵPzr\鶫N/}wk֝쪓WcK/\.jG:!y -u+qoA./o[y;ģ#"Sn岿{hRo<ݻ|Ktuګ$+%F_}'16[)14^!7VxDBLoMԉQk{k,*v.|Ϗeʫg7_L.-SOW[M;ŮOT!\TSx3rjն;nR8ϦGkFwz-}ݣ[Fn=8~cU=?o8?2c_W>M2Kod/%zgw'&"*$>#п&HJIfѓ,5|gW<5]gso_e3]שg9nVuV\N-k\RR)ҋeSʿ[ː>.=})Njq7J,/Zʷ |X埽/V%8[.W$sI-'Yc淋!exֱk"gOד Jt轴$Z렖i㶡iK YD wWJV.+YZq!jZYmSʶ_H.;x>̅ңK!ZŗWvfTqݜO3Ny\QCOB -+*ֿ򭭻dޤyHGgJ ''ZmJaF%LHD(Y1{crxⅸIeWː)>{%(N^)A6_-9p5ZJӶ29?b;ĕXkuSn a ~ a"~m_?v}߻n0oZ ,z2n͞[>/ޤ*sx|Ϊ{EmޓD%"( Q9CUb2DI3JT$&0ۆVYQn[y}κsLsyyS#59b1l ⦳ȸ/=q1W]įJX/B 1 }9~E߯وh4# ,;E+sV4<_|b~ -'|bA峅.4nVP0T۲yƦCӰo0sI㜰ؽss꣈ wmN\ b?aK?AZP_s)F4A^u%ν _4n9[sڎa6BV;bď?߃gM.B(2{=\u3\'?MpeÄ=/nq)!GW=\F޶r]otRAӶMUJ4̭/O?:t~Smo$9Qhtᔊ*:V"#y3 [\,vba//}!v}#< -jq|ohSAw=kiL~.g6ѰB8`dcDd5Ҝ 5ll4 \Wr(^yKo6+ -p&cOa_ٍw`zzvóip͙!£i|hel+?SGNEc qd 2̌!Syt\d7>=3g4eA8rڭ6 ؊^ ёOTQ %p{qŒ_]+y{xǷ -~n;s;r-,? O2ײFFQx!n}D4FLt쐩4vds?sPd#BYEcWvJ4}IVL?sEd [ c \!ye眩D(0aY,m2['I.9hj@%t'vJ}Y5(g^OKԵr]]8ܦ8 ^iz9vNˡ ׻_Y/ =D*S sd= 8l8bL}<2ױ\7푥J4q2lƠi+45M\7 VRmW_ZbIa轰}9w [8%]ϗ]5[럴^u£[YdR2T r,$8ziXg} 2לDlcmf&z6Do1z&23Yۅ 9hX4eEP&T!V&>ptI½͵.n:W yunf}7evݺy>7)v d8)C 0՘G%cenmv8aMb, q|2&:9XX屁c`jHX<$8-6Iu˚l9sill~vۇ]?<|s'P%5x?D6Kq\p?YOpFy8!WBfc}&gxX,ǭDVNhdΠ {#FsWS_Pxkg' {rIXZ~xxigjn_(z)&//#K(ՋWBMaƫ&m:$;/K,'Q%SGhC:C&O\4y qKpN)Bf3h<9朄T9L+_ƂWK>9V~-|"Eow%Qo{9#qOvy~Skm!^oPsֆ-2jۯ7z~ks&c@bhdNGA_jYd!36aAg";ϵȖ@#4ʯh/ȹi'5ZTzKwQ3E\8!a}S33a {) -zr88wλEĈ = ާ<}GU|]1>cDwdn+F]֢9T)MZ0fC}ժ3x|\K-zh8z\y%W5-ۣ"pes疟ƻ/B׏)܏W|WM+Ƨ-Wmÿo_>[ʏ!?V:q/Zqyw -*:)4L5!0ӌGN¹4Z& -F6hG^ќh=vGhb-Ԗ$tU]qWpy(BɍjVʤ?.L믳}^+ -bM !'9&w_U1>KnJٝ_p߼(.chpÖ⡚fΏ&[/R6{yDo -\. D <UhYlҺkF. 3=~$b:eOd!iz߅5"/'!uk!ثx߸zmK0qޞB }~˽mQ&?`G1Zh[E;XF06Mƾ@(& d~#Ċn \iKN\|J^:ݷtK!o֒m?x1-7SgSg3ʏ]S(o] -yt?Mэr`/e=eCRU-t_1i ':z4@56_&$:+*9mף>vNjOdn$D梄՚uEy%~Ml ->'%ܓC%r)Ԟ_e5L-7rŹqܙag?ٝʕ;P'u&M_I?8f̞+CK -53N $B -1??,þ{C'Ox|x䭗ɵw?m -{ChH}v~7Ƿ炓j4z_|R 7b" J !JAt@?ۊisTեd ی'I$FktR qmB4AԜތs>-v}$u"o*B>{̃w#sSW?+<)R>}PP:|}RJY%)n6WVa}в =爐{dmc i\]WKTGR/$Ԯoܨ֛&&76SMvl 4a+훈'0[jX p~^|_?G-o`*E`Q1a+v~0irbc8
 s0^qQd;?':&G K-Y;ڃzfU)}^~ionsv?З>F0ަ=Un/XzջcFx^pkOv'm۟-~\":"20hr'4zZ`id^(4~n䆜1eO,̕ъlHG#dٕTi'"GasVFo+ іhɢHע]xJJn̓T*8t;J qoy7Ln (aR`UQ{QҚ33̞tJ 9cdWxz#++)!2U:Sf)m4_zqtE{mQ;ߺG^r_d_IlASyޚ竂n^S^^;UPYz>Exl:c2Tc2 -1F&#q=Py"wiS[~y*93'Kn6r[ĒDz4ʿ*OHhYh̅(+4Sr#sMBVe֌;//}GԱ$ݯWzf+쫕Cr;zI:ͩ,&~2f?jshd7a-`ץ~{m^(< g':#m>eퟐ퉖Μ\(lR>hhG"dMlv%-]})̩4g -1˨+L^d!þPyvlӥo[#S'+.}k2gKҽuƲ/mŗ ȋO =O'L=:#ya;! Ia̼|?>'wtrY 3u=/b禎lmXBГ.yJ6^4vC}40d۟9GUJ'o22635GgBqpOD/nEɏ=Q*> va_u~{>$KCґrrYң?^sЀWB7B͏jZ%Bb% -PiHRG -WDNi=\6̢=<r"Ua!I=SBg@opxq_Z~kWYN]c;'YTj4J_7'g졷 uٰM{'SelŇ~u Ӫy4(e -(;e&Ӂ/ZLd5KT[bb;z@KOZnZTkog#=f|i -Sk%aZ4M1FϿhG蔊Ѳ]8ü @3tDMUJ担|3$뽻 4 T*o\LnXo"zA}퓝/w `l:=9X.'u?]}h:]kJ嵎dw(zm(}*M݃WUdju?q,XyU<u\ uru\}EDX$ V1vhyD/RCR&kguOyl(Lqz~,ӌj2_]r`2ĊXu -۪PšJzp s^+:c q` -hR=Oq qdI>+iVYB/rcӨ`TW*]Aa>"%S5t#Gf:}]L2jgj3WOa;dzi[L#zCy$;^L{b|Ͼ .|E{/Ճ>^.W*Dϟ QՀ-zSݾR1ÜywBOim>2?G~,DoЖ٠VHӖk:\HxlTr!' TX'*Rʌ 3*:C KRu@+,V|`}Hӫ}=AאI2⳷3}]'E]=U}Ӄ̎Ѓ4w``zg15g8AS"$ZH.-//du-ù펽oi+$UjeTe^de6nD5.bd>{!eν -a/OfpLtv[IctDt߃A61 >,2 /k;>:4dOOI;AԲJ -I;IO%d :L]]@7:8*lJ3SD БsʨtUAdmph=6bNL>^twbw?>Wj- ҽO{KO%o%~pOc䗞O?]}-~neՏjϔMzOQ uFTj1~*񚽬T:BZvd,99̱G+p<%8HUDd'dbSGf*K7ٺCv̮<|jh.1S/nOeC7t7VKfC)Wr` -6̭lf9I6łhl`ŋvLds[,dF:!SN(#z;ے)[5ρ>|ШԆ>i*૾C'Rj Lq0_߹Jvqv}pu?[m<0 A{ -k=D kXS<]/CeJt{gf@w↬B;j޲  XW7Ug<[9~o)􏃞?hGJxiPڀ -B*h' #.b4o2hSrE}4MKcGJkJ5A94U3/}މ=Jq]Z(}GG|cpl֋>Mw L땪W>™tc9\&i,޷~ЋKWoI,Rƅ7Q=dGR!pfk}Y{1FdV& f)\%AS/]&4 -t4h{Hwum3/hu2C&˒)q,LVٔj4q}وIb̬5 %4n*Jw^:W5jpzb./@cM).̾OÏܾR@3NP^[s @{ 4GOie0g@ÌLR>Wny [ @l`AHA]&U[;.qr_yDhE&81z}B&@!6Z _ +MW:$IdXӆhǍ|YD+t|S㧓C߈{= -<ܑңD˛C^^^9*QB_>d _̑D톃/K֔tu峖 o\kGLP&k° ClX6hO jFb@+ -%ϦךkRF@V~v[zF <ХM%B sGxǏL!аgpNY<6$P[,xMm4@j}hi|.zluŞ/ϯ8Ezо!ц?樬=rg Ч5<2!ؗ`Rp|$U*: j?Yr?tIӴD o\zq.sHy4oe!j Ӡ_ -tXRCi5LAnH?M '{^'^ѧdGtq͆&js@ Ult6ԱWf00W_q4𺁯Aw\wd{Bnl :媓wbh qw]Al}Y-Gsms{9cLb!꾸 UVvIOO!]1H18YzǁIPu& Y\b^hcȵ_Z8o|.5`lեlUԘ\r0(Z*&@Yyh6hm*vߓлd֕Bc5 tdYdwaXԙh1F1<+U/:Vp``- ؝_{)7lv86ے5_=w_z*)1]W Q^mL-Cу oYLbk7(D/'SzdSsTlي3'#NNQ\-ѕ==|i.X`}: -w`{_yc*poNXhD&hA\$ܬUFJw0ҁyHW8MYߗkY7cHW lY!ccA[Pl є M戦m>T޴k -H- n̨cK h甠1To|4ĵr#/714.?l<x$zvgq&, g(udpR%CG\iUBXmkl -†'}@lv̍16N]m X6Ԍ|Z^=Edt'|m`gU; wqwv['y \:+(T eVms k18FDKGQk4D" -P6xuAYm K8+{"ȑ^sb=>zXچ,0AwA;ЬhAgx80>T1yR$d }a}Kv -E3I.0C4YcEJ`gEdTewMמWLSxFtnWf8P̬02 -YqG=?? -4lieFM}ӂ:}B uSy*?ux! ME4mX0[_c 1YTVGh sqGoЖ2ja;$uţ=HbLtx& D(yes.!?w/Βr -5Ov$X#( -Pw /#Q:ty|dH&|q+lB{»זk>OVh8 |N^&aMdv7V -Z\J1_f\]gIa PՃ}#U; [7׊YiIXN8f; { R`gAbgV YK?Y,c5!Rj)0wA_r – gB; z~>񊒮ɠM}k4_<Tg< tLXLcvw}+Y`<NX`Zo1m Dz x$#r$"ՠv>0rI> P A*JCYi*6 8P)ĝv@Xe=)7Wwv!恝{% u`g16`+W(R -GŦGOf8~ do -0F΁k>kz3U^50*[}ȩ@ 7uLf!FB;96η]T9 1yzAYeu[s_.\Lvr"=ӨŶt$쬜V`gi\T$sy;@7&,%𓁝U8R;+eATr^`m}oo@N,0ejV uFՀK9"`jdXx<='^? Bv൯2RQH~$-G#wB զ7x]X4f 6 zlt*[-mKJy'ŵ ^Kߊԙ̏@#=6]GOvV:*7G^{^\6Z* QmW̬3;{JPr![z ,|}:2g$*M.; ֗9@J*) -X^fm;K^l`WsaqX7QwZ 9'ܘvk]rqUtp"wb9':pbuH\R!BJ!a=KAStL 15م5O\4X/am}ChK`mVwJۈPT+ǵ*0p`}s)\m<̡1ԭ'ya ׄ}' `F!ly|㥥+}`8?sm[&Ģi|-'OܯS u.]5_W6 -Ø0 ٺp_r>r}1Nb7=\\nxвfȚXϪY֋ /Y|K FV󕹭V$~-3ͧZ|eݓI5_k"µ{.ɥF\\ïߖt݃+mjp>8>GbDi5fb="ԉjpEj#k+K'$!Vhr!iڰX[`TEL2”s[LX2j q=( 6D0uc4KW:]PyVKvNToÂeB.6O0€$:0 g{|F}䥇aMD^;Y-Q l2U^90@oqh E%,6n>Ol3 ͇gÚ#Аk$YO;?m!u] -,2C[s|6 XiYx";s8S- c]Qfu{&=Z8UsֳƖrgHB֬gk6\){  SxոﹻJ.Mڗt=vp]Qy|77Orc'\T -dnz3"ENK|o -{ݸܑHzQQg&UQgpNM}bhu,R$1c8/"X)T#,O{mo"`S9Z֢بX-`m|>lCfst(ZM(=ty鄏B}"p`mz3pϊ0a LEVwYpnIѮiXz -&5{9\RÆ&iS}=ޗDl25>:kRqt4-HZu`/ oxj!+ze߅ͤ1XĤ}3C.%O/6"yJv8eQ5';zvxb.\C=/ǩ0Up\!>L' 8o?=[ϖ9[B>!y(57=cw> n/g-a^OžG =Y[ g%>% q֮ >ܛЀb,>{2`-8El:-$h4*pNpo\sZ9a{iMyϑWkprFyR!ab 97eityŰ>#/ߟ .n `rtį+}I9`r0?X[l u6d˫=hVd Q/',.zjUl|vw -ɝ]St-rplgyߪ|TEA%Wpl>y}%p3a"nDzTx}r&C^%1Ϛ޲ϖH^ZL5_^T66Kl\|{H -vOWHo2VX8媶c$M3ue*dLσk|iJ)y4&Pu< rf bL^9Y -'A<-aϖnifP;˨73 ~Ŝ6V@[XPnBXM>p^a0ǹ-LJO#+`:;cT| -"A\-d꿜9!b>O~(aʌȽRM?1i8߆m,:+tP'66EW] B/ڂRuL qas6uM&vZӆ|r]u|Vo -97~alE%j'\Jg_p_s53Qq`L(3$csOǧt|:>Oǧt|:>Oǧt|:>Oǧt|:>Oǧt|:>SBC X/ދגuIS:'%E%G'ć$[;·X?/$=")h^} eֶ'ΰuޜs%!ֶ/jl-JߔF 20-,~jqH2~J7]"sӷmpk]kmE3 Y; -D[#td Vyr A?T!CkmMw7?DRe4-(Tk4@K'@]WcVǟG."!||(.yW/cԁJƿ -Zj}4A)2QLVSחʩ(Ϭ52|=Ӷ*Y*AAZe -zD+쇋MաSk)񊆳K'9;.>MzIa8h6B6@L0 4(uZg2tPEt&cru٤}Te7a>PC4-KVmf2iOGkk@CBm#zfɫQ6q9zlB.9z37[Ht/.[C_$3=-ԗ*X`{ ؞w2h3>\|.](6VwL֤֗$)GǍEjqY]\\Pp>I<ߔ)>QCL֤C¯QQ/;Qk T<Ĕ -~+?esF@?W~:b*\-R#K3 -t$ s=|=Й z{AIlLHb="QC~4(AS@Oe2XIׁVUF7em&gx|t kI4U$C/#KC)a,_}b.NBo7uM {DYV=3Db4ep0gB scd>vej’~fU)>zHR#bo/+S*MeF<sZDg "'A %c-zOd<~>ItXLD, YAGiB/Hpu&b6:AlNӂa[sE -{%SL@tz@CC\m :nRĪˡ'*_ -^ zDW^+}!EL.h.ȓFoRvM8*ʺl'm2G=-gPUsl :'*J -4**ߤظ|2u1hkZ -u#Ub|]^;tEh#@i ,Ķ}Чђ#m=dԙF%sWMzx..G)gN6+xm#Vv-sAS@YY{2 -2=S|b!ѓz]WbHbzz9&_ G|*A$[Mtב た}p=@O]ʪԀF㸰&GjdM.4Ct@c~RDCNa z - -ob>n˦A/5s*"M/-+nӡöDG.,Yþ9yh:o6~x 誱DI6xq‰@lqzwGXs¼ĞAk 3ȅ@f S7v0D%qfDi}hEJ S¶>$l3g>Pv=2cea$Ea5H<_a^n%¼=9чh5C#-l=lA4؊ö Z"gMmﱂUҏm (@4\5Ұ/ۗ ~ДekoWնOs 4fD100PE7 -bآHw#9g;p{{~~b˜s}c>umh- -]'8΢0k0k&Ff(.<} ;>4(hs)S^*]=Kc)Et[@n Z:&:U?y{"k7yÆ̞ h"XcZsW/~^rA9lEӄo,a l!f& c>qJ 4 @' -h3g.O`R|PIQ6{DC8@ /A8mt Ԭ11&%w6?d5C|9p5hB5nM-P`h@YT_@Ƙ2 -{`dV *жx&fpFqAӁު瓷~ե6Ocq|)EБf_:6hnxtIQ/+1{:, -_%>j -Z1Tоחc?O0p, ŶA -!FY/{-`/~S$b|.#n[ը_dlˍ(f7t阒TJ^ -]Is!p%#+&ԩjԄ 媠2K\X 40.{bej e@\:+S~Ih"?({9^ ʟf#ӁFhV[ -&C^=aЁ]xgccg}&9HsWNbC O" w}2`=rmH/FM\SCg@-/ ߀^aMx(O`|S.sqOzH>W~DQˀvz}VKAxu5޸ -TCE<97Z=fND~e;G AA Z#rg -WAW[Ș?~uõ'M X 럻NDUng2~(D@&EF|!Zq< Qqx/#c`MHۏ~ \ze.=BnGF7Ħx?=&ȧ R*G4t`͝FӦNx.x*-}4(?)1C:͐?R+^/So:? b]!>8քt'Z -̛ iZM=|koN/ Y 9-ƄZvL!u =>a\e어~|j{hPӠw-:ksi%QM]4OD539T}:5y@+kTטpTb^oev Ʒ0f+ޖ+2wH=*,М?i;r|+ }^䞤$vMx"j -_>G/71W"DcކhX-I/:OΆ_ Is0_4Gcb蹘">hlq+s1yBk˱^DaJ(Pxdvj =?99"KyJ|V~HKq`n9guT`%п~P<X!va:h9b=D < fC:)1WCsu^k\}o.52b 뒥Do19ȯb4%GIנۏZf- ~*G endstream endobj 27 0 obj <>stream -A<(wQnPaE~)m[1ᝤO ?.?`ln/ G ➠ -ixq[;\w>hA_qܝyWɔ.:C{&QW:|u"E#=Iw zc >75,wcZ௨fB|(x< p^пuB3ZМ1a,A{LhӄEGGKkܟAxu2<5t!!>ycr&iDz(N:ZkAzX| -Ό|"F1J". .N)d Qmýwxr8l\Mo!9!fzLX8?Z=&n~y)[D5%1s$ -pK#%spŐX\xd,\Fo(xB( F zFJ/xU\W,* +:mt%~+5<&1QWѥ-l55+*ŰCu%8j=<>/ :q2SV:wT˄!7W Þm^q0T;a脢 -56TN)S3^nDyk)P -+\\YJ=[sa]_ -csX W1لJ-J|z^Y9)_|)DejMx?فoϵ^U2` xxLmqzH@r9m$ g4p@l6qTCoEW.)3QwVJҗ@UuhOH6({sFOIxBh)d TkI۱'$xpGm}оW0|1g UuQ2qS7(J*xL96h}"}u.x2x3Wb iG\]=/{|111!= ->Xa)J TQg+UuORTa|' -?|'/S!-r q5rQj&Z^XK4#nPO^<:Q@eAK&%|.p2Ή@NmQxFxP{.+P}Gu^|axZX.9b}eX;eE!h3宒d,5! (a}%g'^Q1"ģ/<{7ǹx!!vY?W4 '^1hre_k􄄱%3K6)xck:ڄv2JNnPf# -|yf.o=:/x]#q>ĕl:Xq(F2w4wO@\%_P'`?^5t-P%YK_ŜR&ὲ8RCTNOO^vɫS<؇P,b+nd4kחaOH N9π^ 셡mf[53!倣D:BD -~X}9Gdg{@?bjhh5Ox -Ç|O̎xޣ {rր rX=XU'+}kp=pԥiT`s!p^rЫF]_ { ^ ~veGxc{Tp1b'L@v0.XA>^{瘹xOehZ]]k&Z2j/_{")o3"rU-~o?gb^n!eyp2%7V=}.񾽄J/ڧ'| ?.eYKg0Xr0h.nm `]qJ랰uƘ>qi4<hPKY@<'g)yaiL%apfB6v%'1;e7N!֗%qK U Vo "50f k؋~ B }~ q_LQk )85C<\\'Ea6'_y&6֤ ASӎ>BiBkAb )OɔJekrF~(~_lƾAVVgawCg'3[Ԡx\Ϻ+>+ e;/74_6ƹ7Z1{Oxr6<EпZ}X煞ǡ -7YZĚS(cx$ar}b1zEh,@a)l:FM{m[c{Q?2@M`||hO[1yq,yf$ 2 HGm~fQ\ {s+WKaC6 ݃YPS9kmE<ʭװO=y楀GqbO*XS\Y [b5lDVYل+{ - zK//lh&K.Q,#lk(pҗ #=ScRy[i/ -iLX&h[`DzTӸ֡蓉jӂoYkՎFe?$55):p2jj>}ذ}CJ%B5k+aXGEx3EC# {@z!P{G@ށ'LZ>*[ +#aen/EMgg^ѩmDMz෎\pX< +JuUqP>a/!=ɐrxcJͅL@ -R.`VX*l -4v͞OFn":@_.OfLhwLl!̥Lr>A feV3j!oփZP״|zmFdÊB 8DV]d Һ\񧙴K<|3wa\|)]\o^iHWa N+j/z'"YXiI{fҌ$J]TwIiڏr.W*j&UE|J%Jj?] \-a!x9Wa8?/(A}dn07؜6i浙STXKѩh!yoUш;`m{]|QHtY,ɭ9dW|N҄Io&ӻTѡ1DŽ^q 1F_fjz0bS_zެ25Tj ->6 忤&eX >(W{]z]G=Gq}֋@7K(a_vFxK[N rO>l#77!_| -K[ĥ&]0yn֬dhຂQ"閰,Imj'̬%>/6JŷEVJr5k2٫ڬU[FGož[<-"Jo}ТSY_T;x~(H^g!󒸿6PC|9M5f>}\%zGo# uvtEmKUz(_=(>..l8+"ީ11-;ye3R2 uz'_[sKR;=caf>C+ەoVKoi,BF&}'i25C|E@? -ߴwgJG}y[vt~)>üd~>FqzX*dʔuYJJ}$}Ee5E&!3>هbqn){eZuȫjV飊#ҧe;- }ˈޡ'F4 -R7jw?걷H*O>ꢨ>)0ے-8Ǽ:T|8Tv^ .jWD  tA!Je>\<$h,y59Sa˼=LeҢP 4q.Gxakx&Hޔ_4wQ_?ZͿ]MG3C>Y -bz%jI3[H{UǥolK\n_b'~QsBrI$j7XJ&496_ a­b+q5OŃ%O_|د}@x4xkV4Wy`\x嘣d.⾦#ĚtJڛڎ~Z2ٗi*FK2'W=GK,~hmR$.mn>)*n=')T9T\o7e_vӯ;Q;2F4P+nXӯLgjz-n%X6Y']mLh͒0=_Oi"mJ66] S>ne - 1kˏ4-?,PǫhaU軩N|!H=mcDN7s:^NNjLO?0Ѹ{]k4|?髊s&CD[V 2h $ބ<1Uu9*̲9%ZpWNԱ[1`qsΗ2aRY5]'K+_<9/yq~2Z^DZoϼy'ƽU–7qgwQΪ㤠1gSj%vs19D&.7M za?9^O\M-wIp Ip N<ߜh F"L5H-@5Pès)[e}?C@ӊ*6h`W&[- W6m6b/n+ueIR7IiI }ud5ȧg GK;JEjK#.4&&z&y&&WzJZ麶"]BS}ܢV9Gf8߬p@]' xP#T}aAfJ5l(~pL7D dsv+ɖגN6JtTş-x%j~#+~-gium1q]녃=ŲCy2Ǟ)<ŔA8ɐ+zP#+?kX۝CO=HjJKkrZy!":bpQmXa]XIuHeuH)&><1jkTPoeCZq@YדH3'z>sy+=8(ѐI.I3Ҏ ަ agC_*TJ ?IKLz+#N愴齷*Ĥm[Mpr@O+:$EwVMKDLH^CdQMXauF7k~Mh~M*4R+]eer7We@SAA)'4 +YƩ -Ĺ%oϚ4G6d }j&Qaɛ BT_~1zT:WGE -[-k }`MnV)'Zo%\mW!Zr;Gf$KLuuOlLa;\u܍wo -LuYJh^G;>+v-q-:ޞ% loVW]Z7X˫anD_Fq/+v/u -M* (O;{\^s|^#w(OAeE^E2(_II>IѹQzo9=לO6I -ʶlaޙ6 -λ׆^qe]jLjr{5E a GPvpnjqH~l$ر*"6'&=jG}eT䨳)Ǜ"=oͺEJzC$%ͭ~Gn]GfMQg2SO7g%p7`C=Ϣ}Ȟln7ѷ#ef}"9 #j#c]#_6xg~ۢdMTǤMQbCcd]"~urƺ=XqB~̌rg~pV*O{'勓^ '֍jZfӤ8$.n5=r/ѱ:*%;j[mcl2@Xֵۉ*]% g2"m+cnuC9y<ܬ0ڼ+'BsR_KMx≶tGpܘmYqcccbT7;^-g_ܡ;S'8{V1~JkewLZ[${7"J"N6eDw܈`<[էèa;ߍJ%{7;o]sd;ÝC}})!, 嬒JAQrGZ-;guJSLQӥ' v9 vQM^ɢePMDSwyR~y=|1}6y^b}ƭ{5[U[ eq)[QiP_ڝJ"_zn -/ -="C /#p13VkU~n,E񡥾 ob߻ɲn.o -Kߞ5Zinh:rX5WX8\O(silt(WMطODh4n;w)s9,[ -dJK iks7+V([ -}>3vUqBAV[gKwYo=b -:-v /( {1"-:+ֻ2 ѭ2$!Wm4jSt)b0C>7@k8ޘm1Y鞂N;%/rE!S;n -Xip"BIa1b1CE$"1PA,DfD5 W|j<&N w+gc"D/lt5u{}s*0.WTZG7(/F]-=.v|!J[Ǒ8G.t/|Z&knuY~iI\p9o-j ۰SXTXf9nzFN󚂶lW"&(b1@%>r?^z,'yx˄"b/JW?*727*BoYzgԕ{nQ/ -\"s \"rbEQoK}*ٯ䯟\/DǼjF5bhts.Nqb(sJ_8t#rj7L F N$FMD? fNYMrU@ucsB9갸9zlVGTasշQYܢP +r 5yYSwΑŞQmQ_[>X7;GΝ/{=w]Bi t DgO@4ET|?\_{h.{Pk~[?e;zAf-#d.Zb^BX$aoc+Bg';QrkGh9-CVGGqx!5&-}Kt"7nMj_uF){R*K( r bƄixnwyuwpMc~H9_$jĂebEb8q؞<Ϝ[I/|e<^ž-pM(+pK)-vNz^[RX[EPѕnhr:m__P6,/c:{cѻa3I8MC9ii#/&&/"YMT8@Tv!v}=B[8!Ԅ(hぼM(D=E25ɯZXt)&2aKwiK/cC\ ?U/`N@Y IqbM;YӶ3 -S''ZGL -ߏ@(GJ,xf%?[n3oxJ(`{/<Pk}\5Ǖ2׌JJRD]ʹǿ \/JĔIh)7ČˉiVƬ!H̜Xa? gb#}I{Fq+ש导 9b,~~?1?QqBb696s6%,Q'-2$f-sԉ bN(˱e[n AQz\,#hw -~AX{Ǩ7Qo5F~[i'<9g84b42!75v91k:b̽Ē5bS sR̥$1s1CQ=~o/T;A6kwٲ<50Ts n Ǿ.rP3P_c;PXST'Y5fp0F=<{CM@N!F"[BP”5h fMM(NML_&Ebbẓ -m_b{;ps[?rG] <#5SuCT[S<̹ZǴ*z䷥NKr{_еxMELG ͫihM^OLDc8 F-"f_m#0k+OU&ļĂ}}bO fOv6C@_X&2{܇}|yYd֬uv-`DTZ6N E(G949]N]bEĴq PnD䕄tiJkL Ă '{bmm^5p*<]*0 ȊWztm%OK">Tل~oK.vHL*q0}a:fC?~+ a1hơ6,|LP<5:llgPN',&g 4&h-o i7KۨZm3 k>Ee(wƼBXhtscBgCRoC:J甎 -G%Ejp[&/q(LDׂ/%-t*Ĭj(W( -3Q L4\;k71g^b -1w1oM,xXJmt+!y/~Wm zy ,L/*t~M/~9mV7F WŻh℅u ն1BQT4N -VsP=^XaM,fF,Y#CCr5耚;{;”Ă4`XzXHcGo 7[SV z9 H󭰓uч:Dl̊M}'TӵӞ(ڈԲ2ې -b}Axm#Ly28ᯀA _N1ah>*SGDL@Xs[xR1lwf읶=k;.=ǯ|j.b-j;k;-Re(lc|$95w-t=QZb!j9SA4C|2V5;e-pF !r};r1<ގ^wuy#x<~o-Foӣb¾ܚ}n"flnf-S t8n'r.VDfBٹ2,ƀ6$!$r 9+g1xR ӍjZnR?Sea$zݔ}T+>>&.h9 8#qtA?f H#2!m;A,հ!<{6SIJBb+0lǣvݗUvg˙_; }~w훵;=j:7zA G*8SAiցPzYYKc{];`{I?U!!AjI *m7RXh6n11gVb&#bIbA]'7E R3ω>=u!3s?mx6Yh~x:9WC39X0WdM󠑺6;vC39#ŢFܑ;Ӓ-$!L؋ zy+6LE*q0O>&qEqt \obW#Oܙ)~3GΉ5q;U˸MݜQڈ2 -HK7hxm|m>Pi4q.`O{ o턶:Q>F2QAd1W|*l9w0wIZT1z!1g ؔG8 aIˉ[f$Am AI_9KOa j9R#usz`3Nq adC R7+8L?rּ6B7ϭ9J-ԵG,?db5Wc$}'ٛtImLuLnc2@]hP1vQuEyq\byԉ B,XCӳ!5|wݷZ% '/=<_(8:V偢+Ni*p#^:X 4 zm 9(EGi9QWHv5z-^^-eDALxvNUI6͌-j>!~_z(k`7u2CQ]A*AsY,ۋy~LE5c:7i%}s(_ Qݥ|s ܠe{mvMb Fz9iLnM>ԛ+>TpBS1xvl4 яԳvg$y1f 'Nﻤ̣v1׿K_7^\ gd7Ulzg6-~b z)n&/m)[8)1)-򔾨=%i [ydf~2d BQam w0 -WT#VX-o[ш_vqBDvvkBPF<%MZ؍7sMY*.<\#-M\< NJلƾmY8GlOϳth 1HВ&w S4tjfTuYΒWOZÿU*%SwI~4kY#T-Gi\z8IɴY_Ѓnǹ:,jS6ZoeNڏFޭBSMl3$:3q!˳91{_mG~M]`*zu)u|cftOu˥mLHمcgG^ L1ίH`xy nO[풫O,^EJ,Ey*eb)`0E^|t<@y?gi^Q0 -( uai  RgRb6I3ihsSi:f7tG5Iv;-6-e?tt1n*5x~olxqP=;Z9vKٓU V3ݶ²o|Z!ϔh>[N?Y͗ۗ"DS%Lf.8M5?4RTқ]Wބmٟ5yY]{ً[Vp/m 6uy($E.b?a}c@%Ulᅽ^aֽ00C߷^6kp;59}_9 ]t_r:Fk?U;> (+?"7O\SE|Kg]lyfB-a62W{waJfû?ԌU6+]o1U9qF}.+ e 3r)M1 3C -Sٺضd9gVB;hx $};bݥ%_#>wiF>էE?\Camg)shxoga&75ɇy׾lQa~ &VIm· lgagSחsUܧЦ_Jli.ٍ\nHSݩ:BdAf*}xhfn1srBtqгM´Tj j/r4 $n5r=Hz{oUV!ww'xTUq!n4hhwq~ߨ1ҡTR3#}N<8y/bŻ#p׿uݯ7}/w|y|F|W>ᘣp6ac4cYݴqf]7k{׌3Y'dnų -1[\9&cf W_:q/<7./5ܜc\zᕁn\GeB^T>fXrr6kz{^>s3g[꣊p_R4g>/:/wsEgbTw(ڝan^Irz-_o[_nn6HO ^=Zlp/wb^g>E"&ՌᓪFN,]x_/Žg ݟ} Ӟ=_(; yX`~P?)u٣{<<+?WY<~ftyf~Q IօZ{XrI:c5[ܺv#ظr!:NdX!G%ڪ ŎČ -I,Dh%'y矩…)|rF{k($U3j Own|ܐG}oߋ݋4)L:ۙ\: ']{eiG*5޹ 7i6ݩqٸOs@`9KmxK4l4hW>930g8rmpOoV/kDc%iW8Jɫ,Jx&K/׵<\wWOpز.u7rmO7qO7}?^u~P7;~oxpVx{?ׁ? ,&nm<0{͞O܇ ckFe B4׏f0MgU.܊p7w<.<ˆ-!tx~ )&Yue&<w?{~aNCb_c,| je%ڞn G2l􃿹pL\}˵=?n:t͎{=4-,qY7{f]vi\{hvcnޒC';B{3Z֟sZ{@sb\tP>4[ڑbYgh%/6`?ՇgQO~7}Jd__&b~Mx[2;Gzmį;0ܡhcTI*mWI޾ɖ;6k6/ޠٵiKCkt>Ֆѹ#4[mp%h'78I7CqjppE -07's{f-6A |fxth&ld8 sRũ{(/]g|QW|y>if՚=[kKcYIp4:aL%d~SDv/soX0JN 757nۤBjTxn6X|Zvl<⤘44uRY=rbVDҒLiGQvS|P3 Ny<sШJƜ4 [K\G{"줬X/ݩ;^no]/H%B:B7;!ɤa͝~4x^ J "Ts?r;7WVrW'W0r ȑCڑ:\j=l?Yw+:hfFI%3M)Ngrk GZ>V4 [+OҸQfB*]l=n]_!6oI{f8Nrp0>ҍo}Gӌ>+7~q>,]} -&Z8~g]ybfm}Dr'K'wD.g>ېnd\1f c*F!q4Ux`taEΟM7 ͸pEfʘb!s,/D+qRJ~t꣄BB_;Z, nq@Hs1 \.\7ۡ!uN#VQs?]x!1`$4\3=WՋ_3_P#@3*?=_,tJhπ ³:?+w/{uݯ6a@L&6{:y .,pwY- t`&l\d`w}h>,k8{r[ۥ<556 :"s>]l J.i!fZL4]yXKhɈ=0T\w\q~i"}UO_0$8J\Ik Ah7_}A-fU{,b.<}㭥zV>_iy;]4׸N7c -Feu0ӟMl |Z>EiavЊbr0 qpäˡKM%U…ߴQ(0 B)o-jxfĢ3gQWFZB۟;\b@.j.2<;12Gz)V4I&'p{%␉q2)1oZ:x_B[.==zu,4gB"ҌJt!>=_o_X6]K;BbXfR`xS0oĠa!bfD>tO1ut< 5'Qfr3}@_9ZNHHV~fx6۷A7QjuTmiU_Zf'}'xVϙF҆lxo֨Ո/ʟœjx -~̗Z,>vNb+GWŧ5C_o}~͆y5hth WVriC*!A/?g*'<@6G?[c8l8R+/-&‚}!y=|9w'KkՀ;z0i{]E┱<@Le1=lN>0īdM(OYʱ -Fl-eLEJ%rh`UU]YV[].0Đ(kԉo+,o5B-^7|gŞ;A3'CS9L1x{*-cTh -)NBD> - )1K/lM,.gp֗'AW`1zWN"L~phեc1-BRbtLU'=)9.{Qx)7W.e=ssb.\mRF NN`۟m3xm$jB;.4[4炒p\D+WO<;5lXVͼ-:GșBBhCK8‰jF-}3P4}">yAqg7{_SѩbHe˱eR%|rݽuЊG 4h%)7bF$1k2U.^mkzty+XJr=Zy 9S)(P_FҊ>0 - -:bW8s\ ~c#/8P3]9JT-4;pg^{;'h2z*,8B|(褋%7]z)('cv1T`Aq34z zۂ7+ҟ,91G9&Bs~eT5 ZHs/Z0 bڰ{ Q}F.W^bE|B=4Ÿ|YʲCnރ g 'g=A`챘1 -||O.' 9:&v]ӝ·Q󂙅 -g _֛rz+5#t~M脢'B׹,VC׮BwwBZ9^?luZseE|3XP:[>fTwq ӳ{U|i|;/ooJEP,ҷ;x~z_RLjE@j@p=CfA$1b}:;[+ˍsJt(%ydhXj5fؿxZzu1_h `^Pw9z[k]-:CBs/JIn1t9ӉSw`w=w{Mw7Z塾foxs<!( -qY1JA!6Xwj\HAhBYu?ګx81`gʷ'X--b9 Q;0NHS`D9#HSl`@$cWX!qtq f$TrSE93R:0;z)]~-fvyiL wyÆ{W>˧knhbgMI`g R?ם屟\*$Vm 6c .jV"$U9A\xyYھag{5hX%iY|`^K梁-E#Pk#XH\H.$|&hz;kH-U< -:IK.;1+u'rde ,g,CLZjytf|5^w%Ьy4Fژ)0e;JJ3꧊CO2j<3IN CR#lggM;J߰Ӈ3:)WNUY<#\[vL"bZ8Ʃ73_oG]{g-HsL -jطl5xH\{n~m4u_nw̓5Ӄj\zrZ$?Tkk*f'a=\0X쉅즩Z kŅB]cy2{5w#vNR!&zz5,#>Sl !6J`09(e8|4Xħj4MRlwt:0~H>yr}7)(gehޠW --n)Q`|3 rRl[5<FU3bfL⏌,b&7G;KyBUk -'/L,#}-_ Uck]h+,q1f";dabICo_:^- mb!h|>ŧ* 3`K# Ycƣ؉|krk V"b'k<@:E%vD?CjĖk?X\R*/h{h51V>X6t6E)>;b {)@ʝU -yY. "j;׬V-*I `T]_F}T&>Bb$#m#=Yuo1q!,7aO:덧(+/-;r-;Klb#2βpZT̥&6!bgO;+2+;+ ;+abTYގ충9 X`b>&@gu̞jfR!g _qe}l]K%"ێd!Vқ&G?\%&9` /j5"~]g5y`0˩N#"~Ei.?'JW6UA-$2\7Q-/,n'%ZFN%[q}/\d쬤Y6FbgY\^!~ZujOAl<[S=vt鱷|x,KCZaZrŖO7˝v n[ 

jNg/JhܣZzy1j114͖kS6#X-kKYlwWNdˆe'Yq!y߰_sJ%]sʳKܚs[ӋwWJ/,RbXr:ţV&nLQ3~CrÑ լ -0-pĎF{` 28Au"sx.ri,_]}ᚊEbXN 4#vR MJ'=[{LO6D;5L>Y>H cPp|)3DYxy=ȥ (FF]{{b|;K>oe1ߐ,Zeﻸk\=XsԆk'-\FX}_R=n+'RCJD!(J:= cy6z_lZ?\K|'VcQQvyVG'yj41RKFGuU K5$9I_E,qxVy{z]l[0h0B<5(#i!y -]쎩`nȅ31cB}D+QrLaĀaT[ bGڋI#S.=g&߈"q` Ả%w| -4RTՐ\> Xf5} /*rٵ<>t/.FOCj(v?!DZ굲WĨɮD/pސE䏠~¿Ć;nm"^[|\hw}PbR8ҔYcxU1l# k61Y= @ 8CڋK똍mPB= ; W=bkTd|mG<;wFÞZ~m)L)}Ih>2<z` ^3|>Kص/陃nRpp\nTR錡˩a 6Cz4:Q# C~%%X=9L̈R3 |+_"&Qō׀99Z(18 --\OebE#F!6!e+Ezjzۘ%kƞ1Ä]_[!oƻܒ\C%{7Ǖ2 kK{9$F<k$֍Ź7s_+JמbMAWoZ?ߪ #9,磜/PrOK0yo[%;YOׂXj5㐳ag/{¹{]o>-P" QV8^88Ea+DL;6L' A\YNh.ǜT=3gR~zz؃DΨ9hHxmkџQק_\ %׏ř1"VIǒ~I -XQv_A`m v|lGd 2j/A>Bj0 f;/'Og.sC9|Nj|ۣMyJ9%UB~=ҁzb}%:q|]~EzJIb֘o2яM9r4#?I.'[ -PK[L81ř2?e*6Nƫ+Ď϶J>a)qX,+sZ>^O5'cI|͙\ǧr?XYT1<|L1"F)C|8!)k&V6G>C.Mk_RSJb9)c5yFK{k5:N< ]\wIoJ)6d`LkuLY92VEܢGZ:XMYAWLX5.\3#O#X賳5$k'ȋ`Z%49Z>'~(OT$o,mnGNxeاK+sBv6]ˋ\bvDžDE&;oė9]KYbM]\ٿ3ط8/f?|˴~Mՙ}36$8$}QhM~ɺ V,]bWZzuKW-__/%//9d/G{opY;ǘŞd ~p~ٮ$wg^q^Lo?^:W-_vyڕoO`?IZ:̡W[ykً\4]lAsOg>wYgVhCf,k92Aڀ+/5|f}=ƃ30Ƙ{].ޚ}{4@22"rm)VZσ>,ôI>ݼ w4ZlQ߿ߗ~[ -1ヒ!ebH "cv4cԕdzs0L-Gc8zqLC~t`?c`<'i˪129"Ö$(g%4FDIxI4 ~#HAX!I"Dgؑ4B`#żьS!Gi! -#c!$ n!V8 oǒ $tKBBU5DㇱbWWKO.0dwO(b&[AZ{l߃P\#I(}70Cl$9qUcZ'!8{!Rt8>dMh&>> L8>O!fKG2CF2̞Af^Z?3w/U%Fps<_`^Qaggה?0wگx)d]B[.V0 -ݞ/荡ti0>-cv@9Vdk9rW=Z(́h6Ys i^_3Hu@H`)-$k545B/Y Վqs={5+I5NjFdCN3 Ng$9R'؏hÇ$Z-@"jCfDlGLHIӘIe:l'n1&dgH*sdI2udw -򃐁}B,H+ ˲c3G`Ҙql -|<%(Æ$NȕT$g -[CFqb`$$f E12o(=HԐxti'Fe19"И^r8sT6cQ4IE1y#tv/=qḡ"mi$ѐ>]/%&QFxAVRzd)"K4߳ݍ=֛!R+G[0v/,9Cxwф1FZdL`"[f^QU>ȵ0~#D~`-2{YH6:Cd)0AO(FLՐH Kh Ok&Ռ5Ė;vjX FQ)$:XcUJQ1Hf`T[Bc@ y -18 -n]Ըj -55z=$?Jo2!فaf_agwO[ 0S,oC^6&Yak #R4^sH/`P1rCՁWY?]{5n`l&8ǖ󍱄<ɠK2Ga)HPbG7BjT MHZ9 c-9'44<ӆ|Xb  Ҩ4ojڱcZ;S!)Q/,oRKNχ/2$8B -K -[!a4UM-.-'׎ ɐA0cgs1hcXCo$^ !~$𞰖0_\L#wWa csB>CR9YmS!S1#,fwW-͆4hh!c*'94ÅӓcGbT|*m;Duq?s0&sh4bA aq!4Ot0J_s/],d 9 |Pcd]4d2'1̱\ L"G@zJ:raRtq>ɾYI̗h([> ?>ZȠ$-cM .cFÓ(T#hLw8#X,d9avgHð&6C.IQ)' pIB%qɵH)`P:䏘mGQe6@XiSŎj^3wiWKLbl+ -9N$S3;.4ј"ls! *|bCc I#E鞊1SdHCr$i|9̾CVͷ'$FщY`8$r =9:df*I2_˳B3 A#l1L ʷvx >K5mdW2xaI6P*o.jFZo{mK75pՅ4U LPlyEtQ੅PR)UȵWa4]A CF`e<,~d _#KŐyJa>B5%ȃho$&hc&l}l -˅l;|Wl(\K>Ƌc,~f %foZht_JQgA#//,62UkWGbV>H QHy@SH#_LH'fB^ebR:KUWcXe/8s8^dm0 -AՂYRt|*d7h<׶~j-lAV2[dE 3-*j[0ײK$f BHxkɅ@FY~L##VbF`ĬI3:&Yst bL=Zc9rRYMfd;(urH(q5cX@|TYA~YvrjI9tt;$ņb"\㍐kn,;|@A3|jH!b54F>', ߢ 9,,Sl%uh-XARnK\N -Mz f\P@֜<7dX;X;(Ll⭰HJMXnQdVC[&|b֣t>qG+$!dkc``Q@G:ctKj $xmpsPsC}gg!&\F) RlH'efE[b,).Ʌ$[C2ZVd-ћ"xxK/J`z '$t)$}8$ V{< gi}eVO:mqӟun&Sc-% 7R+JG. -=R` 1#7άCǧ@FИrjT-O5d  S o5EYd 9NYn )q*ǒe1  `ggK5W$\0~'|"YPv.dd@מy|'5-p Ŕ8RQ x 2ڧ4q B>?j H5pc$ւzY\p}&[~Vִ~^ -2²m%SȐqDql- 4p(c8joVx|4޲JYC::9%|R-i1%|',)Q/2}Ȩ;X|(!#H|fd5.$Sqgc?_j`5Qyꌄ1oB;Ÿ⑈E|ېHA -{rzJe'cvtߐ -f05[AiɇBT9BPS:Uk<=C5j!$Q;咞Y|->J,~&?5PJ {_AxOK5wWBP+X > X"OIқ& ?B҆Cv\hb|+I1Yh7G- =.w<)w\Iŕ)KIK@:S~>+Kţukˀ]pכufWXS`u?Iȇ&Gdhm2G)Afu dѷCd?5$ A$Sn-GW_F0daqc*G}ͥL9(% s8lC Wfvߙ^7A-=g@rQH$.o$OZ?Yǝ/_ ca^j9FkEXOP~)GȩOIE_R2z}襰|!Bob*cLrC,e~j}Ce@1ɓ$QL5B>bj#_ GA6egL䦫-rON횄\ qB*Hnxz1o⢜:Qlxg96໤bB#b^a`cwW_:TjuHא2EBFΗXNbBB zv1ʿ !DysV4_H"{%ᴚ@B(ѩvX;_3!!'.rNjxG ߏ/w$W[X]yz$!w\fn] "'5+8=טlwm"!c;o[>D2xlxgD8gj:7pdgaF:rnIU^" 9Q#?ʅÿ=θIk!)XaoGݕﬠ׆CÙRo_$5fCOg%'B/Fa}`.m9|H0`!p@֮<Ѝ'( '@M5}Q,[lVo,%fB.\t=/ OCҋ{] g ߀+O{BEh'%7s&Ր<0 =,=7@*ug GA6#L.Cq\NkNN[c@D1evi*rbk%BP>tB΄z {TD[l!#7N籫4 Bo A2r,{ !pEB䛐,ҁٍ1@Xk/tNGHlRR?ow[A|N{?aV1,a8 +ȃ=:$MQ =<547CèrEC46kvl٧BV]P^ME {X=ȁIZ+[GQ $7PBׅd=^ԟŕ5@b -9p3|I#HPMw5ҡF7䡤ߴiEppJQnQ}f9Jݵ3!{ - $7Z=Q[JWWH_A~{(dKf+=o=p\BB_l $qGDL P.l -!!rMH_oւoS=a{lXI=GL=:^-<7Ov JީYrFda=Q kK8>[0EԍE]juԿ@x"CM Jn* d>!/+4|FEAƝԪ!{w{j\vոxJS| r/GK?cMDյ8[K{W?\FJ'Ylȶ8sHߺb "w$Dp!!.W25[O<Ξ\Ӑ4ـPצ `X3q梠o6|O Θ;)jQLHn,O_-E6}`tZq$ѻ 3-;uoٽABjg7e'?^vxp6xC􇒭|7HfeS.5Nb\sf\wkZvnX젳=^ ml씺܎޳PJ([˵}hX|ePuk1K NENG9!?0[jYj{5zlף\MnW:q7\|g8R*o.R \l tіޢ7S "z &^XOG ?Y i|HU~R,G@߄`5[j8rM6eyz$0bAtDTp(w"r QR[Pg!]΄_cu*I -K]'>z=`{ݔ?v=?ƹ`sk߇4zJBG+p`;'RQxj${QwhO䂵 TY*&w=jMK|rR_턽|m~‹jYW#3쌈#@7Vйb%~ -!cgpEReWrf`O/aذGv/qV >yjLHʴQX}jRsc$2կWH+Q_}c3&&A/ -}|jTǑG[n./}?SxT&±B`iov)t>&5|V9raR88OU`O{YJBӁ/G=obr|D-^lldXx~pЄoci$՜["1P84ߛ*մsŶϷ(w _`N߱vO]9LZc1[$yS ?ADӳpF@)q<孥rFdj3^P gguYƊ+KAo>D+Sa{CQଂLʁoPa,J#D 32,!rԌh zƩj٩8+ሜ S}߷Bd1;>؈ $ɷ&*>=84*WuSn RVu‘3scq&„#bO; Q;A؝7B*Ȅ,8pCtqvk]/| YJ氚a0.$ - }TԔӎc r] p&c{~bp*}c!| 1@wNb etΠ\jrFQyo<,2NeD>{nE/<7*,"tI.΁gXE} -[YccbBE?`&g|/壇^%$u'HEf P^B =O;cP=(\UN͝.~ҹ?ԛ/ =Q)*WjtOqcDtv]3 :'WZ.u=i<u*͟l#=n6K|.<Gr`pw6:r}11;SQJuE <KpK=(\V?Mr -y'sWgz<]uvooOO߂u_xtJW#}K87=Z6t<'v|&WbvoS^fR~s)jE߼ꉾ};C&`q3bh%KgSу^ mƂ.~,?lv|,XX=LPE {+;ω+k]^:=-=!)yd΀>:7*h;l\.P:̨.-9E$A 5}I^tߔ.ou$$ D9Zx.]Ev2E;P"ydQv(>ٯ%!LvCm1ÍpeI9ї-- ';u3A<&,i7뭒z"i(&|˚/H_tHwTY*r2TvJ|^|v -P1<~ZCktN!jvz)7nm -•]N_\f&zxTNX苐x:,uģ!~mI:J}& &+O϶xoW_h9Yg/骼,.V?B"wTK:]t^A=SZ(2֣Etrj#>:F }&cN!jN?2o~5 ->S0[=CR5UoOU!N5Β=#bAbbP  {4͊l8GGG[„=[>;=7x'}" -P٨.Ci(>*~Tkх=ɻjG/%ɍ:d[u >ؓB0ap ^Q43[wTٴۢYQEq6(ۄU:qև9N¤]ogOPhE(CK_ %:tT̕lϊ MpV>x-f2@G-hqiO^Z skbY#꒗fUG8O,.ou2( WoDeg$%Β:wE$EEW=6ě^ fh5vŵ35ҷW|?a+?Od谨9@W!^/[C. *0*~|%>|tG[%"m&?5NA.~3z۸&zbcA^ZXc-)h1ޯ4-3'u'س>za#zrRIq%V{8'a^uڈkj\E^Ҷ}%1G?u^O={?zUh>PQ lp"^Q坧z/1V/uJoJ:לИw5=XgnHQ9:'l8hG|&REymA1 ߆b"^ǝd]E2`gV=6aws}FC|)e) y%&K˞qv]εfgvǻr%U#k:6J]Ck -ػqh}]$õWqdˈ3c~#$~3{pV!,|#?n7RYddM9a÷#d_'qJW_uWrFpP1ߏu\K̬IHM -iz=#զ~KMsg+D3Īa֌䣝qC?Ev8?K>\GElC+o{w`ך.* _+~igZS# s~Ɣmm7aOG1IcR=OT>4V875y'4fJ:.M]g͇Ke-/G_6G߬퍽TRRs9|sBj`>I>1g[6HtAJV6kydu'ɪIѮڲ]Zen1a1תdwb"+[K.D׸DU48E;ETŸk**\T\ LoJ0~!i{"Ú8%xw ȇ"ѣ :#-%{A2FvP$ÍOՑȀ;H:\e6X+#;z`>` ǥm$ufe> !͗3϶\}', p p߭x玘zunF׭NcQsdQs:טB82k㲫ҫ}jCކs+UFhFD{}w*|ZJBͻbD[O sG+CZ6Q]W_,% !5L]y y0dPDkM8y+l¢x¸ mq)^ 9۶ ٷ-4ů981Kv-n18r,L|VWP!K=ڝ+BwH.oN'qu+Юオ}#"BS/7=SXU`rbI@|v_[S,ȚiG#cXvw2#BC;o]-Z/Ug}#qBǫZ1^1w\cJZ2[${[3OfObCj/'$W^+u?՜{9#h{L:.ܲL2!*i b!k˶m9'sS(H|k5,}ՙ} ;s;r̤9f>zPr^wsLiSEkƐm'"s2=m'mI"|Xo^bdobROM-ɯY^h3!6fHQ+ssVO}Ǿ|0`Fmx]+qYwSc3b/^IК) [nQSd%t˕4Ϻ )7}qjN>yt:ڲ8ަ0=-3)(ӫ!:xWN[Eǝo:ڦA]WnC{bbBOb@Sp -=|s72cxotQdle /aj|d0n*Cbڮ܈!G O0Z{--sʅ>9b1# 1'. (ǢWп=kuy)r8l,UY`>Ps>_;n.4w}Ga~>sofisl``Ftvfkf`1s{Ɗ~ެT/{2跕.QOKcҪ}+/ՆFH͎,bBtx2РkkTBCp8C>- W/z7 L3;0tAakдRo /^Z0L$0LT*+UMLA('i$.icty__g`uW p+b]hRج~|e/^+{\!{S}'cw7ŞљU>q5IL}._΄Ɉ;١FYn^-jb~jm~J^?oڶV,Z )` "{WRw9 ϊIo?)M׀[E`,* %> ſ /X|).?[ػE>/zy;;e^%+]/׆$S_{́/;^p笆v XxVl+L:S97ɳAz`2+7>Ncf׃;Y7F裬C!:'76'%Q~5aO"“m>zSP)K,=֕KBT\xtg 7Pܛœf6c OJi񋀊 -0Uq9 L̞֪yG(ie6Qo=`"ńXxӑaۮȠ} -}e #נ*en17agrV|%xdPTǭlsgsTvՙ,``M@yFOfM7K,jv*gVs{[ۡvuW#Qnvl -}|_.,:P}e+{#-#]Ω -o_~)hT銪pQS~%L@ez2~1i+=X~:`+.Δ? cIH|{Ť"Ībw)Şy%-} R8z\LXIn_~ gqipUplgmsVhx`2TiKU$` wLVlx",[]}\%Y\B /0+/wm|#Qfs,\~ٶ0Nۅ83(spL3g9V97y$``vx֊8qLUm0w&~<8Yb-[^ My[/oJ[ߔ>Tx$Ը% 7$46Tx#Y3tSmϘ&wYcyW9U7oo3Tm _&K7ف5`]=} ՚eNGᦼ@>X;S}[lWG2sݍY]u]ͮ+=`-XN߶͟mx,?(+ - +8f(,-SNe`m[9;}j@䀹[`;X kdO[,uZ%,;X.::]hL9̽`{=1===-g̾9S|CE -@uئg'È={± `ެ`R ,v <xZ^`3X},v1`=S՟0kǙPߢg/=Ooz]TjsɉŰNhre!E_]Tì3gӿ\ 0z)X6q1ˀRAڦ -ϐ۷̞ L\}#<f@,`oKx l-+qRFv'#_~'y VQ-CsFvzԴ -DN1x8Z\p{PXTnbJuAC0­p3 } -[nTj%:@uX9PXŽ{9j5VfmZ.G'}gkKCosOmpKlrK_S֒d wҸ)?cxXXCY}5ٻτ εsys`R.XKڂ`0l:@~Oո->f1W٪p]L&..)'NP݂10TwIcSTT:yڮm<"/9ͲP q5u=-6`XXK (m9%,YO%E`i -6`g -[#{&~fԴ>0QƆ;ߊkʉcךRbڛˉ'Rbc2.D 8G& 1Skưi@ٜ"r/g(-9|8U:S69K 25zx rh"gjO=fVOۜA}xUMsA50k52&ӼOIKIУӭ2Tϡ嚏QZ=5-v3# Y|m3#Dy``:CAX;U6Q` l *M(nr#jŽI;'p/өh^ɚ ~2YZ*FFװu1"62bP΂ڕ sgڨ.ƚ4$7hw{(lՁ.1C~}*̇gYjXy -lz"$ێ]W0e@%Zff-}!Mw(lX=vוxUN9ءuV˜:P_J| Z/bQlj^Dj<#?8iP0=nN=lNJmo;N8%8Dazi\+X3v+u?Xwl<` m`j~' vb!r>Bͬ/թc AæfsEoN?\ ;J/} o0ۍ+!5e$F}|o*N;hq3a폣Too]DjMx%KRUm0 Ser0oNb تkva~`OU]:]V1gDzaָ?ڜ83Gu3\ ;#1X3з bڄ1*f U'<gX׍=w϶f뾓b8'yYvK|>NnS-TVn<eZiwP*zWcmk1{5mY~gLb̫>,F4@y}l׫btm99%U>CFczL^cn` >Jt#ժND"r2_|"j:`!`KZ"pQ5 ~+̛m*< XSW;h4M~X ->łs>1ֆ-42c^3c)a\Lz*`x <킚߭/GƉ3c'/p1?qYx4J *`/c/wwLDJŒW!^s*m] }|F -Wr+r>l*-S:zXl2vRuz[_U9BU.s}U׌VrÍdprW84xSpAܓGg΍zO1{̨1Z7ł|X8) v>lEnM_Q/,JGܑߴ-Gm 苅 FЙ܉KVd)Ø~졂4_N=T˨~d&FÌ)bo>Q_#/M߭ v8gLZK=Oud:%[ >?xskǍo?bԹ'g+/Jޣzn7d">映"0=`h"IB}Dr>7MQ1OxD$eӱKlgc.]*i -0V9/ZA_W._ 1?jÏhr^&&KFO[}%ԃ1v!m!߻ɕyOK.?4&mruCaBGl?}8(k%X4p6,h{K!l$} ?XΚ,\ڃǬ`X_Akt# eмY+=-0 -382;c%_q -yc{ѱf.ĢMG= lT!#c K1%Kh}:d˭DffEƮvzAp^ٓSEM؋Wx7kt''uh׃YbORR s"JN#AU٠9[g񽥢{6v,%&! x1ss -^\2g *sl|Dd=-} x\^(5'| nݎ'<&Ly6E4{]%w7 iry$090Z/c I)h tv5m@ؽB,;'wSl+z}7Xb,j"`y*QveNKD{0[M"a?tG-I~7!U'c rt4m+ 4y/ ~0_N0~![|4u3NnYO_z'(o=VcOV$`umĖ -V6g=T,NyNCҏͰ|,.?b,8qO&L w#~|B*vu<3Sٲ1bjvެf=`txwC{n}uf[f012͌Q)cy1%chtI1K{ -`E@s.`q ysbh"/·89}^Y@VJ܃Uɓ>HŒ}Xz"b3i7G}eq^❥er+ImRZZB27碕!W>6EO:nOkNu,s6w}f#s]ǯe,9^9 M1l6ڣ4B{.óԅ<'RqHOMF{ȫM7>\ň!Ҧr}~9\+\H2[8hʕ<:,z*6YP׆ ´d֨6STx7~V^,ZauoF&`Ƶ0f,8=97c6F>Jjj\˓!vإ $@W]tEC@_\ -]d.7[I|=T^yAbAg-{,BaGul "82IxuIQY``\HQE7b>)sqeZ&׿$Un^SskR7IH_S]0$vn&/:FNr;]+Vw#8p[qh/bIU~ kio -!},+y=rϘG&C<Ca-m]Ig>7-AG5AMAt:  By]OmEJ.>Lzy/.͉Zo@Uc SiL -]Aӕ)y"?*WοM@ #C4aN7T13W:=Fgᙕ¬=Āޮ ٤_" 폧._Y<^hK>z-͢GtmC<_O\~:A<7?bxh*-r86xg7t隊vBPxVrB8e)jt - M ־C@k~`le9\&9✺4Md}\ mE/SwKr{ -}m3LIXtH7.i0-auRǠtY :$OڨMO9B&h|6IK~7\g -OLDbx"C[>X  +0a\ xR^2]qψ :ho\ڪk8)W|| -~>l:~A6C | ~auaOJ<ͨ؃a鰈WM1e.3;oṟ%OVW/%1n庂`rn"8o{a1&1cs weذS{ω|sy+蓎Y([IEqY6ʯLҗ"nB{$nk䯡N*4 Y!8?M=}xPQ j ->SwpՎHG84.QO7b)M}A=vYM\A4!u -{ɷ>Ľoq\tԹ8^p칈xwDOGۍh -7bHŽ{NM"2a<Y짏 +\U#25=\<_mh0m0~:jYt7|X*2z~?>tTIDUœ &fb!wVbVeY{y7 u9`۪`Ì`M`СC@߈c -pz<< tVFA_mZD?`~\Tf!<]11{DprzGrFl YFy&EL,FPIBv[\,6E Ó>#W RJM]V[C_Ve2+gģOx1)cl6-\ jFo -҈Dlx%i:Hw q/.X=L9h?AWV}0,$eoaZ>!Q|JIevO{\6y -b_lƣn$  -8DA?E twey"v,p mz3g%CG8=}Xo܉ 1a^82?8wؑ߭e”=G{JL%jeIM`DP_h ڛo-_5iӄ>9yÚ(߁)X`çКWl%rZ+3wYsM̜5C]GR^h kK{IӊHeX9*N13'q]ѳćƙv/%KI 1pU<0w)rhP's; >W*jijZ5D7ĝ6 wP&xh;B -r[K9HϘXݵ[bah*p9(cCjxlvaGKT448`@@:HK 0,$;ET.$ 69ݺXVoe2KX(=Lj . aq0 1Ckm%4BXtH8S^ga}AB_AfQ3Y§Ĺ I)8H醰:S ǃI_j:'tCxHi3!71\FVn(@5bF!|c5y1}1b=[{iNGQ]Af>4gÌB3='Q> Pނ]?DZtwM`-j2D^[l149| .<8FxcmuU%> LhY:(Ѿ) Gq^' lt )`LXC銛VD,g_[ƲT㥼t` -::tgr )rGW|ዳw˭}GǚF%]̻"^9*߮'Ѿ| =(50| k0*QqZ<nktb)IFHXrRgqVѐk % - UӤC,;DĊB%O:E:Eq[rZjqx3!zQ0'~~K,C {"6*&bk978j E\عigL$s Ro9q𰣒1B - N2 XG `q4P>S *ˈڅtP -` Ⱥnˌr8!j>X-Xjʻ8৽' l0?ucJaJn1~Wd'oBBHXˑ6cQ !SyvʎyfbvTld.@1( pSJH)hϨ9H sÊ۹Gd<<V.csӉ.c1SD!V{*xu97ҨGdTS̻{f'_oZ<s3'ӛ5R z7Q,[%'>=T#+af}Q1Щh -wIj#~#gnf V{}Xj`. sH-!&7O#~)bgay6 -@mcvHn6Ғzo=.K^_1wOL0:+ظ\gcG*΢}He> ӫK8Ehq\9HY* -[tɜA"oАm,)r6`ycJSO6-]tИ4&"}e5O!R|=F*CҕdI2H Q:\Xg@|c+{s=XKƖ> $Y\ZJ!'¹Q䓴P9WU(ӤS+mbs2WX41dΰ!7h-:)AظKW#ĥC2f2;>H/RgмGkN-Df8gSOj=H(Xݫֈ{ <'m, pg~|1o˷J ZM񈢍#a&ZրHwKCDLH"hw|򰼑#m/n_]I .&rMC63Ȩ., WĻ>=iQjga0/?lbj By.>eIQ*u_x(ֲ0".>0~KҠc\g JTgOLΕnD_g6Doq06Wj6g=&#m -Vf/Dgd{igX;sO m!y1j6"E蜨)kױH_^pAaJoAၹ+a9먘EiuڈKeu"#ڪc˝ǢgH ---_y5q[kuCwm̮+'^@k|suLüuIV9 -圬^1Eby؊X6Sc.WΎA96EڍY,طig#,{M{GX {jg'al|HpJBSBeR -m(eV1vMlT"gBLo{rF:[0NiH5rrj:h7XAyZ=,L'&Ҷ?YܬPR^34w؝ YGc{.j|HyNBQ⓮jgE/ s 刴Ez&cN 酡8$;?OH;e:NꖾPb'3{4Pt t,%^/qw"kߑ/wx8~~-sbV#&OB[=qrJӴJ:[7ew;߹͚;/}44n)E^ًϏAk£R”QGYB etTn.sQC~Յ1Lh'tgcO64/]KdBDh}Q-tιB`jg (%hD.G -8|_lQ|X;v/u/>7q|4f=b抽 It=w i|sA(o\bٓ]DֲmIu:VXܔC2&7R4PNE=&FXm,%hZ@<jN:$ -p'7,YTy-=\N󠝥e(jƃjSVS,%֣g,?va--%ہy\8&rO4c@={+G-bt>L%=<Ǖ%r/ET4E1[  e%?@Pne -@FϾ -k-E\Arrۀ>xPm|F t ' -hsn1e 6簇1R|4hR\IC|.e4V¾-T; ,E;˳jg#]\$b!CokJRY-wQ}ke|SKCW@3G {8_!ԯgȻ94)uKSK*k2ԗ[8R9'8>f0Or},jg5$TtͰp  cn-wvKJ1RґRhXCC3ŌˋqgARt b㨛(C z9./1Qzx-b=fHAcEV^߂=wq)WdktZ!iGKFP gPu --&aErY{EqN~,8[M6MN4pǘol|{+]!U |2rlTĈ cb{@_|#zb\ejU{~HBS}Kc$kTg5Oa=%U_R⩹"UfpҰ*CS/,pDw-`z, :N}zu{"U]pgRۄ?ESG`|MXʛ OҙA?N@GLvFƚ>Ƅu u)6|Enhg)gť>z"T6\,9_NzVrrMzޣ%d[1[IW卧X}h9 i亣K"˾ZUR|﩮-\vE U~qŵ҆佒Oτt>$şڹh ɇg!z.mP}'c\#,!OBI&֌Bo5=ttP%'=#k-Lo.Cf$_f 4P7 QSqB'Y9=&{E5<8j[zxmxL:>˹51|"49-|ŰqЬ}.S DNI5 -$n xEqz>:~tSNBMC:,)׫5^2sը j|A 8%| 'vyu!B igˊ!yB+MI>2'jgEm#s* -XMy߲oa4ubT>l.Rbc̫КUx E|mН"sK"|YJHBq䅡v9a]J8;,yla}ݙ n6Gک*ؽukH> _$'cIr}uQGМ9r'CBLM5CAw#q2%a|؜;Q d Rjyc'A -&Q99M9@rYԢ8{gmhm~rI'g9+{AGkbѩTB`tPF/vu֬!j3СVa փ-@  ,.Bž򼻋f1Ƥ3灥Dd} /ңkBĿ+-gu&+ -+EaT&2﹤8I޲xœ'P޿{ϸMTn]XS__ĕ^Op3˥XD}.0+ޗ2{ ' q+EVçҵik[-佈xU{2P NgSܲG/{44UpMXc{l&Vd.2G1tP (z X$F#ܔQW>#F#'E{gĞ% K_ Nвw qy14~ 3y6]G$ 2 y]romrr Vs@K%qF=J S~j})T=k~חb m;jȷXQ'ϿOk=#)RO_.ϻDsszQ9g~\1T{HB' -$GAP6|bGf&I5ko/Л|p;{ aGu>3|M 3 9.;p[yb~Զ1MV-;K@O lI{'aŢW.rgoH?9WF8WSxL]h:S=aW ̧tnQN>7꼢fBp(8zA8sX{F'EA-d3/8=uR=1BuɽҵR&V<5BE6F{` -^G Ov6)f&c+tA_#{  .ƥ㳩&8f>d"ӸI 3gs^#aKOTcM:;|aaWu*OeF;}80GhFM_z|=_z|=_z|=_z|=_z|=?wl[gZۤzbK]::sW{Z`ur<mo^\oko[y,7m,NכE^:o΂Ezsm򤅍mK K-~yK}?o9,Z^p΢K9xz1}{7qYgCLg޴z#cֻyr-4.zi%u&n$_ͣ8oƑH((,&M ĀLM3%4"2Fj$ck!h(l=%?s- gAX:D;Dm) YMQ  ùClܵ fF޽I])C92Uf]Z6鰕Hp [݋UbzK.AEϨ!i _.6sRGHc!z iPIIJ7;p[߱"7sIkĴ Icë%(JX%![ȟIrQzx Pzŏ%:dNm+FaЦNA9hUdC+lhЏgE34BpncɈؾ~G/]dQCA;^ 60[S8b#Ж)4(W|QKa M݆&2ƌۦf |ؘKM̘[h [as>׈ Ra<]I@XyFF.9kh/rJJb/% >Br#|0|#HT9G~hK&!%8$Irǂp¼__J'6/"+&p2n_\P9chs#vCJ@pw.%"NmGAl CYchs9)JŠИL4|ż&1o@,#RoqAG2Cri&lͷqGZ\i  ,>62l<hic(5bd2=dmė{Ak8|?m9p*e l'qڀ݉($#w 2^9F1%kN;_BP/H|AկCŹtn7;`!62.\Jxfmtcȧ_Ļ$ 21v}u㳅sN]Dw A%q d\=I"QFo2|g] N/EB8ئZ tMdhs-o3h8qO0 ;/4BHRD$-n|*>6cIr-HY~Ͱۣ _X!O3j$3NÜr|% -7]4I%zM(ß 1gft6A&=S чs3A䎆`v8\P7x(@ M y@FϹG@\ LJ - !qWpj$V >J$ߠ;#(IEqj;1r(R/=R=j[[>Ch,`~Hd@G,//s[+麴I$$opO)a HMf/9,D~GEULIG"~\ qh6Om3JA>m$gtBȑx` H\VnNs=\O'3 -0;$։[ -!w&(3Me$WQXi\sc-;k$qqh L|b2ĸ30An{īNvnd{x3n7|AhNB^M;(<{"_wh4!}Yil's֣/i@UXħ\{& oF {@MBrq HT!S|b0 nǜɵ F j+l #T]dApqze,Pl'>hj2zf@I#Sk:()%q |M rw@|{b{k.Nkn `+Qrۇ`a h<%&$0GuWC鏹ۤ> >y!qYyk#`n٭ ex;M)?6O| - 3 %sH}$6#9%c&hPL<|خ_ob"_\ v>1eV;{9F|,$E:DyQ_k)K3gFܱ#lQdIQ\#u?LJlW9U]l*\w{>5 -NQk!#w%U6R>"?w4!le֟(Y,rE=! .v_tPܴV([ "2FWB!/D D\K^=C?=O8˺XƠ02Į0)9㞾"rN?ԍMb Hu{[퍜dU W=3(/& Bz$!ul4y~Gq[`_&+C;#i$Ԩzh)hQ @v֣,S∢o}2Ph,|O 34Q8p%<'~s`њ>jHzd}Se@"-Z -$_jJOS}G)J$͡b@hxd*)yuנ"+Mkll"(A rf7uM=CQ_D9?1B4v9|Kx!N gf%/VqY sa". -~cs+!LG۝ELFrx޷ք1{PBIF޳Hax_.C[S-*E՛ S삷 Z br:ag0-wF!?ڔD쐒s03;RQ -pB )qRA]=r,% /Ty:͛k 5|5 g OςOT[ʥ^%æÇ(HmC )#ʿ>w +ȹ9\܉[ 4Ԯ$=|@q$ Bnl -+I)Lrׂ5bӵAƀH&q +.&%17o 'ٔƼsş:0ST4Éy [!<1XO --@I <@p)B.,>Bm$W #Dآ?|}12E|9oB 8s'_| }F endstream endobj 28 0 obj <>stream -vbw3$_[ZwLe<+`%nB]T '69Wr2*v@6ݴV+9@:0GJ".3PO" $-.fiLui" yC;lNIs5Ki@Bqs"PQTy]LyJk*[*v!}ߚIl"gXk/bQ:'~ v!ܡB*+z.ĂBqZ\C9; @r/U(It\)9^)w @ k ,UX-|l؆/f]׭@ 6q Wu"k?{lM,4 ֳyjB[`A.KV.ifvۤy5>Q@<ĺ k.=.etJ:@yw_꟢+_bJרA~ؚ|кG -~ -B-$֣Lݽɷ*3?.J׊7~viQA/i$=.0R?]]G*ݨIdۗM !%;RA2P7djwqG +/}-Zͦ_OQ^<=Ct+Q?1 -9=<ȩ!2/+ kE.خibjo1eHޯ<ȇ)/BuFYܐY&ⰨZ8@,WĨa  LIsz!j!H - 37Vty̩؅f[ja %\]Co輸״>ݫ/ƛkЦ_ܴqDg ?@a!-2/󮯠~Ne\\e_[ H)|}~Ajh^uTJ_lm/),oRVXH:M؊ '|ܡ)T{]- -cUO@D -~G|*v#v!؅۟bY+-I.քIgjs- 5CGh5J<:+\{m(ui1$!p,[յBr47%#*1I#!N -dW3b\Xq.`1o%jR>%͍Mk7271&#]dK.QòP@Xɽ7'./(ݳ7Ori\TZ˸X"}F?!mG~of*vzzlF /b*ő9 v Em͂' ^(F{>˕ չ'u$%ǞUW@suqs*T;D1S k^j7b|!E}m~JeuPRϾ~jri.~<7œWb3E}XQl#[O uD2)%)Q2!,!")LH{{XVEkڵlU_Ԟٌ[ v#B;yaSKJ89b\)tcxBᕓ|a* "5AWJ,kȦݞۡkB Z*rtI$iRX_<׸՘ l9w(#DDׁw& 8h8YLH@l=bh$Mȉ1M{&Aػ;Ufh1B~ǥ#/oI^p){X==BKg*DuuE -HQ -B}&tEiXPG fA\whӇ}ػsT_:yj*_a0ಘj}3̇@ &y_:*rJ~9{ օOPX{b͏qc:{{[KabDuYh#Oxܑ`*=!fpP>f0t֢8lCJtOKz/䭨J3(K(^+g@$؇|fpz6kXcbBE?r" a͔^ +z(Z3l4<*z:ֹ"&Q5 S'@~a /o[ qJaw05pΙ!h+ NACr. j -O +QWL )~2,ɧf#g0UkQ "Maj=G˄ki] CMqniX+7"=f 1~Y/`5 an/b_o5-v% U8н)b]96M/KEg|3GVD>] -H}#t+}&M?~w -;Fݣ{QPGY:쩷qڒj>!>tQI4/ ɉ33S7b_R|%dOLP&oa/|ՅKbN) 46W&uGԌ#K'PD♢Olt5jDܰ/z~u!byPaOXGb`lهSL  !Nce&YEJ.;CYq -I#4.*;כE-+ݧEQ$f-:*ɏ-F #ŝɧKk>b([VN"U9Ц4/(^.}V.B+kW4:8-{Fh3;7 9*KнA̒өBy|iZ -:qkyܺ\̻ -/{P{RQx3bʕؗ(~F4$؞z~cȕ[ϕ<_u< D\yJbem?ski]O9?SQٹ+n_KΤ#3h-=>~ _ClŹl%kmkʶMbuPٲq a'kEY*a1C;BnQ'Y7p^[^tHS8y*7Yd6wV+KNp"c~e$?a'#űO&*[^+73/G54[{9j~5X~g^v߬W~gRm.ߡ<ޠqUo6 h\!HqR8Zɗ_`_|V-3^Z;ުunז*H~Pofv1_ڀ??b]|쉩f H͸E!J_ -7_;J5;k-XG.[>W.|W^VlpLv["2P3:2U{,>`ky -&[޹-TtmduK۬īOW[\dE~{J]ʑVŞ{ ;m[m;ݹoݸ?;(nVq7Kٵ_ܣWn統qb# k[q0],SδY'[xkPDuI8G_cYTp鹵㟌#ӝsT_mG];\.ŕwؤVT|!{'N 쭶m\s- cKͼNn˗Vqf6P˰I}9W?m~ -;u{_ݣ,xK97G3]_ZTe,ߖ%7:'UwX N͇~ҕ&g6Q4XeۍX7LD7^ߴTvSyo6۟/?^ [_pEHjSU]˳3En:][SyV7IζIr?ݸkmeUXfuQ|p={ꣂJz[u$KvN@[vO'n'nVZ{Vj3z孇w,*^-H8o״ؿ7m^|z(U1U|+k^&^Yvk/Q}u:\}{⶗ײ>g((+zqa -MVv)/t9+e<9˿qgv]w^(|ۘ!tI/c|٘tn87S٥ -3gܫ!m//dkNeIㅦUJ݆mst+ -Ol- oJ)^ZdTtgo>فlw3,}鱿b"~FBٙ|꽅\kCsOv(C[V>)K?)r/QQŲ6LY[$\xc-Yf\yʝs?ʕOj÷Thr6I|e6swkQQ䓤'IE%yQOʔ51绬W4dT63e͊N|X՜L͛{Wkg7{K-v?n&~j2ƥ@G.GJ]ٚRwyÿ965ڽb}&{\a;RE)wTJ_ar#m*l+ת΋zМYY>$ =)}o?lNmxZK~kI?Lm`femhVrm\[Ky⇦Wnq\q-cYpgߊ⹮O\/YoP۵[-Yxſ||$M\~x&z0mx`qK/&gUۃTwaM)%ͱeګm{y9[^˹_Ϸu$ǧ#37qD'>[Ӯz!_o>I'Ri2B> -. ˎjHU}bR-?n{p(yUytaMU˽$ΚLnZyŻRuL:Vyutk(D&'?VY?n>sEQR_6pފB[kgkI#Vo_x3]Mksק}۲ -> /r*Qw= u:^ޖTќ$D}1  8]{䯾Zw&( u^mDc,SGz-z"=|->hcnk]'dץ4D&%/߄0ovNS򣸂{Q9{Ed<"U\V[HMcn#F.R$=N>H9|KPhWJ2?[y?^wzYK`㹊?^v0{.w9̼Ć܂Y"rܛKLG7x2avҽe7ݒnn}7gnw{5F7F%Ȩ[ڒ6TFݿoj}79v( -|UUST(,Ϸ5L!Qhn vOBo[Rw78ӻ9yu6oؽ:,i|YHSF\ٮ@u+uv<ݞ4Px7?'6 O~\%-oaʁqkW}GCrL}BnyM={aLLC|_g[֮ N Q_{yNhoCޏȱ}Ib{ yoW'c۞Oq } -mn{#Zry]hSnJi*$ސxh_v^^e2xv7uV40,YYd53oя%yKL@voPTZLYneߎPJ̉&*Ox&U}.*H+,AXr",Z!׈~wڱN -F>/6VYk(231{2]f3<3~FYFd6KUaZW̔㘱1.3Dft/=f\)9qW2Mݘ5#7][OmݟݬF޾q&0V_ua99Qy1 Ea wr1 SߏxE/Pܦ6=)d.̠dӁ?l -<4bQ ]äz~-68۽ Udѓ=F2:f ӏOOHfp=fR3'tU^.8~C[F_wcJU1\l{%Ltxq$71)nTV;j*oGf Rx&,c;3n?({U޾;[/}x3ѹ4Nt/^qckSY157G5WK3e0L/O\h~yf UOZ߷:әy DEc3wWyԷ9rGgݎ{;2k߭7ò -Qyzhq27ՄO κ[ېX''s螎$?ۓU񏖎̴1ӈ#sCJ^fyL58zhҫ+qt>CALoA䧑衳91u?;([]{SYogT ϪE|L{yo.E>Jɇmy~:&2yUGG"֔)&vY}c0zGkKϿ79C|Cia;|߮5?]~#\3*gwRu6$t\t8Ք$;ueMb dcO_Ⱦv;|[!Y-5A9]2O5 tuﺝ-~{rc;f(Q_n}fDcf/_uoR|~8Z|9od_{^p9WoܹsfhV8uh}ZnJm\Nr]Lve=ɻՇk3k#p{#VjΝ9`?z4עvz,7}:z%ÌfjOfƎZLYb]{痁֯%\Q|ح, y%^YqoSVj<AHfM=X;Ihͻj/^i}Əz{?_z{;ӟܡ`:{{3df$fhYhM̌!7W˿DpĄSm凫mjc._YJ| 0>[oۼ2ӻ=3n /`dFk Jg3zB˙1ÿgF\[̌Йnj36`3 $]L^YjY@4n֣I.ҀN\*$ٝ"+:ͣЊ !{67!ZS׽d`Fi1C{"? &OXK&0#{Mc 71p3z:imf%2_5Fx}#(Z|Ko=Ww'aPyw䝸uk|Hݫg]?&|D|/#|?(7d_Č]Č3V=3~)3f3f)3jzfwqS9faC`ΪsW2$gQr'65fٹ5j3s{E bf=u^PFߺ.sJќp9C ~/ =>ghd03Rs434fй?1w0ّ3vȌ`FO3'0#Ggf\fE/7붵i=VWTx)!5 -Iϻq/PaPM~y5= *&5{rb՘S6 zy{=!Ìό85lݡ[ƌ5|3bOMbMs]q̢m't55K?u? zjD[dd= *|X1y Kĥ}kV010-!y5̹ZsAd >gk[YBs5IcV%nwk}f:_LW3m.jHȕٷ5^{nFȈ{ϰu{R"vQ{/(vAXkU ґARl ņ]cXc&{[v.9g=?Hu͙k˘?i^wȹpin\/F_4tӢw[=>3,g=n7{D*{A Q䜆{'!۶ߌ3,&Hj:̰K9iv,3nI3s53ֽ4qaFx2Ťq=cxcX!p5?:;\*|x j 74]!uÛytRÅJ#CLc)͵#eJb#N Ϗ$9f@r~VNLQ7GI@{|ǩ(f8f"fL`#3ʷuإՒÆ $}uMF0Lc5ؠ3Y:f @f_fP7f(OfЌf2jYGMl|d0d-,[ϿE=^[uu}C'[vF>S{mA^wڿUM> -'yָ\Ha=qƌr! N|Rf`?IkÇ,b ]8 reV0ffjfL_qxV3Z.8mˮfy4(8UQ~aM_^.zhK%[7~ S|l :N+I_=/ a뚫ٌ_ϫ'Y^ҒfYC~d #x(fg3%;QŌq rT@̛ : -f5CÌsKa0ӄ&yG&꾱aR  ˣgkķm길ڦ'C~c͛9_ʮk^9=29́ |ؙNec87\aɥ ! 6?o~oZ<{/4L\~C/D?ܐe95OvRk7kxy混}^$߽mZaȌ 7=ُL೙Q̔S>3seytzy97-j=kf]Ͳӆ-=hpr}:}a}C b w5"gy%sm -A,cx"'~be.3/_ҘA$  d/Kfqcf7qg<ۆ9n-~on~fo.zm×6"_ !ޥK?]- {Wƫc+/K| -lI+HsP쏡W:1O`SUC{XL=*f%LU3SӘic<3~3a'3e媍楞5?ykaW5?^TKo~ Mrs RAP2&JK_]꣭//lUpmsJew6޺VVwF;7U=<#j 3l\f fW<•1|fa\k~jA4[borU?f~J_ - *_ : _^4x|2/+W}yʢէ A7oc_hHS_i T0bDXnvC^v.M'tO>6Pzh:w ^Sf)a6cZۦ ɳHg3f8/}ot)5+jH -! \M!@ξ5}kS4H M/*ˎ:(:sVGb, C*N8ߟw-|NRob&Wwn -z~p^5ռЇTg -n5NO>C\}wFq \?˒>&Ù;QÌLf\zѡ~۰'bs;,h|O6o_^1(6R$0UJ3Wo9+|\.=~<:GaòLc 76֖ 5̓}^|/.]}FF޷Kxp{qw!:S9U{F_B=H̘2eK -Y~A!!k]_J_ޕo3wL&0),+okEtd=i U]rસh f8凿y}%rb݇_b2>΄r/nwZ-8C^B6qUPVfE)̨!4Ft?ҌkBx6rPs) K|fг}Tuo7ޟYpu/ſkūn<-Nr9mQ-2j<_^ʖkUlnR_[ +B3]B 1͵_>Nb@2j(`0f2\鲳⃕o$Of^=ѱBx9XoR&pLUyc'3K&f1!pBaE}Ob|aw+G/ޗKNn>'7 -eb6 ?U[osq yBnuǣܾ[sT&*:mCx`~k9'}Rf k!({仞EJAO}b z]|^>G00؝Obqkm vBI?u< -DBh:݉a7BˋlWwv?Uu g& NV?\mS}e J-avcK̖.U0^J4Qoa3~:Շqx-^ޣE#'{z2n1>Z3}IhJmHIǘв ҕG?c_k?[eF]?2W'V[~w]~*}%^̭=0+>_>:l<{5ߘvn`O\At?tAWC?]CE[+w2?jz>[?}QO)GΝϸ,1')^6tUbM|pO#/KG -jakTe0@*h&2[5P;7!O%r~TRuO˄S q.cS -i\և.¹w*c=]jy"#GS -OZ -Ɓm}N5gjUflH_i̿R{uü X K2էWkk?-w;nw;y?|G;_Q(:X,^/Kkw:G-ɏw]\:PxDn[77"^:{i*mHE}ƞ\6D+#-vUunKѽ3H4np9`ৠbbbM.N򚾚83+_ nRVם=L_ 6}6S+_~x48cت0g:~_{cq;1`r)N#A~]%G:ZV?)5 zN#W^cG0_a5&Bqp+Q'<{'z[ ]ݸ=O/" 373k9$}n;/hW%w^*DU4wZu7OߖqVR2$[}tg o_'S!PQ/u43efHs8/2sdS.2\O3u],!sTK]o+$YB+ KmzՌɶM GJGZA\Ep>N`Bfu?9 끻̵gW!܍Ta/I%G 5'|B푉$OUvջ*;S{zy!h; PERWat{^b()@w?;`!`7G&kvޘ#z$| - ?yɟ~)+렎+P^`S6󻆂GR1w$[!G'з"t --2k#$2P#kOLn.ڟ.|'Z΃ >N> "Tվ7B-=o?y!?UއM̷yz L8ꃱk3T-Pu:+0=~3~42f:p,f匏 ܛ6Iox{*o_% O4aA6X`|1"4q[/͕:,;%[ ? U{`btgZ -nޟ7ߝk(¯~Y8*H>"B}^ ؚS\:^/PW7Sƾl2,ӔKhZg> 3}8feR7 4"׮?AvS讁C^3A -zE Cn880B(4H(mu`۞,}4Ui$xbF+hJOgRf g FUǨw};}p;%'wn1'2TXmVYAA&9 -L=uH8{?+V v NLޟ #>|r-_d;R,;n4 }8|V;y;B>H?+Q` vE NDo"ׄK_.hNm˖t WUfb{Zb5 6˜rZnVw ;d.[,2YЛإ TZnF2i5kN1%eDdY3SU0P;8:B#E뢳,;G76_m_w4h~U}R 4#OT꽯T\Ɵ~-h)㺸s$أMbluEd>.)6}2oPyW}_-uv~6P:U|n>,# |Pe0W.3K\2lzkeo};U'ư[퐋z.fk0 FVG#{}CΦ`CW zxuעCa}DU~ ,:hqLw JH%H|i^<+&Q~BF;!Pb'$C!TT`,fۣzupC*1jՒ: Š]^h31$E8|V{N:x_hP`=.{ <}o}}b΀a&A&Z*iq@nX~ AAM jh(\BEJ` ;2Ü-ͯ7mWiͱpc$3\x\}8 VPHQGyILcDC`)`ɈۉTsM8k\}d2e;2|~&}.ObNK =yȇ(XKǟ+t/Xrz*+/z 즏a~1[xnJfy tq3zĺDceX"pF 2{*op唝[jVtKu|;yTzt`*qMg Gߪc?~Z u^&9eG3AH#zbA'YSUklj -,㉯Xg'740  1p'XaKġ12J9UeyA,nxH sad t(3*C>·K/@`xa6.3:|jk6$Ƅ mcJvybV{&8H$Q4{!MC>x!gA!+)g19JRfd3:e\T?Xa,NcI`F i5^j)r$ IUƐ})BRéyჩvv=6Ki> ->xKOِgcM叟W%O5<8BH(e+Mw Rj`65?nȟ?~4QEduI&>vuV\䐚dCRT35ṁ2jo>GwTk>Bمe-#ɧg*1xkqdMfJYCNkbNļ: -GƙI*} Pˌk*K/+T< z`i6J$Ò(tmrAJI4Nll\sp2=ڔ5} } ;r30Cnva`j{\2 g'O"#Ė[q+-4 \T9ka棐Hθ̟:p3Hgg kk*ΨA'>؏~,fRٶ (ܫP_4\ˋےaGj ] 'X[@AF=t4qZF-MщyRRfFJ-WZ/.7~8v-D&n:_l/v ͵]Ƿ: aڲ[t -X|ڽwUoUlX)9>Z{2t=|W~4w=q,cĘ -K1В#OE\iT" XqQØa- l"q^T1uX>O?s_xڟ ;\-DMVT1,Cp|cˁNN7_+}\|뼫麭~"4%#tEuC5'fחɭ7܉mМ6BŮQ$M<*[hbm{IݯnuR𠅊4?fXF k;.چ[mC/g-\P m-w]׈ԵJfШ֟whivTs\ m-qg-CRlzgvwj-ɟGP^ߑ -+^Gw!w= -Jztऋkov"RTYp6G}`|xJ!F1"n7x|dOzit}tY`|PjwKSzqt1sO)ŭ56d\B5]}_] t=X.7̧ڀ/B?B+X{Jw:xΘPo91};wܛOl6;n/@ ,u=``O-_."7t5#츹PP\Ih}D㡏rzwp95UfXsn<1SsG箠i7>R`IeU5]_+:^,*ǕNoBH)u}5֨uH uD)mS?HB3؝J6VS1U/U/5NJ[O&izoX6w BF|zj[hIhgq[I^vR{Aev(^x) LA8_]A]f 0P[E$ނO+ h niJ+l֝ThldZPltY=,nqcD}`q2|nA] )5e b[.V$ڋI^W9@aq|:kuxhJE5.+Gf#6kRK kз>V"P#qWj qY?A9uzCPJ~ڪ3Q=$1N1Cʹ}(s9{>zG\g;S]a`*j/qݱܮG37ϝKםBu;_wnt1Xh>zGb=e<_UI$ 1SoNTb+D%!.Rݬ~J;0ɳӦkHc\nEW/M=|➯}v@;kE:<ڲgŠqlu1V8Lٰ s2m)Sy12ЀȨ nw -6NWI(fm?Yܚ^עC_,?`3˕LNӶ,uS=lO}=DvTg(i(X]vwԇ\G;jPmBhSPmxʥ3lO,m˩9Yb[Кkjȩ|ZS>I."zA_^|o|4^g#~ 4ëĚ}ƳΚ;JϽxZ/%1r;?o=u'_P@!H P{$^oGkNL\pUd)X2usBI MDmZAs=^l]$W=TsMEЎ&Yhg58{a}PʀvQ*g :]0!m|YEyjgE;/7NK\mIurR uбorK_G|!WMLsz`VþDbtk ~'Mwɝןދ4hhjwd3ͱ&%!C7+ 3R!F,tJ;7!ϕsۇ"Aۏ1W !F*!vbJuXWZEtu=zmkM:/.$ t~,MԒֈ5 -kD@="%I[kɲD{GCS6os- .|uv[$'~`v͆S5Mm#]w<k`qKvyOIwܐb PSd+¾U9 --TAH[ۇ O4ZN$fv֎;λXKS; V?Yέ^쿵Jz3E|ѧ#1w׮C+6l8rxv/E^a ^u|h0frM.Wɫ3]==+v,Ifq=)Jq+i0J)#ֳZb%J e(/0*FGrԗmtTqP-ʃcǞԬ܁} 7@암 -#@;8!G~ץM/eSjF9"\H4$ .媮1@];Y˭%Rwd6u8BBn<9SM ׶o!PJwo.es.G$ߺL_i.*$qHp홠%OLɃ|ۅjgQOvU,?EmTnMv/A?Y%=YJS;+c‰Ԓ9/㋶; $eT8 -CVp|t3PdKz8ǻhɵYQ}^modBr!&_Γj5Z'ħCM}4'5yd~/<'P1j}ScuDox_a@w O!RWmn?OabLCN/+IܖOKj9ja󉛼?#=.BO?,T;4MYgmje_5jm+6 \>&8[zv1-x.bz\5'f-8XׇŘB :RY+=wh ߻i6Ξj8ǭZխrU_]?pMŊG16B-Fƈ4?SdTaFbx..w`h'KLvdܫX>_JE,t&Ju{~HA.8+hAc$kTgkn4q6[5xzttٛS}fĝfǧkH - oh -P<3ߐPMh\&@  hzCy yq_EΏXj:MhX4Nn -:_z~_۲D'tO2:e;Fʫ*>DH̷})sH`w>wvD|H{GKT; Z!XZO쳲9_hghgɛ?#6]A[I69\}h uyW;FQ=cgH[/ΣZO_HhUuOn=jOLt>]wj.{!*L=8kwyEtm+\Dj,Zk;>EnDre$*Rh9|ɫmͲev|j D.z(HIuضcauc`]`T4RW4]6hnGDŽ3t;W_&XEè %MɸA5Vۈ4G8:^zmD4坣-m!\Wԣ/ljRe ʮ3r=ۤFCOul;z2GJjg9XזryߎZ BzxT Ry5x WA|r۠;EXCj;K5 ܵOM&ȇCO]]uLRϢo ]_.#3|Ձ%9rt^7)8Z37]Xn֯4HNM5Ww9BwK$gc &Q 1KKzQ-OY5Nз%IT}f9s ,֢83hmzjj?.|0kbLK62k*(^/4ZFBg k%;<&$mСpiӅܒΣ=t? -c&q4ڵyϗiRJk!i2`_xECfCtk!՝>1Cl|횽㩦94oIGs>@Su4gg󻞸sM]oqeV4&?8 ~&S7Օ:{BĿMpOO:y&TS*Ze< y2ŚDe6 DC^{&~ @ۿ\$n8sK\$x {= -b%g6DΊ>%^B h֫nth ^Xh=X NL -D2*8'VVȫ]s=zNӬ?0kQRbK5HF嶺i<73 -bi+M`O_`ɂ%yh'[aRnx4~ u0lz/4ȠWwd*[ NFw_NxZ/v^C=ԪnAo{~g!|f=%XT㱛v5tbpj}Wѽ'Xk -BrǝlMf{:$vv7nFb={6ɑG`͝=c,3Vts@NH̦A2?e췪;Vz0zləb%Ҧ.O/+F -v:r͟|bE0Ǥ.G~ \۵\ӕE,)gYhKᰇdJCW[i VZ$wzh2l\ָ)MRVQ_.$>u8]#57p/S1AjA.;$oJ Զ1Mm yZs@O li˻w -5[vFIu=GЂ`O={%>EnMg'T[:XͩOp&yŚ C1ǥzO:WЊ:;c rJyl@Əgb -ϕk{zb0yNYُ+%94;N$6Vv8hQ2ܧ]g: u:?l8>^w` B7/EsrmtE#h\>:jsMjdD1=7{i|؊N2N%_J_H{ AGn,tzHH#+mtx?x?x?x?x?ر CSCmDon>sUR#SlmOwKI]9 'fEVB>?mh$BR"DfM8M1HD -f&DǰbJ.4DJ4|L9B1oIy=U͆0#-yU&:F 9Hc$@ϽrX:lWAG(g~^T)=J*h*U؊u:{!w.V)К2;)&(:]L9Ow@>5chcA`VE)UF~J-ڒ] R%9k(x :G՚xN);(t*Ĕ[r1lX -K _ioA紸_Pxo,R0l 2jHe֬h u }y;!F{FN7tIBTve.ZV767R[7;Yr;k27k;b˨a?R\aiJ0 [k7];+HvL tP*II+4g'R2tA+4UmABW< c"'Q"dlR`NJ}SKl5QTy5r\b9\ >`*F&$^򺪃55Ǧ26 |K9:@"BjH;JaڢxyrӍE` -z(BrZGƂ:N%Ѡ(P%b"3Ȝ7RrzU|?Zň^+A( !ڈ9[YJo/EPRL RX[l"iJGblVo2+Vd?hYJq.e6>Je.T}v@)]p6.12%Oǒbqd*;tR^z:Ny5]Bd -U쟠pjnZJq9Nn"k%>LthG*+167:a3~Z*)9]=357ukS+iRl^1ϭ]{kc@D;:Fj [_ۉ~ɅrQptNt#qS7A+12.59n&muo]WAwd,a!> 2o.7!`W -_ŀĊy&_ϰ2y=I|x2ā^f6t og?BdSh->dyi._-tGюq~e֚ څ&D?^ 3֠c&ؒ I6PI #ٖ#hRJ72*̳@7r:`[r,O2p0yI+E䰌O_{&2'/hCЄkjߤ]s`|.c(XbT"R9hV'g @1@<ۄqZԕ[BG,: -7JX#04':R5Mݶ f cpB>C)9 TLF_:`kvYWw.a.IN*9vܖM0Wrs,ѥG}* 6pIdVP"0|tz|FPWcH\XY` j*eO"1FE1@`m7c@BPu#ƐSIU7ҘjӫS (S͠#>s(} TVd(mI#:wi# Lc %9tGE?SO1)qݱ bDϤݔB,p@)$jᠭ̈/'ׂW~_ŔRQ -:![ImYHm lz%w%6B8}բ~5c.{Km@bWµ\~/H|ՀiICtn7om >/nh: -WD;J9̓N,9K5 -t&~TttkWs:AC(OƓ2k_d>js՗_#0K 5smi%GA7lt IY\2?(1BrB:O)> -RRGI㕶GR1y?:Ca3"Dώ||ڵLrHUv@% ǧMUj6R_If? - ;2*̑RfK/eݣA?]\T24%m#ړSy,.1]PT[IJOA͋pmI݂-Y*a_X!O)H2RXВVxcXm!ف$Pyig뚣Xb 1r偱b͇A hĬ{pP(kwA^ r+R!qy"(Qy-Z28KL 09-Su-4R0HcIs;?~O*$ s -Y.oEIUw9 - 5#~>s eGaQLR3ǙfI㡨zC傓iGd -$J1 1I% 2˜'ZOiUu_F:k]ɒZ1ZB|7!u{J&uJcC aBL&$/f_u}(~Bh)dF$xe-*RL6!$Z!iJaz dz#Œ:] UԸdU 5": -6 vY$y`RR?l0qb$i&l,m#y5T (5Plۤ>-}$qӏ!Yf(3͠4+EKE)X 8 e K@VH]Dj: Í(QO\Al72H렼sP@㣲1ِUtuKNRG| u#Q_ -E9pjFRゾ  y՟o E cq -*B7aÇS>E|lQ:%kOEA &z/m2@ZA4ԇ-+@*U'A[}$Xjsj{uChӉor1U=X뙔P4JS{9|sw~˨cӵy @3c+kn"Ua5g)o`BLnoDžm_/4Uk [{r&[n̗?pۿtVHJqpԥZ%OJi~Lu'&Kg de.؞zU u?ǯ|COѹIC͕\>΁i)"]\ -"ȍK/ -&lФ!_85wg9(_?0N ]SsɸiD`R#LNŴMt -Kק@~>&s6^W1ǟ`Ҵu'IX 㝳FVn]R~(M<@$ԫr㑼7_[VEд&v~,u4u7A -7,L$='s9Fs!^i{˩D٫DfҖG9E d6Cs\ Sl@"La 92!p^ćm@MHz-O~ek\!eGb.GQ0lSy? H ԭvޣ nR`,FSX<8gWa"sMIrӫ`2O0} _1d?M[c^[n_ ^7)jZR̝Wri\8۫Ӌ|׀O$#>]'?GGqݒ>a_e /TErDi9s1S4%qL ftFc^c6LMJ*:(HI*c4@%0P -ܹqƱ+ -MM( -0>~J:`9U̟MVh5NY0U0(Lb5Hj3d ("30W)D|}+s'_gu#ȿN -hyqGSEy_7|!ryg%_(򺱂ly&N "c(BSQk,Diط;85XERrNqh)؇ل1 $l"k^VJ9u -C><|(àHe[<%0.:LӃ\s<Ԏ/$cw߬1ɐdpN q!5PCgS2!0W2?m@ B|fNi1'Ĥ!5qt(I!Җ߿=&+,BFa˹rg.#bv}A%Z&sUq ؆NvWJGMuːǛN 2N__^o -{OˡF\r-㔡LvoQWs2#$kPǮi- )tݾ7r 1ǾK};En?NF0(xZN Z? @ wh2?/5sQ&F2A&q~4N wc<!}w&L5ǡ17L}&f3R#`^.؆ʏ}=2~/ss -gMgȩo B\ jRdl,AMA=8L8T j6_jWFM|EvkcrA5}⓹ -TȨ]tPzP+߅njףUDD6Pt7U\p7P3%uLrS@܊(ALNcnMx&vO5 蕀<p~ \>|Xxr&9OP&ڎEK3OQ ǀ?jĘ-~S.E4e;IM{Am#m]N B2]@'[%-/rmr&a8 -rXb@ɳab*W+?9^MYM0chg ԁ )>ۥD. V0@z;s} N-gPH'URJAe9< { qW5!585l72+́یw9;n/D#J$tQ T\$2s?S?p~(XS>~q46PdR7"(ya? x#zOʡ:ڿSۣ@=r1{!o5,;_q̄|!(q ^ƀc&`ryaR%ngJN.>S[A  x0 t\AOsjz2'shn`'\ 5,N>ZJ|P=}ey"8ld8n/\'s?DR aO7PaO6 MQ]Mf Ȥ~PCYa_~-a8BG -"r; ykǩAP%;85̟BO+"(BSv^*+_ qa$evTS.Ԅq"Kw1E;;׭G8i$qNhϤv둉ꐗ\,Kwhya5%ym2a4x9䏀NCX^-=}mUA&R!2`;y>`2'_rTA[5 1Ǹz(N*Z&ra HP:;~6A(uHsiX$T*˽[N[=Lr.l՟jGK95;`)[DB ƚRRx(ȵ ).0B=n)KWr~&m9%(~BT#H<4ca f(IT8_U*(+3a*:= K&zejvAМD^QJ8Gfkj%P!؏Isݸ[y))(7Y'u wD׈ox]`8vp=^Q1EELBP g*8nĠl~gS *@_^F=_(+Am 0͡x\Ӗ0J5X] -ypȭH4{8;6qo#y)9eI=reo^V=-=^ĒҔ#QjSoly -LŇ)\=0Fl63.JD`nqVI(nI ɟ'J:ba;Q>i+ %q^A A7-c95SiL|:X&ub(ڑݼ*%ujBߏ.C_0=\MبMlՄ@Uj!s$U=p(uUL |;Ŏa:آ~NY/y)q2#z(s>\_5x7 jG[=?8.`WjSB ؠF +>(IκNrGLj/`}s8fہZ>ܔf]ڋ˫CDS"I >:Mz{K7=*9d_hMoBPO 6q| r;`(RImI72!*Y+95]+A>6+Wr]'Q%@Q<:8quyͩʻ`/5Џpw-r+l{R~W9N뙶y*UkhGhd_A03#ldxߛ+9'ԇ8$[G+r%}܌Nifo.n-h*t:i<r- Cل0jf"GE+BV~X}Z#sdƐdcx Cj\T?_>xx -`v3]e^ j\rSA}jC^k2W P0O.MNxP>8#\vQsA rRg|UKz#`f*=P>r= -\C!py芝3oN \}೾Z8F*(JdKКAIU{mK)1D攚rpAvJ05۠dīDXF.oQO<20p (K=ozz\L/J3?\׀rkK1g -.ܤ|W೸ w6 -xm\\O7uA!f.=G>1uh>~hA8l&uP1ҕE[wq/=S$8r~ W.;[!zrofTKuq `l{g59cB y~״E\^jr|aqsΏ:ԚFm u.Ŝ2[A7'F-x:Ԥ -ꍢ~S5c_E.N -l IOJUȫpkrį )`=\82 ҲHoр\(햺qMV>+1e;D6ryi|6_6s}nZ1nmxEX#v뗅y!8Th*[W[Ctw -iq땄"ԮἋ[!;Se "|ru 9h:J.s#zcu (G꩷8=rKӊ3&Nُ%[qP(aӁymp@y z7Sz})L@ơ{:b*:J^ Tc9>}+5􉚺/x)PX|=g5K@ K╳ )P0+0`%pqSO`/L`fP}]ʠj$.FqJ!(sFx 8<C%Uױܳ*NI@Г >/8rKq^I0uLB.n圹n&n~ҡn n~f0"_3%>r'}"~2m@%TN'őE22$cwC~Fr3eqջA3 }:Oou@sIͪ&6%"J4d=O2]| ׬ -v&񼳊˥rY+GR*z* -aԴJkg4Di HxDLNk2] zG=zf>D?޽֝Dw-yLaIVZt_wY5̽&^Z qv&K66 UNF44zt@ z 谢"^90o'@>x7߬jiV!*Pp^/qk1u\M5|!" +.|_dIun]AW,i>rpYdU)|CH&% -3Y -퀠Z\uF|*z2}'K]$51_"yYBj YJ _])ӡc߳ xkU6֑6-u\bIg15m(jǹR(xB#}G7a&E>'2y/ ysDy@|1$0Z$N> -O?SL¿/D$W^h)iVlHkc@, -GdG([$k1YlĵiT40'j_h%{Q~F|T+iK3ZIzJԬ'N6bҺ'A͊~9%/VD~_+;Α/$tN |+&|"* ]rAa>X2'lfkXKEÆ~{Kj> x49_q)Wo]C!_|{I]E?^#]M_3M< #zUvue-c"k" ]Jl%/kNH6&VIF8$bL":_UM1Q͂(W2X+Sws k͐nN -( -.qN9𛓤f咞_i{7Uq 3AK!a}DҀ4YAI1A~%o2k)6_i>).nYZTi--7ʿ*~|AmI^9U%$cIi~YIAYLMRv -.Gl&kB{Ⱥd}U뢾Fo悒?̈_,_R>^ 6xJ?UZv=|j*>^u[<"*z2VQ'~`i,,4T4ʜ~&a~ϻU8$iQv "!hO˼jTT\V;ɚ*w{f·Q^E=%-+u,\d6`on;1g.F] -;y5#t /qrwCH~&vL[@&|Pn 1z v ^,0NB 1k`і'7-><`pQU軹CV]bHC=SDQ -b %uNݍИ|Qi}'5n3)h"fuy^?37w[gFm"b޻yx j:$T`H~NdFSag¥nuem!7|F7 XsX2㻶0ktEYyӎD !U/Ѳ*/ˎQGڟFVZ\$t vV'LO~7>&w' šoG:'ؓOեWLTo6NaQoɖr -(15b-FHTI>>~scB0'GA“d8?-| ( ؕb7[Ӵ:4MKS!o|Ԗj=iZQ"-p=1OR/\˗_O<$G><9"ZQ+Pa:Xr16ֽ7έ769ң7QR|k;oNpzxpKxZ<>vC^or0^>.GyQ% 'H /I7e7S̭fF/ k|v93\sjq'K;% -k~%*~56exI][S?kx8`wns.R=.^ J9i'xxל5& -0+wx9=`0ioGw n v _e'/*h -|hX?YZ>]bsXrsX|KGOS?`EĒ]sf%Ůf E!ⷍgDJ$I(yvHZxQRc/ҚZw -Djyyk\z ~-iXr1>D8" C$_vg4:EFԺƞiI`:꜏vdE6Ƹ{GF:oWÕUiӫ+baY%Rc홡->&YuUQ~III} - yUla^^偑NUqn>ޱTgVodɯ3:#<-̮.W\\`coYCP,퓍|3}2ϴIgmDe]k`zUir>$)~͕Y/=Qg/?9? -]Vz24l}|crYOEs1@W%zAE/~9dTz)=tsFYz_y.~՚v;q7̢ܤ=nOzOOΗZ#^7z)01\5cz Tᵍ¯J[8|+ ~oF ?ND#&fYS֠u[U38n G5oZ54K(1EAn~=O6:dsGoKUpc7761@g5^H - xMӱMşj0nuan,{3Y\Rjy]e4a:zHϵɛЪcwGhf -YC-U&^tCbhMK:EN1M.Mcj_u -9,#LnTqg ۷߽#hn;S˅h%Y;lO;3kyεawss߸Dw)wI(-v{QSP./ ˥/wyx`RVMu yCE5+˞gcO)fѤa3>M>i. дKѤKDhʘ5hLJ wEĄPpRӁ\M)rcBa_طɛ.ƜӪ9$;{H+ò]zR4Kqeo_`MJY79wӶ#̩[ QqT|F4sp $4Ca=v=e=;k/ZVo?mq4w]DY4kPDsp?[Gj'h~`C)OyZ-kLqK oJ"*"kl#jbkJb` ^$M=<8{C?:9 -)M]}J4sZ|vSw#ii{[a-EOzh;e+*FÇnx}YQ.!\{}R[CB[KxiԲ{_ZϦ*FS1Cj>s-@N״Qьk6U}c4Bs6*g}h7ZDdhLaw3TKٍToKn^!1k{! qam]mvy竣C4߲fOo_ =DJHiB9O{!1+?{7n=[ܙ{тEZ8Z --rFK5Uk4_iVCԞ+yQWy!A]z_=9/) 8gˮpyHWWKg^op)c߿) _l38ۜߧ+[f[[}#&B3cX)5G,'В=hQZZUQ;n]vFqIiKI}CtWvk+s ~Vb:Smtm|Y]l\kɏkV=_d 5xgstşҤ^)6`;i>]#ZHiZ9_xր ;oU+M6lqݳ;8:j16\eYc[gJ\wcBG=-,ok?x-}_RG u:W L|Y7{7;g/-)hQB&Gr6Z`ܩk1o37g;Ɣ

9٘Zdܛ둧nĘ:Ն$UXFFFrH8?1ϣ:.e4}Z4{>Zٗs]]xR |wfN5eSU>E=]U7Ğ3Ğ})0{1Z|89`'] ^)-.t1CN4g,d4j.7eZZ>VheZc3|ۑޏvOգlV[=o7Z*oN kղUogjd˵kYmzVhƊ X`|:"0 +cgʠet !!$"ynw稊. [|` -ܞ_,54s&RZa;h qo*n󫙸;_5O%u_%.)*G׋Y'oT:qg4?a7a| k1xR͊t|J60(ZAXT}^F俩30?nT;}n*L݇gǘV7ת؊R -pU Tֆ>gx-As窠M&hvvh׉;MQ-dWi`OϜ_+lMg?~gi}gyڟYwVj=Hr3bt| m7Ҡ2nxskBu;ou^Ƥb }:FIf=I?'}ڪu!vܒ8߽M6lkh#{Oܙ*aۮ% ~e`}g%؝efnVwum<1$ET"|9=l{Zmڏ-:e5wg\|c|fU32=y;D%1IjTb۔ySASD9E9dgmBm5 KK.?lS,&@K8~m̝ٓƿg>/e 20x׳֤=+cm[|^bA'G֊7C3[SYz<"J6/*q;y}i97FshW>2_^)KoE$yhhąGoE Whۉ;÷]+Ҩdwyqs2qC%׬Tn{íh`?=*1*`y YS%8_] oе1e6Rٱ9?aٖD9ag^+KN|Ǭ5kdر82?t4Dx7]R49E}!"d'9I0)a>ݰH񒬠 apq&VQS ΅mx-ъ\>}؟v9MVqmf|m S 랣]Q |BVe>?ōDvx.<%mt6;29&n>ݹA;?thG[Ͽ5Fm"2}aDVC,='W2&tiz%+,6+-r=%n2[Dz~"d"6}.wo3'D?@kϠg]R_t"vk`t4ȄoLq D%{Gk h9h璹h7\+ }Eo${O3 c( 0KVߡ)JҢr7Q]M ]uA\tHm?zW=`U{Ԫ`ULq8YqỏdP?9JIOd|]K @/cuP CV8kqɍ{M.EMٽ[ W6͝:c-mg:3EzvTRQa'x _p=_5> i{5Zcժh9r hVV W;_OYzgбFΩ3Mn,4Iorw /v FN_J6qy1bc795:=AGCiދz8^Dj +P.Qs1[ȈǛ1oi6|.*G/7ϪV8[1-)vU_Ԅ*+!ysMu=w^ 54c26CWl!uǂɚM~&|t!uai[Ct#^^t ztKpx)hnCFHDfǹd,6 -%  k<"t+ᗳZN_\ܬg^%eKw2Wg3ϝdR>*f7l04TȚʼMn]]0W]x6H6{ 3?XkENvn6]rd5-920a m#*D=χOٯ{F - -=m_]SGf ŃZwvd,2pԛ9|"6?kj|;s1u}[ߴmQnH)lCfvQ -~\V~ЛnV>([W1ngwjFzAP50xbMY?S6<8⌢ɁӊzhǪh߶mHgȃF M}_vs |oIϔ<ϤeR{o%x*|0v߲HUfSNaJsO(yA[$ J?\"yrPZY}wl{AB"BTϋ,bH[3c/eO:1^! |J)})T3kD;TѾf(LnRuk,!x&OxN5],uMT”&U8sͥ k_*xD`0OgRܳ.61J<3q"%f0O虵8nq _UMJ7RTn|al3ԤowSgæ|A&DL^ -b,:4LMeڶtڳAsGu ϲ,=%i0~?֊fJtQ@eRNs}\J4ѳk okfšģ|o􏪂_Qz((}^  ymG1Lgq.3zj1tQgtQg.M6fϖI*-lG.7&)To=Rx5Yީƈ133`n1}pEEI:db0j/Ң3 dwn j2>vK^z0Ǣݩg繈hk/Wԓ ugm"ېZNN'#gf{[ zg6MWvYA85L_b#/*&6sDuU#h44SB#>C #9]&AⵂD%=c@MWfa1KF蒒z͘DN_̿>8C|YEx~+}Vw̴}=+51k}D[pQZVOX?CZ4)8f3pمj4^?؈3 -+D/\:ӌ:j=8=\t `, :MyTArN*KR}: wEϜV=6{YzJ׆g(y6Jj2jwo% +7Êכ؆N;1],89EC-n S,'2zHAjQyM2:"nWNκ߽ԵDxVN U{K0[O(w'\ -ZR!̹?4<0KsHKQ<(ȟ`[rŗ_:xn1O{f-'N8!ιq<`LZ>#Wqe}ԭ}ѭ۾eD=s„kjpY^){!oOZOCy_.LP'RL2R~RN[U"j7mK֣]6#]#_6LgD!-3FVSa xFf-¬?P 87*Lb~2YfvgQKMF;b]3}19fbn -9uRQv#?j_02zCsl;u nYAߴV}K'n3I)cY=a.'L2rmhOЮ0^r -i;-qD^]9t0/!8=L?}y,Hq+QqMۃDG-2Z&J̋/Lɳᛐ\AD duGBAj^"Lj6&Rh/y|9EuDof=89tD?q3f#`$8Ҟ+Z/qO9IFe:P 6ŦIrXFG#idi4-cq1w !JrJ!ޔ۾~㾵֩3#Ygʌ8֜}Z{U^{-:֞O: cG6rEӟtyw+Jų ۶ț56t'[x5Nóx~-nM_3wwUج=0XâIQkHqZvT9tm_uSfͼe}B_kؐ۲]+O_}<栤/m&vM?u6-{آ89nظ{]L1f`}vc'7x5ũO0pK*_F3?,?9tߑ+Bkw5 <:^-{,]gƆ)/oL {o/oX9 dP6熚2㉹|>kJLԲ1f*KG^CYpS#8{C < -;}19+1F`ɡdhSL\^ZKrŮo+O=lT^YENXXc*'9iim=6ۄ2g7}^1NA1 -<Ű#_.dž_6ǰTwFÓWk^csu lyP?1?Ǝ`tspmПnz0&Ptk;alI1cR,m=C_qFR{?y[~ 3|^;M|Ow>" ~;ػHwn, Ƽ7:'vN#~%AsvBbbn&1Z |k"o}˟FѸvB1xijh~z -<.g07H >L3,gמ^ŨY\ywo[m28F1s 'LĘ w,;aɚp qºֲ7--Ro]:)rӗ`q\w*-%'-.C5>cɄ(~sWP,U-Dv-J~RNgenOd[?e"|y:?cO/r$!<l)m?'UW֕]R6lDr `>ĺk[oZNSmrk΢iK=cEa19pQn7~7M-ηK1僯{}{n| -a:#gBKws[,1uЦ@9vx﹯c{<׹xSCm=;&W>aѲI  ladʱ Xڣc r?#rזs0.#yP1wdKWc:aZg/XwbF->bC P.X̧<]7H紆ۓoOiy12ʆ?TB˞\x[xc01!ԾD!ջ,rF]su0m1l?x1QE-wNE`Y|̉:Vdu%AGж`V /8Eao<{%7a3<ÏR̻U9;1w_OZ~Mr׻<ц?PG~EKL9=1k8; .{'EuהW+eoZPV߶|y`VR4$߮kOb@kf/hbgR^y m -<F:]K/_6o{;l3j0,pV0w^}ESz@hkNܜDxI*a|aLZ_,زH7 u\ZBG^AqAtMxU9= -˅9B .gjdb|C-b1F`3B~[ k`F_,GwD_]( -aMZbT\`ǦSz'aG(&1u ;N\Bx=Nw7e;XG|3>QNW| #0V39rwql+S)sȡĞ.9GḌ?3Gy|Jh*r7tzd2i` ߾/}G/yBlۼYs`>ʂK6cɷE;'N&ͫ S]&_%RJ2.:.l_2tY'cRe[ΎݽskF0gT_e -c1o}A~x 9zt@?V7ܹ eR[=8NQ'o|˛bDWZ~?;b? ?{߭<8I,оo¸_K֓)04yal0@_{Sc\8ͧ7<盞|s]Ƶ;.| x*j2d7={ȺiOs3BG1?|o pI1tpKIw)>\% Xm+N 'T07B c~3%][W* $:&aɔwٔ9"vÚ?S"kȇ3N{kSk[|x2dMsv&I0w"ƥwNj^EMky>~1`,(k>1x[ wN}'3û߿bQ_A o9p}W ?v䓺nZr崁~Bÿ -i]\Xzo9e=sw(Tq!|37pYC4uq& keOTC"+]Haog͋9p]6 |O`/BOF}Y$뇹f}SOo8&B+{qyƸMwmfg{\s=&Fvv`/UFƺ ox -{[Ӣ2?rugkn ozm -o>zŷ{ڦ7}?>F3]m`b 19HbwEknQc̭ 4m|pgE]OnΟ b!|x=9z,[kjȝzT pMķqK&@mZe-yq)ފMgE{9O}tkt7S.;7޴ _̱dʃ jbq>:-ʧuw?L64Wykšsmg?Y?m lhc=Ywt+)"7qá+Qco\zu${0It E׽xը+`"#z!-_s"{M{?Pm'&OWrpdD21.'`^^ӟϏmyNjqq ?9) }k>Bq1ߑ0(/._36W1TO m}cnn|pi>X\Tv|须ŜsOgŸ0r=7ֲևA}ok$7mga\U'㼋e֝rcb\nʝGsG?|#1;kZe©BK&b<N'Se ai4|+#s@,t&<3M?]9V(qצ)o]ټt7n\s:TJ#wtsW~9_,wWḙʴ= sGҫNrg=rq2̝TVWLyp u,?Eϝu΢xFĪIt/\yFq۵rCqɘDzqݣc,U _{OGyxV~Vcjߡ4jU>5G n:w>`ĝq_qFߋ/(0f/;|iâ'Dw}l7keBm Gznd Pan@]'O89l8r/xVl7.ۡk9}> -yk7̾rg± A3weF2vaS _'|jL8qʓ}'G'FW<sqS~5.{b䟡QS m˘n==}Պ\C964GsJ1wxgNoQ%ꤸM=%Zs*Pn .𷦏\N 3IFT9-bBy}ϑ&c2̝6rgrg5{x/MKFm 8w1g٨!m\%Go#ҺDʽ囗=t.xh6cnS>5ڸ'͛_&j=S.^*DbX̍GAv@p^]>s%b\=HYm$1q/ZVn#g/0'vxFhOY%#j֗׸s6<}AGrT\;\_1ѧmVK~ǺN[rgQ'ʝ;kn̝u|g/Xݔ)wV#OYYYi#w!-c΋>}1`.ea~s+Py;"wMT3P,kDsܕ1ec,{"zcć^ ?/:m5:zO -[-MD|fa21rɸ700﴿ 8?[` -=NCy eoˀ3wr=wVM;eotXf󙾆l;ƽƕ{79[=(Ϡ| 0/ Ų||y+Qgw%OkaC~~ƒGؖo_yIc~0e oc)`]~Q -rg)w'7Fѣ_܆>hP.uWXh$rIF,\_œ_Sb_$ ?ҵG~ 4ny:]Z}uu<?(o̦<,ʎ(&xk<|>(oI0}xоOo5s\v}Xfd᜺r7ݳr -ܿHЦВ'B mQv1Wl<N ֻNh|:ڷ+>s O?ڵC.8ȸW燻7ߘ>2}~@.rqKHF\< vz2\]o3qcQ~5{.FӼiضoO9pp}S)l-PSw#`7(' -]wq| u}<"`x̋y~B;ߚNcsˬ"YtU_ߌ3|_oءOǏ6yj`zK#==}`Sp_*K;ς |y3 -=4<5/XAZs4ʝBp=N/κW˝ybhO -2qI9[P>=!|ƃQGv#xWtV0ߖS>n< u=浘|`ַ+RWf#P/zXǽz-wVh׏x}Qd[, {?R~oh_qB{\iz.N`_6rgmw6 -zc=vWhقzWwQcƘ#YhjiXއ6;s4l}@?`{쁷7ٷ+oڛT=TmF^[bKhߡ)^W"ygDWxGУ^^/zR̳kx,GYQC\qkb^yMk%ͣ#W5> -׸ބ׮#>7ܽ<\yr{Rto_O?-l|賿 p&5;&O}_M91-|n{u"{B́z{|?TwL]o Lzkϣ.p)w?vA{='=Ǣ#}0:hD_A9U@۴}XA7$#`އ<59-<aO:ۍg羸sp\z]ʯWV @zΊDK~$p -?DJ{qh$pSgYˉ0 -{c;_p}~y"zE畱?aܫ\_KM'ֈ>@[^z-7{OwG74x,فc昖xυ;5y%G3ҝ"\os -u Z0șG3V[ iﰡ}~]郭wPc1Ȳ x{~&c_s7{/9xx}E V1C}'G{f -C{htG= [0fÝ+N9f.A_HSb(w-DW+lG?q.ȿQ[&# &|S+v9)0+C9c`[\k9xwYk&?Ə0)8sbqXtn=3OP/16uHaP?x.mt0+}['pn?8UkLu-7N1Kό:*;tog*)F4uL*3:ƣ1}>nnδ/̴,B T`*39E5_Zf"?zxObNwDgSj1v xjEgg rOw7\;@4=F6IhpcVA"d lŮೣ>ʒNqIw%tc/ݚZluYNtGW')`D;Htڄ힙 X| (&k/ X&cRz&uҴۻ@tǵ]ŵ]еƝ XF%dV%&RqXLƭZ1S6QOmVbi2 *޳uZRHU{{zΪAq,FksL2us6 RL-ޓ >3Jw7{*v[7SqUȝB_ښNg,[<-jW -4Qǰ+Z;1{ Лw^ޕLt:-cY(ΞL|HZKRs >N1UV)J[l!2v"udgYL[ӎ>CC{ vJǚqū-qіCs, 8dGD(jD;;"ls◙dfqG"|cd܏I.tE([Jrx,Ht/J %KO%*t.vGãQI8UөDbm:?"_.`V[d*|Lmu-1"%u;ޖuNENww-Nҋ1[/ws~Vȹ۸i#i.OCu8vڤX'9R[L/U#rp,@"8F1Z[ &"%se$S)Gm& -;U2VIOwhiq7.+>)EJ;(Ջ㝝/JȖ_pԑw5T$ 8J[̂xW$cɻd.v+]97:G}vġ*[%sq\Bq&^o8DZTq>|8?3[^2wE w)Uc;ܧA:f?ڳ"|Y{z=R}qwdJg"pCk<պ AVxwr2iGz Lw:Јvgdg"[w{赵%3ɥ gBۓT!>ש1LɎQ)wxqiufs%$یc73+WǙq8S-v:|rLgZǍ8bb8:ΰLjqq\Bu)F:ng -E>UƵWj|`JR,`xBQ=J/Fcۆq2r^, -\ڟ#%t 4._;_q#&o%Umv7[IrnrZys[)q7Wwscf6j.qa19˾gII$'cIN"u#:vmЮ/Vb5%'Rux_saGi,mt8DK> 'n70 :;c< =~7.YRXlK';ͱl]xfVVbsٔd DGz~-*RH:"?%0I_ck~`4vn`4 -bEZ]Zk{ K/8^`[2?=9O[2,AvE }gG--%g;Yܑ8JIEꔒ$ASǐ=8vsυ,%4wv&Y;3 Q*{7K9<{sv-s-ξg)ݦd{{oO: wWnvLº/Ш#חH˜bJ.Z+Z1xcs: /yP"ԍpUl"tD]ѱJRndq٪nuZ cۆqժr^oq-;z},sPڇms!AXE>W{݉uqZke# sDZP,3:/WF+0SeɶGwD!>Y㣎ej!=ʨV\= L[,}[ -QOi/(e9"G Xȥw.֙xNOY];؝i_&idn Tgʌc7ގL|iًqxObNwDgs "Ԩ#gh"hij-lҡ*0LuO `~&ޝpãx -&5^/DqLڹN*n4%7Ҁȹє{hJ#қd80%pDp:]=ոB+B$F %9Tí$nJtU -Cxػ;>stream -TU喻Snn;2h#iæ՗W0^ަ* z:+X˪{Y<U^"D,{*T*.`,FԔcQYT(x0 @/x/HP=+{y K*i6+ -'qJVD p) 멀j*^xlI -k%hUYI)W0:YU"C0%Ydh-v1/Z h&{Ut40hO!~*̪pmOAXVÊQeC -r}LD@?V!2+(B֒,,9emMig\S f]SļPה~,Q\&|66yH 2pf -;lH49c_Qe9 zJe_Yx_ne$mx9c6Ҭyױ8C@gDQK8.ׇ<$ 1G/,J؍l=< 5p@aXu;{$l Bo%` 0T />5$zh69m p`׿c?LqBeu+#CyLA p86nUËmhJ54DmhЙu 9AC9ă=0dVqUTT\xRFp`[f*c  V]Lpm.<:}CTZ>UÊ:&8XMpv :O\u1c) ->4&8XMp)_E"8 I]T6E(F@p E!Ʊ N&-(dK<ʪxU9 S5HNeC"#`".fPqя mi]\ ۠DaINC҂#y4^-Ia?1[765T6 -QnSIADGnH9i_c/)rЀօmQtqel3xB?TNhHRA$ FNY=1GKs]c~91nj;U!z*hDN7WdkoLM9Aup xUcal -s,#^ -Ί飡yMtM0CJ7jP?-ztƢk|q?V-ngdc־]o0eǻU9reN#{kSx -JW .UÄC0`[(8cenxfkxX鮶c:Z[Y ΘY Dt0QGƄRp@nL'.fE ӵdѪۂHV]j@fUTMNKT9b`oԚK2wOEEKy$MaWUUvԧ3fA(l/陾0OۀicKAI<9*# ysu6О0bӠAۖnI4WSL_*l6t6\8%S̲Fb;mF xgT+&" M ˵Ba8T$Yڨq8VX;-˕c@ $J ,%s(8ƙ[1F駫*kۖn9!Z2QmF.sW( - -I"f{Cw0lm+0mxU_j.'\p"065mk]~2?fmM0 G.(cb2\cvn~a8[qAMdʐ<)me\$NN Y!Zs`٬mS<\8'usvbtYbt 2:N>Z ɪWoX(V5g1%_/F5A);G]%)<#s:.Ж+" -s 9˲Xr$lapV*X0h?s~ cgN%B~#G^"k~ -!9TDZM~ cؽey[B]NF>tTcYԘY  v0@( J^CbZ>Ä~gxeE`x=:#ʁ*P +'Jt^-̳LRb%S¼;F䄀8ɲIsY^EIJ4b`xJv#fbFYW -)xRADgNez'`#YTAY@S@,kkmEEx0H ?b=Qz`B{Е5si;hYN%g%{#On`Nif|bqC>Ca䚡m dK,MR>Oh!hG),U=xG7 Ab>֝9S- "\wJWh{Y1c>jd S@vvf2RzXAjӒ4#6uе֝QjK :*lΩJcfrԡH@\E*'NwR*TK!(O¸WyZ hS۩<]t+-Pe,kJ3\UZͣw?FE 5|tMiCӄh s ˈ{ Py΂LeEr -V,-gbc,|:򨄤J٤=j`0pWTlȤHf"$ˣLY""$;d/˼b2]J^MѪ)fLAYj`f,8 KZN<ڐe 6AZ[,Jx`qY7v&Mf6N1 dRBz wۦBd&f1O^۽N&fhI6YP̊ d&Ȣ fTlbo>/&'51 ͉ỉCvfь3)afh3cT,2+Z`/bsZ2p0 E>v+.vu̢12kCӱheC ɀQqoɄdAN_Y42n@Ll:m:ra׿,drcHW,*1T(J" 1Id:FK- -(X &z{B԰+\ 3Ne, - -E"C'2:Y~oȄ$AemN_㭶̢vQu(9q0lJX4bHQ|As_е_uXߪ2Xq~܏g ǡ~f+zyjz Omw1-߇7ei' }5gKDaSD+--`cKmhes?i)aƎ\o$| -m!nPɼ+wC@vh[+Pa{ndoRy,6K;~Ɂ`:M* uCmt(1Qo`JI tMI7j(֐jdfMv fy۰AT ߲jltsCby[:@:M޶ Z l-dowRO6Ⓑ·1ml!(5D7XP6aED7ֱdZ XMlp![H6Z:F`kUȦl%Cc6,`ɦfv(M66Cl,eéj9u;(E·,u6Cؒ)YdʶT?rf3!lX,eʦfQv(W66 az )[MuH[[6}N!moY*oqipN7,N7, N7,N73jd[dj'ddJj'd[ejce\Aӱ4K94K 5K+SM| LɁrӾ%ݑ8Kd"I ^jJ 9`Y̐zr8\ e//iA"FXM9^xg"\J$쵃 Rv3j ŮBҞ7(Y:o%04e[CaU{CvU$ONXx:02Nu7A.eDQB_0byé^*dЖNqHlAjLnΆ .K`~"@*6/I6粄QEmEdm7uhiRێܦ <|*k[;Nr^U; `(;Q'-'hɸ1o D-5m#;lU]noQE3x:aCjL'k;5ŴӿcBݨnۺcQgM[o3k+:X Κ/͹Wd"u^!cA}_{ -" -M, -'[]F7^@xȽXsjZ=L{pGPpMY -_;o>_>#en1 -0Gi^=$).<%"y\(I*KNAu d=>f%xQk A;WD`cZͶ5+Ũu5y<3iTA3NEsgt*:a^f'(ZNLVX p}5FB€p^TH8aL -2-@ 2NQ/8Z H B;bqK -*I[ASEܚWQ x@VD{P0'\4`ڀC?>>i-BcU F5ګ{myTCC0߮pyLN -F`i$$4x}{M*i}f00[^ZnYX9YèZ Ψr<1|FS^Lu<3,jHșL/1kB&ƾ\rmYQY[I =#-^LK)rvYZx`*PXɏY5C0$ VPm,soVy5鬿Bg'F</ff,Jh.x^~PW&#cs<``wzF#s)(,6Ġ3fw -[ƽ$dn#ĵh -qkm6 - nwp@Ud"Y2. 2,b\b!R[sXWi+~`G_iy:) gB]v1mV!-:S{ԨHZU-LOs@*bU4Urә;[cg~KjFt5Bcߦxsf m!:B8jq<Ѯ!9 @rYUǼYNwղ8. +JԶVɑKEW!G x#W5i>`˱灑ㅖ5;OieN8m>&h ԬY?y4A@]ʢ<ys\53Y[d>l] -ڿ0[5ZUIF96xՏOCCHAe%UBS0'YPǪxT + .a%Eai` ~jf7·X%ڵw0JuԊwJ^p8 %F -}fJ55vCby;onOks=0YND:'Ak]t1v8+inʬ'TٿXg# jZuSTw7u/z% -*e2: d7]~Xtu%<*0OaF֍axFm^$W4hFEErC.E~Kږ;r-ggA3(nCyT\dd6{2^O}mz<S 1Kd?,z -(0' |CuqTEb4 NYEǻ[SlM)"@@2#^ދӌ^+z4M@Gsў90  Lt_/)>3n@OLěy/ tz%K#ӫ]_s)(?Cӣ=A5kmuO${ |xj$[ŻKsC[/5ڕ @=OAVc:}Dk/~yL؋?~6&tvYޓL^`^*Kcc&0őNYzj df( {jmo,AĬdOW*ާNAn 5Sk#fTjsfװޱ(īJ6 `DN'bţr+Z\W h8+NH&y$%SR=DwpIJMIp̾`,:-gao]I闱(ŭ򺳱g#D /32/q"uxNdJ,:;A#]PV5%GZLD=;9SQN\u% -FLLuJJ(~qL*՛,L3ĸȊ,'rxJ2'<+`MV׏gluH9, |=%1ϓlk,'2"fP;)YxPv䘮H eו&!X ֖PSA5Ɏ'zJA&p*.ELdI6܍y;x]l@à|"1*z#qzCT<栣QH>L3\Z~dPyOYQ00yeEa)ωfplM_xLj w^yXI Β,q"O Gxm N^I|&gHd3MM3{xn g̵U]]]QY#IhWV0i7dН!:bOSA:(n! UU  KrDr+@8m`$Hja _O $KRV_g?5ݮ&W),K7hIl|;87&Qz~UW\zÏZtWP&>Z4ׂNj4O3n/jdNЙ7# g)(,CBX$tbz.%<\ZB&[ -{)-ޮ7ioU)\aoKp`ED3KcܮMoxs_?𻙻b+a75a+tKs-OGuxDwpbo3@&Gvtmw+j\dN)5$ϨAgӠH_-t/!MD#j@H*H|l E>'ڡ8(aH2;@1?Yz@'fvx=jZ)_lj38g`j/kؿDkc,N;d<6@r S Y >Y-F1Y_'J0~S\ڞ#o/M`PmG'8Ym}40!ht7/8t~7z>lGVh$Jl(?dsBn;~MCA޼1H)nH6~@K#G)A&}Lnϔ?*)e+գJo2Z{\`Y$flxKxIdkbcSx|M2>D0{/djDR]1Am. -$>qO{QZ- G $ 9ހ$Z?  :1*\P"p'SVIobE-rQ*Xy'2e*Ea -0B/Tx}~pP>剡"GHHQE } pV0.za% N!|=b(g.'h>@т>@!D~tm!+~%̓BR Ä (]r_hIP"`c^ ; -Q,H~PBXԠ4"RP^@lU/':CGY& –&٪%sAm8F fs;ESh<Zl%:I oFD"b!C@;oF`KzX0Gz lȢp 0w?&3~?~:lC˒`XypYJˏFJQUK>V⠱SQ>T@#&*>$Rf]籑^4@]A IDgVBܱk @sAw0u5"PHa%(.E |>vjILSGQ5 `!u "$89v((_u h/Ddc@瓬nQfo,<`;!2 E剬#̇YP , z5I@gM$aI$5$]){a Pҗ9I`rXyQ&~v~%t9 x>DXbu -"Pa]hJ'G'`!nF p2xdG`s kL֥BD|$^ - !Ų>^䌁KPddRay bfȣM4P`ASH惩Ьc.V!-0C/'{8N˺D#x`"+F恁0 k0)-A"Q 9RJR -nǷ/XieNz}X3'Ë5Ff8h:ou!itGz -!}.6 -.k_VB Xh%NX1ȋ5ʢ]ހL"H\l_+S -k -bO/%&,, -''] >0hO.X^A\{)I첸ע~Jh}D=.Hc CN;kЕ0`w/:HcФt{H"rJHy0TVؚ PG*U ҋ - ݠcGOXHLGHKZ Cl|[lܞ, /&A`]c4whe ڏ bn&xy!V@[990E~q)hPO`p{qi!G -p8 tX.^bZJ6\ud*À!P^Bν0x,QlXA]PJ!OqLF+-r޲%^ K*7KGcꫨv0 U_q=)zX.!\%l3.rpk~mQg[+t׈%)kUZsޟ\_Ld lO6%I!4$"G]gm3`̐<$!X/~ ĬZ@'FGaXI\d-?D3w x2#X:ebpxcY0N -g͂, v[4Z  e&1' B$5$9iOlet0 ?ߔG<*0} 9 0ؘi J|*gسNăUÉZQR0 (г`Pb_MC ЀwX~ N<|_`B[ŀ〢ulBZ| /~Kp1>_84֤ž sT`|->$ϕAe*"Z"|;@=kYAP/hM6A -QSdr/F_ p%Y!GoSz{ۮ2 V >""(e)rCiD^K((Fe9f.\Jq.#6ș h;0QvȸgB&@ha{eZCx[|0<` B -h9D^'! esHځoEȥ89R2 $ *Aps0%O{-k.+v{02oڟĩDG|w$ ʨ`t׫fm]^ekmyBz=XFl*׮eg0;[{8 4ƨ*"v4oS-&1]o6#hmzx6Ϳig_]|~߄7&{*p$0|krǮ~=-؟.~^+phŶ9f?eVjGo L7vٶL;T60 Rg^4fGC-9tVC@06`h>>~_RTZ_uVzK59 ~{%ٿCJpC:Vv? t76n1@M CII1 Xn Q+35{EqPu<wꀎ-LZ 7IOM lO-#)^n58hng?j;F~a;%Qϯnv|vٓ(|gbc[,NHOSQ6#\eΆu@yCJ]DAڏuPhZ?)9`+͂q:S5Jčxfӳݾct*!o` ֺxNj78.`G\͸cd?1B[D~S?dgJ۞9CA),W   -XDSeHd`Kd75Z,Rr }_h(y4*ϋQ2oM2b4ȵzvs;. A6D ~X@X|n;a _b{-yytyc<"QFLq4t}h,Xvwjୋ -h~~a.R[ NHv@jbۉD^aF;| S?xB!O)vQMzoZ$(H}`_ sIo`49_ -Mg5PgtG7p5 o7 x7[3o{G]1oN!v"Q uKfM'*5@ÒM,|5Ih^pKbw1.1?!/b0Uk#ҖX=|}|[p6ŎҟLTf-YQyPmٛvtV51}M3hXۮQ -Fi$fbAS%(%!9;ux /X3` -gՑ]w :1u?LMyd_6EP.*}DAC:?b6QPr?Ba -L|vaj8Ww.V_C.:(h<>n&L%)jZytZLL -mJ)a3iRmъOD,5p&\[pE.!Xwg>=WnBor^,Nz?Fr@v~a5^ߞ7WU}*mi=zSt{Q\/C*Aq{Z xҌfsCGm8)M.Uex3AViB6r݋X[zr{#ۖͲ(2fx6S)@@D3F$cZv8:5r -o{(ˏh7x#JI`U eȬxg q9ֽoNIWJ"%@dc|0QCz.cÉA}o 6d ) ĭ|\?.,Nqʩ81OyVa V q=]Ah7t)ྙ, -% )I]jw6 O/pyѬ*pԴ߻ %5A(8h -?=Bt!X+&uX̛TpgFqPumЙ}zQ#}=g{IvMu>H4mGՕ}Yч Դ_X_gS_3fMB |g&=Z3e3I;eKzlɛҥI1a;ݔJ*m| -_qlB7]g*s%=eqM㘛<ԸΎw# ?aR~ώm!4;wxRe0$w뽛%;D}xgNf<|u<1+Wgҹ*7yX'l$[tiJ͢#S)Ȕxg^,(* g|M|ӣygS;||aS vjO8R0t$]taXܘ›\M}vP Z"@.nHfq.An]xO\n}zzJL<\y/Ftu:0ӥ>mJ+znl7vvx^bo!k]klzB3淐L댔31tѪx"r:Eg4:{)9Im\OcClH|yP{}QcY oЫѤ4|RaUOM ;sbbWF351:>~|}2J1&-Ƽ72VmEmw(g|j^bWJScw0n9I>DoL&}8X^M}oe )roĸaMJ)j35wL4Ѧi3,ͺaġfy1}/,a1stdKZj;K{fXi2;˲\x;XTguOV:HޚɆaaqS.m'Kz%9<)GƔ1]w;[=ڞc+f&@}-~)尻/;}s{k랽uE>6ysP#bwviw&g%vu䴾N*[wF\gf9yyoƹ.9wL1 WUZ׮xmW\sw}:L #RqnԲݭrEqވU=I]O5z]0U 5!9qef Zt?x -|c"{6lS#*㚡>Z|Ef ZTlYPTC=9 -L^*,^[>ۇ7x֘d4ƽIoWϳO9|c;nFηw~ܾیޟKLGO&igh=yGȏ RI_9JϠԨT &ͭM!3?FFX3duB^2.ꌦЬMߘbrp:mمrx.ZюV?O}"^mGuIEc-zߛ-c))1gE}^?W`c|rw]uSv =՜'`xjŇ&,v&=N|ZzӤ%2LdiCldΐlS}oT9jԸkOaŪ۝~kOGcL2`e_u̾d-]6c_r-\vProA%mDʧkyiŤlbYx| -ܶh;P(6ӷ5՗])mSSSaIgFZ,4gT3;>Ag^~2򚮺+p5%~e5jC}?Ƈ@vAVOy?z+๫/ yFj4uWG3м4k!k9ُA|)m3?ڠxu,?7Bt4/V:w"y,:r?-cx\vlqe-|] yyq6u" -#Wg_*|Ė} ׹_Bbž4u}x}ʷna ΢$NAAMO%PcLD>s6:NKd㦗ihM1͔=Mݾ=Bj.֜vvtn1v&>VojTo {ܹ?ׇ^Zni˨gn. 9ovF] F3ջ]De0 8_)BJs7,[gj.hv+sqvQ &rfv[֪GTw%!ɋn*[@,7-ْOe# {9j<볧]y^O#8vajR|^Q8=>_<ʗ\~ɫDѪBU8K'SAyE!U3{TE;C{Te튩$TFReXF U&Esճ\)J`l -X :{׾iG-ޓ5W]6&+]1sZ-YϽ3ҏ ms.T~TÀfJD|إ?'an{<1eبONx7jf v[&cYSyӟoI߳˙X˻D:bMҾ1D/ٙ krۅy-ڹtZs +1=~k3hK)4NN)L -aNWΨU}"XI&Qv4~wLI?%'h5o~]2I6}39H4ӟSzc?>kD5ǍW-41%};Kgzy')Qͦ?' I65@ԇ3@kYJTR6mX >~~pܵz_bt7D?\MDYjʩ74>"wi"b|Diӑ\R?lj`9n;`*Xt*s"9&؄?qɝ "gfFXD(k5ol)]gew̩:k$a!8,f&̎  j砖_}s|TJݘ&L'˝%]Ìad(*emE8?SXuc: F5g3i?]8n2rbiƞŪN4OF8u8Tb^hgEPE"[7l$GZyni'IX_=N 7j\<|8…$OKQLqOx$rCH9I>Cw~ dfg>=]5b㉕w^ΘA<"sr}%F鴦Noܘ-k~e=ooIDĕetzdPnoL' -'"~B>vz# ljp&;`SW0 w!&e1P_rzF0Τ1z,WÉ Lh`%1ߘ4pPw}a`: ţ:ŝ $pK%>%,өG+ C-PiTgV$~|I(;Vr䈞njW*Z'FYivq'&u(n|^uj̵9DBbONƒeR[I/䘊gǤ ii^dž씜+-1u:@ڪK~4 <,4f-&Xc0I9Q鄲:W]F\x8Iͺ`efUϜk p;С'.k -׳x[!޲X.%G*]7>\B(;{]zpB[$i'd?IٶBR<^U7"5Emp]Y좃'Z{* ,%?qN4&Rs5ᬾXYvg%po<}TLb3LsW< Hk-O>_HT0t"ctɝ.|23@ȟX֢LTK:Y<#"TѴr=yl_":ssC5ɹm` -8) +ar"'_~W⏺ 6# ܧSsCj spݰwZ !ts*;?81;.GH}vlfuUIYN% C-ڢ=tLF O/G㵙`~(o܋ܮ:ڊ sR!ߘN,oG_Ҿ]yw)bLx}W7|wHqK7 \Mߗ{Xj@;ճwnFGU0 _9O:_0tqk`yct~i[0c 0j{:*^"h+f -:Q|D=jy>@sW̗7 Y[O:䐝h~.#YT*#Zs6z֎CSI.u:mqcb!?2] ?ſ]IcMO|R69L<0Xb`ߝ XJLiu7ےx3L$ؖ8ׄ|kGr܉{m7YL7&9'[*']x)ۑQM@:GRfc#n0wcWr0:E͊ǖ4ccc*J8\mޓx˸ߟ a':z_ցMKȢ# -/Ims<,0霉lK M1/JZv~G]B,\? dϾ{gOӹi&Ffcm8k- ȹ|E2)=tpp.npgJ&f_G3> =<*;3{e~W - -nfNg˫.MXӨda::1JCʗr]Xz"& iiZgEOwbJ] :[:|;ȠYFu qYhȣh䂉+mdhs\4R~b!rH,z#+a@5D.h)'C1gz <6 bc+_/cG9<^g[Hθd"8Rj2,r-tݎGYw֐_wh 3Ps,AZ!EP=3C(FI/$v{ZV|\£y cc_KlE"|7 !o&<C/ޚXi0fE`o7qhN/*>m -HX m_` w4 BZ^Vo>ҭǷVKSӗyXxMJiXcٞ9=<85)<;$ [ {OI.FJwkL1AT4=:c_ xAl$;$N(89㜊34DRLc51v]fh84CT^K.\d3y'<}~Flb߃W&њX'lإXr,ݶx)cDii?0e[DThbXIKqu$Џ՗ON”}gm:3mdf1ƒyhJl|θ:Ix?:'fzgJ(Vp"ީ{K?4fN`tR Cj҅E-RjZKKٹ_OaOϒQ!X#$ASA> -c.LәXjkg}wÅjN0Q38iQ$&0`+?VO oRwGªs8a 5wA,]=7cG(.&((+U\1T`~s\æϡl3.S/Ql[I^GvY7&7[{prxPMa5-v{ 5C{*~YSf/ OgV#G|O&;>_$ -1k=Qq9{/D"!~5Ir!&vzJ9uzyQcߘtvgϾVfLf6HVrL [bʻ6kg0Θ}fbh&1@߁#әn.Ÿ5;i![AhC[hx>(=+L@#:vG@kOOg73؝YqO=K'# g{uDHٮdx,Rc%R\(m0 NȎg86{Sh.O5eK=ǚ(b?#6p!VF`mjx_ T&К"/7ggNfoߦ xfFRh.4c2zWFto;tTž˲PGx¼Y~Ju:D6HKg'R@oLD-+3V`*)9="hI t0{qYEE9f^YQf\$j1њ\e6b-v倦-"n@aWhiR@ƻM/5֝ݿ<۬@[Dhc@oLc}&" 5=f㑨4MQ/d2z2~RZh%y>I} (/cYmpI,WEk%K୩<ٲ( 4po.c6%^4wD\1P#-Pu: -V t6G9;hދ tnw1@KTpB4 ,Dйݳk J(5n -%D@h}ߢNt q.W}4o8a{HuHi\Np~z!A2o#^#FMnXjz -Z!*4@OD <}Zv(8|~*mہ9yܱOǖGhI}bbc%ri|+m{tO_ӡb?L](s_e[Tby6֕{Pze)_Ib(˽m֙ v٧D?}Z<:h 2l>͆3PlzEEiW>X,Pl(E07Y@O(eޘ?kDVZ6(ZȦh -fu<ǟK-势eZ҆n橪?IkKúDԩ4a s#.ZhyXMYG2csъOE,{IedY4qcYYs |p-\t6E@%=QtS[\̬zjFPr~︘XȠ=؏avNFP3 xDo2?!B/I -y B[qR;G1AZ%5?3/1>Nv|7<_C>I ->k̟gX -gV-~$VugJYʽ~]OyIqq)O%EeK(zlQcka' eͬ葦]U+ ,3dp#WҴtb[nUx:bxp޻VF\&H"vFbQjn37b4PZ$%aw{ |a3rOiirnȞђ8qoӵ#z'㠎tgΤt/]/u):Е=Aq. t?/&[dfJR O(zD_$/ypB>'Y, 2N +rJ_q9%ÜUb`35UK7k7hzZÜPNK>+^wEY]Ysh1%y8u7&m3^af fpeR4,\mytXi UkLPuQvb$ߪ1޷H1D0l/}lMX䥜A9VRASɧNE lUڪL>}s؋̣-6:Yq-ԉNjY5 mEBArOSl8) m=,{"QQ< -]\ᓳ$SnymT@<LP,A #)>dHA1]@(-ђ{ۛղVP8 ,H~[A=!ϱkSdm;KA.#OZ۱R"%.`/ u^cp."pKz%ZR,(ɊQ -Ɋ"$ˢЂqC04Bf0I%T7N^A>pli+Nܘt"(Ȣx yrSiLAlJ9FT.Mkhc2>Z ޻G"/v",,ﭗЃMyh|^:+~F4zS=ݘ8xG#M9Fw Hٲ@SC|[ Oա* ? -~ )}]rO )m'ռ [g!oE]%X47oRYSVy7:a퉳ts/G|M?뽓/љ`:%*`iZ)+; )|zcR_ r_'cD\N&RЗ@%nnhxOD0JdzD;zX%ڍ$%iUZ_h0kR\/.bl??h~vIiscJV[6Y[sGt_Ɩ/y12K:3NBg-UB$+f \-K <(0k&9 ޏ6^~~{qE;75%vpgfu!ρ 6_]?yLw?ZY؅6l_e+`Qg?_tZ !K-} p?T/'UPYb cm(Ѕ}b ~t$$$8])H:a[)ҩa'jQ:%s(=$"\5sгQ\iH$8GuySPK)G_A1Qɧlz|暌QHaqwm ݜ=Z31\*F(\gb |wk,b-7&4r42t,毬$= npnsuV7s%]T7cOny _]VЉ*]C\Ae/tՂW)WRC\A'~PC\A'A rBU5ttZjq?X -g: -:l *j-/_ $Jvрd7mV/NM_HKZ:_Zm:v֊tUK1sR :S6>S<>Qrh'zd*U"WJ(I̡\U4IdD ܞ -Wc ׇdǫ:.n4 3! bN9iĘ-v۶zIjnOZfAU3*u&L"/wlԗZ6^U))WڷƪCu%}.Cgjy`# -Iرɚ]U`ȳDń_*q zJ.mE:-tR^NԅLsP#KtAuICqu58{'Oٛ5;{rsх(0ϧS5}k ur4i*qS2(QUwJ5r7*e<񀔏S>Iա>U&,w*R{w E+R{J߯T~NE*7*RQ+RQ/Qv % Dԫl.nPT -'-~+fF)z)B)W?(A%pQA)t|LQ2 ~RT6WUˉB{,Vq&z"Ȩ3a.vsWѸt:/r)w^,{=GQ p^8iljF-}eM=l5:˴!zbBPbRncŎEGT3G=CU5KJ}1ԒS{.:>4]Yj??Nzɍ/"Mwzr;tߋ\[M'j:N*&(^/?n|5T,^:'tpkVUIurB7龩ڧ9_SM'UK1j:X߫]j:)㆟;;tRt2ARn5qzcj:Ȇa5+;UM'g[n5vNԕxOEk~N:(\M'["ʁj:) ^Neg䗪oTIlV5Z%TIsuv]ut-^TX}u(.oW'o]haNg* 2!QMa -2UrHP* -4.'ܘJbU.+$H!+apDZLݑŝ#͕#s۲.5ws4߹NvZ%Uri+Ӕ |gsl2t͝jDq6Ew?掭}SNѦ \yII^gQMlriO]tAj2:<+F5ihQ0O\_PH"Cԑ 9Y [`CSe,u6~Ofa  -J%\s6t?9 -:Ӗѭ،e߯T>|+(p87tT/̮o@E%dz-;LSa결SQgr11V0.YR6Hz߫RrKU]fP+zr9ԣW*SN'_oI\vU> &Ey?^uQxۋRV)l??N8.WQC!U;62li(d wJ; %+{ou7)U>`WnS'vSON7|*p'KR{Ew]ÝSQ k_f:S7sn:t+W>?Brι|Cn^z -SGVTtv.v"&(΋eL7eLZ,Ѯi1-eLAN]E)dT趟VeȪeUj)bDWb~UELrDDM{aT~a(qXS7j\SnSŐrtW]I)ou~h}׎T0U=ܔf+o}04T=׸Jj\2# h|NFƍ)}h4 r5\ݗ}z)KLfb'A]TPwcZ?T%-z𶇏)ɢ2<.WGL&W* Ƣnc%rGYB=vz:x@i; c>#U9ڬw/ )7&D`s2OR&6|s V\4g R@oR t`%4y -2=w>qE{#}v!ێ__I|C =:B}&arڪZhU͕%Eumɴ-0}|dl!],)>+*>_.}$>ْ3{+9V(':5qN)"hMDIZBœؔB%I>Au>}my%M Z!KG՗1]Mݡ-n:>fV))Yy^Rlyɧѹ٣MRCլτ\~v65ƶ\J̥ɚ|XȔ,n]ߠ5^_ngoeqBVhՂ% --V6] ,-𸾝T`֏)$Qs~'ִqWVe\v=h 7򇬪BZ]"ķV`Z*uY>ɰgyڋ>lHe"쨴 +== ^nb7;"J{vj]G~]6T-B|ד#( ?Z^[zY]&/kN >co,5@XNG-%HSPeC,ÎecNK'$N>)׺S'>$ylk$,tGT'=V%F|q%? -Oml`y`]qq< SZ8 I7S{a8XV/Fc,ð0stҫ001ٜ-m"3A6qԇA6Db#-lw}rvj 3&JB{Yss\X'L?[mZ?}c>1 $dz^g[☏ͭc>Y?99W J 6Wab߿dwi 5ޥ ۸8%b9Rh汐5&7- +dXn?ow](]zfq.(]}HV[VQ|1SMc+.[Giء=C>fѶi WyuX6-?=:WqRXWƆJDjy]I|${F{Yr_9>VI[| -;bW%2S/-Vy2/=o&>m0 Od5lUkv]'*{ 5Pj[Re=@ tRf´LCkYŕy*YV<[]}z]7S+y)2=˧k\9,=tW$VEN[tl2_*IY J5V*V3HwJ2"->yZfl[ -iGfmUJ&ӗ $f¸R㽤l}܃ftζijk.'|$0~V'Nݛ)+7)0lםm2N’q>esoMn붷+c yg~;Pݮl h u.}C7:f0XqKY|38u3\xW񒹌ij!,z&-qt11S–⩤KR>%5H͖T67|0IW}i q?1V(Iؔ8T^218?1VT0Ky5K ;.`d c,M(5olKyKJjWiFY_X7AUԧ=_:в 9;˦ه ť'rhEkQcM=$pTaDr/؇ q☴zٜFıeG#FжrՕLQ&}]{lEƆ#J͖U,[V' Ok[#0|--rCnۉ&pvYYcR-4-Rqz_M%1*T0g0ÜU7g}^qLS͑O,*JuZOiOʕJmEw=E%MM-g9ti:UCmM}³A#z.4&?*>ROџ -T>u|Kg.9zgˮ]7#kx9mQ~p仧ɇ*qՓOgq; 9wb$~>=2yhjzUqwU␔P5CLDC1RXQ[hhX RZ} I L)&G \O2I^_˃t'*K!*&M,7' -= 0p1>XM2%iZ)cUb&XpMU=Ĥ))ɯiU{p6eJV7I̘DU'7SMAnk)I͐䍾-HJ'!//cUmIa#C'DG~:@~Z5Ր5̔1֍1q8TܜK!)rf""]5{ˤURk 0aW4W![I{x] X.&ӥ$DXss( )rfXa!VdBUoMa'rzf~$@Jws_ .Rʝ㻗[1" [&kRgfݴ#Y -.&;kEfa$ד(ʊS  `RiWQ3f2TފS2VżaZA DxQL!1 -B SLFǙ,dGҶ?W(+2baeô, T] Q4쎲t7%^]vV_nO|W qBpڋ'Ԑ+s$۱U?.%Â3sKR+\q&dU䑐zO1I$I#Ҩ&xe@$DۥU7I {0 D䐊s#;.V <`yJWכGUנ*lnu\4IA"&oTO_O#(I ݲ*&F/9S3$)Tu3!{FcÝD #T@<MxK_IRLJy-,haNفD/ŪdU%uXpItoCO* 7oǨxHI8hdI{<A!yqSU2y7h̝nx!2yFa _ NZft|?pC.lV@rpJ iqaU;qƜ2frv{ӽ+qHJL__kڟ* ikfuPwox.i(5#dbL㾾Wg0'r -JKfX&m^9I͡n1wG L+v957=0BJCZ$/"GidR1I_Qd{X) caDR4w=I^{5E19,̜ eƉR?P!kS+) fLI219?{Z=]J"f# Hɰ[Q:?,'MyDS3q=A-׺LFRFH* FHe5`_>S0i}^ذ]2|n4涃47Z;68$%,DN9HHiDͲo&HN#;_uȟ: #&1F`{ Ӡɪjr2dzZv'7@*7CRy}H"vSGM" 5yȪd?l>\'Wn,0'LW,[ נb矫z -aHKުf4V@],υC):[jHFo6IW T|fh0s/F/La؈Q!0I!rHujn*^?ԻtIiOjqfh},zC/*^b2htYH 4itlkO-UZei)nwN7ug22br21Iv?kNVar2hd psg&wwGG-PLpk_o.RT@y&܌GyN) 6EQLLFOzq{,jx""襘ԥ~Xa !otϵHy۟i)iz(&o_߿C4y)_CZ`3oB"wsI#@nY9q~ַ#!ުFٗ4diȄdߞj\4`rVOjq X$MFif2B 9ު$pK]uz8D^f7zw7>d&)/2ÀI냀*O80G`4De$sgcznz0B"G۲.~UcV:jԋ}+F"oU3brD}KTʕ'%kuBG2,mH`L$)$%n-Vf|D Gr9FIMA.70g'9|*29Ƭ`C&jFY?씩;Bo  i6V3E,`*m&!Y*7dR -ZNJÌ(#]rHF1[UMׇ͏YS3#19Tq/Ywշc -uv.0]S1?|TE{ I5 -cJ\L~ea"42a:u<;{Π'AլHIC 9^cn}O wE-C)8و4&){=3`rLCG/n`EBGbՊ#(џ yyȱ}~= _g!E7"PͨD 57d]e5߻.tY#hgpAogoDNiJO QpiɋGi7&% V54r\u+틯u_WÚgЌFoߎO19ɿh>}۩r 9o"by!&#!p@pZU8%y;1w-u͘U8ftMUe %L@zhb^ΜܜKQ kyyyPjn$Qc.%iL\yжQy4K䕈hX{C!IoňHXjxŷ{QŦ5ƍǻ=mr8̀ɟ>%Zr哑5q$Qisal&tRQsj&xŸ6:\WO D:ތ!zH$MNvaʕOAHrTa.gV{j+ɓ4ӽ7RäբtS桏DFD@Z{yfY LtWNմvnh0HȤ"1` ɑgp| oT,1"YzO;[1OQc$ϾyK~0 Twjfj1.&;՝IT]9C^CR8>5/U[%>^`Z$`j7z>$ȎOLɏtL&&gP;t$B/vp0hzUT7 aȺ<8J<[oFw7y7kW.V7E5=[1V3hHnɬ?0,`&pEpBcnp -RiqDf$q\<0-y:4շb4&GvNAẌ09tQr$5fDcnţ{TN#ra=BY8TVU]8'V578m9̺&|v0םsm}ceG f/hhbE^!E3jOd^R,QPߗ'p !@f^uN*0N3T#Om2~Ivt(a2i[U͘ljH"FwÚ]АA&&G4W-2,`J# W $:@!U砣ssx -3rvU֪HSwotŨ'hۭp#ϾMz*&#\/XVFE`fy[Z#zI!$R]y#R"~Vf\$ˬ&J%)}K=`2"i1v1fI+*^+$X# 5p1W" f$Vn-X߀tnL9|I-#$mkoxH8t<̀_wȟoI,&ͷS2Mel)c81Vvz tQ1)t"m,wL7 Ez@S$4;~_>(S֖А`j4&$%`r,]O70 ,bz Q^Qec8~= ;AX .C'$b{}/Sͫ!'yjݩ,VQ~Ƙuߺ,6ſ۔]-?zUvoD*9!ѪirV9āZOeߘ{,*nX&.&h/NCB譸?sU9?~9Lwf;='+eo$OqYzg춆[= 4`2"j?>!>5ʕ'Hkz0$G$w&xb9RjYzL{}|k !4&_>Ue}$'K`AOc$iAY(<&Ɗ?C@Rx2SvK0iL]!/YveyriGEELj`JKG=o_x@n8d1E%8{r壚'wgqQWxIAKUA0Uo]E'ouKГGΰ&#SfX+[CF@G  -'#O`V/`탞gC09rZv嘘TdI'L!8:xI'AL? |F"CIpv_<( -4=ؚZQ - .r eTg@3OI'@4+tJArf /6Z}{=wwP˛Г`J4˕Y|L3YncideaB>JA.vT$rv 0sxW= Nk,`jwIUqEѳ0AŚt8 :A孞_G$T8M=z?q)阗O&9<5Z} 8e DC.}l=JAF.&!)A''䟷˟!J2ʕ''U#j sלQlQ(ՌgQT5Ѭ06Cr\yRFT0thI } A -ϳ&}V \n -%8Iߕ4 QnA++ `7t1Y*.ުA͖8y 1`3DLjh8&ӣzvk8I!Ch'A0A^/!1Y RbW9I+YE=qX8}2qSj}sWBF)9uKIROolxn԰e{L!8|M)>m $3^'& jn)&$]&8$f{XLZ4X$Xƾ] 8h+dߌ#twW(c s>so@|&&*V5-JFoDqP8zcH rG"#yp| ߻"$B(G\>V=)B-8݀ 8i9JW"ϡ¹Ih zYfTNIɶ0YQL7Һ.sa(L&M+ܟkܿ,pEUs\!?_=v;3`21JA0 -=v` -na(4-|U5c)N5VaNG}]4IK S6n4IQv;0Q:|W5[J=`2*S'INjI(-o4dIH}|`c'rN%B(s|@w/J/r;8r UPD8o= /Asis;=j<`*8PqxdbSTWghqe'#?^܃2A 8sjaj0:w$ -u_>#3wL6x0N:q֫n+^oB(s]&]/QȾRcժ#!oI4Nn) 8;M]ހ LBB3rXUpLi%uu\0szS@p+ܟ}c@~ルnPi$l.o0ȁZZPГ' $&;(G<=yb2HͰ=\Oةf3qzt`9DR^t}퓰R:d1AOf+SҋYr&xu8"'>UN%ha~&g'y$zA|v^gTd6(!zv#bJpGtzք^ovviΔ,`oNy{ TS8G& QVJ8bQ~UX){ћ|êZFoD !K='쎅\y -^M(ΆV-X'w> vĜ?-PQBOf5e!\bTf6]O2!n' uB~0dGP^(=wg:i꿡_;qрI犬 ?K4Br=a;Ѱ"P<׻6a&$3x_]/SL)1o,*L=Wd OSUtg7*9>]&k¸ܿ,!1!|2-v&p"y͈&S'yD&փn n6Xja E(G $WDL~UP!Iev񓀳9.aPCFPBOfyaU9ߕBG攙Oe,rR d123M1@FE7 P$/z<%~z2&뗏{`U͇U$0. &:qzRГ$Mכٲl?|ҰʅUDLI#{X$o1enz`l^Ü&tz_G$R6GcГ (&٩&_#`2S/CTD : Q=" &+ݟ:y2 ==~7Tʯո:J'{Mpv;=`DqGBo5}(sc4^19ɯv~ħrLiw氠Qv`zR -V 5 -mG}z*߼,@*I#`gO˟!Ji' =Aq:xQBpti$$Q|$;DL~iXWv'n1 ň⎅&(G |ɡ4~V\!bS Q7XuB.sl>>A4IjPg[2$e>Wg=GQL'yRIc搰}d 8<޷;x@BJN:0KҧNM)8LgMָ@03٥I޹GrAB.1%19o?\'7onASDnB7NnTcAp2\J' W )s4z2b:Ld,yI՛=T葓AVkG&19UIܦB$Ĥca^z5EEx+ބ99W7 &$_K=(4>brv0.vf>|"&F^na7Qݶ0 zsIgC zwG&0^݂99Er)ov#%gbaU(Wp!vGhIIByDm H `T>"镮AOPLɿ9FB:Ae'Q)ȱqLFQyc.E )o=yU/ڮвDLkKeb=\T DZ^Sh&A:r?xsГ9<&i}'t(WԌh8z,:.  &?#CbN\ј=9$QߚwA<xC-I"Oo؍ &@Of?vPN)&!C~Nq%avTn" I^ja'SgP2I>5[ $q˳4D"4ŭiEX{^\醊&G427f\T ǙdND  W&-9Г$yh?&mٚ9z,'s k> NEQ^&A9,+j>VLaUY3[nVܦvw1 `Zs?} X)w{!&bz#_Ej{ۥ `2#mJ'9L]U\1'A0s>=#$)9<`rD&q&3ŒQaﱈnn\$_:}eݺ5oƜ'k7ې}qea Y=DvH9 = GބOyQ2y~]gR=I_w M"z$2NתVQLN-LkȚ0락|cI0ʯtH;Io(p{sw ʘ;Z^Ü&\fGP3޷;;e5ۮHI2j򏞔>sHz4VbҙЍ[ ΠNg띕$Wm1!0Gww `2s zMZ5NL뮅B0N`>ˬ%$eN=˕P寞Iop2`/8NyyZ'm .#,7M=Ĝ#Ys^O -v~ׅ &Dzs 8 znŭ[A (!Q7$e6'vKRLLF<%}VDd&S32qZ/0N:EZ U -g7:<#޷;H4Q^>Sij)II A8叨dDo&Y[ҵӓ<`Y_-&2J!1TpgwTYl .5V;Cun"$eUno -D$Q -੔1{%Vv2 -=ul!ġm-T6Ua -$j^o$dBJ3`{ 4L\Ǫh8ʘ;d$עL:G}*<`rXIzfʳTL*KqIGj8 '9YW;&b_>鋱rqg5VƼ3L#'L /GEtFE٭CO:FEI 7%R pej2+emLIђ;[ɍZ8쓎n )Ă hKqo!;K8m!4`2" k/n+2i&z,:6VaH30QfodL|Sf73Iu)82q 'C.vCO heGU`|pD۵4J(WjaogMr;O#a^tU`qԌ~<sed?)}|]S/z=qn -= DDGn^'@ZFPrK^߁!aA@O:9 =iΠ1='#ov*g>_2SOq xqҡQDN 8Rc}AM!vXt"ISa>DQf8|FfPY!`2IC 89UhD'ox. ^ Lqy8#9FBg7ʘ; z r H*]9b9W'I^} 饦4=qT -rt=Yn'AL (3v19y(Wde̅(āqiH0x?qS$OyNa(FzCRf&_ -%/WNOpwS LG8LO* L%KGo=CMaLތ٣'idDV9/^ &_Bi8٭,6 -F? A.F'}>Ĝ#{l'}0-W4`Q3Pg7N_A<]dL#:TPvrI^arD!Y7I%x>Vf},,rV&2l f);9(OD޿,XJ0I䏟>4z,Y`TW˿ո~.r3# R\mߎr 슄8ĤsB'A 4^>2ޙ R՜lJ+rnEﱈL$bg6a-0fyt~G,!,c{UIDO9i!z?J^iHχ 5x\chdv4<ɋvn֍7a\M0k1UG5-v"`҉CO: zŰ1FsV-zgovwC^ Xha솧i`WAyv)yUp/NOɟ˟9! &0!#'{ȊRq8 (?_7 e&PQM?ͲoP4"쓎^v#rbY)Ḿ$LPoN[r>@%_FEA  @!"65Mv>"'y7=U4`f5v_71'vx 턥a7 H]\a"]V9o]!wR*A&_? `stǢ` @"Ham?`VM<`?)}ZOg;NcDnj 41;iFT8DϏD'Ŀ'b;,`e3=QQhULOzuݲʱ$fwr$$Y_o5]094I$-9viQsu"awo5 F6 )^o{$k,`˕CLS-Dq'~$'AGSt8MqZΔ$q C.&_ɓ҈6IwzE h:x8| *+;GE'Z,mAd !jdTUhDE[x@J8EuII.((N_YF/0=Y;Oh’ɁZ.J7)Y&z(m[cC(\^V!)HV%fV{qIɖ(> ’I=$PLr2mou ju|s.)mcBF2t; zk;4 DuAc&'-ODyǏ,~1v.)%{6y缧M_[ ےX\%JdbDZ-ﲭ}Ip_=mI:ik7i$y['qӦf'M#.qQINw`\`}p\o;s?{YB m {QƼ˰ \&ɗ<-e4d/VR\ cl LJR۳R -m#8Sx7WU~& I܄= NҗVfRS{(x<qRt 47*b'qZ`Ad=O6IF~`0\&J,yq18M{4R(+-wԔt"o JibX s`DOFGnvX{z>{GTQ2tR>WR~Jivm{^(e᠖Z|1 07b"I[ݒx*$u*nH`{Ձ[kmQݷFbkd1s߼hZhbv\LGŔR(Yu"o=c 89Z.Ko KY1^kMQ -B\K8L[ -;EKf[ҚWrQʿyȎnG6/sfX0+=XL-bm&Tu? ߦJYX|Xe_w 2G7ZvN1B&f4 #ʫu6d)M'bͻE=P}T&O2(3-ybgK#)OV^ MWJK#PJ > 0.?Eyp#sF[VqMQδĔtSєsX}C4J5}9U_rRʯ+m7(є ߦ yo(-ѳ3iIF}2{Qbo20kttwoj|2B^ .we؞;&66[ҼXCL)tsADs.܁zNd?2[F!,3!Rǯ 쒚LC{)M'⓸sC>h/2A{/Vn?h./·AҜI]0׻bJ~2܄S9fYN' W2(R0zwiq{-XnL/lOʢe&eӎ@) L-ma N勵N#Ey a@ rcV27J龕HJ]ڂb L1bJ>9V;[V@~ᦌ.}/I(eUJ%^Kyz]B$]-p蒯^}?ȁbE v̷cI/Wò eLUX~1 Z2b;_{0)kw1\hTy90wTRmQKWg.A#L}JM$D|)ݪAӼ+=,Rb;Cu伶Jd߷Kjɤ$M - -g'+]a>۠|V_v+eJo}2vVc0WeE0.XǾe)OVXkkRY<5RKmls9+W{1 xqrSJuԽEPvVI(fH7:.9ܜ:32F1A/:} qHU;$?Z_bRzH2->z-h Hl@&ދ(DEh ?}j(b<"ƘN 9 IL Kˤ"WZ=;YE_/X67!_qgUK`; RK-jqv<̉&8{PXB!l2NJ˽rkYJ3œ5&2Y늠wGd-jۑQYxr"'+9'ypP_ՠ|1*]j./Fdp ҒA`;Cum82e))~~!ڞ;e;aCμLJb(9Pc1d^oZQ0343Yb9^z;K{1ƋFzLo!\&UddIł3KLFtj8%c;O8>&%zr9ɼ v^}b),ZP,0b 8Ė'xE"wC XT)2 =L 2G/֡ bLEe4y|g&Z&O^ b;{u%iCwے9IrYR>"$ojLBRv d2"ZC&OBfons r=wD Q*puR &g>9)ZI%N:I&5ދЪA+eGJ0(DL8<ӈw[xJ,=`;ߨR&uɬ@RSR\)OVXkR Ks="fWE/~BG6 tR6Ljbָښ-dQ/`v5b{&o`;C*'Ah(ICe^l/ZX|1=:R<|a s *^k2i.vVHDe{&_\Ldr2re75TtWj|tf)[4`d>GQQQhJP5VIHf&=XGEˤz/f-/WJ2dKo`d~HrZצZX Q{k^d# bָ.i/k͕4A)#[ ' Fes9d-n%}xi|SD ?<UJw+6b&ATҌ(6J̢~5no$40#%ů?l'sk {r{q(fGx]SJwѶ\l_׎OHhd,I*6[8 /k0d :IHQDD~gᗛnr`JGc@#\f`; *4S L:ZDVd`bvr,7K>y[]YP}Y)Ld轘:sh e JGw XUmN{-XƢ"XeznOXFY2+Y{&+!HͳP8%WRr$cg%Лb%Лm܈hM!,TZɟ*scI/}pșv΂ދYJ=QlA)s~6UFpT'RKC <$Ho=@@,0MJ/R.5W3(gZ,u.}RK?+eD)ܷ<; -ƀ=a.uW.wZp1* UJ?2#ViWپJd)mSe['CX=yy -zcB&3s rQUFQxFɾؽÕLP܁+hn'3ZLjyn.7 -ɀmAd.Wb`&XՠɾXr8F/M7UrOffT%DDgi)_t@v2IG|Lf$~e8S9FI}rU }g ?}M^g/"D'jBIB;S_5OBs -xs_.|%o!ƞUeaR)ELlnK^QTTFml]n)9O2iE2%OtK[o(O ݳ/H[0I%T턐4,E]-w:JJLrKF2EK3q+r'3.s kL &X6:4cW7XouiU`2<֯&;Ȥ@—qP_iɧGʼnB&s),!ڨ?G<-d2_r;y --D-!EiѪA3iwUy[X6in d2"nSeS-֖>ZBn#%Y9d&b O1Ĕ-t/%V v endstream endobj 30 0 obj <>stream -dI"![xi n/9Őנbvi΃# Wj+R?Gs{A.w-up;i{h[adv[e,yl#uVA)uCI۳Њ7@3èIq΀ދFC~B?WUo!gb(e/sC&+&ov6Jrr?6xſO- ~}\)@PyGǔRC#IXLcT|#%AX%B2Yb!2 p _a@&b[(RQ{1u2YaIȤ HT.moAL2en@>.3)[ՠܧ?|B)SxsAȤB;HE^z}yM d-uVY-\j/]f9_AB>xR&ctRuk2iig zi5VwYl.+.i(6f[& Й~cJ)a˨Zm4g?/BX +dO;..]&f]^$_(LbR_5/Warj,QJL@&s ^RtN&`@A'|;|u /IuPѷzjB}!d2gKA^4nrK'r1mPs6rS <9I}g N!E(~SeEe]uZ$Vx8+d2W(W/S2%_khfOk ʹkۡSwA /Bq Rkkі k| d$|B zh$5SL:[}e6w' .Ox=C!t ]Lu3|j2Q -Np<>R?$vM%,U`5* ?QʘL 4dŠ&SAe^4w}3SJtc C-Ehy+z "{Q*7jwȇBLdd2nދF2!¯Æe)_҅ovKկBIGNgS*yZU%> @jd5jbH ?WʷHU*2X-jzf< #27yh9H _hhLPv@KE_eCFNRz*Al %Jq#oDl=k+VGU -ypO@p<נ`;mM{;L*ee+Ƀq/xI]4EKoo`iU}N^flk 4LZEeV:^bڨÔ_qVʶ -[%r>W߼ֺG5Pz+rp8dS5&߆C *d2({Y; -zk20BWJ0l52 CEE3'.4yj,RnmHׯi@/ukg*L+t[EEjZ싥R6F9ϒ֊hIJ:x=UrB+~{:iJ)*:~砪|qU-̙IV,ɜ Zg {!~62k.H0)LQ`aKNaX5!ěa{ۭ -Le&ODPRhD̢zMI|VM2mC r6@&MGyMBE#`UL! - _CcJa^rP - MTFΐK2 8X_by~݃ty 2iz{8얮I/qJ!{5f%((A)*J龕mh @eGBfd/c8c JyABRJ.*ͬL%v %h[&7Iw3=m!z/PPj *Rz -e>ɖ->RjUF+7*3AȤI!pYw.z^%XJ٨|>[,|OXJ!&視7:EJ}eM)i2 4%%NyTKpPGW؎98D \ V\#VVpR]>qp x2Y\Œ1Q0{ՠx!tL!(e7< ̥ z*̛y HLzL~cUL^4ᦂ+³*OynͿG|c#drPLphx2%W[_b!<@~_鮶^)Y_qynGVڞ5ѦA$*Е<d -{QAas#6~=X*nWN[aGs>nWz?*:;&dɤ[/OILt; l b [ꪲ6xL{ Rl'$JTW *!΀)b[[EaNK/z/ɕQ!UϝRs9=g™G/ -½2ǀiGnmSYgFowK9 VH.2YH@@PVCE?t׻#Is;/. -Zj z!` -%o?PX-LA,yƋ/ƔRPGhC'oFfm]QC_iU1p(%8$ -"UAN|Zj^?(%0\&LS< -Qxa7^eGӱ y_8?Y'eˬ2 -@&p쨲t $/D9- w0A9WʷaEYy5zZJf`/%ker(DLb"IދqM~qs^<#ѓ7oE-d'Z=b9ݢ[Ƞ%I H~*g-y㬷uh8Ӑ#ydUR&)3J7" n TLTXɓy2 ҂ ^4^mȱQGYo=#ן G7$V )+o27C46ǀ -CGNlC'=t29c{)J=V ;4s74 2I^Ӹ2 2f)^X<’lPH㎆K@su*M^[' 8{K/a_"Z.| ݀8  Q -0qYO>r?ͭFOqN)e[7]Ɓ ޿[C36Q LwLz/ -031-]i+\)ŔRc6K:ˬ*kB E|wE̦zCW"0 VjU76g/K9mt=j{JpdC轨3WZ_ټJYc%JiS/>k2iкoV~C>'S لom`fbҡ(eH}RWJ-]dMچX?-DwSTؐ&Fe?A&A!3,x @8d T ґVASzm2e)CNڤ2Y Q+8*J LXc=BAEpSg0&@PyS'ԶMJ-fDB놣 C)PYf˽҃yuOfBT)UVz}k‹IޢB)I9]nRki蒯+dʷgf ]TY`t$QPQ`%(yv>@DeJdr(V ' Q ԋNyU}47:R(ޤ[듂(Ў簝PJ@<39$h"^L1KMXgdD)OR|YFL";eR|>IPB&uA'K\*Fb "7f C)A^Q6/Z9v Z5ug)OrTaj5zg^? ">M2 d dFE}J}砪J+ Y A$MJ HLA&A‹Hc[/&XELDK,3 ]uC܈"60Km h[`L! @Eݹ&/n6xqur8A(cuk gZKA$E!JJє锒akZe%M">Bj렔 G2)Vo=ދB/8Zs="JI>J)Z$ '7>o= /A轨7\l^a%2٢rDJiXzL$Shf,y4^4lZ;Nº#XFd|/ -B) L~>zuM -Q@'mxMzp~;17j!+e?ǻX]J(%2LB&A%oi ~? _ -; x+xhA}jd "_ L AE}$`~9,S؂$BnXAep^\|7@ qa;cْl4!I>}"^)F5cd$0轨 ]vV{}AHI~ށB%>/³DCGsA(bSD ø'V~[zfJYl)J;dP5z$f -`2Ȑ:{QKޗmd^mvI$Bf)+XV B=]UOc8{E2ug_Qa@Db]2Đ=’>uǁ16W_AIe)&?L^.c`0'm^DwX(Z:fh]J(%ȘD `h!O-I JAD){O"RzD)y!f.27B/y.\o>熞."O'_n*TYJ"4~gC&P8\kIFoCXPͻ >B -ɣhi S^2 -^T Z|2 .<#ҩK י 1TPT.\mѓX3AkYR"K wR&* -@v!uaCFE=ˣ{@d'x+RB) `{&c p!>9Ʃ ʍp7QPR"K d[)U!.$XΜ Z5HFG- Îy$x`@dhJ(Z "~oyIg$[Pk;]Lz/v&g_9nDD(N)$bNKW?'?)7 DMtpCJdLs=m҄h{:RfW_y>x ᓈM)*uUwovW[*kZaM|tt:ˁ!Ň7L H qf7o'tь1QG/C&rI?h ~z ]F1sD[-=!_>({?cO;{OS⟷ٿl_ǵ?K6Y[+mCL T9Ĝ9 vfZy*čmZg;۠@&rw{d(3*]rlj`kvu:#/9ۣ -yA(E?SãZP0HK|E;.T9N4PN2'n;S_pxsJGlyо$|O/Y_8Ĩۻ!UKf(VK2މ:p`̱؍ȝ`295F t9g,wE)A7uQ2B#q L>ڜ:?!1+m>U[n $Q!^4^Vy l -O"r&OxVW#%!8ΤTdՙ;gxL .ϳa4܄g;s{/b;sy +fN '}|6&f<%?\AA)^{Qc$m/h^JUJ$"ѣ= -&HQ (D c2fJIl{f&.v {/cfT J-fR,DHС8ޭD^r -(@(3dU 'mF>mDB6r< OQ -NBXNӍvAf^◯4Xn$"u:'ȋ9 `ne!FՠtIZ>ȍ6H]2v[ǯ 7aA'NtEF۔ ҕᐳ?Z|2DX"%sB轘ZؔaBnI'SA/\"HfQH!b#AȑD :>|~SeE?L@n%sB{/6[mR_LA2Aּ>'sRz/fX'P%ּ{wU$BPwXHL22NGDڴ6bPfMN'I%z/<9<'J7O"DEIWe.q.ALK!~ͮ>?-"{.8KQ/D9-a'=:Q5(9Xkͮޠ`c<>2-1r`v %Ʉf,y'bsQd|h[$H`vY!JL-]eU05[|+xh@d=q&:RyẌދ~ -] |2c"B+VSrFT =~'_z7"_qˁ$Ћ-x0=|fGg3g[:9"F-[vD cd"6_R[ ⓈX!@O.`%CߤyX'{]}srtᓈlA 2AW^L^ÒwZw. k-y_9>Z6EM@_& Yֆ#9i>['? '_oe3<| G$`&}ދI}2ڊKE5oA>?܍݈l/-!&z,#@ ŠB޹ߧXIQ,y*HLePu ~ֶ>R͓KI`8hDi0ƃ&C|a~QQ|d1?~-pF 7`a -C9>yaAQQ'擣I%&`:z\kp(2 U`Wh1wvQ Iث] Yc 2Ҍ"iщ$z/$2I9`qVCYa({QE!4nO:6zɺ"(%¸.z(ވAVA-JZW$/~zǯ']:ƻt$6ZLfSJ -+f>++=R)b!:& =$/(; jQ0~*g /3Ƌ?BF'IWÂI<pb9Ή''KNvDP)( -/D)/AxPhs|ȂE jkkc)J,y# tqD; -(Hf UC! n|'$B`#[F" 0-#NNKMX򦇻ɏOOnuvQ"E5^g=q7:nI -.(7v]'Kag%OmJ,E DDHN] '轘ە.OktwBw1E b)J$B`>GUsr -/D LZ5(,_D|aM%{֮-^)JҍnǺA${/TNLtK&A'S.}V9ةmhA (:,"_> r ZM"ÇC.]u{ӝwX^(n9*@rH/yTNݒ:%VչHw9奄keQJphAwh$hrԢ {q(S\C}Cvw-PG|(yys(?- cQ-@E @vދs#=JOtA>D>X:wX9틉F޹(.Jh!AqQ;^e'AN%a(`;Ui0O},șzѣQHTD8qjn;ݎwQe{?߳՛W >(.JJ`m\n'w7OnS1i:f LQNp/WIp|+= DNA'AnB^vP4=xѠ+2.rWis,Z_}{+֊D."ʘ(A;K2`}6pU՛]+/Dɱ.Gd;Y@-J7hԿ*d_D?١]IyM:oR 2ɍ0vot~O/YSu7/[U)Z'{sh-2|Rwrsv0VB읏!5/!=?`},OFR妍pg8C1C3N;G/tw++dx{[rIo}ѺMwx*$&Z_ v'D$l?m1A^v9iw(5lrszϯve_^i|.OB~o-am7v!:E#&W7^2P$B2܄S)@~JC\8I_J^3~R΁E nwњWoVܴv(|Uv& .rԢ4:/Z'V? =v9w(qhe_cqğVHaMO"%aޮ`)YؒǩY$ g[/VFaŵ.w˽/yrn>%"S<9_z:$݄,_E>^9yr<=UWVܹr*VB{wϧ$RقXJ7k&zy:KIcZeSr#:@>B.|ON^5(,_ﴝqC<82v`t۳uKrO.x|)JLv?oR"/#9 -Ǜfm7vMs#َ>A%aϤ$רXN^ww8~#KY9WhsspbjZ -Q.^UA|_F1m|^gw  .51UYop1 8C@/T!3ใ,3sߙDyRJ#ڴn-N$S9IltD~G!#t'*z8"z;0ɁkKvr[Jv׬>'z&. hxL!yuK%kK W%o$UZ%2$U"J9,#X 9k/p]<^'v$`g Gί?yb<[O]w(*~*$/D DLlHu9>~ThJIiapIi6 XN3rO.())%>YU3ݒJELIƵ׏.]f-cO]W΢uItC':e' JiTU6) ~"Azkm -Do}v:zK;%l<3{?a܂;q0qd1v'o+nj^\H|rŲ'){' Jߊ:$s.yRDL~ o{f:I 薆CRvŃ䒵sٽUغg)J2T߼/br eyؖuU5| E]kK'q*1m|Э$0{)=C*Ҡ;'s婭6)2BI>4Xk҉l<1tHS\SM _K=?yFYJzʓ= 4>-ˏ֎F9VE ݵ2z*'>vNrD^O33U)\%-.wՠp19ΎH8G#NfH:}qmj% %}O^xcgVm[donv\iEh>y1WntYjuެ֖y;o(%"GOr|G 0;+e,sDrr˻lڅ}47.޾i?=0%ӆ#%IX~Ľ/%ygqͫk.V֥dME.9;9`䊻7\^vὼ,ha@d=qɉoE)9VYhj{vK}bͫnsX2v' >yQŒwwF’ ;'w]z^'/U=#2"4F E,_G}r{vʛ|YXd,E ,`e'?`gp>IP0̡1ˬ2Z.=A${Kuh˪{Ui/>~m?3$%ow_Pg{/>ӧڲ;B/*uawjnqƋk\M߄7Y7.^$KQA) <ި'KIPr4Ѝn^*/=r֝KjM$Nt;yr%~[X"q7\pǽUM{!}޺ ?^|<6IIO/!>x[( OhA vg}߿dR YVװC:/km,L&T/yNnz}?=Wo_[GnϼcRskͮ3 Ƕ_pOBɫ/$_y %NyjvSDsPȤ1ay9i@IbP }2wO&;o(KX@Lw r*E=@T+HQ&,yg~]lE7 %Cgi k-c ?[n涒 -{|ɛɹy[( -xO9MN3d+UjdR>Q} L#ח6>OrU̖O'$fRtQ\1ٳC^gʋJ%|[(O78_z'qVҺ.%o' ŒJ/yʺ/b e!+;9 >fZُ:ـ(}V'}&)dڲw[v4$1'z2h|jK'^j W>=ms邞JKn1% W|]]]kI( ){nxy'JׁUxa-?q6sJs՛}\蘓dpE?і{u鲵U?$߹i ;Ҫ;<9WArCފ7.]_}r٪: W%ɁOVNZLG'MW'|{ڍ_~hY.IZ [(vխ]pOէח#' 'y  -dMNrOM RriD{LmnNi-^_ɞ9irǦ5KI1d -s]FW[뺅Owg|;GWJv՛w^'ɗ+O^[v]\zw _'(ĉ]H^P&d0B)h+p)^ŅRiIIfU:#e)wmZsr=Ut;ؠ ;c9ejx'o]ᓅp  <3W:U-юo_'XQY jtҘz@17).~]T4r_5|rݢLɥk Iz˔r8w˖Shix' & Mt>~^A1`Ԭgyw~7вTaM2>EIhw0V2M34_e9_VB}Z|τ_)Y͓wK!G葜-T,\U]rOz}?e'$cRjO*̃kܯ=|ÕL=t#$Rn(u0>Sd>]Rzy'D7Byjnh.ge4">w׬zc}ɹw̽ Wo]M|8$?ûxO瓨Biэ!'gGS -; =7]T[;sD;6!yj[tr46Oj0%v6<9߽,!8v;α}>4{G:l+xn\n$vNI(^mƻw%yG2]PϬrW%%)rY}k ¦N9 }mtBˇo/e;E˖a-ϫHQ&`Uu[#|㛏.k]E&H/,6ٙzᏎ]&>ɻpL 4l6&|x> @dC)cЦ*N38m{V[͛L_)YrsYϞUNز$n{OC݄2܄I!)O^ -Gd~8!-[dR.amߴG7O֧?H.79Yhuk=MHO#޼7|@F=vN>RA0{U.G~ۜm+O8} ݸ_^G/hvCګϒtwx'L'ݱd?}V8OHq'Mך]g]eeOuUuZ~2"|NNm{(:eVy9"j:~]zsjks#|B_OΟeQߍIw{R7'gɛX_GcdLG,+h+h'ᐳ|'^#>95B ,=HN#Vh!t嬉B@ qp5>΋d܄N?^{zxRgSy2')o7T0eAMkheay? 6E.9dp-wT\r٪ū*z7| %'[l7:e$z޸*i2 IwD#K(_w[L Ө'}m= s覸]lZD jPJǣN>_y |dysC |r5'yI[&mN3Ar0ՅI Х{:N햾,wkǷc{ZWۥ솮z8z yKz܌g)wоE+0BF3 ޵]}džr^2tg˫$R -snt$z,`20Y],s$u{n(z9UR29Ds`Fn-38%$UB<ޒOoup]ЊQˉO.>O8ƻ\UpvCjT=ϞQ:z6.ٕrOwU8-Z;ZҾvO~ yGOMw^Q6]J|>Z_}R+i4iKuKF x8<$b_zpyew2nZo_[.F(:yeisq;L1;=êAp;bqܘqի{|i/X|ɚ;hz>YA/5yc&Q>^~GPM''v|.-0?Yk't6זm Rht^oRu@@:lQV\jcc++ޅ#agO d@vnp[K|rC4ҵf:#Z\EƑsJ[7g:Y0IV׷.}rmO.Zᓱ/]k@hjLo@iqQ83m)pTiˠ(oXGyrcww`ѥf3VBpœ&>rH]E|I? ~ R&Xncs{i9lkB@f]ݞ >P>R+x. -3$=dR\ow֭D,}v35M?}io*>L)4emWmeO<?1D wD9[Bdr㧝|%ҺG/(“w4/&mj϶P_Kܛ{DIđJɋ ;,#}<}c-;˽^MIGLن_mx]|^AYgU(/7K's{7-JXϪWZy6 wGCu:&&ֻ9C ^w-gI]ADnđssfTss{+K=|3eI̔~ܧqc՛}>x+HQ}wt eK'{K;\tly'y& +$?x{Uq"⓷Ts\h'ᓦ -vny$id$יU,yIIb֥|%Z B;t0po(6=R0a_͓aY]LtKGY>~#x,+mD##/(29\?ڡ:T)>`Jzgu•uIF$j;RrUNBOИw>ZSY^mIΦs&R9qsLj8>IAŒV4Z 8_yEk$ndU9'LMh\JrA3#!zyO5]vW{ʻ?|ӻ@Hb,$BIYa=K:-rCȞ| F)pRZ $l e{;a{XdK:H|ر-{ށZkA}@&N`#")z* -K+}r!y1~jvT<\R3Nrʇ7(e\ab\ ]<+y=~E59N'#$ǢHiΎؕ-[K~yuLF͢~rE?bzr!_Гqxg+682udshcR_<ԾF@<~{׍W)Ma?X2ӓ9֟5zux{RÚ;/UOuEOLI~Q#9F8{H]vZz]{;'~A v%JƏv@ιAX}61rzjM' =V3>C oь} $One<ڷi ?LħCvˑ渕&fᚳ\u'g]~Г~Г^|mW$a&IӨ,ŷ,Z %8Dž%Xӵ{Oގ28䏝y֟,6G %R2pM4cɏoť擌fd$hLES>M]<7PmHazr›]xً|AO:~;x,E3YRͤnROɘ{~-m$/vRԀ!7kiݝW,o&/b-?Sl (+pfޯOsm]rAG aqR!I9%yUi9?cE?1=9g$-R%54$%{>].0nR3֘s_Z6/Xwwr9;XMo|۳W$qbs̰ҔN%, ֑5v63ThɉД]k1=k d\-i9}N\㕋;JN[HX2+g᜝w t8bR^&0?<(ۘC84o+֟26p -՜}`zr߽go[y'RS%rHAyg3=y_O - SOZ-&er"3qpΒj/Wbrә)g}v K.Ȼ_q4[M$mi-?W2*5rd[mA{INl CAr_| of$h_{:EGf(lю?ŋ) =?Kd=} -:qp $e$܆'3՞,4e?/䦴f더cxsSޓ_~ۢkqz.ϪKQEy͢VĜɵcyr'> s *[*tܭ];nLv8D:ӗR:οzhx7qamI:fp8F2C #)=\a:}=".l_q׌7Ef(L9!N1>H؟ k' } T=IC_*_n4NX΢4nV[ozœ]bM/_RIDեRCgam0<()zEc\xc"QE_jY!x.oQl,[h~ GTr,MO)KK{M_|0 -ԓk"I/>Y_&bsIFCc6'kˮB6's^]>?xW^uݍL}΅#k/Ĝ_|?t̾ $%+U#SN|ahs[<[c"iJNd-8Uz|YLO۠'lJUd%p4]Q-OFr3u- 3yݷ_~pͣi[-k^ -)w:+v[mXjv׬uM!]5k\tdb'od\3'S]xm&=I'/6l9"vU`s_`(,UޤK=a">7龋ux 7𽠾ˆ۲Rd#*(Ԍȝ~"Kb!Rލa'(e;vQ v6EڢkL6gi31 =vܩTvz#'l+))坩ƶ.Rs]=w/k.1Q{iP+jx!J&<ʖP"UcQG:=ec:#ВVĸ9\&&'EX]xRbi: p\~oW,xbzc8iqN-ƥ eÜiWb]6gL,ޭ}'E{…(t2ڴbuəl$%{% +Ye&$0-. ՙR(5=񇋖n~^reSՖ/ݿSbXʘ;JG~Rnn!*Wz ->ݲ-Wtk[y… 'x[F tUM7 տ-UH=3z\;/榠dz7_jY+Kv j[G-TjʓG=>LQOx4v0rf]~ Г^\O >- HR2i4/ iKH CuAks}`~=Ϋn麋Fi.Bk^J#D)'-l Mj,8>ޛURXR alϕd`GB_~J]zE'b8N\|mƎoyp2^/ l") hv|;D<ֿ!%Y9={.͝W_cLc6w*&rW}-l#1#zW /MRTGOsT!6'tSL;HJ P_I[>&I=1 3~qOnYtᕷ*/#i/6zf'.)saitG1Zapr:J[V2{._N]ӌNafY-nCR -!mYHyϸ:(g_i䤾~`O.+̳y=97۳Vʟ/qU-OvrxHyR9>A,adla's DfJT'.|/V 8 @b)oԵ-R귛T*w/ -/9;^P[|m7]pQxn_KerZf@I_Xцf]9>pQs3Qv=8Ɉt:-IbΓ\]J?SVaD*{Ba>L >x_Oޯ/Wzu|^Jv22p7gT$<;!sssewJI&eHO't1[$"$6K<7S0/yk=|)L/>u>d~/H9b/=Qb#;0yWs'ceZ?bz =%R?I|BX))[8? >Fy) Y嵲v:Cl8;!- !¸!Zvnn#=g^"$'žx`ҧk4[PB){ ,U;"0s9$Y8Mz Nc6>1HjGSHU^6m M&)Wb ax2;:ՆIN\OX-\zҩ'Oj` -CRVT?גPUtR&,r6M2]i -A4$¯&jޖM]~bv/|0`b/) >cr^Hㅲ<4SJTȍdZrJGN -{3q'xdAj?D4`Tb,)Rޱ@iΚKt8+BDD)C9bf2 aI]kϡd.eJzҹX:Uڤx@kIV"ȉX } g !F)2xcN^C' n5[Itkb0!rŠX?đ:% ] -ER!]y%|!ԓ-YLOj zҁ`= IJ3>!|IӤ*+)AȰ;{cz~UxGtќ$') =ŋ'7Hy⒘Izڞ k-6 -(|%lɌd21>ȦZa=Xⶓr$!A`|LQ}6(D2=XD1"9_74!뭹| 5'ܪhÁtM.1:.Pq|yr"zsY3q;C;ٹnʏ7j,'vﬓ%ࣰL$XN>{e6wIٝ/W`tz!S=I&&i2%gBL:j8'K>n;i1p(іJفs}8eht j)^%Dq{0$w#et⨘"Q;⃆̗-#pH!"f"VCԓM*;kKLOX =d'-lS&ڒ)oS0zMas|x94qK ag')V?zanQѓ"i&"m$$`*YWnGRV!?۸Փ5IG8FpH%IJڋ9{`6X>FrrȐƑG9L;HX⇾+p*==ԗl6!!)ocw7$ex?5M._HSf;%cdVN1U --O` D×)oBctrEr0X~<!uŁTG*zKA2g@Tx4C&4}iX >^HS0yK<(%-[rG(KuVhK$bn@F3O<8Y0_nn$eXSJ&)VCIad?'})Fdz K8|`4 w9ֿ)0LεdV} II>箱|Ιl5DҎA ?s/,N˃['BDTR5R޶rsάZ%MsDڔ'2IONt -E@ !I iQVr; z -f )ÅI,y,S/]F xvyR,NI/ 6uLQRv3Jk߃vŔbcW^lӹeI',ccMRSOL+rߦDeO\vSJuaQvx&AbآOMJmMPW~ b{N%ct؟c:#SJ&|x˜ew&9'֧`3LT)N%]d?՜1ĶRlK:=RŊ(%'әbcՓ V?KI.5f-%ӖQQ]OdX =i;Ji?v8B1b&Jy< 8 6, IBRvJ~sn͚כQc[Idda8-o~#۹[Pp$F)w pg:b8F2Dy(S;;`d %ӓ n)绡'MPHE8B:-aҥ+_nY2Ҝ1)6f=w]M]C;ctIiJ_8qTKm\OR!NhŃ%W<  Y !)'snaMcadiľ(HmqP-Ƒ'LPiΒc&)C|Η!|vcDт}v ՗AfA0΅M KR"址xё*$x|z5E)k6o ;ٙIiy|NI,A_28xcIS`+$6ct )'DRv+wʗ}>iwl}1Q(8Q%e[mF|21:dCRb ._bz)S|qRRn@2RRzy)< eGd\$w ;&SI&D|.˟.Fg7 ER./: IV\]=LR SʕRO} b\p"3TavZxX:L7 1"-A<ir˟?jHK&͍ٖQe:Ob8n8X|G n3Q}(-)ezCbE+ =)ГvXv2N8C`;{ 4Q#$ec`׮VI?cp5׵TSI1@!$e[eH )4tKonʄћSTӇy{xحtyR T2XV.16hA|ԐvTEs$_׮;Dֻs:X ۣ5dh|N|I@"bZr\atѳÔ2WfђV:sS|:=lғaHP=dnM",J,I& Dy$ĻDȕZlw0=ٜ5ꪡg']\O -?L'EoݪҤR1/v@k%"JsMd Bz23ГV.cK"8F@\n@B䟝?fH+V/XBSjHɄ%og~*̛cG7$eH;/B+K$7Q:qGۺi@pcz4䱇2uSZ[F}[.O##~ -(с >~:w4ۭ4VHv!#ȕu~TB:WC0yY6ƍޯ- a),EQ}A\{u3X,cO3:qP&p.ECcDF[8׵fQUdWre3<!M׫빑 0­?(2s(rTvh"Pz31œ]t^lBg7yP:?V'ХڵrJ^94d2W}LB- 9ĄhšI9Qjя=iڱ?6A`4h4װ(P.W2d19,8fXy>J *,-P 6Fg$;z< =i3q -/v >Nȓ\FxGjTK+\cUE"&˖ӧbr˅b>l7$ _;D=i:/86 lM#ʖ5dk(ן -'X2 =tcHʘU)/t5Г,G3Q'/?#ȺuRJi*r -208 Zvcq;P{ܖhHNH|Q.P{$5ۡ'X|,wQd(U4|yU!RN grztGo!̾>FQQ6{Lmh/B$zLyԨʓ[dzkȣ<8ŌXR2 Wws"!HQa)xlR6zrI+0`-ovm*f Y6DVCASXrq"KmмBdķonZ!&!)chSc& fuMNtBKU%o4+#?$\`Xf;:S &)D%GmNgߝ  oHX,c#\yGPUxn9/MV 9$& N/,uUD,Otx_)4 _4®6B虐^O=2mo15~"bTEfu+RΚ$e䨪ɽDv$娜*%UN@%'%d~HUTٞý"26UIbҬ3.D5΅ё#7jQJHh-ݤX| !#|*rζ\C'WOf²1:plPrTx-9!{ARFaqk $a9/mYU֥' NNq%r;zw]crߐ#9ɢR>͉zmG5|\cjHϮ|UE'8#89X2aM,с7$e(sMt @' te$"WKe\Bgblq1%„eGNc?BRF-Uq*?V8ZȈ@U9Sn ڦؚ굲M<"*"Yr{.E1K)tHIʶ\ߑM LR҉$%1Ffg = ")UiTYΫ"m_L/q]TzNң}gAeԟȊdҤ9Yj )xd4 @4̓OR:rlD~㎕Ԭ#kG˅_tU?nt<JH@|9ߔCX]7=- Hd$2@iUK%pr42 -Nkaޜ wtӑG{*"JH ڨ=7]!~Q|E4ל2 NԒ){S] vkaX -w|@D)ྔ EXa/n_ww+ %7EsMOʽ"k\b|v\jH{k+m7]Ba)(b;1 rJhtH NH^~g"t@gtͺ"P`L,s(Z"/"2FrEʗ*{HBJ9r@1)I&r=-T:r%Yulir2rp"4]q c ,}Ƅe[&tEbj)WߘڱX$UtǘFUdWܚ%{7"H]CǗJui Z90%31:"J~71I)$PH6Al >6d~DsMwܖ#7ڵrٲēCzrj5K/,ѹcB2Uߖk©֖RP){ nVSL ”2g=کMp`W*2Ө,[FUS+1@aNˈIGtې5>CD5$$xp4@|77UGk֐*>?oY0wt3op*ct%%8)jϹ_J%⺺7 !#-#QU4㐉7q(8zM";z4љD[ph;@R%Q>tXFg7fW]۲"@:;zb=.M]O )H;Q SCҏS,$崄 z -]Œ,h3*4g*uʕM?|22zrr3nXAXF|yrct )3zKVRd@4$%PFtvOґ+5ilMR)D#c)&/Ӡ'h 2D1:}CR{OBy0M)SVݬbF'QadNԒ%{7H+TEZ'+WCL:!wtP/74vo$f蜄I񊐑=T٘]+/JgCCZ#&J>LOt*kpGGd̛H!HR ~D*1Ka;DXٕ'7TY "mk &Ē \%}d'I)TÞ?<VRHhGQ;#9B@@m"`9Pa6hcK!#mXb;G)B|(bup%JRDkR,:;+{ -c"P@mH:!'|dol44Q_pŹll$%fb|&LWBpCEB(BcNRe)(r4{R-??DoR -6XHb -7>psߐZ_[w7pot F.+$%3a)Z‡س?Ii&aϦz&JGN -RDtl9-ǗJui B W;zڞ'rbN(}ߦe[XT+>%)'>Ctv $zr5+1 ,(jpGOΝn!쀤,t)le^3VV7(V d2LWS -oɦb=2eGȹo!;=ֿf|D܂sڱ8$%G|['ћc Uk0]DCXf(MjKڑ&j -q$AE)icl.KR[93v:(09=N(a a5=A1:!)}|7]z`E)uMГD Ǟuwǘ)/,*TbCVB%"J)jxY<5t=)AM4ހ;'xKX&;zoHJX*,$ֹ&QUt &1Ise„ew<ݜ-CRžݤBxX\{u٧k9Dvy+%'a٨]=ct>ޖᏐ'y(|ƬJ Lc@zZ X;6ğ;zW>hHPzF"'EʻQwJ (*09&"8i^ 3΄esXct 5GIxo4/K]$CRFvhQmMA䱥u64-CjaYFbaܷ[*,蜾 '9ww&]uu*W7'zmU,b%ctIDep6Pk$AR[hY?)I2§{ L#,&J>TON(ѳ@8 IY,QHJRnw7d'2%$%QEטyWjIHM,l^Cz3%hN63sBt!ʦtڹD]]ؚz01ɔ9 sGRpG\L sߐZʮM3I9YR 5 ]9Ĉ5͉^ 1 -#[=JI痔Es˧+m<ٍ7ǛTT,[O9OLaIK*ZEB(os4%evoaD٦Emf -BO -N/k<8V;H;-box;cM9\R,{Cs*)rȈgbEL'b' @PKþ~"Kj)rD>U8JNi|ϭt)095ټôL 8AXW4pVBC竍Z?a9XROWr TyPpr+D z KX\GSQk&)^ҁҘ+9@LyԮ~`~~{G,;ƴ -RZ:@ʗ2i3'Rܢl'1.My(MTRQD6,,{ - J&>(ٝ<7 LΗ|"&m@7cᱯ;:s.k)~@Jg:s)iϲO7Ę5(q*WPq;zG5o[ĉvZk6>sJ*dǗIuiN6fKx3X-̄nhct )$>/Z%L#J=% AX@} -djx0yM,^C -Z51zlxMYjO2I0 nP5[/nsT$qXEzLXR}(f&@w,Y1:2Ιvx2̚Fj8ft尽֛j @"S&tE'$,O bNPF)F`b Kl%QEB !J,ibdz'"NٱsGSsq/)ꑬzy $o7)lvSS4[*y e9Ah>\49I7>`T<8u-9SE6=0"-G\@yp|D(sL~.g.]{~ig$ϓ$CSR_OWZՇ.K♜GRk7>`\M,~sKjEҖ}&A~^9* H_r,M`}Ҧ\$]7/y$'%f%͘4+INNv%%%''=Bқ-}J~wKi7'}i%Iuz3<4̙$# 'q CLUY9kR`D =H%Sp2 f*Dږtˑ5ctH*m&)}TUF&#WuJ^)6oHsA%s$9=$ ]$=&' IWvPq6WVًRų<LZ$g=0f-ct!)n>sA{d|4#?uE M>?eJ:팤{=$q&ӣPRh"r:6Jݜ1I #J@}\,{stE6 D*"ЅBX&# Fhd#S9t{]uys2i3$x3gq -[@*m,!!)k2)ɮty`=V^9z==$qׯ-s5DSS3Uo4k)@ЯL$e[pR׼ԤY3I1HdV@"9CPʑE]XUũ9{Se] pb2pGL19'!) B O-k\7Od&iIO!]"ldh( ȱ5*T:Oygna9O`*Wa!ч @ pGRle`IJQ#%&KR9#)ifpr1tU)%uAc! tzmSpr$@M,CQ\ -:XYy<(aTΚYUTC>]aamoL0l=WΗC,R:]uɹNb" XjkmT8 -x;З׌<^g -3-%'+bI Ocz7/z s" 8 -eCeܙOCBRX7'(47+AARR4)D QN\+"&˖>ωlݣv]|J|SC2b2PUx TFt:mg큱96&UEp)4BdC^LԐ.yӒ>Ywf&g']$OOp%PevGLQFoZ1{u+eTN)Co1ŘcrCZRÆ{¾[Lff]XWf˭UO>;.N3P$tlF$%Rؒtl}{sj֡`0*#"_4%s8÷no\4* x=]X_xY}Gn}⮚g~~mՎ]*?Yew?7>sf9]_U&%ERLӞvE/^:\!rDј[?a=ӍO2H&= .+]Tϫ꿯 jqŁ';?|^;ށOSߛ;g3'Bqƙ7o^_R&Tm54G&t тg,eiC4jR7<12Ÿ&٥ϯz1}8tez7roEү`8?㓯3=yG8HwJ3f`cJ*WRR|ECZ E?E)[\PWr%SV?jWK*wod?B/{?yS72XA>I(o<90*_Г#LΣ"&Ry?S/\\m38B7* 3)udC1F*eϳҋ5}j[⻯EO䦙n2E&&?@r 8P  D4B^] - 9{JOkQtp[{otS9QKѥ%Wmnc|a1}~I?.R(h|ӈ1qM T⨢qzr*zRmQdg>It`wt&KP9 @ %1-_ܒ7k.1/tm?}'cU[D4oDޠL4Qc,3A!''-&h'ڱvk06XkrDQ㊢?Z" mћsn}_+ן-?nR}Tx$| ]#|QՓwrct͗+c9MW\O LF:1Ec`7gN\&J|cw/;2I.xgЎw^);"Ȥch*bX# [rE]Lg* 7/r?i%&FpbQþ;/hHF3ȾxV<?/n"J?uOWToPR[ɾݝ+^EI1"Q8vE#X"DI|S -I ѵnrKIIt$A2}f.1~ϐ}?V?jkJybU-7Js:#Ì*'''&]Ib$& J}.;krSPE е굱.^6Hwwk~iH)i&)%}CT<@Yzy8?o>WK*1gRF뵜GOF|%R޾ )S5nT*tEpF䣇:_Fݦ}7͞[_ti]E_T?lxŁH1!Ҽ=^Z|+hӞ;=9m4ʢդ>ܚ2(e:/1n;Ⱦ{:=/z*y9ϛ;>o~}ɕ^<@+_ίܵz^|#|!n&ΗC"a-uFFhz2fzR@LzrB -uyZ]ZM//_!!8 ĈӢcJ'u֞EF/L.\L~r)?[+Tݤb}B/:F3;n 'CII̙VI!S9ҨHy &Qw׮mD!nehiӾ{ 1fCڇoOQ6wŕ{-凶3h -F}7׊12`r'Gד<8iԍw(ADڊUֿAG+qnҐ^=>bɾ٥U3^ʭܽr#\i=E'btГɤ$mtkd˧k)V_pa>|֛bKypm <z3"ơQVTzaߝ5duQ+Tqi*e|kk;/IQ}y(Q@OXV9.,v^+>p79gr 'XÌÊO/<%=빠>K>Ú'Ỹ4%beR/]R{^)fϗkݣ8x#%@Oɘ{Nx}$#J1 T"hrNՒDHކ+cwB݅mی_YʗWzurh>nz #;_.@O(.Zў= y$%'Ip)b:FKz۴烈\@jXK(+؝5o˫vdSˁ'i[[~J&$ڥH#OIΗ"FEU'O{zROZs$΃LΗM[LWOi}ƾ1nEsL^./*C1>gڮWO#Y4MӘ1qԎiU@OaL+9s+9)>(p~Iɚ21aV@#zJG~[Ӫ^̭hzWihӾh)f ldX2U&&aC9MW\.{rcLOa5["w‹#$_)ؽbxh;|ڐ}wn@O -=I[gi)R8[X9w:qp 3)ΚC 6./O)F`̮o{yJE0c"eGFaD#W'[K[wGZ)>r t9,s -Np6fǴ᫓L gOd41 ܌uȏbƚ':̗ݛ+Њd߽@Q2}T"ӍΛ?"t#D#H4' IxWNWK+p"V^9441i/BE3D#9xg(+[tQݖjq?ɾ{cc|{XRF|žc_0Ip=$'IlgK%(&hrOx50ׂ:_w0WafzsΣΗҫ6PiT[jq?Vx6(dC+ /ef Id=ެ%mŤX\Rzr_~ -Zrp4d_mY84%Ⱦ1?NuWm?}W4e&+woaruK!ďߡ"aݦMtYO*\O -fk=]Q|fdΤ>}s)(b |O+%{7|g~}Eu>zgyWF釩Kŷ_,{û@Q1q}7G^h $A5hPϮdtnr|4]qTfJimDڟ)ica\wƜeǸڇoyU/fS$TAn}hӥEq/! 'x}_o;Nǯch~ؕѓ⭩MvS 9D Ilyy5${I -N -2S9(!@FNMe9+{/?տ޵탽{˴e$Ax_ftܱXʙݐACHt'2S:rR ;/Yϫkzj~NmWT{ٽ=;{>:=hO_{oʾ{ %Dѓq4Ez3x%$Q+pW!OdRF{zz -:CH_sT%[dzwogb_W>O=>ܷ/~. .{=Qՠy ->β9K>]>]񺵆tͷn٨tv55mww]L[ncb?:^`û?{}q{=ɐ,8~ %vwc9on{nƔ$:%6rH=(^ -tas۶mzJ*xo^O$/?9"mvfƙ,L|xj$w,t0y(`<Sl*=i7mR5Q[6+|p]t|o5|=*L^nLv]K=LJkKדX%% 1 H4-WDGgYuY>tCA% JI9s`CA%I pi&#'/1~ZzEwbWm| }iûۻ?}KCaf%#-Qu @"zm8A]2޼T 5bvS_Í3J|i|_.VPΠvCGk/nP*|P8Ӗo*BG+U$')_rGM_OC:FWH`/6jsfsa_>| ?_5 {^`J|^h@XRd?ط/$ՓB:2t͚ۖ%;WqM]QA2 -#@'EbFFt pAVnjAzxꞭb0Qy@OO"m}駟~}/*s5_rǍW7XM>A|˟^d ~E:{k/=c5ײԳ{ks&8,/owk,{gN6w } g+] _EU3Qkmg4]i}Cθ_5 =sGv=oW{ {ÃlRݢ8ό KOU8g<(>>'[w[Հ @8 PB -%@Bc܀GI@dl5wclq{l|3wSI73=!}m#:eN}129!1wY\tW]=>k/Lzfx{KGl!\GL 7sǥkbny~D˨OoDr7D-V+_~gRD 0-$nXsH= ,zY,7.6)'^3'ŬƊynn, XX[5IY.+G:՛usG?᧿O?r޿Lsy\W!_̇\VUz9顱&%DBVaUOQ𧼇Uxѕ% -cT FJbSi }E3s F+?(\4T2i!nxKXXm$ E|rMƭxt<0μvʼnOg̷x*Z0}1#5v+1g2hNfeх;<5s%;zQ'qkJ.i՞ -#Uրtuy9{gQΛrLo ]Q틼id4_`^{tB¸ λ庫ͯS{Rud?Y4].2ﱞZǚcxN&S1K2g&W0>9,)Fv^?=ܿrVΛY7p}ˀnqwyḎo%tyok~uO=O:&s0.xНtֱMJ9oP'i`pPaƚ[;XQxLx yʋ$kzYFjNK򘿗.La\>>ǒ?ǦnA⹛0D +,i9n?&X^Ͷl1q~fu$Z{0EcOw]%?uN 6]!//^QHKsSk]`.a3%fn:?C QwC-6,7#pU: Q^ŜܬH} ʊ8+gSObJ!]]Iޤ<  ABJzk8vL7pF˛wv/Ʈ^ȇC̴Df b{Ug`ʒ{|l[*'G|=C{@XZ`7m^N27r[J _{>0s~@-DZeq2fsֳ)9qZ9oEsIDu QŠT϶`Ğ=P߲?D}Ƈ[F̌G(Y6 -V')'sj'ɶx7툅ҕϋrMޤ) 'fߌHL^ov^]#•yEtnƲ}3V/n1O-4^n {f+I$hMVzrqf& dʼnl+ݾܖHoo<9us>j QG%*+cÒܕ~LqO8 +4u}oOٞJOb5fC$ēݏ >aމ"hC!wV,q?y6Z%!,[/+c:Z+|n*S* w%Hp7,7D|mHycճB}Ia3śXG;hH@풧r_*(߲e"v*&>=AKŞO=nj/1Y?j4 -azy SXa_{ɉ܍ ww=3'K@2EuEiAb)*k Hok^-.n.u,l/'ֱ-RqVs]9XQx+MЇ sȸޡLJ粃P^.U›w2jH -QeUGO[ +޾4'Y*2JgNA림w2\(^]}Ըm>9,,@uXM OfGfOod%8 -+--%%K.?3YX]^M rҍN˝ &Nn&B;/LG&| ?J}"E_M=sO_7hF̠=๙U҃>^D}ˠggeCԱZi},na} g_校ۼ^nWAX /wOjմkDCVoQ 5@sΖ8+s#yad}ț 2-~䧕*W͟(g;Nz#^RT d=[|Sq!X/VfUnҗU(~YKx'#[&H~̺7#sR|5$Hd uByXg,\?j2&wtI RMqWQBk`[jv/F|z;x{+!5E9/NYܱ %Oz5pm,_p/U*Y&YUJJ?a> lTr$ybV]*{~_xU99y'77z97f ,E'.7,9D}Ni6ݬ[ď>ډ['$TLUz14Mc&ri\Dt1zVi`:jt,gl謺tOY8gEt7\tҸ<Ւ r*lOࠓ"kb$܉۽RvLNvIFM]җ|<|L/+'kZ]'_Նgq]F|Km2z$kfGD.egQBY͍ qLDm}vsLJ5[Cniy6YIȝx8c'B*'GTT K#6|˕f0O{✏n,w t Zpvit]6 $"JmOIZsEԑEa$#Dq~Mo^orIFeZ/WI+#|Lv5V!:&U]'ݰ M7MY+Q^lR ʝ^䳻Jyk"dn()ĿLsQ v+r33K}~\ H_Ƭu^u wz$'ugƃǎg&.ǙMX%04/cq&zpAlZ{ߖY9Fz&8&ś{ }orPǛwrN`@zv*kC2Wf.?%m>nLaM7X̙ddQ޻ΤZxalMIM+kFq)Q _x|G7yY452(0kQ_&ŃC? {\?͗.ZdZ8{ -o,g}Z>"ҟv #+Jo|uk#4ڝc^X1Kŀ(B!̽tLt IkżQ8[EDy!eN-f3rd^{Rb mU/3xs y{jvՀH,X)[t!Aig/p#+į'ę?{^W{ -#1 }Už̲ _cq? m+2}/I 7&}d!}y(Oeb7~kJ -#XZJ_ZYȀeXQ5 t&={19]ܖFwrN&=p=p#Lw2Lsˑz.ʫ_|A_1XYV&?kՂYe܏}iU{wwĎTQ )If#@#oY?E4@cNVR_ƀXuUޫ-|K%./fk|>kt-akÒM3o^^D>nKM:0%i]y{7nkJ?1qNmbe%inpՀt<*V%keynqY~˘X8-]s\Q8{ka_*wK[K/qw璼ikf+qpp˄EI$V-}BL/~ /ɗkΫr볬 ]g!5]49+`lן q ځ,a_3/NJE\1/gXViN3W_[ Bo,7@R{@ziwj HgN=!b+VC("w]xy03s_Y%yl3GXNZ/8' p 2]9?y}EEEMqW\kOefd -(}/L.^_7@[QY+-e2n)j )u+!-y<32޺![6Ӫ,a>g>//xEnKSQi20/1d<p2P -pH=e".e{ )vn,9ŀM^WvR.f_ծod88]ynj,?ԗYV7pFQrrZrQbU k5S@ACfBLb[ɞGJkQZ#8>u_J >ܨ@f.m16] aw -?aQP2=`ܸ঵+ -NaSǩ_OGb<݋r¡tᢞ~qϮtf[t(03%`SVxQ ߯ Mm -n_(0=GjyOfeQ}ZƮ5[U'6d,1[8߽~SZGbߏ{rf@pz/7y U6@USM=, fW 1{^)?j1#On޸ܜM\Ra ]KlV ~!_\6=s8]H;_/jC9L=0cPIc6bVw.Nm:r٤Y+ubӯyȀ9\VG/kM*}3ImYUEvd͝v]qCx탯ʹjM|(Ο&SԶ5\ubBo߯NC{a֣F6yVAXW_&Iwp -a=`.o:{k-;S[LփE?N ؘqMs*g1rHVvD0P -Iد%Oŝjn1EWƮۍhex.Tql/#SدWM1~*[>ycrS"hm2w7Bc-vwpՉM -ɗ9{{jn1,w-,u,'!$3>vS}-ši<$uW6,VB_c7n1Z7_kĦoN5=ӬͩlTZaB~S*#MRnha?οuk5o}$EdljA#/[7 [ޯ7EԁM[ZGOBØnĭװh0ɹk5#P{մo/ -~"DC}M=>:ȓm-}goM2L~~׿WhVYCJ!vq[+݈4{7<$?rf^:5c=o,!.{kĢ34+Wgwq)n5$?WW תP>n9[YXtqoXV0‘~cn[!2bFjeL*rpoy 0l_{#C*Hfo+#7Qؕ4CH]^U\Lf7C+2IP~{N7 k|ߗ~΃|&6ݠSCFPn?W t0cU1gs H_^H=db)5 -`.NZ:!Ups60YMt׷&5U٧򙜯+ɰBmZ.j{6^hC!B^o3w[4dc P؃r.0q:;C:vcvqh.{u&M^}h đ.q<1g(KxHcpdGT3>Fe}2 Eп0G=Ƽ2{XMgŢ {¥l`cv+֋DпXt(- ^J}^BO N0kA,-6D )8𼇲P6Ʀޠ!]_U]nn [=0mZW8l]ƽ/0:Z+?AZuث? <4CP:T#< c{e:_U(ׇcXH88w?# -GۯA{H|%uD <DϾ?C+56}TkxūeY ]T>HK~rWkbApѳOv{mX7MҘIUcٿ}d"ݵhf'{?٪rQItC4v2b[- !mycNGLb(r/7$k 'R^іaONS~wY7*>_&Jߘ&lw;hfTJ{&1On"Hq2n7~:v?E=ĘBu(T[CI/^^xj'¢)!!^TnD5x:JuܯUWvZbHL=jĦSp잣耸4H]yHWMJ&Ʊ4b&3oS[wǦk:({C=$nr$ݏkovggXZd=||mM%7ZwhKۨh{>ু/jԃj_j |(!8/1?d h3iO=ۥ4%7 o7 [8zaCCDI-ހv'7Bo@3nKLF:/f A}/K 7 B0 r.:s6 6)3߳z1ն6= .G٤)KQh h{D$p) W[[{. n$ǢtLpT!L[ϳZrJ]ý}(Sf;S@_Aƹy( -rGqMYxnkVm\l1Y=JթJ?}߳Ak d++on3L]o[سh*irg.  D33VOI*;C&B$J{vE^Ξchao~Bv Ozyt9< ٽƁ?_OMl2W܅Yb2F"wۨcí+a@b}?ks6gjQo :,zӛ.a`D%m78r>,zaҦr#㻑цEo;뭣Ɓ¢o̔o^[ -J!4|d\ǿDnD LnGJn~%ǁ@Ta3JX|)`\rZSI7@nĭt>J1tF4i.}!7y/%" ĢF,Px%RɭTr#6@q@7,F*G(#܈[|JCJ*7"&{>b/Lf:VnS9p*8t~?FsT ph^nub-q$#P-i.%o6=|Ï?;vq@ezs-j &@e*3*?W SI_;:>I?NaဢX4Mo a5Ŀb"a/1v9ŦY Zh (Cw/e5_nx%P~{tz3%>X箄րX6܃M~\Klh Ebƕ9^-mR`g"? 1)caJԀF'۩wTo˂Ħ"Pvs/_Xawma՛Aztj, $MpB nȶ 59?3 }#hvUobi89Lo8\qw+W }azqI>CߎAo\qe:i+AT@[@7$~Fq0I|xM Ǧ{K7wϫ:b#(q_HaSYoxzK(@3j#1cc8 -h{S#-:W>ǡoJhi N>tzsC'1?ytVo=Q{<<@bSYohM.W&"~Ƴ J66=pz+.iq{dW{I>HaOxFW7P(E恞*P*(%Ej4fxBەőn4~8H!126\JwZrqڀzb*MMn4ǦX&ܶu*^J æz(M2uqx(MJM}%@Ml/Tn"};EC:Il޸=(b]0oRy;0 E׍RBn<(91(MR[{7S%01bHyj-c%-y^urb.kW XtyˍY m:6#ܘھ7$Pf/* " 4n)@},ˍKw"RTǦ^+D2({ܛ7&!84M:`ވܕ@p@a, eZL mAp@YlzMJz/Ԅm^7ټXZrt9 -m@p@IZyj֍twp$P y#\p(E7&yџ*j8P ?)0~LbBp@+ 2!@Z6DaoR`3!8!JXIqg iqSԷo~cOG}>Z n.@nl:zcA+,ƢQ;V$@f,\EP/}@&FEZ&2P_p9q7?Jo$}sz鍽킅Rbӕ4I1ɥ.꧛ވ9hv{mo I%}M;&NkV ~nH?ita.M' 8 Ro|O;ȅC] 8 6XxaOF7~w@,fv Lp ! -/]E  JZ;I ȃMؼsz{ HC{cb2~wz$T_H1 3Q^$¦i7 HECR!7 6-3t՛I84[jCE2{_Uoy d]fSwü`ꍽN`zʍeNp}8¦p=1I 6>HOfa_#: ¦7y 6-誥 ·3dX-rA7 6y7 6n Mrfx-62vl7 \N 5V!MA HM7'i3H p'tt@ fހtXtfW$7 }ӣLr0o@>*; $W1i#8\s zdSA-ʻ۵Go&9K$eǘ81q7 -6=rn& =ia endstream endobj 31 0 obj <>stream -:yǑ bm:wlGyqhq.]ڷBo2jڥ& wHCӵ[ؘ "&0rΗIDL"-c 0H ۣ}{wڱCG&/@b~}wԡ)8I 1LoԿw.c+!w!d= اgfbTכAfc  ԯgNb)7 56zAwazS$ ȍMf<%оcM2xZG  \G^iѴ\3q'tCYm ¼ٱ釣F Ч3p1 - 2I!$ش#&ewpO-cӣōE(S#c֑t)1(M_:Mg8HH6瞒8t@޽DD yȥG mLo{S{2Ґjz)#Л%&&I # Eo?u!Lo;+7܀ % v?84clRܐ@R< )NC?<7ݣkXLԖIi9Էww=wH'}6zSF r+(6pI )Lo'5&~`7QA)=v+IyvRFߗ7^%Lr0o@VN|to짆wM_Orh ?ӛ !@ ѿO̾:$c2ճs8I6 hE?WN cF8\opCSz(L{.q@,?Л7/a߳8TB1wr.7'zVo܅=zt$ԛAb8t|nBԖpP"-:0ɿpH?vۮ) o o@#l:Ror'o@'l몮۷pf#cN;ws!y:a;0gޘ}{7]Po& --AV]th/\bO:.b79}p'^8b< w{+ I KLrN"-Z?[TsCH7$whM_^Ɠ0o@32&UtæΏPor:qvtP 'O IJƃqL2d ~Oo܁-1+4plSF7CeLU Ja$SlAE ӐMo184h {No̻}c0]+܈!Lo7H7<Zc OV]tĦvh . b7%6Џȵ3I0BbVo&qeIzBo :qlp@ -C1x ZaІ3ygmXJi[Nٚ]eA; fmh+1w2 D56m.ReX6 wD7][x 辋 +C~N6mu0o[[p2vI8M L}%qhvN4ep'p]'8ij3HNajx -MLo`SgvEHE+ok!yr%N:6{-1[Ap&ycM_*i0oĢZN tC+:z,Z7iC2:pl?܂{9 -Iq:s7#o -Ya{^݂I#̂%Fj.æUp?<sW8K)c砞 zΟOpGDLib&& ;8Ǣ-IE`5ceLr`g2 tNX8{E8tnX8$8LpSq\JN/ }ax)e疗R%YRzHb@#`:2 "4ҔVvy'`h =~ogah=qG!^dh=pYp&9k8Ew\ҢA7EiL㈘H,:3H Xlv(i1XA=Xl%Y7_Ji_xyzl<$38OJ) "Rp4Zw7Wprax4A:-i4yH$,ǤY& q_b@Gfv1#B @`)9& p(hL2kc8t_ @qh|sSM< MƱ(-II1(Z0}QI-:}J7q_߽I&.܆ f&9& ̈́)qMT84JJ?}&d7 ͅ;ye-@2͆'ynT#J< %@ʏ~סQo -Wp^cL`hLp6qem_4hLu)JZT}!`'WAh9LF{o 7!cw8n-JxB {>!7&{'=_ 'Jo?$Ep#?$g킁 \0wC10pJ$ vDL6=p*L+8ƒE8!:4o7M\lApNk8tσC @顳:C~ao6xݧ7X)ܹK$YK3{9s9νz7 2O!@|JOpp;Q tEH98'ɷz@DrpOP{ 8'l P37Y{C |'_ҽs9&PVS㭹QJŗh@@x N0Ǜ$ߤ#@x e4 %'Wȧ.ڽ+-wr08P""wzxdXW!ހ -}MbFF|7vn%P<ܨ]98sp0߆Ș܍1y2ǧ$ಾES\_7ްzw7$#龻7@;c7 =w@_p3N dpGS}N$t+P{ Ԇ~Ñ/Tpaf4 g;wQ=yYP.X!c`@y鳨=tdgn]9ҙ~ΘAAn#9vp޼37S](JR _~>#P,tg%'i{K="8/SNͥdU Fp_{%̏yȏ lⴓC.(YPQ,|9w`b@m16t=9J|ppΐ_"I93i)?2Iڷ/b9@ëXto ciHGpq d+:.5 l PwU|y;r"Vsz1@gK57YGpv=ɡ%pnl4L|q$J|L'Jг78${g&z '!~x7N~f=]bsLO4(үΔ›Q)\ddo0?rf - ct,+@pf$yʀ/_9bGf|X -_l}8rr'_j-AUoif~tn=R],%_q"p)v՛nYN+y`#3L)CMئB&I;#”  r) 3 ZGސ44 GHANOprFYɠtp%m>hʄ”~'0DoN927KCMR `bt_V\D鷱=d+lҲ{6,Øu9/|/Ks<o1btft2 ȣɜPv}Ҳ{n]JIs奙 #T S})f{c#g_ B6LD鑙ES'@Q)[p]j-S_6C #C^65d89\*s յqnnUr@ɬ;[- :c0ӚcuDpΠn w=sp_`+ du{ 01rlB7VڟBC:dX -?gOBP涋mL=C) -~КVQGKf ytETmTdE8Y?^MՓ$8qJ\02&VQG_<26OGݴoX &9&Lz<9p(dl^K/r3"]0^2܋|% ?~ЛN-"ŖΟc#Ǣ#KS~69>QtG` oca+C o޾5olah/pnix %*O A^+cenGziYJS/ܨ _;8w}=M$>+gWTQ,7ҀHn d]޷ye< Rnp7S -G&ǀx47L:0{[ixgyrlKs4?yx9r4.|)6;08y it ei7 ba(s;SOms{sCE{]-.irms8ITx#L>7/ lHR졥±[y77`p+y3뎟릜%]!⽽^kahfտ>FYNp`koqɽ}'p(O\xMM2H;8fף/<XBB>L~ (fǬ=(x dl /"+D- s(Jf_3IbLl[i;8d馽P CdZ<}l7q}DX1R[i=pZeE\>5g&H0ZNB*AfcJ /#A S -WE>!pr@͍˿r׮1H^ҋ/P6dd2T\ 6#ܥ*#FƏI]ç/঍?/0iml3R -Mڐr#rM7AԱc}m߸᧫V2(&C@S -_Zv׿Xnt8<]>xG~~݆9_u!ԍd~Z@.]Zaz1M †8/xs3ݵ 1qK]17|s|@X -G#_o/-m5YQgM ңKf}]\{Ւ>X" Lƈ0y0psDB9z|(1/ bJS_JCqZ3V6 /+ՆgF/-)6*t5fMsRs"Kc(N4S_>U$P&)+mE GceQk\~']6Tgmm6Լ^Wc5g*cbE Ćx:)>n%ӶZפmЬ7g袲YzGsW`.#-ɘ7+Fkmj}|֛#W?k¦FCAQ]rCF˺ڼx}A`lN6}[Mb[fLQe5E&;Őܬ.]ї9یMٵm%re|v`-Ԣj-2R樾 -C2l27kʲB]_zuEkFV\L6ڠQw>j0#ݽ˴5EZr%}n\bj{RuD93rx G^r0&ϤꯏfΡ@ݗѮh rfENMݘST4 -mIT:VQ -}Vowö^5ݍ{cs"fjZL}wQc02c;ӝ>Rl[)qW6z]P"~`s+[EbZBDQnSMC(ΑXߝ:؝[d2csbm֖ ߥ,mj0QoӒX3@ggT*+ kkʛu9jg8^ng2R' )UUҟѕnHoo.p4$GMΠFh2s\^m1e^#<tNZ`TimYPG>t ~TP:-Jԓ+\e6ZU*SSuԷv)6Z,EI}v^ұ/МߝC40w#d*]Ѣn+ ڢWeO16UeFVk]Uݥ+Փf!r؃&ܙs͚5GPF[SRBY.ĤkY-Ime)U&92ͨn.6W-3[Zcu ZmkIF[\e'yP'+RB兆h&+ٸ^Zҥ5e)v}JSTȭv9QNhlNXƩ]ָ3Ra6%SJ][i[M9efBJwŕJ -"'9h,dRLQZJӔjcIAǪ|٘֘ڣcIi)-2IUV:*q[< = -p*EwmlعU{O쫀mش*0L1YU_{̻˳/T Vɭ -xTs4> -LL*\q3Q5 J,(I endstream endobj 15 0 obj [/ICCBased 19 0 R] endobj 6 0 obj [5 0 R] endobj 32 0 obj <> endobj xref 0 33 0000000000 65535 f -0000000016 00000 n -0000000144 00000 n -0000047649 00000 n -0000000000 00000 f -0000163121 00000 n -0000593503 00000 n -0000047700 00000 n -0000048109 00000 n -0000048283 00000 n -0000163420 00000 n -0000139682 00000 n -0000163307 00000 n -0000049181 00000 n -0000048344 00000 n -0000593468 00000 n -0000048620 00000 n -0000048668 00000 n -0000139717 00000 n -0000160473 00000 n -0000163191 00000 n -0000163222 00000 n -0000163494 00000 n -0000163800 00000 n -0000165099 00000 n -0000187851 00000 n -0000253439 00000 n -0000319027 00000 n -0000384615 00000 n -0000450203 00000 n -0000515791 00000 n -0000581379 00000 n -0000593526 00000 n -trailer <]>> startxref 593722 %%EOF \ No newline at end of file diff --git a/ui/classic/design/chromeStorePics/promo1400560.png b/ui/classic/design/chromeStorePics/promo1400560.png deleted file mode 100644 index d3637ecc8..000000000 Binary files a/ui/classic/design/chromeStorePics/promo1400560.png and /dev/null differ diff --git a/ui/classic/design/chromeStorePics/promo440280.png b/ui/classic/design/chromeStorePics/promo440280.png deleted file mode 100644 index c1f92b1c0..000000000 Binary files a/ui/classic/design/chromeStorePics/promo440280.png and /dev/null differ diff --git a/ui/classic/design/chromeStorePics/promo920680.png b/ui/classic/design/chromeStorePics/promo920680.png deleted file mode 100644 index 726bd810a..000000000 Binary files a/ui/classic/design/chromeStorePics/promo920680.png and /dev/null differ diff --git a/ui/classic/design/chromeStorePics/screen_dao_accounts.png b/ui/classic/design/chromeStorePics/screen_dao_accounts.png deleted file mode 100644 index 1a2e8052c..000000000 Binary files a/ui/classic/design/chromeStorePics/screen_dao_accounts.png and /dev/null differ diff --git a/ui/classic/design/chromeStorePics/screen_dao_locked.png b/ui/classic/design/chromeStorePics/screen_dao_locked.png deleted file mode 100644 index 6592c17e4..000000000 Binary files a/ui/classic/design/chromeStorePics/screen_dao_locked.png and /dev/null differ diff --git a/ui/classic/design/chromeStorePics/screen_dao_notification.png b/ui/classic/design/chromeStorePics/screen_dao_notification.png deleted file mode 100644 index baeb2ec39..000000000 Binary files a/ui/classic/design/chromeStorePics/screen_dao_notification.png and /dev/null differ diff --git a/ui/classic/design/chromeStorePics/screen_wei_account.png b/ui/classic/design/chromeStorePics/screen_wei_account.png deleted file mode 100644 index 23301e4bf..000000000 Binary files a/ui/classic/design/chromeStorePics/screen_wei_account.png and /dev/null differ diff --git a/ui/classic/design/chromeStorePics/screen_wei_notification.png b/ui/classic/design/chromeStorePics/screen_wei_notification.png deleted file mode 100644 index 7a763e5df..000000000 Binary files a/ui/classic/design/chromeStorePics/screen_wei_notification.png and /dev/null differ diff --git a/ui/classic/design/metamask-logo-eyes.png b/ui/classic/design/metamask-logo-eyes.png deleted file mode 100644 index c29331b28..000000000 Binary files a/ui/classic/design/metamask-logo-eyes.png and /dev/null differ diff --git a/ui/classic/design/wireframes/1st_time_use.png b/ui/classic/design/wireframes/1st_time_use.png deleted file mode 100644 index c18ced5e2..000000000 Binary files a/ui/classic/design/wireframes/1st_time_use.png and /dev/null differ diff --git a/ui/classic/design/wireframes/metamask_wfs_jan_13.pdf b/ui/classic/design/wireframes/metamask_wfs_jan_13.pdf deleted file mode 100644 index c77c9274a..000000000 Binary files a/ui/classic/design/wireframes/metamask_wfs_jan_13.pdf and /dev/null differ diff --git a/ui/classic/design/wireframes/metamask_wfs_jan_13.png b/ui/classic/design/wireframes/metamask_wfs_jan_13.png deleted file mode 100644 index d71d7bdb4..000000000 Binary files a/ui/classic/design/wireframes/metamask_wfs_jan_13.png and /dev/null differ diff --git a/ui/classic/design/wireframes/metamask_wfs_jan_18.pdf b/ui/classic/design/wireframes/metamask_wfs_jan_18.pdf deleted file mode 100644 index 592ba8532..000000000 Binary files a/ui/classic/design/wireframes/metamask_wfs_jan_18.pdf and /dev/null differ diff --git a/ui/classic/example.js b/ui/classic/example.js deleted file mode 100644 index 4627c0e9c..000000000 --- a/ui/classic/example.js +++ /dev/null @@ -1,123 +0,0 @@ -const injectCss = require('inject-css') -const MetaMaskUi = require('./index.js') -const MetaMaskUiCss = require('./css.js') -const EventEmitter = require('events').EventEmitter - -// account management - -var identities = { - '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111': { - name: 'Walrus', - img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', - address: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', - balance: 220, - txCount: 4, - }, - '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222': { - name: 'Tardus', - img: 'QmQYaRdrf2EhRhJWaHnts8Meu1mZiXrNib5W1P6cYmXWRL', - address: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222', - balance: 10.005, - txCount: 16, - }, - '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333': { - name: 'Gambler', - img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', - address: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333', - balance: 0.000001, - txCount: 1, - }, -} - -var unapprovedTxs = {} -addUnconfTx({ - from: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222', - to: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', - value: '0x123', -}) -addUnconfTx({ - from: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', - to: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333', - value: '0x0000', - data: '0x000462427bcc9133bb46e88bcbe39cd7ef0e7000', -}) - -function addUnconfTx (txParams) { - var time = (new Date()).getTime() - var id = createRandomId() - unapprovedTxs[id] = { - id: id, - txParams: txParams, - time: time, - } -} - -var isUnlocked = false -var selectedAccount = null - -function getState () { - return { - isUnlocked: isUnlocked, - identities: isUnlocked ? identities : {}, - unapprovedTxs: isUnlocked ? unapprovedTxs : {}, - selectedAccount: selectedAccount, - } -} - -var accountManager = new EventEmitter() - -accountManager.getState = function (cb) { - cb(null, getState()) -} - -accountManager.setLocked = function () { - isUnlocked = false - this._didUpdate() -} - -accountManager.submitPassword = function (password, cb) { - if (password === 'test') { - isUnlocked = true - cb(null, getState()) - this._didUpdate() - } else { - cb(new Error('Bad password -- try "test"')) - } -} - -accountManager.setSelectedAccount = function (address, cb) { - selectedAccount = address - cb(null, getState()) - this._didUpdate() -} - -accountManager.signTransaction = function (txParams, cb) { - alert('signing tx....') -} - -accountManager._didUpdate = function () { - this.emit('update', getState()) -} - -// start app - -var container = document.getElementById('app-content') - -var css = MetaMaskUiCss() -injectCss(css) - -MetaMaskUi({ - container: container, - accountManager: accountManager, -}) - -// util - -function createRandomId () { - // 13 time digits - var datePart = new Date().getTime() * Math.pow(10, 3) - // 3 random digits - var extraPart = Math.floor(Math.random() * Math.pow(10, 3)) - // 16 digits - return datePart + extraPart -} diff --git a/ui/classic/index.html b/ui/classic/index.html deleted file mode 100644 index 9dfaefbb3..000000000 --- a/ui/classic/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - MetaMask - - - - -

- - - - -
- -
- - - diff --git a/ui/classic/index.js b/ui/classic/index.js deleted file mode 100644 index a729138d3..000000000 --- a/ui/classic/index.js +++ /dev/null @@ -1,58 +0,0 @@ -const render = require('react-dom').render -const h = require('react-hyperscript') -const Root = require('./app/root') -const actions = require('./app/actions') -const configureStore = require('./app/store') -const txHelper = require('./lib/tx-helper') -global.log = require('loglevel') - -module.exports = launchMetamaskUi - - -log.setLevel(global.METAMASK_DEBUG ? 'debug' : 'warn') - -function launchMetamaskUi (opts, cb) { - var accountManager = opts.accountManager - actions._setBackgroundConnection(accountManager) - // check if we are unlocked first - accountManager.getState(function (err, metamaskState) { - if (err) return cb(err) - const store = startApp(metamaskState, accountManager, opts) - cb(null, store) - }) -} - -function startApp (metamaskState, accountManager, opts) { - // parse opts - const store = configureStore({ - - // metamaskState represents the cross-tab state - metamask: metamaskState, - - // appState represents the current tab's popup state - appState: {}, - - // Which blockchain we are using: - networkVersion: opts.networkVersion, - }) - - // if unconfirmed txs, start on txConf page - const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.network) - if (unapprovedTxsAll.length > 0) { - store.dispatch(actions.showConfTxPage()) - } - - accountManager.on('update', function (metamaskState) { - store.dispatch(actions.updateMetamaskState(metamaskState)) - }) - - // start app - render( - h(Root, { - // inject initial state - store: store, - } - ), opts.container) - - return store -} diff --git a/ui/classic/lib/account-link.js b/ui/classic/lib/account-link.js deleted file mode 100644 index d061d0ad1..000000000 --- a/ui/classic/lib/account-link.js +++ /dev/null @@ -1,26 +0,0 @@ -module.exports = function (address, network) { - const net = parseInt(network) - let link - switch (net) { - case 1: // main net - link = `http://etherscan.io/address/${address}` - break - case 2: // morden test net - link = `http://morden.etherscan.io/address/${address}` - break - case 3: // ropsten test net - link = `http://ropsten.etherscan.io/address/${address}` - break - case 4: // rinkeby test net - link = `http://rinkeby.etherscan.io/address/${address}` - break - case 42: // kovan test net - link = `http://kovan.etherscan.io/address/${address}` - break - default: - link = '' - break - } - - return link -} diff --git a/ui/classic/lib/contract-namer.js b/ui/classic/lib/contract-namer.js deleted file mode 100644 index f05e770cc..000000000 --- a/ui/classic/lib/contract-namer.js +++ /dev/null @@ -1,33 +0,0 @@ -/* CONTRACT NAMER - * - * Takes an address, - * Returns a nicname if we have one stored, - * otherwise returns null. - */ - -const contractMap = require('eth-contract-metadata') -const ethUtil = require('ethereumjs-util') - -module.exports = function (addr, identities = {}) { - const checksummed = ethUtil.toChecksumAddress(addr) - if (contractMap[checksummed] && contractMap[checksummed].name) { - return contractMap[checksummed].name - } - - const address = addr.toLowerCase() - const ids = hashFromIdentities(identities) - return addrFromHash(address, ids) -} - -function hashFromIdentities (identities) { - const result = {} - for (const key in identities) { - result[key] = identities[key].name - } - return result -} - -function addrFromHash (addr, hash) { - const address = addr.toLowerCase() - return hash[address] || null -} diff --git a/ui/classic/lib/etherscan-prefix-for-network.js b/ui/classic/lib/etherscan-prefix-for-network.js deleted file mode 100644 index 2c1904f1c..000000000 --- a/ui/classic/lib/etherscan-prefix-for-network.js +++ /dev/null @@ -1,21 +0,0 @@ -module.exports = function (network) { - const net = parseInt(network) - let prefix - switch (net) { - case 1: // main net - prefix = '' - break - case 3: // ropsten test net - prefix = 'ropsten.' - break - case 4: // rinkeby test net - prefix = 'rinkeby.' - break - case 42: // kovan test net - prefix = 'kovan.' - break - default: - prefix = '' - } - return prefix -} diff --git a/ui/classic/lib/explorer-link.js b/ui/classic/lib/explorer-link.js deleted file mode 100644 index 3b82ecd5f..000000000 --- a/ui/classic/lib/explorer-link.js +++ /dev/null @@ -1,6 +0,0 @@ -const prefixForNetwork = require('./etherscan-prefix-for-network') - -module.exports = function (hash, network) { - const prefix = prefixForNetwork(network) - return `http://${prefix}etherscan.io/tx/${hash}` -} diff --git a/ui/classic/lib/icon-factory.js b/ui/classic/lib/icon-factory.js deleted file mode 100644 index 27a74de66..000000000 --- a/ui/classic/lib/icon-factory.js +++ /dev/null @@ -1,65 +0,0 @@ -var iconFactory -const isValidAddress = require('ethereumjs-util').isValidAddress -const toChecksumAddress = require('ethereumjs-util').toChecksumAddress -const contractMap = require('eth-contract-metadata') - -module.exports = function (jazzicon) { - if (!iconFactory) { - iconFactory = new IconFactory(jazzicon) - } - return iconFactory -} - -function IconFactory (jazzicon) { - this.jazzicon = jazzicon - this.cache = {} -} - -IconFactory.prototype.iconForAddress = function (address, diameter) { - const addr = toChecksumAddress(address) - if (iconExistsFor(addr)) { - return imageElFor(addr) - } - - return this.generateIdenticonSvg(address, diameter) -} - -// returns svg dom element -IconFactory.prototype.generateIdenticonSvg = function (address, diameter) { - var cacheId = `${address}:${diameter}` - // check cache, lazily generate and populate cache - var identicon = this.cache[cacheId] || (this.cache[cacheId] = this.generateNewIdenticon(address, diameter)) - // create a clean copy so you can modify it - var cleanCopy = identicon.cloneNode(true) - return cleanCopy -} - -// creates a new identicon -IconFactory.prototype.generateNewIdenticon = function (address, diameter) { - var numericRepresentation = jsNumberForAddress(address) - var identicon = this.jazzicon(diameter, numericRepresentation) - return identicon -} - -// util - -function iconExistsFor (address) { - return contractMap[address] && isValidAddress(address) && contractMap[address].logo -} - -function imageElFor (address) { - const contract = contractMap[address] - const fileName = contract.logo - const path = `images/contract/${fileName}` - const img = document.createElement('img') - img.src = path - img.style.width = '75%' - return img -} - -function jsNumberForAddress (address) { - var addr = address.slice(2, 10) - var seed = parseInt(addr, 16) - return seed -} - diff --git a/ui/classic/lib/lost-accounts-notice.js b/ui/classic/lib/lost-accounts-notice.js deleted file mode 100644 index 948b13db6..000000000 --- a/ui/classic/lib/lost-accounts-notice.js +++ /dev/null @@ -1,23 +0,0 @@ -const summary = require('../app/util').addressSummary - -module.exports = function (lostAccounts) { - return { - date: new Date().toDateString(), - title: 'Account Problem Caught', - body: `MetaMask has fixed a bug where some accounts were previously mis-generated. This was a rare issue, but you were affected! - -We have successfully imported the accounts that were mis-generated, but they will no longer be recovered with your normal seed phrase. - -We have marked the affected accounts as "Loose", and recommend you transfer ether and tokens away from those accounts, or export & back them up elsewhere. - -Your affected accounts are: -${lostAccounts.map(acct => ` - ${summary(acct)}`).join('\n')} - -These accounts have been marked as "Loose" so they will be easy to recognize in the account list. - -For more information, please read [our blog post.][1] - -[1]: https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd#.7d8ktj4h3 - `, - } -} diff --git a/ui/classic/lib/persistent-form.js b/ui/classic/lib/persistent-form.js deleted file mode 100644 index d4dc20b03..000000000 --- a/ui/classic/lib/persistent-form.js +++ /dev/null @@ -1,61 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const defaultKey = 'persistent-form-default' -const eventName = 'keyup' - -module.exports = PersistentForm - -function PersistentForm () { - Component.call(this) -} - -inherits(PersistentForm, Component) - -PersistentForm.prototype.componentDidMount = function () { - const fields = document.querySelectorAll('[data-persistent-formid]') - const store = this.getPersistentStore() - - for (var i = 0; i < fields.length; i++) { - const field = fields[i] - const key = field.getAttribute('data-persistent-formid') - const cached = store[key] - if (cached !== undefined) { - field.value = cached - } - - field.addEventListener(eventName, this.persistentFieldDidUpdate.bind(this)) - } -} - -PersistentForm.prototype.getPersistentStore = function () { - let store = window.localStorage[this.persistentFormParentId || defaultKey] - if (store && store !== 'null') { - store = JSON.parse(store) - } else { - store = {} - } - return store -} - -PersistentForm.prototype.setPersistentStore = function (newStore) { - window.localStorage[this.persistentFormParentId || defaultKey] = JSON.stringify(newStore) -} - -PersistentForm.prototype.persistentFieldDidUpdate = function (event) { - const field = event.target - const store = this.getPersistentStore() - const key = field.getAttribute('data-persistent-formid') - const val = field.value - store[key] = val - this.setPersistentStore(store) -} - -PersistentForm.prototype.componentWillUnmount = function () { - const fields = document.querySelectorAll('[data-persistent-formid]') - for (var i = 0; i < fields.length; i++) { - const field = fields[i] - field.removeEventListener(eventName, this.persistentFieldDidUpdate.bind(this)) - } - this.setPersistentStore({}) -} - diff --git a/ui/classic/lib/tx-helper.js b/ui/classic/lib/tx-helper.js deleted file mode 100644 index ec19daf64..000000000 --- a/ui/classic/lib/tx-helper.js +++ /dev/null @@ -1,17 +0,0 @@ -const valuesFor = require('../app/util').valuesFor - -module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, network) { - log.debug('tx-helper called with params:') - log.debug({ unapprovedTxs, unapprovedMsgs, personalMsgs, network }) - - const txValues = network ? valuesFor(unapprovedTxs).filter(txMeta => txMeta.metamaskNetworkId === network) : valuesFor(unapprovedTxs) - log.debug(`tx helper found ${txValues.length} unapproved txs`) - const msgValues = valuesFor(unapprovedMsgs) - log.debug(`tx helper found ${msgValues.length} unsigned messages`) - let allValues = txValues.concat(msgValues) - const personalValues = valuesFor(personalMsgs) - log.debug(`tx helper found ${personalValues.length} unsigned personal messages`) - allValues = allValues.concat(personalValues) - - return allValues.sort(txMeta => txMeta.time) -} diff --git a/ui/css.js b/ui/css.js new file mode 100644 index 000000000..7c394a87b --- /dev/null +++ b/ui/css.js @@ -0,0 +1,29 @@ +const fs = require('fs') +const path = require('path') + +module.exports = bundleCss + +var cssFiles = { + 'fonts.css': fs.readFileSync(path.join(__dirname, '/app/css/fonts.css'), 'utf8'), + 'reset.css': fs.readFileSync(path.join(__dirname, '/app/css/reset.css'), 'utf8'), + 'lib.css': fs.readFileSync(path.join(__dirname, '/app/css/lib.css'), 'utf8'), + 'index.css': fs.readFileSync(path.join(__dirname, '/app/css/index.css'), 'utf8'), + 'transitions.css': fs.readFileSync(path.join(__dirname, '/app/css/transitions.css'), 'utf8'), + 'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'), + 'react-css': fs.readFileSync(path.join(__dirname, '..', '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'), +} + +function bundleCss () { + var cssBundle = Object.keys(cssFiles).reduce(function (bundle, fileName) { + var fileContent = cssFiles[fileName] + var output = String() + + output += '/*========== ' + fileName + ' ==========*/\n\n' + output += fileContent + output += '\n\n' + + return bundle + output + }, String()) + + return cssBundle +} diff --git a/ui/design/00-metamask-SignIn.jpg b/ui/design/00-metamask-SignIn.jpg new file mode 100644 index 000000000..2becdb032 Binary files /dev/null and b/ui/design/00-metamask-SignIn.jpg differ diff --git a/ui/design/01-metamask-SelectAcc.jpg b/ui/design/01-metamask-SelectAcc.jpg new file mode 100644 index 000000000..239091a98 Binary files /dev/null and b/ui/design/01-metamask-SelectAcc.jpg differ diff --git a/ui/design/02-metamask-AccDetails.jpg b/ui/design/02-metamask-AccDetails.jpg new file mode 100644 index 000000000..d7d408ffc Binary files /dev/null and b/ui/design/02-metamask-AccDetails.jpg differ diff --git a/ui/design/02a-metamask-AccDetails-OverToken.jpg b/ui/design/02a-metamask-AccDetails-OverToken.jpg new file mode 100644 index 000000000..f26ff31e8 Binary files /dev/null and b/ui/design/02a-metamask-AccDetails-OverToken.jpg differ diff --git a/ui/design/02a-metamask-AccDetails-OverTransaction.jpg b/ui/design/02a-metamask-AccDetails-OverTransaction.jpg new file mode 100644 index 000000000..8a06be6b9 Binary files /dev/null and b/ui/design/02a-metamask-AccDetails-OverTransaction.jpg differ diff --git a/ui/design/02a-metamask-AccDetails.jpg b/ui/design/02a-metamask-AccDetails.jpg new file mode 100644 index 000000000..c37e0f539 Binary files /dev/null and b/ui/design/02a-metamask-AccDetails.jpg differ diff --git a/ui/design/02b-metamask-AccDetails-Send.jpg b/ui/design/02b-metamask-AccDetails-Send.jpg new file mode 100644 index 000000000..10f2d27fd Binary files /dev/null and b/ui/design/02b-metamask-AccDetails-Send.jpg differ diff --git a/ui/design/03-metamask-Qr.jpg b/ui/design/03-metamask-Qr.jpg new file mode 100644 index 000000000..9c09de42f Binary files /dev/null and b/ui/design/03-metamask-Qr.jpg differ diff --git a/ui/design/05-metamask-Menu.jpg b/ui/design/05-metamask-Menu.jpg new file mode 100644 index 000000000..0a43d7b2a Binary files /dev/null and b/ui/design/05-metamask-Menu.jpg differ diff --git a/ui/design/chromeStorePics/final_screen_dao_accounts.png b/ui/design/chromeStorePics/final_screen_dao_accounts.png new file mode 100644 index 000000000..805cc96b6 Binary files /dev/null and b/ui/design/chromeStorePics/final_screen_dao_accounts.png differ diff --git a/ui/design/chromeStorePics/final_screen_dao_locked.png b/ui/design/chromeStorePics/final_screen_dao_locked.png new file mode 100644 index 000000000..9d9e33930 Binary files /dev/null and b/ui/design/chromeStorePics/final_screen_dao_locked.png differ diff --git a/ui/design/chromeStorePics/final_screen_dao_notification.png b/ui/design/chromeStorePics/final_screen_dao_notification.png new file mode 100644 index 000000000..d56a5ce62 Binary files /dev/null and b/ui/design/chromeStorePics/final_screen_dao_notification.png differ diff --git a/ui/design/chromeStorePics/final_screen_wei_account.png b/ui/design/chromeStorePics/final_screen_wei_account.png new file mode 100644 index 000000000..d503ff301 Binary files /dev/null and b/ui/design/chromeStorePics/final_screen_wei_account.png differ diff --git a/ui/design/chromeStorePics/final_screen_wei_notification.png b/ui/design/chromeStorePics/final_screen_wei_notification.png new file mode 100644 index 000000000..3560c51ff Binary files /dev/null and b/ui/design/chromeStorePics/final_screen_wei_notification.png differ diff --git a/ui/design/chromeStorePics/icon-128.png b/ui/design/chromeStorePics/icon-128.png new file mode 100644 index 000000000..ae687147d Binary files /dev/null and b/ui/design/chromeStorePics/icon-128.png differ diff --git a/ui/design/chromeStorePics/icon-64.png b/ui/design/chromeStorePics/icon-64.png new file mode 100644 index 000000000..7062cf4f1 Binary files /dev/null and b/ui/design/chromeStorePics/icon-64.png differ diff --git a/ui/design/chromeStorePics/metamask_icon.ai b/ui/design/chromeStorePics/metamask_icon.ai new file mode 100644 index 000000000..27400c5a4 --- /dev/null +++ b/ui/design/chromeStorePics/metamask_icon.ai @@ -0,0 +1,2383 @@ +%PDF-1.5 % +1 0 obj <>/OCGs[5 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + metamask_icon + + + Adobe Illustrator CC 2015 (Macintosh) + 2016-06-15T14:23:12-04:00 + 2016-06-15T14:23:12-04:00 + 2016-06-15T14:23:12-04:00 + + + + 240 + 256 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAADwAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXnP5r/mvB5Tg/RmnAT6/cJyUMKx28bVAkf+ZjT4U+k7UDYuo1HBsObl6bTce5+l5X+Wf5t6jonm KZtfu5rzTNVcG+lkZpHilACLOAamgUBWC/sgUrxAzEwakxl6uRczUaYSj6eYfS9vcQXEEdxA6ywT KskUimqsjCqsCOoIObUG3UkUvxQ7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXln5rfnHb+X1l0bQnWfXCCs0+zR2tfEGoaTwXoO/hmJqNTw7Dm5mm0plvL6fvfO U889xPJcXEjTTzM0ksshLO7saszMdySTUk5qybdsBSzAl7R+Rv5ni0dPKutTqto5ppNw/KqyOwH1 c0BHFuVVJpTpvUUz9Jnr0n4Ov1mnv1D4ve82LrHYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FUn1Pzl5W0yF5r3VLeNI9no4kYfNU5N+GY89XijsZC/mfkHIhpMstxE18h8ynCmqg7iorQ7HMhx 3Yq7FXYq7FXYq7FXjf5v/nG2nPL5e8tXAN9Ro9Rv039A7D04WBp6nUM1Ph7fFXjg6nU16Y83P0ul v1S5PAWZmYsxJYmpJ3JJzWu0axV2KuxV9Ifkr+Zq67py6FrF1y121+G3eUjlcwKtQeRPxyIAeXci jbnkc2mlz8Qo83U6vT8J4h9L1PMxwnYq7FXYq7FXYq7FXYq7FXYq7FXYqlN95s8t2I/0jUYQQaFE b1HHzWPk34Zi5Nbhh9Uh9/3OVi0Waf0xP3fex7UfzX0WEOtjBNdSCoR2AjjPgak8/wDhcwcvbWIf SDL7B+v7HOxdi5T9REftP6vtYre/mf5ouD+5eK0UdoowxPzMnqfhmsydsZpcqj7h+u3aY+x8Eedy 95/VTFdc803n1dptVv5powSVjeRmqx7IhNMxPEy5jRJPx2cwY8WEWAB8N0l8gRXnm/z/AKXazpXT 7WX65NCo5II4PjHqVrXk3FDX+btXNtodLETDqddqiYH7H1LnQPOuxV2KuxV2KuZlVSzEBQKknYAD FXhX5t/nOsyzaB5XuCEDBbzVoXZSSrA8Ld0I2qKM/foNt81+o1X8Mfm7LTaT+KXyeI5r3YuxV2Ku xV2KqtpdXNpdQ3VrI0NzA6yQyoaMroaqwPiCMINboIsUX1j+XH5g6f5w0WOcNHDq0I439irbqwoD IiklvTaux7dK1GbnBmEx5ukz4DjPky3Lmh2KuxV2KuxV2KuxVKb/AM2eW7CoudQhDA0KI3qMD7rH yYZjZdbhh9Uh9/3OVi0Waf0xP3fex6//ADY0OHktnbzXTKaKxpFGw8QTyb/hcwMnbWIfSDL7B+Pg 5+PsXKa4iI/afx8WO3/5ra9OJEtYIbVG2RqNJIo/1iQv/C5gZe2sp+kCP2n8fBz8XYuIVxEy+wfj 4sVvdY1a+FLy8muFBLBZJGZQT4KTQZrMmfJP6pE/F2ePBjh9MQPghMqbXYqler+YLPTgUJ9W57Qq en+se2X4dPKfuaMueMPewW+vrm9uGnuH5Ox2G/FR/KoPQZtYQERQdZOZkbL3L/nG7y8Y7PVPMEqj lOy2VqxBDBEpJKQSPsszINu6nNpoYbGTqdfPcRe1ZnuvdirsVdirsVeF/n1+Y96l1N5O04+lCERt Vn35uXAkWBfBOJVmI+1XjsAeWv1ec3wD4uy0eAVxn4PEM17sXYq7FXYq7FXYq7FU18seZNU8uaxD qumymOeKqsBQh0YUZCGDDceI2O+ESlH6TRYmEZbSFh7bbfmL5mubeO4iv6xSqHQ+lD0YV/kzVy7U 1MTRl9kf1Owj2XpiLEftP61T/Hvmv/lt/wCSUP8AzRkf5W1P877I/qZfyTp/5v2n9bv8e+a/+W3/ AJJQ/wDNGP8AK2p/nfZH9S/yTp/5v2n9bv8AHvmv/lt/5JQ/80Y/ytqf532R/Uv8k6f+b9p/W7/H vmv/AJbf+SUP/NGP8ran+d9kf1L/ACTp/wCb9p/W7/Hvmv8A5bf+SUP/ADRj/K2p/nfZH9S/yTp/ 5v2n9bHtW1/WtUeuoXTz8eiGioCNtkUKv4ZDNqsmX6zbdh0uPF9ApL8ob3Yq7FXYq7FWO695pW0d rWyo9wtRJKd1Q+A8WH3ZmYNLxby5OJn1PDtHmw6SR5JGkclnclmY9STuTmzArZ1xN7uhhlmmSGJS 8sjBI0HUsxoAPpwhiS+zvLGhxaF5e0/SIiGFlAkTOoIDuB8b0JNOb1bN5jhwxAdBknxSJ70zybB2 KuxV2KpX5o12HQfL2oaxNxK2UDyKjHiHkpSOOu9ObkL9OQyT4Yks8cOKQHe+Nr6+u7+8mvLyVp7q 4cyTTOaszNuSc0ZJJsu/AAFBRwJdirsVdirsVdirsVdirKvI2tmC6OmzN+5uDWEmlFkp03/mp9/z zX67BY4hzDm6PNR4T1Z5mpdo7FXYq7FXYqg7laSn33y2PJgVLJIdirsVWu6IjO7BUUEsxNAAOpJO IFqTTD9c81yT1gsGaKIH4px8LtT+Xuo/HNlg0gG8ubrs2qJ2ixzM1w3Yqzz8k/L66z5/s2kAMGmK 2oSAkgkwkCKlO4ldDTwBzJ0sOKfucbVz4YHz2fU+bd0rsVdirsVdirxT/nIzzWi2ll5ZtZ1Mkj/W dRjQnkqoB6KPTajli9D/ACqcwNbk2EQ7DQ49zIvB81zs3Yq7FXYq7FXYq7FXYq7FW0d0dXRirqQV YGhBG4IIxItQXp3ljWjqunc5Cv1qI8J1Xb/Van+UPxrmh1WDw5bcnc6fNxx35pvmO5DsVdirsVQ1 2v2W+gnJwYlD5YxdiqheXttZwGe4cRxjap6k+AHc5KEDI0GM5iIssE1fzBeakeB/dWw6Qqevux7n Nth08Ye91eXPKfuSzL2h2KuxV9If84/eVk03ys+tyEm51lqhSKcIYHdEArv8Zq3uKZtdHjqN97qN bkuVdz1PMtw3Yq7FXYqp3V1b2ltNdXMgit4EaWaVjRVRByZifAAYCaSBez418169Nr/mPUdYl5Vv Z2kjVyCyRVpEhIp9iMKv0Zo8k+KRLv8AHDhiB3JVkGbsVdirsVdirsVdirsVdirsVTPy7q50vU45 2J9B/guFHdD36H7J3/DKNTh8SFdejdgy8Er6PUYpY5okljblHIoZGHQqwqDmhIINF3QNiwuwJdir sVUrlaxH23yUeaCg8tYJfq2t2Wmx/vW5TleUcC/abtv/ACj3OXYsEp8uTVlzRhz5sD1DULm+uGnu GLE/ZX9lR/KozbY8YgKDqsmQyNlDZNg7FXYqiNN0+51HUbXT7UBrm8mjggUmgLysEWp7bnDEWaRK VCy+0tM0+307TbXT7YEW9nDHbwgmp4RKEWp+QzfRFCnnpSs2UThQ7FXYq7FXmX59ebIdL8ovpEMw Goauyx+mrFXW2U8pH2/Zbj6dD15HwOYmryVGupczR4uKd9A+ac1Tt3Yq7FXYq7FXYq7FXYq7FXYq 7FXYqzXyJrSem2lztRgS9sSQAQT8SD3ruPpzV6/Bvxj4ux0Wb+EsxzWuwdirsVadeSlfEEYhDE9b 8zwWLNb24E10pKuK/AhHjTqa9s2ODSme52Dh5tSI7DcsJmmlmkaWVy8jmrOxqTm0AAFB1hJJsrMK HYq7FXYq9S/5x+8qvqXmp9bkIFtoq1CEV5zTo6IBXb4RVq9jTMzR47lfc4WtyVHh730jm0dS7FXY q7FXYq+X/wA+dQmuvzGu4JPsWMFvBF/qtGJ/+JTHNTq5Xk9zudHGsY83nmYrlOxV2KuxV2KuxV2K uxV2KuxV2KtqrMwVQSxNABuSTirN/Lfl9LBVurhQ1626g7iMeA/yvE/R89VqdRx7D6XZ6fT8O55s tUggEdDuM1zmt4pdirsVYZ5s8s+pLJfWS0lNXmhH7fcsv+V4jv8APrs9JqaHDJ1+p01+qLDM2brn Yq7FXYq7FX0n/wA48to48kyR2cwfUPrLyanEdmjZvhiA2rwMaAj35ZtdHXBtzdRrr49+XR6hmW4b sVdirsVdir5I/Ne/+vfmJrs1QeFx6G3/AC7osP8AzLzTag3Mu800axhieUN7sVdirsVdirsVdirs VdirsVdirMPLHl8wAXt4hE5/uYmFCg/mI8f1ZrdVqL9MeTsdNgr1HmyXMJzEXatWOndf1ZVMbswr ZFLsVdiqGu13VvoOTgWJYV5o8vFS9/aL8O7XEQ7eLj28c2ml1H8MnXanT/xBi+Z7guxV2KuxVP8A yLf+abLzPZP5ZDyarI4SO3XdZVO7JKKgenQVYkjiPiqKVFuKUhIcPNqzRiYni5PsKIymJDKFWXiP UCElQ1N6EgEivtm7dCuxV2KuxV4P+Zn5i/m7o189tNbRaNYszi2urVBOsqEkD/SJAw5bV2VG8QNs 12fNlie4Oy0+DFId5eL3FxPczyXFxI01xMzSTSuSzu7GrMzHckk1JzBJt2AFLMCXYq7FXYq7FXYq 7FXYq7FXYqyvyv5fZWF9ex0IobaNvv5kfq/2s1+q1H8Mfi5+mwfxS+DKswHOdiqrbuVkA7Nsf4ZG Q2SEZlTN2KuxVTnTlEfbcfRhid0FBZcwYd5k8uNAz3tkg+rdZYl6oe7KKfZ/V8umy02pv0y5uu1G nr1R5MbzNcN2KuxVnH5Z/mdP5MuXjayhudPu5Fa9cJS6CAEUjkqoIFa8W2/1ak5kYM/B02cbUafx Ou76X8s+ZdL8x6RDqumGU2s32TLG8R5DZl+IUbi1VJQlajrm1hMSFh1GTGYGimmTYOxV2KqN9HZS Wk0d8sT2boVuEnCmIodiHDfDT54DVbpF3s+b/wAyfI/kCy9fU/LvmWzJZix0f1BOQSWJWF4OZXsq q6/N81efFAbxkPc7bBmmdpRPveZZiOY7FXYq7FXYq7FXYq7FXYqybyz5cMhjv7xaRD4oYSPteDN/ k+Hj8uuDqdTXpi5um09+osvzXOwdirsVdiqYIwZA3iMoIZt4pdirsVS9l4sV8DTLg1tEAih6YVYT 5k8vGzY3dsC1qxJdf99knpt+z4ZtNNqOLY83W6jT8O45JBmW4jsVZP8Alp5c07zH5z0/SNQd1tZz I7rH1f0o2l4V/ZDcNzl2CAlMAtOomYQJD65gggt4I7e3jWGCFRHFFGAqIiiiqqjYADYAZugKdGTa /FDsVdirHfNvkDyv5rjUava87iNGSC7iYxzRhvBhs1DuA4I9sqyYYz5tuLNKHJ4v5q/5x58xWBlu NAuE1S1G6270iuQCTtv+7fitN+QJ7LmDk0Uh9O7sMeuifq2eUzQzQTPDMjRTRMUkjcFWVlNCrA7g g5hkU5oNrMCXYq7FXYq7FXYq7FWR+XfLJuAl5eDjBUGKEjdx4nwX9fy64Wo1NemPNzNPpr9UuTMs 1rsXYq7FXYq7FUVavVSh6jcfLK5hkFfIMnYq7FUHdLSWviK/wyyHJgVLJoWuiOjI4DIwKsp3BB2I OINKRbB/MPl19Pb6xb1ezY713MZPY+3gfo+e10+o49j9Tq9Rp+DcckkzKcZmP5P3EsH5k6G8cTTM ZZIyqgkhZIXRm27IrFj7DL9Mf3gcfVC8ZfWWbl0jsVdirsVdirwr87POX5jaTqj6dFIdP0O4VTaX dorK8o6lXuDusgZTVUI+HrUHfX6rLOJrkHZaTFjkL5l4jmvdi7FXYq7FXYq7FXYqn/lzy6t8purr kLZTREG3Mg77/wAvbbMTU6jg2HNy9Pp+Lc8maqqooVQFVRRVGwAHYZqyXZAN4q7FXYq7FXYqvhfj Ip7dD9ORkNkhHZUzdirsVULpKoG/l/jkoFiULlrF2KrJYYpozHKiyRt9pGAIP0HCCQbCCAdiwTzD obabcB4qm0lJ9M7nif5Sf1ZttPn4xvzdXqMPAduT6E/Jz8tofLejx6pqMStrt+iyNzSj20bLtCOY DK9G/edN/h7VO+02DhFnmXn9Vn4zQ+kPSMynEdirsVdirsVS7zBoGl6/pM+l6nCJrScUI6MrD7Lo ezKehyM4CQos4TMTYfKfn3yJq3lDWHs7pWkspCTYX1KJMgp4Vo61oy9vlQ5p82EwNF3WHMMgsMYq MpbnVGKuxVvFXYqnnlvQDfSfWbhSLRDsP9+MOw9h3zF1Oo4BQ5uVp8HEbPJm6IiIqIoVFACqBQAD YADNUTbswKXYq7FXYq7FXYq7FXYqjoX5xg9+h+eUyFFmF+BLsVWyLyRl8RtiChAZewdirsVTjyfZ JeeZ9NidOarOkpUio/dH1Afo45mdnx4s8R5/du4faE+HBI+X37Pds7N4t2KuxV2KuxV2KuxV5Z+Y esC91gWcZBhsKpUb1kahf7qcfozke2dTx5eEcoff1/U9b2PpuDFxHnP7un62K5p3buxV2KuxV1Bi qFukowcdDsfnlkCxKhk2LsVdirsVdirsVdirsVRFo/xFfHcZCYZBE5WydirsVQEq8ZGHv+vLgdmB W4UOxVmH5WQCTzOzn/dFvI4+kqn/ABvm27GiDm90T+h1PbUiMI85D9L17OpeVdirsVdirsVdiqG1 K/i0+wuL2X7ECFyK0qR0Ue7HbKs+UY4GZ6Btw4jkmIjqXh000k0zzSsWkkYu7HqWY1Jzz+UjIknm XvYxEQAOQWYGTsVdirsVdiq2ROaFfHp88INIKAIIJB6jrlrB2FXYq7FXYq7FXYq7FV0bcXDeBwEJ CPylm7FXYqhbtTyDdiKfTlkCxKhk2LsVZ7+UduW1S+uO0cCx/TI4P/MvN32HC5yl3Cvn/Y6PtydQ jHvN/L+16jnSPNuxV2KuxV2KuxVhP5l60IrOPSY6+rccZZj29NSeI+l1r9GaHtzUgQGMc5bn3f2/ c73sTTEzOQ8o7D3/ANn3vOM5d6d2KuxV2KuxV2KuxVCXiFT6iqWB+1Sn8csgejEhBfW4/Bvw/rlv Chr64n8px4Va+uD+T8ceBWvrv+R+P9mHgVv67/kfj/ZjwK19cP8AL+OPArX1yT+UY8KtfXJfBfx/ rjwhU2s5Ge3Ut9rv/DMeYosgr5FLsVUL3l9WZlFWX4hX26/hkoc0FKDdSnwHyGZPCGKhZ6oLy3We CTlGxIBoBupKnt4jLMuA45cMhu1Yc0ckeKPJ6F+UmpSxapdQMapPGrGvjG1BT/kYc2vYs6nKPeL+ X9rqO3IeiMu418/7HsA3GdE807FXYq7FXYq7FXi/mfVP0nrl1dA1i5cIaGo9NPhUj/Wpy+nOF7Qz +LmlLpyHuH4t7jQYPCwxj15n3n8UlWYbmOxV2KuxV2KuxV2KuIB2PTFUDdWaV5caqe/cZbCbAhAy WjCpQ8h4d8tElUCCDQ9ckrWKpZrHmHTtKUeuxeY9II6F6eJBIoMztJoMmf6dh3nk4Os7Rxaceo2e 4c2Far5x1a+5JE31W3bb04z8RHu/X7qZ0ul7IxYtz6pef6v7XltX2zmy7A8EfL9f9id+R9d9WP8A Rc5q8YLW7kjdR1Tfeo6j2+WaztrRcJ8WPI8/1/jr73adh6/iHgy5jl7u78dPcy5RyYL4mmc+9GnN o1HK9iP1ZjzCQisrZOxVp1DoynowIPyOIKscl/dc+ewSvL2p1zNiL5NZNCy888na79QvDa3DgWlw d2Y0CPTZvDfofo8M67tfQ+LDiiPXH7Q8b2Nr/CnwSPol9h7/ANb2PytcvZ6rYSqwX96odu3FzRq/ Qc5fRZTDPEjvr57PT6/GJ4ZA91/J79A3KJT7Z2bxS/FXYq7FXYqk/m7VRpug3UyvwnkX0oN6Hm+1 V91FWzC7Rz+FhkevIe8/i3N7PweLmiOnM/D8U8azhnt3Yq7FXYq7FXYq7FXYq7FXEAih6Yqg54Ch 5L9j9WWRlbAhQeNHFGFcmChJ9b0DXL6ALpN8lt2kVwysfcSLyI+hfpzP0WqwY5XliZfju/b8HB12 DPkjWKQj+O/9nxYTe/l55tilc+gt11Zpo5VPInc7OUcn6M6XD23pSBvw+RH6rDzGXsXUgnbi8wf1 0Uiu9K1SzAa7s5rdTsGljZAfkWAzZYtTjyfTKMvcQ67Jp8kPqiY+8KNrczWtxHcQNwliYMje4yeX HGcTGXIscWWWOQlHmHrei39tqUMVzAQyMKuvdGAqVb3GcDqsEsMjGX9vm+g6bUxzQE4/2eSco3Fw 3gcwyHITAEEAjodxlLJ2KXYqx/X7J5UubeJgj3MThHaoVWcFakgHau+Z2kyiMoyPKJH2OPqMZnjl EcyCEB5f/LjSLBEm1AC+ux1Dbwqd+iftf7KvyGZ2t7dy5DUPRH7fn+p1ej7DxYxc/XL7Pl+tkDoI 3KqAoX7IGwA7UzUg3u7iq2fQWlzGfTbadl4mWJHK+HJQaZ30JcUQe8PAzjwyI7iiskxdirsVdirz L8y9S9fV4rFSeFmnxj/iySjH/heOcp25n4sogP4R9p/ZT1PYmDhxmZ/iP2D9tsPzSO7dirsVdirs VdirsVdirsVdiriARQ9MVQc8BT4hun6ssjK2BDdq1JCP5h+IxmNkhF5WydiqAu9A0O8Ltc2FvLJJ 9uQxrzP+zpy/HMnHrc0KEZyAHnt8nGyaPDO+KEST5b/NRsPLOlaYH/R0RgEn205u6kjvRy1D8snn 12TNXiG68h+hjp9Hjw2MYoHzP6VVlZTRhQ5SC5CJt5l4hCaEdK98hKKQVfIMnYqo3dstxEV2DjdG 8DkoSooKhp8jrW2mqJE3UH+XJZB1ChddLSWviMYcmJfQsESwwRxL9mNQo+QFM9BAfPyV+FDsVdiq jeXdvZ2st1cOEhhUs7HwGQyZIwiZS2AZ48cpyEY7kvD7+7kvL2e7kFHuJGkYDoORrQfLOAzZDOZk ept73FjEICI6ClDK2x2KuxV2KuxV2KuxV2KuxV2KuxVxAIoeh64qhZIjE4dd0B+7LAbY1SKBBAI6 HplbJ2KuxV2KrJI1kWh69jhBpBCElhaM77jscsErYkK8NwGor/a7HxyEopBV8iydiqhcW5kKyRkL Mn2GPT3ByUZVz5IbVDcyQLTizuI2U9mYgZZijcuEdWGSXDEnufQeegPn7sVdirsVYb+ZmqGDS4dP Q/Fdvyk6f3cRBp47tT7s0fbmfhxiA/iP2D9tO67EwcWQzP8ACPtP7LeaZyr1TsVdirsVdirsVdir sVdirsVdirsVWvJGgq7BR2qaYgEoQc2qW4BVVMn4A/x/DLRiKLVIbscAGQjbpWtPbtgMFtEJIj/Z NfbvlZFJtdil2KuxVogEUIqMUIWa3K1ZN18O4yyMkEKkFxyoj9ex8cEoqCr5Bk7FUTpEdv8Apmxe YqkQuYWmZjReIcVJJ2+z3zI0kgMsCeXEPvcfVRJxSA58J+57pnfPBuxV2KuxV5B531M3/mK4INYr b/R46eEZPL/hy2cV2rn8TOe6O3y/bb2fZeHw8A75b/P9lJDmudi7FXYq7FXYq7FXYq7FXYqoSXtq nWQE+A3/AFZIQJRaEk1c7iOP5Fj/AAH9csGHvRaFkv7t+shA8F2/VvlgxgLagSSanqckhVtY+UnI 9F3+nBIqjcrQ7FVRZ5V/aqPA75ExCbVUuwdnFPcZEwTauro32SDkCEt4pdiqhNbhqsmzeHY5KMmJ DoJzXhJs3YnDKPUKCr5Bk7FWd+RvOPp+npOoyfu9ltJ2/Z7CNj4fy+HTpnQ9k9pVWKZ/qn9H6vk8 92r2dd5YD3j9P6/m9CzpXnHYq7FXhnnLS30vzHeW4BWF3M1vQED05PiAX2U1X6M4vX4PDzSHTmPj +Ke00GfxMMT15H4fi0l5N4nMOnMdybxONK7kfHFXVPjirVT44q6p8cVdU+OKuqcKrZEEiFT9B8Di DSoB0ZGKt1GWgpW4q7FWwCTQdT0xVHxR+mgXv1Jysm0L8CuxV2KuxV2KqiXEq96jwORMQm1dbpD9 oFfxGQMCm1VWVhVSD8sjSVssSyDfY9jhBpSFkbsh4S9f2W7HCRfJVbIpdir0fyR5zW4WPStRci5A 421wxr6ngjH+bw8fn16jsvtTjrHk+roe/wAvf9/v58x2n2Zw3kx/T1Hd5+77vdym2b50TsVef/m1 pJktbTVY1FYCYJyAa8X3Qk+CtUf7LNH21guImOmx/H45u97Ez1IwPXcfj8cnmOc49G7FXYq7FXYq 7FXYq7FXYqpTw+otR9odMINKgiCDQ9csS1iqItI6vzPRenzyMiqLyCHYq7FXYq7FXYq7FXYq4Eg1 HXFVVLmRdj8Q9+uRMQm1UXETijinz3GR4SE2vRgopXkg6N1p88iUoTWNf0bRoBNqd3Hao1eAc1Zq UrxQVZqV3oMtw6fJlNQFtWbUQxC5mmMXn5w+TbYqYJbi8J7wRFeP/I4xfhmwx9i6g86j7z+q3Ayd sYByuXuH66ehfl//AM5Q+UtVuk0rzAH0mT4Y7XUp6GGToo9dgW9JjWpY/B1qV79Tp4zEAJkGQeX1 BgZkwFRe3wzRTRJNC6yRSANHIhDKyncEEbEHL2hC6xpcGq6ZcafOSIp1oWHUEEMp+hgDlWfCMkDA 8i24MxxTExzDwG5t5ra5ltpl4zQO0ci9aMh4kfeM4ecDEkHmHuYTEoiQ5FTyLJ2KuxV2KuxV2Kux V2KuxVDXMNayL/shkolKGAJIA6nYZNUwRAihR2yolC7FXYq7FXYq7FW1VmPFQWJ6AbnEC+Skgc0V DpGpzbpbPTxYcR/w1MyIaTLLlEuPPV4o85BEDy3rJNDBT3Lp/A5aOzs3837Q1HtHD/O+wq48pamR UvCPYs38Fy4dk5e+P4+DSe1sXdL7P1q8Xk+cj97cqp/yVLfrK5ZHsiXWX4+xql2vHpH8farR+Tog f3l0zDwVAv6y2Wx7IHWX2Ncu1z0j9quvlLTlIPqzVH+Uv/NOWfyTi75fZ+pq/lbL3R+39b5x/OyS VfzBvrLmWt7JII7ZDT4VeFJW6UrV5Cc2uk00MUKiHV6rUTyyuRYJmS4zsVfU/wDziJp/mFdA1bUJ 72QaC8/oWOnHiUNwqq00+68h8JRBxeh+LkKgHCgvoLFDx/8AM3SBZeYfrMakQ36erWlF9RfhkA8e zH55yna+Dgy8Q5S+/r+v4vV9kZ+PFwnnHb4dP1fBiOat2rsVdirsVdirsVdirsVdiqvaWF9euUtL eW4cdViRnI+fEHJ48Up/SCfcwyZYw+oge9PY/wArfMqQrdskahhX0ORaVK+KqD+GbQdkZjGzQdbL tnCDQstDybIrFJboJKv2k4EkfOrKckOxz1l9jWe2B0j9v7FaLyfbj++uHf8A1AF/XyyyPZEesj+P m1y7Xl0iPv8A1Ky+U9MBqXlYeBZf4KMsHZWLvl+Pg1HtXKekfx8VceW9G/5Z6+/N/wDmrLv5Ow/z ftP62r+Uc3877B+pXTSNLQUFrER/lKGP3tXLY6TEP4R8mqWryn+I/NWis7SI1igjjPiqgfqGWRww jyAHwapZpy5kn4q2WNbsVdirsVdirsVdir5U/O7/AMmdrP8A0bf9QsWZEOTRPmwbJMU28p+XL3zL 5l03QbIH6xqNwkAcKX9NWPxysq78Y0q7ewOKv0B8teXNJ8t6FZ6HpEPoafYpwgjJLHclmZierMzF ifE4WKZYqxn8wtFj1Hy7PMIw11YqZoHrSiggyj6UB28QM13amAZMJPWO/wCv7HY9l6g48wH8Mtv1 fa8XzkXr3Yq7FXYq7FXYqmOm+Xdc1Ir9SspZkbYS8eMe3/FjUT8cvxaXLk+mJP3fNx82qxY/qkB9 /wAubK9I/KjUpZEfVJ0t4OrRRHnL8q04D51ObTB2LMm8hoeXP9X3usz9tQArGLPny/X9zL9N/L3y tYgH6r9akH+7Lk+pX5rtH/wubTF2Zgh0s+e/7PsdVm7Tzz60PLb9v2sghhhhiWKFFjiQUSNAFUD2 A2zPjEAUOTgSkSbPNfhQo3NnaXScLmFJV3oHANK+HhjSQUovPKNhIpNo720gHwgMXSvuG5fgcrOM MxkKQ3fl7zBa1IjW6Qb8o9zT/V+E/cMgcZbBkCXNdCNzHNG8Ug2ZWFCCPHvkKZ2vW4gbo4+nb9eB VTFLsUOxV2KuxV2KuxVpnVBViAPE4q8U89flBq3mfzvf6yt/b2un3Xo8Kh3mAjhSNqpRF6pt8eWx nQa5Qsr7b/nH3yssai51C+llH2mjaKNT/sTHIR/wWPiFfDD178qfyf8AKnli6/Ttrp3p35jMVrcT SPI4jcDm4ViVUsNgwANKjocnC+rXOuj1DJsHYq0yqylWAZWFGU7gg9sVeBa/pbaXrN3YN0gkIQ+K H4kP0qQc4fVYfDySj3H+x7nS5vExxl3j+1L8ob21VmYKoJYmgA3JOICkp5p3kjzRfjlFYPHHt8c9 IhuKggPQn6Bmbi7OzT5Rr37OFl7RwQ5yv3bsp0z8o3qj6nfLQN8cNupNV9pG40/4DNli7E6zl8B+ v9jrM3bnSEfif1ftZlp3lLy5p9Da2EQdSGWRx6jgjuGfkR9GbbFosOP6Yj7/AL3U5dbmyfVI/d9y bZlOK7FXYq7FXYq7FXYq7FVKe1tbheM8KSgdA6huvz+WNKCkWoeSdNnFbVjav4CrqfoJr+OQOMNg yFILvylrlpVolE6AVLQtv16cTRvuyswLYMgSx5b23k9OZWRx1SRSp/GhyBDMFUXUF/aQj5Gv9MaV VW7ganxUJ7EYFVVZWFVII8RviriQBUmgHUnFUNNfINo/iPiemGlQTu7mrmpxVrFWReVfLr3cyXt0 g+poaorb+ow26fyg9a/LxyyEba5zrZneXNDsVdirsVYV538iXeuajBe2Uscb8PSnEpIFFJKsOIap 3pmo7Q7OlmmJRIG1G3cdndpRwwMZAnexShp35S6ZEQ1/eS3J2PCICJfcGvMn6KZDF2JAfVIy+xnl 7bmfpiI/b+plmk+X9H0lWXT7VYOf22BLMfYsxZqfTmzwabHi+gU6vPqcmX6zaYZe0OxV2KuxV2Ku xV2KuxV2KuxV2KuxV2KuxVRu7K1vITDcxiSM9j/AjcYCLSDSQ3vkbTpSWtZHtif2f7xfuJDf8NkD jDMZCkV55O1m3HJEW4UVJMR3FP8AJbifurkDAsxkCXjRtX/5Yrjb/ip/6YOEsuIK40PX5lANpKQO nIcev+tTHgK8YVB5S8wEf7y/8lI/+asPAUcYVU8ma4w3RE9mcfwrj4ZR4gRlr5EvfXT61NGIK/vP TLF6eAqoGSGNByBmccaRRrHGOKIAqqOwAoBlrSuxV2Kv/9k= + + + + proof:pdf + uuid:65E6390686CF11DBA6E2D887CEACB407 + xmp.did:d4d07395-aa96-47c2-a9e5-d0351947bb0c + uuid:c63c1031-e157-9748-9c58-86481308e954 + + uuid:1abccb90-0c26-4942-b156-fd2eb962e3e1 + xmp.did:58fdc1b8-1448-3a44-9e20-282d8ec1cf95 + uuid:65E6390686CF11DBA6E2D887CEACB407 + proof:pdf + + + + + saved + xmp.iid:d4d07395-aa96-47c2-a9e5-d0351947bb0c + 2016-06-15T14:23:10-04:00 + Adobe Illustrator CC 2015 (Macintosh) + / + + + + Web + Document + 1 + True + False + + 128.000000 + 128.000000 + Pixels + + + + Cyan + Magenta + Yellow + Black + + + + + + Default Swatch Group + 0 + + + + White + RGB + PROCESS + 255 + 255 + 255 + + + Black + RGB + PROCESS + 0 + 0 + 0 + + + RGB Red + RGB + PROCESS + 255 + 0 + 0 + + + RGB Yellow + RGB + PROCESS + 255 + 255 + 0 + + + RGB Green + RGB + PROCESS + 0 + 255 + 0 + + + RGB Cyan + RGB + PROCESS + 0 + 255 + 255 + + + RGB Blue + RGB + PROCESS + 0 + 0 + 255 + + + RGB Magenta + RGB + PROCESS + 255 + 0 + 255 + + + R=193 G=39 B=45 + RGB + PROCESS + 193 + 39 + 45 + + + R=237 G=28 B=36 + RGB + PROCESS + 237 + 28 + 36 + + + R=241 G=90 B=36 + RGB + PROCESS + 241 + 90 + 36 + + + R=247 G=147 B=30 + RGB + PROCESS + 247 + 147 + 30 + + + R=251 G=176 B=59 + RGB + PROCESS + 251 + 176 + 59 + + + R=252 G=238 B=33 + RGB + PROCESS + 252 + 238 + 33 + + + R=217 G=224 B=33 + RGB + PROCESS + 217 + 224 + 33 + + + R=140 G=198 B=63 + RGB + PROCESS + 140 + 198 + 63 + + + R=57 G=181 B=74 + RGB + PROCESS + 57 + 181 + 74 + + + R=0 G=146 B=69 + RGB + PROCESS + 0 + 146 + 69 + + + R=0 G=104 B=55 + RGB + PROCESS + 0 + 104 + 55 + + + R=34 G=181 B=115 + RGB + PROCESS + 34 + 181 + 115 + + + R=0 G=169 B=157 + RGB + PROCESS + 0 + 169 + 157 + + + R=41 G=171 B=226 + RGB + PROCESS + 41 + 171 + 226 + + + R=0 G=113 B=188 + RGB + PROCESS + 0 + 113 + 188 + + + R=46 G=49 B=146 + RGB + PROCESS + 46 + 49 + 146 + + + R=27 G=20 B=100 + RGB + PROCESS + 27 + 20 + 100 + + + R=102 G=45 B=145 + RGB + PROCESS + 102 + 45 + 145 + + + R=147 G=39 B=143 + RGB + PROCESS + 147 + 39 + 143 + + + R=158 G=0 B=93 + RGB + PROCESS + 158 + 0 + 93 + + + R=212 G=20 B=90 + RGB + PROCESS + 212 + 20 + 90 + + + R=237 G=30 B=121 + RGB + PROCESS + 237 + 30 + 121 + + + R=199 G=178 B=153 + RGB + PROCESS + 199 + 178 + 153 + + + R=153 G=134 B=117 + RGB + PROCESS + 153 + 134 + 117 + + + R=115 G=99 B=87 + RGB + PROCESS + 115 + 99 + 87 + + + R=83 G=71 B=65 + RGB + PROCESS + 83 + 71 + 65 + + + R=198 G=156 B=109 + RGB + PROCESS + 198 + 156 + 109 + + + R=166 G=124 B=82 + RGB + PROCESS + 166 + 124 + 82 + + + R=140 G=98 B=57 + RGB + PROCESS + 140 + 98 + 57 + + + R=117 G=76 B=36 + RGB + PROCESS + 117 + 76 + 36 + + + R=96 G=56 B=19 + RGB + PROCESS + 96 + 56 + 19 + + + R=66 G=33 B=11 + RGB + PROCESS + 66 + 33 + 11 + + + + + + Grays + 1 + + + + R=0 G=0 B=0 + RGB + PROCESS + 0 + 0 + 0 + + + R=26 G=26 B=26 + RGB + PROCESS + 26 + 26 + 26 + + + R=51 G=51 B=51 + RGB + PROCESS + 51 + 51 + 51 + + + R=77 G=77 B=77 + RGB + PROCESS + 77 + 77 + 77 + + + R=102 G=102 B=102 + RGB + PROCESS + 102 + 102 + 102 + + + R=128 G=128 B=128 + RGB + PROCESS + 128 + 128 + 128 + + + R=153 G=153 B=153 + RGB + PROCESS + 153 + 153 + 153 + + + R=179 G=179 B=179 + RGB + PROCESS + 179 + 179 + 179 + + + R=204 G=204 B=204 + RGB + PROCESS + 204 + 204 + 204 + + + R=230 G=230 B=230 + RGB + PROCESS + 230 + 230 + 230 + + + R=242 G=242 B=242 + RGB + PROCESS + 242 + 242 + 242 + + + + + + Web Color Group + 1 + + + + R=63 G=169 B=245 + RGB + PROCESS + 63 + 169 + 245 + + + R=122 G=201 B=67 + RGB + PROCESS + 122 + 201 + 67 + + + R=255 G=147 B=30 + RGB + PROCESS + 255 + 147 + 30 + + + R=255 G=29 B=37 + RGB + PROCESS + 255 + 29 + 37 + + + R=255 G=123 B=172 + RGB + PROCESS + 255 + 123 + 172 + + + R=189 G=204 B=212 + RGB + PROCESS + 189 + 204 + 212 + + + + + + + Adobe PDF library 15.00 + + + + + + + + + + + + + + + + + + + + + + + + + endstream endobj 3 0 obj <> endobj 7 0 obj <>/Resources<>/ExtGState<>/ProcSet[/PDF/ImageC]/Properties<>/XObject<>>>/Thumb 14 0 R/TrimBox[0.0 0.0 128.0 128.0]/Type/Page>> endobj 8 0 obj <>stream +HwVu6PprqV*234R04S32P4ճT(J +W*w6PH/H+X)Hwr.gK>W /@.ӊ endstream endobj 9 0 obj <> endobj 14 0 obj <>stream +8;W:dYmnJk$j=`^PKX*GV"-/6MPPhMW4o*jFegTA5n:ROqi. +8M?-(/t#IN>re.=TbIMqYWQK1D%b&pOLGa]H?hKs'8Gqa4A/k;[i&\e-=4:h!/H6BW;~> endstream endobj 16 0 obj [/Indexed/DeviceRGB 255 17 0 R] endobj 17 0 obj <>stream +8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 +b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` +E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn +6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( +l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 13 0 obj <>/Filter/FlateDecode/Height 947/Intent/RelativeColorimetric/Length 90241/Name/X/SMask 18 0 R/Subtype/Image/Type/XObject/Width 880>>stream +Hoi@Hy&8_nyA'?6G3+ZHҥYakOj6gיoU GHII_AgK/EcF6LrchI 2$҆ԘU4w$5_7BQUm"Ť>&k2W$%Nib;Iߓuavտ,HJ \u.&1ٌ^₞@Ǥl_Lrs:#ј,32] IJ7d+65i1$Lb#d]G>&Y=g답*_/*:p.uʙcRIf") ˬ#q4Ό=sL&=(P{ HJ+b~n+cSFsm0'&&cܼXI=3zER,D#0)2=r +I Ә}즟 (9?l?ݳ;݃Q~twoo `41)"g476WxzGMݞx7hpqh{ƃn\ w Zᶂ37{M23>)25Eܩo|+>8q/8m y3=??~wL#I\dΕ/doޱ=Hh|d]ү$*wOc} yz<*\@~R/}}FRHxw G]as &lu9x")`m;=-Ƀ -O8Cmȑ{mG.&?){3];,V01o`it4)'ѭU, ]?b<ݳN=;.]Lں*_w6}hL[I$np+bdjlb46[ܩ0k`I{-ɯG_>]zt8rI_K}4јغOinӭng`HUN4ݛ|yRr #+x/>骞SyXAQȃękfUXDvE#&sBe< HQ%\Ωdg'sǟá:>xQ +!K +W<* '%Y%Vmao!ǩkv>w u{=Q<\ȃ*fƸmqY%ŏRpV{ ueW&)!)sE2Jݓ?ҋӆgohԎeɝiFGb}/g. +,%m.7'FX!TՀITV $y9Iפ?_ȼ0;Wi;h9:FQ]itB)`5yIe[j*lraպS"u 3$hۯNT´A}ٷO.ϡqˤ3!"Iڻkha˳J)@) iq1J٦oչrEIWt+>]hlrW9,-nr_}i#eR=椔 5 ](?"aIK9;z>g9d68 =FGY/Հ@@ 9ۋFJT2[̟~>:ekG<Q2B&M)}YƢ}\ekfeJ#.-3 +iHF'>hd,I#_ыTj~Q5cR`n:s e8 P/di]Ҩm +!g֝V=@nI${%3Tj[ԣ`Į;m$XOT4==Aŵ͆ޭ|hˑ"6XWvZY,{&y` wɵs/NٮMDz3z2 F^ęA r۝gB7hu ȲhI})CF +WWzٶ:lgl7ɃHiJ&/ Ӻg.}C'dD|V֪'9TL*4I]6 x74MVK%X T8ENZ!f8ah@&͚-AsׄOQ"2sO-ʃ#db-tgHIFHVj.Y!х@Cdҕ@2ǯeqyJΎC43nw0"D2ȥXiQϖJ'&:?Ed!ªGIKSيb$utõǭ2'}~dbNct`d K񕨈\29juC ڣy 5,ҧ9.~&g(r\&$zkddjd_&n,dk딝]|ڷт$lw)-H](H&, tHU4ѐxIE$\+Kl֓ȁI*?^^O/N*)iit<~O&=۠SZ0LK578hsZ?5ĬeV"k K +>#gV-?}= TjOK<>Nh auOBnY#qkB)fiQ@ 7@Olo-n 9 =)~erŗzC9z9Ilr&JO-NbW3Ӳh adR<+}D?NJiPJG%:?5pn2TI2v֋rBk &l f'<[wm}2I4KtwH r"!hͣ .:17f͝$Wq`Z Z 'R\%n~2/f|i|a*J&r>-fj@eRc}'yGBvr*'r +>|.WB~0ɱ{H ܌232ɤMRe7r!ic/_Ȗm͇OJ!dfH)OtE9#jԝj'C]aպWf+>KctȁL&rK%SͧH&1'ejuQA2p!Ϟ* UKW?-02hZ!nKO.? ZdzѤ٣wrLI*΍Sі2+TI,5N$6ぺ G7 whQXI4:?5ƫhq-Ǿ(v,vHz&.aKiݵdTOph2E ɤ0J>-zBb8,A6Mgd͝$K I,E[ 8ƶ0yTS rlS]|ѩ+&_9Ezb(Jr2h+ɓ)⇻A!_:۪.%ٱ4E3w~ sOq9F$~EH(M"0>"C|)4 {edUŭ߿/|}e.tD _(^u)TJ 8([E;ZgbDR!TY;g$÷=W__h8pü8SqO㠸*5:Mb)r2`Ny@:iXg*-X=zb-osW̟Y[V$J|!h|$p6V2LRwsU""YA(\A[u0#0j>k6NZ *P$Idv`;K?29G3@/)hqGaLH&)#f2ݥ:"@ +c1BuUU!hB +m?IXqBf=O-uS]*pb Lp=a d0 '%}QJ1kv-E&Y%͇ѓ!L6y֯-ZNſw@ME<V ++Qf1XGbu.AL}{;j:1XM;`m)ݒr2??bӥ^"T.4{7V:7cO n]&IIʴ׭]׳L&ټ~e?618qW 6$уS-J+j &HR#Y(u3C"vaVO qˤg/{Nd* w4~8`ਡODT +( ƃ(rVu6z0F|iU6_Up_ |7//y e26kaE9JTh<'| e\xy(7QQ1Z)7#5eoӈ0g+ۨwCxc XSg")-nkEJN[* FAKLLR7R.LBME#@* +~U݊tEEVOt7U콊ؾxԜ'isjf=O[ZO ((A>&]r"т|f0`A|0/2}+58{:!ELTǝuB1HzGQ0g[|Q_[VSor^Gy?lD$g=?ȩՕLN9 +K*RYģpu0%K'*- lpD ID2MnݾbL)OYׯR3OF"R j"iO8%{Pqs ?Hee}-XJNm\-H/}GϩZ&L/u>7&I wQ) /+P.bP"$N55B]={2`[Hnk?-s\粓y*dqP9I,1k[`^A/n3ՕcVna-_s%YSM{b FǠoZnkE%yt$mAs%Ev]JW^xA Xk0v_KӉ i$Fߪ/u( jLIO%k/Sr7B>Y,770ݙs)]ubQ9OΈL12$jn*2*쾊O&iV"sr+ 2LjȞCxҜJ 9:Jʌ H(hxA&xk i&Iu$6ԕj:.E Q\-^*%V@V;RM]dLW<}*w&Kߊ{-7@-&;. +C66 @9TBUfI[#v1r`,f/5n TLֹޤosIwT&\ߍ#UBXJ&uo6TE-EHLu[FUf +x謖Xz{FEr6qiVd>սl +\Uv^dKCR&p6kڄo@)ɛzxZZfBv5nFC `r{Lŷy7g2H&;x@kASYQC,29Wp +c!{)r*Rj!&#8ˁScM}Zi*H&Mf$\P +Ŵ\Id eDҐIЍF1|CeH ldԬ6i.2K8t׎&t(Q+ZfB*R&~?g4|W~ !$1[NIkqS(T['iͧ4*m~@?>KT)ΕJ3t +dEÀ."!|g_F>";,o)%OQ~Z2FBt-Ŵ=ݗJ)Rbd/}0i +3%f_,%.u;}oZ_`>19)ۂ֙Ĥ b- 2z[&;BEz1i6Cӯ!G9htj9'I#8ˁB!=*t-T:zTG\2z;F3}ZgLhHӍָ!fiVL:,N0EwHVsR I !-U֐L1#4zvB`V|u 4i$\"&^Xjbޟ mR q6H JER/-N&w',͌ƹӿ8!fI|1TBS?D%}35fW̊CȊ\!/5n:VC1@#&R&2rÚ!"H OS&c1[pUyuN'v96c9)Ӿ/I W%f<}*Gz{-/0D֧)T!LSXva?:n[ʜUX% *R&~o㹤w-dr>و'r*+*1=9)zKy$Sp$"ӔIWcQ@&~!AZۑ[W *'W|쌰95n}ăF.i(xyB [(58|i+&Yjhz>˜2m^?fA)\('N,1^{,gI&}<Ǿ dTVԀaI$7XUkt sU dl:OOٯ<2w)6E =Lq<{ hK|7%FE=ikA(#cia78Hw:;i'}h%Z^N^7VҺuQIHNcMft+ +0 '0$:HJ\ ;``pPL=NM.H1Eb~ԓvsK`TO=hwѓ7|GOYZaȎL7e Ջ[녤&Ɍ;b1lx"Iw!}9pXfv`7xgV~π\\zπD-/؁_~ɬebJz!QyrTS蕴.}?]$i;o"):F)(&ۂS^/Ǧ1IG )!bZ1 p8ĕT-@Kp +m crE?m}F!e_JRPF +7b1T}<x4zV,&읲yeTJ=q#cz>)Rv:5[QГO"5o) c^mXyTعT=%o-oK2U~c͠B>(h1*h|:6Ll' ޑ4 =ui7eGo{s͈KjDE1!3e00R,I %y)1]5ά>#Vgr|%vVV>&I5lS<v Z ;RLj/1'{uO +ؐsz( o?;I&mT@L>`|wdF[pY!=;Yk AR;o^2Lh ~_ٛ|GdO q/zRqcw_}9~eiq$i[?~$N%Y72zىSEx1,HDlEg3JJy}F}7)?'|(GoŤ*isFGO=`r3R8)p/MM$j٪u=9(&˜|m5(t75zM'O \]]ITٱ3u0Ǜe/$iF1Da*b1Tzz_W8/wzg'=srV~@?];(&0H1ʿ[P=kW˻zBf`d.d*XJZC^mt.'h V QLqr9wTҺ辣QEx=D19-d!}?d!}3Z#)nmDzly_|1^Nd`Q0l9'0NnbX9T0ZdWf˂8d=pJl#&BsԨA7FDqڇJZ*レT=eSH'YTo4>doE|~ $#&1¾W` ڑ1)د6:'P}O࿠ne*Fرs|q +(iC4P+ $ +cT6^b-4je˷O|zS~?_qCjRr̖E˓>jEk.C-n#E<IO{vE5ӄ&EN& ݆)-:< I'%}8HwяV4d=Yv 1|Dh*; <Okr(Ny%*~*Yy&WA4!x2zsY-L"=\Le5ƔRFI%IF\3_87 0>hq x2`+IǙBh#R8-w*Ó1YeVO[x!mh?[ <$|`2s2l嚽O EҌX)#Z!Sр4Yoy?ªf8jO1O_9O"%z dy.nNY2u5UV\Q~ɲ|kxrd'?apK tE7s!m`jqZv[>hZ-%6}az,ڜdKɷGM)،xd"6T\l,&c<'Uf; +w&B gw)#„S]\QVQ>$I_jJX,\^YSd|'zD%{o1!䅇qx';qڈ>
khYӷ@mwGyxbr ~=ͱ{9hsۈ!x2< !f!mf" e!ONYW,B-%'>,;} ^&CE"OtzK68]dGRfɈt}B)ILN-^3d[ɿ/3GlV&77ug#K1P)^I\D}&/o>߭SHh+<1Iy_uA^ +sMzC*d\'\z1zADd& +9$Y"?LtzK_14*Y|!ԯ)7$URyuf۲/ɖ$yhs:ڏa<#<(){;qSdLt&}ZHy$yyLܘ: ew}\yYj|aIQ%#?xCE"Oq1Nb5˵I~t֌ZcEb-%Շ8}@i?qqN~ Ndl'\z¤.o !جlݜ(B],YoSO&w"0fr +L&\Pby?ޓur!m FZKGbrΓͲc)eE+WqytT?w ]]}["֫K5Ez~J+T3YnO26hSEH'㷢MO$ta0?j9VuK'/K~CcζI-OV/KѸmkҡuΦ)"&㸔]LyĂX)cML=yn\Ω۬ crї'ma5r(E=pu7< + [rd{d7.`w(d;wr(M=zRy +7]=!Ij9Cidy!NmSiǯƆX9r R:wP<+y^{᬴$eYn;ﷂ2^%)uũ Kw Eߊ. UxlidW)I5Ip΍y%gMGƔd Z9"~t]utڵɲ2E#{Xtp7 #i4i>f-2x jL?bGœ{y9k +AQש'=FE4b2&al6>` +hB");Is*QY9c"1鲒z2klvy0 7>`%JN dXn; ŘWO8@g,צ)_cvH$q\ѾM_@%Ƈ؛XBRcΜJcRΆ1xZU]è-9NEw'c뜠I=]c fi~>?!NI&ļfb2 Z8,W䥌e|a(!me?MQH'cMsY*+!\VuSLQ5Kp#}֓mj:SHú5\bØC)I0> jYn>_+c t57*pT̛6=nW%4EQ4z2~+ɜEO1$|T9c^Jt,Ύ9AŵK2Ɏ'+{,uCL/ڻAZ" +d֕MW.oBgDtʿv(uX4Kp}Gߓ-8,]o^ѓ[J^c(NY]$eo h9[ƣ:1vt᥌e| q'Kp4 b>HAB t2n{'ͩ(*rGOӡz>(-ɘ-d6=г.k؉NO&37{UGɓ*Ysgzҋ>XA[ &f.oqC󢔱gs.$e/;?n>.0&cT51}<;(*FOkE;a\.S+S=˖&EǗo3rLdA˿}yb/IE\E"*;pIыeCbuZ ){ٻ #=I_cN|k)rd@Q?h.3Ed^ts*g5 xQX욮&VO䩪(Idt1>ðh[[AEX̕ϺܓyK{./T˱^>xL,Mo'svy[/*|OJgC{::EQ1SDLk%>>Ѕ<I t -eo$7-gHIΆ(&-^^rs *BadVؓ-W*j͉IvHʞNLO:W%_31dӨXܸvH^|Tݓ+9/Hrdz_wq\e?@MQ̙ݙ"NuhEA(iK̙}v(AHQ"@D(WDUlMĎ-'Eyf&٦Q|~GV ?|mzR3|}pӬ3 QJoJpd\nc\7n,Aײmɏzs ޡ22JywoK2/X'& ځNJ* pbR3|w) A7ɤbrc )#f dp[M_&g][ه?@O*v'd5Ä-`˨[ GOFntLλ=95fPL +&!&;=@OK1Ŏ=5:2J.778&$k4RJGL*sͽ֨+IN,Iij&}`K闍sEvDRϿd}OIb_93Da`(r\|@O@Moߎ$gi)R~cb]_+$H!n~F]1GL*vZFi2T'{ z\ARFMbz~wb1Qe5+zRb25em-3_~Ȯ) ֧I/_Ox^JIQOyT=F9CW鉣)fʴRڴ1[Ig + &NA@Ot\ᏻNoV0[%dRїbۤARJwu oX7h4b0$m~a'U:냰6G8XГOГWuNVL1҅ Qp00bIe΍{֠ 6 =q(B:w6Ñrޯ[?^ORLRIv+/gRJ:A{MAOzIN0n/{Nac5H$,+ԓr~ ~OWllfC+0+ГwځZWBA9%gѓ-y唋w-GF)Uk(5?C%;=GLj/bIIzYw~<HיմFi1GL*t\۵ŤH5$gP!||5Y,_;$8hdẂHh C~aГʜ` 2$R 2-D=yq{IeMИJ)1 wT2#&:=FI'@L&ƥfZDRʲ2n%%AL)~(_"H1IW0J W'91S;nZk PJk3=) {=^)&/75fPOG3-#&7@.d{LO\ ~OywtALL%h//8IeN@7sbHbۼ1W2\jn} bRn@z4M6 +'?Ztw +٫0T?^Г" [od% I'{}q{LII6WeVgROS8 K@D\>&;sp6"l\Z= SԞ89c-|~ۘlr\iƌU?D'3&Haaőom“ߓ?wP9Ô"BH$&;m5wI'6WvyCi~tw g#̵͎.p9 FЧziۈ$)&$#})r%R+ [=ړ~t; ՛!Qײ^Gzr}\10Ouc+#C͂&(_HP(D +d!թXKtkcZ*yͅ (  w¯<\*Db,$=OVp'1K.:|/lӥ+i&o练Ɏ[UlL=t _wHY{D'C%A)Cyt)8tDzPˠ|!B!DDEg ̓~WF/Vs'A\U/! +.a{0Ç)zfnڛ>< +.ĕ#_uMLzb)ZOVfc+UA)" +4D')58=26L">^&Ư~nc#{Uҭ' T Z; $U:ri +_͒K 쳷x#LJ4K\4^mΔX][XVBf@)5:'7}OV2L=Piϲgc0Yh-8iҧVk6\o'Nq|$T($)y6Aߓg O"Hb1flsarEtku*F?L$ |)> R ѸoB(L7H>IwUhc}[3;/)go2qCJ= RHOY$BkѧJ6)b Fl{h-8թrbVyZgcF;HҢt@ʰߓŤA#v齌3BILODxRzI;e5 Rkw'w9OD)Ý$)Cy|O[X)7PG$E,̿6D\GLJ_VO,Zhֻ\/of>!Ç6A0ߓN(+MC d.ia1^j&VmXuw}q:ZLa\RM24Iaw ! +yš|%0KeX\vIِ)H{fZ;qRC{ /Dny]&OkꉥUS=l'凯Gw%)H3ct:BxO 3$0.e#PɶO +|XZE_\(ZODhғŔA-]%f.>nEѫpE/zT(ЄOJs-M*_*PEJ}{ Pm3\)W>[w*]d:@,wZ$IQ@97,aNEd$KeR,}jV]RDP($)]ߎccC$BhGlkFbRz@>ZVN|H[l$R=y:QE>HiϯFxbJjVV5ܮyR^ +rk'eG!% :W!G{DNhJ\9\wACl +wϱR>"j'3J)_PKwG&) wZtݠVwgc)ßHaO&nr#󬦲l'3yxY}fdKJdARH9E}%TTҌS4<zbtXG٧WgB'WVD1T}gLbi[wǚS$B Lٹ#VLXC+;ݪd #+oIA{0]ceR(d֙F3$Bxt@V IғVm̴dE!)yJ>D*:!Zy1Mz/PBIf0 Ҏɸ; Gځ*z͹6HOI`)\NW~㠧£aITVߓMӬ$B'ctL&ݪ\Ylik>Wccyh)GՋ`Ji\1W݅JHrmL*w@ʡxѲHzCx /+΅cf%&B_$Gc&/ ɴ.>)=yV`pB_5B#:ʹv't,;fs8KUeD 0p1>$Bhg91jILJup6ꭕ7k6$?:L2zקb`};=R=fyq($&jkeMkoxs9["L +UB.t/MO0tx!Dn}~yLҿV]=2f^CQ_uyp%(I͹䈬!I-yBs95kIAQnԩ{Sѯp篧Gm2=d7R&Ӻ4M 54;=NGc2ncRt`AJxw&ӷ4p#BΣ)Óe6y0)%L^_׫rhe{-G^O9&%4j2Q/ +LAHiL"CɇvMå)30PfۿՂXa0)QqVgsIV^UB~l׋ Gސp3 L4RI9&M?V !$)n+Ye6\V0YO'j Sm,u^)dw>R CҠ/eO 5`2 j'#=%JXHPe9zTw?pf}iTnQntwR)OX"pO%tT&Ϗ]3)$7ePf[pr޸||l5pndғ+ĽH9zK6]mŤ zͿR8!>u=oD!h`EE}^:zO)vNVB%Ϩtg13nՅvkj,2Y'@]>';qQ)dzٳbT O? :wu\hvߟ#zٜl]CV rneu&w{LJw'R-FE?!R/DJrI$d<12,REV%U<7g:fg^d`bcꩶ[:+7>VΫq ҴP+}coVD0+ [uBKZ~ RyUs.++3UgD0:=/mCԁ*#iU}$]9e88 ?N!"::-_:kúXQG9zչ'Iy\8R*KƸ/!~Tk4NFIʞ.9])3#ByoL>{cRnn[n7V>)WKRQY#UzO?'s=Рg]duC. R> +'}nMty!׸/0y([v7t%OZ`bsI=P'L'ol3{%RJ }&|DQ5M,$)KBcuq\B銞SV'Ofw57'H#RΘs9'R/)Ȗ1BrTk,pY +}+;B(Ř ɯGE'ts+ mF\wO;KlTȒ_SOcf@=-M//:GnW?qPgE9oO%`^ xm{5Hܞ^Ĕ™M:'Z!2Wƶ4ro+nopEfDjtKГAbk&V=Bj%ܡHIc8gRchJ\#YYZi\\h<]R]Q_^vЮPv7>>i <]NlskT?'P5mIF@OU)xUaT}#F;B@ =~_ `rm,Z="]H ?jjR*~yT TI*{zԢ,HF +W!DdUb;<޵s[#Vۦ-Q|_緍Thwr+PKR*B@E` kR.Itiai^ArȥkP_ڃbDJl2ˣfgIrlX?gw>X<}"W +*e Oh)5ЋPŅ]lxh7&\B{ԭxhvRzE,Y0C>yF UJA)_~D7DAA$;)Q&%AGt)K^y4ν* o{MO8pr\r@x"Bqبrۑ]JƾИk7lJ){'@oJMNJ"\ Fc}IJӴq%M d* ,l(:KU+͌'ᏏGJ)23,I#kLZ 9:F-G'\Aθnqޒ`w.kY=ʆ7 ?>:ϕJ1+4V(*il"K!ʓ2G9.]*ӱU@)[r7]>cےuLDMbV [FQ )zeK W2|2& %n0I]4ukǢYNfh;Afbke2$op푮^J2\2޴ )HR>}yRE*oyd } iAbI_ :KUli +d4<@{d΍b}rJ4E7l;|@*w&$!ϧUke2t-:] +,!߼l]}~ =oz{֤X5Q$>eL)Lҹא/] +Og6*beC>7WH+#bX&8)rXu$|RGmkÛVlR޼lURՙ+ڑB#0UIEKأ9~,bԲ surX`,Pvx#Er TUUnMt)NOsT,pa]~C@0 蓉-#$Z|f—)E\%.rolϛ͂ / ߍbE2yL{L& "t{~@C"a(R +tRuf:NReؚ3CQJXkl cfS,hICc=u0_Wfk>knL1ז^O> ~Q'tz`'#W xV +t`O=?7F{Nvfowvv*QJ*0 +D?ޙa B J_$<z;i{wF#e={\&C[r!7&'kn¼~Ѻ{]2 @ *n{Q^Qw+eǔwT>~',U)+DBGbe!z/E"-|tʌWXbvF<6NHP&?pdrA[_Wm_ +5?&PF1J'3p|R]]9M]9LL2 Q +LrHP<ɤv4ΒV^ZYv?`vFRB(M(  +H4JoէX)Ϣ G)<Ʈ@C*p&̟\q7H&5UQ^Z^u-R)E7?A|^u60H%LϐORКr{$$A@$n|^v$zn₰WSo_Z[sSrdRޛ>||R +% +X3J*%0|,ϙ"g,39!+\JdR"NtgQ^ҊRlr?R)i'a,P * Jycq?DVI1? IM<(.-i[-gb\~{ ֟!ɥOZ:,Ø9{ٵJ:36pVݕII- o޾Ѳcݷ85kk,K(;9g' 8r[a/#<4+, +:VInI(o d^r@ԛ/{w?p_&4(eDRcёD>]+Xkdqj22y{6pdRw S^yK RE)10Kҟ(. E6L, bT!ЕLnTνe%U-V* [}iIX+ٯUBT&C86OʳDP1[]\/&ְUڪKKjn#2LvɩO%JzQΎ~.' τ9+RTDL.tdR>"V+[Wo__w7B/W3 O.9+Sl>t]ӉO"oOB|r +VNͪ^32 X)mP ?sbֺVf{D0/#o`7ΒVm_Z_~ BpSETTzaLtZv2Z)8Jq%S&I?IHd:A7.ɲG=Pkɳ)?(_;YDlgO_!;FJ**e' )2H& zӏz,T=Y]=+eQ/0g"cb|蛲IkF $!YrڪKK4.»5Jj;>i:':nA){;,nXepx}P<4MXyd@=K?IFmr[ŬUT=Nr I!ԡ +b(9qarJans=Iu f'-'Lu,QJ RtRJHEx<'*\aOpw@ӣe nhzuFa·-T_~M2I<L3+vm n a`Vx|€q*&L:t+/,C&MXeUH8mL^UI2IV9{5)uR +ƏA)(#nao$<2cIGG&/Zw$Q ھފ}!iD_~ϾzdÈ40ɴb)+2 dtd}wCxkW 1  >U ~lUH&6(^q10Po=&L_v|ș$+Aer@B6.䉳:/ Jy'Qʹ sx7o}'oLO sSao^<I) -2 +$lWS/`_wt7U6?J)p0g%z*u#"#eDy ڔן8ȈggnW4c[4R~zŰ:,,G L}ʳAHLBoHxVۗ~,9ʛ;`:A)10C|E"Edxvۭ +tbX:OZ` RFyxQh$TIcz78'a0y&'2Yd̟L/b]U/`_w[Q|$wYKRwEWp =NdcTWd@gBDHvrPXx^`aL?$I&Q`OnfY)*͎LL cz$GD<Rѕ۳dIⅉvkLn{yL2U+»=J$FwB! LouM_9[ƵR`#JA }IlVk^ݍ[[$<9Z; z>I)E߆Eܘ&LiÐ/Q/|e0A EߊH&~ȑq?, TUt<d`_3MG*&􏩧w +H\A=xraOjVDEI`NI&Ό8隧ϗ7E69t}y^&+tz“/޽%vp\ԧ">AnB.WC(yS&rt+YHJ W"U|C~KJǬwRŔ!3c[1 FdI2qǐxCo)Bi)LN0&%6$5KL#xG;n䟴T%m8<9%S4o6 I@Kq=ow;Gnۊhх|!`Jd}<v,Yl6I&ozIki[іp뺤u13O*QL#dI'LH;, o+R1 ;ϝ DDX| R&K!R]?y;i'|WKCr0X,>{;P7`Hdt~ìsVBF *`V7'98QN;&Uqk¤Th:0l.0Ϲm>h7JmTEHy R}ӿ_J9pIcT~B{Lh"axov% L"%˛:0Nܮ7Jm7XۊdJY>;)E{d ;L*;[0&|X$Hl٘LD'qYTjd]#mcw%R/oSa~/؉0+ ^&? +\CD}#IgJE>Bm'Rӆ"ixؐcχn' =B3]LItf-"`*E'GI)\R&'vؒNK)¤V3,L*> E}o|| +Dң[+lvu,ޒfZ˭+G_2T$fvS3DJUzDJF+7$; S2[+K}t]aBz1e m л~> R"eΙFc!έ|F W"%FI O)KHrڰ8:3Rƿ$ %R$Z㼩{W"=q t)pmyۊd@}zY:3JJ[F,qB&$Haos\7h=$%L&8ͤj߹4n1ȡ3)틤 % +n^_G,hZm|R0e"<9XiI$O.>j<3!R i^L LrД/15D6ĖmEl6uA]W6<=H"Hk!.I4yIܬEKLj6#k[F|r1 [C YI2ܟ!M]t-u:~lDAVtĥ3LMU߬JI瑒:vT +Q$EmHfDSɼ߽4Z&Q0"v%Ls|'瑒PJV4 h0yd…F e3LV[җZ.)Hm^sH=10 H^;ly,pg/B~\:Ôi| ٘F,& z BF +&H㑒#RʆBl, m+ +L`ڪlѠ6~TK''W"y0i%#gL襔AOI,g~et)AAII(kWu%2?X[E"\NHZ[Ѯ&QPs/Ƞ)_bɆ+Z%\yѿ^% cG_ I҆)щ7dDo·=D >V`%^n_&|l!#Of!!e +D'a?t1<|)zXZgz9x;$"%v)i)юec^$RӔ/7hJd]kUҳg}qOb&3IYJD~Fە30k1.zmE[_=.FZA<#VNɁ.g*|Rܨ=ާ&Ir}dEGwL~F4ƤD&sQ> &nl.]bj` Džؠvuf|c0~̈ Dh_ne6v/r+.& -BcO"N*HsVpIzQ.h% P@ Oq-ko&zcǛwy4;k5$wxPѰ@cl.7x[:qΙPJ0${p!YKVb1`= 5Z-0~),ȸgG)OEI)[K@#$|=+qPODo[kVf'g1sg-gf"p]N@w 3߈%-Y,p|Zyǂiy1ո*DIOu=tNZací( (8s '%$!@6/cAѲ*e}Y>Qc33gC)CB2de 2 ?eGneel LY(Q4:]rD *l= aϼ/_-t쯥,vAh,XM}ow#$D筫3y(% t0b3F+d/O8 &d3cAjBtl/hA;];ICJQJJ d©o-Tz*iʉXF:72'zڬvaf@eJ;}3R=L3/NFL>tZyZb*UVf/9!l{JL@). d]h +V$*#%RJsci$—0R.dL@&@@R+Q,8+δqh_=DXq(%8wr⧟L ڼj,zIQ6ǃj\kNA[fk$^rθ%rď?;s +2 h"V <44^WGúZU6v=JIF. +ẅ́c=M~_ghf]Sɷϩ`6SVOVd-6秋1}ᓈ/U# ??I}G> ;9G'#~,CڹI9=3 z+{ak?qz8gd%$}Ye喱CBN=sXlQOO"~ɫ#旗Y[>}OM ʫߌON 6L_=>~|'ޙOFLYO9ϙ`hg&W$m=4óI@A3+YLYyrAg;5MWځL"~6r+WԻEI0 KVs[ dE-irB ʿy?oGxzt/DhG╯O]Ͼ0&ѽsg#>|Ȩɿ?+V / cAeò1?7|M<\ݷ.I[GK3Sԭ7.J|7ux纇>?<{_}d)*n?&LC+YСLmp$x>}QҘe1LWe^9RgΈ7QdDGp#oU]t;w%ǂF 09IJO"~Jh(^_^Tfm34{ɞ~{xyiIR'k$4; $hY4J D|suIng=UW'#4&+qDO8YuH&UY.q|2ho,J,4҂ک]zTe{lڼOM5ZI'1G+a#5!aa@ʉ͔*ڢGhs'io K0Hr$>,ffQֲm[lE:>"KVF={jٕm (hql!!&$ +g8& Dỷ^/Z,iUJ|β-d@[I$ٿ̀Uո*bw'C7|B4<});B/R^`|2&V溳CI7Rr}Ew `]iiJ @_hF65'7leDőh|[)v9|a6'E̊X @hF I>l'(kYӝeO"=~͌h_VDK#-JZ $zf@lO&F[uΨ \|]T-V~ +$HOkidm'W'sY37%{HVЀT)R04+q`8AW'v_+owQ87+PUOKi 4k ybk:jO"1ƥh_FiEIwsgh@jh)+ T㪨UEKc rI`KЀTOkpʊSK-*|aJȜ7xLO}iz,iU.O|ƂmXmSuF>Er$SMQ\M&> +<}!jqj!!GuW'g!$”P'/\FϩeI+T=FZH3'H~6aF50t +J)H ®A]T*Cp&NlLnfUYT*V%6a50C00D?Փ+os9ؿAtjJ|LGq'l/-~zG7 0OhAW\m5-2V*Z<!Q=dF-V"`R!ZZͿ |;?E*80K2HGܲ,K̡x9Ulu,hOҰUEĵ.d쑭J (h5iI5˾:b-#0o#%E?+IFxl'Qw`4smH*3Ͽ9b#|9.Ī:Id .2}'lY; {oCEM+>#XJ=5k vi-:ӿl-ŗ^ao}^ty`$u-ž+fg+jZб>mHȢ+[wQ>j`"!jU.ZF'GeXM)p_0D[߉+vhh5ֳҵPZyhRUhs|_Wm(ًz%QOC)MMrЫcK3;>;|z2 vA' F;Ͽҳ*G@ΩV,|oakh9> nI4E##ɋD\[4s"%6 *Ap'6Mrg> ^L* uKg>9ڜOя>5,)^I^4sKj[EYӸJSՔ$2;A9+[:hZyt>#kIw.=5-3(zv||Wasrx8qYOIM$Uwъ -k)>%`tsg^. +{0y=k=+78\ɲE*'k_k>1+m;QOD= `7twKKj"T,)'t_S>)yvtp2 v*(lKj"Y+ldTmNwv*'q$me -znh)2ҵ8'VfE">|ڐI0&`@6,{IMd(X\_IO"`ƾa߉'D# `7) ^*R[US$qrأsm`?opW.I]D6rh*s'dJ\^LEgoĬQ솖bsB!΂& +=Sb#VS2H'?]/},6P. +w0iO6si"=[Դ-=ұ7'#_Gp[rHsē%^ lJR +$EFj-3>YM#D?Vx

`t ~9:X%I$i(%@A-#kY>?v|:H$'GG߉ eZ$@QӪ[_O٢+X(z uȅ333dC%H#{0C&iǽ& F,N>y=?})'~fso l?3tb]L$kI;:֙OfVTN|I"qn蓉D>g^mboz%HKnX9`yi[EY2WԔȨc~xLtrId?Βw;}lUנV3'kiJ4'g~/W.$.p}蓉DJ[A`Y/>Cz%QOP +C#-%4 l0VE>љxH$D#=>D += $AYH\4:襑SO|#܊⟞={G.\?}|ٿS+Kڸ'Kz%QOC)JJ6sµ,LT&)Ъ?n8dU%璤䓉D[EJb_yv.`ti- {7͂^z6eBC4G?㙙SHO>u|tr/}A`~ +s}tHOFBx7q!D5z[Z_ϊGG77?EI#^x:{i:<.! |2뭧 oc4Ό lf`̈'苪RJmݒ؀9Ćv~JժR/zҶ `!Y`se睱6Cד< 3 e+vPa>z^dPϦ5%JiB(.vnBn=4f]WyFd~Usd/0_OUOJu"aǰm$%[/MJ(1M*%H Z {X1Ԯ(e4}K1%.ghjR^6'Kj'!f+-ubY?GOb< +8TSsm֕$+F".P(. +Źڬ6:TQߵO"4TJ͕Wr'x(9$ IO= XN=? ++38B0 gS[=%;ˋ/qUb'D}$C*,=\C8/C1ԮԊ@;mx""5r tHoN ekk+k1r#@`>vt[#™ƨ'KfM d'S|%3YBWR/YCDk'(κh +@S8e1[ gY4UrgI(9+ʝ'%ItuKkK8ӤDtT0|vƐ8{bRI6eGX(Z9-A:E1/'|Ŵɲnw4e驒/tDyC=nzu ^<CO / QL#8K&<'A˰ßɋ$;)rOt:D姻e3}lߛDObh{@Rbxi"/žI)(*~]xAp=q S͸CfT>|{1~05$Ia6e?*/W5;glkJ,h,v(uP}J>0:x)[KUW:Rc}?)% + JZ$O|v؟ _ +P 3>o tC, U͂d7; V %gI${r5Tpi`ԓNߛDObZjwW,[\{S󥐏D|~H՗(/)K$ 0"'?nLv$Nv;U 5K$tpvx~Oe=)N#|=!%0#\ vF +sS0Hb<)V:o(Ic\&zb2|1$m$;āko`\}|0O%_߁RȧEr |'Puqn9dԜ;x@߇uZH?Jm K]T{I.;aCk(9 +ji4.;Nꌒi2:dm\xLd>v`n+̿>.ҟTQy$K!߼>^U+qGp)gQ9ݔw6' $惩ڝ ^f{w>Ki8` _~\j07Yf;0,u8l'u:kn 7)0k ;]cwՁEiUQS7b`ޒ*0{򹌽v.ԮOUK)6tۉ-XnF+6bTj&ٓC5S6{;/$_"U2lh/qsH}_  +-vY`+Iѩ"[pi4agi.uR1Nɬ[x_zBRamOv KjbgHvPJQImHoT'iWB?ZF|2.u/S(rc*'}JJvfT"xL_?;Hɂ6eEk nU[_NdQaUJZkшvw1qR +5mYlQlDne6$@ڡO?wd[:(Ԛyo5bxpmZ >ū +VkkWX 2.$<y =VCyY_)*=)$OwJRozj?D?@h|8և77_!xK}rBv6!f'up-0mA J~̀|%G||RWqTmήtkC%n'OJɕXB"ÉMRd|Or i/@#5<֊@/wy |rayxU6E)|/Od^msN̸RvIٙ^pN}I-נ nvTSST>rOZq ,|2J}WB)mVJ`ٞ)ia K c=>r 6qvHBgzf;&_%\ai/^3# {a5U{-铚d; $څu&(ڻ(\'u'Q5ݪ7j}($TPR -)w.G#ʛT&){xf LZeG>ӁVͱBY*kR 3]+U73k[)g]'{0v,6~ ASyZɺAF̞{ cmcS°/f@g~R*ӖZ]I]|ׂO*q|rQd*I7ʞr3\mV""2)vY3Jqq Vs~k}b9EM +dKz9A|X|/㬶#/ÀR*b}LԦVӄd.-uךxge#V϶# &O +.KwfZiS痧2.&>)=bxIǫv|'QMMJ)vZRp_cVn-" aLJ pZe&9 +B/Gne^;͓SufuG%A}C<*xKVߜ('5froo? b,*^uZ vš\>k'_27ɼ<ņ$xt{]Y)V“>ʜ D 8Ҏ<'gy'G&zʃp}0c7ӳDo]BGr "$\x7533> +olMze[nw hyɞI>j[IJ)J"`>enX +EZU%RܨCRe]`&Q0,Oo2L~r ?L8vVS>'"+=r!cTVPv D)/n_) +YʙJ* Vلfتy&R'=KWnH'EUvHD7YIpB0/ZpmU-)BǙг[I_xQAvX Sٵ&($U%cں8$Ϲcشݾ`M% &Iv/ɦj*R2MUjkތ1yH3̐tUsJB˵WGWߗs~x < "<{`8LVѢ)QV)U<}BSTG{XØ~!7J~pOsW֗dy%#Qqdd=a딈('lHɟpL/8t y7>S{&JMa$ )38qf R$~,]r@#,O>LM%~[Mp ~a'N +,gGCO֗$Ħ223؍{UQ0!"z^"eT*'TмM9%Tkժ $e:;__r'8j)Iԫ.]k#8O +ϓpC`:Tjϓu4-ZCIOKÅV7~fyuJoyR]eJsDx2O-vGض^DDY? pUΞ*b6IYq oe Ӳ|x9 7}tp΅ƻDJҴ%-4BDe<7JZsOټጭҵ8q $DAiB<8DϜ9lJ.fO'AP `h);ă\A%vy^;ʀ'J RUa2yr ^njtH_@JÃ8؏'{$~rH\3NH?)4ij,2 Y7Os%Ӌ A1d]-k|p"hQ6ɣTaQYVeUjNo2&I <]HIx2dZ$"]E9H fN-OişbJ|NW~1ӷbS.J_rBP.Def>]0ICkަ1td'h4Xzl)<OR#dy xD}V^v[ZE?MP""arO:%[*/bIr΅~Js}},(:A1@7}| ؃3nhҩ"jeU +cA + 4"E(@cC㝣2H!:ovj'+j' *'nb`rZb$"24RrqpUwL%@`_FJYAZG̺>- Iy *Waq;zGh9 @^ ;[qPAC`5OjZU6EU3]i&IW +PJPpL>L:_HIWi͊ +5U +{2-nt IHR2{r,҉B܀1`u s% L^IJwM./?O¦x6yp8"SgQw%aTB6P!J.ԘsFbJ'\ :b҅E&VR]z94x I(3 <)1 )[*nE u$Eg`CTȠMz1+b;jAeF]/~\0B^j-ڃqK)Td9; KյIFCaH*J?!*@v<·=˄ǮjsFӍڬ Y8|vG=EO@b/FѵL~"a2?h.+ ؕt~ }A)4N=05@vI x2[[M<&^9ӳ^:$u두l$,) \ijāa&F=64Մ>?A_xc$L)vK2bs7u x́'SQ?qoLՅEqD;p +4OҋcHJ{2cr2l'5)Ry<8ϡ"dAqx-,On!LPP` ɦTO\RFr!~F 'cA￙c. ݆cʇ_{;:1kڸ9G(j2fV-9iL7j. ޓو[[E '-D ePv|&oo8>3}=y/<2!/#ړgme_ +./g MA~5xR/Ynne@=c$5!“?iHfUφ8Ucl3]\cZ$<%cat3jؙx2b^HHH"dPejHkX5!\;hGЅ(jO45qd=sQ"y$~zfp3kVyD7%s3/iȜLn +B~ri~?5c2 $HCDaAř$""WW$uwjfvvڭڣfv-P rprk췻!NBw߫OP <}- O[|'O#e#g`RJ13C_^SlFI?}I8nf`N$|@ e,ydMUn$9K1|N=@~{ 太7$ŻI_2FB"l3~n@♨z2|Rا:Dx|r])O03[<+$xa0.uN3zގZk`QS}m>@,5:,kQ0P~"@#!񰊿 ^)\3g%݋N0O|?Z}1I +DPÁ2$BGFťs>7gO` 伍r_`bc RJX䨙m^ CWۘZ=u[͂\ mRJݦֶ̗́{CG>0P KqQ>UD .ҐstӢSV6 &a!0ZtЄ~wQ>$bUI`5`SJ`qmN(`فX{VF)y}U|RWT"< b?<ɾRꑙ-O,_2"ƼZYk>sa8 o +r+9g[9mj6FO&@FZ{->9_b uR +'TYXSpmx5t1۪Od%N?`jb9nyƎDwOe$o>9lBރT1S G%įNL&6'$;ۘXMY L+`" |2;[2}r 3ye -/1 1JY+v"X꫟vC0d-1K$0(\.Uiiڗt ĒeBߎ(i&©Y) UjL6E+Ep%L \!@}co1q쳢VLѥϸy-e>La 9;\  :fYJYC=i[IqK=&\CkZn%a|Ju>~W-m(SJTӼ غdÄDl.탄<' ί".2rA Quj2&jBWb̌}2d.p! vGZb0#~6z`^[<3-;iP0Gne䝒_D*(ֻ)2Rh-܆Of ۳ådWዄ7<r7x9KrPTY'~a\ުPY\zllfLt'v"<:Rn|BrKbƊ%3˔[_Dr*#B}ĩR_!/ +]bfi"p~}SL<'(%Dp)"`G~) MĬ5lkz9o'hHpoW|>"WyxbjZꁣڍ&X?B{O¬V'u~` zb3Ta0AC.B@.9ȱjIdªH bAJ' +|;d!I쓻b0[K家т>Uʑjۀ͚Khw+VN-=ƨ_SgM 23Le0/!׽ѫx$#}k.b+9@BRJ.O-cv{e ߛI3㓐-—>UJ`J +Z\z服`/~κMTQ)=p HoJ b!Orw?tTŇC"b3L-E!r[FCWI_g4d`}]yyjIIddV&m?Ttwb`vTJ،96!=i,Ҟ;ƨqi T/8L\KJ`s:=ީ'z PG>9PF +tC9O皇WI=f~"Xu>:63;;n3>Њ<"*%,Z-.;гv`Vrʈ__:\?HO9S[sKf^p)UDxɡ +ꑙ&5Ԩ]U.D*K&NWl3|M7呜OTTO-Fx?֢hG bm f;M)a%5̮$y-5{.@&A)W8üܝj=P+?vu܉pHʷ)Sdœ t ԿE{RV \ܧw(xXEX5 ~OBHO鑚/دۧ;Й1sZiW*AY+,i)jʃ" +< ‹ng:w7}v:ӝo;l _');-T[L)AGuD5KO   wQ@L0Rj$vϜ$ 긣dV_𹒚- #l5VOJܗhr*-:*LW\VA_x#?(fouʊglkԖVRp0 [1Hv}#eQF5 òGRf!C1 HvY CMƜoG-4ƿR9үx'MwL?! +veGT +^txZ`vIr@ 1P^A]t3snZ9zO*O>q*eɍOB,0LfZmq'SVuǖ֡&Rj= =aٳәqG}'O>w`&mI6d`nāRid!`KaS^x{x/c瀵_]=ٲŨpqÙ_pN:XG`z·uIwEr?0OadmjV2D炝CnE:r\IMO"&udV01wJfrc"<Ò6ZDT ĔW1eqN8{մW~~'U'ږ-/xA*hxKrӓ6UGR;@!dFg0 CK-w@5%&ГlJբ>aՏD f7&'Hi NF-iWI77]>RYiyEEqYM +s_Iǘ'JO⾑[q#%O#V\/HRDa)dfhjoeN[CBy9zRM)`w>/ %I LwzÖ ⪟9p_x;ieeHuJni]tEJ Яxő&4'E {BvhZWyڌWM.ozil2rw#> ;YpQuϪ+>&ܿۗM[1gt,1湐Tjג"ela`F-xJI$-d2'1OuHd(0LNeEnrow"jS<$e:K ? (1hNpxI2i)̥]oU+Mdoa 񻸄16>)a?R%]E8)€TI=XVd) %EJVXp.idڌЛL&^ɇ9Fx 2)Fv_|d#΀xcrs̵sXd`6ҟ)fMÊWl!g۴{RۤQ (H=߶:(m/ 6)-DS9H`OJ SĤ +)AdH LXZwyøEKЛL'jjE/ &)6*sę|,$CJ`v1Rk݄'%$zRK='1L2)+wO"JSVs$'IO҆I65Gd 2cnx'udV/8<4_ &5RZCDOrJ8kcY)tFlEA hT9mr^9MO6MM{O +'?K6H2$li0gmN:Bk"%& +X8rKfãÒ2-wsh9Ȓ U6!KR<<^B>aBIk  >Ʀ%W*aKkջ)h7'k'G x>2Â{f*v@ReK쮨L@43Lzj Jyd/nj[C-=/$~$+1N>ۯ+u>?1Է: Z)g,'S(XJYO7!JI_s6R:L\,I9n?'CVOMN)iVK` d3ZA@)gY7QʷtۓIԢa仇>Pܟ&\f4+ѝtj>iyIJrcH>' vvƨb|#~z"!)7O(5SycPJjteO9C5n@)CɢH䓞j5-8 +oH\6_?৖ +AEdR r+TOnגRTY%rwͅJI_O,^cId(ʕɞNM[0r3 v;wŁJI⌎<*D +-QO^g*J= \gyhTԕh$y(VC)C;V7wͅJIΰEg|䓥m$|2 eS$`LW)w~RC!c)eJO)[i_)I554<|2䧂!zIݭ3UY3`CR̒$igC(𔲙/oi}rkQ{~C'FOpj|OR4SsՆGk}ROSIVƝT?N+ʱ"Td/ojd畒<]}u(k _m@>YI@&CPnF:zL= nJ9 ؉~82Rc\ʏ5:fe _Nz߶bKK>ڐ}^ʻ=`LE) +ոͩ.;'sRrO^)s2t"CwUuŲ^cN譛g^p9H*XxhyILa55GO ڻZwE6УS(a Ԝ瓿jT 'Hrf= Pŕ&KwR1rعW)ê"ƽr%>'7h$4)*DjDEd@v,7L *%MLո} ,8'AT)ͅo|-fR)],E|2 u'|Hꁻd?F_P)Տ|lwɲw6UJ}Э&mK'i*>83.āe(0 ČWJFm3;ǝ#~}G\J)m-:Yt%8'O_ pLW1Aabx +%RqY(%m~ apink_)%II_)9N2?'Kl[* |2%i/ytTw)2R)eatV?ͻ$tPJ1֓Mm\Њt!܋f˚+ ^\өX2e +LyY&SJ27.H+*1; ܏7JIAѷ~3lGy'=O;kd $JĻ쓝 %f +K@) IcUR#O|\);i(d= pm\ ?5Sy[@_R铕:AbR +۩D֢vV)rmjhg%dKJ *ھ{ @3RTThyYi(/H wg}Ma餔9ZcF^槕R$]?~RM~d2}23RD{S+q.4, _k#e;l˚HX~?+'Tr}0}\`JIP%ftW3"$LD $jlN9~O"2{U7!TQ.AsS%ftŝ +% opV 􁧔RnǙ3T6(ja?!%QO\m&@*(m;Uaq(e |qC?VEuN?,Fc.>rs3LcaH*tԥa ^{2mέ L͕+/ɀՆLSftq_o'ϻ+J ekO>ד>^~oGy2wg |2H%dVy[kzhr)~_f;wՇ,:J +X\H)iN/wp'*>'0+O6d݂5sP"H$jIcR.fC3˱Lh`z֢݇TJV)AWed~/\qHRepU?}m F+`y C_ycc.#1r[2iN}5ՌeszِŃLTr8Fk?#d kn'@R[<04.o)|rS2FOvqs[[ .ᓣ器+5rRJ$ApRH(uВ_MYro_bK;z'G&:DGz`F)iNPm?!R\KJ0}8ߙQJ\d%sSҕ*I>92k`͔8p^|{\!y"?jR1JYgsy5TRS"繝zi<\H|rtBբw].;7E,'I-?^?d AYJ)3إ;Hm[O(#B)vn-(eHbi@t9o (e<^/ﰭ)xGE.߅]/Q U[>>֛ +9qD`dIyPJR%)?cY> ePЅh5n 2$򞬯*`j?(9_dXcyf=7jO@*d7HOv!kRd9n-ҷG^/Fxs:91@~6mwQ$03 =-)AJy%kY~ӺOŘ^6$* N lsUU8p /#_ 'Q~PJK'9v:>үuNj#<2rǷH#-Y~65 ϺF,!?<A$~R۹t#̆\.hǔOӌ +Z֛HRzbۙa[]{Tх$5myS:~ I)3 jRg<$'IK4@Cxg9մ9 |=Y9~?$[PbXh IuŨkZbJKj-5S$OyaadƦPT+j_ȏߋ^4w*q^<}yy]=ܾ/pDΈḙRRkA){)&3rϛN?O'$HFW#ZRfG?-XĒps(eƯ&[ZjBe{#~FF/aX?d;3J igQ~h$8ZR܆z Nn{RC3?>i'~A׆~-o Hy}ٜt6Ccwg=730nyOEd).{7?*:20>쟕'JFZ[SH3I+q~w؛{4r@"= zb-<r{ojڪ(,E)P nդřIJFJkH(-Yj!h"~0qDT|ӞkJ=U +lÆHFO=Ac7RNk%7F֕FWE%Ώy!EL)Cwa)K>˓&e= Q 2@(!$xPCgۏ)v؄4& ~!vپtR3XY0vэQ'gv4 Ő<%{kJ|H; Α{F03ΈԾEiM +hT`HN90/,9cka َqvЅEqH#uۀ,X;: R>R嬢p?|,c ד02󖢨T ?/: IF{rgkazX,E^-)Ja"ŜHF}):^W{)zZ\i|wmL +ifɍIp q!,iT*X6},I[G"c59G6@BOvV E#)qeȿb;$[/p'g"]8WvJ:HJ;5)z֧b3C"Ͽb[S +ӭ?2g+XiT\$$lR_0(LʠRu_Nt)Hȓw8Qw0uL ӾEu=x43ȋ8Dq #9䘱3˵_ʷl ,qe}>l)+K  +JEFYxGðZڼp6//g}C՘(Y0vf8'ILp<B*ZQYK×{A "HILeOw8?^:'Ӿ|EuPG;'Iq;6'QdvTɝ9~dmֿ,,&.I#:YvRRiJQ@1)1ɹYIJ GQ}SC6`ȋ8//ۚ2Gɍ # l0BUq2,qܐ?u&<N";^RHyDR&MTs"巆aΚ}({u12< ߟh1Oz98L + ՘ X>egQQLeNVH Po$dzHa#f#YdEB- ߛCC*ƈi/ ,S;3KuiQFqo(u/ '%h԰՗ۛ()F+eȿb;3 9tl`ܚo*KERXv~KRB^((Cօ]ey R1^lfj_RШ 0/,9ckȵ^|~@,S,DXF@A ʒ7,MH2©VL59^{-iiǮbEB%۞fhHb{r؞DVx,1e79L,]g%_' bO2O"<,}u}<8_"ߗiE޿VO:ug몴S2ރ(+/+=m(x&P=K쳔| TqtDܹZ)Uw3ĤLIkAQbd-!*a^\0ٯ64Πg眝G[1vplK*ef]#!گ +F|Oo6riwȐhsv{ɓN-߳e9,1 Kp5$Lh@֤l ?ehZ)_$񗃺Z'ъ3T`0醴*,޿6툢,1rj]dQnpW?R g=r`0E$+HĞ0og NߵACFؙc}4sȏ;{l[D] +7DH;~аLf +Sf 6D~^#eAqnuMx!ruA<(`W8[eWha dڂƤ=uN2,.ۙ@JH3s@_n#HŐ8*kZd:B0("(.3Fp@*))߃eނe5I K0A)^)fgk|1LC0R3Kl[A9,1^_e4YZi^I7QL)'e+c4"Lwƥe*YT$(77׏ "%z{ 끚KPAI%!&H9j R*wzR*E};S΢Hy97D/oN8Xx˒MOlkx76)O ָ/wҦr.8=)ʅ))=_ j(DD-I N+5qT{{xRֈ@M~e/҃*)OJX_raJ,ϼA>0e@9|ϖ%# `paXa6`N=x?3z6|IHiH +}!ORԤ{6XrK H~P.A^ +㨨%Dx`U@4nrEʙrh߳஻ Re0; F +sr KU+m)7{biƬw"X,wrI 3 ak')jB= D;`)T (?et@T +Hhe/ 斗5~,(YΡoOrkW8:<}g7GQ_ކuC4,AtI0RH)úlgŅ\DWXAIIQL%A8>2IXbe)^0"3iC4f +<&)j#H9`za>(jrٰ,*QjL6t4.~XDʷYѓf(*RJ6N(Er>pCC_k٪AX⼙l0lzUh"0}\@vZhz@|?M&oyH9`V_?2WȆig HiQt ]5^#Ð$=ا 3~ͧ)RCx;< >rw ^K}~F;S-ϰĆFљ`oLJ²)j){^(̐ RRԤ{̴ `>aVWUكqT,[Uo 8/(9)ZykUjzOI֢sJFQn "ay#_? "t94_s]0R4R"BnqD%H=NJ%;dʩ|@yUМGr~#R23D_G\*ᠨD9"j 3i>ib8 qRͲ&kaҡ8m3ug=r`vu&r_}X<o8 pΠHY [-SE][5 +Rv! 6&uW,3t9Fw*ʃ{ٰuK8C0p%<'[i>vw& +s.}93e(;=aÇ.4s@_5 ``V +Y\e0I:T'%ybH͌HٽTۄ<BHD{(jJTPR2ϓ†eF7TKp8I9?ɌO3LL9Г9zi#8οvwIxΜːV6j+5UWizjWEj6UwەY !!`\CUO"c}Z. !m`n1$ߙ, ig`~W3g,j.,Xh#&HE׽^,eD8٤>fugy5sR宺_y(iMF2lnL^UKa+;*[.2cڙ%j>TyYI SKcJg)exJ_7HJ +sZG~58cL2a~ɁeRZXa PҖՄ _"!1aRDgT.c 5!`f#Mt'$>0r`9-E* 9 M;6НH')ZL>oVs* +MȺ6v1zDR>Փ.1|(aKvX.(Xfj"C>1L@d"'Fփ(W+ +J|=jR ? ~cU>H[09Dڄ°fX·82ӫ蒋1vJw[I{-NKnp6D`6KNsKv9g{,Ťu +N8W5@/32WP-;E/jRF- ,RFŃ/Zmҙ _lox `cGvȹ!#M4.cg)p31R'c&SA_ Z>&)Oü<<^HJǓ/3+a^<@߲Q |MwuR߹@7`X˅n(i0")K=S'Zs떚po!)1LWtxC 0V-T-vseGPq)]Z@z&3_x3Rj3yYO‰߽H7`+Ii +Mn-MHx .b[k`&]Oz5^B뿓1̳Tu1̻Ik*i!%)/a3Sw@w!f6rݗy.(JlI-e1$O7LpEsߣ?8I^3 g`A)ڭ•T#ud3"0LKWvӓcL50m fHwq`H)[?f l&}x?gr97 Ai3giۏSwOAWUFu2dmb$xj55LS 3R XoT .1v9&H#l1 {*mD:Wn[3Pk.#eI^{?/w%kI[[f(@r8\ td`%}sO:*+++ŹI~ڞJ <<\pZas``]j/OBlQ+jgc~mc?Go;cֿI5F2Ɨ+VRDm=7M +^jRV͉ao6pj#MNb(ޟ ,sT;T\K=('HJ!!8JngDoi rǘ_u* > r;U:;Pg^\Kô'V>ܨ~{{-Lu0lm JDr!pOw +{DJУj1 o + 娟C_+gO'Z%;0$l$SㄘQJ6)N5@A˹ߐwi^`r9._d 24-g"/=pn6 c:) {7lC+?WYz7@W*CUFUu <@#K@>#sDYR}t*xB;0fzH9@[o'#FM|*7ѩ z.njWMJX:|cKjg̓DJ8;|h9JoSBb)X^K=0j{6p;{mo%Ga kki!5q! ;>KI)s$#iM^# ?Z1̳ſt_X,W"b&)T=ej뱺0j ̀Nj'%R[ŏKJ}aVHZ㗙vJ;=aVe)2}.Ul +΅s#%a-ƲJdeJY>UJYIɁIiVaӽK 0;oU0m)Uc6VcΟ8W4վZ`(LiKmɴIai0iPsؒm|M(%@6`1i2+r8@6͸ϻ+ɒ6vw֫]iw0aDJ*W3e09StM+}E[%djj+bilLY=<&>!Ϭ*cF%>X7ɃaXrj]YORu6fat +`鰼!o@U-@ʁLFV{[De%:BgO<><9P>=aҦ)"0lZw6{`U ++ԗ֒oxػ*b R*ŰJId>a0 Q00`RYç=0l'jY"o߃*j2Mhm`)Z !)؍d~q3$ɟ#ؘqtO6  þ3$Aw]`q"6AQ7Soq%jo~<˚e%ɇHiTr|n=@K(aX4ݻ6iI Y'vaؔ*Fp.c+mnG֊j%˂zWߛi"o338n='/;@JI̜0R e-?iI#0,M 4GheEfh$9Z|%|Z#zihǛ= z/ptDLq9iY!Z9s-誴ZZ!/ů Ib(\fOq=m~0 ıJB7E3YW4-V޺,ݿ z?pd=jo^O&wݭjQ]ptysYГ< ΢EٓqR{d OQ zY2 e>8wI2m$G#i lZ| +bFoUpvhRJ_ß<ǕmNyjȸ+M5*+rgC<=bb?&&ޔ,/ iķQ]3d̓kxa0J1+Ŋ:A]՟&jK@]+ |mm>9s3^,_(yYHĨt5}ؐD ++e]iEOyXfA0Ko0,}jݳnҐ2 9ُ ace(Սbt#h)EZ|tQ-_zh|w۰zsrIjZ|]GcW(;Gq(>f13ƄdΓpa5IJ$*/ &ia'mI aXGK1h^b,]~ּYx8(6T.f\ m:X-=FPbZ4>ѓI({ndaبIb0$- Ű za 4ԍNc̈ۦS˗Kõ? }ЮsQ14z4]rO%pZ ĸlPbИ)7'$Vkf=94tb=x-/#n ȟ#YaNMJ4n#݊q\Kj 7C{9'n6\:Kwe'cυu1&)#z=i/ZN)t?|0 Linw )Z^u 8`ؔ*FG8 ( ]ֈqu%ڌgi Zx1\jxC{|7IDb^ϹC#JI J|蘄FA\/%L`cRIEZ8P>;Ma.;nI g"&1,mb:4:.11hLF(9juW. Zط*pd};]9_t|\<\:`9]&a46q=ԞV{ɡ":HJ Ib0\&p.e} ıs H4R74mk_׹|_|9w~\Y.Уբlp$iFaS\%2mg;wתk8wkQaDQgT +>BxFgydI=i{?~>IEؠud~iZ#L˄>IR@y-= rVrzl/fa1+xr[/|/PHݗEo8x45OD??чD(<<|4&cEGNk(j|٦8)$(ta}i\TŨQb"auTIFA) x;T xl_q_[yN꾄bo[N(`^a!q8ч-nwkQ?ߑFF򤮘LKI<$,/rV|Ơ(j86gA'!ٮ0W% ܞhKŕxr|f\AO*s]oDZrPVԸ莡KɍyraO2L wkI%~bUYg͟c~?}5$ ?+e?@%*Eaq41sCC*-\V|kW? fwJ|mFZv(`(Gt#p("1&0 `R=cSb{¡(jxȴ|,F[֯uUٴ] p:?en\ʄ!W _soR&uWb-qY2"Flo.;XPBԋG &W:GE'%pU8_euјF8,c$oô݀hgbqrts[/)p9;Ǥ*V+ +#*T4{U1^\F@Ejݨ?xU<6|Ѩ'gЏ8+=JulY+IP]* Y4Ba]l FbُC U/}G9RK}Yې1btrЈ9]MPDT1Ӫ>rziE-@Rt:%ȐS}l2GňhdЍU=ǔe[B,t5{uo}w.J=WŁ&a DbD+@c܍oIb'YI +Orx_GȓR, %.4>"Jc,mZ +Ew~=|ts||f|~7>򵞖ګˢ[lFF0Уq0w87OjEL <^*)a2FuⰊ_ua(Ƹ3-* r1?%b7Lb2&;ʑ \Prտ~Xk)/z0$ Fc!DD^6w9t$(RyRK`I6Or}"OR+T`E`zЪ@X\7XG##uEqf(%o O$|Q1^KsFi[끐Wցح~xA4)"O03Ƀ<X<^8,>bU=R86gy>Lj 暧_؅dRܘnۀhteOn}sOY[_{uI)?SWsfHa̭rѭޣ4%&H%gIH`I*ODIqZIS.EQΟc>&ŃfЪ,Ŕs)gShZcV[0ä.!W +^iFrLj.ub0 +2FC1=tGHȓ'SSg 33GL0>v9RZ)9H̳̚zhPhԔXS[`/gSk4N:S1T,uTKݗEOD̍{~0!vƚgE'!3 3|3 }R|?]S…f@>)2HBn.Y%"V"yЍX.}3\%\5T#x+{ B:p0%n8=XO@ąHh^ȓXbR}qxVaS-9VDZgä:.U tAt1b7#aܤݬ+tP{r.5{u -eRG'ychcg\R8E%C'- +&Lr{1Sb$E8oS\>N)BBU~PJ4h[WuhҤp~iRPB;qqbqǹlv9>vHy$&  y?z~>me)+qe~j$RZFJѤSkL=x@)v*bhbbLCI@W+6"~رN6;\ +\8ꁋ3!OSk?'険QI}VBa&5{R<)mM{F-E)[JO + D!H _E*߮h$l4.4/Y*ɥpEKXQX%QLȎ'/~7"m(. +V4 +^~q|zh=ԊtT|bzR'7RJk}>z&[`߾]ѽZ9aExS*)&|/?SQi]=µI45 O$E@cfg`{G=7fJf.VFai%@B͔O(1f]7NŮňnİ5=0)}OJl3 MIaf+1){rf{y nV B1/ױ㿛=ٿP1X"G#]B} 1<VLdP\YM VM̓k)hߪ6ʓa#pn2L +oiГL&chbP\bf»ӈ?b|eoϴTDZFaP0h'ꑹ$Iؓ)bİM_s۶mRxR"0 Up0PYՉ&C© R~wh\JJfa&YV,ͣ14%'ErX9qx[英 {glHu:{64|mN>*F>hhYa*JIكIvGRiaC(7oieR=~̭F32icInTIFYӉڭTGvĎvkцweÑΏC&aN;0(n>ab~ wht24.w#T +=j&Zy 7*ѓ.Vޣf3.W$dɏR~,׈"*&=?BD?o gދtԅ]gB?σ YE[ҟdJK hD7bJjF2L.)~bL6e=pl Ё'C{򓥚T&Yn(Z65?Mz.~wΞ=1:=BsK Ђ!(.\Tbf(F ,zR$ I)ѓh'1\$yzTݎ'j_"j"pt! ~;U(qb2@/Qh%Q=TIYI=3F"a>`$Rf*ɛU[F;sf]z봽n*x[c=d8}؀0'=]Qa:S' +!%Ub#$FOI P0E)yٚ0O +wՀǕ/}e_t8C7#F Vj휜@O$`$ݪ6ГEC^ݩ5-SrcRZ7dSI4`x]}Hi#oV)#1II1^mXX[vRF>C8vXp3AmeO;j vx%0l}Oj +uI >hߪ6'1%rbQuwkV("͑Lpɦ 09nt:),G:ݶqɾ+^;RCðuٺQ~~IZ|_ 1j[H!F^P6.u֬lRKֵ>12m{Ntuϱ%qr#ݝ=T'%X-Ap|{>"nyIꪢP(IQ&u:K`5ׄy)goX#!, n4O3FJaF؀1*|L`lq7v !GolN/[Xƈ[v#c}-) +eYJ'P2)v~MMsc5 sI,b>De?(lH\v8oq42֡.pW$VJޚ˼[K].pˉN.-Jp:.}Q(T}ZT +%쓂Ї5c*%R FF3I"LjC悈@2%%zZS-Y$+㕜az[Eǔ6v'"ީ-CBY{J'5*P2IYϚ[jRw7.]$g+#Y?1 LҀۦ_ݹd+F.;EVI&fE.c}|*`JR$࿌ty&"ɘ:)GPֆRI_a}RC{A\#PhRۜBg +_33̭f"&l_H&MwBhJo{^+HOPؕ>N\Q薶cu}? PP6'Eut Q*UBYP("lă|R1n41')wobC*UmȎ-CrS +)8[\ب+&|l  M!hO'qK]jH*P(I:g:FFTj~mpHR%z掯}h/̚쓒;du|R;kgz+eikwL,p/s d2pcn6klx=J-Lcc3MP@t2$yR({d=!5*B^UUQYRR1mٽ~G^<{̟|mgY,^AffQ5*yS(_FEtn%(&Lv!l2EgUq`j(\Iۢkbٞ.d- l:hr'"ީe%A'Eu $qHryEeٖe堔ۿ\g=z虧BˉglhKӖco*` /F=nW]9szv) *z 7A#L2۠Q_sL&+Kqo[_ّўNH:R/zt>vt%B5Bi,|PGRV(UUe[*|`cowO + r ADPoK8 I(φRedЭ̤yR rŗ^.F&k6|nq#R&ȨF~Q*|LA XX8o]Uet-S~|pkC2lsMǥ/P +(:F4BU] ƀF* ޯ?xgק;p} +8ǃ@B=d9a®36&w֬>Iٸd5Ks̭fuMm!X4>!pprl"&C6l|!:>?[tKNuOT6:랈xh$&b7)BG٣F#Q]UrR Vco{/'^=fwIm( b!S[ωWsn]*JhOf*Rr7oi/p3!!܀B +$$.VH+jsݴ$!^ ,4cO$>Ӻ)Hۍ$mzb+ϙuv$c;Gq{k_w9{*ֹsm6.e۲F6{1N&CxLjƬ1R+Dp GD _fS0%A,O ᓐɎ';N8ucǎ=zȑc>j~ ׿~zgu/5HHbih{hRY#caÝJ@&=頺(;\e(eil3DLUC#7|YgS>Ҋҵ>m![5UUu 녈 +,d"0 0&y9`-Ǽ!ˆ&Rp{GYNԯ3|䓟z)|{?zo]u:Oކ.!샓q1-Fuo|:q>ol3t 7\Ds$%+]UX%*̾zs'8NFWR=np\ZJ +PsmA?!3Ҙ~3C!۵$$ŸG'vr{_k (%& {v9 H0r?aL1ƬQ"65OmE8!En?0H&wK赑Fw?{FP?G"'3thq4RH@%< %mZp<aUPQVַ}1'fsLq\Qq0sܻ 44R\ر4Yu8d쳩Р\aFwSl*]#$ggq4< ?7 +uwcݽ\wқxZ|J^:/A0I8+0IfPC H#}1̵k\,#}9F*&TI[iwQ^?6@W[ɤ"U8_jhCoE$.ؓ=ǾʇAͦBŨWBaRPՉzxp>Z<Ơ"ki2YKQ"|Jٶ|9\2|Uτ›Wᒱ<2i߹`oh>FR=Qīy9 YW +pp*`z<&9Lj-}dr0\C d&)d"lc݄k^^sJ.YX#J l%DXיcu}mf9$~VTU52i\+FP;eF^9raKXe[hbO7.KG1ʂfzzDp4cA3 +M Zx2%d?3<]vK r>ak3Xrf%êG?$%r!!/J VEN\6c+PQHչ?H^@-yL%`2/i "ZDWJfS\r 96ܑ3Í`!0(qت2nRCRc%?ﻥnʹfضT^g3i~#l"RJnS'Tpa$ ,cۈb.WuxRi5!~5/M- +(,⠨8 dk{Wp^Uk}eiH6H,G:g"Ckg[9n9<\Ӆ^B,M$$Y}Э&l'DmP-XgR6ǰǪ0IC#_k?rvw؋;ZGmFRs1USG]XΦP1.gu%JCsh!nIr1vI biẆ=1z%ç{;WMdwE{O&!ﺤn & Q{ ᐤT 'E[o؍aV+UZR"" i6 NHJJp`sN>b'Ƙd}0vubh;{#UbgVV$ò?닳[9gK2ެRIҰG}d%^awXgڌT.yNG!b',*!㉓AR ,eY_dl5:*Yq&:c4/][J5[[H6Bp%^2c,I]uIM_B q񋠎6R~p' 8M/:xEnߝ<H2\RW2aFanfj lC|bR^4]^-hi^} ^W0$ |M#ߘ +s' w a/f8 +?|"tABeQF#m4^D8oMӋ 8A #1 ab/3.,rj}>Q6Y6qx fgan`˽2%w`лc}q<#I[[HښTqhN%y-'(v s0)EXVP<o9H"3`.'yoS)-lkT5+QL;(޸OڲFX6j&Xk,k`K IX3Q'd,_H M:ܽ=y|M÷'˪hH +"Щ$KyǒP=ԖeSլ{j{R2L|}qRO~V +XF6UMNJ$Iӭ-‹Yθo=^?=$^z›OnHAO',yr{Ԑw#2lE3R2dw'YC6JcL VNLD[ޠbQI.kL c$IqSn0B_ѓKK(Ey@vsLxf$uH/k)zw|f6sZ𲚎7)ɳg}u>QL#tu^F% wV[y;턩!<dsW !#1kA V( pT2Źp'] F͙., jMjng;- +/`n3vYZTu+jgwya6WSbF fHJ Z|_2 FE8`nT-e1> +S%̻Յd͊3*eϖeSclj'-U]VcEscqpZx<<%]9y1CM'$ez0OOmmà?L#cwπ*!Z2.*aUұFVf,y W\nVbcin{Eظ+[PW;%{ʥ*2tM491bB$v Ꭾ+5d!"0EXh(kfCrZD8#K`F&t%6E}6d:R2;UƴR}[U닳em'/{)+SI6aWgIhR fqy7'-]_$;aݴ ;2C<-rA ,RѐlDZOM# G3[fk lj{E- '<0 <7] ,?fziWSbCP_H$ rJC^^{9uw)IR8NF8֬DJT3ļZ(jT;5ڲWXVIv  c.IZ- +H1WI+=7 +"dGoO8($y4`0c?Y^&jdђFgeT6jV܅e2l Nfbk ɭ`P%kg]>/qWIضh6) t MRR^'#$u``>lGpf1)h2 |r3*2#C([N^-`˽-p@9b$L)KZ^BsWz着ՔؕS EXM <{𭳷xc0 +0"ꌭSIںsgtЊ Ll,5liA$oZ&<ZIYxIV@tI=A||"8򝺥T !#1DgU*` ېZLF*pOK J,|ǚ_[HnfS;ypxWSkV%qVGs%ɾV/Hy[Dl'AO;i=:ACb0al#KUrkmasCjϖ8/r%78_U7ڶ3hՋJ[cje怱f7؎RloY{osƬc_(@8o}m \;3 0h]34_ +뻉9'+||Ig5ȓr6I`Ħd* ճ뽳_wO^w@`0 w?[H$H#`J&R2%xYVB^X%,z +&\eb׀&`ͿX_Ƙo#b\CގJN'T 8Rҹjq0RnE\.$۳ZV.x%גߠ+ڶ젫To.[~Tg I+|kKpBL^(wѮ:A%TϏ{zM.L´Ur4d f`B)1pH 0e [0͵B*^DWfkIzt&]od^ʬQoL0YA h +X%_z7HJC }H{_|raf8! f0#~-5ogj˰2X9˹R6*%%d+z>ԼTtR Q53 9ҜAL[J_wݧr UvUUv鯺g$O;`0gv.,=Z Zv`Y2t$"–p&[DWf ϟR +.-mSc9sNRD nx[<@Fx^@=֒.k0RN'7?%nҋhzG0 fC)mpH6euA'-LZ>R0[H9ğ-$7z*vlsW1[(Z wZPa/y@OGoH8#OʷF#dCb˧.`0 YӼZ*wgCHm.߲e%lObz}ް^HçL|yMJMR*y3I0Rs۔#~b~!S+]Wj"#1-Hk A j6VIGZjQe!ؒlf7uaK`Y/$KJ&|K.%#X_`2d}0W}i}`&]W8rard?{~} `0:ҬXNGVf@S!!m[CJ& +n \r^¾SxdVZh^ _}H<)*$89C?## 'gwϛx.d$\/⇔۲k$$2{}9@˹R6M%SKaK fd5ՐUԥՈp5_N +#mq<  p`-$[7vL0`07XgAga U'HQA*JGG$RǼo@tǁ>y%F?$i;/Q!}B}fH [(MR=jAy}u.)T,,i-Us9_uፐێQ~FUݖs:@mudW#F^Ģ\ l+W`Qg I y (!ܔeZ 2 _(OA}G~mqܧC?z8#sdϫ|Ey~2! 3@R_,#0XWfr:Pm}[⸞O"3bCHKnƵ鼃-< <[tx~O!ECb0P4Eq:\FLAvu. Bey>kx={ɧ^ lc}yGMz\H PJ,=RƸtfPnidb0Tm쀴6({P.:HqNzl8lyh8@l۠#t§ |u!^1B6,7 6*t`%TUsqqsZ#;*9P$IzNp㡏N(L(ޡ gGoEFb0̇3PYf>[e"fQp}d"t`>q=#?Fnp㡟'py`]vOX koVqI( &&u ^K0@l b^۹uhc;׶4J+lC$Mz5Z'- Ǒ EQW56T@R]v*9lDʏd~VpK%U|nRlrg^2uL#)'(BNrv04gƱl %JL[[,)+n,E;B(/pc긿cl}mlss,1j|Iu)^75iN2$EQu~qΣTr۳`rtKg)1~ab%*.j`>Rkթ0Xr lWb=9Bt}l㞎 ȐBE8FÕD&&V.׵S;#>4_ EQԅ9|$= @聭 -"rͼs1dE3bD v*n5!̳?:5 ݻ$v{i,=F2’+d6N >nzw+v=WqhJo꣎i[H(Ҏ7H wI ASj.S1N7`M7TsbJ_ƕ6# +!Ü.Vimt«~P"Sg]C(]*yg?'Ckr晷,eB%e_ԩii@UʴY &X.n,/l0PP0Ca+.;zgu-,:b߄({gE؞((Tv&tMߑ̓~d;8A#D:GRUbpn5ەv긜RapI0&|hs}G-*"zƝ= JqnL^An5ԏ((TҷgW.&\[,/߄btb1>y@g-65ki8<DT =%; b h\1떹R+bkYJO)=*:REQEQ,Ym=z ms&< /.\2”۱>mJʱ:򆣇;:\ns3>4nһVL%cS3 +EQEQuFLXBʊ0&;|UnL2|TOH*1RbiQEQEQ0GOvcu)((9C7y(JREQEQE` endstream endobj 11 0 obj [/ICCBased 19 0 R] endobj 18 0 obj <>/Filter/FlateDecode/Height 947/Intent/RelativeColorimetric/Length 20505/Name/X/Subtype/Image/Type/XObject/Width 880>>stream +H pT/!&_aBA+ʄbJZDhjhb"SlSF[qvBP +P)-7Bd I&;=&_{{|!;Lsw9W=!ግPڇ#=^v^fV$Ax 6G<=\ k˰n3,- gGX+)G`{[-6t 8W{17# 8nz GJyH&AwB tTV \D06ns'Ab G.⬊FI8 dw AZ-hV7%\( "~,z2YlKA bYqM V&u8minO1"`y;A^[7`:58vԆw^Z"VPQ $A.c'nnJm4RD,8>F75RI8; pћ"AрMV7Q#(Ag^PD ,=A$ۂXtSADXtSZM#%A} '&3>A$ u]NgO Z,;ȘuS+\$Aǥ|Z⚠nƩnc; 6,n>DACƔaw 0CKDSDr * FVFw w? GM!Unٕ#A݊a2, CŮ~ :vW74g58hr׹Z܉&j,v&pu$A\ t_7%܆4R8g ?#n#N{1L9# KM?)Txԯm4Q eS/1uC&n>v3>'$x i} RBsNOuSnk GH1>pRfDݦJuS­LOEne9< +]J{X/-0GOKK;ֺW̷Baפ =R2XG8ciLaԾG\},Rԗ)2NZʄs$\Ed\1f9t]RE?%H7%$\QO>KUsj Á]Sb}X & q kR"D-JS2sO}Uj9(%INtSsR"=Hla|9.X@S0 ~}}?X;8bcG[ʇ +|f|k|njMV Ϙ0"# F‘~$K֦F`__C KsM]f[:pfMeVtmdkP!(2K g\^t4饝T7HAھHL%}eu]EȨ;\߸`@7s%\X:]{x~'a,Z=lcx⼑\†n Nd{pnqY8ҕ2rr(Km{i]ذ xx;?GuGƼ#Mo' _qScm5k?bަTpt57mbtSLg ("dw2{J0ۅ}.6DŽ'<20äFFD@pa=x]aO) 've_ pPY+EЦI7H8cdo x".@״lᛇUrB7mDy=D yrEcU'tf<'_21uMq!~pz7Gqc:{loP%ZH:kծJp_ky6\ME}ߍrAp3j Nk˃iqV(8dݔp$\Bk9~{6& %dngZ8A(S7WSdZg AZ=$7`&&X护M5*~Z${S:ƎVBLJ`Y+TH$\gR|%1닶43hXz4J w @(_R `_ g,]ƜrlY.8.lg;jȾX8w޸`oO:ݔpZDE-#*5D^A;LI56(VX 2"sAVãTl1qVx`L2% 4MTદpuwVUn2"`Cpٯ+ sAGETdTQXRP#m5)hՖg}Ԉ $!)f,Q*>ZEB ַG0ujEq@{usIg0ufs_7ZY_z_W9.Or?KSy/?X?'[Ư +q'+l魌~_$ۘ\zFܠ=F_.LkQG+Y I1ӠMå;o]syk.Z5׽FĖ .?Ǐ}Q$^k0p2E~L4Z ^b7k'7-Xx]#]Z\)߽+k"IJj~ނU7:>"p3{+P֬&Y}4cI,ҝ~eݣV_y{&!DqK;]F>pqO׳UQ+fRn r[mEeZv7a񺷛p^k͖wpR{YP^:y-[W!=AvZk^w7nquEٍ+y*+E/lͼn=XN|qڏۏ}kO*lXZu^{Nj7hk k!9n>JTO2A: 8A ߆xm8 Wgqys޾;[Y5kmx] ~G7K~ t"\ZjU\^y[sΜ ~lm}r~̜~=oT*ZKc2 J +에T{aku)\`f+Qg>q,^yײjv}5}_CC9?׍5py¸<{{n9ZFp699%D@7˚qDuX7MA +0PGDrz-oD]][,Sghۃ,o)ZOXSK[j_Md,o) =doٽ]w:I߭Vym: NQ)#5Pya:1Mda +0D3ܩIF) rv7u2Vysr5NZ_kWG,7.W1n*I'cT/#^ "DA7Yy#H^GN(JPysRn@ͽZWm$Bd8bnPy!X,o*%`b ͵vaDm9*k`U74\U䷌*G`R 7{~oỲIFAJYAj'c*ay#Pvs$^e#'$;#%#7Btu /]PY(n6 }j2%5JoN3o*(SQZ@{`%(n~̼*8_$75vQ89m}3e>F7 ȼ2v3NzeVoNƔ7?NFbW6]l' ׍H)Jt!N7i_o}2M\FtIXV:}w7Q{ER*2(D8d,#YV0 'n u vqﰼ8&2M7ew"ER"IuSQ{9'Jt շ틉@;7•#%,o,Hm7as^N~FNzp┷H~2(k'ÔH5 N;Ɉ&ˊD,o,0[$'o$-#4i'c`ck9-#4O N27BVֹ0qs!NokHv2Q{p.$Zb*7'?`y#`y +Nyʼ0 Nz$ 7k'{7uv!D P´vYsv.Jy3wh9#5P%(-+y#d^oɡ7B"Cd\¼2k'qIYiT!J}q#d!7'a/:$nVަ4A7'FٯX2>c:$Ok +a "TZKX3@-DL~sCBؚJRa#jdSaSB=Pj~>?7}uݼH:ӕ8Ps%!kQ}լ'0@5s7y1zޚ: ym184hR3 ̷݃]0@5O u |d TsZB tS%dN=ycՌyh :z˨oy:I9jJym TFM~V /ɡ@N#bnױMJo St>Ղ9G|hFqK>IEou_yvX' ]0@7[Trߦc44 Is(C= T79-fo1rq},*Z Я ଔIۤ t|b- A|i{o畤79ϑ(rq@3;z(e7M>)Y'QynAy: kN`rb$z + 6 ؿɨEqWMz -AYtQNÎz:wEny gI/#Mɯ7Hq|qAJn$ʅ "o47e !en&3_$Cr[Zd3 RQVtJş y8no ~IئɋݧA"<UnƘ8MsљGm gSD;[߳8s GUd'.z$H>#+?G&/j|_KbyBg*y3]p8r T6mܭϝ8st9 skw?"cb4G1k[[[i-rgClPN^P}ŝ A-)Kt[yS2Dl;/#7\QW{!cA2fSߠ*'<A)QB^AOʓj{_l*9u3Z0694zKɈ=kԦkgX5œC/Duj+du#j{\mT W5,\BBfTuնr5Uˮ&,ZcjnEqD5򑩕\F>>qPS,*@n[1⠆+Uyk8*;傥's$d#RS.[T 9Oᅡ 56܈iOEqin5n:9 :b .lq6rX*! 75F0h3X*!/eu89~2*#6}ن~agoqԼ 8LޛUF;2ʼX>Y0R\£*C# ~}1@)kȄARl)U8t8ڏ(oEi`9sH ay|AS#3'yuwѼRR8^5xo.}?Y9GH }C/>?f69^ȣ5HWPNZ,2χP7 Uk]R'*_U|>{r Ns7RUVT J1Em(-'P6Κ#7%6FO%ɏrsλre֐e}^ݿŵQ,rm[%Hogqn 9U$Mw X~LI&M{.@;*BR"J _-e Ax!mX렯{.z?Xo˛t>g\q WIge9frBnI  Drpx+p77A,w41ތ Kc-6{3 dr+nY1@bUJ?ފYf9M2Vj_}*oR[vz[ `9N}n2}ox,7Ie;K{nR=θJBd^`rΫ"8<v{u.0me`BkX!8}x H3Cn  wzop e#8K!7*K%\X~@[AH}xe 8scs nyv6Pu fJ}WZ;I R@?G?| WJPѡ?z`A,oEoEps8zWhPyEFTj#wŕ!|o){n9@?O%ߙiͷr +pNUo1cpMCp?!;24 J9o[o 8)ur> 8ЩʏG7$8\)An^F=3u|lRbz=,p%s[ p}h{3t~7P$翛h{Ki1rM,oMbqEkoM/gi\ko e+1@[fha7Dݑ&5IPE8sְ x91JhiXGQ;AM.Wo ˹x7[J2byCs%Tz?X^]_417CcN9>$%C3W)B9fbo xD՛+N`R9`LυRd7P4w: J AIz&8CN7PJzMo =@+٥xz3t&PuuB0@-'-gi$.1@-'F˥X\JS7)JU54xoTWʢ-11@/ mn*G2 r斆L^pk鳄%s-2Ny(%-PR;: 7tHr]Ku'羷=Io*Z[L.;0@19Ul[B\xèЁK7P󰼷}DeP&-gh1fsK h e)L_o1@7#d5ԕw4}`~Yq?3;33n(Mj6J1zAŠ5P\`$1i0FBQ"4 mѴTUD:ҨW +5|k҆@1J NPlgƶfҟMTo8NV>P'z  y(ɘos|l]k^.dRgsНӟl Ut*P9bf p\&Q߰c3K"^ʉZK%ԛGk^T "zo>8nT2}Ǡ<7,$پ|Ypɸ͠RƛppgH}{%Uo9y/YkZ9T^gXaYqdQ+5e>(HOnorT,8CYR^LHꅽkzϦP: I0Mȏ\552\)uIheCkW5ToL2.fp=@3 '&r!M)4fQE655[\-G\Ћ{ZѪW +0N-8$_])yts\YOlӉX4bBoޅ tPp ˛…\sZ#N˚5[6iHom^=vh\gSr#7Г\`|tPߗQ߄VMc=-4$aykh؟uP څu4%ݔ|`Yͷě=J=>KzwR1j5L҄~[V\rg ;pmU +tR͡Ƴjlor7|HR̦z 7#ۍ@1J"9!wc,w{{j6PjޛGkNcQ[SԪloOu'70jb.7}{s~L4NZ)͡1IB>5[˺(Ǒ@sB.|m{si٫X'_yoDwb+H>Ų-9 +2/jyi$!C|.M{{ , }{si 7-"zt+XC|>mo.5:h.E<ձ7օj!'3x9܃ Grxu8{r}Ft#7G닩t<u!%ߑi,eAT.¥=_Kn$Hհ7J9#l>״7>2CIǣ_!q \>d\ͣx|ojV QuG1]{NU:3u]oy"7K7=i`bxSmIzhٛCu ` ]y$E򹛂}-I#B٭ш_o.M$XFoڝo݌|I06O5ͣ![$źiNϳ $X'Ro`FXGt!' #ycoN|HvK_~4w^62[o>8>ަ]oj˝^?NQȣ`Gp٩6kכKzIv^\* l$X4m^֬7Do`!ϔHͥI`%: V^9 @qi+X'Nt>7ݤUo.Eo`)՛GeMJx[ ެF>_1[uǿ A5lMeH:f` C.50ӦJij>d+ְrRqfP@&E~߷﹵\}=:sޯk'ΖZޑͦRR +X2q etӴ"ݓ +H9{I5+H7*Po|s -=z +N욾!yۙ7G~2o(1Ņ-ۅ U]Ϸ_tPZ;2"˻dPf^OZޡ%#UC3AW{2ZrC}آqN̼nY"9so۰A$B-Y(5Oe)D1o(7)FoLZMo(7w.Fo!詺ϷHF.aPr;[Ho(9?,Dov^>o) +qOƲJ~/(ȾUzC~R_9PvNɿXXμBkG ׬r +My9 +䝛W +꿖9-gPAwo7Tg["W*k3r-v&_HZ4&"Z{+Dʼ".38{;m`˲E*#aT~E2'pN:*+8;'傼}8'Q)^9CS|K7Tӭ%HF?d;P)N%rk*[fqtJd'Tw[,]XIo } .C?7 n>\PQ($ rNlkI߂d>$~g7TWP +ˇ6ͩߟB=$1(#ѴnҜtO*i=.71o'upL{y4]'|βhzĞG .Y=:$&KaLn٭0YpL9 pB,+kUpW](y"D?-qet@x<({aހt^erӳn2zށ{+[3А +(x@-Sz506{xgF?PP9"Q].Lpe۵g +ƣ .3ug[,< ӧ -V08]55刭4O镅Hj+ h x],ݥg0OXl\6 ˔AzK& Ɉ8(lf\"s8(Ƭs3 .3p@c^w? Pp|.<M8|DE88+'\"'=>}`E24tۧn{M9}dB}|? +PxqK~ h.][ '_Z` 8n$2yzZ`\u\$О5pe} ;]p|;I92'\,=qaT"YJo@>;b9q?prrEj^p@R7\8UNCL{ހV8XFhEpc߀<]w\"fY.D UNIȜW(681_⎂d}Z;[7\"(?&.u7Ʀk~o46X2Ercɸk_\,cW2p@E҂79[8]m U\"S_7}ܶ+RkkF,ڔNܺSǶ5q5舷py[Ge,$83VܫWgE-w%ЩwIZmfTzT1ӂki_Z6 +#Q˙AC?3 +"{QiL- s@7V&7;WǞq̓;Np@wgܚkM'.1孢k\\$=KlT\"6qP uFWc}KnL{^pjYV܋osd#XKȰ v@մ@ .8 +6qs-aJR_E!([>)ɰwȆAy9d 39p96J ^_ޤ7{}ݮNi еjz{ZyNM-[8K^׾jj&WAyIz{ӵSZ-)9(_QUw?啘 +$AQ#+X +>x4 "2h;NA* +% a)Pa HA) ;gY{w?;ݻ;{o4Hz%"ADo)E$8P"HH $9Lo8T98I"/!S9R{K[# #xVVX)Aȏ| NUl@p Po&vPfKo|& ]`M~.48qmX͡BQreI(6Lli-c%ob)IJ)70qDPQvm{uJ:7\* 6b 1X_5؁ L4X vb3{#$@D9Z}hp9L +8'%& {PnE$"4)m778&Lwnoepxo%?L,pn\㨷1Ln+5qtA7X]6%xPrɋ `(9gwre';ZvQ.Yt^T$ m7 +O!_k#5ݯ4cH $JIz j{.i&-y1%L.Vbp=ymAlzov>BQrEP̌"Eme^M?#K\ `4`z8YtЪ2:ʑn/qHw7&yU]T]5)ly=5HN\o'`9rsGn&=/l+洄nYMjJc!YC{쒀,J(`b__4߀vMdd`U,{7aZ~ҏ1r~En$7Ħ7ʯ<൦!x\oou~lQTlaboқ|La(L(pݳ2EFӑ-z]\ш`b;$B^4yV}["l{ e+Oã1 *-{#$@@Szj.l 6a䬮TΔa}ܫ)rQ i"eͿ?Ckĭ4z\*ʡۻgo`pSEo>gz>iJbӄzJ|1.ڛGS:qdT.D64b 6lκpy $Ey&ZI,OMUXjL6rw&ܖͻY{tL..]IyJ +sN/ߗs) K.|"T= Kz3H=7t}wjjC,"[_)ZzD%E_/bL!8Ӂ)R&x-wޏ/0rރn ʜT$2fRUWY.{j0y *674[*%gP[qlo.y &}XIFqcҡ53u tFZYE?W{>,Ɇ&q h!7!6Mh1/ԋE%&*z?M&yI]^ TUoeAP6g.$HlΉEy:i NF6{jwb[B?+fA4D{7bk}RLlN?TEgkaad \` Fr+b"ٶ!k=UŦ euɶ/@6UqM8`} ?Mcp̟@Qr3:-f:^$gvcVL.5`,;q(%Dr)g `yٔPZ fTY.3mqYH\[c)7-w'8W&?,Me"{7c Uqb*7(BT_|fUo)d"SVJ.0q!c~^6h D62t!Z4L $z3HE&Tp +Gmd aҲrJVʂ>l̘ n/Jj{Sư5B҇d[$g$;|gp\-AW-<>nT="8 ydt|C}aLÀkA܄3Q1.r?)xұ[V +h3 d t"=T͖ '[wFeK!) R6V +49{Yx} -m?zΛPQ;Fvw(97uUK6S7M^uJ.*cGׄ .$SF\ˌ_kD0;$c .gPBTs1h3NrMACD2<'jp!eOd(AA7cM(/Va*g_i)Wc֜c(k'ە(KԮzy$ 򏭢3C@$0?!vi! +%QSE@EXݒ?lVC]A Eإ +*hE`/ymiiݖ7ܙs?sWO.X}[2t6BmE`6NBnn l@) '?X>Y e ^#S?YB\ܑnƒskl_m^FPB-/+?faP!E2 .dup}R-Ԩg + Y1T.k'Ql o܅φoKll}aW%rɳ&$Rxؾb11ԁ'lO\)d9ҭ)>Mp]7Z‚G':u΀(q) +u$dlM +'wk S-| O;y] +1ԍ]7T5ھGOݸ$kTF`OV|PčM]tMcY }v,n \fcj{.)ٚUŠrLV$B@u*B7F6y2|rr;+\[ιrptrCx!r](P +g=c(1 fB8P +G]d i9++m'AUdQn3%ilrO8Ӫo+9ETOiSWhrFdw3:{ϣlW .6lY{ex!8t_xW>mΈȀ5{J=WFsLhPv$}_RMפ} +˴{Bzr5x.UI&${C[#Eqx(կa9EP\%wEOwS8q'^ӘD٢#AJSTY+v0ZGM0٪]lQm>Pvy$+^D & H}Bp8o/wo$_u.7Zհ 7D-z VwԼuYa0N@Ye囍 'cmt-ƗYkC#01*SuS,٩p܅[C_qbhjF Y.&L0W7O.(Uf?UʍlE0%ݹ@"qy*`@vyIy%YqpF$Xu{9"yFmNp*{BeQ-f^}szs>^D lZu4']G67TN@]6dA3$6(OY99Q#a9q 9\=Mx$U /> o$Co0:')8\٣!=zЎˋ5~Q6#]1rZ1O/h_ ~]e7yasrvlRB$@Ifs3FJ1!]Cmhykr-!إ?ns෣Ξr ONۯ0BBʆt/6ds;t>u>-.3oJM:h$c۶2kEȫ`KE9 +~&enc*wnjIcw5kΗ@,'k;}Y.Z.\\)+;=V~M+\`I)^*-O0 ! AB\u߃7%˵ .}w[<Ċsdr#Go?"Z-6K1 +$d/:0\}]7> +vTUC:ˉA€e>Ś>stream +HyTSwoɞc [5laQIBHADED2mtFOE.c}08׎8GNg9w߽'0 ֠Jb  + 2y.-;!KZ ^i"L0- @8(r;q7Ly&Qq4j|9 +V)gB0iW8#8wթ8_٥ʨQQj@&A)/g>'Kt;\ ӥ$պFZUn(4T%)뫔0C&Zi8bxEB;Pӓ̹A om?W= +x-[0}y)7ta>jT7@tܛ`q2ʀ&6ZLĄ?_yxg)˔zçLU*uSkSeO4?׸c. R ߁-25 S>ӣVd`rn~Y&+`;A4 A9=-tl`;~p Gp| [`L`< "A YA+Cb(R,*T2B- +ꇆnQt}MA0alSx k&^>0|>_',G!"F$H:R!zFQd?r 9\A&G rQ hE]a4zBgE#H *B=0HIpp0MxJ$D1D, VĭKĻYdE"EI2EBGt4MzNr!YK ?%_&#(0J:EAiQ(()ӔWT6U@P+!~mD eԴ!hӦh/']B/ҏӿ?a0nhF!X8܌kc&5S6lIa2cKMA!E#ƒdV(kel }}Cq9 +N')].uJr + wG xR^[oƜchg`>b$*~ :Eb~,m,-ݖ,Y¬*6X[ݱF=3뭷Y~dó ti zf6~`{v.Ng#{}}jc1X6fm;'_9 r:8q:˜O:ϸ8uJqnv=MmR 4 +n3ܣkGݯz=[==<=GTB(/S,]6*-W:#7*e^YDY}UjAyT`#D="b{ų+ʯ:!kJ4Gmt}uC%K7YVfFY .=b?SƕƩȺy چ k5%4m7lqlioZlG+Zz͹mzy]?uuw|"űNwW&e֥ﺱ*|j5kyݭǯg^ykEklD_p߶7Dmo꿻1ml{Mś nLl<9O[$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! +zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)Km endstream endobj 5 0 obj <> endobj 20 0 obj [/View/Design] endobj 21 0 obj <>>> endobj 12 0 obj <> endobj 10 0 obj <> endobj 22 0 obj <> endobj 23 0 obj <>stream +%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 17.0 %%AI8_CreatorVersion: 19.2.1 %%For: (Zachary Mitton) () %%Title: (metamask_icon) %%CreationDate: 6/15/16 2:23 PM %%Canvassize: 16383 %%BoundingBox: 98 -140 188 -44 %%HiResBoundingBox: 98.7919746568114 -140 188 -44 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 13.0 %AI12_BuildNumber: 147 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBProcessColor: 0 0 0 ([Registration]) %AI3_Cropmarks: 79 -156 207 -28 %AI3_TemplateBox: 180.5 -120.5 180.5 -120.5 %AI3_TileBox: -163 -488 449 304 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 6 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 1 %AI17_Begin_Content_if_version_gt:17 1 %AI9_OpenToView: -39.6666666666679 23.666666666667 3 1419 866 18 0 0 -5 38 0 0 0 1 1 0 1 1 0 1 %AI17_Alternate_Content %AI9_OpenToView: -39.6666666666679 23.666666666667 3 1419 866 18 0 0 -5 38 0 0 0 1 1 0 1 1 0 1 %AI17_End_Versioned_Content %AI5_OpenViewLayers: 7 %%PageOrigin:-220 -420 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 24 0 obj <>stream +%%BoundingBox: 98 -140 188 -44 %%HiResBoundingBox: 98.7919746568114 -140 188 -44 %AI7_Thumbnail: 120 128 8 %%BeginData: 22554 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C45FD24FFA776FD75FFA04A4AA1FD73FFA04A754A75A8FD71FF7C4475 %4A6F4A6FA8FD6FFFA04A754B754B754A75FD6EFF764A6F4A754A6F4A754A %76FD6CFF764A754B754A754B754A754AA1FD69FFA8754A6F4A754A6F4A75 %4A6F4A6F4AA1FD44FFA7C9A075A8FD1EFFA8754A754B754B754B754B754B %754B754ACAFD3FFFCFC9C299C1997476FD1EFFA76F4A754A6F4A754A6F4A %754A6F4A754A4B4AFD3BFFA7C99FC198BB98C198754AA8FD1DFFA8754A75 %4A754B754A754B754A754B754A754B6F76FD37FFC9C99FC198C199C199C1 %99754A75FD1DFFA74B4A754A6F4A754A6F4A754A6F4A754A6F4A754A4A76 %FD31FFA8C9A0C1999998C1999F99C199C1746F4A4A76FD1CFFA1754A754B %754B754B754B754B754B754B754B754B754B6F7CFD2CFFCAC9C89FC198C1 %99C199C199C199C1C1C175754B754ACAFD1BFF7D4A4A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A6FA8FD27FFCAC9A1C2989998C199C199 %C199C199C199C199C16E4B4A754A75A8FD1AFF7C6F4A754B754A754B754A %754B754A754B754A754B754A754B754A75FD24FFC9C99FC199C199C199C1 %99C199C199C199C199C1C1994A754B754A6F76FD1AFF764A4A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A76A8FD1DFFA7C9A0C1 %99BB989998C1999F99C1999F99C1999F99C199C199994A4B4A754A6F4AA8 %FD19FF756F4B754B754B754B754B754B754B754B754B754B754B754B754B %754B754A76FD1AFFCAC299C198C199C199C199C199C199C199C199C199C1 %99C199C199994B754B754B754A7CFD19FF764A4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A99FD04C199C1C1C199C1 %C1C199C1C1C199C1C1C199C1C1C199C198C199C199C199C199C199C199C1 %99C199C199C199C199C199754A754A6F4A754A4A7DFD18FF7C6E4B754A75 %4B754A754B754A754B754A754B754A754B754A754B754A754B754A754BFD %05C1BBC1C1C1BBC1C1C1BBC1C1C1BBC1C1C1BBC1C1C199C199C199C199C1 %99C199C199C199C199C199C199C199C199754B754A754B754A754BFD19FF %754A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A4B74C199C199C199C199C199C199C199C199C199C199C199C199 %9F99C1999F99C1999F99C1999F99C1999F99C1999F99C1994B4A754A6F4A %754A6F4A76FD18FFA14A754B754B754B754B754B754B754B754B754B754B %754B754B754B754B754B754B754B7599C2FD16C199C199C199C199C199C1 %99C199C199C199C199C199C175754B754B754B754B754B75A1FD18FF4A4B %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1 %99C199C199C199C199C199C199C199C199C199C16E4B4A6F4A754A6F4A75 %4A6F4AFD18FFA16F4B754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B75FD16C199C199C199C199C199C199 %C199C199C199C1999F6F754A754B754A754B754A754A7CFD18FF764A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A9999C199C199C199C199C199C199C199C199C199C199C199 %9F99C1999F99C1999F99C1999F99C199994A6F4A6F4A754A6F4A754A6F4A %4A7DFD17FFCA4A754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B7575FD17C199C199C199C199C199C1 %99C199C1C1994B754B754B754B754B754B754B754BFD18FF764A4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A4B74C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1 %99C199C199C199C199C199C199754A6F4A754A6F4A754A6F4A754A6F4A7C %FD18FF754B754A754B754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B7599FD13C1BBC199C199C199C199C1 %99C199C199754A754B754A754B754A754B754A754B75A8FD17FFA04A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A7599C199C199C199C199C199C199C199C199C199 %C199C1999F99C1999F99C199C174754A6F4A754A6F4A754A6F4A754A6F4A %6F75FD18FF4A754B754B754B754B754B754B754B754B754B754B754B754B %754B754B754B754B754B754B754B754B754B754A9FFD14C199C199C199C1 %99C199C175754B754B754B754B754B754B754B754B754AA7FD17FF7D4A4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A4B4AC1C1C199C1BBC199C1BBC199C1BBC199 %C1BBC199C199C199C199C199C16F4B4A754A6F4A754A6F4A754A6F4A754A %6F4A75A8FD17FF764A754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B7575FD13C199C1 %99C199C1BBC16F754B754A754B754A754B754A754B754A754B6F75FD17FF %A84A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A4B74C199C199C199C199C199 %C199C199C199C199C199C199C199994A4B4A754A6F4A754A6F4A754A6F4A %754A6F4A754AA1FD17FF76754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %9FFD11C199C199C199994B754B754B754B754B754B754B754B754B754B75 %4B75A8FD16FFA86F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A99C1C1 %99C1BBC199C1BBC199C1BBC199C1BBC199C199994A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A6F4A6F75FD17FFA74A754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A754B754A754B754A754B754A %754B754A754B754AFD13C199754B754A754B754A754B754A754B754A754B %754A754B754ACAFD16FFCA4A6F4A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A4B4AC1BBC199C199C199C199C199C199C199C1996F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A75FD17FF7D6F4B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B7599FD10C19F4A754B754B754B754B754B %754B754B754B754B754B754B6F7CFD17FF764A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A7599C1BBC199C1BBC199C1BBC199C1BBC199C1C199 %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754AA8FD17FF75754A %754B754A754B754A754B754A754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A7599C199FD12C1994A754B75 %4A754B754A754B754A754B754A754B754A75FD17FFA8754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A7599C1999999C199C199C199C199C199C199 %C199C199C199994A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A7CFD %17FF75754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B7599C199C199FD13C1 %99994B754B754B754B754B754B754B754B754B754B754A7CFD15FFA8754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A7599C199C199C199C1BBC199C1BB %C199C1BBC199C1BBC199C199C199994A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A76A8FD13FFA84A754B754A754B754A754B754A754B754A75 %4B754A754B754A754B754A754B754A754B754A754B754A754B754A754B75 %99C199C199C199C199FD0FC1BBC199C199994B754A754B754A754B754A75 %4B754A754B754A754AFD14FFA14A4A754A6F4A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %75C199C1999F99C199C199C199C199C199C199C199C199C199C199C199C1 %99754A6F4A754A6F4A754A6F4A754A6F4A754A4B4AA8FD14FF7C4A754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B7599C199C199C199C199C199FD11C199C199C1 %C1754A754B754B754B754B754B754B754B7576FD12FFA8A151754A6F4A75 %4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A4B74C199C199C199C199C199C199C1BBC199C1 %BBC199C1BBC199C1BBC199C199C199C199754A754A6F4A754A6F4A754A6F %4A754A757DFD11FFA14A4A4A754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A754B754A754B754A7575C199 %C199C199C199C199C199C1BBFD0FC199C199C199C199754A754B754A754B %754A754B754A754A4A75CFFD10FFA8754A4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %4B6EC1999F99C1999F99C1999F99C199C199C199C199C199C199C199C199 %C199C199C1999F99C199754A754A6F4A754A6F4A754A6F4A754A4A4ACAFD %11FFA8754A754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B7575C199C199C199C199C199C199C1 %99C199FD11C199C199C199C199754B754B754B754B754B754B754B754ACA %FD13FFA87C4A4B4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A756EC199C199C199C199C199C199C199 %C199C199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C199754A %6F4A754A6F4A754A6F4A754AA7FD17FF75754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B756FC199C199C1 %99C199C199C199C199C199C199FD11C199C199C199C199C199754B754A75 %4B754A754B754A76FD13FFA8CA7DA176754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A4B6EC199C1999F99 %C1999F99C1999F99C1999F99C199C199C199C199C199C199C199C199C199 %9F99C1999F99C199C198754A6F4A754A6F4A754A4B4AFD12FFA87C4A4A4A %754B754B754B754B754B754B754B754B754B754B754B754B754B754B754B %754B754B754B7575C199C199C199C199C199C199C199C199C199C199FD11 %C199C199C199C199C199C199754B754B754B754B754A75FD14FFA8754A4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A4B4A9F99C199C199C199C199C199C199C199C199C199C199C199 %C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C199C1994B4A754A %6F4A754A6F4AFD16FFA8A14B754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A7575C199C199C199C199C199C199C199 %C199C199C199C199C199FD11C199C199C199C199C199C199754A754B754A %754B6FA7FD18FF516F4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A4B4AC1999F99C1999F99C1999F99C1999F99C1999F99 %C1999F99C199C199C199C199C199C199C199C199C1999F99C1999F99C199 %9F99C1754B4A754A6F4A6F4AA8FD17FFA1754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754BC1C1C199C199C199C199C1 %99C199C199C199C199C199C199C199FD11C199C199C199C199C199C199C1 %75754B754B754AA7FD15FFA8A14B4A4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A756E9999C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C1BBC199C1BBC199C1BBC199C199 %C199C199C199C199C199C199C174754A6F4AA1FD17FF4B6F4B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B754AC1C1C199C1 %99C199C199C199C199C199C199C199C199C199C199C199FD11C199C199C1 %99C199C199C199C199C175754A76FD18FFCA4B4B4A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A6F4A9999C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C199C199C199C199C199C1 %99C199C199C1999F99C1999F99C1999F99C199C16E4BA8FD1AFF756F4B75 %4B754B754B754B754B754B754B754B754B754B754B754B756F9F99C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199FD0FC1 %99C199C199C199C199C199C199C199C1A1FD1CFF4B4B4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A4B4A9F99C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C1BBC199C1BBC1 %99C1BBC199C199C199C199C199C199C199C199C198CAFD1CFFCF4A754A75 %4B754A754B754A754B754A754B754A754B754A754B9999C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199FD0FC199C1 %99C199C199C199C199C199C199C1CAFD1DFFA84A6F4A754A6F4A754A6F4A %754A754A754A754A754A6F4A99999F99C1999F99C1999F99C1999F99C199 %9F99C1999F99C1999F99C1999F99C1999F99C199C199C199C199C199C199 %C199C199C1999F99C1999F99C1999F99C199FD1FFFC299C1C1C19FFD0FC1 %99C1C1C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199FD11C199C199C199C199C199C199C199C2FD1EFFCABBC1 %99C1C1C199C1C1C199C1C1C199C1C1C199C1C1C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1BBC199C1BBC199C1BBC199C199C199C199C199C199C199C1A0FD1EFF %FD18C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199FD0FC199C199C199C199C199C199C198C9FD1DFF %C9C199C199C199C199C199C199C199C199C199C199C199C199C199C1999F %99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1 %999F99C199C199C199C199C199C199C199C199C1999F99C1999F99C19999 %A1FD1DFFC9BBFD19C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199FD0FC199C199C199C199C199C199C198 %C9FD1DFFC1C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C1 %99C199BBA7FD1CFFC9FD1BC199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199FD0FC199C199C199C199C1 %99C199CFFD1CFFC298C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C1999F99C1999F99C1999F99C1999F99C1999F99C1999F %99C1999F99C1999F99C1999F99C199C199C199C199C199C199C1999F99C1 %999F99C1999F98C1A8FD1CFFFD1EC199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199FD0FC199C199C199C199 %C199C199FD1CFFC9C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C199C199C199C199C199C199C199C199C199C199C199 %C199C1999999C1999999C1999999C1BBC199C1BBC199C1BBC199C1999999 %C19999989999999899A8FD1BFFC2FD1EC1BBC199C199C199C199C199C199 %C1999999C1999999C1999999C199BB99C1999999C1999999FD0DC1999999 %C1999999C1999998C9FD1AFFC998C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199999899999998999999989999 %99989999999899999998BB999998999999989999C199C199C199C199C199 %C199C199C199999899279998999999A0FD1AFFC2FD23C199C199C199C199 %C199C199C199C199C199C199C1999F515299C199C199C199C199FD0DC199 %9999C1992E4BC199C199C2A8FD18FFCAC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C19999989999 %99989999999899999998C175510528057598999999989999C199C1BBC199 %C1BBC199C1BBC199C19999989927286FBB99C198C9FD18FFC9BBFD25C199 %C199C199C1999999C1999999C1BB994B2E0628272E279999C1999999C199 %FD0DC1BBC199BB992E282E99C199C1A0FD18FF9FC199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C1999F99 %C1999998999999989999BB98752706052827280528279998FD0499C199C1 %99C199C199C199C199C199C1989998990528054B98C198A0A9FD16FFCAFD %29C199C199C199C199C1999F7552282E272E272E272E272875C199C199C1 %99FD0FC199C1752E062E51C1BBC199FD17FFC998C1BBC199C1BBC199C1BB %C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C199C199C199C199992706052805280528272805280528989999C199C199 %C199C1BBC199C1BBC199C1BBC199999951057699C199C1999FA8FD16FFFD %2AC199C199C199C199C199A07576272E2728052E2728272E277599C199C1 %99C199FD0DC199C1759FFD04C199C199CAFD15FFA7C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C1999F99C199C1BBC1C1C1999F7575272827280528057598C1 %999F99C199C199C199C199C199C199C199C198BBC1C199C1999F98BBA7FD %15FFC2FD2EC199C199C199FD0BC17576512E27C19FC199C199FD0FC199FD %05C199C199CFFD14FFCA98C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1999F99C1 %999F99C1BBC199C1BBC199FD05C1999F99C199C199C199C199C1BBC199C1 %BBC199C1BBC1999999C199C1BBC198C1CAFD14FFC2FD31C199C199FD11C1 %99C199C199C199FD0DC199C199FD05C199FD14FFA8C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C1999F99C199C199C199C199C199C199C199C199C1 %99C199C1999F99C199C199C199C199C199C199C199C1999999C199C199C1 %CAFD13FFCFFD32C199C199FD15C199C199C199FD0FC1BBC1C1C199FD14FF %A0C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1 %BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C199C199C1BBC199C1BBC199C1 %BBC199C1999999C199C1CAFD13FFC1BBFD33C199C199FD15C199C199C199 %FD0DC199C1C1C1C2FD13FFC998C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1999F99C199C199C199C199C199C199C199C198C2FD13FFFD52C199C1 %99FD0DC199C1A1FD12FFA7C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1 %BBC199C1BBC199C199C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C199C199C199C199C1BBC199C1BBC199C1BBC198C9FD12FFC2BBFD35C1 %99C199C199C199FD17C199C199FD0DC1C9FD12FF99C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199999899999998C1999999C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %98C2FD11FFC9FD31C199C199BB99C199BB99C199C199C199C199FD15C199 %C199FD0BC1BAC9FD10FFC2BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1FD0499 %98999999989999C199C199C199C199C199C199C199C1BBC199C1BBC199C1 %BBC199C1BBC199C1999F99C1BBC199C1BBC199C1BBC198C9FD0EFFCFBBFD %2BC199C1999999C1999999C1999999C199C199C199C199C199C199C199C1 %99FD11C199C199FD0BC1BAC9FD0DFFA0C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C19999989999 %99989999999899999998FD0499C1999F99C1999F99C1999F99C1999F99C1 %99C199C199C199C199C199C199C199C1999F99C199C199C199C199C199C1 %98C9FD0CFFC299C19FC199C19FC199C19FC199C199C199C199C199C199C1 %99C199C199C199C199C199C199C1999999C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C19FFD0FC199FD %0CC1CFFD0BFFA79999C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C19999989999999899999998999999 %989999C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %BBC199C1BBC199C1BBC199C199C199C1BBC199C1BBC199C199CFFD0BFF99 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C1999999C1999999C1999999C1999999C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C19FC1BBFD09C199 %FD0CC1CFFD0AFFC2989F99C1999F99C1999F99C1999F99C1999F99C1999F %99C1999F99C1999F99C1999F99C199C1FD0499989999999899999998FD04 %99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1 %999F99C199C199C199C199C199C199C199C199C199C199C199CFFD09FFCA %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %FD07C199FD0CC1FD0AFF99C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199999899999998999999 %989999C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C1C1C199C1BBC199C1BBC199FD04C1 %FD09FFC999C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C1999999C1999999C1999999C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C1C1C199FD07C19975754B27A8FD07FFA8C1999F99 %C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C199 %9F99C1999F99C199999899999998FD0499C1999F99C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F %99C199C199C199C14A27F827F805F8F8F852A8FD06FF9FC199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1BBC175270027F82727272027F82752FD05FFCA98C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C1999998999999989999C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C198BB98C198C199C199C199C2A0A0A0 %C9A127F827F827F827F827F827F8F87DFD05FFC299C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C1999999C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C299C199C2A0C3A0C9A1CAA7CAA7CAA8CAA8CAA8A8 %2727F8272727F8272727F8274BFD06FFA0BB999F99C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C19999 %98FD0499C1999F99C1999F99C1999F98C1999998C198BB98C1999F99C199 %A09FA1A1A7A1A8A1A8A1A8A8A8A7A8A7A8A1A8A7A8A1A8A7A8A127F827F8 %27F827F827F827F8A8FD06FFCF99C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C29FC199C2A0C9A0C9A0C9A7CAA7CAA7CAA8 %CAA8CAA8CAA8CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA8CA27272027272720 %272727F852FD08FFC299C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C1989998BB99C199C199 %C2A0A0A0C3A0A7A1A8A7A8A1FD07A8A7A8A7A8A1A8A7A8A1A8A7A8A1A8A7 %A8A1A8A7A8A1A8A7A8A1A8A7A8A127F827F827F827F827F827A8FD08FFA1 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C198C2A1C9A0C9A1CAA7CAA7CAA8CAA8CAA8CAA7 %CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8 %CAA7CAA8CAA7CAA8A8FD0427F8272727F82752FD09FFCF98C1999F99C199 %9F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999998 %BB98C1A0C9CAFFAFFFA8A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7 %A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1 %CAA127F827F827F827F827F8A8FD0AFFC299C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C2C9CFFD0AFFA8A8 %A7A8A7CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CA %A8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8A8FD0427F827F827F87DFD0BFF %A8C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %989999C9A7CFFD10FFA8A87DA7A1A8A7A8A7CAA7A8A1A8A7A8A1A8A7A8A1 %A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1CAA127F82727 %5252767CA1A8FD0CFF9FC199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C2C9CFFD16FFA8A8A1A8A7A8A7CAA8CAA7CAA8CAA7CA %A8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7A8527D %7DA8A7CAA7A8A8FD0DFFC998C1999F99C1999F99C1999F99C1999F99C199 %9F98BB989999C9A7FD1CFFCFA7A87DA17DA8A1A8A1A8A7A8A1A8A7A8A1A8 %A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A1A77DA8A1A77DA7A1A1 %A7FD0EFFCAC199C199C199C199C199C199C199C199C199C199C2A0C9CAFD %23FFA8CAA1A8A1A8A7A8A7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CA %A7CAA8CAA7CAA7A8A1A8A1A8A1A8A1A8A8FD10FFA0C199C199C199C199C1 %99C199C199BB98C199C9CAFD29FFA8A77DA7A1A77DA8A1A8A1A8A7A8A7CA %A7A8A1A8A7A8A1A8A7A8A1CAA7A87DA8A1A77DA8A1A77DA7A1FD11FFCA98 %C199C199C199C199C199C198C2A0C9CAFD2FFFA8A8A1A7A1A8A1A8A1A8A7 %A8A7CAA8CAA7CAA8CAA7CAA8CAA7A8A1A8A1A8A1A8A1A8A1A8A1FD12FFCA %C198C1999F99C1999998C2A0CAA8FD33FFA8FFA8A87DA77DA77DA77DA77D %A8A1A8A1A8A7A8A1A8A1A77DA7A1A77DA7A1A77DA7A1FD14FFA0C199C199 %C199C1A0FD3DFFA8A8A1A8A1A8A1A8A1A8A1A8A7A8A7A8A1A8A1A8A1A8A1 %A8A1A8A1A8A1FD15FFCA98BB99C2A0CFFD41FFCFA7A8A1A17DA8A1A77DA8 %A1A77DA8A1A77DA8A1A77DA77DA17DCAFD16FFC9A7FD49FFA8A8A1A8A1A7 %A1A8A1A8A1A8A7A8A1FD04A8FFA8FD66FFA8CAA8A8A8FFA8FFA8FFFFFFA8 %FD12FFFF %%EndData endstream endobj 25 0 obj <>stream +%AI12_CompressedDatax$&?d& C;\;fJ-n[[YUZ(xd&,f #+3q98OxOq< ?wo ~7_{ˏ~/&G4M~Vۯ߼LG{4?oqwo^_^ޡݻ篞g/PWnC} 4`e߱=&7Ô?L/ޣvu&3%Co^|O߾yq7/߼W?>y~^|0;qv~c7/o^a|t/_/tqzWw0R<3ٯOaC)?l/Jo|ۿi[Lݘج{K̬̂1??J[x?~W~NguG|˻FѤS7_ܽD/ H1oɦ >Kz3 3y}vgӭw2IM~?WyOtҺ2!Lj}.6(^{_G<Ѽb |aoSl߿DŽkѽ_bٚO1';=I~R4!o;H]cձW?y=5w7_j|ӷR}To?~_^bqQ>mŃߔ?뿏 Bӛ{U>}|CsL!޽zn +!}Ho_wg߾܎Ͽzz_~˧ߩO n_|=w.̙/|ܿ8~_xNuTOX[˷Zߥfi>$_>ksP3|q-y=?F?!]϶j~xx7/~tS/>\?߿cwwI|+r;鳶|0e> loq\߼3L/pba,H=;0?)vU\) ߀%%Ӧ\ \܌x[f`*nU-LDIo^iSi.繜5Jz7ѵ][*FAiUU*'-VmӯVuY[a^^Zd]fU͛V+ebe7g]k;la]/WkdYԬU)۵Z)*և:YeXtMe@CY#չk)7ܲԓŗYUeLIɭ̍zʵؔF2 ssλɝP-Vx>'O[L .C +S +p7v v)esUsSʧ|o-&7 LNyni̕W*^rdNONtemwp|:[l6#ut}>_\ތXwoM7 usnnnn#n1aoz^m7r*U9զL _^*qSªUq 8R$l!z7M9kӪ\ʴ*ySҪU M*vU̪KS>_֣_WENf]Z%. bXv 2ʌd)v64o򬡙R&)$%JR\)vWL%Ϫ?')WLRr)8K R|)%Ѓֵ;zeY eeگed&G/fdndbN2yw|Q^Z^Jd^Fq``2Ϡ[W_t,yP5 j>H73_J F1aoטt@H tr@x#HuN D;x;p{{@}w & +D5CpqnX. K׌ucq7g\DW);2UnC|Ey x;Rٙ-خv8Pz? gx'H˗v؅0ܮH *`Cld!2r.y 9&*w"6`ټ.b/+>P,"eXalX~PdS }bٛ2<duXE3Ϩr;.E9J\d('}(bs=U-l4W=9"}ZӬYAL6%Ts끼VJ{OX 2=ye[3UCwD翇d?Sԇos<;XԵ?ʏOBF7mLE߰s8҆+ H$" (" tAy\( !DʢJBFǭH|! ,DiȪ4$uN"e(rE"R,RQш‘Q ,e$JI OeSB%'ЈjFĥkK(2Qhؔ|J5t[듖|97nI?FյX4̲P,~)uuxIx" ?4 Qs E6< KCvD1l4맕aýE|r.VOoGEq3- -U +ͪLTTJэEUZ*mXM]JY\9UDi%%2r5=Ҵъ %s(It8i4fCT +a);12y?Ӌ+[ @c&*PV5m\86 ŸV+-7bU[#w)Zf{% 0<@jXp1frb=]9It=G9 [>EXy+3KoJO+ï~QQ0:1L<98Ilj> -Ѿ8Jl+u!3)]Hh% \hB#ڸLo{[Z&qk"fÊmIWCv8x}i؎u'^{߇qmCIJϞ=c@09Bk۱iccwt6sM&;XݸCI1TQoZ@,qx)G7?}HH|5FPH!H58RZ$n҇U}|OV#pDnyu:q/#7I0c (0 ++tǵl⟿Z +R>qV#6Cxji7~zQfd @,zv4./_bS;;c4`&# 4؜6%0ySIRм m{=jP]||s|<:@> lm/zr._S  +_rUXBa3|!<4S<0< Sy+De&W@AAr-^uUjKTˈXōuXᒗe?#uFnfVŜzQ27sPf'z3 mdq<9+r>3_;ug{YʪHNEa십]ԕd1U w]<[ )8c(bb<;]=),IȥId,v+POD QuVoʬ*rk$V_ba_U[X.}7IʲC#rOL,lEkY, En%ʦ"ƞD1V] ֯$XJ:vl`{6VY=+2ҡYAnVط6{;,Y ugeV^am|O;>ͩ3/Յ8/+w J.zꓨW|٘X \bU鵧_o(r = wxw<}e|m>rz|/GTy/Ҡ `+0{ط>WG|/\,O_{ُF-I}34Yݵuw}\&vřTi: }S͍2.",W(^QFl~34mٷ!,ԅl#AiEqڐ9,%#s֡%bn#'g=<r9?3:Iq?:?'3{T|ʞ:/yE X0=QnEk Zi:EBm9Ɍ \:H͕}g02x˩yj4>/=?S=w טiąYpo8&joEI'Oo>sQcb J"cTՎb&)xEUܔ&`dgFzv%ś9rJAKqj6Myt a\~ا~+7^ݽJhgyus]j/iR:SnBL%4#Kq _ KX*8^Kz\e>l/ư7TVrǩ0 7U/ޅA[lH8$&qOTqʅZ%.B5!% KJ)@(|GY:>LBaK\NKyUoItfm6ŭݐ\¦?)NUWe6>%BE6Q%J>q唚mWCyZy"obݮuc*qj+}ZX9>p,JͫoD Dޤ$;;mXGƕl_f#2-dpeQSEN[cnm4;h\; Vymff g,Ju}o14Q$:i\-.ns8ič+9m6VkϚ|['hZzvvAuaG`}t`}YhG_:m8GN84hu*, _ CkA|,|aGWuw#my{"Vފݹ(|مavWJů9jZ~,wr Ez_dxZ_KY˻JR EV 襠qwU)y@R޸0fwnxQ4;9LAǦ6 +wz·2_}q|t0>\v,нe| +(Cq7o]ͷ`['sثj[d˧1rQNت8 ETԑ<}>S|efk Hʾ_>Ctu;,0D9,'%CS9 .N[ZcqaO΃q5Ovi7bE@Om>nzw)6>FaCmq$8I?I7 ,BԽMٻ +M^A*V*#9Փi]nLʼnigLìQ&[,_?5@'Q +oDT7 F\'uY6@  ,[eh>O*r.V+÷h#exZ:i0$3QN +ߑ6V<ϒ^XEH92kV'jX\+KdP8SGYр3ɡeNqV1 e'%:U9J1џP:vlu{L[5*F6Ԯ/+B ƪ-; eфaǓJږ߶<' Oo_KM ]"\EkF0EDϤE Ȕθϔog|VUu^v)H! {'xT:UjCITҒآ(ZJ(Z4KY薮lZJa\"6Bi(D9P{i{::6KOyíO0i5*alX!X%RK努Q>E(c4,Zۙ;^o;r%( />1]3HJW.=Cq Q; JliJB%۵C^ײҫjL[ExDZ䤵 nI˴~ԻzMj,v8'2<9> Oo_êD Oس&F$(0SNeb + +0jhMMop-~v*fPj0<м4A͗ƪ]\ /0)zdp[_bݫjZL]kmrkX6ָ՚.XƬuɨ1i=d.LYO^IS)exZ 2< NO' +O' +xm7?a>Ykpi>d8$\09 f‡B'zgFxqO/e⎭cT1;ɸC0tmojj{ (qp͂C@z Nim)x?NvU!qnܺ//rvEi]ŧMi|T3ٷO'ؤ\L+u6u&٦|t,JtѲK]j=?Q`%֜k,pHyTak5u1'@IQAl P8MJ&7nJiQ<YOdF0DG3yvhCx/ew9ˣ/h-UPhnu&rSwOfc[x^$؝lxqq}csnٖIİX#!uDmw2<@7[I%~d<>WMeuv]a v2K%ѳ.ڟsq\\40Ž[Ѭ 7A 7_,~bN|v#n`^E.'[dՊ>R: ^G=,>($&K}id5VZڨpk ,ٴ 0 JJTgb60rOd`WIj>Q)3G^<-g@GpAYj,D9&# n-ƄA2 m2v'}2(W]c~TQ9oXVͨt?{<% rv 3nl=ثcvL !crU8+}Nߓ%@q+%ǭ$"~(2^ +Ma\Qݙ>Y+|Z[1z[gz[1Ԙ۴mJUr5Y|Vnodw`xNRKisl\7P΍||TGYugnKE$%V`t +6>+j::T\Phel銻PnC%oS5 +YSh +fWX1V *:I2SOBI44!eVk?xܓ'cOLj4-oKYuUh% JLfd-ytu<+qeYmˇ_CUVN`7(<ˌpaN ƍ]崂v + 6<žr[D\,-9n^$ D8aw2\0[ԡz*--ZLTFh7@K`nQ@밆#^MnRD_yrwQvpѹჲpb?McCa7Mt;HsM8)4y%qi$!wb-jqo1Pm+?:W]:HC'tJ!v\Y"fzw)n.(RD +K gѢ_ \߹D!˔] bjQ"#ΐSm2a+|;rudȼ]A>Q?ulR0Ԝn`uEss9 +Q37fQ;NLthp) n)n6WZnE*:]yu}; ӕdqYJZ,=*SZ< JӐH~[͎csߥ ÅR$Azae+̬䜽)Ò)SA$ZܬKV׬q:k)=As<2mS/LtMo@] ?}S>wihx9{JgU|>i?)D=:Kb%5ީ xyN$ȫik. ~s>4P}h/=vY@M~ ƽ#j&btan"k9hj/$s.!OE8]O$opLs*~$neqb:<7BB.oN4;rN/+"V}ZC^2TX5wb!xS`*ffs9 : i.!JUfQ?H*F!uV[6Nա$ͲWa⇰0IRnJ:'hp!bBY +`E;p8O +n2 ;aN(FXg `ᡭXbmZbw k7ax-(ÅS[/|um*][Wm!n;2._=11i8cT]#w-ՇI~ݔ÷Q~^/CQJ Rz$-Hi[Ȥ"k?hR{W5qn UVi+~ + +whpC=C~ӷݿOVrbXݽ} ?9DaSt~;X43a{GOl6O2+o:O?1'?O}t+T>:oZv{{~ywo^?ïD{ӛ7/1y)wo>;=WL?ܿ{݋w8w|ȯZ>@ȒgEYkpZtSacBM~˵)\Y$gq81A`EtY5$fL!Lzv#`!1<5ق6$CkG9`g9A&0=޲;z|ŏM?!0}<&|;t> C0/6R[¾3>8bx 7鈹3]Gg #.6ÄJ*1*ɁDݡ ؚ#{6+#Xu<*ln[T8lǀ`!k_&.GN΄bia&P,. G_;3GxbcY16TCG)D[^6Wc,d:ɓGd1IN'LH0%@;K'Y(G(MO#6%] @PbsO' d$S߆d-J4Rwg4udo+qo5!|~lb>Y Vg= !9P`W^éȝGV` jTZYBؓuZuvd1elYSg#n +}[Ϲ0qufۿdf7_/ ʃs7_ٛ? ojõL{آ!TfEbѝ(xL(2f㴸˸$n +,L"C"zJЄ \7T[ʤd + 6vBoeW]!DW1oHwm%H2kdRq'CՎXZ[i;Fx#B޵] yoeNl3_cD9D/*m% +dބSy]- u•HJbcx[ w?* rYrBʟ:3̑nsS.eUdSik3G>fT9i\h?qsiC;+x#@E:PlXJŽKun|ƛy$ 8C(A]Z0Oׄm x{\F0TOEȓHfwaaJ@Cr`%@|R%RU#>Lo;yp"MfP"0=Xpn |9APr- +23A(LOř\'"Dӂ3 +|=g>/ݡR҉Ѩ#3'7=.{&a1[f^qɷb!qVxT,@m gS+ + ~ +gZh+6,QYޚYռ9>sǹ %ґ?l mm]㽃)Lp轑ChM + SI8\Յ<%44.i+഻2>=ρo?Rݵ41?U58?!Yި&2qňLeLf9m#p\ Kdve~e K¶#}0UOe$|xA*4>:Q[# + LTU$ ֎&&4yr($bx(5M0Ɉprwݗ#-F6<_fn.(`wy|nME&ڑFȄ/;b)q&L{30D>fL4Z$.H=%7}+.Xl Vnp4s. .N:іDkP'_C:QӍŪNT)/#*tNN."PtHݼBsEn>nlG؊ TV7SBH`dp,d kEڴqƑ-@#v∣ؔfAR !D^Kh׊309Pd.WjZQJ`t-vߩkNM7nv[y=Anu =U^d*NIq<4Q) +4֣VԻs9)߲zNWQFm;-:ϱ?3RONYϹ4wJ?Y1F7lֵ, F8J\oY}68j @)7TەTEਟ +ic c&N-pdd?ѭLc̍w0SkY~x;(&68`d]@i U}q@3ŭOʨ4|nނ, xʀ1MO#iڸ&H-nqdq]$v0qTɣR;{#OR?WjJ'$q+M[1MFU7$#C tm!NLa\h{:ldneMq @G׾QKr>2k;AMx_}puB^C?1*!jT-tC"GӰ׏-8`Ǹ;EN/ik0C?* RFnvn~Uh:}|KCpބTmOM4{ޭ8Ix!@cnY`LXV@--4%"Epj|E$GΈdɄx%hkQQomh=mCQoOwN $. +4"Fj0Z̝eHӐKaDcJj{ eY[El]lB8y@A;^|Dڋ(@\▃@:usFtΐ"hνIB/>RzH#wm=Y sΈr ̎\DK墿8 W&:nХb%uV7K$3)ގI^c_SroT#xCAa#4[So+8ո{|*&D +l0;ut0ۙ\L2(D/qpoTWF\ n6!XH/@]/"#RBեx9D +1 fEh5X1FHXPIX X|5^ɓIr=t]F3}7Q0Yuoe%D>9tɆ/Ge#* BBu Vѫ!-W'0rǺ~,!^iTvtItu:+! H{D>GYƕn\/ iǩ'+z&;vʈ!5p O| ߩw{$${b[BPO;y,Ͼ YKfa5;-gLP>]yl?w"CuQ*v>ͫ֋БGqE{'{: +豛ɗWi+vwf7V'˒Qv]j.gT쒑hL~^eY݄6:oCت(^@Úd7^_y“-]- T?Q{jԿYgOcV/ɂ3xo]:y_~lukjȣ^}V%/VQ92RඈPūV Xԓ;Nev=ϻJbOӪwCi֠cû3P.v^z:p}:5I /P'U`fxX;Eu} e?HS߼N"7m&E&<0A4۴-fRny|WF$C2KaR`/v&ӋKD?:\ + DLsL^:~"r|ws5mn%n!#\ +얍y09ġxJxI&0!Sl <ཽAz#௑(k$2JS` L%NO;/< &*0yf0 +XOV:GKoe'o/^wDFFWfn +8ݱ ɉ)Z\ɹxf#~2`gWuSq!nH "74w`>`ő<`z*Po1գguΐ!?穋H#o1)g 5k78'*%PbNHj2pWFpJO^6GA0SXb;VF h=Yt՘‡Ob4Q4d1VTGKN,"6ΜF %3a-W3e^\ zr] Ki +/ƺ7EN)(ꦑ6~,VE VSӢRfn~:}t0%GQ Dj[1"FQQb3Q]?h+&@zLYB +,%Mw'Ia$ Q=uYsTB -ݓU g&TQLQLgyiP6Ǝ}]7iR^!FKBᦢ5Jd2Ɲ:qu#U +H)4v-@BN ifSeO>I"Ntl?Us*Oi4K;!ql%1- "%(ѥܹLoSSᕝm( +֌ܪ+sFFWod,QbdB(*dtI$  F`3fP"0 }̼aiටkp=YS‰7-b$'186h^H6 +Gbgy@h <):o^i&망n( +"deA53cK^3C"xfB+DuŅ*MfHV@ cX=dԥ bkug(]ꓚVI`4f !m :Ez8ӿR@LM7P[y=A + D T C׊Vc}jxɠj)BН+e <*%2.Aܖnb;_G韬蓺VɕVv,]ߠwjڵm?cDtp4Gb@ +(•<j"y/R;θ:Q4rS%f6tw"zSv[NC*FJ""9XvZ4OZYե洣ԏ8^/\NMe4c ݲ +X dp> .hۈ~$t$kUeGʀ<K^ԷxQ$d B(^먭ŞBע}*M694O +XΛ +u,_w-/ߢZ {[;=qUZ*Qu஺/[etl mѾQZ7nv`܆EU vY};ڏ %^VDoɻ^taecDX)pÉꮅm3׭8mP d\K 6oѼЋaNt43ҌGS!xzFж^pݴt3zT9BET"C ":] È@:~$@":$:Jz^.8DׁCt|8D8D5lסD0};-^_  $C@"*_ 1CAB~-D *.D*`Pwa*&`P;P P6Cp~` U-.C:Eoha;px( .N8ZH] P"ҷ~~۱tk(M+%X8ag$ ٿQBT'z7[e 'bh3PW$PfOVtE9JF;z␙q@2\!"y҆Dp-T>jhGV,J;#[4oajRAq膂i_-􍚉ick4>ْ`86:~rѴcu{BXX,VR0HP]0OθN*E >eat <~*6Gۑ"ok ԯLpǖǻ*L{U8HJtRFmKr }#B;Ӷ > +|\oa\=y)t3!-m߈7p@y7E:B޵Ҕ=׼{R?G|?Va$T1Oب*Ed\5!soD 4@.3b$nJ{Us8^}]U~9)-ר>M!:4iR#e6ݏiXeiig#[%.hb7Ϡ=[/abrQkG0^3Y'{: Ė: virwb+VUcն$4M?ʼӽP짠<-1$[&mI1_VbFVcF2i@TkʸLŰA[#4S~^Ʀ5u<4O>NJE(پv +s.MCoELIənܶx`;(VLG+qv^BdIkQdX佗ak4Y|X;;iӍ4h|^3\ĺ&qeߩiQfn~:\l(neGQj~ZߊU!n+*Z3Vbk%Iu3f퀴$݇ϢzYTW$Kn_ $\VbE Xnfa}\":*K5*Z z*[^ŕb:Qw3H`P=)I`M]aU +3ryDgQMtz8EECẠ~ +E{,6A2 xA @ɹkAv*«;- dCBDn}'dхt_"1chFZ@*̓dZި\yřLb +---8 "d/]T̴t>PI(v u 4O8oub){4W`Chr)8E h`hK}LZ*hE4i[4gcj#;DKeuk#i[Ni9sR ,i@`-~k9N.mm]+[Tɹ@tWiߣķϩ{@:Kg~LAǴB_y22O8:}UaFE#b)#N( + ,0+\10WsZpyt{˵ jD$}4E} o~߼}wwᗿynovw_^߿~ֿgx۷o^_:7_m]iFϻ/ٛw n޽}qR0Y߾y|YڛgWqn^QпOw_޿./GEQ ɺQ1FF>gg|p4smYn^۬Ù߉|@c5eD!'1ծ뀑V+Fky>٩* ~ y#ogclJ)+J&H4 +f`E +ZT:УRS9@3%O,#/hF1CvU@uyPk%dK0^;:#x#vbΜ%gǤ_YR'$MhAܖFJÒI_G5@T0{7+\Τ7710 0@,n&V 0RUBn>GRW9E&?Y#Њ +lJFIVE [VW}-^pG|L8Mo F&ЃP=c0a`oM }8&\&)x,l'PX&8gc`RIM$h8t8*sȘV<$XzUAtDTTK{K(f@Z`@Nb}Koiٗ.F|ȀGԈ4тk ɳ`KZ9M5XXI3m"3'!Dk:$$'5A:ы;I0ђ#Hs9虙x3$f +TىVl K+nKv b@LjHE# +&4240=#%sM9I $Q,*WNXYI*J GFO%]POH(ߢTseѲWT<0ƬF&v +FB&?iRa}4J[1Ez.GLLĞ=ܻͻqt[ C5>N]d`Q8nvr-[2o5ȓo";fQlk=V1s!&imI*VpdJ11GA[.T¨heg4UȔ(1qK2g ] D`~K+U։SSZcIӚ\ܬu>B|59F$x8X3=,'(SOHc\|(6sZ3ʕqpǒ:Uh!v%,z8)Js [1I9(zdŅvj>i"eې4NRcy ;j@6WIF})Gbc-I * ̜!+(+oe#BC]Ѧ K*`R/}Wq 9W|.<(8"d譙PɕLU+Y/|d^+¬[I,$ےB L +W aҏe + +/AxC!A]U8VwC !oOsᓗCj%\B[.|&HpxQ3t+d`X)dVǩ +4+GNIeΥ3I bDI `xV4HE=sJeg %h>g8H\;nZne3ٙYL4tR9%\UصءIT|>T~>T*YIxYq;gn0+)["|=8:W4nDL_BQI>lC$;w$tд;#/%~ԥbœoG_0;hAr^9\o'“ +QWT &?H8 5`RoF9Hl Iev#Y B\j'ADUÒCTG|z!VXV."=X>w 8Fi$RJ[JW|_poe-v8GO| !v6*,W`-eIMu_HBRg_T]$:߆6P8|!= +IEmla˴Rvg *t-#dD| n1w81ge/$R`i qE8+e^2e[5ע>)~-~(i"^Yш*W#CJ '~L#?ϸKOՀhJu38IL/EQYɭ@E'R]3D"8Y!.=cVL7I/n[s .O99s}?B8AMQ2z݋RYajQ2u?Oύ}$)%M*CE[Br3:[!Q䗩rd(0-.J9*&rcDC +R1cţߑt0w7E%IKMj7؄^hqIj!HBT (M)j@%$q)dFF! TSF&7DaSGT'S= k +!#.ZUzZi)Vy ~TD܄tYx w &:GX~i+;$2d9&Ee'i! VAY)VQAEṑHTjŭQaW]2Ĭ HdffT7G2ydVgVZ*=IHyt 뢹rq=*7 ~&\jYP0ⓖE +j/P݉2L8zt?PsiFOIqK&W'5!4ĥj(YQ( +XվUPdlV@Dw<%ߜbF=EQ30YUJY9A}7 +jO*ijI[9p>;2U%Dc&ƴ0'NˎcyB *¥$ @k[ _K@(w˚fIP},PǼ(gn$:PIc㘊O0gT@t;)-",$U 1=ȗN/m3x6i^dMf., &b HdIЊQHğ5\j"Gs`c~\|P$=Dz8N4jsM$PJq^GY׈Ҡ4ptkO +} +%EEWg8չƩA]xnY!gŐIYW)¨{D*$q F'wc2xL=h+)WXQ/ Gf[4%XCJ損Pa^04yB +3ٙĊL$-8hRdwrzjsKlWMxI)v3d>J3CnYvܼhv 9z|`1m +`N_7y:x5uQO`̚($C#Q&fMΆuEXRQ`L{Y2gI@a!cհѧK-Kdr08OOQ[ptNL]6,J`I#ĕu=ADQL/@^I|ſGc!qʦL{>ggf0.JR]rOԸR]w 5{i, X]ź G+)%k\i"5(&)TJ' P^f/E Qԍ:5Kk5gPﲑFL55[!_DhXW] ă'$^υyt!D8 +YOlOEa)J2jBޘgA^($p$轕dpeՊłd ec9d_ +PHEw21hH#u;(DMv,ٓ-  ;IdgzW{8C@_al=0` eC<+ܟkR<0pg3"L2bgCh49r0GGu'x,Z0y2jLղ=;(fnzӸWl88@-aF`"Z̵I6$py30ܚ8oDXr82Ռ~Zq&nMDt 0ng=ܞq) l  {Fn^ +4Zc[,kl\bI+(5Sgw!9~qpT40c/[FbʞaRE7kg~D8ࡉ1`V aKb%J*c9dм(=L N7"Lbu9%6W`_ +2Ar+Pj#؂\͍i&YS~IY6e mCۨjk!atZ=x9[m5{z`fyv>C',|du2M JOjߑfI b಴^˥ǐ%0~VjA`Xl_nZCu$ (f_Ŝr}A'V e쳄mgB+ |%v1_#NjJم10tfW +'-L#!<؍IMMΪn0ǟ` cu + n-K#!!?FT 4ISiAKXZ^!TztAwJ6:7~:*GCb!1; 8[]>mS*|)겍@ .(W <:; :զ3 +h8qML(=\2)@xYȫ3{!n ؿ? +mD=ߞ+#QZ)N,czA-\7t6lN B{7qes P)|ïMCcK-8>4 34Lh=^Q HW,7)ӣ3?P8 +!FRd% Hi&v * r*Y6zELS "XG}v `ͿMA7R-̫{f4-?BEf/3~bpGhvcPS|]rߘd 2J9|J6 +m!Dr< K9o)ζ !~H㓃6-$ж|PN *>q<QRpQU?9/amK,?hca&ť6htKwO- G +U!|j l_p$7G:j'Rdq! U~~־ Em;np A*&<8'cQiTr[LmG@^J).bf_yXz|+ǞaV'%Sf'\<]1)fv=%LVe%wb$T*돐Cʉ뱉|^@r|,9Ff g*;7;v-n9,Vl(Z5420 2( νD͗8Q)XE4}<ZM7Jh]5y曲71RddYl=yEpw_L!;!N?P Gk6H~)H$(Ңa~=ЋorUO _7t~o }\vS)uIMaSw }҆߇ORޞ @1Bʰ'p PےZ(<A[lg ym <FumI ! ~mm.?pٓ~4袽H 22w΍kDSRꢺP)a戀~>$4B;>P_]{|WG(tϐf(r>Rǜgˎڵko +nSVfNMԴG<qҘ<35\Ҏ]5ۖt0ɰB;/1'YV4%AN$ErrR:u`+R_.5luG&Bv.̯iUsh_jә!f?PzE2U|\WFU:wX#= +ψ5vW=JEאd;3]助Q2ztN+_`}{"}4*T*QГB~_d)봳4Fړ{f) $yضi9ؿ`W@hMZ,[P B0@Z,a$|3Y4st(?~N!$s0 ͵ +ku{aR9'tv5e +K'S>!1=@tPWfl;Mu8Z]oTXó.?@i d30P^6@(AG`Se'.`+V]ˠ^B|,r:ҢcI]^_T6 tzU{9S5L-H AFce5GiH x7)G~9ї{>&0 +?:2˴elX,&6IGt&s۠qJr6:Z$Q6D0N{+X POM#; +g +2㧢Ѓ^Nω*0sʤ>G VrljIfmP%YU, yB h;+bPi,hvC%5x,=V_Kdt5첷f0L6OL:l[Wk"qƼre̋899C!)Bqpu»!^ҵ HAbB?Ww7 lgHH BW3|[(@"UZK!餍m=V̥Wx`p}{]$B~EjQf9!jBt<ފI \=)GW(fKQŲg.+sC?#Du>zoh.0C0i˔ē&鈀¤|3nJ `.GVk>nX9l +@0*'7v ?b՘ᒏZa6`P"̎垔]Ur'2Q ΨWDH B%}C[7c"))C_Fgl9B s^op"T6 XEoIdtĄ N"'L|[R=8;by!MRZLk&}9EoB1ws44&䩡4"t}n U (as!Sf6CeU3<  3ޖ,8aEv~@3ڑ*BDzL%gƟS mA92@E h1tzE-h 6= |^qD*bT`[BDˋM9W._ Cgz#_iYn!T{Z^ =xYahT<RQ{%"'M86P,vKnr-XJTi2sh2Gaɳj͇Gy6N +]l뫜%[U>C 8L޷8d8(s7QF (a6Ķ)2Li(X +G8x^+g+)}ǯxeQ@!= + X{3Y=aYLRIN+v\)3a +i, +MH^ƒd_w)sC~IPLzfW$dm&*n뫜ThN*m6RT)RŠc U>1n=PKG{ah̼r #moaN#Yl粄~ 术I_3gX Xus]|fAJ̗վ +8bʋң9rK֚FHU[O]Ad xހ?7L|' 8e%1`KQ,S +JytwFWr-ԼgT9ǁ77 MVDFO%a=WV8s{9DUpfB)R|N9E.6݊'5&%5*G*عJ pm}jHg/!I̼޸rqޕ9TVr(zD+="F$qG9mFpU,T·T3s1He>w#,fY\5Whߘ ˆYȽ ^(T=z sUU(K5ڟ 0F9RՆ ă蜉p{nakS-V'q˂Iɧ] +o}un`׵\YZHiQסostqRj{6aΟȡ1K mFvʫRBP%D-Ά9 xg + &\[SYg?Q] {I>xu/e|ڱOBɪXq*3o-D:ET]p?BtuG41E,4 Ż}d()#գKv66o +OX@(X8bZgw@C!'AQ{`w+9qVr6%}L +u I)#Eq&GUӒJCxzC>s4fYHx{DdǒԱ p\lwMgn0.PQV<S72=rj륯pTխQd:35k3;wPgRlx _lBGR׼:TbOqZno6A&F?FyP+Ytg3dk5^l4xSy`áͤWbw}5m:OX:If\i,o%6H2_57b;s7Z~C 8 Or;UMg,{2rfc:Sm?VXY0;ܾOX@u2M,6ª3e3xc 9YxAEIA`YZh`aQ+I?exBMZ0d 8ܾ !I{D>2@T:sYvpDvF>"'oz3ι0\2;zc@fC!!.(zUsF)WcќpCG`GRs^9(-J\s +7\%`7Ľ?#O MMV>żyH|+D3Sry,$GyЀ@@ceJ% =5tn+C'P|oUa$4{,g0 t _}8PHG PbΛS$@Ǣ*_A%$I)rmD1׎ʶFe^;^F‘|ȫ|7B<쉀.-- +AQe|(UXjno*I!CuԐEVAd\d[V%$M-V;C <36لdi$s̳8ȅ(ߧ5y- dnѽW "^m1$d,_zDD9Ƕ>4#XhD;uܦ$Ks9MGjToRn_Ds??O?ӿOO?OOO޾ͭ0'Ϸ`<១6`V1g ܪn17Zr7ŭa~|f{ 1S=V!osT^!<!# +I#*6(g}|щ|BȺb+[)>$;fqZvBIUsB0W0HQLslT Rg/?/zKԾ{#'A =fy_ݧo(?[.&ʧ}! 틐09 +a__$Z_ )Y=LٞG}okzJWGz)o#z>HBd|垦2oC?W-O˷<#Iʀ~6Ĵxm]<4O.)s6Wl[KG'xoξH}'Wݻp+Avos.~tqs6=| s~wB~Xryw6gj˓rmŎ"0ۂ} xf;'BOu>=hKPE:t{o/u1#D.;q2]N jendYG!,aq[m L1}QRP.C̲|>-tyahJG8402~~'woy| DQ?% 1QSnduh5OhZRQ9 ++t/ksG55x/FQ6J7lq((Π4~8/moct: .? /d`!_>+/Bnz1uNi[s!˰!Afy[ _~V۶ܱ[Y9nQ2L^ї!#W~G/ݮ5(}O%0"(߲oyX3Đ|C7!Dn4\m&ᔿ NJa !X?YEŒf]"j7Fp;,=/u[{7.OG<[|0.Sy۶jpaEnZOnS +mҝZ<'Y yF~FIDpFżvp<})?m-)e 4fZjOmIm]SlScⴎne$FV厍 +m 4uΧ6~gq!?k,ݚ=Mˈ}?\]T8o/ND۰H|7QA1n{j+5[+53 یfӃKmip7bt-P>Z~rf /jk8Lp҄\vYExI4$WmhNsy fDa _3J.oh B}l2{噿A=͝9E d;9smsFn,ݤDK:fIJ+@mD͉2y!кn2xOX[S2k\<#c[ݕ]Se6JGy)'Pd+S^~ɇ% e 6G.nrx.ܞȔK{Y!_a 2 +(=NZK*#69ON/scu/scdiS^V遼>H,a Q{HlpEJ|0#Ƕ1R.^8|)YnIak4OgA*&-yY v'v>T 7{Aέ.,̂r=*f *Qxdw'`V@ Gƿ棞Km{V.d9+`b|:g2=%Ǜ/BmVC.L{w>zYQ0wŲlg^\ko>"ۇVx:?Qo +c؋1.qZ{Ɓ6C&im\#f>o2/r/l6kqiO{[rp҇R}brE +1mu0~[2Xtч-TbW~ǖ;meq3d-yɟ?R.TIb%ݦgY+ Du}ڻ=5S$Vk&zʴ=]=htoGX,2_޳Oωg*QYJT=oWWGjCUw.iWb Neۻf4>we8&]8MH,W?e9?e5iQ쀄˛+ TtU~1{`_y1h>q žËm^="㵔7Wb/UCl4,U39\UiT 9\}w%Ai:yȵtܭy{k4Frկ*`r0nW,ίle6'=25R(1\ί=`˄bRsˣA|<`amg_֯4E=C/XzHgH hl^HG4ڗW|r􂅜-kg/X@8Yx?` B4Zxsei+?&@L`9윆)96G?{Bd1@x.;}ZVlWe9>+=1}8R@=9u 7Lvί/|< k畈A,l?6zw  Fv]t;AWͨн_ikwY +v| p5!)ެ_(cթm8iƑOW!)Zcێ"Ηe+ۈʲInåi6V$JyWQ\aTBv|WL2'(k#_{ۆӒNEMaU!<`zZÈtz}:ӻ{2N?RC&u^;: #Ӎz ~{֙s>3fX@t:TIo۾B:)"Mڛ i3=oR^ádlz2}4=m:m)sQt;.(^^x~ۈ9ú9)ܛkԏnP0IZGn'6Ed6NқRnflV|->Ufa$.kOe\@ -G/϶-۾/qa։/`<:NBd,#'xrv+R$SwMf[l{lX ;ŶyMeU32l->L2 @1I)ļTn'L#-@n'LP`敍y8acNB&pGSj+Lo|`N8 O4 %ˉd:e9r%I W٥W{s1. wTbP4^xذe9 +G78oKBt^'EmzIF&KXe!II"M4֥"Drm/Me3,ߍ7_3 +=U YD[".s^Z)& 4rS0(50x&6T0)o6JlL('F 0%׷ ﺝ0mcu\ }ZRUn(Pb! ezү oڑN+Ey(4+΀2yxb~E2I3.ٺfSs.3SuVggOOLɟ!cqeC\R)paĔVM o +$ϮgH( ?ζD:oLF@ΗñIb+d64M /ME+P2 RAV49_\2P6ΧexT7CgDɥsiѦ +z>&jkҷϥY}^A +lWKl3K)᩼yXAA a[Wvݎ]fTɱS2:*;-%+]ێvқdDZmsZN$I= &;e2e0˪ƒm7~HS6-&"֛"wɷsmE=MD+ vƞ&މtܖ\GҍJ.ȔN\-"ZULe9%އnC9j=!QZBf%_ml!,Ů o1zಷ}!oٮG+jl]^KxZba9mHx?OBJgߙ ڮ-6< {yeslvO`6"<59K63+.!I͏O#B/%4o#B#FfWq[! B0A)4 $$ +tֈ*H̨k 6/] v E(YUm@{θ/ ׷Dc/6HugSj&-|y ^aDRf_NO +6S/R¿cT ?0hpj4 M͂//>3ı,Wr[y42i!:޼ec/h%ؠoAb7\~}Y2Pg-N:IG!U*@ܯUFg N9nS@!n h?[(voK@Rbv{J6Vy4N/G@j,#@@L:6}s8ZFrT0EX`Cɂ2{8;# Pn'@`բ\ޜgo'@ I/Hhu9N`u(*< r: ]oF|Vumu'o6J5g/@Q9yWmHr rU9&4QQx31&01ڪ Г) +9<t4)}gJ A+y6q2^~@E6䜁J})"ڰzO?(Z[h05WEWRX|xsӇĐv@vP@;|y\S9( +v3}hw!\y;y:RpiwG-FQ$>AiO; f|8@iy +6QdDZ$]w']ZsIߑ{Q j + ݟ&xv~ q(/jESSyQrlx}7.?."R7Z}EsD}iI҆@`?w7ට׋93t{GMmO?qJ=nj㯚E7Zy_(۝gYj۪f+}T $J,cۏoޛ MpWX) oqs]p| +TR͎%^=M]oӷjo U.7e樟ooTŀYl_pܺ6o&n@nﶞBr[O6аsCZicWm6~UDF6[=BhZ=՚^vTXH-g٦ZG.-u3 ,Y=TDht7|V%73sdKY[|7+Wnqq +-j~0>f{ːYe=O2U5[ɲu͒l˹n'XdU1a2C/Tebʝ0@N|l+14(=djzui%,`}v{ ߬EEeU']:#ܼBb4꛻WE( k#"#Miu)vBC/.F/O׹eP()#pcqAW͸X.@)%C!&.,4@X@֗-BZs`xi/-c%gemLlY2-i\Klr/*y^f(zx]lvbwU,Р8C+xcj∐J۪|UQe"}}2@x.8D3m? :{[yOd!0"b{MEvh[L)W7g)f!5Mּ}G4 +uh&5ET!KT,~_O\.m51ށAhcmtiH/z;;QKcۆN^}@Q/x7 vX˵)̄GT&Md/6-O'}l0n+jmT5(l>=mԗrA z8 Qu58IuP]hI5_&H k28tFw[ roH*1;Lo~Gcin#SFdRK{352)_2Ůuys.b۱h@2*%џhE R yTߨ>i:@n!͆d 7!Tr+ng! /C!1~&okj>]Igz8 Yѐ,H0bϗO?Yv?Âm|4Rz=}ˋGOKZ%6={d,_~(K Ӟ/ӕCǻʷ$H/?x4r4inx켎N[r_%,O|,Lto wNQN 4,s/xѯq((۝S6))#oޗ Zownb@6WgDyXp=/7e[S`ӭ8c!LjJ +7MA'K( ۉ3_1 h_2) :Y%,iC:Oq7D/G]_Nb=@ $k5sכ"D"ߌ\EK /&46v'[xfc䑊chsi2JLI:5e99K)`9ˁS vOj3yӰ1Cx\DB( <ޗ`zb^<ʄc*fZGѹ@P0!Ӛ+5+ v:{_P;>whK90%ud`]թO.ѩa"ۊJ + LHxNb0,sjnWEaV֜9)DtM(6gQ{*hK4{U6^DI-1JصME˯sN +V4C}H~s#W0h.sZD&?B:?UXiZ m0!NBwcPv@ +TbOd[3FՊ+=DRי+u3Ϝ[)w@3x8JhL-hZG(uA) x%f }ZQKѹa7 Cjb.uv4v[XVb:R X| 8Eu˅WS͜7Mm:B(:7RfYYWG:gxsc¢Y@vM}*0^Fo +# ܄zMъYV!:{ac=ؗ5}y9eS,v"88@flf"jQ̕ED,Cp0dZђmM} K\qs:A"LeY5̶ׁs8ey+¥oq$Ґl;AdMwU~j+l.фeuVQ1ΐ–*1Ѓ,: 5$M̼ayhSZA)ex$@V9PGCc.huzE^8P0L8Or7A|ʯ͸ (/z7=m+J8U Ĥ@:|=20UNXD;e)+SkPg/ΡUE³Nyuy(dqI#z 7Efb'ZKN}K\j*h@>7,-k +.E[Afx^nFVQIh86|ƟcyϲR6icTPqtaijI#zkVRsa=?Cʤ%8L\{47T6^qj}h"JM`V%qmN=$@F!DFK%hT?$x!P1W^Sy/t b +BZB6 36tB(qj_JϑG/B'e~5~+եL%NglfHtng[̙6h>8ה)3a'ё0#z!'Luѷ)';f[4 uTʪL +&E"1@Ys'8˹B䮴jk_]%W_6J IRZ,u B&9 V5Wp9K[[[ZFVs ۩]M{mP?4|yI"lCN~|Jo}Q|S}hb(?y,1h]aŋ M!wH:PJq'!He1vumel~MO!=!?ΑqKGK +3 zOPBpc'Oj%`E6!h2&Q&ufv"b;ח]*\HhEi6v;z=[z-9#v-" +%MGZ`v;)T <k"AZT)}R5%gD (T!!=c_=fdpWBFD-݅4R0;J3J``\IV3km{e?Xu6WĄi TLQ-?SH_ˢDmEW2&,ނA.clӌӹ]p=Ѱ1Bz&AΑ ]!2yg];\Hy> s/ ø%46©hbj2.޾9l5-tZ7g:(q*m:VBYw}.C,'3D u6kSaT<(Y~2RC&ZI!s* 3G%Vi dԔC_5=z?ap*:=b_ PH& c:NmɽT`$ǖ *3maWBѰ &f)W̯F nyh8$Ա!k)la iFMkG#$H4VpTL{׼RRlPA ӎe8ڭ;ؼĚ'5ch5WL^{̢3|XD: X {bY-[|ܱm9.3 }cFԂ5$[bO8J 2챾 6DZsrQFNa)KfeN>SLLV6^9Q4"9Kf$-ܑkxaWk,uv e%Ѳm#9sY229dd< 5va͹#d@;זfe4{ uWIk_!}E)oV3!ORfFx \S) S/"yORRcjY;0*fDB (&6:%!G~/۹ 2g~%LIۿ ,1 Y^.Y:A+{/$HvX>ƕ9^AR,(Yβ! =5*݈vrɫ5i1nmeц̝Cns  ➿|ϯVZ`2~ o^LjQl"  Z+PG;6{F{ߔy=n,ߔs(_,uك,% 2as+P54$1Odkc#iIwntLىTsnCr!+>-,]r@!)\ +^C19+lIoy +4FDbe =;~[pp5WaBqrd (0]e/~1vm^젅@'U G8fSpud< tC- xm~l\E_#@ 8tSFaD/3vqaȸBp%36 J(m瘘q̰G6%|o60RkFJX,wfڢ /x N#;ٗ>qz<=J Kݒ9ߏoiA8CMuW.^{Q/YMsޅl~Y/˒# $Jit65Sև M(F:-1z9 EQ(cH&4Cl~\; +bP~lhґ Z#;[ ͊pm/,:%f'4h5H͋G,NC:l؇(5ۙFh +Bj hP3N +dM#/P\p7DHq F +4| gJyk52=c +{n=qXF$_q[V(Ʀ@e}CUz =PitSAfɴ ]MzT4=ۻvMM|)i`XXIc9!r;Iٱ\RfWt*_Q#-`m` 6ъ]W?.HNF:='K,M{=1b|FiLzhRSئxm`uyΦ~#d8֍yhi)Z2(.kTˏ/IcXU]pz:uiP/\b8WerP\bs:L!;ju?֒Qb2~),h|K:-IoX6Vu @)YhjXX, e8 8;Nbߊj"^pCH4\案,Z%:/dXu몐C.:G2J/BlQ GhXbAE@ OD\?`FsK_` ̀<"v=pzQKᕗ/BI4Vt6qL5?P`[SwҔ~PͰ-]%xLQ.-]UBҘq*SE I& +q IqԍzCPt+eBSABOz!yI/D"]R)pQ;Y AFRW  @mHGcc5RH|hv }9KIN0nA-دVH K },x8y)\&I]3\.g& wDD`;aiL; +mR!gotF:U DZؼ:;˾#[_sgb@.L?DCbK43o/]jU&ӒHzﲂ>?=Y+#dK/rx+ ,^k=_,JP4g%hS_%HMxU ഗgfIPIЃJ8\x̀?mq?̺۫x[ul7:2߯T +Bn/\qgXSpҁ ܄[ 4{|?M0"q^F 抌8{^%f'}QiXom=0缊ױ,o=b%zr̀ܶC-S "=ܨ5yhaJ/ҕEbd>A5{tC.rudp 뛨:Q.]ak]ݤU^Wh/Iy`^#(8yr-X06񎖾[`YK"LW*X7k/-%A(` +3@ pa3a.@${.0N {W6Q:4{B8Gyd&.Z!hEdE "<-5)D%jAL/rU ^b9" ߏb`T)o,/r:uDƽvp`0-9 elO%=Q)@DQ9o]2ywz)1q'9I`CX#C45JFklm]N$:'^W ]G#&p6zJl*#??Jwp! wHA ¹sZ'B8x}=HkrDԋpo) '6'Q*J@'r2X + -g{BHkKi&-Ez\ %@O9GӤ}5>>}C=qu0ˁ_wwr6EqTjD;=:7nmai_uVi9Ic|ǜH0QF~T&BqW丹KCCȏ1ѵ6<%;>6Sb#B pf:IΜp}A⇸"#X=9/9Rt D19M=Q\+,w(M@mr՜דșRS$\;I-zkpTqiVZ?|+{ ڱv"@`ZPߤ"[yAt8#H¤HU\{}G\R&aaVYRBNN_jо5KKUo?k) 3a;֩6{"ͽZեo9X}N+)$gϺ:(>ǽWJ odQDe-!ʽ(g&8mW@2zIǸkQemG* {=E8k\a9L J 4JZCf_xxHbפpQl8Kqe@}eO&lc= +fsii1v)\ 'ÿ^KSD'g,pD%CP A:"DRo`g{QuJzj*9]05ԃ i.2 "|\ 6+s=<sT3cM4rHc+Ňnؕ;jdRZnn B[a$ʪTwJToo~cZ;|Qfp998+Cru/F|"P 4lt+ 5Ab|Mj7RRhg!/7|]dr\AQSjo<?mޚXGdUd~N~]rpQb?¬M5ucIw)7f^1yga\oq {Ѵi{c _"[Kw=PC6* +x=1F_CvcRhd-gT'jm$+9/SH_W9ToĂVB +2 /`0@xTJxgTH/ϠLC4}V0 $$kX0\,ZXWk %I!@LwEHP\η>LK٫uv5x|B05Nޏ[4`BDIL9^0t +?dd4d|n:DtN߱q-GZN)HTk) W04JU* fZ 﫻Mֱ8sVu5O5-Dzᣌ(vP9)T6Tx.'/-/w$`ok՝Pv0(0^!uƂi +zWi+&## mQ2  S8=b8 m؅3@r^ŝ]=䒣$:Q׾@^014~]ԃDi l]%'U`0I,#;uG=r%w|0[t@'G*w63OuZpʁhQCW +> ԡ3˭l7I|m +JT ) )F^dEIRU=b9_EJ#C1#EzөqB(Hs9ԢMΔ(%7nzF+qbr6b؂Mλ0 !܊@ɍT:/M +ra5qQ9!J)7ԫHwSe{\g* gIᠺK`f(MH~ KT* e~ ı FӔ@ !Bg^& +ux5x EZ xY#4!A,# |"u5n#7 JAU}r)^[xbU=13e +OJCDA BG8Z;0J(N5䝖9b-'n;Jj׫# ^Fw"ʾ^%SsX)HW=lTS[D9Pt2 D2Ca"јiK&Fg`])qLKVa!%u l){$9.`1b sߥPx!(=ٗ#*`זej1&0Շ"PA:ɘOjgTϱ%BSEےװc#PΌSdlb|?eV&NƣC̽S#(F7YyQ3be={(0JmQ9+_\,UkZptw2l#NNXPcՄ72.Nw;[;)?+aQKHAWO=.fJ Xcy]2)/e !$2֯$gX4P/28 +\Ӎ{U0]n8Sݏ *IT'bS,C#ߢ =lm5t=RO5 [=8xK n\F[Z`(o%o+=|q,Ӏ7~1n:Y 3ڵ;@(݋~U0Fc.6@1UXfa9a(@>p w^oD&bp2ɋ.qc&ˢkZ)INV#MuU7(XÅ#NтH~D{Rr2֠ghjկd(qP 1@N1:m6U[2=닿c)s!ș(rw +4yeMrj5@qhcP/Pj(!C.SI*\d 4oe.PسW,݆ɘ%V=epnWg"2+p 9j\eD!i5ӞVP#z JT -5vE4Lft{V,cnwE* 6l9#SSh9$VXC\b) ۪`Y[/Yu 9YA\šnb'jI h/i/6D@iJC@%Z=VĉæK?{CWC` W\=(G%B[JI;9UzٰH҈[bInh`Mw;ȬyUK͓ ۸p^-.+D|JINw !жSʝuZ\1@yc!M2 +xطh^wCe [= +ɏW)YMDRyD$Ujsn$y.f߱Y/f&`xq-dӫ|sM`\ u +P{''"@l'D m +L"ќ mاEm=NFI +w(!Mj챙}ǜc-~SX@ܯrN9f;ЃPF/[vHlzmj܉`v<´B'Tdz s{b/K~'qwsT!]OL~2#eȓ v"Kb^LOF(wF H-Tԋ<$F؂ĽĴ\Pn)e}S jX2|()F'`2B{ /&O*k\6cͻ-Rpa+6K*lIQ\"2BSV^bi(aϳ% +M\V)!d!B'h|ԍ(B +,MQִ*R4!M,K x !rH<$@ H Li"S4zc9);{ړ]\0?])%{}ZIa9w@j 잤"v* Xl[B}!֦^uGT77hœޙ%ǜ2n QW7)7Ȏ,5mpa\~EOxzOM1gfFL]b$d`ثN[|[3LA5Pkcdh SDʂ6L]PGp rZu"9@^M A!Wuu1EPI7" Hc`Tv2X6&c¸ė-5Gf{%S=AUrKҰ5X-vlrRu*zH +e_iZ0{ +;kAje_) 'E\3P3q7BzJo4K[k)hk3$':oIQ(!r\!:!L[1^x\1@ +M!"(cT|˱H {#K=?Cz~I%Yy„f+K<lyC< Z쁎M+oLjFSWU~!+].TrZaՇJ79+A-Rryf4?!R)S:c'"fllxmBb!بÉe4("rRt.D1rG}$?_uYgKH:sSIpfBV҄5[e!d^L1`6^J̛,n->.=X=e10&Rg>b--`|;`>40VIA(KHݣ+1iڥWWy ++Ib˩;B}Jh;O/ i~eRBdrR8cHA=X$4Y\L>("D7y+.mT>,Hނ<$#ju{"eɷp`a%׌ƜAOD7=X&޹n$O.qb{؃D iRoRl6Cկ @YU.`Qd6`{e""R>AiWsXAm2^D!9jrZ@&QHx|`7%_Jx(z)~,ȭ>vw5{Vv|s8`Q4h[lO[A( dO_ijFA^΃+2݋Mu5]D endstream endobj 26 0 obj <>stream +hFux(cŻ,ыqyh +.GQSC +ѫ!dPPfD2ӍWUЈw[H8K{hzH#0'V%s_DnQ|?$D *#U..yr(mȺּqmvU~WMfd)~u[%ggKe$pKN!p1Wﲩu͎>?ţD^T_ߟߨ>_#Mn1أ#ܤ#0sJ\ +Tct9\\"$H"=lRW| 9 iŃS] lB*.=R|>U=h \<pᤗLxt!\>GF$ H/$b51h3Ov~?`Co9K7-,K"j5w])r#[IC ~DS[Esx#U1tuEg:ԁsEzv` +d] n*T_:Je)CI[uŅeZ5KZ"@R]ZVw{NhA:tW?(r9$ӂ7y^MӖKboMh +v9ٛ+?M)dD_uO瑒90{q8v_m#DdӤf"qdTPv\ud4|*b/K=O769X7+!m)M0%)$Ԭ~xuٮPOz ~c"8h; Y%l&f;G6lI{~hb iyCuKۖ"`{u c:6|*PK!tʧ !2O&tUwa7Cߣ n &QUa*3w$W߯XlmA +i;=&GaށRµ%܊%ރfGjǯ5} Xa@vuS +ᐰ1PWy-_C}7t`TdҘn6#-#*2^Z=qlȿi3砺L!OfA +͍M-8ŐKƍ)I;JΑ}\.=]տE~+4PKUWmpqKAJD'Cu86 ^J'Vq}bH0RVXGJy +{L-,;B/;@=W3^RMKy1Mצ* ۏgGoT/9lFNR E]RWWЯ-S@}Џ(;b!g)YéԜ)t&5?o7ﳐ̘jL%I-,űw,>ʗF+@s-I3iLŎ܂Xmܾpąo<(ry>v>gqwv2P:z,)v\Cn%n&܏A@Ra;6qSu Bo6 _ʤp5}NnpGaYInvA@Φ9_hh7!Hmz($*PP|) MޏHua]o/NS@BmdvK/E) +yu^඗覙 .p0ir0dUzQ`(lY; 1x#p63Y4]0ʤnId̃+23ҟ)*e~j6꯳k)?s3Ik2ih{5~'Z;F#Ě:/ɭ2ЇkAɟ!g'wG[/d[^ 23[-%} ȗHKݰ_ +~)>Ct<8 jΰ(H*ֶv͎vP$G.d~UWKc +|? +oHDiQ`)Y_awxupKTff>">B5 8Ԝ.ݗFcQ>`EjR; Bg1|RmCe!Vodz=$.a)lm#(RRA7s^vۄi/9X +)׋ͬ`MA5}8=  [/4`C* )_K)ч4^a@7Wvܱg\J䙺@a=[m 6Z|pE0X,k2YԞ&Gi\?.;|vX[x= +E1_ :Pv:k;W/$M;~ZF"E K嶄5;vaCTn5WXA] m$hv 95B@\"pm{ldEA{0-%C6GwĞՕ؏:\-Q!P~`x6<QRaD6AηgZ$x0Ym]>p :X2X+y5*Uռb9IcR B&'QtbTj{PQi0 CH]I+Ps= ˫!VYby˓{\7Ce )!9P?ڟ&ܾv4&i.&'aY5*,o2cBC,ޛ_ғ<Òk\хԀّV H6+Lb{*S}G-I"SjztSM2yR=Xa?cbg#/l$ow\crb!I5޽%xL^e(DBJsUGoF2ILn]譒6%'݃\}q$UCυ(RYdKMU"!s|PfBC+UQJi#_7T 'ЎIV :Rm^\ujiKCV+|q$e4aaAY#;pipR=Hh3z-;=YZ+I YK(uڊHd¼١o"&aQ ڑk!@ҟMCT "qpgG.7G2Zz`o ;OK9Apj?19-iP={ 0+|HQwP9o3y`Ҏ wz *xWS;]w(ˠr.zڦ ^+&} OP%q9@IPE,Nרw4硦I1S@v`BbRn%Z@0'эHCaf`XƢ_D*?x{(j~27 121X$3 O*zQBHrPL(Ӑm p |Zo2i(-qgd`=r$&E,ɉQS{%P[(#N7l F!գ bbJ!F携ZW* VŒ#k80\H!Q%8L4ນ\ 47K+헓:P%_ZPLg)YVݸ}o#YN@O5U"w(9=RAdO5zF1n>I-,T!3B76cp<@!5Y{/H4/z}I&b鹯jI-Q2*ôH8KJ}B8= k, Yl"ZCa2Lw[r~"!2nfj)-5HcJg]m{u&恹;Q,fڹ`-~gb+/C&L]r~‘ʕP M܄eSU=CkC{oT\J%U(d!aIn;W/9(W{Kh;x!# #\W(C0Kþ#sYSp@hc"/P۪8.;)&W\CE(l%9 +*sEKV3Q).I/i +|ĥdlVFf6\n=|Gy'45%tX;wnb$(.&:n *r1U"K%!H.^*ݳac?]1N#29NҼ‰"/!ds4unxr A T:@mAv"{.tkuMUP(2~oq؂ 5J:jVgH4xDlSĞQ۸Uسt>%(SIJÍ6O\(&BZaE @3RzĮ")CbJS6ݸ?`*jJJ#䀷 +̻t}7݁ᑯ-xɺsUlw  HX a?=7z-#jnFC,0 +8A`b0G`K/R1)w\Sy>K +bwd~k[ Pq VyAt1$DHR}Pcn⒟j@[3w,)S3-۪ʑ!?qNh@=zP̶ |OgVKa/G-D^ 9~8?ؕO x*L+(&q5X'wU_R:(G(5NJ9Yt@?~il?ǵj`I2XZ>YLoaKPpo&EK{\$鶷Q;<4!^OEF?_)0ՒK"XU~HsEgnpv! Gq nlWµK`n ܠ|Yg[J+0JkP 5{lDM_%]7<صʡ8R߃b #+h\Sbc}ΕN7،Y%n({s.Bݷ2L@}v0 `J%$1fACfP1 Qo$ofk9KR ̠ab'i>eiq>l04k7GȎ~pwh/_.,{sʛ4Y 鰽~vAq~9'`wn%U E%$#mw!q>V вO8WLE (\,?!KdƁ/Y+A OZͨ~([ +XͣJF ePlIHC()PV>}ciuT +ʉ.1&t 'Lɴ-Ety/$Y@`k?R=T~( ,TmѪʯF{Ԭk&5T~+*$upqX+|G-CY"G3w/_d!/PɄr9m'2G +B)1aj^($ !@*%(OGWFL[RM-;=J TD2zh,|y +/P+Hx!v`^cAЀ%P[#Ģg 36:S9Fpa7Xo ' 0ako%m<\C=;ƃ6krƬ 񊬪0C301N<-}|<(RR+~!Ȇ렰"o:5K<$K:0FEBAA}^xÍW|@L`3RÓBG8_v-> ^ H4=3(S$D롪fHI6Nľ_^OOWDvA2ܑlх9!A'*UGqF^{C!b{ϩnRlCI9$րfy3Bl!E)M;@iiD$`S0vr-I@ek2lBs4^%xUBRd,VjP·\$N.K-2Oe-VB (XHBH9_ag8_[5M՞ȤP6ܞe-!xjI m^uMN52Q΋%NB9$>㼬+*jCN/K^IW g( + lO͌(e \l<}K.&e' wqx{`uJh|5ǶTK?6/0]KMrv$1` 7idx +Ri/}R fA噀.sDX0(&uґwMhb,F̻yF s4A:ɥ 1GJC6{ 2ʜ!Ez6Kꏑ:v -͚d*fk/p#0m *GdjF*%({Q4 8Ƶ[k,v* t٦â̧ol^` CBTc|Wā:`ԉ^s0fߗFj(0j!дD420PR&YxOC@jIz4@"i;Iդg-7R(HrI逮 N QOSULlj<%!t_@N$@('7d_;=f&T*%Ca%E%SXE匨LSvn_ImkC?.m. ~X?`Z\O +^t|v%ugK*k8#s tt] +Rl䰊 _CV8Cᤴ:h%qu?__0CQZ$MwV?љւE{їn@ޥyb݈.rDPc3mwܢfT>A)dk0/G{29Wܷ&n= +ZhT"ہLOszqe_Y/  *ɰ-GPQwݾ;9HĪ7 X9DjՔ훹2̓¨I\sl<.Y,w"{ΗS +ؿ\/r] XROjd:4*pxu}TQϢ@א\-G^ bCXBô8L) ;(,\5вw!`o^i,u(eB +ZԂ'"Ԙd n g$̂ȄDP;17-RJYz$1D,~%Fi⑭A]3ܡ?7T/_1$"98*⮴_r{_WK88D@J`+x?gWs\߯YPbCYv~l'mˢ$D]JCR\RN +xmsG"IC%v7-Edဧd$ ʟO|eH%(g9";A}$f;dogDo"{ߐ$ +T Lq?DG६׶(#%}î_UF=jo?K#kELlL@>T@# +1{Ű{I|ҫ1͆Uң J0jΣF!Tk|(Š$RLg͈7 zS=*$u*@ %էiu䡁]ڋ I6(SY)b;w<|3pHW cU0&9~H2FLTLFTZ)c'M{HHm=+u!? $J$"f&G 6>[w? !Q\QE')TX^7pHz;@w[$4Jr{ŷ6wrMk-HE'- )Z+)nRf^Zj5ɠ%bFr:L :if=-Xޛ$NQ`?uu?j֎ $Z,42t`M@zz}U]BeNG1vmVU=ˣoI,4?F"uW?5@ ],='8zcbN'}}YM!u]8SVqF\Z21DQ a^IP; WfX9SʋI1ō`ygǐ=U|{x;gx9铧- +)A&Uv4;+I2\a9N|q I@de]y]VnWI ' ^쾉gtfDZ)Vċ!Lwzln +[gѐT QAf@I(E_+,9=q3K΀87zU_ C1א%9i ʁX+/2뼔'cO]xŘM,*)tqV%&0ij 2qd4?3Ե kPޯL}~34+S`v +ȝYRl4w4% ySG%uz+߁҆W'1 q>n\Qaر'E8◙,G2=x5C"`_qeD~IK"fm)'5}n'5kl#e^tˏJ0ebŧG<.F8(]!3u<yF Ug.FN܆]16RV +@V빃.+H({T;f$[Pi/]&$U׮eU-#c J'v@T2XPviQ4z &=B} ʋ Q﷟M-Il?$A޹.C{T>85^F//Vvl&Tgs&E6 +!]Ԁ!Kn1%>?^.O4ChcƙtOaYÁɦ6et,S.%Bd剅ap~$,ݻdf"aGIP"w-< +Vk/MKBN@"s:T}ܕLS-faG~IGl8д$yi~~r +ݓVN6c{om~اzXf?dT&/R.b_%~5EZ`FSUbp;'q(uɲ,~TTo2MA-$DrX{,\Ҝ8-9}%lCfڙ%}z8:OboG$2]ΐd'A>Ȅjp`aT8b[6|`َMA;> +ET^W?юs]g'P~0qoaGꦀlRA3`IV|Fp,N<J!HshNDjͰRWj:cJEyHt$Ԋ"?ïMe ~3Lb PM{ +E]<5R幈ZSQՖwJXsИe|[cI(X tJ5'CAfPP [l, 3a,C Q| A9'%6-C0lM!YQ}QM7vU׳xcX}u9IbKNtոTW+A}áA2TA* ,Pbf`w/r֪̒\/B++ꥃځ$,[rR#xlIk)gmӞ |6 O#~ 8CGf&X*i-Hb4{fLM3H|zij3ّ1CaT-'DnǛ|c"][+?2N`HOt ץ3ќʆk4%MB*7֛} Jpy'jyn$sf*JQ a!jz1n r@3 &H q@$@3ULiS3ڗ/um_K-ݟXi{ +]+nah2A8t$[en2 BK%.kg(PIӁn*_xEұ W )?uɶCZdJu|^(8fQwGO%bR 2Qe'4)Chv,ě/C( C mbp @"~J%EMHctKTxhxN`dC +Hpٽ QW`Kع~¥ƾj!=fZDKOLY88ntqZ9m-knj^%L_vH) Rv$~_ʗ ׀ޖT@KL_ho/<%XE> f埭[M)b]LnC@ 7"AdžbqXjv|['6i1"qvQK+2˦ +BV +40[ J# 0 .t:ܫ)s Jt1&}ZuIfD*K)ME4>b&/*'☣lX
s.5pu;Y1R[y\{dV\0- 1:MPma~{Mo Y_`V$mɇp ++f]uײc ,"tPYM]Yʬn@y_:ph{]1J`cC!ֽ+J5ʒ{62@+`F;x@ۙ+6"!U@eԲdw +.;m^K:VJev|8{iA=_̣jxD,j)[KeIG67L-L3nޑBr`&SRW>`bnb'ThZq:cJ])(-PtIqn_Ot:DX)pMLF83iVj(oj}"qBL"6(+)V.aDW(h^TIB JډO~bcng#ܔrhT~]j|A({ڭ #Nb*.w2ETͨy<ɳnNbĊNmOVj=w֞9KUu+hjʅ`J⬆yUR@E$V<|nV/•Ϋ\Xu2[O8=@uiɆ}Vjp'RCRFXkﰠ"m%IQ-W("Qa +=G}:bEWa"n#2+5x!Kxfs?r9#:ň*Bʴ~0Ywlazx d<f[k9YV!F 8nq:@BogwT/ovsAK ʣ*mH& T>/FR|0bH{:Wߵry9S"s%/:()uK*dtg-W&=W`}̭r ܦҥ| 8J( ̏$R +$u,u^]yB}jB(b!Tn[} xBz5zX.ϸ* +CNőudY~"A܎[]iGدs:Dwأ &[eͶ@|=>h<;8^W0]ቼ9h(Qm()9 BEut&OCrDp_kF&L/I6S4eDz-+nK;o[)N3)o?Oo7o?7?~W~?wo_ =~'w?t}O_?n7ww?[j~|lS~ x͇WW귿Sο?ovʻ̑#㷿m_\oRq+/u{ko_W_6Ibq"~{!yYb 퇓L9o_$pw rIXZEYu%d;D!KG Zį Qû{^_}! ;=SE#*j|э7q|ظ||~su`HsXq*dyr#d ~ +wGJ?uBqʛ~pRqNtsx`3wD9~FWϗ|X%H )8,*eZ=/'` ^ |WC+C4ƙ 58A8-?=eh pv{jJ4x~'􍞍D<_/b9وN4ӈ[ st:<&3>Ύ+ GZ< +2ʖ?O+h\IiΩ|D4q ĝz En. Fˢq8X7y_ Qx^xb+U9+=#-uij^ +NPO5Ϗw'xyibYqiTUNSW [6^> k|ncw^A=;~f<8Wk'1{`jqĸrN %Kgh1C[ _ @U>0٫zvdU/55daL373Zk;ss;I?}ICco|>b 9<+9 {QU+LDi1֕8o~ξ1()w^A`ިE^k\"ƙOA I:o=!'1 5rN< +HٯUO*P\A1&12\3H=X8i+x)ΉFE[#*M& '8sŁ3h( _7`+3L76s>s\?)zXqa ~W]^}bm6_m{%q8CWϛw(;_w!T4kFعz7tsyi U||^_1c8|e79yL%zs#'qS4"2v(_ +ԝ<'ڮ◰;F8"xHD#hl2cѲohDcobfSkzij'#P_~- j)Y{Uc_?.׻95]9pz/aM4fј>l'YZNWl n9TpǷ &kjmB9mnbcl7;UF9|PhuA#)ϙJ?gs=ƹvD}'%WnV$[h9.7K73iO+4W+rNGNHŧ%z\ֽ\g >}DJ!oaBܾeEh^ y)϶ʟKcȠdg}|%ϔZ(4휠?DdPN^s;>2N$˒["ª +p=k}+ɸ|\@zyظgA)9a;)ۻ:2=zyDL]2,vO(R;A:Cɷ6X4^H_2|1;J j>ypc.o$lONPy邲3crx3{Oc ly/WS (@dڟ/R&V5VFDU+bIgX=x_9Zjفb~LNPW_$lFd/ +˵w<Ӳxv}ˍٜ#yVmą P"n#{yEL5q~yYk[Wl'Z։FLJAԒnrc;  LJ}/ o :^FMgcN4QŤu Z8YryLEWR6"쨈O{u裝v{sWzYcjGXep4SF;\Nl (l|V;.kn,R\h#$(|HgW}G'oyZ)g:kW{[FI)_"N ]9 u-f|6.,ˏ##kq.9/,CS̬S`؞}0jMAK)Ol<)೾ )O}`F㨙.( Y#f>1`?_؅Iگ^Y9аcOQߥl_!+O سc|2Tc\]40Yj%'w;6~4`AP':nZ7's4 oF{k kE.@Qys^9zoyhfSoQŬ}YKɾbFEۈ)NrSvO3hgnH0/|x9@try9g/4ڍ̜j PU8H3 +"w*iiVk.0x^iyȭ-?+<IW ֿ udew]q[/йfc;1o}v6%>69dN(<V{'n7{?gsԅGE˷<$fGg3}Ŵg@9끷wqs20%{w ެZb09cV-.(?tVghgKT!l}i9`PxR,w*;:+#y fF+y#X NNsͧp+Ϲ9|Z<\$UN4A +E!jYES̩2} |FvL4=b :Hrn<]½+u/ZS0YFZDQ$i8MMjq_Q}QO3S O ߍQB30\R n(23 W9\!ٕ-͑''Fz]}9^տSA6'"#|2EĉM Oj'gE*S/AK$ s-u]gA3O\IchHzWc@X,, u?_1\jL +I뙪{}q%ڕtu2 d}ؕ29ۙ E=\{բmmfX=AC_jG^W~r'/F=MOLyb1Ȭ+skM*CaO姜p9}(g5WR͚+Y65nq"QQ x61Ո|JN=*U3{]~K&֥ḽTc04#y۽1+* f>r\×8՜F>RǞC^{ԨOw{yaRȑ?@9hZjvqW't[w#ժ'rC7.~k;f6g:A ΓBVoD=d:6sF98"%D.p?D\ +]Ɖ#e[3.C|I:Ʈ+{*h泍ƽQ8th;״6E|PYON䏽ڕC~y+ZY]m4WΗyϼvzwkƔ됕ƉhȀI 2xGh\'i9UvrOj^4]zw8uZyH}hHa_sU6aq'+\'x?^l_'ONWlɵ5#XrB9ih~h;Nq1HcɞP^+dv6$m}ngD~W"l/0'Oczʅ|$5?4+! :)I,#",s=n\+aW,ܫHcgOeZ>=PD5j)ܠOԺѳא~=TCKz +}y·8A + P܋EΠo=_ש-@ +ٶg˙ IS,9U;(OkYN)2w,lA9ܘ;1NPj\s'fAcs̼HuhM:9oL)A=._g}sIm3K$lLSBi<2=|&~>|6TI_˾9U3D3xwBA8T!c 8HrYgx'ȒCwhl9fkJn+?8T!dt G'xmLJ߄un5+$I.! JN|sإ;'י<",d*?nwוdW䥮@IY=SW*~V3U*V ^~™2E. '*]#V+N0F6gV&g Ȱd]\tN%#2ΚJ~I +/bH렽ƻhH o 'ȓH}}MۜąF` rRdJ4bCWT8 n|YpvE/p, GCu͚[Yя9Uצy]OQPߣ,_29I9=iNk Npbe r"^'Z/vئ Q, +\g'H(t'yo +/z_ +A{LXQ'xr^{E?6f}*nqZJ^؍xa  |r'YVߣ3G}#>gBIh*}۩(KK!U5qGWő4o{.FrqEF].~nwg=@ٞ߯@RZjU,|UBL^:xU[qH__xc9id"dMBF)F}=*A@?(Sc- "*S}U5jQI25ā*&f"kRn,lӇb6P0fj`>@(5 * +~Wf*Oz@fߧ +O IH _7pZpZh) G_JW7NЫfd~:8;X;L4d2+ +LK^„_VPL;XU$Fը)bս 4M=8D|XK*v fP( J&0cUQdkZDb*fPPGG |jQTi5UL#`HhBBx*2x_}ʄ #U23o /w"/f&%sՍYCxS58F(Xg*KU6 (lõTTW . n6|@M2vBTLQ4zv +TW9a&bh( +3&S/㭎Û75$DfVi FJeB 蘚è5jd-Jn͸QFFRME]Z +ex U9 J +h'PUɍLj,rtu5Hkh F 5h5D苾T%-S3S5k`5ve*mIlR9Iwv3hоf q/՗]ƒT`WyMՁpLP_k*bGa]8AZ6iz&Qo&̔ %UFPGIQo ʽp/!/S5LT(' e* 2T2S~Ѓ$Tc\Bi +EhJ ! +,[+z.*k[Ruؾ-̭>T:a+YpH d + F}K-?=q]jR`5)tF33C͔n ʣ(XQjl,\+SF_ƚ1|Ш+o'eve`"RTsvezFaߪۨ:0cpMf+L]E>*؎u[eb(rYF!,E3]δj/Q|#Fd⪌j8b)^UVt1& +jjbҗo~0R\Hc.M|^hNdF8\}3HbY$j P4ZC3bQ1M57>AmJE$K4ePUK#-S5 +)@ß;Дǀfqqje :T[̠Q8}@]P[MkMRoi nXP*Ə؁NDk w"a,kjG*pPz}}B.<+B,RS+ XLjNJdh;1D(Ǘd*K' Ȍ 4Lq1*:h" w6C#Z31L_(R(3!V4GakM,gXtԆ7Im F]4&}MT oZ*dh,V."JPLT¬oTfFMe7 獪53AOV6*qҾB؀f 2Uh43KUs5jeB'hD.Dt|h:MdgUzP^ #ԵXEoYZHKM?TqbJkuڨSO#@su)]h4ƌBH 8!.ŝWxUeGv&}Pc!j U_j"W׫˪kM]KbyOo(ȝ-7Ԝڢbth>(T!IOJbj;)5]"WL 5S#"p@?fX .;\bR"4DEjMJMl c.13c?(ri&⻙諪Iy$lH•BBJ +܁N-y{+5ybۯn4EɮrPTO`/^+s0 J=Vb5Չ:xMZ ◉F7c1rPW[6S: oX_fj`*l+>0եLUUDui,V3PRj7i XD#7r,W>| Raȵ,TIdLF#S#qbwQ_R3d* D:|o)DC0cB'QK)˚bmj3[%VxR>e8S h6GHŚ3&j+5*1U:OۧvPTb,͚R6ҭZ柺$n,VѓKY}Xoa VF+fm45F3 /lV'^.>mЁz}^A]3FA=/uiH]D@1QCՈC+5\Z_VZȋثQ,m R4QR?UVjd"tWMl&aϫ:Eoz.nvt.`7XdY曑H5JHSoÍFu<ʸQ@¾zu+3ygbZB9f1TepU+F?dMs\eo&d )ԁ!k5447)LS/>O* S;5vS_7WhYlgNJ/U 妿cx۟NqfuNJFO EDXEu^'`pLR'j֔YA]*0B G,, +<";U.'UE6U vڤ9鏵܄󐄍,Z5xZLфXPXRG j*U,Uo&$jND|CaCDn \PPal"f5RO}Q2SUVH':kՇbu }\cm]5 +%!j%mi ͌LjPa:UQ)̇E<̇e=Cn!/Nf="1Ssfl #>&w\C#١Cў9ĺ|JpoyIJa>4k,=$ W8 +G_]Uu&u/$)$3SIL`p9\\d>~;L#2#A oOIΆ"NŇN3,ds.C3^1C( \B4.n2KmÅgRG2ICA6lJtS7TtLD6(g48ԆGsrFπ9a<'S^a||t:(m$2 "y#XǚՖ3h.uc͸kmHl$Vr~r61p +AkޡB)fnZ:cC.:H X*Y.~{ :x 0Ȇ׆{ t@#~=Ϗ{  o!OhSaY!rWۆCzGuO…Gs^QC8A|h8exx!?ޟLGg2e3Q"yC3Cr){6d҉LK|b4>,{5RLlh^?L`t(< Cxx&8k4%.yL6"&g,^MjB}M<sSo*b&n*?dEr6 bz,:R>}6U?U4k%%?2à6HmzXX+7{g S}*}j+l 8 ]P~֤}? Q6P{@-l s曁8@GuH~}S1ѥQ4W)TNoNg˦dž#uiKa\oPxɇ5;t I e.y6K3@+v9F1ly „qS<#܃׆,D:*b?7`;C>x"p\Xx.,c < 6dtHf.:Pt$0lXh>aX +K&$k4 tUJ .f<'-Q6䐏>I3l<:s<:TytT$:at&\ǦM7MerF.YHɒ3\ڮ9l$h,|O#4*@IkDMlzM2d68g4M8L=t@eLZcHn"dz>IHH`l(7Ⱦh33\zC]؆~ Yy݌*8,$r.`Dz>tTo«FWp klxx6bR6éȢq Z\ropЩ; Lm& 4-n9Dt% +,۾Y#[nV5_r}lꯋ)؏ +2WLyt@2yX:c{&ml*B:}]9.@EŠR_Dd;ZRzjɃ1_0kXk`R!'S"10; , +X_dc0yc{V`>D4{_)j{& +N,b맂|bܨ:p5!'٭ Gxv`~ |LȾ0ȾƘ2<]48,p CCg<oa{]DdCXSl]7l-35&rM9ÄeϠa;gC::q{)hgH!{G9"WIe~ڂ dž}J#K!΁lցSXc=$t@H'g6(kq®³=iCܑMG\` 6X_`P$YhS|H{(}u~:dP;Zrh- ^n!l ;iac |ֶ qjӮhm{Oݢ2ap6q$4GRw&k{ ^_b'*:ޙwcM33* ]_qr'+Gw!K4 Aa|`b|8|(KW nQ6U0oP't0O."} &8i; +k#ԟDؔʇ l3E1g#^<Zd}=Ekn]pt)5A=裢sFT'Vǫ)7!Q!y#51鉮] ~!kCGb\]x`&03pt $w`Xk XǢ߇.lXQ. j{!UKN;kqAoHCκjä[=Il΄Gki=t@ +qK[O#NAQHv"#yn#c,Z atC:;a\`hFށl4hXW%OK蒳23"KlQ}Q|ˈ)x "\){ߑQi#?a +ߨǃ[=)jMb>Cx yH@-:Xe,멣illyLk!7G .p_%!Y ̧V`3[璥eĶ'+_  Μx[KE PE `'^%+ЅʉIȁlv=Y&yS?Sac ftL}* +4F]*=)Ej>K_0I0>F6B:슱| 7U|)wO`s"2v$)L ~-? :0 ||#AF"=9L^1OF>it%ljLN7(K!1 +THH KttRt̟wcK6=& 0_`' Ɇ˩Tb4Ld$举2yc:u *8o4l^9)z;+ooҋ&>sK£K ii$Z-/a$ox53 |v&wuTX805ȒLjt. *sɂSK# s;Q1֔2 +|z`l |X/$H߂L8<%bĄmj55Wt%ws d81[2(=O~=HIYGN"w@;$qNG!?9Ħ8$\ph> +|b y> {4x%N~p} A5_ | hL2wys ?tM陜#@'UWLR9AF?P >'KD%cEM3*\7w84]ވ{';$\Z,)x%ٔ'M Et:W}w%[`vh>` #r5kT1]?SemD3Ll|-Ͼ:`%S3b=К A)<&q I( 5Im_%T ܣ 1oC\>;{2'>"_ -a.tXT|K,ɣp| #ȉm\\_G82;|3Y;1iۿ~&oga[2T fd9sGŝa} _Hڄg|0_Ll0:'!y`r/fvʧaΤψ:O0Uxl)Y~Έ,8|\ Rc+'CLIBeźԽhY0b{`bgI x 1)&6;>OR l~:{c,8z߈!v ~5OLL؁^ c5+U~g3Ph٦3y^;VՈc>Wgl9omVwF^uۘLc{2NM ؆-8O_6o/~ :'tq)Dٔz=L\C+#,{B؇>PgzOP+*ܴ@r z0`AG <(u!@;!ǻsgC HwqH^<]trF-z.zlR46񈬝HbKd L`XqQV[BܓFv֓cp{^O؇W5N=>@8b; I ̡7LNA,֥;F@\ o8X3*=aHY^CσG*=pż-4u4Z2j ۏx%'2W )g Cy) ݔcI稁ce Nɻ#E|Im3!V\@V^7%ko|M]FU}rf[0!oyRS_c ^(>o37 ;s#~gZm  x).J8?1 wćN6א"Sn9O`.!=m-r~re6CF|ŏ9&%̰ΛJ!Gy(rH9 &'s8ँr +JAl)? O~9'=Jyk@)|o9> p!UBe~s >|=릵~)YFoK6n$kqC<,riH3IGDv{C쳏ΟI5zG=L5[+ 1e鍳;G6gdˋdRtX't91"nQ5 J>ZT\TRC;)53 kHPhIm}FP.h.vn뮅.Ą'n~MW\1>@`Q6K7j|ڜhl/dۋDӣ"| H_yE〗:_>?qΡytCI>,.cOQx!CF#A>97uk,$+VlXg%q<p'poQxhh8aָ|"!|Aiĥݹƛ 26{W1x[z۳ duTE 3 +fy \|蚩\%L\`(٠D@ š|tdr7_P[=uqK@Er,U XuV5cϛ(`>B)\jLC+OS{,a~){/ko.o[3 wVgA7PswArB [3DWl :tZ,1aI#s !k/=g4. `lSgStʈڷ`va+0uwVэ7W,`6~ +wDE}*2"ͧ +PY @ +]yr@Α2r<9r:Ta*(8}G|02G5=xG|+|uڷ*Ύww_n >{/j*=|tx(:o|jWfLM:6I,9 ekolg?o +:j^G^1fZ3}U: 0q<)T!.Dpn#B +y㍯ĶD@H2t,yz`:|^\RtS1U~l  Oe +醻+54v UxZJ?(>42O,cVT\1_-֤l*Ϡn: y p#Kت٦udErGbq$vo'ax7+`-gPaa5/8p@H*jϛMW#%c(>:'yfGT]2!PrGW0av^IiCLRfr.B'"R1 l>Z`' `b l6>_Ŝ6uU{}Www-[w5>_trfgZ?"aP V(k GHn[rw!.:i@\@r1tT,0 A.@7Zzٻ2uS3.0dD8 ]1ȖbumdVߓqт|X} 4s7DϤ=%+pΑAx C3'z2+jҩ\֮K2˜du[9<[sB!`c͝g$aW8>Y$S{@J\V8Upt1ؤzm3K)jn{fe ')KW^2񟄚i%nFdb@D9ς|#0هQ53w/9}v&^ʹ̤kو3)zp@kQ Y1>8H?1! 1?%}c3t mi1Z>D`0Kfm Flaq*bts%\ܽN;0 M gF8[k6-p,~Wx @vAo@TxwU;šg7GS|/BmA~Y1x*RۧI +|=[fL9FkocgĊ w&jnن/XĤ_8/4 ,hD 4csGn8W|ΤFMfGs%Wl.Fp?bS s;cۏGw!{ +u~4*G|K6>3Vx VW@.OP:q*UxސekAax ұ!7pE叧c&A^ +]p@5egK&DnƴfC'>/[Qi3Z{YpÌi}l}{ cKȊD2= qTďͦlyh!]tl)6=YEzdIh yw!ܽuCh~hz*!q9Te)Z6HlzmLu| 8< q)פxJ\CqwdۗdyG81Gn=:iL*|E9/`񊠢QDtD2atEMbw[lvF~[~%J}O;>oT}^M~XC{eÝvxMQ-ѕWM~ ƌޏd}NӍ];٭ͷM7٪+@ʘZ<+{߀?# 7HM0D؞n{jj~jx&!ko\s2ySwʛ/M'l g9ٶ[@θ̽ /z ~ҩgE'׍-W+l{hԳR[xF+j~rrM4=[#7k#:\a;y͓Ne</NPP^A:mrv7{ˎ>" +oWLZF}I:=;;d '++SW̸;Ozk9Uu+6jxR^zYlYf{讣{Nd/kɂKlʑyl%pL+E`)?A1PCo'C%zCsHƷM%wLWiYPˉsK'/,[mn:O~b^?O!/S޾aμu&O|`S9CO^9([:5|Tcj{y% +N.,Xm@Oײ'WKGO Hm:)Y`3$oA#})MyЧmeO>pa>ufw=#k_[^.w?>(4,~BdE`ExF_?hwZ{Zyo^@ sT ck'eڟlnQ${0TKZ{9>eP4?_h}fw=6+Dɂ>4qOƕ=r+でs;ֳ>#_6ˏEq'>E~g~aOw~lw7ஒѳj{.>oxknzF`kvtU[t9!j{Rm_k[[%˷?!Iǜm{n}mNg}(OgK^3 y59|zM+5ͬuY^UXwzW(~.eg0ݧ$ݯ ko?S}xǾ~|0RקgkwS8r䏿leοv=P{x+w0C|zinP=VpW^n{J뗒߈tzY*߮F";Go{aBc.jTyuk:o>_>e>?Ϣ|~E..p3I{OOsy[i/Ͻwy~R\Yw6yEO_ǏNV*?,v];G:w=b~b= otu\Hcrs/_n>DxNjpÏx<[W_dm]ihX"sa>LUU|Q`dpr'#mn͍SY~g7(LsGcYR1FhcO?r=w;Xy'#[}RqM{^xÝ}͝x~Aq͊c)dsO;ww3_])q~ɮ:חޝd?Ieov??{۽\lӽ=A몃Ww5Ty_F`Noo l~n]CGBU&t@WK6?̨(~XWSNl z߲Y9׳rd>X M2yu='/^w}wXl>ӿLY߳gGWŽ,O7wMq-.Zn>ۻ֢rثOVl9Mu7un\K%&pzяYnOW;>;Qp]PwȮҪ{eKގ/?›]!W:".މ,Vp^XMwËn܈*>z3fBY*G͕(`?tf;:Qx_~6'zkCՆ:edI5J27_o󘗏Sϓ Ϸ oe)({{z2?m+ K;rf4mw|y{%JW]^\RYp/`GtEVx < SQ%GкkPVByԻ9-n2E7]rGor.ݸdJk%.?yUwn9w+l)9w6?RʜOs&?NeĠF'{^A|ݻk%/ _)._Uy?q[zAۋ5IݹǗ.ρ%ߣy:w+쵸Sb܈-\lgq;Uy#ԋq?(}/ +F4$Y*nVwV_K̹]G|ocuW+Oy2kVl\ W}}ᝌzwlćq/nw7?۫kSAG{y;}mq~y~t;}+0~a+|ݍ,s/dQ%W5=py}u^7we[oqvOtTͨTz%,nYÆ2=7^*tϻr'Y=9C2&Z*=STY?©_;\^+wxwYUGH}p>ލ[~m>1}Kxh[ n%޾x+^b󫽥V vӛ^n^~^}/c/V HHQsǽܦ{]뉰ב`]̽Ʋu"ws]g샻 GZ:J"VT~r\IjOW).vx}ՑYMuO*J#_a^w!yJ=UURd2{W?E/~J>m*1 zWX͛n^kXz3īdNȎ]/bݞIWwR|%G.%ᅵPzr\鶫N/}wk֝쪓WcK/\.jG:!y +u+qoA./o[y;ģ#"Sn岿{hRo<ݻ|Ktuګ$+%F_}'16[)14^!7VxDBLoMԉQk{k,*v.|Ϗeʫg7_L.-SOW[M;ŮOT!\TSx3rjն;nR8ϦGkFwz-}ݣ[Fn=8~cU=?o8?2c_W>M2Kod/%zgw'&"*$>#п&HJIfѓ,5|gW<5]gso_e3]שg9nVuV\N-k\RR)ҋeSʿ[ː>.=})Njq7J,/Zʷ |X埽/V%8[.W$sI-'Yc淋!exֱk"gOד Jt轴$Z렖i㶡iK YD wWJV.+YZq!jZYmSʶ_H.;x>̅ңK!ZŗWvfTqݜO3Ny\QCOB ++*ֿ򭭻dޤyHGgJ ''ZmJaF%LHD(Y1{crxⅸIeWː)>{%(N^)A6_-9p5ZJӶ29?b;ĕXkuSn a ~ a"~m_?v}߻n0oZ ,z2n͞[>/ޤ*sx|Ϊ{EmޓD%"( Q9CUb2DI3JT$&0ۆVYQn[y}κsLsyyS#59b1l ⦳ȸ/=q1W]įJX/B 1 }9~E߯وh4# ,;E+sV4<_|b~ -'|bA峅.4nVP0T۲yƦCӰo0sI㜰ؽss꣈ wmN\ b?aK?AZP_s)F4A^u%ν _4n9[sڎa6BV;bď?߃gM.B(2{=\u3\'?MpeÄ=/nq)!GW=\F޶r]otRAӶMUJ4̭/O?:t~Smo$9Qhtᔊ*:V"#y3 [\,vba//}!v}#< +jq|ohSAw=kiL~.g6ѰB8`dcDd5Ҝ 5ll4 \Wr(^yKo6+ +p&cOa_ٍw`zzvóip͙!£i|hel+?SGNEc qd 2̌!Syt\d7>=3g4eA8rڭ6 ؊^ ёOTQ %p{qŒ_]+y{xǷ +~n;s;r-,? O2ײFFQx!n}D4FLt쐩4vds?sPd#BYEcWvJ4}IVL?sEd [ c \!ye眩D(0aY,m2['I.9hj@%t'vJ}Y5(g^OKԵr]]8ܦ8 ^iz9vNˡ ׻_Y/ =D*S sd= 8l8bL}<2ױ\7푥J4q2lƠi+45M\7 VRmW_ZbIa轰}9w [8%]ϗ]5[럴^u£[YdR2T r,$8ziXg} 2לDlcmf&z6Do1z&23Yۅ 9hX4eEP&T!V&>ptI½͵.n:W yunf}7evݺy>7)v d8)C 0՘G%cenmv8aMb, q|2&:9XX屁c`jHX<$8-6Iu˚l9sill~vۇ]?<|s'P%5x?D6Kq\p?YOpFy8!WBfc}&gxX,ǭDVNhdΠ {#FsWS_Pxkg' {rIXZ~xxigjn_(z)&//#K(ՋWBMaƫ&m:$;/K,'Q%SGhC:C&O\4y qKpN)Bf3h<9朄T9L+_ƂWK>9V~-|"Eow%Qo{9#qOvy~Skm!^oPsֆ-2jۯ7z~ks&c@bhdNGA_jYd!36aAg";ϵȖ@#4ʯh/ȹi'5ZTzKwQ3E\8!a}S33a {) +zr88wλEĈ = ާ<}GU|]1>cDwdn+F]֢9T)MZ0fC}ժ3x|\K-zh8z\y%W5-ۣ"pes疟ƻ/B׏)܏W|WM+Ƨ-Wmÿo_>[ʏ!?V:q/Zqyw +*:)4L5!0ӌGN¹4Z& +F6hG^ќh=vGhb-Ԗ$tU]qWpy(BɍjVʤ?.L믳}^+ +bM !'9&w_U1>KnJٝ_p߼(.chpÖ⡚fΏ&[/R6{yDo +\. D <UhYlҺkF. 3=~$b:eOd!iz߅5"/'!uk!ثx߸zmK0qޞB }~˽mQ&?`G1Zh[E;XF06Mƾ@(& d~#Ċn \iKN\|J^:ݷtK!o֒m?x1-7SgSg3ʏ]S(o] +yt?Mэr`/e=eCRU-t_1i ':z4@56_&$:+*9mף>vNjOdn$D梄՚uEy%~Ml +>'%ܓC%r)Ԟ_e5L-7rŹqܙag?ٝʕ;P'u&M_I?8f̞+CK +53N $B +1??,þ{C'Ox|x䭗ɵw?m +{ChH}v~7Ƿ炓j4z_|R 7b" J !JAt@?ۊisTեd ی'I$FktR qmB4AԜތs>-v}$u"o*B>{̃w#sSW?+<)R>}PP:|}RJY%)n6WVa}в =爐{dmc i\]WKTGR/$Ԯoܨ֛&&76SMvl 4a+훈'0[jX p~^|_?G-o`*E`Q1a+v~0irbc8
 s0^qQd;?':&G K-Y;ڃzfU)}^~ionsv?З>F0ަ=Un/XzջcFx^pkOv'm۟-~\":"20hr'4zZ`id^(4~n䆜1eO,̕ъlHG#dٕTi'"GasVFo+ іhɢHע]xJJn̓T*8t;J qoy7Ln (aR`UQ{QҚ33̞tJ 9cdWxz#++)!2U:Sf)m4_zqtE{mQ;ߺG^r_d_IlASyޚ竂n^S^^;UPYz>Exl:c2Tc2 +1F&#q=Py"wiS[~y*93'Kn6r[ĒDz4ʿ*OHhYh̅(+4Sr#sMBVe֌;//}GԱ$ݯWzf+쫕Cr;zI:ͩ,&~2f?jshd7a-`ץ~{m^(< g':#m>eퟐ퉖Μ\(lR>hhG"dMlv%-]})̩4g +1˨+L^d!þPyvlӥo[#S'+.}k2gKҽuƲ/mŗ ȋO =O'L=:#ya;! Ia̼|?>'wtrY 3u=/b禎lmXBГ.yJ6^4vC}40d۟9GUJ'o22635GgBqpOD/nEɏ=Q*> va_u~{>$KCґrrYң?^sЀWB7B͏jZ%Bb% +PiHRG +WDNi=\6̢=<r"Ua!I=SBg@opxq_Z~kWYN]c;'YTj4J_7'g졷 uٰM{'SelŇ~u Ӫy4(e +(;e&Ӂ/ZLd5KT[bb;z@KOZnZTkog#=f|i +Sk%aZ4M1FϿhG蔊Ѳ]8ü @3tDMUJ担|3$뽻 4 T*o\LnXo"zA}퓝/w `l:=9X.'u?]}h:]kJ嵎dw(zm(}*M݃WUdju?q,XyU<u\ uru\}EDX$ V1vhyD/RCR&kguOyl(Lqz~,ӌj2_]r`2ĊXu +۪PšJzp s^+:c q` +hR=Oq qdI>+iVYB/rcӨ`TW*]Aa>"%S5t#Gf:}]L2jgj3WOa;dzi[L#zCy$;^L{b|Ͼ .|E{/Ճ>^.W*Dϟ QՀ-zSݾR1ÜywBOim>2?G~,DoЖ٠VHӖk:\HxlTr!' TX'*Rʌ 3*:C KRu@+,V|`}Hӫ}=AאI2⳷3}]'E]=U}Ӄ̎Ѓ4w``zg15g8AS"$ZH.-//du-ù펽oi+$UjeTe^de6nD5.bd>{!eν +a/OfpLtv[IctDt߃A61 >,2 /k;>:4dOOI;AԲJ +I;IO%d :L]]@7:8*lJ3SD БsʨtUAdmph=6bNL>^twbw?>Wj- ҽO{KO%o%~pOc䗞O?]}-~neՏjϔMzOQ uFTj1~*񚽬T:BZvd,99̱G+p<%8HUDd'dbSGf*K7ٺCv̮<|jh.1S/nOeC7t7VKfC)Wr` +6̭lf9I6łhl`ŋvLds[,dF:!SN(#z;ے)[5ρ>|ШԆ>i*૾C'Rj Lq0_߹Jvqv}pu?[m<0 A{ +k=D kXS<]/CeJt{gf@w↬B;j޲  XW7Ug<[9~o)􏃞?hGJxiPڀ +B*h' #.b4o2hSrE}4MKcGJkJ5A94U3/}މ=Jq]Z(}GG|cpl֋>Mw L땪W>™tc9\&i,޷~ЋKWoI,Rƅ7Q=dGR!pfk}Y{1FdV& f)\%AS/]&4 +t4h{Hwum3/hu2C&˒)q,LVٔj4q}وIb̬5 %4n*Jw^:W5jpzb./@cM).̾OÏܾR@3NP^[s @{ 4GOie0g@ÌLR>Wny [ @l`AHA]&U[;.qr_yDhE&81z}B&@!6Z _ +MW:$IdXӆhǍ|YD+t|S㧓C߈{= +<ܑңD˛C^^^9*QB_>d _̑D톃/K֔tu峖 o\kGLP&k° ClX6hO jFb@+ +%ϦךkRF@V~v[zF <ХM%B sGxǏL!аgpNY<6$P[,xMm4@j}hi|.zluŞ/ϯ8Ezо!ц?樬=rg Ч5<2!ؗ`Rp|$U*: j?Yr?tIӴD o\zq.sHy4oe!j Ӡ_ +tXRCi5LAnH?M '{^'^ѧdGtq͆&js@ Ult6ԱWf00W_q4𺁯Aw\wd{Bnl :媓wbh qw]Al}Y-Gsms{9cLb!꾸 UVvIOO!]1H18YzǁIPu& Y\b^hcȵ_Z8o|.5`lեlUԘ\r0(Z*&@Yyh6hm*vߓлd֕Bc5 tdYdwaXԙh1F1<+U/:Vp``- ؝_{)7lv86ے5_=w_z*)1]W Q^mL-Cу oYLbk7(D/'SzdSsTlي3'#NNQ\-ѕ==|i.X`}: +w`{_yc*poNXhD&hA\$ܬUFJw0ҁyHW8MYߗkY7cHW lY!ccA[Pl є M戦m>T޴k +H- n̨cK h甠1To|4ĵr#/714.?l<x$zvgq&, g(udpR%CG\iUBXmkl +†'}@lv̍16N]m X6Ԍ|Z^=Edt'|m`gU; wqwv['y \:+(T eVms k18FDKGQk4D" -P6xuAYm K8+{"ȑ^sb=>zXچ,0AwA;ЬhAgx80>T1yR$d }a}Kv +E3I.0C4YcEJ`gEdTewMמWLSxFtnWf8P̬02 +YqG=?? +4lieFM}ӂ:}B uSy*?ux! ME4mX0[_c 1YTVGh sqGoЖ2ja;$uţ=HbLtx& D(yes.!?w/Βr +5Ov$X#( +Pw /#Q:ty|dH&|q+lB{»זk>OVh8 |N^&aMdv7V +Z\J1_f\]gIa PՃ}#U; [7׊YiIXN8f; { R`gAbgV YK?Y,c5!Rj)0wA_r – gB; z~>񊒮ɠM}k4_<Tg< tLXLcvw}+Y`<NX`Zo1m Dz x$#r$"ՠv>0rI> P A*JCYi*6 8P)ĝv@Xe=)7Wwv!恝{% u`g16`+W(R +GŦGOf8~ do +0F΁k>kz3U^50*[}ȩ@ 7uLf!FB;96η]T9 1yzAYeu[s_.\Lvr"=ӨŶt$쬜V`gi\T$sy;@7&,%𓁝U8R;+eATr^`m}oo@N,0ejV uFՀK9"`jdXx<='^? Bv൯2RQH~$-G#wB զ7x]X4f 6 zlt*[-mKJy'ŵ ^Kߊԙ̏@#=6]GOvV:*7G^{^\6Z* QmW̬3;{JPr![z ,|}:2g$*M.; ֗9@J*) +X^fm;K^l`WsaqX7QwZ 9'ܘvk]rqUtp"wb9':pbuH\R!BJ!a=KAStL 15م5O\4X/am}ChK`mVwJۈPT+ǵ*0p`}s)\m<̡1ԭ'ya ׄ}' `F!ly|㥥+}`8?sm[&Ģi|-'OܯS u.]5_W6 +Ø0 ٺp_r>r}1Nb7=\\nxвfȚXϪY֋ /Y|K FV󕹭V$~-3ͧZ|eݓI5_k"µ{.ɥF\\ïߖt݃+mjp>8>GbDi5fb="ԉjpEj#k+K'$!Vhr!iڰX[`TEL2”s[LX2j q=( 6D0uc4KW:]PyVKvNToÂeB.6O0€$:0 g{|F}䥇aMD^;Y-Q l2U^90@oqh E%,6n>Ol3 ͇gÚ#Аk$YO;?m!u] +,2C[s|6 XiYx";s8S- c]Qfu{&=Z8UsֳƖrgHB֬gk6\){  SxոﹻJ.Mڗt=vp]Qy|77Orc'\T +dnz3"ENK|o +{ݸܑHzQQg&UQgpNM}bhu,R$1c8/"X)T#,O{mo"`S9Z֢بX-`m|>lCfst(ZM(=ty鄏B}"p`mz3pϊ0a LEVwYpnIѮiXz +&5{9\RÆ&iS}=ޗDl25>:kRqt4-HZu`/ oxj!+ze߅ͤ1XĤ}3C.%O/6"yJv8eQ5';zvxb.\C=/ǩ0Up\!>L' 8o?=[ϖ9[B>!y(57=cw> n/g-a^OžG =Y[ g%>% q֮ >ܛЀb,>{2`-8El:-$h4*pNpo\sZ9a{iMyϑWkprFyR!ab 97eityŰ>#/ߟ .n `rtį+}I9`r0?X[l u6d˫=hVd Q/',.zjUl|vw +ɝ]St-rplgyߪ|TEA%Wpl>y}%p3a"nDzTx}r&C^%1Ϛ޲ϖH^ZL5_^T66Kl\|{H +vOWHo2VX8媶c$M3ue*dLσk|iJ)y4&Pu< rf bL^9Y +'A<-aϖnifP;˨73 ~Ŝ6V@[XPnBXM>p^a0ǹ-LJO#+`:;cT| +"A\-d꿜9!b>O~(aʌȽRM?1i8߆m,:+tP'66EW] B/ڂRuL qas6uM&vZӆ|r]u|Vo +97~alE%j'\Jg_p_s53Qq`L(3$csOǧt|:>Oǧt|:>Oǧt|:>Oǧt|:>Oǧt|:>SBC X/ދגuIS:'%E%G'ć$[;·X?/$=")h^} eֶ'ΰuޜs%!ֶ/jl-JߔF 20-,~jqH2~J7]"sӷmpk]kmE3 Y; +D[#td Vyr A?T!CkmMw7?DRe4-(Tk4@K'@]WcVǟG."!||(.yW/cԁJƿ +Zj}4A)2QLVSחʩ(Ϭ52|=Ӷ*Y*AAZe +zD+쇋MաSk)񊆳K'9;.>MzIa8h6B6@L0 4(uZg2tPEt&cru٤}Te7a>PC4-KVmf2iOGkk@CBm#zfɫQ6q9zlB.9z37[Ht/.[C_$3=-ԗ*X`{ ؞w2h3>\|.](6VwL֤֗$)GǍEjqY]\\Pp>I<ߔ)>QCL֤C¯QQ/;Qk T<Ĕ +~+?esF@?W~:b*\-R#K3 +t$ s=|=Й z{AIlLHb="QC~4(AS@Oe2XIׁVUF7em&gx|t kI4U$C/#KC)a,_}b.NBo7uM {DYV=3Db4ep0gB scd>vej’~fU)>zHR#bo/+S*MeF<sZDg "'A %c-zOd<~>ItXLD, YAGiB/Hpu&b6:AlNӂa[sE +{%SL@tz@CC\m :nRĪˡ'*_ +^ zDW^+}!EL.h.ȓFoRvM8*ʺl'm2G=-gPUsl :'*J +4**ߤظ|2u1hkZ -u#Ub|]^;tEh#@i ,Ķ}Чђ#m=dԙF%sWMzx..G)gN6+xm#Vv-sAS@YY{2 +2=S|b!ѓz]WbHbzz9&_ G|*A$[Mtב た}p=@O]ʪԀF㸰&GjdM.4Ct@c~RDCNa z + +ob>n˦A/5s*"M/-+nӡöDG.,Yþ9yh:o6~x 誱DI6xq‰@lqzwGXs¼ĞAk 3ȅ@f S7v0D%qfDi}hEJ S¶>$l3g>Pv=2cea$Ea5H<_a^n%¼=9чh5C#-l=lA4؊ö Z"gMmﱂUҏm (@4\5Ұ/ۗ ~ДekoWնOs 4fD100PE7 +bآHw#9g;p{{~~b˜s}c>umh- -]'8΢0k0k&Ff(.<} ;>4(hs)S^*]=Kc)Et[@n Z:&:U?y{"k7yÆ̞ h"XcZsW/~^rA9lEӄo,a l!f& c>qJ 4 @' +h3g.O`R|PIQ6{DC8@ /A8mt Ԭ11&%w6?d5C|9p5hB5nM-P`h@YT_@Ƙ2 +{`dV *жx&fpFqAӁު瓷~ե6Ocq|)EБf_:6hnxtIQ/+1{:, +_%>j +Z1Tоחc?O0p, ŶA +!FY/{-`/~S$b|.#n[ը_dlˍ(f7t阒TJ^ +]Is!p%#+&ԩjԄ 媠2K\X 40.{bej e@\:+S~Ih"?({9^ ʟf#ӁFhV[ -&C^=aЁ]xgccg}&9HsWNbC O" w}2`=rmH/FM\SCg@-/ ߀^aMx(O`|S.sqOzH>W~DQˀvz}VKAxu5޸ +TCE<97Z=fND~e;G AA Z#rg +WAW[Ș?~uõ'M X 럻NDUng2~(D@&EF|!Zq< Qqx/#c`MHۏ~ \ze.=BnGF7Ħx?=&ȧ R*G4t`͝FӦNx.x*-}4(?)1C:͐?R+^/So:? b]!>8քt'Z +̛ iZM=|koN/ Y 9-ƄZvL!u =>a\e어~|j{hPӠw-:ksi%QM]4OD539T}:5y@+kTטpTb^oev Ʒ0f+ޖ+2wH=*,М?i;r|+ }^䞤$vMx"j +_>G/71W"DcކhX-I/:OΆ_ Is0_4Gcb蹘">hlq+s1yBk˱^DaJ(Pxdvj =?99"KyJ|V~HKq`n9guT`%п~P<X!va:h9b=D < fC:)1WCsu^k\}o.52b 뒥Do19ȯb4%GIנۏZf- ~*G endstream endobj 27 0 obj <>stream +A<(wQnPaE~)m[1ᝤO ?.?`ln/ G ➠ +ixq[;\w>hA_qܝyWɔ.:C{&QW:|u"E#=Iw zc >75,wcZ௨fB|(x< p^пuB3ZМ1a,A{LhӄEGGKkܟAxu2<5t!!>ycr&iDz(N:ZkAzX| +Ό|"F1J". .N)d Qmýwxr8l\Mo!9!fzLX8?Z=&n~y)[D5%1s$ +pK#%spŐX\xd,\Fo(xB( F zFJ/xU\W,* +:mt%~+5<&1QWѥ-l55+*ŰCu%8j=<>/ :q2SV:wT˄!7W Þm^q0T;a脢 +56TN)S3^nDyk)P ++\\YJ=[sa]_ +csX W1لJ-J|z^Y9)_|)DejMx?فoϵ^U2` xxLmqzH@r9m$ g4p@l6qTCoEW.)3QwVJҗ@UuhOH6({sFOIxBh)d TkI۱'$xpGm}оW0|1g UuQ2qS7(J*xL96h}"}u.x2x3Wb iG\]=/{|111!= +>Xa)J TQg+UuORTa|' +?|'/S!-r q5rQj&Z^XK4#nPO^<:Q@eAK&%|.p2Ή@NmQxFxP{.+P}Gu^|axZX.9b}eX;eE!h3宒d,5! (a}%g'^Q1"ģ/<{7ǹx!!vY?W4 '^1hre_k􄄱%3K6)xck:ڄv2JNnPf# +|yf.o=:/x]#q>ĕl:Xq(F2w4wO@\%_P'`?^5t-P%YK_ŜR&ὲ8RCTNOO^vɫS<؇P,b+nd4kחaOH N9π^ 셡mf[53!倣D:BD +~X}9Gdg{@?bjhh5Ox +Ç|O̎xޣ {rր rX=XU'+}kp=pԥiT`s!p^rЫF]_ { ^ ~veGxc{Tp1b'L@v0.XA>^{瘹xOehZ]]k&Z2j/_{")o3"rU-~o?gb^n!eyp2%7V=}.񾽄J/ڧ'| ?.eYKg0Xr0h.nm `]qJ랰uƘ>qi4<hPKY@<'g)yaiL%apfB6v%'1;e7N!֗%qK U Vo "50f k؋~ B }~ q_LQk )85C<\\'Ea6'_y&6֤ ASӎ>BiBkAb )OɔJekrF~(~_lƾAVVgawCg'3[Ԡx\Ϻ+>+ e;/74_6ƹ7Z1{Oxr6<EпZ}X煞ǡ +7YZĚS(cx$ar}b1zEh,@a)l:FM{m[c{Q?2@M`||hO[1yq,yf$ 2 HGm~fQ\ {s+WKaC6 ݃YPS9kmE<ʭװO=y楀GqbO*XS\Y [b5lDVYل+{ + zK//lh&K.Q,#lk(pҗ #=ScRy[i/ +iLX&h[`DzTӸ֡蓉jӂoYkՎFe?$55):p2jj>}ذ}CJ%B5k+aXGEx3EC# {@z!P{G@ށ'LZ>*[ +#aen/EMgg^ѩmDMz෎\pX< +JuUqP>a/!=ɐrxcJͅL@ +R.`VX*l +4v͞OFn":@_.OfLhwLl!̥Lr>A feV3j!oփZP״|zmFdÊB 8DV]d Һ\񧙴K<|3wa\|)]\o^iHWa N+j/z'"YXiI{fҌ$J]TwIiڏr.W*j&UE|J%Jj?] \-a!x9Wa8?/(A}dn07؜6i浙STXKѩh!yoUш;`m{]|QHtY,ɭ9dW|N҄Io&ӻTѡ1DŽ^q 1F_fjz0bS_zެ25Tj +>6 忤&eX >(W{]z]G=Gq}֋@7K(a_vFxK[N rO>l#77!_| +K[ĥ&]0yn֬dhຂQ"閰,Imj'̬%>/6JŷEVJr5k2٫ڬU[FGož[<-"Jo}ТSY_T;x~(H^g!󒸿6PC|9M5f>}\%zGo# uvtEmKUz(_=(>..l8+"ީ11-;ye3R2 uz'_[sKR;=caf>C+ەoVKoi,BF&}'i25C|E@? +ߴwgJG}y[vt~)>üd~>FqzX*dʔuYJJ}$}Ee5E&!3>هbqn){eZuȫjV飊#ҧe;- }ˈޡ'F4 +R7jw?걷H*O>ꢨ>)0ے-8Ǽ:T|8Tv^ .jWD  tA!Je>\<$h,y59Sa˼=LeҢP 4q.Gxakx&Hޔ_4wQ_?ZͿ]MG3C>Y +bz%jI3[H{UǥolK\n_b'~QsBrI$j7XJ&496_ a­b+q5OŃ%O_|د}@x4xkV4Wy`\x嘣d.⾦#ĚtJڛڎ~Z2ٗi*FK2'W=GK,~hmR$.mn>)*n=')T9T\o7e_vӯ;Q;2F4P+nXӯLgjz-n%X6Y']mLh͒0=_Oi"mJ66] S>ne + 1kˏ4-?,PǫhaU軩N|!H=mcDN7s:^NNjLO?0Ѹ{]k4|?髊s&CD[V 2h $ބ<1Uu9*̲9%ZpWNԱ[1`qsΗ2aRY5]'K+_<9/yq~2Z^DZoϼy'ƽU–7qgwQΪ㤠1gSj%vs19D&.7M za?9^O\M-wIp Ip N<ߜh F"L5H-@5Pès)[e}?C@ӊ*6h`W&[- W6m6b/n+ueIR7IiI }ud5ȧg GK;JEjK#.4&&z&y&&WzJZ麶"]BS}ܢV9Gf8߬p@]' xP#T}aAfJ5l(~pL7D dsv+ɖגN6JtTş-x%j~#+~-gium1q]녃=ŲCy2Ǟ)<ŔA8ɐ+zP#+?kX۝CO=HjJKkrZy!":bpQmXa]XIuHeuH)&><1jkTPoeCZq@YדH3'z>sy+=8(ѐI.I3Ҏ ަ agC_*TJ ?IKLz+#N愴齷*Ĥm[Mpr@O+:$EwVMKDLH^CdQMXauF7k~Mh~M*4R+]eer7We@SAA)'4 +YƩ +Ĺ%oϚ4G6d }j&Qaɛ BT_~1zT:WGE +[-k }`MnV)'Zo%\mW!Zr;Gf$KLuuOlLa;\u܍wo +LuYJh^G;>+v-q-:ޞ% loVW]Z7X˫anD_Fq/+v/u +M* (O;{\^s|^#w(OAeE^E2(_II>IѹQzo9=לO6I +ʶlaޙ6 +λ׆^qe]jLjr{5E a GPvpnjqH~l$ر*"6'&=jG}eT䨳)Ǜ"=oͺEJzC$%ͭ~Gn]GfMQg2SO7g%p7`C=Ϣ}Ȟln7ѷ#ef}"9 #j#c]#_6xg~ۢdMTǤMQbCcd]"~urƺ=XqB~̌rg~pV*O{'勓^ '֍jZfӤ8$.n5=r/ѱ:*%;j[mcl2@Xֵۉ*]% g2"m+cnuC9y<ܬ0ڼ+'BsR_KMx≶tGpܘmYqcccbT7;^-g_ܡ;S'8{V1~JkewLZ[${7"J"N6eDw܈`<[էèa;ߍJ%{7;o]sd;ÝC}})!, 嬒JAQrGZ-;guJSLQӥ' v9 vQM^ɢePMDSwyR~y=|1}6y^b}ƭ{5[U[ eq)[QiP_ڝJ"_zn +/ +="C /#p13VkU~n,E񡥾 ob߻ɲn.o +Kߞ5Zinh:rX5WX8\O(silt(WMطODh4n;w)s9,[ +dJK iks7+V([ -}>3vUqBAV[gKwYo=b +:-v /( {1"-:+ֻ2 ѭ2$!Wm4jSt)b0C>7@k8ޘm1Y鞂N;%/rE!S;n +Xip"BIa1b1CE$"1PA,DfD5 W|j<&N w+gc"D/lt5u{}s*0.WTZG7(/F]-=.v|!J[Ǒ8G.t/|Z&knuY~iI\p9o-j ۰SXTXf9nzFN󚂶lW"&(b1@%>r?^z,'yx˄"b/JW?*727*BoYzgԕ{nQ/ +\"s \"rbEQoK}*ٯ䯟\/DǼjF5bhts.Nqb(sJ_8t#rj7L F N$FMD? fNYMrU@ucsB9갸9zlVGTasշQYܢP +r 5yYSwΑŞQmQ_[>X7;GΝ/{=w]Bi t DgO@4ET|?\_{h.{Pk~[?e;zAf-#d.Zb^BX$aoc+Bg';QrkGh9-CVGGqx!5&-}Kt"7nMj_uF){R*K( r bƄixnwyuwpMc~H9_$jĂebEb8q؞<Ϝ[I/|e<^ž-pM(+pK)-vNz^[RX[EPѕnhr:m__P6,/c:{cѻa3I8MC9ii#/&&/"YMT8@Tv!v}=B[8!Ԅ(hぼM(D=E25ɯZXt)&2aKwiK/cC\ ?U/`N@Y IqbM;YӶ3 +S''ZGL +ߏ@(GJ,xf%?[n3oxJ(`{/<Pk}\5Ǖ2׌JJRD]ʹǿ \/JĔIh)7ČˉiVƬ!H̜Xa? gb#}I{Fq+ש导 9b,~~?1?QqBb696s6%,Q'-2$f-sԉ bN(˱e[n AQz\,#hw +~AX{Ǩ7Qo5F~[i'<9g84b42!75v91k:b̽Ē5bS sR̥$1s1CQ=~o/T;A6kwٲ<50Ts n Ǿ.rP3P_c;PXST'Y5fp0F=<{CM@N!F"[BP”5h fMM(NML_&Ebbẓ +m_b{;ps[?rG] <#5SuCT[S<̹ZǴ*z䷥NKr{_еxMELG ͫihM^OLDc8 F-"f_m#0k+OU&ļĂ}}bO fOv6C@_X&2{܇}|yYd֬uv-`DTZ6N E(G949]N]bEĴq PnD䕄tiJkL Ă '{bmm^5p*<]*0 ȊWztm%OK">Tل~oK.vHL*q0}a:fC?~+ a1hơ6,|LP<5:llgPN',&g 4&h-o i7KۨZm3 k>Ee(wƼBXhtscBgCRoC:J甎 +G%Ejp[&/q(LDׂ/%-t*Ĭj(W( +3Q L4\;k71g^b +1w1oM,xXJmt+!y/~Wm zy ,L/*t~M/~9mV7F WŻh℅u ն1BQT4N +VsP=^XaM,fF,Y#CCr5耚;{;”Ă4`XzXHcGo 7[SV z9 H󭰓uч:Dl̊M}'TӵӞ(ڈԲ2ې +b}Axm#Ly28ᯀA _N1ah>*SGDL@Xs[xR1lwf읶=k;.=ǯ|j.b-j;k;-Re(lc|$95w-t=QZb!j9SA4C|2V5;e-pF !r};r1<ގ^wuy#x<~o-Foӣb¾ܚ}n"flnf-S t8n'r.VDfBٹ2,ƀ6$!$r 9+g1xR ӍjZnR?Sea$zݔ}T+>>&.h9 8#qtA?f H#2!m;A,հ!<{6SIJBb+0lǣvݗUvg˙_; }~w훵;=j:7zA G*8SAiցPzYYKc{];`{I?U!!AjI *m7RXh6n11gVb&#bIbA]'7E R3ω>=u!3s?mx6Yh~x:9WC39X0WdM󠑺6;vC39#ŢFܑ;Ӓ-$!L؋ zy+6LE*q0O>&qEqt \obW#Oܙ)~3GΉ5q;U˸MݜQڈ2 +HK7hxm|m>Pi4q.`O{ o턶:Q>F2QAd1W|*l9w0wIZT1z!1g ؔG8 aIˉ[f$Am AI_9KOa j9R#usz`3Nq adC R7+8L?rּ6B7ϭ9J-ԵG,?db5Wc$}'ٛtImLuLnc2@]hP1vQuEyq\byԉ B,XCӳ!5|wݷZ% '/=<_(8:V偢+Ni*p#^:X 4 zm 9(EGi9QWHv5z-^^-eDALxvNUI6͌-j>!~_z(k`7u2CQ]A*AsY,ۋy~LE5c:7i%}s(_ Qݥ|s ܠe{mvMb Fz9iLnM>ԛ+>TpBS1xvl4 яԳvg$y1f 'Nﻤ̣v1׿K_7^\ gd7Ulzg6-~b z)n&/m)[8)1)-򔾨=%i [ydf~2d BQam w0 +WT#VX-o[ш_vqBDvvkBPF<%MZ؍7sMY*.<\#-M\< NJلƾmY8GlOϳth 1HВ&w S4tjfTuYΒWOZÿU*%SwI~4kY#T-Gi\z8IɴY_Ѓnǹ:,jS6ZoeNڏFޭBSMl3$:3q!˳91{_mG~M]`*zu)u|cftOu˥mLHمcgG^ L1ίH`xy nO[풫O,^EJ,Ey*eb)`0E^|t<@y?gi^Q0 +( uai  RgRb6I3ihsSi:f7tG5Iv;-6-e?tt1n*5x~olxqP=;Z9vKٓU V3ݶ²o|Z!ϔh>[N?Y͗ۗ"DS%Lf.8M5?4RTқ]Wބmٟ5yY]{ً[Vp/m 6uy($E.b?a}c@%Ulᅽ^aֽ00C߷^6kp;59}_9 ]t_r:Fk?U;> (+?"7O\SE|Kg]lyfB-a62W{waJfû?ԌU6+]o1U9qF}.+ e 3r)M1 3C +Sٺضd9gVB;hx $};bݥ%_#>wiF>էE?\Camg)shxoga&75ɇy׾lQa~ &VIm· lgagSחsUܧЦ_Jli.ٍ\nHSݩ:BdAf*}xhfn1srBtqгM´Tj j/r4 $n5r=Hz{oUV!ww'xTUq!n4hhwq~ߨ1ҡTR3#}N<8y/bŻ#p׿uݯ7}/w|y|F|W>ᘣp6ac4cYݴqf]7k{׌3Y'dnų +1[\9&cf W_:q/<7./5ܜc\zᕁn\GeB^T>fXrr6kz{^>s3g[꣊p_R4g>/:/wsEgbTw(ڝan^Irz-_o[_nn6HO ^=Zlp/wb^g>E"&ՌᓪFN,]x_/Žg ݟ} Ӟ=_(; yX`~P?)u٣{<<+?WY<~ftyf~Q IօZ{XrI:c5[ܺv#ظr!:NdX!G%ڪ ŎČ +I,Dh%'y矩…)|rF{k($U3j Own|ܐG}oߋ݋4)L:ۙ\: ']{eiG*5޹ 7i6ݩqٸOs@`9KmxK4l4hW>930g8rmpOoV/kDc%iW8Jɫ,Jx&K/׵<\wWOpز.u7rmO7qO7}?^u~P7;~oxpVx{?ׁ? ,&nm<0{͞O܇ ckFe B4׏f0MgU.܊p7w<.<ˆ-!tx~ )&Yue&<w?{~aNCb_c,| je%ڞn G2l􃿹pL\}˵=?n:t͎{=4-,qY7{f]vi\{hvcnޒC';B{3Z֟sZ{@sb\tP>4[ڑbYgh%/6`?ՇgQO~7}Jd__&b~Mx[2;Gzmį;0ܡhcTI*mWI޾ɖ;6k6/ޠٵiKCkt>Ֆѹ#4[mp%h'78I7CqjppE +07's{f-6A |fxth&ld8 sRũ{(/]g|QW|y>if՚=[kKcYIp4:aL%d~SDv/soX0JN 757nۤBjTxn6X|Zvl<⤘44uRY=rbVDҒLiGQvS|P3 Ny<sШJƜ4 [K\G{"줬X/ݩ;^no]/H%B:B7;!ɤa͝~4x^ J "Ts?r;7WVrW'W0r ȑCڑ:\j=l?Yw+:hfFI%3M)Ngrk GZ>V4 [+OҸQfB*]l=n]_!6oI{f8Nrp0>ҍo}Gӌ>+7~q>,]} +&Z8~g]ybfm}Dr'K'wD.g>ېnd\1f c*F!q4Ux`taEΟM7 ͸pEfʘb!s,/D+qRJ~t꣄BB_;Z, nq@Hs1 \.\7ۡ!uN#VQs?]x!1`$4\3=WՋ_3_P#@3*?=_,tJhπ ³:?+w/{uݯ6a@L&6{:y .,pwY- t`&l\d`w}h>,k8{r[ۥ<556 :"s>]l J.i!fZL4]yXKhɈ=0T\w\q~i"}UO_0$8J\Ik Ah7_}A-fU{,b.<}㭥zV>_iy;]4׸N7c +Feu0ӟMl |Z>EiavЊbr0 qpäˡKM%U…ߴQ(0 B)o-jxfĢ3gQWFZB۟;\b@.j.2<;12Gz)V4I&'p{%␉q2)1oZ:x_B[.==zu,4gB"ҌJt!>=_o_X6]K;BbXfR`xS0oĠa!bfD>tO1ut< 5'Qfr3}@_9ZNHHV~fx6۷A7QjuTmiU_Zf'}'xVϙF҆lxo֨Ո/ʟœjx +~̗Z,>vNb+GWŧ5C_o}~͆y5hth WVriC*!A/?g*'<@6G?[c8l8R+/-&‚}!y=|9w'KkՀ;z0i{]E┱<@Le1=lN>0īdM(OYʱ +Fl-eLEJ%rh`UU]YV[].0Đ(kԉo+,o5B-^7|gŞ;A3'CS9L1x{*-cTh +)NBD> + )1K/lM,.gp֗'AW`1zWN"L~phեc1-BRbtLU'=)9.{Qx)7W.e=ssb.\mRF NN`۟m3xm$jB;.4[4炒p\D+WO<;5lXVͼ-:GșBBhCK8‰jF-}3P4}">yAqg7{_SѩbHe˱eR%|rݽuЊG 4h%)7bF$1k2U.^mkzty+XJr=Zy 9S)(P_FҊ>0 + +:bW8s\ ~c#/8P3]9JT-4;pg^{;'h2z*,8B|(褋%7]z)('cv1T`Aq34z zۂ7+ҟ,91G9&Bs~eT5 ZHs/Z0 bڰ{ Q}F.W^bE|B=4Ÿ|YʲCnރ g 'g=A`챘1 +||O.' 9:&v]ӝ·Q󂙅 +g _֛rz+5#t~M脢'B׹,VC׮BwwBZ9^?luZseE|3XP:[>fTwq ӳ{U|i|;/ooJEP,ҷ;x~z_RLjE@j@p=CfA$1b}:;[+ˍsJt(%ydhXj5fؿxZzu1_h `^Pw9z[k]-:CBs/JIn1t9ӉSw`w=w{Mw7Z塾foxs<!( +qY1JA!6Xwj\HAhBYu?ګx81`gʷ'X--b9 Q;0NHS`D9#HSl`@$cWX!qtq f$TrSE93R:0;z)]~-fvyiL wyÆ{W>˧knhbgMI`g R?ם屟\*$Vm 6c .jV"$U9A\xyYھag{5hX%iY|`^K梁-E#Pk#XH\H.$|&hz;kH-U< +:IK.;1+u'rde ,g,CLZjytf|5^w%Ьy4Fژ)0e;JJ3꧊CO2j<3IN CR#lggM;J߰Ӈ3:)WNUY<#\[vL"bZ8Ʃ73_oG]{g-HsL +jطl5xH\{n~m4u_nw̓5Ӄj\zrZ$?Tkk*f'a=\0X쉅즩Z kŅB]cy2{5w#vNR!&zz5,#>Sl !6J`09(e8|4Xħj4MRlwt:0~H>yr}7)(gehޠW +-n)Q`|3 rRl[5<FU3bfL⏌,b&7G;KyBUk +'/L,#}-_ Uck]h+,q1f";dabICo_:^- mb!h|>ŧ* 3`K# Ycƣ؉|krk V"b'k<@:E%vD?CjĖk?X\R*/h{h51V>X6t6E)>;b {)@ʝU +yY. "j;׬V-*I `T]_F}T&>Bb$#m#=Yuo1q!,7aO:덧(+/-;r-;Klb#2βpZT̥&6!bgO;+2+;+ ;+abTYގ충9 X`b>&@gu̞jfR!g _qe}l]K%"ێd!Vқ&G?\%&9` /j5"~]g5y`0˩N#"~Ei.?'JW6UA-$2\7Q-/,n'%ZFN%[q}/\d쬤Y6FbgY\^!~ZujOAl<[S=vt鱷|x,KCZaZrŖO7˝v n[ 

jNg/JhܣZzy1j114͖kS6#X-kKYlwWNdˆe'Yq!y߰_sJ%]sʳKܚs[ӋwWJ/,RbXr:ţV&nLQ3~CrÑ լ +0-pĎF{` 28Au"sx.ri,_]}ᚊEbXN 4#vR MJ'=[{LO6D;5L>Y>H cPp|)3DYxy=ȥ (FF]{{b|;K>oe1ߐ,Zeﻸk\=XsԆk'-\FX}_R=n+'RCJD!(J:= cy6z_lZ?\K|'VcQQvyVG'yj41RKFGuU K5$9I_E,qxVy{z]l[0h0B<5(#i!y +]쎩`nȅ31cB}D+QrLaĀaT[ bGڋI#S.=g&߈"q` Ả%w| +4RTՐ\> Xf5} /*rٵ<>t/.FOCj(v?!DZ굲WĨɮD/pސE䏠~¿Ć;nm"^[|\hw}PbR8ҔYcxU1l# k61Y= @ 8CڋK똍mPB= ; W=bkTd|mG<;wFÞZ~m)L)}Ih>2<z` ^3|>Kص/陃nRpp\nTR錡˩a 6Cz4:Q# C~%%X=9L̈R3 |+_"&Qō׀99Z(18 +-\OebE#F!6!e+Ezjzۘ%kƞ1Ä]_[!oƻܒ\C%{7Ǖ2 kK{9$F<k$֍Ź7s_+JמbMAWoZ?ߪ #9,磜/PrOK0yo[%;YOׂXj5㐳ag/{¹{]o>-P" QV8^88Ea+DL;6L' A\YNh.ǜT=3gR~zz؃DΨ9hHxmkџQק_\ %׏ř1"VIǒ~I +XQv_A`m v|lGd 2j/A>Bj0 f;/'Og.sC9|Nj|ۣMyJ9%UB~=ҁzb}%:q|]~EzJIb֘o2яM9r4#?I.'[ -PK[L81ř2?e*6Nƫ+Ď϶J>a)qX,+sZ>^O5'cI|͙\ǧr?XYT1<|L1"F)C|8!)k&V6G>C.Mk_RSJb9)c5yFK{k5:N< ]\wIoJ)6d`LkuLY92VEܢGZ:XMYAWLX5.\3#O#X賳5$k'ȋ`Z%49Z>'~(OT$o,mnGNxeاK+sBv6]ˋ\bvDžDE&;oė9]KYbM]\ٿ3ط8/f?|˴~Mՙ}36$8$}QhM~ɺ V,]bWZzuKW-__/%//9d/G{opY;ǘŞd ~p~ٮ$wg^q^Lo?^:W-_vyڕoO`?IZ:̡W[ykً\4]lAsOg>wYgVhCf,k92Aڀ+/5|f}=ƃ30Ƙ{].ޚ}{4@22"rm)VZσ>,ôI>ݼ w4ZlQ߿ߗ~[ +1ヒ!ebH "cv4cԕdzs0L-Gc8zqLC~t`?c`<'i˪129"Ö$(g%4FDIxI4 ~#HAX!I"Dgؑ4B`#żьS!Gi! +#c!$ n!V8 oǒ $tKBBU5DㇱbWWKO.0dwO(b&[AZ{l߃P\#I(}70Cl$9qUcZ'!8{!Rt8>dMh&>> L8>O!fKG2CF2̞Af^Z?3w/U%Fps<_`^Qaggה?0wگx)d]B[.V0 +ݞ/荡ti0>-cv@9Vdk9rW=Z(́h6Ys i^_3Hu@H`)-$k545B/Y Վqs={5+I5NjFdCN3 Ng$9R'؏hÇ$Z-@"jCfDlGLHIӘIe:l'n1&dgH*sdI2udw +򃐁}B,H+ ˲c3G`Ҙql +|<%(Æ$NȕT$g +[CFqb`$$f E12o(=HԐxti'Fe19"И^r8sT6cQ4IE1y#tv/=qḡ"mi$ѐ>]/%&QFxAVRzd)"K4߳ݍ=֛!R+G[0v/,9Cxwф1FZdL`"[f^QU>ȵ0~#D~`-2{YH6:Cd)0AO(FLՐH Kh Ok&Ռ5Ė;vjX FQ)$:XcUJQ1Hf`T[Bc@ y +18 +n]Ըj -55z=$?Jo2!فaf_agwO[ 0S,oC^6&Yak #R4^sH/`P1rCՁWY?]{5n`l&8ǖ󍱄<ɠK2Ga)HPbG7BjT MHZ9 c-9'44<ӆ|Xb  Ҩ4ojڱcZ;S!)Q/,oRKNχ/2$8B +K -[!a4UM-.-'׎ ɐA0cgs1hcXCo$^ !~$𞰖0_\L#wWa csB>CR9YmS!S1#,fwW-͆4hh!c*'94ÅӓcGbT|*m;Duq?s0&sh4bA aq!4Ot0J_s/],d 9 |Pcd]4d2'1̱\ L"G@zJ:raRtq>ɾYI̗h([> ?>ZȠ$-cM .cFÓ(T#hLw8#X,d9avgHð&6C.IQ)' pIB%qɵH)`P:䏘mGQe6@XiSŎj^3wiWKLbl+ +9N$S3;.4ј"ls! *|bCc I#E鞊1SdHCr$i|9̾CVͷ'$FщY`8$r =9:df*I2_˳B3 A#l1L ʷvx >K5mdW2xaI6P*o.jFZo{mK75pՅ4U LPlyEtQ੅PR)UȵWa4]A CF`e<,~d _#KŐyJa>B5%ȃho$&hc&l}l +˅l;|Wl(\K>Ƌc,~f %foZht_JQgA#//,62UkWGbV>H QHy@SH#_LH'fB^ebR:KUWcXe/8s8^dm0 +AՂYRt|*d7h<׶~j-lAV2[dE 3-*j[0ײK$f BHxkɅ@FY~L##VbF`ĬI3:&Yst bL=Zc9rRYMfd;(urH(q5cX@|TYA~YvrjI9tt;$ņb"\㍐kn,;|@A3|jH!b54F>', ߢ 9,,Sl%uh-XARnK\N +Mz f\P@֜<7dX;X;(Ll⭰HJMXnQdVC[&|b֣t>qG+$!dkc``Q@G:ctKj $xmpsPsC}gg!&\F) RlH'efE[b,).Ʌ$[C2ZVd-ћ"xxK/J`z '$t)$}8$ V{< gi}eVO:mqӟun&Sc-% 7R+JG. +=R` 1#7άCǧ@FИrjT-O5d  S o5EYd 9NYn )q*ǒe1  `ggK5W$\0~'|"YPv.dd@מy|'5-p Ŕ8RQ x 2ڧ4q B>?j H5pc$ւzY\p}&[~Vִ~^ +2²m%SȐqDql- 4p(c8joVx|4޲JYC::9%|R-i1%|',)Q/2}Ȩ;X|(!#H|fd5.$Sqgc?_j`5Qyꌄ1oB;Ÿ⑈E|ېHA +{rzJe'cvtߐ +f05[AiɇBT9BPS:Uk<=C5j!$Q;咞Y|->J,~&?5PJ {_AxOK5wWBP+X > X"OIқ& ?B҆Cv\hb|+I1Yh7G- =.w<)w\Iŕ)KIK@:S~>+Kţukˀ]pכufWXS`u?Iȇ&Gdhm2G)Afu dѷCd?5$ A$Sn-GW_F0daqc*G}ͥL9(% s8lC Wfvߙ^7A-=g@rQH$.o$OZ?Yǝ/_ ca^j9FkEXOP~)GȩOIE_R2z}襰|!Bob*cLrC,e~j}Ce@1ɓ$QL5B>bj#_ GA6egL䦫-rON횄\ qB*Hnxz1o⢜:Qlxg96໤bB#b^a`cwW_:TjuHא2EBFΗXNbBB zv1ʿ !DysV4_H"{%ᴚ@B(ѩvX;_3!!'.rNjxG ߏ/w$W[X]yz$!w\fn] "'5+8=טlwm"!c;o[>D2xlxgD8gj:7pdgaF:rnIU^" 9Q#?ʅÿ=θIk!)XaoGݕﬠ׆CÙRo_$5fCOg%'B/Fa}`.m9|H0`!p@֮<Ѝ'( '@M5}Q,[lVo,%fB.\t=/ OCҋ{] g ߀+O{BEh'%7s&Ր<0 =,=7@*ug GA6#L.Cq\NkNN[c@D1evi*rbk%BP>tB΄z {TD[l!#7N籫4 Bo A2r,{ !pEB䛐,ҁٍ1@Xk/tNGHlRR?ow[A|N{?aV1,a8 +ȃ=:$MQ =<547CèrEC46kvl٧BV]P^ME {X=ȁIZ+[GQ $7PBׅd=^ԟŕ5@b +9p3|I#HPMw5ҡF7䡤ߴiEppJQnQ}f9Jݵ3!{ + $7Z=Q[JWWH_A~{(dKf+=o=p\BB_l $qGDL P.l +!!rMH_oւoS=a{lXI=GL=:^-<7Ov JީYrFda=Q kK8>[0EԍE]juԿ@x"CM Jn* d>!/+4|FEAƝԪ!{w{j\vոxJS| r/GK?cMDյ8[K{W?\FJ'Ylȶ8sHߺb "w$Dp!!.W25[O<Ξ\Ӑ4ـPצ `X3q梠o6|O Θ;)jQLHn,O_-E6}`tZq$ѻ 3-;uoٽABjg7e'?^vxp6xC􇒭|7HfeS.5Nb\sf\wkZvnX젳=^ ml씺܎޳PJ([˵}hX|ePuk1K NENG9!?0[jYj{5zlף\MnW:q7\|g8R*o.R \l tіޢ7S "z &^XOG ?Y i|HU~R,G@߄`5[j8rM6eyz$0bAtDTp(w"r QR[Pg!]΄_cu*I +K]'>z=`{ݔ?v=?ƹ`sk߇4zJBG+p`;'RQxj${QwhO䂵 TY*&w=jMK|rR_턽|m~‹jYW#3쌈#@7Vйb%~ +!cgpEReWrf`O/aذGv/qV >yjLHʴQX}jRsc$2կWH+Q_}c3&&A/ +}|jTǑG[n./}?SxT&±B`iov)t>&5|V9raR88OU`O{YJBӁ/G=obr|D-^lldXx~pЄoci$՜["1P84ߛ*մsŶϷ(w _`N߱vO]9LZc1[$yS ?ADӳpF@)q<孥rFdj3^P gguYƊ+KAo>D+Sa{CQଂLʁoPa,J#D 32,!rԌh zƩj٩8+ሜ S}߷Bd1;>؈ $ɷ&*>=84*WuSn RVu‘3scq&„#bO; Q;A؝7B*Ȅ,8pCtqvk]/| YJ氚a0.$ + }TԔӎc r] p&c{~bp*}c!| 1@wNb etΠ\jrFQyo<,2NeD>{nE/<7*,"tI.΁gXE} +[YccbBE?`&g|/壇^%$u'HEf P^B =O;cP=(\UN͝.~ҹ?ԛ/ =Q)*WjtOqcDtv]3 :'WZ.u=i<u*͟l#=n6K|.<Gr`pw6:r}11;SQJuE <KpK=(\V?Mr +y'sWgz<]uvooOO߂u_xtJW#}K87=Z6t<'v|&WbvoS^fR~s)jE߼ꉾ};C&`q3bh%KgSу^ mƂ.~,?lv|,XX=LPE {+;ω+k]^:=-=!)yd΀>:7*h;l\.P:̨.-9E$A 5}I^tߔ.ou$$ D9Zx.]Ev2E;P"ydQv(>ٯ%!LvCm1ÍpeI9ї-- ';u3A<&,i7뭒z"i(&|˚/H_tHwTY*r2TvJ|^|v +P1<~ZCktN!jvz)7nm +•]N_\f&zxTNX苐x:,uģ!~mI:J}& &+O϶xoW_h9Yg/骼,.V?B"wTK:]t^A=SZ(2֣Etrj#>:F }&cN!jN?2o~5 +>S0[=CR5UoOU!N5Β=#bAbbP  {4͊l8GGG[„=[>;=7x'}" +P٨.Ci(>*~Tkх=ɻjG/%ɍ:d[u >ؓB0ap ^Q43[wTٴۢYQEq6(ۄU:qև9N¤]ogOPhE(CK_ %:tT̕lϊ MpV>x-f2@G-hqiO^Z skbY#꒗fUG8O,.ou2( WoDeg$%Β:wE$EEW=6ě^ fh5vŵ35ҷW|?a+?Od谨9@W!^/[C. *0*~|%>|tG[%"m&?5NA.~3z۸&zbcA^ZXc-)h1ޯ4-3'u'س>za#zrRIq%V{8'a^uڈkj\E^Ҷ}%1G?u^O={?zUh>PQ lp"^Q坧z/1V/uJoJ:לИw5=XgnHQ9:'l8hG|&REymA1 ߆b"^ǝd]E2`gV=6aws}FC|)e) y%&K˞qv]εfgvǻr%U#k:6J]Ck +ػqh}]$õWqdˈ3c~#$~3{pV!,|#?n7RYddM9a÷#d_'qJW_uWrFpP1ߏu\K̬IHM +iz=#զ~KMsg+D3Īa֌䣝qC?Ev8?K>\GElC+o{w`ך.* _+~igZS# s~Ɣmm7aOG1IcR=OT>4V875y'4fJ:.M]g͇Ke-/G_6G߬퍽TRRs9|sBj`>I>1g[6HtAJV6kydu'ɪIѮڲ]Zen1a1תdwb"+[K.D׸DU48E;ETŸk**\T\ LoJ0~!i{"Ú8%xw ȇ"ѣ :#-%{A2FvP$ÍOՑȀ;H:\e6X+#;z`>` ǥm$ufe> !͗3϶\}', p p߭x玘zunF׭NcQsdQs:טB82k㲫ҫ}jCކs+UFhFD{}w*|ZJBͻbD[O sG+CZ6Q]W_,% !5L]y y0dPDkM8y+l¢x¸ mq)^ 9۶ ٷ-4ů981Kv-n18r,L|VWP!K=ڝ+BwH.oN'qu+Юオ}#"BS/7=SXU`rbI@|v_[S,ȚiG#cXvw2#BC;o]-Z/Ug}#qBǫZ1^1w\cJZ2[${[3OfObCj/'$W^+u?՜{9#h{L:.ܲL2!*i b!k˶m9'sS(H|k5,}ՙ} ;s;r̤9f>zPr^wsLiSEkƐm'"s2=m'mI"|Xo^bdobROM-ɯY^h3!6fHQ+ssVO}Ǿ|0`Fmx]+qYwSc3b/^IК) [nQSd%t˕4Ϻ )7}qjN>yt:ڲ8ަ0=-3)(ӫ!:xWN[Eǝo:ڦA]WnC{bbBOb@Sp +=|s72cxotQdle /aj|d0n*Cbڮ܈!G O0Z{--sʅ>9b1# 1'. (ǢWп=kuy)r8l,UY`>Ps>_;n.4w}Ga~>sofisl``Ftvfkf`1s{Ɗ~ެT/{2跕.QOKcҪ}+/ՆFH͎,bBtx2РkkTBCp8C>- W/z7 L3;0tAakдRo /^Z0L$0LT*+UMLA('i$.icty__g`uW p+b]hRج~|e/^+{\!{S}'cw7ŞљU>q5IL}._΄Ɉ;١FYn^-jb~jm~J^?oڶV,Z )` "{WRw9 ϊIo?)M׀[E`,* %> ſ /X|).?[ػE>/zy;;e^%+]/׆$S_{́/;^p笆v XxVl+L:S97ɳAz`2+7>Ncf׃;Y7F裬C!:'76'%Q~5aO"“m>zSP)K,=֕KBT\xtg 7Pܛœf6c OJi񋀊 +0Uq9 L̞֪yG(ie6Qo=`"ńXxӑaۮȠ} +}e #נ*en17agrV|%xdPTǭlsgsTvՙ,``M@yFOfM7K,jv*gVs{[ۡvuW#Qnvl +}|_.,:P}e+{#-#]Ω +o_~)hT銪pQS~%L@ez2~1i+=X~:`+.Δ? cIH|{Ť"Ībw)Şy%-} R8z\LXIn_~ gqipUplgmsVhx`2TiKU$` wLVlx",[]}\%Y\B /0+/wm|#Qfs,\~ٶ0Nۅ83(spL3g9V97y$``vx֊8qLUm0w&~<8Yb-[^ My[/oJ[ߔ>Tx$Ը% 7$46Tx#Y3tSmϘ&wYcyW9U7oo3Tm _&K7ف5`]=} ՚eNGᦼ@>X;S}[lWG2sݍY]u]ͮ+=`-XN߶͟mx,?(+ + +8f(,-SNe`m[9;}j@䀹[`;X kdO[,uZ%,;X.::]hL9̽`{=1===-g̾9S|CE +@uئg'È={± `ެ`R ,v <xZ^`3X},v1`=S՟0kǙPߢg/=Ooz]TjsɉŰNhre!E_]Tì3gӿ\ 0z)X6q1ˀRAڦ +ϐ۷̞ L\}#<f@,`oKx l-+qRFv'#_~'y VQ-CsFvzԴ +DN1x8Z\p{PXTnbJuAC0­p3 } +[nTj%:@uX9PXŽ{9j5VfmZ.G'}gkKCosOmpKlrK_S֒d wҸ)?cxXXCY}5ٻτ εsys`R.XKڂ`0l:@~Oո->f1W٪p]L&..)'NP݂10TwIcSTT:yڮm<"/9ͲP q5u=-6`XXK (m9%,YO%E`i +6`g +[#{&~fԴ>0QƆ;ߊkʉcךRbڛˉ'Rbc2.D 8G& 1Skưi@ٜ"r/g(-9|8U:S69K 25zx rh"gjO=fVOۜA}xUMsA50k52&ӼOIKIУӭ2Tϡ嚏QZ=5-v3# Y|m3#Dy``:CAX;U6Q` l *M(nr#jŽI;'p/өh^ɚ ~2YZ*FFװu1"62bP΂ڕ sgڨ.ƚ4$7hw{(lՁ.1C~}*̇gYjXy +lz"$ێ]W0e@%Zff-}!Mw(lX=vוxUN9ءuV˜:P_J| Z/bQlj^Dj<#?8iP0=nN=lNJmo;N8%8Dazi\+X3v+u?Xwl<` m`j~' vb!r>Bͬ/թc AæfsEoN?\ ;J/} o0ۍ+!5e$F}|o*N;hq3a폣Too]DjMx%KRUm0 Ser0oNb تkva~`OU]:]V1gDzaָ?ڜ83Gu3\ ;#1X3з bڄ1*f U'<gX׍=w϶f뾓b8'yYvK|>NnS-TVn<eZiwP*zWcmk1{5mY~gLb̫>,F4@y}l׫btm99%U>CFczL^cn` >Jt#ժND"r2_|"j:`!`KZ"pQ5 ~+̛m*< XSW;h4M~X +>łs>1ֆ-42c^3c)a\Lz*`x <킚߭/GƉ3c'/p1?qYx4J *`/c/wwLDJŒW!^s*m] }|F +Wr+r>l*-S:zXl2vRuz[_U9BU.s}U׌VrÍdprW84xSpAܓGg΍zO1{̨1Z7ł|X8) v>lEnM_Q/,JGܑߴ-Gm 苅 FЙ܉KVd)Ø~졂4_N=T˨~d&FÌ)bo>Q_#/M߭ v8gLZK=Oud:%[ >?xskǍo?bԹ'g+/Jޣzn7d">映"0=`h"IB}Dr>7MQ1OxD$eӱKlgc.]*i +0V9/ZA_W._ 1?jÏhr^&&KFO[}%ԃ1v!m!߻ɕyOK.?4&mruCaBGl?}8(k%X4p6,h{K!l$} ?XΚ,\ڃǬ`X_Akt# eмY+=-0 +382;c%_q +yc{ѱf.ĢMG= lT!#c K1%Kh}:d˭DffEƮvzAp^ٓSEM؋Wx7kt''uh׃YbORR s"JN#AU٠9[g񽥢{6v,%&! x1ss +^\2g *sl|Dd=-} x\^(5'| nݎ'<&Ly6E4{]%w7 iry$090Z/c I)h tv5m@ؽB,;'wSl+z}7Xb,j"`y*QveNKD{0[M"a?tG-I~7!U'c rt4m+ 4y/ ~0_N0~![|4u3NnYO_z'(o=VcOV$`umĖ +V6g=T,NyNCҏͰ|,.?b,8qO&L w#~|B*vu<3Sٲ1bjvެf=`txwC{n}uf[f012͌Q)cy1%chtI1K{ +`E@s.`q ysbh"/·89}^Y@VJ܃Uɓ>HŒ}Xz"b3i7G}eq^❥er+ImRZZB27碕!W>6EO:nOkNu,s6w}f#s]ǯe,9^9 M1l6ڣ4B{.óԅ<'RqHOMF{ȫM7>\ň!Ҧr}~9\+\H2[8hʕ<:,z*6YP׆ ´d֨6STx7~V^,ZauoF&`Ƶ0f,8=97c6F>Jjj\˓!vإ $@W]tEC@_\ +]d.7[I|=T^yAbAg-{,BaGul "82IxuIQY``\HQE7b>)sqeZ&׿$Un^SskR7IH_S]0$vn&/:FNr;]+Vw#8p[qh/bIU~ kio +!},+y=rϘG&C<Ca-m]Ig>7-AG5AMAt:  By]OmEJ.>Lzy/.͉Zo@Uc SiL +]Aӕ)y"?*WοM@ #C4aN7T13W:=Fgᙕ¬=Āޮ ٤_" 폧._Y<^hK>z-͢GtmC<_O\~:A<7?bxh*-r86xg7t隊vBPxVrB8e)jt + M ־C@k~`le9\&9✺4Md}\ mE/SwKr{ +}m3LIXtH7.i0-auRǠtY :$OڨMO9B&h|6IK~7\g +OLDbx"C[>X  +0a\ xR^2]qψ :ho\ڪk8)W|| +~>l:~A6C | ~auaOJ<ͨ؃a鰈WM1e.3;oṟ%OVW/%1n庂`rn"8o{a1&1cs weذS{ω|sy+蓎Y([IEqY6ʯLҗ"nB{$nk䯡N*4 Y!8?M=}xPQ j +>SwpՎHG84.QO7b)M}A=vYM\A4!u +{ɷ>Ľoq\tԹ8^p칈xwDOGۍh +7bHŽ{NM"2a<Y짏 +\U#25=\<_mh0m0~:jYt7|X*2z~?>tTIDUœ &fb!wVbVeY{y7 u9`۪`Ì`M`СC@߈c +pz<< tVFA_mZD?`~\Tf!<]11{DprzGrFl YFy&EL,FPIBv[\,6E Ó>#W RJM]V[C_Ve2+gģOx1)cl6-\ jFo +҈Dlx%i:Hw q/.X=L9h?AWV}0,$eoaZ>!Q|JIevO{\6y +b_lƣn$  +8DA?E twey"v,p mz3g%CG8=}Xo܉ 1a^82?8wؑ߭e”=G{JL%jeIM`DP_h ڛo-_5iӄ>9yÚ(߁)X`çКWl%rZ+3wYsM̜5C]GR^h kK{IӊHeX9*N13'q]ѳćƙv/%KI 1pU<0w)rhP's; >W*jijZ5D7ĝ6 wP&xh;B +r[K9HϘXݵ[bah*p9(cCjxlvaGKT448`@@:HK 0,$;ET.$ 69ݺXVoe2KX(=Lj . aq0 1Ckm%4BXtH8S^ga}AB_AfQ3Y§Ĺ I)8H醰:S ǃI_j:'tCxHi3!71\FVn(@5bF!|c5y1}1b=[{iNGQ]Af>4gÌB3='Q> Pނ]?DZtwM`-j2D^[l149| .<8FxcmuU%> LhY:(Ѿ) Gq^' lt )`LXC銛VD,g_[ƲT㥼t` +::tgr )rGW|ዳw˭}GǚF%]̻"^9*߮'Ѿ| =(50| k0*QqZ<nktb)IFHXrRgqVѐk % - UӤC,;DĊB%O:E:Eq[rZjqx3!zQ0'~~K,C {"6*&bk978j E\عigL$s Ro9q𰣒1B + N2 XG `q4P>S *ˈڅtP +` Ⱥnˌr8!j>X-Xjʻ8৽' l0?ucJaJn1~Wd'oBBHXˑ6cQ !SyvʎyfbvTld.@1( pSJH)hϨ9H sÊ۹Gd<<V.csӉ.c1SD!V{*xu97ҨGdTS̻{f'_oZ<s3'ӛ5R z7Q,[%'>=T#+af}Q1Щh +wIj#~#gnf V{}Xj`. sH-!&7O#~)bgay6 +@mcvHn6Ғzo=.K^_1wOL0:+ظ\gcG*΢}He> ӫK8Ehq\9HY* +[tɜA"oАm,)r6`ycJSO6-]tИ4&"}e5O!R|=F*CҕdI2H Q:\Xg@|c+{s=XKƖ> $Y\ZJ!'¹Q䓴P9WU(ӤS+mbs2WX41dΰ!7h-:)AظKW#ĥC2f2;>H/RgмGkN-Df8gSOj=H(Xݫֈ{ <'m, pg~|1o˷J ZM񈢍#a&ZրHwKCDLH"hw|򰼑#m/n_]I .&rMC63Ȩ., WĻ>=iQjga0/?lbj By.>eIQ*u_x(ֲ0".>0~KҠc\g JTgOLΕnD_g6Doq06Wj6g=&#m +Vf/Dgd{igX;sO m!y1j6"E蜨)kױH_^pAaJoAၹ+a9먘EiuڈKeu"#ڪc˝ǢgH +--_y5q[kuCwm̮+'^@k|suLüuIV9 +圬^1Eby؊X6Sc.WΎA96EڍY,طig#,{M{GX {jg'al|HpJBSBeR +m(eV1vMlT"gBLo{rF:[0NiH5rrj:h7XAyZ=,L'&Ҷ?YܬPR^34w؝ YGc{.j|HyNBQ⓮jgE/ s 刴Ez&cN 酡8$;?OH;e:NꖾPb'3{4Pt t,%^/qw"kߑ/wx8~~-sbV#&OB[=qrJӴJ:[7ew;߹͚;/}44n)E^ًϏAk£R”QGYB etTn.sQC~Յ1Lh'tgcO64/]KdBDh}Q-tιB`jg (%hD.G +8|_lQ|X;v/u/>7q|4f=b抽 It=w i|sA(o\bٓ]DֲmIu:VXܔC2&7R4PNE=&FXm,%hZ@<jN:$ +p'7,YTy-=\N󠝥e(jƃjSVS,%֣g,?va--%ہy\8&rO4c@={+G-bt>L%=<Ǖ%r/ET4E1[  e%?@Pne +@FϾ +k-E\Arrۀ>xPm|F t ' +hsn1e 6簇1R|4hR\IC|.e4V¾-T; ,E;˳jg#]\$b!CokJRY-wQ}ke|SKCW@3G {8_!ԯgȻ94)uKSK*k2ԗ[8R9'8>f0Or},jg5$TtͰp  cn-wvKJ1RґRhXCC3ŌˋqgARt b㨛(C z9./1Qzx-b=fHAcEV^߂=wq)WdktZ!iGKFP gPu +-&aErY{EqN~,8[M6MN4pǘol|{+]!U |2rlTĈ cb{@_|#zb\ejU{~HBS}Kc$kTg5Oa=%U_R⩹"UfpҰ*CS/,pDw-`z, :N}zu{"U]pgRۄ?ESG`|MXʛ OҙA?N@GLvFƚ>Ƅu u)6|Enhg)gť>z"T6\,9_NzVrrMzޣ%d[1[IW卧X}h9 i亣K"˾ZUR|﩮-\vE U~qŵ҆佒Oτt>$şڹh ɇg!z.mP}'c\#,!OBI&֌Bo5=ttP%'=#k-Lo.Cf$_f 4P7 QSqB'Y9=&{E5<8j[zxmxL:>˹51|"49-|ŰqЬ}.S DNI5 +$n xEqz>:~tSNBMC:,)׫5^2sը j|A 8%| 'vyu!B igˊ!yB+MI>2'jgEm#s* +XMy߲oa4ubT>l.Rbc̫КUx E|mН"sK"|YJHBq䅡v9a]J8;,yla}ݙ n6Gک*ؽukH> _$'cIr}uQGМ9r'CBLM5CAw#q2%a|؜;Q d Rjyc'A +&Q99M9@rYԢ8{gmhm~rI'g9+{AGkbѩTB`tPF/vu֬!j3СVa փ-@  ,.Bž򼻋f1Ƥ3灥Dd} /ңkBĿ+-gu&+ ++EaT&2﹤8I޲xœ'P޿{ϸMTn]XS__ĕ^Op3˥XD}.0+ޗ2{ ' q+EVçҵik[-佈xU{2P NgSܲG/{44UpMXc{l&Vd.2G1tP (z X$F#ܔQW>#F#'E{gĞ% K_ Nвw qy14~ 3y6]G$ 2 y]romrr Vs@K%qF=J S~j})T=k~חb m;jȷXQ'ϿOk=#)RO_.ϻDsszQ9g~\1T{HB' +$GAP6|bGf&I5ko/Л|p;{ aGu>3|M 3 9.;p[yb~Զ1MV-;K@O lI{'aŢW.rgoH?9WF8WSxL]h:S=aW ̧tnQN>7꼢fBp(8zA8sX{F'EA-d3/8=uR=1BuɽҵR&V<5BE6F{` +^G Ov6)f&c+tA_#{  .ƥ㳩&8f>d"ӸI 3gs^#aKOTcM:;|aaWu*OeF;}80GhFM_z|=_z|=_z|=_z|=_z|=?wl[gZۤzbK]::sW{Z`ur<mo^\oko[y,7m,NכE^:o΂Ezsm򤅍mK K-~yK}?o9,Z^p΢K9xz1}{7qYgCLg޴z#cֻyr-4.zi%u&n$_ͣ8oƑH((,&M ĀLM3%4"2Fj$ck!h(l=%?s- gAX:D;Dm) YMQ  ùClܵ fF޽I])C92Uf]Z6鰕Hp [݋UbzK.AEϨ!i _.6sRGHc!z iPIIJ7;p[߱"7sIkĴ Icë%(JX%![ȟIrQzx Pzŏ%:dNm+FaЦNA9hUdC+lhЏgE34BpncɈؾ~G/]dQCA;^ 60[S8b#Ж)4(W|QKa M݆&2ƌۦf |ؘKM̘[h [as>׈ Ra<]I@XyFF.9kh/rJJb/% >Br#|0|#HT9G~hK&!%8$Irǂp¼__J'6/"+&p2n_\P9chs#vCJ@pw.%"NmGAl CYchs9)JŠИL4|ż&1o@,#RoqAG2Cri&lͷqGZ\i  ,>62l<hic(5bd2=dmė{Ak8|?m9p*e l'qڀ݉($#w 2^9F1%kN;_BP/H|AկCŹtn7;`!62.\Jxfmtcȧ_Ļ$ 21v}u㳅sN]Dw A%q d\=I"QFo2|g] N/EB8ئZ tMdhs-o3h8qO0 ;/4BHRD$-n|*>6cIr-HY~Ͱۣ _X!O3j$3NÜr|% +7]4I%zM(ß 1gft6A&=S чs3A䎆`v8\P7x(@ M y@FϹG@\ LJ + !qWpj$V >J$ߠ;#(IEqj;1r(R/=R=j[[>Ch,`~Hd@G,//s[+麴I$$opO)a HMf/9,D~GEULIG"~\ qh6Om3JA>m$gtBȑx` H\VnNs=\O'3 +0;$։[ +!w&(3Me$WQXi\sc-;k$qqh L|b2ĸ30An{īNvnd{x3n7|AhNB^M;(<{"_wh4!}Yil's֣/i@UXħ\{& oF {@MBrq HT!S|b0 nǜɵ F j+l #T]dApqze,Pl'>hj2zf@I#Sk:()%q |M rw@|{b{k.Nkn `+Qrۇ`a h<%&$0GuWC鏹ۤ> >y!qYyk#`n٭ ex;M)?6O| + 3 %sH}$6#9%c&hPL<|خ_ob"_\ v>1eV;{9F|,$E:DyQ_k)K3gFܱ#lQdIQ\#u?LJlW9U]l*\w{>5 +NQk!#w%U6R>"?w4!le֟(Y,rE=! .v_tPܴV([ "2FWB!/D D\K^=C?=O8˺XƠ02Į0)9㞾"rN?ԍMb Hu{[퍜dU W=3(/& Bz$!ul4y~Gq[`_&+C;#i$Ԩzh)hQ @v֣,S∢o}2Ph,|O 34Q8p%<'~s`њ>jHzd}Se@"-Z +$_jJOS}G)J$͡b@hxd*)yuנ"+Mkll"(A rf7uM=CQ_D9?1B4v9|Kx!N gf%/VqY sa". +~cs+!LG۝ELFrx޷ք1{PBIF޳Hax_.C[S-*E՛ S삷 Z br:ag0-wF!?ڔD쐒s03;RQ +pB )qRA]=r,% /Ty:͛k 5|5 g OςOT[ʥ^%æÇ(HmC )#ʿ>w +ȹ9\܉[ 4Ԯ$=|@q$ Bnl ++I)Lrׂ5bӵAƀH&q +.&%17o 'ٔƼsş:0ST4Éy [!<1XO +-@I <@p)B.,>Bm$W #Dآ?|}12E|9oB 8s'_| }F endstream endobj 28 0 obj <>stream +vbw3$_[ZwLe<+`%nB]T '69Wr2*v@6ݴV+9@:0GJ".3PO" $-.fiLui" yC;lNIs5Ki@Bqs"PQTy]LyJk*[*v!}ߚIl"gXk/bQ:'~ v!ܡB*+z.ĂBqZ\C9; @r/U(It\)9^)w @ k ,UX-|l؆/f]׭@ 6q Wu"k?{lM,4 ֳyjB[`A.KV.ifvۤy5>Q@<ĺ k.=.etJ:@yw_꟢+_bJרA~ؚ|кG +~ +B-$֣Lݽɷ*3?.J׊7~viQA/i$=.0R?]]G*ݨIdۗM !%;RA2P7djwqG +/}-Zͦ_OQ^<=Ct+Q?1 +9=<ȩ!2/+ kE.خibjo1eHޯ<ȇ)/BuFYܐY&ⰨZ8@,WĨa  LIsz!j!H + 37Vty̩؅f[ja %\]Co輸״>ݫ/ƛkЦ_ܴqDg ?@a!-2/󮯠~Ne\\e_[ H)|}~Ajh^uTJ_lm/),oRVXH:M؊ '|ܡ)T{]- -cUO@D +~G|*v#v!؅۟bY+-I.քIgjs- 5CGh5J<:+\{m(ui1$!p,[յBr47%#*1I#!N +dW3b\Xq.`1o%jR>%͍Mk7271&#]dK.QòP@Xɽ7'./(ݳ7Ori\TZ˸X"}F?!mG~of*vzzlF /b*ő9 v Em͂' ^(F{>˕ չ'u$%ǞUW@suqs*T;D1S k^j7b|!E}m~JeuPRϾ~jri.~<7œWb3E}XQl#[O uD2)%)Q2!,!")LH{{XVEkڵlU_Ԟٌ[ v#B;yaSKJ89b\)tcxBᕓ|a* "5AWJ,kȦݞۡkB Z*rtI$iRX_<׸՘ l9w(#DDׁw& 8h8YLH@l=bh$Mȉ1M{&Aػ;Ufh1B~ǥ#/oI^p){X==BKg*DuuE +HQ +B}&tEiXPG fA\whӇ}ػsT_:yj*_a0ಘj}3̇@ &y_:*rJ~9{ օOPX{b͏qc:{{[KabDuYh#Oxܑ`*=!fpP>f0t֢8lCJtOKz/䭨J3(K(^+g@$؇|fpz6kXcbBE?r" a͔^ +z(Z3l4<*z:ֹ"&Q5 S'@~a /o[ qJaw05pΙ!h+ NACr. j +O +QWL )~2,ɧf#g0UkQ "Maj=G˄ki] CMqniX+7"=f 1~Y/`5 an/b_o5-v% U8н)b]96M/KEg|3GVD>] +H}#t+}&M?~w +;Fݣ{QPGY:쩷qڒj>!>tQI4/ ɉ33S7b_R|%dOLP&oa/|ՅKbN) 46W&uGԌ#K'PD♢Olt5jDܰ/z~u!byPaOXGb`lهSL  !Nce&YEJ.;CYq +I#4.*;כE-+ݧEQ$f-:*ɏ-F #ŝɧKk>b([VN"U9Ц4/(^.}V.B+kW4:8-{Fh3;7 9*KнA̒өBy|iZ +:qkyܺ\̻ +/{P{RQx3bʕؗ(~F4$؞z~cȕ[ϕ<_u< D\yJbem?ski]O9?SQٹ+n_KΤ#3h-=>~ _ClŹl%kmkʶMbuPٲq a'kEY*a1C;BnQ'Y7p^[^tHS8y*7Yd6wV+KNp"c~e$?a'#űO&*[^+73/G54[{9j~5X~g^v߬W~gRm.ߡ<ޠqUo6 h\!HqR8Zɗ_`_|V-3^Z;ުunז*H~Pofv1_ڀ??b]|쉩f H͸E!J_ +7_;J5;k-XG.[>W.|W^VlpLv["2P3:2U{,>`ky +&[޹-TtmduK۬īOW[\dE~{J]ʑVŞ{ ;m[m;ݹoݸ?;(nVq7Kٵ_ܣWn統qb# k[q0],SδY'[xkPDuI8G_cYTp鹵㟌#ӝsT_mG];\.ŕwؤVT|!{'N 쭶m\s- cKͼNn˗Vqf6P˰I}9W?m~ +;u{_ݣ,xK97G3]_ZTe,ߖ%7:'UwX N͇~ҕ&g6Q4XeۍX7LD7^ߴTvSyo6۟/?^ [_pEHjSU]˳3En:][SyV7IζIr?ݸkmeUXfuQ|p={ꣂJz[u$KvN@[vO'n'nVZ{Vj3z孇w,*^-H8o״ؿ7m^|z(U1U|+k^&^Yvk/Q}u:\}{⶗ײ>g((+zqa +MVv)/t9+e<9˿qgv]w^(|ۘ!tI/c|٘tn87S٥ +3gܫ!m//dkNeIㅦUJ݆mst+ -Ol- oJ)^ZdTtgo>فlw3,}鱿b"~FBٙ|꽅\kCsOv(C[V>)K?)r/QQŲ6LY[$\xc-Yf\yʝs?ʕOj÷Thr6I|e6swkQQ䓤'IE%yQOʔ51绬W4dT63e͊N|X՜L͛{Wkg7{K-v?n&~j2ƥ@G.GJ]ٚRwyÿ965ڽb}&{\a;RE)wTJ_ar#m*l+ת΋zМYY>$ =)}o?lNmxZK~kI?Lm`femhVrm\[Ky⇦Wnq\q-cYpgߊ⹮O\/YoP۵[-Yxſ||$M\~x&z0mx`qK/&gUۃTwaM)%ͱeګm{y9[^˹_Ϸu$ǧ#37qD'>[Ӯz!_o>I'Ri2B> +. ˎjHU}bR-?n{p(yUytaMU˽$ΚLnZyŻRuL:Vyutk(D&'?VY?n>sEQR_6pފB[kgkI#Vo_x3]Mksק}۲ +> /r*Qw= u:^ޖTќ$D}1  8]{䯾Zw&( u^mDc,SGz-z"=|->hcnk]'dץ4D&%/߄0ovNS򣸂{Q9{Ed<"U\V[HMcn#F.R$=N>H9|KPhWJ2?[y?^wzYK`㹊?^v0{.w9̼Ć܂Y"rܛKLG7x2avҽe7ݒnn}7gnw{5F7F%Ȩ[ڒ6TFݿoj}79v( +|UUST(,Ϸ5L!Qhn vOBo[Rw78ӻ9yu6oؽ:,i|YHSF\ٮ@u+uv<ݞ4Px7?'6 O~\%-oaʁqkW}GCrL}BnyM={aLLC|_g[֮ N Q_{yNhoCޏȱ}Ib{ yoW'c۞Oq } +mn{#Zry]hSnJi*$ސxh_v^^e2xv7uV40,YYd53oя%yKL@voPTZLYneߎPJ̉&*Ox&U}.*H+,AXr",Z!׈~wڱN -F>/6VYk(231{2]f3<3~FYFd6KUaZW̔㘱1.3Dft/=f\)9qW2Mݘ5#7][OmݟݬF޾q&0V_ua99Qy1 Ea wr1 SߏxE/Pܦ6=)d.̠dӁ?l +<4bQ ]äz~-68۽ Udѓ=F2:f ӏOOHfp=fR3'tU^.8~C[F_wcJU1\l{%Ltxq$71)nTV;j*oGf Rx&,c;3n?({U޾;[/}x3ѹ4Nt/^qckSY157G5WK3e0L/O\h~yf UOZ߷:әy DEc3wWyԷ9rGgݎ{;2k߭7ò +Qyzhq27ՄO κ[ېX''s螎$?ۓU񏖎̴1ӈ#sCJ^fyL58zhҫ+qt>CALoA䧑衳91u?;([]{SYogT ϪE|L{yo.E>Jɇmy~:&2yUGG"֔)&vY}c0zGkKϿ79C|Cia;|߮5?]~#\3*gwRu6$t\t8Ք$;ueMb dcO_Ⱦv;|[!Y-5A9]2O5 tuﺝ-~{rc;f(Q_n}fDcf/_uoR|~8Z|9od_{^p9WoܹsfhV8uh}ZnJm\Nr]Lve=ɻՇk3k#p{#VjΝ9`?z4עvz,7}:z%ÌfjOfƎZLYb]{痁֯%\Q|ح, y%^YqoSVj<AHfM=X;Ihͻj/^i}Əz{?_z{;ӟܡ`:{{3df$fhYhM̌!7W˿DpĄSm凫mjc._YJ| 0>[oۼ2ӻ=3n /`dFk Jg3zB˙1ÿgF\[̌Йnj36`3 $]L^YjY@4n֣I.ҀN\*$ٝ"+:ͣЊ !{67!ZS׽d`Fi1C{"? &OXK&0#{Mc 71p3z:imf%2_5Fx}#(Z|Ko=Ww'aPyw䝸uk|Hݫg]?&|D|/#|?(7d_Č]Č3V=3~)3f3f)3jzfwqS9faC`ΪsW2$gQr'65fٹ5j3s{E bf=u^PFߺ.sJќp9C ~/ =>ghd03Rs434fй?1w0ّ3vȌ`FO3'0#Ggf\fE/7붵i=VWTx)!5 +Iϻq/PaPM~y5= *&5{rb՘S6 zy{=!Ìό85lݡ[ƌ5|3bOMbMs]q̢m't55K?u? zjD[dd= *|X1y Kĥ}kV010-!y5̹ZsAd >gk[YBs5IcV%nwk}f:_LW3m.jHȕٷ5^{nFȈ{ϰu{R"vQ{/(vAXkU ґARl ņ]cXc&{[v.9g=?Hu͙k˘?i^wȹpin\/F_4tӢw[=>3,g=n7{D*{A Q䜆{'!۶ߌ3,&Hj:̰K9iv,3nI3s53ֽ4qaFx2Ťq=cxcX!p5?:;\*|x j 74]!uÛytRÅJ#CLc)͵#eJb#N Ϗ$9f@r~VNLQ7GI@{|ǩ(f8f"fL`#3ʷuإՒÆ $}uMF0Lc5ؠ3Y:f @f_fP7f(OfЌf2jYGMl|d0d-,[ϿE=^[uu}C'[vF>S{mA^wڿUM> +'yָ\Ha=qƌr! N|Rf`?IkÇ,b ]8 reV0ffjfL_qxV3Z.8mˮfy4(8UQ~aM_^.zhK%[7~ S|l :N+I_=/ a뚫ٌ_ϫ'Y^ҒfYC~d #x(fg3%;QŌq rT@̛ : +f5CÌsKa0ӄ&yG&꾱aR  ˣgkķm길ڦ'C~c͛9_ʮk^9=29́ |ؙNec87\aɥ ! 6?o~oZ<{/4L\~C/D?ܐe95OvRk7kxy混}^$߽mZaȌ 7=ُL೙Q̔S>3seytzy97-j=kf]Ͳӆ-=hpr}:}a}C b w5"gy%sm +A,cx"'~be.3/_ҘA$  d/Kfqcf7qg<ۆ9n-~on~fo.zm×6"_ !ޥK?]- {Wƫc+/K| +lI+HsP쏡W:1O`SUC{XL=*f%LU3SӘic<3~3a'3e媍楞5?ykaW5?^TKo~ Mrs RAP2&JK_]꣭//lUpmsJew6޺VVwF;7U=<#j 3l\f fW<•1|fa\k~jA4[borU?f~J_ + *_ : _^4x|2/+W}yʢէ A7oc_hHS_i T0bDXnvC^v.M'tO>6Pzh:w ^Sf)a6cZۦ ɳHg3f8/}ot)5+jH +! \M!@ξ5}kS4H M/*ˎ:(:sVGb, C*N8ߟw-|NRob&Wwn +z~p^5ռЇTg -n5NO>C\}wFq \?˒>&Ù;QÌLf\zѡ~۰'bs;,h|O6o_^1(6R$0UJ3Wo9+|\.=~<:GaòLc 76֖ 5̓}^|/.]}FF޷Kxp{qw!:S9U{F_B=H̘2eK +Y~A!!k]_J_ޕo3wL&0),+okEtd=i U]rસh f8凿y}%rb݇_b2>΄r/nwZ-8C^B6qUPVfE)̨!4Ft?ҌkBx6rPs) K|fг}Tuo7ޟYpu/ſkūn<-Nr9mQ-2j<_^ʖkUlnR_[ +B3]B 1͵_>Nb@2j(`0f2\鲳⃕o$Of^=ѱBx9XoR&pLUyc'3K&f1!pBaE}Ob|aw+G/ޗKNn>'7 +eb6 ?U[osq yBnuǣܾ[sT&*:mCx`~k9'}Rf k!({仞EJAO}b z]|^>G00؝Obqkm vBI?u< +DBh:݉a7BˋlWwv?Uu g& NV?\mS}e J-avcK̖.U0^J4Qoa3~:Շqx-^ޣE#'{z2n1>Z3}IhJmHIǘв ҕG?c_k?[eF]?2W'V[~w]~*}%^̭=0+>_>:l<{5ߘvn`O\At?tAWC?]CE[+w2?jz>[?}QO)GΝϸ,1')^6tUbM|pO#/KG +jakTe0@*h&2[5P;7!O%r~TRuO˄S q.cS +i\և.¹w*c=]jy"#GS +OZ +Ɓm}N5gjUflH_i̿R{uü X K2էWkk?-w;nw;y?|G;_Q(:X,^/Kkw:G-ɏw]\:PxDn[77"^:{i*mHE}ƞ\6D+#-vUunKѽ3H4np9`ৠbbbM.N򚾚83+_ nRVם=L_ 6}6S+_~x48cت0g:~_{cq;1`r)N#A~]%G:ZV?)5 zN#W^cG0_a5&Bqp+Q'<{'z[ ]ݸ=O/" 373k9$}n;/hW%w^*DU4wZu7OߖqVR2$[}tg o_'S!PQ/u43efHs8/2sdS.2\O3u],!sTK]o+$YB+ KmzՌɶM GJGZA\Ep>N`Bfu?9 끻̵gW!܍Ta/I%G 5'|B푉$OUvջ*;S{zy!h; PERWat{^b()@w?;`!`7G&kvޘ#z$| + ?yɟ~)+렎+P^`S6󻆂GR1w$[!G'з"t +-2k#$2P#kOLn.ڟ.|'Z΃ >N> "Tվ7B-=o?y!?UއM̷yz L8ꃱk3T-Pu:+0=~3~42f:p,f匏 ܛ6Iox{*o_% O4aA6X`|1"4q[/͕:,;%[ ? U{`btgZ +nޟ7ߝk(¯~Y8*H>"B}^ ؚS\:^/PW7Sƾl2,ӔKhZg> 3}8feR7 4"׮?AvS讁C^3A +zE Cn880B(4H(mu`۞,}4Ui$xbF+hJOgRf g FUǨw};}p;%'wn1'2TXmVYAA&9 +L=uH8{?+V v NLޟ #>|r-_d;R,;n4 }8|V;y;B>H?+Q` vE NDo"ׄK_.hNm˖t WUfb{Zb5 6˜rZnVw ;d.[,2YЛإ TZnF2i5kN1%eDdY3SU0P;8:B#E뢳,;G76_m_w4h~U}R 4#OT꽯T\Ɵ~-h)㺸s$أMbluEd>.)6}2oPyW}_-uv~6P:U|n>,# |Pe0W.3K\2lzkeo};U'ư[퐋z.fk0 FVG#{}CΦ`CW zxuעCa}DU~ ,:hqLw JH%H|i^<+&Q~BF;!Pb'$C!TT`,fۣzupC*1jՒ: Š]^h31$E8|V{N:x_hP`=.{ <}o}}b΀a&A&Z*iq@nX~ AAM jh(\BEJ` ;2Ü-ͯ7mWiͱpc$3\x\}8 VPHQGyILcDC`)`ɈۉTsM8k\}d2e;2|~&}.ObNK =yȇ(XKǟ+t/Xrz*+/z 즏a~1[xnJfy tq3zĺDceX"pF 2{*op唝[jVtKu|;yTzt`*qMg Gߪc?~Z u^&9eG3AH#zbA'YSUklj +,㉯Xg'740  1p'XaKġ12J9UeyA,nxH sad t(3*C>·K/@`xa6.3:|jk6$Ƅ mcJvybV{&8H$Q4{!MC>x!gA!+)g19JRfd3:e\T?Xa,NcI`F i5^j)r$ IUƐ})BRéyჩvv=6Ki> +>xKOِgcM叟W%O5<8BH(e+Mw Rj`65?nȟ?~4QEduI&>vuV\䐚dCRT35ṁ2jo>GwTk>Bمe-#ɧg*1xkqdMfJYCNkbNļ: -GƙI*} Pˌk*K/+T< z`i6J$Ò(tmrAJI4Nll\sp2=ڔ5} } ;r30Cnva`j{\2 g'O"#Ė[q+-4 \T9ka棐Hθ̟:p3Hgg kk*ΨA'>؏~,fRٶ (ܫP_4\ˋےaGj ] 'X[@AF=t4qZF-MщyRRfFJ-WZ/.7~8v-D&n:_l/v ͵]Ƿ: aڲ[t +X|ڽwUoUlX)9>Z{2t=|W~4w=q,cĘ +K1В#OE\iT" XqQØa- l"q^T1uX>O?s_xڟ ;\-DMVT1,Cp|cˁNN7_+}\|뼫麭~"4%#tEuC5'fחɭ7܉mМ6BŮQ$M<*[hbm{IݯnuR𠅊4?fXF k;.چ[mC/g-\P m-w]׈ԵJfШ֟whivTs\ m-qg-CRlzgvwj-ɟGP^ߑ ++^Gw!w= +Jztऋkov"RTYp6G}`|xJ!F1"n7x|dOzit}tY`|PjwKSzqt1sO)ŭ56d\B5]}_] t=X.7̧ڀ/B?B+X{Jw:xΘPo91};wܛOl6;n/@ ,u=``O-_."7t5#츹PP\Ih}D㡏rzwp95UfXsn<1SsG箠i7>R`IeU5]_+:^,*ǕNoBH)u}5֨uH uD)mS?HB3؝J6VS1U/U/5NJ[O&izoX6w BF|zj[hIhgq[I^vR{Aev(^x) LA8_]A]f 0P[E$ނO+ h niJ+l֝ThldZPltY=,nqcD}`q2|nA] )5e b[.V$ڋI^W9@aq|:kuxhJE5.+Gf#6kRK kз>V"P#qWj qY?A9uzCPJ~ڪ3Q=$1N1Cʹ}(s9{>zG\g;S]a`*j/qݱܮG37ϝKםBu;_wnt1Xh>zGb=e<_UI$ 1SoNTb+D%!.Rݬ~J;0ɳӦkHc\nEW/M=|➯}v@;kE:<ڲgŠqlu1V8Lٰ s2m)Sy12ЀȨ nw +6NWI(fm?Yܚ^עC_,?`3˕LNӶ,uS=lO}=DvTg(i(X]vwԇ\G;jPmBhSPmxʥ3lO,m˩9Yb[Кkjȩ|ZS>I."zA_^|o|4^g#~ 4ëĚ}ƳΚ;JϽxZ/%1r;?o=u'_P@!H P{$^oGkNL\pUd)X2usBI MDmZAs=^l]$W=TsMEЎ&Yhg58{a}PʀvQ*g :]0!m|YEyjgE;/7NK\mIurR uбorK_G|!WMLsz`VþDbtk ~'Mwɝןދ4hhjwd3ͱ&%!C7+ 3R!F,tJ;7!ϕsۇ"Aۏ1W !F*!vbJuXWZEtu=zmkM:/.$ t~,MԒֈ5 +kD@="%I[kɲD{GCS6os- .|uv[$'~`v͆S5Mm#]w<k`qKvyOIwܐb PSd+¾U9 +-TAH[ۇ O4ZN$fv֎;λXKS; V?Yέ^쿵Jz3E|ѧ#1w׮C+6l8rxv/E^a ^u|h0frM.Wɫ3]==+v,Ifq=)Jq+i0J)#ֳZb%J e(/0*FGrԗmtTqP-ʃcǞԬ܁} 7@암 +#@;8!G~ץM/eSjF9"\H4$ .媮1@];Y˭%Rwd6u8BBn<9SM ׶o!PJwo.es.G$ߺL_i.*$qHp홠%OLɃ|ۅjgQOvU,?EmTnMv/A?Y%=YJS;+c‰Ԓ9/㋶; $eT8 +CVp|t3PdKz8ǻhɵYQ}^modBr!&_Γj5Z'ħCM}4'5yd~/<'P1j}ScuDox_a@w O!RWmn?OabLCN/+IܖOKj9ja󉛼?#=.BO?,T;4MYgmje_5jm+6 \>&8[zv1-x.bz\5'f-8XׇŘB :RY+=wh ߻i6Ξj8ǭZխrU_]?pMŊG16B-Fƈ4?SdTaFbx..w`h'KLvdܫX>_JE,t&Ju{~HA.8+hAc$kTgkn4q6[5xzttٛS}fĝfǧkH + oh +P<3ߐPMh\&@  hzCy yq_EΏXj:MhX4Nn +:_z~_۲D'tO2:e;Fʫ*>DH̷})sH`w>wvD|H{GKT; Z!XZO쳲9_hghgɛ?#6]A[I69\}h uyW;FQ=cgH[/ΣZO_HhUuOn=jOLt>]wj.{!*L=8kwyEtm+\Dj,Zk;>EnDre$*Rh9|ɫmͲev|j D.z(HIuضcauc`]`T4RW4]6hnGDŽ3t;W_&XEè %MɸA5Vۈ4G8:^zmD4坣-m!\Wԣ/ljRe ʮ3r=ۤFCOul;z2GJjg9XזryߎZ BzxT Ry5x WA|r۠;EXCj;K5 ܵOM&ȇCO]]uLRϢo ]_.#3|Ձ%9rt^7)8Z37]Xn֯4HNM5Ww9BwK$gc &Q 1KKzQ-OY5Nз%IT}f9s ,֢83hmzjj?.|0kbLK62k*(^/4ZFBg k%;<&$mСpiӅܒΣ=t? +c&q4ڵyϗiRJk!i2`_xECfCtk!՝>1Cl|횽㩦94oIGs>@Su4gg󻞸sM]oqeV4&?8 ~&S7Օ:{BĿMpOO:y&TS*Ze< y2ŚDe6 DC^{&~ @ۿ\$n8sK\$x {= +b%g6DΊ>%^B h֫nth ^Xh=X NL +D2*8'VVȫ]s=zNӬ?0kQRbK5HF嶺i<73 +bi+M`O_`ɂ%yh'[aRnx4~ u0lz/4ȠWwd*[ NFw_NxZ/v^C=ԪnAo{~g!|f=%XT㱛v5tbpj}Wѽ'Xk +BrǝlMf{:$vv7nFb={6ɑG`͝=c,3Vts@NH̦A2?e췪;Vz0zləb%Ҧ.O/+F +v:r͟|bE0Ǥ.G~ \۵\ӕE,)gYhKᰇdJCW[i VZ$wzh2l\ָ)MRVQ_.$>u8]#57p/S1AjA.;$oJ Զ1Mm yZs@O li˻w +5[vFIu=GЂ`O={%>EnMg'T[:XͩOp&yŚ C1ǥzO:WЊ:;c rJyl@Əgb +ϕk{zb0yNYُ+%94;N$6Vv8hQ2ܧ]g: u:?l8>^w` B7/EsrmtE#h\>:jsMjdD1=7{i|؊N2N%_J_H{ AGn,tzHH#+mtx?x?x?x?x?ر CSCmDon>sUR#SlmOwKI]9 'fEVB>?mh$BR"DfM8M1HD +f&DǰbJ.4DJ4|L9B1oIy=U͆0#-yU&:F 9Hc$@ϽrX:lWAG(g~^T)=J*h*U؊u:{!w.V)К2;)&(:]L9Ow@>5chcA`VE)UF~J-ڒ] R%9k(x :G՚xN);(t*Ĕ[r1lX +K _ioA紸_Pxo,R0l 2jHe֬h u }y;!F{FN7tIBTve.ZV767R[7;Yr;k27k;b˨a?R\aiJ0 [k7];+HvL tP*II+4g'R2tA+4UmABW< c"'Q"dlR`NJ}SKl5QTy5r\b9\ >`*F&$^򺪃55Ǧ26 |K9:@"BjH;JaڢxyrӍE` +z(BrZGƂ:N%Ѡ(P%b"3Ȝ7RrzU|?Zň^+A( !ڈ9[YJo/EPRL RX[l"iJGblVo2+Vd?hYJq.e6>Je.T}v@)]p6.12%Oǒbqd*;tR^z:Ny5]Bd +U쟠pjnZJq9Nn"k%>LthG*+167:a3~Z*)9]=357ukS+iRl^1ϭ]{kc@D;:Fj [_ۉ~ɅrQptNt#qS7A+12.59n&muo]WAwd,a!> 2o.7!`W +_ŀĊy&_ϰ2y=I|x2ā^f6t og?BdSh->dyi._-tGюq~e֚ څ&D?^ 3֠c&ؒ I6PI #ٖ#hRJ72*̳@7r:`[r,O2p0yI+E䰌O_{&2'/hCЄkjߤ]s`|.c(XbT"R9hV'g @1@<ۄqZԕ[BG,: +7JX#04':R5Mݶ f cpB>C)9 TLF_:`kvYWw.a.IN*9vܖM0Wrs,ѥG}* 6pIdVP"0|tz|FPWcH\XY` j*eO"1FE1@`m7c@BPu#ƐSIU7ҘjӫS (S͠#>s(} TVd(mI#:wi# Lc %9tGE?SO1)qݱ bDϤݔB,p@)$jᠭ̈/'ׂW~_ŔRQ +:![ImYHm lz%w%6B8}բ~5c.{Km@bWµ\~/H|ՀiICtn7om >/nh: +WD;J9̓N,9K5 +t&~TttkWs:AC(OƓ2k_d>js՗_#0K 5smi%GA7lt IY\2?(1BrB:O)> +RRGI㕶GR1y?:Ca3"Dώ||ڵLrHUv@% ǧMUj6R_If? + ;2*̑RfK/eݣA?]\T24%m#ړSy,.1]PT[IJOA͋pmI݂-Y*a_X!O)H2RXВVxcXm!ف$Pyig뚣Xb 1r偱b͇A hĬ{pP(kwA^ r+R!qy"(Qy-Z28KL 09-Su-4R0HcIs;?~O*$ s +Y.oEIUw9 + 5#~>s eGaQLR3ǙfI㡨zC傓iGd +$J1 1I% 2˜'ZOiUu_F:k]ɒZ1ZB|7!u{J&uJcC aBL&$/f_u}(~Bh)dF$xe-*RL6!$Z!iJaz dz#Œ:] UԸdU 5": +6 vY$y`RR?l0qb$i&l,m#y5T (5Plۤ>-}$qӏ!Yf(3͠4+EKE)X 8 e K@VH]Dj: Í(QO\Al72H렼sP@㣲1ِUtuKNRG| u#Q_ +E9pjFRゾ  y՟o E cq +*B7aÇS>E|lQ:%kOEA &z/m2@ZA4ԇ-+@*U'A[}$Xjsj{uChӉor1U=X뙔P4JS{9|sw~˨cӵy @3c+kn"Ua5g)o`BLnoDžm_/4Uk [{r&[n̗?pۿtVHJqpԥZ%OJi~Lu'&Kg de.؞zU u?ǯ|COѹIC͕\>΁i)"]\ +"ȍK/ +&lФ!_85wg9(_?0N ]SsɸiD`R#LNŴMt +Kק@~>&s6^W1ǟ`Ҵu'IX 㝳FVn]R~(M<@$ԫr㑼7_[VEд&v~,u4u7A +7,L$='s9Fs!^i{˩D٫DfҖG9E d6Cs\ Sl@"La 92!p^ćm@MHz-O~ek\!eGb.GQ0lSy? H ԭvޣ nR`,FSX<8gWa"sMIrӫ`2O0} _1d?M[c^[n_ ^7)jZR̝Wri\8۫Ӌ|׀O$#>]'?GGqݒ>a_e /TErDi9s1S4%qL ftFc^c6LMJ*:(HI*c4@%0P +ܹqƱ+ +MM( +0>~J:`9U̟MVh5NY0U0(Lb5Hj3d ("30W)D|}+s'_gu#ȿN +hyqGSEy_7|!ryg%_(򺱂ly&N "c(BSQk,Diط;85XERrNqh)؇ل1 $l"k^VJ9u +C><|(àHe[<%0.:LӃ\s<Ԏ/$cw߬1ɐdpN q!5PCgS2!0W2?m@ B|fNi1'Ĥ!5qt(I!Җ߿=&+,BFa˹rg.#bv}A%Z&sUq ؆NvWJGMuːǛN 2N__^o +{OˡF\r-㔡LvoQWs2#$kPǮi- )tݾ7r 1ǾK};En?NF0(xZN Z? @ wh2?/5sQ&F2A&q~4N wc<!}w&L5ǡ17L}&f3R#`^.؆ʏ}=2~/ss +gMgȩo B\ jRdl,AMA=8L8T j6_jWFM|EvkcrA5}⓹ +TȨ]tPzP+߅njףUDD6Pt7U\p7P3%uLrS@܊(ALNcnMx&vO5 蕀<p~ \>|Xxr&9OP&ڎEK3OQ ǀ?jĘ-~S.E4e;IM{Am#m]N B2]@'[%-/rmr&a8 +rXb@ɳab*W+?9^MYM0chg ԁ )>ۥD. V0@z;s} N-gPH'URJAe9< { qW5!585l72+́یw9;n/D#J$tQ T\$2s?S?p~(XS>~q46PdR7"(ya? x#zOʡ:ڿSۣ@=r1{!o5,;_q̄|!(q ^ƀc&`ryaR%ngJN.>S[A  x0 t\AOsjz2'shn`'\ 5,N>ZJ|P=}ey"8ld8n/\'s?DR aO7PaO6 MQ]Mf Ȥ~PCYa_~-a8BG -"r; ykǩAP%;85̟BO+"(BSv^*+_ qa$evTS.Ԅq"Kw1E;;׭G8i$qNhϤv둉ꐗ\,Kwhya5%ym2a4x9䏀NCX^-=}mUA&R!2`;y>`2'_rTA[5 1Ǹz(N*Z&ra HP:;~6A(uHsiX$T*˽[N[=Lr.l՟jGK95;`)[DB ƚRRx(ȵ ).0B=n)KWr~&m9%(~BT#H<4ca f(IT8_U*(+3a*:= K&zejvAМD^QJ8Gfkj%P!؏Isݸ[y))(7Y'u wD׈ox]`8vp=^Q1EELBP g*8nĠl~gS *@_^F=_(+Am 0͡x\Ӗ0J5X] +ypȭH4{8;6qo#y)9eI=reo^V=-=^ĒҔ#QjSoly +LŇ)\=0Fl63.JD`nqVI(nI ɟ'J:ba;Q>i+ %q^A A7-c95SiL|:X&ub(ڑݼ*%ujBߏ.C_0=\MبMlՄ@Uj!s$U=p(uUL |;Ŏa:آ~NY/y)q2#z(s>\_5x7 jG[=?8.`WjSB ؠF +>(IκNrGLj/`}s8fہZ>ܔf]ڋ˫CDS"I >:Mz{K7=*9d_hMoBPO 6q| r;`(RImI72!*Y+95]+A>6+Wr]'Q%@Q<:8quyͩʻ`/5Џpw-r+l{R~W9N뙶y*UkhGhd_A03#ldxߛ+9'ԇ8$[G+r%}܌Nifo.n-h*t:i<r- Cل0jf"GE+BV~X}Z#sdƐdcx Cj\T?_>xx +`v3]e^ j\rSA}jC^k2W P0O.MNxP>8#\vQsA rRg|UKz#`f*=P>r= +\C!py芝3oN \}೾Z8F*(JdKКAIU{mK)1D攚rpAvJ05۠dīDXF.oQO<20p (K=ozz\L/J3?\׀rkK1g +.ܤ|W೸ w6 +xm\\O7uA!f.=G>1uh>~hA8l&uP1ҕE[wq/=S$8r~ W.;[!zrofTKuq `l{g59cB y~״E\^jr|aqsΏ:ԚFm u.Ŝ2[A7'F-x:Ԥ +ꍢ~S5c_E.N +l IOJUȫpkrį )`=\82 ҲHoр\(햺qMV>+1e;D6ryi|6_6s}nZ1nmxEX#v뗅y!8Th*[W[Ctw +iq땄"ԮἋ[!;Se "|ru 9h:J.s#zcu (G꩷8=rKӊ3&Nُ%[qP(aӁymp@y z7Sz})L@ơ{:b*:J^ Tc9>}+5􉚺/x)PX|=g5K@ K╳ )P0+0`%pqSO`/L`fP}]ʠj$.FqJ!(sFx 8<C%Uױܳ*NI@Г >/8rKq^I0uLB.n圹n&n~ҡn n~f0"_3%>r'}"~2m@%TN'őE22$cwC~Fr3eqջA3 }:Oou@sIͪ&6%"J4d=O2]| ׬ +v&񼳊˥rY+GR*z* +aԴJkg4Di HxDLNk2] zG=zf>D?޽֝Dw-yLaIVZt_wY5̽&^Z qv&K66 UNF44zt@ z 谢"^90o'@>x7߬jiV!*Pp^/qk1u\M5|!" +.|_dIun]AW,i>rpYdU)|CH&% +3Y +퀠Z\uF|*z2}'K]$51_"yYBj YJ _])ӡc߳ xkU6֑6-u\bIg15m(jǹR(xB#}G7a&E>'2y/ ysDy@|1$0Z$N> +O?SL¿/D$W^h)iVlHkc@, +GdG([$k1YlĵiT40'j_h%{Q~F|T+iK3ZIzJԬ'N6bҺ'A͊~9%/VD~_+;Α/$tN |+&|"* ]rAa>X2'lfkXKEÆ~{Kj> x49_q)Wo]C!_|{I]E?^#]M_3M< #zUvue-c"k" ]Jl%/kNH6&VIF8$bL":_UM1Q͂(W2X+Sws k͐nN +( +.qN9𛓤f咞_i{7Uq 3AK!a}DҀ4YAI1A~%o2k)6_i>).nYZTi--7ʿ*~|AmI^9U%$cIi~YIAYLMRv +.Gl&kB{Ⱥd}U뢾Fo悒?̈_,_R>^ 6xJ?UZv=|j*>^u[<"*z2VQ'~`i,,4T4ʜ~&a~ϻU8$iQv "!hO˼jTT\V;ɚ*w{f·Q^E=%-+u,\d6`on;1g.F] +;y5#t /qrwCH~&vL[@&|Pn 1z v ^,0NB 1k`і'7-><`pQU軹CV]bHC=SDQ +b %uNݍИ|Qi}'5n3)h"fuy^?37w[gFm"b޻yx j:$T`H~NdFSag¥nuem!7|F7 XsX2㻶0ktEYyӎD !U/Ѳ*/ˎQGڟFVZ\$t vV'LO~7>&w' šoG:'ؓOեWLTo6NaQoɖr +(15b-FHTI>>~scB0'GA“d8?-| ( ؕb7[Ӵ:4MKS!o|Ԗj=iZQ"-p=1OR/\˗_O<$G><9"ZQ+Pa:Xr16ֽ7έ769ң7QR|k;oNpzxpKxZ<>vC^or0^>.GyQ% 'H /I7e7S̭fF/ k|v93\sjq'K;% +k~%*~56exI][S?kx8`wns.R=.^ J9i'xxל5& +0+wx9=`0ioGw n v _e'/*h +|hX?YZ>]bsXrsX|KGOS?`EĒ]sf%Ůf E!ⷍgDJ$I(yvHZxQRc/ҚZw +Djyyk\z ~-iXr1>D8" C$_vg4:EFԺƞiI`:꜏vdE6Ƹ{GF:oWÕUiӫ+baY%Rc홡->&YuUQ~III} + yUla^^偑NUqn>ޱTgVodɯ3:#<-̮.W\\`coYCP,퓍|3}2ϴIgmDe]k`zUir>$)~͕Y/=Qg/?9? +]Vz24l}|crYOEs1@W%zAE/~9dTz)=tsFYz_y.~՚v;q7̢ܤ=nOzOOΗZ#^7z)01\5cz Tᵍ¯J[8|+ ~oF ?ND#&fYS֠u[U38n G5oZ54K(1EAn~=O6:dsGoKUpc7761@g5^H + xMӱMşj0nuan,{3Y\Rjy]e4a:zHϵɛЪcwGhf +YC-U&^tCbhMK:EN1M.Mcj_u +9,#LnTqg ۷߽#hn;S˅h%Y;lO;3kyεawss߸Dw)wI(-v{QSP./ ˥/wyx`RVMu yCE5+˞gcO)fѤa3>M>i. дKѤKDhʘ5hLJ wEĄPpRӁ\M)rcBa_طɛ.ƜӪ9$;{H+ò]zR4Kqeo_`MJY79wӶ#̩[ QqT|F4sp $4Ca=v=e=;k/ZVo?mq4w]DY4kPDsp?[Gj'h~`C)OyZ-kLqK oJ"*"kl#jbkJb` ^$M=<8{C?:9 +)M]}J4sZ|vSw#ii{[a-EOzh;e+*FÇnx}YQ.!\{}R[CB[KxiԲ{_ZϦ*FS1Cj>s-@N״Qьk6U}c4Bs6*g}h7ZDdhLaw3TKٍToKn^!1k{! qam]mvy竣C4߲fOo_ =DJHiB9O{!1+?{7n=[ܙ{тEZ8Z +-rFK5Uk4_iVCԞ+yQWy!A]z_=9/) 8gˮpyHWWKg^op)c߿) _l38ۜߧ+[f[[}#&B3cX)5G,'В=hQZZUQ;n]vFqIiKI}CtWvk+s ~Vb:Smtm|Y]l\kɏkV=_d 5xgstşҤ^)6`;i>]#ZHiZ9_xր ;oU+M6lqݳ;8:j16\eYc[gJ\wcBG=-,ok?x-}_RG u:W L|Y7{7;g/-)hQB&Gr6Z`ܩk1o37g;Ɣ

9٘Zdܛ둧nĘ:Ն$UXFFFrH8?1ϣ:.e4}Z4{>Zٗs]]xR |wfN5eSU>E=]U7Ğ3Ğ})0{1Z|89`'] ^)-.t1CN4g,d4j.7eZZ>VheZc3|ۑޏvOգlV[=o7Z*oN kղUogjd˵kYmzVhƊ X`|:"0 +cgʠet !!$"ynw稊. [|` +ܞ_,54s&RZa;h qo*n󫙸;_5O%u_%.)*G׋Y'oT:qg4?a7a| k1xR͊t|J60(ZAXT}^F俩30?nT;}n*L݇gǘV7ת؊R +pU Tֆ>gx-As窠M&hvvh׉;MQ-dWi`OϜ_+lMg?~gi}gyڟYwVj=Hr3bt| m7Ҡ2nxskBu;ou^Ƥb }:FIf=I?'}ڪu!vܒ8߽M6lkh#{Oܙ*aۮ% ~e`}g%؝efnVwum<1$ET"|9=l{Zmڏ-:e5wg\|c|fU32=y;D%1IjTb۔ySASD9E9dgmBm5 KK.?lS,&@K8~m̝ٓƿg>/e 20x׳֤=+cm[|^bA'G֊7C3[SYz<"J6/*q;y}i97FshW>2_^)KoE$yhhąGoE Whۉ;÷]+Ҩdwyqs2qC%׬Tn{íh`?=*1*`y YS%8_] oе1e6Rٱ9?aٖD9ag^+KN|Ǭ5kdر82?t4Dx7]R49E}!"d'9I0)a>ݰH񒬠 apq&VQS ΅mx-ъ\>}؟v9MVqmf|m S 랣]Q |BVe>?ōDvx.<%mt6;29&n>ݹA;?thG[Ͽ5Fm"2}aDVC,='W2&tiz%+,6+-r=%n2[Dz~"d"6}.wo3'D?@kϠg]R_t"vk`t4ȄoLq D%{Gk h9h璹h7\+ }Eo${O3 c( 0KVߡ)JҢr7Q]M ]uA\tHm?zW=`U{Ԫ`ULq8YqỏdP?9JIOd|]K @/cuP CV8kqɍ{M.EMٽ[ W6͝:c-mg:3EzvTRQa'x _p=_5> i{5Zcժh9r hVV W;_OYzgбFΩ3Mn,4Iorw /v FN_J6qy1bc795:=AGCiދz8^Dj +P.Qs1[ȈǛ1oi6|.*G/7ϪV8[1-)vU_Ԅ*+!ysMu=w^ 54c26CWl!uǂɚM~&|t!uai[Ct#^^t ztKpx)hnCFHDfǹd,6 +%  k<"t+ᗳZN_\ܬg^%eKw2Wg3ϝdR>*f7l04TȚʼMn]]0W]x6H6{ 3?XkENvn6]rd5-920a m#*D=χOٯ{F + +=m_]SGf ŃZwvd,2pԛ9|"6?kj|;s1u}[ߴmQnH)lCfvQ -~\V~ЛnV>([W1ngwjFzAP50xbMY?S6<8⌢ɁӊzhǪh߶mHgȃF M}_vs |oIϔ<ϤeR{o%x*|0v߲HUfSNaJsO(yA[$ J?\"yrPZY}wl{AB"BTϋ,bH[3c/eO:1^! |J)})T3kD;TѾf(LnRuk,!x&OxN5],uMT”&U8sͥ k_*xD`0OgRܳ.61J<3q"%f0O虵8nq _UMJ7RTn|al3ԤowSgæ|A&DL^ +b,:4LMeڶtڳAsGu ϲ,=%i0~?֊fJtQ@eRNs}\J4ѳk okfšģ|o􏪂_Qz((}^  ymG1Lgq.3zj1tQgtQg.M6fϖI*-lG.7&)To=Rx5Yީƈ133`n1}pEEI:db0j/Ң3 dwn j2>vK^z0Ǣݩg繈hk/Wԓ ugm"ېZNN'#gf{[ zg6MWvYA85L_b#/*&6sDuU#h44SB#>C #9]&AⵂD%=c@MWfa1KF蒒z͘DN_̿>8C|YEx~+}Vw̴}=+51k}D[pQZVOX?CZ4)8f3pمj4^?؈3 ++D/\:ӌ:j=8=\t `, :MyTArN*KR}: wEϜV=6{YzJ׆g(y6Jj2jwo% +7Êכ؆N;1],89EC-n S,'2zHAjQyM2:"nWNκ߽ԵDxVN U{K0[O(w'\ +ZR!̹?4<0KsHKQ<(ȟ`[rŗ_:xn1O{f-'N8!ιq<`LZ>#Wqe}ԭ}ѭ۾eD=s„kjpY^){!oOZOCy_.LP'RL2R~RN[U"j7mK֣]6#]#_6LgD!-3FVSa xFf-¬?P 87*Lb~2YfvgQKMF;b]3}19fbn +9uRQv#?j_02zCsl;u nYAߴV}K'n3I)cY=a.'L2rmhOЮ0^r +i;-qD^]9t0/!8=L?}y,Hq+QqMۃDG-2Z&J̋/Lɳᛐ\AD duGBAj^"Lj6&Rh/y|9EuDof=89tD?q3f#`$8Ҟ+Z/qO9IFe:P 6ŦIrXFG#idi4-cq1w !JrJ!ޔ۾~㾵֩3#Ygʌ8֜}Z{U^{-:֞O: cG6rEӟtyw+Jų ۶ț56t'[x5Nóx~-nM_3wwUج=0XâIQkHqZvT9tm_uSfͼe}B_kؐ۲]+O_}<栤/m&vM?u6-{آ89nظ{]L1f`}vc'7x5ũO0pK*_F3?,?9tߑ+Bkw5 <:^-{,]gƆ)/oL {o/oX9 dP6熚2㉹|>kJLԲ1f*KG^CYpS#8{C < +;}19+1F`ɡdhSL\^ZKrŮo+O=lT^YENXXc*'9iim=6ۄ2g7}^1NA1 +<Ű#_.dž_6ǰTwFÓWk^csu lyP?1?Ǝ`tspmПnz0&Ptk;alI1cR,m=C_qFR{?y[~ 3|^;M|Ow>" ~;ػHwn, Ƽ7:'vN#~%AsvBbbn&1Z |k"o}˟FѸvB1xijh~z +<.g07H >L3,gמ^ŨY\ywo[m28F1s 'LĘ w,;aɚp qºֲ7--Ro]:)rӗ`q\w*-%'-.C5>cɄ(~sWP,U-Dv-J~RNgenOd[?e"|y:?cO/r$!<l)m?'UW֕]R6lDr `>ĺk[oZNSmrk΢iK=cEa19pQn7~7M-ηK1僯{}{n| +a:#gBKws[,1uЦ@9vx﹯c{<׹xSCm=;&W>aѲI  ladʱ Xڣc r?#rזs0.#yP1wdKWc:aZg/XwbF->bC P.X̧<]7H紆ۓoOiy12ʆ?TB˞\x[xc01!ԾD!ջ,rF]su0m1l?x1QE-wNE`Y|̉:Vdu%AGж`V /8Eao<{%7a3<ÏR̻U9;1w_OZ~Mr׻<ц?PG~EKL9=1k8; .{'EuהW+eoZPV߶|y`VR4$߮kOb@kf/hbgR^y m +<F:]K/_6o{;l3j0,pV0w^}ESz@hkNܜDxI*a|aLZ_,زH7 u\ZBG^AqAtMxU9= +˅9B .gjdb|C-b1F`3B~[ k`F_,GwD_]( +aMZbT\`ǦSz'aG(&1u ;N\Bx=Nw7e;XG|3>QNW| #0V39rwql+S)sȡĞ.9GḌ?3Gy|Jh*r7tzd2i` ߾/}G/yBlۼYs`>ʂK6cɷE;'N&ͫ S]&_%RJ2.:.l_2tY'cRe[ΎݽskF0gT_e +c1o}A~x 9zt@?V7ܹ eR[=8NQ'o|˛bDWZ~?;b? ?{߭<8I,оo¸_K֓)04yal0@_{Sc\8ͧ7<盞|s]Ƶ;.| x*j2d7={ȺiOs3BG1?|o pI1tpKIw)>\% Xm+N 'T07B c~3%][W* $:&aɔwٔ9"vÚ?S"kȇ3N{kSk[|x2dMsv&I0w"ƥwNj^EMky>~1`,(k>1x[ wN}'3û߿bQ_A o9p}W ?v䓺nZr崁~Bÿ +i]\Xzo9e=sw(Tq!|37pYC4uq& keOTC"+]Haog͋9p]6 |O`/BOF}Y$뇹f}SOo8&B+{qyƸMwmfg{\s=&Fvv`/UFƺ ox +{[Ӣ2?rugkn ozm +o>zŷ{ڦ7}?>F3]m`b 19HbwEknQc̭ 4m|pgE]OnΟ b!|x=9z,[kjȝzT pMķqK&@mZe-yq)ފMgE{9O}tkt7S.;7޴ _̱dʃ jbq>:-ʧuw?L64Wykšsmg?Y?m lhc=Ywt+)"7qá+Qco\zu${0It E׽xը+`"#z!-_s"{M{?Pm'&OWrpdD21.'`^^ӟϏmyNjqq ?9) }k>Bq1ߑ0(/._36W1TO m}cnn|pi>X\Tv|须ŜsOgŸ0r=7ֲևA}ok$7mga\U'㼋e֝rcb\nʝGsG?|#1;kZe©BK&b<N'Se ai4|+#s@,t&<3M?]9V(qצ)o]ټt7n\s:TJ#wtsW~9_,wWḙʴ= sGҫNrg=rq2̝TVWLyp u,?Eϝu΢xFĪIt/\yFq۵rCqɘDzqݣc,U _{OGyxV~Vcjߡ4jU>5G n:w>`ĝq_qFߋ/(0f/;|iâ'Dw}l7keBm Gznd Pan@]'O89l8r/xVl7.ۡk9}> -yk7̾rg± A3weF2vaS _'|jL8qʓ}'G'FW<sqS~5.{b䟡QS m˘n==}Պ\C964GsJ1wxgNoQ%ꤸM=%Zs*Pn .𷦏\N 3IFT9-bBy}ϑ&c2̝6rgrg5{x/MKFm 8w1g٨!m\%Go#ҺDʽ囗=t.xh6cnS>5ڸ'͛_&j=S.^*DbX̍GAv@p^]>s%b\=HYm$1q/ZVn#g/0'vxFhOY%#j֗׸s6<}AGrT\;\_1ѧmVK~ǺN[rgQ'ʝ;kn̝u|g/Xݔ)wV#OYYYi#w!-c΋>}1`.ea~s+Py;"wMT3P,kDsܕ1ec,{"zcć^ ?/:m5:zO +[-MD|fa21rɸ700﴿ 8?[` +=NCy eoˀ3wr=wVM;eotXf󙾆l;ƽƕ{79[=(Ϡ| 0/ Ų||y+Qgw%OkaC~~ƒGؖo_yIc~0e oc)`]~Q -rg)w'7Fѣ_܆>hP.uWXh$rIF,\_œ_Sb_$ ?ҵG~ 4ny:]Z}uu<?(o̦<,ʎ(&xk<|>(oI0}xоOo5s\v}Xfd᜺r7ݳr +ܿHЦВ'B mQv1Wl<N ֻNh|:ڷ+>s O?ڵC.8ȸW燻7ߘ>2}~@.rqKHF\< vz2\]o3qcQ~5{.FӼiضoO9pp}S)l-PSw#`7(' +]wq| u}<"`x̋y~B;ߚNcsˬ"YtU_ߌ3|_oءOǏ6yj`zK#==}`Sp_*K;ς |y3 +=4<5/XAZs4ʝBp=N/κW˝ybhO +2qI9[P>=!|ƃQGv#xWtV0ߖS>n< u=浘|`ַ+RWf#P/zXǽz-wVh׏x}Qd[, {?R~oh_qB{\iz.N`_6rgmw6 +zc=vWhقzWwQcƘ#YhjiXއ6;s4l}@?`{쁷7ٷ+oڛT=TmF^[bKhߡ)^W"ygDWxGУ^^/zR̳kx,GYQC\qkb^yMk%ͣ#W5> +׸ބ׮#>7ܽ<\yr{Rto_O?-l|賿 p&5;&O}_M91-|n{u"{B́z{|?TwL]o Lzkϣ.p)w?vA{='=Ǣ#}0:hD_A9U@۴}XA7$#`އ<59-<aO:ۍg羸sp\z]ʯWV @zΊDK~$p +?DJ{qh$pSgYˉ0 +{c;_p}~y"zE畱?aܫ\_KM'ֈ>@[^z-7{OwG74x,فc昖xυ;5y%G3ҝ"\os +u Z0șG3V[ iﰡ}~]郭wPc1Ȳ x{~&c_s7{/9xx}E V1C}'G{f +C{htG= [0fÝ+N9f.A_HSb(w-DW+lG?q.ȿQ[&# &|S+v9)0+C9c`[\k9xwYk&?Ə0)8sbqXtn=3OP/16uHaP?x.mt0+}['pn?8UkLu-7N1Kό:*;tog*)F4uL*3:ƣ1}>nnδ/̴,B T`*39E5_Zf"?zxObNwDgSj1v xjEgg rOw7\;@4=F6IhpcVA"d lŮೣ>ʒNqIw%tc/ݚZluYNtGW')`D;Htڄ힙 X| (&k/ X&cRz&uҴۻ@tǵ]ŵ]еƝ XF%dV%&RqXLƭZ1S6QOmVbi2 *޳uZRHU{{zΪAq,FksL2us6 RL-ޓ >3Jw7{*v[7SqUȝB_ښNg,[<-jW +4Qǰ+Z;1{ Лw^ޕLt:-cY(ΞL|HZKRs >N1UV)J[l!2v"udgYL[ӎ>CC{ vJǚqū-qіCs, 8dGD(jD;;"ls◙dfqG"|cd܏I.tE([Jrx,Ht/J %KO%*t.vGãQI8UөDbm:?"_.`V[d*|Lmu-1"%u;ޖuNENww-Nҋ1[/ws~Vȹ۸i#i.OCu8vڤX'9R[L/U#rp,@"8F1Z[ &"%se$S)Gm& +;U2VIOwhiq7.+>)EJ;(Ջ㝝/JȖ_pԑw5T$ 8J[̂xW$cɻd.v+]97:G}vġ*[%sq\Bq&^o8DZTq>|8?3[^2wE w)Uc;ܧA:f?ڳ"|Y{z=R}qwdJg"pCk<պ AVxwr2iGz Lw:Јvgdg"[w{赵%3ɥ gBۓT!>ש1LɎQ)wxqiufs%$یc73+WǙq8S-v:|rLgZǍ8bb8:ΰLjqq\Bu)F:ng +E>UƵWj|`JR,`xBQ=J/Fcۆq2r^, +\ڟ#%t 4._;_q#&o%Umv7[IrnrZys[)q7Wwscf6j.qa19˾gII$'cIN"u#:vmЮ/Vb5%'Rux_saGi,mt8DK> 'n70 :;c< =~7.YRXlK';ͱl]xfVVbsٔd DGz~-*RH:"?%0I_ck~`4vn`4 +bEZ]Zk{ K/8^`[2?=9O[2,AvE }gG--%g;Yܑ8JIEꔒ$ASǐ=8vsυ,%4wv&Y;3 Q*{7K9<{sv-s-ξg)ݦd{{oO: wWnvLº/Ш#חH˜bJ.Z+Z1xcs: /yP"ԍpUl"tD]ѱJRndq٪nuZ cۆqժr^oq-;z},sPڇms!AXE>W{݉uqZke# sDZP,3:/WF+0SeɶGwD!>Y㣎ej!=ʨV\= L[,}[ +QOi/(e9"G Xȥw.֙xNOY];؝i_&idn Tgʌc7ގL|iًqxObNwDgs "Ԩ#gh"hij-lҡ*0LuO `~&ޝpãx +&5^/DqLڹN*n4%7Ҁȹє{hJ#қd80%pDp:]=ոB+B$F %9Tí$nJtU +Cxػ;>stream +TU喻Snn;2h#iæ՗W0^ަ* z:+X˪{Y<U^"D,{*T*.`,FԔcQYT(x0 @/x/HP=+{y K*i6+ +'qJVD p) 멀j*^xlI +k%hUYI)W0:YU"C0%Ydh-v1/Z h&{Ut40hO!~*̪pmOAXVÊQeC +r}LD@?V!2+(B֒,,9emMig\S f]SļPה~,Q\&|66yH 2pf +;lH49c_Qe9 zJe_Yx_ne$mx9c6Ҭyױ8C@gDQK8.ׇ<$ 1G/,J؍l=< 5p@aXu;{$l Bo%` 0T />5$zh69m p`׿c?LqBeu+#CyLA p86nUËmhJ54DmhЙu 9AC9ă=0dVqUTT\xRFp`[f*c  V]Lpm.<:}CTZ>UÊ:&8XMpv :O\u1c) +>4&8XMp)_E"8 I]T6E(F@p E!Ʊ N&-(dK<ʪxU9 S5HNeC"#`".fPqя mi]\ ۠DaINC҂#y4^-Ia?1[765T6 +QnSIADGnH9i_c/)rЀօmQtqel3xB?TNhHRA$ FNY=1GKs]c~91nj;U!z*hDN7WdkoLM9Aup xUcal +s,#^ +Ί飡yMtM0CJ7jP?-ztƢk|q?V-ngdc־]o0eǻU9reN#{kSx +JW .UÄC0`[(8cenxfkxX鮶c:Z[Y ΘY Dt0QGƄRp@nL'.fE ӵdѪۂHV]j@fUTMNKT9b`oԚK2wOEEKy$MaWUUvԧ3fA(l/陾0OۀicKAI<9*# ysu6О0bӠAۖnI4WSL_*l6t6\8%S̲Fb;mF xgT+&" M ˵Ba8T$Yڨq8VX;-˕c@ $J ,%s(8ƙ[1F駫*kۖn9!Z2QmF.sW( + +I"f{Cw0lm+0mxU_j.'\p"065mk]~2?fmM0 G.(cb2\cvn~a8[qAMdʐ<)me\$NN Y!Zs`٬mS<\8'usvbtYbt 2:N>Z ɪWoX(V5g1%_/F5A);G]%)<#s:.Ж+" +s 9˲Xr$lapV*X0h?s~ cgN%B~#G^"k~ +!9TDZM~ cؽey[B]NF>tTcYԘY  v0@( J^CbZ>Ä~gxeE`x=:#ʁ*P +'Jt^-̳LRb%S¼;F䄀8ɲIsY^EIJ4b`xJv#fbFYW +)xRADgNez'`#YTAY@S@,kkmEEx0H ?b=Qz`B{Е5si;hYN%g%{#On`Nif|bqC>Ca䚡m dK,MR>Oh!hG),U=xG7 Ab>֝9S- "\wJWh{Y1c>jd S@vvf2RzXAjӒ4#6uе֝QjK :*lΩJcfrԡH@\E*'NwR*TK!(O¸WyZ hS۩<]t+-Pe,kJ3\UZͣw?FE 5|tMiCӄh s ˈ{ Py΂LeEr +V,-gbc,|:򨄤J٤=j`0pWTlȤHf"$ˣLY""$;d/˼b2]J^MѪ)fLAYj`f,8 KZN<ڐe 6AZ[,Jx`qY7v&Mf6N1 dRBz wۦBd&f1O^۽N&fhI6YP̊ d&Ȣ fTlbo>/&'51 ͉ỉCvfь3)afh3cT,2+Z`/bsZ2p0 E>v+.vu̢12kCӱheC ɀQqoɄdAN_Y42n@Ll:m:ra׿,drcHW,*1T(J" 1Id:FK- +(X &z{B԰+\ 3Ne, + +E"C'2:Y~oȄ$AemN_㭶̢vQu(9q0lJX4bHQ|As_е_uXߪ2Xq~܏g ǡ~f+zyjz Omw1-߇7ei' }5gKDaSD+--`cKmhes?i)aƎ\o$| +m!nPɼ+wC@vh[+Pa{ndoRy,6K;~Ɂ`:M* uCmt(1Qo`JI tMI7j(֐jdfMv fy۰AT ߲jltsCby[:@:M޶ Z l-dowRO6Ⓑ·1ml!(5D7XP6aED7ֱdZ XMlp![H6Z:F`kUȦl%Cc6,`ɦfv(M66Cl,eéj9u;(E·,u6Cؒ)YdʶT?rf3!lX,eʦfQv(W66 az )[MuH[[6}N!moY*oqipN7,N7, N7,N73jd[dj'ddJj'd[ejce\Aӱ4K94K 5K+SM| LɁrӾ%ݑ8Kd"I ^jJ 9`Y̐zr8\ e//iA"FXM9^xg"\J$쵃 Rv3j ŮBҞ7(Y:o%04e[CaU{CvU$ONXx:02Nu7A.eDQB_0byé^*dЖNqHlAjLnΆ .K`~"@*6/I6粄QEmEdm7uhiRێܦ <|*k[;Nr^U; `(;Q'-'hɸ1o D-5m#;lU]noQE3x:aCjL'k;5ŴӿcBݨnۺcQgM[o3k+:X Κ/͹Wd"u^!cA}_{ +" +M, +'[]F7^@xȽXsjZ=L{pGPpMY +_;o>_>#en1 +0Gi^=$).<%"y\(I*KNAu d=>f%xQk A;WD`cZͶ5+Ũu5y<3iTA3NEsgt*:a^f'(ZNLVX p}5FB€p^TH8aL +2-@ 2NQ/8Z H B;bqK +*I[ASEܚWQ x@VD{P0'\4`ڀC?>>i-BcU F5ګ{myTCC0߮pyLN +F`i$$4x}{M*i}f00[^ZnYX9YèZ Ψr<1|FS^Lu<3,jHșL/1kB&ƾ\rmYQY[I =#-^LK)rvYZx`*PXɏY5C0$ VPm,soVy5鬿Bg'F</ff,Jh.x^~PW&#cs<``wzF#s)(,6Ġ3fw +[ƽ$dn#ĵh +qkm6 + nwp@Ud"Y2. 2,b\b!R[sXWi+~`G_iy:) gB]v1mV!-:S{ԨHZU-LOs@*bU4Urә;[cg~KjFt5Bcߦxsf m!:B8jq<Ѯ!9 @rYUǼYNwղ8. +JԶVɑKEW!G x#W5i>`˱灑ㅖ5;OieN8m>&h ԬY?y4A@]ʢ<ys\53Y[d>l] +ڿ0[5ZUIF96xՏOCCHAe%UBS0'YPǪxT + .a%Eai` ~jf7·X%ڵw0JuԊwJ^p8 %F +}fJ55vCby;onOks=0YND:'Ak]t1v8+inʬ'TٿXg# jZuSTw7u/z% +*e2: d7]~Xtu%<*0OaF֍axFm^$W4hFEErC.E~Kږ;r-ggA3(nCyT\dd6{2^O}mz<S 1Kd?,z +(0' |CuqTEb4 NYEǻ[SlM)"@@2#^ދӌ^+z4M@Gsў90  Lt_/)>3n@OLěy/ tz%K#ӫ]_s)(?Cӣ=A5kmuO${ |xj$[ŻKsC[/5ڕ @=OAVc:}Dk/~yL؋?~6&tvYޓL^`^*Kcc&0őNYzj df( {jmo,AĬdOW*ާNAn 5Sk#fTjsfװޱ(īJ6 `DN'bţr+Z\W h8+NH&y$%SR=DwpIJMIp̾`,:-gao]I闱(ŭ򺳱g#D /32/q"uxNdJ,:;A#]PV5%GZLD=;9SQN\u% +FLLuJJ(~qL*՛,L3ĸȊ,'rxJ2'<+`MV׏gluH9, |=%1ϓlk,'2"fP;)YxPv䘮H eו&!X ֖PSA5Ɏ'zJA&p*.ELdI6܍y;x]l@à|"1*z#qzCT<栣QH>L3\Z~dPyOYQ00yeEa)ωfplM_xLj w^yXI Β,q"O Gxm N^I|&gHd3MM3{xn g̵U]]]QY#IhWV0i7dН!:bOSA:(n! UU  KrDr+@8m`$Hja _O $KRV_g?5ݮ&W),K7hIl|;87&Qz~UW\zÏZtWP&>Z4ׂNj4O3n/jdNЙ7# g)(,CBX$tbz.%<\ZB&[ +{)-ޮ7ioU)\aoKp`ED3KcܮMoxs_?𻙻b+a75a+tKs-OGuxDwpbo3@&Gvtmw+j\dN)5$ϨAgӠH_-t/!MD#j@H*H|l E>'ڡ8(aH2;@1?Yz@'fvx=jZ)_lj38g`j/kؿDkc,N;d<6@r S Y >Y-F1Y_'J0~S\ڞ#o/M`PmG'8Ym}40!ht7/8t~7z>lGVh$Jl(?dsBn;~MCA޼1H)nH6~@K#G)A&}Lnϔ?*)e+գJo2Z{\`Y$flxKxIdkbcSx|M2>D0{/djDR]1Am. +$>qO{QZ- G $ 9ހ$Z?  :1*\P"p'SVIobE-rQ*Xy'2e*Ea +0B/Tx}~pP>剡"GHHQE } pV0.za% N!|=b(g.'h>@т>@!D~tm!+~%̓BR Ä (]r_hIP"`c^ ; +Q,H~PBXԠ4"RP^@lU/':CGY& –&٪%sAm8F fs;ESh<Zl%:I oFD"b!C@;oF`KzX0Gz lȢp 0w?&3~?~:lC˒`XypYJˏFJQUK>V⠱SQ>T@#&*>$Rf]籑^4@]A IDgVBܱk @sAw0u5"PHa%(.E |>vjILSGQ5 `!u "$89v((_u h/Ddc@瓬nQfo,<`;!2 E剬#̇YP , z5I@gM$aI$5$]){a Pҗ9I`rXyQ&~v~%t9 x>DXbu +"Pa]hJ'G'`!nF p2xdG`s kL֥BD|$^ + !Ų>^䌁KPddRay bfȣM4P`ASH惩Ьc.V!-0C/'{8N˺D#x`"+F恁0 k0)-A"Q 9RJR +nǷ/XieNz}X3'Ë5Ff8h:ou!itGz +!}.6 +.k_VB Xh%NX1ȋ5ʢ]ހL"H\l_+S +k +bO/%&,, +''] >0hO.X^A\{)I첸ע~Jh}D=.Hc CN;kЕ0`w/:HcФt{H"rJHy0TVؚ PG*U ҋ + ݠcGOXHLGHKZ Cl|[lܞ, /&A`]c4whe ڏ bn&xy!V@[990E~q)hPO`p{qi!G +p8 tX.^bZJ6\ud*À!P^Bν0x,QlXA]PJ!OqLF+-r޲%^ K*7KGcꫨv0 U_q=)zX.!\%l3.rpk~mQg[+t׈%)kUZsޟ\_Ld lO6%I!4$"G]gm3`̐<$!X/~ ĬZ@'FGaXI\d-?D3w x2#X:ebpxcY0N +g͂, v[4Z  e&1' B$5$9iOlet0 ?ߔG<*0} 9 0ؘi J|*gسNăUÉZQR0 (г`Pb_MC ЀwX~ N<|_`B[ŀ〢ulBZ| /~Kp1>_84֤ž sT`|->$ϕAe*"Z"|;@=kYAP/hM6A +QSdr/F_ p%Y!GoSz{ۮ2 V >""(e)rCiD^K((Fe9f.\Jq.#6ș h;0QvȸgB&@ha{eZCx[|0<` B +h9D^'! esHځoEȥ89R2 $ *Aps0%O{-k.+v{02oڟĩDG|w$ ʨ`t׫fm]^ekmyBz=XFl*׮eg0;[{8 4ƨ*"v4oS-&1]o6#hmzx6Ϳig_]|~߄7&{*p$0|krǮ~=-؟.~^+phŶ9f?eVjGo L7vٶL;T60 Rg^4fGC-9tVC@06`h>>~_RTZ_uVzK59 ~{%ٿCJpC:Vv? t76n1@M CII1 Xn Q+35{EqPu<wꀎ-LZ 7IOM lO-#)^n58hng?j;F~a;%Qϯnv|vٓ(|gbc[,NHOSQ6#\eΆu@yCJ]DAڏuPhZ?)9`+͂q:S5Jčxfӳݾct*!o` ֺxNj78.`G\͸cd?1B[D~S?dgJ۞9CA),W   +XDSeHd`Kd75Z,Rr }_h(y4*ϋQ2oM2b4ȵzvs;. A6D ~X@X|n;a _b{-yytyc<"QFLq4t}h,Xvwjୋ +h~~a.R[ NHv@jbۉD^aF;| S?xB!O)vQMzoZ$(H}`_ sIo`49_ +Mg5PgtG7p5 o7 x7[3o{G]1oN!v"Q uKfM'*5@ÒM,|5Ih^pKbw1.1?!/b0Uk#ҖX=|}|[p6ŎҟLTf-YQyPmٛvtV51}M3hXۮQ +Fi$fbAS%(%!9;ux /X3` +gՑ]w :1u?LMyd_6EP.*}DAC:?b6QPr?Ba +L|vaj8Ww.V_C.:(h<>n&L%)jZytZLL +mJ)a3iRmъOD,5p&\[pE.!Xwg>=WnBor^,Nz?Fr@v~a5^ߞ7WU}*mi=zSt{Q\/C*Aq{Z xҌfsCGm8)M.Uex3AViB6r݋X[zr{#ۖͲ(2fx6S)@@D3F$cZv8:5r +o{(ˏh7x#JI`U eȬxg q9ֽoNIWJ"%@dc|0QCz.cÉA}o 6d ) ĭ|\?.,Nqʩ81OyVa V q=]Ah7t)ྙ, +% )I]jw6 O/pyѬ*pԴ߻ %5A(8h +?=Bt!X+&uX̛TpgFqPumЙ}zQ#}=g{IvMu>H4mGՕ}Yч Դ_X_gS_3fMB |g&=Z3e3I;eKzlɛҥI1a;ݔJ*m| -_qlB7]g*s%=eqM㘛<ԸΎw# ?aR~ώm!4;wxRe0$w뽛%;D}xgNf<|u<1+Wgҹ*7yX'l$[tiJ͢#S)Ȕxg^,(* g|M|ӣygS;||aS vjO8R0t$]taXܘ›\M}vP Z"@.nHfq.An]xO\n}zzJL<\y/Ftu:0ӥ>mJ+znl7vvx^bo!k]klzB3淐L댔31tѪx"r:Eg4:{)9Im\OcClH|yP{}QcY oЫѤ4|RaUOM ;sbbWF351:>~|}2J1&-Ƽ72VmEmw(g|j^bWJScw0n9I>DoL&}8X^M}oe )roĸaMJ)j35wL4Ѧi3,ͺaġfy1}/,a1stdKZj;K{fXi2;˲\x;XTguOV:HޚɆaaqS.m'Kz%9<)GƔ1]w;[=ڞc+f&@}-~)尻/;}s{k랽uE>6ysP#bwviw&g%vu䴾N*[wF\gf9yyoƹ.9wL1 WUZ׮xmW\sw}:L #RqnԲݭrEqވU=I]O5z]0U 5!9qef Zt?x +|c"{6lS#*㚡>Z|Ef ZTlYPTC=9 +L^*,^[>ۇ7x֘d4ƽIoWϳO9|c;nFηw~ܾیޟKLGO&igh=yGȏ RI_9JϠԨT &ͭM!3?FFX3duB^2.ꌦЬMߘbrp:mمrx.ZюV?O}"^mGuIEc-zߛ-c))1gE}^?W`c|rw]uSv =՜'`xjŇ&,v&=N|ZzӤ%2LdiCldΐlS}oT9jԸkOaŪ۝~kOGcL2`e_u̾d-]6c_r-\vProA%mDʧkyiŤlbYx| +ܶh;P(6ӷ5՗])mSSSaIgFZ,4gT3;>Ag^~2򚮺+p5%~e5jC}?Ƈ@vAVOy?z+๫/ yFj4uWG3м4k!k9ُA|)m3?ڠxu,?7Bt4/V:w"y,:r?-cx\vlqe-|] yyq6u" -#Wg_*|Ė} ׹_Bbž4u}x}ʷna ΢$NAAMO%PcLD>s6:NKd㦗ihM1͔=Mݾ=Bj.֜vvtn1v&>VojTo {ܹ?ׇ^Zni˨gn. 9ovF] F3ջ]De0 8_)BJs7,[gj.hv+sqvQ &rfv[֪GTw%!ɋn*[@,7-ْOe# {9j<볧]y^O#8vajR|^Q8=>_<ʗ\~ɫDѪBU8K'SAyE!U3{TE;C{Te튩$TFReXF U&Esճ\)J`l +X :{׾iG-ޓ5W]6&+]1sZ-YϽ3ҏ ms.T~TÀfJD|إ?'an{<1eبONx7jf v[&cYSyӟoI߳˙X˻D:bMҾ1D/ٙ krۅy-ڹtZs +1=~k3hK)4NN)L +aNWΨU}"XI&Qv4~wLI?%'h5o~]2I6}39H4ӟSzc?>kD5ǍW-41%};Kgzy')Qͦ?' I65@ԇ3@kYJTR6mX >~~pܵz_bt7D?\MDYjʩ74>"wi"b|Diӑ\R?lj`9n;`*Xt*s"9&؄?qɝ "gfFXD(k5ol)]gew̩:k$a!8,f&̎  j砖_}s|TJݘ&L'˝%]Ìad(*emE8?SXuc: F5g3i?]8n2rbiƞŪN4OF8u8Tb^hgEPE"[7l$GZyni'IX_=N 7j\<|8…$OKQLqOx$rCH9I>Cw~ dfg>=]5b㉕w^ΘA<"sr}%F鴦Noܘ-k~e=ooIDĕetzdPnoL' +'"~B>vz# ljp&;`SW0 w!&e1P_rzF0Τ1z,WÉ Lh`%1ߘ4pPw}a`: ţ:ŝ $pK%>%,өG+ C-PiTgV$~|I(;Vr䈞njW*Z'FYivq'&u(n|^uj̵9DBbONƒeR[I/䘊gǤ ii^dž씜+-1u:@ڪK~4 <,4f-&Xc0I9Q鄲:W]F\x8Iͺ`efUϜk p;С'.k +׳x[!޲X.%G*]7>\B(;{]zpB[$i'd?IٶBR<^U7"5Emp]Y좃'Z{* ,%?qN4&Rs5ᬾXYvg%po<}TLb3LsW< Hk-O>_HT0t"ctɝ.|23@ȟX֢LTK:Y<#"TѴr=yl_":ssC5ɹm` -8) +ar"'_~W⏺ 6# ܧSsCj spݰwZ !ts*;?81;.GH}vlfuUIYN% C-ڢ=tLF O/G㵙`~(o܋ܮ:ڊ sR!ߘN,oG_Ҿ]yw)bLx}W7|wHqK7 \Mߗ{Xj@;ճwnFGU0 _9O:_0tqk`yct~i[0c 0j{:*^"h+f +:Q|D=jy>@sW̗7 Y[O:䐝h~.#YT*#Zs6z֎CSI.u:mqcb!?2] ?ſ]IcMO|R69L<0Xb`ߝ XJLiu7ےx3L$ؖ8ׄ|kGr܉{m7YL7&9'[*']x)ۑQM@:GRfc#n0wcWr0:E͊ǖ4ccc*J8\mޓx˸ߟ a':z_ցMKȢ# +/Ims<,0霉lK M1/JZv~G]B,\? dϾ{gOӹi&Ffcm8k- ȹ|E2)=tpp.npgJ&f_G3> =<*;3{e~W + +nfNg˫.MXӨda::1JCʗr]Xz"& iiZgEOwbJ] :[:|;ȠYFu qYhȣh䂉+mdhs\4R~b!rH,z#+a@5D.h)'C1gz <6 bc+_/cG9<^g[Hθd"8Rj2,r-tݎGYw֐_wh 3Ps,AZ!EP=3C(FI/$v{ZV|\£y cc_KlE"|7 !o&<C/ޚXi0fE`o7qhN/*>m +HX m_` w4 BZ^Vo>ҭǷVKSӗyXxMJiXcٞ9=<85)<;$ [ {OI.FJwkL1AT4=:c_ xAl$;$N(89㜊34DRLc51v]fh84CT^K.\d3y'<}~Flb߃W&њX'lإXr,ݶx)cDii?0e[DThbXIKqu$Џ՗ON”}gm:3mdf1ƒyhJl|θ:Ix?:'fzgJ(Vp"ީ{K?4fN`tR Cj҅E-RjZKKٹ_OaOϒQ!X#$ASA> +c.LәXjkg}wÅjN0Q38iQ$&0`+?VO oRwGªs8a 5wA,]=7cG(.&((+U\1T`~s\æϡl3.S/Ql[I^GvY7&7[{prxPMa5-v{ 5C{*~YSf/ OgV#G|O&;>_$ +1k=Qq9{/D"!~5Ir!&vzJ9uzyQcߘtvgϾVfLf6HVrL [bʻ6kg0Θ}fbh&1@߁#әn.Ÿ5;i![AhC[hx>(=+L@#:vG@kOOg73؝YqO=K'# g{uDHٮdx,Rc%R\(m0 NȎg86{Sh.O5eK=ǚ(b?#6p!VF`mjx_ T&К"/7ggNfoߦ xfFRh.4c2zWFto;tTž˲PGx¼Y~Ju:D6HKg'R@oLD-+3V`*)9="hI t0{qYEE9f^YQf\$j1њ\e6b-v倦-"n@aWhiR@ƻM/5֝ݿ<۬@[Dhc@oLc}&" 5=f㑨4MQ/d2z2~RZh%y>I} (/cYmpI,WEk%K୩<ٲ( 4po.c6%^4wD\1P#-Pu: +V t6G9;hދ tnw1@KTpB4 ,Dйݳk J(5n +%D@h}ߢNt q.W}4o8a{HuHi\Np~z!A2o#^#FMnXjz +Z!*4@OD <}Zv(8|~*mہ9yܱOǖGhI}bbc%ri|+m{tO_ӡb?L](s_e[Tby6֕{Pze)_Ib(˽m֙ v٧D?}Z<:h 2l>͆3PlzEEiW>X,Pl(E07Y@O(eޘ?kDVZ6(ZȦh +fu<ǟK-势eZ҆n橪?IkKúDԩ4a s#.ZhyXMYG2csъOE,{IedY4qcYYs |p-\t6E@%=QtS[\̬zjFPr~︘XȠ=؏avNFP3 xDo2?!B/I +y B[qR;G1AZ%5?3/1>Nv|7<_C>I +>k̟gX +gV-~$VugJYʽ~]OyIqq)O%EeK(zlQcka' eͬ葦]U+ ,3dp#WҴtb[nUx:bxp޻VF\&H"vFbQjn37b4PZ$%aw{ |a3rOiirnȞђ8qoӵ#z'㠎tgΤt/]/u):Е=Aq. t?/&[dfJR O(zD_$/ypB>'Y, 2N +rJ_q9%ÜUb`35UK7k7hzZÜPNK>+^wEY]Ysh1%y8u7&m3^af fpeR4,\mytXi UkLPuQvb$ߪ1޷H1D0l/}lMX䥜A9VRASɧNE lUڪL>}s؋̣-6:Yq-ԉNjY5 mEBArOSl8) m=,{"QQ< +]\ᓳ$SnymT@<LP,A #)>dHA1]@(-ђ{ۛղVP8 ,H~[A=!ϱkSdm;KA.#OZ۱R"%.`/ u^cp."pKz%ZR,(ɊQ +Ɋ"$ˢЂqC04Bf0I%T7N^A>pli+Nܘt"(Ȣx yrSiLAlJ9FT.Mkhc2>Z ޻G"/v",,ﭗЃMyh|^:+~F4zS=ݘ8xG#M9Fw Hٲ@SC|[ Oա* ? +~ )}]rO )m'ռ [g!oE]%X47oRYSVy7:a퉳ts/G|M?뽓/љ`:%*`iZ)+; )|zcR_ r_'cD\N&RЗ@%nnhxOD0JdzD;zX%ڍ$%iUZ_h0kR\/.bl??h~vIiscJV[6Y[sGt_Ɩ/y12K:3NBg-UB$+f \-K <(0k&9 ޏ6^~~{qE;75%vpgfu!ρ 6_]?yLw?ZY؅6l_e+`Qg?_tZ !K-} p?T/'UPYb cm(Ѕ}b ~t$$$8])H:a[)ҩa'jQ:%s(=$"\5sгQ\iH$8GuySPK)G_A1Qɧlz|暌QHaqwm ݜ=Z31\*F(\gb |wk,b-7&4r42t,毬$= npnsuV7s%]T7cOny _]VЉ*]C\Ae/tՂW)WRC\A'~PC\A'A rBU5ttZjq?X +g: +:l *j-/_ $Jvрd7mV/NM_HKZ:_Zm:v֊tUK1sR :S6>S<>Qrh'zd*U"WJ(I̡\U4IdD ܞ +Wc ׇdǫ:.n4 3! bN9iĘ-v۶zIjnOZfAU3*u&L"/wlԗZ6^U))WڷƪCu%}.Cgjy`# +Iرɚ]U`ȳDń_*q zJ.mE:-tR^NԅLsP#KtAuICqu58{'Oٛ5;{rsх(0ϧS5}k ur4i*qS2(QUwJ5r7*e<񀔏S>Iա>U&,w*R{w E+R{J߯T~NE*7*RQ+RQ/Qv % Dԫl.nPT +'-~+fF)z)B)W?(A%pQA)t|LQ2 ~RT6WUˉB{,Vq&z"Ȩ3a.vsWѸt:/r)w^,{=GQ p^8iljF-}eM=l5:˴!zbBPbRncŎEGT3G=CU5KJ}1ԒS{.:>4]Yj??Nzɍ/"Mwzr;tߋ\[M'j:N*&(^/?n|5T,^:'tpkVUIurB7龩ڧ9_SM'UK1j:X߫]j:)㆟;;tRt2ARn5qzcj:Ȇa5+;UM'g[n5vNԕxOEk~N:(\M'["ʁj:) ^Neg䗪oTIlV5Z%TIsuv]ut-^TX}u(.oW'o]haNg* 2!QMa +2UrHP* +4.'ܘJbU.+$H!+apDZLݑŝ#͕#s۲.5ws4߹NvZ%Uri+Ӕ |gsl2t͝jDq6Ew?掭}SNѦ \yII^gQMlriO]tAj2:<+F5ihQ0O\_PH"Cԑ 9Y [`CSe,u6~Ofa  +J%\s6t?9 +:Ӗѭ،e߯T>|+(p87tT/̮o@E%dz-;LSa결SQgr11V0.YR6Hz߫RrKU]fP+zr9ԣW*SN'_oI\vU> &Ey?^uQxۋRV)l??N8.WQC!U;62li(d wJ; %+{ou7)U>`WnS'vSON7|*p'KR{Ew]ÝSQ k_f:S7sn:t+W>?Brι|Cn^z +SGVTtv.v"&(΋eL7eLZ,Ѯi1-eLAN]E)dT趟VeȪeUj)bDWb~UELrDDM{aT~a(qXS7j\SnSŐrtW]I)ou~h}׎T0U=ܔf+o}04T=׸Jj\2# h|NFƍ)}h4 r5\ݗ}z)KLfb'A]TPwcZ?T%-z𶇏)ɢ2<.WGL&W* Ƣnc%rGYB=vz:x@i; c>#U9ڬw/ )7&D`s2OR&6|s V\4g R@oR t`%4y +2=w>qE{#}v!ێ__I|C =:B}&arڪZhU͕%Eumɴ-0}|dl!],)>+*>_.}$>ْ3{+9V(':5qN)"hMDIZBœؔB%I>Au>}my%M Z!KG՗1]Mݡ-n:>fV))Yy^Rlyɧѹ٣MRCլτ\~v65ƶ\J̥ɚ|XȔ,n]ߠ5^_ngoeqBVhՂ% +-V6] ,-𸾝T`֏)$Qs~'ִqWVe\v=h 7򇬪BZ]"ķV`Z*uY>ɰgyڋ>lHe"쨴 +== ^nb7;"J{vj]G~]6T-B|ד#( ?Z^[zY]&/kN >co,5@XNG-%HSPeC,ÎecNK'$N>)׺S'>$ylk$,tGT'=V%F|q%? -Oml`y`]qq< SZ8 I7S{a8XV/Fc,ð0stҫ001ٜ-m"3A6qԇA6Db#-lw}rvj 3&JB{Yss\X'L?[mZ?}c>1 $dz^g[☏ͭc>Y?99W J 6Wab߿dwi 5ޥ ۸8%b9Rh汐5&7- +dXn?ow](]zfq.(]}HV[VQ|1SMc+.[Giء=C>fѶi WyuX6-?=:WqRXWƆJDjy]I|${F{Yr_9>VI[| +;bW%2S/-Vy2/=o&>m0 Od5lUkv]'*{ 5Pj[Re=@ tRf´LCkYŕy*YV<[]}z]7S+y)2=˧k\9,=tW$VEN[tl2_*IY J5V*V3HwJ2"->yZfl[ -iGfmUJ&ӗ $f¸R㽤l}܃ftζijk.'|$0~V'Nݛ)+7)0lםm2N’q>esoMn붷+c yg~;Pݮl h u.}C7:f0XqKY|38u3\xW񒹌ij!,z&-qt11S–⩤KR>%5H͖T67|0IW}i q?1V(Iؔ8T^218?1VT0Ky5K ;.`d c,M(5olKyKJjWiFY_X7AUԧ=_:в 9;˦ه ť'rhEkQcM=$pTaDr/؇ q☴zٜFıeG#FжrՕLQ&}]{lEƆ#J͖U,[V' Ok[#0|--rCnۉ&pvYYcR-4-Rqz_M%1*T0g0ÜU7g}^qLS͑O,*JuZOiOʕJmEw=E%MM-g9ti:UCmM}³A#z.4&?*>ROџ +T>u|Kg.9zgˮ]7#kx9mQ~p仧ɇ*qՓOgq; 9wb$~>=2yhjzUqwU␔P5CLDC1RXQ[hhX RZ} I L)&G \O2I^_˃t'*K!*&M,7' += 0p1>XM2%iZ)cUb&XpMU=Ĥ))ɯiU{p6eJV7I̘DU'7SMAnk)I͐䍾-HJ'!//cUmIa#C'DG~:@~Z5Ր5̔1֍1q8TܜK!)rf""]5{ˤURk 0aW4W![I{x] X.&ӥ$DXss( )rfXa!VdBUoMa'rzf~$@Jws_ .Rʝ㻗[1" [&kRgfݴ#Y +.&;kEfa$ד(ʊS  `RiWQ3f2TފS2VżaZA DxQL!1 +B SLFǙ,dGҶ?W(+2baeô, T] Q4쎲t7%^]vV_nO|W qBpڋ'Ԑ+s$۱U?.%Â3sKR+\q&dU䑐zO1I$I#Ҩ&xe@$DۥU7I {0 D䐊s#;.V <`yJWכGUנ*lnu\4IA"&oTO_O#(I ݲ*&F/9S3$)Tu3!{FcÝD #T@<MxK_IRLJy-,haNفD/ŪdU%uXpItoCO* 7oǨxHI8hdI{<A!yqSU2y7h̝nx!2yFa _ NZft|?pC.lV@rpJ iqaU;qƜ2frv{ӽ+qHJL__kڟ* ikfuPwox.i(5#dbL㾾Wg0'r +JKfX&m^9I͡n1wG L+v957=0BJCZ$/"GidR1I_Qd{X) caDR4w=I^{5E19,̜ eƉR?P!kS+) fLI219?{Z=]J"f# Hɰ[Q:?,'MyDS3q=A-׺LFRFH* FHe5`_>S0i}^ذ]2|n4涃47Z;68$%,DN9HHiDͲo&HN#;_uȟ: #&1F`{ Ӡɪjr2dzZv'7@*7CRy}H"vSGM" 5yȪd?l>\'Wn,0'LW,[ נb矫z +aHKުf4V@],υC):[jHFo6IW T|fh0s/F/La؈Q!0I!rHujn*^?ԻtIiOjqfh},zC/*^b2htYH 4itlkO-UZei)nwN7ug22br21Iv?kNVar2hd psg&wwGG-PLpk_o.RT@y&܌GyN) 6EQLLFOzq{,jx""襘ԥ~Xa !otϵHy۟i)iz(&o_߿C4y)_CZ`3oB"wsI#@nY9q~ַ#!ުFٗ4diȄdߞj\4`rVOjq X$MFif2B 9ު$pK]uz8D^f7zw7>d&)/2ÀI냀*O80G`4De$sgcznz0B"G۲.~UcV:jԋ}+F"oU3brD}KTʕ'%kuBG2,mH`L$)$%n-Vf|D Gr9FIMA.70g'9|*29Ƭ`C&jFY?씩;Bo  i6V3E,`*m&!Y*7dR +ZNJÌ(#]rHF1[UMׇ͏YS3#19Tq/Ywշc +uv.0]S1?|TE{ I5 +cJ\L~ea"42a:u<;{Π'AլHIC 9^cn}O wE-C)8و4&){=3`rLCG/n`EBGbՊ#(џ yyȱ}~= _g!E7"PͨD 57d]e5߻.tY#hgpAogoDNiJO QpiɋGi7&% V54r\u+틯u_WÚgЌFoߎO19ɿh>}۩r 9o"by!&#!p@pZU8%y;1w-u͘U8ftMUe %L@zhb^ΜܜKQ kyyyPjn$Qc.%iL\yжQy4K䕈hX{C!IoňHXjxŷ{QŦ5ƍǻ=mr8̀ɟ>%Zr哑5q$Qisal&tRQsj&xŸ6:\WO D:ތ!zH$MNvaʕOAHrTa.gV{j+ɓ4ӽ7RäբtS桏DFD@Z{yfY LtWNմvnh0HȤ"1` ɑgp| oT,1"YzO;[1OQc$ϾyK~0 Twjfj1.&;՝IT]9C^CR8>5/U[%>^`Z$`j7z>$ȎOLɏtL&&gP;t$B/vp0hzUT7 aȺ<8J<[oFw7y7kW.V7E5=[1V3hHnɬ?0,`&pEpBcnp +RiqDf$q\<0-y:4շb4&GvNAẌ09tQr$5fDcnţ{TN#ra=BY8TVU]8'V578m9̺&|v0םsm}ceG f/hhbE^!E3jOd^R,QPߗ'p !@f^uN*0N3T#Om2~Ivt(a2i[U͘ljH"FwÚ]АA&&G4W-2,`J# W $:@!U砣ssx +3rvU֪HSwotŨ'hۭp#ϾMz*&#\/XVFE`fy[Z#zI!$R]y#R"~Vf\$ˬ&J%)}K=`2"i1v1fI+*^+$X# 5p1W" f$Vn-X߀tnL9|I-#$mkoxH8t<̀_wȟoI,&ͷS2Mel)c81Vvz tQ1)t"m,wL7 Ez@S$4;~_>(S֖А`j4&$%`r,]O70 ,bz Q^Qec8~= ;AX .C'$b{}/Sͫ!'yjݩ,VQ~Ƙuߺ,6ſ۔]-?zUvoD*9!ѪirV9āZOeߘ{,*nX&.&h/NCB譸?sU9?~9Lwf;='+eo$OqYzg춆[= 4`2"j?>!>5ʕ'Hkz0$G$w&xb9RjYzL{}|k !4&_>Ue}$'K`AOc$iAY(<&Ɗ?C@Rx2SvK0iL]!/YveyriGEELj`JKG=o_x@n8d1E%8{r壚'wgqQWxIAKUA0Uo]E'ouKГGΰ&#SfX+[CF@G  +'#O`V/`탞gC09rZv嘘TdI'L!8:xI'AL? |F"CIpv_<( +4=ؚZQ + .r eTg@3OI'@4+tJArf /6Z}{=wwP˛Г`J4˕Y|L3YncideaB>JA.vT$rv 0sxW= Nk,`jwIUqEѳ0AŚt8 :A孞_G$T8M=z?q)阗O&9<5Z} 8e DC.}l=JAF.&!)A''䟷˟!J2ʕ''U#j sלQlQ(ՌgQT5Ѭ06Cr\yRFT0thI } A +ϳ&}V \n +%8Iߕ4 QnA++ `7t1Y*.ުA͖8y 1`3DLjh8&ӣzvk8I!Ch'A0A^/!1Y RbW9I+YE=qX8}2qSj}sWBF)9uKIROolxn԰e{L!8|M)>m $3^'& jn)&$]&8$f{XLZ4X$Xƾ] 8h+dߌ#twW(c s>so@|&&*V5-JFoDqP8zcH rG"#yp| ߻"$B(G\>V=)B-8݀ 8i9JW"ϡ¹Ih zYfTNIɶ0YQL7Һ.sa(L&M+ܟkܿ,pEUs\!?_=v;3`21JA0 +=v` +na(4-|U5c)N5VaNG}]4IK S6n4IQv;0Q:|W5[J=`2*S'INjI(-o4dIH}|`c'rN%B(s|@w/J/r;8r UPD8o= /Asis;=j<`*8PqxdbSTWghqe'#?^܃2A 8sjaj0:w$ +u_>#3wL6x0N:q֫n+^oB(s]&]/QȾRcժ#!oI4Nn) 8;M]ހ LBB3rXUpLi%uu\0szS@p+ܟ}c@~ルnPi$l.o0ȁZZPГ' $&;(G<=yb2HͰ=\Oةf3qzt`9DR^t}퓰R:d1AOf+SҋYr&xu8"'>UN%ha~&g'y$zA|v^gTd6(!zv#bJpGtzք^ovviΔ,`oNy{ TS8G& QVJ8bQ~UX){ћ|êZFoD !K='쎅\y +^M(ΆV-X'w> vĜ?-PQBOf5e!\bTf6]O2!n' uB~0dGP^(=wg:i꿡_;qрI犬 ?K4Br=a;Ѱ"P<׻6a&$3x_]/SL)1o,*L=Wd OSUtg7*9>]&k¸ܿ,!1!|2-v&p"y͈&S'yD&փn n6Xja E(G $WDL~UP!Iev񓀳9.aPCFPBOfyaU9ߕBG攙Oe,rR d123M1@FE7 P$/z<%~z2&뗏{`U͇U$0. &:qzRГ$Mכٲl?|ҰʅUDLI#{X$o1enz`l^Ü&tz_G$R6GcГ (&٩&_#`2S/CTD : Q=" &+ݟ:y2 ==~7Tʯո:J'{Mpv;=`DqGBo5}(sc4^19ɯv~ħrLiw氠Qv`zR -V 5 +mG}z*߼,@*I#`gO˟!Ji' =Aq:xQBpti$$Q|$;DL~iXWv'n1 ň⎅&(G |ɡ4~V\!bS Q7XuB.sl>>A4IjPg[2$e>Wg=GQL'yRIc搰}d 8<޷;x@BJN:0KҧNM)8LgMָ@03٥I޹GrAB.1%19o?\'7onASDnB7NnTcAp2\J' W )s4z2b:Ld,yI՛=T葓AVkG&19UIܦB$Ĥca^z5EEx+ބ99W7 &$_K=(4>brv0.vf>|"&F^na7Qݶ0 zsIgC zwG&0^݂99Er)ov#%gbaU(Wp!vGhIIByDm H `T>"镮AOPLɿ9FB:Ae'Q)ȱqLFQyc.E )o=yU/ڮвDLkKeb=\T DZ^Sh&A:r?xsГ9<&i}'t(WԌh8z,:.  &?#CbN\ј=9$QߚwA<xC-I"Oo؍ &@Of?vPN)&!C~Nq%avTn" I^ja'SgP2I>5[ $q˳4D"4ŭiEX{^\醊&G427f\T ǙdND  W&-9Г$yh?&mٚ9z,'s k> NEQ^&A9,+j>VLaUY3[nVܦvw1 `Zs?} X)w{!&bz#_Ej{ۥ `2#mJ'9L]U\1'A0s>=#$)9<`rD&q&3ŒQaﱈnn\$_:}eݺ5oƜ'k7ې}qea Y=DvH9 = GބOyQ2y~]gR=I_w M"z$2NתVQLN-LkȚ0락|cI0ʯtH;Io(p{sw ʘ;Z^Ü&\fGP3޷;;e5ۮHI2j򏞔>sHz4VbҙЍ[ ΠNg띕$Wm1!0Gww `2s zMZ5NL뮅B0N`>ˬ%$eN=˕P寞Iop2`/8NyyZ'm .#,7M=Ĝ#Ys^O +v~ׅ &Dzs 8 znŭ[A (!Q7$e6'vKRLLF<%}VDd&S32qZ/0N:EZ U +g7:<#޷;H4Q^>Sij)II A8叨dDo&Y[ҵӓ<`Y_-&2J!1TpgwTYl .5V;Cun"$eUno +D$Q +੔1{%Vv2 +=ul!ġm-T6Ua -$j^o$dBJ3`{ 4L\Ǫh8ʘ;d$עL:G}*<`rXIzfʳTL*KqIGj8 '9YW;&b_>鋱rqg5VƼ3L#'L /GEtFE٭CO:FEI 7%R pej2+emLIђ;[ɍZ8쓎n )Ă hKqo!;K8m!4`2" k/n+2i&z,:6VaH30QfodL|Sf73Iu)82q 'C.vCO heGU`|pD۵4J(WjaogMr;O#a^tU`qԌ~<sed?)}|]S/z=qn += DDGn^'@ZFPrK^߁!aA@O:9 =iΠ1='#ov*g>_2SOq xqҡQDN 8Rc}AM!vXt"ISa>DQf8|FfPY!`2IC 89UhD'ox. ^ Lqy8#9FBg7ʘ; z r H*]9b9W'I^} 饦4=qT +rt=Yn'AL (3v19y(Wde̅(āqiH0x?qS$OyNa(FzCRf&_ +%/WNOpwS LG8LO* L%KGo=CMaLތ٣'idDV9/^ &_Bi8٭,6 +F? A.F'}>Ĝ#{l'}0-W4`Q3Pg7N_A<]dL#:TPvrI^arD!Y7I%x>Vf},,rV&2l f);9(OD޿,XJ0I䏟>4z,Y`TW˿ո~.r3# R\mߎr 슄8ĤsB'A 4^>2ޙ R՜lJ+rnEﱈL$bg6a-0fyt~G,!,c{UIDO9i!z?J^iHχ 5x\chdv4<ɋvn֍7a\M0k1UG5-v"`҉CO: zŰ1FsV-zgovwC^ Xha솧i`WAyv)yUp/NOɟ˟9! &0!#'{ȊRq8 (?_7 e&PQM?ͲoP4"쓎^v#rbY)Ḿ$LPoN[r>@%_FEA  @!"65Mv>"'y7=U4`f5v_71'vx 턥a7 H]\a"]V9o]!wR*A&_? `stǢ` @"Ham?`VM<`?)}ZOg;NcDnj 41;iFT8DϏD'Ŀ'b;,`e3=QQhULOzuݲʱ$fwr$$Y_o5]094I$-9viQsu"awo5 F6 )^o{$k,`˕CLS-Dq'~$'AGSt8MqZΔ$q C.&_ɓ҈6IwzE h:x8| *+;GE'Z,mAd !jdTUhDE[x@J8EuII.((N_YF/0=Y;Oh’ɁZ.J7)Y&z(m[cC(\^V!)HV%fV{qIɖ(> ’I=$PLr2mou ju|s.)mcBF2t; zk;4 DuAc&'-ODyǏ,~1v.)%{6y缧M_[ ےX\%JdbDZ-ﲭ}Ip_=mI:ik7i$y['qӦf'M#.qQINw`\`}p\o;s?{YB m {QƼ˰ \&ɗ<-e4d/VR\ cl LJR۳R +m#8Sx7WU~& I܄= NҗVfRS{(x<qRt 47*b'qZ`Ad=O6IF~`0\&J,yq18M{4R(+-wԔt"o JibX s`DOFGnvX{z>{GTQ2tR>WR~Jivm{^(e᠖Z|1 07b"I[ݒx*$u*nH`{Ձ[kmQݷFbkd1s߼hZhbv\LGŔR(Yu"o=c 89Z.Ko KY1^kMQ +B\K8L[ +;EKf[ҚWrQʿyȎnG6/sfX0+=XL-bm&Tu? ߦJYX|Xe_w 2G7ZvN1B&f4 #ʫu6d)M'bͻE=P}T&O2(3-ybgK#)OV^ MWJK#PJ > 0.?Eyp#sF[VqMQδĔtSєsX}C4J5}9U_rRʯ+m7(є ߦ yo(-ѳ3iIF}2{Qbo20kttwoj|2B^ .we؞;&66[ҼXCL)tsADs.܁zNd?2[F!,3!Rǯ 쒚LC{)M'⓸sC>h/2A{/Vn?h./·AҜI]0׻bJ~2܄S9fYN' W2(R0zwiq{-XnL/lOʢe&eӎ@) L-ma N勵N#Ey a@ rcV27J龕HJ]ڂb L1bJ>9V;[V@~ᦌ.}/I(eUJ%^Kyz]B$]-p蒯^}?ȁbE v̷cI/Wò eLUX~1 Z2b;_{0)kw1\hTy90wTRmQKWg.A#L}JM$D|)ݪAӼ+=,Rb;Cu伶Jd߷Kjɤ$M + +g'+]a>۠|V_v+eJo}2vVc0WeE0.XǾe)OVXkkRY<5RKmls9+W{1 xqrSJuԽEPvVI(fH7:.9ܜ:32F1A/:} qHU;$?Z_bRzH2->z-h Hl@&ދ(DEh ?}j(b<"ƘN 9 IL Kˤ"WZ=;YE_/X67!_qgUK`; RK-jqv<̉&8{PXB!l2NJ˽rkYJ3œ5&2Y늠wGd-jۑQYxr"'+9'ypP_ՠ|1*]j./Fdp ҒA`;Cum82e))~~!ڞ;e;aCμLJb(9Pc1d^oZQ0343Yb9^z;K{1ƋFzLo!\&UddIł3KLFtj8%c;O8>&%zr9ɼ v^}b),ZP,0b 8Ė'xE"wC XT)2 =L 2G/֡ bLEe4y|g&Z&O^ b;{u%iCwے9IrYR>"$ojLBRv d2"ZC&OBfons r=wD Q*puR &g>9)ZI%N:I&5ދЪA+eGJ0(DL8<ӈw[xJ,=`;ߨR&uɬ@RSR\)OVXkR Ks="fWE/~BG6 tR6Ljbָښ-dQ/`v5b{&o`;C*'Ah(ICe^l/ZX|1=:R<|a s *^k2i.vVHDe{&_\Ldr2re75TtWj|tf)[4`d>GQQQhJP5VIHf&=XGEˤz/f-/WJ2dKo`d~HrZצZX Q{k^d# bָ.i/k͕4A)#[ ' Fes9d-n%}xi|SD ?<UJw+6b&ATҌ(6J̢~5no$40#%ů?l'sk {r{q(fGx]SJwѶ\l_׎OHhd,I*6[8 /k0d :IHQDD~gᗛnr`JGc@#\f`; *4S L:ZDVd`bvr,7K>y[]YP}Y)Ld轘:sh e JGw XUmN{-XƢ"XeznOXFY2+Y{&+!HͳP8%WRr$cg%Лb%Лm܈hM!,TZɟ*scI/}pșv΂ދYJ=QlA)s~6UFpT'RKC <$Ho=@@,0MJ/R.5W3(gZ,u.}RK?+eD)ܷ<; +ƀ=a.uW.wZp1* UJ?2#ViWپJd)mSe['CX=yy +zcB&3s rQUFQxFɾؽÕLP܁+hn'3ZLjyn.7 +ɀmAd.Wb`&XՠɾXr8F/M7UrOffT%DDgi)_t@v2IG|Lf$~e8S9FI}rU }g ?}M^g/"D'jBIB;S_5OBs +xs_.|%o!ƞUeaR)ELlnK^QTTFml]n)9O2iE2%OtK[o(O ݳ/H[0I%T턐4,E]-w:JJLrKF2EK3q+r'3.s kL &X6:4cW7XouiU`2<֯&;Ȥ@—qP_iɧGʼnB&s),!ڨ?G<-d2_r;y +-D-!EiѪA3iwUy[X6in d2"nSeS-֖>ZBn#%Y9d&b O1Ĕ-t/%V v endstream endobj 30 0 obj <>stream +dI"![xi n/9Őנbvi΃# Wj+R?Gs{A.w-up;i{h[adv[e,yl#uVA)uCI۳Њ7@3èIq΀ދFC~B?WUo!gb(e/sC&+&ov6Jrr?6xſO- ~}\)@PyGǔRC#IXLcT|#%AX%B2Yb!2 p _a@&b[(RQ{1u2YaIȤ HT.moAL2en@>.3)[ՠܧ?|B)SxsAȤB;HE^z}yM d-uVY-\j/]f9_AB>xR&ctRuk2iig zi5VwYl.+.i(6f[& Й~cJ)a˨Zm4g?/BX +dO;..]&f]^$_(LbR_5/Warj,QJL@&s ^RtN&`@A'|;|u /IuPѷzjB}!d2gKA^4nrK'r1mPs6rS <9I}g N!E(~SeEe]uZ$Vx8+d2W(W/S2%_khfOk ʹkۡSwA /Bq Rkkі k| d$|B zh$5SL:[}e6w' .Ox=C!t ]Lu3|j2Q +Np<>R?$vM%,U`5* ?QʘL 4dŠ&SAe^4w}3SJtc C-Ehy+z "{Q*7jwȇBLdd2nދF2!¯Æe)_҅ovKկBIGNgS*yZU%> @jd5jbH ?WʷHU*2X-jzf< #27yh9H _hhLPv@KE_eCFNRz*Al %Jq#oDl=k+VGU +ypO@p<נ`;mM{;L*ee+Ƀq/xI]4EKoo`iU}N^flk 4LZEeV:^bڨÔ_qVʶ +[%r>W߼ֺG5Pz+rp8dS5&߆C *d2({Y; +zk20BWJ0l52 CEE3'.4yj,RnmHׯi@/ukg*L+t[EEjZ싥R6F9ϒ֊hIJ:x=UrB+~{:iJ)*:~砪|qU-̙IV,ɜ Zg {!~62k.H0)LQ`aKNaX5!ěa{ۭ +Le&ODPRhD̢zMI|VM2mC r6@&MGyMBE#`UL! + _CcJa^rP + MTFΐK2 8X_by~݃ty 2iz{8얮I/qJ!{5f%((A)*J龕mh @eGBfd/c8c JyABRJ.*ͬL%v %h[&7Iw3=m!z/PPj *Rz +e>ɖ->RjUF+7*3AȤI!pYw.z^%XJ٨|>[,|OXJ!&視7:EJ}eM)i2 4%%NyTKpPGW؎98D \ V\#VVpR]>qp x2Y\Œ1Q0{ՠx!tL!(e7< ̥ z*̛y HLzL~cUL^4ᦂ+³*OynͿG|c#drPLphx2%W[_b!<@~_鮶^)Y_qynGVڞ5ѦA$*Е<d +{QAas#6~=X*nWN[aGs>nWz?*:;&dɤ[/OILt; l b [ꪲ6xL{ Rl'$JTW *!΀)b[[EaNK/z/ɕQ!UϝRs9=g™G/ +½2ǀiGnmSYgFowK9 VH.2YH@@PVCE?t׻#Is;/. +Zj z!` +%o?PX-LA,yƋ/ƔRPGhC'oFfm]QC_iU1p(%8$ +"UAN|Zj^?(%0\&LS< +Qxa7^eGӱ y_8?Y'eˬ2 +@&p쨲t $/D9- w0A9WʷaEYy5zZJf`/%ker(DLb"IދqM~qs^<#ѓ7oE-d'Z=b9ݢ[Ƞ%I H~*g-y㬷uh8Ӑ#ydUR&)3J7" n TLTXɓy2 ҂ ^4^mȱQGYo=#ן G7$V )+o27C46ǀ +CGNlC'=t29c{)J=V ;4s74 2I^Ӹ2 2f)^X<’lPH㎆K@su*M^[' 8{K/a_"Z.| ݀8  Q +0qYO>r?ͭFOqN)e[7]Ɓ ޿[C36Q LwLz/ +031-]i+\)ŔRc6K:ˬ*kB E|wE̦zCW"0 VjU76g/K9mt=j{JpdC轨3WZ_ټJYc%JiS/>k2iкoV~C>'S لom`fbҡ(eH}RWJ-]dMچX?-DwSTؐ&Fe?A&A!3,x @8d T ґVASzm2e)CNڤ2Y Q+8*J LXc=BAEpSg0&@PyS'ԶMJ-fDB놣 C)PYf˽҃yuOfBT)UVz}k‹IޢB)I9]nRki蒯+dʷgf ]TY`t$QPQ`%(yv>@DeJdr(V ' Q ԋNyU}47:R(ޤ[듂(Ў簝PJ@<39$h"^L1KMXgdD)OR|YFL";eR|>IPB&uA'K\*Fb "7f C)A^Q6/Z9v Z5ug)OrTaj5zg^? ">M2 d dFE}J}砪J+ Y A$MJ HLA&A‹Hc[/&XELDK,3 ]uC܈"60Km h[`L! @Eݹ&/n6xqur8A(cuk gZKA$E!JJє锒akZe%M">Bj렔 G2)Vo=ދB/8Zs="JI>J)Z$ '7>o= /A轨7\l^a%2٢rDJiXzL$Shf,y4^4lZ;Nº#XFd|/ +B) L~>zuM +Q@'mxMzp~;17j!+e?ǻX]J(%2LB&A%oi ~? _ +; x+xhA}jd "_ L AE}$`~9,S؂$BnXAep^\|7@ qa;cْl4!I>}"^)F5cd$0轨 ]vV{}AHI~ށB%>/³DCGsA(bSD ø'V~[zfJYl)J;dP5z$f +`2Ȑ:{QKޗmd^mvI$Bf)+XV B=]UOc8{E2ug_Qa@Db]2Đ=’>uǁ16W_AIe)&?L^.c`0'm^DwX(Z:fh]J(%ȘD `h!O-I JAD){O"RzD)y!f.27B/y.\o>熞."O'_n*TYJ"4~gC&P8\kIFoCXPͻ >B +ɣhi S^2 +^T Z|2 .<#ҩK י 1TPT.\mѓX3AkYR"K wR&* +@v!uaCFE=ˣ{@d'x+RB) `{&c p!>9Ʃ ʍp7QPR"K d[)U!.$XΜ Z5HFG- Îy$x`@dhJ(Z "~oyIg$[Pk;]Lz/v&g_9nDD(N)$bNKW?'?)7 DMtpCJdLs=m҄h{:RfW_y>x ᓈM)*uUwovW[*kZaM|tt:ˁ!Ň7L H qf7o'tь1QG/C&rI?h ~z ]F1sD[-=!_>({?cO;{OS⟷ٿl_ǵ?K6Y[+mCL T9Ĝ9 vfZy*čmZg;۠@&rw{d(3*]rlj`kvu:#/9ۣ +yA(E?SãZP0HK|E;.T9N4PN2'n;S_pxsJGlyо$|O/Y_8Ĩۻ!UKf(VK2މ:p`̱؍ȝ`295F t9g,wE)A7uQ2B#q L>ڜ:?!1+m>U[n $Q!^4^Vy l +O"r&OxVW#%!8ΤTdՙ;gxL .ϳa4܄g;s{/b;sy +fN '}|6&f<%?\AA)^{Qc$m/h^JUJ$"ѣ= +&HQ (D c2fJIl{f&.v {/cfT J-fR,DHС8ޭD^r +(@(3dU 'mF>mDB6r< OQ +NBXNӍvAf^◯4Xn$"u:'ȋ9 `ne!FՠtIZ>ȍ6H]2v[ǯ 7aA'NtEF۔ ҕᐳ?Z|2DX"%sB轘ZؔaBnI'SA/\"HfQH!b#AȑD :>|~SeE?L@n%sB{/6[mR_LA2Aּ>'sRz/fX'P%ּ{wU$BPwXHL22NGDڴ6bPfMN'I%z/<9<'J7O"DEIWe.q.ALK!~ͮ>?-"{.8KQ/D9-a'=:Q5(9Xkͮޠ`c<>2-1r`v %Ʉf,y'bsQd|h[$H`vY!JL-]eU05[|+xh@d=q&:RyẌދ~ +] |2c"B+VSrFT =~'_z7"_qˁ$Ћ-x0=|fGg3g[:9"F-[vD cd"6_R[ ⓈX!@O.`%CߤyX'{]}srtᓈlA 2AW^L^ÒwZw. k-y_9>Z6EM@_& Yֆ#9i>['? '_oe3<| G$`&}ދI}2ڊKE5oA>?܍݈l/-!&z,#@ ŠB޹ߧXIQ,y*HLePu ~ֶ>R͓KI`8hDi0ƃ&C|a~QQ|d1?~-pF 7`a +C9>yaAQQ'擣I%&`:z\kp(2 U`Wh1wvQ Iث] Yc 2Ҍ"iщ$z/$2I9`qVCYa({QE!4nO:6zɺ"(%¸.z(ވAVA-JZW$/~zǯ']:ƻt$6ZLfSJ ++f>++=R)b!:& =$/(; jQ0~*g /3Ƌ?BF'IWÂI<pb9Ή''KNvDP)( +/D)/AxPhs|ȂE jkkc)J,y# tqD; +(Hf UC! n|'$B`#[F" 0-#NNKMX򦇻ɏOOnuvQ"E5^g=q7:nI +.(7v]'Kag%OmJ,E DDHN] '轘ە.OktwBw1E b)J$B`>GUsr +/D LZ5(,_D|aM%{֮-^)JҍnǺA${/TNLtK&A'S.}V9ةmhA (:,"_> r ZM"ÇC.]u{ӝwX^(n9*@rH/yTNݒ:%VչHw9奄keQJphAwh$hrԢ {q(S\C}Cvw-PG|(yys(?- cQ-@E @vދs#=JOtA>D>X:wX9틉F޹(.Jh!AqQ;^e'AN%a(`;Ui0O},șzѣQHTD8qjn;ݎwQe{?߳՛W >(.JJ`m\n'w7OnS1i:f LQNp/WIp|+= DNA'AnB^vP4=xѠ+2.rWis,Z_}{+֊D."ʘ(A;K2`}6pU՛]+/Dɱ.Gd;Y@-J7hԿ*d_D?١]IyM:oR 2ɍ0vot~O/YSu7/[U)Z'{sh-2|Rwrsv0VB읏!5/!=?`},OFR妍pg8C1C3N;G/tw++dx{[rIo}ѺMwx*$&Z_ v'D$l?m1A^v9iw(5lrszϯve_^i|.OB~o-am7v!:E#&W7^2P$B2܄S)@~JC\8I_J^3~R΁E nwњWoVܴv(|Uv& .rԢ4:/Z'V? =v9w(qhe_cqğVHaMO"%aޮ`)YؒǩY$ g[/VFaŵ.w˽/yrn>%"S<9_z:$݄,_E>^9yr<=UWVܹr*VB{wϧ$RقXJ7k&zy:KIcZeSr#:@>B.|ON^5(,_ﴝqC<82v`t۳uKrO.x|)JLv?oR"/#9 -Ǜfm7vMs#َ>A%aϤ$רXN^ww8~#KY9WhsspbjZ +Q.^UA|_F1m|^gw  .51UYop1 8C@/T!3ใ,3sߙDyRJ#ڴn-N$S9IltD~G!#t'*z8"z;0ɁkKvr[Jv׬>'z&. hxL!yuK%kK W%o$UZ%2$U"J9,#X 9k/p]<^'v$`g Gί?yb<[O]w(*~*$/D DLlHu9>~ThJIiapIi6 XN3rO.())%>YU3ݒJELIƵ׏.]f-cO]W΢uItC':e' JiTU6) ~"Azkm +Do}v:zK;%l<3{?a܂;q0qd1v'o+nj^\H|rŲ'){' Jߊ:$s.yRDL~ o{f:I 薆CRvŃ䒵sٽUغg)J2T߼/br eyؖuU5| E]kK'q*1m|Э$0{)=C*Ҡ;'s婭6)2BI>4Xk҉l<1tHS\SM _K=?yFYJzʓ= 4>-ˏ֎F9VE ݵ2z*'>vNrD^O33U)\%-.wՠp19ΎH8G#NfH:}qmj% %}O^xcgVm[donv\iEh>y1WntYjuެ֖y;o(%"GOr|G 0;+e,sDrr˻lڅ}47.޾i?=0%ӆ#%IX~Ľ/%ygqͫk.V֥dME.9;9`䊻7\^vὼ,ha@d=qɉoE)9VYhj{vK}bͫnsX2v' >yQŒwwF’ ;'w]z^'/U=#2"4F E,_G}r{vʛ|YXd,E ,`e'?`gp>IP0̡1ˬ2Z.=A${Kuh˪{Ui/>~m?3$%ow_Pg{/>ӧڲ;B/*uawjnqƋk\M߄7Y7.^$KQA) <ި'KIPr4Ѝn^*/=r֝KjM$Nt;yr%~[X"q7\pǽUM{!}޺ ?^|<6IIO/!>x[( OhA vg}߿dR YVװC:/km,L&T/yNnz}?=Wo_[GnϼcRskͮ3 Ƕ_pOBɫ/$_y %NyjvSDsPȤ1ay9i@IbP }2wO&;o(KX@Lw r*E=@T+HQ&,yg~]lE7 %Cgi k-c ?[n涒 +{|ɛɹy[( -xO9MN3d+UjdR>Q} L#ח6>OrU̖O'$fRtQ\1ٳC^gʋJ%|[(O78_z'qVҺ.%o' ŒJ/yʺ/b e!+;9 >fZُ:ـ(}V'}&)dڲw[v4$1'z2h|jK'^j W>=ms邞JKn1% W|]]]kI( ){nxy'JׁUxa-?q6sJs՛}\蘓dpE?і{u鲵U?$߹i ;Ҫ;<9WArCފ7.]_}r٪: W%ɁOVNZLG'MW'|{ڍ_~hY.IZ [(vխ]pOէח#' 'y  +dMNrOM RriD{LmnNi-^_ɞ9irǦ5KI1d +s]FW[뺅Owg|;GWJv՛w^'ɗ+O^[v]\zw _'(ĉ]H^P&d0B)h+p)^ŅRiIIfU:#e)wmZsr=Ut;ؠ ;c9ejx'o]ᓅp  <3W:U-юo_'XQY jtҘz@17).~]T4r_5|rݢLɥk Iz˔r8w˖Shix' & Mt>~^A1`Ԭgyw~7вTaM2>EIhw0V2M34_e9_VB}Z|τ_)Y͓wK!G葜-T,\U]rOz}?e'$cRjO*̃kܯ=|ÕL=t#$Rn(u0>Sd>]Rzy'D7Byjnh.ge4">w׬zc}ɹw̽ Wo]M|8$?ûxO瓨Biэ!'gGS +; =7]T[;sD;6!yj[tr46Oj0%v6<9߽,!8v;α}>4{G:l+xn\n$vNI(^mƻw%yG2]PϬrW%%)rY}k ¦N9 }mtBˇo/e;E˖a-ϫHQ&`Uu[#|㛏.k]E&H/,6ٙzᏎ]&>ɻpL 4l6&|x> @dC)cЦ*N38m{V[͛L_)YrsYϞUNز$n{OC݄2܄I!)O^ +Gd~8!-[dR.amߴG7O֧?H.79Yhuk=MHO#޼7|@F=vN>RA0{U.G~ۜm+O8} ݸ_^G/hvCګϒtwx'L'ݱd?}V8OHq'Mך]g]eeOuUuZ~2"|NNm{(:eVy9"j:~]zsjks#|B_OΟeQߍIw{R7'gɛX_GcdLG,+h+h'ᐳ|'^#>95B ,=HN#Vh!t嬉B@ qp5>΋d܄N?^{zxRgSy2')o7T0eAMkheay? 6E.9dp-wT\r٪ū*z7| %'[l7:e$z޸*i2 IwD#K(_w[L Ө'}m= s覸]lZD jPJǣN>_y |dysC |r5'yI[&mN3Ar0ՅI Х{:N햾,wkǷc{ZWۥ솮z8z yKz܌g)wоE+0BF3 ޵]}džr^2tg˫$R +snt$z,`20Y],s$u{n(z9UR29Ds`Fn-38%$UB<ޒOoup]ЊQˉO.>O8ƻ\UpvCjT=ϞQ:z6.ٕrOwU8-Z;ZҾvO~ yGOMw^Q6]J|>Z_}R+i4iKuKF x8<$b_zpyew2nZo_[.F(:yeisq;L1;=êAp;bqܘqի{|i/X|ɚ;hz>YA/5yc&Q>^~GPM''v|.-0?Yk't6זm Rht^oRu@@:lQV\jcc++ޅ#agO d@vnp[K|rC4ҵf:#Z\EƑsJ[7g:Y0IV׷.}rmO.Zᓱ/]k@hjLo@iqQ83m)pTiˠ(oXGyrcww`ѥf3VBpœ&>rH]E|I? ~ R&Xncs{i9lkB@f]ݞ >P>R+x. +3$=dR\ow֭D,}v35M?}io*>L)4emWmeO<?1D wD9[Bdr㧝|%ҺG/(“w4/&mj϶P_Kܛ{DIđJɋ ;,#}<}c-;˽^MIGLن_mx]|^AYgU(/7K's{7-JXϪWZy6 wGCu:&&ֻ9C ^w-gI]ADnđssfTss{+K=|3eI̔~ܧqc՛}>x+HQ}wt eK'{K;\tly'y& +$?x{Uq"⓷Ts\h'ᓦ +vny$id$יU,yIIb֥|%Z B;t0po(6=R0a_͓aY]LtKGY>~#x,+mD##/(29\?ڡ:T)>`Jzgu•uIF$j;RrUNBOИw>ZSY^mIΦs&R9qsLj8>IAŒV4Z 8_yEk$ndU9'LMh\JrA3#!zyO5]vW{ʻ?|ӻ@Hb,$BIYa=K:-rCȞ| F)pRZ $l e{;a{XdK:H|ر-{ށZkA}@&N`#")z* +K+}r!y1~jvT<\R3Nrʇ7(e\ab\ ]<+y=~E59N'#$ǢHiΎؕ-[K~yuLF͢~rE?bzr!_Гqxg+682udshcR_<ԾF@<~{׍W)Ma?X2ӓ9֟5zux{RÚ;/UOuEOLI~Q#9F8{H]vZz]{;'~A v%JƏv@ιAX}61rzjM' =V3>C oь} $One<ڷi ?LħCvˑ渕&fᚳ\u'g]~Г~Г^|mW$a&IӨ,ŷ,Z %8Dž%Xӵ{Oގ28䏝y֟,6G %R2pM4cɏoť擌fd$hLES>M]<7PmHazr›]xً|AO:~;x,E3YRͤnROɘ{~-m$/vRԀ!7kiݝW,o&/b-?Sl (+pfޯOsm]rAG aqR!I9%yUi9?cE?1=9g$-R%54$%{>].0nR3֘s_Z6/Xwwr9;XMo|۳W$qbs̰ҔN%, ֑5v63ThɉД]k1=k d\-i9}N\㕋;JN[HX2+g᜝w t8bR^&0?<(ۘC84o+֟26p +՜}`zr߽go[y'RS%rHAyg3=y_O + SOZ-&er"3qpΒj/Wbrә)g}v K.Ȼ_q4[M$mi-?W2*5rd[mA{INl CAr_| of$h_{:EGf(lю?ŋ) =?Kd=} +:qp $e$܆'3՞,4e?/䦴f더cxsSޓ_~ۢkqz.ϪKQEy͢VĜɵcyr'> s *[*tܭ];nLv8D:ӗR:οzhx7qamI:fp8F2C #)=\a:}=".l_q׌7Ef(L9!N1>H؟ k' } T=IC_*_n4NX΢4nV[ozœ]bM/_RIDեRCgam0<()zEc\xc"QE_jY!x.oQl,[h~ GTr,MO)KK{M_|0 +ԓk"I/>Y_&bsIFCc6'kˮB6's^]>?xW^uݍL}΅#k/Ĝ_|?t̾ $%+U#SN|ahs[<[c"iJNd-8Uz|YLO۠'lJUd%p4]Q-OFr3u- 3yݷ_~pͣi[-k^ +)w:+v[mXjv׬uM!]5k\tdb'od\3'S]xm&=I'/6l9"vU`s_`(,UޤK=a">7龋ux 7𽠾ˆ۲Rd#*(Ԍȝ~"Kb!Rލa'(e;vQ v6EڢkL6gi31 =vܩTvz#'l+))坩ƶ.Rs]=w/k.1Q{iP+jx!J&<ʖP"UcQG:=ec:#ВVĸ9\&&'EX]xRbi: p\~oW,xbzc8iqN-ƥ eÜiWb]6gL,ޭ}'E{…(t2ڴbuəl$%{% +Ye&$0-. ՙR(5=񇋖n~^reSՖ/ݿSbXʘ;JG~Rnn!*Wz +>ݲ-Wtk[y… 'x[F tUM7 տ-UH=3z\;/榠dz7_jY+Kv j[G-TjʓG=>LQOx4v0rf]~ Г^\O >- HR2i4/ iKH CuAks}`~=Ϋn麋Fi.Bk^J#D)'-l Mj,8>ޛURXR alϕd`GB_~J]zE'b8N\|mƎoyp2^/ l") hv|;D<ֿ!%Y9={.͝W_cLc6w*&rW}-l#1#zW /MRTGOsT!6'tSL;HJ P_I[>&I=1 3~qOnYtᕷ*/#i/6zf'.)saitG1Zapr:J[V2{._N]ӌNafY-nCR +!mYHyϸ:(g_i䤾~`O.+̳y=97۳Vʟ/qU-OvrxHyR9>A,adla's DfJT'.|/V 8 @b)oԵ-R귛T*w/ +/9;^P[|m7]pQxn_KerZf@I_Xцf]9>pQs3Qv=8Ɉt:-IbΓ\]J?SVaD*{Ba>L >x_Oޯ/Wzu|^Jv22p7gT$<;!sssewJI&eHO't1[$"$6K<7S0/yk=|)L/>u>d~/H9b/=Qb#;0yWs'ceZ?bz =%R?I|BX))[8? >Fy) Y嵲v:Cl8;!- !¸!Zvnn#=g^"$'žx`ҧk4[PB){ ,U;"0s9$Y8Mz Nc6>1HjGSHU^6m M&)Wb ax2;:ՆIN\OX-\zҩ'Oj` +CRVT?גPUtR&,r6M2]i +A4$¯&jޖM]~bv/|0`b/) >cr^Hㅲ<4SJTȍdZrJGN +{3q'xdAj?D4`Tb,)Rޱ@iΚKt8+BDD)C9bf2 aI]kϡd.eJzҹX:Uڤx@kIV"ȉX } g !F)2xcN^C' n5[Itkb0!rŠX?đ:% ] +ER!]y%|!ԓ-YLOj zҁ`= IJ3>!|IӤ*+)AȰ;{cz~UxGtќ$') =ŋ'7Hy⒘Izڞ k-6 +(|%lɌd21>ȦZa=Xⶓr$!A`|LQ}6(D2=XD1"9_74!뭹| 5'ܪhÁtM.1:.Pq|yr"zsY3q;C;ٹnʏ7j,'vﬓ%ࣰL$XN>{e6wIٝ/W`tz!S=I&&i2%gBL:j8'K>n;i1p(іJفs}8eht j)^%Dq{0$w#et⨘"Q;⃆̗-#pH!"f"VCԓM*;kKLOX =d'-lS&ڒ)oS0zMas|x94qK ag')V?zanQѓ"i&"m$$`*YWnGRV!?۸Փ5IG8FpH%IJڋ9{`6X>FrrȐƑG9L;HX⇾+p*==ԗl6!!)ocw7$ex?5M._HSf;%cdVN1U +-O` D×)oBctrEr0X~<!uŁTG*zKA2g@Tx4C&4}iX >^HS0yK<(%-[rG(KuVhK$bn@F3O<8Y0_nn$eXSJ&)VCIad?'})Fdz K8|`4 w9ֿ)0LεdV} II>箱|Ιl5DҎA ?s/,N˃['BDTR5R޶rsάZ%MsDڔ'2IONt +E@ !I iQVr; z +f )ÅI,y,S/]F xvyR,NI/ 6uLQRv3Jk߃vŔbcW^lӹeI',ccMRSOL+rߦDeO\vSJuaQvx&AbآOMJmMPW~ b{N%ct؟c:#SJ&|x˜ew&9'֧`3LT)N%]d?՜1ĶRlK:=RŊ(%'әbcՓ V?KI.5f-%ӖQQ]OdX =i;Ji?v8B1b&Jy< 8 6, IBRvJ~sn͚כQc[Idda8-o~#۹[Pp$F)w pg:b8F2Dy(S;;`d %ӓ n)绡'MPHE8B:-aҥ+_nY2Ҝ1)6f=w]M]C;ctIiJ_8qTKm\OR!NhŃ%W<  Y !)'snaMcadiľ(HmqP-Ƒ'LPiΒc&)C|Η!|vcDт}v ՗AfA0΅M KR"址xё*$x|z5E)k6o ;ٙIiy|NI,A_28xcIS`+$6ct )'DRv+wʗ}>iwl}1Q(8Q%e[mF|21:dCRb ._bz)S|qRRn@2RRzy)< eGd\$w ;&SI&D|.˟.Fg7 ER./: IV\]=LR SʕRO} b\p"3TavZxX:L7 1"-A<ir˟?jHK&͍ٖQe:Ob8n8X|G n3Q}(-)ezCbE+ =)ГvXv2N8C`;{ 4Q#$ec`׮VI?cp5׵TSI1@!$e[eH )4tKonʄћSTӇy{xحtyR T2XV.16hA|ԐvTEs$_׮;Dֻs:X ۣ5dh|N|I@"bZr\atѳÔ2WfђV:sS|:=lғaHP=dnM",J,I& Dy$ĻDȕZlw0=ٜ5ꪡg']\O +?L'EoݪҤR1/v@k%"JsMd Bz23ГV.cK"8F@\n@B䟝?fH+V/XBSjHɄ%og~*̛cG7$eH;/B+K$7Q:qGۺi@pcz4䱇2uSZ[F}[.O##~ +(с >~:w4ۭ4VHv!#ȕu~TB:WC0yY6ƍޯ- a),EQ}A\{u3X,cO3:qP&p.ECcDF[8׵fQUdWre3<!M׫빑 0­?(2s(rTvh"Pz31œ]t^lBg7yP:?V'ХڵrJ^94d2W}LB- 9ĄhšI9Qjя=iڱ?6A`4h4װ(P.W2d19,8fXy>J *,-P 6Fg$;z< =i3q +/v >Nȓ\FxGjTK+\cUE"&˖ӧbr˅b>l7$ _;D=i:/86 lM#ʖ5dk(ן +'X2 =tcHʘU)/t5Г,G3Q'/?#ȺuRJi*r +208 Zvcq;P{ܖhHNH|Q.P{$5ۡ'X|,wQd(U4|yU!RN grztGo!̾>FQQ6{Lmh/B$zLyԨʓ[dzkȣ<8ŌXR2 Wws"!HQa)xlR6zrI+0`-ovm*f Y6DVCASXrq"KmмBdķonZ!&!)chSc& fuMNtBKU%o4+#?$\`Xf;:S &)D%GmNgߝ  oHX,c#\yGPUxn9/MV 9$& N/,uUD,Otx_)4 _4®6B虐^O=2mo15~"bTEfu+RΚ$e䨪ɽDv$娜*%UN@%'%d~HUTٞý"26UIbҬ3.D5΅ё#7jQJHh-ݤX| !#|*rζ\C'WOf²1:plPrTx-9!{ARFaqk $a9/mYU֥' NNq%r;zw]crߐ#9ɢR>͉zmG5|\cjHϮ|UE'8#89X2aM,с7$e(sMt @' te$"WKe\Bgblq1%„eGNc?BRF-Uq*?V8ZȈ@U9Sn ڦؚ굲M<"*"Yr{.E1K)tHIʶ\ߑM LR҉$%1Ffg = ")UiTYΫ"m_L/q]TzNң}gAeԟȊdҤ9Yj )xd4 @4̓OR:rlD~㎕Ԭ#kG˅_tU?nt<JH@|9ߔCX]7=- Hd$2@iUK%pr42 +Nkaޜ wtӑG{*"JH ڨ=7]!~Q|E4ל2 NԒ){S] vkaX +w|@D)ྔ EXa/n_ww+ %7EsMOʽ"k\b|v\jH{k+m7]Ba)(b;1 rJhtH NH^~g"t@gtͺ"P`L,s(Z"/"2FrEʗ*{HBJ9r@1)I&r=-T:r%Yulir2rp"4]q c ,}Ƅe[&tEbj)WߘڱX$UtǘFUdWܚ%{7"H]CǗJui Z90%31:"J~71I)$PH6Al >6d~DsMwܖ#7ڵrٲēCzrj5K/,ѹcB2Uߖk©֖RP){ nVSL ”2g=کMp`W*2Ө,[FUS+1@aNˈIGtې5>CD5$$xp4@|77UGk֐*>?oY0wt3op*ct%%8)jϹ_J%⺺7 !#-#QU4㐉7q(8zM";z4љD[ph;@R%Q>tXFg7fW]۲"@:;zb=.M]O )H;Q SCҏS,$崄 z +]Œ,h3*4g*uʕM?|22zrr3nXAXF|yrct )3zKVRd@4$%PFtvOґ+5ilMR)D#c)&/Ӡ'h 2D1:}CR{OBy0M)SVݬbF'QadNԒ%{7H+TEZ'+WCL:!wtP/74vo$f蜄I񊐑=T٘]+/JgCCZ#&J>LOt*kpGGd̛H!HR ~D*1Ka;DXٕ'7TY "mk &Ē \%}d'I)TÞ?<VRHhGQ;#9B@@m"`9Pa6hcK!#mXb;G)B|(bup%JRDkR,:;+{ +c"P@mH:!'|dol44Q_pŹll$%fb|&LWBpCEB(BcNRe)(r4{R-??DoR +6XHb +7>psߐZ_[w7pot F.+$%3a)Z‡س?Ii&aϦz&JGN +RDtl9-ǗJui B W;zڞ'rbN(}ߦe[XT+>%)'>Ctv $zr5+1 ,(jpGOΝn!쀤,t)le^3VV7(V d2LWS +oɦb=2eGȹo!;=ֿf|D܂sڱ8$%G|['ћc Uk0]DCXf(MjKڑ&j +q$AE)icl.KR[93v:(09=N(a a5=A1:!)}|7]z`E)uMГD Ǟuwǘ)/,*TbCVB%"J)jxY<5t=)AM4ހ;'xKX&;zoHJX*,$ֹ&QUt &1Ise„ew<ݜ-CRžݤBxX\{u٧k9Dvy+%'a٨]=ct>ޖᏐ'y(|ƬJ Lc@zZ X;6ğ;zW>hHPzF"'EʻQwJ (*09&"8i^ 3΄esXct 5GIxo4/K]$CRFvhQmMA䱥u64-CjaYFbaܷ[*,蜾 '9ww&]uu*W7'zmU,b%ctIDep6Pk$AR[hY?)I2§{ L#,&J>TON(ѳ@8 IY,QHJRnw7d'2%$%QEטyWjIHM,l^Cz3%hN63sBt!ʦtڹD]]ؚz01ɔ9 sGRpG\L sߐZʮM3I9YR 5 ]9Ĉ5͉^ 1 +#[=JI痔Es˧+m<ٍ7ǛTT,[O9OLaIK*ZEB(os4%evoaD٦Emf +BO +N/k<8V;H;-box;cM9\R,{Cs*)rȈgbEL'b' @PKþ~"Kj)rD>U8JNi|ϭt)095ټôL 8AXW4pVBC竍Z?a9XROWr TyPpr+D z KX\GSQk&)^ҁҘ+9@LyԮ~`~~{G,;ƴ +RZ:@ʗ2i3'Rܢl'1.My(MTRQD6,,{ + J&>(ٝ<7 LΗ|"&m@7cᱯ;:s.k)~@Jg:s)iϲO7Ę5(q*WPq;zG5o[ĉvZk6>sJ*dǗIuiN6fKx3X-̄nhct )$>/Z%L#J=% AX@} +djx0yM,^C +Z51zlxMYjO2I0 nP5[/nsT$qXEzLXR}(f&@w,Y1:2Ιvx2̚Fj8ft尽֛j @"S&tE'$,O bNPF)F`b Kl%QEB !J,ibdz'"NٱsGSsq/)ꑬzy $o7)lvSS4[*y e9Ah>\49I7>`T<8u-9SE6=0"-G\@yp|D(sL~.g.]{~ig$ϓ$CSR_OWZՇ.K♜GRk7>`\M,~sKjEҖ}&A~^9* H_r,M`}Ҧ\$]7/y$'%f%͘4+INNv%%%''=Bқ-}J~wKi7'}i%Iuz3<4̙$# 'q CLUY9kR`D =H%Sp2 f*Dږtˑ5ctH*m&)}TUF&#WuJ^)6oHsA%s$9=$ ]$=&' IWvPq6WVًRų<LZ$g=0f-ct!)n>sA{d|4#?uE M>?eJ:팤{=$q&ӣPRh"r:6Jݜ1I #J@}\,{stE6 D*"ЅBX&# Fhd#S9t{]uys2i3$x3gq +[@*m,!!)k2)ɮty`=V^9z==$qׯ-s5DSS3Uo4k)@ЯL$e[pR׼ԤY3I1HdV@"9CPʑE]XUũ9{Se] pb2pGL19'!) B O-k\7Od&iIO!]"ldh( ȱ5*T:Oygna9O`*Wa!ч @ pGRle`IJQ#%&KR9#)ifpr1tU)%uAc! tzmSpr$@M,CQ\ +:XYy<(aTΚYUTC>]aamoL0l=WΗC,R:]uɹNb" XjkmT8 +x;З׌<^g +3-%'+bI Ocz7/z s" 8 +eCeܙOCBRX7'(47+AARR4)D QN\+"&˖>ωlݣv]|J|SC2b2PUx TFt:mg큱96&UEp)4BdC^LԐ.yӒ>Ywf&g']$OOp%PevGLQFoZ1{u+eTN)Co1ŘcrCZRÆ{¾[Lff]XWf˭UO>;.N3P$tlF$%Rؒtl}{sj֡`0*#"_4%s8÷no\4* x=]X_xY}Gn}⮚g~~mՎ]*?Yew?7>sf9]_U&%ERLӞvE/^:\!rDј[?a=ӍO2H&= .+]Tϫ꿯 jqŁ';?|^;ށOSߛ;g3'Bqƙ7o^_R&Tm54G&t тg,eiC4jR7<12Ÿ&٥ϯz1}8tez7roEү`8?㓯3=yG8HwJ3f`cJ*WRR|ECZ E?E)[\PWr%SV?jWK*wod?B/{?yS72XA>I(o<90*_Г#LΣ"&Ry?S/\\m38B7* 3)udC1F*eϳҋ5}j[⻯EO䦙n2E&&?@r 8P  D4B^] + 9{JOkQtp[{otS9QKѥ%Wmnc|a1}~I?.R(h|ӈ1qM T⨢qzr*zRmQdg>It`wt&KP9 @ %1-_ܒ7k.1/tm?}'cU[D4oDޠL4Qc,3A!''-&h'ڱvk06XkrDQ㊢?Z" mћsn}_+ן-?nR}Tx$| ]#|QՓwrct͗+c9MW\O LF:1Ec`7gN\&J|cw/;2I.xgЎw^);"Ȥch*bX# [rE]Lg* 7/r?i%&FpbQþ;/hHF3ȾxV<?/n"J?uOWToPR[ɾݝ+^EI1"Q8vE#X"DI|S +I ѵnrKIIt$A2}f.1~ϐ}?V?jkJybU-7Js:#Ì*'''&]Ib$& J}.;krSPE е굱.^6Hwwk~iH)i&)%}CT<@Yzy8?o>WK*1gRF뵜GOF|%R޾ )S5nT*tEpF䣇:_Fݦ}7͞[_ti]E_T?lxŁH1!Ҽ=^Z|+hӞ;=9m4ʢդ>ܚ2(e:/1n;Ⱦ{:=/z*y9ϛ;>o~}ɕ^<@+_ίܵz^|#|!n&ΗC"a-uFFhz2fzR@LzrB +uyZ]ZM//_!!8 ĈӢcJ'u֞EF/L.\L~r)?[+Tݤb}B/:F3;n 'CII̙VI!S9ҨHy &Qw׮mD!nehiӾ{ 1fCڇoOQ6wŕ{-凶3h +F}7׊12`r'Gד<8iԍw(ADڊUֿAG+qnҐ^=>bɾ٥U3^ʭܽr#\i=E'btГɤ$mtkd˧k)V_pa>|֛bKypm <z3"ơQVTzaߝ5duQ+Tqi*e|kk;/IQ}y(Q@OXV9.,v^+>p79gr 'XÌÊO/<%=빠>K>Ú'Ỹ4%beR/]R{^)fϗkݣ8x#%@Oɘ{Nx}$#J1 T"hrNՒDHކ+cwB݅mی_YʗWzurh>nz #;_.@O(.Zў= y$%'Ip)b:FKz۴烈\@jXK(+؝5o˫vdSˁ'i[[~J&$ڥH#OIΗ"FEU'O{zROZs$΃LΗM[LWOi}ƾ1nEsL^./*C1>gڮWO#Y4MӘ1qԎiU@OaL+9s+9)>(p~Iɚ21aV@#zJG~[Ӫ^̭hzWihӾh)f ldX2U&&aC9MW\.{rcLOa5["w‹#$_)ؽbxh;|ڐ}wn@O +=I[gi)R8[X9w:qp 3)ΚC 6./O)F`̮o{yJE0c"eGFaD#W'[K[wGZ)>r t9,s +Np6fǴ᫓L gOd41 ܌uȏbƚ':̗ݛ+Њd߽@Q2}T"ӍΛ?"t#D#H4' IxWNWK+p"V^9441i/BE3D#9xg(+[tQݖjq?ɾ{cc|{XRF|žc_0Ip=$'IlgK%(&hrOx50ׂ:_w0WafzsΣΗҫ6PiT[jq?Vx6(dC+ /ef Id=ެ%mŤX\Rzr_~ +Zrp4d_mY84%Ⱦ1?NuWm?}W4e&+woaruK!ďߡ"aݦMtYO*\O +fk=]Q|fdΤ>}s)(b |O+%{7|g~}Eu>zgyWF釩Kŷ_,{û@Q1q}7G^h $A5hPϮdtnr|4]qTfJimDڟ)ica\wƜeǸڇoyU/fS$TAn}hӥEq/! 'x}_o;Nǯch~ؕѓ⭩MvS 9D Ilyy5${I +N +2S9(!@FNMe9+{/?տ޵탽{˴e$Ax_ftܱXʙݐACHt'2S:rR ;/Yϫkzj~NmWT{ٽ=;{>:=hO_{oʾ{ %Dѓq4Ez3x%$Q+pW!OdRF{zz +:CH_sT%[dzwogb_W>O=>ܷ/~. .{=Qՠy +>β9K>]>]񺵆tͷn٨tv55mww]L[ncb?:^`û?{}q{=ɐ,8~ %vwc9on{nƔ$:%6rH=(^ +tas۶mzJ*xo^O$/?9"mvfƙ,L|xj$w,t0y(`<Sl*=i7mR5Q[6+|p]t|o5|=*L^nLv]K=LJkKדX%% 1 H4-WDGgYuY>tCA% JI9s`CA%I pi&#'/1~ZzEwbWm| }iûۻ?}KCaf%#-Qu @"zm8A]2޼T 5bvS_Í3J|i|_.VPΠvCGk/nP*|P8Ӗo*BG+U$')_rGM_OC:FWH`/6jsfsa_>| ?_5 {^`J|^h@XRd?ط/$ՓB:2t͚ۖ%;WqM]QA2 +#@'EbFFt pAVnjAzxꞭb0Qy@OO"m}駟~}/*s5_rǍW7XM>A|˟^d ~E:{k/=c5ײԳ{ks&8,/owk,{gN6w } g+] _EU3Qkmg4]i}Cθ_5 =sGv=oW{ {ÃlRݢ8ό KOU8g<(>>'[w[Հ @8 PB +%@Bc܀GI@dl5wclq{l|3wSI73=!}m#:eN}129!1wY\tW]=>k/Lzfx{KGl!\GL 7sǥkbny~D˨OoDr7D-V+_~gRD 0-$nXsH= ,zY,7.6)'^3'ŬƊynn, XX[5IY.+G:՛usG?᧿O?r޿Lsy\W!_̇\VUz9顱&%DBVaUOQ𧼇Uxѕ% +cT FJbSi }E3s F+?(\4T2i!nxKXXm$ E|rMƭxt<0μvʼnOg̷x*Z0}1#5v+1g2hNfeх;<5s%;zQ'qkJ.i՞ +#Uրtuy9{gQΛrLo ]Q틼id4_`^{tB¸ λ庫ͯS{Rud?Y4].2ﱞZǚcxN&S1K2g&W0>9,)Fv^?=ܿrVΛY7p}ˀnqwyḎo%tyok~uO=O:&s0.xНtֱMJ9oP'i`pPaƚ[;XQxLx yʋ$kzYFjNK򘿗.La\>>ǒ?ǦnA⹛0D +,i9n?&X^Ͷl1q~fu$Z{0EcOw]%?uN 6]!//^QHKsSk]`.a3%fn:?C QwC-6,7#pU: Q^ŜܬH} ʊ8+gSObJ!]]Iޤ<  ABJzk8vL7pF˛wv/Ʈ^ȇC̴Df b{Ug`ʒ{|l[*'G|=C{@XZ`7m^N27r[J _{>0s~@-DZeq2fsֳ)9qZ9oEsIDu QŠT϶`Ğ=P߲?D}Ƈ[F̌G(Y6 +V')'sj'ɶx7툅ҕϋrMޤ) 'fߌHL^ov^]#•yEtnƲ}3V/n1O-4^n {f+I$hMVzrqf& dʼnl+ݾܖHoo<9us>j QG%*+cÒܕ~LqO8 +4u}oOٞJOb5fC$ēݏ >aމ"hC!wV,q?y6Z%!,[/+c:Z+|n*S* w%Hp7,7D|mHycճB}Ia3śXG;hH@풧r_*(߲e"v*&>=AKŞO=nj/1Y?j4 +azy SXa_{ɉ܍ ww=3'K@2EuEiAb)*k Hok^-.n.u,l/'ֱ-RqVs]9XQx+MЇ sȸޡLJ粃P^.U›w2jH +QeUGO[ +޾4'Y*2JgNA림w2\(^]}Ըm>9,,@uXM OfGfOod%8 -+--%%K.?3YX]^M rҍN˝ &Nn&B;/LG&| ?J}"E_M=sO_7hF̠=๙U҃>^D}ˠggeCԱZi},na} g_校ۼ^nWAX /wOjմkDCVoQ 5@sΖ8+s#yad}ț 2-~䧕*W͟(g;Nz#^RT d=[|Sq!X/VfUnҗU(~YKx'#[&H~̺7#sR|5$Hd uByXg,\?j2&wtI RMqWQBk`[jv/F|z;x{+!5E9/NYܱ %Oz5pm,_p/U*Y&YUJJ?a> lTr$ybV]*{~_xU99y'77z97f ,E'.7,9D}Ni6ݬ[ď>ډ['$TLUz14Mc&ri\Dt1zVi`:jt,gl謺tOY8gEt7\tҸ<Ւ r*lOࠓ"kb$܉۽RvLNvIFM]җ|<|L/+'kZ]'_Նgq]F|Km2z$kfGD.egQBY͍ qLDm}vsLJ5[Cniy6YIȝx8c'B*'GTT K#6|˕f0O{✏n,w t Zpvit]6 $"JmOIZsEԑEa$#Dq~Mo^orIFeZ/WI+#|Lv5V!:&U]'ݰ M7MY+Q^lR ʝ^䳻Jyk"dn()ĿLsQ v+r33K}~\ H_Ƭu^u wz$'ugƃǎg&.ǙMX%04/cq&zpAlZ{ߖY9Fz&8&ś{ }orPǛwrN`@zv*kC2Wf.?%m>nLaM7X̙ddQ޻ΤZxalMIM+kFq)Q _x|G7yY452(0kQ_&ŃC? {\?͗.ZdZ8{ +o,g}Z>"ҟv #+Jo|uk#4ڝc^X1Kŀ(B!̽tLt IkżQ8[EDy!eN-f3rd^{Rb mU/3xs y{jvՀH,X)[t!Aig/p#+į'ę?{^W{ +#1 }Už̲ _cq? m+2}/I 7&}d!}y(Oeb7~kJ +#XZJ_ZYȀeXQ5 t&={19]ܖFwrN&=p=p#Lw2Lsˑz.ʫ_|A_1XYV&?kՂYe܏}iU{wwĎTQ )If#@#oY?E4@cNVR_ƀXuUޫ-|K%./fk|>kt-akÒM3o^^D>nKM:0%i]y{7nkJ?1qNmbe%inpՀt<*V%keynqY~˘X8-]s\Q8{ka_*wK[K/qw璼ikf+qpp˄EI$V-}BL/~ /ɗkΫr볬 ]g!5]49+`lן q ځ,a_3/NJE\1/gXViN3W_[ Bo,7@R{@ziwj HgN=!b+VC("w]xy03s_Y%yl3GXNZ/8' p 2]9?y}EEEMqW\kOefd +(}/L.^_7@[QY+-e2n)j )u+!-y<32޺![6Ӫ,a>g>//xEnKSQi20/1d<p2P +pH=e".e{ )vn,9ŀM^WvR.f_ծod88]ynj,?ԗYV7pFQrrZrQbU k5S@ACfBLb[ɞGJkQZ#8>u_J >ܨ@f.m16] aw +?aQP2=`ܸ঵+ +NaSǩ_OGb<݋r¡tᢞ~qϮtf[t(03%`SVxQ ߯ Mm +n_(0=GjyOfeQ}ZƮ5[U'6d,1[8߽~SZGbߏ{rf@pz/7y U6@USM=, fW 1{^)?j1#On޸ܜM\Ra ]KlV ~!_\6=s8]H;_/jC9L=0cPIc6bVw.Nm:r٤Y+ubӯyȀ9\VG/kM*}3ImYUEvd͝v]qCx탯ʹjM|(Ο&SԶ5\ubBo߯NC{a֣F6yVAXW_&Iwp +a=`.o:{k-;S[LփE?N ؘqMs*g1rHVvD0P +Iد%Oŝjn1EWƮۍhex.Tql/#SدWM1~*[>ycrS"hm2w7Bc-vwpՉM +ɗ9{{jn1,w-,u,'!$3>vS}-ši<$uW6,VB_c7n1Z7_kĦoN5=ӬͩlTZaB~S*#MRnha?οuk5o}$EdljA#/[7 [ޯ7EԁM[ZGOBØnĭװh0ɹk5#P{մo/ +~"DC}M=>:ȓm-}goM2L~~׿WhVYCJ!vq[+݈4{7<$?rf^:5c=o,!.{kĢ34+Wgwq)n5$?WW תP>n9[YXtqoXV0‘~cn[!2bFjeL*rpoy 0l_{#C*Hfo+#7Qؕ4CH]^U\Lf7C+2IP~{N7 k|ߗ~΃|&6ݠSCFPn?W t0cU1gs H_^H=db)5 +`.NZ:!Ups60YMt׷&5U٧򙜯+ɰBmZ.j{6^hC!B^o3w[4dc P؃r.0q:;C:vcvqh.{u&M^}h đ.q<1g(KxHcpdGT3>Fe}2 Eп0G=Ƽ2{XMgŢ {¥l`cv+֋DпXt(- ^J}^BO N0kA,-6D )8𼇲P6Ʀޠ!]_U]nn [=0mZW8l]ƽ/0:Z+?AZuث? <4CP:T#< c{e:_U(ׇcXH88w?# +GۯA{H|%uD <DϾ?C+56}TkxūeY ]T>HK~rWkbApѳOv{mX7MҘIUcٿ}d"ݵhf'{?٪rQItC4v2b[- !mycNGLb(r/7$k 'R^іaONS~wY7*>_&Jߘ&lw;hfTJ{&1On"Hq2n7~:v?E=ĘBu(T[CI/^^xj'¢)!!^TnD5x:JuܯUWvZbHL=jĦSp잣耸4H]yHWMJ&Ʊ4b&3oS[wǦk:({C=$nr$ݏkovggXZd=||mM%7ZwhKۨh{>ু/jԃj_j |(!8/1?d h3iO=ۥ4%7 o7 [8zaCCDI-ހv'7Bo@3nKLF:/f A}/K 7 B0 r.:s6 6)3߳z1ն6= .G٤)KQh h{D$p) W[[{. n$ǢtLpT!L[ϳZrJ]ý}(Sf;S@_Aƹy( +rGqMYxnkVm\l1Y=JթJ?}߳Ak d++on3L]o[سh*irg.  D33VOI*;C&B$J{vE^Ξchao~Bv Ozyt9< ٽƁ?_OMl2W܅Yb2F"wۨcí+a@b}?ks6gjQo :,zӛ.a`D%m78r>,zaҦr#㻑цEo;뭣Ɓ¢o̔o^[ +J!4|d\ǿDnD LnGJn~%ǁ@Ta3JX|)`\rZSI7@nĭt>J1tF4i.}!7y/%" ĢF,Px%RɭTr#6@q@7,F*G(#܈[|JCJ*7"&{>b/Lf:VnS9p*8t~?FsT ph^nub-q$#P-i.%o6=|Ï?;vq@ezs-j &@e*3*?W SI_;:>I?NaဢX4Mo a5Ŀb"a/1v9ŦY Zh (Cw/e5_nx%P~{tz3%>X箄րX6܃M~\Klh Ebƕ9^-mR`g"? 1)caJԀF'۩wTo˂Ħ"Pvs/_Xawma՛Aztj, $MpB nȶ 59?3 }#hvUobi89Lo8\qw+W }azqI>CߎAo\qe:i+AT@[@7$~Fq0I|xM Ǧ{K7wϫ:b#(q_HaSYoxzK(@3j#1cc8 +h{S#-:W>ǡoJhi N>tzsC'1?ytVo=Q{<<@bSYohM.W&"~Ƴ J66=pz+.iq{dW{I>HaOxFW7P(E恞*P*(%Ej4fxBەőn4~8H!126\JwZrqڀzb*MMn4ǦX&ܶu*^J æz(M2uqx(MJM}%@Ml/Tn"};EC:Il޸=(b]0oRy;0 E׍RBn<(91(MR[{7S%01bHyj-c%-y^urb.kW XtyˍY m:6#ܘھ7$Pf/* " 4n)@},ˍKw"RTǦ^+D2({ܛ7&!84M:`ވܕ@p@a, eZL mAp@YlzMJz/Ԅm^7ټXZrt9 +m@p@IZyj֍twp$P y#\p(E7&yџ*j8P ?)0~LbBp@+ 2!@Z6DaoR`3!8!JXIqg iqSԷo~cOG}>Z n.@nl:zcA+,ƢQ;V$@f,\EP/}@&FEZ&2P_p9q7?Jo$}sz鍽킅Rbӕ4I1ɥ.꧛ވ9hv{mo I%}M;&NkV ~nH?ita.M' 8 Ro|O;ȅC] 8 6XxaOF7~w@,fv Lp ! -/]E  JZ;I ȃMؼsz{ HC{cb2~wz$T_H1 3Q^$¦i7 HECR!7 6-3t՛I84[jCE2{_Uoy d]fSwü`ꍽN`zʍeNp}8¦p=1I 6>HOfa_#: ¦7y 6-誥 ·3dX-rA7 6y7 6n Mrfx-62vl7 \N 5V!MA HM7'i3H p'tt@ fހtXtfW$7 }ӣLr0o@>*; $W1i#8\s zdSA-ʻ۵Go&9K$eǘ81q7 -6=rn& =ia endstream endobj 31 0 obj <>stream +:yǑ bm:wlGyqhq.]ڷBo2jڥ& wHCӵ[ؘ "&0rΗIDL"-c 0H ۣ}{wڱCG&/@b~}wԡ)8I 1LoԿw.c+!w!d= اgfbTכAfc  ԯgNb)7 56zAwazS$ ȍMf<%оcM2xZG  \G^iѴ\3q'tCYm ¼ٱ釣F Ч3p1 + 2I!$ش#&ewpO-cӣōE(S#c֑t)1(M_:Mg8HH6瞒8t@޽DD yȥG mLo{S{2Ґjz)#Л%&&I # Eo?u!Lo;+7܀ % v?84clRܐ@R< )NC?<7ݣkXLԖIi9Էww=wH'}6zSF r+(6pI )Lo'5&~`7QA)=v+IyvRFߗ7^%Lr0o@VN|to짆wM_Orh ?ӛ !@ ѿO̾:$c2ճs8I6 hE?WN cF8\opCSz(L{.q@,?Л7/a߳8TB1wr.7'zVo܅=zt$ԛAb8t|nBԖpP"-:0ɿpH?vۮ) o o@#l:Ror'o@'l몮۷pf#cN;ws!y:a;0gޘ}{7]Po& +-AV]th/\bO:.b79}p'^8b< w{+ I KLrN"-Z?[TsCH7$whM_^Ɠ0o@32&UtæΏPor:qvtP 'O IJƃqL2d ~Oo܁-1+4plSF7CeLU Ja$SlAE ӐMo184h {No̻}c0]+܈!Lo7H7<Zc OV]tĦvh . b7%6Џȵ3I0BbVo&qeIzBo :qlp@ +C1x ZaІ3ygmXJi[Nٚ]eA; fmh+1w2 D56m.ReX6 wD7][x 辋 +C~N6mu0o[[p2vI8M L}%qhvN4ep'p]'8ij3HNajx +MLo`SgvEHE+ok!yr%N:6{-1[Ap&ycM_*i0oĢZN tC+:z,Z7iC2:pl?܂{9 +Iq:s7#o +Ya{^݂I#̂%Fj.æUp?<sW8K)c砞 zΟOpGDLib&& ;8Ǣ-IE`5ceLr`g2 tNX8{E8tnX8$8LpSq\JN/ }ax)e疗R%YRzHb@#`:2 "4ҔVvy'`h =~ogah=qG!^dh=pYp&9k8Ew\ҢA7EiL㈘H,:3H Xlv(i1XA=Xl%Y7_Ji_xyzl<$38OJ) "Rp4Zw7Wprax4A:-i4yH$,ǤY& q_b@Gfv1#B @`)9& p(hL2kc8t_ @qh|sSM< MƱ(-II1(Z0}QI-:}J7q_߽I&.܆ f&9& ̈́)qMT84JJ?}&d7 ͅ;ye-@2͆'ynT#J< %@ʏ~סQo +Wp^cL`hLp6qem_4hLu)JZT}!`'WAh9LF{o 7!cw8n-JxB {>!7&{'=_ 'Jo?$Ep#?$g킁 \0wC10pJ$ vDL6=p*L+8ƒE8!:4o7M\lApNk8tσC @顳:C~ao6xݧ7X)ܹK$YK3{9s9νz7 2O!@|JOpp;Q tEH98'ɷz@DrpOP{ 8'l P37Y{C |'_ҽs9&PVS㭹QJŗh@@x N0Ǜ$ߤ#@x e4 %'Wȧ.ڽ+-wr08P""wzxdXW!ހ +}MbFF|7vn%P<ܨ]98sp0߆Ș܍1y2ǧ$ಾES\_7ްzw7$#龻7@;c7 =w@_p3N dpGS}N$t+P{ Ԇ~Ñ/Tpaf4 g;wQ=yYP.X!c`@y鳨=tdgn]9ҙ~ΘAAn#9vp޼37S](JR _~>#P,tg%'i{K="8/SNͥdU Fp_{%̏yȏ lⴓC.(YPQ,|9w`b@m16t=9J|ppΐ_"I93i)?2Iڷ/b9@ëXto ciHGpq d+:.5 l PwU|y;r"Vsz1@gK57YGpv=ɡ%pnl4L|q$J|L'Jг78${g&z '!~x7N~f=]bsLO4(үΔ›Q)\ddo0?rf + ct,+@pf$yʀ/_9bGf|X +_l}8rr'_j-AUoif~tn=R],%_q"p)v՛nYN+y`#3L)CMئB&I;#”  r) 3 ZGސ44 GHANOprFYɠtp%m>hʄ”~'0DoN927KCMR `bt_V\D鷱=d+lҲ{6,Øu9/|/Ks<o1btft2 ȣɜPv}Ҳ{n]JIs奙 #T S})f{c#g_ B6LD鑙ES'@Q)[p]j-S_6C #C^65d89\*s յqnnUr@ɬ;[- :c0ӚcuDpΠn w=sp_`+ du{ 01rlB7VڟBC:dX +?gOBP涋mL=C) +~КVQGKf ytETmTdE8Y?^MՓ$8qJ\02&VQG_<26OGݴoX &9&Lz<9p(dl^K/r3"]0^2܋|% ?~ЛN-"ŖΟc#Ǣ#KS~69>QtG` oca+C o޾5olah/pnix %*O A^+cenGziYJS/ܨ _;8w}=M$>+gWTQ,7ҀHn d]޷ye< Rnp7S +G&ǀx47L:0{[ixgyrlKs4?yx9r4.|)6;08y it ei7 ba(s;SOms{sCE{]-.irms8ITx#L>7/ lHR졥±[y77`p+y3뎟릜%]!⽽^kahfտ>FYNp`koqɽ}'p(O\xMM2H;8fף/<XBB>L~ (fǬ=(x dl /"+D- s(Jf_3IbLl[i;8d馽P CdZ<}l7q}DX1R[i=pZeE\>5g&H0ZNB*AfcJ /#A S +WE>!pr@͍˿r׮1H^ҋ/P6dd2T\ 6#ܥ*#FƏI]ç/঍?/0iml3R +Mڐr#rM7AԱc}m߸᧫V2(&C@S +_Zv׿Xnt8<]>xG~~݆9_u!ԍd~Z@.]Zaz1M †8/xs3ݵ 1qK]17|s|@X +G#_o/-m5YQgM ңKf}]\{Ւ>X" Lƈ0y0psDB9z|(1/ bJS_JCqZ3V6 /+ՆgF/-)6*t5fMsRs"Kc(N4S_>U$P&)+mE GceQk\~']6Tgmm6Լ^Wc5g*cbE Ćx:)>n%ӶZפmЬ7g袲YzGsW`.#-ɘ7+Fkmj}|֛#W?k¦FCAQ]rCF˺ڼx}A`lN6}[Mb[fLQe5E&;Őܬ.]ї9یMٵm%re|v`-Ԣj-2R樾 +C2l27kʲB]_zuEkFV\L6ڠQw>j0#ݽ˴5EZr%}n\bj{RuD93rx G^r0&ϤꯏfΡ@ݗѮh rfENMݘST4 +mIT:VQ +}Vowö^5ݍ{cs"fjZL}wQc02c;ӝ>Rl[)qW6z]P"~`s+[EbZBDQnSMC(ΑXߝ:؝[d2csbm֖ ߥ,mj0QoӒX3@ggT*+ kkʛu9jg8^ng2R' )UUҟѕnHoo.p4$GMΠFh2s\^m1e^#<tNZ`TimYPG>t ~TP:-Jԓ+\e6ZU*SSuԷv)6Z,EI}v^ұ/МߝC40w#d*]Ѣn+ ڢWeO16UeFVk]Uݥ+Փf!r؃&ܙs͚5GPF[SRBY.ĤkY-Ime)U&92ͨn.6W-3[Zcu ZmkIF[\e'yP'+RB兆h&+ٸ^Zҥ5e)v}JSTȭv9QNhlNXƩ]ָ3Ra6%SJ][i[M9efBJwŕJ +"'9h,dRLQZJӔjcIAǪ|٘֘ڣcIi)-2IUV:*q[< = +p*EwmlعU{O쫀mش*0L1YU_{̻˳/T Vɭ +xTs4> +LL*\q3Q5 J,(I endstream endobj 15 0 obj [/ICCBased 19 0 R] endobj 6 0 obj [5 0 R] endobj 32 0 obj <> endobj xref 0 33 0000000000 65535 f +0000000016 00000 n +0000000144 00000 n +0000047649 00000 n +0000000000 00000 f +0000163121 00000 n +0000593503 00000 n +0000047700 00000 n +0000048109 00000 n +0000048283 00000 n +0000163420 00000 n +0000139682 00000 n +0000163307 00000 n +0000049181 00000 n +0000048344 00000 n +0000593468 00000 n +0000048620 00000 n +0000048668 00000 n +0000139717 00000 n +0000160473 00000 n +0000163191 00000 n +0000163222 00000 n +0000163494 00000 n +0000163800 00000 n +0000165099 00000 n +0000187851 00000 n +0000253439 00000 n +0000319027 00000 n +0000384615 00000 n +0000450203 00000 n +0000515791 00000 n +0000581379 00000 n +0000593526 00000 n +trailer <]>> startxref 593722 %%EOF \ No newline at end of file diff --git a/ui/design/chromeStorePics/promo1400560.png b/ui/design/chromeStorePics/promo1400560.png new file mode 100644 index 000000000..d3637ecc8 Binary files /dev/null and b/ui/design/chromeStorePics/promo1400560.png differ diff --git a/ui/design/chromeStorePics/promo440280.png b/ui/design/chromeStorePics/promo440280.png new file mode 100644 index 000000000..c1f92b1c0 Binary files /dev/null and b/ui/design/chromeStorePics/promo440280.png differ diff --git a/ui/design/chromeStorePics/promo920680.png b/ui/design/chromeStorePics/promo920680.png new file mode 100644 index 000000000..726bd810a Binary files /dev/null and b/ui/design/chromeStorePics/promo920680.png differ diff --git a/ui/design/chromeStorePics/screen_dao_accounts.png b/ui/design/chromeStorePics/screen_dao_accounts.png new file mode 100644 index 000000000..1a2e8052c Binary files /dev/null and b/ui/design/chromeStorePics/screen_dao_accounts.png differ diff --git a/ui/design/chromeStorePics/screen_dao_locked.png b/ui/design/chromeStorePics/screen_dao_locked.png new file mode 100644 index 000000000..6592c17e4 Binary files /dev/null and b/ui/design/chromeStorePics/screen_dao_locked.png differ diff --git a/ui/design/chromeStorePics/screen_dao_notification.png b/ui/design/chromeStorePics/screen_dao_notification.png new file mode 100644 index 000000000..baeb2ec39 Binary files /dev/null and b/ui/design/chromeStorePics/screen_dao_notification.png differ diff --git a/ui/design/chromeStorePics/screen_wei_account.png b/ui/design/chromeStorePics/screen_wei_account.png new file mode 100644 index 000000000..23301e4bf Binary files /dev/null and b/ui/design/chromeStorePics/screen_wei_account.png differ diff --git a/ui/design/chromeStorePics/screen_wei_notification.png b/ui/design/chromeStorePics/screen_wei_notification.png new file mode 100644 index 000000000..7a763e5df Binary files /dev/null and b/ui/design/chromeStorePics/screen_wei_notification.png differ diff --git a/ui/design/metamask-logo-eyes.png b/ui/design/metamask-logo-eyes.png new file mode 100644 index 000000000..c29331b28 Binary files /dev/null and b/ui/design/metamask-logo-eyes.png differ diff --git a/ui/design/wireframes/1st_time_use.png b/ui/design/wireframes/1st_time_use.png new file mode 100644 index 000000000..c18ced5e2 Binary files /dev/null and b/ui/design/wireframes/1st_time_use.png differ diff --git a/ui/design/wireframes/metamask_wfs_jan_13.pdf b/ui/design/wireframes/metamask_wfs_jan_13.pdf new file mode 100644 index 000000000..c77c9274a Binary files /dev/null and b/ui/design/wireframes/metamask_wfs_jan_13.pdf differ diff --git a/ui/design/wireframes/metamask_wfs_jan_13.png b/ui/design/wireframes/metamask_wfs_jan_13.png new file mode 100644 index 000000000..d71d7bdb4 Binary files /dev/null and b/ui/design/wireframes/metamask_wfs_jan_13.png differ diff --git a/ui/design/wireframes/metamask_wfs_jan_18.pdf b/ui/design/wireframes/metamask_wfs_jan_18.pdf new file mode 100644 index 000000000..592ba8532 Binary files /dev/null and b/ui/design/wireframes/metamask_wfs_jan_18.pdf differ diff --git a/ui/example.js b/ui/example.js new file mode 100644 index 000000000..4627c0e9c --- /dev/null +++ b/ui/example.js @@ -0,0 +1,123 @@ +const injectCss = require('inject-css') +const MetaMaskUi = require('./index.js') +const MetaMaskUiCss = require('./css.js') +const EventEmitter = require('events').EventEmitter + +// account management + +var identities = { + '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111': { + name: 'Walrus', + img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', + address: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', + balance: 220, + txCount: 4, + }, + '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222': { + name: 'Tardus', + img: 'QmQYaRdrf2EhRhJWaHnts8Meu1mZiXrNib5W1P6cYmXWRL', + address: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222', + balance: 10.005, + txCount: 16, + }, + '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333': { + name: 'Gambler', + img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', + address: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333', + balance: 0.000001, + txCount: 1, + }, +} + +var unapprovedTxs = {} +addUnconfTx({ + from: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222', + to: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', + value: '0x123', +}) +addUnconfTx({ + from: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', + to: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333', + value: '0x0000', + data: '0x000462427bcc9133bb46e88bcbe39cd7ef0e7000', +}) + +function addUnconfTx (txParams) { + var time = (new Date()).getTime() + var id = createRandomId() + unapprovedTxs[id] = { + id: id, + txParams: txParams, + time: time, + } +} + +var isUnlocked = false +var selectedAccount = null + +function getState () { + return { + isUnlocked: isUnlocked, + identities: isUnlocked ? identities : {}, + unapprovedTxs: isUnlocked ? unapprovedTxs : {}, + selectedAccount: selectedAccount, + } +} + +var accountManager = new EventEmitter() + +accountManager.getState = function (cb) { + cb(null, getState()) +} + +accountManager.setLocked = function () { + isUnlocked = false + this._didUpdate() +} + +accountManager.submitPassword = function (password, cb) { + if (password === 'test') { + isUnlocked = true + cb(null, getState()) + this._didUpdate() + } else { + cb(new Error('Bad password -- try "test"')) + } +} + +accountManager.setSelectedAccount = function (address, cb) { + selectedAccount = address + cb(null, getState()) + this._didUpdate() +} + +accountManager.signTransaction = function (txParams, cb) { + alert('signing tx....') +} + +accountManager._didUpdate = function () { + this.emit('update', getState()) +} + +// start app + +var container = document.getElementById('app-content') + +var css = MetaMaskUiCss() +injectCss(css) + +MetaMaskUi({ + container: container, + accountManager: accountManager, +}) + +// util + +function createRandomId () { + // 13 time digits + var datePart = new Date().getTime() * Math.pow(10, 3) + // 3 random digits + var extraPart = Math.floor(Math.random() * Math.pow(10, 3)) + // 16 digits + return datePart + extraPart +} diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 000000000..9dfaefbb3 --- /dev/null +++ b/ui/index.html @@ -0,0 +1,20 @@ + + + + + MetaMask + + + + +

+ + + + +
+ +
+ + + diff --git a/ui/index.js b/ui/index.js new file mode 100644 index 000000000..a729138d3 --- /dev/null +++ b/ui/index.js @@ -0,0 +1,58 @@ +const render = require('react-dom').render +const h = require('react-hyperscript') +const Root = require('./app/root') +const actions = require('./app/actions') +const configureStore = require('./app/store') +const txHelper = require('./lib/tx-helper') +global.log = require('loglevel') + +module.exports = launchMetamaskUi + + +log.setLevel(global.METAMASK_DEBUG ? 'debug' : 'warn') + +function launchMetamaskUi (opts, cb) { + var accountManager = opts.accountManager + actions._setBackgroundConnection(accountManager) + // check if we are unlocked first + accountManager.getState(function (err, metamaskState) { + if (err) return cb(err) + const store = startApp(metamaskState, accountManager, opts) + cb(null, store) + }) +} + +function startApp (metamaskState, accountManager, opts) { + // parse opts + const store = configureStore({ + + // metamaskState represents the cross-tab state + metamask: metamaskState, + + // appState represents the current tab's popup state + appState: {}, + + // Which blockchain we are using: + networkVersion: opts.networkVersion, + }) + + // if unconfirmed txs, start on txConf page + const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.network) + if (unapprovedTxsAll.length > 0) { + store.dispatch(actions.showConfTxPage()) + } + + accountManager.on('update', function (metamaskState) { + store.dispatch(actions.updateMetamaskState(metamaskState)) + }) + + // start app + render( + h(Root, { + // inject initial state + store: store, + } + ), opts.container) + + return store +} diff --git a/ui/lib/account-link.js b/ui/lib/account-link.js new file mode 100644 index 000000000..d061d0ad1 --- /dev/null +++ b/ui/lib/account-link.js @@ -0,0 +1,26 @@ +module.exports = function (address, network) { + const net = parseInt(network) + let link + switch (net) { + case 1: // main net + link = `http://etherscan.io/address/${address}` + break + case 2: // morden test net + link = `http://morden.etherscan.io/address/${address}` + break + case 3: // ropsten test net + link = `http://ropsten.etherscan.io/address/${address}` + break + case 4: // rinkeby test net + link = `http://rinkeby.etherscan.io/address/${address}` + break + case 42: // kovan test net + link = `http://kovan.etherscan.io/address/${address}` + break + default: + link = '' + break + } + + return link +} diff --git a/ui/lib/contract-namer.js b/ui/lib/contract-namer.js new file mode 100644 index 000000000..f05e770cc --- /dev/null +++ b/ui/lib/contract-namer.js @@ -0,0 +1,33 @@ +/* CONTRACT NAMER + * + * Takes an address, + * Returns a nicname if we have one stored, + * otherwise returns null. + */ + +const contractMap = require('eth-contract-metadata') +const ethUtil = require('ethereumjs-util') + +module.exports = function (addr, identities = {}) { + const checksummed = ethUtil.toChecksumAddress(addr) + if (contractMap[checksummed] && contractMap[checksummed].name) { + return contractMap[checksummed].name + } + + const address = addr.toLowerCase() + const ids = hashFromIdentities(identities) + return addrFromHash(address, ids) +} + +function hashFromIdentities (identities) { + const result = {} + for (const key in identities) { + result[key] = identities[key].name + } + return result +} + +function addrFromHash (addr, hash) { + const address = addr.toLowerCase() + return hash[address] || null +} diff --git a/ui/lib/etherscan-prefix-for-network.js b/ui/lib/etherscan-prefix-for-network.js new file mode 100644 index 000000000..2c1904f1c --- /dev/null +++ b/ui/lib/etherscan-prefix-for-network.js @@ -0,0 +1,21 @@ +module.exports = function (network) { + const net = parseInt(network) + let prefix + switch (net) { + case 1: // main net + prefix = '' + break + case 3: // ropsten test net + prefix = 'ropsten.' + break + case 4: // rinkeby test net + prefix = 'rinkeby.' + break + case 42: // kovan test net + prefix = 'kovan.' + break + default: + prefix = '' + } + return prefix +} diff --git a/ui/lib/explorer-link.js b/ui/lib/explorer-link.js new file mode 100644 index 000000000..3b82ecd5f --- /dev/null +++ b/ui/lib/explorer-link.js @@ -0,0 +1,6 @@ +const prefixForNetwork = require('./etherscan-prefix-for-network') + +module.exports = function (hash, network) { + const prefix = prefixForNetwork(network) + return `http://${prefix}etherscan.io/tx/${hash}` +} diff --git a/ui/lib/icon-factory.js b/ui/lib/icon-factory.js new file mode 100644 index 000000000..27a74de66 --- /dev/null +++ b/ui/lib/icon-factory.js @@ -0,0 +1,65 @@ +var iconFactory +const isValidAddress = require('ethereumjs-util').isValidAddress +const toChecksumAddress = require('ethereumjs-util').toChecksumAddress +const contractMap = require('eth-contract-metadata') + +module.exports = function (jazzicon) { + if (!iconFactory) { + iconFactory = new IconFactory(jazzicon) + } + return iconFactory +} + +function IconFactory (jazzicon) { + this.jazzicon = jazzicon + this.cache = {} +} + +IconFactory.prototype.iconForAddress = function (address, diameter) { + const addr = toChecksumAddress(address) + if (iconExistsFor(addr)) { + return imageElFor(addr) + } + + return this.generateIdenticonSvg(address, diameter) +} + +// returns svg dom element +IconFactory.prototype.generateIdenticonSvg = function (address, diameter) { + var cacheId = `${address}:${diameter}` + // check cache, lazily generate and populate cache + var identicon = this.cache[cacheId] || (this.cache[cacheId] = this.generateNewIdenticon(address, diameter)) + // create a clean copy so you can modify it + var cleanCopy = identicon.cloneNode(true) + return cleanCopy +} + +// creates a new identicon +IconFactory.prototype.generateNewIdenticon = function (address, diameter) { + var numericRepresentation = jsNumberForAddress(address) + var identicon = this.jazzicon(diameter, numericRepresentation) + return identicon +} + +// util + +function iconExistsFor (address) { + return contractMap[address] && isValidAddress(address) && contractMap[address].logo +} + +function imageElFor (address) { + const contract = contractMap[address] + const fileName = contract.logo + const path = `images/contract/${fileName}` + const img = document.createElement('img') + img.src = path + img.style.width = '75%' + return img +} + +function jsNumberForAddress (address) { + var addr = address.slice(2, 10) + var seed = parseInt(addr, 16) + return seed +} + diff --git a/ui/lib/lost-accounts-notice.js b/ui/lib/lost-accounts-notice.js new file mode 100644 index 000000000..948b13db6 --- /dev/null +++ b/ui/lib/lost-accounts-notice.js @@ -0,0 +1,23 @@ +const summary = require('../app/util').addressSummary + +module.exports = function (lostAccounts) { + return { + date: new Date().toDateString(), + title: 'Account Problem Caught', + body: `MetaMask has fixed a bug where some accounts were previously mis-generated. This was a rare issue, but you were affected! + +We have successfully imported the accounts that were mis-generated, but they will no longer be recovered with your normal seed phrase. + +We have marked the affected accounts as "Loose", and recommend you transfer ether and tokens away from those accounts, or export & back them up elsewhere. + +Your affected accounts are: +${lostAccounts.map(acct => ` - ${summary(acct)}`).join('\n')} + +These accounts have been marked as "Loose" so they will be easy to recognize in the account list. + +For more information, please read [our blog post.][1] + +[1]: https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd#.7d8ktj4h3 + `, + } +} diff --git a/ui/lib/persistent-form.js b/ui/lib/persistent-form.js new file mode 100644 index 000000000..d4dc20b03 --- /dev/null +++ b/ui/lib/persistent-form.js @@ -0,0 +1,61 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const defaultKey = 'persistent-form-default' +const eventName = 'keyup' + +module.exports = PersistentForm + +function PersistentForm () { + Component.call(this) +} + +inherits(PersistentForm, Component) + +PersistentForm.prototype.componentDidMount = function () { + const fields = document.querySelectorAll('[data-persistent-formid]') + const store = this.getPersistentStore() + + for (var i = 0; i < fields.length; i++) { + const field = fields[i] + const key = field.getAttribute('data-persistent-formid') + const cached = store[key] + if (cached !== undefined) { + field.value = cached + } + + field.addEventListener(eventName, this.persistentFieldDidUpdate.bind(this)) + } +} + +PersistentForm.prototype.getPersistentStore = function () { + let store = window.localStorage[this.persistentFormParentId || defaultKey] + if (store && store !== 'null') { + store = JSON.parse(store) + } else { + store = {} + } + return store +} + +PersistentForm.prototype.setPersistentStore = function (newStore) { + window.localStorage[this.persistentFormParentId || defaultKey] = JSON.stringify(newStore) +} + +PersistentForm.prototype.persistentFieldDidUpdate = function (event) { + const field = event.target + const store = this.getPersistentStore() + const key = field.getAttribute('data-persistent-formid') + const val = field.value + store[key] = val + this.setPersistentStore(store) +} + +PersistentForm.prototype.componentWillUnmount = function () { + const fields = document.querySelectorAll('[data-persistent-formid]') + for (var i = 0; i < fields.length; i++) { + const field = fields[i] + field.removeEventListener(eventName, this.persistentFieldDidUpdate.bind(this)) + } + this.setPersistentStore({}) +} + diff --git a/ui/lib/tx-helper.js b/ui/lib/tx-helper.js new file mode 100644 index 000000000..ec19daf64 --- /dev/null +++ b/ui/lib/tx-helper.js @@ -0,0 +1,17 @@ +const valuesFor = require('../app/util').valuesFor + +module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, network) { + log.debug('tx-helper called with params:') + log.debug({ unapprovedTxs, unapprovedMsgs, personalMsgs, network }) + + const txValues = network ? valuesFor(unapprovedTxs).filter(txMeta => txMeta.metamaskNetworkId === network) : valuesFor(unapprovedTxs) + log.debug(`tx helper found ${txValues.length} unapproved txs`) + const msgValues = valuesFor(unapprovedMsgs) + log.debug(`tx helper found ${msgValues.length} unsigned messages`) + let allValues = txValues.concat(msgValues) + const personalValues = valuesFor(personalMsgs) + log.debug(`tx helper found ${personalValues.length} unsigned personal messages`) + allValues = allValues.concat(personalValues) + + return allValues.sort(txMeta => txMeta.time) +} diff --git a/ui/responsive/.gitignore b/ui/responsive/.gitignore deleted file mode 100644 index c6b1254b5..000000000 --- a/ui/responsive/.gitignore +++ /dev/null @@ -1,66 +0,0 @@ - -# Created by https://www.gitignore.io/api/osx,node - -### OSX ### -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - - -### Node ### -# Logs -logs -*.log -npm-debug.log* - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directory -# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git -node_modules - -# Optional npm cache directory -.npm - -# Optional REPL history -.node_repl_history - diff --git a/ui/responsive/app/account-detail.js b/ui/responsive/app/account-detail.js deleted file mode 100644 index da1ddf98b..000000000 --- a/ui/responsive/app/account-detail.js +++ /dev/null @@ -1,289 +0,0 @@ -const inherits = require('util').inherits -const extend = require('xtend') -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') -const valuesFor = require('./util').valuesFor -const Identicon = require('./components/identicon') -const EthBalance = require('./components/eth-balance') -const TransactionList = require('./components/transaction-list') -const ExportAccountView = require('./components/account-export') -const ethUtil = require('ethereumjs-util') -const EditableLabel = require('./components/editable-label') -const TabBar = require('./components/tab-bar') -const TokenList = require('./components/token-list') -const AccountDropdowns = require('./components/account-dropdowns').AccountDropdowns - -module.exports = connect(mapStateToProps)(AccountDetailScreen) - -function mapStateToProps (state) { - return { - metamask: state.metamask, - identities: state.metamask.identities, - accounts: state.metamask.accounts, - address: state.metamask.selectedAddress, - accountDetail: state.appState.accountDetail, - network: state.metamask.network, - unapprovedMsgs: valuesFor(state.metamask.unapprovedMsgs), - shapeShiftTxList: state.metamask.shapeShiftTxList, - transactions: state.metamask.selectedAddressTxList || [], - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - currentAccountTab: state.metamask.currentAccountTab, - tokens: state.metamask.tokens, - } -} - -inherits(AccountDetailScreen, Component) -function AccountDetailScreen () { - Component.call(this) -} - -AccountDetailScreen.prototype.render = function () { - var props = this.props - var selected = props.address || Object.keys(props.accounts)[0] - var checksumAddress = selected && ethUtil.toChecksumAddress(selected) - var identity = props.identities[selected] - var account = props.accounts[selected] - const { network, conversionRate, currentCurrency } = props - - return ( - - h('.account-detail-section', [ - - // identicon, label, balance, etc - h('.account-data-subsection', { - style: { - margin: '0 20px', - maxWidth: '320px', - }, - }, [ - - // header - identicon + nav - h('div', { - style: { - paddingTop: '20px', - display: 'flex', - justifyContent: 'flex-start', - alignItems: 'flex-start', - }, - }, [ - - // large identicon and addresses - h('.identicon-wrapper.select-none', [ - h(Identicon, { - diameter: 62, - address: selected, - }), - ]), - h('flex-column', { - style: { - lineHeight: '10px', - marginLeft: '15px', - }, - }, [ - h(EditableLabel, { - textValue: identity ? identity.name : '', - state: { - isEditingLabel: false, - }, - saveText: (text) => { - props.dispatch(actions.saveAccountLabel(selected, text)) - }, - }, [ - - // What is shown when not editing + edit text: - h('label.editing-label', [h('.edit-text', 'edit')]), - h( - 'div', - { - style: { - display: 'flex', - justifyContent: 'flex-start', - alignItems: 'center', - }, - }, - [ - h( - 'h2.font-medium.color-forest', - { - name: 'edit', - style: { - }, - }, - [ - identity && identity.name, - ] - ), - h( - AccountDropdowns, - { - style: { - marginRight: '8px', - marginLeft: 'auto', - }, - selected, - network, - identities: props.identities, - }, - ), - ] - ), - ]), - h('.flex-row', { - style: { - width: '15em', - justifyContent: 'space-between', - alignItems: 'baseline', - }, - }, [ - - // address - - h('div', { - style: { - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingTop: '3px', - width: '5em', - fontSize: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - marginTop: '10px', - marginBottom: '15px', - color: '#AEAEAE', - }, - }, checksumAddress), - ]), - - // account ballence - - ]), - ]), - h('.flex-row', { - style: { - justifyContent: 'space-between', - alignItems: 'flex-start', - }, - }, [ - - h(EthBalance, { - value: account && account.balance, - conversionRate, - currentCurrency, - style: { - lineHeight: '7px', - marginTop: '10px', - }, - }), - - h('button', { - onClick: () => props.dispatch(actions.buyEthView(selected)), - style: { - marginBottom: '20px', - marginRight: '8px', - position: 'absolute', - left: '219px', - }, - }, 'BUY'), - - h('button', { - onClick: () => props.dispatch(actions.showSendPage()), - style: { - marginBottom: '20px', - marginRight: '8px', - }, - }, 'SEND'), - - ]), - ]), - - // subview (tx history, pk export confirm, buy eth warning) - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.subview(), - ]), - - ]) - ) -} - -AccountDetailScreen.prototype.subview = function () { - var subview - try { - subview = this.props.accountDetail.subview - } catch (e) { - subview = null - } - - switch (subview) { - case 'transactions': - return this.tabSections() - case 'export': - var state = extend({key: 'export'}, this.props) - return h(ExportAccountView, state) - default: - return this.tabSections() - } -} - -AccountDetailScreen.prototype.tabSections = function () { - const { currentAccountTab } = this.props - - return h('section.tabSection', [ - - h(TabBar, { - tabs: [ - { content: 'Sent', key: 'history' }, - { content: 'Tokens', key: 'tokens' }, - ], - defaultTab: currentAccountTab || 'history', - tabSelected: (key) => { - this.props.dispatch(actions.setCurrentAccountTab(key)) - }, - }), - - this.tabSwitchView(), - ]) -} - -AccountDetailScreen.prototype.tabSwitchView = function () { - const props = this.props - const { address, network } = props - const { currentAccountTab, tokens } = this.props - - switch (currentAccountTab) { - case 'tokens': - return h(TokenList, { - userAddress: address, - network, - tokens, - addToken: () => this.props.dispatch(actions.showAddTokenPage()), - }) - default: - return this.transactionList() - } -} - -AccountDetailScreen.prototype.transactionList = function () { - const {transactions, unapprovedMsgs, address, - network, shapeShiftTxList, conversionRate } = this.props - - return h(TransactionList, { - transactions: transactions.sort((a, b) => b.time - a.time), - network, - unapprovedMsgs, - conversionRate, - address, - shapeShiftTxList, - viewPendingTx: (txId) => { - this.props.dispatch(actions.viewPendingTx(txId)) - }, - }) -} diff --git a/ui/responsive/app/accounts/import/index.js b/ui/responsive/app/accounts/import/index.js deleted file mode 100644 index 97b387229..000000000 --- a/ui/responsive/app/accounts/import/index.js +++ /dev/null @@ -1,100 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../../actions') -import Select from 'react-select' - -// Subviews -const JsonImportView = require('./json.js') -const PrivateKeyImportView = require('./private-key.js') - -const menuItems = [ - 'Private Key', - 'JSON File', -] - -module.exports = connect(mapStateToProps)(AccountImportSubview) - -function mapStateToProps (state) { - return { - menuItems, - } -} - -inherits(AccountImportSubview, Component) -function AccountImportSubview () { - Component.call(this) -} - -AccountImportSubview.prototype.render = function () { - const props = this.props - const state = this.state || {} - const { menuItems } = props - const { type } = state - - return ( - h('div', { - style: { - }, - }, [ - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: (event) => { - props.dispatch(actions.goHome()) - }, - }), - h('h2.page-subtitle', 'Import Accounts'), - ]), - h('div', { - style: { - padding: '10px', - color: 'rgb(174, 174, 174)', - }, - }, [ - - h('h3', { style: { padding: '3px' } }, 'SELECT TYPE'), - - h('style', ` - .has-value.Select--single > .Select-control .Select-value .Select-value-label, .Select-value-label { - color: rgb(174,174,174); - } - `), - - h(Select, { - name: 'import-type-select', - clearable: false, - value: type || menuItems[0], - options: menuItems.map((type) => { - return { - value: type, - label: type, - } - }), - onChange: (opt) => { - this.setState({ type: opt.value }) - }, - }), - ]), - - this.renderImportView(), - ]) - ) -} - -AccountImportSubview.prototype.renderImportView = function () { - const props = this.props - const state = this.state || {} - const { type } = state - const { menuItems } = props - const current = type || menuItems[0] - - switch (current) { - case 'Private Key': - return h(PrivateKeyImportView) - case 'JSON File': - return h(JsonImportView) - default: - return h(JsonImportView) - } -} diff --git a/ui/responsive/app/accounts/import/json.js b/ui/responsive/app/accounts/import/json.js deleted file mode 100644 index 158a3c923..000000000 --- a/ui/responsive/app/accounts/import/json.js +++ /dev/null @@ -1,100 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../../actions') -const FileInput = require('react-simple-file-input').default - -const HELP_LINK = 'https://github.com/MetaMask/faq/blob/master/README.md#q-i-cant-use-the-import-feature-for-uploading-a-json-file-the-window-keeps-closing-when-i-try-to-select-a-file' - -module.exports = connect(mapStateToProps)(JsonImportSubview) - -function mapStateToProps (state) { - return { - error: state.appState.warning, - } -} - -inherits(JsonImportSubview, Component) -function JsonImportSubview () { - Component.call(this) -} - -JsonImportSubview.prototype.render = function () { - const { error } = this.props - - return ( - h('div', { - style: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - padding: '5px 15px 0px 15px', - }, - }, [ - - h('p', 'Used by a variety of different clients'), - h('a.warning', { href: HELP_LINK, target: '_blank' }, 'File import not working? Click here!'), - - h(FileInput, { - readAs: 'text', - onLoad: this.onLoad.bind(this), - style: { - margin: '20px 0px 12px 20px', - fontSize: '15px', - }, - }), - - h('input.large-input.letter-spacey', { - type: 'password', - placeholder: 'Enter password', - id: 'json-password-box', - onKeyPress: this.createKeyringOnEnter.bind(this), - style: { - width: 260, - marginTop: 12, - }, - }), - - h('button.primary', { - onClick: this.createNewKeychain.bind(this), - style: { - margin: 12, - }, - }, 'Import'), - - error ? h('span.error', error) : null, - ]) - ) -} - -JsonImportSubview.prototype.onLoad = function (event, file) { - this.setState({file: file, fileContents: event.target.result}) -} - -JsonImportSubview.prototype.createKeyringOnEnter = function (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.createNewKeychain() - } -} - -JsonImportSubview.prototype.createNewKeychain = function () { - const state = this.state - const { fileContents } = state - - if (!fileContents) { - const message = 'You must select a file to import.' - return this.props.dispatch(actions.displayWarning(message)) - } - - const passwordInput = document.getElementById('json-password-box') - const password = passwordInput.value - - if (!password) { - const message = 'You must enter a password for the selected file.' - return this.props.dispatch(actions.displayWarning(message)) - } - - this.props.dispatch(actions.importNewAccount('JSON File', [ fileContents, password ])) -} diff --git a/ui/responsive/app/accounts/import/private-key.js b/ui/responsive/app/accounts/import/private-key.js deleted file mode 100644 index 68ccee58e..000000000 --- a/ui/responsive/app/accounts/import/private-key.js +++ /dev/null @@ -1,67 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../../actions') - -module.exports = connect(mapStateToProps)(PrivateKeyImportView) - -function mapStateToProps (state) { - return { - error: state.appState.warning, - } -} - -inherits(PrivateKeyImportView, Component) -function PrivateKeyImportView () { - Component.call(this) -} - -PrivateKeyImportView.prototype.render = function () { - const { error } = this.props - - return ( - h('div', { - style: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - padding: '5px 15px 0px 15px', - }, - }, [ - h('span', 'Paste your private key string here'), - - h('input.large-input.letter-spacey', { - type: 'password', - id: 'private-key-box', - onKeyPress: this.createKeyringOnEnter.bind(this), - style: { - width: 260, - marginTop: 12, - }, - }), - - h('button.primary', { - onClick: this.createNewKeychain.bind(this), - style: { - margin: 12, - }, - }, 'Import'), - - error ? h('span.error', error) : null, - ]) - ) -} - -PrivateKeyImportView.prototype.createKeyringOnEnter = function (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.createNewKeychain() - } -} - -PrivateKeyImportView.prototype.createNewKeychain = function () { - const input = document.getElementById('private-key-box') - const privateKey = input.value - this.props.dispatch(actions.importNewAccount('Private Key', [ privateKey ])) -} diff --git a/ui/responsive/app/accounts/import/seed.js b/ui/responsive/app/accounts/import/seed.js deleted file mode 100644 index b4a7c0afa..000000000 --- a/ui/responsive/app/accounts/import/seed.js +++ /dev/null @@ -1,30 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect - -module.exports = connect(mapStateToProps)(SeedImportSubview) - -function mapStateToProps (state) { - return {} -} - -inherits(SeedImportSubview, Component) -function SeedImportSubview () { - Component.call(this) -} - -SeedImportSubview.prototype.render = function () { - return ( - h('div', { - style: { - }, - }, [ - `Paste your seed phrase here!`, - h('textarea'), - h('br'), - h('button', 'Submit'), - ]) - ) -} - diff --git a/ui/responsive/app/actions.js b/ui/responsive/app/actions.js deleted file mode 100644 index 2c60448dd..000000000 --- a/ui/responsive/app/actions.js +++ /dev/null @@ -1,1031 +0,0 @@ -const getBuyEthUrl = require('../../../app/scripts/lib/buy-eth-url') - -var actions = { - _setBackgroundConnection: _setBackgroundConnection, - - GO_HOME: 'GO_HOME', - goHome: goHome, - // menu state - getNetworkStatus: 'getNetworkStatus', - // transition state - TRANSITION_FORWARD: 'TRANSITION_FORWARD', - TRANSITION_BACKWARD: 'TRANSITION_BACKWARD', - transitionForward, - transitionBackward, - // remote state - UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE', - updateMetamaskState: updateMetamaskState, - // notices - MARK_NOTICE_READ: 'MARK_NOTICE_READ', - markNoticeRead: markNoticeRead, - SHOW_NOTICE: 'SHOW_NOTICE', - showNotice: showNotice, - CLEAR_NOTICES: 'CLEAR_NOTICES', - clearNotices: clearNotices, - markAccountsFound, - // intialize screen - CREATE_NEW_VAULT_IN_PROGRESS: 'CREATE_NEW_VAULT_IN_PROGRESS', - SHOW_CREATE_VAULT: 'SHOW_CREATE_VAULT', - SHOW_RESTORE_VAULT: 'SHOW_RESTORE_VAULT', - FORGOT_PASSWORD: 'FORGOT_PASSWORD', - forgotPassword: forgotPassword, - SHOW_INIT_MENU: 'SHOW_INIT_MENU', - SHOW_NEW_VAULT_SEED: 'SHOW_NEW_VAULT_SEED', - SHOW_INFO_PAGE: 'SHOW_INFO_PAGE', - SHOW_IMPORT_PAGE: 'SHOW_IMPORT_PAGE', - unlockMetamask: unlockMetamask, - unlockFailed: unlockFailed, - showCreateVault: showCreateVault, - showRestoreVault: showRestoreVault, - showInitializeMenu: showInitializeMenu, - showImportPage, - createNewVaultAndKeychain: createNewVaultAndKeychain, - createNewVaultAndRestore: createNewVaultAndRestore, - createNewVaultInProgress: createNewVaultInProgress, - addNewKeyring, - importNewAccount, - addNewAccount, - NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN', - navigateToNewAccountScreen, - showNewVaultSeed: showNewVaultSeed, - showInfoPage: showInfoPage, - // seed recovery actions - REVEAL_SEED_CONFIRMATION: 'REVEAL_SEED_CONFIRMATION', - revealSeedConfirmation: revealSeedConfirmation, - requestRevealSeed: requestRevealSeed, - // unlock screen - UNLOCK_IN_PROGRESS: 'UNLOCK_IN_PROGRESS', - UNLOCK_FAILED: 'UNLOCK_FAILED', - UNLOCK_METAMASK: 'UNLOCK_METAMASK', - LOCK_METAMASK: 'LOCK_METAMASK', - tryUnlockMetamask: tryUnlockMetamask, - lockMetamask: lockMetamask, - unlockInProgress: unlockInProgress, - // error handling - displayWarning: displayWarning, - DISPLAY_WARNING: 'DISPLAY_WARNING', - HIDE_WARNING: 'HIDE_WARNING', - hideWarning: hideWarning, - // accounts screen - SET_SELECTED_ACCOUNT: 'SET_SELECTED_ACCOUNT', - SHOW_ACCOUNT_DETAIL: 'SHOW_ACCOUNT_DETAIL', - SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE', - SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE', - SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE', - SET_CURRENT_FIAT: 'SET_CURRENT_FIAT', - setCurrentCurrency: setCurrentCurrency, - setCurrentAccountTab, - // account detail screen - SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', - showSendPage: showSendPage, - ADD_TO_ADDRESS_BOOK: 'ADD_TO_ADDRESS_BOOK', - addToAddressBook: addToAddressBook, - REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT', - requestExportAccount: requestExportAccount, - EXPORT_ACCOUNT: 'EXPORT_ACCOUNT', - exportAccount: exportAccount, - SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY', - showPrivateKey: showPrivateKey, - SAVE_ACCOUNT_LABEL: 'SAVE_ACCOUNT_LABEL', - saveAccountLabel: saveAccountLabel, - // tx conf screen - COMPLETED_TX: 'COMPLETED_TX', - TRANSACTION_ERROR: 'TRANSACTION_ERROR', - NEXT_TX: 'NEXT_TX', - PREVIOUS_TX: 'PREV_TX', - signMsg: signMsg, - cancelMsg: cancelMsg, - signPersonalMsg, - cancelPersonalMsg, - sendTx: sendTx, - signTx: signTx, - updateAndApproveTx, - cancelTx: cancelTx, - completedTx: completedTx, - txError: txError, - nextTx: nextTx, - previousTx: previousTx, - viewPendingTx: viewPendingTx, - VIEW_PENDING_TX: 'VIEW_PENDING_TX', - // app messages - confirmSeedWords: confirmSeedWords, - showAccountDetail: showAccountDetail, - BACK_TO_ACCOUNT_DETAIL: 'BACK_TO_ACCOUNT_DETAIL', - backToAccountDetail: backToAccountDetail, - showAccountsPage: showAccountsPage, - showConfTxPage: showConfTxPage, - // config screen - SHOW_CONFIG_PAGE: 'SHOW_CONFIG_PAGE', - SET_RPC_TARGET: 'SET_RPC_TARGET', - SET_DEFAULT_RPC_TARGET: 'SET_DEFAULT_RPC_TARGET', - SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE', - USE_ETHERSCAN_PROVIDER: 'USE_ETHERSCAN_PROVIDER', - useEtherscanProvider: useEtherscanProvider, - showConfigPage, - SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', - showAddTokenPage, - addToken, - setRpcTarget: setRpcTarget, - setDefaultRpcTarget: setDefaultRpcTarget, - setProviderType: setProviderType, - // loading overlay - SHOW_LOADING: 'SHOW_LOADING_INDICATION', - HIDE_LOADING: 'HIDE_LOADING_INDICATION', - showLoadingIndication: showLoadingIndication, - hideLoadingIndication: hideLoadingIndication, - // buy Eth with coinbase - BUY_ETH: 'BUY_ETH', - buyEth: buyEth, - buyEthView: buyEthView, - BUY_ETH_VIEW: 'BUY_ETH_VIEW', - COINBASE_SUBVIEW: 'COINBASE_SUBVIEW', - coinBaseSubview: coinBaseSubview, - SHAPESHIFT_SUBVIEW: 'SHAPESHIFT_SUBVIEW', - shapeShiftSubview: shapeShiftSubview, - PAIR_UPDATE: 'PAIR_UPDATE', - pairUpdate: pairUpdate, - coinShiftRquest: coinShiftRquest, - SHOW_SUB_LOADING_INDICATION: 'SHOW_SUB_LOADING_INDICATION', - showSubLoadingIndication: showSubLoadingIndication, - HIDE_SUB_LOADING_INDICATION: 'HIDE_SUB_LOADING_INDICATION', - hideSubLoadingIndication: hideSubLoadingIndication, -// QR STUFF: - SHOW_QR: 'SHOW_QR', - showQrView: showQrView, - reshowQrCode: reshowQrCode, - SHOW_QR_VIEW: 'SHOW_QR_VIEW', -// FORGOT PASSWORD: - BACK_TO_INIT_MENU: 'BACK_TO_INIT_MENU', - goBackToInitView: goBackToInitView, - RECOVERY_IN_PROGRESS: 'RECOVERY_IN_PROGRESS', - BACK_TO_UNLOCK_VIEW: 'BACK_TO_UNLOCK_VIEW', - backToUnlockView: backToUnlockView, - // SHOWING KEYCHAIN - SHOW_NEW_KEYCHAIN: 'SHOW_NEW_KEYCHAIN', - showNewKeychain: showNewKeychain, - - callBackgroundThenUpdate, - forceUpdateMetamaskState, -} - -module.exports = actions - -var background = null -function _setBackgroundConnection (backgroundConnection) { - background = backgroundConnection -} - -function goHome () { - return { - type: actions.GO_HOME, - } -} - -// async actions - -function tryUnlockMetamask (password) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - dispatch(actions.unlockInProgress()) - log.debug(`background.submitPassword`) - background.submitPassword(password, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - dispatch(actions.unlockFailed(err.message)) - } else { - dispatch(actions.transitionForward()) - forceUpdateMetamaskState(dispatch) - } - }) - } -} - -function transitionForward () { - return { - type: this.TRANSITION_FORWARD, - } -} - -function transitionBackward () { - return { - type: this.TRANSITION_BACKWARD, - } -} - -function confirmSeedWords () { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.clearSeedWordCache`) - background.clearSeedWordCache((err, account) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - - log.info('Seed word cache cleared. ' + account) - dispatch(actions.showAccountDetail(account)) - }) - } -} - -function createNewVaultAndRestore (password, seed) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.createNewVaultAndRestore`) - background.createNewVaultAndRestore(password, seed, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.showAccountsPage()) - }) - } -} - -function createNewVaultAndKeychain (password) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.createNewVaultAndKeychain`) - background.createNewVaultAndKeychain(password, (err) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - log.debug(`background.placeSeedWords`) - background.placeSeedWords((err) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch(actions.hideLoadingIndication()) - forceUpdateMetamaskState(dispatch) - }) - }) - } -} - -function revealSeedConfirmation () { - return { - type: this.REVEAL_SEED_CONFIRMATION, - } -} - -function requestRevealSeed (password) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.submitPassword`) - background.submitPassword(password, (err) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - log.debug(`background.placeSeedWords`) - background.placeSeedWords((err, result) => { - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.hideLoadingIndication()) - dispatch(actions.showNewVaultSeed(result)) - }) - }) - } -} - -function addNewKeyring (type, opts) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.addNewKeyring`) - background.addNewKeyring(type, opts, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.showAccountsPage()) - }) - } -} - -function importNewAccount (strategy, args) { - return (dispatch) => { - dispatch(actions.showLoadingIndication('This may take a while, be patient.')) - log.debug(`background.importAccountWithStrategy`) - background.importAccountWithStrategy(strategy, args, (err) => { - if (err) return dispatch(actions.displayWarning(err.message)) - log.debug(`background.getState`) - background.getState((err, newState) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch(actions.updateMetamaskState(newState)) - dispatch({ - type: actions.SHOW_ACCOUNT_DETAIL, - value: newState.selectedAddress, - }) - }) - }) - } -} - -function navigateToNewAccountScreen () { - return { - type: this.NEW_ACCOUNT_SCREEN, - } -} - -function addNewAccount () { - log.debug(`background.addNewAccount`) - return callBackgroundThenUpdate(background.addNewAccount) -} - -function showInfoPage () { - return { - type: actions.SHOW_INFO_PAGE, - } -} - -function setCurrentCurrency (currencyCode) { - return (dispatch) => { - dispatch(this.showLoadingIndication()) - log.debug(`background.setCurrentCurrency`) - background.setCurrentCurrency(currencyCode, (err, data) => { - dispatch(this.hideLoadingIndication()) - if (err) { - log.error(err.stack) - return dispatch(actions.displayWarning(err.message)) - } - dispatch({ - type: this.SET_CURRENT_FIAT, - value: { - currentCurrency: data.currentCurrency, - conversionRate: data.conversionRate, - conversionDate: data.conversionDate, - }, - }) - }) - } -} - -function signMsg (msgData) { - log.debug('action - signMsg') - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - - log.debug(`actions calling background.signMessage`) - background.signMessage(msgData, (err, newState) => { - log.debug('signMessage called back') - dispatch(actions.updateMetamaskState(newState)) - dispatch(actions.hideLoadingIndication()) - - if (err) log.error(err) - if (err) return dispatch(actions.displayWarning(err.message)) - - dispatch(actions.completedTx(msgData.metamaskId)) - }) - } -} - -function signPersonalMsg (msgData) { - log.debug('action - signPersonalMsg') - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - - log.debug(`actions calling background.signPersonalMessage`) - background.signPersonalMessage(msgData, (err, newState) => { - log.debug('signPersonalMessage called back') - dispatch(actions.updateMetamaskState(newState)) - dispatch(actions.hideLoadingIndication()) - - if (err) log.error(err) - if (err) return dispatch(actions.displayWarning(err.message)) - - dispatch(actions.completedTx(msgData.metamaskId)) - }) - } -} - -function signTx (txData) { - return (dispatch) => { - global.ethQuery.sendTransaction(txData, (err, data) => { - dispatch(actions.hideLoadingIndication()) - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.hideWarning()) - }) - dispatch(this.showConfTxPage()) - } -} - -function sendTx (txData) { - log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`) - return (dispatch) => { - log.debug(`actions calling background.approveTransaction`) - background.approveTransaction(txData.id, (err) => { - if (err) { - dispatch(actions.txError(err)) - return log.error(err.message) - } - dispatch(actions.completedTx(txData.id)) - }) - } -} - -function updateAndApproveTx (txData) { - log.info('actions: updateAndApproveTx: ' + JSON.stringify(txData)) - return (dispatch) => { - log.debug(`actions calling background.updateAndApproveTx`) - background.updateAndApproveTransaction(txData, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - dispatch(actions.txError(err)) - return log.error(err.message) - } - dispatch(actions.completedTx(txData.id)) - }) - } -} - -function completedTx (id) { - return { - type: actions.COMPLETED_TX, - value: id, - } -} - -function txError (err) { - return { - type: actions.TRANSACTION_ERROR, - message: err.message, - } -} - -function cancelMsg (msgData) { - log.debug(`background.cancelMessage`) - background.cancelMessage(msgData.id) - return actions.completedTx(msgData.id) -} - -function cancelPersonalMsg (msgData) { - const id = msgData.id - background.cancelPersonalMessage(id) - return actions.completedTx(id) -} - -function cancelTx (txData) { - log.debug(`background.cancelTransaction`) - background.cancelTransaction(txData.id) - return actions.completedTx(txData.id) -} - -// -// initialize screen -// - -function showCreateVault () { - return { - type: actions.SHOW_CREATE_VAULT, - } -} - -function showRestoreVault () { - return { - type: actions.SHOW_RESTORE_VAULT, - } -} - -function forgotPassword () { - return { - type: actions.FORGOT_PASSWORD, - } -} - -function showInitializeMenu () { - return { - type: actions.SHOW_INIT_MENU, - } -} - -function showImportPage () { - return { - type: actions.SHOW_IMPORT_PAGE, - } -} - -function createNewVaultInProgress () { - return { - type: actions.CREATE_NEW_VAULT_IN_PROGRESS, - } -} - -function showNewVaultSeed (seed) { - return { - type: actions.SHOW_NEW_VAULT_SEED, - value: seed, - } -} - -function backToUnlockView () { - return { - type: actions.BACK_TO_UNLOCK_VIEW, - } -} - -function showNewKeychain () { - return { - type: actions.SHOW_NEW_KEYCHAIN, - } -} - -// -// unlock screen -// - -function unlockInProgress () { - return { - type: actions.UNLOCK_IN_PROGRESS, - } -} - -function unlockFailed (message) { - return { - type: actions.UNLOCK_FAILED, - value: message, - } -} - -function unlockMetamask (account) { - return { - type: actions.UNLOCK_METAMASK, - value: account, - } -} - -function updateMetamaskState (newState) { - return { - type: actions.UPDATE_METAMASK_STATE, - value: newState, - } -} - -function lockMetamask () { - log.debug(`background.setLocked`) - return callBackgroundThenUpdate(background.setLocked) -} - -function setCurrentAccountTab (newTabName) { - log.debug(`background.setCurrentAccountTab: ${newTabName}`) - return callBackgroundThenUpdateNoSpinner(background.setCurrentAccountTab, newTabName) -} - -function showAccountDetail (address) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.setSelectedAddress`) - background.setSelectedAddress(address, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch({ - type: actions.SHOW_ACCOUNT_DETAIL, - value: address, - }) - }) - } -} - -function backToAccountDetail (address) { - return { - type: actions.BACK_TO_ACCOUNT_DETAIL, - value: address, - } -} - -function showAccountsPage () { - return { - type: actions.SHOW_ACCOUNTS_PAGE, - } -} - -function showConfTxPage (transForward = true) { - return { - type: actions.SHOW_CONF_TX_PAGE, - transForward: transForward, - } -} - -function nextTx () { - return { - type: actions.NEXT_TX, - } -} - -function viewPendingTx (txId) { - return { - type: actions.VIEW_PENDING_TX, - value: txId, - } -} - -function previousTx () { - return { - type: actions.PREVIOUS_TX, - } -} - -function showConfigPage (transitionForward = true) { - return { - type: actions.SHOW_CONFIG_PAGE, - value: transitionForward, - } -} - -function showAddTokenPage (transitionForward = true) { - return { - type: actions.SHOW_ADD_TOKEN_PAGE, - value: transitionForward, - } -} - -function addToken (address, symbol, decimals) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - background.addToken(address, symbol, decimals, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - setTimeout(() => { - dispatch(actions.goHome()) - }, 250) - }) - } -} - -function goBackToInitView () { - return { - type: actions.BACK_TO_INIT_MENU, - } -} - -// -// notice -// - -function markNoticeRead (notice) { - return (dispatch) => { - dispatch(this.showLoadingIndication()) - log.debug(`background.markNoticeRead`) - background.markNoticeRead(notice, (err, notice) => { - dispatch(this.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err)) - } - if (notice) { - return dispatch(actions.showNotice(notice)) - } else { - dispatch(this.clearNotices()) - return { - type: actions.SHOW_ACCOUNTS_PAGE, - } - } - }) - } -} - -function showNotice (notice) { - return { - type: actions.SHOW_NOTICE, - value: notice, - } -} - -function clearNotices () { - return { - type: actions.CLEAR_NOTICES, - } -} - -function markAccountsFound () { - log.debug(`background.markAccountsFound`) - return callBackgroundThenUpdate(background.markAccountsFound) -} - -// -// config -// - -// default rpc target refers to localhost:8545 in this instance. -function setDefaultRpcTarget (rpcList) { - log.debug(`background.setDefaultRpcTarget`) - return (dispatch) => { - background.setDefaultRpc((err, result) => { - if (err) { - log.error(err) - return dispatch(self.displayWarning('Had a problem changing networks.')) - } - }) - } -} - -function setRpcTarget (newRpc) { - log.debug(`background.setRpcTarget`) - return (dispatch) => { - background.setCustomRpc(newRpc, (err, result) => { - if (err) { - log.error(err) - return dispatch(self.displayWarning('Had a problem changing networks!')) - } - }) - } -} - -// Calls the addressBookController to add a new address. -function addToAddressBook (recipient, nickname) { - log.debug(`background.addToAddressBook`) - return (dispatch) => { - background.setAddressBook(recipient, nickname, (err, result) => { - if (err) { - log.error(err) - return dispatch(self.displayWarning('Address book failed to update')) - } - }) - } -} - -function setProviderType (type) { - log.debug(`background.setProviderType`) - background.setProviderType(type) - return { - type: actions.SET_PROVIDER_TYPE, - value: type, - } -} - -function useEtherscanProvider () { - log.debug(`background.useEtherscanProvider`) - background.useEtherscanProvider() - return { - type: actions.USE_ETHERSCAN_PROVIDER, - } -} - -function showLoadingIndication (message) { - return { - type: actions.SHOW_LOADING, - value: message, - } -} - -function hideLoadingIndication () { - return { - type: actions.HIDE_LOADING, - } -} - -function showSubLoadingIndication () { - return { - type: actions.SHOW_SUB_LOADING_INDICATION, - } -} - -function hideSubLoadingIndication () { - return { - type: actions.HIDE_SUB_LOADING_INDICATION, - } -} - -function displayWarning (text) { - return { - type: actions.DISPLAY_WARNING, - value: text, - } -} - -function hideWarning () { - return { - type: actions.HIDE_WARNING, - } -} - -function requestExportAccount () { - return { - type: actions.REQUEST_ACCOUNT_EXPORT, - } -} - -function exportAccount (password, address) { - var self = this - - return function (dispatch) { - dispatch(self.showLoadingIndication()) - - log.debug(`background.submitPassword`) - background.submitPassword(password, function (err) { - if (err) { - log.error('Error in submiting password.') - dispatch(self.hideLoadingIndication()) - return dispatch(self.displayWarning('Incorrect Password.')) - } - log.debug(`background.exportAccount`) - background.exportAccount(address, function (err, result) { - dispatch(self.hideLoadingIndication()) - - if (err) { - log.error(err) - return dispatch(self.displayWarning('Had a problem exporting the account.')) - } - - dispatch(self.showPrivateKey(result)) - }) - }) - } -} - -function showPrivateKey (key) { - return { - type: actions.SHOW_PRIVATE_KEY, - value: key, - } -} - -function saveAccountLabel (account, label) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.saveAccountLabel`) - background.saveAccountLabel(account, label, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch({ - type: actions.SAVE_ACCOUNT_LABEL, - value: { account, label }, - }) - }) - } -} - -function showSendPage () { - return { - type: actions.SHOW_SEND_PAGE, - } -} - -function buyEth (opts) { - return (dispatch) => { - const url = getBuyEthUrl(opts) - global.platform.openWindow({ url }) - dispatch({ - type: actions.BUY_ETH, - }) - } -} - -function buyEthView (address) { - return { - type: actions.BUY_ETH_VIEW, - value: address, - } -} - -function coinBaseSubview () { - return { - type: actions.COINBASE_SUBVIEW, - } -} - -function pairUpdate (coin) { - return (dispatch) => { - dispatch(actions.showSubLoadingIndication()) - dispatch(actions.hideWarning()) - shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { - dispatch(actions.hideSubLoadingIndication()) - dispatch({ - type: actions.PAIR_UPDATE, - value: { - marketinfo: mktResponse, - }, - }) - }) - } -} - -function shapeShiftSubview (network) { - var pair = 'btc_eth' - - return (dispatch) => { - dispatch(actions.showSubLoadingIndication()) - shapeShiftRequest('marketinfo', {pair}, (mktResponse) => { - shapeShiftRequest('getcoins', {}, (response) => { - dispatch(actions.hideSubLoadingIndication()) - if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) - dispatch({ - type: actions.SHAPESHIFT_SUBVIEW, - value: { - marketinfo: mktResponse, - coinOptions: response, - }, - }) - }) - }) - } -} - -function coinShiftRquest (data, marketData) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - shapeShiftRequest('shift', { method: 'POST', data}, (response) => { - dispatch(actions.hideLoadingIndication()) - if (response.error) return dispatch(actions.displayWarning(response.error)) - var message = ` - Deposit your ${response.depositType} to the address bellow:` - log.debug(`background.createShapeShiftTx`) - background.createShapeShiftTx(response.deposit, response.depositType) - dispatch(actions.showQrView(response.deposit, [message].concat(marketData))) - }) - } -} - -function showQrView (data, message) { - return { - type: actions.SHOW_QR_VIEW, - value: { - message: message, - data: data, - }, - } -} -function reshowQrCode (data, coin) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { - if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) - - var message = [ - `Deposit your ${coin} to the address bellow:`, - `Deposit Limit: ${mktResponse.limit}`, - `Deposit Minimum:${mktResponse.minimum}`, - ] - - dispatch(actions.hideLoadingIndication()) - return dispatch(actions.showQrView(data, message)) - }) - } -} - -function shapeShiftRequest (query, options, cb) { - var queryResponse, method - !options ? options = {} : null - options.method ? method = options.method : method = 'GET' - - var requestListner = function (request) { - queryResponse = JSON.parse(this.responseText) - cb ? cb(queryResponse) : null - return queryResponse - } - - var shapShiftReq = new XMLHttpRequest() - shapShiftReq.addEventListener('load', requestListner) - shapShiftReq.open(method, `https://shapeshift.io/${query}/${options.pair ? options.pair : ''}`, true) - - if (options.method === 'POST') { - var jsonObj = JSON.stringify(options.data) - shapShiftReq.setRequestHeader('Content-Type', 'application/json') - return shapShiftReq.send(jsonObj) - } else { - return shapShiftReq.send() - } -} - -// Call Background Then Update -// -// A function generator for a common pattern wherein: -// We show loading indication. -// We call a background method. -// We hide loading indication. -// If it errored, we show a warning. -// If it didn't, we update the state. -function callBackgroundThenUpdateNoSpinner (method, ...args) { - return (dispatch) => { - method.call(background, ...args, (err) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - forceUpdateMetamaskState(dispatch) - }) - } -} - -function callBackgroundThenUpdate (method, ...args) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - method.call(background, ...args, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - forceUpdateMetamaskState(dispatch) - }) - } -} - -function forceUpdateMetamaskState (dispatch) { - log.debug(`background.getState`) - background.getState((err, newState) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch(actions.updateMetamaskState(newState)) - }) -} diff --git a/ui/responsive/app/add-token.js b/ui/responsive/app/add-token.js deleted file mode 100644 index b303b5c0d..000000000 --- a/ui/responsive/app/add-token.js +++ /dev/null @@ -1,219 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') - -const ethUtil = require('ethereumjs-util') -const abi = require('human-standard-token-abi') -const Eth = require('ethjs-query') -const EthContract = require('ethjs-contract') - -const emptyAddr = '0x0000000000000000000000000000000000000000' - -module.exports = connect(mapStateToProps)(AddTokenScreen) - -function mapStateToProps (state) { - return { - } -} - -inherits(AddTokenScreen, Component) -function AddTokenScreen () { - this.state = { - warning: null, - address: null, - symbol: 'TOKEN', - decimals: 18, - } - Component.call(this) -} - -AddTokenScreen.prototype.render = function () { - const state = this.state - const props = this.props - const { warning, symbol, decimals } = state - - return ( - h('.flex-column.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: (event) => { - props.dispatch(actions.goHome()) - }, - }), - h('h2.page-subtitle', 'Add Token'), - ]), - - h('.error', { - style: { - display: warning ? 'block' : 'none', - padding: '0 20px', - textAlign: 'center', - }, - }, warning), - - // conf view - h('.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-space-around', { - style: { - padding: '20px', - }, - }, [ - - h('div', [ - h('span', { - style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Token Address'), - ]), - - h('section.flex-row.flex-center', [ - h('input#token-address', { - name: 'address', - placeholder: 'Token Address', - onChange: this.tokenAddressDidChange.bind(this), - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - }), - ]), - - h('div', [ - h('span', { - style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Token Sybmol'), - ]), - - h('div', { style: {display: 'flex'} }, [ - h('input#token_symbol', { - placeholder: `Like "ETH"`, - value: symbol, - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - onChange: (event) => { - var element = event.target - var symbol = element.value - this.setState({ symbol }) - }, - }), - ]), - - h('div', [ - h('span', { - style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Decimals of Precision'), - ]), - - h('div', { style: {display: 'flex'} }, [ - h('input#token_decimals', { - value: decimals, - type: 'number', - min: 0, - max: 36, - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - onChange: (event) => { - var element = event.target - var decimals = element.value.trim() - this.setState({ decimals }) - }, - }), - ]), - - h('button', { - style: { - alignSelf: 'center', - }, - onClick: (event) => { - const valid = this.validateInputs() - if (!valid) return - - const { address, symbol, decimals } = this.state - this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) - }, - }, 'Add'), - ]), - ]), - ]) - ) -} - -AddTokenScreen.prototype.componentWillMount = function () { - if (typeof global.ethereumProvider === 'undefined') return - - this.eth = new Eth(global.ethereumProvider) - this.contract = new EthContract(this.eth) - this.TokenContract = this.contract(abi) -} - -AddTokenScreen.prototype.tokenAddressDidChange = function (event) { - const el = event.target - const address = el.value.trim() - if (ethUtil.isValidAddress(address) && address !== emptyAddr) { - this.setState({ address }) - this.attemptToAutoFillTokenParams(address) - } -} - -AddTokenScreen.prototype.validateInputs = function () { - let msg = '' - const state = this.state - const { address, symbol, decimals } = state - - const validAddress = ethUtil.isValidAddress(address) - if (!validAddress) { - msg += 'Address is invalid. ' - } - - const validDecimals = decimals >= 0 && decimals < 36 - if (!validDecimals) { - msg += 'Decimals must be at least 0, and not over 36. ' - } - - const symbolLen = symbol.trim().length - const validSymbol = symbolLen > 0 && symbolLen < 10 - if (!validSymbol) { - msg += 'Symbol must be between 0 and 10 characters.' - } - - const isValid = validAddress && validDecimals - - if (!isValid) { - this.setState({ - warning: msg, - }) - } else { - this.setState({ warning: null }) - } - - return isValid -} - -AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) { - const contract = this.TokenContract.at(address) - - const results = await Promise.all([ - contract.symbol(), - contract.decimals(), - ]) - - const [ symbol, decimals ] = results - if (symbol && decimals) { - console.log('SETTING SYMBOL AND DECIMALS', { symbol, decimals }) - this.setState({ symbol: symbol[0], decimals: decimals[0].toString() }) - } -} - diff --git a/ui/responsive/app/app.js b/ui/responsive/app/app.js deleted file mode 100644 index 1cfa2d7a9..000000000 --- a/ui/responsive/app/app.js +++ /dev/null @@ -1,580 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('./actions') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') -// init -const InitializeMenuScreen = require('./first-time/init-menu') -const NewKeyChainScreen = require('./new-keychain') -// unlock -const UnlockScreen = require('./unlock') -// accounts -const AccountDetailScreen = require('./account-detail') -const SendTransactionScreen = require('./send') -const ConfirmTxScreen = require('./conf-tx') -// notice -const NoticeScreen = require('./components/notice') -const generateLostAccountsNotice = require('../lib/lost-accounts-notice') -// other views -const ConfigScreen = require('./config') -const AddTokenScreen = require('./add-token') -const Import = require('./accounts/import') -const InfoScreen = require('./info') -const Loading = require('./components/loading') -const SandwichExpando = require('sandwich-expando') -const Dropdown = require('./components/dropdown').Dropdown -const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem -const NetworkIndicator = require('./components/network') -const BuyView = require('./components/buy-button-subview') -const QrView = require('./components/qr-code') -const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete') -const HDRestoreVaultScreen = require('./keychains/hd/restore-vault') -const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation') - -module.exports = connect(mapStateToProps)(App) - -inherits(App, Component) -function App () { Component.call(this) } - -function mapStateToProps (state) { - return { - // state from plugin - isLoading: state.appState.isLoading, - loadingMessage: state.appState.loadingMessage, - noActiveNotices: state.metamask.noActiveNotices, - isInitialized: state.metamask.isInitialized, - isUnlocked: state.metamask.isUnlocked, - currentView: state.appState.currentView, - activeAddress: state.appState.activeAddress, - transForward: state.appState.transForward, - seedWords: state.metamask.seedWords, - unapprovedTxs: state.metamask.unapprovedTxs, - unapprovedMsgs: state.metamask.unapprovedMsgs, - menuOpen: state.appState.menuOpen, - network: state.metamask.network, - provider: state.metamask.provider, - forgottenPassword: state.appState.forgottenPassword, - lastUnreadNotice: state.metamask.lastUnreadNotice, - lostAccounts: state.metamask.lostAccounts, - frequentRpcList: state.metamask.frequentRpcList || [], - } -} - -App.prototype.render = function () { - var props = this.props - const { isLoading, loadingMessage, transForward, network } = props - const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config' - const loadMessage = loadingMessage || isLoadingNetwork ? - `Connecting to ${this.getNetworkName()}` : null - - log.debug('Main ui render function') - - return ( - - h('.flex-column.flex-grow.full-height', { - style: { - // Windows was showing a vertical scroll bar: - overflow: 'hidden', - position: 'relative', - }, - }, [ - - // app bar - this.renderAppBar(), - this.renderNetworkDropdown(), - this.renderDropdown(), - - h(Loading, { - isLoading: isLoading || isLoadingNetwork, - loadingMessage: loadMessage, - }), - - // panel content - h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), [ - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.renderPrimary(), - ]), - ]), - ]) - ) -} - -App.prototype.renderAppBar = function () { - if (window.METAMASK_UI_TYPE === 'notification') { - return null - } - - const props = this.props - const state = this.state || {} - const isNetworkMenuOpen = state.isNetworkMenuOpen || false - - return ( - - h('div', [ - - h('.app-header.flex-row.flex-space-between', { - style: { - alignItems: 'center', - visibility: props.isUnlocked ? 'visible' : 'none', - background: props.isUnlocked ? 'white' : 'none', - height: '38px', - position: 'relative', - zIndex: 12, - }, - }, [ - - h('div.left-menu-section', { - style: { - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - }, - }, [ - - // mini logo - h('img', { - height: 24, - width: 24, - src: '/images/icon-128.png', - }), - - h(NetworkIndicator, { - network: this.props.network, - provider: this.props.provider, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen }) - }, - }), - ]), - - // metamask name - props.isUnlocked && h('h1', { - style: { - position: 'relative', - left: '9px', - }, - }, 'MetaMask'), - - props.isUnlocked && h('div', { - style: { - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - }, - }, [ - - // hamburger - props.isUnlocked && h(SandwichExpando, { - width: 16, - barHeight: 2, - padding: 0, - isOpen: state.isMainMenuOpen, - color: 'rgb(247,146,30)', - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - this.setState({ isMainMenuOpen: !state.isMainMenuOpen }) - }, - }), - ]), - ]), - ]) - ) -} - -App.prototype.renderNetworkDropdown = function () { - const props = this.props - const { provider: { type: providerType, rpcTarget: activeNetwork } } = props - const rpcList = props.frequentRpcList - const state = this.state || {} - const isOpen = state.isNetworkMenuOpen - - return h(Dropdown, { - isOpen, - onClickOutside: (event) => { - this.setState({ isNetworkMenuOpen: !isOpen }) - }, - zIndex: 11, - style: { - position: 'absolute', - left: '2px', - top: '36px', - }, - innerStyle: {}, - }, [ - - h( - DropdownMenuItem, - { - closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), - onClick: () => props.dispatch(actions.setProviderType('mainnet')), - }, - [ - h('.menu-icon.diamond'), - 'Main Ethereum Network', - providerType === 'mainnet' ? h('.check', '✓') : null, - ] - ), - - h( - DropdownMenuItem, - { - closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), - onClick: () => props.dispatch(actions.setProviderType('ropsten')), - }, - [ - h('.menu-icon.red-dot'), - 'Ropsten Test Network', - providerType === 'ropsten' ? h('.check', '✓') : null, - ] - ), - - h( - DropdownMenuItem, - { - closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), - onClick: () => props.dispatch(actions.setProviderType('kovan')), - }, - [ - h('.menu-icon.hollow-diamond'), - 'Kovan Test Network', - providerType === 'kovan' ? h('.check', '✓') : null, - ] - ), - - h( - DropdownMenuItem, - { - closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), - onClick: () => props.dispatch(actions.setProviderType('rinkeby')), - }, - [ - h('.menu-icon.golden-square'), - 'Rinkeby Test Network', - providerType === 'rinkeby' ? h('.check', '✓') : null, - ] - ), - - h( - DropdownMenuItem, - { - closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), - onClick: () => props.dispatch(actions.setDefaultRpcTarget(rpcList)), - }, - [ - h('i.fa.fa-question-circle.fa-lg.menu-icon'), - 'Localhost 8545', - activeNetwork === 'http://localhost:8545' ? h('.check', '✓') : null, - ] - ), - - this.renderCustomOption(props.provider), - this.renderCommonRpc(rpcList, props.provider), - - h( - DropdownMenuItem, - { - closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), - onClick: () => this.props.dispatch(actions.showConfigPage()), - }, - [ - h('i.fa.fa-question-circle.fa-lg.menu-icon'), - 'Custom RPC', - activeNetwork === 'custom' ? h('.check', '✓') : null, - ] - ), - - ]) -} - -App.prototype.renderDropdown = function () { - const state = this.state || {} - const isOpen = state.isMainMenuOpen - - return h(Dropdown, { - isOpen: isOpen, - zIndex: 11, - onClickOutside: (event) => { - this.setState({ isMainMenuOpen: !isOpen }) - }, - style: { - position: 'absolute', - right: '2px', - top: '38px', - }, - innerStyle: {}, - }, [ - h(DropdownMenuItem, { - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - onClick: () => { this.props.dispatch(actions.showConfigPage()) }, - }, 'Settings'), - - h(DropdownMenuItem, { - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - onClick: () => { this.props.dispatch(actions.showImportPage()) }, - }, 'Import Account'), - - h(DropdownMenuItem, { - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - onClick: () => { this.props.dispatch(actions.lockMetamask()) }, - }, 'Lock'), - - h(DropdownMenuItem, { - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - onClick: () => { this.props.dispatch(actions.showInfoPage()) }, - }, 'Info/Help'), - ]) -} - -App.prototype.renderBackButton = function (style, justArrow = false) { - var props = this.props - return ( - h('.flex-row', { - key: 'leftArrow', - style: style, - onClick: () => props.dispatch(actions.goBackToInitView()), - }, [ - h('i.fa.fa-arrow-left.cursor-pointer'), - justArrow ? null : h('div.cursor-pointer', { - style: { - marginLeft: '3px', - }, - onClick: () => props.dispatch(actions.goBackToInitView()), - }, 'BACK'), - ]) - ) -} - -App.prototype.renderPrimary = function () { - log.debug('rendering primary') - var props = this.props - - // notices - if (!props.noActiveNotices) { - log.debug('rendering notice screen for unread notices.') - return h(NoticeScreen, { - notice: props.lastUnreadNotice, - key: 'NoticeScreen', - onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)), - }) - } else if (props.lostAccounts && props.lostAccounts.length > 0) { - log.debug('rendering notice screen for lost accounts view.') - return h(NoticeScreen, { - notice: generateLostAccountsNotice(props.lostAccounts), - key: 'LostAccountsNotice', - onConfirm: () => props.dispatch(actions.markAccountsFound()), - }) - } - - if (props.seedWords) { - log.debug('rendering seed words') - return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'}) - } - - // show initialize screen - if (!props.isInitialized || props.forgottenPassword) { - // show current view - log.debug('rendering an initialize screen') - switch (props.currentView.name) { - - case 'restoreVault': - log.debug('rendering restore vault screen') - return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) - - default: - log.debug('rendering menu screen') - return h(InitializeMenuScreen, {key: 'menuScreenInit'}) - } - } - - // show unlock screen - if (!props.isUnlocked) { - switch (props.currentView.name) { - - case 'restoreVault': - log.debug('rendering restore vault screen') - return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) - - case 'config': - log.debug('rendering config screen from unlock screen.') - return h(ConfigScreen, {key: 'config'}) - - default: - log.debug('rendering locked screen') - return h(UnlockScreen, {key: 'locked'}) - } - } - - // show current view - switch (props.currentView.name) { - - case 'accountDetail': - log.debug('rendering account detail screen') - return h(AccountDetailScreen, {key: 'account-detail'}) - - case 'sendTransaction': - log.debug('rendering send tx screen') - return h(SendTransactionScreen, {key: 'send-transaction'}) - - case 'newKeychain': - log.debug('rendering new keychain screen') - return h(NewKeyChainScreen, {key: 'new-keychain'}) - - case 'confTx': - log.debug('rendering confirm tx screen') - return h(ConfirmTxScreen, {key: 'confirm-tx'}) - - case 'add-token': - log.debug('rendering add-token screen from unlock screen.') - return h(AddTokenScreen, {key: 'add-token'}) - - case 'config': - log.debug('rendering config screen') - return h(ConfigScreen, {key: 'config'}) - - case 'import-menu': - log.debug('rendering import screen') - return h(Import, {key: 'import-menu'}) - - case 'reveal-seed-conf': - log.debug('rendering reveal seed confirmation screen') - return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'}) - - case 'info': - log.debug('rendering info screen') - return h(InfoScreen, {key: 'info'}) - - case 'buyEth': - log.debug('rendering buy ether screen') - return h(BuyView, {key: 'buyEthView'}) - - case 'qr': - log.debug('rendering show qr screen') - return h('div', { - style: { - position: 'absolute', - height: '100%', - top: '0px', - left: '0px', - }, - }, [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { - onClick: () => props.dispatch(actions.backToAccountDetail(props.activeAddress)), - style: { - marginLeft: '10px', - marginTop: '50px', - }, - }), - h('div', { - style: { - position: 'absolute', - left: '44px', - width: '285px', - }, - }, [ - h(QrView, {key: 'qr'}), - ]), - ]) - - default: - log.debug('rendering default, account detail screen') - return h(AccountDetailScreen, {key: 'account-detail'}) - } -} - -App.prototype.toggleMetamaskActive = function () { - if (!this.props.isUnlocked) { - // currently inactive: redirect to password box - var passwordBox = document.querySelector('input[type=password]') - if (!passwordBox) return - passwordBox.focus() - } else { - // currently active: deactivate - this.props.dispatch(actions.lockMetamask(false)) - } -} - -App.prototype.renderCustomOption = function (provider) { - const { rpcTarget, type } = provider - if (type !== 'rpc') return null - - // Concatenate long URLs - let label = rpcTarget - if (rpcTarget.length > 31) { - label = label.substr(0, 34) + '...' - } - - switch (rpcTarget) { - - case 'http://localhost:8545': - return null - - default: - return h( - DropdownMenuItem, - { - key: rpcTarget, - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - }, - [ - h('i.fa.fa-question-circle.fa-lg.menu-icon'), - label, - h('.check', '✓'), - ] - ) - } -} - -App.prototype.getNetworkName = function () { - const { provider } = this.props - const providerName = provider.type - - let name - - if (providerName === 'mainnet') { - name = 'Main Ethereum Network' - } else if (providerName === 'ropsten') { - name = 'Ropsten Test Network' - } else if (providerName === 'kovan') { - name = 'Kovan Test Network' - } else if (providerName === 'rinkeby') { - name = 'Rinkeby Test Network' - } else { - name = 'Unknown Private Network' - } - - return name -} - -App.prototype.renderCommonRpc = function (rpcList, provider) { - const { rpcTarget } = provider - const props = this.props - - return rpcList.map((rpc) => { - if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { - return null - } else { - return h( - DropdownMenuItem, - { - key: rpc, - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setRpcTarget(rpc)), - }, - [ - h('i.fa.fa-question-circle.fa-lg.menu-icon'), - rpc, - h('.check', '✓'), - ] - ) - } - }) -} diff --git a/ui/responsive/app/components/account-dropdowns.js b/ui/responsive/app/components/account-dropdowns.js deleted file mode 100644 index d1d319477..000000000 --- a/ui/responsive/app/components/account-dropdowns.js +++ /dev/null @@ -1,227 +0,0 @@ -const Component = require('react').Component -const PropTypes = require('react').PropTypes -const h = require('react-hyperscript') -const actions = require('../actions') -const genAccountLink = require('../../lib/account-link.js') -const connect = require('react-redux').connect -const Dropdown = require('./dropdown').Dropdown -const DropdownMenuItem = require('./dropdown').DropdownMenuItem -const Identicon = require('./identicon') -const ethUtil = require('ethereumjs-util') -const copyToClipboard = require('copy-to-clipboard') - -class AccountDropdowns extends Component { - constructor (props) { - super(props) - this.state = { - accountSelectorActive: false, - optionsMenuActive: false, - } - } - - renderAccounts () { - const { identities, selected } = this.props - - return Object.keys(identities).map((key) => { - const identity = identities[key] - const isSelected = identity.address === selected - - return h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => { - this.props.actions.showAccountDetail(identity.address) - }, - }, - [ - h( - Identicon, - { - address: identity.address, - diameter: 16, - }, - ), - h('span', { style: { marginLeft: '10px' } }, identity.name || ''), - h('span', { style: { marginLeft: '10px' } }, isSelected ? h('.check', '✓') : null), - ] - ) - }) - } - - renderAccountSelector () { - const { actions } = this.props - const { accountSelectorActive } = this.state - - return h( - Dropdown, - { - style: { - marginLeft: '-125px', - minWidth: '180px', - }, - isOpen: accountSelectorActive, - onClickOutside: () => { this.setState({ accountSelectorActive: false }) }, - }, - [ - ...this.renderAccounts(), - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => actions.addNewAccount(), - }, - [ - h( - Identicon, - { - diameter: 16, - }, - ), - h('span', { style: { marginLeft: '10px' } }, 'Create Account'), - ], - ), - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => actions.showImportPage(), - }, - [ - h( - Identicon, - { - diameter: 16, - }, - ), - h('span', { style: { marginLeft: '10px' } }, 'Import Account'), - ] - ), - ] - ) - } - - renderAccountOptions () { - const { actions } = this.props - const { optionsMenuActive } = this.state - - return h( - Dropdown, - { - style: { - marginLeft: '-162px', - minWidth: '180px', - }, - isOpen: optionsMenuActive, - onClickOutside: () => { this.setState({ optionsMenuActive: false }) }, - }, - [ - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => actions.showConfigPage(), - }, - 'Account Settings', - ), - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => { - const { selected, network } = this.props - const url = genAccountLink(selected, network) - global.platform.openWindow({ url }) - }, - }, - 'View account on Etherscan', - ), - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => { - const { selected } = this.props - const checkSumAddress = selected && ethUtil.toChecksumAddress(selected) - copyToClipboard(checkSumAddress) - }, - }, - 'Copy Address to clipboard', - ), - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => { - actions.requestAccountExport() - }, - }, - 'Export Private Key', - ), - ] - ) - } - - render () { - const { style } = this.props - const { optionsMenuActive, accountSelectorActive } = this.state - - return h( - 'span', - { - style: style, - }, - [ - h( - 'i.fa.fa-angle-down', - { - style: {}, - onClick: (event) => { - event.stopPropagation() - this.setState({ - accountSelectorActive: !accountSelectorActive, - optionsMenuActive: false, - }) - }, - }, - this.renderAccountSelector(), - ), - h( - 'i.fa.fa-ellipsis-h', - { - style: { 'marginLeft': '10px'}, - onClick: (event) => { - event.stopPropagation() - this.setState({ - accountSelectorActive: false, - optionsMenuActive: !optionsMenuActive, - }) - }, - }, - this.renderAccountOptions() - ), - ] - ) - } -} - -AccountDropdowns.propTypes = { - identities: PropTypes.objectOf(PropTypes.object), - selected: PropTypes.string, -} - -const mapDispatchToProps = (dispatch) => { - return { - actions: { - showConfigPage: () => dispatch(actions.showConfigPage()), - requestAccountExport: () => dispatch(actions.requestExportAccount()), - showAccountDetail: (address) => dispatch(actions.showAccountDetail(address)), - addNewAccount: () => dispatch(actions.addNewAccount()), - showImportPage: () => dispatch(actions.showImportPage()), - }, - } -} - -module.exports = { - AccountDropdowns: connect(null, mapDispatchToProps)(AccountDropdowns), -} diff --git a/ui/responsive/app/components/account-export.js b/ui/responsive/app/components/account-export.js deleted file mode 100644 index 394d878f7..000000000 --- a/ui/responsive/app/components/account-export.js +++ /dev/null @@ -1,122 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const copyToClipboard = require('copy-to-clipboard') -const actions = require('../actions') -const ethUtil = require('ethereumjs-util') -const connect = require('react-redux').connect - -module.exports = connect(mapStateToProps)(ExportAccountView) - -inherits(ExportAccountView, Component) -function ExportAccountView () { - Component.call(this) -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -ExportAccountView.prototype.render = function () { - var state = this.props - var accountDetail = state.accountDetail - - if (!accountDetail) return h('div') - var accountExport = accountDetail.accountExport - - var notExporting = accountExport === 'none' - var exportRequested = accountExport === 'requested' - var accountExported = accountExport === 'completed' - - if (notExporting) return h('div') - - if (exportRequested) { - var warning = `Export private keys at your own risk.` - return ( - h('div', { - style: { - display: 'inline-block', - textAlign: 'center', - }, - }, - [ - h('div', { - key: 'exporting', - style: { - margin: '0 20px', - }, - }, [ - h('p.error', warning), - h('input#exportAccount.sizing-input', { - type: 'password', - placeholder: 'confirm password', - onKeyPress: this.onExportKeyPress.bind(this), - style: { - position: 'relative', - top: '1.5px', - marginBottom: '7px', - }, - }), - ]), - h('div', { - key: 'buttons', - style: { - margin: '0 20px', - }, - }, - [ - h('button', { - onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }), - style: { - marginRight: '10px', - }, - }, 'Submit'), - h('button', { - onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), - }, 'Cancel'), - ]), - (this.props.warning) && ( - h('span.error', { - style: { - margin: '20px', - }, - }, this.props.warning.split('-')) - ), - ]) - ) - } - - if (accountExported) { - return h('div.privateKey', { - style: { - margin: '0 20px', - }, - }, [ - h('label', 'Your private key (click to copy):'), - h('p.error.cursor-pointer', { - style: { - textOverflow: 'ellipsis', - overflow: 'hidden', - webkitUserSelect: 'text', - width: '100%', - }, - onClick: function (event) { - copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey)) - }, - }, ethUtil.stripHexPrefix(accountDetail.privateKey)), - h('button', { - onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), - }, 'Done'), - ]) - } -} - -ExportAccountView.prototype.onExportKeyPress = function (event) { - if (event.key !== 'Enter') return - event.preventDefault() - - var input = document.getElementById('exportAccount').value - this.props.dispatch(actions.exportAccount(input, this.props.address)) -} diff --git a/ui/responsive/app/components/account-panel.js b/ui/responsive/app/components/account-panel.js deleted file mode 100644 index abaaf8163..000000000 --- a/ui/responsive/app/components/account-panel.js +++ /dev/null @@ -1,86 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const Identicon = require('./identicon') -const formatBalance = require('../util').formatBalance -const addressSummary = require('../util').addressSummary - -module.exports = AccountPanel - - -inherits(AccountPanel, Component) -function AccountPanel () { - Component.call(this) -} - -AccountPanel.prototype.render = function () { - var state = this.props - var identity = state.identity || {} - var account = state.account || {} - var isFauceting = state.isFauceting - - var panelState = { - key: `accountPanel${identity.address}`, - identiconKey: identity.address, - identiconLabel: identity.name || '', - attributes: [ - { - key: 'ADDRESS', - value: addressSummary(identity.address), - }, - balanceOrFaucetingIndication(account, isFauceting), - ], - } - - return ( - - h('.identity-panel.flex-row.flex-space-between', { - style: { - flex: '1 0 auto', - cursor: panelState.onClick ? 'pointer' : undefined, - }, - onClick: panelState.onClick, - }, [ - - // account identicon - h('.identicon-wrapper.flex-column.select-none', [ - h(Identicon, { - address: panelState.identiconKey, - imageify: state.imageifyIdenticons, - }), - h('span.font-small', panelState.identiconLabel.substring(0, 7) + '...'), - ]), - - // account address, balance - h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [ - - panelState.attributes.map((attr) => { - return h('.flex-row.flex-space-between', { - key: '' + Math.round(Math.random() * 1000000), - }, [ - h('label.font-small.no-select', attr.key), - h('span.font-small', attr.value), - ]) - }), - ]), - - ]) - - ) -} - -function balanceOrFaucetingIndication (account, isFauceting) { - // Temporarily deactivating isFauceting indication - // because it shows fauceting for empty restored accounts. - if (/* isFauceting*/ false) { - return { - key: 'Account is auto-funding.', - value: 'Please wait.', - } - } else { - return { - key: 'BALANCE', - value: formatBalance(account.balance), - } - } -} diff --git a/ui/responsive/app/components/balance.js b/ui/responsive/app/components/balance.js deleted file mode 100644 index 57ca84564..000000000 --- a/ui/responsive/app/components/balance.js +++ /dev/null @@ -1,89 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const formatBalance = require('../util').formatBalance -const generateBalanceObject = require('../util').generateBalanceObject -const Tooltip = require('./tooltip.js') -const FiatValue = require('./fiat-value.js') - -module.exports = EthBalanceComponent - -inherits(EthBalanceComponent, Component) -function EthBalanceComponent () { - Component.call(this) -} - -EthBalanceComponent.prototype.render = function () { - var props = this.props - let { value } = props - var style = props.style - var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true - value = value ? formatBalance(value, 6, needsParse) : '...' - var width = props.width - - return ( - - h('.ether-balance.ether-balance-amount', { - style: style, - }, [ - h('div', { - style: { - display: 'inline', - width: width, - }, - }, this.renderBalance(value)), - ]) - - ) -} -EthBalanceComponent.prototype.renderBalance = function (value) { - var props = this.props - if (value === 'None') return value - if (value === '...') return value - var balanceObj = generateBalanceObject(value, props.shorten ? 1 : 3) - var balance - var splitBalance = value.split(' ') - var ethNumber = splitBalance[0] - var ethSuffix = splitBalance[1] - const showFiat = 'showFiat' in props ? props.showFiat : true - - if (props.shorten) { - balance = balanceObj.shortBalance - } else { - balance = balanceObj.balance - } - - var label = balanceObj.label - - return ( - h(Tooltip, { - position: 'bottom', - title: `${ethNumber} ${ethSuffix}`, - }, h('div.flex-column', [ - h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('div', { - style: { - width: '100%', - textAlign: 'right', - }, - }, this.props.incoming ? `+${balance}` : balance), - h('div', { - style: { - color: ' #AEAEAE', - fontSize: '12px', - marginLeft: '5px', - }, - }, label), - ]), - - showFiat ? h(FiatValue, { value: props.value }) : null, - ])) - ) -} diff --git a/ui/responsive/app/components/binary-renderer.js b/ui/responsive/app/components/binary-renderer.js deleted file mode 100644 index 0b6a1f5c2..000000000 --- a/ui/responsive/app/components/binary-renderer.js +++ /dev/null @@ -1,46 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ethUtil = require('ethereumjs-util') -const extend = require('xtend') - -module.exports = BinaryRenderer - -inherits(BinaryRenderer, Component) -function BinaryRenderer () { - Component.call(this) -} - -BinaryRenderer.prototype.render = function () { - const props = this.props - const { value, style } = props - const text = this.hexToText(value) - - const defaultStyle = extend({ - width: '315px', - maxHeight: '210px', - resize: 'none', - border: 'none', - background: 'white', - padding: '3px', - }, style) - - return ( - h('textarea.font-small', { - readOnly: true, - style: defaultStyle, - defaultValue: text, - }) - ) -} - -BinaryRenderer.prototype.hexToText = function (hex) { - try { - const stripped = ethUtil.stripHexPrefix(hex) - const buff = Buffer.from(stripped, 'hex') - return buff.toString('utf8') - } catch (e) { - return hex - } -} - diff --git a/ui/responsive/app/components/bn-as-decimal-input.js b/ui/responsive/app/components/bn-as-decimal-input.js deleted file mode 100644 index f3ace4720..000000000 --- a/ui/responsive/app/components/bn-as-decimal-input.js +++ /dev/null @@ -1,174 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const extend = require('xtend') - -module.exports = BnAsDecimalInput - -inherits(BnAsDecimalInput, Component) -function BnAsDecimalInput () { - this.state = { invalid: null } - Component.call(this) -} - -/* Bn as Decimal Input - * - * A component for allowing easy, decimal editing - * of a passed in bn string value. - * - * On change, calls back its `onChange` function parameter - * and passes it an updated bn string. - */ - -BnAsDecimalInput.prototype.render = function () { - const props = this.props - const state = this.state - - const { value, scale, precision, onChange, min, max } = props - - const suffix = props.suffix - const style = props.style - const valueString = value.toString(10) - const newValue = this.downsize(valueString, scale, precision) - - return ( - h('.flex-column', [ - h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('input.hex-input', { - type: 'number', - step: 'any', - required: true, - min, - max, - style: extend({ - display: 'block', - textAlign: 'right', - backgroundColor: 'transparent', - border: '1px solid #bdbdbd', - - }, style), - value: newValue, - onBlur: (event) => { - this.updateValidity(event) - }, - onChange: (event) => { - this.updateValidity(event) - const value = (event.target.value === '') ? '' : event.target.value - - - const scaledNumber = this.upsize(value, scale, precision) - const precisionBN = new BN(scaledNumber, 10) - onChange(precisionBN, event.target.checkValidity()) - }, - onInvalid: (event) => { - const msg = this.constructWarning() - if (msg === state.invalid) { - return - } - this.setState({ invalid: msg }) - event.preventDefault() - return false - }, - }), - h('div', { - style: { - color: ' #AEAEAE', - fontSize: '12px', - marginLeft: '5px', - marginRight: '6px', - width: '20px', - }, - }, suffix), - ]), - - state.invalid ? h('span.error', { - style: { - position: 'absolute', - right: '0px', - textAlign: 'right', - transform: 'translateY(26px)', - padding: '3px', - background: 'rgba(255,255,255,0.85)', - zIndex: '1', - textTransform: 'capitalize', - border: '2px solid #E20202', - }, - }, state.invalid) : null, - ]) - ) -} - -BnAsDecimalInput.prototype.setValid = function (message) { - this.setState({ invalid: null }) -} - -BnAsDecimalInput.prototype.updateValidity = function (event) { - const target = event.target - const value = this.props.value - const newValue = target.value - - if (value === newValue) { - return - } - - const valid = target.checkValidity() - - if (valid) { - this.setState({ invalid: null }) - } -} - -BnAsDecimalInput.prototype.constructWarning = function () { - const { name, min, max } = this.props - let message = name ? name + ' ' : '' - - if (min && max) { - message += `must be greater than or equal to ${min} and less than or equal to ${max}.` - } else if (min) { - message += `must be greater than or equal to ${min}.` - } else if (max) { - message += `must be less than or equal to ${max}.` - } else { - message += 'Invalid input.' - } - - return message -} - - -BnAsDecimalInput.prototype.downsize = function (number, scale, precision) { - // if there is no scaling, simply return the number - if (scale === 0) { - return Number(number) - } else { - // if the scale is the same as the precision, account for this edge case. - var decimals = (scale === precision) ? -1 : scale - precision - return Number(number.slice(0, -scale) + '.' + number.slice(-scale, decimals)) - } -} - -BnAsDecimalInput.prototype.upsize = function (number, scale, precision) { - var stringArray = number.toString().split('.') - var decimalLength = stringArray[1] ? stringArray[1].length : 0 - var newString = stringArray[0] - - // If there is scaling and decimal parts exist, integrate them in. - if ((scale !== 0) && (decimalLength !== 0)) { - newString += stringArray[1].slice(0, precision) - } - - // Add 0s to account for the upscaling. - for (var i = decimalLength; i < scale; i++) { - newString += '0' - } - return newString -} diff --git a/ui/responsive/app/components/buy-button-subview.js b/ui/responsive/app/components/buy-button-subview.js deleted file mode 100644 index 87084f92d..000000000 --- a/ui/responsive/app/components/buy-button-subview.js +++ /dev/null @@ -1,197 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const connect = require('react-redux').connect -const actions = require('../actions') -const CoinbaseForm = require('./coinbase-form') -const ShapeshiftForm = require('./shapeshift-form') -const Loading = require('./loading') -const AccountPanel = require('./account-panel') -const RadioList = require('./custom-radio-list') - -module.exports = connect(mapStateToProps)(BuyButtonSubview) - -function mapStateToProps (state) { - return { - identity: state.appState.identity, - account: state.metamask.accounts[state.appState.buyView.buyAddress], - warning: state.appState.warning, - buyView: state.appState.buyView, - network: state.metamask.network, - provider: state.metamask.provider, - context: state.appState.currentView.context, - isSubLoading: state.appState.isSubLoading, - } -} - -inherits(BuyButtonSubview, Component) -function BuyButtonSubview () { - Component.call(this) -} - -BuyButtonSubview.prototype.render = function () { - const props = this.props - const isLoading = props.isSubLoading - - return ( - h('.buy-eth-section.flex-column', { - style: { - alignItems: 'center', - }, - }, [ - // back button - h('.flex-row', { - style: { - alignItems: 'center', - justifyContent: 'center', - }, - }, [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { - onClick: this.backButtonContext.bind(this), - style: { - position: 'absolute', - left: '10px', - }, - }), - h('h2.text-transform-uppercase.flex-center', { - style: { - width: '100vw', - background: 'rgb(235, 235, 235)', - color: 'rgb(174, 174, 174)', - paddingTop: '4px', - paddingBottom: '4px', - }, - }, 'Buy Eth'), - ]), - h('div', { - style: { - position: 'absolute', - top: '57vh', - left: '49vw', - }, - }, [ - h(Loading, {isLoading}), - ]), - h('div', { - style: { - width: '80%', - }, - }, [ - h(AccountPanel, { - showFullAddress: true, - identity: props.identity, - account: props.account, - }), - ]), - h('h3.text-transform-uppercase', { - style: { - paddingLeft: '15px', - fontFamily: 'Montserrat Light', - width: '100vw', - background: 'rgb(235, 235, 235)', - color: 'rgb(174, 174, 174)', - paddingTop: '4px', - paddingBottom: '4px', - }, - }, 'Select Service'), - h('.flex-row.selected-exchange', { - style: { - position: 'relative', - right: '35px', - marginTop: '20px', - marginBottom: '20px', - }, - }, [ - h(RadioList, { - defaultFocus: props.buyView.subview, - labels: [ - 'Coinbase', - 'ShapeShift', - ], - subtext: { - 'Coinbase': 'Crypto/FIAT (USA only)', - 'ShapeShift': 'Crypto', - }, - onClick: this.radioHandler.bind(this), - }), - ]), - h('h3.text-transform-uppercase', { - style: { - paddingLeft: '15px', - fontFamily: 'Montserrat Light', - width: '100vw', - background: 'rgb(235, 235, 235)', - color: 'rgb(174, 174, 174)', - paddingTop: '4px', - paddingBottom: '4px', - }, - }, props.buyView.subview), - this.formVersionSubview(), - ]) - ) -} - -BuyButtonSubview.prototype.formVersionSubview = function () { - const network = this.props.network - if (network === '1') { - if (this.props.buyView.formView.coinbase) { - return h(CoinbaseForm, this.props) - } else if (this.props.buyView.formView.shapeshift) { - return h(ShapeshiftForm, this.props) - } - } else { - return h('div.flex-column', { - style: { - alignItems: 'center', - margin: '50px', - }, - }, [ - h('h3.text-transform-uppercase', { - style: { - width: '225px', - marginBottom: '15px', - }, - }, 'In order to access this feature, please switch to the Main Network'), - ((network === '3') || (network === '4') || (network === '42')) ? h('h3.text-transform-uppercase', 'or go to the') : null, - (network === '3') ? h('button.text-transform-uppercase', { - onClick: () => this.props.dispatch(actions.buyEth({ network })), - style: { - marginTop: '15px', - }, - }, 'Ropsten Test Faucet') : null, - (network === '4') ? h('button.text-transform-uppercase', { - onClick: () => this.props.dispatch(actions.buyEth({ network })), - style: { - marginTop: '15px', - }, - }, 'Rinkeby Test Faucet') : null, - (network === '42') ? h('button.text-transform-uppercase', { - onClick: () => this.props.dispatch(actions.buyEth({ network })), - style: { - marginTop: '15px', - }, - }, 'Kovan Test Faucet') : null, - ]) - } -} - -BuyButtonSubview.prototype.navigateTo = function (url) { - global.platform.openWindow({ url }) -} - -BuyButtonSubview.prototype.backButtonContext = function () { - if (this.props.context === 'confTx') { - this.props.dispatch(actions.showConfTxPage(false)) - } else { - this.props.dispatch(actions.goHome()) - } -} - -BuyButtonSubview.prototype.radioHandler = function (event) { - switch (event.target.title) { - case 'Coinbase': - return this.props.dispatch(actions.coinBaseSubview()) - case 'ShapeShift': - return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type)) - } -} diff --git a/ui/responsive/app/components/coinbase-form.js b/ui/responsive/app/components/coinbase-form.js deleted file mode 100644 index f44d86045..000000000 --- a/ui/responsive/app/components/coinbase-form.js +++ /dev/null @@ -1,63 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const connect = require('react-redux').connect -const actions = require('../actions') - -module.exports = connect(mapStateToProps)(CoinbaseForm) - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -inherits(CoinbaseForm, Component) - -function CoinbaseForm () { - Component.call(this) -} - -CoinbaseForm.prototype.render = function () { - var props = this.props - - return h('.flex-column', { - style: { - marginTop: '35px', - padding: '25px', - width: '100%', - }, - }, [ - h('.flex-row', { - style: { - justifyContent: 'space-around', - margin: '33px', - marginTop: '0px', - }, - }, [ - h('button.btn-green', { - onClick: this.toCoinbase.bind(this), - }, 'Continue to Coinbase'), - - h('button.btn-red', { - onClick: () => props.dispatch(actions.backTobuyView(props.accounts.address)), - }, 'Cancel'), - ]), - ]) -} - -CoinbaseForm.prototype.toCoinbase = function () { - const props = this.props - const address = props.buyView.buyAddress - props.dispatch(actions.buyEth({ network: '1', address, amount: 0 })) -} - -CoinbaseForm.prototype.renderLoading = function () { - return h('img', { - style: { - width: '27px', - marginRight: '-27px', - }, - src: 'images/loading.svg', - }) -} diff --git a/ui/responsive/app/components/copyButton.js b/ui/responsive/app/components/copyButton.js deleted file mode 100644 index a25d0719c..000000000 --- a/ui/responsive/app/components/copyButton.js +++ /dev/null @@ -1,59 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const copyToClipboard = require('copy-to-clipboard') - -const Tooltip = require('./tooltip') - -module.exports = CopyButton - -inherits(CopyButton, Component) -function CopyButton () { - Component.call(this) -} - -// As parameters, accepts: -// "value", which is the value to copy (mandatory) -// "title", which is the text to show on hover (optional, defaults to 'Copy') -CopyButton.prototype.render = function () { - const props = this.props - const state = this.state || {} - - const value = props.value - const copied = state.copied - - const message = copied ? 'Copied' : props.title || ' Copy ' - - return h('.copy-button', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - - h(Tooltip, { - title: message, - }, [ - h('i.fa.fa-clipboard.cursor-pointer.color-orange', { - style: { - margin: '5px', - }, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - copyToClipboard(value) - this.debounceRestore() - }, - }), - ]), - - ]) -} - -CopyButton.prototype.debounceRestore = function () { - this.setState({ copied: true }) - clearTimeout(this.timeout) - this.timeout = setTimeout(() => { - this.setState({ copied: false }) - }, 850) -} diff --git a/ui/responsive/app/components/copyable.js b/ui/responsive/app/components/copyable.js deleted file mode 100644 index a4f6f4bc6..000000000 --- a/ui/responsive/app/components/copyable.js +++ /dev/null @@ -1,46 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const Tooltip = require('./tooltip') -const copyToClipboard = require('copy-to-clipboard') - -module.exports = Copyable - -inherits(Copyable, Component) -function Copyable () { - Component.call(this) - this.state = { - copied: false, - } -} - -Copyable.prototype.render = function () { - const props = this.props - const state = this.state - const { value, children } = props - const { copied } = state - - return h(Tooltip, { - title: copied ? 'Copied!' : 'Copy', - position: 'bottom', - }, h('span', { - style: { - cursor: 'pointer', - }, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - copyToClipboard(value) - this.debounceRestore() - }, - }, children)) -} - -Copyable.prototype.debounceRestore = function () { - this.setState({ copied: true }) - clearTimeout(this.timeout) - this.timeout = setTimeout(() => { - this.setState({ copied: false }) - }, 850) -} diff --git a/ui/responsive/app/components/custom-radio-list.js b/ui/responsive/app/components/custom-radio-list.js deleted file mode 100644 index a4c525396..000000000 --- a/ui/responsive/app/components/custom-radio-list.js +++ /dev/null @@ -1,60 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = RadioList - -inherits(RadioList, Component) -function RadioList () { - Component.call(this) -} - -RadioList.prototype.render = function () { - const props = this.props - const activeClass = '.custom-radio-selected' - const inactiveClass = '.custom-radio-inactive' - const { - labels, - defaultFocus, - } = props - - - return ( - h('.flex-row', { - style: { - fontSize: '12px', - }, - }, [ - h('.flex-column.custom-radios', { - style: { - marginRight: '5px', - }, - }, - labels.map((lable, i) => { - let isSelcted = (this.state !== null) - isSelcted = isSelcted ? (this.state.selected === lable) : (defaultFocus === lable) - return h(isSelcted ? activeClass : inactiveClass, { - title: lable, - onClick: (event) => { - this.setState({selected: event.target.title}) - props.onClick(event) - }, - }) - }) - ), - h('.text', {}, - labels.map((lable) => { - if (props.subtext) { - return h('.flex-row', {}, [ - h('.radio-titles', lable), - h('.radio-titles-subtext', `- ${props.subtext[lable]}`), - ]) - } else { - return h('.radio-titles', lable) - } - }) - ), - ]) - ) -} - diff --git a/ui/responsive/app/components/dropdown.js b/ui/responsive/app/components/dropdown.js deleted file mode 100644 index e77b4c40c..000000000 --- a/ui/responsive/app/components/dropdown.js +++ /dev/null @@ -1,89 +0,0 @@ -const Component = require('react').Component -const PropTypes = require('react').PropTypes -const h = require('react-hyperscript') -const MenuDroppo = require('menu-droppo') - -const noop = () => {} - -class Dropdown extends Component { - render () { - const { isOpen, onClickOutside, style, children } = this.props - - return h( - MenuDroppo, - { - isOpen, - zIndex: 11, - onClickOutside, - style, - innerStyle: { - borderRadius: '4px', - padding: '8px 16px', - background: 'rgba(0, 0, 0, 0.8)', - boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', - }, - }, - [ - h( - 'style', - ` - li.dropdown-menu-item:hover { color:rgb(225, 225, 225); } - li.dropdown-menu-item { color: rgb(185, 185, 185); } - ` - ), - ...children, - ] - ) - } -} - -Dropdown.defaultProps = { - isOpen: false, - onClick: noop, -} - -Dropdown.propTypes = { - isOpen: PropTypes.bool.isRequired, - onClick: PropTypes.func.isRequired, - children: PropTypes.node, - style: PropTypes.object.isRequired, -} - -class DropdownMenuItem extends Component { - render () { - const { onClick, closeMenu, children } = this.props - - return h( - 'li.dropdown-menu-item', - { - onClick: () => { - onClick() - closeMenu() - }, - style: { - listStyle: 'none', - padding: '8px 0px 8px 0px', - fontSize: '12px', - fontStyle: 'normal', - fontFamily: 'Montserrat Regular', - cursor: 'pointer', - display: 'flex', - justifyContent: 'flex-start', - alignItems: 'center', - }, - }, - children - ) - } -} - -DropdownMenuItem.propTypes = { - closeMenu: PropTypes.func.isRequired, - onClick: PropTypes.func.isRequired, - children: PropTypes.node, -} - -module.exports = { - Dropdown, - DropdownMenuItem, -} diff --git a/ui/responsive/app/components/editable-label.js b/ui/responsive/app/components/editable-label.js deleted file mode 100644 index 167be7eaf..000000000 --- a/ui/responsive/app/components/editable-label.js +++ /dev/null @@ -1,56 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const findDOMNode = require('react-dom').findDOMNode - -module.exports = EditableLabel - -inherits(EditableLabel, Component) -function EditableLabel () { - Component.call(this) -} - -EditableLabel.prototype.render = function () { - const props = this.props - const state = this.state - - if (state && state.isEditingLabel) { - return h('div.editable-label', [ - h('input.sizing-input', { - defaultValue: props.textValue, - maxLength: '20', - onKeyPress: (event) => { - this.saveIfEnter(event) - }, - }), - h('button.editable-button', { - onClick: () => this.saveText(), - }, 'Save'), - ]) - } else { - return h('div.name-label', { - onClick: (event) => { - const nameAttribute = event.target.getAttribute('name') - // checks for class to handle smaller CTA above the account name - const classAttribute = event.target.getAttribute('class') - if (nameAttribute === 'edit' || classAttribute === 'edit-text') { - this.setState({ isEditingLabel: true }) - } - }, - }, this.props.children) - } -} - -EditableLabel.prototype.saveIfEnter = function (event) { - if (event.key === 'Enter') { - this.saveText() - } -} - -EditableLabel.prototype.saveText = function () { - var container = findDOMNode(this) - var text = container.querySelector('.editable-label input').value - var truncatedText = text.substring(0, 20) - this.props.saveText(truncatedText) - this.setState({ isEditingLabel: false, textLabel: truncatedText }) -} diff --git a/ui/responsive/app/components/ens-input.js b/ui/responsive/app/components/ens-input.js deleted file mode 100644 index 3a33ebf74..000000000 --- a/ui/responsive/app/components/ens-input.js +++ /dev/null @@ -1,170 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const extend = require('xtend') -const debounce = require('debounce') -const copyToClipboard = require('copy-to-clipboard') -const ENS = require('ethjs-ens') -const networkMap = require('ethjs-ens/lib/network-map.json') -const ensRE = /.+\.eth$/ -const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' - - -module.exports = EnsInput - -inherits(EnsInput, Component) -function EnsInput () { - Component.call(this) -} - -EnsInput.prototype.render = function () { - const props = this.props - const opts = extend(props, { - list: 'addresses', - onChange: () => { - const network = this.props.network - const networkHasEnsSupport = getNetworkEnsSupport(network) - if (!networkHasEnsSupport) return - - const recipient = document.querySelector('input[name="address"]').value - if (recipient.match(ensRE) === null) { - return this.setState({ - loadingEns: false, - ensResolution: null, - ensFailure: null, - }) - } - - this.setState({ - loadingEns: true, - }) - this.checkName() - }, - }) - return h('div', { - style: { width: '100%' }, - }, [ - h('input.large-input', opts), - // The address book functionality. - h('datalist#addresses', - [ - // Corresponds to the addresses owned. - Object.keys(props.identities).map((key) => { - const identity = props.identities[key] - return h('option', { - value: identity.address, - label: identity.name, - key: identity.address, - }) - }), - // Corresponds to previously sent-to addresses. - props.addressBook.map((identity) => { - return h('option', { - value: identity.address, - label: identity.name, - key: identity.address, - }) - }), - ]), - this.ensIcon(), - ]) -} - -EnsInput.prototype.componentDidMount = function () { - const network = this.props.network - const networkHasEnsSupport = getNetworkEnsSupport(network) - this.setState({ ensResolution: ZERO_ADDRESS }) - - if (networkHasEnsSupport) { - const provider = global.ethereumProvider - this.ens = new ENS({ provider, network }) - this.checkName = debounce(this.lookupEnsName.bind(this), 200) - } -} - -EnsInput.prototype.lookupEnsName = function () { - const recipient = document.querySelector('input[name="address"]').value - const { ensResolution } = this.state - - log.info(`ENS attempting to resolve name: ${recipient}`) - this.ens.lookup(recipient.trim()) - .then((address) => { - if (address === ZERO_ADDRESS) throw new Error('No address has been set for this name.') - if (address !== ensResolution) { - this.setState({ - loadingEns: false, - ensResolution: address, - nickname: recipient.trim(), - hoverText: address + '\nClick to Copy', - ensFailure: false, - }) - } - }) - .catch((reason) => { - log.error(reason) - return this.setState({ - loadingEns: false, - ensResolution: ZERO_ADDRESS, - ensFailure: true, - hoverText: reason.message, - }) - }) -} - -EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) { - const state = this.state || {} - const ensResolution = state.ensResolution - // If an address is sent without a nickname, meaning not from ENS or from - // the user's own accounts, a default of a one-space string is used. - const nickname = state.nickname || ' ' - if (prevState && ensResolution && this.props.onChange && - ensResolution !== prevState.ensResolution) { - this.props.onChange(ensResolution, nickname) - } -} - -EnsInput.prototype.ensIcon = function (recipient) { - const { hoverText } = this.state || {} - return h('span', { - title: hoverText, - style: { - position: 'absolute', - padding: '9px', - transform: 'translatex(-40px)', - }, - }, this.ensIconContents(recipient)) -} - -EnsInput.prototype.ensIconContents = function (recipient) { - const { loadingEns, ensFailure, ensResolution } = this.state || { ensResolution: ZERO_ADDRESS} - - if (loadingEns) { - return h('img', { - src: 'images/loading.svg', - style: { - width: '30px', - height: '30px', - transform: 'translateY(-6px)', - }, - }) - } - - if (ensFailure) { - return h('i.fa.fa-warning.fa-lg.warning') - } - - if (ensResolution && (ensResolution !== ZERO_ADDRESS)) { - return h('i.fa.fa-check-circle.fa-lg.cursor-pointer', { - style: { color: 'green' }, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - copyToClipboard(ensResolution) - }, - }) - } -} - -function getNetworkEnsSupport (network) { - return Boolean(networkMap[network]) -} diff --git a/ui/responsive/app/components/eth-balance.js b/ui/responsive/app/components/eth-balance.js deleted file mode 100644 index 4f538fd31..000000000 --- a/ui/responsive/app/components/eth-balance.js +++ /dev/null @@ -1,89 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const formatBalance = require('../util').formatBalance -const generateBalanceObject = require('../util').generateBalanceObject -const Tooltip = require('./tooltip.js') -const FiatValue = require('./fiat-value.js') - -module.exports = EthBalanceComponent - -inherits(EthBalanceComponent, Component) -function EthBalanceComponent () { - Component.call(this) -} - -EthBalanceComponent.prototype.render = function () { - var props = this.props - let { value } = props - const { style, width } = props - var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true - value = value ? formatBalance(value, 6, needsParse) : '...' - - return ( - - h('.ether-balance.ether-balance-amount', { - style, - }, [ - h('div', { - style: { - display: 'inline', - width, - }, - }, this.renderBalance(value)), - ]) - - ) -} -EthBalanceComponent.prototype.renderBalance = function (value) { - var props = this.props - const { conversionRate, shorten, incoming, currentCurrency } = props - if (value === 'None') return value - if (value === '...') return value - var balanceObj = generateBalanceObject(value, shorten ? 1 : 3) - var balance - var splitBalance = value.split(' ') - var ethNumber = splitBalance[0] - var ethSuffix = splitBalance[1] - const showFiat = 'showFiat' in props ? props.showFiat : true - - if (shorten) { - balance = balanceObj.shortBalance - } else { - balance = balanceObj.balance - } - - var label = balanceObj.label - - return ( - h(Tooltip, { - position: 'bottom', - title: `${ethNumber} ${ethSuffix}`, - }, h('div.flex-column', [ - h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('div', { - style: { - width: '100%', - textAlign: 'right', - }, - }, incoming ? `+${balance}` : balance), - h('div', { - style: { - color: ' #AEAEAE', - fontSize: '12px', - marginLeft: '5px', - }, - }, label), - ]), - - showFiat ? h(FiatValue, { value: props.value, conversionRate, currentCurrency }) : null, - ])) - ) -} diff --git a/ui/responsive/app/components/fiat-value.js b/ui/responsive/app/components/fiat-value.js deleted file mode 100644 index 8a64a1cfc..000000000 --- a/ui/responsive/app/components/fiat-value.js +++ /dev/null @@ -1,63 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const formatBalance = require('../util').formatBalance - -module.exports = FiatValue - -inherits(FiatValue, Component) -function FiatValue () { - Component.call(this) -} - -FiatValue.prototype.render = function () { - const props = this.props - const { conversionRate, currentCurrency } = props - - const value = formatBalance(props.value, 6) - - if (value === 'None') return value - var fiatDisplayNumber, fiatTooltipNumber - var splitBalance = value.split(' ') - - if (conversionRate !== 0) { - fiatTooltipNumber = Number(splitBalance[0]) * conversionRate - fiatDisplayNumber = fiatTooltipNumber.toFixed(2) - } else { - fiatDisplayNumber = 'N/A' - fiatTooltipNumber = 'Unknown' - } - - return fiatDisplay(fiatDisplayNumber, currentCurrency) -} - -function fiatDisplay (fiatDisplayNumber, fiatSuffix) { - if (fiatDisplayNumber !== 'N/A') { - return h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('div', { - style: { - width: '100%', - textAlign: 'right', - fontSize: '12px', - color: '#333333', - }, - }, fiatDisplayNumber), - h('div', { - style: { - color: '#AEAEAE', - marginLeft: '5px', - fontSize: '12px', - }, - }, fiatSuffix), - ]) - } else { - return h('div') - } -} diff --git a/ui/responsive/app/components/hex-as-decimal-input.js b/ui/responsive/app/components/hex-as-decimal-input.js deleted file mode 100644 index 4a71e9585..000000000 --- a/ui/responsive/app/components/hex-as-decimal-input.js +++ /dev/null @@ -1,154 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const extend = require('xtend') - -module.exports = HexAsDecimalInput - -inherits(HexAsDecimalInput, Component) -function HexAsDecimalInput () { - this.state = { invalid: null } - Component.call(this) -} - -/* Hex as Decimal Input - * - * A component for allowing easy, decimal editing - * of a passed in hex string value. - * - * On change, calls back its `onChange` function parameter - * and passes it an updated hex string. - */ - -HexAsDecimalInput.prototype.render = function () { - const props = this.props - const state = this.state - - const { value, onChange, min, max } = props - - const toEth = props.toEth - const suffix = props.suffix - const decimalValue = decimalize(value, toEth) - const style = props.style - - return ( - h('.flex-column', [ - h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('input.hex-input', { - type: 'number', - required: true, - min: min, - max: max, - style: extend({ - display: 'block', - textAlign: 'right', - backgroundColor: 'transparent', - border: '1px solid #bdbdbd', - - }, style), - value: parseInt(decimalValue), - onBlur: (event) => { - this.updateValidity(event) - }, - onChange: (event) => { - this.updateValidity(event) - const hexString = (event.target.value === '') ? '' : hexify(event.target.value) - onChange(hexString) - }, - onInvalid: (event) => { - const msg = this.constructWarning() - if (msg === state.invalid) { - return - } - this.setState({ invalid: msg }) - event.preventDefault() - return false - }, - }), - h('div', { - style: { - color: ' #AEAEAE', - fontSize: '12px', - marginLeft: '5px', - marginRight: '6px', - width: '20px', - }, - }, suffix), - ]), - - state.invalid ? h('span.error', { - style: { - position: 'absolute', - right: '0px', - textAlign: 'right', - transform: 'translateY(26px)', - padding: '3px', - background: 'rgba(255,255,255,0.85)', - zIndex: '1', - textTransform: 'capitalize', - border: '2px solid #E20202', - }, - }, state.invalid) : null, - ]) - ) -} - -HexAsDecimalInput.prototype.setValid = function (message) { - this.setState({ invalid: null }) -} - -HexAsDecimalInput.prototype.updateValidity = function (event) { - const target = event.target - const value = this.props.value - const newValue = target.value - - if (value === newValue) { - return - } - - const valid = target.checkValidity() - if (valid) { - this.setState({ invalid: null }) - } -} - -HexAsDecimalInput.prototype.constructWarning = function () { - const { name, min, max } = this.props - let message = name ? name + ' ' : '' - - if (min && max) { - message += `must be greater than or equal to ${min} and less than or equal to ${max}.` - } else if (min) { - message += `must be greater than or equal to ${min}.` - } else if (max) { - message += `must be less than or equal to ${max}.` - } else { - message += 'Invalid input.' - } - - return message -} - -function hexify (decimalString) { - const hexBN = new BN(parseInt(decimalString), 10) - return '0x' + hexBN.toString('hex') -} - -function decimalize (input, toEth) { - if (input === '') { - return '' - } else { - const strippedInput = ethUtil.stripHexPrefix(input) - const inputBN = new BN(strippedInput, 'hex') - return inputBN.toString(10) - } -} diff --git a/ui/responsive/app/components/identicon.js b/ui/responsive/app/components/identicon.js deleted file mode 100644 index c754bc6ba..000000000 --- a/ui/responsive/app/components/identicon.js +++ /dev/null @@ -1,72 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const isNode = require('detect-node') -const findDOMNode = require('react-dom').findDOMNode -const jazzicon = require('jazzicon') -const iconFactoryGen = require('../../lib/icon-factory') -const iconFactory = iconFactoryGen(jazzicon) - -module.exports = IdenticonComponent - -inherits(IdenticonComponent, Component) -function IdenticonComponent () { - Component.call(this) - - this.defaultDiameter = 46 -} - -IdenticonComponent.prototype.render = function () { - var props = this.props - var diameter = props.diameter || this.defaultDiameter - return ( - h('div', { - key: 'identicon-' + this.props.address, - style: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - height: diameter, - width: diameter, - borderRadius: diameter / 2, - overflow: 'hidden', - }, - }) - ) -} - -IdenticonComponent.prototype.componentDidMount = function () { - var props = this.props - const { address } = props - - if (!address) return - - var container = findDOMNode(this) - - var diameter = props.diameter || this.defaultDiameter - if (!isNode) { - var img = iconFactory.iconForAddress(address, diameter) - container.appendChild(img) - } -} - -IdenticonComponent.prototype.componentDidUpdate = function () { - var props = this.props - const { address } = props - - if (!address) return - - var container = findDOMNode(this) - - var children = container.children - for (var i = 0; i < children.length; i++) { - container.removeChild(children[i]) - } - - var diameter = props.diameter || this.defaultDiameter - if (!isNode) { - var img = iconFactory.iconForAddress(address, diameter) - container.appendChild(img) - } -} - diff --git a/ui/responsive/app/components/loading.js b/ui/responsive/app/components/loading.js deleted file mode 100644 index 87d6f5d20..000000000 --- a/ui/responsive/app/components/loading.js +++ /dev/null @@ -1,53 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') - - -inherits(LoadingIndicator, Component) -module.exports = LoadingIndicator - -function LoadingIndicator () { - Component.call(this) -} - -LoadingIndicator.prototype.render = function () { - const { isLoading, loadingMessage } = this.props - - return ( - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'loader', - transitionEnterTimeout: 150, - transitionLeaveTimeout: 150, - }, [ - - isLoading ? h('div', { - style: { - zIndex: 10, - position: 'absolute', - flexDirection: 'column', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - height: '100%', - width: '100%', - background: 'rgba(255, 255, 255, 0.8)', - }, - }, [ - h('img', { - src: 'images/loading.svg', - }), - - h('br'), - - showMessageIfAny(loadingMessage), - ]) : null, - ]) - ) -} - -function showMessageIfAny (loadingMessage) { - if (!loadingMessage) return null - return h('span', loadingMessage) -} diff --git a/ui/responsive/app/components/mascot.js b/ui/responsive/app/components/mascot.js deleted file mode 100644 index 973ec2cad..000000000 --- a/ui/responsive/app/components/mascot.js +++ /dev/null @@ -1,59 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const metamaskLogo = require('metamask-logo') -const debounce = require('debounce') - -module.exports = Mascot - -inherits(Mascot, Component) -function Mascot () { - Component.call(this) - this.logo = metamaskLogo({ - followMouse: true, - pxNotRatio: true, - width: 200, - height: 200, - }) - - this.refollowMouse = debounce(this.logo.setFollowMouse.bind(this.logo, true), 1000) - this.unfollowMouse = this.logo.setFollowMouse.bind(this.logo, false) -} - -Mascot.prototype.render = function () { - // this is a bit hacky - // the event emitter is on `this.props` - // and we dont get that until render - this.handleAnimationEvents() - - return h('#metamask-mascot-container', { - style: { zIndex: 0 }, - }) -} - -Mascot.prototype.componentDidMount = function () { - var targetDivId = 'metamask-mascot-container' - var container = document.getElementById(targetDivId) - container.appendChild(this.logo.container) -} - -Mascot.prototype.componentWillUnmount = function () { - this.animations = this.props.animationEventEmitter - this.animations.removeAllListeners() - this.logo.container.remove() - this.logo.stopAnimation() -} - -Mascot.prototype.handleAnimationEvents = function () { - // only setup listeners once - if (this.animations) return - this.animations = this.props.animationEventEmitter - this.animations.on('point', this.lookAt.bind(this)) - this.animations.on('setFollowMouse', this.logo.setFollowMouse.bind(this.logo)) -} - -Mascot.prototype.lookAt = function (target) { - this.unfollowMouse() - this.logo.lookAt(target) - this.refollowMouse() -} diff --git a/ui/responsive/app/components/mini-account-panel.js b/ui/responsive/app/components/mini-account-panel.js deleted file mode 100644 index c09cf5b7a..000000000 --- a/ui/responsive/app/components/mini-account-panel.js +++ /dev/null @@ -1,74 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const Identicon = require('./identicon') - -module.exports = AccountPanel - - -inherits(AccountPanel, Component) -function AccountPanel () { - Component.call(this) -} - -AccountPanel.prototype.render = function () { - var props = this.props - var picOrder = props.picOrder || 'left' - const { imageSeed } = props - - return ( - - h('.identity-panel.flex-row.flex-left', { - style: { - cursor: props.onClick ? 'pointer' : undefined, - }, - onClick: props.onClick, - }, [ - - this.genIcon(imageSeed, picOrder), - - h('div.flex-column.flex-justify-center', { - style: { - lineHeight: '15px', - order: 2, - display: 'flex', - alignItems: picOrder === 'left' ? 'flex-begin' : 'flex-end', - }, - }, this.props.children), - ]) - ) -} - -AccountPanel.prototype.genIcon = function (seed, picOrder) { - const props = this.props - - // When there is no seed value, this is a contract creation. - // We then show the contract icon. - if (!seed) { - return h('.identicon-wrapper.flex-column.select-none', { - style: { - order: picOrder === 'left' ? 1 : 3, - }, - }, [ - h('i.fa.fa-file-text-o.fa-lg', { - style: { - fontSize: '42px', - transform: 'translate(0px, -16px)', - }, - }), - ]) - } - - // If there was a seed, we return an identicon for that address. - return h('.identicon-wrapper.flex-column.select-none', { - style: { - order: picOrder === 'left' ? 1 : 3, - }, - }, [ - h(Identicon, { - address: seed, - imageify: props.imageifyIdenticons, - }), - ]) -} - diff --git a/ui/responsive/app/components/network.js b/ui/responsive/app/components/network.js deleted file mode 100644 index 698a0bbb9..000000000 --- a/ui/responsive/app/components/network.js +++ /dev/null @@ -1,124 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = Network - -inherits(Network, Component) - -function Network () { - Component.call(this) -} - -Network.prototype.render = function () { - const props = this.props - const networkNumber = props.network - let providerName - try { - providerName = props.provider.type - } catch (e) { - providerName = null - } - let iconName, hoverText - - if (networkNumber === 'loading') { - return h('span', { - style: { - display: 'flex', - alignItems: 'center', - flexDirection: 'row', - }, - onClick: (event) => this.props.onClick(event), - }, [ - h('img', { - title: 'Attempting to connect to blockchain.', - style: { - width: '27px', - }, - src: 'images/loading.svg', - }), - h('i.fa.fa-sort-desc'), - ]) - } else if (providerName === 'mainnet') { - hoverText = 'Main Ethereum Network' - iconName = 'ethereum-network' - } else if (providerName === 'ropsten') { - hoverText = 'Ropsten Test Network' - iconName = 'ropsten-test-network' - } else if (parseInt(networkNumber) === 3) { - hoverText = 'Ropsten Test Network' - iconName = 'ropsten-test-network' - } else if (providerName === 'kovan') { - hoverText = 'Kovan Test Network' - iconName = 'kovan-test-network' - } else if (providerName === 'rinkeby') { - hoverText = 'Rinkeby Test Network' - iconName = 'rinkeby-test-network' - } else { - hoverText = 'Unknown Private Network' - iconName = 'unknown-private-network' - } - - return ( - h('#network_component.pointer', { - title: hoverText, - onClick: (event) => this.props.onClick(event), - }, [ - (function () { - switch (iconName) { - case 'ethereum-network': - return h('.network-indicator', [ - h('.menu-icon.diamond'), - h('.network-name', { - style: { - color: '#039396', - }}, - 'Ethereum Main Net'), - ]) - case 'ropsten-test-network': - return h('.network-indicator', [ - h('.menu-icon.red-dot'), - h('.network-name', { - style: { - color: '#ff6666', - }}, - 'Ropsten Test Net'), - ]) - case 'kovan-test-network': - return h('.network-indicator', [ - h('.menu-icon.hollow-diamond'), - h('.network-name', { - style: { - color: '#690496', - }}, - 'Kovan Test Net'), - ]) - case 'rinkeby-test-network': - return h('.network-indicator', [ - h('.menu-icon.golden-square'), - h('.network-name', { - style: { - color: '#e7a218', - }}, - 'Rinkeby Test Net'), - ]) - default: - return h('.network-indicator', [ - h('i.fa.fa-question-circle.fa-lg', { - style: { - margin: '10px', - color: 'rgb(125, 128, 130)', - }, - }), - - h('.network-name', { - style: { - color: '#AEAEAE', - }}, - 'Private Network'), - ]) - } - })(), - ]) - ) -} diff --git a/ui/responsive/app/components/notice.js b/ui/responsive/app/components/notice.js deleted file mode 100644 index d9f0067cd..000000000 --- a/ui/responsive/app/components/notice.js +++ /dev/null @@ -1,126 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const ReactMarkdown = require('react-markdown') -const linker = require('extension-link-enabler') -const findDOMNode = require('react-dom').findDOMNode - -module.exports = Notice - -inherits(Notice, Component) -function Notice () { - Component.call(this) -} - -Notice.prototype.render = function () { - const { notice, onConfirm } = this.props - const { title, date, body } = notice - const state = this.state || { disclaimerDisabled: true } - const disabled = state.disclaimerDisabled - - return ( - h('.flex-column.flex-center.flex-grow', [ - h('h3.flex-center.text-transform-uppercase.terms-header', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - width: '100%', - fontSize: '20px', - textAlign: 'center', - padding: 6, - }, - }, [ - title, - ]), - - h('h5.flex-center.text-transform-uppercase.terms-header', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginBottom: 24, - width: '100%', - fontSize: '20px', - textAlign: 'center', - padding: 6, - }, - }, [ - date, - ]), - - h('style', ` - - .markdown { - overflow-x: hidden; - } - - .markdown h1, .markdown h2, .markdown h3 { - margin: 10px 0; - font-weight: bold; - } - - .markdown strong { - font-weight: bold; - } - .markdown em { - font-style: italic; - } - - .markdown p { - margin: 10px 0; - } - - .markdown a { - color: #df6b0e; - } - - `), - - h('div.markdown', { - onScroll: (e) => { - var object = e.currentTarget - if (object.offsetHeight + object.scrollTop + 100 >= object.scrollHeight) { - this.setState({disclaimerDisabled: false}) - } - }, - style: { - background: 'rgb(235, 235, 235)', - height: '310px', - padding: '6px', - width: '90%', - overflowY: 'scroll', - scroll: 'auto', - }, - }, [ - h(ReactMarkdown, { - className: 'notice-box', - source: body, - skipHtml: true, - }), - ]), - - h('button', { - disabled, - onClick: () => { - this.setState({disclaimerDisabled: true}) - onConfirm() - }, - style: { - marginTop: '18px', - }, - }, 'Accept'), - ]) - ) -} - -Notice.prototype.componentDidMount = function () { - var node = findDOMNode(this) - linker.setupListener(node) - if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) { - this.setState({disclaimerDisabled: false}) - } -} - -Notice.prototype.componentWillUnmount = function () { - var node = findDOMNode(this) - linker.teardownListener(node) -} diff --git a/ui/responsive/app/components/pending-msg-details.js b/ui/responsive/app/components/pending-msg-details.js deleted file mode 100644 index 16308d121..000000000 --- a/ui/responsive/app/components/pending-msg-details.js +++ /dev/null @@ -1,50 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const AccountPanel = require('./account-panel') - -module.exports = PendingMsgDetails - -inherits(PendingMsgDetails, Component) -function PendingMsgDetails () { - Component.call(this) -} - -PendingMsgDetails.prototype.render = function () { - var state = this.props - var msgData = state.txData - - var msgParams = msgData.msgParams || {} - var address = msgParams.from || state.selectedAddress - var identity = state.identities[address] || { address: address } - var account = state.accounts[address] || { address: address } - - return ( - h('div', { - key: msgData.id, - style: { - margin: '10px 20px', - }, - }, [ - - // account that will sign - h(AccountPanel, { - showFullAddress: true, - identity: identity, - account: account, - imageifyIdenticons: state.imageifyIdenticons, - }), - - // message data - h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-row.flex-space-between', [ - h('label.font-small', 'MESSAGE'), - h('span.font-small', msgParams.data), - ]), - ]), - - ]) - ) -} - diff --git a/ui/responsive/app/components/pending-msg.js b/ui/responsive/app/components/pending-msg.js deleted file mode 100644 index b2cac164a..000000000 --- a/ui/responsive/app/components/pending-msg.js +++ /dev/null @@ -1,56 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const PendingTxDetails = require('./pending-msg-details') - -module.exports = PendingMsg - -inherits(PendingMsg, Component) -function PendingMsg () { - Component.call(this) -} - -PendingMsg.prototype.render = function () { - var state = this.props - var msgData = state.txData - - return ( - - h('div', { - key: msgData.id, - }, [ - - // header - h('h3', { - style: { - fontWeight: 'bold', - textAlign: 'center', - }, - }, 'Sign Message'), - - h('.error', { - style: { - margin: '10px', - }, - }, `Signing this message can have - dangerous side effects. Only sign messages from - sites you fully trust with your entire account. - This will be fixed in a future version.`), - - // message details - h(PendingTxDetails, state), - - // sign + cancel - h('.flex-row.flex-space-around', [ - h('button', { - onClick: state.cancelMessage, - }, 'Cancel'), - h('button', { - onClick: state.signMessage, - }, 'Sign'), - ]), - ]) - - ) -} - diff --git a/ui/responsive/app/components/pending-personal-msg-details.js b/ui/responsive/app/components/pending-personal-msg-details.js deleted file mode 100644 index 1050513f2..000000000 --- a/ui/responsive/app/components/pending-personal-msg-details.js +++ /dev/null @@ -1,60 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const AccountPanel = require('./account-panel') -const BinaryRenderer = require('./binary-renderer') - -module.exports = PendingMsgDetails - -inherits(PendingMsgDetails, Component) -function PendingMsgDetails () { - Component.call(this) -} - -PendingMsgDetails.prototype.render = function () { - var state = this.props - var msgData = state.txData - - var msgParams = msgData.msgParams || {} - var address = msgParams.from || state.selectedAddress - var identity = state.identities[address] || { address: address } - var account = state.accounts[address] || { address: address } - - var { data } = msgParams - - return ( - h('div', { - key: msgData.id, - style: { - margin: '10px 20px', - }, - }, [ - - // account that will sign - h(AccountPanel, { - showFullAddress: true, - identity: identity, - account: account, - imageifyIdenticons: state.imageifyIdenticons, - }), - - // message data - h('div', { - style: { - height: '260px', - }, - }, [ - h('label.font-small', { style: { display: 'block' } }, 'MESSAGE'), - h(BinaryRenderer, { - value: data, - style: { - height: '215px', - }, - }), - ]), - - ]) - ) -} - diff --git a/ui/responsive/app/components/pending-personal-msg.js b/ui/responsive/app/components/pending-personal-msg.js deleted file mode 100644 index 4542adb28..000000000 --- a/ui/responsive/app/components/pending-personal-msg.js +++ /dev/null @@ -1,47 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const PendingTxDetails = require('./pending-personal-msg-details') - -module.exports = PendingMsg - -inherits(PendingMsg, Component) -function PendingMsg () { - Component.call(this) -} - -PendingMsg.prototype.render = function () { - var state = this.props - var msgData = state.txData - - return ( - - h('div', { - key: msgData.id, - }, [ - - // header - h('h3', { - style: { - fontWeight: 'bold', - textAlign: 'center', - }, - }, 'Sign Message'), - - // message details - h(PendingTxDetails, state), - - // sign + cancel - h('.flex-row.flex-space-around', [ - h('button', { - onClick: state.cancelPersonalMessage, - }, 'Cancel'), - h('button', { - onClick: state.signPersonalMessage, - }, 'Sign'), - ]), - ]) - - ) -} - diff --git a/ui/responsive/app/components/pending-tx.js b/ui/responsive/app/components/pending-tx.js deleted file mode 100644 index 962680d30..000000000 --- a/ui/responsive/app/components/pending-tx.js +++ /dev/null @@ -1,480 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const actions = require('../actions') -const clone = require('clone') - -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const hexToBn = require('../../../../app/scripts/lib/hex-to-bn') -const util = require('../util') -const MiniAccountPanel = require('./mini-account-panel') -const Copyable = require('./copyable') -const EthBalance = require('./eth-balance') -const addressSummary = util.addressSummary -const nameForAddress = require('../../lib/contract-namer') -const BNInput = require('./bn-as-decimal-input') - -const MIN_GAS_PRICE_GWEI_BN = new BN(2) -const GWEI_FACTOR = new BN(1e9) -const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) -const MIN_GAS_LIMIT_BN = new BN(21000) - -module.exports = PendingTx -inherits(PendingTx, Component) -function PendingTx () { - Component.call(this) - this.state = { - valid: true, - txData: null, - submitting: false, - } -} - -PendingTx.prototype.render = function () { - const props = this.props - const { currentCurrency, blockGasLimit } = props - - const conversionRate = props.conversionRate - const txMeta = this.gatherTxMeta() - const txParams = txMeta.txParams || {} - - // Account Details - const address = txParams.from || props.selectedAddress - const identity = props.identities[address] || { address: address } - const account = props.accounts[address] - const balance = account ? account.balance : '0x0' - - // recipient check - const isValidAddress = !txParams.to || util.isValidAddress(txParams.to) - - // Gas - const gas = txParams.gas - const gasBn = hexToBn(gas) - const gasLimit = new BN(parseInt(blockGasLimit)) - const safeGasLimit = this.bnMultiplyByFraction(gasLimit, 19, 20).toString(10) - - // Gas Price - const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) - const gasPriceBn = hexToBn(gasPrice) - - const txFeeBn = gasBn.mul(gasPriceBn) - const valueBn = hexToBn(txParams.value) - const maxCost = txFeeBn.add(valueBn) - - const dataLength = txParams.data ? (txParams.data.length - 2) / 2 : 0 - - const balanceBn = hexToBn(balance) - const insufficientBalance = balanceBn.lt(maxCost) - - this.inputs = [] - - return ( - - h('div', { - key: txMeta.id, - }, [ - - h('form#pending-tx-form', { - onSubmit: this.onSubmit.bind(this), - - }, [ - - // tx info - h('div', [ - - h('.flex-row.flex-center', { - style: { - maxWidth: '100%', - }, - }, [ - - h(MiniAccountPanel, { - imageSeed: address, - picOrder: 'right', - }, [ - h('span.font-small', { - style: { - fontFamily: 'Montserrat Bold, Montserrat, sans-serif', - }, - }, identity.name), - - h(Copyable, { - value: ethUtil.toChecksumAddress(address), - }, [ - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, addressSummary(address, 6, 4, false)), - ]), - - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, [ - h(EthBalance, { - value: balance, - conversionRate, - currentCurrency, - inline: true, - labelColor: '#F7861C', - }), - ]), - ]), - - forwardCarrat(), - - this.miniAccountPanelForRecipient(), - ]), - - h('style', ` - .table-box { - margin: 7px 0px 0px 0px; - width: 100%; - } - .table-box .row { - margin: 0px; - background: rgb(236,236,236); - display: flex; - justify-content: space-between; - font-family: Montserrat Light, sans-serif; - font-size: 13px; - padding: 5px 25px; - } - .table-box .row .value { - font-family: Montserrat Regular; - } - `), - - h('.table-box', [ - - // Ether Value - // Currently not customizable, but easily modified - // in the way that gas and gasLimit currently are. - h('.row', [ - h('.cell.label', 'Amount'), - h(EthBalance, { value: txParams.value, currentCurrency, conversionRate }), - ]), - - // Gas Limit (customizable) - h('.cell.row', [ - h('.cell.label', 'Gas Limit'), - h('.cell.value', { - }, [ - h(BNInput, { - name: 'Gas Limit', - value: gasBn, - precision: 0, - scale: 0, - // The hard lower limit for gas. - min: MIN_GAS_LIMIT_BN.toString(10), - max: safeGasLimit, - suffix: 'UNITS', - style: { - position: 'relative', - top: '5px', - }, - onChange: this.gasLimitChanged.bind(this), - - ref: (hexInput) => { this.inputs.push(hexInput) }, - }), - ]), - ]), - - // Gas Price (customizable) - h('.cell.row', [ - h('.cell.label', 'Gas Price'), - h('.cell.value', { - }, [ - h(BNInput, { - name: 'Gas Price', - value: gasPriceBn, - precision: 9, - scale: 9, - suffix: 'GWEI', - min: MIN_GAS_PRICE_GWEI_BN.toString(10), - style: { - position: 'relative', - top: '5px', - }, - onChange: this.gasPriceChanged.bind(this), - ref: (hexInput) => { this.inputs.push(hexInput) }, - }), - ]), - ]), - - // Max Transaction Fee (calculated) - h('.cell.row', [ - h('.cell.label', 'Max Transaction Fee'), - h(EthBalance, { value: txFeeBn.toString(16), currentCurrency, conversionRate }), - ]), - - h('.cell.row', { - style: { - fontFamily: 'Montserrat Regular', - background: 'white', - padding: '10px 25px', - }, - }, [ - h('.cell.label', 'Max Total'), - h('.cell.value', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - h(EthBalance, { - value: maxCost.toString(16), - currentCurrency, - conversionRate, - inline: true, - labelColor: 'black', - fontSize: '16px', - }), - ]), - ]), - - // Data size row: - h('.cell.row', { - style: { - background: '#f7f7f7', - paddingBottom: '0px', - }, - }, [ - h('.cell.label'), - h('.cell.value', { - style: { - fontFamily: 'Montserrat Light', - fontSize: '11px', - }, - }, `Data included: ${dataLength} bytes`), - ]), - ]), // End of Table - - ]), - - h('style', ` - .conf-buttons button { - margin-left: 10px; - text-transform: uppercase; - } - `), - - txMeta.simulationFails ? - h('.error', { - style: { - marginLeft: 50, - fontSize: '0.9em', - }, - }, 'Transaction Error. Exception thrown in contract code.') - : null, - - !isValidAddress ? - h('.error', { - style: { - marginLeft: 50, - fontSize: '0.9em', - }, - }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.') - : null, - - insufficientBalance ? - h('span.error', { - style: { - marginLeft: 50, - fontSize: '0.9em', - }, - }, 'Insufficient balance for transaction') - : null, - - // send + cancel - h('.flex-row.flex-space-around.conf-buttons', { - style: { - display: 'flex', - justifyContent: 'flex-end', - margin: '14px 25px', - }, - }, [ - - - insufficientBalance ? - h('button.btn-green', { - onClick: props.buyEth, - }, 'Buy Ether') - : null, - - h('button', { - onClick: (event) => { - this.resetGasFields() - event.preventDefault() - }, - }, 'Reset'), - - // Accept Button - h('input.confirm.btn-green', { - type: 'submit', - value: 'SUBMIT', - style: { marginLeft: '10px' }, - disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, - }), - - h('button.cancel.btn-red', { - onClick: props.cancelTransaction, - }, 'Reject'), - ]), - ]), - ]) - ) -} - -PendingTx.prototype.miniAccountPanelForRecipient = function () { - const props = this.props - const txData = props.txData - const txParams = txData.txParams || {} - const isContractDeploy = !('to' in txParams) - - // If it's not a contract deploy, send to the account - if (!isContractDeploy) { - return h(MiniAccountPanel, { - imageSeed: txParams.to, - picOrder: 'left', - }, [ - - h('span.font-small', { - style: { - fontFamily: 'Montserrat Bold, Montserrat, sans-serif', - }, - }, nameForAddress(txParams.to, props.identities)), - - h(Copyable, { - value: ethUtil.toChecksumAddress(txParams.to), - }, [ - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, addressSummary(txParams.to, 6, 4, false)), - ]), - - ]) - } else { - return h(MiniAccountPanel, { - picOrder: 'left', - }, [ - - h('span.font-small', { - style: { - fontFamily: 'Montserrat Bold, Montserrat, sans-serif', - }, - }, 'New Contract'), - - ]) - } -} - -PendingTx.prototype.gasPriceChanged = function (newBN, valid) { - log.info(`Gas price changed to: ${newBN.toString(10)}`) - const txMeta = this.gatherTxMeta() - txMeta.txParams.gasPrice = '0x' + newBN.toString('hex') - this.setState({ - txData: clone(txMeta), - valid, - }) -} - -PendingTx.prototype.gasLimitChanged = function (newBN, valid) { - log.info(`Gas limit changed to ${newBN.toString(10)}`) - const txMeta = this.gatherTxMeta() - txMeta.txParams.gas = '0x' + newBN.toString('hex') - this.setState({ - txData: clone(txMeta), - valid, - }) -} - -PendingTx.prototype.resetGasFields = function () { - log.debug(`pending-tx resetGasFields`) - - this.inputs.forEach((hexInput) => { - if (hexInput) { - hexInput.setValid() - } - }) - - this.setState({ - txData: null, - valid: true, - }) -} - -PendingTx.prototype.onSubmit = function (event) { - event.preventDefault() - const txMeta = this.gatherTxMeta() - const valid = this.checkValidity() - this.setState({ valid, submitting: true }) - if (valid && this.verifyGasParams()) { - this.props.sendTransaction(txMeta, event) - } else { - this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) - this.setState({ submitting: false }) - } -} - -PendingTx.prototype.checkValidity = function () { - const form = this.getFormEl() - const valid = form.checkValidity() - return valid -} - -PendingTx.prototype.getFormEl = function () { - const form = document.querySelector('form#pending-tx-form') - // Stub out form for unit tests: - if (!form) { - return { checkValidity () { return true } } - } - return form -} - -// After a customizable state value has been updated, -PendingTx.prototype.gatherTxMeta = function () { - log.debug(`pending-tx gatherTxMeta`) - const props = this.props - const state = this.state - const txData = clone(state.txData) || clone(props.txData) - - log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) - return txData -} - -PendingTx.prototype.verifyGasParams = function () { - // We call this in case the gas has not been modified at all - if (!this.state) { return true } - return ( - this._notZeroOrEmptyString(this.state.gas) && - this._notZeroOrEmptyString(this.state.gasPrice) - ) -} - -PendingTx.prototype._notZeroOrEmptyString = function (obj) { - return obj !== '' && obj !== '0x0' -} - -PendingTx.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) { - const numBN = new BN(numerator) - const denomBN = new BN(denominator) - return targetBN.mul(numBN).div(denomBN) -} - -function forwardCarrat () { - return ( - h('img', { - src: 'images/forward-carrat.svg', - style: { - padding: '5px 6px 0px 10px', - height: '37px', - }, - }) - ) -} diff --git a/ui/responsive/app/components/qr-code.js b/ui/responsive/app/components/qr-code.js deleted file mode 100644 index 06b9aed9b..000000000 --- a/ui/responsive/app/components/qr-code.js +++ /dev/null @@ -1,79 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const qrCode = require('qrcode-npm').qrcode -const inherits = require('util').inherits -const connect = require('react-redux').connect -const isHexPrefixed = require('ethereumjs-util').isHexPrefixed -const CopyButton = require('./copyButton') - -module.exports = connect(mapStateToProps)(QrCodeView) - -function mapStateToProps (state) { - return { - Qr: state.appState.Qr, - buyView: state.appState.buyView, - warning: state.appState.warning, - } -} - -inherits(QrCodeView, Component) - -function QrCodeView () { - Component.call(this) -} - -QrCodeView.prototype.render = function () { - const props = this.props - const Qr = props.Qr - const address = `${isHexPrefixed(Qr.data) ? 'ethereum:' : ''}${Qr.data}` - const qrImage = qrCode(4, 'M') - qrImage.addData(address) - qrImage.make() - return h('.main-container.flex-column', { - key: 'qr', - style: { - justifyContent: 'center', - paddingBottom: '45px', - paddingLeft: '45px', - paddingRight: '45px', - alignItems: 'center', - }, - }, [ - Array.isArray(Qr.message) ? h('.message-container', this.renderMultiMessage()) : h('.qr-header', Qr.message), - - this.props.warning ? this.props.warning && h('span.error.flex-center', { - style: { - textAlign: 'center', - width: '229px', - height: '82px', - }, - }, - this.props.warning) : null, - - h('#qr-container.flex-column', { - style: { - marginTop: '25px', - marginBottom: '15px', - }, - dangerouslySetInnerHTML: { - __html: qrImage.createTableTag(4), - }, - }), - h('.flex-row', [ - h('h3.ellip-address', { - style: { - width: '247px', - }, - }, Qr.data), - h(CopyButton, { - value: Qr.data, - }), - ]), - ]) -} - -QrCodeView.prototype.renderMultiMessage = function () { - var Qr = this.props.Qr - var multiMessage = Qr.message.map((message) => h('.qr-message', message)) - return multiMessage -} diff --git a/ui/responsive/app/components/range-slider.js b/ui/responsive/app/components/range-slider.js deleted file mode 100644 index 823f5eb01..000000000 --- a/ui/responsive/app/components/range-slider.js +++ /dev/null @@ -1,58 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = RangeSlider - -inherits(RangeSlider, Component) -function RangeSlider () { - Component.call(this) -} - -RangeSlider.prototype.render = function () { - const state = this.state || {} - const props = this.props - const onInput = props.onInput || function () {} - const name = props.name - const { - min = 0, - max = 100, - increment = 1, - defaultValue = 50, - mirrorInput = false, - } = this.props.options - const {container, input, range} = props.style - - return ( - h('.flex-row', { - style: container, - }, [ - h('input', { - type: 'range', - name: name, - min: min, - max: max, - step: increment, - style: range, - value: state.value || defaultValue, - onChange: mirrorInput ? this.mirrorInputs.bind(this, event) : onInput, - }), - - // Mirrored input for range - mirrorInput ? h('input.large-input', { - type: 'number', - name: `${name}Mirror`, - min: min, - max: max, - value: state.value || defaultValue, - step: increment, - style: input, - onChange: this.mirrorInputs.bind(this, event), - }) : null, - ]) - ) -} - -RangeSlider.prototype.mirrorInputs = function (event) { - this.setState({value: event.target.value}) -} diff --git a/ui/responsive/app/components/shapeshift-form.js b/ui/responsive/app/components/shapeshift-form.js deleted file mode 100644 index e0a720426..000000000 --- a/ui/responsive/app/components/shapeshift-form.js +++ /dev/null @@ -1,306 +0,0 @@ -const PersistentForm = require('../../lib/persistent-form') -const h = require('react-hyperscript') -const inherits = require('util').inherits -const connect = require('react-redux').connect -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') -const actions = require('../actions') -const Qr = require('./qr-code') -const isValidAddress = require('../util').isValidAddress -module.exports = connect(mapStateToProps)(ShapeshiftForm) - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - isSubLoading: state.appState.isSubLoading, - qrRequested: state.appState.qrRequested, - } -} - -inherits(ShapeshiftForm, PersistentForm) - -function ShapeshiftForm () { - PersistentForm.call(this) - this.persistentFormParentId = 'shapeshift-buy-form' -} - -ShapeshiftForm.prototype.render = function () { - return h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.props.qrRequested ? h(Qr, {key: 'qr'}) : this.renderMain(), - ]) -} - -ShapeshiftForm.prototype.renderMain = function () { - const marketinfo = this.props.buyView.formView.marketinfo - const coinOptions = this.props.buyView.formView.coinOptions - var coin = marketinfo.pair.split('_')[0].toUpperCase() - - return h('.flex-column', { - style: { - // marginTop: '10px', - padding: '25px', - paddingTop: '5px', - width: '100%', - minHeight: '215px', - alignItems: 'center', - overflowY: 'auto', - }, - }, [ - h('.flex-row', { - style: { - justifyContent: 'center', - alignItems: 'baseline', - height: '42px', - }, - }, [ - h('img', { - src: coinOptions[coin].image, - width: '25px', - height: '25px', - style: { - marginRight: '5px', - }, - }), - - h('.input-container', [ - h('input#fromCoin.buy-inputs.ex-coins', { - type: 'text', - list: 'coinList', - autoFocus: true, - dataset: { - persistentFormId: 'input-coin', - }, - style: { - boxSizing: 'border-box', - }, - onChange: this.handleLiveInput.bind(this), - defaultValue: 'BTC', - }), - - this.renderCoinList(), - - h('i.fa.fa-pencil-square-o.edit-text', { - style: { - fontSize: '12px', - color: '#F7861C', - position: 'relative', - bottom: '48px', - left: '106px', - }, - }), - ]), - - h('.icon-control', [ - h('i.fa.fa-refresh.fa-4.orange', { - style: { - bottom: '5px', - left: '5px', - color: '#F7861C', - }, - onClick: this.updateCoin.bind(this), - }), - h('i.fa.fa-chevron-right.fa-4.orange', { - style: { - position: 'relative', - bottom: '26px', - left: '10px', - color: '#F7861C', - }, - onClick: this.updateCoin.bind(this), - }), - ]), - - h('#toCoin.ex-coins', marketinfo.pair.split('_')[1].toUpperCase()), - - h('img', { - src: coinOptions[marketinfo.pair.split('_')[1].toUpperCase()].image, - width: '25px', - height: '25px', - style: { - marginLeft: '5px', - }, - }), - ]), - h('.flex-column', { - style: { - alignItems: 'flex-start', - }, - }, [ - this.props.warning ? this.props.warning && h('span.error.flex-center', { - style: { - textAlign: 'center', - width: '229px', - height: '82px', - }, - }, - this.props.warning) : this.renderInfo(), - ]), - - h(this.activeToggle('.input-container'), { - style: { - padding: '10px', - paddingTop: '0px', - width: '100%', - }, - }, [ - - h('div', `${coin} Address:`), - - h('input#fromCoinAddress.buy-inputs', { - type: 'text', - placeholder: `Your ${coin} Refund Address`, - dataset: { - persistentFormId: 'refund-address', - }, - style: { - boxSizing: 'border-box', - width: '227px', - height: '30px', - padding: ' 5px ', - }, - }), - - h('i.fa.fa-pencil-square-o.edit-text', { - style: { - fontSize: '12px', - color: '#F7861C', - position: 'relative', - bottom: '10px', - right: '11px', - }, - }), - h('.flex-row', { - style: { - justifyContent: 'flex-end', - }, - }, [ - h('button', { - onClick: this.shift.bind(this), - style: { - marginTop: '10px', - position: 'relative', - bottom: '40px', - }, - }, - 'Submit'), - ]), - ]), - ]) -} - -ShapeshiftForm.prototype.shift = function () { - var props = this.props - var withdrawal = this.props.buyView.buyAddress - var returnAddress = document.getElementById('fromCoinAddress').value - var pair = this.props.buyView.formView.marketinfo.pair - var data = { - 'withdrawal': withdrawal, - 'pair': pair, - 'returnAddress': returnAddress, - // Public api key - 'apiKey': '803d1f5df2ed1b1476e4b9e6bcd089e34d8874595dda6a23b67d93c56ea9cc2445e98a6748b219b2b6ad654d9f075f1f1db139abfa93158c04e825db122c14b6', - } - var message = [ - `Deposit Limit: ${props.buyView.formView.marketinfo.limit}`, - `Deposit Minimum:${props.buyView.formView.marketinfo.minimum}`, - ] - if (isValidAddress(withdrawal)) { - this.props.dispatch(actions.coinShiftRquest(data, message)) - } -} - -ShapeshiftForm.prototype.renderCoinList = function () { - var list = Object.keys(this.props.buyView.formView.coinOptions).map((item) => { - return h('option', { - value: item, - }, item) - }) - - return h('datalist#coinList', { - onClick: (event) => { - event.preventDefault() - }, - }, list) -} - -ShapeshiftForm.prototype.updateCoin = function (event) { - event.preventDefault() - const props = this.props - var coinOptions = this.props.buyView.formView.coinOptions - var coin = document.getElementById('fromCoin').value - - if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { - var message = 'Not a valid coin' - return props.dispatch(actions.displayWarning(message)) - } else { - return props.dispatch(actions.pairUpdate(coin)) - } -} - -ShapeshiftForm.prototype.handleLiveInput = function () { - const props = this.props - var coinOptions = this.props.buyView.formView.coinOptions - var coin = document.getElementById('fromCoin').value - - if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { - return null - } else { - return props.dispatch(actions.pairUpdate(coin)) - } -} - -ShapeshiftForm.prototype.renderInfo = function () { - const marketinfo = this.props.buyView.formView.marketinfo - const coinOptions = this.props.buyView.formView.coinOptions - var coin = marketinfo.pair.split('_')[0].toUpperCase() - - return h('span', { - style: { - }, - }, [ - h('h3.flex-row.text-transform-uppercase', { - style: { - color: '#868686', - paddingTop: '4px', - justifyContent: 'space-around', - textAlign: 'center', - fontSize: '17px', - }, - }, `Market Info for ${marketinfo.pair.replace('_', ' to ').toUpperCase()}:`), - h('.marketinfo', ['Status : ', `${coinOptions[coin].status}`]), - h('.marketinfo', ['Exchange Rate: ', `${marketinfo.rate}`]), - h('.marketinfo', ['Limit: ', `${marketinfo.limit}`]), - h('.marketinfo', ['Minimum : ', `${marketinfo.minimum}`]), - ]) -} - -ShapeshiftForm.prototype.activeToggle = function (elementType) { - if (!this.props.buyView.formView.response || this.props.warning) return elementType - return `${elementType}.inactive` -} - -ShapeshiftForm.prototype.renderLoading = function () { - return h('span', { - style: { - position: 'absolute', - left: '70px', - bottom: '194px', - background: 'transparent', - width: '229px', - height: '82px', - display: 'flex', - justifyContent: 'center', - }, - }, [ - h('img', { - style: { - width: '60px', - }, - src: 'images/loading.svg', - }), - ]) -} diff --git a/ui/responsive/app/components/shift-list-item.js b/ui/responsive/app/components/shift-list-item.js deleted file mode 100644 index 32bfbeda4..000000000 --- a/ui/responsive/app/components/shift-list-item.js +++ /dev/null @@ -1,204 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const vreme = new (require('vreme')) -const explorerLink = require('../../lib/explorer-link') -const actions = require('../actions') -const addressSummary = require('../util').addressSummary - -const CopyButton = require('./copyButton') -const EthBalance = require('./eth-balance') -const Tooltip = require('./tooltip') - - -module.exports = connect(mapStateToProps)(ShiftListItem) - -function mapStateToProps (state) { - return { - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - } -} - -inherits(ShiftListItem, Component) - -function ShiftListItem () { - Component.call(this) -} - -ShiftListItem.prototype.render = function () { - return ( - h('.transaction-list-item.flex-row', { - style: { - paddingTop: '20px', - paddingBottom: '20px', - justifyContent: 'space-around', - alignItems: 'center', - }, - }, [ - h('div', { - style: { - width: '0px', - position: 'relative', - bottom: '19px', - }, - }, [ - h('img', { - src: 'https://info.shapeshift.io/sites/default/files/logo.png', - style: { - height: '35px', - width: '132px', - position: 'absolute', - clip: 'rect(0px,23px,34px,0px)', - }, - }), - ]), - - this.renderInfo(), - this.renderUtilComponents(), - ]) - ) -} - -function formatDate (date) { - return vreme.format(new Date(date), 'March 16 2014 14:30') -} - -ShiftListItem.prototype.renderUtilComponents = function () { - var props = this.props - const { conversionRate, currentCurrency } = props - - switch (props.response.status) { - case 'no_deposits': - return h('.flex-row', [ - h(CopyButton, { - value: this.props.depositAddress, - }), - h(Tooltip, { - title: 'QR Code', - }, [ - h('i.fa.fa-qrcode.pointer.pop-hover', { - onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType)), - style: { - margin: '5px', - marginLeft: '23px', - marginRight: '12px', - fontSize: '20px', - color: '#F7861C', - }, - }), - ]), - ]) - case 'received': - return h('.flex-row') - - case 'complete': - return h('.flex-row', [ - h(CopyButton, { - value: this.props.response.transaction, - }), - h(EthBalance, { - value: `${props.response.outgoingCoin}`, - conversionRate, - currentCurrency, - width: '55px', - shorten: true, - needsParse: false, - incoming: true, - style: { - fontSize: '15px', - color: '#01888C', - }, - }), - ]) - - case 'failed': - return '' - default: - return '' - } -} - -ShiftListItem.prototype.renderInfo = function () { - var props = this.props - switch (props.response.status) { - case 'no_deposits': - return h('.flex-column', { - style: { - width: '200px', - overflow: 'hidden', - }, - }, [ - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, `${props.depositType} to ETH via ShapeShift`), - h('div', 'No deposits received'), - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, formatDate(props.time)), - ]) - case 'received': - return h('.flex-column', { - style: { - width: '200px', - overflow: 'hidden', - }, - }, [ - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, `${props.depositType} to ETH via ShapeShift`), - h('div', 'Conversion in progress'), - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, formatDate(props.time)), - ]) - case 'complete': - var url = explorerLink(props.response.transaction, parseInt('1')) - - return h('.flex-column.pointer', { - style: { - width: '200px', - overflow: 'hidden', - }, - onClick: () => global.platform.openWindow({ url }), - }, [ - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, 'From ShapeShift'), - h('div', formatDate(props.time)), - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, addressSummary(props.response.transaction)), - ]) - - case 'failed': - return h('span.error', '(Failed)') - default: - return '' - } -} diff --git a/ui/responsive/app/components/tab-bar.js b/ui/responsive/app/components/tab-bar.js deleted file mode 100644 index 6295e7dd9..000000000 --- a/ui/responsive/app/components/tab-bar.js +++ /dev/null @@ -1,36 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = TabBar - -inherits(TabBar, Component) -function TabBar () { - Component.call(this) -} - -TabBar.prototype.render = function () { - const props = this.props - const state = this.state || {} - const { tabs = [], defaultTab, tabSelected } = props - const { subview = defaultTab } = state - - return ( - h('.flex-row.space-around.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - paddingTop: '4px', - }, - }, tabs.map((tab) => { - const { key, content } = tab - return h(subview === key ? '.activeForm' : '.inactiveForm.pointer', { - onClick: () => { - this.setState({ subview: key }) - tabSelected(key) - }, - }, content) - })) - ) -} - diff --git a/ui/responsive/app/components/template.js b/ui/responsive/app/components/template.js deleted file mode 100644 index b6ed8eaa0..000000000 --- a/ui/responsive/app/components/template.js +++ /dev/null @@ -1,18 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = NewComponent - -inherits(NewComponent, Component) -function NewComponent () { - Component.call(this) -} - -NewComponent.prototype.render = function () { - const props = this.props - - return ( - h('span', props.message) - ) -} diff --git a/ui/responsive/app/components/token-cell.js b/ui/responsive/app/components/token-cell.js deleted file mode 100644 index 19d7139bb..000000000 --- a/ui/responsive/app/components/token-cell.js +++ /dev/null @@ -1,72 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const Identicon = require('./identicon') -const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') - -module.exports = TokenCell - -inherits(TokenCell, Component) -function TokenCell () { - Component.call(this) -} - -TokenCell.prototype.render = function () { - const props = this.props - const { address, symbol, string, network, userAddress } = props - - return ( - h('li.token-cell', { - style: { cursor: network === '1' ? 'pointer' : 'default' }, - onClick: this.view.bind(this, address, userAddress, network), - }, [ - - h(Identicon, { - diameter: 50, - address, - network, - }), - - h('h3', `${string || 0} ${symbol}`), - - h('span', { style: { flex: '1 0 auto' } }), - - /* - h('button', { - onClick: this.send.bind(this, address), - }, 'SEND'), - */ - - ]) - ) -} - -TokenCell.prototype.send = function (address, event) { - event.preventDefault() - event.stopPropagation() - const url = tokenFactoryFor(address) - if (url) { - navigateTo(url) - } -} - -TokenCell.prototype.view = function (address, userAddress, network, event) { - const url = etherscanLinkFor(address, userAddress, network) - if (url) { - navigateTo(url) - } -} - -function navigateTo (url) { - global.platform.openWindow({ url }) -} - -function etherscanLinkFor (tokenAddress, address, network) { - const prefix = prefixForNetwork(network) - return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}` -} - -function tokenFactoryFor (tokenAddress) { - return `https://tokenfactory.surge.sh/#/token/${tokenAddress}` -} - diff --git a/ui/responsive/app/components/token-list.js b/ui/responsive/app/components/token-list.js deleted file mode 100644 index 20cfa897e..000000000 --- a/ui/responsive/app/components/token-list.js +++ /dev/null @@ -1,192 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const TokenTracker = require('eth-token-tracker') -const TokenCell = require('./token-cell.js') -const normalizeAddress = require('eth-sig-util').normalize - -const defaultTokens = [] -const contracts = require('eth-contract-metadata') -for (const address in contracts) { - const contract = contracts[address] - if (contract.erc20) { - contract.address = address - defaultTokens.push(contract) - } -} - -module.exports = TokenList - -inherits(TokenList, Component) -function TokenList () { - this.state = { - tokens: [], - isLoading: true, - network: null, - } - Component.call(this) -} - -TokenList.prototype.render = function () { - const state = this.state - const { tokens, isLoading, error } = state - const { userAddress, network } = this.props - - if (isLoading) { - return this.message('Loading') - } - - if (error) { - log.error(error) - return this.message('There was a problem loading your token balances.') - } - - const tokenViews = tokens.map((tokenData) => { - tokenData.network = network - tokenData.userAddress = userAddress - return h(TokenCell, tokenData) - }) - - return h('div', [ - h('ol', { - style: { - height: '260px', - overflowY: 'auto', - display: 'flex', - flexDirection: 'column', - }, - }, [ - h('style', ` - - li.token-cell { - display: flex; - flex-direction: row; - align-items: center; - padding: 10px; - } - - li.token-cell > h3 { - margin-left: 12px; - } - - li.token-cell:hover { - background: white; - cursor: pointer; - } - - `), - ...tokenViews, - tokenViews.length ? null : this.message('No Tokens Found.'), - ]), - this.addTokenButtonElement(), - ]) -} - -TokenList.prototype.addTokenButtonElement = function () { - return h('div', [ - h('div.footer.hover-white.pointer', { - key: 'reveal-account-bar', - onClick: () => { - this.props.addToken() - }, - style: { - display: 'flex', - height: '40px', - padding: '10px', - justifyContent: 'center', - alignItems: 'center', - }, - }, [ - h('i.fa.fa-plus.fa-lg'), - ]), - ]) -} - -TokenList.prototype.message = function (body) { - return h('div', { - style: { - display: 'flex', - height: '250px', - alignItems: 'center', - justifyContent: 'center', - padding: '30px', - }, - }, body) -} - -TokenList.prototype.componentDidMount = function () { - this.createFreshTokenTracker() -} - -TokenList.prototype.createFreshTokenTracker = function () { - if (this.tracker) { - // Clean up old trackers when refreshing: - this.tracker.stop() - this.tracker.removeListener('update', this.balanceUpdater) - this.tracker.removeListener('error', this.showError) - } - - if (!global.ethereumProvider) return - const { userAddress } = this.props - this.tracker = new TokenTracker({ - userAddress, - provider: global.ethereumProvider, - tokens: uniqueMergeTokens(defaultTokens, this.props.tokens), - pollingInterval: 8000, - }) - - - // Set up listener instances for cleaning up - this.balanceUpdater = this.updateBalances.bind(this) - this.showError = (error) => { - this.setState({ error, isLoading: false }) - } - this.tracker.on('update', this.balanceUpdater) - this.tracker.on('error', this.showError) - - this.tracker.updateBalances() - .then(() => { - this.updateBalances(this.tracker.serialize()) - }) - .catch((reason) => { - log.error(`Problem updating balances`, reason) - this.setState({ isLoading: false }) - }) -} - -TokenList.prototype.componentWillUpdate = function (nextProps) { - if (nextProps.network === 'loading') return - const oldNet = this.props.network - const newNet = nextProps.network - - if (oldNet && newNet && newNet !== oldNet) { - this.setState({ isLoading: true }) - this.createFreshTokenTracker() - } -} - -TokenList.prototype.updateBalances = function (tokens) { - const heldTokens = tokens.filter(token => { - return token.balance !== '0' && token.string !== '0.000' - }) - this.setState({ tokens: heldTokens, isLoading: false }) -} - -TokenList.prototype.componentWillUnmount = function () { - if (!this.tracker) return - this.tracker.stop() -} - -function uniqueMergeTokens (tokensA, tokensB) { - const uniqueAddresses = [] - const result = [] - tokensA.concat(tokensB).forEach((token) => { - const normal = normalizeAddress(token.address) - if (!uniqueAddresses.includes(normal)) { - uniqueAddresses.push(normal) - result.push(token) - } - }) - return result -} - diff --git a/ui/responsive/app/components/tooltip.js b/ui/responsive/app/components/tooltip.js deleted file mode 100644 index edbc074bb..000000000 --- a/ui/responsive/app/components/tooltip.js +++ /dev/null @@ -1,22 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ReactTooltip = require('react-tooltip-component') - -module.exports = Tooltip - -inherits(Tooltip, Component) -function Tooltip () { - Component.call(this) -} - -Tooltip.prototype.render = function () { - const props = this.props - const { position, title, children } = props - - return h(ReactTooltip, { - position: position || 'left', - title, - fixed: false, - }, children) -} diff --git a/ui/responsive/app/components/transaction-list-item-icon.js b/ui/responsive/app/components/transaction-list-item-icon.js deleted file mode 100644 index 431054340..000000000 --- a/ui/responsive/app/components/transaction-list-item-icon.js +++ /dev/null @@ -1,68 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const Tooltip = require('./tooltip') - -const Identicon = require('./identicon') - -module.exports = TransactionIcon - -inherits(TransactionIcon, Component) -function TransactionIcon () { - Component.call(this) -} - -TransactionIcon.prototype.render = function () { - const { transaction, txParams, isMsg } = this.props - switch (transaction.status) { - case 'unapproved': - return h(!isMsg ? '.unapproved-tx-icon' : 'i.fa.fa-certificate.fa-lg') - - case 'rejected': - return h('i.fa.fa-exclamation-triangle.fa-lg.warning', { - style: { - width: '24px', - }, - }) - - case 'failed': - return h('i.fa.fa-exclamation-triangle.fa-lg.error', { - style: { - width: '24px', - }, - }) - - case 'submitted': - return h(Tooltip, { - title: 'Pending', - position: 'bottom', - }, [ - h('i.fa.fa-ellipsis-h', { - style: { - fontSize: '27px', - }, - }), - ]) - } - - if (isMsg) { - return h('i.fa.fa-certificate.fa-lg', { - style: { - width: '24px', - }, - }) - } - - if (txParams.to) { - return h(Identicon, { - diameter: 24, - address: txParams.to || transaction.hash, - }) - } else { - return h('i.fa.fa-file-text-o.fa-lg', { - style: { - width: '24px', - }, - }) - } -} diff --git a/ui/responsive/app/components/transaction-list-item.js b/ui/responsive/app/components/transaction-list-item.js deleted file mode 100644 index dbda66a31..000000000 --- a/ui/responsive/app/components/transaction-list-item.js +++ /dev/null @@ -1,165 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const EthBalance = require('./eth-balance') -const addressSummary = require('../util').addressSummary -const explorerLink = require('../../lib/explorer-link') -const CopyButton = require('./copyButton') -const vreme = new (require('vreme')) -const Tooltip = require('./tooltip') -const numberToBN = require('number-to-bn') - -const TransactionIcon = require('./transaction-list-item-icon') -const ShiftListItem = require('./shift-list-item') -module.exports = TransactionListItem - -inherits(TransactionListItem, Component) -function TransactionListItem () { - Component.call(this) -} - -TransactionListItem.prototype.render = function () { - const { transaction, network, conversionRate, currentCurrency } = this.props - if (transaction.key === 'shapeshift') { - if (network === '1') return h(ShiftListItem, transaction) - } - var date = formatDate(transaction.time) - - let isLinkable = false - const numericNet = parseInt(network) - isLinkable = numericNet === 1 || numericNet === 3 || numericNet === 4 || numericNet === 42 - - var isMsg = ('msgParams' in transaction) - var isTx = ('txParams' in transaction) - var isPending = transaction.status === 'unapproved' - let txParams - if (isTx) { - txParams = transaction.txParams - } else if (isMsg) { - txParams = transaction.msgParams - } - - const nonce = txParams.nonce ? numberToBN(txParams.nonce).toString(10) : '' - - const isClickable = ('hash' in transaction && isLinkable) || isPending - return ( - h(`.transaction-list-item.flex-row.flex-space-between${isClickable ? '.pointer' : ''}`, { - onClick: (event) => { - if (isPending) { - this.props.showTx(transaction.id) - } - event.stopPropagation() - if (!transaction.hash || !isLinkable) return - var url = explorerLink(transaction.hash, parseInt(network)) - global.platform.openWindow({ url }) - }, - style: { - padding: '20px 0', - }, - }, [ - - h('.identicon-wrapper.flex-column.flex-center.select-none', [ - h('.pop-hover', { - onClick: (event) => { - event.stopPropagation() - if (!isTx || isPending) return - var url = `https://metamask.github.io/eth-tx-viz/?tx=${transaction.hash}` - global.platform.openWindow({ url }) - }, - }, [ - h(TransactionIcon, { txParams, transaction, isTx, isMsg }), - ]), - ]), - - h(Tooltip, { - title: 'Transaction Number', - position: 'bottom', - }, [ - h('span', { - style: { - display: 'flex', - cursor: 'normal', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - padding: '10px', - }, - }, nonce), - ]), - - h('.flex-column', {style: {width: '200px', overflow: 'hidden'}}, [ - domainField(txParams), - h('div', date), - recipientField(txParams, transaction, isTx, isMsg), - ]), - - // Places a copy button if tx is successful, else places a placeholder empty div. - transaction.hash ? h(CopyButton, { value: transaction.hash }) : h('div', {style: { display: 'flex', alignItems: 'center', width: '26px' }}), - - isTx ? h(EthBalance, { - value: txParams.value, - conversionRate, - currentCurrency, - width: '55px', - shorten: true, - showFiat: false, - style: {fontSize: '15px'}, - }) : h('.flex-column'), - ]) - ) -} - -function domainField (txParams) { - return h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - overflow: 'hidden', - textOverflow: 'ellipsis', - width: '100%', - }, - }, [ - txParams.origin, - ]) -} - -function recipientField (txParams, transaction, isTx, isMsg) { - let message - - if (isMsg) { - message = 'Signature Requested' - } else if (txParams.to) { - message = addressSummary(txParams.to) - } else { - message = 'Contract Published' - } - - return h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - }, - }, [ - message, - failIfFailed(transaction), - ]) -} - -function formatDate (date) { - return vreme.format(new Date(date), 'March 16 2014 14:30') -} - -function failIfFailed (transaction) { - if (transaction.status === 'rejected') { - return h('span.error', ' (Rejected)') - } - if (transaction.err) { - return h(Tooltip, { - title: transaction.err.message, - position: 'bottom', - }, [ - h('span.error', ' (Failed)'), - ]) - } -} diff --git a/ui/responsive/app/components/transaction-list.js b/ui/responsive/app/components/transaction-list.js deleted file mode 100644 index 3b4ba741e..000000000 --- a/ui/responsive/app/components/transaction-list.js +++ /dev/null @@ -1,79 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const TransactionListItem = require('./transaction-list-item') - -module.exports = TransactionList - - -inherits(TransactionList, Component) -function TransactionList () { - Component.call(this) -} - -TransactionList.prototype.render = function () { - const { transactions, network, unapprovedMsgs, conversionRate } = this.props - - var shapeShiftTxList - if (network === '1') { - shapeShiftTxList = this.props.shapeShiftTxList - } - const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList) - .sort((a, b) => b.time - a.time) - - return ( - - h('section.transaction-list', [ - - h('style', ` - .transaction-list .transaction-list-item:not(:last-of-type) { - border-bottom: 1px solid #D4D4D4; - } - .transaction-list .transaction-list-item .ether-balance-label { - display: block !important; - font-size: small; - } - `), - - h('.tx-list', { - style: { - overflowY: 'auto', - height: '300px', - padding: '0 20px', - textAlign: 'center', - }, - }, [ - - txsToRender.length - ? txsToRender.map((transaction, i) => { - let key - switch (transaction.key) { - case 'shapeshift': - const { depositAddress, time } = transaction - key = `shift-tx-${depositAddress}-${time}-${i}` - break - default: - key = `tx-${transaction.id}-${i}` - } - return h(TransactionListItem, { - transaction, i, network, key, - conversionRate, - showTx: (txId) => { - this.props.viewPendingTx(txId) - }, - }) - }) - : h('.flex-center', { - style: { - flexDirection: 'column', - height: '100%', - }, - }, [ - 'No transaction history.', - ]), - ]), - ]) - ) -} - diff --git a/ui/responsive/app/conf-tx.js b/ui/responsive/app/conf-tx.js deleted file mode 100644 index 63b77ef7f..000000000 --- a/ui/responsive/app/conf-tx.js +++ /dev/null @@ -1,213 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') -const NetworkIndicator = require('./components/network') -const txHelper = require('../lib/tx-helper') -const isPopupOrNotification = require('../../../app/scripts/lib/is-popup-or-notification') - -const PendingTx = require('./components/pending-tx') -const PendingMsg = require('./components/pending-msg') -const PendingPersonalMsg = require('./components/pending-personal-msg') -const Loading = require('./components/loading') - -module.exports = connect(mapStateToProps)(ConfirmTxScreen) - -function mapStateToProps (state) { - return { - identities: state.metamask.identities, - accounts: state.metamask.accounts, - selectedAddress: state.metamask.selectedAddress, - unapprovedTxs: state.metamask.unapprovedTxs, - unapprovedMsgs: state.metamask.unapprovedMsgs, - unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs, - index: state.appState.currentView.context, - warning: state.appState.warning, - network: state.metamask.network, - provider: state.metamask.provider, - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - blockGasLimit: state.metamask.currentBlockGasLimit, - } -} - -inherits(ConfirmTxScreen, Component) -function ConfirmTxScreen () { - Component.call(this) -} - -ConfirmTxScreen.prototype.render = function () { - const props = this.props - const { network, provider, unapprovedTxs, currentCurrency, - unapprovedMsgs, unapprovedPersonalMsgs, conversionRate, blockGasLimit } = props - - var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) - - var txData = unconfTxList[props.index] || {} - var txParams = txData.params || {} - var isNotification = isPopupOrNotification() === 'notification' - - - log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`) - if (unconfTxList.length === 0) return h(Loading, { isLoading: true }) - - return ( - - h('.flex-column.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - !isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: this.goHome.bind(this), - }) : null, - h('h2.page-subtitle', 'Confirm Transaction'), - isNotification ? h(NetworkIndicator, { - network: network, - provider: provider, - }) : null, - ]), - - h('h3', { - style: { - alignSelf: 'center', - display: unconfTxList.length > 1 ? 'block' : 'none', - }, - }, [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - style: { - display: props.index === 0 ? 'none' : 'inline-block', - }, - onClick: () => props.dispatch(actions.previousTx()), - }), - ` ${props.index + 1} of ${unconfTxList.length} `, - h('i.fa.fa-arrow-right.fa-lg.cursor-pointer', { - style: { - display: props.index + 1 === unconfTxList.length ? 'none' : 'inline-block', - }, - onClick: () => props.dispatch(actions.nextTx()), - }), - ]), - - warningIfExists(props.warning), - - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - - currentTxView({ - // Properties - txData: txData, - key: txData.id, - selectedAddress: props.selectedAddress, - accounts: props.accounts, - identities: props.identities, - conversionRate, - currentCurrency, - blockGasLimit, - // Actions - buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), - sendTransaction: this.sendTransaction.bind(this), - cancelTransaction: this.cancelTransaction.bind(this, txData), - signMessage: this.signMessage.bind(this, txData), - signPersonalMessage: this.signPersonalMessage.bind(this, txData), - cancelMessage: this.cancelMessage.bind(this, txData), - cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), - }), - - ]), - ]) - ) -} - -function currentTxView (opts) { - log.info('rendering current tx view') - const { txData } = opts - const { txParams, msgParams, type } = txData - - if (txParams) { - log.debug('txParams detected, rendering pending tx') - return h(PendingTx, opts) - } else if (msgParams) { - log.debug('msgParams detected, rendering pending msg') - - if (type === 'eth_sign') { - log.debug('rendering eth_sign message') - return h(PendingMsg, opts) - } else if (type === 'personal_sign') { - log.debug('rendering personal_sign message') - return h(PendingPersonalMsg, opts) - } - } -} - -ConfirmTxScreen.prototype.buyEth = function (address, event) { - event.preventDefault() - this.props.dispatch(actions.buyEthView(address)) -} - -ConfirmTxScreen.prototype.sendTransaction = function (txData, event) { - this.stopPropagation(event) - this.props.dispatch(actions.updateAndApproveTx(txData)) -} - -ConfirmTxScreen.prototype.cancelTransaction = function (txData, event) { - this.stopPropagation(event) - event.preventDefault() - this.props.dispatch(actions.cancelTx(txData)) -} - -ConfirmTxScreen.prototype.signMessage = function (msgData, event) { - log.info('conf-tx.js: signing message') - var params = msgData.msgParams - params.metamaskId = msgData.id - this.stopPropagation(event) - this.props.dispatch(actions.signMsg(params)) -} - -ConfirmTxScreen.prototype.stopPropagation = function (event) { - if (event.stopPropagation) { - event.stopPropagation() - } -} - -ConfirmTxScreen.prototype.signPersonalMessage = function (msgData, event) { - log.info('conf-tx.js: signing personal message') - var params = msgData.msgParams - params.metamaskId = msgData.id - this.stopPropagation(event) - this.props.dispatch(actions.signPersonalMsg(params)) -} - -ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) { - log.info('canceling message') - this.stopPropagation(event) - this.props.dispatch(actions.cancelMsg(msgData)) -} - -ConfirmTxScreen.prototype.cancelPersonalMessage = function (msgData, event) { - log.info('canceling personal message') - this.stopPropagation(event) - this.props.dispatch(actions.cancelPersonalMsg(msgData)) -} - -ConfirmTxScreen.prototype.goHome = function (event) { - this.stopPropagation(event) - this.props.dispatch(actions.goHome()) -} - -function warningIfExists (warning) { - if (warning && - // Do not display user rejections on this screen: - warning.indexOf('User denied transaction signature') === -1) { - return h('.error', { - style: { - margin: 'auto', - }, - }, warning) - } -} diff --git a/ui/responsive/app/config.js b/ui/responsive/app/config.js deleted file mode 100644 index 62785c49b..000000000 --- a/ui/responsive/app/config.js +++ /dev/null @@ -1,211 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') -const currencies = require('./conversion.json').rows -const validUrl = require('valid-url') -const copyToClipboard = require('copy-to-clipboard') - -module.exports = connect(mapStateToProps)(ConfigScreen) - -function mapStateToProps (state) { - return { - metamask: state.metamask, - warning: state.appState.warning, - } -} - -inherits(ConfigScreen, Component) -function ConfigScreen () { - Component.call(this) -} - -ConfigScreen.prototype.render = function () { - var state = this.props - var metamaskState = state.metamask - var warning = state.warning - - return ( - h('.flex-column.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: (event) => { - state.dispatch(actions.goHome()) - }, - }), - h('h2.page-subtitle', 'Settings'), - ]), - - h('.error', { - style: { - display: warning ? 'block' : 'none', - padding: '0 20px', - textAlign: 'center', - }, - }, warning), - - // conf view - h('.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-space-around', { - style: { - padding: '20px', - }, - }, [ - - currentProviderDisplay(metamaskState), - - h('div', { style: {display: 'flex'} }, [ - h('input#new_rpc', { - placeholder: 'New RPC URL', - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - onKeyPress (event) { - if (event.key === 'Enter') { - var element = event.target - var newRpc = element.value - rpcValidation(newRpc, state) - } - }, - }), - h('button', { - style: { - alignSelf: 'center', - }, - onClick (event) { - event.preventDefault() - var element = document.querySelector('input#new_rpc') - var newRpc = element.value - rpcValidation(newRpc, state) - }, - }, 'Save'), - ]), - - h('hr.horizontal-line'), - - currentConversionInformation(metamaskState, state), - - h('hr.horizontal-line'), - - h('div', { - style: { - marginTop: '20px', - }, - }, [ - h('p', { - style: { - fontFamily: 'Montserrat Light', - fontSize: '13px', - }, - }, `State logs contain your public account addresses and sent transactions.`), - h('br'), - h('button', { - style: { - alignSelf: 'center', - }, - onClick (event) { - copyToClipboard(window.logState()) - }, - }, 'Copy State Logs'), - ]), - - h('hr.horizontal-line'), - - h('div', { - style: { - marginTop: '20px', - }, - }, [ - h('button', { - style: { - alignSelf: 'center', - }, - onClick (event) { - event.preventDefault() - state.dispatch(actions.revealSeedConfirmation()) - }, - }, 'Reveal Seed Words'), - ]), - - ]), - ]), - ]) - ) -} - -function rpcValidation (newRpc, state) { - if (validUrl.isWebUri(newRpc)) { - state.dispatch(actions.setRpcTarget(newRpc)) - } else { - var appendedRpc = `http://${newRpc}` - if (validUrl.isWebUri(appendedRpc)) { - state.dispatch(actions.displayWarning('URIs require the appropriate HTTP/HTTPS prefix.')) - } else { - state.dispatch(actions.displayWarning('Invalid RPC URI')) - } - } -} - -function currentConversionInformation (metamaskState, state) { - var currentCurrency = metamaskState.currentCurrency - var conversionDate = metamaskState.conversionDate - return h('div', [ - h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, 'Current Conversion'), - h('span', {style: { fontWeight: 'bold', paddingRight: '10px', fontSize: '13px'}}, `Updated ${Date(conversionDate)}`), - h('select#currentCurrency', { - onChange (event) { - event.preventDefault() - var element = document.getElementById('currentCurrency') - var newCurrency = element.value - state.dispatch(actions.setCurrentCurrency(newCurrency)) - }, - defaultValue: currentCurrency, - }, currencies.map((currency) => { - return h('option', {key: currency.code, value: currency.code}, `${currency.code} - ${currency.name}`) - }) - ), - ]) -} - -function currentProviderDisplay (metamaskState) { - var provider = metamaskState.provider - var title, value - - switch (provider.type) { - - case 'mainnet': - title = 'Current Network' - value = 'Main Ethereum Network' - break - - case 'ropsten': - title = 'Current Network' - value = 'Ropsten Test Network' - break - - case 'kovan': - title = 'Current Network' - value = 'Kovan Test Network' - break - - case 'rinkeby': - title = 'Current Network' - value = 'Rinkeby Test Network' - break - - default: - title = 'Current RPC' - value = metamaskState.provider.rpcTarget - } - - return h('div', [ - h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, title), - h('span', value), - ]) -} diff --git a/ui/responsive/app/conversion.json b/ui/responsive/app/conversion.json deleted file mode 100644 index 155ffc4fc..000000000 --- a/ui/responsive/app/conversion.json +++ /dev/null @@ -1,207 +0,0 @@ -{ - "rows": [ - { - "code": "REP", - "name": "Augur", - "statuses": [ - "primary" - ] - }, - { - "code": "BCN", - "name": "Bytecoin", - "statuses": [ - "primary" - ] - }, - { - "code": "BTC", - "name": "Bitcoin", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "BTS", - "name": "BitShares", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "BLK", - "name": "Blackcoin", - "statuses": [ - "primary" - ] - }, - { - "code": "GBP", - "name": "British Pound Sterling", - "statuses": [ - "secondary" - ] - }, - { - "code": "CAD", - "name": "Canadian Dollar", - "statuses": [ - "secondary" - ] - }, - { - "code": "CNY", - "name": "Chinese Yuan", - "statuses": [ - "secondary" - ] - }, - { - "code": "DSH", - "name": "Dashcoin", - "statuses": [ - "primary" - ] - }, - { - "code": "DOGE", - "name": "Dogecoin", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "ETC", - "name": "Ethereum Classic", - "statuses": [ - "primary" - ] - }, - { - "code": "EUR", - "name": "Euro", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "GNO", - "name": "GNO", - "statuses": [ - "primary" - ] - }, - { - "code": "GNT", - "name": "GNT", - "statuses": [ - "primary" - ] - }, - { - "code": "JPY", - "name": "Japanese Yen", - "statuses": [ - "secondary" - ] - }, - { - "code": "LTC", - "name": "Litecoin", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "MAID", - "name": "MaidSafeCoin", - "statuses": [ - "primary" - ] - }, - { - "code": "XEM", - "name": "NEM", - "statuses": [ - "primary" - ] - }, - { - "code": "XLM", - "name": "Stellar", - "statuses": [ - "primary" - ] - }, - { - "code": "XMR", - "name": "Monero", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "XRP", - "name": "Ripple", - "statuses": [ - "primary" - ] - }, - { - "code": "RUR", - "name": "Ruble", - "statuses": [ - "secondary" - ] - }, - { - "code": "STEEM", - "name": "Steem", - "statuses": [ - "primary" - ] - }, - { - "code": "STRAT", - "name": "STRAT", - "statuses": [ - "primary" - ] - }, - { - "code": "UAH", - "name": "Ukrainian Hryvnia", - "statuses": [ - "secondary" - ] - }, - { - "code": "USD", - "name": "US Dollar", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "WAVES", - "name": "WAVES", - "statuses": [ - "primary" - ] - }, - { - "code": "ZEC", - "name": "Zcash", - "statuses": [ - "primary" - ] - } - ] -} diff --git a/ui/responsive/app/css/debug.css b/ui/responsive/app/css/debug.css deleted file mode 100644 index 3e125bcd4..000000000 --- a/ui/responsive/app/css/debug.css +++ /dev/null @@ -1,21 +0,0 @@ -/* -debug / dev -*/ - -#app-content { - border: 2px solid green; -} - -#design-container { - position: absolute; - left: 360px; - top: -42px; - width: calc(100vw - 360px); - height: 100vh; - overflow: scroll; -} - -#design-container img { - width: 2000px; - margin-right: 600px; -} \ No newline at end of file diff --git a/ui/responsive/app/css/fonts.css b/ui/responsive/app/css/fonts.css deleted file mode 100644 index 3b9f581b9..000000000 --- a/ui/responsive/app/css/fonts.css +++ /dev/null @@ -1,36 +0,0 @@ -@import url(https://fonts.googleapis.com/css?family=Roboto:300,500); -@import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css); - -@font-face { - font-family: 'Montserrat Regular'; - src: url('/fonts/Montserrat/Montserrat-Regular.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-Regular.ttf') format('truetype'); - font-weight: normal; - font-style: normal; - font-size: 'small'; - -} - -@font-face { - font-family: 'Montserrat Bold'; - src: url('/fonts/Montserrat/Montserrat-Bold.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-Bold.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'Montserrat Light'; - src: url('/fonts/Montserrat/Montserrat-Light.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-Light.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'Montserrat UltraLight'; - src: url('/fonts/Montserrat/Montserrat-UltraLight.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-UltraLight.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} diff --git a/ui/responsive/app/css/index.css b/ui/responsive/app/css/index.css deleted file mode 100644 index c82c1b21b..000000000 --- a/ui/responsive/app/css/index.css +++ /dev/null @@ -1,674 +0,0 @@ -/* -faint orange (textfield shades) #FAF6F0 -light orange (button shades): #F5C26D -dark orange (text): #F5A623 -borders/font/any gray: #4A4A4A -*/ - -/* -application specific styles -*/ - -* { - box-sizing: border-box; -} - -html, body { - font-family: 'Montserrat Regular', Arial; - color: #4D4D4D; - font-weight: 300; - line-height: 1.4em; - background: #F7F7F7; - width: 100%; - height: 100%; - margin: 0; - padding: 0; -} - -.css-transition-group { - flex: 1; -} - -input:focus, textarea:focus { - outline: none; -} - -#app-content { - overflow-x: hidden; - min-width: 357px; -} - -button, input[type="submit"] { - font-family: 'Montserrat Bold'; - outline: none; - cursor: pointer; - padding: 8px 12px; - border: none; - color: white; - transform-origin: center center; - transition: transform 50ms ease-in; - /* default orange */ - background: rgba(247, 134, 28, 1); - box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); -} - -.btn-green, input[type="submit"].btn-green { - background: rgba(106, 195, 96, 1); - box-shadow: 0px 3px 6px rgba(106, 195, 96, 0.36); -} - -.btn-red { - background: rgba(254, 35, 17, 1); - box-shadow: 0px 3px 6px rgba(254, 35, 17, 0.36); -} - -button[disabled], input[type="submit"][disabled] { - cursor: not-allowed; - background: rgba(197, 197, 197, 1); - box-shadow: 0px 3px 6px rgba(197, 197, 197, 0.36); -} - -button.spaced { - margin: 2px; -} - -button:not([disabled]):hover, input[type="submit"]:not([disabled]):hover { - transform: scale(1.1); -} -button:not([disabled]):active, input[type="submit"]:not([disabled]):active { - transform: scale(0.95); -} - -a { - text-decoration: none; - color: inherit; -} - -a:hover{ - color: #df6b0e; -} - -/* -app -*/ - -.active { - color: #909090; -} - -button.primary { - padding: 8px 12px; - background: #F7861C; - box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); - color: white; - font-size: 1.1em; - font-family: 'Montserrat Regular'; - text-transform: uppercase; -} - -button.btn-thin { - border: 1px solid; - border-color: #4D4D4D; - color: #4D4D4D; - background: rgb(255, 174, 41); - border-radius: 4px; - min-width: 200px; - margin: 12px 0; - padding: 6px; - font-size: 13px; -} - -.app-header { - padding: 6px 8px; -} - -.app-header h1 { - font-family: 'Montserrat Regular'; - text-transform: uppercase; - color: #AEAEAE; -} - -h2.page-subtitle { - font-family: 'Montserrat Regular'; - text-transform: uppercase; - color: #AEAEAE; - font-size: 1em; - margin: 12px; -} - -.app-primary { - -} - -.app-footer { - padding-bottom: 10px; - align-items: center; -} - -.identicon { - height: 46px; - width: 46px; - background-size: cover; - border-radius: 100%; - border: 3px solid gray; -} - -textarea.twelve-word-phrase { - padding: 12px; - width: 300px; - height: 140px; - font-size: 16px; - background: white; - resize: none; -} - -.network-indicator { - display: flex; - align-items: center; - font-size: 0.6em; - -} - -.network-name { - width: 5.2em; - line-height: 9px; - text-rendering: geometricPrecision; -} - -.check { - margin-left: 7px; - color: #F7861C; - flex: 1 0 auto; - display: flex; - justify-content: flex-end; -} -/* -app sections -*/ - -/* initialize */ - -.initialize-screen hr { - width: 60px; - margin: 12px; - border-color: #F7861C; - border-style: solid; -} - -.initialize-screen label { - margin-top: 20px; -} - -.initialize-screen button.create-vault { - margin-top: 40px; -} - -.initialize-screen .warning { - font-size: 14px; - margin: 0 16px; -} - -/* unlock */ -.error { - color: #E20202; -} - -.warning { - color: #FFAE00; -} - -.lock { - width: 50px; - height: 50px; -} - -.lock.locked { - transform: scale(1.5); - opacity: 0.0; - transition: opacity 400ms ease-in, transform 400ms ease-in; -} -.lock.unlocked { - transform: scale(1); - opacity: 1; - transition: opacity 500ms ease-out, transform 500ms ease-out, background 200ms ease-in; -} - -.lock.locked .lock-top { - transform: scaleX(1) translateX(0); - transition: transform 250ms ease-in; -} -.lock.unlocked .lock-top { - transform: scaleX(-1) translateX(-12px); - transition: transform 250ms ease-in; -} -.lock.unlocked:hover { - border-radius: 4px; - background: #e5e5e5; - border: 1px solid #b1b1b1; -} -.lock.unlocked:active { - background: #c3c3c3; -} - -.section-title .fa-arrow-left { - margin: -2px 8px 0px -8px; -} - -.unlock-screen #metamask-mascot-container { - margin-top: 24px; -} - -.unlock-screen h1 { - margin-top: -28px; - margin-bottom: 42px; -} - -.unlock-screen input[type=password] { - width: 260px; - /*height: 36px; - margin-bottom: 24px; - padding: 8px;*/ -} - -.sizing-input{ - font-size: 14px; - height: 30px; - padding-left: 5px; -} -.editable-label{ - display: flex; -} -/* Webkit */ -.unlock-screen input::-webkit-input-placeholder { - text-align: center; - font-size: 1.2em; -} -/* Firefox 18- */ -.unlock-screen input:-moz-placeholder { - text-align: center; - font-size: 1.2em; -} -/* Firefox 19+ */ -.unlock-screen input::-moz-placeholder { - text-align: center; - font-size: 1.2em; -} -/* IE */ -.unlock-screen input:-ms-input-placeholder { - text-align: center; - font-size: 1.2em; -} - -input.large-input, textarea.large-input { - /*margin-bottom: 24px;*/ - padding: 8px; -} - -input.large-input { - height: 36px; -} - -.letter-spacey { - letter-spacing: 0.1em; -} - - - -/* accounts */ - -.accounts-section { - margin: 0 0px; -} - -.accounts-section .horizontal-line { - margin: 0px 18px; -} - -.accounts-list-option { - height: 120px; -} - -.accounts-list-option .identicon-wrapper { - width: 100px; -} - -.unconftx-link { - margin-top: 24px; - cursor: pointer; -} - -.unconftx-link .fa-arrow-right { - margin: 0px -8px 0px 8px; -} - -/* identity panel */ - -.identity-panel { - font-weight: 500; -} - -.identity-panel .identicon-wrapper { - margin: 4px; - margin-top: 8px; - display: flex; - align-items: center; -} - -.identity-panel .identicon-wrapper span { - margin: 0 auto; -} - -.identity-panel .identity-data { - margin: 8px 8px 8px 18px; -} - -.identity-panel i { - margin-top: 32px; - margin-right: 6px; - color: #B9B9B9; -} - -.identity-panel .arrow-right { - padding-left: 18px; - width: 42px; - min-width: 18px; - height: 100%; -} - -.identity-copy.flex-column { - flex: 0.25 0 auto; - justify-content: center; -} - -/* accounts screen */ - -.identity-section { - -} - -.identity-section .identity-panel { - background: #E9E9E9; - border-bottom: 1px solid #B1B1B1; - cursor: pointer; -} - -.identity-section .identity-panel.selected { - background: white; - color: #F3C83E; -} - -.identity-section .identity-panel.selected .identicon { - border-color: orange; -} - -.identity-section .accounts-list-option:hover, -.identity-section .accounts-list-option.selected { - background:white; -} - -/* account detail screen */ - -.account-detail-section { - display: flex; - flex-wrap: wrap; -} -.name-label{ - -} - -.unapproved-tx-icon { - height: 16px; - width: 16px; - background: rgb(47, 174, 244); - border-color: #AEAEAE; - border-radius: 13px; -} - -.edit-text { - height: 100%; - visibility: hidden; -} -.editing-label { - display: flex; - justify-content: flex-start; - margin-left: 50px; - margin-bottom: 2px; - font-size: 11px; - text-rendering: geometricPrecision; - color: #F7861C; -} -.name-label:hover .edit-text { - visibility: visible; -} -/* tx confirm */ - -.unconftx-section input[type=password] { - height: 22px; - padding: 2px; - margin: 12px; - margin-bottom: 24px; - border-radius: 4px; - border: 2px solid #F3C83E; - background: #FAF6F0; -} - -/* Send Screen */ - -.send-screen { - -} - -.send-screen section { - margin: 8px 16px; -} - -.send-screen input { - width: 100%; - font-size: 12px; -} - -/* Ether Balance Widget */ - -.ether-balance-amount { - color: #F7861C; -} - -.ether-balance-label { - color: #ABA9AA; -} - -/* Info screen */ -.info-gray{ - font-family: 'Montserrat Regular'; - text-transform: uppercase; - color: #AEAEAE; -} - -.icon-size{ - width: 20px; -} - -.info{ - font-family: 'Montserrat Regular', Arial; - padding-bottom: 10px; - display: inline-block; - padding-left: 5px; -} - -/* buy eth warning screen */ -.custom-radios { - justify-content: space-around; - align-items: center; -} - - -.custom-radio-selected { - width: 17px; - height: 17px; - border: solid; - border-style: double; - border-radius: 15px; - border-width: 5px; - background: rgba(247, 134, 28, 1); - border-color: #F7F7F7; -} - -.custom-radio-inactive { - width: 14px; - height: 14px; - border: solid; - border-width: 1px; - border-radius: 24px; - border-color: #AEAEAE; -} - -.radio-titles { - color: rgba(247, 134, 28, 1); -} - -.radio-titles-subtext { - -} - -.selected-exchange { - -} - -.buy-radio { - -} - -.eth-warning{ - transition: opacity 400ms ease-in, transform 400ms ease-in; -} - -.buy-subview{ - transition: opacity 400ms ease-in, transform 400ms ease-in; -} - -.input-container:hover .edit-text{ - visibility: visible; -} - -.buy-inputs{ - font-family: 'Montserrat Light'; - font-size: 13px; - height: 20px; - background: transparent; - box-sizing: border-box; - border: solid; - border-color: transparent; - border-width: 0.5px; - border-radius: 2px; - -} -.input-container:hover .buy-inputs{ - box-sizing: inherit; - border: solid; - border-color: #F7861C; - border-width: 0.5px; - border-radius: 2px; -} - -.buy-inputs:focus{ - border: solid; - border-color: #F7861C; - border-width: 0.5px; - border-radius: 2px; -} - -.activeForm { - background: #F7F7F7; - border: none; - border-radius: 8px 8px 0px 0px; - width: 50%; - text-align: center; - padding-bottom: 4px; - -} - -.inactiveForm { - border: none; - border-radius: 8px 8px 0px 0px; - width: 50%; - text-align: center; - padding-bottom: 4px; -} - -.ex-coins { - font-family: 'Montserrat Regular'; - text-transform: uppercase; - text-align: center; - font-size: 33px; - width: 118px; - height: 42px; - padding: 1px; - color: #4D4D4D; -} - -.marketinfo{ - font-family: 'Montserrat light'; - color: #AEAEAE; - font-size: 15px; - line-height: 17px; -} - -#fromCoin::-webkit-calendar-picker-indicator { - display: none; -} - -#coinList { - width: 400px; - height: 500px; - overflow: scroll; -} - -.icon-control .fa-refresh{ - visibility: hidden; -} - -.icon-control:hover .fa-refresh{ - visibility: visible; -} - -.icon-control:hover .fa-chevron-right{ - visibility: hidden; -} - -.inactive { - color: #AEAEAE; -} - -.inactive button{ - background: #AEAEAE; - color: white; -} - -.ellip-address { - overflow: hidden; - text-overflow: ellipsis; - width: 5em; - font-size: 14px; - font-family: "Montserrat Light"; - margin-left: 5px; -} - -.qr-header { - font-size: 25px; - margin-top: 40px; -} - -.qr-message { - font-size: 12px; - color: #F7861C; -} - -div.message-container > div:first-child { - margin-top: 18px; - font-size: 15px; - color: #4D4D4D; -} - -.pop-hover:hover { - transform: scale(1.1); -} diff --git a/ui/responsive/app/css/lib.css b/ui/responsive/app/css/lib.css deleted file mode 100644 index 910a24ee2..000000000 --- a/ui/responsive/app/css/lib.css +++ /dev/null @@ -1,268 +0,0 @@ -/* color */ - -.color-orange { - color: #F7861C; -} - -.color-forest { - color: #0A5448; -} - -/* lib */ - -.full-width { - width: 100%; -} - -.full-height { - height: 100%; -} - -.flex-column { - display: flex; - flex-direction: column; -} - -.space-between { - justify-content: space-between; -} - -.space-around { - justify-content: space-around; -} - -.flex-column-bottom { - display: flex; - flex-direction: column-reverse; -} - -.flex-row { - display: flex; - flex-direction: row; -} - -.flex-space-between { - justify-content: space-between; -} - -.flex-space-around { - justify-content: space-around; -} - -.flex-right { - display: flex; - flex-direction: row; - justify-content: flex-end; -} - -.flex-left { - display: flex; - flex-direction: row; - justify-content: flex-start; -} - -.flex-fixed { - flex: none; -} - -.flex-basis-auto { - flex-basis: auto; -} - -.flex-grow { - flex: 1 1 auto; -} - -.flex-wrap { - flex-wrap: wrap; -} - -.flex-center { - display: flex; - justify-content: center; - align-items: center; -} - -.flex-justify-center { - justify-content: center; -} - -.flex-align-center { - align-items: center; -} - -.flex-self-end { - align-self: flex-end; -} - -.flex-self-stretch { - align-self: stretch; -} - -.flex-vertical { - flex-direction: column; -} - -.z-bump { - z-index: 1; -} - -.select-none { - cursor: inherit; - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; -} - -.pointer { - cursor: pointer; -} -.cursor-pointer { - cursor: pointer; - transform-origin: center center; - transition: transform 50ms ease-in-out; -} -.cursor-pointer:hover { - transform: scale(1.1); -} -.cursor-pointer:active { - transform: scale(0.95); -} - -.cursor-disabled { - cursor: not-allowed; -} - -.margin-bottom-sml { - margin-bottom: 20px; -} - -.margin-bottom-med { - margin-bottom: 40px; -} - -.margin-right-left { - margin: 0 20px; -} - -.bold { - font-weight: bold; -} - -.text-transform-uppercase { - text-transform: uppercase; -} - -.font-small { - font-size: 12px; -} - -.font-medium { - font-size: 1.2em; -} - -hr.horizontal-line { - display: block; - height: 1px; - border: 0; - border-top: 1px solid #ccc; - margin: 1em 0; - padding: 0; -} - -.hover-white:hover { - background: white; -} - -.red-dot { - background: #E91550; - color: white; - border-radius: 10px; -} - -.diamond { - transform: rotate(45deg); - background: #038789; -} - -.hollow-diamond { - transform: rotate(45deg); - border: 3px solid #690496; -} - -.golden-square { - background: #EBB33F; -} - -.pending-dot { - background: red; - left: 14px; - top: 14px; - color: white; - border-radius: 10px; - height: 20px; - min-width: 20px; - position: relative; - display: flex; - align-items: center; - justify-content: center; - padding: 4px; - z-index: 1; -} - -.keyring-label { - z-index: 1; - font-size: 11px; - background: rgba(255,0,0,0.8); - bottom: -47px; - color: white; - border-radius: 10px; - height: 20px; - min-width: 20px; - position: relative; - display: flex; - align-items: center; - justify-content: center; - padding: 4px; -} - -.ether-balance { - display: flex; - align-items: center; -} - -.menu-icon { - display: inline-block; - height: 9px; - min-width: 9px; - margin: 13px; -} -.ether-icon { - background: rgb(0, 163, 68); - border-radius: 20px; -} -.testnet-icon { - background: #2465E1; -} - -.drop-menu-item { - display: flex; - align-items: center; -} - -.invisible { - visibility: hidden; -} - -.one-line-concat { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.critical-error { - text-align: center; - margin-top: 20px; - color: red; -} diff --git a/ui/responsive/app/css/reset.css b/ui/responsive/app/css/reset.css deleted file mode 100644 index 9ce89e8bc..000000000 --- a/ui/responsive/app/css/reset.css +++ /dev/null @@ -1,48 +0,0 @@ -/* http://meyerweb.com/eric/tools/css/reset/ - v2.0 | 20110126 - License: none (public domain) -*/ - -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; -} \ No newline at end of file diff --git a/ui/responsive/app/css/transitions.css b/ui/responsive/app/css/transitions.css deleted file mode 100644 index 393a944f9..000000000 --- a/ui/responsive/app/css/transitions.css +++ /dev/null @@ -1,42 +0,0 @@ -/* universal */ -.app-primary .main-enter { - position: absolute; - width: 100%; -} - -/* center position */ -.app-primary.from-right .main-enter-active, -.app-primary.from-left .main-enter-active { - overflow-x: hidden; - transform: translateX(0px); - transition: transform 300ms ease-in; -} - -/* exited positions */ -.app-primary.from-left .main-leave-active { - transform: translateX(360px); - transition: transform 300ms ease-in; -} -.app-primary.from-right .main-leave-active { - transform: translateX(-360px); - transition: transform 300ms ease-in; -} - -/* loader transitions */ -.loader-enter, .loader-leave-active { - opacity: 0.0; - transition: opacity 150 ease-in; -} -.loader-enter-active, .loader-leave { - opacity: 1.0; - transition: opacity 150 ease-in; -} - -/* entering positions */ -.app-primary.from-right .main-enter:not(.main-enter-active) { - transform: translateX(360px); -} -.app-primary.from-left .main-enter:not(.main-enter-active) { - transform: translateX(-360px); -} - diff --git a/ui/responsive/app/first-time/init-menu.js b/ui/responsive/app/first-time/init-menu.js deleted file mode 100644 index cc7c51bd3..000000000 --- a/ui/responsive/app/first-time/init-menu.js +++ /dev/null @@ -1,179 +0,0 @@ -const inherits = require('util').inherits -const EventEmitter = require('events').EventEmitter -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const Mascot = require('../components/mascot') -const actions = require('../actions') -const Tooltip = require('../components/tooltip') -const getCaretCoordinates = require('textarea-caret') - -module.exports = connect(mapStateToProps)(InitializeMenuScreen) - -inherits(InitializeMenuScreen, Component) -function InitializeMenuScreen () { - Component.call(this) - this.animationEventEmitter = new EventEmitter() -} - -function mapStateToProps (state) { - return { - // state from plugin - currentView: state.appState.currentView, - warning: state.appState.warning, - } -} - -InitializeMenuScreen.prototype.render = function () { - var state = this.props - - switch (state.currentView.name) { - - default: - return this.renderMenu(state) - - } -} - -// InitializeMenuScreen.prototype.componentDidMount = function(){ -// document.getElementById('password-box').focus() -// } - -InitializeMenuScreen.prototype.renderMenu = function (state) { - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - h(Mascot, { - animationEventEmitter: this.animationEventEmitter, - }), - - h('h1', { - style: { - fontSize: '1.3em', - textTransform: 'uppercase', - color: '#7F8082', - marginBottom: 10, - }, - }, 'MetaMask'), - - - h('div', [ - h('h3', { - style: { - fontSize: '0.8em', - color: '#7F8082', - display: 'inline', - }, - }, 'Encrypt your new DEN'), - - h(Tooltip, { - title: 'Your DEN is your password-encrypted storage within MetaMask.', - }, [ - h('i.fa.fa-question-circle.pointer', { - style: { - fontSize: '18px', - position: 'relative', - color: 'rgb(247, 134, 28)', - top: '2px', - marginLeft: '4px', - }, - }), - ]), - ]), - - h('span.in-progress-notification', state.warning), - - // password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box', - placeholder: 'New Password (min 8 chars)', - onInput: this.inputChanged.bind(this), - style: { - width: 260, - marginTop: 12, - }, - }), - - // confirm password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box-confirm', - placeholder: 'Confirm Password', - onKeyPress: this.createVaultOnEnter.bind(this), - onInput: this.inputChanged.bind(this), - style: { - width: 260, - marginTop: 16, - }, - }), - - - h('button.primary', { - onClick: this.createNewVaultAndKeychain.bind(this), - style: { - margin: 12, - }, - }, 'Create'), - - h('.flex-row.flex-center.flex-grow', [ - h('p.pointer', { - onClick: this.showRestoreVault.bind(this), - style: { - fontSize: '0.8em', - color: 'rgb(247, 134, 28)', - textDecoration: 'underline', - }, - }, 'Import Existing DEN'), - ]), - - ]) - ) -} - -InitializeMenuScreen.prototype.createVaultOnEnter = function (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.createNewVaultAndKeychain() - } -} - -InitializeMenuScreen.prototype.componentDidMount = function () { - document.getElementById('password-box').focus() -} - -InitializeMenuScreen.prototype.showRestoreVault = function () { - this.props.dispatch(actions.showRestoreVault()) -} - -InitializeMenuScreen.prototype.createNewVaultAndKeychain = function () { - var passwordBox = document.getElementById('password-box') - var password = passwordBox.value - var passwordConfirmBox = document.getElementById('password-box-confirm') - var passwordConfirm = passwordConfirmBox.value - - if (password.length < 8) { - this.warning = 'password not long enough' - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - if (password !== passwordConfirm) { - this.warning = 'passwords don\'t match' - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - - this.props.dispatch(actions.createNewVaultAndKeychain(password)) -} - -InitializeMenuScreen.prototype.inputChanged = function (event) { - // tell mascot to look at page action - var element = event.target - var boundingRect = element.getBoundingClientRect() - var coordinates = getCaretCoordinates(element, element.selectionEnd) - this.animationEventEmitter.emit('point', { - x: boundingRect.left + coordinates.left - element.scrollLeft, - y: boundingRect.top + coordinates.top - element.scrollTop, - }) -} diff --git a/ui/responsive/app/img/identicon-tardigrade.png b/ui/responsive/app/img/identicon-tardigrade.png deleted file mode 100644 index 1742a32b8..000000000 Binary files a/ui/responsive/app/img/identicon-tardigrade.png and /dev/null differ diff --git a/ui/responsive/app/img/identicon-walrus.png b/ui/responsive/app/img/identicon-walrus.png deleted file mode 100644 index d58fae912..000000000 Binary files a/ui/responsive/app/img/identicon-walrus.png and /dev/null differ diff --git a/ui/responsive/app/info.js b/ui/responsive/app/info.js deleted file mode 100644 index e8470de97..000000000 --- a/ui/responsive/app/info.js +++ /dev/null @@ -1,154 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') - -module.exports = connect(mapStateToProps)(InfoScreen) - -function mapStateToProps (state) { - return {} -} - -inherits(InfoScreen, Component) -function InfoScreen () { - Component.call(this) -} - -InfoScreen.prototype.render = function () { - const state = this.props - const version = global.platform.getVersion() - - return ( - h('.flex-column.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: (event) => { - state.dispatch(actions.goHome()) - }, - }), - h('h2.page-subtitle', 'Info'), - ]), - - // main view - h('.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-space-around', { - style: { - padding: '20px', - }, - }, [ - // current version number - - h('.info.info-gray', [ - h('div', 'Metamask'), - h('div', { - style: { - marginBottom: '10px', - }, - }, `Version: ${version}`), - ]), - - h('div', { - style: { - marginBottom: '5px', - }}, - [ - h('div', [ - h('a', { - href: 'https://metamask.io/privacy.html', - target: '_blank', - onClick (event) { this.navigateTo(event.target.href) }, - }, [ - h('div.info', 'Privacy Policy'), - ]), - ]), - h('div', [ - h('a', { - href: 'https://metamask.io/terms.html', - target: '_blank', - onClick (event) { this.navigateTo(event.target.href) }, - }, [ - h('div.info', 'Terms of Use'), - ]), - ]), - h('div', [ - h('a', { - href: 'https://metamask.io/attributions.html', - target: '_blank', - onClick (event) { this.navigateTo(event.target.href) }, - }, [ - h('div.info', 'Attributions'), - ]), - ]), - ] - ), - - h('hr', { - style: { - margin: '10px 0 ', - width: '7em', - }, - }), - - h('div', { - style: { - paddingLeft: '30px', - }}, - [ - h('div.fa.fa-github', [ - h('a.info', { - href: 'https://github.com/MetaMask/faq', - target: '_blank', - }, 'Need Help? Read our FAQ!'), - ]), - h('div', [ - h('a', { - href: 'https://metamask.io/', - target: '_blank', - }, [ - h('img.icon-size', { - src: 'images/icon-128.png', - style: { - // IE6-9 - filter: 'grayscale(100%)', - // Microsoft Edge and Firefox 35+ - WebkitFilter: 'grayscale(100%)', - }, - }), - h('div.info', 'Visit our web site'), - ]), - ]), - h('div.fa.fa-slack', [ - h('a.info', { - href: 'http://slack.metamask.io', - target: '_blank', - }, 'Join the conversation on Slack'), - ]), - - h('div.fa.fa-twitter', [ - h('a.info', { - href: 'https://twitter.com/metamask_io', - target: '_blank', - }, 'Follow us on Twitter'), - ]), - - h('div.fa.fa-envelope', [ - h('a.info', { - target: '_blank', - style: { width: '85vw' }, - href: 'mailto:help@metamask.io?subject=Feedback', - }, 'Email us!'), - ]), - ]), - ]), - ]), - ]) - ) -} - -InfoScreen.prototype.navigateTo = function (url) { - global.platform.openWindow({ url }) -} - diff --git a/ui/responsive/app/keychains/hd/create-vault-complete.js b/ui/responsive/app/keychains/hd/create-vault-complete.js deleted file mode 100644 index c32751fff..000000000 --- a/ui/responsive/app/keychains/hd/create-vault-complete.js +++ /dev/null @@ -1,76 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('../../actions') - -module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen) - -inherits(CreateVaultCompleteScreen, Component) -function CreateVaultCompleteScreen () { - Component.call(this) -} - -function mapStateToProps (state) { - return { - seed: state.appState.currentView.seedWords, - cachedSeed: state.metamask.seedWords, - } -} - -CreateVaultCompleteScreen.prototype.render = function () { - var state = this.props - var seed = state.seed || state.cachedSeed || '' - - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - // // subtitle and nav - // h('.section-title.flex-row.flex-center', [ - // h('h2.page-subtitle', 'Vault Created'), - // ]), - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginTop: 36, - marginBottom: 8, - width: '100%', - fontSize: '20px', - padding: 6, - }, - }, [ - 'Vault Created', - ]), - - h('div', { - style: { - fontSize: '1em', - marginTop: '10px', - textAlign: 'center', - }, - }, [ - h('span.error', 'These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret.'), - ]), - - h('textarea.twelve-word-phrase', { - readOnly: true, - value: seed, - }), - - h('button.primary', { - onClick: () => this.confirmSeedWords(), - style: { - margin: '24px', - fontSize: '0.9em', - }, - }, 'I\'ve copied it somewhere safe'), - ]) - ) -} - -CreateVaultCompleteScreen.prototype.confirmSeedWords = function () { - this.props.dispatch(actions.confirmSeedWords()) -} diff --git a/ui/responsive/app/keychains/hd/recover-seed/confirmation.js b/ui/responsive/app/keychains/hd/recover-seed/confirmation.js deleted file mode 100644 index 4ccbec9fc..000000000 --- a/ui/responsive/app/keychains/hd/recover-seed/confirmation.js +++ /dev/null @@ -1,118 +0,0 @@ -const inherits = require('util').inherits - -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('../../../actions') - -module.exports = connect(mapStateToProps)(RevealSeedConfirmation) - -inherits(RevealSeedConfirmation, Component) -function RevealSeedConfirmation () { - Component.call(this) -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -RevealSeedConfirmation.prototype.render = function () { - const props = this.props - - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginBottom: 24, - width: '100%', - fontSize: '20px', - padding: 6, - }, - }, [ - 'Reveal Seed Words', - ]), - - h('.div', { - style: { - display: 'flex', - flexDirection: 'column', - padding: '20px', - justifyContent: 'center', - }, - }, [ - - h('h4', 'Do not recover your seed words in a public place! These words can be used to steal all your accounts.'), - - // confirmation - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box', - placeholder: 'Enter your password to confirm', - onKeyPress: this.checkConfirmation.bind(this), - style: { - width: 260, - marginTop: '12px', - }, - }), - - h('.flex-row.flex-space-between', { - style: { - marginTop: 30, - width: '50%', - }, - }, [ - // cancel - h('button.primary', { - onClick: this.goHome.bind(this), - }, 'CANCEL'), - - // submit - h('button.primary', { - onClick: this.revealSeedWords.bind(this), - }, 'OK'), - - ]), - - (props.warning) && ( - h('span.error', { - style: { - margin: '20px', - }, - }, props.warning.split('-')) - ), - - props.inProgress && ( - h('span.in-progress-notification', 'Generating Seed...') - ), - ]), - ]) - ) -} - -RevealSeedConfirmation.prototype.componentDidMount = function () { - document.getElementById('password-box').focus() -} - -RevealSeedConfirmation.prototype.goHome = function () { - this.props.dispatch(actions.showConfigPage(false)) -} - -// create vault - -RevealSeedConfirmation.prototype.checkConfirmation = function (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.revealSeedWords() - } -} - -RevealSeedConfirmation.prototype.revealSeedWords = function () { - var password = document.getElementById('password-box').value - this.props.dispatch(actions.requestRevealSeed(password)) -} diff --git a/ui/responsive/app/keychains/hd/restore-vault.js b/ui/responsive/app/keychains/hd/restore-vault.js deleted file mode 100644 index 06e51d9b3..000000000 --- a/ui/responsive/app/keychains/hd/restore-vault.js +++ /dev/null @@ -1,152 +0,0 @@ -const inherits = require('util').inherits -const PersistentForm = require('../../../lib/persistent-form') -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('../../actions') - -module.exports = connect(mapStateToProps)(RestoreVaultScreen) - -inherits(RestoreVaultScreen, PersistentForm) -function RestoreVaultScreen () { - PersistentForm.call(this) -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - forgottenPassword: state.appState.forgottenPassword, - } -} - -RestoreVaultScreen.prototype.render = function () { - var state = this.props - this.persistentFormParentId = 'restore-vault-form' - - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginBottom: 24, - width: '100%', - fontSize: '20px', - padding: 6, - }, - }, [ - 'Restore Vault', - ]), - - // wallet seed entry - h('h3', 'Wallet Seed'), - h('textarea.twelve-word-phrase.letter-spacey', { - dataset: { - persistentFormId: 'wallet-seed', - }, - placeholder: 'Enter your secret twelve word phrase here to restore your vault.', - }), - - // password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box', - placeholder: 'New Password (min 8 chars)', - dataset: { - persistentFormId: 'password', - }, - style: { - width: 260, - marginTop: 12, - }, - }), - - // confirm password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box-confirm', - placeholder: 'Confirm Password', - onKeyPress: this.createOnEnter.bind(this), - dataset: { - persistentFormId: 'password-confirmation', - }, - style: { - width: 260, - marginTop: 16, - }, - }), - - (state.warning) && ( - h('span.error.in-progress-notification', state.warning) - ), - - // submit - - h('.flex-row.flex-space-between', { - style: { - marginTop: 30, - width: '50%', - }, - }, [ - - // cancel - h('button.primary', { - onClick: this.showInitializeMenu.bind(this), - }, 'CANCEL'), - - // submit - h('button.primary', { - onClick: this.createNewVaultAndRestore.bind(this), - }, 'OK'), - - ]), - ]) - - ) -} - -RestoreVaultScreen.prototype.showInitializeMenu = function () { - if (this.props.forgottenPassword) { - this.props.dispatch(actions.backToUnlockView()) - } else { - this.props.dispatch(actions.showInitializeMenu()) - } -} - -RestoreVaultScreen.prototype.createOnEnter = function (event) { - if (event.key === 'Enter') { - this.createNewVaultAndRestore() - } -} - -RestoreVaultScreen.prototype.createNewVaultAndRestore = function () { - // check password - var passwordBox = document.getElementById('password-box') - var password = passwordBox.value - var passwordConfirmBox = document.getElementById('password-box-confirm') - var passwordConfirm = passwordConfirmBox.value - if (password.length < 8) { - this.warning = 'Password not long enough' - - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - if (password !== passwordConfirm) { - this.warning = 'Passwords don\'t match' - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - // check seed - var seedBox = document.querySelector('textarea.twelve-word-phrase') - var seed = seedBox.value.trim() - if (seed.split(' ').length !== 12) { - this.warning = 'seed phrases are 12 words long' - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - // submit - this.warning = null - this.props.dispatch(actions.displayWarning(this.warning)) - this.props.dispatch(actions.createNewVaultAndRestore(password, seed)) -} diff --git a/ui/responsive/app/new-keychain.js b/ui/responsive/app/new-keychain.js deleted file mode 100644 index cc9633166..000000000 --- a/ui/responsive/app/new-keychain.js +++ /dev/null @@ -1,29 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect - -module.exports = connect(mapStateToProps)(NewKeychain) - -function mapStateToProps (state) { - return {} -} - -inherits(NewKeychain, Component) -function NewKeychain () { - Component.call(this) -} - -NewKeychain.prototype.render = function () { - // const props = this.props - - return ( - h('div', { - style: { - background: 'blue', - }, - }, [ - h('h1', `Here's a list!!!!`), - ]) - ) -} diff --git a/ui/responsive/app/reducers.js b/ui/responsive/app/reducers.js deleted file mode 100644 index 11efca529..000000000 --- a/ui/responsive/app/reducers.js +++ /dev/null @@ -1,52 +0,0 @@ -const extend = require('xtend') - -// -// Sub-Reducers take in the complete state and return their sub-state -// -const reduceIdentities = require('./reducers/identities') -const reduceMetamask = require('./reducers/metamask') -const reduceApp = require('./reducers/app') - -window.METAMASK_CACHED_LOG_STATE = null - -module.exports = rootReducer - -function rootReducer (state, action) { - // clone - state = extend(state) - - if (action.type === 'GLOBAL_FORCE_UPDATE') { - return action.value - } - - // - // Identities - // - - state.identities = reduceIdentities(state, action) - - // - // MetaMask - // - - state.metamask = reduceMetamask(state, action) - - // - // AppState - // - - state.appState = reduceApp(state, action) - - window.METAMASK_CACHED_LOG_STATE = state - return state -} - -window.logState = function () { - var stateString = JSON.stringify(window.METAMASK_CACHED_LOG_STATE, removeSeedWords, 2) - console.log(stateString) - return stateString -} - -function removeSeedWords (key, value) { - return key === 'seedWords' ? undefined : value -} diff --git a/ui/responsive/app/reducers/app.js b/ui/responsive/app/reducers/app.js deleted file mode 100644 index 2fcc9bfe0..000000000 --- a/ui/responsive/app/reducers/app.js +++ /dev/null @@ -1,585 +0,0 @@ -const extend = require('xtend') -const actions = require('../actions') -const txHelper = require('../../lib/tx-helper') - -module.exports = reduceApp - - -function reduceApp (state, action) { - log.debug('App Reducer got ' + action.type) - // clone and defaults - const selectedAddress = state.metamask.selectedAddress - const hasUnconfActions = checkUnconfActions(state) - let name = 'accounts' - if (selectedAddress) { - name = 'accountDetail' - } - if (hasUnconfActions) { - log.debug('pending txs detected, defaulting to conf-tx view.') - name = 'confTx' - } - - var defaultView = { - name, - detailView: null, - context: selectedAddress, - } - - // confirm seed words - var seedWords = state.metamask.seedWords - var seedConfView = { - name: 'createVaultComplete', - seedWords, - } - - // default state - var appState = extend({ - shouldClose: false, - menuOpen: false, - currentView: seedWords ? seedConfView : defaultView, - accountDetail: { - subview: 'transactions', - }, - transForward: true, // Used to render transition direction - isLoading: false, // Used to display loading indicator - warning: null, // Used to display error text - }, state.appState) - - switch (action.type) { - - // transition methods - - case actions.TRANSITION_FORWARD: - return extend(appState, { - transForward: true, - }) - - case actions.TRANSITION_BACKWARD: - return extend(appState, { - transForward: false, - }) - - // intialize - - case actions.SHOW_CREATE_VAULT: - return extend(appState, { - currentView: { - name: 'createVault', - }, - transForward: true, - warning: null, - }) - - case actions.SHOW_RESTORE_VAULT: - return extend(appState, { - currentView: { - name: 'restoreVault', - }, - transForward: true, - forgottenPassword: true, - }) - - case actions.FORGOT_PASSWORD: - return extend(appState, { - currentView: { - name: 'restoreVault', - }, - transForward: false, - forgottenPassword: true, - }) - - case actions.SHOW_INIT_MENU: - return extend(appState, { - currentView: defaultView, - transForward: false, - }) - - case actions.SHOW_CONFIG_PAGE: - return extend(appState, { - currentView: { - name: 'config', - context: appState.currentView.context, - }, - transForward: action.value, - }) - - case actions.SHOW_ADD_TOKEN_PAGE: - return extend(appState, { - currentView: { - name: 'add-token', - context: appState.currentView.context, - }, - transForward: action.value, - }) - - case actions.SHOW_IMPORT_PAGE: - - return extend(appState, { - currentView: { - name: 'import-menu', - }, - transForward: true, - }) - - case actions.SHOW_INFO_PAGE: - return extend(appState, { - currentView: { - name: 'info', - context: appState.currentView.context, - }, - transForward: true, - }) - - case actions.CREATE_NEW_VAULT_IN_PROGRESS: - return extend(appState, { - currentView: { - name: 'createVault', - inProgress: true, - }, - transForward: true, - isLoading: true, - }) - - case actions.SHOW_NEW_VAULT_SEED: - return extend(appState, { - currentView: { - name: 'createVaultComplete', - seedWords: action.value, - }, - transForward: true, - isLoading: false, - }) - - case actions.NEW_ACCOUNT_SCREEN: - return extend(appState, { - currentView: { - name: 'new-account', - context: appState.currentView.context, - }, - transForward: true, - }) - - case actions.SHOW_SEND_PAGE: - return extend(appState, { - currentView: { - name: 'sendTransaction', - context: appState.currentView.context, - }, - transForward: true, - warning: null, - }) - - case actions.SHOW_NEW_KEYCHAIN: - return extend(appState, { - currentView: { - name: 'newKeychain', - context: appState.currentView.context, - }, - transForward: true, - }) - - // unlock - - case actions.UNLOCK_METAMASK: - return extend(appState, { - forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null, - detailView: {}, - transForward: true, - isLoading: false, - warning: null, - }) - - case actions.LOCK_METAMASK: - return extend(appState, { - currentView: defaultView, - transForward: false, - warning: null, - }) - - case actions.BACK_TO_INIT_MENU: - return extend(appState, { - warning: null, - transForward: false, - forgottenPassword: true, - currentView: { - name: 'InitMenu', - }, - }) - - case actions.BACK_TO_UNLOCK_VIEW: - return extend(appState, { - warning: null, - transForward: true, - forgottenPassword: false, - currentView: { - name: 'UnlockScreen', - }, - }) - // reveal seed words - - case actions.REVEAL_SEED_CONFIRMATION: - return extend(appState, { - currentView: { - name: 'reveal-seed-conf', - }, - transForward: true, - warning: null, - }) - - // accounts - - case actions.SET_SELECTED_ACCOUNT: - return extend(appState, { - activeAddress: action.value, - }) - - case actions.GO_HOME: - return extend(appState, { - currentView: extend(appState.currentView, { - name: 'accountDetail', - }), - accountDetail: { - subview: 'transactions', - accountExport: 'none', - privateKey: '', - }, - transForward: false, - warning: null, - }) - - case actions.SHOW_ACCOUNT_DETAIL: - return extend(appState, { - forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null, - currentView: { - name: 'accountDetail', - context: action.value, - }, - accountDetail: { - subview: 'transactions', - accountExport: 'none', - privateKey: '', - }, - transForward: false, - }) - - case actions.BACK_TO_ACCOUNT_DETAIL: - return extend(appState, { - currentView: { - name: 'accountDetail', - context: action.value, - }, - accountDetail: { - subview: 'transactions', - accountExport: 'none', - privateKey: '', - }, - transForward: false, - }) - - case actions.SHOW_ACCOUNTS_PAGE: - return extend(appState, { - currentView: { - name: seedWords ? 'createVaultComplete' : 'accounts', - seedWords, - }, - transForward: true, - isLoading: false, - warning: null, - scrollToBottom: false, - forgottenPassword: false, - }) - - case actions.SHOW_NOTICE: - return extend(appState, { - transForward: true, - isLoading: false, - }) - - case actions.REVEAL_ACCOUNT: - return extend(appState, { - scrollToBottom: true, - }) - - case actions.SHOW_CONF_TX_PAGE: - return extend(appState, { - currentView: { - name: 'confTx', - context: 0, - }, - transForward: action.transForward, - warning: null, - isLoading: false, - }) - - case actions.SHOW_CONF_MSG_PAGE: - return extend(appState, { - currentView: { - name: hasUnconfActions ? 'confTx' : 'account-detail', - context: 0, - }, - transForward: true, - warning: null, - isLoading: false, - }) - - case actions.COMPLETED_TX: - log.debug('reducing COMPLETED_TX for tx ' + action.value) - const otherUnconfActions = getUnconfActionList(state) - .filter(tx => tx.id !== action.value) - const hasOtherUnconfActions = otherUnconfActions.length > 0 - - if (hasOtherUnconfActions) { - log.debug('reducer detected txs - rendering confTx view') - return extend(appState, { - transForward: false, - currentView: { - name: 'confTx', - context: 0, - }, - warning: null, - }) - } else { - log.debug('attempting to close popup') - return extend(appState, { - // indicate notification should close - shouldClose: true, - transForward: false, - warning: null, - currentView: { - name: 'accountDetail', - context: state.metamask.selectedAddress, - }, - accountDetail: { - subview: 'transactions', - }, - }) - } - - case actions.NEXT_TX: - return extend(appState, { - transForward: true, - currentView: { - name: 'confTx', - context: ++appState.currentView.context, - warning: null, - }, - }) - - case actions.VIEW_PENDING_TX: - const context = indexForPending(state, action.value) - return extend(appState, { - transForward: true, - currentView: { - name: 'confTx', - context, - warning: null, - }, - }) - - case actions.PREVIOUS_TX: - return extend(appState, { - transForward: false, - currentView: { - name: 'confTx', - context: --appState.currentView.context, - warning: null, - }, - }) - - case actions.TRANSACTION_ERROR: - return extend(appState, { - currentView: { - name: 'confTx', - errorMessage: 'There was a problem submitting this transaction.', - }, - }) - - case actions.UNLOCK_FAILED: - return extend(appState, { - warning: action.value || 'Incorrect password. Try again.', - }) - - case actions.SHOW_LOADING: - return extend(appState, { - isLoading: true, - loadingMessage: action.value, - }) - - case actions.HIDE_LOADING: - return extend(appState, { - isLoading: false, - }) - - case actions.SHOW_SUB_LOADING_INDICATION: - return extend(appState, { - isSubLoading: true, - }) - - case actions.HIDE_SUB_LOADING_INDICATION: - return extend(appState, { - isSubLoading: false, - }) - case actions.CLEAR_SEED_WORD_CACHE: - return extend(appState, { - transForward: true, - currentView: {}, - isLoading: false, - accountDetail: { - subview: 'transactions', - accountExport: 'none', - privateKey: '', - }, - }) - - case actions.DISPLAY_WARNING: - return extend(appState, { - warning: action.value, - isLoading: false, - }) - - case actions.HIDE_WARNING: - return extend(appState, { - warning: undefined, - }) - - case actions.REQUEST_ACCOUNT_EXPORT: - return extend(appState, { - transForward: true, - currentView: { - name: 'accountDetail', - context: appState.currentView.context, - }, - accountDetail: { - subview: 'export', - accountExport: 'requested', - }, - }) - - case actions.EXPORT_ACCOUNT: - return extend(appState, { - accountDetail: { - subview: 'export', - accountExport: 'completed', - }, - }) - - case actions.SHOW_PRIVATE_KEY: - return extend(appState, { - accountDetail: { - subview: 'export', - accountExport: 'completed', - privateKey: action.value, - }, - }) - - case actions.BUY_ETH_VIEW: - return extend(appState, { - transForward: true, - currentView: { - name: 'buyEth', - context: appState.currentView.name, - }, - identity: state.metamask.identities[action.value], - buyView: { - subview: 'Coinbase', - amount: '15.00', - buyAddress: action.value, - formView: { - coinbase: true, - shapeshift: false, - }, - }, - }) - - case actions.COINBASE_SUBVIEW: - return extend(appState, { - buyView: { - subview: 'Coinbase', - formView: { - coinbase: true, - shapeshift: false, - }, - buyAddress: appState.buyView.buyAddress, - amount: appState.buyView.amount, - }, - }) - - case actions.SHAPESHIFT_SUBVIEW: - return extend(appState, { - buyView: { - subview: 'ShapeShift', - formView: { - coinbase: false, - shapeshift: true, - marketinfo: action.value.marketinfo, - coinOptions: action.value.coinOptions, - }, - buyAddress: appState.buyView.buyAddress, - amount: appState.buyView.amount, - }, - }) - - case actions.PAIR_UPDATE: - return extend(appState, { - buyView: { - subview: 'ShapeShift', - formView: { - coinbase: false, - shapeshift: true, - marketinfo: action.value.marketinfo, - coinOptions: appState.buyView.formView.coinOptions, - }, - buyAddress: appState.buyView.buyAddress, - amount: appState.buyView.amount, - warning: null, - }, - }) - - case actions.SHOW_QR: - return extend(appState, { - qrRequested: true, - transForward: true, - - Qr: { - message: action.value.message, - data: action.value.data, - }, - }) - - case actions.SHOW_QR_VIEW: - return extend(appState, { - currentView: { - name: 'qr', - context: appState.currentView.context, - }, - transForward: true, - Qr: { - message: action.value.message, - data: action.value.data, - }, - }) - default: - return appState - } -} - -function checkUnconfActions (state) { - const unconfActionList = getUnconfActionList(state) - const hasUnconfActions = unconfActionList.length > 0 - return hasUnconfActions -} - -function getUnconfActionList (state) { - const { unapprovedTxs, unapprovedMsgs, - unapprovedPersonalMsgs, network } = state.metamask - - const unconfActionList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) - return unconfActionList -} - -function indexForPending (state, txId) { - const unconfTxList = getUnconfActionList(state) - const match = unconfTxList.find((tx) => tx.id === txId) - const index = unconfTxList.indexOf(match) - return index -} diff --git a/ui/responsive/app/reducers/identities.js b/ui/responsive/app/reducers/identities.js deleted file mode 100644 index 341a404e7..000000000 --- a/ui/responsive/app/reducers/identities.js +++ /dev/null @@ -1,15 +0,0 @@ -const extend = require('xtend') - -module.exports = reduceIdentities - -function reduceIdentities (state, action) { - // clone + defaults - var idState = extend({ - - }, state.identities) - - switch (action.type) { - default: - return idState - } -} diff --git a/ui/responsive/app/reducers/metamask.js b/ui/responsive/app/reducers/metamask.js deleted file mode 100644 index e0c416c2d..000000000 --- a/ui/responsive/app/reducers/metamask.js +++ /dev/null @@ -1,137 +0,0 @@ -const extend = require('xtend') -const actions = require('../actions') - -module.exports = reduceMetamask - -function reduceMetamask (state, action) { - let newState - - // clone + defaults - var metamaskState = extend({ - isInitialized: false, - isUnlocked: false, - rpcTarget: 'https://rawtestrpc.metamask.io/', - identities: {}, - unapprovedTxs: {}, - noActiveNotices: true, - lastUnreadNotice: undefined, - frequentRpcList: [], - addressBook: [], - }, state.metamask) - - switch (action.type) { - - case actions.SHOW_ACCOUNTS_PAGE: - newState = extend(metamaskState) - delete newState.seedWords - return newState - - case actions.SHOW_NOTICE: - return extend(metamaskState, { - noActiveNotices: false, - lastUnreadNotice: action.value, - }) - - case actions.CLEAR_NOTICES: - return extend(metamaskState, { - noActiveNotices: true, - }) - - case actions.UPDATE_METAMASK_STATE: - return extend(metamaskState, action.value) - - case actions.UNLOCK_METAMASK: - return extend(metamaskState, { - isUnlocked: true, - isInitialized: true, - selectedAddress: action.value, - }) - - case actions.LOCK_METAMASK: - return extend(metamaskState, { - isUnlocked: false, - }) - - case actions.SET_RPC_LIST: - return extend(metamaskState, { - frequentRpcList: action.value, - }) - - case actions.SET_RPC_TARGET: - return extend(metamaskState, { - provider: { - type: 'rpc', - rpcTarget: action.value, - }, - }) - - case actions.SET_PROVIDER_TYPE: - return extend(metamaskState, { - provider: { - type: action.value, - }, - }) - - case actions.COMPLETED_TX: - var stringId = String(action.id) - newState = extend(metamaskState, { - unapprovedTxs: {}, - unapprovedMsgs: {}, - }) - for (const id in metamaskState.unapprovedTxs) { - if (id !== stringId) { - newState.unapprovedTxs[id] = metamaskState.unapprovedTxs[id] - } - } - for (const id in metamaskState.unapprovedMsgs) { - if (id !== stringId) { - newState.unapprovedMsgs[id] = metamaskState.unapprovedMsgs[id] - } - } - return newState - - case actions.SHOW_NEW_VAULT_SEED: - return extend(metamaskState, { - isUnlocked: true, - isInitialized: false, - seedWords: action.value, - }) - - case actions.CLEAR_SEED_WORD_CACHE: - newState = extend(metamaskState, { - isUnlocked: true, - isInitialized: true, - selectedAddress: action.value, - }) - delete newState.seedWords - return newState - - case actions.SHOW_ACCOUNT_DETAIL: - newState = extend(metamaskState, { - isUnlocked: true, - isInitialized: true, - selectedAddress: action.value, - }) - delete newState.seedWords - return newState - - case actions.SAVE_ACCOUNT_LABEL: - const account = action.value.account - const name = action.value.label - var id = {} - id[account] = extend(metamaskState.identities[account], { name }) - var identities = extend(metamaskState.identities, id) - return extend(metamaskState, { identities }) - - case actions.SET_CURRENT_FIAT: - return extend(metamaskState, { - currentCurrency: action.value.currentCurrency, - conversionRate: action.value.conversionRate, - conversionDate: action.value.conversionDate, - }) - - default: - return metamaskState - - } -} diff --git a/ui/responsive/app/root.js b/ui/responsive/app/root.js deleted file mode 100644 index 9e7314b20..000000000 --- a/ui/responsive/app/root.js +++ /dev/null @@ -1,22 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const Provider = require('react-redux').Provider -const h = require('react-hyperscript') -const App = require('./app') - -module.exports = Root - -inherits(Root, Component) -function Root () { Component.call(this) } - -Root.prototype.render = function () { - return ( - - h(Provider, { - store: this.props.store, - }, [ - h(App), - ]) - - ) -} diff --git a/ui/responsive/app/send.js b/ui/responsive/app/send.js deleted file mode 100644 index a21a219eb..000000000 --- a/ui/responsive/app/send.js +++ /dev/null @@ -1,288 +0,0 @@ -const inherits = require('util').inherits -const PersistentForm = require('../lib/persistent-form') -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const Identicon = require('./components/identicon') -const actions = require('./actions') -const util = require('./util') -const numericBalance = require('./util').numericBalance -const addressSummary = require('./util').addressSummary -const isHex = require('./util').isHex -const EthBalance = require('./components/eth-balance') -const EnsInput = require('./components/ens-input') -const ethUtil = require('ethereumjs-util') -module.exports = connect(mapStateToProps)(SendTransactionScreen) - -function mapStateToProps (state) { - var result = { - address: state.metamask.selectedAddress, - accounts: state.metamask.accounts, - identities: state.metamask.identities, - warning: state.appState.warning, - network: state.metamask.network, - addressBook: state.metamask.addressBook, - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - } - - result.error = result.warning && result.warning.split('.')[0] - - result.account = result.accounts[result.address] - result.identity = result.identities[result.address] - result.balance = result.account ? numericBalance(result.account.balance) : null - - return result -} - -inherits(SendTransactionScreen, PersistentForm) -function SendTransactionScreen () { - PersistentForm.call(this) -} - -SendTransactionScreen.prototype.render = function () { - this.persistentFormParentId = 'send-tx-form' - - const props = this.props - const { - address, - account, - identity, - network, - identities, - addressBook, - conversionRate, - currentCurrency, - } = props - - return ( - - h('.send-screen.flex-column.flex-grow', [ - - // - // Sender Profile - // - - h('.account-data-subsection.flex-row.flex-grow', { - style: { - margin: '0 20px', - }, - }, [ - - // header - identicon + nav - h('.flex-row.flex-space-between', { - style: { - marginTop: '15px', - }, - }, [ - // back button - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { - onClick: this.back.bind(this), - }), - - // large identicon - h('.identicon-wrapper.flex-column.flex-center.select-none', [ - h(Identicon, { - diameter: 62, - address: address, - }), - ]), - - // invisible place holder - h('i.fa.fa-users.fa-lg.invisible', { - style: { - marginTop: '28px', - }, - }), - - ]), - - // account label - - h('.flex-column', { - style: { - marginTop: '10px', - alignItems: 'flex-start', - }, - }, [ - h('h2.font-medium.color-forest.flex-center', { - style: { - paddingTop: '8px', - marginBottom: '8px', - }, - }, identity && identity.name), - - // address and getter actions - h('.flex-row.flex-center', { - style: { - marginBottom: '8px', - }, - }, [ - - h('div', { - style: { - lineHeight: '16px', - }, - }, addressSummary(address)), - - ]), - - // balance - h('.flex-row.flex-center', [ - - h(EthBalance, { - value: account && account.balance, - conversionRate, - currentCurrency, - }), - - ]), - ]), - ]), - - // - // Required Fields - // - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginTop: '15px', - marginBottom: '16px', - }, - }, [ - 'Send Transaction', - ]), - - // error message - props.error && h('span.error.flex-center', props.error), - - // 'to' field - h('section.flex-row.flex-center', [ - h(EnsInput, { - name: 'address', - placeholder: 'Recipient Address', - onChange: this.recipientDidChange.bind(this), - network, - identities, - addressBook, - }), - ]), - - // 'amount' and send button - h('section.flex-row.flex-center', [ - - h('input.large-input', { - name: 'amount', - placeholder: 'Amount', - type: 'number', - style: { - marginRight: '6px', - }, - dataset: { - persistentFormId: 'tx-amount', - }, - }), - - h('button.primary', { - onClick: this.onSubmit.bind(this), - style: { - textTransform: 'uppercase', - }, - }, 'Next'), - - ]), - - // - // Optional Fields - // - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginTop: '16px', - marginBottom: '16px', - }, - }, [ - 'Transaction Data (optional)', - ]), - - // 'data' field - h('section.flex-column.flex-center', [ - h('input.large-input', { - name: 'txData', - placeholder: '0x01234', - style: { - width: '100%', - resize: 'none', - }, - dataset: { - persistentFormId: 'tx-data', - }, - }), - ]), - ]) - ) -} - -SendTransactionScreen.prototype.navigateToAccounts = function (event) { - event.stopPropagation() - this.props.dispatch(actions.showAccountsPage()) -} - -SendTransactionScreen.prototype.back = function () { - var address = this.props.address - this.props.dispatch(actions.backToAccountDetail(address)) -} - -SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickname) { - this.setState({ - recipient: recipient, - nickname: nickname, - }) -} - -SendTransactionScreen.prototype.onSubmit = function () { - const state = this.state || {} - const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '') - const nickname = state.nickname || ' ' - const input = document.querySelector('input[name="amount"]').value - const value = util.normalizeEthStringToWei(input) - const txData = document.querySelector('input[name="txData"]').value - const balance = this.props.balance - let message - - if (value.gt(balance)) { - message = 'Insufficient funds.' - return this.props.dispatch(actions.displayWarning(message)) - } - - if (input < 0) { - message = 'Can not send negative amounts of ETH.' - return this.props.dispatch(actions.displayWarning(message)) - } - - if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) { - message = 'Recipient address is invalid.' - return this.props.dispatch(actions.displayWarning(message)) - } - - if (!isHex(ethUtil.stripHexPrefix(txData)) && txData) { - message = 'Transaction data must be hex string.' - return this.props.dispatch(actions.displayWarning(message)) - } - - this.props.dispatch(actions.hideWarning()) - - this.props.dispatch(actions.addToAddressBook(recipient, nickname)) - - var txParams = { - from: this.props.address, - value: '0x' + value.toString(16), - } - - if (recipient) txParams.to = ethUtil.addHexPrefix(recipient) - if (txData) txParams.data = txData - - this.props.dispatch(actions.signTx(txParams)) -} diff --git a/ui/responsive/app/settings.js b/ui/responsive/app/settings.js deleted file mode 100644 index 454cc95e0..000000000 --- a/ui/responsive/app/settings.js +++ /dev/null @@ -1,59 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') - -module.exports = connect(mapStateToProps)(AppSettingsPage) - -function mapStateToProps (state) { - return {} -} - -inherits(AppSettingsPage, Component) -function AppSettingsPage () { - Component.call(this) -} - -AppSettingsPage.prototype.render = function () { - return ( - - h('.account-detail-section.flex-column.flex-grow', [ - - // subtitle and nav - h('.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: this.navigateToAccounts.bind(this), - }), - h('h2.page-subtitle', 'Settings'), - ]), - - h('label', { - htmlFor: 'settings-rpc-endpoint', - }, 'RPC Endpoint:'), - h('input', { - type: 'url', - id: 'settings-rpc-endpoint', - onKeyPress: this.onKeyPress.bind(this), - }), - - ]) - - ) -} - -AppSettingsPage.prototype.componentDidMount = function () { - document.querySelector('input').focus() -} - -AppSettingsPage.prototype.onKeyPress = function (event) { - // get submit event - if (event.key === 'Enter') { - // this.submitPassword(event) - } -} - -AppSettingsPage.prototype.navigateToAccounts = function (event) { - event.stopPropagation() - this.props.dispatch(actions.showAccountsPage()) -} diff --git a/ui/responsive/app/store.js b/ui/responsive/app/store.js deleted file mode 100644 index ba9e58b49..000000000 --- a/ui/responsive/app/store.js +++ /dev/null @@ -1,21 +0,0 @@ -const createStore = require('redux').createStore -const applyMiddleware = require('redux').applyMiddleware -const thunkMiddleware = require('redux-thunk') -const rootReducer = require('./reducers') -const createLogger = require('redux-logger') - -global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' - -module.exports = configureStore - -const loggerMiddleware = createLogger({ - predicate: () => global.METAMASK_DEBUG, -}) - -const middlewares = [thunkMiddleware, loggerMiddleware] - -const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore) - -function configureStore (initialState) { - return createStoreWithMiddleware(rootReducer, initialState) -} diff --git a/ui/responsive/app/template.js b/ui/responsive/app/template.js deleted file mode 100644 index d15b30fd2..000000000 --- a/ui/responsive/app/template.js +++ /dev/null @@ -1,30 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect - -module.exports = connect(mapStateToProps)(COMPONENTNAME) - -function mapStateToProps (state) { - return {} -} - -inherits(COMPONENTNAME, Component) -function COMPONENTNAME () { - Component.call(this) -} - -COMPONENTNAME.prototype.render = function () { - const props = this.props - - return ( - h('div', { - style: { - background: 'blue', - }, - }, [ - `Hello, ${props.sender}`, - ]) - ) -} - diff --git a/ui/responsive/app/unlock.js b/ui/responsive/app/unlock.js deleted file mode 100644 index 1aee3c5d0..000000000 --- a/ui/responsive/app/unlock.js +++ /dev/null @@ -1,118 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') -const getCaretCoordinates = require('textarea-caret') -const EventEmitter = require('events').EventEmitter - -const Mascot = require('./components/mascot') - -module.exports = connect(mapStateToProps)(UnlockScreen) - -inherits(UnlockScreen, Component) -function UnlockScreen () { - Component.call(this) - this.animationEventEmitter = new EventEmitter() -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -UnlockScreen.prototype.render = function () { - const state = this.props - const warning = state.warning - return ( - h('.flex-column', [ - h('.unlock-screen.flex-column.flex-center.flex-grow', [ - - h(Mascot, { - animationEventEmitter: this.animationEventEmitter, - }), - - h('h1', { - style: { - fontSize: '1.4em', - textTransform: 'uppercase', - color: '#7F8082', - }, - }, 'MetaMask'), - - h('input.large-input', { - type: 'password', - id: 'password-box', - placeholder: 'enter password', - style: { - - }, - onKeyPress: this.onKeyPress.bind(this), - onInput: this.inputChanged.bind(this), - }), - - h('.error', { - style: { - display: warning ? 'block' : 'none', - padding: '0 20px', - textAlign: 'center', - }, - }, warning), - - h('button.primary.cursor-pointer', { - onClick: this.onSubmit.bind(this), - style: { - margin: 10, - }, - }, 'Unlock'), - ]), - - h('.flex-row.flex-center.flex-grow', [ - h('p.pointer', { - onClick: () => this.props.dispatch(actions.forgotPassword()), - style: { - fontSize: '0.8em', - color: 'rgb(247, 134, 28)', - textDecoration: 'underline', - }, - }, 'I forgot my password.'), - ]), - ]) - ) -} - -UnlockScreen.prototype.componentDidMount = function () { - document.getElementById('password-box').focus() -} - -UnlockScreen.prototype.onSubmit = function (event) { - const input = document.getElementById('password-box') - const password = input.value - this.props.dispatch(actions.tryUnlockMetamask(password)) -} - -UnlockScreen.prototype.onKeyPress = function (event) { - if (event.key === 'Enter') { - this.submitPassword(event) - } -} - -UnlockScreen.prototype.submitPassword = function (event) { - var element = event.target - var password = element.value - // reset input - element.value = '' - this.props.dispatch(actions.tryUnlockMetamask(password)) -} - -UnlockScreen.prototype.inputChanged = function (event) { - // tell mascot to look at page action - var element = event.target - var boundingRect = element.getBoundingClientRect() - var coordinates = getCaretCoordinates(element, element.selectionEnd) - this.animationEventEmitter.emit('point', { - x: boundingRect.left + coordinates.left - element.scrollLeft, - y: boundingRect.top + coordinates.top - element.scrollTop, - }) -} diff --git a/ui/responsive/app/util.js b/ui/responsive/app/util.js deleted file mode 100644 index ac3f42c6b..000000000 --- a/ui/responsive/app/util.js +++ /dev/null @@ -1,217 +0,0 @@ -const ethUtil = require('ethereumjs-util') - -var valueTable = { - wei: '1000000000000000000', - kwei: '1000000000000000', - mwei: '1000000000000', - gwei: '1000000000', - szabo: '1000000', - finney: '1000', - ether: '1', - kether: '0.001', - mether: '0.000001', - gether: '0.000000001', - tether: '0.000000000001', -} -var bnTable = {} -for (var currency in valueTable) { - bnTable[currency] = new ethUtil.BN(valueTable[currency], 10) -} - -module.exports = { - valuesFor: valuesFor, - addressSummary: addressSummary, - miniAddressSummary: miniAddressSummary, - isAllOneCase: isAllOneCase, - isValidAddress: isValidAddress, - numericBalance: numericBalance, - parseBalance: parseBalance, - formatBalance: formatBalance, - generateBalanceObject: generateBalanceObject, - dataSize: dataSize, - readableDate: readableDate, - normalizeToWei: normalizeToWei, - normalizeEthStringToWei: normalizeEthStringToWei, - normalizeNumberToWei: normalizeNumberToWei, - valueTable: valueTable, - bnTable: bnTable, - isHex: isHex, -} - -function valuesFor (obj) { - if (!obj) return [] - return Object.keys(obj) - .map(function (key) { return obj[key] }) -} - -function addressSummary (address, firstSegLength = 10, lastSegLength = 4, includeHex = true) { - if (!address) return '' - let checked = ethUtil.toChecksumAddress(address) - if (!includeHex) { - checked = ethUtil.stripHexPrefix(checked) - } - return checked ? checked.slice(0, firstSegLength) + '...' + checked.slice(checked.length - lastSegLength) : '...' -} - -function miniAddressSummary (address) { - if (!address) return '' - var checked = ethUtil.toChecksumAddress(address) - return checked ? checked.slice(0, 4) + '...' + checked.slice(-4) : '...' -} - -function isValidAddress (address) { - var prefixed = ethUtil.addHexPrefix(address) - if (address === '0x0000000000000000000000000000000000000000') return false - return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed) -} - -function isAllOneCase (address) { - if (!address) return true - var lower = address.toLowerCase() - var upper = address.toUpperCase() - return address === lower || address === upper -} - -// Takes wei Hex, returns wei BN, even if input is null -function numericBalance (balance) { - if (!balance) return new ethUtil.BN(0, 16) - var stripped = ethUtil.stripHexPrefix(balance) - return new ethUtil.BN(stripped, 16) -} - -// Takes hex, returns [beforeDecimal, afterDecimal] -function parseBalance (balance) { - var beforeDecimal, afterDecimal - const wei = numericBalance(balance) - var weiString = wei.toString() - const trailingZeros = /0+$/ - - beforeDecimal = weiString.length > 18 ? weiString.slice(0, weiString.length - 18) : '0' - afterDecimal = ('000000000000000000' + wei).slice(-18).replace(trailingZeros, '') - if (afterDecimal === '') { afterDecimal = '0' } - return [beforeDecimal, afterDecimal] -} - -// Takes wei hex, returns an object with three properties. -// Its "formatted" property is what we generally use to render values. -function formatBalance (balance, decimalsToKeep, needsParse = true) { - var parsed = needsParse ? parseBalance(balance) : balance.split('.') - var beforeDecimal = parsed[0] - var afterDecimal = parsed[1] - var formatted = 'None' - if (decimalsToKeep === undefined) { - if (beforeDecimal === '0') { - if (afterDecimal !== '0') { - var sigFigs = afterDecimal.match(/^0*(.{2})/) // default: grabs 2 most significant digits - if (sigFigs) { afterDecimal = sigFigs[0] } - formatted = '0.' + afterDecimal + ' ETH' - } - } else { - formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ' ETH' - } - } else { - afterDecimal += Array(decimalsToKeep).join('0') - formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ' ETH' - } - return formatted -} - - -function generateBalanceObject (formattedBalance, decimalsToKeep = 1) { - var balance = formattedBalance.split(' ')[0] - var label = formattedBalance.split(' ')[1] - var beforeDecimal = balance.split('.')[0] - var afterDecimal = balance.split('.')[1] - var shortBalance = shortenBalance(balance, decimalsToKeep) - - if (beforeDecimal === '0' && afterDecimal.substr(0, 5) === '00000') { - // eslint-disable-next-line eqeqeq - if (afterDecimal == 0) { - balance = '0' - } else { - balance = '<1.0e-5' - } - } else if (beforeDecimal !== '0') { - balance = `${beforeDecimal}.${afterDecimal.slice(0, decimalsToKeep)}` - } - - return { balance, label, shortBalance } -} - -function shortenBalance (balance, decimalsToKeep = 1) { - var truncatedValue - var convertedBalance = parseFloat(balance) - if (convertedBalance > 1000000) { - truncatedValue = (balance / 1000000).toFixed(decimalsToKeep) - return `${truncatedValue}m` - } else if (convertedBalance > 1000) { - truncatedValue = (balance / 1000).toFixed(decimalsToKeep) - return `${truncatedValue}k` - } else if (convertedBalance === 0) { - return '0' - } else if (convertedBalance < 0.001) { - return '<0.001' - } else if (convertedBalance < 1) { - var stringBalance = convertedBalance.toString() - if (stringBalance.split('.')[1].length > 3) { - return convertedBalance.toFixed(3) - } else { - return stringBalance - } - } else { - return convertedBalance.toFixed(decimalsToKeep) - } -} - -function dataSize (data) { - var size = data ? ethUtil.stripHexPrefix(data).length : 0 - return size + ' bytes' -} - -// Takes a BN and an ethereum currency name, -// returns a BN in wei -function normalizeToWei (amount, currency) { - try { - return amount.mul(bnTable.wei).div(bnTable[currency]) - } catch (e) {} - return amount -} - -function normalizeEthStringToWei (str) { - const parts = str.split('.') - let eth = new ethUtil.BN(parts[0], 10).mul(bnTable.wei) - if (parts[1]) { - var decimal = parts[1] - while (decimal.length < 18) { - decimal += '0' - } - const decimalBN = new ethUtil.BN(decimal, 10) - eth = eth.add(decimalBN) - } - return eth -} - -var multiple = new ethUtil.BN('10000', 10) -function normalizeNumberToWei (n, currency) { - var enlarged = n * 10000 - var amount = new ethUtil.BN(String(enlarged), 10) - return normalizeToWei(amount, currency).div(multiple) -} - -function readableDate (ms) { - var date = new Date(ms) - var month = date.getMonth() - var day = date.getDate() - var year = date.getFullYear() - var hours = date.getHours() - var minutes = '0' + date.getMinutes() - var seconds = '0' + date.getSeconds() - - var dateStr = `${month}/${day}/${year}` - var time = `${hours}:${minutes.substr(-2)}:${seconds.substr(-2)}` - return `${dateStr} ${time}` -} - -function isHex (str) { - return Boolean(str.match(/^(0x)?[0-9a-fA-F]+$/)) -} diff --git a/ui/responsive/css.js b/ui/responsive/css.js deleted file mode 100644 index 7c394a87b..000000000 --- a/ui/responsive/css.js +++ /dev/null @@ -1,29 +0,0 @@ -const fs = require('fs') -const path = require('path') - -module.exports = bundleCss - -var cssFiles = { - 'fonts.css': fs.readFileSync(path.join(__dirname, '/app/css/fonts.css'), 'utf8'), - 'reset.css': fs.readFileSync(path.join(__dirname, '/app/css/reset.css'), 'utf8'), - 'lib.css': fs.readFileSync(path.join(__dirname, '/app/css/lib.css'), 'utf8'), - 'index.css': fs.readFileSync(path.join(__dirname, '/app/css/index.css'), 'utf8'), - 'transitions.css': fs.readFileSync(path.join(__dirname, '/app/css/transitions.css'), 'utf8'), - 'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'), - 'react-css': fs.readFileSync(path.join(__dirname, '..', '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'), -} - -function bundleCss () { - var cssBundle = Object.keys(cssFiles).reduce(function (bundle, fileName) { - var fileContent = cssFiles[fileName] - var output = String() - - output += '/*========== ' + fileName + ' ==========*/\n\n' - output += fileContent - output += '\n\n' - - return bundle + output - }, String()) - - return cssBundle -} diff --git a/ui/responsive/design/00-metamask-SignIn.jpg b/ui/responsive/design/00-metamask-SignIn.jpg deleted file mode 100644 index 2becdb032..000000000 Binary files a/ui/responsive/design/00-metamask-SignIn.jpg and /dev/null differ diff --git a/ui/responsive/design/01-metamask-SelectAcc.jpg b/ui/responsive/design/01-metamask-SelectAcc.jpg deleted file mode 100644 index 239091a98..000000000 Binary files a/ui/responsive/design/01-metamask-SelectAcc.jpg and /dev/null differ diff --git a/ui/responsive/design/02-metamask-AccDetails.jpg b/ui/responsive/design/02-metamask-AccDetails.jpg deleted file mode 100644 index d7d408ffc..000000000 Binary files a/ui/responsive/design/02-metamask-AccDetails.jpg and /dev/null differ diff --git a/ui/responsive/design/02a-metamask-AccDetails-OverToken.jpg b/ui/responsive/design/02a-metamask-AccDetails-OverToken.jpg deleted file mode 100644 index f26ff31e8..000000000 Binary files a/ui/responsive/design/02a-metamask-AccDetails-OverToken.jpg and /dev/null differ diff --git a/ui/responsive/design/02a-metamask-AccDetails-OverTransaction.jpg b/ui/responsive/design/02a-metamask-AccDetails-OverTransaction.jpg deleted file mode 100644 index 8a06be6b9..000000000 Binary files a/ui/responsive/design/02a-metamask-AccDetails-OverTransaction.jpg and /dev/null differ diff --git a/ui/responsive/design/02a-metamask-AccDetails.jpg b/ui/responsive/design/02a-metamask-AccDetails.jpg deleted file mode 100644 index c37e0f539..000000000 Binary files a/ui/responsive/design/02a-metamask-AccDetails.jpg and /dev/null differ diff --git a/ui/responsive/design/02b-metamask-AccDetails-Send.jpg b/ui/responsive/design/02b-metamask-AccDetails-Send.jpg deleted file mode 100644 index 10f2d27fd..000000000 Binary files a/ui/responsive/design/02b-metamask-AccDetails-Send.jpg and /dev/null differ diff --git a/ui/responsive/design/03-metamask-Qr.jpg b/ui/responsive/design/03-metamask-Qr.jpg deleted file mode 100644 index 9c09de42f..000000000 Binary files a/ui/responsive/design/03-metamask-Qr.jpg and /dev/null differ diff --git a/ui/responsive/design/05-metamask-Menu.jpg b/ui/responsive/design/05-metamask-Menu.jpg deleted file mode 100644 index 0a43d7b2a..000000000 Binary files a/ui/responsive/design/05-metamask-Menu.jpg and /dev/null differ diff --git a/ui/responsive/design/chromeStorePics/final_screen_dao_accounts.png b/ui/responsive/design/chromeStorePics/final_screen_dao_accounts.png deleted file mode 100644 index 805cc96b6..000000000 Binary files a/ui/responsive/design/chromeStorePics/final_screen_dao_accounts.png and /dev/null differ diff --git a/ui/responsive/design/chromeStorePics/final_screen_dao_locked.png b/ui/responsive/design/chromeStorePics/final_screen_dao_locked.png deleted file mode 100644 index 9d9e33930..000000000 Binary files a/ui/responsive/design/chromeStorePics/final_screen_dao_locked.png and /dev/null differ diff --git a/ui/responsive/design/chromeStorePics/final_screen_dao_notification.png b/ui/responsive/design/chromeStorePics/final_screen_dao_notification.png deleted file mode 100644 index d56a5ce62..000000000 Binary files a/ui/responsive/design/chromeStorePics/final_screen_dao_notification.png and /dev/null differ diff --git a/ui/responsive/design/chromeStorePics/final_screen_wei_account.png b/ui/responsive/design/chromeStorePics/final_screen_wei_account.png deleted file mode 100644 index d503ff301..000000000 Binary files a/ui/responsive/design/chromeStorePics/final_screen_wei_account.png and /dev/null differ diff --git a/ui/responsive/design/chromeStorePics/final_screen_wei_notification.png b/ui/responsive/design/chromeStorePics/final_screen_wei_notification.png deleted file mode 100644 index 3560c51ff..000000000 Binary files a/ui/responsive/design/chromeStorePics/final_screen_wei_notification.png and /dev/null differ diff --git a/ui/responsive/design/chromeStorePics/icon-128.png b/ui/responsive/design/chromeStorePics/icon-128.png deleted file mode 100644 index ae687147d..000000000 Binary files a/ui/responsive/design/chromeStorePics/icon-128.png and /dev/null differ diff --git a/ui/responsive/design/chromeStorePics/icon-64.png b/ui/responsive/design/chromeStorePics/icon-64.png deleted file mode 100644 index 7062cf4f1..000000000 Binary files a/ui/responsive/design/chromeStorePics/icon-64.png and /dev/null differ diff --git a/ui/responsive/design/chromeStorePics/metamask_icon.ai b/ui/responsive/design/chromeStorePics/metamask_icon.ai deleted file mode 100644 index 27400c5a4..000000000 --- a/ui/responsive/design/chromeStorePics/metamask_icon.ai +++ /dev/null @@ -1,2383 +0,0 @@ -%PDF-1.5 % -1 0 obj <>/OCGs[5 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream - - - - - application/pdf - - - metamask_icon - - - Adobe Illustrator CC 2015 (Macintosh) - 2016-06-15T14:23:12-04:00 - 2016-06-15T14:23:12-04:00 - 2016-06-15T14:23:12-04:00 - - - - 240 - 256 - JPEG - /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAADwAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXnP5r/mvB5Tg/RmnAT6/cJyUMKx28bVAkf+ZjT4U+k7UDYuo1HBsObl6bTce5+l5X+Wf5t6jonm KZtfu5rzTNVcG+lkZpHilACLOAamgUBWC/sgUrxAzEwakxl6uRczUaYSj6eYfS9vcQXEEdxA6ywT KskUimqsjCqsCOoIObUG3UkUvxQ7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXln5rfnHb+X1l0bQnWfXCCs0+zR2tfEGoaTwXoO/hmJqNTw7Dm5mm0plvL6fvfO U889xPJcXEjTTzM0ksshLO7saszMdySTUk5qybdsBSzAl7R+Rv5ni0dPKutTqto5ppNw/KqyOwH1 c0BHFuVVJpTpvUUz9Jnr0n4Ov1mnv1D4ve82LrHYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FUn1Pzl5W0yF5r3VLeNI9no4kYfNU5N+GY89XijsZC/mfkHIhpMstxE18h8ynCmqg7iorQ7HMhx 3Yq7FXYq7FXYq7FXjf5v/nG2nPL5e8tXAN9Ro9Rv039A7D04WBp6nUM1Ph7fFXjg6nU16Y83P0ul v1S5PAWZmYsxJYmpJ3JJzWu0axV2KuxV9Ifkr+Zq67py6FrF1y121+G3eUjlcwKtQeRPxyIAeXci jbnkc2mlz8Qo83U6vT8J4h9L1PMxwnYq7FXYq7FXYq7FXYq7FXYq7FXYqlN95s8t2I/0jUYQQaFE b1HHzWPk34Zi5Nbhh9Uh9/3OVi0Waf0xP3fex7UfzX0WEOtjBNdSCoR2AjjPgak8/wDhcwcvbWIf SDL7B+v7HOxdi5T9REftP6vtYre/mf5ouD+5eK0UdoowxPzMnqfhmsydsZpcqj7h+u3aY+x8Eedy 95/VTFdc803n1dptVv5powSVjeRmqx7IhNMxPEy5jRJPx2cwY8WEWAB8N0l8gRXnm/z/AKXazpXT 7WX65NCo5II4PjHqVrXk3FDX+btXNtodLETDqddqiYH7H1LnQPOuxV2KuxV2KuZlVSzEBQKknYAD FXhX5t/nOsyzaB5XuCEDBbzVoXZSSrA8Ld0I2qKM/foNt81+o1X8Mfm7LTaT+KXyeI5r3YuxV2Ku xV2KqtpdXNpdQ3VrI0NzA6yQyoaMroaqwPiCMINboIsUX1j+XH5g6f5w0WOcNHDq0I439irbqwoD IiklvTaux7dK1GbnBmEx5ukz4DjPky3Lmh2KuxV2KuxV2KuxVKb/AM2eW7CoudQhDA0KI3qMD7rH yYZjZdbhh9Uh9/3OVi0Waf0xP3fex6//ADY0OHktnbzXTKaKxpFGw8QTyb/hcwMnbWIfSDL7B+Pg 5+PsXKa4iI/afx8WO3/5ra9OJEtYIbVG2RqNJIo/1iQv/C5gZe2sp+kCP2n8fBz8XYuIVxEy+wfj 4sVvdY1a+FLy8muFBLBZJGZQT4KTQZrMmfJP6pE/F2ePBjh9MQPghMqbXYqler+YLPTgUJ9W57Qq en+se2X4dPKfuaMueMPewW+vrm9uGnuH5Ox2G/FR/KoPQZtYQERQdZOZkbL3L/nG7y8Y7PVPMEqj lOy2VqxBDBEpJKQSPsszINu6nNpoYbGTqdfPcRe1ZnuvdirsVdirsVeF/n1+Y96l1N5O04+lCERt Vn35uXAkWBfBOJVmI+1XjsAeWv1ec3wD4uy0eAVxn4PEM17sXYq7FXYq7FXYq7FU18seZNU8uaxD qumymOeKqsBQh0YUZCGDDceI2O+ESlH6TRYmEZbSFh7bbfmL5mubeO4iv6xSqHQ+lD0YV/kzVy7U 1MTRl9kf1Owj2XpiLEftP61T/Hvmv/lt/wCSUP8AzRkf5W1P877I/qZfyTp/5v2n9bv8e+a/+W3/ AJJQ/wDNGP8AK2p/nfZH9S/yTp/5v2n9bv8AHvmv/lt/5JQ/80Y/ytqf532R/Uv8k6f+b9p/W7/H vmv/AJbf+SUP/NGP8ran+d9kf1L/ACTp/wCb9p/W7/Hvmv8A5bf+SUP/ADRj/K2p/nfZH9S/yTp/ 5v2n9bHtW1/WtUeuoXTz8eiGioCNtkUKv4ZDNqsmX6zbdh0uPF9ApL8ob3Yq7FXYq7FWO695pW0d rWyo9wtRJKd1Q+A8WH3ZmYNLxby5OJn1PDtHmw6SR5JGkclnclmY9STuTmzArZ1xN7uhhlmmSGJS 8sjBI0HUsxoAPpwhiS+zvLGhxaF5e0/SIiGFlAkTOoIDuB8b0JNOb1bN5jhwxAdBknxSJ70zybB2 KuxV2KpX5o12HQfL2oaxNxK2UDyKjHiHkpSOOu9ObkL9OQyT4Yks8cOKQHe+Nr6+u7+8mvLyVp7q 4cyTTOaszNuSc0ZJJsu/AAFBRwJdirsVdirsVdirsVdirKvI2tmC6OmzN+5uDWEmlFkp03/mp9/z zX67BY4hzDm6PNR4T1Z5mpdo7FXYq7FXYqg7laSn33y2PJgVLJIdirsVWu6IjO7BUUEsxNAAOpJO IFqTTD9c81yT1gsGaKIH4px8LtT+Xuo/HNlg0gG8ubrs2qJ2ixzM1w3Yqzz8k/L66z5/s2kAMGmK 2oSAkgkwkCKlO4ldDTwBzJ0sOKfucbVz4YHz2fU+bd0rsVdirsVdirxT/nIzzWi2ll5ZtZ1Mkj/W dRjQnkqoB6KPTajli9D/ACqcwNbk2EQ7DQ49zIvB81zs3Yq7FXYq7FXYq7FXYq7FW0d0dXRirqQV YGhBG4IIxItQXp3ljWjqunc5Cv1qI8J1Xb/Van+UPxrmh1WDw5bcnc6fNxx35pvmO5DsVdirsVQ1 2v2W+gnJwYlD5YxdiqheXttZwGe4cRxjap6k+AHc5KEDI0GM5iIssE1fzBeakeB/dWw6Qqevux7n Nth08Ye91eXPKfuSzL2h2KuxV9If84/eVk03ys+tyEm51lqhSKcIYHdEArv8Zq3uKZtdHjqN97qN bkuVdz1PMtw3Yq7FXYqp3V1b2ltNdXMgit4EaWaVjRVRByZifAAYCaSBez418169Nr/mPUdYl5Vv Z2kjVyCyRVpEhIp9iMKv0Zo8k+KRLv8AHDhiB3JVkGbsVdirsVdirsVdirsVdirsVTPy7q50vU45 2J9B/guFHdD36H7J3/DKNTh8SFdejdgy8Er6PUYpY5okljblHIoZGHQqwqDmhIINF3QNiwuwJdir sVUrlaxH23yUeaCg8tYJfq2t2Wmx/vW5TleUcC/abtv/ACj3OXYsEp8uTVlzRhz5sD1DULm+uGnu GLE/ZX9lR/KozbY8YgKDqsmQyNlDZNg7FXYqiNN0+51HUbXT7UBrm8mjggUmgLysEWp7bnDEWaRK VCy+0tM0+307TbXT7YEW9nDHbwgmp4RKEWp+QzfRFCnnpSs2UThQ7FXYq7FXmX59ebIdL8ovpEMw Goauyx+mrFXW2U8pH2/Zbj6dD15HwOYmryVGupczR4uKd9A+ac1Tt3Yq7FXYq7FXYq7FXYq7FXYq 7FXYqzXyJrSem2lztRgS9sSQAQT8SD3ruPpzV6/Bvxj4ux0Wb+EsxzWuwdirsVadeSlfEEYhDE9b 8zwWLNb24E10pKuK/AhHjTqa9s2ODSme52Dh5tSI7DcsJmmlmkaWVy8jmrOxqTm0AAFB1hJJsrMK HYq7FXYq9S/5x+8qvqXmp9bkIFtoq1CEV5zTo6IBXb4RVq9jTMzR47lfc4WtyVHh730jm0dS7FXY q7FXYq+X/wA+dQmuvzGu4JPsWMFvBF/qtGJ/+JTHNTq5Xk9zudHGsY83nmYrlOxV2KuxV2KuxV2K uxV2KuxV2KtqrMwVQSxNABuSTirN/Lfl9LBVurhQ1626g7iMeA/yvE/R89VqdRx7D6XZ6fT8O55s tUggEdDuM1zmt4pdirsVYZ5s8s+pLJfWS0lNXmhH7fcsv+V4jv8APrs9JqaHDJ1+p01+qLDM2brn Yq7FXYq7FX0n/wA48to48kyR2cwfUPrLyanEdmjZvhiA2rwMaAj35ZtdHXBtzdRrr49+XR6hmW4b sVdirsVdir5I/Ne/+vfmJrs1QeFx6G3/AC7osP8AzLzTag3Mu800axhieUN7sVdirsVdirsVdirs VdirsVdirMPLHl8wAXt4hE5/uYmFCg/mI8f1ZrdVqL9MeTsdNgr1HmyXMJzEXatWOndf1ZVMbswr ZFLsVdiqGu13VvoOTgWJYV5o8vFS9/aL8O7XEQ7eLj28c2ml1H8MnXanT/xBi+Z7guxV2KuxVP8A yLf+abLzPZP5ZDyarI4SO3XdZVO7JKKgenQVYkjiPiqKVFuKUhIcPNqzRiYni5PsKIymJDKFWXiP UCElQ1N6EgEivtm7dCuxV2KuxV4P+Zn5i/m7o189tNbRaNYszi2urVBOsqEkD/SJAw5bV2VG8QNs 12fNlie4Oy0+DFId5eL3FxPczyXFxI01xMzSTSuSzu7GrMzHckk1JzBJt2AFLMCXYq7FXYq7FXYq 7FXYq7FXYqyvyv5fZWF9ex0IobaNvv5kfq/2s1+q1H8Mfi5+mwfxS+DKswHOdiqrbuVkA7Nsf4ZG Q2SEZlTN2KuxVTnTlEfbcfRhid0FBZcwYd5k8uNAz3tkg+rdZYl6oe7KKfZ/V8umy02pv0y5uu1G nr1R5MbzNcN2KuxVnH5Z/mdP5MuXjayhudPu5Fa9cJS6CAEUjkqoIFa8W2/1ak5kYM/B02cbUafx Ou76X8s+ZdL8x6RDqumGU2s32TLG8R5DZl+IUbi1VJQlajrm1hMSFh1GTGYGimmTYOxV2KqN9HZS Wk0d8sT2boVuEnCmIodiHDfDT54DVbpF3s+b/wAyfI/kCy9fU/LvmWzJZix0f1BOQSWJWF4OZXsq q6/N81efFAbxkPc7bBmmdpRPveZZiOY7FXYq7FXYq7FXYq7FXYqybyz5cMhjv7xaRD4oYSPteDN/ k+Hj8uuDqdTXpi5um09+osvzXOwdirsVdiqYIwZA3iMoIZt4pdirsVS9l4sV8DTLg1tEAih6YVYT 5k8vGzY3dsC1qxJdf99knpt+z4ZtNNqOLY83W6jT8O45JBmW4jsVZP8Alp5c07zH5z0/SNQd1tZz I7rH1f0o2l4V/ZDcNzl2CAlMAtOomYQJD65gggt4I7e3jWGCFRHFFGAqIiiiqqjYADYAZugKdGTa /FDsVdirHfNvkDyv5rjUava87iNGSC7iYxzRhvBhs1DuA4I9sqyYYz5tuLNKHJ4v5q/5x58xWBlu NAuE1S1G6270iuQCTtv+7fitN+QJ7LmDk0Uh9O7sMeuifq2eUzQzQTPDMjRTRMUkjcFWVlNCrA7g g5hkU5oNrMCXYq7FXYq7FXYq7FWR+XfLJuAl5eDjBUGKEjdx4nwX9fy64Wo1NemPNzNPpr9UuTMs 1rsXYq7FXYq7FUVavVSh6jcfLK5hkFfIMnYq7FUHdLSWviK/wyyHJgVLJoWuiOjI4DIwKsp3BB2I OINKRbB/MPl19Pb6xb1ezY713MZPY+3gfo+e10+o49j9Tq9Rp+DcckkzKcZmP5P3EsH5k6G8cTTM ZZIyqgkhZIXRm27IrFj7DL9Mf3gcfVC8ZfWWbl0jsVdirsVdirwr87POX5jaTqj6dFIdP0O4VTaX dorK8o6lXuDusgZTVUI+HrUHfX6rLOJrkHZaTFjkL5l4jmvdi7FXYq7FXYq7FXYqn/lzy6t8purr kLZTREG3Mg77/wAvbbMTU6jg2HNy9Pp+Lc8maqqooVQFVRRVGwAHYZqyXZAN4q7FXYq7FXYqvhfj Ip7dD9ORkNkhHZUzdirsVULpKoG/l/jkoFiULlrF2KrJYYpozHKiyRt9pGAIP0HCCQbCCAdiwTzD obabcB4qm0lJ9M7nif5Sf1ZttPn4xvzdXqMPAduT6E/Jz8tofLejx6pqMStrt+iyNzSj20bLtCOY DK9G/edN/h7VO+02DhFnmXn9Vn4zQ+kPSMynEdirsVdirsVS7zBoGl6/pM+l6nCJrScUI6MrD7Lo ezKehyM4CQos4TMTYfKfn3yJq3lDWHs7pWkspCTYX1KJMgp4Vo61oy9vlQ5p82EwNF3WHMMgsMYq MpbnVGKuxVvFXYqnnlvQDfSfWbhSLRDsP9+MOw9h3zF1Oo4BQ5uVp8HEbPJm6IiIqIoVFACqBQAD YADNUTbswKXYq7FXYq7FXYq7FXYqjoX5xg9+h+eUyFFmF+BLsVWyLyRl8RtiChAZewdirsVTjyfZ JeeZ9NidOarOkpUio/dH1Afo45mdnx4s8R5/du4faE+HBI+X37Pds7N4t2KuxV2KuxV2KuxV5Z+Y esC91gWcZBhsKpUb1kahf7qcfozke2dTx5eEcoff1/U9b2PpuDFxHnP7un62K5p3buxV2KuxV1Bi qFukowcdDsfnlkCxKhk2LsVdirsVdirsVdirsVRFo/xFfHcZCYZBE5WydirsVQEq8ZGHv+vLgdmB W4UOxVmH5WQCTzOzn/dFvI4+kqn/ABvm27GiDm90T+h1PbUiMI85D9L17OpeVdirsVdirsVdiqG1 K/i0+wuL2X7ECFyK0qR0Ue7HbKs+UY4GZ6Btw4jkmIjqXh000k0zzSsWkkYu7HqWY1Jzz+UjIknm XvYxEQAOQWYGTsVdirsVdiq2ROaFfHp88INIKAIIJB6jrlrB2FXYq7FXYq7FXYq7FV0bcXDeBwEJ CPylm7FXYqhbtTyDdiKfTlkCxKhk2LsVZ7+UduW1S+uO0cCx/TI4P/MvN32HC5yl3Cvn/Y6PtydQ jHvN/L+16jnSPNuxV2KuxV2KuxVhP5l60IrOPSY6+rccZZj29NSeI+l1r9GaHtzUgQGMc5bn3f2/ c73sTTEzOQ8o7D3/ANn3vOM5d6d2KuxV2KuxV2KuxVCXiFT6iqWB+1Sn8csgejEhBfW4/Bvw/rlv Chr64n8px4Va+uD+T8ceBWvrv+R+P9mHgVv67/kfj/ZjwK19cP8AL+OPArX1yT+UY8KtfXJfBfx/ rjwhU2s5Ge3Ut9rv/DMeYosgr5FLsVUL3l9WZlFWX4hX26/hkoc0FKDdSnwHyGZPCGKhZ6oLy3We CTlGxIBoBupKnt4jLMuA45cMhu1Yc0ckeKPJ6F+UmpSxapdQMapPGrGvjG1BT/kYc2vYs6nKPeL+ X9rqO3IeiMu418/7HsA3GdE807FXYq7FXYq7FXi/mfVP0nrl1dA1i5cIaGo9NPhUj/Wpy+nOF7Qz +LmlLpyHuH4t7jQYPCwxj15n3n8UlWYbmOxV2KuxV2KuxV2KuIB2PTFUDdWaV5caqe/cZbCbAhAy WjCpQ8h4d8tElUCCDQ9ckrWKpZrHmHTtKUeuxeY9II6F6eJBIoMztJoMmf6dh3nk4Os7Rxaceo2e 4c2Far5x1a+5JE31W3bb04z8RHu/X7qZ0ul7IxYtz6pef6v7XltX2zmy7A8EfL9f9id+R9d9WP8A Rc5q8YLW7kjdR1Tfeo6j2+WaztrRcJ8WPI8/1/jr73adh6/iHgy5jl7u78dPcy5RyYL4mmc+9GnN o1HK9iP1ZjzCQisrZOxVp1DoynowIPyOIKscl/dc+ewSvL2p1zNiL5NZNCy888na79QvDa3DgWlw d2Y0CPTZvDfofo8M67tfQ+LDiiPXH7Q8b2Nr/CnwSPol9h7/ANb2PytcvZ6rYSqwX96odu3FzRq/ Qc5fRZTDPEjvr57PT6/GJ4ZA91/J79A3KJT7Z2bxS/FXYq7FXYqk/m7VRpug3UyvwnkX0oN6Hm+1 V91FWzC7Rz+FhkevIe8/i3N7PweLmiOnM/D8U8azhnt3Yq7FXYq7FXYq7FXYq7FXEAih6Yqg54Ch 5L9j9WWRlbAhQeNHFGFcmChJ9b0DXL6ALpN8lt2kVwysfcSLyI+hfpzP0WqwY5XliZfju/b8HB12 DPkjWKQj+O/9nxYTe/l55tilc+gt11Zpo5VPInc7OUcn6M6XD23pSBvw+RH6rDzGXsXUgnbi8wf1 0Uiu9K1SzAa7s5rdTsGljZAfkWAzZYtTjyfTKMvcQ67Jp8kPqiY+8KNrczWtxHcQNwliYMje4yeX HGcTGXIscWWWOQlHmHrei39tqUMVzAQyMKuvdGAqVb3GcDqsEsMjGX9vm+g6bUxzQE4/2eSco3Fw 3gcwyHITAEEAjodxlLJ2KXYqx/X7J5UubeJgj3MThHaoVWcFakgHau+Z2kyiMoyPKJH2OPqMZnjl EcyCEB5f/LjSLBEm1AC+ux1Dbwqd+iftf7KvyGZ2t7dy5DUPRH7fn+p1ej7DxYxc/XL7Pl+tkDoI 3KqAoX7IGwA7UzUg3u7iq2fQWlzGfTbadl4mWJHK+HJQaZ30JcUQe8PAzjwyI7iiskxdirsVdirz L8y9S9fV4rFSeFmnxj/iySjH/heOcp25n4sogP4R9p/ZT1PYmDhxmZ/iP2D9tsPzSO7dirsVdirs VdirsVdirsVdiriARQ9MVQc8BT4hun6ssjK2BDdq1JCP5h+IxmNkhF5WydiqAu9A0O8Ltc2FvLJJ 9uQxrzP+zpy/HMnHrc0KEZyAHnt8nGyaPDO+KEST5b/NRsPLOlaYH/R0RgEn205u6kjvRy1D8snn 12TNXiG68h+hjp9Hjw2MYoHzP6VVlZTRhQ5SC5CJt5l4hCaEdK98hKKQVfIMnYqo3dstxEV2DjdG 8DkoSooKhp8jrW2mqJE3UH+XJZB1ChddLSWviMYcmJfQsESwwRxL9mNQo+QFM9BAfPyV+FDsVdiq jeXdvZ2st1cOEhhUs7HwGQyZIwiZS2AZ48cpyEY7kvD7+7kvL2e7kFHuJGkYDoORrQfLOAzZDOZk ept73FjEICI6ClDK2x2KuxV2KuxV2KuxV2KuxV2KuxVxAIoeh64qhZIjE4dd0B+7LAbY1SKBBAI6 HplbJ2KuxV2KrJI1kWh69jhBpBCElhaM77jscsErYkK8NwGor/a7HxyEopBV8iydiqhcW5kKyRkL Mn2GPT3ByUZVz5IbVDcyQLTizuI2U9mYgZZijcuEdWGSXDEnufQeegPn7sVdirsVYb+ZmqGDS4dP Q/Fdvyk6f3cRBp47tT7s0fbmfhxiA/iP2D9tO67EwcWQzP8ACPtP7LeaZyr1TsVdirsVdirsVdir sVdirsVdirsVWvJGgq7BR2qaYgEoQc2qW4BVVMn4A/x/DLRiKLVIbscAGQjbpWtPbtgMFtEJIj/Z NfbvlZFJtdil2KuxVogEUIqMUIWa3K1ZN18O4yyMkEKkFxyoj9ex8cEoqCr5Bk7FUTpEdv8Apmxe YqkQuYWmZjReIcVJJ2+z3zI0kgMsCeXEPvcfVRJxSA58J+57pnfPBuxV2KuxV5B531M3/mK4INYr b/R46eEZPL/hy2cV2rn8TOe6O3y/bb2fZeHw8A75b/P9lJDmudi7FXYq7FXYq7FXYq7FXYqoSXtq nWQE+A3/AFZIQJRaEk1c7iOP5Fj/AAH9csGHvRaFkv7t+shA8F2/VvlgxgLagSSanqckhVtY+UnI 9F3+nBIqjcrQ7FVRZ5V/aqPA75ExCbVUuwdnFPcZEwTauro32SDkCEt4pdiqhNbhqsmzeHY5KMmJ DoJzXhJs3YnDKPUKCr5Bk7FWd+RvOPp+npOoyfu9ltJ2/Z7CNj4fy+HTpnQ9k9pVWKZ/qn9H6vk8 92r2dd5YD3j9P6/m9CzpXnHYq7FXhnnLS30vzHeW4BWF3M1vQED05PiAX2U1X6M4vX4PDzSHTmPj +Ke00GfxMMT15H4fi0l5N4nMOnMdybxONK7kfHFXVPjirVT44q6p8cVdU+OKuqcKrZEEiFT9B8Di DSoB0ZGKt1GWgpW4q7FWwCTQdT0xVHxR+mgXv1Jysm0L8CuxV2KuxV2KqiXEq96jwORMQm1dbpD9 oFfxGQMCm1VWVhVSD8sjSVssSyDfY9jhBpSFkbsh4S9f2W7HCRfJVbIpdir0fyR5zW4WPStRci5A 421wxr6ngjH+bw8fn16jsvtTjrHk+roe/wAvf9/v58x2n2Zw3kx/T1Hd5+77vdym2b50TsVef/m1 pJktbTVY1FYCYJyAa8X3Qk+CtUf7LNH21guImOmx/H45u97Ez1IwPXcfj8cnmOc49G7FXYq7FXYq 7FXYq7FXYqpTw+otR9odMINKgiCDQ9csS1iqItI6vzPRenzyMiqLyCHYq7FXYq7FXYq7FXYq4Eg1 HXFVVLmRdj8Q9+uRMQm1UXETijinz3GR4SE2vRgopXkg6N1p88iUoTWNf0bRoBNqd3Hao1eAc1Zq UrxQVZqV3oMtw6fJlNQFtWbUQxC5mmMXn5w+TbYqYJbi8J7wRFeP/I4xfhmwx9i6g86j7z+q3Ayd sYByuXuH66ehfl//AM5Q+UtVuk0rzAH0mT4Y7XUp6GGToo9dgW9JjWpY/B1qV79Tp4zEAJkGQeX1 BgZkwFRe3wzRTRJNC6yRSANHIhDKyncEEbEHL2hC6xpcGq6ZcafOSIp1oWHUEEMp+hgDlWfCMkDA 8i24MxxTExzDwG5t5ra5ltpl4zQO0ci9aMh4kfeM4ecDEkHmHuYTEoiQ5FTyLJ2KuxV2KuxV2Kux V2KuxVDXMNayL/shkolKGAJIA6nYZNUwRAihR2yolC7FXYq7FXYq7FW1VmPFQWJ6AbnEC+Skgc0V DpGpzbpbPTxYcR/w1MyIaTLLlEuPPV4o85BEDy3rJNDBT3Lp/A5aOzs3837Q1HtHD/O+wq48pamR UvCPYs38Fy4dk5e+P4+DSe1sXdL7P1q8Xk+cj97cqp/yVLfrK5ZHsiXWX4+xql2vHpH8farR+Tog f3l0zDwVAv6y2Wx7IHWX2Ncu1z0j9quvlLTlIPqzVH+Uv/NOWfyTi75fZ+pq/lbL3R+39b5x/OyS VfzBvrLmWt7JII7ZDT4VeFJW6UrV5Cc2uk00MUKiHV6rUTyyuRYJmS4zsVfU/wDziJp/mFdA1bUJ 72QaC8/oWOnHiUNwqq00+68h8JRBxeh+LkKgHCgvoLFDx/8AM3SBZeYfrMakQ36erWlF9RfhkA8e zH55yna+Dgy8Q5S+/r+v4vV9kZ+PFwnnHb4dP1fBiOat2rsVdirsVdirsVdirsVdiqvaWF9euUtL eW4cdViRnI+fEHJ48Up/SCfcwyZYw+oge9PY/wArfMqQrdskahhX0ORaVK+KqD+GbQdkZjGzQdbL tnCDQstDybIrFJboJKv2k4EkfOrKckOxz1l9jWe2B0j9v7FaLyfbj++uHf8A1AF/XyyyPZEesj+P m1y7Xl0iPv8A1Ky+U9MBqXlYeBZf4KMsHZWLvl+Pg1HtXKekfx8VceW9G/5Z6+/N/wDmrLv5Ow/z ftP62r+Uc3877B+pXTSNLQUFrER/lKGP3tXLY6TEP4R8mqWryn+I/NWis7SI1igjjPiqgfqGWRww jyAHwapZpy5kn4q2WNbsVdirsVdirsVdir5U/O7/AMmdrP8A0bf9QsWZEOTRPmwbJMU28p+XL3zL 5l03QbIH6xqNwkAcKX9NWPxysq78Y0q7ewOKv0B8teXNJ8t6FZ6HpEPoafYpwgjJLHclmZierMzF ifE4WKZYqxn8wtFj1Hy7PMIw11YqZoHrSiggyj6UB28QM13amAZMJPWO/wCv7HY9l6g48wH8Mtv1 fa8XzkXr3Yq7FXYq7FXYqmOm+Xdc1Ir9SspZkbYS8eMe3/FjUT8cvxaXLk+mJP3fNx82qxY/qkB9 /wAubK9I/KjUpZEfVJ0t4OrRRHnL8q04D51ObTB2LMm8hoeXP9X3usz9tQArGLPny/X9zL9N/L3y tYgH6r9akH+7Lk+pX5rtH/wubTF2Zgh0s+e/7PsdVm7Tzz60PLb9v2sghhhhiWKFFjiQUSNAFUD2 A2zPjEAUOTgSkSbPNfhQo3NnaXScLmFJV3oHANK+HhjSQUovPKNhIpNo720gHwgMXSvuG5fgcrOM MxkKQ3fl7zBa1IjW6Qb8o9zT/V+E/cMgcZbBkCXNdCNzHNG8Ug2ZWFCCPHvkKZ2vW4gbo4+nb9eB VTFLsUOxV2KuxV2KuxVpnVBViAPE4q8U89flBq3mfzvf6yt/b2un3Xo8Kh3mAjhSNqpRF6pt8eWx nQa5Qsr7b/nH3yssai51C+llH2mjaKNT/sTHIR/wWPiFfDD178qfyf8AKnli6/Ttrp3p35jMVrcT SPI4jcDm4ViVUsNgwANKjocnC+rXOuj1DJsHYq0yqylWAZWFGU7gg9sVeBa/pbaXrN3YN0gkIQ+K H4kP0qQc4fVYfDySj3H+x7nS5vExxl3j+1L8ob21VmYKoJYmgA3JOICkp5p3kjzRfjlFYPHHt8c9 IhuKggPQn6Bmbi7OzT5Rr37OFl7RwQ5yv3bsp0z8o3qj6nfLQN8cNupNV9pG40/4DNli7E6zl8B+ v9jrM3bnSEfif1ftZlp3lLy5p9Da2EQdSGWRx6jgjuGfkR9GbbFosOP6Yj7/AL3U5dbmyfVI/d9y bZlOK7FXYq7FXYq7FXYq7FVKe1tbheM8KSgdA6huvz+WNKCkWoeSdNnFbVjav4CrqfoJr+OQOMNg yFILvylrlpVolE6AVLQtv16cTRvuyswLYMgSx5b23k9OZWRx1SRSp/GhyBDMFUXUF/aQj5Gv9MaV VW7ganxUJ7EYFVVZWFVII8RviriQBUmgHUnFUNNfINo/iPiemGlQTu7mrmpxVrFWReVfLr3cyXt0 g+poaorb+ow26fyg9a/LxyyEba5zrZneXNDsVdirsVYV538iXeuajBe2Uscb8PSnEpIFFJKsOIap 3pmo7Q7OlmmJRIG1G3cdndpRwwMZAnexShp35S6ZEQ1/eS3J2PCICJfcGvMn6KZDF2JAfVIy+xnl 7bmfpiI/b+plmk+X9H0lWXT7VYOf22BLMfYsxZqfTmzwabHi+gU6vPqcmX6zaYZe0OxV2KuxV2Ku xV2KuxV2KuxV2KuxV2KuxVRu7K1vITDcxiSM9j/AjcYCLSDSQ3vkbTpSWtZHtif2f7xfuJDf8NkD jDMZCkV55O1m3HJEW4UVJMR3FP8AJbifurkDAsxkCXjRtX/5Yrjb/ip/6YOEsuIK40PX5lANpKQO nIcev+tTHgK8YVB5S8wEf7y/8lI/+asPAUcYVU8ma4w3RE9mcfwrj4ZR4gRlr5EvfXT61NGIK/vP TLF6eAqoGSGNByBmccaRRrHGOKIAqqOwAoBlrSuxV2Kv/9k= - - - - proof:pdf - uuid:65E6390686CF11DBA6E2D887CEACB407 - xmp.did:d4d07395-aa96-47c2-a9e5-d0351947bb0c - uuid:c63c1031-e157-9748-9c58-86481308e954 - - uuid:1abccb90-0c26-4942-b156-fd2eb962e3e1 - xmp.did:58fdc1b8-1448-3a44-9e20-282d8ec1cf95 - uuid:65E6390686CF11DBA6E2D887CEACB407 - proof:pdf - - - - - saved - xmp.iid:d4d07395-aa96-47c2-a9e5-d0351947bb0c - 2016-06-15T14:23:10-04:00 - Adobe Illustrator CC 2015 (Macintosh) - / - - - - Web - Document - 1 - True - False - - 128.000000 - 128.000000 - Pixels - - - - Cyan - Magenta - Yellow - Black - - - - - - Default Swatch Group - 0 - - - - White - RGB - PROCESS - 255 - 255 - 255 - - - Black - RGB - PROCESS - 0 - 0 - 0 - - - RGB Red - RGB - PROCESS - 255 - 0 - 0 - - - RGB Yellow - RGB - PROCESS - 255 - 255 - 0 - - - RGB Green - RGB - PROCESS - 0 - 255 - 0 - - - RGB Cyan - RGB - PROCESS - 0 - 255 - 255 - - - RGB Blue - RGB - PROCESS - 0 - 0 - 255 - - - RGB Magenta - RGB - PROCESS - 255 - 0 - 255 - - - R=193 G=39 B=45 - RGB - PROCESS - 193 - 39 - 45 - - - R=237 G=28 B=36 - RGB - PROCESS - 237 - 28 - 36 - - - R=241 G=90 B=36 - RGB - PROCESS - 241 - 90 - 36 - - - R=247 G=147 B=30 - RGB - PROCESS - 247 - 147 - 30 - - - R=251 G=176 B=59 - RGB - PROCESS - 251 - 176 - 59 - - - R=252 G=238 B=33 - RGB - PROCESS - 252 - 238 - 33 - - - R=217 G=224 B=33 - RGB - PROCESS - 217 - 224 - 33 - - - R=140 G=198 B=63 - RGB - PROCESS - 140 - 198 - 63 - - - R=57 G=181 B=74 - RGB - PROCESS - 57 - 181 - 74 - - - R=0 G=146 B=69 - RGB - PROCESS - 0 - 146 - 69 - - - R=0 G=104 B=55 - RGB - PROCESS - 0 - 104 - 55 - - - R=34 G=181 B=115 - RGB - PROCESS - 34 - 181 - 115 - - - R=0 G=169 B=157 - RGB - PROCESS - 0 - 169 - 157 - - - R=41 G=171 B=226 - RGB - PROCESS - 41 - 171 - 226 - - - R=0 G=113 B=188 - RGB - PROCESS - 0 - 113 - 188 - - - R=46 G=49 B=146 - RGB - PROCESS - 46 - 49 - 146 - - - R=27 G=20 B=100 - RGB - PROCESS - 27 - 20 - 100 - - - R=102 G=45 B=145 - RGB - PROCESS - 102 - 45 - 145 - - - R=147 G=39 B=143 - RGB - PROCESS - 147 - 39 - 143 - - - R=158 G=0 B=93 - RGB - PROCESS - 158 - 0 - 93 - - - R=212 G=20 B=90 - RGB - PROCESS - 212 - 20 - 90 - - - R=237 G=30 B=121 - RGB - PROCESS - 237 - 30 - 121 - - - R=199 G=178 B=153 - RGB - PROCESS - 199 - 178 - 153 - - - R=153 G=134 B=117 - RGB - PROCESS - 153 - 134 - 117 - - - R=115 G=99 B=87 - RGB - PROCESS - 115 - 99 - 87 - - - R=83 G=71 B=65 - RGB - PROCESS - 83 - 71 - 65 - - - R=198 G=156 B=109 - RGB - PROCESS - 198 - 156 - 109 - - - R=166 G=124 B=82 - RGB - PROCESS - 166 - 124 - 82 - - - R=140 G=98 B=57 - RGB - PROCESS - 140 - 98 - 57 - - - R=117 G=76 B=36 - RGB - PROCESS - 117 - 76 - 36 - - - R=96 G=56 B=19 - RGB - PROCESS - 96 - 56 - 19 - - - R=66 G=33 B=11 - RGB - PROCESS - 66 - 33 - 11 - - - - - - Grays - 1 - - - - R=0 G=0 B=0 - RGB - PROCESS - 0 - 0 - 0 - - - R=26 G=26 B=26 - RGB - PROCESS - 26 - 26 - 26 - - - R=51 G=51 B=51 - RGB - PROCESS - 51 - 51 - 51 - - - R=77 G=77 B=77 - RGB - PROCESS - 77 - 77 - 77 - - - R=102 G=102 B=102 - RGB - PROCESS - 102 - 102 - 102 - - - R=128 G=128 B=128 - RGB - PROCESS - 128 - 128 - 128 - - - R=153 G=153 B=153 - RGB - PROCESS - 153 - 153 - 153 - - - R=179 G=179 B=179 - RGB - PROCESS - 179 - 179 - 179 - - - R=204 G=204 B=204 - RGB - PROCESS - 204 - 204 - 204 - - - R=230 G=230 B=230 - RGB - PROCESS - 230 - 230 - 230 - - - R=242 G=242 B=242 - RGB - PROCESS - 242 - 242 - 242 - - - - - - Web Color Group - 1 - - - - R=63 G=169 B=245 - RGB - PROCESS - 63 - 169 - 245 - - - R=122 G=201 B=67 - RGB - PROCESS - 122 - 201 - 67 - - - R=255 G=147 B=30 - RGB - PROCESS - 255 - 147 - 30 - - - R=255 G=29 B=37 - RGB - PROCESS - 255 - 29 - 37 - - - R=255 G=123 B=172 - RGB - PROCESS - 255 - 123 - 172 - - - R=189 G=204 B=212 - RGB - PROCESS - 189 - 204 - 212 - - - - - - - Adobe PDF library 15.00 - - - - - - - - - - - - - - - - - - - - - - - - - endstream endobj 3 0 obj <> endobj 7 0 obj <>/Resources<>/ExtGState<>/ProcSet[/PDF/ImageC]/Properties<>/XObject<>>>/Thumb 14 0 R/TrimBox[0.0 0.0 128.0 128.0]/Type/Page>> endobj 8 0 obj <>stream -HwVu6PprqV*234R04S32P4ճT(J -W*w6PH/H+X)Hwr.gK>W /@.ӊ endstream endobj 9 0 obj <> endobj 14 0 obj <>stream -8;W:dYmnJk$j=`^PKX*GV"-/6MPPhMW4o*jFegTA5n:ROqi. -8M?-(/t#IN>re.=TbIMqYWQK1D%b&pOLGa]H?hKs'8Gqa4A/k;[i&\e-=4:h!/H6BW;~> endstream endobj 16 0 obj [/Indexed/DeviceRGB 255 17 0 R] endobj 17 0 obj <>stream -8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 -b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` -E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn -6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( -l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 13 0 obj <>/Filter/FlateDecode/Height 947/Intent/RelativeColorimetric/Length 90241/Name/X/SMask 18 0 R/Subtype/Image/Type/XObject/Width 880>>stream -Hoi@Hy&8_nyA'?6G3+ZHҥYakOj6gיoU GHII_AgK/EcF6LrchI 2$҆ԘU4w$5_7BQUm"Ť>&k2W$%Nib;Iߓuavտ,HJ \u.&1ٌ^₞@Ǥl_Lrs:#ј,32] IJ7d+65i1$Lb#d]G>&Y=g답*_/*:p.uʙcRIf") ˬ#q4Ό=sL&=(P{ HJ+b~n+cSFsm0'&&cܼXI=3zER,D#0)2=r -I Ә}즟 (9?l?ݳ;݃Q~twoo `41)"g476WxzGMݞx7hpqh{ƃn\ w Zᶂ37{M23>)25Eܩo|+>8q/8m y3=??~wL#I\dΕ/doޱ=Hh|d]ү$*wOc} yz<*\@~R/}}FRHxw G]as &lu9x")`m;=-Ƀ -O8Cmȑ{mG.&?){3];,V01o`it4)'ѭU, ]?b<ݳN=;.]Lں*_w6}hL[I$np+bdjlb46[ܩ0k`I{-ɯG_>]zt8rI_K}4јغOinӭng`HUN4ݛ|yRr #+x/>骞SyXAQȃękfUXDvE#&sBe< HQ%\Ωdg'sǟá:>xQ -!K -W<* '%Y%Vmao!ǩkv>w u{=Q<\ȃ*fƸmqY%ŏRpV{ ueW&)!)sE2Jݓ?ҋӆgohԎeɝiFGb}/g. -,%m.7'FX!TՀITV $y9Iפ?_ȼ0;Wi;h9:FQ]itB)`5yIe[j*lraպS"u 3$hۯNT´A}ٷO.ϡqˤ3!"Iڻkha˳J)@) iq1J٦oչrEIWt+>]hlrW9,-nr_}i#eR=椔 5 ](?"aIK9;z>g9d68 =FGY/Հ@@ 9ۋFJT2[̟~>:ekG<Q2B&M)}YƢ}\ekfeJ#.-3 -iHF'>hd,I#_ыTj~Q5cR`n:s e8 P/di]Ҩm +!g֝V=@nI${%3Tj[ԣ`Į;m$XOT4==Aŵ͆ޭ|hˑ"6XWvZY,{&y` wɵs/NٮMDz3z2 F^ęA r۝gB7hu ȲhI})CF -WWzٶ:lgl7ɃHiJ&/ Ӻg.}C'dD|V֪'9TL*4I]6 x74MVK%X T8ENZ!f8ah@&͚-AsׄOQ"2sO-ʃ#db-tgHIFHVj.Y!х@Cdҕ@2ǯeqyJΎC43nw0"D2ȥXiQϖJ'&:?Ed!ªGIKSيb$utõǭ2'}~dbNct`d K񕨈\29juC ڣy 5,ҧ9.~&g(r\&$zkddjd_&n,dk딝]|ڷт$lw)-H](H&, tHU4ѐxIE$\+Kl֓ȁI*?^^O/N*)iit<~O&=۠SZ0LK578hsZ?5ĬeV"k K ->#gV-?}= TjOK<>Nh auOBnY#qkB)fiQ@ 7@Olo-n 9 =)~erŗzC9z9Ilr&JO-NbW3Ӳh adR<+}D?NJiPJG%:?5pn2TI2v֋rBk &l f'<[wm}2I4KtwH r"!hͣ .:17f͝$Wq`Z Z 'R\%n~2/f|i|a*J&r>-fj@eRc}'yGBvr*'r ->|.WB~0ɱ{H ܌232ɤMRe7r!ic/_Ȗm͇OJ!dfH)OtE9#jԝj'C]aպWf+>KctȁL&rK%SͧH&1'ejuQA2p!Ϟ* UKW?-02hZ!nKO.? ZdzѤ٣wrLI*΍Sі2+TI,5N$6ぺ G7 whQXI4:?5ƫhq-Ǿ(v,vHz&.aKiݵdTOph2E ɤ0J>-zBb8,A6Mgd͝$K I,E[ 8ƶ0yTS rlS]|ѩ+&_9Ezb(Jr2h+ɓ)⇻A!_:۪.%ٱ4E3w~ sOq9F$~EH(M"0>"C|)4 {edUŭ߿/|}e.tD _(^u)TJ 8([E;ZgbDR!TY;g$÷=W__h8pü8SqO㠸*5:Mb)r2`Ny@:iXg*-X=zb-osW̟Y[V$J|!h|$p6V2LRwsU""YA(\A[u0#0j>k6NZ *P$Idv`;K?29G3@/)hqGaLH&)#f2ݥ:"@ -c1BuUU!hB -m?IXqBf=O-uS]*pb Lp=a d0 '%}QJ1kv-E&Y%͇ѓ!L6y֯-ZNſw@ME<V -+Qf1XGbu.AL}{;j:1XM;`m)ݒr2??bӥ^"T.4{7V:7cO n]&IIʴ׭]׳L&ټ~e?618qW 6$уS-J+j &HR#Y(u3C"vaVO qˤg/{Nd* w4~8`ਡODT -( ƃ(rVu6z0F|iU6_Up_ |7//y e26kaE9JTh<'| e\xy(7QQ1Z)7#5eoӈ0g+ۨwCxc XSg")-nkEJN[* FAKLLR7R.LBME#@* -~U݊tEEVOt7U콊ؾxԜ'isjf=O[ZO ((A>&]r"т|f0`A|0/2}+58{:!ELTǝuB1HzGQ0g[|Q_[VSor^Gy?lD$g=?ȩՕLN9 -K*RYģpu0%K'*- lpD ID2MnݾbL)OYׯR3OF"R j"iO8%{Pqs ?Hee}-XJNm\-H/}GϩZ&L/u>7&I wQ) /+P.bP"$N55B]={2`[Hnk?-s\粓y*dqP9I,1k[`^A/n3ՕcVna-_s%YSM{b FǠoZnkE%yt$mAs%Ev]JW^xA Xk0v_KӉ i$Fߪ/u( jLIO%k/Sr7B>Y,770ݙs)]ubQ9OΈL12$jn*2*쾊O&iV"sr+ 2LjȞCxҜJ 9:Jʌ H(hxA&xk i&Iu$6ԕj:.E Q\-^*%V@V;RM]dLW<}*w&Kߊ{-7@-&;. -C66 @9TBUfI[#v1r`,f/5n TLֹޤosIwT&\ߍ#UBXJ&uo6TE-EHLu[FUf -x謖Xz{FEr6qiVd>սl -\Uv^dKCR&p6kڄo@)ɛzxZZfBv5nFC `r{Lŷy7g2H&;x@kASYQC,29Wp -c!{)r*Rj!&#8ˁScM}Zi*H&Mf$\P -Ŵ\Id eDҐIЍF1|CeH ldԬ6i.2K8t׎&t(Q+ZfB*R&~?g4|W~ !$1[NIkqS(T['iͧ4*m~@?>KT)ΕJ3t -dEÀ."!|g_F>";,o)%OQ~Z2FBt-Ŵ=ݗJ)Rbd/}0i -3%f_,%.u;}oZ_`>19)ۂ֙Ĥ b- 2z[&;BEz1i6Cӯ!G9htj9'I#8ˁB!=*t-T:zTG\2z;F3}ZgLhHӍָ!fiVL:,N0EwHVsR I !-U֐L1#4zvB`V|u 4i$\"&^Xjbޟ mR q6H JER/-N&w',͌ƹӿ8!fI|1TBS?D%}35fW̊CȊ\!/5n:VC1@#&R&2rÚ!"H OS&c1[pUyuN'v96c9)Ӿ/I W%f<}*Gz{-/0D֧)T!LSXva?:n[ʜUX% *R&~o㹤w-dr>و'r*+*1=9)zKy$Sp$"ӔIWcQ@&~!AZۑ[W *'W|쌰95n}ăF.i(xyB [(58|i+&Yjhz>˜2m^?fA)\('N,1^{,gI&}<Ǿ dTVԀaI$7XUkt sU dl:OOٯ<2w)6E =Lq<{ hK|7%FE=ikA(#cia78Hw:;i'}h%Z^N^7VҺuQIHNcMft+ -0 '0$:HJ\ ;``pPL=NM.H1Eb~ԓvsK`TO=hwѓ7|GOYZaȎL7e Ջ[녤&Ɍ;b1lx"Iw!}9pXfv`7xgV~π\\zπD-/؁_~ɬebJz!QyrTS蕴.}?]$i;o"):F)(&ۂS^/Ǧ1IG )!bZ1 p8ĕT-@Kp -m crE?m}F!e_JRPF -7b1T}<x4zV,&읲yeTJ=q#cz>)Rv:5[QГO"5o) c^mXyTعT=%o-oK2U~c͠B>(h1*h|:6Ll' ޑ4 =ui7eGo{s͈KjDE1!3e00R,I %y)1]5ά>#Vgr|%vVV>&I5lS<v Z ;RLj/1'{uO -ؐsz( o?;I&mT@L>`|wdF[pY!=;Yk AR;o^2Lh ~_ٛ|GdO q/zRqcw_}9~eiq$i[?~$N%Y72zىSEx1,HDlEg3JJy}F}7)?'|(GoŤ*isFGO=`r3R8)p/MM$j٪u=9(&˜|m5(t75zM'O \]]ITٱ3u0Ǜe/$iF1Da*b1Tzz_W8/wzg'=srV~@?];(&0H1ʿ[P=kW˻zBf`d.d*XJZC^mt.'h V QLqr9wTҺ辣QEx=D19-d!}?d!}3Z#)nmDzly_|1^Nd`Q0l9'0NnbX9T0ZdWf˂8d=pJl#&BsԨA7FDqڇJZ*レT=eSH'YTo4>doE|~ $#&1¾W` ڑ1)د6:'P}O࿠ne*Fرs|q -(iC4P+ $ -cT6^b-4je˷O|zS~?_qCjRr̖E˓>jEk.C-n#E<IO{vE5ӄ&EN& ݆)-:< I'%}8HwяV4d=Yv 1|Dh*; <Okr(Ny%*~*Yy&WA4!x2zsY-L"=\Le5ƔRFI%IF\3_87 0>hq x2`+IǙBh#R8-w*Ó1YeVO[x!mh?[ <$|`2s2l嚽O EҌX)#Z!Sр4Yoy?ªf8jO1O_9O"%z dy.nNY2u5UV\Q~ɲ|kxrd'?apK tE7s!m`jqZv[>hZ-%6}az,ڜdKɷGM)،xd"6T\l,&c<'Uf; -w&B gw)#„S]\QVQ>$I_jJX,\^YSd|'zD%{o1!䅇qx';qڈ>
khYӷ@mwGyxbr ~=ͱ{9hsۈ!x2< !f!mf" e!ONYW,B-%'>,;} ^&CE"OtzK68]dGRfɈt}B)ILN-^3d[ɿ/3GlV&77ug#K1P)^I\D}&/o>߭SHh+<1Iy_uA^ -sMzC*d\'\z1zADd& -9$Y"?LtzK_14*Y|!ԯ)7$URyuf۲/ɖ$yhs:ڏa<#<(){;qSdLt&}ZHy$yyLܘ: ew}\yYj|aIQ%#?xCE"Oq1Nb5˵I~t֌ZcEb-%Շ8}@i?qqN~ Ndl'\z¤.o !جlݜ(B],YoSO&w"0fr -L&\Pby?ޓur!m FZKGbrΓͲc)eE+WqytT?w ]]}["֫K5Ez~J+T3YnO26hSEH'㷢MO$ta0?j9VuK'/K~CcζI-OV/KѸmkҡuΦ)"&㸔]LyĂX)cML=yn\Ω۬ crї'ma5r(E=pu7< - [rd{d7.`w(d;wr(M=zRy -7]=!Ij9Cidy!NmSiǯƆX9r R:wP<+y^{᬴$eYn;ﷂ2^%)uũ Kw Eߊ. UxlidW)I5Ip΍y%gMGƔd Z9"~t]utڵɲ2E#{Xtp7 #i4i>f-2x jL?bGœ{y9k -AQש'=FE4b2&al6>` -hB");Is*QY9c"1鲒z2klvy0 7>`%JN dXn; ŘWO8@g,צ)_cvH$q\ѾM_@%Ƈ؛XBRcΜJcRΆ1xZU]è-9NEw'c뜠I=]c fi~>?!NI&ļfb2 Z8,W䥌e|a(!me?MQH'cMsY*+!\VuSLQ5Kp#}֓mj:SHú5\bØC)I0> jYn>_+c t57*pT̛6=nW%4EQ4z2~+ɜEO1$|T9c^Jt,Ύ9AŵK2Ɏ'+{,uCL/ڻAZ" -d֕MW.oBgDtʿv(uX4Kp}Gߓ-8,]o^ѓ[J^c(NY]$eo h9[ƣ:1vt᥌e| q'Kp4 b>HAB t2n{'ͩ(*rGOӡz>(-ɘ-d6=г.k؉NO&37{UGɓ*Ysgzҋ>XA[ &f.oqC󢔱gs.$e/;?n>.0&cT51}<;(*FOkE;a\.S+S=˖&EǗo3rLdA˿}yb/IE\E"*;pIыeCbuZ ){ٻ #=I_cN|k)rd@Q?h.3Ed^ts*g5 xQX욮&VO䩪(Idt1>ðh[[AEX̕ϺܓyK{./T˱^>xL,Mo'svy[/*|OJgC{::EQ1SDLk%>>Ѕ<I t -eo$7-gHIΆ(&-^^rs *BadVؓ-W*j͉IvHʞNLO:W%_31dӨXܸvH^|Tݓ+9/Hrdz_wq\e?@MQ̙ݙ"NuhEA(iK̙}v(AHQ"@D(WDUlMĎ-'Eyf&٦Q|~GV ?|mzR3|}pӬ3 QJoJpd\nc\7n,Aײmɏzs ޡ22JywoK2/X'& ځNJ* pbR3|w) A7ɤbrc )#f dp[M_&g][ه?@O*v'd5Ä-`˨[ GOFntLλ=95fPL -&!&;=@OK1Ŏ=5:2J.778&$k4RJGL*sͽ֨+IN,Iij&}`K闍sEvDRϿd}OIb_93Da`(r\|@O@Moߎ$gi)R~cb]_+$H!n~F]1GL*vZFi2T'{ z\ARFMbz~wb1Qe5+zRb25em-3_~Ȯ) ֧I/_Ox^JIQOyT=F9CW鉣)fʴRڴ1[Ig - &NA@Ot\ᏻNoV0[%dRїbۤARJwu oX7h4b0$m~a'U:냰6G8XГOГWuNVL1҅ Qp00bIe΍{֠ 6 =q(B:w6Ñrޯ[?^ORLRIv+/gRJ:A{MAOzIN0n/{Nac5H$,+ԓr~ ~OWllfC+0+ГwځZWBA9%gѓ-y唋w-GF)Uk(5?C%;=GLj/bIIzYw~<HיմFi1GL*t\۵ŤH5$gP!||5Y,_;$8hdẂHh C~aГʜ` 2$R 2-D=yq{IeMИJ)1 wT2#&:=FI'@L&ƥfZDRʲ2n%%AL)~(_"H1IW0J W'91S;nZk PJk3=) {=^)&/75fPOG3-#&7@.d{LO\ ~OywtALL%h//8IeN@7sbHbۼ1W2\jn} bRn@z4M6 -'?Ztw -٫0T?^Г" [od% I'{}q{LII6WeVgROS8 K@D\>&;sp6"l\Z= SԞ89c-|~ۘlr\iƌU?D'3&Haaőom“ߓ?wP9Ô"BH$&;m5wI'6WvyCi~tw g#̵͎.p9 FЧziۈ$)&$#})r%R+ [=ړ~t; ՛!Qײ^Gzr}\10Ouc+#C͂&(_HP(D -d!թXKtkcZ*yͅ (  w¯<\*Db,$=OVp'1K.:|/lӥ+i&o练Ɏ[UlL=t _wHY{D'C%A)Cyt)8tDzPˠ|!B!DDEg ̓~WF/Vs'A\U/! -.a{0Ç)zfnڛ>< -.ĕ#_uMLzb)ZOVfc+UA)" -4D')58=26L">^&Ư~nc#{Uҭ' T Z; $U:ri -_͒K 쳷x#LJ4K\4^mΔX][XVBf@)5:'7}OV2L=Piϲgc0Yh-8iҧVk6\o'Nq|$T($)y6Aߓg O"Hb1flsarEtku*F?L$ |)> R ѸoB(L7H>IwUhc}[3;/)go2qCJ= RHOY$BkѧJ6)b Fl{h-8թrbVyZgcF;HҢt@ʰߓŤA#v齌3BILODxRzI;e5 Rkw'w9OD)Ý$)Cy|O[X)7PG$E,̿6D\GLJ_VO,Zhֻ\/of>!Ç6A0ߓN(+MC d.ia1^j&VmXuw}q:ZLa\RM24Iaw ! -yš|%0KeX\vIِ)H{fZ;qRC{ /Dny]&OkꉥUS=l'凯Gw%)H3ct:BxO 3$0.e#PɶO -|XZE_\(ZODhғŔA-]%f.>nEѫpE/zT(ЄOJs-M*_*PEJ}{ Pm3\)W>[w*]d:@,wZ$IQ@97,aNEd$KeR,}jV]RDP($)]ߎccC$BhGlkFbRz@>ZVN|H[l$R=y:QE>HiϯFxbJjVV5ܮyR^ -rk'eG!% :W!G{DNhJ\9\wACl -wϱR>"j'3J)_PKwG&) wZtݠVwgc)ßHaO&nr#󬦲l'3yxY}fdKJdARH9E}%TTҌS4<zbtXG٧WgB'WVD1T}gLbi[wǚS$B Lٹ#VLXC+;ݪd #+oIA{0]ceR(d֙F3$Bxt@V IғVm̴dE!)yJ>D*:!Zy1Mz/PBIf0 Ҏɸ; Gځ*z͹6HOI`)\NW~㠧£aITVߓMӬ$B'ctL&ݪ\Ylik>Wccyh)GՋ`Ji\1W݅JHrmL*w@ʡxѲHzCx /+΅cf%&B_$Gc&/ ɴ.>)=yV`pB_5B#:ʹv't,;fs8KUeD 0p1>$Bhg91jILJup6ꭕ7k6$?:L2zקb`};=R=fyq($&jkeMkoxs9["L -UB.t/MO0tx!Dn}~yLҿV]=2f^CQ_uyp%(I͹䈬!I-yBs95kIAQnԩ{Sѯp篧Gm2=d7R&Ӻ4M 54;=NGc2ncRt`AJxw&ӷ4p#BΣ)Óe6y0)%L^_׫rhe{-G^O9&%4j2Q/ -LAHiL"CɇvMå)30PfۿՂXa0)QqVgsIV^UB~l׋ Gސp3 L4RI9&M?V !$)n+Ye6\V0YO'j Sm,u^)dw>R CҠ/eO 5`2 j'#=%JXHPe9zTw?pf}iTnQntwR)OX"pO%tT&Ϗ]3)$7ePf[pr޸||l5pndғ+ĽH9zK6]mŤ zͿR8!>u=oD!h`EE}^:zO)vNVB%Ϩtg13nՅvkj,2Y'@]>';qQ)dzٳbT O? :wu\hvߟ#zٜl]CV rneu&w{LJw'R-FE?!R/DJrI$d<12,REV%U<7g:fg^d`bcꩶ[:+7>VΫq ҴP+}coVD0+ [uBKZ~ RyUs.++3UgD0:=/mCԁ*#iU}$]9e88 ?N!"::-_:kúXQG9zչ'Iy\8R*KƸ/!~Tk4NFIʞ.9])3#ByoL>{cRnn[n7V>)WKRQY#UzO?'s=Рg]duC. R> -'}nMty!׸/0y([v7t%OZ`bsI=P'L'ol3{%RJ }&|DQ5M,$)KBcuq\B銞SV'Ofw57'H#RΘs9'R/)Ȗ1BrTk,pY -}+;B(Ř ɯGE'ts+ mF\wO;KlTȒ_SOcf@=-M//:GnW?qPgE9oO%`^ xm{5Hܞ^Ĕ™M:'Z!2Wƶ4ro+nopEfDjtKГAbk&V=Bj%ܡHIc8gRchJ\#YYZi\\h<]R]Q_^vЮPv7>>i <]NlskT?'P5mIF@OU)xUaT}#F;B@ =~_ `rm,Z="]H ?jjR*~yT TI*{zԢ,HF -W!DdUb;<޵s[#Vۦ-Q|_緍Thwr+PKR*B@E` kR.Itiai^ArȥkP_ڃbDJl2ˣfgIrlX?gw>X<}"W -*e Oh)5ЋPŅ]lxh7&\B{ԭxhvRzE,Y0C>yF UJA)_~D7DAA$;)Q&%AGt)K^y4ν* o{MO8pr\r@x"Bqبrۑ]JƾИk7lJ){'@oJMNJ"\ Fc}IJӴq%M d* ,l(:KU+͌'ᏏGJ)23,I#kLZ 9:F-G'\Aθnqޒ`w.kY=ʆ7 ?>:ϕJ1+4V(*il"K!ʓ2G9.]*ӱU@)[r7]>cےuLDMbV [FQ )zeK W2|2& %n0I]4ukǢYNfh;Afbke2$op푮^J2\2޴ )HR>}yRE*oyd } iAbI_ :KUli -d4<@{d΍b}rJ4E7l;|@*w&$!ϧUke2t-:] -,!߼l]}~ =oz{֤X5Q$>eL)Lҹא/] -Og6*beC>7WH+#bX&8)rXu$|RGmkÛVlR޼lURՙ+ڑB#0UIEKأ9~,bԲ surX`,Pvx#Er TUUnMt)NOsT,pa]~C@0 蓉-#$Z|f—)E\%.rolϛ͂ / ߍbE2yL{L& "t{~@C"a(R -tRuf:NReؚ3CQJXkl cfS,hICc=u0_Wfk>knL1ז^O> ~Q'tz`'#W xV -t`O=?7F{Nvfowvv*QJ*0 -D?ޙa B J_$<z;i{wF#e={\&C[r!7&'kn¼~Ѻ{]2 @ *n{Q^Qw+eǔwT>~',U)+DBGbe!z/E"-|tʌWXbvF<6NHP&?pdrA[_Wm_ -5?&PF1J'3p|R]]9M]9LL2 Q -LrHP<ɤv4ΒV^ZYv?`vFRB(M(  -H4JoէX)Ϣ G)<Ʈ@C*p&̟\q7H&5UQ^Z^u-R)E7?A|^u60H%LϐORКr{$$A@$n|^v$zn₰WSo_Z[sSrdRޛ>||R -% -X3J*%0|,ϙ"g,39!+\JdR"NtgQ^ҊRlr?R)i'a,P * Jycq?DVI1? IM<(.-i[-gb\~{ ֟!ɥOZ:,Ø9{ٵJ:36pVݕII- o޾Ѳcݷ85kk,K(;9g' 8r[a/#<4+, -:VInI(o d^r@ԛ/{w?p_&4(eDRcёD>]+Xkdqj22y{6pdRw S^yK RE)10Kҟ(. E6L, bT!ЕLnTνe%U-V* [}iIX+ٯUBT&C86OʳDP1[]\/&ְUڪKKjn#2LvɩO%JzQΎ~.' τ9+RTDL.tdR>"V+[Wo__w7B/W3 O.9+Sl>t]ӉO"oOB|r -VNͪ^32 X)mP ?sbֺVf{D0/#o`7ΒVm_Z_~ BpSETTzaLtZv2Z)8Jq%S&I?IHd:A7.ɲG=Pkɳ)?(_;YDlgO_!;FJ**e' )2H& zӏz,T=Y]=+eQ/0g"cb|蛲IkF $!YrڪKK4.»5Jj;>i:':nA){;,nXepx}P<4MXyd@=K?IFmr[ŬUT=Nr I!ԡ +b(9qarJans=Iu f'-'Lu,QJ RtRJHEx<'*\aOpw@ӣe nhzuFa·-T_~M2I<L3+vm n a`Vx|€q*&L:t+/,C&MXeUH8mL^UI2IV9{5)uR -ƏA)(#nao$<2cIGG&/Zw$Q ھފ}!iD_~ϾzdÈ40ɴb)+2 dtd}wCxkW 1  >U ~lUH&6(^q10Po=&L_v|ș$+Aer@B6.䉳:/ Jy'Qʹ sx7o}'oLO sSao^<I) -2 -$lWS/`_wt7U6?J)p0g%z*u#"#eDy ڔן8ȈggnW4c[4R~zŰ:,,G L}ʳAHLBoHxVۗ~,9ʛ;`:A)10C|E"Edxvۭ -tbX:OZ` RFyxQh$TIcz78'a0y&'2Yd̟L/b]U/`_w[Q|$wYKRwEWp =NdcTWd@gBDHvrPXx^`aL?$I&Q`OnfY)*͎LL cz$GD<Rѕ۳dIⅉvkLn{yL2U+»=J$FwB! LouM_9[ƵR`#JA }IlVk^ݍ[[$<9Z; z>I)E߆Eܘ&LiÐ/Q/|e0A EߊH&~ȑq?, TUt<d`_3MG*&􏩧w -H\A=xraOjVDEI`NI&Ό8隧ϗ7E69t}y^&+tz“/޽%vp\ԧ">AnB.WC(yS&rt+YHJ W"U|C~KJǬwRŔ!3c[1 FdI2qǐxCo)Bi)LN0&%6$5KL#xG;n䟴T%m8<9%S4o6 I@Kq=ow;Gnۊhх|!`Jd}<v,Yl6I&ozIki[іp뺤u13O*QL#dI'LH;, o+R1 ;ϝ DDX| R&K!R]?y;i'|WKCr0X,>{;P7`Hdt~ìsVBF *`V7'98QN;&Uqk¤Th:0l.0Ϲm>h7JmTEHy R}ӿ_J9pIcT~B{Lh"axov% L"%˛:0Nܮ7Jm7XۊdJY>;)E{d ;L*;[0&|X$Hl٘LD'qYTjd]#mcw%R/oSa~/؉0+ ^&? -\CD}#IgJE>Bm'Rӆ"ixؐcχn' =B3]LItf-"`*E'GI)\R&'vؒNK)¤V3,L*> E}o|| -Dң[+lvu,ޒfZ˭+G_2T$fvS3DJUzDJF+7$; S2[+K}t]aBz1e m л~> R"eΙFc!έ|F W"%FI O)KHrڰ8:3Rƿ$ %R$Z㼩{W"=q t)pmyۊd@}zY:3JJ[F,qB&$Haos\7h=$%L&8ͤj߹4n1ȡ3)틤 % -n^_G,hZm|R0e"<9XiI$O.>j<3!R i^L LrД/15D6ĖmEl6uA]W6<=H"Hk!.I4yIܬEKLj6#k[F|r1 [C YI2ܟ!M]t-u:~lDAVtĥ3LMU߬JI瑒:vT -Q$EmHfDSɼ߽4Z&Q0"v%Ls|'瑒PJV4 h0yd…F e3LV[җZ.)Hm^sH=10 H^;ly,pg/B~\:Ôi| ٘F,& z BF -&H㑒#RʆBl, m+ -L`ڪlѠ6~TK''W"y0i%#gL襔AOI,g~et)AAII(kWu%2?X[E"\NHZ[Ѯ&QPs/Ƞ)_bɆ+Z%\yѿ^% cG_ I҆)щ7dDo·=D >V`%^n_&|l!#Of!!e -D'a?t1<|)zXZgz9x;$"%v)i)юec^$RӔ/7hJd]kUҳg}qOb&3IYJD~Fە30k1.zmE[_=.FZA<#VNɁ.g*|Rܨ=ާ&Ir}dEGwL~F4ƤD&sQ> &nl.]bj` Džؠvuf|c0~̈ Dh_ne6v/r+.& -BcO"N*HsVpIzQ.h% P@ Oq-ko&zcǛwy4;k5$wxPѰ@cl.7x[:qΙPJ0${p!YKVb1`= 5Z-0~),ȸgG)OEI)[K@#$|=+qPODo[kVf'g1sg-gf"p]N@w 3߈%-Y,p|Zyǂiy1ո*DIOu=tNZací( (8s '%$!@6/cAѲ*e}Y>Qc33gC)CB2de 2 ?eGneel LY(Q4:]rD *l= aϼ/_-t쯥,vAh,XM}ow#$D筫3y(% t0b3F+d/O8 &d3cAjBtl/hA;];ICJQJJ d©o-Tz*iʉXF:72'zڬvaf@eJ;}3R=L3/NFL>tZyZb*UVf/9!l{JL@). d]h -V$*#%RJsci$—0R.dL@&@@R+Q,8+δqh_=DXq(%8wr⧟L ڼj,zIQ6ǃj\kNA[fk$^rθ%rď?;s -2 h"V <44^WGúZU6v=JIF. -ẅ́c=M~_ghf]Sɷϩ`6SVOVd-6秋1}ᓈ/U# ??I}G> ;9G'#~,CڹI9=3 z+{ak?qz8gd%$}Ye喱CBN=sXlQOO"~ɫ#旗Y[>}OM ʫߌON 6L_=>~|'ޙOFLYO9ϙ`hg&W$m=4óI@A3+YLYyrAg;5MWځL"~6r+WԻEI0 KVs[ dE-irB ʿy?oGxzt/DhG╯O]Ͼ0&ѽsg#>|Ȩɿ?+V / cAeò1?7|M<\ݷ.I[GK3Sԭ7.J|7ux纇>?<{_}d)*n?&LC+YСLmp$x>}QҘe1LWe^9RgΈ7QdDGp#oU]t;w%ǂF 09IJO"~Jh(^_^Tfm34{ɞ~{xyiIR'k$4; $hY4J D|suIng=UW'#4&+qDO8YuH&UY.q|2ho,J,4҂ک]zTe{lڼOM5ZI'1G+a#5!aa@ʉ͔*ڢGhs'io K0Hr$>,ffQֲm[lE:>"KVF={jٕm (hql!!&$ -g8& Dỷ^/Z,iUJ|β-d@[I$ٿ̀Uո*bw'C7|B4<});B/R^`|2&V溳CI7Rr}Ew `]iiJ @_hF65'7leDőh|[)v9|a6'E̊X @hF I>l'(kYӝeO"=~͌h_VDK#-JZ $zf@lO&F[uΨ \|]T-V~ -$HOkidm'W'sY37%{HVЀT)R04+q`8AW'v_+owQ87+PUOKi 4k ybk:jO"1ƥh_FiEIwsgh@jh)+ T㪨UEKc rI`KЀTOkpʊSK-*|aJȜ7xLO}iz,iU.O|ƂmXmSuF>Er$SMQ\M&> -<}!jqj!!GuW'g!$”P'/\FϩeI+T=FZH3'H~6aF50t -J)H ®A]T*Cp&NlLnfUYT*V%6a50C00D?Փ+os9ؿAtjJ|LGq'l/-~zG7 0OhAW\m5-2V*Z<!Q=dF-V"`R!ZZͿ |;?E*80K2HGܲ,K̡x9Ulu,hOҰUEĵ.d쑭J (h5iI5˾:b-#0o#%E?+IFxl'Qw`4smH*3Ͽ9b#|9.Ī:Id .2}'lY; {oCEM+>#XJ=5k vi-:ӿl-ŗ^ao}^ty`$u-ž+fg+jZб>mHȢ+[wQ>j`"!jU.ZF'GeXM)p_0D[߉+vhh5ֳҵPZyhRUhs|_Wm(ًz%QOC)MMrЫcK3;>;|z2 vA' F;Ͽҳ*G@ΩV,|oakh9> nI4E##ɋD\[4s"%6 *Ap'6Mrg> ^L* uKg>9ڜOя>5,)^I^4sKj[EYӸJSՔ$2;A9+[:hZyt>#kIw.=5-3(zv||Wasrx8qYOIM$Uwъ -k)>%`tsg^. -{0y=k=+78\ɲE*'k_k>1+m;QOD= `7twKKj"T,)'t_S>)yvtp2 v*(lKj"Y+ldTmNwv*'q$me -znh)2ҵ8'VfE">|ڐI0&`@6,{IMd(X\_IO"`ƾa߉'D# `7) ^*R[US$qrأsm`?opW.I]D6rh*s'dJ\^LEgoĬQ솖bsB!΂& -=Sb#VS2H'?]/},6P. -w0iO6si"=[Դ-=ұ7'#_Gp[rHsē%^ lJR -$EFj-3>YM#D?Vx

`t ~9:X%I$i(%@A-#kY>?v|:H$'GG߉ eZ$@QӪ[_O٢+X(z uȅ333dC%H#{0C&iǽ& F,N>y=?})'~fso l?3tb]L$kI;:֙OfVTN|I"qn蓉D>g^mboz%HKnX9`yi[EY2WԔȨc~xLtrId?Βw;}lUנV3'kiJ4'g~/W.$.p}蓉DJ[A`Y/>Cz%QOP -C#-%4 l0VE>љxH$D#=>D += $AYH\4:襑SO|#܊⟞={G.\?}|ٿS+Kڸ'Kz%QOC)JJ6sµ,LT&)Ъ?n8dU%璤䓉D[EJb_yv.`ti- {7͂^z6eBC4G?㙙SHO>u|tr/}A`~ -s}tHOFBx7q!D5z[Z_ϊGG77?EI#^x:{i:<.! |2뭧 oc4Ό lf`̈'苪RJmݒ؀9Ćv~JժR/zҶ `!Y`se睱6Cד< 3 e+vPa>z^dPϦ5%JiB(.vnBn=4f]WyFd~Usd/0_OUOJu"aǰm$%[/MJ(1M*%H Z {X1Ԯ(e4}K1%.ghjR^6'Kj'!f+-ubY?GOb< -8TSsm֕$+F".P(. -Źڬ6:TQߵO"4TJ͕Wr'x(9$ IO= XN=? -+38B0 gS[=%;ˋ/qUb'D}$C*,=\C8/C1ԮԊ@;mx""5r tHoN ekk+k1r#@`>vt[#™ƨ'KfM d'S|%3YBWR/YCDk'(κh -@S8e1[ gY4UrgI(9+ʝ'%ItuKkK8ӤDtT0|vƐ8{bRI6eGX(Z9-A:E1/'|Ŵɲnw4e驒/tDyC=nzu ^<CO / QL#8K&<'A˰ßɋ$;)rOt:D姻e3}lߛDObh{@Rbxi"/žI)(*~]xAp=q S͸CfT>|{1~05$Ia6e?*/W5;glkJ,h,v(uP}J>0:x)[KUW:Rc}?)% - JZ$O|v؟ _ -P 3>o tC, U͂d7; V %gI${r5Tpi`ԓNߛDObZjwW,[\{S󥐏D|~H՗(/)K$ 0"'?nLv$Nv;U 5K$tpvx~Oe=)N#|=!%0#\ vF -sS0Hb<)V:o(Ic\&zb2|1$m$;āko`\}|0O%_߁RȧEr |'Puqn9dԜ;x@߇uZH?Jm K]T{I.;aCk(9 -ji4.;Nꌒi2:dm\xLd>v`n+̿>.ҟTQy$K!߼>^U+qGp)gQ9ݔw6' $惩ڝ ^f{w>Ki8` _~\j07Yf;0,u8l'u:kn 7)0k ;]cwՁEiUQS7b`ޒ*0{򹌽v.ԮOUK)6tۉ-XnF+6bTj&ٓC5S6{;/$_"U2lh/qsH}_  --vY`+Iѩ"[pi4agi.uR1Nɬ[x_zBRamOv KjbgHvPJQImHoT'iWB?ZF|2.u/S(rc*'}JJvfT"xL_?;Hɂ6eEk nU[_NdQaUJZkшvw1qR -5mYlQlDne6$@ڡO?wd[:(Ԛyo5bxpmZ >ū -VkkWX 2.$<y =VCyY_)*=)$OwJRozj?D?@h|8և77_!xK}rBv6!f'up-0mA J~̀|%G||RWqTmήtkC%n'OJɕXB"ÉMRd|Or i/@#5<֊@/wy |rayxU6E)|/Od^msN̸RvIٙ^pN}I-נ nvTSST>rOZq ,|2J}WB)mVJ`ٞ)ia K c=>r 6qvHBgzf;&_%\ai/^3# {a5U{-铚d; $څu&(ڻ(\'u'Q5ݪ7j}($TPR -)w.G#ʛT&){xf LZeG>ӁVͱBY*kR 3]+U73k[)g]'{0v,6~ ASyZɺAF̞{ cmcS°/f@g~R*ӖZ]I]|ׂO*q|rQd*I7ʞr3\mV""2)vY3Jqq Vs~k}b9EM -dKz9A|X|/㬶#/ÀR*b}LԦVӄd.-uךxge#V϶# &O -.KwfZiS痧2.&>)=bxIǫv|'QMMJ)vZRp_cVn-" aLJ pZe&9 -B/Gne^;͓SufuG%A}C<*xKVߜ('5froo? b,*^uZ vš\>k'_27ɼ<ņ$xt{]Y)V“>ʜ D 8Ҏ<'gy'G&zʃp}0c7ӳDo]BGr "$\x7533> -olMze[nw hyɞI>j[IJ)J"`>enX -EZU%RܨCRe]`&Q0,Oo2L~r ?L8vVS>'"+=r!cTVPv D)/n_) -YʙJ* Vلfتy&R'=KWnH'EUvHD7YIpB0/ZpmU-)BǙг[I_xQAvX Sٵ&($U%cں8$Ϲcشݾ`M% &Iv/ɦj*R2MUjkތ1yH3̐tUsJB˵WGWߗs~x < "<{`8LVѢ)QV)U<}BSTG{XØ~!7J~pOsW֗dy%#Qqdd=a딈('lHɟpL/8t y7>S{&JMa$ )38qf R$~,]r@#,O>LM%~[Mp ~a'N -,gGCO֗$Ħ223؍{UQ0!"z^"eT*'TмM9%Tkժ $e:;__r'8j)Iԫ.]k#8O -ϓpC`:Tjϓu4-ZCIOKÅV7~fyuJoyR]eJsDx2O-vGض^DDY? pUΞ*b6IYq oe Ӳ|x9 7}tp΅ƻDJҴ%-4BDe<7JZsOټጭҵ8q $DAiB<8DϜ9lJ.fO'AP `h);ă\A%vy^;ʀ'J RUa2yr ^njtH_@JÃ8؏'{$~rH\3NH?)4ij,2 Y7Os%Ӌ A1d]-k|p"hQ6ɣTaQYVeUjNo2&I <]HIx2dZ$"]E9H fN-OişbJ|NW~1ӷbS.J_rBP.Def>]0ICkަ1td'h4Xzl)<OR#dy xD}V^v[ZE?MP""arO:%[*/bIr΅~Js}},(:A1@7}| ؃3nhҩ"jeU -cA - 4"E(@cC㝣2H!:ovj'+j' *'nb`rZb$"24RrqpUwL%@`_FJYAZG̺>- Iy *Waq;zGh9 @^ ;[qPAC`5OjZU6EU3]i&IW -PJPpL>L:_HIWi͊ -5U -{2-nt IHR2{r,҉B܀1`u s% L^IJwM./?O¦x6yp8"SgQw%aTB6P!J.ԘsFbJ'\ :b҅E&VR]z94x I(3 <)1 )[*nE u$Eg`CTȠMz1+b;jAeF]/~\0B^j-ڃqK)Td9; KյIFCaH*J?!*@v<·=˄ǮjsFӍڬ Y8|vG=EO@b/FѵL~"a2?h.+ ؕt~ }A)4N=05@vI x2[[M<&^9ӳ^:$u두l$,) \ijāa&F=64Մ>?A_xc$L)vK2bs7u x́'SQ?qoLՅEqD;p -4OҋcHJ{2cr2l'5)Ry<8ϡ"dAqx-,On!LPP` ɦTO\RFr!~F 'cA￙c. ݆cʇ_{;:1kڸ9G(j2fV-9iL7j. ޓو[[E '-D ePv|&oo8>3}=y/<2!/#ړgme_ -./g MA~5xR/Ynne@=c$5!“?iHfUφ8Ucl3]\cZ$<%cat3jؙx2b^HHH"dPejHkX5!\;hGЅ(jO45qd=sQ"y$~zfp3kVyD7%s3/iȜLn -B~ri~?5c2 $HCDaAř$""WW$uwjfvvڭڣfv-P rprk췻!NBw߫OP <}- O[|'O#e#g`RJ13C_^SlFI?}I8nf`N$|@ e,ydMUn$9K1|N=@~{ 太7$ŻI_2FB"l3~n@♨z2|Rا:Dx|r])O03[<+$xa0.uN3zގZk`QS}m>@,5:,kQ0P~"@#!񰊿 ^)\3g%݋N0O|?Z}1I -DPÁ2$BGFťs>7gO` 伍r_`bc RJX䨙m^ CWۘZ=u[͂\ mRJݦֶ̗́{CG>0P KqQ>UD .ҐstӢSV6 &a!0ZtЄ~wQ>$bUI`5`SJ`qmN(`فX{VF)y}U|RWT"< b?<ɾRꑙ-O,_2"ƼZYk>sa8 o -r+9g[9mj6FO&@FZ{->9_b uR -'TYXSpmx5t1۪Od%N?`jb9nyƎDwOe$o>9lBރT1S G%įNL&6'$;ۘXMY L+`" |2;[2}r 3ye -/1 1JY+v"X꫟vC0d-1K$0(\.Uiiڗt ĒeBߎ(i&©Y) UjL6E+Ep%L \!@}co1q쳢VLѥϸy-e>La 9;\  :fYJYC=i[IqK=&\CkZn%a|Ju>~W-m(SJTӼ غdÄDl.탄<' ί".2rA Quj2&jBWb̌}2d.p! vGZb0#~6z`^[<3-;iP0Gne䝒_D*(ֻ)2Rh-܆Of ۳ådWዄ7<r7x9KrPTY'~a\ުPY\zllfLt'v"<:Rn|BrKbƊ%3˔[_Dr*#B}ĩR_!/ -]bfi"p~}SL<'(%Dp)"`G~) MĬ5lkz9o'hHpoW|>"WyxbjZꁣڍ&X?B{O¬V'u~` zb3Ta0AC.B@.9ȱjIdªH bAJ' -|;d!I쓻b0[K家т>Uʑjۀ͚Khw+VN-=ƨ_SgM 23Le0/!׽ѫx$#}k.b+9@BRJ.O-cv{e ߛI3㓐-—>UJ`J -Z\z服`/~κMTQ)=p HoJ b!Orw?tTŇC"b3L-E!r[FCWI_g4d`}]yyjIIddV&m?Ttwb`vTJ،96!=i,Ҟ;ƨqi T/8L\KJ`s:=ީ'z PG>9PF -tC9O皇WI=f~"Xu>:63;;n3>Њ<"*%,Z-.;гv`Vrʈ__:\?HO9S[sKf^p)UDxɡ -ꑙ&5Ԩ]U.D*K&NWl3|M7呜OTTO-Fx?֢hG bm f;M)a%5̮$y-5{.@&A)W8üܝj=P+?vu܉pHʷ)Sdœ t ԿE{RV \ܧw(xXEX5 ~OBHO鑚/دۧ;Й1sZiW*AY+,i)jʃ" -< ‹ng:w7}v:ӝo;l _');-T[L)AGuD5KO   wQ@L0Rj$vϜ$ 긣dV_𹒚- #l5VOJܗhr*-:*LW\VA_x#?(fouʊglkԖVRp0 [1Hv}#eQF5 òGRf!C1 HvY CMƜoG-4ƿR9үx'MwL?! -veGT -^txZ`vIr@ 1P^A]t3snZ9zO*O>q*eɍOB,0LfZmq'SVuǖ֡&Rj= =aٳәqG}'O>w`&mI6d`nāRid!`KaS^x{x/c瀵_]=ٲŨpqÙ_pN:XG`z·uIwEr?0OadmjV2D炝CnE:r\IMO"&udV01wJfrc"<Ò6ZDT ĔW1eqN8{մW~~'U'ږ-/xA*hxKrӓ6UGR;@!dFg0 CK-w@5%&ГlJբ>aՏD f7&'Hi NF-iWI77]>RYiyEEqYM -s_Iǘ'JO⾑[q#%O#V\/HRDa)dfhjoeN[CBy9zRM)`w>/ %I LwzÖ ⪟9p_x;ieeHuJni]tEJ Яxő&4'E {BvhZWyڌWM.ozil2rw#> ;YpQuϪ+>&ܿۗM[1gt,1湐Tjג"ela`F-xJI$-d2'1OuHd(0LNeEnrow"jS<$e:K ? (1hNpxI2i)̥]oU+Mdoa 񻸄16>)a?R%]E8)€TI=XVd) %EJVXp.idڌЛL&^ɇ9Fx 2)Fv_|d#΀xcrs̵sXd`6ҟ)fMÊWl!g۴{RۤQ (H=߶:(m/ 6)-DS9H`OJ SĤ -)AdH LXZwyøEKЛL'jjE/ &)6*sę|,$CJ`v1Rk݄'%$zRK='1L2)+wO"JSVs$'IO҆I65Gd 2cnx'udV/8<4_ &5RZCDOrJ8kcY)tFlEA hT9mr^9MO6MM{O -'?K6H2$li0gmN:Bk"%& -X8rKfãÒ2-wsh9Ȓ U6!KR<<^B>aBIk  >Ʀ%W*aKkջ)h7'k'G x>2Â{f*v@ReK쮨L@43Lzj Jyd/nj[C-=/$~$+1N>ۯ+u>?1Է: Z)g,'S(XJYO7!JI_s6R:L\,I9n?'CVOMN)iVK` d3ZA@)gY7QʷtۓIԢa仇>Pܟ&\f4+ѝtj>iyIJrcH>' vvƨb|#~z"!)7O(5SycPJjteO9C5n@)CɢH䓞j5-8 -oH\6_?৖ -AEdR r+TOnגRTY%rwͅJI_O,^cId(ʕɞNM[0r3 v;wŁJI⌎<*D --QO^g*J= \gyhTԕh$y(VC)C;V7wͅJIΰEg|䓥m$|2 eS$`LW)w~RC!c)eJO)[i_)I554<|2䧂!zIݭ3UY3`CR̒$igC(𔲙/oi}rkQ{~C'FOpj|OR4SsՆGk}ROSIVƝT?N+ʱ"Td/ojd畒<]}u(k _m@>YI@&CPnF:zL= nJ9 ؉~82Rc\ʏ5:fe _Nz߶bKK>ڐ}^ʻ=`LE) -ոͩ.;'sRrO^)s2t"CwUuŲ^cN譛g^p9H*XxhyILa55GO ڻZwE6УS(a Ԝ瓿jT 'Hrf= Pŕ&KwR1rعW)ê"ƽr%>'7h$4)*DjDEd@v,7L *%MLո} ,8'AT)ͅo|-fR)],E|2 u'|Hꁻd?F_P)Տ|lwɲw6UJ}Э&mK'i*>83.āe(0 ČWJFm3;ǝ#~}G\J)m-:Yt%8'O_ pLW1Aabx -%RqY(%m~ apink_)%II_)9N2?'Kl[* |2%i/ytTw)2R)eatV?ͻ$tPJ1֓Mm\Њt!܋f˚+ ^\өX2e -LyY&SJ27.H+*1; ܏7JIAѷ~3lGy'=O;kd $JĻ쓝 %f -K@) IcUR#O|\);i(d= pm\ ?5Sy[@_R铕:AbR -۩D֢vV)rmjhg%dKJ *ھ{ @3RTThyYi(/H wg}Ma餔9ZcF^槕R$]?~RM~d2}23RD{S+q.4, _k#e;l˚HX~?+'Tr}0}\`JIP%ftW3"$LD $jlN9~O"2{U7!TQ.AsS%ftŝ -% opV 􁧔RnǙ3T6(ja?!%QO\m&@*(m;Uaq(e |qC?VEuN?,Fc.>rs3LcaH*tԥa ^{2mέ L͕+/ɀՆLSftq_o'ϻ+J ekO>ד>^~oGy2wg |2H%dVy[kzhr)~_f;wՇ,:J -X\H)iN/wp'*>'0+O6d݂5sP"H$jIcR.fC3˱Lh`z֢݇TJV)AWed~/\qHRepU?}m F+`y C_ycc.#1r[2iN}5ՌeszِŃLTr8Fk?#d kn'@R[<04.o)|rS2FOvqs[[ .ᓣ器+5rRJ$ApRH(uВ_MYro_bK;z'G&:DGz`F)iNPm?!R\KJ0}8ߙQJ\d%sSҕ*I>92k`͔8p^|{\!y"?jR1JYgsy5TRS"繝zi<\H|rtBբw].;7E,'I-?^?d AYJ)3إ;Hm[O(#B)vn-(eHbi@t9o (e<^/ﰭ)xGE.߅]/Q U[>>֛ -9qD`dIyPJR%)?cY> ePЅh5n 2$򞬯*`j?(9_dXcyf=7jO@*d7HOv!kRd9n-ҷG^/Fxs:91@~6mwQ$03 =-)AJy%kY~ӺOŘ^6$* N lsUU8p /#_ 'Q~PJK'9v:>үuNj#<2rǷH#-Y~65 ϺF,!?<A$~R۹t#̆\.hǔOӌ -Z֛HRzbۙa[]{Tх$5myS:~ I)3 jRg<$'IK4@Cxg9մ9 |=Y9~?$[PbXh IuŨkZbJKj-5S$OyaadƦPT+j_ȏߋ^4w*q^<}yy]=ܾ/pDΈḙRRkA){)&3rϛN?O'$HFW#ZRfG?-XĒps(eƯ&[ZjBe{#~FF/aX?d;3J igQ~h$8ZR܆z Nn{RC3?>i'~A׆~-o Hy}ٜt6Ccwg=730nyOEd).{7?*:20>쟕'JFZ[SH3I+q~w؛{4r@"= zb-<r{ojڪ(,E)P nդřIJFJkH(-Yj!h"~0qDT|ӞkJ=U -lÆHFO=Ac7RNk%7F֕FWE%Ώy!EL)Cwa)K>˓&e= Q 2@(!$xPCgۏ)v؄4& ~!vپtR3XY0vэQ'gv4 Ő<%{kJ|H; Α{F03ΈԾEiM -hT`HN90/,9cka َqvЅEqH#uۀ,X;: R>R嬢p?|,c ד02󖢨T ?/: IF{rgkazX,E^-)Ja"ŜHF}):^W{)zZ\i|wmL -ifɍIp q!,iT*X6},I[G"c59G6@BOvV E#)qeȿb;$[/p'g"]8WvJ:HJ;5)z֧b3C"Ͽb[S -ӭ?2g+XiT\$$lR_0(LʠRu_Nt)Hȓw8Qw0uL ӾEu=x43ȋ8Dq #9䘱3˵_ʷl ,qe}>l)+K  -JEFYxGðZڼp6//g}C՘(Y0vf8'ILp<B*ZQYK×{A "HILeOw8?^:'Ӿ|EuPG;'Iq;6'QdvTɝ9~dmֿ,,&.I#:YvRRiJQ@1)1ɹYIJ GQ}SC6`ȋ8//ۚ2Gɍ # l0BUq2,qܐ?u&<N";^RHyDR&MTs"巆aΚ}({u12< ߟh1Oz98L - ՘ X>egQQLeNVH Po$dzHa#f#YdEB- ߛCC*ƈi/ ,S;3KuiQFqo(u/ '%h԰՗ۛ()F+eȿb;3 9tl`ܚo*KERXv~KRB^((Cօ]ey R1^lfj_RШ 0/,9ckȵ^|~@,S,DXF@A ʒ7,MH2©VL59^{-iiǮbEB%۞fhHb{r؞DVx,1e79L,]g%_' bO2O"<,}u}<8_"ߗiE޿VO:ug몴S2ރ(+/+=m(x&P=K쳔| TqtDܹZ)Uw3ĤLIkAQbd-!*a^\0ٯ64Πg眝G[1vplK*ef]#!گ -F|Oo6riwȐhsv{ɓN-߳e9,1 Kp5$Lh@֤l ?ehZ)_$񗃺Z'ъ3T`0醴*,޿6툢,1rj]dQnpW?R g=r`0E$+HĞ0og NߵACFؙc}4sȏ;{l[D] -7DH;~аLf -Sf 6D~^#eAqnuMx!ruA<(`W8[eWha dڂƤ=uN2,.ۙ@JH3s@_n#HŐ8*kZd:B0("(.3Fp@*))߃eނe5I K0A)^)fgk|1LC0R3Kl[A9,1^_e4YZi^I7QL)'e+c4"Lwƥe*YT$(77׏ "%z{ 끚KPAI%!&H9j R*wzR*E};S΢Hy97D/oN8Xx˒MOlkx76)O ָ/wҦr.8=)ʅ))=_ j(DD-I N+5qT{{xRֈ@M~e/҃*)OJX_raJ,ϼA>0e@9|ϖ%# `paXa6`N=x?3z6|IHiH -}!ORԤ{6XrK H~P.A^ -㨨%Dx`U@4nrEʙrh߳஻ Re0; F -sr KU+m)7{biƬw"X,wrI 3 ak')jB= D;`)T (?et@T +Hhe/ 斗5~,(YΡoOrkW8:<}g7GQ_ކuC4,AtI0RH)úlgŅ\DWXAIIQL%A8>2IXbe)^0"3iC4f -<&)j#H9`za>(jrٰ,*QjL6t4.~XDʷYѓf(*RJ6N(Er>pCC_k٪AX⼙l0lzUh"0}\@vZhz@|?M&oyH9`V_?2WȆig HiQt ]5^#Ð$=ا 3~ͧ)RCx;< >rw ^K}~F;S-ϰĆFљ`oLJ²)j){^(̐ RRԤ{̴ `>aVWUكqT,[Uo 8/(9)ZykUjzOI֢sJFQn "ay#_? "t94_s]0R4R"BnqD%H=NJ%;dʩ|@yUМGr~#R23D_G\*ᠨD9"j 3i>ib8 qRͲ&kaҡ8m3ug=r`vu&r_}X<o8 pΠHY [-SE][5 -Rv! 6&uW,3t9Fw*ʃ{ٰuK8C0p%<'[i>vw& -s.}93e(;=aÇ.4s@_5 ``V -Y\e0I:T'%ybH͌HٽTۄ<BHD{(jJTPR2ϓ†eF7TKp8I9?ɌO3LL9Г9zi#8οvwIxΜːV6j+5UWizjWEj6UwەY !!`\CUO"c}Z. !m`n1$ߙ, ig`~W3g,j.,Xh#&HE׽^,eD8٤>fugy5sR宺_y(iMF2lnL^UKa+;*[.2cڙ%j>TyYI SKcJg)exJ_7HJ +sZG~58cL2a~ɁeRZXa PҖՄ _"!1aRDgT.c 5!`f#Mt'$>0r`9-E* 9 M;6НH')ZL>oVs* -MȺ6v1zDR>Փ.1|(aKvX.(Xfj"C>1L@d"'Fփ(W+ -J|=jR ? ~cU>H[09Dڄ°fX·82ӫ蒋1vJw[I{-NKnp6D`6KNsKv9g{,Ťu -N8W5@/32WP-;E/jRF- ,RFŃ/Zmҙ _lox `cGvȹ!#M4.cg)p31R'c&SA_ Z>&)Oü<<^HJǓ/3+a^<@߲Q |MwuR߹@7`X˅n(i0")K=S'Zs떚po!)1LWtxC 0V-T-vseGPq)]Z@z&3_x3Rj3yYO‰߽H7`+Ii -Mn-MHx .b[k`&]Oz5^B뿓1̳Tu1̻Ik*i!%)/a3Sw@w!f6rݗy.(JlI-e1$O7LpEsߣ?8I^3 g`A)ڭ•T#ud3"0LKWvӓcL50m fHwq`H)[?f l&}x?gr97 Ai3giۏSwOAWUFu2dmb$xj55LS 3R XoT .1v9&H#l1 {*mD:Wn[3Pk.#eI^{?/w%kI[[f(@r8\ td`%}sO:*+++ŹI~ڞJ <<\pZas``]j/OBlQ+jgc~mc?Go;cֿI5F2Ɨ+VRDm=7M -^jRV͉ao6pj#MNb(ޟ ,sT;T\K=('HJ!!8JngDoi rǘ_u* > r;U:;Pg^\Kô'V>ܨ~{{-Lu0lm JDr!pOw -{DJУj1 o - 娟C_+gO'Z%;0$l$SㄘQJ6)N5@A˹ߐwi^`r9._d 24-g"/=pn6 c:) {7lC+?WYz7@W*CUFUu <@#K@>#sDYR}t*xB;0fzH9@[o'#FM|*7ѩ z.njWMJX:|cKjg̓DJ8;|h9JoSBb)X^K=0j{6p;{mo%Ga kki!5q! ;>KI)s$#iM^# ?Z1̳ſt_X,W"b&)T=ej뱺0j ̀Nj'%R[ŏKJ}aVHZ㗙vJ;=aVe)2}.Ul -΅s#%a-ƲJdeJY>UJYIɁIiVaӽK 0;oU0m)Uc6VcΟ8W4վZ`(LiKmɴIai0iPsؒm|M(%@6`1i2+r8@6͸ϻ+ɒ6vw֫]iw0aDJ*W3e09StM+}E[%djj+bilLY=<&>!Ϭ*cF%>X7ɃaXrj]YORu6fat -`鰼!o@U-@ʁLFV{[De%:BgO<><9P>=aҦ)"0lZw6{`U -+ԗ֒oxػ*b R*ŰJId>a0 Q00`RYç=0l'jY"o߃*j2Mhm`)Z !)؍d~q3$ɟ#ؘqtO6  þ3$Aw]`q"6AQ7Soq%jo~<˚e%ɇHiTr|n=@K(aX4ݻ6iI Y'vaؔ*Fp.c+mnG֊j%˂zWߛi"o338n='/;@JI̜0R e-?iI#0,M 4GheEfh$9Z|%|Z#zihǛ= z/ptDLq9iY!Z9s-誴ZZ!/ů Ib(\fOq=m~0 ıJB7E3YW4-V޺,ݿ z?pd=jo^O&wݭjQ]ptysYГ< ΢EٓqR{d OQ zY2 e>8wI2m$G#i lZ| -bFoUpvhRJ_ß<ǕmNyjȸ+M5*+rgC<=bb?&&ޔ,/ iķQ]3d̓kxa0J1+Ŋ:A]՟&jK@]+ |mm>9s3^,_(yYHĨt5}ؐD -+e]iEOyXfA0Ko0,}jݳnҐ2 9ُ ace(Սbt#h)EZ|tQ-_zh|w۰zsrIjZ|]GcW(;Gq(>f13ƄdΓpa5IJ$*/ &ia'mI aXGK1h^b,]~ּYx8(6T.f\ m:X-=FPbZ4>ѓI({ndaبIb0$- Ű za 4ԍNc̈ۦS˗Kõ? }ЮsQ14z4]rO%pZ ĸlPbИ)7'$Vkf=94tb=x-/#n ȟ#YaNMJ4n#݊q\Kj 7C{9'n6\:Kwe'cυu1&)#z=i/ZN)t?|0 Linw )Z^u 8`ؔ*FG8 ( ]ֈqu%ڌgi Zx1\jxC{|7IDb^ϹC#JI J|蘄FA\/%L`cRIEZ8P>;Ma.;nI g"&1,mb:4:.11hLF(9juW. Zط*pd};]9_t|\<\:`9]&a46q=ԞV{ɡ":HJ Ib0\&p.e} ıs H4R74mk_׹|_|9w~\Y.Уբlp$iFaS\%2mg;wתk8wkQaDQgT ->BxFgydI=i{?~>IEؠud~iZ#L˄>IR@y-= rVrzl/fa1+xr[/|/PHݗEo8x45OD??чD(<<|4&cEGNk(j|٦8)$(ta}i\TŨQb"auTIFA) x;T xl_q_[yN꾄bo[N(`^a!q8ч-nwkQ?ߑFF򤮘LKI<$,/rV|Ơ(j86gA'!ٮ0W% ܞhKŕxr|f\AO*s]oDZrPVԸ莡KɍyraO2L wkI%~bUYg͟c~?}5$ ?+e?@%*Eaq41sCC*-\V|kW? fwJ|mFZv(`(Gt#p("1&0 `R=cSb{¡(jxȴ|,F[֯uUٴ] p:?en\ʄ!W _soR&uWb-qY2"Flo.;XPBԋG &W:GE'%pU8_euјF8,c$oô݀hgbqrts[/)p9;Ǥ*V+ -#*T4{U1^\F@Ejݨ?xU<6|Ѩ'gЏ8+=JulY+IP]* Y4Ba]l FbُC U/}G9RK}Yې1btrЈ9]MPDT1Ӫ>rziE-@Rt:%ȐS}l2GňhdЍU=ǔe[B,t5{uo}w.J=WŁ&a DbD+@c܍oIb'YI -Orx_GȓR, %.4>"Jc,mZ -Ew~=|ts||f|~7>򵞖ګˢ[lFF0Уq0w87OjEL <^*)a2FuⰊ_ua(Ƹ3-* r1?%b7Lb2&;ʑ \Prտ~Xk)/z0$ Fc!DD^6w9t$(RyRK`I6Or}"OR+T`E`zЪ@X\7XG##uEqf(%o O$|Q1^KsFi[끐Wցح~xA4)"O03Ƀ<X<^8,>bU=R86gy>Lj 暧_؅dRܘnۀhteOn}sOY[_{uI)?SWsfHa̭rѭޣ4%&H%gIH`I*ODIqZIS.EQΟc>&ŃfЪ,Ŕs)gShZcV[0ä.!W -^iFrLj.ub0 -2FC1=tGHȓ'SSg 33GL0>v9RZ)9H̳̚zhPhԔXS[`/gSk4N:S1T,uTKݗEOD̍{~0!vƚgE'!3 3|3 }R|?]S…f@>)2HBn.Y%"V"yЍX.}3\%\5T#x+{ B:p0%n8=XO@ąHh^ȓXbR}qxVaS-9VDZgä:.U tAt1b7#aܤݬ+tP{r.5{u -eRG'ychcg\R8E%C'- +&Lr{1Sb$E8oS\>N)BBU~PJ4h[WuhҤp~iRPB;qqbqǹlv9>vHy$&  y?z~>me)+qe~j$RZFJѤSkL=x@)v*bhbbLCI@W+6"~رN6;\ -\8ꁋ3!OSk?'険QI}VBa&5{R<)mM{F-E)[JO - D!H _E*߮h$l4.4/Y*ɥpEKXQX%QLȎ'/~7"m(. -V4 -^~q|zh=ԊtT|bzR'7RJk}>z&[`߾]ѽZ9aExS*)&|/?SQi]=µI45 O$E@cfg`{G=7fJf.VFai%@B͔O(1f]7NŮňnİ5=0)}OJl3 MIaf+1){rf{y nV B1/ױ㿛=ٿP1X"G#]B} 1<VLdP\YM VM̓k)hߪ6ʓa#pn2L -oiГL&chbP\bf»ӈ?b|eoϴTDZFaP0h'ꑹ$Iؓ)bİM_s۶mRxR"0 Up0PYՉ&C© R~wh\JJfa&YV,ͣ14%'ErX9qx[英 {glHu:{64|mN>*F>hhYa*JIكIvGRiaC(7oieR=~̭F32icInTIFYӉڭTGvĎvkцweÑΏC&aN;0(n>ab~ wht24.w#T -=j&Zy 7*ѓ.Vޣf3.W$dɏR~,׈"*&=?BD?o gދtԅ]gB?σ YE[ҟdJK hD7bJjF2L.)~bL6e=pl Ё'C{򓥚T&Yn(Z65?Mz.~wΞ=1:=BsK Ђ!(.\Tbf(F ,zR$ I)ѓh'1\$yzTݎ'j_"j"pt! ~;U(qb2@/Qh%Q=TIYI=3F"a>`$Rf*ɛU[F;sf]z봽n*x[c=d8}؀0'=]Qa:S' -!%Ub#$FOI P0E)yٚ0O -wՀǕ/}e_t8C7#F Vj휜@O$`$ݪ6ГEC^ݩ5-SrcRZ7dSI4`x]}Hi#oV)#1II1^mXX[vRF>C8vXp3AmeO;j vx%0l}Oj -uI >hߪ6'1%rbQuwkV("͑Lpɦ 09nt:),G:ݶqɾ+^;RCðuٺQ~~IZ|_ 1j[H!F^P6.u֬lRKֵ>12m{Ntuϱ%qr#ݝ=T'%X-Ap|{>"nyIꪢP(IQ&u:K`5ׄy)goX#!, n4O3FJaF؀1*|L`lq7v !GolN/[Xƈ[v#c}-) -eYJ'P2)v~MMsc5 sI,b>De?(lH\v8oq42֡.pW$VJޚ˼[K].pˉN.-Jp:.}Q(T}ZT -%쓂Ї5c*%R FF3I"LjC悈@2%%zZS-Y$+㕜az[Eǔ6v'"ީ-CBY{J'5*P2IYϚ[jRw7.]$g+#Y?1 LҀۦ_ݹd+F.;EVI&fE.c}|*`JR$࿌ty&"ɘ:)GPֆRI_a}RC{A\#PhRۜBg -_33̭f"&l_H&MwBhJo{^+HOPؕ>N\Q薶cu}? PP6'Eut Q*UBYP("lă|R1n41')wobC*UmȎ-CrS -)8[\ب+&|l  M!hO'qK]jH*P(I:g:FFTj~mpHR%z掯}h/̚쓒;du|R;kgz+eikwL,p/s d2pcn6klx=J-Lcc3MP@t2$yR({d=!5*B^UUQYRR1mٽ~G^<{̟|mgY,^AffQ5*yS(_FEtn%(&Lv!l2EgUq`j(\Iۢkbٞ.d- l:hr'"ީe%A'Eu $qHryEeٖe堔ۿ\g=z虧BˉglhKӖco*` /F=nW]9szv) *z 7A#L2۠Q_sL&+Kqo[_ّўNH:R/zt>vt%B5Bi,|PGRV(UUe[*|`cowO - r ADPoK8 I(φRedЭ̤yR rŗ^.F&k6|nq#R&ȨF~Q*|LA XX8o]Uet-S~|pkC2lsMǥ/P -(:F4BU] ƀF* ޯ?xgק;p} -8ǃ@B=d9a®36&w֬>Iٸd5Ks̭fuMm!X4>!pprl"&C6l|!:>?[tKNuOT6:랈xh$&b7)BG٣F#Q]UrR Vco{/'^=fwIm( b!S[ωWsn]*JhOf*Rr7oi/p3!!܀B -$$.VH+jsݴ$!^ ,4cO$>Ӻ)Hۍ$mzb+ϙuv$c;Gq{k_w9{*ֹsm6.e۲F6{1N&CxLjƬ1R+Dp GD _fS0%A,O ᓐɎ';N8ucǎ=zȑc>j~ ׿~zgu/5HHbih{hRY#caÝJ@&=頺(;\e(eil3DLUC#7|YgS>Ҋҵ>m![5UUu 녈 -,d"0 0&y9`-Ǽ!ˆ&Rp{GYNԯ3|䓟z)|{?zo]u:Oކ.!샓q1-Fuo|:q>ol3t 7\Ds$%+]UX%*̾zs'8NFWR=np\ZJ -PsmA?!3Ҙ~3C!۵$$ŸG'vr{_k (%& {v9 H0r?aL1ƬQ"65OmE8!En?0H&wK赑Fw?{FP?G"'3thq4RH@%< %mZp<aUPQVַ}1'fsLq\Qq0sܻ 44R\ر4Yu8d쳩Р\aFwSl*]#$ggq4< ?7 -uwcݽ\wқxZ|J^:/A0I8+0IfPC H#}1̵k\,#}9F*&TI[iwQ^?6@W[ɤ"U8_jhCoE$.ؓ=ǾʇAͦBŨWBaRPՉzxp>Z<Ơ"ki2YKQ"|Jٶ|9\2|Uτ›Wᒱ<2i߹`oh>FR=Qīy9 YW -pp*`z<&9Lj-}dr0\C d&)d"lc݄k^^sJ.YX#J l%DXיcu}mf9$~VTU52i\+FP;eF^9raKXe[hbO7.KG1ʂfzzDp4cA3 -M Zx2%d?3<]vK r>ak3Xrf%êG?$%r!!/J VEN\6c+PQHչ?H^@-yL%`2/i "ZDWJfS\r 96ܑ3Í`!0(qت2nRCRc%?ﻥnʹfضT^g3i~#l"RJnS'Tpa$ ,cۈb.WuxRi5!~5/M- -(,⠨8 dk{Wp^Uk}eiH6H,G:g"Ckg[9n9<\Ӆ^B,M$$Y}Э&l'DmP-XgR6ǰǪ0IC#_k?rvw؋;ZGmFRs1USG]XΦP1.gu%JCsh!nIr1vI biẆ=1z%ç{;WMdwE{O&!ﺤn & Q{ ᐤT 'E[o؍aV+UZR"" i6 NHJJp`sN>b'Ƙd}0vubh;{#UbgVV$ò?닳[9gK2ެRIҰG}d%^awXgڌT.yNG!b',*!㉓AR ,eY_dl5:*Yq&:c4/][J5[[H6Bp%^2c,I]uIM_B q񋠎6R~p' 8M/:xEnߝ<H2\RW2aFanfj lC|bR^4]^-hi^} ^W0$ |M#ߘ -s' w a/f8 -?|"tABeQF#m4^D8oMӋ 8A #1 ab/3.,rj}>Q6Y6qx fgan`˽2%w`лc}q<#I[[HښTqhN%y-'(v s0)EXVP<o9H"3`.'yoS)-lkT5+QL;(޸OڲFX6j&Xk,k`K IX3Q'd,_H M:ܽ=y|M÷'˪hH -"Щ$KyǒP=ԖeSլ{j{R2L|}qRO~V -XF6UMNJ$Iӭ-‹Yθo=^?=$^z›OnHAO',yr{Ԑw#2lE3R2dw'YC6JcL VNLD[ޠbQI.kL c$IqSn0B_ѓKK(Ey@vsLxf$uH/k)zw|f6sZ𲚎7)ɳg}u>QL#tu^F% wV[y;턩!<dsW !#1kA V( pT2Źp'] F͙., jMjng;- -/`n3vYZTu+jgwya6WSbF fHJ Z|_2 FE8`nT-e1> -S%̻Յd͊3*eϖeSclj'-U]VcEscqpZx<<%]9y1CM'$ez0OOmmà?L#cwπ*!Z2.*aUұFVf,y W\nVbcin{Eظ+[PW;%{ʥ*2tM491bB$v Ꭾ+5d!"0EXh(kfCrZD8#K`F&t%6E}6d:R2;UƴR}[U닳em'/{)+SI6aWgIhR fqy7'-]_$;aݴ ;2C<-rA ,RѐlDZOM# G3[fk lj{E- '<0 <7] ,?fziWSbCP_H$ rJC^^{9uw)IR8NF8֬DJT3ļZ(jT;5ڲWXVIv  c.IZ- -H1WI+=7 +"dGoO8($y4`0c?Y^&jdђFgeT6jV܅e2l Nfbk ɭ`P%kg]>/qWIضh6) t MRR^'#$u``>lGpf1)h2 |r3*2#C([N^-`˽-p@9b$L)KZ^BsWz着ՔؕS EXM <{𭳷xc0 +0"ꌭSIںsgtЊ Ll,5liA$oZ&<ZIYxIV@tI=A||"8򝺥T !#1DgU*` ېZLF*pOK J,|ǚ_[HnfS;ypxWSkV%qVGs%ɾV/Hy[Dl'AO;i=:ACb0al#KUrkmasCjϖ8/r%78_U7ڶ3hՋJ[cje怱f7؎RloY{osƬc_(@8o}m \;3 0h]34_ +뻉9'+||Ig5ȓr6I`Ħd* ճ뽳_wO^w@`0 w?[H$H#`J&R2%xYVB^X%,z -&\eb׀&`ͿX_Ƙo#b\CގJN'T 8Rҹjq0RnE\.$۳ZV.x%גߠ+ڶ젫To.[~Tg I+|kKpBL^(wѮ:A%TϏ{zM.L´Ur4d f`B)1pH 0e [0͵B*^DWfkIzt&]od^ʬQoL0YA h -X%_z7HJC }H{_|raf8! f0#~-5ogj˰2X9˹R6*%%d+z>ԼTtR Q53 9ҜAL[J_wݧr UvUUv鯺g$O;`0gv.,=Z Zv`Y2t$"–p&[DWf ϟR -.-mSc9sNRD nx[<@Fx^@=֒.k0RN'7?%nҋhzG0 fC)mpH6euA'-LZ>R0[H9ğ-$7z*vlsW1[(Z wZPa/y@OGoH8#OʷF#dCb˧.`0 YӼZ*wgCHm.߲e%lObz}ް^HçL|yMJMR*y3I0Rs۔#~b~!S+]Wj"#1-Hk A j6VIGZjQe!ؒlf7uaK`Y/$KJ&|K.%#X_`2d}0W}i}`&]W8rard?{~} `0:ҬXNGVf@S!!m[CJ& -n \r^¾SxdVZh^ _}H<)*$89C?## 'gwϛx.d$\/⇔۲k$$2{}9@˹R6M%SKaK fd5ՐUԥՈp5_N -#mq<  p`-$[7vL0`07XgAga U'HQA*JGG$RǼo@tǁ>y%F?$i;/Q!}B}fH [(MR=jAy}u.)T,,i-Us9_uፐێQ~FUݖs:@mudW#F^Ģ\ l+W`Qg I y (!ܔeZ 2 _(OA}G~mqܧC?z8#sdϫ|Ey~2! 3@R_,#0XWfr:Pm}[⸞O"3bCHKnƵ鼃-< <[tx~O!ECb0P4Eq:\FLAvu. Bey>kx={ɧ^ lc}yGMz\H PJ,=RƸtfPnidb0Tm쀴6({P.:HqNzl8lyh8@l۠#t§ |u!^1B6,7 6*t`%TUsqqsZ#;*9P$IzNp㡏N(L(ޡ gGoEFb0̇3PYf>[e"fQp}d"t`>q=#?Fnp㡟'py`]vOX koVqI( &&u ^K0@l b^۹uhc;׶4J+lC$Mz5Z'- Ǒ EQW56T@R]v*9lDʏd~VpK%U|nRlrg^2uL#)'(BNrv04gƱl %JL[[,)+n,E;B(/pc긿cl}mlss,1j|Iu)^75iN2$EQu~qΣTr۳`rtKg)1~ab%*.j`>Rkթ0Xr lWb=9Bt}l㞎 ȐBE8FÕD&&V.׵S;#>4_ EQԅ9|$= @聭 -"rͼs1dE3bD v*n5!̳?:5 ݻ$v{i,=F2’+d6N >nzw+v=WqhJo꣎i[H(Ҏ7H wI ASj.S1N7`M7TsbJ_ƕ6# -!Ü.Vimt«~P"Sg]C(]*yg?'Ckr晷,eB%e_ԩii@UʴY &X.n,/l0PP0Ca+.;zgu-,:b߄({gE؞((Tv&tMߑ̓~d;8A#D:GRUbpn5ەv긜RapI0&|hs}G-*"zƝ= JqnL^An5ԏ((TҷgW.&\[,/߄btb1>y@g-65ki8<DT =%; b h\1떹R+bkYJO)=*:REQEQ,Ym=z ms&< /.\2”۱>mJʱ:򆣇;:\ns3>4nһVL%cS3 -EQEQuFLXBʊ0&;|UnL2|TOH*1RbiQEQEQ0GOvcu)((9C7y(JREQEQE` endstream endobj 11 0 obj [/ICCBased 19 0 R] endobj 18 0 obj <>/Filter/FlateDecode/Height 947/Intent/RelativeColorimetric/Length 20505/Name/X/Subtype/Image/Type/XObject/Width 880>>stream -H pT/!&_aBA+ʄbJZDhjhb"SlSF[qvBP -P)-7Bd I&;=&_{{|!;Lsw9W=!ግPڇ#=^v^fV$Ax 6G<=\ k˰n3,- gGX+)G`{[-6t 8W{17# 8nz GJyH&AwB tTV \D06ns'Ab G.⬊FI8 dw AZ-hV7%\( "~,z2YlKA bYqM V&u8minO1"`y;A^[7`:58vԆw^Z"VPQ $A.c'nnJm4RD,8>F75RI8; pћ"AрMV7Q#(Ag^PD ,=A$ۂXtSADXtSZM#%A} '&3>A$ u]NgO Z,;ȘuS+\$Aǥ|Z⚠nƩnc; 6,n>DACƔaw 0CKDSDr * FVFw w? GM!Unٕ#A݊a2, CŮ~ :vW74g58hr׹Z܉&j,v&pu$A\ t_7%܆4R8g ?#n#N{1L9# KM?)Txԯm4Q eS/1uC&n>v3>'$x i} RBsNOuSnk GH1>pRfDݦJuS­LOEne9< -]J{X/-0GOKK;ֺW̷Baפ =R2XG8ciLaԾG\},Rԗ)2NZʄs$\Ed\1f9t]RE?%H7%$\QO>KUsj Á]Sb}X & q kR"D-JS2sO}Uj9(%INtSsR"=Hla|9.X@S0 ~}}?X;8bcG[ʇ +|f|k|njMV Ϙ0"# F‘~$K֦F`__C KsM]f[:pfMeVtmdkP!(2K g\^t4饝T7HAھHL%}eu]EȨ;\߸`@7s%\X:]{x~'a,Z=lcx⼑\†n Nd{pnqY8ҕ2rr(Km{i]ذ xx;?GuGƼ#Mo' _qScm5k?bަTpt57mbtSLg ("dw2{J0ۅ}.6DŽ'<20äFFD@pa=x]aO) 've_ pPY+EЦI7H8cdo x".@״lᛇUrB7mDy=D yrEcU'tf<'_21uMq!~pz7Gqc:{loP%ZH:kծJp_ky6\ME}ߍrAp3j Nk˃iqV(8dݔp$\Bk9~{6& %dngZ8A(S7WSdZg AZ=$7`&&X护M5*~Z${S:ƎVBLJ`Y+TH$\gR|%1닶43hXz4J w @(_R `_ g,]ƜrlY.8.lg;jȾX8w޸`oO:ݔpZDE-#*5D^A;LI56(VX 2"sAVãTl1qVx`L2% 4MTદpuwVUn2"`Cpٯ+ sAGETdTQXRP#m5)hՖg}Ԉ $!)f,Q*>ZEB ַG0ujEq@{usIg0ufs_7ZY_z_W9.Or?KSy/?X?'[Ư -q'+l魌~_$ۘ\zFܠ=F_.LkQG+Y I1ӠMå;o]syk.Z5׽FĖ .?Ǐ}Q$^k0p2E~L4Z ^b7k'7-Xx]#]Z\)߽+k"IJj~ނU7:>"p3{+P֬&Y}4cI,ҝ~eݣV_y{&!DqK;]F>pqO׳UQ+fRn r[mEeZv7a񺷛p^k͖wpR{YP^:y-[W!=AvZk^w7nquEٍ+y*+E/lͼn=XN|qڏۏ}kO*lXZu^{Nj7hk k!9n>JTO2A: 8A ߆xm8 Wgqys޾;[Y5kmx] ~G7K~ t"\ZjU\^y[sΜ ~lm}r~̜~=oT*ZKc2 J -에T{aku)\`f+Qg>q,^yײjv}5}_CC9?׍5py¸<{{n9ZFp699%D@7˚qDuX7MA -0PGDrz-oD]][,Sghۃ,o)ZOXSK[j_Md,o) =doٽ]w:I߭Vym: NQ)#5Pya:1Mda -0D3ܩIF) rv7u2Vysr5NZ_kWG,7.W1n*I'cT/#^ "DA7Yy#H^GN(JPysRn@ͽZWm$Bd8bnPy!X,o*%`b ͵vaDm9*k`U74\U䷌*G`R 7{~oỲIFAJYAj'c*ay#Pvs$^e#'$;#%#7Btu /]PY(n6 }j2%5JoN3o*(SQZ@{`%(n~̼*8_$75vQ89m}3e>F7 ȼ2v3NzeVoNƔ7?NFbW6]l' ׍H)Jt!N7i_o}2M\FtIXV:}w7Q{ER*2(D8d,#YV0 'n u vqﰼ8&2M7ew"ER"IuSQ{9'Jt շ틉@;7•#%,o,Hm7as^N~FNzp┷H~2(k'ÔH5 N;Ɉ&ˊD,o,0[$'o$-#4i'c`ck9-#4O N27BVֹ0qs!NokHv2Q{p.$Zb*7'?`y#`y -Nyʼ0 Nz$ 7k'{7uv!D P´vYsv.Jy3wh9#5P%(-+y#d^oɡ7B"Cd\¼2k'qIYiT!J}q#d!7'a/:$nVަ4A7'FٯX2>c:$Ok -a "TZKX3@-DL~sCBؚJRa#jdSaSB=Pj~>?7}uݼH:ӕ8Ps%!kQ}լ'0@5s7y1zޚ: ym184hR3 ̷݃]0@5O u |d TsZB tS%dN=ycՌyh :z˨oy:I9jJym TFM~V /ɡ@N#bnױMJo St>Ղ9G|hFqK>IEou_yvX' ]0@7[Trߦc44 Is(C= T79-fo1rq},*Z Я ଔIۤ t|b- A|i{o畤79ϑ(rq@3;z(e7M>)Y'QynAy: kN`rb$z - 6 ؿɨEqWMz -AYtQNÎz:wEny gI/#Mɯ7Hq|qAJn$ʅ "o47e !en&3_$Cr[Zd3 RQVtJş y8no ~IئɋݧA"<UnƘ8MsљGm gSD;[߳8s GUd'.z$H>#+?G&/j|_KbyBg*y3]p8r T6mܭϝ8st9 skw?"cb4G1k[[[i-rgClPN^P}ŝ A-)Kt[yS2Dl;/#7\QW{!cA2fSߠ*'<A)QB^AOʓj{_l*9u3Z0694zKɈ=kԦkgX5œC/Duj+du#j{\mT W5,\BBfTuնr5Uˮ&,ZcjnEqD5򑩕\F>>qPS,*@n[1⠆+Uyk8*;傥's$d#RS.[T 9Oᅡ 56܈iOEqin5n:9 :b .lq6rX*! 75F0h3X*!/eu89~2*#6}ن~agoqԼ 8LޛUF;2ʼX>Y0R\£*C# ~}1@)kȄARl)U8t8ڏ(oEi`9sH ay|AS#3'yuwѼRR8^5xo.}?Y9GH }C/>?f69^ȣ5HWPNZ,2χP7 Uk]R'*_U|>{r Ns7RUVT J1Em(-'P6Κ#7%6FO%ɏrsλre֐e}^ݿŵQ,rm[%Hogqn 9U$Mw X~LI&M{.@;*BR"J _-e Ax!mX렯{.z?Xo˛t>g\q WIge9frBnI  Drpx+p77A,w41ތ Kc-6{3 dr+nY1@bUJ?ފYf9M2Vj_}*oR[vz[ `9N}n2}ox,7Ie;K{nR=θJBd^`rΫ"8<v{u.0me`BkX!8}x H3Cn  wzop e#8K!7*K%\X~@[AH}xe 8scs nyv6Pu fJ}WZ;I R@?G?| WJPѡ?z`A,oEoEps8zWhPyEFTj#wŕ!|o){n9@?O%ߙiͷr -pNUo1cpMCp?!;24 J9o[o 8)ur> 8ЩʏG7$8\)An^F=3u|lRbz=,p%s[ p}h{3t~7P$翛h{Ki1rM,oMbqEkoM/gi\ko e+1@[fha7Dݑ&5IPE8sְ x91JhiXGQ;AM.Wo ˹x7[J2byCs%Tz?X^]_417CcN9>$%C3W)B9fbo xD՛+N`R9`LυRd7P4w: J AIz&8CN7PJzMo =@+٥xz3t&PuuB0@-'-gi$.1@-'F˥X\JS7)JU54xoTWʢ-11@/ mn*G2 r斆L^pk鳄%s-2Ny(%-PR;: 7tHr]Ku'羷=Io*Z[L.;0@19Ul[B\xèЁK7P󰼷}DeP&-gh1fsK h e)L_o1@7#d5ԕw4}`~Yq?3;33n(Mj6J1zAŠ5P\`$1i0FBQ"4 mѴTUD:ҨW -5|k҆@1J NPlgƶfҟMTo8NV>P'z  y(ɘos|l]k^.dRgsНӟl Ut*P9bf p\&Q߰c3K"^ʉZK%ԛGk^T "zo>8nT2}Ǡ<7,$پ|Ypɸ͠RƛppgH}{%Uo9y/YkZ9T^gXaYqdQ+5e>(HOnorT,8CYR^LHꅽkzϦP: I0Mȏ\552\)uIheCkW5ToL2.fp=@3 '&r!M)4fQE655[\-G\Ћ{ZѪW -0N-8$_])yts\YOlӉX4bBoޅ tPp ˛…\sZ#N˚5[6iHom^=vh\gSr#7Г\`|tPߗQ߄VMc=-4$aykh؟uP څu4%ݔ|`Yͷě=J=>KzwR1j5L҄~[V\rg ;pmU -tR͡Ƴjlor7|HR̦z 7#ۍ@1J"9!wc,w{{j6PjޛGkNcQ[SԪloOu'70jb.7}{s~L4NZ)͡1IB>5[˺(Ǒ@sB.|m{si٫X'_yoDwb+H>Ų-9 -2/jyi$!C|.M{{ , }{si 7-"zt+XC|>mo.5:h.E<ձ7օj!'3x9܃ Grxu8{r}Ft#7G닩t<u!%ߑi,eAT.¥=_Kn$Hհ7J9#l>״7>2CIǣ_!q \>d\ͣx|ojV QuG1]{NU:3u]oy"7K7=i`bxSmIzhٛCu ` ]y$E򹛂}-I#B٭ш_o.M$XFoڝo݌|I06O5ͣ![$źiNϳ $X'Ro`FXGt!' #ycoN|HvK_~4w^62[o>8>ަ]oj˝^?NQȣ`Gp٩6kכKzIv^\* l$X4m^֬7Do`!ϔHͥI`%: V^9 @qi+X'Nt>7ݤUo.Eo`)՛GeMJx[ ެF>_1[uǿ A5lMeH:f` C.50ӦJij>d+ְrRqfP@&E~߷﹵\}=:sޯk'ΖZޑͦRR -X2q etӴ"ݓ -H9{I5+H7*Po|s -=z +N욾!yۙ7G~2o(1Ņ-ۅ U]Ϸ_tPZ;2"˻dPf^OZޡ%#UC3AW{2ZrC}آqN̼nY"9so۰A$B-Y(5Oe)D1o(7)FoLZMo(7w.Fo!詺ϷHF.aPr;[Ho(9?,Dov^>o) -qOƲJ~/(ȾUzC~R_9PvNɿXXμBkG ׬r -My9 -䝛W -꿖9-gPAwo7Tg["W*k3r-v&_HZ4&"Z{+Dʼ".38{;m`˲E*#aT~E2'pN:*+8;'傼}8'Q)^9CS|K7Tӭ%HF?d;P)N%rk*[fqtJd'Tw[,]XIo } .C?7 n>\PQ($ rNlkI߂d>$~g7TWP -ˇ6ͩߟB=$1(#ѴnҜtO*i=.71o'upL{y4]'|βhzĞG .Y=:$&KaLn٭0YpL9 pB,+kUpW](y"D?-qet@x<({aހt^erӳn2zށ{+[3А -(x@-Sz506{xgF?PP9"Q].Lpe۵g -ƣ .3ug[,< ӧ -V08]55刭4O镅Hj+ h x],ݥg0OXl\6 ˔AzK& Ɉ8(lf\"s8(Ƭs3 .3p@c^w? Pp|.<M8|DE88+'\"'=>}`E24tۧn{M9}dB}|? -PxqK~ h.][ '_Z` 8n$2yzZ`\u\$О5pe} ;]p|;I92'\,=qaT"YJo@>;b9q?prrEj^p@R7\8UNCL{ހV8XFhEpc߀<]w\"fY.D UNIȜW(681_⎂d}Z;[7\"(?&.u7Ʀk~o46X2Ercɸk_\,cW2p@E҂79[8]m U\"S_7}ܶ+RkkF,ڔNܺSǶ5q5舷py[Ge,$83VܫWgE-w%ЩwIZmfTzT1ӂki_Z6 -#Q˙AC?3 -"{QiL- s@7V&7;WǞq̓;Np@wgܚkM'.1孢k\\$=KlT\"6qP uFWc}KnL{^pjYV܋osd#XKȰ v@մ@ .8 +6qs-aJR_E!([>)ɰwȆAy9d 39p96J ^_ޤ7{}ݮNi еjz{ZyNM-[8K^׾jj&WAyIz{ӵSZ-)9(_QUw?啘 -$AQ#+X ->x4 "2h;NA* -% a)Pa HA) ;gY{w?;ݻ;{o4Hz%"ADo)E$8P"HH $9Lo8T98I"/!S9R{K[# #xVVX)Aȏ| NUl@p Po&vPfKo|& ]`M~.48qmX͡BQreI(6Lli-c%ob)IJ)70qDPQvm{uJ:7\* 6b 1X_5؁ L4X vb3{#$@D9Z}hp9L -8'%& {PnE$"4)m778&Lwnoepxo%?L,pn\㨷1Ln+5qtA7X]6%xPrɋ `(9gwre';ZvQ.Yt^T$ m7 -O!_k#5ݯ4cH $JIz j{.i&-y1%L.Vbp=ymAlzov>BQrEP̌"Eme^M?#K\ `4`z8YtЪ2:ʑn/qHw7&yU]T]5)ly=5HN\o'`9rsGn&=/l+洄nYMjJc!YC{쒀,J(`b__4߀vMdd`U,{7aZ~ҏ1r~En$7Ħ7ʯ<൦!x\oou~lQTlaboқ|La(L(pݳ2EFӑ-z]\ш`b;$B^4yV}["l{ e+Oã1 *-{#$@@Szj.l 6a䬮TΔa}ܫ)rQ i"eͿ?Ckĭ4z\*ʡۻgo`pSEo>gz>iJbӄzJ|1.ڛGS:qdT.D64b 6lκpy $Ey&ZI,OMUXjL6rw&ܖͻY{tL..]IyJ -sN/ߗs) K.|"T= Kz3H=7t}wjjC,"[_)ZzD%E_/bL!8Ӂ)R&x-wޏ/0rރn ʜT$2fRUWY.{j0y *674[*%gP[qlo.y &}XIFqcҡ53u tFZYE?W{>,Ɇ&q h!7!6Mh1/ԋE%&*z?M&yI]^ TUoeAP6g.$HlΉEy:i NF6{jwb[B?+fA4D{7bk}RLlN?TEgkaad \` Fr+b"ٶ!k=UŦ euɶ/@6UqM8`} ?Mcp̟@Qr3:-f:^$gvcVL.5`,;q(%Dr)g `yٔPZ fTY.3mqYH\[c)7-w'8W&?,Me"{7c Uqb*7(BT_|fUo)d"SVJ.0q!c~^6h D62t!Z4L $z3HE&Tp -Gmd aҲrJVʂ>l̘ n/Jj{Sư5B҇d[$g$;|gp\-AW-<>nT="8 ydt|C}aLÀkA܄3Q1.r?)xұ[V -h3 d t"=T͖ '[wFeK!) R6V -49{Yx} -m?zΛPQ;Fvw(97uUK6S7M^uJ.*cGׄ .$SF\ˌ_kD0;$c .gPBTs1h3NrMACD2<'jp!eOd(AA7cM(/Va*g_i)Wc֜c(k'ە(KԮzy$ 򏭢3C@$0?!vi! -%QSE@EXݒ?lVC]A Eإ -*hE`/ymiiݖ7ܙs?sWO.X}[2t6BmE`6NBnn l@) '?X>Y e ^#S?YB\ܑnƒskl_m^FPB-/+?faP!E2 .dup}R-Ԩg - Y1T.k'Ql o܅φoKll}aW%rɳ&$Rxؾb11ԁ'lO\)d9ҭ)>Mp]7Z‚G':u΀(q) -u$dlM -'wk S-| O;y] -1ԍ]7T5ھGOݸ$kTF`OV|PčM]tMcY }v,n \fcj{.)ٚUŠrLV$B@u*B7F6y2|rr;+\[ιrptrCx!r](P -g=c(1 fB8P -G]d i9++m'AUdQn3%ilrO8Ӫo+9ETOiSWhrFdw3:{ϣlW .6lY{ex!8t_xW>mΈȀ5{J=WFsLhPv$}_RMפ} -˴{Bzr5x.UI&${C[#Eqx(կa9EP\%wEOwS8q'^ӘD٢#AJSTY+v0ZGM0٪]lQm>Pvy$+^D & H}Bp8o/wo$_u.7Zհ 7D-z VwԼuYa0N@Ye囍 'cmt-ƗYkC#01*SuS,٩p܅[C_qbhjF Y.&L0W7O.(Uf?UʍlE0%ݹ@"qy*`@vyIy%YqpF$Xu{9"yFmNp*{BeQ-f^}szs>^D lZu4']G67TN@]6dA3$6(OY99Q#a9q 9\=Mx$U /> o$Co0:')8\٣!=zЎˋ5~Q6#]1rZ1O/h_ ~]e7yasrvlRB$@Ifs3FJ1!]Cmhykr-!إ?ns෣Ξr ONۯ0BBʆt/6ds;t>u>-.3oJM:h$c۶2kEȫ`KE9 -~&enc*wnjIcw5kΗ@,'k;}Y.Z.\\)+;=V~M+\`I)^*-O0 ! AB\u߃7%˵ .}w[<Ċsdr#Go?"Z-6K1 -$d/:0\}]7> -vTUC:ˉA€e>Ś>stream -HyTSwoɞc [5laQIBHADED2mtFOE.c}08׎8GNg9w߽'0 ֠Jb  - 2y.-;!KZ ^i"L0- @8(r;q7Ly&Qq4j|9 -V)gB0iW8#8wթ8_٥ʨQQj@&A)/g>'Kt;\ ӥ$պFZUn(4T%)뫔0C&Zi8bxEB;Pӓ̹A om?W= -x-[0}y)7ta>jT7@tܛ`q2ʀ&6ZLĄ?_yxg)˔zçLU*uSkSeO4?׸c. R ߁-25 S>ӣVd`rn~Y&+`;A4 A9=-tl`;~p Gp| [`L`< "A YA+Cb(R,*T2B- -ꇆnQt}MA0alSx k&^>0|>_',G!"F$H:R!zFQd?r 9\A&G rQ hE]a4zBgE#H *B=0HIpp0MxJ$D1D, VĭKĻYdE"EI2EBGt4MzNr!YK ?%_&#(0J:EAiQ(()ӔWT6U@P+!~mD eԴ!hӦh/']B/ҏӿ?a0nhF!X8܌kc&5S6lIa2cKMA!E#ƒdV(kel }}Cq9 -N')].uJr - wG xR^[oƜchg`>b$*~ :Eb~,m,-ݖ,Y¬*6X[ݱF=3뭷Y~dó ti zf6~`{v.Ng#{}}jc1X6fm;'_9 r:8q:˜O:ϸ8uJqnv=MmR 4 -n3ܣkGݯz=[==<=GTB(/S,]6*-W:#7*e^YDY}UjAyT`#D="b{ų+ʯ:!kJ4Gmt}uC%K7YVfFY .=b?SƕƩȺy چ k5%4m7lqlioZlG+Zz͹mzy]?uuw|"űNwW&e֥ﺱ*|j5kyݭǯg^ykEklD_p߶7Dmo꿻1ml{Mś nLl<9O[$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! -zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)Km endstream endobj 5 0 obj <> endobj 20 0 obj [/View/Design] endobj 21 0 obj <>>> endobj 12 0 obj <> endobj 10 0 obj <> endobj 22 0 obj <> endobj 23 0 obj <>stream -%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 17.0 %%AI8_CreatorVersion: 19.2.1 %%For: (Zachary Mitton) () %%Title: (metamask_icon) %%CreationDate: 6/15/16 2:23 PM %%Canvassize: 16383 %%BoundingBox: 98 -140 188 -44 %%HiResBoundingBox: 98.7919746568114 -140 188 -44 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 13.0 %AI12_BuildNumber: 147 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBProcessColor: 0 0 0 ([Registration]) %AI3_Cropmarks: 79 -156 207 -28 %AI3_TemplateBox: 180.5 -120.5 180.5 -120.5 %AI3_TileBox: -163 -488 449 304 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 6 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 1 %AI17_Begin_Content_if_version_gt:17 1 %AI9_OpenToView: -39.6666666666679 23.666666666667 3 1419 866 18 0 0 -5 38 0 0 0 1 1 0 1 1 0 1 %AI17_Alternate_Content %AI9_OpenToView: -39.6666666666679 23.666666666667 3 1419 866 18 0 0 -5 38 0 0 0 1 1 0 1 1 0 1 %AI17_End_Versioned_Content %AI5_OpenViewLayers: 7 %%PageOrigin:-220 -420 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 24 0 obj <>stream -%%BoundingBox: 98 -140 188 -44 %%HiResBoundingBox: 98.7919746568114 -140 188 -44 %AI7_Thumbnail: 120 128 8 %%BeginData: 22554 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C45FD24FFA776FD75FFA04A4AA1FD73FFA04A754A75A8FD71FF7C4475 %4A6F4A6FA8FD6FFFA04A754B754B754A75FD6EFF764A6F4A754A6F4A754A %76FD6CFF764A754B754A754B754A754AA1FD69FFA8754A6F4A754A6F4A75 %4A6F4A6F4AA1FD44FFA7C9A075A8FD1EFFA8754A754B754B754B754B754B %754B754ACAFD3FFFCFC9C299C1997476FD1EFFA76F4A754A6F4A754A6F4A %754A6F4A754A4B4AFD3BFFA7C99FC198BB98C198754AA8FD1DFFA8754A75 %4A754B754A754B754A754B754A754B6F76FD37FFC9C99FC198C199C199C1 %99754A75FD1DFFA74B4A754A6F4A754A6F4A754A6F4A754A6F4A754A4A76 %FD31FFA8C9A0C1999998C1999F99C199C1746F4A4A76FD1CFFA1754A754B %754B754B754B754B754B754B754B754B754B6F7CFD2CFFCAC9C89FC198C1 %99C199C199C199C1C1C175754B754ACAFD1BFF7D4A4A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A6FA8FD27FFCAC9A1C2989998C199C199 %C199C199C199C199C16E4B4A754A75A8FD1AFF7C6F4A754B754A754B754A %754B754A754B754A754B754A754B754A75FD24FFC9C99FC199C199C199C1 %99C199C199C199C199C1C1994A754B754A6F76FD1AFF764A4A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A76A8FD1DFFA7C9A0C1 %99BB989998C1999F99C1999F99C1999F99C199C199994A4B4A754A6F4AA8 %FD19FF756F4B754B754B754B754B754B754B754B754B754B754B754B754B %754B754A76FD1AFFCAC299C198C199C199C199C199C199C199C199C199C1 %99C199C199994B754B754B754A7CFD19FF764A4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A99FD04C199C1C1C199C1 %C1C199C1C1C199C1C1C199C1C1C199C198C199C199C199C199C199C199C1 %99C199C199C199C199C199754A754A6F4A754A4A7DFD18FF7C6E4B754A75 %4B754A754B754A754B754A754B754A754B754A754B754A754B754A754BFD %05C1BBC1C1C1BBC1C1C1BBC1C1C1BBC1C1C1BBC1C1C199C199C199C199C1 %99C199C199C199C199C199C199C199C199754B754A754B754A754BFD19FF %754A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A4B74C199C199C199C199C199C199C199C199C199C199C199C199 %9F99C1999F99C1999F99C1999F99C1999F99C1999F99C1994B4A754A6F4A %754A6F4A76FD18FFA14A754B754B754B754B754B754B754B754B754B754B %754B754B754B754B754B754B754B7599C2FD16C199C199C199C199C199C1 %99C199C199C199C199C199C175754B754B754B754B754B75A1FD18FF4A4B %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1 %99C199C199C199C199C199C199C199C199C199C16E4B4A6F4A754A6F4A75 %4A6F4AFD18FFA16F4B754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B75FD16C199C199C199C199C199C199 %C199C199C199C1999F6F754A754B754A754B754A754A7CFD18FF764A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A9999C199C199C199C199C199C199C199C199C199C199C199 %9F99C1999F99C1999F99C1999F99C199994A6F4A6F4A754A6F4A754A6F4A %4A7DFD17FFCA4A754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B7575FD17C199C199C199C199C199C1 %99C199C1C1994B754B754B754B754B754B754B754BFD18FF764A4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A4B74C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1 %99C199C199C199C199C199C199754A6F4A754A6F4A754A6F4A754A6F4A7C %FD18FF754B754A754B754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B7599FD13C1BBC199C199C199C199C1 %99C199C199754A754B754A754B754A754B754A754B75A8FD17FFA04A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A7599C199C199C199C199C199C199C199C199C199 %C199C1999F99C1999F99C199C174754A6F4A754A6F4A754A6F4A754A6F4A %6F75FD18FF4A754B754B754B754B754B754B754B754B754B754B754B754B %754B754B754B754B754B754B754B754B754B754A9FFD14C199C199C199C1 %99C199C175754B754B754B754B754B754B754B754B754AA7FD17FF7D4A4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A4B4AC1C1C199C1BBC199C1BBC199C1BBC199 %C1BBC199C199C199C199C199C16F4B4A754A6F4A754A6F4A754A6F4A754A %6F4A75A8FD17FF764A754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B7575FD13C199C1 %99C199C1BBC16F754B754A754B754A754B754A754B754A754B6F75FD17FF %A84A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A4B74C199C199C199C199C199 %C199C199C199C199C199C199C199994A4B4A754A6F4A754A6F4A754A6F4A %754A6F4A754AA1FD17FF76754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %9FFD11C199C199C199994B754B754B754B754B754B754B754B754B754B75 %4B75A8FD16FFA86F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A99C1C1 %99C1BBC199C1BBC199C1BBC199C1BBC199C199994A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A6F4A6F75FD17FFA74A754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A754B754A754B754A754B754A %754B754A754B754AFD13C199754B754A754B754A754B754A754B754A754B %754A754B754ACAFD16FFCA4A6F4A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A4B4AC1BBC199C199C199C199C199C199C199C1996F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A75FD17FF7D6F4B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B7599FD10C19F4A754B754B754B754B754B %754B754B754B754B754B754B6F7CFD17FF764A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A7599C1BBC199C1BBC199C1BBC199C1BBC199C1C199 %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754AA8FD17FF75754A %754B754A754B754A754B754A754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A7599C199FD12C1994A754B75 %4A754B754A754B754A754B754A754B754A75FD17FFA8754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A7599C1999999C199C199C199C199C199C199 %C199C199C199994A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A7CFD %17FF75754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B7599C199C199FD13C1 %99994B754B754B754B754B754B754B754B754B754B754A7CFD15FFA8754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A7599C199C199C199C1BBC199C1BB %C199C1BBC199C1BBC199C199C199994A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A76A8FD13FFA84A754B754A754B754A754B754A754B754A75 %4B754A754B754A754B754A754B754A754B754A754B754A754B754A754B75 %99C199C199C199C199FD0FC1BBC199C199994B754A754B754A754B754A75 %4B754A754B754A754AFD14FFA14A4A754A6F4A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %75C199C1999F99C199C199C199C199C199C199C199C199C199C199C199C1 %99754A6F4A754A6F4A754A6F4A754A6F4A754A4B4AA8FD14FF7C4A754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B7599C199C199C199C199C199FD11C199C199C1 %C1754A754B754B754B754B754B754B754B7576FD12FFA8A151754A6F4A75 %4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A4B74C199C199C199C199C199C199C1BBC199C1 %BBC199C1BBC199C1BBC199C199C199C199754A754A6F4A754A6F4A754A6F %4A754A757DFD11FFA14A4A4A754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A754B754A754B754A7575C199 %C199C199C199C199C199C1BBFD0FC199C199C199C199754A754B754A754B %754A754B754A754A4A75CFFD10FFA8754A4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %4B6EC1999F99C1999F99C1999F99C199C199C199C199C199C199C199C199 %C199C199C1999F99C199754A754A6F4A754A6F4A754A6F4A754A4A4ACAFD %11FFA8754A754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B7575C199C199C199C199C199C199C1 %99C199FD11C199C199C199C199754B754B754B754B754B754B754B754ACA %FD13FFA87C4A4B4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A756EC199C199C199C199C199C199C199 %C199C199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C199754A %6F4A754A6F4A754A6F4A754AA7FD17FF75754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B756FC199C199C1 %99C199C199C199C199C199C199FD11C199C199C199C199C199754B754A75 %4B754A754B754A76FD13FFA8CA7DA176754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A4B6EC199C1999F99 %C1999F99C1999F99C1999F99C199C199C199C199C199C199C199C199C199 %9F99C1999F99C199C198754A6F4A754A6F4A754A4B4AFD12FFA87C4A4A4A %754B754B754B754B754B754B754B754B754B754B754B754B754B754B754B %754B754B754B7575C199C199C199C199C199C199C199C199C199C199FD11 %C199C199C199C199C199C199754B754B754B754B754A75FD14FFA8754A4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A4B4A9F99C199C199C199C199C199C199C199C199C199C199C199 %C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C199C1994B4A754A %6F4A754A6F4AFD16FFA8A14B754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A7575C199C199C199C199C199C199C199 %C199C199C199C199C199FD11C199C199C199C199C199C199754A754B754A %754B6FA7FD18FF516F4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A4B4AC1999F99C1999F99C1999F99C1999F99C1999F99 %C1999F99C199C199C199C199C199C199C199C199C1999F99C1999F99C199 %9F99C1754B4A754A6F4A6F4AA8FD17FFA1754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754BC1C1C199C199C199C199C1 %99C199C199C199C199C199C199C199FD11C199C199C199C199C199C199C1 %75754B754B754AA7FD15FFA8A14B4A4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A756E9999C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C1BBC199C1BBC199C1BBC199C199 %C199C199C199C199C199C199C174754A6F4AA1FD17FF4B6F4B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B754AC1C1C199C1 %99C199C199C199C199C199C199C199C199C199C199C199FD11C199C199C1 %99C199C199C199C199C175754A76FD18FFCA4B4B4A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A6F4A9999C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C199C199C199C199C199C1 %99C199C199C1999F99C1999F99C1999F99C199C16E4BA8FD1AFF756F4B75 %4B754B754B754B754B754B754B754B754B754B754B754B756F9F99C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199FD0FC1 %99C199C199C199C199C199C199C199C1A1FD1CFF4B4B4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A4B4A9F99C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C1BBC199C1BBC1 %99C1BBC199C199C199C199C199C199C199C199C198CAFD1CFFCF4A754A75 %4B754A754B754A754B754A754B754A754B754A754B9999C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199FD0FC199C1 %99C199C199C199C199C199C199C1CAFD1DFFA84A6F4A754A6F4A754A6F4A %754A754A754A754A754A6F4A99999F99C1999F99C1999F99C1999F99C199 %9F99C1999F99C1999F99C1999F99C1999F99C199C199C199C199C199C199 %C199C199C1999F99C1999F99C1999F99C199FD1FFFC299C1C1C19FFD0FC1 %99C1C1C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199FD11C199C199C199C199C199C199C199C2FD1EFFCABBC1 %99C1C1C199C1C1C199C1C1C199C1C1C199C1C1C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1BBC199C1BBC199C1BBC199C199C199C199C199C199C199C1A0FD1EFF %FD18C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199FD0FC199C199C199C199C199C199C198C9FD1DFF %C9C199C199C199C199C199C199C199C199C199C199C199C199C199C1999F %99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1 %999F99C199C199C199C199C199C199C199C199C1999F99C1999F99C19999 %A1FD1DFFC9BBFD19C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199FD0FC199C199C199C199C199C199C198 %C9FD1DFFC1C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C1 %99C199BBA7FD1CFFC9FD1BC199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199FD0FC199C199C199C199C1 %99C199CFFD1CFFC298C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C1999F99C1999F99C1999F99C1999F99C1999F99C1999F %99C1999F99C1999F99C1999F99C199C199C199C199C199C199C1999F99C1 %999F99C1999F98C1A8FD1CFFFD1EC199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199FD0FC199C199C199C199 %C199C199FD1CFFC9C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C199C199C199C199C199C199C199C199C199C199C199 %C199C1999999C1999999C1999999C1BBC199C1BBC199C1BBC199C1999999 %C19999989999999899A8FD1BFFC2FD1EC1BBC199C199C199C199C199C199 %C1999999C1999999C1999999C199BB99C1999999C1999999FD0DC1999999 %C1999999C1999998C9FD1AFFC998C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199999899999998999999989999 %99989999999899999998BB999998999999989999C199C199C199C199C199 %C199C199C199999899279998999999A0FD1AFFC2FD23C199C199C199C199 %C199C199C199C199C199C199C1999F515299C199C199C199C199FD0DC199 %9999C1992E4BC199C199C2A8FD18FFCAC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C19999989999 %99989999999899999998C175510528057598999999989999C199C1BBC199 %C1BBC199C1BBC199C19999989927286FBB99C198C9FD18FFC9BBFD25C199 %C199C199C1999999C1999999C1BB994B2E0628272E279999C1999999C199 %FD0DC1BBC199BB992E282E99C199C1A0FD18FF9FC199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C1999F99 %C1999998999999989999BB98752706052827280528279998FD0499C199C1 %99C199C199C199C199C199C1989998990528054B98C198A0A9FD16FFCAFD %29C199C199C199C199C1999F7552282E272E272E272E272875C199C199C1 %99FD0FC199C1752E062E51C1BBC199FD17FFC998C1BBC199C1BBC199C1BB %C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C199C199C199C199992706052805280528272805280528989999C199C199 %C199C1BBC199C1BBC199C1BBC199999951057699C199C1999FA8FD16FFFD %2AC199C199C199C199C199A07576272E2728052E2728272E277599C199C1 %99C199FD0DC199C1759FFD04C199C199CAFD15FFA7C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C1999F99C199C1BBC1C1C1999F7575272827280528057598C1 %999F99C199C199C199C199C199C199C199C198BBC1C199C1999F98BBA7FD %15FFC2FD2EC199C199C199FD0BC17576512E27C19FC199C199FD0FC199FD %05C199C199CFFD14FFCA98C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1999F99C1 %999F99C1BBC199C1BBC199FD05C1999F99C199C199C199C199C1BBC199C1 %BBC199C1BBC1999999C199C1BBC198C1CAFD14FFC2FD31C199C199FD11C1 %99C199C199C199FD0DC199C199FD05C199FD14FFA8C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C1999F99C199C199C199C199C199C199C199C199C1 %99C199C1999F99C199C199C199C199C199C199C199C1999999C199C199C1 %CAFD13FFCFFD32C199C199FD15C199C199C199FD0FC1BBC1C1C199FD14FF %A0C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1 %BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C199C199C1BBC199C1BBC199C1 %BBC199C1999999C199C1CAFD13FFC1BBFD33C199C199FD15C199C199C199 %FD0DC199C1C1C1C2FD13FFC998C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1999F99C199C199C199C199C199C199C199C198C2FD13FFFD52C199C1 %99FD0DC199C1A1FD12FFA7C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1 %BBC199C1BBC199C199C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C199C199C199C199C1BBC199C1BBC199C1BBC198C9FD12FFC2BBFD35C1 %99C199C199C199FD17C199C199FD0DC1C9FD12FF99C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199999899999998C1999999C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %98C2FD11FFC9FD31C199C199BB99C199BB99C199C199C199C199FD15C199 %C199FD0BC1BAC9FD10FFC2BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1FD0499 %98999999989999C199C199C199C199C199C199C199C1BBC199C1BBC199C1 %BBC199C1BBC199C1999F99C1BBC199C1BBC199C1BBC198C9FD0EFFCFBBFD %2BC199C1999999C1999999C1999999C199C199C199C199C199C199C199C1 %99FD11C199C199FD0BC1BAC9FD0DFFA0C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C19999989999 %99989999999899999998FD0499C1999F99C1999F99C1999F99C1999F99C1 %99C199C199C199C199C199C199C199C1999F99C199C199C199C199C199C1 %98C9FD0CFFC299C19FC199C19FC199C19FC199C199C199C199C199C199C1 %99C199C199C199C199C199C199C1999999C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C19FFD0FC199FD %0CC1CFFD0BFFA79999C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C19999989999999899999998999999 %989999C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %BBC199C1BBC199C1BBC199C199C199C1BBC199C1BBC199C199CFFD0BFF99 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C1999999C1999999C1999999C1999999C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C19FC1BBFD09C199 %FD0CC1CFFD0AFFC2989F99C1999F99C1999F99C1999F99C1999F99C1999F %99C1999F99C1999F99C1999F99C199C1FD0499989999999899999998FD04 %99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1 %999F99C199C199C199C199C199C199C199C199C199C199C199CFFD09FFCA %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %FD07C199FD0CC1FD0AFF99C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199999899999998999999 %989999C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C1C1C199C1BBC199C1BBC199FD04C1 %FD09FFC999C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C1999999C1999999C1999999C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C1C1C199FD07C19975754B27A8FD07FFA8C1999F99 %C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C199 %9F99C1999F99C199999899999998FD0499C1999F99C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F %99C199C199C199C14A27F827F805F8F8F852A8FD06FF9FC199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1BBC175270027F82727272027F82752FD05FFCA98C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C1999998999999989999C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C198BB98C198C199C199C199C2A0A0A0 %C9A127F827F827F827F827F827F8F87DFD05FFC299C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C1999999C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C299C199C2A0C3A0C9A1CAA7CAA7CAA8CAA8CAA8A8 %2727F8272727F8272727F8274BFD06FFA0BB999F99C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C19999 %98FD0499C1999F99C1999F99C1999F98C1999998C198BB98C1999F99C199 %A09FA1A1A7A1A8A1A8A1A8A8A8A7A8A7A8A1A8A7A8A1A8A7A8A127F827F8 %27F827F827F827F8A8FD06FFCF99C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C29FC199C2A0C9A0C9A0C9A7CAA7CAA7CAA8 %CAA8CAA8CAA8CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA8CA27272027272720 %272727F852FD08FFC299C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C1989998BB99C199C199 %C2A0A0A0C3A0A7A1A8A7A8A1FD07A8A7A8A7A8A1A8A7A8A1A8A7A8A1A8A7 %A8A1A8A7A8A1A8A7A8A1A8A7A8A127F827F827F827F827F827A8FD08FFA1 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C198C2A1C9A0C9A1CAA7CAA7CAA8CAA8CAA8CAA7 %CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8 %CAA7CAA8CAA7CAA8A8FD0427F8272727F82752FD09FFCF98C1999F99C199 %9F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999998 %BB98C1A0C9CAFFAFFFA8A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7 %A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1 %CAA127F827F827F827F827F8A8FD0AFFC299C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C2C9CFFD0AFFA8A8 %A7A8A7CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CA %A8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8A8FD0427F827F827F87DFD0BFF %A8C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %989999C9A7CFFD10FFA8A87DA7A1A8A7A8A7CAA7A8A1A8A7A8A1A8A7A8A1 %A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1CAA127F82727 %5252767CA1A8FD0CFF9FC199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C2C9CFFD16FFA8A8A1A8A7A8A7CAA8CAA7CAA8CAA7CA %A8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7A8527D %7DA8A7CAA7A8A8FD0DFFC998C1999F99C1999F99C1999F99C1999F99C199 %9F98BB989999C9A7FD1CFFCFA7A87DA17DA8A1A8A1A8A7A8A1A8A7A8A1A8 %A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A1A77DA8A1A77DA7A1A1 %A7FD0EFFCAC199C199C199C199C199C199C199C199C199C199C2A0C9CAFD %23FFA8CAA1A8A1A8A7A8A7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CA %A7CAA8CAA7CAA7A8A1A8A1A8A1A8A1A8A8FD10FFA0C199C199C199C199C1 %99C199C199BB98C199C9CAFD29FFA8A77DA7A1A77DA8A1A8A1A8A7A8A7CA %A7A8A1A8A7A8A1A8A7A8A1CAA7A87DA8A1A77DA8A1A77DA7A1FD11FFCA98 %C199C199C199C199C199C198C2A0C9CAFD2FFFA8A8A1A7A1A8A1A8A1A8A7 %A8A7CAA8CAA7CAA8CAA7CAA8CAA7A8A1A8A1A8A1A8A1A8A1A8A1FD12FFCA %C198C1999F99C1999998C2A0CAA8FD33FFA8FFA8A87DA77DA77DA77DA77D %A8A1A8A1A8A7A8A1A8A1A77DA7A1A77DA7A1A77DA7A1FD14FFA0C199C199 %C199C1A0FD3DFFA8A8A1A8A1A8A1A8A1A8A1A8A7A8A7A8A1A8A1A8A1A8A1 %A8A1A8A1A8A1FD15FFCA98BB99C2A0CFFD41FFCFA7A8A1A17DA8A1A77DA8 %A1A77DA8A1A77DA8A1A77DA77DA17DCAFD16FFC9A7FD49FFA8A8A1A8A1A7 %A1A8A1A8A1A8A7A8A1FD04A8FFA8FD66FFA8CAA8A8A8FFA8FFA8FFFFFFA8 %FD12FFFF %%EndData endstream endobj 25 0 obj <>stream -%AI12_CompressedDatax$&?d& C;\;fJ-n[[YUZ(xd&,f #+3q98OxOq< ?wo ~7_{ˏ~/&G4M~Vۯ߼LG{4?oqwo^_^ޡݻ篞g/PWnC} 4`e߱=&7Ô?L/ޣvu&3%Co^|O߾yq7/߼W?>y~^|0;qv~c7/o^a|t/_/tqzWw0R<3ٯOaC)?l/Jo|ۿi[Lݘج{K̬̂1??J[x?~W~NguG|˻FѤS7_ܽD/ H1oɦ >Kz3 3y}vgӭw2IM~?WyOtҺ2!Lj}.6(^{_G<Ѽb |aoSl߿DŽkѽ_bٚO1';=I~R4!o;H]cձW?y=5w7_j|ӷR}To?~_^bqQ>mŃߔ?뿏 Bӛ{U>}|CsL!޽zn -!}Ho_wg߾܎Ͽzz_~˧ߩO n_|=w.̙/|ܿ8~_xNuTOX[˷Zߥfi>$_>ksP3|q-y=?F?!]϶j~xx7/~tS/>\?߿cwwI|+r;鳶|0e> loq\߼3L/pba,H=;0?)vU\) ߀%%Ӧ\ \܌x[f`*nU-LDIo^iSi.繜5Jz7ѵ][*FAiUU*'-VmӯVuY[a^^Zd]fU͛V+ebe7g]k;la]/WkdYԬU)۵Z)*և:YeXtMe@CY#չk)7ܲԓŗYUeLIɭ̍zʵؔF2 ssλɝP-Vx>'O[L .C -S -p7v v)esUsSʧ|o-&7 LNyni̕W*^rdNONtemwp|:[l6#ut}>_\ތXwoM7 usnnnn#n1aoz^m7r*U9զL _^*qSªUq 8R$l!z7M9kӪ\ʴ*ySҪU M*vU̪KS>_֣_WENf]Z%. bXv 2ʌd)v64o򬡙R&)$%JR\)vWL%Ϫ?')WLRr)8K R|)%Ѓֵ;zeY eeگed&G/fdndbN2yw|Q^Z^Jd^Fq``2Ϡ[W_t,yP5 j>H73_J F1aoטt@H tr@x#HuN D;x;p{{@}w & -D5CpqnX. K׌ucq7g\DW);2UnC|Ey x;Rٙ-خv8Pz? gx'H˗v؅0ܮH *`Cld!2r.y 9&*w"6`ټ.b/+>P,"eXalX~PdS }bٛ2<duXE3Ϩr;.E9J\d('}(bs=U-l4W=9"}ZӬYAL6%Ts끼VJ{OX 2=ye[3UCwD翇d?Sԇos<;XԵ?ʏOBF7mLE߰s8҆+ H$" (" tAy\( !DʢJBFǭH|! ,DiȪ4$uN"e(rE"R,RQш‘Q ,e$JI OeSB%'ЈjFĥkK(2Qhؔ|J5t[듖|97nI?FյX4̲P,~)uuxIx" ?4 Qs E6< KCvD1l4맕aýE|r.VOoGEq3- -U -ͪLTTJэEUZ*mXM]JY\9UDi%%2r5=Ҵъ %s(It8i4fCT -a);12y?Ӌ+[ @c&*PV5m\86 ŸV+-7bU[#w)Zf{% 0<@jXp1frb=]9It=G9 [>EXy+3KoJO+ï~QQ0:1L<98Ilj> -Ѿ8Jl+u!3)]Hh% \hB#ڸLo{[Z&qk"fÊmIWCv8x}i؎u'^{߇qmCIJϞ=c@09Bk۱iccwt6sM&;XݸCI1TQoZ@,qx)G7?}HH|5FPH!H58RZ$n҇U}|OV#pDnyu:q/#7I0c (0 -+tǵl⟿Z +R>qV#6Cxji7~zQfd @,zv4./_bS;;c4`&# 4؜6%0ySIRм m{=jP]||s|<:@> lm/zr._S  -_rUXBa3|!<4S<0< Sy+De&W@AAr-^uUjKTˈXōuXᒗe?#uFnfVŜzQ27sPf'z3 mdq<9+r>3_;ug{YʪHNEa십]ԕd1U w]<[ )8c(bb<;]=),IȥId,v+POD QuVoʬ*rk$V_ba_U[X.}7IʲC#rOL,lEkY, En%ʦ"ƞD1V] ֯$XJ:vl`{6VY=+2ҡYAnVط6{;,Y ugeV^am|O;>ͩ3/Յ8/+w J.zꓨW|٘X \bU鵧_o(r = wxw<}e|m>rz|/GTy/Ҡ `+0{ط>WG|/\,O_{ُF-I}34Yݵuw}\&vřTi: }S͍2.",W(^QFl~34mٷ!,ԅl#AiEqڐ9,%#s֡%bn#'g=<r9?3:Iq?:?'3{T|ʞ:/yE X0=QnEk Zi:EBm9Ɍ \:H͕}g02x˩yj4>/=?S=w טiąYpo8&joEI'Oo>sQcb J"cTՎb&)xEUܔ&`dgFzv%ś9rJAKqj6Myt a\~ا~+7^ݽJhgyus]j/iR:SnBL%4#Kq _ KX*8^Kz\e>l/ư7TVrǩ0 7U/ޅA[lH8$&qOTqʅZ%.B5!% KJ)@(|GY:>LBaK\NKyUoItfm6ŭݐ\¦?)NUWe6>%BE6Q%J>q唚mWCyZy"obݮuc*qj+}ZX9>p,JͫoD Dޤ$;;mXGƕl_f#2-dpeQSEN[cnm4;h\; Vymff g,Ju}o14Q$:i\-.ns8ič+9m6VkϚ|['hZzvvAuaG`}t`}YhG_:m8GN84hu*, _ CkA|,|aGWuw#my{"Vފݹ(|مavWJů9jZ~,wr Ez_dxZ_KY˻JR EV 襠qwU)y@R޸0fwnxQ4;9LAǦ6 -wz·2_}q|t0>\v,нe| -(Cq7o]ͷ`['sثj[d˧1rQNت8 ETԑ<}>S|efk Hʾ_>Ctu;,0D9,'%CS9 .N[ZcqaO΃q5Ovi7bE@Om>nzw)6>FaCmq$8I?I7 ,BԽMٻ -M^A*V*#9Փi]nLʼnigLìQ&[,_?5@'Q -oDT7 F\'uY6@  ,[eh>O*r.V+÷h#exZ:i0$3QN -ߑ6V<ϒ^XEH92kV'jX\+KdP8SGYр3ɡeNqV1 e'%:U9J1џP:vlu{L[5*F6Ԯ/+B ƪ-; eфaǓJږ߶<' Oo_KM ]"\EkF0EDϤE Ȕθϔog|VUu^v)H! {'xT:UjCITҒآ(ZJ(Z4KY薮lZJa\"6Bi(D9P{i{::6KOyíO0i5*alX!X%RK努Q>E(c4,Zۙ;^o;r%( />1]3HJW.=Cq Q; JliJB%۵C^ײҫjL[ExDZ䤵 nI˴~ԻzMj,v8'2<9> Oo_êD Oس&F$(0SNeb - -0jhMMop-~v*fPj0<м4A͗ƪ]\ /0)zdp[_bݫjZL]kmrkX6ָ՚.XƬuɨ1i=d.LYO^IS)exZ 2< NO' -O' -xm7?a>Ykpi>d8$\09 f‡B'zgFxqO/e⎭cT1;ɸC0tmojj{ (qp͂C@z Nim)x?NvU!qnܺ//rvEi]ŧMi|T3ٷO'ؤ\L+u6u&٦|t,JtѲK]j=?Q`%֜k,pHyTak5u1'@IQAl P8MJ&7nJiQ<YOdF0DG3yvhCx/ew9ˣ/h-UPhnu&rSwOfc[x^$؝lxqq}csnٖIİX#!uDmw2<@7[I%~d<>WMeuv]a v2K%ѳ.ڟsq\\40Ž[Ѭ 7A 7_,~bN|v#n`^E.'[dՊ>R: ^G=,>($&K}id5VZڨpk ,ٴ 0 JJTgb60rOd`WIj>Q)3G^<-g@GpAYj,D9&# n-ƄA2 m2v'}2(W]c~TQ9oXVͨt?{<% rv 3nl=ثcvL !crU8+}Nߓ%@q+%ǭ$"~(2^ -Ma\Qݙ>Y+|Z[1z[gz[1Ԙ۴mJUr5Y|Vnodw`xNRKisl\7P΍||TGYugnKE$%V`t -6>+j::T\Phel銻PnC%oS5 -YSh -fWX1V *:I2SOBI44!eVk?xܓ'cOLj4-oKYuUh% JLfd-ytu<+qeYmˇ_CUVN`7(<ˌpaN ƍ]崂v - 6<žr[D\,-9n^$ D8aw2\0[ԡz*--ZLTFh7@K`nQ@밆#^MnRD_yrwQvpѹჲpb?McCa7Mt;HsM8)4y%qi$!wb-jqo1Pm+?:W]:HC'tJ!v\Y"fzw)n.(RD -K gѢ_ \߹D!˔] bjQ"#ΐSm2a+|;rudȼ]A>Q?ulR0Ԝn`uEss9 +Q37fQ;NLthp) n)n6WZnE*:]yu}; ӕdqYJZ,=*SZ< JӐH~[͎csߥ ÅR$Azae+̬䜽)Ò)SA$ZܬKV׬q:k)=As<2mS/LtMo@] ?}S>wihx9{JgU|>i?)D=:Kb%5ީ xyN$ȫik. ~s>4P}h/=vY@M~ ƽ#j&btan"k9hj/$s.!OE8]O$opLs*~$neqb:<7BB.oN4;rN/+"V}ZC^2TX5wb!xS`*ffs9 : i.!JUfQ?H*F!uV[6Nա$ͲWa⇰0IRnJ:'hp!bBY -`E;p8O -n2 ;aN(FXg `ᡭXbmZbw k7ax-(ÅS[/|um*][Wm!n;2._=11i8cT]#w-ՇI~ݔ÷Q~^/CQJ Rz$-Hi[Ȥ"k?hR{W5qn UVi+~ - -whpC=C~ӷݿOVrbXݽ} ?9DaSt~;X43a{GOl6O2+o:O?1'?O}t+T>:oZv{{~ywo^?ïD{ӛ7/1y)wo>;=WL?ܿ{݋w8w|ȯZ>@ȒgEYkpZtSacBM~˵)\Y$gq81A`EtY5$fL!Lzv#`!1<5ق6$CkG9`g9A&0=޲;z|ŏM?!0}<&|;t> C0/6R[¾3>8bx 7鈹3]Gg #.6ÄJ*1*ɁDݡ ؚ#{6+#Xu<*ln[T8lǀ`!k_&.GN΄bia&P,. G_;3GxbcY16TCG)D[^6Wc,d:ɓGd1IN'LH0%@;K'Y(G(MO#6%] @PbsO' d$S߆d-J4Rwg4udo+qo5!|~lb>Y Vg= !9P`W^éȝGV` jTZYBؓuZuvd1elYSg#n -}[Ϲ0qufۿdf7_/ ʃs7_ٛ? ojõL{آ!TfEbѝ(xL(2f㴸˸$n -,L"C"zJЄ \7T[ʤd + 6vBoeW]!DW1oHwm%H2kdRq'CՎXZ[i;Fx#B޵] yoeNl3_cD9D/*m% -dބSy]- u•HJbcx[ w?* rYrBʟ:3̑nsS.eUdSik3G>fT9i\h?qsiC;+x#@E:PlXJŽKun|ƛy$ 8C(A]Z0Oׄm x{\F0TOEȓHfwaaJ@Cr`%@|R%RU#>Lo;yp"MfP"0=Xpn |9APr- -23A(LOř\'"Dӂ3 -|=g>/ݡR҉Ѩ#3'7=.{&a1[f^qɷb!qVxT,@m gS+ + ~ -gZh+6,QYޚYռ9>sǹ %ґ?l mm]㽃)Lp轑ChM - SI8\Յ<%44.i+഻2>=ρo?Rݵ41?U58?!Yި&2qňLeLf9m#p\ Kdve~e K¶#}0UOe$|xA*4>:Q[# - LTU$ ֎&&4yr($bx(5M0Ɉprwݗ#-F6<_fn.(`wy|nME&ڑFȄ/;b)q&L{30D>fL4Z$.H=%7}+.Xl Vnp4s. .N:іDkP'_C:QӍŪNT)/#*tNN."PtHݼBsEn>nlG؊ TV7SBH`dp,d kEڴqƑ-@#v∣ؔfAR !D^Kh׊309Pd.WjZQJ`t-vߩkNM7nv[y=Anu =U^d*NIq<4Q) -4֣VԻs9)߲zNWQFm;-:ϱ?3RONYϹ4wJ?Y1F7lֵ, F8J\oY}68j @)7TەTEਟ -ic c&N-pdd?ѭLc̍w0SkY~x;(&68`d]@i U}q@3ŭOʨ4|nނ, xʀ1MO#iڸ&H-nqdq]$v0qTɣR;{#OR?WjJ'$q+M[1MFU7$#C tm!NLa\h{:ldneMq @G׾QKr>2k;AMx_}puB^C?1*!jT-tC"GӰ׏-8`Ǹ;EN/ik0C?* RFnvn~Uh:}|KCpބTmOM4{ޭ8Ix!@cnY`LXV@--4%"Epj|E$GΈdɄx%hkQQomh=mCQoOwN $. -4"Fj0Z̝eHӐKaDcJj{ eY[El]lB8y@A;^|Dڋ(@\▃@:usFtΐ"hνIB/>RzH#wm=Y sΈr ̎\DK墿8 W&:nХb%uV7K$3)ގI^c_SroT#xCAa#4[So+8ո{|*&D -l0;ut0ۙ\L2(D/qpoTWF\ n6!XH/@]/"#RBեx9D -1 fEh5X1FHXPIX X|5^ɓIr=t]F3}7Q0Yuoe%D>9tɆ/Ge#* BBu Vѫ!-W'0rǺ~,!^iTvtItu:+! H{D>GYƕn\/ iǩ'+z&;vʈ!5p O| ߩw{$${b[BPO;y,Ͼ YKfa5;-gLP>]yl?w"CuQ*v>ͫ֋БGqE{'{: -豛ɗWi+vwf7V'˒Qv]j.gT쒑hL~^eY݄6:oCت(^@Úd7^_y“-]- T?Q{jԿYgOcV/ɂ3xo]:y_~lukjȣ^}V%/VQ92RඈPūV Xԓ;Nev=ϻJbOӪwCi֠cû3P.v^z:p}:5I /P'U`fxX;Eu} e?HS߼N"7m&E&<0A4۴-fRny|WF$C2KaR`/v&ӋKD?:\ - DLsL^:~"r|ws5mn%n!#\ -얍y09ġxJxI&0!Sl <ཽAz#௑(k$2JS` L%NO;/< &*0yf0 -XOV:GKoe'o/^wDFFWfn -8ݱ ɉ)Z\ɹxf#~2`gWuSq!nH "74w`>`ő<`z*Po1գguΐ!?穋H#o1)g 5k78'*%PbNHj2pWFpJO^6GA0SXb;VF h=Yt՘‡Ob4Q4d1VTGKN,"6ΜF %3a-W3e^\ zr] Ki -/ƺ7EN)(ꦑ6~,VE VSӢRfn~:}t0%GQ Dj[1"FQQb3Q]?h+&@zLYB -,%Mw'Ia$ Q=uYsTB -ݓU g&TQLQLgyiP6Ǝ}]7iR^!FKBᦢ5Jd2Ɲ:qu#U -H)4v-@BN ifSeO>I"Ntl?Us*Oi4K;!ql%1- "%(ѥܹLoSSᕝm( +֌ܪ+sFFWod,QbdB(*dtI$  F`3fP"0 }̼aiටkp=YS‰7-b$'186h^H6 -Gbgy@h <):o^i&망n( -"deA53cK^3C"xfB+DuŅ*MfHV@ cX=dԥ bkug(]ꓚVI`4f !m :Ez8ӿR@LM7P[y=A - D T C׊Vc}jxɠj)BН+e <*%2.Aܖnb;_G韬蓺VɕVv,]ߠwjڵm?cDtp4Gb@ +(•<j"y/R;θ:Q4rS%f6tw"zSv[NC*FJ""9XvZ4OZYե洣ԏ8^/\NMe4c ݲ -X dp> .hۈ~$t$kUeGʀ<K^ԷxQ$d B(^먭ŞBע}*M694O -XΛ -u,_w-/ߢZ {[;=qUZ*Qu஺/[etl mѾQZ7nv`܆EU vY};ڏ %^VDoɻ^taecDX)pÉꮅm3׭8mP d\K 6oѼЋaNt43ҌGS!xzFж^pݴt3zT9BET"C ":] È@:~$@":$:Jz^.8DׁCt|8D8D5lסD0};-^_  $C@"*_ 1CAB~-D *.D*`Pwa*&`P;P P6Cp~` U-.C:Eoha;px( .N8ZH] P"ҷ~~۱tk(M+%X8ag$ ٿQBT'z7[e 'bh3PW$PfOVtE9JF;z␙q@2\!"y҆Dp-T>jhGV,J;#[4oajRAq膂i_-􍚉ick4>ْ`86:~rѴcu{BXX,VR0HP]0OθN*E >eat <~*6Gۑ"ok ԯLpǖǻ*L{U8HJtRFmKr }#B;Ӷ > -|\oa\=y)t3!-m߈7p@y7E:B޵Ҕ=׼{R?G|?Va$T1Oب*Ed\5!soD 4@.3b$nJ{Us8^}]U~9)-ר>M!:4iR#e6ݏiXeiig#[%.hb7Ϡ=[/abrQkG0^3Y'{: Ė: virwb+VUcն$4M?ʼӽP짠<-1$[&mI1_VbFVcF2i@TkʸLŰA[#4S~^Ʀ5u<4O>NJE(پv -s.MCoELIənܶx`;(VLG+qv^BdIkQdX佗ak4Y|X;;iӍ4h|^3\ĺ&qeߩiQfn~:\l(neGQj~ZߊU!n+*Z3Vbk%Iu3f퀴$݇ϢzYTW$Kn_ $\VbE Xnfa}\":*K5*Z z*[^ŕb:Qw3H`P=)I`M]aU +3ryDgQMtz8EECẠ~ -E{,6A2 xA @ɹkAv*«;- dCBDn}'dхt_"1chFZ@*̓dZި\yřLb ----8 "d/]T̴t>PI(v u 4O8oub){4W`Chr)8E h`hK}LZ*hE4i[4gcj#;DKeuk#i[Ni9sR ,i@`-~k9N.mm]+[Tɹ@tWiߣķϩ{@:Kg~LAǴB_y22O8:}UaFE#b)#N( - ,0+\10WsZpyt{˵ jD$}4E} o~߼}wwᗿynovw_^߿~ֿgx۷o^_:7_m]iFϻ/ٛw n޽}qR0Y߾y|YڛgWqn^QпOw_޿./GEQ ɺQ1FF>gg|p4smYn^۬Ù߉|@c5eD!'1ծ뀑V+Fky>٩* ~ y#ogclJ)+J&H4 -f`E -ZT:УRS9@3%O,#/hF1CvU@uyPk%dK0^;:#x#vbΜ%gǤ_YR'$MhAܖFJÒI_G5@T0{7+\Τ7710 0@,n&V 0RUBn>GRW9E&?Y#Њ -lJFIVE [VW}-^pG|L8Mo F&ЃP=c0a`oM }8&\&)x,l'PX&8gc`RIM$h8t8*sȘV<$XzUAtDTTK{K(f@Z`@Nb}Koiٗ.F|ȀGԈ4тk ɳ`KZ9M5XXI3m"3'!Dk:$$'5A:ы;I0ђ#Hs9虙x3$f -TىVl K+nKv b@LjHE# -&4240=#%sM9I $Q,*WNXYI*J GFO%]POH(ߢTseѲWT<0ƬF&v -FB&?iRa}4J[1Ez.GLLĞ=ܻͻqt[ C5>N]d`Q8nvr-[2o5ȓo";fQlk=V1s!&imI*VpdJ11GA[.T¨heg4UȔ(1qK2g ] D`~K+U։SSZcIӚ\ܬu>B|59F$x8X3=,'(SOHc\|(6sZ3ʕqpǒ:Uh!v%,z8)Js [1I9(zdŅvj>i"eې4NRcy ;j@6WIF})Gbc-I * ̜!+(+oe#BC]Ѧ K*`R/}Wq 9W|.<(8"d譙PɕLU+Y/|d^+¬[I,$ےB L -W aҏe - -/AxC!A]U8VwC !oOsᓗCj%\B[.|&HpxQ3t+d`X)dVǩ -4+GNIeΥ3I bDI `xV4HE=sJeg %h>g8H\;nZne3ٙYL4tR9%\UصءIT|>T~>T*YIxYq;gn0+)["|=8:W4nDL_BQI>lC$;w$tд;#/%~ԥbœoG_0;hAr^9\o'“ -QWT &?H8 5`RoF9Hl Iev#Y B\j'ADUÒCTG|z!VXV."=X>w 8Fi$RJ[JW|_poe-v8GO| !v6*,W`-eIMu_HBRg_T]$:߆6P8|!= -IEmla˴Rvg *t-#dD| n1w81ge/$R`i qE8+e^2e[5ע>)~-~(i"^Yш*W#CJ '~L#?ϸKOՀhJu38IL/EQYɭ@E'R]3D"8Y!.=cVL7I/n[s .O99s}?B8AMQ2z݋RYajQ2u?Oύ}$)%M*CE[Br3:[!Q䗩rd(0-.J9*&rcDC +R1cţߑt0w7E%IKMj7؄^hqIj!HBT (M)j@%$q)dFF! TSF&7DaSGT'S= k -!#.ZUzZi)Vy ~TD܄tYx w &:GX~i+;$2d9&Ee'i! VAY)VQAEṑHTjŭQaW]2Ĭ HdffT7G2ydVgVZ*=IHyt 뢹rq=*7 ~&\jYP0ⓖE -j/P݉2L8zt?PsiFOIqK&W'5!4ĥj(YQ( -XվUPdlV@Dw<%ߜbF=EQ30YUJY9A}7 -jO*ijI[9p>;2U%Dc&ƴ0'NˎcyB *¥$ @k[ _K@(w˚fIP},PǼ(gn$:PIc㘊O0gT@t;)-",$U 1=ȗN/m3x6i^dMf., &b HdIЊQHğ5\j"Gs`c~\|P$=Dz8N4jsM$PJq^GY׈Ҡ4ptkO -} -%EEWg8չƩA]xnY!gŐIYW)¨{D*$q F'wc2xL=h+)WXQ/ Gf[4%XCJ損Pa^04yB -3ٙĊL$-8hRdwrzjsKlWMxI)v3d>J3CnYvܼhv 9z|`1m -`N_7y:x5uQO`̚($C#Q&fMΆuEXRQ`L{Y2gI@a!cհѧK-Kdr08OOQ[ptNL]6,J`I#ĕu=ADQL/@^I|ſGc!qʦL{>ggf0.JR]rOԸR]w 5{i, X]ź G+)%k\i"5(&)TJ' P^f/E Qԍ:5Kk5gPﲑFL55[!_DhXW] ă'$^υyt!D8 -YOlOEa)J2jBޘgA^($p$轕dpeՊłd ec9d_ -PHEw21hH#u;(DMv,ٓ-  ;IdgzW{8C@_al=0` eC<+ܟkR<0pg3"L2bgCh49r0GGu'x,Z0y2jLղ=;(fnzӸWl88@-aF`"Z̵I6$py30ܚ8oDXr82Ռ~Zq&nMDt 0ng=ܞq) l  {Fn^ -4Zc[,kl\bI+(5Sgw!9~qpT40c/[FbʞaRE7kg~D8ࡉ1`V aKb%J*c9dм(=L N7"Lbu9%6W`_ -2Ar+Pj#؂\͍i&YS~IY6e mCۨjk!atZ=x9[m5{z`fyv>C',|du2M JOjߑfI b಴^˥ǐ%0~VjA`Xl_nZCu$ (f_Ŝr}A'V e쳄mgB+ |%v1_#NjJم10tfW -'-L#!<؍IMMΪn0ǟ` cu - n-K#!!?FT 4ISiAKXZ^!TztAwJ6:7~:*GCb!1; 8[]>mS*|)겍@ .(W <:; :զ3 -h8qML(=\2)@xYȫ3{!n ؿ? -mD=ߞ+#QZ)N,czA-\7t6lN B{7qes P)|ïMCcK-8>4 34Lh=^Q HW,7)ӣ3?P8 -!FRd% Hi&v * r*Y6zELS "XG}v `ͿMA7R-̫{f4-?BEf/3~bpGhvcPS|]rߘd 2J9|J6 -m!Dr< K9o)ζ !~H㓃6-$ж|PN *>q<QRpQU?9/amK,?hca&ť6htKwO- G -U!|j l_p$7G:j'Rdq! U~~־ Em;np A*&<8'cQiTr[LmG@^J).bf_yXz|+ǞaV'%Sf'\<]1)fv=%LVe%wb$T*돐Cʉ뱉|^@r|,9Ff g*;7;v-n9,Vl(Z5420 2( νD͗8Q)XE4}<ZM7Jh]5y曲71RddYl=yEpw_L!;!N?P Gk6H~)H$(Ңa~=ЋorUO _7t~o }\vS)uIMaSw }҆߇ORޞ @1Bʰ'p PےZ(<A[lg ym <FumI ! ~mm.?pٓ~4袽H 22w΍kDSRꢺP)a戀~>$4B;>P_]{|WG(tϐf(r>Rǜgˎڵko -nSVfNMԴG<qҘ<35\Ҏ]5ۖt0ɰB;/1'YV4%AN$ErrR:u`+R_.5luG&Bv.̯iUsh_jә!f?PzE2U|\WFU:wX#= -ψ5vW=JEאd;3]助Q2ztN+_`}{"}4*T*QГB~_d)봳4Fړ{f) $yضi9ؿ`W@hMZ,[P B0@Z,a$|3Y4st(?~N!$s0 ͵ -ku{aR9'tv5e -K'S>!1=@tPWfl;Mu8Z]oTXó.?@i d30P^6@(AG`Se'.`+V]ˠ^B|,r:ҢcI]^_T6 tzU{9S5L-H AFce5GiH x7)G~9ї{>&0 -?:2˴elX,&6IGt&s۠qJr6:Z$Q6D0N{+X POM#; -g +2㧢Ѓ^Nω*0sʤ>G VrljIfmP%YU, yB h;+bPi,hvC%5x,=V_Kdt5첷f0L6OL:l[Wk"qƼre̋899C!)Bqpu»!^ҵ HAbB?Ww7 lgHH BW3|[(@"UZK!餍m=V̥Wx`p}{]$B~EjQf9!jBt<ފI \=)GW(fKQŲg.+sC?#Du>zoh.0C0i˔ē&鈀¤|3nJ `.GVk>nX9l -@0*'7v ?b՘ᒏZa6`P"̎垔]Ur'2Q ΨWDH B%}C[7c"))C_Fgl9B s^op"T6 XEoIdtĄ N"'L|[R=8;by!MRZLk&}9EoB1ws44&䩡4"t}n U (as!Sf6CeU3<  3ޖ,8aEv~@3ڑ*BDzL%gƟS mA92@E h1tzE-h 6= |^qD*bT`[BDˋM9W._ Cgz#_iYn!T{Z^ =xYahT<RQ{%"'M86P,vKnr-XJTi2sh2Gaɳj͇Gy6N -]l뫜%[U>C 8L޷8d8(s7QF (a6Ķ)2Li(X -G8x^+g+)}ǯxeQ@!= - X{3Y=aYLRIN+v\)3a +i, -MH^ƒd_w)sC~IPLzfW$dm&*n뫜ThN*m6RT)RŠc U>1n=PKG{ah̼r #moaN#Yl粄~ 术I_3gX Xus]|fAJ̗վ -8bʋң9rK֚FHU[O]Ad xހ?7L|' 8e%1`KQ,S -JytwFWr-ԼgT9ǁ77 MVDFO%a=WV8s{9DUpfB)R|N9E.6݊'5&%5*G*عJ pm}jHg/!I̼޸rqޕ9TVr(zD+="F$qG9mFpU,T·T3s1He>w#,fY\5Whߘ ˆYȽ ^(T=z sUU(K5ڟ 0F9RՆ ă蜉p{nakS-V'q˂Iɧ] -o}un`׵\YZHiQסostqRj{6aΟȡ1K mFvʫRBP%D-Ά9 xg - &\[SYg?Q] {I>xu/e|ڱOBɪXq*3o-D:ET]p?BtuG41E,4 Ż}d()#գKv66o -OX@(X8bZgw@C!'AQ{`w+9qVr6%}L -u I)#Eq&GUӒJCxzC>s4fYHx{DdǒԱ p\lwMgn0.PQV<S72=rj륯pTխQd:35k3;wPgRlx _lBGR׼:TbOqZno6A&F?FyP+Ytg3dk5^l4xSy`áͤWbw}5m:OX:If\i,o%6H2_57b;s7Z~C 8 Or;UMg,{2rfc:Sm?VXY0;ܾOX@u2M,6ª3e3xc 9YxAEIA`YZh`aQ+I?exBMZ0d 8ܾ !I{D>2@T:sYvpDvF>"'oz3ι0\2;zc@fC!!.(zUsF)WcќpCG`GRs^9(-J\s -7\%`7Ľ?#O MMV>żyH|+D3Sry,$GyЀ@@ceJ% =5tn+C'P|oUa$4{,g0 t _}8PHG PbΛS$@Ǣ*_A%$I)rmD1׎ʶFe^;^F‘|ȫ|7B<쉀.-- -AQe|(UXjno*I!CuԐEVAd\d[V%$M-V;C <36لdi$s̳8ȅ(ߧ5y- dnѽW "^m1$d,_zDD9Ƕ>4#XhD;uܦ$Ks9MGjToRn_Ds??O?ӿOO?OOO޾ͭ0'Ϸ`<១6`V1g ܪn17Zr7ŭa~|f{ 1S=V!osT^!<!# +I#*6(g}|щ|BȺb+[)>$;fqZvBIUsB0W0HQLslT Rg/?/zKԾ{#'A =fy_ݧo(?[.&ʧ}! 틐09 -a__$Z_ )Y=LٞG}okzJWGz)o#z>HBd|垦2oC?W-O˷<#Iʀ~6Ĵxm]<4O.)s6Wl[KG'xoξH}'Wݻp+Avos.~tqs6=| s~wB~Xryw6gj˓rmŎ"0ۂ} xf;'BOu>=hKPE:t{o/u1#D.;q2]N jendYG!,aq[m L1}QRP.C̲|>-tyahJG8402~~'woy| DQ?% 1QSnduh5OhZRQ9 -+t/ksG55x/FQ6J7lq((Π4~8/moct: .? /d`!_>+/Bnz1uNi[s!˰!Afy[ _~V۶ܱ[Y9nQ2L^ї!#W~G/ݮ5(}O%0"(߲oyX3Đ|C7!Dn4\m&ᔿ NJa !X?YEŒf]"j7Fp;,=/u[{7.OG<[|0.Sy۶jpaEnZOnS -mҝZ<'Y yF~FIDpFżvp<})?m-)e 4fZjOmIm]SlScⴎne$FV厍 +m 4uΧ6~gq!?k,ݚ=Mˈ}?\]T8o/ND۰H|7QA1n{j+5[+53 یfӃKmip7bt-P>Z~rf /jk8Lp҄\vYExI4$WmhNsy fDa _3J.oh B}l2{噿A=͝9E d;9smsFn,ݤDK:fIJ+@mD͉2y!кn2xOX[S2k\<#c[ݕ]Se6JGy)'Pd+S^~ɇ% e 6G.nrx.ܞȔK{Y!_a 2 -(=NZK*#69ON/scu/scdiS^V遼>H,a Q{HlpEJ|0#Ƕ1R.^8|)YnIak4OgA*&-yY v'v>T 7{Aέ.,̂r=*f *Qxdw'`V@ Gƿ棞Km{V.d9+`b|:g2=%Ǜ/BmVC.L{w>zYQ0wŲlg^\ko>"ۇVx:?Qo -c؋1.qZ{Ɓ6C&im\#f>o2/r/l6kqiO{[rp҇R}brE -1mu0~[2Xtч-TbW~ǖ;meq3d-yɟ?R.TIb%ݦgY+ Du}ڻ=5S$Vk&zʴ=]=htoGX,2_޳Oωg*QYJT=oWWGjCUw.iWb Neۻf4>we8&]8MH,W?e9?e5iQ쀄˛+ TtU~1{`_y1h>q žËm^="㵔7Wb/UCl4,U39\UiT 9\}w%Ai:yȵtܭy{k4Frկ*`r0nW,ίle6'=25R(1\ί=`˄bRsˣA|<`amg_֯4E=C/XzHgH hl^HG4ڗW|r􂅜-kg/X@8Yx?` B4Zxsei+?&@L`9윆)96G?{Bd1@x.;}ZVlWe9>+=1}8R@=9u 7Lvί/|< k畈A,l?6zw  Fv]t;AWͨн_ikwY -v| p5!)ެ_(cթm8iƑOW!)Zcێ"Ηe+ۈʲInåi6V$JyWQ\aTBv|WL2'(k#_{ۆӒNEMaU!<`zZÈtz}:ӻ{2N?RC&u^;: #Ӎz ~{֙s>3fX@t:TIo۾B:)"Mڛ i3=oR^ádlz2}4=m:m)sQt;.(^^x~ۈ9ú9)ܛkԏnP0IZGn'6Ed6NқRnflV|->Ufa$.kOe\@ -G/϶-۾/qa։/`<:NBd,#'xrv+R$SwMf[l{lX ;ŶyMeU32l->L2 @1I)ļTn'L#-@n'LP`敍y8acNB&pGSj+Lo|`N8 O4 %ˉd:e9r%I W٥W{s1. wTbP4^xذe9 -G78oKBt^'EmzIF&KXe!II"M4֥"Drm/Me3,ߍ7_3 -=U YD[".s^Z)& 4rS0(50x&6T0)o6JlL('F 0%׷ ﺝ0mcu\ }ZRUn(Pb! ezү oڑN+Ey(4+΀2yxb~E2I3.ٺfSs.3SuVggOOLɟ!cqeC\R)paĔVM o -$ϮgH( ?ζD:oLF@ΗñIb+d64M /ME+P2 RAV49_\2P6ΧexT7CgDɥsiѦ -z>&jkҷϥY}^A -lWKl3K)᩼yXAA a[Wvݎ]fTɱS2:*;-%+]ێvқdDZmsZN$I= &;e2e0˪ƒm7~HS6-&"֛"wɷsmE=MD+ vƞ&މtܖ\GҍJ.ȔN\-"ZULe9%އnC9j=!QZBf%_ml!,Ů o1zಷ}!oٮG+jl]^KxZba9mHx?OBJgߙ ڮ-6< {yeslvO`6"<59K63+.!I͏O#B/%4o#B#FfWq[! B0A)4 $$ +tֈ*H̨k 6/] v E(YUm@{θ/ ׷Dc/6HugSj&-|y ^aDRf_NO -6S/R¿cT ?0hpj4 M͂//>3ı,Wr[y42i!:޼ec/h%ؠoAb7\~}Y2Pg-N:IG!U*@ܯUFg N9nS@!n h?[(voK@Rbv{J6Vy4N/G@j,#@@L:6}s8ZFrT0EX`Cɂ2{8;# Pn'@`բ\ޜgo'@ I/Hhu9N`u(*< r: ]oF|Vumu'o6J5g/@Q9yWmHr rU9&4QQx31&01ڪ Г) -9<t4)}gJ A+y6q2^~@E6䜁J})"ڰzO?(Z[h05WEWRX|xsӇĐv@vP@;|y\S9( -v3}hw!\y;y:RpiwG-FQ$>AiO; f|8@iy -6QdDZ$]w']ZsIߑ{Q j - ݟ&xv~ q(/jESSyQrlx}7.?."R7Z}EsD}iI҆@`?w7ට׋93t{GMmO?qJ=nj㯚E7Zy_(۝gYj۪f+}T $J,cۏoޛ MpWX) oqs]p| -TR͎%^=M]oӷjo U.7e樟ooTŀYl_pܺ6o&n@nﶞBr[O6аsCZicWm6~UDF6[=BhZ=՚^vTXH-g٦ZG.-u3 ,Y=TDht7|V%73sdKY[|7+Wnqq --j~0>f{ːYe=O2U5[ɲu͒l˹n'XdU1a2C/Tebʝ0@N|l+14(=djzui%,`}v{ ߬EEeU']:#ܼBb4꛻WE( k#"#Miu)vBC/.F/O׹eP()#pcqAW͸X.@)%C!&.,4@X@֗-BZs`xi/-c%gemLlY2-i\Klr/*y^f(zx]lvbwU,Р8C+xcj∐J۪|UQe"}}2@x.8D3m? :{[yOd!0"b{MEvh[L)W7g)f!5Mּ}G4 -uh&5ET!KT,~_O\.m51ށAhcmtiH/z;;QKcۆN^}@Q/x7 vX˵)̄GT&Md/6-O'}l0n+jmT5(l>=mԗrA z8 Qu58IuP]hI5_&H k28tFw[ roH*1;Lo~Gcin#SFdRK{352)_2Ůuys.b۱h@2*%џhE R yTߨ>i:@n!͆d 7!Tr+ng! /C!1~&okj>]Igz8 Yѐ,H0bϗO?Yv?Âm|4Rz=}ˋGOKZ%6={d,_~(K Ӟ/ӕCǻʷ$H/?x4r4inx켎N[r_%,O|,Lto wNQN 4,s/xѯq((۝S6))#oޗ Zownb@6WgDyXp=/7e[S`ӭ8c!LjJ -7MA'K( ۉ3_1 h_2) :Y%,iC:Oq7D/G]_Nb=@ $k5sכ"D"ߌ\EK /&46v'[xfc䑊chsi2JLI:5e99K)`9ˁS vOj3yӰ1Cx\DB( <ޗ`zb^<ʄc*fZGѹ@P0!Ӛ+5+ v:{_P;>whK90%ud`]թO.ѩa"ۊJ - LHxNb0,sjnWEaV֜9)DtM(6gQ{*hK4{U6^DI-1JصME˯sN -V4C}H~s#W0h.sZD&?B:?UXiZ m0!NBwcPv@ -TbOd[3FՊ+=DRי+u3Ϝ[)w@3x8JhL-hZG(uA) x%f }ZQKѹa7 Cjb.uv4v[XVb:R X| 8Eu˅WS͜7Mm:B(:7RfYYWG:gxsc¢Y@vM}*0^Fo -# ܄zMъYV!:{ac=ؗ5}y9eS,v"88@flf"jQ̕ED,Cp0dZђmM} K\qs:A"LeY5̶ׁs8ey+¥oq$Ґl;AdMwU~j+l.фeuVQ1ΐ–*1Ѓ,: 5$M̼ayhSZA)ex$@V9PGCc.huzE^8P0L8Or7A|ʯ͸ (/z7=m+J8U Ĥ@:|=20UNXD;e)+SkPg/ΡUE³Nyuy(dqI#z 7Efb'ZKN}K\j*h@>7,-k -.E[Afx^nFVQIh86|ƟcyϲR6icTPqtaijI#zkVRsa=?Cʤ%8L\{47T6^qj}h"JM`V%qmN=$@F!DFK%hT?$x!P1W^Sy/t b -BZB6 36tB(qj_JϑG/B'e~5~+եL%NglfHtng[̙6h>8ה)3a'ё0#z!'Luѷ)';f[4 uTʪL -&E"1@Ys'8˹B䮴jk_]%W_6J IRZ,u B&9 V5Wp9K[[[ZFVs ۩]M{mP?4|yI"lCN~|Jo}Q|S}hb(?y,1h]aŋ M!wH:PJq'!He1vumel~MO!=!?ΑqKGK -3 zOPBpc'Oj%`E6!h2&Q&ufv"b;ח]*\HhEi6v;z=[z-9#v-" -%MGZ`v;)T <k"AZT)}R5%gD (T!!=c_=fdpWBFD-݅4R0;J3J``\IV3km{e?Xu6WĄi TLQ-?SH_ˢDmEW2&,ނA.clӌӹ]p=Ѱ1Bz&AΑ ]!2yg];\Hy> s/ ø%46©hbj2.޾9l5-tZ7g:(q*m:VBYw}.C,'3D u6kSaT<(Y~2RC&ZI!s* 3G%Vi dԔC_5=z?ap*:=b_ PH& c:NmɽT`$ǖ *3maWBѰ &f)W̯F nyh8$Ա!k)la iFMkG#$H4VpTL{׼RRlPA ӎe8ڭ;ؼĚ'5ch5WL^{̢3|XD: X {bY-[|ܱm9.3 }cFԂ5$[bO8J 2챾 6DZsrQFNa)KfeN>SLLV6^9Q4"9Kf$-ܑkxaWk,uv e%Ѳm#9sY229dd< 5va͹#d@;זfe4{ uWIk_!}E)oV3!ORfFx \S) S/"yORRcjY;0*fDB (&6:%!G~/۹ 2g~%LIۿ ,1 Y^.Y:A+{/$HvX>ƕ9^AR,(Yβ! =5*݈vrɫ5i1nmeц̝Cns  ➿|ϯVZ`2~ o^LjQl"  Z+PG;6{F{ߔy=n,ߔs(_,uك,% 2as+P54$1Odkc#iIwntLىTsnCr!+>-,]r@!)\ -^C19+lIoy -4FDbe =;~[pp5WaBqrd (0]e/~1vm^젅@'U G8fSpud< tC- xm~l\E_#@ 8tSFaD/3vqaȸBp%36 J(m瘘q̰G6%|o60RkFJX,wfڢ /x N#;ٗ>qz<=J Kݒ9ߏoiA8CMuW.^{Q/YMsޅl~Y/˒# $Jit65Sև M(F:-1z9 EQ(cH&4Cl~\; -bP~lhґ Z#;[ ͊pm/,:%f'4h5H͋G,NC:l؇(5ۙFh -Bj hP3N -dM#/P\p7DHq F +4| gJyk52=c -{n=qXF$_q[V(Ʀ@e}CUz =PitSAfɴ ]MzT4=ۻvMM|)i`XXIc9!r;Iٱ\RfWt*_Q#-`m` 6ъ]W?.HNF:='K,M{=1b|FiLzhRSئxm`uyΦ~#d8֍yhi)Z2(.kTˏ/IcXU]pz:uiP/\b8WerP\bs:L!;ju?֒Qb2~),h|K:-IoX6Vu @)YhjXX, e8 8;Nbߊj"^pCH4\案,Z%:/dXu몐C.:G2J/BlQ GhXbAE@ OD\?`FsK_` ̀<"v=pzQKᕗ/BI4Vt6qL5?P`[SwҔ~PͰ-]%xLQ.-]UBҘq*SE I& -q IqԍzCPt+eBSABOz!yI/D"]R)pQ;Y AFRW  @mHGcc5RH|hv }9KIN0nA-دVH K },x8y)\&I]3\.g& wDD`;aiL; -mR!gotF:U DZؼ:;˾#[_sgb@.L?DCbK43o/]jU&ӒHzﲂ>?=Y+#dK/rx+ ,^k=_,JP4g%hS_%HMxU ഗgfIPIЃJ8\x̀?mq?̺۫x[ul7:2߯T -Bn/\qgXSpҁ ܄[ 4{|?M0"q^F 抌8{^%f'}QiXom=0缊ױ,o=b%zr̀ܶC-S "=ܨ5yhaJ/ҕEbd>A5{tC.rudp 뛨:Q.]ak]ݤU^Wh/Iy`^#(8yr-X06񎖾[`YK"LW*X7k/-%A(` -3@ pa3a.@${.0N {W6Q:4{B8Gyd&.Z!hEdE "<-5)D%jAL/rU ^b9" ߏb`T)o,/r:uDƽvp`0-9 elO%=Q)@DQ9o]2ywz)1q'9I`CX#C45JFklm]N$:'^W ]G#&p6zJl*#??Jwp! wHA ¹sZ'B8x}=HkrDԋpo) '6'Q*J@'r2X - -g{BHkKi&-Ez\ %@O9GӤ}5>>}C=qu0ˁ_wwr6EqTjD;=:7nmai_uVi9Ic|ǜH0QF~T&BqW丹KCCȏ1ѵ6<%;>6Sb#B pf:IΜp}A⇸"#X=9/9Rt D19M=Q\+,w(M@mr՜דșRS$\;I-zkpTqiVZ?|+{ ڱv"@`ZPߤ"[yAt8#H¤HU\{}G\R&aaVYRBNN_jо5KKUo?k) 3a;֩6{"ͽZեo9X}N+)$gϺ:(>ǽWJ odQDe-!ʽ(g&8mW@2zIǸkQemG* {=E8k\a9L J 4JZCf_xxHbפpQl8Kqe@}eO&lc= -fsii1v)\ 'ÿ^KSD'g,pD%CP A:"DRo`g{QuJzj*9]05ԃ i.2 "|\ 6+s=<sT3cM4rHc+Ňnؕ;jdRZnn B[a$ʪTwJToo~cZ;|Qfp998+Cru/F|"P 4lt+ 5Ab|Mj7RRhg!/7|]dr\AQSjo<?mޚXGdUd~N~]rpQb?¬M5ucIw)7f^1yga\oq {Ѵi{c _"[Kw=PC6* -x=1F_CvcRhd-gT'jm$+9/SH_W9ToĂVB -2 /`0@xTJxgTH/ϠLC4}V0 $$kX0\,ZXWk %I!@LwEHP\η>LK٫uv5x|B05Nޏ[4`BDIL9^0t -?dd4d|n:DtN߱q-GZN)HTk) W04JU* fZ 﫻Mֱ8sVu5O5-Dzᣌ(vP9)T6Tx.'/-/w$`ok՝Pv0(0^!uƂi -zWi+&## mQ2  S8=b8 m؅3@r^ŝ]=䒣$:Q׾@^014~]ԃDi l]%'U`0I,#;uG=r%w|0[t@'G*w63OuZpʁhQCW -> ԡ3˭l7I|m -JT ) )F^dEIRU=b9_EJ#C1#EzөqB(Hs9ԢMΔ(%7nzF+qbr6b؂Mλ0 !܊@ɍT:/M -ra5qQ9!J)7ԫHwSe{\g* gIᠺK`f(MH~ KT* e~ ı FӔ@ !Bg^& -ux5x EZ xY#4!A,# |"u5n#7 JAU}r)^[xbU=13e -OJCDA BG8Z;0J(N5䝖9b-'n;Jj׫# ^Fw"ʾ^%SsX)HW=lTS[D9Pt2 D2Ca"јiK&Fg`])qLKVa!%u l){$9.`1b sߥPx!(=ٗ#*`זej1&0Շ"PA:ɘOjgTϱ%BSEےװc#PΌSdlb|?eV&NƣC̽S#(F7YyQ3be={(0JmQ9+_\,UkZptw2l#NNXPcՄ72.Nw;[;)?+aQKHAWO=.fJ Xcy]2)/e !$2֯$gX4P/28 +\Ӎ{U0]n8Sݏ *IT'bS,C#ߢ =lm5t=RO5 [=8xK n\F[Z`(o%o+=|q,Ӏ7~1n:Y 3ڵ;@(݋~U0Fc.6@1UXfa9a(@>p w^oD&bp2ɋ.qc&ˢkZ)INV#MuU7(XÅ#NтH~D{Rr2֠ghjկd(qP 1@N1:m6U[2=닿c)s!ș(rw -4yeMrj5@qhcP/Pj(!C.SI*\d 4oe.PسW,݆ɘ%V=epnWg"2+p 9j\eD!i5ӞVP#z JT -5vE4Lft{V,cnwE* 6l9#SSh9$VXC\b) ۪`Y[/Yu 9YA\šnb'jI h/i/6D@iJC@%Z=VĉæK?{CWC` W\=(G%B[JI;9UzٰH҈[bInh`Mw;ȬyUK͓ ۸p^-.+D|JINw !жSʝuZ\1@yc!M2 -xطh^wCe [= -ɏW)YMDRyD$Ujsn$y.f߱Y/f&`xq-dӫ|sM`\ u +P{''"@l'D m -L"ќ mاEm=NFI -w(!Mj챙}ǜc-~SX@ܯrN9f;ЃPF/[vHlzmj܉`v<´B'Tdz s{b/K~'qwsT!]OL~2#eȓ v"Kb^LOF(wF H-Tԋ<$F؂ĽĴ\Pn)e}S jX2|()F'`2B{ /&O*k\6cͻ-Rpa+6K*lIQ\"2BSV^bi(aϳ% -M\V)!d!B'h|ԍ(B -,MQִ*R4!M,K x !rH<$@ H Li"S4zc9);{ړ]\0?])%{}ZIa9w@j 잤"v* Xl[B}!֦^uGT77hœޙ%ǜ2n QW7)7Ȏ,5mpa\~EOxzOM1gfFL]b$d`ثN[|[3LA5Pkcdh SDʂ6L]PGp rZu"9@^M A!Wuu1EPI7" Hc`Tv2X6&c¸ė-5Gf{%S=AUrKҰ5X-vlrRu*zH -e_iZ0{ -;kAje_) 'E\3P3q7BzJo4K[k)hk3$':oIQ(!r\!:!L[1^x\1@ -M!"(cT|˱H {#K=?Cz~I%Yy„f+K<lyC< Z쁎M+oLjFSWU~!+].TrZaՇJ79+A-Rryf4?!R)S:c'"fllxmBb!بÉe4("rRt.D1rG}$?_uYgKH:sSIpfBV҄5[e!d^L1`6^J̛,n->.=X=e10&Rg>b--`|;`>40VIA(KHݣ+1iڥWWy -+Ib˩;B}Jh;O/ i~eRBdrR8cHA=X$4Y\L>("D7y+.mT>,Hނ<$#ju{"eɷp`a%׌ƜAOD7=X&޹n$O.qb{؃D iRoRl6Cկ @YU.`Qd6`{e""R>AiWsXAm2^D!9jrZ@&QHx|`7%_Jx(z)~,ȭ>vw5{Vv|s8`Q4h[lO[A( dO_ijFA^΃+2݋Mu5]D endstream endobj 26 0 obj <>stream -hFux(cŻ,ыqyh -.GQSC -ѫ!dPPfD2ӍWUЈw[H8K{hzH#0'V%s_DnQ|?$D *#U..yr(mȺּqmvU~WMfd)~u[%ggKe$pKN!p1Wﲩu͎>?ţD^T_ߟߨ>_#Mn1أ#ܤ#0sJ\ -Tct9\\"$H"=lRW| 9 iŃS] lB*.=R|>U=h \<pᤗLxt!\>GF$ H/$b51h3Ov~?`Co9K7-,K"j5w])r#[IC ~DS[Esx#U1tuEg:ԁsEzv` -d] n*T_:Je)CI[uŅeZ5KZ"@R]ZVw{NhA:tW?(r9$ӂ7y^MӖKboMh -v9ٛ+?M)dD_uO瑒90{q8v_m#DdӤf"qdTPv\ud4|*b/K=O769X7+!m)M0%)$Ԭ~xuٮPOz ~c"8h; Y%l&f;G6lI{~hb iyCuKۖ"`{u c:6|*PK!tʧ !2O&tUwa7Cߣ n &QUa*3w$W߯XlmA -i;=&GaށRµ%܊%ރfGjǯ5} Xa@vuS +ᐰ1PWy-_C}7t`TdҘn6#-#*2^Z=qlȿi3砺L!OfA -͍M-8ŐKƍ)I;JΑ}\.=]տE~+4PKUWmpqKAJD'Cu86 ^J'Vq}bH0RVXGJy -{L-,;B/;@=W3^RMKy1Mצ* ۏgGoT/9lFNR E]RWWЯ-S@}Џ(;b!g)YéԜ)t&5?o7ﳐ̘jL%I-,űw,>ʗF+@s-I3iLŎ܂Xmܾpąo<(ry>v>gqwv2P:z,)v\Cn%n&܏A@Ra;6qSu Bo6 _ʤp5}NnpGaYInvA@Φ9_hh7!Hmz($*PP|) MޏHua]o/NS@BmdvK/E) -yu^඗覙 .p0ir0dUzQ`(lY; 1x#p63Y4]0ʤnId̃+23ҟ)*e~j6꯳k)?s3Ik2ih{5~'Z;F#Ě:/ɭ2ЇkAɟ!g'wG[/d[^ 23[-%} ȗHKݰ_ -~)>Ct<8 jΰ(H*ֶv͎vP$G.d~UWKc -|? -oHDiQ`)Y_awxupKTff>">B5 8Ԝ.ݗFcQ>`EjR; Bg1|RmCe!Vodz=$.a)lm#(RRA7s^vۄi/9X -)׋ͬ`MA5}8=  [/4`C* )_K)ч4^a@7Wvܱg\J䙺@a=[m 6Z|pE0X,k2YԞ&Gi\?.;|vX[x= -E1_ :Pv:k;W/$M;~ZF"E K嶄5;vaCTn5WXA] m$hv 95B@\"pm{ldEA{0-%C6GwĞՕ؏:\-Q!P~`x6<QRaD6AηgZ$x0Ym]>p :X2X+y5*Uռb9IcR B&'QtbTj{PQi0 CH]I+Ps= ˫!VYby˓{\7Ce )!9P?ڟ&ܾv4&i.&'aY5*,o2cBC,ޛ_ғ<Òk\хԀّV H6+Lb{*S}G-I"SjztSM2yR=Xa?cbg#/l$ow\crb!I5޽%xL^e(DBJsUGoF2ILn]譒6%'݃\}q$UCυ(RYdKMU"!s|PfBC+UQJi#_7T 'ЎIV :Rm^\ujiKCV+|q$e4aaAY#;pipR=Hh3z-;=YZ+I YK(uڊHd¼١o"&aQ ڑk!@ҟMCT "qpgG.7G2Zz`o ;OK9Apj?19-iP={ 0+|HQwP9o3y`Ҏ wz *xWS;]w(ˠr.zڦ ^+&} OP%q9@IPE,Nרw4硦I1S@v`BbRn%Z@0'эHCaf`XƢ_D*?x{(j~27 121X$3 O*zQBHrPL(Ӑm p |Zo2i(-qgd`=r$&E,ɉQS{%P[(#N7l F!գ bbJ!F携ZW* VŒ#k80\H!Q%8L4ນ\ 47K+헓:P%_ZPLg)YVݸ}o#YN@O5U"w(9=RAdO5zF1n>I-,T!3B76cp<@!5Y{/H4/z}I&b鹯jI-Q2*ôH8KJ}B8= k, Yl"ZCa2Lw[r~"!2nfj)-5HcJg]m{u&恹;Q,fڹ`-~gb+/C&L]r~‘ʕP M܄eSU=CkC{oT\J%U(d!aIn;W/9(W{Kh;x!# #\W(C0Kþ#sYSp@hc"/P۪8.;)&W\CE(l%9 -*sEKV3Q).I/i -|ĥdlVFf6\n=|Gy'45%tX;wnb$(.&:n *r1U"K%!H.^*ݳac?]1N#29NҼ‰"/!ds4unxr A T:@mAv"{.tkuMUP(2~oq؂ 5J:jVgH4xDlSĞQ۸Uسt>%(SIJÍ6O\(&BZaE @3RzĮ")CbJS6ݸ?`*jJJ#䀷 -̻t}7݁ᑯ-xɺsUlw  HX a?=7z-#jnFC,0 -8A`b0G`K/R1)w\Sy>K -bwd~k[ Pq VyAt1$DHR}Pcn⒟j@[3w,)S3-۪ʑ!?qNh@=zP̶ |OgVKa/G-D^ 9~8?ؕO x*L+(&q5X'wU_R:(G(5NJ9Yt@?~il?ǵj`I2XZ>YLoaKPpo&EK{\$鶷Q;<4!^OEF?_)0ՒK"XU~HsEgnpv! Gq nlWµK`n ܠ|Yg[J+0JkP 5{lDM_%]7<صʡ8R߃b #+h\Sbc}ΕN7،Y%n({s.Bݷ2L@}v0 `J%$1fACfP1 Qo$ofk9KR ̠ab'i>eiq>l04k7GȎ~pwh/_.,{sʛ4Y 鰽~vAq~9'`wn%U E%$#mw!q>V вO8WLE (\,?!KdƁ/Y+A OZͨ~([ -XͣJF ePlIHC()PV>}ciuT -ʉ.1&t 'Lɴ-Ety/$Y@`k?R=T~( ,TmѪʯF{Ԭk&5T~+*$upqX+|G-CY"G3w/_d!/PɄr9m'2G -B)1aj^($ !@*%(OGWFL[RM-;=J TD2zh,|y -/P+Hx!v`^cAЀ%P[#Ģg 36:S9Fpa7Xo ' 0ako%m<\C=;ƃ6krƬ 񊬪0C301N<-}|<(RR+~!Ȇ렰"o:5K<$K:0FEBAA}^xÍW|@L`3RÓBG8_v-> ^ H4=3(S$D롪fHI6Nľ_^OOWDvA2ܑlх9!A'*UGqF^{C!b{ϩnRlCI9$րfy3Bl!E)M;@iiD$`S0vr-I@ek2lBs4^%xUBRd,VjP·\$N.K-2Oe-VB (XHBH9_ag8_[5M՞ȤP6ܞe-!xjI m^uMN52Q΋%NB9$>㼬+*jCN/K^IW g( - lO͌(e \l<}K.&e' wqx{`uJh|5ǶTK?6/0]KMrv$1` 7idx -Ri/}R fA噀.sDX0(&uґwMhb,F̻yF s4A:ɥ 1GJC6{ 2ʜ!Ez6Kꏑ:v -͚d*fk/p#0m *GdjF*%({Q4 8Ƶ[k,v* t٦â̧ol^` CBTc|Wā:`ԉ^s0fߗFj(0j!дD420PR&YxOC@jIz4@"i;Iդg-7R(HrI逮 N QOSULlj<%!t_@N$@('7d_;=f&T*%Ca%E%SXE匨LSvn_ImkC?.m. ~X?`Z\O -^t|v%ugK*k8#s tt] -Rl䰊 _CV8Cᤴ:h%qu?__0CQZ$MwV?љւE{їn@ޥyb݈.rDPc3mwܢfT>A)dk0/G{29Wܷ&n= -ZhT"ہLOszqe_Y/  *ɰ-GPQwݾ;9HĪ7 X9DjՔ훹2̓¨I\sl<.Y,w"{ΗS -ؿ\/r] XROjd:4*pxu}TQϢ@א\-G^ bCXBô8L) ;(,\5вw!`o^i,u(eB +ZԂ'"Ԙd n g$̂ȄDP;17-RJYz$1D,~%Fi⑭A]3ܡ?7T/_1$"98*⮴_r{_WK88D@J`+x?gWs\߯YPbCYv~l'mˢ$D]JCR\RN -xmsG"IC%v7-Edဧd$ ʟO|eH%(g9";A}$f;dogDo"{ߐ$ -T Lq?DG६׶(#%}î_UF=jo?K#kELlL@>T@# -1{Ű{I|ҫ1͆Uң J0jΣF!Tk|(Š$RLg͈7 zS=*$u*@ %էiu䡁]ڋ I6(SY)b;w<|3pHW cU0&9~H2FLTLFTZ)c'M{HHm=+u!? $J$"f&G 6>[w? !Q\QE')TX^7pHz;@w[$4Jr{ŷ6wrMk-HE'- )Z+)nRf^Zj5ɠ%bFr:L :if=-Xޛ$NQ`?uu?j֎ $Z,42t`M@zz}U]BeNG1vmVU=ˣoI,4?F"uW?5@ ],='8zcbN'}}YM!u]8SVqF\Z21DQ a^IP; WfX9SʋI1ō`ygǐ=U|{x;gx9铧- -)A&Uv4;+I2\a9N|q I@de]y]VnWI ' ^쾉gtfDZ)Vċ!Lwzln -[gѐT QAf@I(E_+,9=q3K΀87zU_ C1א%9i ʁX+/2뼔'cO]xŘM,*)tqV%&0ij 2qd4?3Ե kPޯL}~34+S`v -ȝYRl4w4% ySG%uz+߁҆W'1 q>n\Qaر'E8◙,G2=x5C"`_qeD~IK"fm)'5}n'5kl#e^tˏJ0ebŧG<.F8(]!3u<yF Ug.FN܆]16RV -@V빃.+H({T;f$[Pi/]&$U׮eU-#c J'v@T2XPviQ4z &=B} ʋ Q﷟M-Il?$A޹.C{T>85^F//Vvl&Tgs&E6 -!]Ԁ!Kn1%>?^.O4ChcƙtOaYÁɦ6et,S.%Bd剅ap~$,ݻdf"aGIP"w-< -Vk/MKBN@"s:T}ܕLS-faG~IGl8д$yi~~r -ݓVN6c{om~اzXf?dT&/R.b_%~5EZ`FSUbp;'q(uɲ,~TTo2MA-$DrX{,\Ҝ8-9}%lCfڙ%}z8:OboG$2]ΐd'A>Ȅjp`aT8b[6|`َMA;> -ET^W?юs]g'P~0qoaGꦀlRA3`IV|Fp,N<J!HshNDjͰRWj:cJEyHt$Ԋ"?ïMe ~3Lb PM{ -E]<5R幈ZSQՖwJXsИe|[cI(X tJ5'CAfPP [l, 3a,C Q| A9'%6-C0lM!YQ}QM7vU׳xcX}u9IbKNtոTW+A}áA2TA* ,Pbf`w/r֪̒\/B++ꥃځ$,[rR#xlIk)gmӞ |6 O#~ 8CGf&X*i-Hb4{fLM3H|zij3ّ1CaT-'DnǛ|c"][+?2N`HOt ץ3ќʆk4%MB*7֛} Jpy'jyn$sf*JQ a!jz1n r@3 &H q@$@3ULiS3ڗ/um_K-ݟXi{ -]+nah2A8t$[en2 BK%.kg(PIӁn*_xEұ W )?uɶCZdJu|^(8fQwGO%bR 2Qe'4)Chv,ě/C( C mbp @"~J%EMHctKTxhxN`dC -Hpٽ QW`Kع~¥ƾj!=fZDKOLY88ntqZ9m-knj^%L_vH) Rv$~_ʗ ׀ޖT@KL_ho/<%XE> f埭[M)b]LnC@ 7"AdžbqXjv|['6i1"qvQK+2˦ -BV -40[ J# 0 .t:ܫ)s Jt1&}ZuIfD*K)ME4>b&/*'☣lX
s.5pu;Y1R[y\{dV\0- 1:MPma~{Mo Y_`V$mɇp -+f]uײc ,"tPYM]Yʬn@y_:ph{]1J`cC!ֽ+J5ʒ{62@+`F;x@ۙ+6"!U@eԲdw -.;m^K:VJev|8{iA=_̣jxD,j)[KeIG67L-L3nޑBr`&SRW>`bnb'ThZq:cJ])(-PtIqn_Ot:DX)pMLF83iVj(oj}"qBL"6(+)V.aDW(h^TIB JډO~bcng#ܔrhT~]j|A({ڭ #Nb*.w2ETͨy<ɳnNbĊNmOVj=w֞9KUu+hjʅ`J⬆yUR@E$V<|nV/•Ϋ\Xu2[O8=@uiɆ}Vjp'RCRFXkﰠ"m%IQ-W("Qa -=G}:bEWa"n#2+5x!Kxfs?r9#:ň*Bʴ~0Ywlazx d<f[k9YV!F 8nq:@BogwT/ovsAK ʣ*mH& T>/FR|0bH{:Wߵry9S"s%/:()uK*dtg-W&=W`}̭r ܦҥ| 8J( ̏$R -$u,u^]yB}jB(b!Tn[} xBz5zX.ϸ* -CNőudY~"A܎[]iGدs:Dwأ &[eͶ@|=>h<;8^W0]ቼ9h(Qm()9 BEut&OCrDp_kF&L/I6S4eDz-+nK;o[)N3)o?Oo7o?7?~W~?wo_ =~'w?t}O_?n7ww?[j~|lS~ x͇WW귿Sο?ovʻ̑#㷿m_\oRq+/u{ko_W_6Ibq"~{!yYb 퇓L9o_$pw rIXZEYu%d;D!KG Zį Qû{^_}! ;=SE#*j|э7q|ظ||~su`HsXq*dyr#d ~ -wGJ?uBqʛ~pRqNtsx`3wD9~FWϗ|X%H )8,*eZ=/'` ^ |WC+C4ƙ 58A8-?=eh pv{jJ4x~'􍞍D<_/b9وN4ӈ[ st:<&3>Ύ+ GZ< -2ʖ?O+h\IiΩ|D4q ĝz En. Fˢq8X7y_ Qx^xb+U9+=#-uij^ -NPO5Ϗw'xyibYqiTUNSW [6^> k|ncw^A=;~f<8Wk'1{`jqĸrN %Kgh1C[ _ @U>0٫zvdU/55daL373Zk;ss;I?}ICco|>b 9<+9 {QU+LDi1֕8o~ξ1()w^A`ިE^k\"ƙOA I:o=!'1 5rN< -HٯUO*P\A1&12\3H=X8i+x)ΉFE[#*M& '8sŁ3h( _7`+3L76s>s\?)zXqa ~W]^}bm6_m{%q8CWϛw(;_w!T4kFعz7tsyi U||^_1c8|e79yL%zs#'qS4"2v(_ -ԝ<'ڮ◰;F8"xHD#hl2cѲohDcobfSkzij'#P_~- j)Y{Uc_?.׻95]9pz/aM4fј>l'YZNWl n9TpǷ &kjmB9mnbcl7;UF9|PhuA#)ϙJ?gs=ƹvD}'%WnV$[h9.7K73iO+4W+rNGNHŧ%z\ֽ\g >}DJ!oaBܾeEh^ y)϶ʟKcȠdg}|%ϔZ(4휠?DdPN^s;>2N$˒["ª -p=k}+ɸ|\@zyظgA)9a;)ۻ:2=zyDL]2,vO(R;A:Cɷ6X4^H_2|1;J j>ypc.o$lONPy邲3crx3{Oc ly/WS (@dڟ/R&V5VFDU+bIgX=x_9Zjفb~LNPW_$lFd/ +˵w<Ӳxv}ˍٜ#yVmą P"n#{yEL5q~yYk[Wl'Z։FLJAԒnrc;  LJ}/ o :^FMgcN4QŤu Z8YryLEWR6"쨈O{u裝v{sWzYcjGXep4SF;\Nl (l|V;.kn,R\h#$(|HgW}G'oyZ)g:kW{[FI)_"N ]9 u-f|6.,ˏ##kq.9/,CS̬S`؞}0jMAK)Ol<)೾ )O}`F㨙.( Y#f>1`?_؅Iگ^Y9аcOQߥl_!+O سc|2Tc\]40Yj%'w;6~4`AP':nZ7's4 oF{k kE.@Qys^9zoyhfSoQŬ}YKɾbFEۈ)NrSvO3hgnH0/|x9@try9g/4ڍ̜j PU8H3 -"w*iiVk.0x^iyȭ-?+<IW ֿ udew]q[/йfc;1o}v6%>69dN(<V{'n7{?gsԅGE˷<$fGg3}Ŵg@9끷wqs20%{w ެZb09cV-.(?tVghgKT!l}i9`PxR,w*;:+#y fF+y#X NNsͧp+Ϲ9|Z<\$UN4A -E!jYES̩2} |FvL4=b :Hrn<]½+u/ZS0YFZDQ$i8MMjq_Q}QO3S O ߍQB30\R n(23 W9\!ٕ-͑''Fz]}9^տSA6'"#|2EĉM Oj'gE*S/AK$ s-u]gA3O\IchHzWc@X,, u?_1\jL +I뙪{}q%ڕtu2 d}ؕ29ۙ E=\{բmmfX=AC_jG^W~r'/F=MOLyb1Ȭ+skM*CaO姜p9}(g5WR͚+Y65nq"QQ x61Ո|JN=*U3{]~K&֥ḽTc04#y۽1+* f>r\×8՜F>RǞC^{ԨOw{yaRȑ?@9hZjvqW't[w#ժ'rC7.~k;f6g:A ΓBVoD=d:6sF98"%D.p?D\ +]Ɖ#e[3.C|I:Ʈ+{*h泍ƽQ8th;״6E|PYON䏽ڕC~y+ZY]m4WΗyϼvzwkƔ됕ƉhȀI 2xGh\'i9UvrOj^4]zw8uZyH}hHa_sU6aq'+\'x?^l_'ONWlɵ5#XrB9ih~h;Nq1HcɞP^+dv6$m}ngD~W"l/0'Oczʅ|$5?4+! :)I,#",s=n\+aW,ܫHcgOeZ>=PD5j)ܠOԺѳא~=TCKz -}y·8A + P܋EΠo=_ש-@ -ٶg˙ IS,9U;(OkYN)2w,lA9ܘ;1NPj\s'fAcs̼HuhM:9oL)A=._g}sIm3K$lLSBi<2=|&~>|6TI_˾9U3D3xwBA8T!c 8HrYgx'ȒCwhl9fkJn+?8T!dt G'xmLJ߄un5+$I.! JN|sإ;'י<",d*?nwוdW䥮@IY=SW*~V3U*V ^~™2E. '*]#V+N0F6gV&g Ȱd]\tN%#2ΚJ~I -/bH렽ƻhH o 'ȓH}}MۜąF` rRdJ4bCWT8 n|YpvE/p, GCu͚[Yя9Uצy]OQPߣ,_29I9=iNk Npbe r"^'Z/vئ Q, -\g'H(t'yo -/z_ -A{LXQ'xr^{E?6f}*nqZJ^؍xa  |r'YVߣ3G}#>gBIh*}۩(KK!U5qGWő4o{.FrqEF].~nwg=@ٞ߯@RZjU,|UBL^:xU[qH__xc9id"dMBF)F}=*A@?(Sc- "*S}U5jQI25ā*&f"kRn,lӇb6P0fj`>@(5 * -~Wf*Oz@fߧ -O IH _7pZpZh) G_JW7NЫfd~:8;X;L4d2+ +LK^„_VPL;XU$Fը)bս 4M=8D|XK*v fP( J&0cUQdkZDb*fPPGG |jQTi5UL#`HhBBx*2x_}ʄ #U23o /w"/f&%sՍYCxS58F(Xg*KU6 (lõTTW . n6|@M2vBTLQ4zv -TW9a&bh( -3&S/㭎Û75$DfVi FJeB 蘚è5jd-Jn͸QFFRME]Z -ex U9 J -h'PUɍLj,rtu5Hkh F 5h5D苾T%-S3S5k`5ve*mIlR9Iwv3hоf q/՗]ƒT`WyMՁpLP_k*bGa]8AZ6iz&Qo&̔ %UFPGIQo ʽp/!/S5LT(' e* 2T2S~Ѓ$Tc\Bi -EhJ ! -,[+z.*k[Ruؾ-̭>T:a+YpH d - F}K-?=q]jR`5)tF33C͔n ʣ(XQjl,\+SF_ƚ1|Ш+o'eve`"RTsvezFaߪۨ:0cpMf+L]E>*؎u[eb(rYF!,E3]δj/Q|#Fd⪌j8b)^UVt1& -jjbҗo~0R\Hc.M|^hNdF8\}3HbY$j P4ZC3bQ1M57>AmJE$K4ePUK#-S5 -)@ß;Дǀfqqje :T[̠Q8}@]P[MkMRoi nXP*Ə؁NDk w"a,kjG*pPz}}B.<+B,RS+ XLjNJdh;1D(Ǘd*K' Ȍ 4Lq1*:h" w6C#Z31L_(R(3!V4GakM,gXtԆ7Im F]4&}MT oZ*dh,V."JPLT¬oTfFMe7 獪53AOV6*qҾB؀f 2Uh43KUs5jeB'hD.Dt|h:MdgUzP^ #ԵXEoYZHKM?TqbJkuڨSO#@su)]h4ƌBH 8!.ŝWxUeGv&}Pc!j U_j"W׫˪kM]KbyOo(ȝ-7Ԝڢbth>(T!IOJbj;)5]"WL 5S#"p@?fX .;\bR"4DEjMJMl c.13c?(ri&⻙諪Iy$lH•BBJ -܁N-y{+5ybۯn4EɮrPTO`/^+s0 J=Vb5Չ:xMZ ◉F7c1rPW[6S: oX_fj`*l+>0եLUUDui,V3PRj7i XD#7r,W>| Raȵ,TIdLF#S#qbwQ_R3d* D:|o)DC0cB'QK)˚bmj3[%VxR>e8S h6GHŚ3&j+5*1U:OۧvPTb,͚R6ҭZ柺$n,VѓKY}Xoa VF+fm45F3 /lV'^.>mЁz}^A]3FA=/uiH]D@1QCՈC+5\Z_VZȋثQ,m R4QR?UVjd"tWMl&aϫ:Eoz.nvt.`7XdY曑H5JHSoÍFu<ʸQ@¾zu+3ygbZB9f1TepU+F?dMs\eo&d )ԁ!k5447)LS/>O* S;5vS_7WhYlgNJ/U 妿cx۟NqfuNJFO EDXEu^'`pLR'j֔YA]*0B G,, -<";U.'UE6U vڤ9鏵܄󐄍,Z5xZLфXPXRG j*U,Uo&$jND|CaCDn \PPal"f5RO}Q2SUVH':kՇbu }\cm]5 -%!j%mi ͌LjPa:UQ)̇E<̇e=Cn!/Nf="1Ssfl #>&w\C#١Cў9ĺ|JpoyIJa>4k,=$ W8 -G_]Uu&u/$)$3SIL`p9\\d>~;L#2#A oOIΆ"NŇN3,ds.C3^1C( \B4.n2KmÅgRG2ICA6lJtS7TtLD6(g48ԆGsrFπ9a<'S^a||t:(m$2 "y#XǚՖ3h.uc͸kmHl$Vr~r61p -AkޡB)fnZ:cC.:H X*Y.~{ :x 0Ȇ׆{ t@#~=Ϗ{  o!OhSaY!rWۆCzGuO…Gs^QC8A|h8exx!?ޟLGg2e3Q"yC3Cr){6d҉LK|b4>,{5RLlh^?L`t(< Cxx&8k4%.yL6"&g,^MjB}M<sSo*b&n*?dEr6 bz,:R>}6U?U4k%%?2à6HmzXX+7{g S}*}j+l 8 ]P~֤}? Q6P{@-l s曁8@GuH~}S1ѥQ4W)TNoNg˦dž#uiKa\oPxɇ5;t I e.y6K3@+v9F1ly „qS<#܃׆,D:*b?7`;C>x"p\Xx.,c < 6dtHf.:Pt$0lXh>aX +K&$k4 tUJ .f<'-Q6䐏>I3l<:s<:TytT$:at&\ǦM7MerF.YHɒ3\ڮ9l$h,|O#4*@IkDMlzM2d68g4M8L=t@eLZcHn"dz>IHH`l(7Ⱦh33\zC]؆~ Yy݌*8,$r.`Dz>tTo«FWp klxx6bR6éȢq Z\ropЩ; Lm& 4-n9Dt% -,۾Y#[nV5_r}lꯋ)؏ +2WLyt@2yX:c{&ml*B:}]9.@EŠR_Dd;ZRzjɃ1_0kXk`R!'S"10; , -X_dc0yc{V`>D4{_)j{& -N,b맂|bܨ:p5!'٭ Gxv`~ |LȾ0ȾƘ2<]48,p CCg<oa{]DdCXSl]7l-35&rM9ÄeϠa;gC::q{)hgH!{G9"WIe~ڂ dž}J#K!΁lցSXc=$t@H'g6(kq®³=iCܑMG\` 6X_`P$YhS|H{(}u~:dP;Zrh- ^n!l ;iac |ֶ qjӮhm{Oݢ2ap6q$4GRw&k{ ^_b'*:ޙwcM33* ]_qr'+Gw!K4 Aa|`b|8|(KW nQ6U0oP't0O."} &8i; -k#ԟDؔʇ l3E1g#^<Zd}=Ekn]pt)5A=裢sFT'Vǫ)7!Q!y#51鉮] ~!kCGb\]x`&03pt $w`Xk XǢ߇.lXQ. j{!UKN;kqAoHCκjä[=Il΄Gki=t@ -qK[O#NAQHv"#yn#c,Z atC:;a\`hFށl4hXW%OK蒳23"KlQ}Q|ˈ)x "\){ߑQi#?a -ߨǃ[=)jMb>Cx yH@-:Xe,멣illyLk!7G .p_%!Y ̧V`3[璥eĶ'+_  Μx[KE PE `'^%+ЅʉIȁlv=Y&yS?Sac ftL}* -4F]*=)Ej>K_0I0>F6B:슱| 7U|)wO`s"2v$)L ~-? :0 ||#AF"=9L^1OF>it%ljLN7(K!1 -THH KttRt̟wcK6=& 0_`' Ɇ˩Tb4Ld$举2yc:u *8o4l^9)z;+ooҋ&>sK£K ii$Z-/a$ox53 |v&wuTX805ȒLjt. *sɂSK# s;Q1֔2 -|z`l |X/$H߂L8<%bĄmj55Wt%ws d81[2(=O~=HIYGN"w@;$qNG!?9Ħ8$\ph> +|b y> {4x%N~p} A5_ | hL2wys ?tM陜#@'UWLR9AF?P >'KD%cEM3*\7w84]ވ{';$\Z,)x%ٔ'M Et:W}w%[`vh>` #r5kT1]?SemD3Ll|-Ͼ:`%S3b=К A)<&q I( 5Im_%T ܣ 1oC\>;{2'>"_ -a.tXT|K,ɣp| #ȉm\\_G82;|3Y;1iۿ~&oga[2T fd9sGŝa} _Hڄg|0_Ll0:'!y`r/fvʧaΤψ:O0Uxl)Y~Έ,8|\ Rc+'CLIBeźԽhY0b{`bgI x 1)&6;>OR l~:{c,8z߈!v ~5OLL؁^ c5+U~g3Ph٦3y^;VՈc>Wgl9omVwF^uۘLc{2NM ؆-8O_6o/~ :'tq)Dٔz=L\C+#,{B؇>PgzOP+*ܴ@r z0`AG <(u!@;!ǻsgC HwqH^<]trF-z.zlR46񈬝HbKd L`XqQV[BܓFv֓cp{^O؇W5N=>@8b; I ̡7LNA,֥;F@\ o8X3*=aHY^CσG*=pż-4u4Z2j ۏx%'2W )g Cy) ݔcI稁ce Nɻ#E|Im3!V\@V^7%ko|M]FU}rf[0!oyRS_c ^(>o37 ;s#~gZm  x).J8?1 wćN6א"Sn9O`.!=m-r~re6CF|ŏ9&%̰ΛJ!Gy(rH9 &'s8ँr -JAl)? O~9'=Jyk@)|o9> p!UBe~s >|=릵~)YFoK6n$kqC<,riH3IGDv{C쳏ΟI5zG=L5[+ 1e鍳;G6gdˋdRtX't91"nQ5 J>ZT\TRC;)53 kHPhIm}FP.h.vn뮅.Ą'n~MW\1>@`Q6K7j|ڜhl/dۋDӣ"| H_yE〗:_>?qΡytCI>,.cOQx!CF#A>97uk,$+VlXg%q<p'poQxhh8aָ|"!|Aiĥݹƛ 26{W1x[z۳ duTE 3 -fy \|蚩\%L\`(٠D@ š|tdr7_P[=uqK@Er,U XuV5cϛ(`>B)\jLC+OS{,a~){/ko.o[3 wVgA7PswArB [3DWl :tZ,1aI#s !k/=g4. `lSgStʈڷ`va+0uwVэ7W,`6~ -wDE}*2"ͧ -PY @ -]yr@Α2r<9r:Ta*(8}G|02G5=xG|+|uڷ*Ύww_n >{/j*=|tx(:o|jWfLM:6I,9 ekolg?o -:j^G^1fZ3}U: 0q<)T!.Dpn#B -y㍯ĶD@H2t,yz`:|^\RtS1U~l  Oe -醻+54v UxZJ?(>42O,cVT\1_-֤l*Ϡn: y p#Kت٦udErGbq$vo'ax7+`-gPaa5/8p@H*jϛMW#%c(>:'yfGT]2!PrGW0av^IiCLRfr.B'"R1 l>Z`' `b l6>_Ŝ6uU{}Www-[w5>_trfgZ?"aP V(k GHn[rw!.:i@\@r1tT,0 A.@7Zzٻ2uS3.0dD8 ]1ȖbumdVߓqт|X} 4s7DϤ=%+pΑAx C3'z2+jҩ\֮K2˜du[9<[sB!`c͝g$aW8>Y$S{@J\V8Upt1ؤzm3K)jn{fe ')KW^2񟄚i%nFdb@D9ς|#0هQ53w/9}v&^ʹ̤kو3)zp@kQ Y1>8H?1! 1?%}c3t mi1Z>D`0Kfm Flaq*bts%\ܽN;0 M gF8[k6-p,~Wx @vAo@TxwU;šg7GS|/BmA~Y1x*RۧI -|=[fL9FkocgĊ w&jnن/XĤ_8/4 ,hD 4csGn8W|ΤFMfGs%Wl.Fp?bS s;cۏGw!{ -u~4*G|K6>3Vx VW@.OP:q*UxސekAax ұ!7pE叧c&A^ -]p@5egK&DnƴfC'>/[Qi3Z{YpÌi}l}{ cKȊD2= qTďͦlyh!]tl)6=YEzdIh yw!ܽuCh~hz*!q9Te)Z6HlzmLu| 8< q)פxJ\CqwdۗdyG81Gn=:iL*|E9/`񊠢QDtD2atEMbw[lvF~[~%J}O;>oT}^M~XC{eÝvxMQ-ѕWM~ ƌޏd}NӍ];٭ͷM7٪+@ʘZ<+{߀?# 7HM0D؞n{jj~jx&!ko\s2ySwʛ/M'l g9ٶ[@θ̽ /z ~ҩgE'׍-W+l{hԳR[xF+j~rrM4=[#7k#:\a;y͓Ne</NPP^A:mrv7{ˎ>" -oWLZF}I:=;;d '++SW̸;Ozk9Uu+6jxR^zYlYf{讣{Nd/kɂKlʑyl%pL+E`)?A1PCo'C%zCsHƷM%wLWiYPˉsK'/,[mn:O~b^?O!/S޾aμu&O|`S9CO^9([:5|Tcj{y% -N.,Xm@Oײ'WKGO Hm:)Y`3$oA#})MyЧmeO>pa>ufw=#k_[^.w?>(4,~BdE`ExF_?hwZ{Zyo^@ sT ck'eڟlnQ${0TKZ{9>eP4?_h}fw=6+Dɂ>4qOƕ=r+でs;ֳ>#_6ˏEq'>E~g~aOw~lw7ஒѳj{.>oxknzF`kvtU[t9!j{Rm_k[[%˷?!Iǜm{n}mNg}(OgK^3 y59|zM+5ͬuY^UXwzW(~.eg0ݧ$ݯ ko?S}xǾ~|0RקgkwS8r䏿leοv=P{x+w0C|zinP=VpW^n{J뗒߈tzY*߮F";Go{aBc.jTyuk:o>_>e>?Ϣ|~E..p3I{OOsy[i/Ͻwy~R\Yw6yEO_ǏNV*?,v];G:w=b~b= otu\Hcrs/_n>DxNjpÏx<[W_dm]ihX"sa>LUU|Q`dpr'#mn͍SY~g7(LsGcYR1FhcO?r=w;Xy'#[}RqM{^xÝ}͝x~Aq͊c)dsO;ww3_])q~ɮ:חޝd?Ieov??{۽\lӽ=A몃Ww5Ty_F`Noo l~n]CGBU&t@WK6?̨(~XWSNl z߲Y9׳rd>X M2yu='/^w}wXl>ӿLY߳gGWŽ,O7wMq-.Zn>ۻ֢rثOVl9Mu7un\K%&pzяYnOW;>;Qp]PwȮҪ{eKގ/?›]!W:".މ,Vp^XMwËn܈*>z3fBY*G͕(`?tf;:Qx_~6'zkCՆ:edI5J27_o󘗏Sϓ Ϸ oe)({{z2?m+ K;rf4mw|y{%JW]^\RYp/`GtEVx < SQ%GкkPVByԻ9-n2E7]rGor.ݸdJk%.?yUwn9w+l)9w6?RʜOs&?NeĠF'{^A|ݻk%/ _)._Uy?q[zAۋ5IݹǗ.ρ%ߣy:w+쵸Sb܈-\lgq;Uy#ԋq?(}/ -F4$Y*nVwV_K̹]G|ocuW+Oy2kVl\ W}}ᝌzwlćq/nw7?۫kSAG{y;}mq~y~t;}+0~a+|ݍ,s/dQ%W5=py}u^7we[oqvOtTͨTz%,nYÆ2=7^*tϻr'Y=9C2&Z*=STY?©_;\^+wxwYUGH}p>ލ[~m>1}Kxh[ n%޾x+^b󫽥V vӛ^n^~^}/c/V HHQsǽܦ{]뉰ב`]̽Ʋu"ws]g샻 GZ:J"VT~r\IjOW).vx}ՑYMuO*J#_a^w!yJ=UURd2{W?E/~J>m*1 zWX͛n^kXz3īdNȎ]/bݞIWwR|%G.%ᅵPzr\鶫N/}wk֝쪓WcK/\.jG:!y -u+qoA./o[y;ģ#"Sn岿{hRo<ݻ|Ktuګ$+%F_}'16[)14^!7VxDBLoMԉQk{k,*v.|Ϗeʫg7_L.-SOW[M;ŮOT!\TSx3rjն;nR8ϦGkFwz-}ݣ[Fn=8~cU=?o8?2c_W>M2Kod/%zgw'&"*$>#п&HJIfѓ,5|gW<5]gso_e3]שg9nVuV\N-k\RR)ҋeSʿ[ː>.=})Njq7J,/Zʷ |X埽/V%8[.W$sI-'Yc淋!exֱk"gOד Jt轴$Z렖i㶡iK YD wWJV.+YZq!jZYmSʶ_H.;x>̅ңK!ZŗWvfTqݜO3Ny\QCOB -+*ֿ򭭻dޤyHGgJ ''ZmJaF%LHD(Y1{crxⅸIeWː)>{%(N^)A6_-9p5ZJӶ29?b;ĕXkuSn a ~ a"~m_?v}߻n0oZ ,z2n͞[>/ޤ*sx|Ϊ{EmޓD%"( Q9CUb2DI3JT$&0ۆVYQn[y}κsLsyyS#59b1l ⦳ȸ/=q1W]įJX/B 1 }9~E߯وh4# ,;E+sV4<_|b~ -'|bA峅.4nVP0T۲yƦCӰo0sI㜰ؽss꣈ wmN\ b?aK?AZP_s)F4A^u%ν _4n9[sڎa6BV;bď?߃gM.B(2{=\u3\'?MpeÄ=/nq)!GW=\F޶r]otRAӶMUJ4̭/O?:t~Smo$9Qhtᔊ*:V"#y3 [\,vba//}!v}#< -jq|ohSAw=kiL~.g6ѰB8`dcDd5Ҝ 5ll4 \Wr(^yKo6+ -p&cOa_ٍw`zzvóip͙!£i|hel+?SGNEc qd 2̌!Syt\d7>=3g4eA8rڭ6 ؊^ ёOTQ %p{qŒ_]+y{xǷ -~n;s;r-,? O2ײFFQx!n}D4FLt쐩4vds?sPd#BYEcWvJ4}IVL?sEd [ c \!ye眩D(0aY,m2['I.9hj@%t'vJ}Y5(g^OKԵr]]8ܦ8 ^iz9vNˡ ׻_Y/ =D*S sd= 8l8bL}<2ױ\7푥J4q2lƠi+45M\7 VRmW_ZbIa轰}9w [8%]ϗ]5[럴^u£[YdR2T r,$8ziXg} 2לDlcmf&z6Do1z&23Yۅ 9hX4eEP&T!V&>ptI½͵.n:W yunf}7evݺy>7)v d8)C 0՘G%cenmv8aMb, q|2&:9XX屁c`jHX<$8-6Iu˚l9sill~vۇ]?<|s'P%5x?D6Kq\p?YOpFy8!WBfc}&gxX,ǭDVNhdΠ {#FsWS_Pxkg' {rIXZ~xxigjn_(z)&//#K(ՋWBMaƫ&m:$;/K,'Q%SGhC:C&O\4y qKpN)Bf3h<9朄T9L+_ƂWK>9V~-|"Eow%Qo{9#qOvy~Skm!^oPsֆ-2jۯ7z~ks&c@bhdNGA_jYd!36aAg";ϵȖ@#4ʯh/ȹi'5ZTzKwQ3E\8!a}S33a {) -zr88wλEĈ = ާ<}GU|]1>cDwdn+F]֢9T)MZ0fC}ժ3x|\K-zh8z\y%W5-ۣ"pes疟ƻ/B׏)܏W|WM+Ƨ-Wmÿo_>[ʏ!?V:q/Zqyw -*:)4L5!0ӌGN¹4Z& -F6hG^ќh=vGhb-Ԗ$tU]qWpy(BɍjVʤ?.L믳}^+ -bM !'9&w_U1>KnJٝ_p߼(.chpÖ⡚fΏ&[/R6{yDo -\. D <UhYlҺkF. 3=~$b:eOd!iz߅5"/'!uk!ثx߸zmK0qޞB }~˽mQ&?`G1Zh[E;XF06Mƾ@(& d~#Ċn \iKN\|J^:ݷtK!o֒m?x1-7SgSg3ʏ]S(o] -yt?Mэr`/e=eCRU-t_1i ':z4@56_&$:+*9mף>vNjOdn$D梄՚uEy%~Ml ->'%ܓC%r)Ԟ_e5L-7rŹqܙag?ٝʕ;P'u&M_I?8f̞+CK -53N $B -1??,þ{C'Ox|x䭗ɵw?m -{ChH}v~7Ƿ炓j4z_|R 7b" J !JAt@?ۊisTեd ی'I$FktR qmB4AԜތs>-v}$u"o*B>{̃w#sSW?+<)R>}PP:|}RJY%)n6WVa}в =爐{dmc i\]WKTGR/$Ԯoܨ֛&&76SMvl 4a+훈'0[jX p~^|_?G-o`*E`Q1a+v~0irbc8
 s0^qQd;?':&G K-Y;ڃzfU)}^~ionsv?З>F0ަ=Un/XzջcFx^pkOv'm۟-~\":"20hr'4zZ`id^(4~n䆜1eO,̕ъlHG#dٕTi'"GasVFo+ іhɢHע]xJJn̓T*8t;J qoy7Ln (aR`UQ{QҚ33̞tJ 9cdWxz#++)!2U:Sf)m4_zqtE{mQ;ߺG^r_d_IlASyޚ竂n^S^^;UPYz>Exl:c2Tc2 -1F&#q=Py"wiS[~y*93'Kn6r[ĒDz4ʿ*OHhYh̅(+4Sr#sMBVe֌;//}GԱ$ݯWzf+쫕Cr;zI:ͩ,&~2f?jshd7a-`ץ~{m^(< g':#m>eퟐ퉖Μ\(lR>hhG"dMlv%-]})̩4g -1˨+L^d!þPyvlӥo[#S'+.}k2gKҽuƲ/mŗ ȋO =O'L=:#ya;! Ia̼|?>'wtrY 3u=/b禎lmXBГ.yJ6^4vC}40d۟9GUJ'o22635GgBqpOD/nEɏ=Q*> va_u~{>$KCґrrYң?^sЀWB7B͏jZ%Bb% -PiHRG -WDNi=\6̢=<r"Ua!I=SBg@opxq_Z~kWYN]c;'YTj4J_7'g졷 uٰM{'SelŇ~u Ӫy4(e -(;e&Ӂ/ZLd5KT[bb;z@KOZnZTkog#=f|i -Sk%aZ4M1FϿhG蔊Ѳ]8ü @3tDMUJ担|3$뽻 4 T*o\LnXo"zA}퓝/w `l:=9X.'u?]}h:]kJ嵎dw(zm(}*M݃WUdju?q,XyU<u\ uru\}EDX$ V1vhyD/RCR&kguOyl(Lqz~,ӌj2_]r`2ĊXu -۪PšJzp s^+:c q` -hR=Oq qdI>+iVYB/rcӨ`TW*]Aa>"%S5t#Gf:}]L2jgj3WOa;dzi[L#zCy$;^L{b|Ͼ .|E{/Ճ>^.W*Dϟ QՀ-zSݾR1ÜywBOim>2?G~,DoЖ٠VHӖk:\HxlTr!' TX'*Rʌ 3*:C KRu@+,V|`}Hӫ}=AאI2⳷3}]'E]=U}Ӄ̎Ѓ4w``zg15g8AS"$ZH.-//du-ù펽oi+$UjeTe^de6nD5.bd>{!eν -a/OfpLtv[IctDt߃A61 >,2 /k;>:4dOOI;AԲJ -I;IO%d :L]]@7:8*lJ3SD БsʨtUAdmph=6bNL>^twbw?>Wj- ҽO{KO%o%~pOc䗞O?]}-~neՏjϔMzOQ uFTj1~*񚽬T:BZvd,99̱G+p<%8HUDd'dbSGf*K7ٺCv̮<|jh.1S/nOeC7t7VKfC)Wr` -6̭lf9I6łhl`ŋvLds[,dF:!SN(#z;ے)[5ρ>|ШԆ>i*૾C'Rj Lq0_߹Jvqv}pu?[m<0 A{ -k=D kXS<]/CeJt{gf@w↬B;j޲  XW7Ug<[9~o)􏃞?hGJxiPڀ -B*h' #.b4o2hSrE}4MKcGJkJ5A94U3/}މ=Jq]Z(}GG|cpl֋>Mw L땪W>™tc9\&i,޷~ЋKWoI,Rƅ7Q=dGR!pfk}Y{1FdV& f)\%AS/]&4 -t4h{Hwum3/hu2C&˒)q,LVٔj4q}وIb̬5 %4n*Jw^:W5jpzb./@cM).̾OÏܾR@3NP^[s @{ 4GOie0g@ÌLR>Wny [ @l`AHA]&U[;.qr_yDhE&81z}B&@!6Z _ +MW:$IdXӆhǍ|YD+t|S㧓C߈{= -<ܑңD˛C^^^9*QB_>d _̑D톃/K֔tu峖 o\kGLP&k° ClX6hO jFb@+ -%ϦךkRF@V~v[zF <ХM%B sGxǏL!аgpNY<6$P[,xMm4@j}hi|.zluŞ/ϯ8Ezо!ц?樬=rg Ч5<2!ؗ`Rp|$U*: j?Yr?tIӴD o\zq.sHy4oe!j Ӡ_ -tXRCi5LAnH?M '{^'^ѧdGtq͆&js@ Ult6ԱWf00W_q4𺁯Aw\wd{Bnl :媓wbh qw]Al}Y-Gsms{9cLb!꾸 UVvIOO!]1H18YzǁIPu& Y\b^hcȵ_Z8o|.5`lեlUԘ\r0(Z*&@Yyh6hm*vߓлd֕Bc5 tdYdwaXԙh1F1<+U/:Vp``- ؝_{)7lv86ے5_=w_z*)1]W Q^mL-Cу oYLbk7(D/'SzdSsTlي3'#NNQ\-ѕ==|i.X`}: -w`{_yc*poNXhD&hA\$ܬUFJw0ҁyHW8MYߗkY7cHW lY!ccA[Pl є M戦m>T޴k -H- n̨cK h甠1To|4ĵr#/714.?l<x$zvgq&, g(udpR%CG\iUBXmkl -†'}@lv̍16N]m X6Ԍ|Z^=Edt'|m`gU; wqwv['y \:+(T eVms k18FDKGQk4D" -P6xuAYm K8+{"ȑ^sb=>zXچ,0AwA;ЬhAgx80>T1yR$d }a}Kv -E3I.0C4YcEJ`gEdTewMמWLSxFtnWf8P̬02 -YqG=?? -4lieFM}ӂ:}B uSy*?ux! ME4mX0[_c 1YTVGh sqGoЖ2ja;$uţ=HbLtx& D(yes.!?w/Βr -5Ov$X#( -Pw /#Q:ty|dH&|q+lB{»זk>OVh8 |N^&aMdv7V -Z\J1_f\]gIa PՃ}#U; [7׊YiIXN8f; { R`gAbgV YK?Y,c5!Rj)0wA_r – gB; z~>񊒮ɠM}k4_<Tg< tLXLcvw}+Y`<NX`Zo1m Dz x$#r$"ՠv>0rI> P A*JCYi*6 8P)ĝv@Xe=)7Wwv!恝{% u`g16`+W(R -GŦGOf8~ do -0F΁k>kz3U^50*[}ȩ@ 7uLf!FB;96η]T9 1yzAYeu[s_.\Lvr"=ӨŶt$쬜V`gi\T$sy;@7&,%𓁝U8R;+eATr^`m}oo@N,0ejV uFՀK9"`jdXx<='^? Bv൯2RQH~$-G#wB զ7x]X4f 6 zlt*[-mKJy'ŵ ^Kߊԙ̏@#=6]GOvV:*7G^{^\6Z* QmW̬3;{JPr![z ,|}:2g$*M.; ֗9@J*) -X^fm;K^l`WsaqX7QwZ 9'ܘvk]rqUtp"wb9':pbuH\R!BJ!a=KAStL 15م5O\4X/am}ChK`mVwJۈPT+ǵ*0p`}s)\m<̡1ԭ'ya ׄ}' `F!ly|㥥+}`8?sm[&Ģi|-'OܯS u.]5_W6 -Ø0 ٺp_r>r}1Nb7=\\nxвfȚXϪY֋ /Y|K FV󕹭V$~-3ͧZ|eݓI5_k"µ{.ɥF\\ïߖt݃+mjp>8>GbDi5fb="ԉjpEj#k+K'$!Vhr!iڰX[`TEL2”s[LX2j q=( 6D0uc4KW:]PyVKvNToÂeB.6O0€$:0 g{|F}䥇aMD^;Y-Q l2U^90@oqh E%,6n>Ol3 ͇gÚ#Аk$YO;?m!u] -,2C[s|6 XiYx";s8S- c]Qfu{&=Z8UsֳƖrgHB֬gk6\){  SxոﹻJ.Mڗt=vp]Qy|77Orc'\T -dnz3"ENK|o -{ݸܑHzQQg&UQgpNM}bhu,R$1c8/"X)T#,O{mo"`S9Z֢بX-`m|>lCfst(ZM(=ty鄏B}"p`mz3pϊ0a LEVwYpnIѮiXz -&5{9\RÆ&iS}=ޗDl25>:kRqt4-HZu`/ oxj!+ze߅ͤ1XĤ}3C.%O/6"yJv8eQ5';zvxb.\C=/ǩ0Up\!>L' 8o?=[ϖ9[B>!y(57=cw> n/g-a^OžG =Y[ g%>% q֮ >ܛЀb,>{2`-8El:-$h4*pNpo\sZ9a{iMyϑWkprFyR!ab 97eityŰ>#/ߟ .n `rtį+}I9`r0?X[l u6d˫=hVd Q/',.zjUl|vw -ɝ]St-rplgyߪ|TEA%Wpl>y}%p3a"nDzTx}r&C^%1Ϛ޲ϖH^ZL5_^T66Kl\|{H -vOWHo2VX8媶c$M3ue*dLσk|iJ)y4&Pu< rf bL^9Y -'A<-aϖnifP;˨73 ~Ŝ6V@[XPnBXM>p^a0ǹ-LJO#+`:;cT| -"A\-d꿜9!b>O~(aʌȽRM?1i8߆m,:+tP'66EW] B/ڂRuL qas6uM&vZӆ|r]u|Vo -97~alE%j'\Jg_p_s53Qq`L(3$csOǧt|:>Oǧt|:>Oǧt|:>Oǧt|:>Oǧt|:>SBC X/ދגuIS:'%E%G'ć$[;·X?/$=")h^} eֶ'ΰuޜs%!ֶ/jl-JߔF 20-,~jqH2~J7]"sӷmpk]kmE3 Y; -D[#td Vyr A?T!CkmMw7?DRe4-(Tk4@K'@]WcVǟG."!||(.yW/cԁJƿ -Zj}4A)2QLVSחʩ(Ϭ52|=Ӷ*Y*AAZe -zD+쇋MաSk)񊆳K'9;.>MzIa8h6B6@L0 4(uZg2tPEt&cru٤}Te7a>PC4-KVmf2iOGkk@CBm#zfɫQ6q9zlB.9z37[Ht/.[C_$3=-ԗ*X`{ ؞w2h3>\|.](6VwL֤֗$)GǍEjqY]\\Pp>I<ߔ)>QCL֤C¯QQ/;Qk T<Ĕ -~+?esF@?W~:b*\-R#K3 -t$ s=|=Й z{AIlLHb="QC~4(AS@Oe2XIׁVUF7em&gx|t kI4U$C/#KC)a,_}b.NBo7uM {DYV=3Db4ep0gB scd>vej’~fU)>zHR#bo/+S*MeF<sZDg "'A %c-zOd<~>ItXLD, YAGiB/Hpu&b6:AlNӂa[sE -{%SL@tz@CC\m :nRĪˡ'*_ -^ zDW^+}!EL.h.ȓFoRvM8*ʺl'm2G=-gPUsl :'*J -4**ߤظ|2u1hkZ -u#Ub|]^;tEh#@i ,Ķ}Чђ#m=dԙF%sWMzx..G)gN6+xm#Vv-sAS@YY{2 -2=S|b!ѓz]WbHbzz9&_ G|*A$[Mtב た}p=@O]ʪԀF㸰&GjdM.4Ct@c~RDCNa z - -ob>n˦A/5s*"M/-+nӡöDG.,Yþ9yh:o6~x 誱DI6xq‰@lqzwGXs¼ĞAk 3ȅ@f S7v0D%qfDi}hEJ S¶>$l3g>Pv=2cea$Ea5H<_a^n%¼=9чh5C#-l=lA4؊ö Z"gMmﱂUҏm (@4\5Ұ/ۗ ~ДekoWնOs 4fD100PE7 -bآHw#9g;p{{~~b˜s}c>umh- -]'8΢0k0k&Ff(.<} ;>4(hs)S^*]=Kc)Et[@n Z:&:U?y{"k7yÆ̞ h"XcZsW/~^rA9lEӄo,a l!f& c>qJ 4 @' -h3g.O`R|PIQ6{DC8@ /A8mt Ԭ11&%w6?d5C|9p5hB5nM-P`h@YT_@Ƙ2 -{`dV *жx&fpFqAӁު瓷~ե6Ocq|)EБf_:6hnxtIQ/+1{:, -_%>j -Z1Tоחc?O0p, ŶA -!FY/{-`/~S$b|.#n[ը_dlˍ(f7t阒TJ^ -]Is!p%#+&ԩjԄ 媠2K\X 40.{bej e@\:+S~Ih"?({9^ ʟf#ӁFhV[ -&C^=aЁ]xgccg}&9HsWNbC O" w}2`=rmH/FM\SCg@-/ ߀^aMx(O`|S.sqOzH>W~DQˀvz}VKAxu5޸ -TCE<97Z=fND~e;G AA Z#rg -WAW[Ș?~uõ'M X 럻NDUng2~(D@&EF|!Zq< Qqx/#c`MHۏ~ \ze.=BnGF7Ħx?=&ȧ R*G4t`͝FӦNx.x*-}4(?)1C:͐?R+^/So:? b]!>8քt'Z -̛ iZM=|koN/ Y 9-ƄZvL!u =>a\e어~|j{hPӠw-:ksi%QM]4OD539T}:5y@+kTטpTb^oev Ʒ0f+ޖ+2wH=*,М?i;r|+ }^䞤$vMx"j -_>G/71W"DcކhX-I/:OΆ_ Is0_4Gcb蹘">hlq+s1yBk˱^DaJ(Pxdvj =?99"KyJ|V~HKq`n9guT`%п~P<X!va:h9b=D < fC:)1WCsu^k\}o.52b 뒥Do19ȯb4%GIנۏZf- ~*G endstream endobj 27 0 obj <>stream -A<(wQnPaE~)m[1ᝤO ?.?`ln/ G ➠ -ixq[;\w>hA_qܝyWɔ.:C{&QW:|u"E#=Iw zc >75,wcZ௨fB|(x< p^пuB3ZМ1a,A{LhӄEGGKkܟAxu2<5t!!>ycr&iDz(N:ZkAzX| -Ό|"F1J". .N)d Qmýwxr8l\Mo!9!fzLX8?Z=&n~y)[D5%1s$ -pK#%spŐX\xd,\Fo(xB( F zFJ/xU\W,* +:mt%~+5<&1QWѥ-l55+*ŰCu%8j=<>/ :q2SV:wT˄!7W Þm^q0T;a脢 -56TN)S3^nDyk)P -+\\YJ=[sa]_ -csX W1لJ-J|z^Y9)_|)DejMx?فoϵ^U2` xxLmqzH@r9m$ g4p@l6qTCoEW.)3QwVJҗ@UuhOH6({sFOIxBh)d TkI۱'$xpGm}оW0|1g UuQ2qS7(J*xL96h}"}u.x2x3Wb iG\]=/{|111!= ->Xa)J TQg+UuORTa|' -?|'/S!-r q5rQj&Z^XK4#nPO^<:Q@eAK&%|.p2Ή@NmQxFxP{.+P}Gu^|axZX.9b}eX;eE!h3宒d,5! (a}%g'^Q1"ģ/<{7ǹx!!vY?W4 '^1hre_k􄄱%3K6)xck:ڄv2JNnPf# -|yf.o=:/x]#q>ĕl:Xq(F2w4wO@\%_P'`?^5t-P%YK_ŜR&ὲ8RCTNOO^vɫS<؇P,b+nd4kחaOH N9π^ 셡mf[53!倣D:BD -~X}9Gdg{@?bjhh5Ox -Ç|O̎xޣ {rր rX=XU'+}kp=pԥiT`s!p^rЫF]_ { ^ ~veGxc{Tp1b'L@v0.XA>^{瘹xOehZ]]k&Z2j/_{")o3"rU-~o?gb^n!eyp2%7V=}.񾽄J/ڧ'| ?.eYKg0Xr0h.nm `]qJ랰uƘ>qi4<hPKY@<'g)yaiL%apfB6v%'1;e7N!֗%qK U Vo "50f k؋~ B }~ q_LQk )85C<\\'Ea6'_y&6֤ ASӎ>BiBkAb )OɔJekrF~(~_lƾAVVgawCg'3[Ԡx\Ϻ+>+ e;/74_6ƹ7Z1{Oxr6<EпZ}X煞ǡ -7YZĚS(cx$ar}b1zEh,@a)l:FM{m[c{Q?2@M`||hO[1yq,yf$ 2 HGm~fQ\ {s+WKaC6 ݃YPS9kmE<ʭװO=y楀GqbO*XS\Y [b5lDVYل+{ - zK//lh&K.Q,#lk(pҗ #=ScRy[i/ -iLX&h[`DzTӸ֡蓉jӂoYkՎFe?$55):p2jj>}ذ}CJ%B5k+aXGEx3EC# {@z!P{G@ށ'LZ>*[ +#aen/EMgg^ѩmDMz෎\pX< +JuUqP>a/!=ɐrxcJͅL@ -R.`VX*l -4v͞OFn":@_.OfLhwLl!̥Lr>A feV3j!oփZP״|zmFdÊB 8DV]d Һ\񧙴K<|3wa\|)]\o^iHWa N+j/z'"YXiI{fҌ$J]TwIiڏr.W*j&UE|J%Jj?] \-a!x9Wa8?/(A}dn07؜6i浙STXKѩh!yoUш;`m{]|QHtY,ɭ9dW|N҄Io&ӻTѡ1DŽ^q 1F_fjz0bS_zެ25Tj ->6 忤&eX >(W{]z]G=Gq}֋@7K(a_vFxK[N rO>l#77!_| -K[ĥ&]0yn֬dhຂQ"閰,Imj'̬%>/6JŷEVJr5k2٫ڬU[FGož[<-"Jo}ТSY_T;x~(H^g!󒸿6PC|9M5f>}\%zGo# uvtEmKUz(_=(>..l8+"ީ11-;ye3R2 uz'_[sKR;=caf>C+ەoVKoi,BF&}'i25C|E@? -ߴwgJG}y[vt~)>üd~>FqzX*dʔuYJJ}$}Ee5E&!3>هbqn){eZuȫjV飊#ҧe;- }ˈޡ'F4 -R7jw?걷H*O>ꢨ>)0ے-8Ǽ:T|8Tv^ .jWD  tA!Je>\<$h,y59Sa˼=LeҢP 4q.Gxakx&Hޔ_4wQ_?ZͿ]MG3C>Y -bz%jI3[H{UǥolK\n_b'~QsBrI$j7XJ&496_ a­b+q5OŃ%O_|د}@x4xkV4Wy`\x嘣d.⾦#ĚtJڛڎ~Z2ٗi*FK2'W=GK,~hmR$.mn>)*n=')T9T\o7e_vӯ;Q;2F4P+nXӯLgjz-n%X6Y']mLh͒0=_Oi"mJ66] S>ne - 1kˏ4-?,PǫhaU軩N|!H=mcDN7s:^NNjLO?0Ѹ{]k4|?髊s&CD[V 2h $ބ<1Uu9*̲9%ZpWNԱ[1`qsΗ2aRY5]'K+_<9/yq~2Z^DZoϼy'ƽU–7qgwQΪ㤠1gSj%vs19D&.7M za?9^O\M-wIp Ip N<ߜh F"L5H-@5Pès)[e}?C@ӊ*6h`W&[- W6m6b/n+ueIR7IiI }ud5ȧg GK;JEjK#.4&&z&y&&WzJZ麶"]BS}ܢV9Gf8߬p@]' xP#T}aAfJ5l(~pL7D dsv+ɖגN6JtTş-x%j~#+~-gium1q]녃=ŲCy2Ǟ)<ŔA8ɐ+zP#+?kX۝CO=HjJKkrZy!":bpQmXa]XIuHeuH)&><1jkTPoeCZq@YדH3'z>sy+=8(ѐI.I3Ҏ ަ agC_*TJ ?IKLz+#N愴齷*Ĥm[Mpr@O+:$EwVMKDLH^CdQMXauF7k~Mh~M*4R+]eer7We@SAA)'4 +YƩ -Ĺ%oϚ4G6d }j&Qaɛ BT_~1zT:WGE -[-k }`MnV)'Zo%\mW!Zr;Gf$KLuuOlLa;\u܍wo -LuYJh^G;>+v-q-:ޞ% loVW]Z7X˫anD_Fq/+v/u -M* (O;{\^s|^#w(OAeE^E2(_II>IѹQzo9=לO6I -ʶlaޙ6 -λ׆^qe]jLjr{5E a GPvpnjqH~l$ر*"6'&=jG}eT䨳)Ǜ"=oͺEJzC$%ͭ~Gn]GfMQg2SO7g%p7`C=Ϣ}Ȟln7ѷ#ef}"9 #j#c]#_6xg~ۢdMTǤMQbCcd]"~urƺ=XqB~̌rg~pV*O{'勓^ '֍jZfӤ8$.n5=r/ѱ:*%;j[mcl2@Xֵۉ*]% g2"m+cnuC9y<ܬ0ڼ+'BsR_KMx≶tGpܘmYqcccbT7;^-g_ܡ;S'8{V1~JkewLZ[${7"J"N6eDw܈`<[էèa;ߍJ%{7;o]sd;ÝC}})!, 嬒JAQrGZ-;guJSLQӥ' v9 vQM^ɢePMDSwyR~y=|1}6y^b}ƭ{5[U[ eq)[QiP_ڝJ"_zn -/ -="C /#p13VkU~n,E񡥾 ob߻ɲn.o -Kߞ5Zinh:rX5WX8\O(silt(WMطODh4n;w)s9,[ -dJK iks7+V([ -}>3vUqBAV[gKwYo=b -:-v /( {1"-:+ֻ2 ѭ2$!Wm4jSt)b0C>7@k8ޘm1Y鞂N;%/rE!S;n -Xip"BIa1b1CE$"1PA,DfD5 W|j<&N w+gc"D/lt5u{}s*0.WTZG7(/F]-=.v|!J[Ǒ8G.t/|Z&knuY~iI\p9o-j ۰SXTXf9nzFN󚂶lW"&(b1@%>r?^z,'yx˄"b/JW?*727*BoYzgԕ{nQ/ -\"s \"rbEQoK}*ٯ䯟\/DǼjF5bhts.Nqb(sJ_8t#rj7L F N$FMD? fNYMrU@ucsB9갸9zlVGTasշQYܢP +r 5yYSwΑŞQmQ_[>X7;GΝ/{=w]Bi t DgO@4ET|?\_{h.{Pk~[?e;zAf-#d.Zb^BX$aoc+Bg';QrkGh9-CVGGqx!5&-}Kt"7nMj_uF){R*K( r bƄixnwyuwpMc~H9_$jĂebEb8q؞<Ϝ[I/|e<^ž-pM(+pK)-vNz^[RX[EPѕnhr:m__P6,/c:{cѻa3I8MC9ii#/&&/"YMT8@Tv!v}=B[8!Ԅ(hぼM(D=E25ɯZXt)&2aKwiK/cC\ ?U/`N@Y IqbM;YӶ3 -S''ZGL -ߏ@(GJ,xf%?[n3oxJ(`{/<Pk}\5Ǖ2׌JJRD]ʹǿ \/JĔIh)7ČˉiVƬ!H̜Xa? gb#}I{Fq+ש导 9b,~~?1?QqBb696s6%,Q'-2$f-sԉ bN(˱e[n AQz\,#hw -~AX{Ǩ7Qo5F~[i'<9g84b42!75v91k:b̽Ē5bS sR̥$1s1CQ=~o/T;A6kwٲ<50Ts n Ǿ.rP3P_c;PXST'Y5fp0F=<{CM@N!F"[BP”5h fMM(NML_&Ebbẓ -m_b{;ps[?rG] <#5SuCT[S<̹ZǴ*z䷥NKr{_еxMELG ͫihM^OLDc8 F-"f_m#0k+OU&ļĂ}}bO fOv6C@_X&2{܇}|yYd֬uv-`DTZ6N E(G949]N]bEĴq PnD䕄tiJkL Ă '{bmm^5p*<]*0 ȊWztm%OK">Tل~oK.vHL*q0}a:fC?~+ a1hơ6,|LP<5:llgPN',&g 4&h-o i7KۨZm3 k>Ee(wƼBXhtscBgCRoC:J甎 -G%Ejp[&/q(LDׂ/%-t*Ĭj(W( -3Q L4\;k71g^b -1w1oM,xXJmt+!y/~Wm zy ,L/*t~M/~9mV7F WŻh℅u ն1BQT4N -VsP=^XaM,fF,Y#CCr5耚;{;”Ă4`XzXHcGo 7[SV z9 H󭰓uч:Dl̊M}'TӵӞ(ڈԲ2ې -b}Axm#Ly28ᯀA _N1ah>*SGDL@Xs[xR1lwf읶=k;.=ǯ|j.b-j;k;-Re(lc|$95w-t=QZb!j9SA4C|2V5;e-pF !r};r1<ގ^wuy#x<~o-Foӣb¾ܚ}n"flnf-S t8n'r.VDfBٹ2,ƀ6$!$r 9+g1xR ӍjZnR?Sea$zݔ}T+>>&.h9 8#qtA?f H#2!m;A,հ!<{6SIJBb+0lǣvݗUvg˙_; }~w훵;=j:7zA G*8SAiցPzYYKc{];`{I?U!!AjI *m7RXh6n11gVb&#bIbA]'7E R3ω>=u!3s?mx6Yh~x:9WC39X0WdM󠑺6;vC39#ŢFܑ;Ӓ-$!L؋ zy+6LE*q0O>&qEqt \obW#Oܙ)~3GΉ5q;U˸MݜQڈ2 -HK7hxm|m>Pi4q.`O{ o턶:Q>F2QAd1W|*l9w0wIZT1z!1g ؔG8 aIˉ[f$Am AI_9KOa j9R#usz`3Nq adC R7+8L?rּ6B7ϭ9J-ԵG,?db5Wc$}'ٛtImLuLnc2@]hP1vQuEyq\byԉ B,XCӳ!5|wݷZ% '/=<_(8:V偢+Ni*p#^:X 4 zm 9(EGi9QWHv5z-^^-eDALxvNUI6͌-j>!~_z(k`7u2CQ]A*AsY,ۋy~LE5c:7i%}s(_ Qݥ|s ܠe{mvMb Fz9iLnM>ԛ+>TpBS1xvl4 яԳvg$y1f 'Nﻤ̣v1׿K_7^\ gd7Ulzg6-~b z)n&/m)[8)1)-򔾨=%i [ydf~2d BQam w0 -WT#VX-o[ш_vqBDvvkBPF<%MZ؍7sMY*.<\#-M\< NJلƾmY8GlOϳth 1HВ&w S4tjfTuYΒWOZÿU*%SwI~4kY#T-Gi\z8IɴY_Ѓnǹ:,jS6ZoeNڏFޭBSMl3$:3q!˳91{_mG~M]`*zu)u|cftOu˥mLHمcgG^ L1ίH`xy nO[풫O,^EJ,Ey*eb)`0E^|t<@y?gi^Q0 -( uai  RgRb6I3ihsSi:f7tG5Iv;-6-e?tt1n*5x~olxqP=;Z9vKٓU V3ݶ²o|Z!ϔh>[N?Y͗ۗ"DS%Lf.8M5?4RTқ]Wބmٟ5yY]{ً[Vp/m 6uy($E.b?a}c@%Ulᅽ^aֽ00C߷^6kp;59}_9 ]t_r:Fk?U;> (+?"7O\SE|Kg]lyfB-a62W{waJfû?ԌU6+]o1U9qF}.+ e 3r)M1 3C -Sٺضd9gVB;hx $};bݥ%_#>wiF>էE?\Camg)shxoga&75ɇy׾lQa~ &VIm· lgagSחsUܧЦ_Jli.ٍ\nHSݩ:BdAf*}xhfn1srBtqгM´Tj j/r4 $n5r=Hz{oUV!ww'xTUq!n4hhwq~ߨ1ҡTR3#}N<8y/bŻ#p׿uݯ7}/w|y|F|W>ᘣp6ac4cYݴqf]7k{׌3Y'dnų -1[\9&cf W_:q/<7./5ܜc\zᕁn\GeB^T>fXrr6kz{^>s3g[꣊p_R4g>/:/wsEgbTw(ڝan^Irz-_o[_nn6HO ^=Zlp/wb^g>E"&ՌᓪFN,]x_/Žg ݟ} Ӟ=_(; yX`~P?)u٣{<<+?WY<~ftyf~Q IօZ{XrI:c5[ܺv#ظr!:NdX!G%ڪ ŎČ -I,Dh%'y矩…)|rF{k($U3j Own|ܐG}oߋ݋4)L:ۙ\: ']{eiG*5޹ 7i6ݩqٸOs@`9KmxK4l4hW>930g8rmpOoV/kDc%iW8Jɫ,Jx&K/׵<\wWOpز.u7rmO7qO7}?^u~P7;~oxpVx{?ׁ? ,&nm<0{͞O܇ ckFe B4׏f0MgU.܊p7w<.<ˆ-!tx~ )&Yue&<w?{~aNCb_c,| je%ڞn G2l􃿹pL\}˵=?n:t͎{=4-,qY7{f]vi\{hvcnޒC';B{3Z֟sZ{@sb\tP>4[ڑbYgh%/6`?ՇgQO~7}Jd__&b~Mx[2;Gzmį;0ܡhcTI*mWI޾ɖ;6k6/ޠٵiKCkt>Ֆѹ#4[mp%h'78I7CqjppE -07's{f-6A |fxth&ld8 sRũ{(/]g|QW|y>if՚=[kKcYIp4:aL%d~SDv/soX0JN 757nۤBjTxn6X|Zvl<⤘44uRY=rbVDҒLiGQvS|P3 Ny<sШJƜ4 [K\G{"줬X/ݩ;^no]/H%B:B7;!ɤa͝~4x^ J "Ts?r;7WVrW'W0r ȑCڑ:\j=l?Yw+:hfFI%3M)Ngrk GZ>V4 [+OҸQfB*]l=n]_!6oI{f8Nrp0>ҍo}Gӌ>+7~q>,]} -&Z8~g]ybfm}Dr'K'wD.g>ېnd\1f c*F!q4Ux`taEΟM7 ͸pEfʘb!s,/D+qRJ~t꣄BB_;Z, nq@Hs1 \.\7ۡ!uN#VQs?]x!1`$4\3=WՋ_3_P#@3*?=_,tJhπ ³:?+w/{uݯ6a@L&6{:y .,pwY- t`&l\d`w}h>,k8{r[ۥ<556 :"s>]l J.i!fZL4]yXKhɈ=0T\w\q~i"}UO_0$8J\Ik Ah7_}A-fU{,b.<}㭥zV>_iy;]4׸N7c -Feu0ӟMl |Z>EiavЊbr0 qpäˡKM%U…ߴQ(0 B)o-jxfĢ3gQWFZB۟;\b@.j.2<;12Gz)V4I&'p{%␉q2)1oZ:x_B[.==zu,4gB"ҌJt!>=_o_X6]K;BbXfR`xS0oĠa!bfD>tO1ut< 5'Qfr3}@_9ZNHHV~fx6۷A7QjuTmiU_Zf'}'xVϙF҆lxo֨Ո/ʟœjx -~̗Z,>vNb+GWŧ5C_o}~͆y5hth WVriC*!A/?g*'<@6G?[c8l8R+/-&‚}!y=|9w'KkՀ;z0i{]E┱<@Le1=lN>0īdM(OYʱ -Fl-eLEJ%rh`UU]YV[].0Đ(kԉo+,o5B-^7|gŞ;A3'CS9L1x{*-cTh -)NBD> - )1K/lM,.gp֗'AW`1zWN"L~phեc1-BRbtLU'=)9.{Qx)7W.e=ssb.\mRF NN`۟m3xm$jB;.4[4炒p\D+WO<;5lXVͼ-:GșBBhCK8‰jF-}3P4}">yAqg7{_SѩbHe˱eR%|rݽuЊG 4h%)7bF$1k2U.^mkzty+XJr=Zy 9S)(P_FҊ>0 - -:bW8s\ ~c#/8P3]9JT-4;pg^{;'h2z*,8B|(褋%7]z)('cv1T`Aq34z zۂ7+ҟ,91G9&Bs~eT5 ZHs/Z0 bڰ{ Q}F.W^bE|B=4Ÿ|YʲCnރ g 'g=A`챘1 -||O.' 9:&v]ӝ·Q󂙅 -g _֛rz+5#t~M脢'B׹,VC׮BwwBZ9^?luZseE|3XP:[>fTwq ӳ{U|i|;/ooJEP,ҷ;x~z_RLjE@j@p=CfA$1b}:;[+ˍsJt(%ydhXj5fؿxZzu1_h `^Pw9z[k]-:CBs/JIn1t9ӉSw`w=w{Mw7Z塾foxs<!( -qY1JA!6Xwj\HAhBYu?ګx81`gʷ'X--b9 Q;0NHS`D9#HSl`@$cWX!qtq f$TrSE93R:0;z)]~-fvyiL wyÆ{W>˧knhbgMI`g R?ם屟\*$Vm 6c .jV"$U9A\xyYھag{5hX%iY|`^K梁-E#Pk#XH\H.$|&hz;kH-U< -:IK.;1+u'rde ,g,CLZjytf|5^w%Ьy4Fژ)0e;JJ3꧊CO2j<3IN CR#lggM;J߰Ӈ3:)WNUY<#\[vL"bZ8Ʃ73_oG]{g-HsL -jطl5xH\{n~m4u_nw̓5Ӄj\zrZ$?Tkk*f'a=\0X쉅즩Z kŅB]cy2{5w#vNR!&zz5,#>Sl !6J`09(e8|4Xħj4MRlwt:0~H>yr}7)(gehޠW --n)Q`|3 rRl[5<FU3bfL⏌,b&7G;KyBUk -'/L,#}-_ Uck]h+,q1f";dabICo_:^- mb!h|>ŧ* 3`K# Ycƣ؉|krk V"b'k<@:E%vD?CjĖk?X\R*/h{h51V>X6t6E)>;b {)@ʝU -yY. "j;׬V-*I `T]_F}T&>Bb$#m#=Yuo1q!,7aO:덧(+/-;r-;Klb#2βpZT̥&6!bgO;+2+;+ ;+abTYގ충9 X`b>&@gu̞jfR!g _qe}l]K%"ێd!Vқ&G?\%&9` /j5"~]g5y`0˩N#"~Ei.?'JW6UA-$2\7Q-/,n'%ZFN%[q}/\d쬤Y6FbgY\^!~ZujOAl<[S=vt鱷|x,KCZaZrŖO7˝v n[ 

jNg/JhܣZzy1j114͖kS6#X-kKYlwWNdˆe'Yq!y߰_sJ%]sʳKܚs[ӋwWJ/,RbXr:ţV&nLQ3~CrÑ լ -0-pĎF{` 28Au"sx.ri,_]}ᚊEbXN 4#vR MJ'=[{LO6D;5L>Y>H cPp|)3DYxy=ȥ (FF]{{b|;K>oe1ߐ,Zeﻸk\=XsԆk'-\FX}_R=n+'RCJD!(J:= cy6z_lZ?\K|'VcQQvyVG'yj41RKFGuU K5$9I_E,qxVy{z]l[0h0B<5(#i!y -]쎩`nȅ31cB}D+QrLaĀaT[ bGڋI#S.=g&߈"q` Ả%w| -4RTՐ\> Xf5} /*rٵ<>t/.FOCj(v?!DZ굲WĨɮD/pސE䏠~¿Ć;nm"^[|\hw}PbR8ҔYcxU1l# k61Y= @ 8CڋK똍mPB= ; W=bkTd|mG<;wFÞZ~m)L)}Ih>2<z` ^3|>Kص/陃nRpp\nTR錡˩a 6Cz4:Q# C~%%X=9L̈R3 |+_"&Qō׀99Z(18 --\OebE#F!6!e+Ezjzۘ%kƞ1Ä]_[!oƻܒ\C%{7Ǖ2 kK{9$F<k$֍Ź7s_+JמbMAWoZ?ߪ #9,磜/PrOK0yo[%;YOׂXj5㐳ag/{¹{]o>-P" QV8^88Ea+DL;6L' A\YNh.ǜT=3gR~zz؃DΨ9hHxmkџQק_\ %׏ř1"VIǒ~I -XQv_A`m v|lGd 2j/A>Bj0 f;/'Og.sC9|Nj|ۣMyJ9%UB~=ҁzb}%:q|]~EzJIb֘o2яM9r4#?I.'[ -PK[L81ř2?e*6Nƫ+Ď϶J>a)qX,+sZ>^O5'cI|͙\ǧr?XYT1<|L1"F)C|8!)k&V6G>C.Mk_RSJb9)c5yFK{k5:N< ]\wIoJ)6d`LkuLY92VEܢGZ:XMYAWLX5.\3#O#X賳5$k'ȋ`Z%49Z>'~(OT$o,mnGNxeاK+sBv6]ˋ\bvDžDE&;oė9]KYbM]\ٿ3ط8/f?|˴~Mՙ}36$8$}QhM~ɺ V,]bWZzuKW-__/%//9d/G{opY;ǘŞd ~p~ٮ$wg^q^Lo?^:W-_vyڕoO`?IZ:̡W[ykً\4]lAsOg>wYgVhCf,k92Aڀ+/5|f}=ƃ30Ƙ{].ޚ}{4@22"rm)VZσ>,ôI>ݼ w4ZlQ߿ߗ~[ -1ヒ!ebH "cv4cԕdzs0L-Gc8zqLC~t`?c`<'i˪129"Ö$(g%4FDIxI4 ~#HAX!I"Dgؑ4B`#żьS!Gi! -#c!$ n!V8 oǒ $tKBBU5DㇱbWWKO.0dwO(b&[AZ{l߃P\#I(}70Cl$9qUcZ'!8{!Rt8>dMh&>> L8>O!fKG2CF2̞Af^Z?3w/U%Fps<_`^Qaggה?0wگx)d]B[.V0 -ݞ/荡ti0>-cv@9Vdk9rW=Z(́h6Ys i^_3Hu@H`)-$k545B/Y Վqs={5+I5NjFdCN3 Ng$9R'؏hÇ$Z-@"jCfDlGLHIӘIe:l'n1&dgH*sdI2udw -򃐁}B,H+ ˲c3G`Ҙql -|<%(Æ$NȕT$g -[CFqb`$$f E12o(=HԐxti'Fe19"И^r8sT6cQ4IE1y#tv/=qḡ"mi$ѐ>]/%&QFxAVRzd)"K4߳ݍ=֛!R+G[0v/,9Cxwф1FZdL`"[f^QU>ȵ0~#D~`-2{YH6:Cd)0AO(FLՐH Kh Ok&Ռ5Ė;vjX FQ)$:XcUJQ1Hf`T[Bc@ y -18 -n]Ըj -55z=$?Jo2!فaf_agwO[ 0S,oC^6&Yak #R4^sH/`P1rCՁWY?]{5n`l&8ǖ󍱄<ɠK2Ga)HPbG7BjT MHZ9 c-9'44<ӆ|Xb  Ҩ4ojڱcZ;S!)Q/,oRKNχ/2$8B -K -[!a4UM-.-'׎ ɐA0cgs1hcXCo$^ !~$𞰖0_\L#wWa csB>CR9YmS!S1#,fwW-͆4hh!c*'94ÅӓcGbT|*m;Duq?s0&sh4bA aq!4Ot0J_s/],d 9 |Pcd]4d2'1̱\ L"G@zJ:raRtq>ɾYI̗h([> ?>ZȠ$-cM .cFÓ(T#hLw8#X,d9avgHð&6C.IQ)' pIB%qɵH)`P:䏘mGQe6@XiSŎj^3wiWKLbl+ -9N$S3;.4ј"ls! *|bCc I#E鞊1SdHCr$i|9̾CVͷ'$FщY`8$r =9:df*I2_˳B3 A#l1L ʷvx >K5mdW2xaI6P*o.jFZo{mK75pՅ4U LPlyEtQ੅PR)UȵWa4]A CF`e<,~d _#KŐyJa>B5%ȃho$&hc&l}l -˅l;|Wl(\K>Ƌc,~f %foZht_JQgA#//,62UkWGbV>H QHy@SH#_LH'fB^ebR:KUWcXe/8s8^dm0 -AՂYRt|*d7h<׶~j-lAV2[dE 3-*j[0ײK$f BHxkɅ@FY~L##VbF`ĬI3:&Yst bL=Zc9rRYMfd;(urH(q5cX@|TYA~YvrjI9tt;$ņb"\㍐kn,;|@A3|jH!b54F>', ߢ 9,,Sl%uh-XARnK\N -Mz f\P@֜<7dX;X;(Ll⭰HJMXnQdVC[&|b֣t>qG+$!dkc``Q@G:ctKj $xmpsPsC}gg!&\F) RlH'efE[b,).Ʌ$[C2ZVd-ћ"xxK/J`z '$t)$}8$ V{< gi}eVO:mqӟun&Sc-% 7R+JG. -=R` 1#7άCǧ@FИrjT-O5d  S o5EYd 9NYn )q*ǒe1  `ggK5W$\0~'|"YPv.dd@מy|'5-p Ŕ8RQ x 2ڧ4q B>?j H5pc$ւzY\p}&[~Vִ~^ -2²m%SȐqDql- 4p(c8joVx|4޲JYC::9%|R-i1%|',)Q/2}Ȩ;X|(!#H|fd5.$Sqgc?_j`5Qyꌄ1oB;Ÿ⑈E|ېHA -{rzJe'cvtߐ -f05[AiɇBT9BPS:Uk<=C5j!$Q;咞Y|->J,~&?5PJ {_AxOK5wWBP+X > X"OIқ& ?B҆Cv\hb|+I1Yh7G- =.w<)w\Iŕ)KIK@:S~>+Kţukˀ]pכufWXS`u?Iȇ&Gdhm2G)Afu dѷCd?5$ A$Sn-GW_F0daqc*G}ͥL9(% s8lC Wfvߙ^7A-=g@rQH$.o$OZ?Yǝ/_ ca^j9FkEXOP~)GȩOIE_R2z}襰|!Bob*cLrC,e~j}Ce@1ɓ$QL5B>bj#_ GA6egL䦫-rON횄\ qB*Hnxz1o⢜:Qlxg96໤bB#b^a`cwW_:TjuHא2EBFΗXNbBB zv1ʿ !DysV4_H"{%ᴚ@B(ѩvX;_3!!'.rNjxG ߏ/w$W[X]yz$!w\fn] "'5+8=טlwm"!c;o[>D2xlxgD8gj:7pdgaF:rnIU^" 9Q#?ʅÿ=θIk!)XaoGݕﬠ׆CÙRo_$5fCOg%'B/Fa}`.m9|H0`!p@֮<Ѝ'( '@M5}Q,[lVo,%fB.\t=/ OCҋ{] g ߀+O{BEh'%7s&Ր<0 =,=7@*ug GA6#L.Cq\NkNN[c@D1evi*rbk%BP>tB΄z {TD[l!#7N籫4 Bo A2r,{ !pEB䛐,ҁٍ1@Xk/tNGHlRR?ow[A|N{?aV1,a8 +ȃ=:$MQ =<547CèrEC46kvl٧BV]P^ME {X=ȁIZ+[GQ $7PBׅd=^ԟŕ5@b -9p3|I#HPMw5ҡF7䡤ߴiEppJQnQ}f9Jݵ3!{ - $7Z=Q[JWWH_A~{(dKf+=o=p\BB_l $qGDL P.l -!!rMH_oւoS=a{lXI=GL=:^-<7Ov JީYrFda=Q kK8>[0EԍE]juԿ@x"CM Jn* d>!/+4|FEAƝԪ!{w{j\vոxJS| r/GK?cMDյ8[K{W?\FJ'Ylȶ8sHߺb "w$Dp!!.W25[O<Ξ\Ӑ4ـPצ `X3q梠o6|O Θ;)jQLHn,O_-E6}`tZq$ѻ 3-;uoٽABjg7e'?^vxp6xC􇒭|7HfeS.5Nb\sf\wkZvnX젳=^ ml씺܎޳PJ([˵}hX|ePuk1K NENG9!?0[jYj{5zlף\MnW:q7\|g8R*o.R \l tіޢ7S "z &^XOG ?Y i|HU~R,G@߄`5[j8rM6eyz$0bAtDTp(w"r QR[Pg!]΄_cu*I -K]'>z=`{ݔ?v=?ƹ`sk߇4zJBG+p`;'RQxj${QwhO䂵 TY*&w=jMK|rR_턽|m~‹jYW#3쌈#@7Vйb%~ -!cgpEReWrf`O/aذGv/qV >yjLHʴQX}jRsc$2կWH+Q_}c3&&A/ -}|jTǑG[n./}?SxT&±B`iov)t>&5|V9raR88OU`O{YJBӁ/G=obr|D-^lldXx~pЄoci$՜["1P84ߛ*մsŶϷ(w _`N߱vO]9LZc1[$yS ?ADӳpF@)q<孥rFdj3^P gguYƊ+KAo>D+Sa{CQଂLʁoPa,J#D 32,!rԌh zƩj٩8+ሜ S}߷Bd1;>؈ $ɷ&*>=84*WuSn RVu‘3scq&„#bO; Q;A؝7B*Ȅ,8pCtqvk]/| YJ氚a0.$ - }TԔӎc r] p&c{~bp*}c!| 1@wNb etΠ\jrFQyo<,2NeD>{nE/<7*,"tI.΁gXE} -[YccbBE?`&g|/壇^%$u'HEf P^B =O;cP=(\UN͝.~ҹ?ԛ/ =Q)*WjtOqcDtv]3 :'WZ.u=i<u*͟l#=n6K|.<Gr`pw6:r}11;SQJuE <KpK=(\V?Mr -y'sWgz<]uvooOO߂u_xtJW#}K87=Z6t<'v|&WbvoS^fR~s)jE߼ꉾ};C&`q3bh%KgSу^ mƂ.~,?lv|,XX=LPE {+;ω+k]^:=-=!)yd΀>:7*h;l\.P:̨.-9E$A 5}I^tߔ.ou$$ D9Zx.]Ev2E;P"ydQv(>ٯ%!LvCm1ÍpeI9ї-- ';u3A<&,i7뭒z"i(&|˚/H_tHwTY*r2TvJ|^|v -P1<~ZCktN!jvz)7nm -•]N_\f&zxTNX苐x:,uģ!~mI:J}& &+O϶xoW_h9Yg/骼,.V?B"wTK:]t^A=SZ(2֣Etrj#>:F }&cN!jN?2o~5 ->S0[=CR5UoOU!N5Β=#bAbbP  {4͊l8GGG[„=[>;=7x'}" -P٨.Ci(>*~Tkх=ɻjG/%ɍ:d[u >ؓB0ap ^Q43[wTٴۢYQEq6(ۄU:qև9N¤]ogOPhE(CK_ %:tT̕lϊ MpV>x-f2@G-hqiO^Z skbY#꒗fUG8O,.ou2( WoDeg$%Β:wE$EEW=6ě^ fh5vŵ35ҷW|?a+?Od谨9@W!^/[C. *0*~|%>|tG[%"m&?5NA.~3z۸&zbcA^ZXc-)h1ޯ4-3'u'س>za#zrRIq%V{8'a^uڈkj\E^Ҷ}%1G?u^O={?zUh>PQ lp"^Q坧z/1V/uJoJ:לИw5=XgnHQ9:'l8hG|&REymA1 ߆b"^ǝd]E2`gV=6aws}FC|)e) y%&K˞qv]εfgvǻr%U#k:6J]Ck -ػqh}]$õWqdˈ3c~#$~3{pV!,|#?n7RYddM9a÷#d_'qJW_uWrFpP1ߏu\K̬IHM -iz=#զ~KMsg+D3Īa֌䣝qC?Ev8?K>\GElC+o{w`ך.* _+~igZS# s~Ɣmm7aOG1IcR=OT>4V875y'4fJ:.M]g͇Ke-/G_6G߬퍽TRRs9|sBj`>I>1g[6HtAJV6kydu'ɪIѮڲ]Zen1a1תdwb"+[K.D׸DU48E;ETŸk**\T\ LoJ0~!i{"Ú8%xw ȇ"ѣ :#-%{A2FvP$ÍOՑȀ;H:\e6X+#;z`>` ǥm$ufe> !͗3϶\}', p p߭x玘zunF׭NcQsdQs:טB82k㲫ҫ}jCކs+UFhFD{}w*|ZJBͻbD[O sG+CZ6Q]W_,% !5L]y y0dPDkM8y+l¢x¸ mq)^ 9۶ ٷ-4ů981Kv-n18r,L|VWP!K=ڝ+BwH.oN'qu+Юオ}#"BS/7=SXU`rbI@|v_[S,ȚiG#cXvw2#BC;o]-Z/Ug}#qBǫZ1^1w\cJZ2[${[3OfObCj/'$W^+u?՜{9#h{L:.ܲL2!*i b!k˶m9'sS(H|k5,}ՙ} ;s;r̤9f>zPr^wsLiSEkƐm'"s2=m'mI"|Xo^bdobROM-ɯY^h3!6fHQ+ssVO}Ǿ|0`Fmx]+qYwSc3b/^IК) [nQSd%t˕4Ϻ )7}qjN>yt:ڲ8ަ0=-3)(ӫ!:xWN[Eǝo:ڦA]WnC{bbBOb@Sp -=|s72cxotQdle /aj|d0n*Cbڮ܈!G O0Z{--sʅ>9b1# 1'. (ǢWп=kuy)r8l,UY`>Ps>_;n.4w}Ga~>sofisl``Ftvfkf`1s{Ɗ~ެT/{2跕.QOKcҪ}+/ՆFH͎,bBtx2РkkTBCp8C>- W/z7 L3;0tAakдRo /^Z0L$0LT*+UMLA('i$.icty__g`uW p+b]hRج~|e/^+{\!{S}'cw7ŞљU>q5IL}._΄Ɉ;١FYn^-jb~jm~J^?oڶV,Z )` "{WRw9 ϊIo?)M׀[E`,* %> ſ /X|).?[ػE>/zy;;e^%+]/׆$S_{́/;^p笆v XxVl+L:S97ɳAz`2+7>Ncf׃;Y7F裬C!:'76'%Q~5aO"“m>zSP)K,=֕KBT\xtg 7Pܛœf6c OJi񋀊 -0Uq9 L̞֪yG(ie6Qo=`"ńXxӑaۮȠ} -}e #נ*en17agrV|%xdPTǭlsgsTvՙ,``M@yFOfM7K,jv*gVs{[ۡvuW#Qnvl -}|_.,:P}e+{#-#]Ω -o_~)hT銪pQS~%L@ez2~1i+=X~:`+.Δ? cIH|{Ť"Ībw)Şy%-} R8z\LXIn_~ gqipUplgmsVhx`2TiKU$` wLVlx",[]}\%Y\B /0+/wm|#Qfs,\~ٶ0Nۅ83(spL3g9V97y$``vx֊8qLUm0w&~<8Yb-[^ My[/oJ[ߔ>Tx$Ը% 7$46Tx#Y3tSmϘ&wYcyW9U7oo3Tm _&K7ف5`]=} ՚eNGᦼ@>X;S}[lWG2sݍY]u]ͮ+=`-XN߶͟mx,?(+ - +8f(,-SNe`m[9;}j@䀹[`;X kdO[,uZ%,;X.::]hL9̽`{=1===-g̾9S|CE -@uئg'È={± `ެ`R ,v <xZ^`3X},v1`=S՟0kǙPߢg/=Ooz]TjsɉŰNhre!E_]Tì3gӿ\ 0z)X6q1ˀRAڦ -ϐ۷̞ L\}#<f@,`oKx l-+qRFv'#_~'y VQ-CsFvzԴ -DN1x8Z\p{PXTnbJuAC0­p3 } -[nTj%:@uX9PXŽ{9j5VfmZ.G'}gkKCosOmpKlrK_S֒d wҸ)?cxXXCY}5ٻτ εsys`R.XKڂ`0l:@~Oո->f1W٪p]L&..)'NP݂10TwIcSTT:yڮm<"/9ͲP q5u=-6`XXK (m9%,YO%E`i -6`g -[#{&~fԴ>0QƆ;ߊkʉcךRbڛˉ'Rbc2.D 8G& 1Skưi@ٜ"r/g(-9|8U:S69K 25zx rh"gjO=fVOۜA}xUMsA50k52&ӼOIKIУӭ2Tϡ嚏QZ=5-v3# Y|m3#Dy``:CAX;U6Q` l *M(nr#jŽI;'p/өh^ɚ ~2YZ*FFװu1"62bP΂ڕ sgڨ.ƚ4$7hw{(lՁ.1C~}*̇gYjXy -lz"$ێ]W0e@%Zff-}!Mw(lX=vוxUN9ءuV˜:P_J| Z/bQlj^Dj<#?8iP0=nN=lNJmo;N8%8Dazi\+X3v+u?Xwl<` m`j~' vb!r>Bͬ/թc AæfsEoN?\ ;J/} o0ۍ+!5e$F}|o*N;hq3a폣Too]DjMx%KRUm0 Ser0oNb تkva~`OU]:]V1gDzaָ?ڜ83Gu3\ ;#1X3з bڄ1*f U'<gX׍=w϶f뾓b8'yYvK|>NnS-TVn<eZiwP*zWcmk1{5mY~gLb̫>,F4@y}l׫btm99%U>CFczL^cn` >Jt#ժND"r2_|"j:`!`KZ"pQ5 ~+̛m*< XSW;h4M~X ->łs>1ֆ-42c^3c)a\Lz*`x <킚߭/GƉ3c'/p1?qYx4J *`/c/wwLDJŒW!^s*m] }|F -Wr+r>l*-S:zXl2vRuz[_U9BU.s}U׌VrÍdprW84xSpAܓGg΍zO1{̨1Z7ł|X8) v>lEnM_Q/,JGܑߴ-Gm 苅 FЙ܉KVd)Ø~졂4_N=T˨~d&FÌ)bo>Q_#/M߭ v8gLZK=Oud:%[ >?xskǍo?bԹ'g+/Jޣzn7d">映"0=`h"IB}Dr>7MQ1OxD$eӱKlgc.]*i -0V9/ZA_W._ 1?jÏhr^&&KFO[}%ԃ1v!m!߻ɕyOK.?4&mruCaBGl?}8(k%X4p6,h{K!l$} ?XΚ,\ڃǬ`X_Akt# eмY+=-0 -382;c%_q -yc{ѱf.ĢMG= lT!#c K1%Kh}:d˭DffEƮvzAp^ٓSEM؋Wx7kt''uh׃YbORR s"JN#AU٠9[g񽥢{6v,%&! x1ss -^\2g *sl|Dd=-} x\^(5'| nݎ'<&Ly6E4{]%w7 iry$090Z/c I)h tv5m@ؽB,;'wSl+z}7Xb,j"`y*QveNKD{0[M"a?tG-I~7!U'c rt4m+ 4y/ ~0_N0~![|4u3NnYO_z'(o=VcOV$`umĖ -V6g=T,NyNCҏͰ|,.?b,8qO&L w#~|B*vu<3Sٲ1bjvެf=`txwC{n}uf[f012͌Q)cy1%chtI1K{ -`E@s.`q ysbh"/·89}^Y@VJ܃Uɓ>HŒ}Xz"b3i7G}eq^❥er+ImRZZB27碕!W>6EO:nOkNu,s6w}f#s]ǯe,9^9 M1l6ڣ4B{.óԅ<'RqHOMF{ȫM7>\ň!Ҧr}~9\+\H2[8hʕ<:,z*6YP׆ ´d֨6STx7~V^,ZauoF&`Ƶ0f,8=97c6F>Jjj\˓!vإ $@W]tEC@_\ -]d.7[I|=T^yAbAg-{,BaGul "82IxuIQY``\HQE7b>)sqeZ&׿$Un^SskR7IH_S]0$vn&/:FNr;]+Vw#8p[qh/bIU~ kio -!},+y=rϘG&C<Ca-m]Ig>7-AG5AMAt:  By]OmEJ.>Lzy/.͉Zo@Uc SiL -]Aӕ)y"?*WοM@ #C4aN7T13W:=Fgᙕ¬=Āޮ ٤_" 폧._Y<^hK>z-͢GtmC<_O\~:A<7?bxh*-r86xg7t隊vBPxVrB8e)jt - M ־C@k~`le9\&9✺4Md}\ mE/SwKr{ -}m3LIXtH7.i0-auRǠtY :$OڨMO9B&h|6IK~7\g -OLDbx"C[>X  +0a\ xR^2]qψ :ho\ڪk8)W|| -~>l:~A6C | ~auaOJ<ͨ؃a鰈WM1e.3;oṟ%OVW/%1n庂`rn"8o{a1&1cs weذS{ω|sy+蓎Y([IEqY6ʯLҗ"nB{$nk䯡N*4 Y!8?M=}xPQ j ->SwpՎHG84.QO7b)M}A=vYM\A4!u -{ɷ>Ľoq\tԹ8^p칈xwDOGۍh -7bHŽ{NM"2a<Y짏 +\U#25=\<_mh0m0~:jYt7|X*2z~?>tTIDUœ &fb!wVbVeY{y7 u9`۪`Ì`M`СC@߈c -pz<< tVFA_mZD?`~\Tf!<]11{DprzGrFl YFy&EL,FPIBv[\,6E Ó>#W RJM]V[C_Ve2+gģOx1)cl6-\ jFo -҈Dlx%i:Hw q/.X=L9h?AWV}0,$eoaZ>!Q|JIevO{\6y -b_lƣn$  -8DA?E twey"v,p mz3g%CG8=}Xo܉ 1a^82?8wؑ߭e”=G{JL%jeIM`DP_h ڛo-_5iӄ>9yÚ(߁)X`çКWl%rZ+3wYsM̜5C]GR^h kK{IӊHeX9*N13'q]ѳćƙv/%KI 1pU<0w)rhP's; >W*jijZ5D7ĝ6 wP&xh;B -r[K9HϘXݵ[bah*p9(cCjxlvaGKT448`@@:HK 0,$;ET.$ 69ݺXVoe2KX(=Lj . aq0 1Ckm%4BXtH8S^ga}AB_AfQ3Y§Ĺ I)8H醰:S ǃI_j:'tCxHi3!71\FVn(@5bF!|c5y1}1b=[{iNGQ]Af>4gÌB3='Q> Pނ]?DZtwM`-j2D^[l149| .<8FxcmuU%> LhY:(Ѿ) Gq^' lt )`LXC銛VD,g_[ƲT㥼t` -::tgr )rGW|ዳw˭}GǚF%]̻"^9*߮'Ѿ| =(50| k0*QqZ<nktb)IFHXrRgqVѐk % - UӤC,;DĊB%O:E:Eq[rZjqx3!zQ0'~~K,C {"6*&bk978j E\عigL$s Ro9q𰣒1B - N2 XG `q4P>S *ˈڅtP -` Ⱥnˌr8!j>X-Xjʻ8৽' l0?ucJaJn1~Wd'oBBHXˑ6cQ !SyvʎyfbvTld.@1( pSJH)hϨ9H sÊ۹Gd<<V.csӉ.c1SD!V{*xu97ҨGdTS̻{f'_oZ<s3'ӛ5R z7Q,[%'>=T#+af}Q1Щh -wIj#~#gnf V{}Xj`. sH-!&7O#~)bgay6 -@mcvHn6Ғzo=.K^_1wOL0:+ظ\gcG*΢}He> ӫK8Ehq\9HY* -[tɜA"oАm,)r6`ycJSO6-]tИ4&"}e5O!R|=F*CҕdI2H Q:\Xg@|c+{s=XKƖ> $Y\ZJ!'¹Q䓴P9WU(ӤS+mbs2WX41dΰ!7h-:)AظKW#ĥC2f2;>H/RgмGkN-Df8gSOj=H(Xݫֈ{ <'m, pg~|1o˷J ZM񈢍#a&ZրHwKCDLH"hw|򰼑#m/n_]I .&rMC63Ȩ., WĻ>=iQjga0/?lbj By.>eIQ*u_x(ֲ0".>0~KҠc\g JTgOLΕnD_g6Doq06Wj6g=&#m -Vf/Dgd{igX;sO m!y1j6"E蜨)kױH_^pAaJoAၹ+a9먘EiuڈKeu"#ڪc˝ǢgH ---_y5q[kuCwm̮+'^@k|suLüuIV9 -圬^1Eby؊X6Sc.WΎA96EڍY,طig#,{M{GX {jg'al|HpJBSBeR -m(eV1vMlT"gBLo{rF:[0NiH5rrj:h7XAyZ=,L'&Ҷ?YܬPR^34w؝ YGc{.j|HyNBQ⓮jgE/ s 刴Ez&cN 酡8$;?OH;e:NꖾPb'3{4Pt t,%^/qw"kߑ/wx8~~-sbV#&OB[=qrJӴJ:[7ew;߹͚;/}44n)E^ًϏAk£R”QGYB etTn.sQC~Յ1Lh'tgcO64/]KdBDh}Q-tιB`jg (%hD.G -8|_lQ|X;v/u/>7q|4f=b抽 It=w i|sA(o\bٓ]DֲmIu:VXܔC2&7R4PNE=&FXm,%hZ@<jN:$ -p'7,YTy-=\N󠝥e(jƃjSVS,%֣g,?va--%ہy\8&rO4c@={+G-bt>L%=<Ǖ%r/ET4E1[  e%?@Pne -@FϾ -k-E\Arrۀ>xPm|F t ' -hsn1e 6簇1R|4hR\IC|.e4V¾-T; ,E;˳jg#]\$b!CokJRY-wQ}ke|SKCW@3G {8_!ԯgȻ94)uKSK*k2ԗ[8R9'8>f0Or},jg5$TtͰp  cn-wvKJ1RґRhXCC3ŌˋqgARt b㨛(C z9./1Qzx-b=fHAcEV^߂=wq)WdktZ!iGKFP gPu --&aErY{EqN~,8[M6MN4pǘol|{+]!U |2rlTĈ cb{@_|#zb\ejU{~HBS}Kc$kTg5Oa=%U_R⩹"UfpҰ*CS/,pDw-`z, :N}zu{"U]pgRۄ?ESG`|MXʛ OҙA?N@GLvFƚ>Ƅu u)6|Enhg)gť>z"T6\,9_NzVrrMzޣ%d[1[IW卧X}h9 i亣K"˾ZUR|﩮-\vE U~qŵ҆佒Oτt>$şڹh ɇg!z.mP}'c\#,!OBI&֌Bo5=ttP%'=#k-Lo.Cf$_f 4P7 QSqB'Y9=&{E5<8j[zxmxL:>˹51|"49-|ŰqЬ}.S DNI5 -$n xEqz>:~tSNBMC:,)׫5^2sը j|A 8%| 'vyu!B igˊ!yB+MI>2'jgEm#s* -XMy߲oa4ubT>l.Rbc̫КUx E|mН"sK"|YJHBq䅡v9a]J8;,yla}ݙ n6Gک*ؽukH> _$'cIr}uQGМ9r'CBLM5CAw#q2%a|؜;Q d Rjyc'A -&Q99M9@rYԢ8{gmhm~rI'g9+{AGkbѩTB`tPF/vu֬!j3СVa փ-@  ,.Bž򼻋f1Ƥ3灥Dd} /ңkBĿ+-gu&+ -+EaT&2﹤8I޲xœ'P޿{ϸMTn]XS__ĕ^Op3˥XD}.0+ޗ2{ ' q+EVçҵik[-佈xU{2P NgSܲG/{44UpMXc{l&Vd.2G1tP (z X$F#ܔQW>#F#'E{gĞ% K_ Nвw qy14~ 3y6]G$ 2 y]romrr Vs@K%qF=J S~j})T=k~חb m;jȷXQ'ϿOk=#)RO_.ϻDsszQ9g~\1T{HB' -$GAP6|bGf&I5ko/Л|p;{ aGu>3|M 3 9.;p[yb~Զ1MV-;K@O lI{'aŢW.rgoH?9WF8WSxL]h:S=aW ̧tnQN>7꼢fBp(8zA8sX{F'EA-d3/8=uR=1BuɽҵR&V<5BE6F{` -^G Ov6)f&c+tA_#{  .ƥ㳩&8f>d"ӸI 3gs^#aKOTcM:;|aaWu*OeF;}80GhFM_z|=_z|=_z|=_z|=_z|=?wl[gZۤzbK]::sW{Z`ur<mo^\oko[y,7m,NכE^:o΂Ezsm򤅍mK K-~yK}?o9,Z^p΢K9xz1}{7qYgCLg޴z#cֻyr-4.zi%u&n$_ͣ8oƑH((,&M ĀLM3%4"2Fj$ck!h(l=%?s- gAX:D;Dm) YMQ  ùClܵ fF޽I])C92Uf]Z6鰕Hp [݋UbzK.AEϨ!i _.6sRGHc!z iPIIJ7;p[߱"7sIkĴ Icë%(JX%![ȟIrQzx Pzŏ%:dNm+FaЦNA9hUdC+lhЏgE34BpncɈؾ~G/]dQCA;^ 60[S8b#Ж)4(W|QKa M݆&2ƌۦf |ؘKM̘[h [as>׈ Ra<]I@XyFF.9kh/rJJb/% >Br#|0|#HT9G~hK&!%8$Irǂp¼__J'6/"+&p2n_\P9chs#vCJ@pw.%"NmGAl CYchs9)JŠИL4|ż&1o@,#RoqAG2Cri&lͷqGZ\i  ,>62l<hic(5bd2=dmė{Ak8|?m9p*e l'qڀ݉($#w 2^9F1%kN;_BP/H|AկCŹtn7;`!62.\Jxfmtcȧ_Ļ$ 21v}u㳅sN]Dw A%q d\=I"QFo2|g] N/EB8ئZ tMdhs-o3h8qO0 ;/4BHRD$-n|*>6cIr-HY~Ͱۣ _X!O3j$3NÜr|% -7]4I%zM(ß 1gft6A&=S чs3A䎆`v8\P7x(@ M y@FϹG@\ LJ - !qWpj$V >J$ߠ;#(IEqj;1r(R/=R=j[[>Ch,`~Hd@G,//s[+麴I$$opO)a HMf/9,D~GEULIG"~\ qh6Om3JA>m$gtBȑx` H\VnNs=\O'3 -0;$։[ -!w&(3Me$WQXi\sc-;k$qqh L|b2ĸ30An{īNvnd{x3n7|AhNB^M;(<{"_wh4!}Yil's֣/i@UXħ\{& oF {@MBrq HT!S|b0 nǜɵ F j+l #T]dApqze,Pl'>hj2zf@I#Sk:()%q |M rw@|{b{k.Nkn `+Qrۇ`a h<%&$0GuWC鏹ۤ> >y!qYyk#`n٭ ex;M)?6O| - 3 %sH}$6#9%c&hPL<|خ_ob"_\ v>1eV;{9F|,$E:DyQ_k)K3gFܱ#lQdIQ\#u?LJlW9U]l*\w{>5 -NQk!#w%U6R>"?w4!le֟(Y,rE=! .v_tPܴV([ "2FWB!/D D\K^=C?=O8˺XƠ02Į0)9㞾"rN?ԍMb Hu{[퍜dU W=3(/& Bz$!ul4y~Gq[`_&+C;#i$Ԩzh)hQ @v֣,S∢o}2Ph,|O 34Q8p%<'~s`њ>jHzd}Se@"-Z -$_jJOS}G)J$͡b@hxd*)yuנ"+Mkll"(A rf7uM=CQ_D9?1B4v9|Kx!N gf%/VqY sa". -~cs+!LG۝ELFrx޷ք1{PBIF޳Hax_.C[S-*E՛ S삷 Z br:ag0-wF!?ڔD쐒s03;RQ -pB )qRA]=r,% /Ty:͛k 5|5 g OςOT[ʥ^%æÇ(HmC )#ʿ>w +ȹ9\܉[ 4Ԯ$=|@q$ Bnl -+I)Lrׂ5bӵAƀH&q +.&%17o 'ٔƼsş:0ST4Éy [!<1XO --@I <@p)B.,>Bm$W #Dآ?|}12E|9oB 8s'_| }F endstream endobj 28 0 obj <>stream -vbw3$_[ZwLe<+`%nB]T '69Wr2*v@6ݴV+9@:0GJ".3PO" $-.fiLui" yC;lNIs5Ki@Bqs"PQTy]LyJk*[*v!}ߚIl"gXk/bQ:'~ v!ܡB*+z.ĂBqZ\C9; @r/U(It\)9^)w @ k ,UX-|l؆/f]׭@ 6q Wu"k?{lM,4 ֳyjB[`A.KV.ifvۤy5>Q@<ĺ k.=.etJ:@yw_꟢+_bJרA~ؚ|кG -~ -B-$֣Lݽɷ*3?.J׊7~viQA/i$=.0R?]]G*ݨIdۗM !%;RA2P7djwqG +/}-Zͦ_OQ^<=Ct+Q?1 -9=<ȩ!2/+ kE.خibjo1eHޯ<ȇ)/BuFYܐY&ⰨZ8@,WĨa  LIsz!j!H - 37Vty̩؅f[ja %\]Co輸״>ݫ/ƛkЦ_ܴqDg ?@a!-2/󮯠~Ne\\e_[ H)|}~Ajh^uTJ_lm/),oRVXH:M؊ '|ܡ)T{]- -cUO@D -~G|*v#v!؅۟bY+-I.քIgjs- 5CGh5J<:+\{m(ui1$!p,[յBr47%#*1I#!N -dW3b\Xq.`1o%jR>%͍Mk7271&#]dK.QòP@Xɽ7'./(ݳ7Ori\TZ˸X"}F?!mG~of*vzzlF /b*ő9 v Em͂' ^(F{>˕ չ'u$%ǞUW@suqs*T;D1S k^j7b|!E}m~JeuPRϾ~jri.~<7œWb3E}XQl#[O uD2)%)Q2!,!")LH{{XVEkڵlU_Ԟٌ[ v#B;yaSKJ89b\)tcxBᕓ|a* "5AWJ,kȦݞۡkB Z*rtI$iRX_<׸՘ l9w(#DDׁw& 8h8YLH@l=bh$Mȉ1M{&Aػ;Ufh1B~ǥ#/oI^p){X==BKg*DuuE -HQ -B}&tEiXPG fA\whӇ}ػsT_:yj*_a0ಘj}3̇@ &y_:*rJ~9{ օOPX{b͏qc:{{[KabDuYh#Oxܑ`*=!fpP>f0t֢8lCJtOKz/䭨J3(K(^+g@$؇|fpz6kXcbBE?r" a͔^ +z(Z3l4<*z:ֹ"&Q5 S'@~a /o[ qJaw05pΙ!h+ NACr. j -O +QWL )~2,ɧf#g0UkQ "Maj=G˄ki] CMqniX+7"=f 1~Y/`5 an/b_o5-v% U8н)b]96M/KEg|3GVD>] -H}#t+}&M?~w -;Fݣ{QPGY:쩷qڒj>!>tQI4/ ɉ33S7b_R|%dOLP&oa/|ՅKbN) 46W&uGԌ#K'PD♢Olt5jDܰ/z~u!byPaOXGb`lهSL  !Nce&YEJ.;CYq -I#4.*;כE-+ݧEQ$f-:*ɏ-F #ŝɧKk>b([VN"U9Ц4/(^.}V.B+kW4:8-{Fh3;7 9*KнA̒өBy|iZ -:qkyܺ\̻ -/{P{RQx3bʕؗ(~F4$؞z~cȕ[ϕ<_u< D\yJbem?ski]O9?SQٹ+n_KΤ#3h-=>~ _ClŹl%kmkʶMbuPٲq a'kEY*a1C;BnQ'Y7p^[^tHS8y*7Yd6wV+KNp"c~e$?a'#űO&*[^+73/G54[{9j~5X~g^v߬W~gRm.ߡ<ޠqUo6 h\!HqR8Zɗ_`_|V-3^Z;ުunז*H~Pofv1_ڀ??b]|쉩f H͸E!J_ -7_;J5;k-XG.[>W.|W^VlpLv["2P3:2U{,>`ky -&[޹-TtmduK۬īOW[\dE~{J]ʑVŞ{ ;m[m;ݹoݸ?;(nVq7Kٵ_ܣWn統qb# k[q0],SδY'[xkPDuI8G_cYTp鹵㟌#ӝsT_mG];\.ŕwؤVT|!{'N 쭶m\s- cKͼNn˗Vqf6P˰I}9W?m~ -;u{_ݣ,xK97G3]_ZTe,ߖ%7:'UwX N͇~ҕ&g6Q4XeۍX7LD7^ߴTvSyo6۟/?^ [_pEHjSU]˳3En:][SyV7IζIr?ݸkmeUXfuQ|p={ꣂJz[u$KvN@[vO'n'nVZ{Vj3z孇w,*^-H8o״ؿ7m^|z(U1U|+k^&^Yvk/Q}u:\}{⶗ײ>g((+zqa -MVv)/t9+e<9˿qgv]w^(|ۘ!tI/c|٘tn87S٥ -3gܫ!m//dkNeIㅦUJ݆mst+ -Ol- oJ)^ZdTtgo>فlw3,}鱿b"~FBٙ|꽅\kCsOv(C[V>)K?)r/QQŲ6LY[$\xc-Yf\yʝs?ʕOj÷Thr6I|e6swkQQ䓤'IE%yQOʔ51绬W4dT63e͊N|X՜L͛{Wkg7{K-v?n&~j2ƥ@G.GJ]ٚRwyÿ965ڽb}&{\a;RE)wTJ_ar#m*l+ת΋zМYY>$ =)}o?lNmxZK~kI?Lm`femhVrm\[Ky⇦Wnq\q-cYpgߊ⹮O\/YoP۵[-Yxſ||$M\~x&z0mx`qK/&gUۃTwaM)%ͱeګm{y9[^˹_Ϸu$ǧ#37qD'>[Ӯz!_o>I'Ri2B> -. ˎjHU}bR-?n{p(yUytaMU˽$ΚLnZyŻRuL:Vyutk(D&'?VY?n>sEQR_6pފB[kgkI#Vo_x3]Mksק}۲ -> /r*Qw= u:^ޖTќ$D}1  8]{䯾Zw&( u^mDc,SGz-z"=|->hcnk]'dץ4D&%/߄0ovNS򣸂{Q9{Ed<"U\V[HMcn#F.R$=N>H9|KPhWJ2?[y?^wzYK`㹊?^v0{.w9̼Ć܂Y"rܛKLG7x2avҽe7ݒnn}7gnw{5F7F%Ȩ[ڒ6TFݿoj}79v( -|UUST(,Ϸ5L!Qhn vOBo[Rw78ӻ9yu6oؽ:,i|YHSF\ٮ@u+uv<ݞ4Px7?'6 O~\%-oaʁqkW}GCrL}BnyM={aLLC|_g[֮ N Q_{yNhoCޏȱ}Ib{ yoW'c۞Oq } -mn{#Zry]hSnJi*$ސxh_v^^e2xv7uV40,YYd53oя%yKL@voPTZLYneߎPJ̉&*Ox&U}.*H+,AXr",Z!׈~wڱN -F>/6VYk(231{2]f3<3~FYFd6KUaZW̔㘱1.3Dft/=f\)9qW2Mݘ5#7][OmݟݬF޾q&0V_ua99Qy1 Ea wr1 SߏxE/Pܦ6=)d.̠dӁ?l -<4bQ ]äz~-68۽ Udѓ=F2:f ӏOOHfp=fR3'tU^.8~C[F_wcJU1\l{%Ltxq$71)nTV;j*oGf Rx&,c;3n?({U޾;[/}x3ѹ4Nt/^qckSY157G5WK3e0L/O\h~yf UOZ߷:әy DEc3wWyԷ9rGgݎ{;2k߭7ò -Qyzhq27ՄO κ[ېX''s螎$?ۓU񏖎̴1ӈ#sCJ^fyL58zhҫ+qt>CALoA䧑衳91u?;([]{SYogT ϪE|L{yo.E>Jɇmy~:&2yUGG"֔)&vY}c0zGkKϿ79C|Cia;|߮5?]~#\3*gwRu6$t\t8Ք$;ueMb dcO_Ⱦv;|[!Y-5A9]2O5 tuﺝ-~{rc;f(Q_n}fDcf/_uoR|~8Z|9od_{^p9WoܹsfhV8uh}ZnJm\Nr]Lve=ɻՇk3k#p{#VjΝ9`?z4עvz,7}:z%ÌfjOfƎZLYb]{痁֯%\Q|ح, y%^YqoSVj<AHfM=X;Ihͻj/^i}Əz{?_z{;ӟܡ`:{{3df$fhYhM̌!7W˿DpĄSm凫mjc._YJ| 0>[oۼ2ӻ=3n /`dFk Jg3zB˙1ÿgF\[̌Йnj36`3 $]L^YjY@4n֣I.ҀN\*$ٝ"+:ͣЊ !{67!ZS׽d`Fi1C{"? &OXK&0#{Mc 71p3z:imf%2_5Fx}#(Z|Ko=Ww'aPyw䝸uk|Hݫg]?&|D|/#|?(7d_Č]Č3V=3~)3f3f)3jzfwqS9faC`ΪsW2$gQr'65fٹ5j3s{E bf=u^PFߺ.sJќp9C ~/ =>ghd03Rs434fй?1w0ّ3vȌ`FO3'0#Ggf\fE/7붵i=VWTx)!5 -Iϻq/PaPM~y5= *&5{rb՘S6 zy{=!Ìό85lݡ[ƌ5|3bOMbMs]q̢m't55K?u? zjD[dd= *|X1y Kĥ}kV010-!y5̹ZsAd >gk[YBs5IcV%nwk}f:_LW3m.jHȕٷ5^{nFȈ{ϰu{R"vQ{/(vAXkU ґARl ņ]cXc&{[v.9g=?Hu͙k˘?i^wȹpin\/F_4tӢw[=>3,g=n7{D*{A Q䜆{'!۶ߌ3,&Hj:̰K9iv,3nI3s53ֽ4qaFx2Ťq=cxcX!p5?:;\*|x j 74]!uÛytRÅJ#CLc)͵#eJb#N Ϗ$9f@r~VNLQ7GI@{|ǩ(f8f"fL`#3ʷuإՒÆ $}uMF0Lc5ؠ3Y:f @f_fP7f(OfЌf2jYGMl|d0d-,[ϿE=^[uu}C'[vF>S{mA^wڿUM> -'yָ\Ha=qƌr! N|Rf`?IkÇ,b ]8 reV0ffjfL_qxV3Z.8mˮfy4(8UQ~aM_^.zhK%[7~ S|l :N+I_=/ a뚫ٌ_ϫ'Y^ҒfYC~d #x(fg3%;QŌq rT@̛ : -f5CÌsKa0ӄ&yG&꾱aR  ˣgkķm길ڦ'C~c͛9_ʮk^9=29́ |ؙNec87\aɥ ! 6?o~oZ<{/4L\~C/D?ܐe95OvRk7kxy混}^$߽mZaȌ 7=ُL೙Q̔S>3seytzy97-j=kf]Ͳӆ-=hpr}:}a}C b w5"gy%sm -A,cx"'~be.3/_ҘA$  d/Kfqcf7qg<ۆ9n-~on~fo.zm×6"_ !ޥK?]- {Wƫc+/K| -lI+HsP쏡W:1O`SUC{XL=*f%LU3SӘic<3~3a'3e媍楞5?ykaW5?^TKo~ Mrs RAP2&JK_]꣭//lUpmsJew6޺VVwF;7U=<#j 3l\f fW<•1|fa\k~jA4[borU?f~J_ - *_ : _^4x|2/+W}yʢէ A7oc_hHS_i T0bDXnvC^v.M'tO>6Pzh:w ^Sf)a6cZۦ ɳHg3f8/}ot)5+jH -! \M!@ξ5}kS4H M/*ˎ:(:sVGb, C*N8ߟw-|NRob&Wwn -z~p^5ռЇTg -n5NO>C\}wFq \?˒>&Ù;QÌLf\zѡ~۰'bs;,h|O6o_^1(6R$0UJ3Wo9+|\.=~<:GaòLc 76֖ 5̓}^|/.]}FF޷Kxp{qw!:S9U{F_B=H̘2eK -Y~A!!k]_J_ޕo3wL&0),+okEtd=i U]rસh f8凿y}%rb݇_b2>΄r/nwZ-8C^B6qUPVfE)̨!4Ft?ҌkBx6rPs) K|fг}Tuo7ޟYpu/ſkūn<-Nr9mQ-2j<_^ʖkUlnR_[ +B3]B 1͵_>Nb@2j(`0f2\鲳⃕o$Of^=ѱBx9XoR&pLUyc'3K&f1!pBaE}Ob|aw+G/ޗKNn>'7 -eb6 ?U[osq yBnuǣܾ[sT&*:mCx`~k9'}Rf k!({仞EJAO}b z]|^>G00؝Obqkm vBI?u< -DBh:݉a7BˋlWwv?Uu g& NV?\mS}e J-avcK̖.U0^J4Qoa3~:Շqx-^ޣE#'{z2n1>Z3}IhJmHIǘв ҕG?c_k?[eF]?2W'V[~w]~*}%^̭=0+>_>:l<{5ߘvn`O\At?tAWC?]CE[+w2?jz>[?}QO)GΝϸ,1')^6tUbM|pO#/KG -jakTe0@*h&2[5P;7!O%r~TRuO˄S q.cS -i\և.¹w*c=]jy"#GS -OZ -Ɓm}N5gjUflH_i̿R{uü X K2էWkk?-w;nw;y?|G;_Q(:X,^/Kkw:G-ɏw]\:PxDn[77"^:{i*mHE}ƞ\6D+#-vUunKѽ3H4np9`ৠbbbM.N򚾚83+_ nRVם=L_ 6}6S+_~x48cت0g:~_{cq;1`r)N#A~]%G:ZV?)5 zN#W^cG0_a5&Bqp+Q'<{'z[ ]ݸ=O/" 373k9$}n;/hW%w^*DU4wZu7OߖqVR2$[}tg o_'S!PQ/u43efHs8/2sdS.2\O3u],!sTK]o+$YB+ KmzՌɶM GJGZA\Ep>N`Bfu?9 끻̵gW!܍Ta/I%G 5'|B푉$OUvջ*;S{zy!h; PERWat{^b()@w?;`!`7G&kvޘ#z$| - ?yɟ~)+렎+P^`S6󻆂GR1w$[!G'з"t --2k#$2P#kOLn.ڟ.|'Z΃ >N> "Tվ7B-=o?y!?UއM̷yz L8ꃱk3T-Pu:+0=~3~42f:p,f匏 ܛ6Iox{*o_% O4aA6X`|1"4q[/͕:,;%[ ? U{`btgZ -nޟ7ߝk(¯~Y8*H>"B}^ ؚS\:^/PW7Sƾl2,ӔKhZg> 3}8feR7 4"׮?AvS讁C^3A -zE Cn880B(4H(mu`۞,}4Ui$xbF+hJOgRf g FUǨw};}p;%'wn1'2TXmVYAA&9 -L=uH8{?+V v NLޟ #>|r-_d;R,;n4 }8|V;y;B>H?+Q` vE NDo"ׄK_.hNm˖t WUfb{Zb5 6˜rZnVw ;d.[,2YЛإ TZnF2i5kN1%eDdY3SU0P;8:B#E뢳,;G76_m_w4h~U}R 4#OT꽯T\Ɵ~-h)㺸s$أMbluEd>.)6}2oPyW}_-uv~6P:U|n>,# |Pe0W.3K\2lzkeo};U'ư[퐋z.fk0 FVG#{}CΦ`CW zxuעCa}DU~ ,:hqLw JH%H|i^<+&Q~BF;!Pb'$C!TT`,fۣzupC*1jՒ: Š]^h31$E8|V{N:x_hP`=.{ <}o}}b΀a&A&Z*iq@nX~ AAM jh(\BEJ` ;2Ü-ͯ7mWiͱpc$3\x\}8 VPHQGyILcDC`)`ɈۉTsM8k\}d2e;2|~&}.ObNK =yȇ(XKǟ+t/Xrz*+/z 즏a~1[xnJfy tq3zĺDceX"pF 2{*op唝[jVtKu|;yTzt`*qMg Gߪc?~Z u^&9eG3AH#zbA'YSUklj -,㉯Xg'740  1p'XaKġ12J9UeyA,nxH sad t(3*C>·K/@`xa6.3:|jk6$Ƅ mcJvybV{&8H$Q4{!MC>x!gA!+)g19JRfd3:e\T?Xa,NcI`F i5^j)r$ IUƐ})BRéyჩvv=6Ki> ->xKOِgcM叟W%O5<8BH(e+Mw Rj`65?nȟ?~4QEduI&>vuV\䐚dCRT35ṁ2jo>GwTk>Bمe-#ɧg*1xkqdMfJYCNkbNļ: -GƙI*} Pˌk*K/+T< z`i6J$Ò(tmrAJI4Nll\sp2=ڔ5} } ;r30Cnva`j{\2 g'O"#Ė[q+-4 \T9ka棐Hθ̟:p3Hgg kk*ΨA'>؏~,fRٶ (ܫP_4\ˋےaGj ] 'X[@AF=t4qZF-MщyRRfFJ-WZ/.7~8v-D&n:_l/v ͵]Ƿ: aڲ[t -X|ڽwUoUlX)9>Z{2t=|W~4w=q,cĘ -K1В#OE\iT" XqQØa- l"q^T1uX>O?s_xڟ ;\-DMVT1,Cp|cˁNN7_+}\|뼫麭~"4%#tEuC5'fחɭ7܉mМ6BŮQ$M<*[hbm{IݯnuR𠅊4?fXF k;.چ[mC/g-\P m-w]׈ԵJfШ֟whivTs\ m-qg-CRlzgvwj-ɟGP^ߑ -+^Gw!w= -Jztऋkov"RTYp6G}`|xJ!F1"n7x|dOzit}tY`|PjwKSzqt1sO)ŭ56d\B5]}_] t=X.7̧ڀ/B?B+X{Jw:xΘPo91};wܛOl6;n/@ ,u=``O-_."7t5#츹PP\Ih}D㡏rzwp95UfXsn<1SsG箠i7>R`IeU5]_+:^,*ǕNoBH)u}5֨uH uD)mS?HB3؝J6VS1U/U/5NJ[O&izoX6w BF|zj[hIhgq[I^vR{Aev(^x) LA8_]A]f 0P[E$ނO+ h niJ+l֝ThldZPltY=,nqcD}`q2|nA] )5e b[.V$ڋI^W9@aq|:kuxhJE5.+Gf#6kRK kз>V"P#qWj qY?A9uzCPJ~ڪ3Q=$1N1Cʹ}(s9{>zG\g;S]a`*j/qݱܮG37ϝKםBu;_wnt1Xh>zGb=e<_UI$ 1SoNTb+D%!.Rݬ~J;0ɳӦkHc\nEW/M=|➯}v@;kE:<ڲgŠqlu1V8Lٰ s2m)Sy12ЀȨ nw -6NWI(fm?Yܚ^עC_,?`3˕LNӶ,uS=lO}=DvTg(i(X]vwԇ\G;jPmBhSPmxʥ3lO,m˩9Yb[Кkjȩ|ZS>I."zA_^|o|4^g#~ 4ëĚ}ƳΚ;JϽxZ/%1r;?o=u'_P@!H P{$^oGkNL\pUd)X2usBI MDmZAs=^l]$W=TsMEЎ&Yhg58{a}PʀvQ*g :]0!m|YEyjgE;/7NK\mIurR uбorK_G|!WMLsz`VþDbtk ~'Mwɝןދ4hhjwd3ͱ&%!C7+ 3R!F,tJ;7!ϕsۇ"Aۏ1W !F*!vbJuXWZEtu=zmkM:/.$ t~,MԒֈ5 -kD@="%I[kɲD{GCS6os- .|uv[$'~`v͆S5Mm#]w<k`qKvyOIwܐb PSd+¾U9 --TAH[ۇ O4ZN$fv֎;λXKS; V?Yέ^쿵Jz3E|ѧ#1w׮C+6l8rxv/E^a ^u|h0frM.Wɫ3]==+v,Ifq=)Jq+i0J)#ֳZb%J e(/0*FGrԗmtTqP-ʃcǞԬ܁} 7@암 -#@;8!G~ץM/eSjF9"\H4$ .媮1@];Y˭%Rwd6u8BBn<9SM ׶o!PJwo.es.G$ߺL_i.*$qHp홠%OLɃ|ۅjgQOvU,?EmTnMv/A?Y%=YJS;+c‰Ԓ9/㋶; $eT8 -CVp|t3PdKz8ǻhɵYQ}^modBr!&_Γj5Z'ħCM}4'5yd~/<'P1j}ScuDox_a@w O!RWmn?OabLCN/+IܖOKj9ja󉛼?#=.BO?,T;4MYgmje_5jm+6 \>&8[zv1-x.bz\5'f-8XׇŘB :RY+=wh ߻i6Ξj8ǭZխrU_]?pMŊG16B-Fƈ4?SdTaFbx..w`h'KLvdܫX>_JE,t&Ju{~HA.8+hAc$kTgkn4q6[5xzttٛS}fĝfǧkH - oh -P<3ߐPMh\&@  hzCy yq_EΏXj:MhX4Nn -:_z~_۲D'tO2:e;Fʫ*>DH̷})sH`w>wvD|H{GKT; Z!XZO쳲9_hghgɛ?#6]A[I69\}h uyW;FQ=cgH[/ΣZO_HhUuOn=jOLt>]wj.{!*L=8kwyEtm+\Dj,Zk;>EnDre$*Rh9|ɫmͲev|j D.z(HIuضcauc`]`T4RW4]6hnGDŽ3t;W_&XEè %MɸA5Vۈ4G8:^zmD4坣-m!\Wԣ/ljRe ʮ3r=ۤFCOul;z2GJjg9XזryߎZ BzxT Ry5x WA|r۠;EXCj;K5 ܵOM&ȇCO]]uLRϢo ]_.#3|Ձ%9rt^7)8Z37]Xn֯4HNM5Ww9BwK$gc &Q 1KKzQ-OY5Nз%IT}f9s ,֢83hmzjj?.|0kbLK62k*(^/4ZFBg k%;<&$mСpiӅܒΣ=t? -c&q4ڵyϗiRJk!i2`_xECfCtk!՝>1Cl|횽㩦94oIGs>@Su4gg󻞸sM]oqeV4&?8 ~&S7Օ:{BĿMpOO:y&TS*Ze< y2ŚDe6 DC^{&~ @ۿ\$n8sK\$x {= -b%g6DΊ>%^B h֫nth ^Xh=X NL -D2*8'VVȫ]s=zNӬ?0kQRbK5HF嶺i<73 -bi+M`O_`ɂ%yh'[aRnx4~ u0lz/4ȠWwd*[ NFw_NxZ/v^C=ԪnAo{~g!|f=%XT㱛v5tbpj}Wѽ'Xk -BrǝlMf{:$vv7nFb={6ɑG`͝=c,3Vts@NH̦A2?e췪;Vz0zləb%Ҧ.O/+F -v:r͟|bE0Ǥ.G~ \۵\ӕE,)gYhKᰇdJCW[i VZ$wzh2l\ָ)MRVQ_.$>u8]#57p/S1AjA.;$oJ Զ1Mm yZs@O li˻w -5[vFIu=GЂ`O={%>EnMg'T[:XͩOp&yŚ C1ǥzO:WЊ:;c rJyl@Əgb -ϕk{zb0yNYُ+%94;N$6Vv8hQ2ܧ]g: u:?l8>^w` B7/EsrmtE#h\>:jsMjdD1=7{i|؊N2N%_J_H{ AGn,tzHH#+mtx?x?x?x?x?ر CSCmDon>sUR#SlmOwKI]9 'fEVB>?mh$BR"DfM8M1HD -f&DǰbJ.4DJ4|L9B1oIy=U͆0#-yU&:F 9Hc$@ϽrX:lWAG(g~^T)=J*h*U؊u:{!w.V)К2;)&(:]L9Ow@>5chcA`VE)UF~J-ڒ] R%9k(x :G՚xN);(t*Ĕ[r1lX -K _ioA紸_Pxo,R0l 2jHe֬h u }y;!F{FN7tIBTve.ZV767R[7;Yr;k27k;b˨a?R\aiJ0 [k7];+HvL tP*II+4g'R2tA+4UmABW< c"'Q"dlR`NJ}SKl5QTy5r\b9\ >`*F&$^򺪃55Ǧ26 |K9:@"BjH;JaڢxyrӍE` -z(BrZGƂ:N%Ѡ(P%b"3Ȝ7RrzU|?Zň^+A( !ڈ9[YJo/EPRL RX[l"iJGblVo2+Vd?hYJq.e6>Je.T}v@)]p6.12%Oǒbqd*;tR^z:Ny5]Bd -U쟠pjnZJq9Nn"k%>LthG*+167:a3~Z*)9]=357ukS+iRl^1ϭ]{kc@D;:Fj [_ۉ~ɅrQptNt#qS7A+12.59n&muo]WAwd,a!> 2o.7!`W -_ŀĊy&_ϰ2y=I|x2ā^f6t og?BdSh->dyi._-tGюq~e֚ څ&D?^ 3֠c&ؒ I6PI #ٖ#hRJ72*̳@7r:`[r,O2p0yI+E䰌O_{&2'/hCЄkjߤ]s`|.c(XbT"R9hV'g @1@<ۄqZԕ[BG,: -7JX#04':R5Mݶ f cpB>C)9 TLF_:`kvYWw.a.IN*9vܖM0Wrs,ѥG}* 6pIdVP"0|tz|FPWcH\XY` j*eO"1FE1@`m7c@BPu#ƐSIU7ҘjӫS (S͠#>s(} TVd(mI#:wi# Lc %9tGE?SO1)qݱ bDϤݔB,p@)$jᠭ̈/'ׂW~_ŔRQ -:![ImYHm lz%w%6B8}բ~5c.{Km@bWµ\~/H|ՀiICtn7om >/nh: -WD;J9̓N,9K5 -t&~TttkWs:AC(OƓ2k_d>js՗_#0K 5smi%GA7lt IY\2?(1BrB:O)> -RRGI㕶GR1y?:Ca3"Dώ||ڵLrHUv@% ǧMUj6R_If? - ;2*̑RfK/eݣA?]\T24%m#ړSy,.1]PT[IJOA͋pmI݂-Y*a_X!O)H2RXВVxcXm!ف$Pyig뚣Xb 1r偱b͇A hĬ{pP(kwA^ r+R!qy"(Qy-Z28KL 09-Su-4R0HcIs;?~O*$ s -Y.oEIUw9 - 5#~>s eGaQLR3ǙfI㡨zC傓iGd -$J1 1I% 2˜'ZOiUu_F:k]ɒZ1ZB|7!u{J&uJcC aBL&$/f_u}(~Bh)dF$xe-*RL6!$Z!iJaz dz#Œ:] UԸdU 5": -6 vY$y`RR?l0qb$i&l,m#y5T (5Plۤ>-}$qӏ!Yf(3͠4+EKE)X 8 e K@VH]Dj: Í(QO\Al72H렼sP@㣲1ِUtuKNRG| u#Q_ -E9pjFRゾ  y՟o E cq -*B7aÇS>E|lQ:%kOEA &z/m2@ZA4ԇ-+@*U'A[}$Xjsj{uChӉor1U=X뙔P4JS{9|sw~˨cӵy @3c+kn"Ua5g)o`BLnoDžm_/4Uk [{r&[n̗?pۿtVHJqpԥZ%OJi~Lu'&Kg de.؞zU u?ǯ|COѹIC͕\>΁i)"]\ -"ȍK/ -&lФ!_85wg9(_?0N ]SsɸiD`R#LNŴMt -Kק@~>&s6^W1ǟ`Ҵu'IX 㝳FVn]R~(M<@$ԫr㑼7_[VEд&v~,u4u7A -7,L$='s9Fs!^i{˩D٫DfҖG9E d6Cs\ Sl@"La 92!p^ćm@MHz-O~ek\!eGb.GQ0lSy? H ԭvޣ nR`,FSX<8gWa"sMIrӫ`2O0} _1d?M[c^[n_ ^7)jZR̝Wri\8۫Ӌ|׀O$#>]'?GGqݒ>a_e /TErDi9s1S4%qL ftFc^c6LMJ*:(HI*c4@%0P -ܹqƱ+ -MM( -0>~J:`9U̟MVh5NY0U0(Lb5Hj3d ("30W)D|}+s'_gu#ȿN -hyqGSEy_7|!ryg%_(򺱂ly&N "c(BSQk,Diط;85XERrNqh)؇ل1 $l"k^VJ9u -C><|(àHe[<%0.:LӃ\s<Ԏ/$cw߬1ɐdpN q!5PCgS2!0W2?m@ B|fNi1'Ĥ!5qt(I!Җ߿=&+,BFa˹rg.#bv}A%Z&sUq ؆NvWJGMuːǛN 2N__^o -{OˡF\r-㔡LvoQWs2#$kPǮi- )tݾ7r 1ǾK};En?NF0(xZN Z? @ wh2?/5sQ&F2A&q~4N wc<!}w&L5ǡ17L}&f3R#`^.؆ʏ}=2~/ss -gMgȩo B\ jRdl,AMA=8L8T j6_jWFM|EvkcrA5}⓹ -TȨ]tPzP+߅njףUDD6Pt7U\p7P3%uLrS@܊(ALNcnMx&vO5 蕀<p~ \>|Xxr&9OP&ڎEK3OQ ǀ?jĘ-~S.E4e;IM{Am#m]N B2]@'[%-/rmr&a8 -rXb@ɳab*W+?9^MYM0chg ԁ )>ۥD. V0@z;s} N-gPH'URJAe9< { qW5!585l72+́یw9;n/D#J$tQ T\$2s?S?p~(XS>~q46PdR7"(ya? x#zOʡ:ڿSۣ@=r1{!o5,;_q̄|!(q ^ƀc&`ryaR%ngJN.>S[A  x0 t\AOsjz2'shn`'\ 5,N>ZJ|P=}ey"8ld8n/\'s?DR aO7PaO6 MQ]Mf Ȥ~PCYa_~-a8BG -"r; ykǩAP%;85̟BO+"(BSv^*+_ qa$evTS.Ԅq"Kw1E;;׭G8i$qNhϤv둉ꐗ\,Kwhya5%ym2a4x9䏀NCX^-=}mUA&R!2`;y>`2'_rTA[5 1Ǹz(N*Z&ra HP:;~6A(uHsiX$T*˽[N[=Lr.l՟jGK95;`)[DB ƚRRx(ȵ ).0B=n)KWr~&m9%(~BT#H<4ca f(IT8_U*(+3a*:= K&zejvAМD^QJ8Gfkj%P!؏Isݸ[y))(7Y'u wD׈ox]`8vp=^Q1EELBP g*8nĠl~gS *@_^F=_(+Am 0͡x\Ӗ0J5X] -ypȭH4{8;6qo#y)9eI=reo^V=-=^ĒҔ#QjSoly -LŇ)\=0Fl63.JD`nqVI(nI ɟ'J:ba;Q>i+ %q^A A7-c95SiL|:X&ub(ڑݼ*%ujBߏ.C_0=\MبMlՄ@Uj!s$U=p(uUL |;Ŏa:آ~NY/y)q2#z(s>\_5x7 jG[=?8.`WjSB ؠF +>(IκNrGLj/`}s8fہZ>ܔf]ڋ˫CDS"I >:Mz{K7=*9d_hMoBPO 6q| r;`(RImI72!*Y+95]+A>6+Wr]'Q%@Q<:8quyͩʻ`/5Џpw-r+l{R~W9N뙶y*UkhGhd_A03#ldxߛ+9'ԇ8$[G+r%}܌Nifo.n-h*t:i<r- Cل0jf"GE+BV~X}Z#sdƐdcx Cj\T?_>xx -`v3]e^ j\rSA}jC^k2W P0O.MNxP>8#\vQsA rRg|UKz#`f*=P>r= -\C!py芝3oN \}೾Z8F*(JdKКAIU{mK)1D攚rpAvJ05۠dīDXF.oQO<20p (K=ozz\L/J3?\׀rkK1g -.ܤ|W೸ w6 -xm\\O7uA!f.=G>1uh>~hA8l&uP1ҕE[wq/=S$8r~ W.;[!zrofTKuq `l{g59cB y~״E\^jr|aqsΏ:ԚFm u.Ŝ2[A7'F-x:Ԥ -ꍢ~S5c_E.N -l IOJUȫpkrį )`=\82 ҲHoр\(햺qMV>+1e;D6ryi|6_6s}nZ1nmxEX#v뗅y!8Th*[W[Ctw -iq땄"ԮἋ[!;Se "|ru 9h:J.s#zcu (G꩷8=rKӊ3&Nُ%[qP(aӁymp@y z7Sz})L@ơ{:b*:J^ Tc9>}+5􉚺/x)PX|=g5K@ K╳ )P0+0`%pqSO`/L`fP}]ʠj$.FqJ!(sFx 8<C%Uױܳ*NI@Г >/8rKq^I0uLB.n圹n&n~ҡn n~f0"_3%>r'}"~2m@%TN'őE22$cwC~Fr3eqջA3 }:Oou@sIͪ&6%"J4d=O2]| ׬ -v&񼳊˥rY+GR*z* -aԴJkg4Di HxDLNk2] zG=zf>D?޽֝Dw-yLaIVZt_wY5̽&^Z qv&K66 UNF44zt@ z 谢"^90o'@>x7߬jiV!*Pp^/qk1u\M5|!" +.|_dIun]AW,i>rpYdU)|CH&% -3Y -퀠Z\uF|*z2}'K]$51_"yYBj YJ _])ӡc߳ xkU6֑6-u\bIg15m(jǹR(xB#}G7a&E>'2y/ ysDy@|1$0Z$N> -O?SL¿/D$W^h)iVlHkc@, -GdG([$k1YlĵiT40'j_h%{Q~F|T+iK3ZIzJԬ'N6bҺ'A͊~9%/VD~_+;Α/$tN |+&|"* ]rAa>X2'lfkXKEÆ~{Kj> x49_q)Wo]C!_|{I]E?^#]M_3M< #zUvue-c"k" ]Jl%/kNH6&VIF8$bL":_UM1Q͂(W2X+Sws k͐nN -( -.qN9𛓤f咞_i{7Uq 3AK!a}DҀ4YAI1A~%o2k)6_i>).nYZTi--7ʿ*~|AmI^9U%$cIi~YIAYLMRv -.Gl&kB{Ⱥd}U뢾Fo悒?̈_,_R>^ 6xJ?UZv=|j*>^u[<"*z2VQ'~`i,,4T4ʜ~&a~ϻU8$iQv "!hO˼jTT\V;ɚ*w{f·Q^E=%-+u,\d6`on;1g.F] -;y5#t /qrwCH~&vL[@&|Pn 1z v ^,0NB 1k`і'7-><`pQU軹CV]bHC=SDQ -b %uNݍИ|Qi}'5n3)h"fuy^?37w[gFm"b޻yx j:$T`H~NdFSag¥nuem!7|F7 XsX2㻶0ktEYyӎD !U/Ѳ*/ˎQGڟFVZ\$t vV'LO~7>&w' šoG:'ؓOեWLTo6NaQoɖr -(15b-FHTI>>~scB0'GA“d8?-| ( ؕb7[Ӵ:4MKS!o|Ԗj=iZQ"-p=1OR/\˗_O<$G><9"ZQ+Pa:Xr16ֽ7έ769ң7QR|k;oNpzxpKxZ<>vC^or0^>.GyQ% 'H /I7e7S̭fF/ k|v93\sjq'K;% -k~%*~56exI][S?kx8`wns.R=.^ J9i'xxל5& -0+wx9=`0ioGw n v _e'/*h -|hX?YZ>]bsXrsX|KGOS?`EĒ]sf%Ůf E!ⷍgDJ$I(yvHZxQRc/ҚZw -Djyyk\z ~-iXr1>D8" C$_vg4:EFԺƞiI`:꜏vdE6Ƹ{GF:oWÕUiӫ+baY%Rc홡->&YuUQ~III} - yUla^^偑NUqn>ޱTgVodɯ3:#<-̮.W\\`coYCP,퓍|3}2ϴIgmDe]k`zUir>$)~͕Y/=Qg/?9? -]Vz24l}|crYOEs1@W%zAE/~9dTz)=tsFYz_y.~՚v;q7̢ܤ=nOzOOΗZ#^7z)01\5cz Tᵍ¯J[8|+ ~oF ?ND#&fYS֠u[U38n G5oZ54K(1EAn~=O6:dsGoKUpc7761@g5^H - xMӱMşj0nuan,{3Y\Rjy]e4a:zHϵɛЪcwGhf -YC-U&^tCbhMK:EN1M.Mcj_u -9,#LnTqg ۷߽#hn;S˅h%Y;lO;3kyεawss߸Dw)wI(-v{QSP./ ˥/wyx`RVMu yCE5+˞gcO)fѤa3>M>i. дKѤKDhʘ5hLJ wEĄPpRӁ\M)rcBa_طɛ.ƜӪ9$;{H+ò]zR4Kqeo_`MJY79wӶ#̩[ QqT|F4sp $4Ca=v=e=;k/ZVo?mq4w]DY4kPDsp?[Gj'h~`C)OyZ-kLqK oJ"*"kl#jbkJb` ^$M=<8{C?:9 -)M]}J4sZ|vSw#ii{[a-EOzh;e+*FÇnx}YQ.!\{}R[CB[KxiԲ{_ZϦ*FS1Cj>s-@N״Qьk6U}c4Bs6*g}h7ZDdhLaw3TKٍToKn^!1k{! qam]mvy竣C4߲fOo_ =DJHiB9O{!1+?{7n=[ܙ{тEZ8Z --rFK5Uk4_iVCԞ+yQWy!A]z_=9/) 8gˮpyHWWKg^op)c߿) _l38ۜߧ+[f[[}#&B3cX)5G,'В=hQZZUQ;n]vFqIiKI}CtWvk+s ~Vb:Smtm|Y]l\kɏkV=_d 5xgstşҤ^)6`;i>]#ZHiZ9_xր ;oU+M6lqݳ;8:j16\eYc[gJ\wcBG=-,ok?x-}_RG u:W L|Y7{7;g/-)hQB&Gr6Z`ܩk1o37g;Ɣ

9٘Zdܛ둧nĘ:Ն$UXFFFrH8?1ϣ:.e4}Z4{>Zٗs]]xR |wfN5eSU>E=]U7Ğ3Ğ})0{1Z|89`'] ^)-.t1CN4g,d4j.7eZZ>VheZc3|ۑޏvOգlV[=o7Z*oN kղUogjd˵kYmzVhƊ X`|:"0 +cgʠet !!$"ynw稊. [|` -ܞ_,54s&RZa;h qo*n󫙸;_5O%u_%.)*G׋Y'oT:qg4?a7a| k1xR͊t|J60(ZAXT}^F俩30?nT;}n*L݇gǘV7ת؊R -pU Tֆ>gx-As窠M&hvvh׉;MQ-dWi`OϜ_+lMg?~gi}gyڟYwVj=Hr3bt| m7Ҡ2nxskBu;ou^Ƥb }:FIf=I?'}ڪu!vܒ8߽M6lkh#{Oܙ*aۮ% ~e`}g%؝efnVwum<1$ET"|9=l{Zmڏ-:e5wg\|c|fU32=y;D%1IjTb۔ySASD9E9dgmBm5 KK.?lS,&@K8~m̝ٓƿg>/e 20x׳֤=+cm[|^bA'G֊7C3[SYz<"J6/*q;y}i97FshW>2_^)KoE$yhhąGoE Whۉ;÷]+Ҩdwyqs2qC%׬Tn{íh`?=*1*`y YS%8_] oе1e6Rٱ9?aٖD9ag^+KN|Ǭ5kdر82?t4Dx7]R49E}!"d'9I0)a>ݰH񒬠 apq&VQS ΅mx-ъ\>}؟v9MVqmf|m S 랣]Q |BVe>?ōDvx.<%mt6;29&n>ݹA;?thG[Ͽ5Fm"2}aDVC,='W2&tiz%+,6+-r=%n2[Dz~"d"6}.wo3'D?@kϠg]R_t"vk`t4ȄoLq D%{Gk h9h璹h7\+ }Eo${O3 c( 0KVߡ)JҢr7Q]M ]uA\tHm?zW=`U{Ԫ`ULq8YqỏdP?9JIOd|]K @/cuP CV8kqɍ{M.EMٽ[ W6͝:c-mg:3EzvTRQa'x _p=_5> i{5Zcժh9r hVV W;_OYzgбFΩ3Mn,4Iorw /v FN_J6qy1bc795:=AGCiދz8^Dj +P.Qs1[ȈǛ1oi6|.*G/7ϪV8[1-)vU_Ԅ*+!ysMu=w^ 54c26CWl!uǂɚM~&|t!uai[Ct#^^t ztKpx)hnCFHDfǹd,6 -%  k<"t+ᗳZN_\ܬg^%eKw2Wg3ϝdR>*f7l04TȚʼMn]]0W]x6H6{ 3?XkENvn6]rd5-920a m#*D=χOٯ{F - -=m_]SGf ŃZwvd,2pԛ9|"6?kj|;s1u}[ߴmQnH)lCfvQ -~\V~ЛnV>([W1ngwjFzAP50xbMY?S6<8⌢ɁӊzhǪh߶mHgȃF M}_vs |oIϔ<ϤeR{o%x*|0v߲HUfSNaJsO(yA[$ J?\"yrPZY}wl{AB"BTϋ,bH[3c/eO:1^! |J)})T3kD;TѾf(LnRuk,!x&OxN5],uMT”&U8sͥ k_*xD`0OgRܳ.61J<3q"%f0O虵8nq _UMJ7RTn|al3ԤowSgæ|A&DL^ -b,:4LMeڶtڳAsGu ϲ,=%i0~?֊fJtQ@eRNs}\J4ѳk okfšģ|o􏪂_Qz((}^  ymG1Lgq.3zj1tQgtQg.M6fϖI*-lG.7&)To=Rx5Yީƈ133`n1}pEEI:db0j/Ң3 dwn j2>vK^z0Ǣݩg繈hk/Wԓ ugm"ېZNN'#gf{[ zg6MWvYA85L_b#/*&6sDuU#h44SB#>C #9]&AⵂD%=c@MWfa1KF蒒z͘DN_̿>8C|YEx~+}Vw̴}=+51k}D[pQZVOX?CZ4)8f3pمj4^?؈3 -+D/\:ӌ:j=8=\t `, :MyTArN*KR}: wEϜV=6{YzJ׆g(y6Jj2jwo% +7Êכ؆N;1],89EC-n S,'2zHAjQyM2:"nWNκ߽ԵDxVN U{K0[O(w'\ -ZR!̹?4<0KsHKQ<(ȟ`[rŗ_:xn1O{f-'N8!ιq<`LZ>#Wqe}ԭ}ѭ۾eD=s„kjpY^){!oOZOCy_.LP'RL2R~RN[U"j7mK֣]6#]#_6LgD!-3FVSa xFf-¬?P 87*Lb~2YfvgQKMF;b]3}19fbn -9uRQv#?j_02zCsl;u nYAߴV}K'n3I)cY=a.'L2rmhOЮ0^r -i;-qD^]9t0/!8=L?}y,Hq+QqMۃDG-2Z&J̋/Lɳᛐ\AD duGBAj^"Lj6&Rh/y|9EuDof=89tD?q3f#`$8Ҟ+Z/qO9IFe:P 6ŦIrXFG#idi4-cq1w !JrJ!ޔ۾~㾵֩3#Ygʌ8֜}Z{U^{-:֞O: cG6rEӟtyw+Jų ۶ț56t'[x5Nóx~-nM_3wwUج=0XâIQkHqZvT9tm_uSfͼe}B_kؐ۲]+O_}<栤/m&vM?u6-{آ89nظ{]L1f`}vc'7x5ũO0pK*_F3?,?9tߑ+Bkw5 <:^-{,]gƆ)/oL {o/oX9 dP6熚2㉹|>kJLԲ1f*KG^CYpS#8{C < -;}19+1F`ɡdhSL\^ZKrŮo+O=lT^YENXXc*'9iim=6ۄ2g7}^1NA1 -<Ű#_.dž_6ǰTwFÓWk^csu lyP?1?Ǝ`tspmПnz0&Ptk;alI1cR,m=C_qFR{?y[~ 3|^;M|Ow>" ~;ػHwn, Ƽ7:'vN#~%AsvBbbn&1Z |k"o}˟FѸvB1xijh~z -<.g07H >L3,gמ^ŨY\ywo[m28F1s 'LĘ w,;aɚp qºֲ7--Ro]:)rӗ`q\w*-%'-.C5>cɄ(~sWP,U-Dv-J~RNgenOd[?e"|y:?cO/r$!<l)m?'UW֕]R6lDr `>ĺk[oZNSmrk΢iK=cEa19pQn7~7M-ηK1僯{}{n| -a:#gBKws[,1uЦ@9vx﹯c{<׹xSCm=;&W>aѲI  ladʱ Xڣc r?#rזs0.#yP1wdKWc:aZg/XwbF->bC P.X̧<]7H紆ۓoOiy12ʆ?TB˞\x[xc01!ԾD!ջ,rF]su0m1l?x1QE-wNE`Y|̉:Vdu%AGж`V /8Eao<{%7a3<ÏR̻U9;1w_OZ~Mr׻<ц?PG~EKL9=1k8; .{'EuהW+eoZPV߶|y`VR4$߮kOb@kf/hbgR^y m -<F:]K/_6o{;l3j0,pV0w^}ESz@hkNܜDxI*a|aLZ_,زH7 u\ZBG^AqAtMxU9= -˅9B .gjdb|C-b1F`3B~[ k`F_,GwD_]( -aMZbT\`ǦSz'aG(&1u ;N\Bx=Nw7e;XG|3>QNW| #0V39rwql+S)sȡĞ.9GḌ?3Gy|Jh*r7tzd2i` ߾/}G/yBlۼYs`>ʂK6cɷE;'N&ͫ S]&_%RJ2.:.l_2tY'cRe[ΎݽskF0gT_e -c1o}A~x 9zt@?V7ܹ eR[=8NQ'o|˛bDWZ~?;b? ?{߭<8I,оo¸_K֓)04yal0@_{Sc\8ͧ7<盞|s]Ƶ;.| x*j2d7={ȺiOs3BG1?|o pI1tpKIw)>\% Xm+N 'T07B c~3%][W* $:&aɔwٔ9"vÚ?S"kȇ3N{kSk[|x2dMsv&I0w"ƥwNj^EMky>~1`,(k>1x[ wN}'3û߿bQ_A o9p}W ?v䓺nZr崁~Bÿ -i]\Xzo9e=sw(Tq!|37pYC4uq& keOTC"+]Haog͋9p]6 |O`/BOF}Y$뇹f}SOo8&B+{qyƸMwmfg{\s=&Fvv`/UFƺ ox -{[Ӣ2?rugkn ozm -o>zŷ{ڦ7}?>F3]m`b 19HbwEknQc̭ 4m|pgE]OnΟ b!|x=9z,[kjȝzT pMķqK&@mZe-yq)ފMgE{9O}tkt7S.;7޴ _̱dʃ jbq>:-ʧuw?L64Wykšsmg?Y?m lhc=Ywt+)"7qá+Qco\zu${0It E׽xը+`"#z!-_s"{M{?Pm'&OWrpdD21.'`^^ӟϏmyNjqq ?9) }k>Bq1ߑ0(/._36W1TO m}cnn|pi>X\Tv|须ŜsOgŸ0r=7ֲևA}ok$7mga\U'㼋e֝rcb\nʝGsG?|#1;kZe©BK&b<N'Se ai4|+#s@,t&<3M?]9V(qצ)o]ټt7n\s:TJ#wtsW~9_,wWḙʴ= sGҫNrg=rq2̝TVWLyp u,?Eϝu΢xFĪIt/\yFq۵rCqɘDzqݣc,U _{OGyxV~Vcjߡ4jU>5G n:w>`ĝq_qFߋ/(0f/;|iâ'Dw}l7keBm Gznd Pan@]'O89l8r/xVl7.ۡk9}> -yk7̾rg± A3weF2vaS _'|jL8qʓ}'G'FW<sqS~5.{b䟡QS m˘n==}Պ\C964GsJ1wxgNoQ%ꤸM=%Zs*Pn .𷦏\N 3IFT9-bBy}ϑ&c2̝6rgrg5{x/MKFm 8w1g٨!m\%Go#ҺDʽ囗=t.xh6cnS>5ڸ'͛_&j=S.^*DbX̍GAv@p^]>s%b\=HYm$1q/ZVn#g/0'vxFhOY%#j֗׸s6<}AGrT\;\_1ѧmVK~ǺN[rgQ'ʝ;kn̝u|g/Xݔ)wV#OYYYi#w!-c΋>}1`.ea~s+Py;"wMT3P,kDsܕ1ec,{"zcć^ ?/:m5:zO -[-MD|fa21rɸ700﴿ 8?[` -=NCy eoˀ3wr=wVM;eotXf󙾆l;ƽƕ{79[=(Ϡ| 0/ Ų||y+Qgw%OkaC~~ƒGؖo_yIc~0e oc)`]~Q -rg)w'7Fѣ_܆>hP.uWXh$rIF,\_œ_Sb_$ ?ҵG~ 4ny:]Z}uu<?(o̦<,ʎ(&xk<|>(oI0}xоOo5s\v}Xfd᜺r7ݳr -ܿHЦВ'B mQv1Wl<N ֻNh|:ڷ+>s O?ڵC.8ȸW燻7ߘ>2}~@.rqKHF\< vz2\]o3qcQ~5{.FӼiضoO9pp}S)l-PSw#`7(' -]wq| u}<"`x̋y~B;ߚNcsˬ"YtU_ߌ3|_oءOǏ6yj`zK#==}`Sp_*K;ς |y3 -=4<5/XAZs4ʝBp=N/κW˝ybhO -2qI9[P>=!|ƃQGv#xWtV0ߖS>n< u=浘|`ַ+RWf#P/zXǽz-wVh׏x}Qd[, {?R~oh_qB{\iz.N`_6rgmw6 -zc=vWhقzWwQcƘ#YhjiXއ6;s4l}@?`{쁷7ٷ+oڛT=TmF^[bKhߡ)^W"ygDWxGУ^^/zR̳kx,GYQC\qkb^yMk%ͣ#W5> -׸ބ׮#>7ܽ<\yr{Rto_O?-l|賿 p&5;&O}_M91-|n{u"{B́z{|?TwL]o Lzkϣ.p)w?vA{='=Ǣ#}0:hD_A9U@۴}XA7$#`އ<59-<aO:ۍg羸sp\z]ʯWV @zΊDK~$p -?DJ{qh$pSgYˉ0 -{c;_p}~y"zE畱?aܫ\_KM'ֈ>@[^z-7{OwG74x,فc昖xυ;5y%G3ҝ"\os -u Z0șG3V[ iﰡ}~]郭wPc1Ȳ x{~&c_s7{/9xx}E V1C}'G{f -C{htG= [0fÝ+N9f.A_HSb(w-DW+lG?q.ȿQ[&# &|S+v9)0+C9c`[\k9xwYk&?Ə0)8sbqXtn=3OP/16uHaP?x.mt0+}['pn?8UkLu-7N1Kό:*;tog*)F4uL*3:ƣ1}>nnδ/̴,B T`*39E5_Zf"?zxObNwDgSj1v xjEgg rOw7\;@4=F6IhpcVA"d lŮೣ>ʒNqIw%tc/ݚZluYNtGW')`D;Htڄ힙 X| (&k/ X&cRz&uҴۻ@tǵ]ŵ]еƝ XF%dV%&RqXLƭZ1S6QOmVbi2 *޳uZRHU{{zΪAq,FksL2us6 RL-ޓ >3Jw7{*v[7SqUȝB_ښNg,[<-jW -4Qǰ+Z;1{ Лw^ޕLt:-cY(ΞL|HZKRs >N1UV)J[l!2v"udgYL[ӎ>CC{ vJǚqū-qіCs, 8dGD(jD;;"ls◙dfqG"|cd܏I.tE([Jrx,Ht/J %KO%*t.vGãQI8UөDbm:?"_.`V[d*|Lmu-1"%u;ޖuNENww-Nҋ1[/ws~Vȹ۸i#i.OCu8vڤX'9R[L/U#rp,@"8F1Z[ &"%se$S)Gm& -;U2VIOwhiq7.+>)EJ;(Ջ㝝/JȖ_pԑw5T$ 8J[̂xW$cɻd.v+]97:G}vġ*[%sq\Bq&^o8DZTq>|8?3[^2wE w)Uc;ܧA:f?ڳ"|Y{z=R}qwdJg"pCk<պ AVxwr2iGz Lw:Јvgdg"[w{赵%3ɥ gBۓT!>ש1LɎQ)wxqiufs%$یc73+WǙq8S-v:|rLgZǍ8bb8:ΰLjqq\Bu)F:ng -E>UƵWj|`JR,`xBQ=J/Fcۆq2r^, -\ڟ#%t 4._;_q#&o%Umv7[IrnrZys[)q7Wwscf6j.qa19˾gII$'cIN"u#:vmЮ/Vb5%'Rux_saGi,mt8DK> 'n70 :;c< =~7.YRXlK';ͱl]xfVVbsٔd DGz~-*RH:"?%0I_ck~`4vn`4 -bEZ]Zk{ K/8^`[2?=9O[2,AvE }gG--%g;Yܑ8JIEꔒ$ASǐ=8vsυ,%4wv&Y;3 Q*{7K9<{sv-s-ξg)ݦd{{oO: wWnvLº/Ш#חH˜bJ.Z+Z1xcs: /yP"ԍpUl"tD]ѱJRndq٪nuZ cۆqժr^oq-;z},sPڇms!AXE>W{݉uqZke# sDZP,3:/WF+0SeɶGwD!>Y㣎ej!=ʨV\= L[,}[ -QOi/(e9"G Xȥw.֙xNOY];؝i_&idn Tgʌc7ގL|iًqxObNwDgs "Ԩ#gh"hij-lҡ*0LuO `~&ޝpãx -&5^/DqLڹN*n4%7Ҁȹє{hJ#қd80%pDp:]=ոB+B$F %9Tí$nJtU -Cxػ;>stream -TU喻Snn;2h#iæ՗W0^ަ* z:+X˪{Y<U^"D,{*T*.`,FԔcQYT(x0 @/x/HP=+{y K*i6+ -'qJVD p) 멀j*^xlI -k%hUYI)W0:YU"C0%Ydh-v1/Z h&{Ut40hO!~*̪pmOAXVÊQeC -r}LD@?V!2+(B֒,,9emMig\S f]SļPה~,Q\&|66yH 2pf -;lH49c_Qe9 zJe_Yx_ne$mx9c6Ҭyױ8C@gDQK8.ׇ<$ 1G/,J؍l=< 5p@aXu;{$l Bo%` 0T />5$zh69m p`׿c?LqBeu+#CyLA p86nUËmhJ54DmhЙu 9AC9ă=0dVqUTT\xRFp`[f*c  V]Lpm.<:}CTZ>UÊ:&8XMpv :O\u1c) ->4&8XMp)_E"8 I]T6E(F@p E!Ʊ N&-(dK<ʪxU9 S5HNeC"#`".fPqя mi]\ ۠DaINC҂#y4^-Ia?1[765T6 -QnSIADGnH9i_c/)rЀօmQtqel3xB?TNhHRA$ FNY=1GKs]c~91nj;U!z*hDN7WdkoLM9Aup xUcal -s,#^ -Ί飡yMtM0CJ7jP?-ztƢk|q?V-ngdc־]o0eǻU9reN#{kSx -JW .UÄC0`[(8cenxfkxX鮶c:Z[Y ΘY Dt0QGƄRp@nL'.fE ӵdѪۂHV]j@fUTMNKT9b`oԚK2wOEEKy$MaWUUvԧ3fA(l/陾0OۀicKAI<9*# ysu6О0bӠAۖnI4WSL_*l6t6\8%S̲Fb;mF xgT+&" M ˵Ba8T$Yڨq8VX;-˕c@ $J ,%s(8ƙ[1F駫*kۖn9!Z2QmF.sW( - -I"f{Cw0lm+0mxU_j.'\p"065mk]~2?fmM0 G.(cb2\cvn~a8[qAMdʐ<)me\$NN Y!Zs`٬mS<\8'usvbtYbt 2:N>Z ɪWoX(V5g1%_/F5A);G]%)<#s:.Ж+" -s 9˲Xr$lapV*X0h?s~ cgN%B~#G^"k~ -!9TDZM~ cؽey[B]NF>tTcYԘY  v0@( J^CbZ>Ä~gxeE`x=:#ʁ*P +'Jt^-̳LRb%S¼;F䄀8ɲIsY^EIJ4b`xJv#fbFYW -)xRADgNez'`#YTAY@S@,kkmEEx0H ?b=Qz`B{Е5si;hYN%g%{#On`Nif|bqC>Ca䚡m dK,MR>Oh!hG),U=xG7 Ab>֝9S- "\wJWh{Y1c>jd S@vvf2RzXAjӒ4#6uе֝QjK :*lΩJcfrԡH@\E*'NwR*TK!(O¸WyZ hS۩<]t+-Pe,kJ3\UZͣw?FE 5|tMiCӄh s ˈ{ Py΂LeEr -V,-gbc,|:򨄤J٤=j`0pWTlȤHf"$ˣLY""$;d/˼b2]J^MѪ)fLAYj`f,8 KZN<ڐe 6AZ[,Jx`qY7v&Mf6N1 dRBz wۦBd&f1O^۽N&fhI6YP̊ d&Ȣ fTlbo>/&'51 ͉ỉCvfь3)afh3cT,2+Z`/bsZ2p0 E>v+.vu̢12kCӱheC ɀQqoɄdAN_Y42n@Ll:m:ra׿,drcHW,*1T(J" 1Id:FK- -(X &z{B԰+\ 3Ne, - -E"C'2:Y~oȄ$AemN_㭶̢vQu(9q0lJX4bHQ|As_е_uXߪ2Xq~܏g ǡ~f+zyjz Omw1-߇7ei' }5gKDaSD+--`cKmhes?i)aƎ\o$| -m!nPɼ+wC@vh[+Pa{ndoRy,6K;~Ɂ`:M* uCmt(1Qo`JI tMI7j(֐jdfMv fy۰AT ߲jltsCby[:@:M޶ Z l-dowRO6Ⓑ·1ml!(5D7XP6aED7ֱdZ XMlp![H6Z:F`kUȦl%Cc6,`ɦfv(M66Cl,eéj9u;(E·,u6Cؒ)YdʶT?rf3!lX,eʦfQv(W66 az )[MuH[[6}N!moY*oqipN7,N7, N7,N73jd[dj'ddJj'd[ejce\Aӱ4K94K 5K+SM| LɁrӾ%ݑ8Kd"I ^jJ 9`Y̐zr8\ e//iA"FXM9^xg"\J$쵃 Rv3j ŮBҞ7(Y:o%04e[CaU{CvU$ONXx:02Nu7A.eDQB_0byé^*dЖNqHlAjLnΆ .K`~"@*6/I6粄QEmEdm7uhiRێܦ <|*k[;Nr^U; `(;Q'-'hɸ1o D-5m#;lU]noQE3x:aCjL'k;5ŴӿcBݨnۺcQgM[o3k+:X Κ/͹Wd"u^!cA}_{ -" -M, -'[]F7^@xȽXsjZ=L{pGPpMY -_;o>_>#en1 -0Gi^=$).<%"y\(I*KNAu d=>f%xQk A;WD`cZͶ5+Ũu5y<3iTA3NEsgt*:a^f'(ZNLVX p}5FB€p^TH8aL -2-@ 2NQ/8Z H B;bqK -*I[ASEܚWQ x@VD{P0'\4`ڀC?>>i-BcU F5ګ{myTCC0߮pyLN -F`i$$4x}{M*i}f00[^ZnYX9YèZ Ψr<1|FS^Lu<3,jHșL/1kB&ƾ\rmYQY[I =#-^LK)rvYZx`*PXɏY5C0$ VPm,soVy5鬿Bg'F</ff,Jh.x^~PW&#cs<``wzF#s)(,6Ġ3fw -[ƽ$dn#ĵh -qkm6 - nwp@Ud"Y2. 2,b\b!R[sXWi+~`G_iy:) gB]v1mV!-:S{ԨHZU-LOs@*bU4Urә;[cg~KjFt5Bcߦxsf m!:B8jq<Ѯ!9 @rYUǼYNwղ8. +JԶVɑKEW!G x#W5i>`˱灑ㅖ5;OieN8m>&h ԬY?y4A@]ʢ<ys\53Y[d>l] -ڿ0[5ZUIF96xՏOCCHAe%UBS0'YPǪxT + .a%Eai` ~jf7·X%ڵw0JuԊwJ^p8 %F -}fJ55vCby;onOks=0YND:'Ak]t1v8+inʬ'TٿXg# jZuSTw7u/z% -*e2: d7]~Xtu%<*0OaF֍axFm^$W4hFEErC.E~Kږ;r-ggA3(nCyT\dd6{2^O}mz<S 1Kd?,z -(0' |CuqTEb4 NYEǻ[SlM)"@@2#^ދӌ^+z4M@Gsў90  Lt_/)>3n@OLěy/ tz%K#ӫ]_s)(?Cӣ=A5kmuO${ |xj$[ŻKsC[/5ڕ @=OAVc:}Dk/~yL؋?~6&tvYޓL^`^*Kcc&0őNYzj df( {jmo,AĬdOW*ާNAn 5Sk#fTjsfװޱ(īJ6 `DN'bţr+Z\W h8+NH&y$%SR=DwpIJMIp̾`,:-gao]I闱(ŭ򺳱g#D /32/q"uxNdJ,:;A#]PV5%GZLD=;9SQN\u% -FLLuJJ(~qL*՛,L3ĸȊ,'rxJ2'<+`MV׏gluH9, |=%1ϓlk,'2"fP;)YxPv䘮H eו&!X ֖PSA5Ɏ'zJA&p*.ELdI6܍y;x]l@à|"1*z#qzCT<栣QH>L3\Z~dPyOYQ00yeEa)ωfplM_xLj w^yXI Β,q"O Gxm N^I|&gHd3MM3{xn g̵U]]]QY#IhWV0i7dН!:bOSA:(n! UU  KrDr+@8m`$Hja _O $KRV_g?5ݮ&W),K7hIl|;87&Qz~UW\zÏZtWP&>Z4ׂNj4O3n/jdNЙ7# g)(,CBX$tbz.%<\ZB&[ -{)-ޮ7ioU)\aoKp`ED3KcܮMoxs_?𻙻b+a75a+tKs-OGuxDwpbo3@&Gvtmw+j\dN)5$ϨAgӠH_-t/!MD#j@H*H|l E>'ڡ8(aH2;@1?Yz@'fvx=jZ)_lj38g`j/kؿDkc,N;d<6@r S Y >Y-F1Y_'J0~S\ڞ#o/M`PmG'8Ym}40!ht7/8t~7z>lGVh$Jl(?dsBn;~MCA޼1H)nH6~@K#G)A&}Lnϔ?*)e+գJo2Z{\`Y$flxKxIdkbcSx|M2>D0{/djDR]1Am. -$>qO{QZ- G $ 9ހ$Z?  :1*\P"p'SVIobE-rQ*Xy'2e*Ea -0B/Tx}~pP>剡"GHHQE } pV0.za% N!|=b(g.'h>@т>@!D~tm!+~%̓BR Ä (]r_hIP"`c^ ; -Q,H~PBXԠ4"RP^@lU/':CGY& –&٪%sAm8F fs;ESh<Zl%:I oFD"b!C@;oF`KzX0Gz lȢp 0w?&3~?~:lC˒`XypYJˏFJQUK>V⠱SQ>T@#&*>$Rf]籑^4@]A IDgVBܱk @sAw0u5"PHa%(.E |>vjILSGQ5 `!u "$89v((_u h/Ddc@瓬nQfo,<`;!2 E剬#̇YP , z5I@gM$aI$5$]){a Pҗ9I`rXyQ&~v~%t9 x>DXbu -"Pa]hJ'G'`!nF p2xdG`s kL֥BD|$^ - !Ų>^䌁KPddRay bfȣM4P`ASH惩Ьc.V!-0C/'{8N˺D#x`"+F恁0 k0)-A"Q 9RJR -nǷ/XieNz}X3'Ë5Ff8h:ou!itGz -!}.6 -.k_VB Xh%NX1ȋ5ʢ]ހL"H\l_+S -k -bO/%&,, -''] >0hO.X^A\{)I첸ע~Jh}D=.Hc CN;kЕ0`w/:HcФt{H"rJHy0TVؚ PG*U ҋ - ݠcGOXHLGHKZ Cl|[lܞ, /&A`]c4whe ڏ bn&xy!V@[990E~q)hPO`p{qi!G -p8 tX.^bZJ6\ud*À!P^Bν0x,QlXA]PJ!OqLF+-r޲%^ K*7KGcꫨv0 U_q=)zX.!\%l3.rpk~mQg[+t׈%)kUZsޟ\_Ld lO6%I!4$"G]gm3`̐<$!X/~ ĬZ@'FGaXI\d-?D3w x2#X:ebpxcY0N -g͂, v[4Z  e&1' B$5$9iOlet0 ?ߔG<*0} 9 0ؘi J|*gسNăUÉZQR0 (г`Pb_MC ЀwX~ N<|_`B[ŀ〢ulBZ| /~Kp1>_84֤ž sT`|->$ϕAe*"Z"|;@=kYAP/hM6A -QSdr/F_ p%Y!GoSz{ۮ2 V >""(e)rCiD^K((Fe9f.\Jq.#6ș h;0QvȸgB&@ha{eZCx[|0<` B -h9D^'! esHځoEȥ89R2 $ *Aps0%O{-k.+v{02oڟĩDG|w$ ʨ`t׫fm]^ekmyBz=XFl*׮eg0;[{8 4ƨ*"v4oS-&1]o6#hmzx6Ϳig_]|~߄7&{*p$0|krǮ~=-؟.~^+phŶ9f?eVjGo L7vٶL;T60 Rg^4fGC-9tVC@06`h>>~_RTZ_uVzK59 ~{%ٿCJpC:Vv? t76n1@M CII1 Xn Q+35{EqPu<wꀎ-LZ 7IOM lO-#)^n58hng?j;F~a;%Qϯnv|vٓ(|gbc[,NHOSQ6#\eΆu@yCJ]DAڏuPhZ?)9`+͂q:S5Jčxfӳݾct*!o` ֺxNj78.`G\͸cd?1B[D~S?dgJ۞9CA),W   -XDSeHd`Kd75Z,Rr }_h(y4*ϋQ2oM2b4ȵzvs;. A6D ~X@X|n;a _b{-yytyc<"QFLq4t}h,Xvwjୋ -h~~a.R[ NHv@jbۉD^aF;| S?xB!O)vQMzoZ$(H}`_ sIo`49_ -Mg5PgtG7p5 o7 x7[3o{G]1oN!v"Q uKfM'*5@ÒM,|5Ih^pKbw1.1?!/b0Uk#ҖX=|}|[p6ŎҟLTf-YQyPmٛvtV51}M3hXۮQ -Fi$fbAS%(%!9;ux /X3` -gՑ]w :1u?LMyd_6EP.*}DAC:?b6QPr?Ba -L|vaj8Ww.V_C.:(h<>n&L%)jZytZLL -mJ)a3iRmъOD,5p&\[pE.!Xwg>=WnBor^,Nz?Fr@v~a5^ߞ7WU}*mi=zSt{Q\/C*Aq{Z xҌfsCGm8)M.Uex3AViB6r݋X[zr{#ۖͲ(2fx6S)@@D3F$cZv8:5r -o{(ˏh7x#JI`U eȬxg q9ֽoNIWJ"%@dc|0QCz.cÉA}o 6d ) ĭ|\?.,Nqʩ81OyVa V q=]Ah7t)ྙ, -% )I]jw6 O/pyѬ*pԴ߻ %5A(8h -?=Bt!X+&uX̛TpgFqPumЙ}zQ#}=g{IvMu>H4mGՕ}Yч Դ_X_gS_3fMB |g&=Z3e3I;eKzlɛҥI1a;ݔJ*m| -_qlB7]g*s%=eqM㘛<ԸΎw# ?aR~ώm!4;wxRe0$w뽛%;D}xgNf<|u<1+Wgҹ*7yX'l$[tiJ͢#S)Ȕxg^,(* g|M|ӣygS;||aS vjO8R0t$]taXܘ›\M}vP Z"@.nHfq.An]xO\n}zzJL<\y/Ftu:0ӥ>mJ+znl7vvx^bo!k]klzB3淐L댔31tѪx"r:Eg4:{)9Im\OcClH|yP{}QcY oЫѤ4|RaUOM ;sbbWF351:>~|}2J1&-Ƽ72VmEmw(g|j^bWJScw0n9I>DoL&}8X^M}oe )roĸaMJ)j35wL4Ѧi3,ͺaġfy1}/,a1stdKZj;K{fXi2;˲\x;XTguOV:HޚɆaaqS.m'Kz%9<)GƔ1]w;[=ڞc+f&@}-~)尻/;}s{k랽uE>6ysP#bwviw&g%vu䴾N*[wF\gf9yyoƹ.9wL1 WUZ׮xmW\sw}:L #RqnԲݭrEqވU=I]O5z]0U 5!9qef Zt?x -|c"{6lS#*㚡>Z|Ef ZTlYPTC=9 -L^*,^[>ۇ7x֘d4ƽIoWϳO9|c;nFηw~ܾیޟKLGO&igh=yGȏ RI_9JϠԨT &ͭM!3?FFX3duB^2.ꌦЬMߘbrp:mمrx.ZюV?O}"^mGuIEc-zߛ-c))1gE}^?W`c|rw]uSv =՜'`xjŇ&,v&=N|ZzӤ%2LdiCldΐlS}oT9jԸkOaŪ۝~kOGcL2`e_u̾d-]6c_r-\vProA%mDʧkyiŤlbYx| -ܶh;P(6ӷ5՗])mSSSaIgFZ,4gT3;>Ag^~2򚮺+p5%~e5jC}?Ƈ@vAVOy?z+๫/ yFj4uWG3м4k!k9ُA|)m3?ڠxu,?7Bt4/V:w"y,:r?-cx\vlqe-|] yyq6u" -#Wg_*|Ė} ׹_Bbž4u}x}ʷna ΢$NAAMO%PcLD>s6:NKd㦗ihM1͔=Mݾ=Bj.֜vvtn1v&>VojTo {ܹ?ׇ^Zni˨gn. 9ovF] F3ջ]De0 8_)BJs7,[gj.hv+sqvQ &rfv[֪GTw%!ɋn*[@,7-ْOe# {9j<볧]y^O#8vajR|^Q8=>_<ʗ\~ɫDѪBU8K'SAyE!U3{TE;C{Te튩$TFReXF U&Esճ\)J`l -X :{׾iG-ޓ5W]6&+]1sZ-YϽ3ҏ ms.T~TÀfJD|إ?'an{<1eبONx7jf v[&cYSyӟoI߳˙X˻D:bMҾ1D/ٙ krۅy-ڹtZs +1=~k3hK)4NN)L -aNWΨU}"XI&Qv4~wLI?%'h5o~]2I6}39H4ӟSzc?>kD5ǍW-41%};Kgzy')Qͦ?' I65@ԇ3@kYJTR6mX >~~pܵz_bt7D?\MDYjʩ74>"wi"b|Diӑ\R?lj`9n;`*Xt*s"9&؄?qɝ "gfFXD(k5ol)]gew̩:k$a!8,f&̎  j砖_}s|TJݘ&L'˝%]Ìad(*emE8?SXuc: F5g3i?]8n2rbiƞŪN4OF8u8Tb^hgEPE"[7l$GZyni'IX_=N 7j\<|8…$OKQLqOx$rCH9I>Cw~ dfg>=]5b㉕w^ΘA<"sr}%F鴦Noܘ-k~e=ooIDĕetzdPnoL' -'"~B>vz# ljp&;`SW0 w!&e1P_rzF0Τ1z,WÉ Lh`%1ߘ4pPw}a`: ţ:ŝ $pK%>%,өG+ C-PiTgV$~|I(;Vr䈞njW*Z'FYivq'&u(n|^uj̵9DBbONƒeR[I/䘊gǤ ii^dž씜+-1u:@ڪK~4 <,4f-&Xc0I9Q鄲:W]F\x8Iͺ`efUϜk p;С'.k -׳x[!޲X.%G*]7>\B(;{]zpB[$i'd?IٶBR<^U7"5Emp]Y좃'Z{* ,%?qN4&Rs5ᬾXYvg%po<}TLb3LsW< Hk-O>_HT0t"ctɝ.|23@ȟX֢LTK:Y<#"TѴr=yl_":ssC5ɹm` -8) +ar"'_~W⏺ 6# ܧSsCj spݰwZ !ts*;?81;.GH}vlfuUIYN% C-ڢ=tLF O/G㵙`~(o܋ܮ:ڊ sR!ߘN,oG_Ҿ]yw)bLx}W7|wHqK7 \Mߗ{Xj@;ճwnFGU0 _9O:_0tqk`yct~i[0c 0j{:*^"h+f -:Q|D=jy>@sW̗7 Y[O:䐝h~.#YT*#Zs6z֎CSI.u:mqcb!?2] ?ſ]IcMO|R69L<0Xb`ߝ XJLiu7ےx3L$ؖ8ׄ|kGr܉{m7YL7&9'[*']x)ۑQM@:GRfc#n0wcWr0:E͊ǖ4ccc*J8\mޓx˸ߟ a':z_ցMKȢ# -/Ims<,0霉lK M1/JZv~G]B,\? dϾ{gOӹi&Ffcm8k- ȹ|E2)=tpp.npgJ&f_G3> =<*;3{e~W - -nfNg˫.MXӨda::1JCʗr]Xz"& iiZgEOwbJ] :[:|;ȠYFu qYhȣh䂉+mdhs\4R~b!rH,z#+a@5D.h)'C1gz <6 bc+_/cG9<^g[Hθd"8Rj2,r-tݎGYw֐_wh 3Ps,AZ!EP=3C(FI/$v{ZV|\£y cc_KlE"|7 !o&<C/ޚXi0fE`o7qhN/*>m -HX m_` w4 BZ^Vo>ҭǷVKSӗyXxMJiXcٞ9=<85)<;$ [ {OI.FJwkL1AT4=:c_ xAl$;$N(89㜊34DRLc51v]fh84CT^K.\d3y'<}~Flb߃W&њX'lإXr,ݶx)cDii?0e[DThbXIKqu$Џ՗ON”}gm:3mdf1ƒyhJl|θ:Ix?:'fzgJ(Vp"ީ{K?4fN`tR Cj҅E-RjZKKٹ_OaOϒQ!X#$ASA> -c.LәXjkg}wÅjN0Q38iQ$&0`+?VO oRwGªs8a 5wA,]=7cG(.&((+U\1T`~s\æϡl3.S/Ql[I^GvY7&7[{prxPMa5-v{ 5C{*~YSf/ OgV#G|O&;>_$ -1k=Qq9{/D"!~5Ir!&vzJ9uzyQcߘtvgϾVfLf6HVrL [bʻ6kg0Θ}fbh&1@߁#әn.Ÿ5;i![AhC[hx>(=+L@#:vG@kOOg73؝YqO=K'# g{uDHٮdx,Rc%R\(m0 NȎg86{Sh.O5eK=ǚ(b?#6p!VF`mjx_ T&К"/7ggNfoߦ xfFRh.4c2zWFto;tTž˲PGx¼Y~Ju:D6HKg'R@oLD-+3V`*)9="hI t0{qYEE9f^YQf\$j1њ\e6b-v倦-"n@aWhiR@ƻM/5֝ݿ<۬@[Dhc@oLc}&" 5=f㑨4MQ/d2z2~RZh%y>I} (/cYmpI,WEk%K୩<ٲ( 4po.c6%^4wD\1P#-Pu: -V t6G9;hދ tnw1@KTpB4 ,Dйݳk J(5n -%D@h}ߢNt q.W}4o8a{HuHi\Np~z!A2o#^#FMnXjz -Z!*4@OD <}Zv(8|~*mہ9yܱOǖGhI}bbc%ri|+m{tO_ӡb?L](s_e[Tby6֕{Pze)_Ib(˽m֙ v٧D?}Z<:h 2l>͆3PlzEEiW>X,Pl(E07Y@O(eޘ?kDVZ6(ZȦh -fu<ǟK-势eZ҆n橪?IkKúDԩ4a s#.ZhyXMYG2csъOE,{IedY4qcYYs |p-\t6E@%=QtS[\̬zjFPr~︘XȠ=؏avNFP3 xDo2?!B/I -y B[qR;G1AZ%5?3/1>Nv|7<_C>I ->k̟gX -gV-~$VugJYʽ~]OyIqq)O%EeK(zlQcka' eͬ葦]U+ ,3dp#WҴtb[nUx:bxp޻VF\&H"vFbQjn37b4PZ$%aw{ |a3rOiirnȞђ8qoӵ#z'㠎tgΤt/]/u):Е=Aq. t?/&[dfJR O(zD_$/ypB>'Y, 2N +rJ_q9%ÜUb`35UK7k7hzZÜPNK>+^wEY]Ysh1%y8u7&m3^af fpeR4,\mytXi UkLPuQvb$ߪ1޷H1D0l/}lMX䥜A9VRASɧNE lUڪL>}s؋̣-6:Yq-ԉNjY5 mEBArOSl8) m=,{"QQ< -]\ᓳ$SnymT@<LP,A #)>dHA1]@(-ђ{ۛղVP8 ,H~[A=!ϱkSdm;KA.#OZ۱R"%.`/ u^cp."pKz%ZR,(ɊQ -Ɋ"$ˢЂqC04Bf0I%T7N^A>pli+Nܘt"(Ȣx yrSiLAlJ9FT.Mkhc2>Z ޻G"/v",,ﭗЃMyh|^:+~F4zS=ݘ8xG#M9Fw Hٲ@SC|[ Oա* ? -~ )}]rO )m'ռ [g!oE]%X47oRYSVy7:a퉳ts/G|M?뽓/љ`:%*`iZ)+; )|zcR_ r_'cD\N&RЗ@%nnhxOD0JdzD;zX%ڍ$%iUZ_h0kR\/.bl??h~vIiscJV[6Y[sGt_Ɩ/y12K:3NBg-UB$+f \-K <(0k&9 ޏ6^~~{qE;75%vpgfu!ρ 6_]?yLw?ZY؅6l_e+`Qg?_tZ !K-} p?T/'UPYb cm(Ѕ}b ~t$$$8])H:a[)ҩa'jQ:%s(=$"\5sгQ\iH$8GuySPK)G_A1Qɧlz|暌QHaqwm ݜ=Z31\*F(\gb |wk,b-7&4r42t,毬$= npnsuV7s%]T7cOny _]VЉ*]C\Ae/tՂW)WRC\A'~PC\A'A rBU5ttZjq?X -g: -:l *j-/_ $Jvрd7mV/NM_HKZ:_Zm:v֊tUK1sR :S6>S<>Qrh'zd*U"WJ(I̡\U4IdD ܞ -Wc ׇdǫ:.n4 3! bN9iĘ-v۶zIjnOZfAU3*u&L"/wlԗZ6^U))WڷƪCu%}.Cgjy`# -Iرɚ]U`ȳDń_*q zJ.mE:-tR^NԅLsP#KtAuICqu58{'Oٛ5;{rsх(0ϧS5}k ur4i*qS2(QUwJ5r7*e<񀔏S>Iա>U&,w*R{w E+R{J߯T~NE*7*RQ+RQ/Qv % Dԫl.nPT -'-~+fF)z)B)W?(A%pQA)t|LQ2 ~RT6WUˉB{,Vq&z"Ȩ3a.vsWѸt:/r)w^,{=GQ p^8iljF-}eM=l5:˴!zbBPbRncŎEGT3G=CU5KJ}1ԒS{.:>4]Yj??Nzɍ/"Mwzr;tߋ\[M'j:N*&(^/?n|5T,^:'tpkVUIurB7龩ڧ9_SM'UK1j:X߫]j:)㆟;;tRt2ARn5qzcj:Ȇa5+;UM'g[n5vNԕxOEk~N:(\M'["ʁj:) ^Neg䗪oTIlV5Z%TIsuv]ut-^TX}u(.oW'o]haNg* 2!QMa -2UrHP* -4.'ܘJbU.+$H!+apDZLݑŝ#͕#s۲.5ws4߹NvZ%Uri+Ӕ |gsl2t͝jDq6Ew?掭}SNѦ \yII^gQMlriO]tAj2:<+F5ihQ0O\_PH"Cԑ 9Y [`CSe,u6~Ofa  -J%\s6t?9 -:Ӗѭ،e߯T>|+(p87tT/̮o@E%dz-;LSa결SQgr11V0.YR6Hz߫RrKU]fP+zr9ԣW*SN'_oI\vU> &Ey?^uQxۋRV)l??N8.WQC!U;62li(d wJ; %+{ou7)U>`WnS'vSON7|*p'KR{Ew]ÝSQ k_f:S7sn:t+W>?Brι|Cn^z -SGVTtv.v"&(΋eL7eLZ,Ѯi1-eLAN]E)dT趟VeȪeUj)bDWb~UELrDDM{aT~a(qXS7j\SnSŐrtW]I)ou~h}׎T0U=ܔf+o}04T=׸Jj\2# h|NFƍ)}h4 r5\ݗ}z)KLfb'A]TPwcZ?T%-z𶇏)ɢ2<.WGL&W* Ƣnc%rGYB=vz:x@i; c>#U9ڬw/ )7&D`s2OR&6|s V\4g R@oR t`%4y -2=w>qE{#}v!ێ__I|C =:B}&arڪZhU͕%Eumɴ-0}|dl!],)>+*>_.}$>ْ3{+9V(':5qN)"hMDIZBœؔB%I>Au>}my%M Z!KG՗1]Mݡ-n:>fV))Yy^Rlyɧѹ٣MRCլτ\~v65ƶ\J̥ɚ|XȔ,n]ߠ5^_ngoeqBVhՂ% --V6] ,-𸾝T`֏)$Qs~'ִqWVe\v=h 7򇬪BZ]"ķV`Z*uY>ɰgyڋ>lHe"쨴 +== ^nb7;"J{vj]G~]6T-B|ד#( ?Z^[zY]&/kN >co,5@XNG-%HSPeC,ÎecNK'$N>)׺S'>$ylk$,tGT'=V%F|q%? -Oml`y`]qq< SZ8 I7S{a8XV/Fc,ð0stҫ001ٜ-m"3A6qԇA6Db#-lw}rvj 3&JB{Yss\X'L?[mZ?}c>1 $dz^g[☏ͭc>Y?99W J 6Wab߿dwi 5ޥ ۸8%b9Rh汐5&7- +dXn?ow](]zfq.(]}HV[VQ|1SMc+.[Giء=C>fѶi WyuX6-?=:WqRXWƆJDjy]I|${F{Yr_9>VI[| -;bW%2S/-Vy2/=o&>m0 Od5lUkv]'*{ 5Pj[Re=@ tRf´LCkYŕy*YV<[]}z]7S+y)2=˧k\9,=tW$VEN[tl2_*IY J5V*V3HwJ2"->yZfl[ -iGfmUJ&ӗ $f¸R㽤l}܃ftζijk.'|$0~V'Nݛ)+7)0lםm2N’q>esoMn붷+c yg~;Pݮl h u.}C7:f0XqKY|38u3\xW񒹌ij!,z&-qt11S–⩤KR>%5H͖T67|0IW}i q?1V(Iؔ8T^218?1VT0Ky5K ;.`d c,M(5olKyKJjWiFY_X7AUԧ=_:в 9;˦ه ť'rhEkQcM=$pTaDr/؇ q☴zٜFıeG#FжrՕLQ&}]{lEƆ#J͖U,[V' Ok[#0|--rCnۉ&pvYYcR-4-Rqz_M%1*T0g0ÜU7g}^qLS͑O,*JuZOiOʕJmEw=E%MM-g9ti:UCmM}³A#z.4&?*>ROџ -T>u|Kg.9zgˮ]7#kx9mQ~p仧ɇ*qՓOgq; 9wb$~>=2yhjzUqwU␔P5CLDC1RXQ[hhX RZ} I L)&G \O2I^_˃t'*K!*&M,7' -= 0p1>XM2%iZ)cUb&XpMU=Ĥ))ɯiU{p6eJV7I̘DU'7SMAnk)I͐䍾-HJ'!//cUmIa#C'DG~:@~Z5Ր5̔1֍1q8TܜK!)rf""]5{ˤURk 0aW4W![I{x] X.&ӥ$DXss( )rfXa!VdBUoMa'rzf~$@Jws_ .Rʝ㻗[1" [&kRgfݴ#Y -.&;kEfa$ד(ʊS  `RiWQ3f2TފS2VżaZA DxQL!1 -B SLFǙ,dGҶ?W(+2baeô, T] Q4쎲t7%^]vV_nO|W qBpڋ'Ԑ+s$۱U?.%Â3sKR+\q&dU䑐zO1I$I#Ҩ&xe@$DۥU7I {0 D䐊s#;.V <`yJWכGUנ*lnu\4IA"&oTO_O#(I ݲ*&F/9S3$)Tu3!{FcÝD #T@<MxK_IRLJy-,haNفD/ŪdU%uXpItoCO* 7oǨxHI8hdI{<A!yqSU2y7h̝nx!2yFa _ NZft|?pC.lV@rpJ iqaU;qƜ2frv{ӽ+qHJL__kڟ* ikfuPwox.i(5#dbL㾾Wg0'r -JKfX&m^9I͡n1wG L+v957=0BJCZ$/"GidR1I_Qd{X) caDR4w=I^{5E19,̜ eƉR?P!kS+) fLI219?{Z=]J"f# Hɰ[Q:?,'MyDS3q=A-׺LFRFH* FHe5`_>S0i}^ذ]2|n4涃47Z;68$%,DN9HHiDͲo&HN#;_uȟ: #&1F`{ Ӡɪjr2dzZv'7@*7CRy}H"vSGM" 5yȪd?l>\'Wn,0'LW,[ נb矫z -aHKުf4V@],υC):[jHFo6IW T|fh0s/F/La؈Q!0I!rHujn*^?ԻtIiOjqfh},zC/*^b2htYH 4itlkO-UZei)nwN7ug22br21Iv?kNVar2hd psg&wwGG-PLpk_o.RT@y&܌GyN) 6EQLLFOzq{,jx""襘ԥ~Xa !otϵHy۟i)iz(&o_߿C4y)_CZ`3oB"wsI#@nY9q~ַ#!ުFٗ4diȄdߞj\4`rVOjq X$MFif2B 9ު$pK]uz8D^f7zw7>d&)/2ÀI냀*O80G`4De$sgcznz0B"G۲.~UcV:jԋ}+F"oU3brD}KTʕ'%kuBG2,mH`L$)$%n-Vf|D Gr9FIMA.70g'9|*29Ƭ`C&jFY?씩;Bo  i6V3E,`*m&!Y*7dR -ZNJÌ(#]rHF1[UMׇ͏YS3#19Tq/Ywշc -uv.0]S1?|TE{ I5 -cJ\L~ea"42a:u<;{Π'AլHIC 9^cn}O wE-C)8و4&){=3`rLCG/n`EBGbՊ#(џ yyȱ}~= _g!E7"PͨD 57d]e5߻.tY#hgpAogoDNiJO QpiɋGi7&% V54r\u+틯u_WÚgЌFoߎO19ɿh>}۩r 9o"by!&#!p@pZU8%y;1w-u͘U8ftMUe %L@zhb^ΜܜKQ kyyyPjn$Qc.%iL\yжQy4K䕈hX{C!IoňHXjxŷ{QŦ5ƍǻ=mr8̀ɟ>%Zr哑5q$Qisal&tRQsj&xŸ6:\WO D:ތ!zH$MNvaʕOAHrTa.gV{j+ɓ4ӽ7RäբtS桏DFD@Z{yfY LtWNմvnh0HȤ"1` ɑgp| oT,1"YzO;[1OQc$ϾyK~0 Twjfj1.&;՝IT]9C^CR8>5/U[%>^`Z$`j7z>$ȎOLɏtL&&gP;t$B/vp0hzUT7 aȺ<8J<[oFw7y7kW.V7E5=[1V3hHnɬ?0,`&pEpBcnp -RiqDf$q\<0-y:4շb4&GvNAẌ09tQr$5fDcnţ{TN#ra=BY8TVU]8'V578m9̺&|v0םsm}ceG f/hhbE^!E3jOd^R,QPߗ'p !@f^uN*0N3T#Om2~Ivt(a2i[U͘ljH"FwÚ]АA&&G4W-2,`J# W $:@!U砣ssx -3rvU֪HSwotŨ'hۭp#ϾMz*&#\/XVFE`fy[Z#zI!$R]y#R"~Vf\$ˬ&J%)}K=`2"i1v1fI+*^+$X# 5p1W" f$Vn-X߀tnL9|I-#$mkoxH8t<̀_wȟoI,&ͷS2Mel)c81Vvz tQ1)t"m,wL7 Ez@S$4;~_>(S֖А`j4&$%`r,]O70 ,bz Q^Qec8~= ;AX .C'$b{}/Sͫ!'yjݩ,VQ~Ƙuߺ,6ſ۔]-?zUvoD*9!ѪirV9āZOeߘ{,*nX&.&h/NCB譸?sU9?~9Lwf;='+eo$OqYzg춆[= 4`2"j?>!>5ʕ'Hkz0$G$w&xb9RjYzL{}|k !4&_>Ue}$'K`AOc$iAY(<&Ɗ?C@Rx2SvK0iL]!/YveyriGEELj`JKG=o_x@n8d1E%8{r壚'wgqQWxIAKUA0Uo]E'ouKГGΰ&#SfX+[CF@G  -'#O`V/`탞gC09rZv嘘TdI'L!8:xI'AL? |F"CIpv_<( -4=ؚZQ - .r eTg@3OI'@4+tJArf /6Z}{=wwP˛Г`J4˕Y|L3YncideaB>JA.vT$rv 0sxW= Nk,`jwIUqEѳ0AŚt8 :A孞_G$T8M=z?q)阗O&9<5Z} 8e DC.}l=JAF.&!)A''䟷˟!J2ʕ''U#j sלQlQ(ՌgQT5Ѭ06Cr\yRFT0thI } A -ϳ&}V \n -%8Iߕ4 QnA++ `7t1Y*.ުA͖8y 1`3DLjh8&ӣzvk8I!Ch'A0A^/!1Y RbW9I+YE=qX8}2qSj}sWBF)9uKIROolxn԰e{L!8|M)>m $3^'& jn)&$]&8$f{XLZ4X$Xƾ] 8h+dߌ#twW(c s>so@|&&*V5-JFoDqP8zcH rG"#yp| ߻"$B(G\>V=)B-8݀ 8i9JW"ϡ¹Ih zYfTNIɶ0YQL7Һ.sa(L&M+ܟkܿ,pEUs\!?_=v;3`21JA0 -=v` -na(4-|U5c)N5VaNG}]4IK S6n4IQv;0Q:|W5[J=`2*S'INjI(-o4dIH}|`c'rN%B(s|@w/J/r;8r UPD8o= /Asis;=j<`*8PqxdbSTWghqe'#?^܃2A 8sjaj0:w$ -u_>#3wL6x0N:q֫n+^oB(s]&]/QȾRcժ#!oI4Nn) 8;M]ހ LBB3rXUpLi%uu\0szS@p+ܟ}c@~ルnPi$l.o0ȁZZPГ' $&;(G<=yb2HͰ=\Oةf3qzt`9DR^t}퓰R:d1AOf+SҋYr&xu8"'>UN%ha~&g'y$zA|v^gTd6(!zv#bJpGtzք^ovviΔ,`oNy{ TS8G& QVJ8bQ~UX){ћ|êZFoD !K='쎅\y -^M(ΆV-X'w> vĜ?-PQBOf5e!\bTf6]O2!n' uB~0dGP^(=wg:i꿡_;qрI犬 ?K4Br=a;Ѱ"P<׻6a&$3x_]/SL)1o,*L=Wd OSUtg7*9>]&k¸ܿ,!1!|2-v&p"y͈&S'yD&փn n6Xja E(G $WDL~UP!Iev񓀳9.aPCFPBOfyaU9ߕBG攙Oe,rR d123M1@FE7 P$/z<%~z2&뗏{`U͇U$0. &:qzRГ$Mכٲl?|ҰʅUDLI#{X$o1enz`l^Ü&tz_G$R6GcГ (&٩&_#`2S/CTD : Q=" &+ݟ:y2 ==~7Tʯո:J'{Mpv;=`DqGBo5}(sc4^19ɯv~ħrLiw氠Qv`zR -V 5 -mG}z*߼,@*I#`gO˟!Ji' =Aq:xQBpti$$Q|$;DL~iXWv'n1 ň⎅&(G |ɡ4~V\!bS Q7XuB.sl>>A4IjPg[2$e>Wg=GQL'yRIc搰}d 8<޷;x@BJN:0KҧNM)8LgMָ@03٥I޹GrAB.1%19o?\'7onASDnB7NnTcAp2\J' W )s4z2b:Ld,yI՛=T葓AVkG&19UIܦB$Ĥca^z5EEx+ބ99W7 &$_K=(4>brv0.vf>|"&F^na7Qݶ0 zsIgC zwG&0^݂99Er)ov#%gbaU(Wp!vGhIIByDm H `T>"镮AOPLɿ9FB:Ae'Q)ȱqLFQyc.E )o=yU/ڮвDLkKeb=\T DZ^Sh&A:r?xsГ9<&i}'t(WԌh8z,:.  &?#CbN\ј=9$QߚwA<xC-I"Oo؍ &@Of?vPN)&!C~Nq%avTn" I^ja'SgP2I>5[ $q˳4D"4ŭiEX{^\醊&G427f\T ǙdND  W&-9Г$yh?&mٚ9z,'s k> NEQ^&A9,+j>VLaUY3[nVܦvw1 `Zs?} X)w{!&bz#_Ej{ۥ `2#mJ'9L]U\1'A0s>=#$)9<`rD&q&3ŒQaﱈnn\$_:}eݺ5oƜ'k7ې}qea Y=DvH9 = GބOyQ2y~]gR=I_w M"z$2NתVQLN-LkȚ0락|cI0ʯtH;Io(p{sw ʘ;Z^Ü&\fGP3޷;;e5ۮHI2j򏞔>sHz4VbҙЍ[ ΠNg띕$Wm1!0Gww `2s zMZ5NL뮅B0N`>ˬ%$eN=˕P寞Iop2`/8NyyZ'm .#,7M=Ĝ#Ys^O -v~ׅ &Dzs 8 znŭ[A (!Q7$e6'vKRLLF<%}VDd&S32qZ/0N:EZ U -g7:<#޷;H4Q^>Sij)II A8叨dDo&Y[ҵӓ<`Y_-&2J!1TpgwTYl .5V;Cun"$eUno -D$Q -੔1{%Vv2 -=ul!ġm-T6Ua -$j^o$dBJ3`{ 4L\Ǫh8ʘ;d$עL:G}*<`rXIzfʳTL*KqIGj8 '9YW;&b_>鋱rqg5VƼ3L#'L /GEtFE٭CO:FEI 7%R pej2+emLIђ;[ɍZ8쓎n )Ă hKqo!;K8m!4`2" k/n+2i&z,:6VaH30QfodL|Sf73Iu)82q 'C.vCO heGU`|pD۵4J(WjaogMr;O#a^tU`qԌ~<sed?)}|]S/z=qn -= DDGn^'@ZFPrK^߁!aA@O:9 =iΠ1='#ov*g>_2SOq xqҡQDN 8Rc}AM!vXt"ISa>DQf8|FfPY!`2IC 89UhD'ox. ^ Lqy8#9FBg7ʘ; z r H*]9b9W'I^} 饦4=qT -rt=Yn'AL (3v19y(Wde̅(āqiH0x?qS$OyNa(FzCRf&_ -%/WNOpwS LG8LO* L%KGo=CMaLތ٣'idDV9/^ &_Bi8٭,6 -F? A.F'}>Ĝ#{l'}0-W4`Q3Pg7N_A<]dL#:TPvrI^arD!Y7I%x>Vf},,rV&2l f);9(OD޿,XJ0I䏟>4z,Y`TW˿ո~.r3# R\mߎr 슄8ĤsB'A 4^>2ޙ R՜lJ+rnEﱈL$bg6a-0fyt~G,!,c{UIDO9i!z?J^iHχ 5x\chdv4<ɋvn֍7a\M0k1UG5-v"`҉CO: zŰ1FsV-zgovwC^ Xha솧i`WAyv)yUp/NOɟ˟9! &0!#'{ȊRq8 (?_7 e&PQM?ͲoP4"쓎^v#rbY)Ḿ$LPoN[r>@%_FEA  @!"65Mv>"'y7=U4`f5v_71'vx 턥a7 H]\a"]V9o]!wR*A&_? `stǢ` @"Ham?`VM<`?)}ZOg;NcDnj 41;iFT8DϏD'Ŀ'b;,`e3=QQhULOzuݲʱ$fwr$$Y_o5]094I$-9viQsu"awo5 F6 )^o{$k,`˕CLS-Dq'~$'AGSt8MqZΔ$q C.&_ɓ҈6IwzE h:x8| *+;GE'Z,mAd !jdTUhDE[x@J8EuII.((N_YF/0=Y;Oh’ɁZ.J7)Y&z(m[cC(\^V!)HV%fV{qIɖ(> ’I=$PLr2mou ju|s.)mcBF2t; zk;4 DuAc&'-ODyǏ,~1v.)%{6y缧M_[ ےX\%JdbDZ-ﲭ}Ip_=mI:ik7i$y['qӦf'M#.qQINw`\`}p\o;s?{YB m {QƼ˰ \&ɗ<-e4d/VR\ cl LJR۳R -m#8Sx7WU~& I܄= NҗVfRS{(x<qRt 47*b'qZ`Ad=O6IF~`0\&J,yq18M{4R(+-wԔt"o JibX s`DOFGnvX{z>{GTQ2tR>WR~Jivm{^(e᠖Z|1 07b"I[ݒx*$u*nH`{Ձ[kmQݷFbkd1s߼hZhbv\LGŔR(Yu"o=c 89Z.Ko KY1^kMQ -B\K8L[ -;EKf[ҚWrQʿyȎnG6/sfX0+=XL-bm&Tu? ߦJYX|Xe_w 2G7ZvN1B&f4 #ʫu6d)M'bͻE=P}T&O2(3-ybgK#)OV^ MWJK#PJ > 0.?Eyp#sF[VqMQδĔtSєsX}C4J5}9U_rRʯ+m7(є ߦ yo(-ѳ3iIF}2{Qbo20kttwoj|2B^ .we؞;&66[ҼXCL)tsADs.܁zNd?2[F!,3!Rǯ 쒚LC{)M'⓸sC>h/2A{/Vn?h./·AҜI]0׻bJ~2܄S9fYN' W2(R0zwiq{-XnL/lOʢe&eӎ@) L-ma N勵N#Ey a@ rcV27J龕HJ]ڂb L1bJ>9V;[V@~ᦌ.}/I(eUJ%^Kyz]B$]-p蒯^}?ȁbE v̷cI/Wò eLUX~1 Z2b;_{0)kw1\hTy90wTRmQKWg.A#L}JM$D|)ݪAӼ+=,Rb;Cu伶Jd߷Kjɤ$M - -g'+]a>۠|V_v+eJo}2vVc0WeE0.XǾe)OVXkkRY<5RKmls9+W{1 xqrSJuԽEPvVI(fH7:.9ܜ:32F1A/:} qHU;$?Z_bRzH2->z-h Hl@&ދ(DEh ?}j(b<"ƘN 9 IL Kˤ"WZ=;YE_/X67!_qgUK`; RK-jqv<̉&8{PXB!l2NJ˽rkYJ3œ5&2Y늠wGd-jۑQYxr"'+9'ypP_ՠ|1*]j./Fdp ҒA`;Cum82e))~~!ڞ;e;aCμLJb(9Pc1d^oZQ0343Yb9^z;K{1ƋFzLo!\&UddIł3KLFtj8%c;O8>&%zr9ɼ v^}b),ZP,0b 8Ė'xE"wC XT)2 =L 2G/֡ bLEe4y|g&Z&O^ b;{u%iCwے9IrYR>"$ojLBRv d2"ZC&OBfons r=wD Q*puR &g>9)ZI%N:I&5ދЪA+eGJ0(DL8<ӈw[xJ,=`;ߨR&uɬ@RSR\)OVXkR Ks="fWE/~BG6 tR6Ljbָښ-dQ/`v5b{&o`;C*'Ah(ICe^l/ZX|1=:R<|a s *^k2i.vVHDe{&_\Ldr2re75TtWj|tf)[4`d>GQQQhJP5VIHf&=XGEˤz/f-/WJ2dKo`d~HrZצZX Q{k^d# bָ.i/k͕4A)#[ ' Fes9d-n%}xi|SD ?<UJw+6b&ATҌ(6J̢~5no$40#%ů?l'sk {r{q(fGx]SJwѶ\l_׎OHhd,I*6[8 /k0d :IHQDD~gᗛnr`JGc@#\f`; *4S L:ZDVd`bvr,7K>y[]YP}Y)Ld轘:sh e JGw XUmN{-XƢ"XeznOXFY2+Y{&+!HͳP8%WRr$cg%Лb%Лm܈hM!,TZɟ*scI/}pșv΂ދYJ=QlA)s~6UFpT'RKC <$Ho=@@,0MJ/R.5W3(gZ,u.}RK?+eD)ܷ<; -ƀ=a.uW.wZp1* UJ?2#ViWپJd)mSe['CX=yy -zcB&3s rQUFQxFɾؽÕLP܁+hn'3ZLjyn.7 -ɀmAd.Wb`&XՠɾXr8F/M7UrOffT%DDgi)_t@v2IG|Lf$~e8S9FI}rU }g ?}M^g/"D'jBIB;S_5OBs -xs_.|%o!ƞUeaR)ELlnK^QTTFml]n)9O2iE2%OtK[o(O ݳ/H[0I%T턐4,E]-w:JJLrKF2EK3q+r'3.s kL &X6:4cW7XouiU`2<֯&;Ȥ@—qP_iɧGʼnB&s),!ڨ?G<-d2_r;y --D-!EiѪA3iwUy[X6in d2"nSeS-֖>ZBn#%Y9d&b O1Ĕ-t/%V v endstream endobj 30 0 obj <>stream -dI"![xi n/9Őנbvi΃# Wj+R?Gs{A.w-up;i{h[adv[e,yl#uVA)uCI۳Њ7@3èIq΀ދFC~B?WUo!gb(e/sC&+&ov6Jrr?6xſO- ~}\)@PyGǔRC#IXLcT|#%AX%B2Yb!2 p _a@&b[(RQ{1u2YaIȤ HT.moAL2en@>.3)[ՠܧ?|B)SxsAȤB;HE^z}yM d-uVY-\j/]f9_AB>xR&ctRuk2iig zi5VwYl.+.i(6f[& Й~cJ)a˨Zm4g?/BX +dO;..]&f]^$_(LbR_5/Warj,QJL@&s ^RtN&`@A'|;|u /IuPѷzjB}!d2gKA^4nrK'r1mPs6rS <9I}g N!E(~SeEe]uZ$Vx8+d2W(W/S2%_khfOk ʹkۡSwA /Bq Rkkі k| d$|B zh$5SL:[}e6w' .Ox=C!t ]Lu3|j2Q -Np<>R?$vM%,U`5* ?QʘL 4dŠ&SAe^4w}3SJtc C-Ehy+z "{Q*7jwȇBLdd2nދF2!¯Æe)_҅ovKկBIGNgS*yZU%> @jd5jbH ?WʷHU*2X-jzf< #27yh9H _hhLPv@KE_eCFNRz*Al %Jq#oDl=k+VGU -ypO@p<נ`;mM{;L*ee+Ƀq/xI]4EKoo`iU}N^flk 4LZEeV:^bڨÔ_qVʶ -[%r>W߼ֺG5Pz+rp8dS5&߆C *d2({Y; -zk20BWJ0l52 CEE3'.4yj,RnmHׯi@/ukg*L+t[EEjZ싥R6F9ϒ֊hIJ:x=UrB+~{:iJ)*:~砪|qU-̙IV,ɜ Zg {!~62k.H0)LQ`aKNaX5!ěa{ۭ -Le&ODPRhD̢zMI|VM2mC r6@&MGyMBE#`UL! - _CcJa^rP - MTFΐK2 8X_by~݃ty 2iz{8얮I/qJ!{5f%((A)*J龕mh @eGBfd/c8c JyABRJ.*ͬL%v %h[&7Iw3=m!z/PPj *Rz -e>ɖ->RjUF+7*3AȤI!pYw.z^%XJ٨|>[,|OXJ!&視7:EJ}eM)i2 4%%NyTKpPGW؎98D \ V\#VVpR]>qp x2Y\Œ1Q0{ՠx!tL!(e7< ̥ z*̛y HLzL~cUL^4ᦂ+³*OynͿG|c#drPLphx2%W[_b!<@~_鮶^)Y_qynGVڞ5ѦA$*Е<d -{QAas#6~=X*nWN[aGs>nWz?*:;&dɤ[/OILt; l b [ꪲ6xL{ Rl'$JTW *!΀)b[[EaNK/z/ɕQ!UϝRs9=g™G/ -½2ǀiGnmSYgFowK9 VH.2YH@@PVCE?t׻#Is;/. -Zj z!` -%o?PX-LA,yƋ/ƔRPGhC'oFfm]QC_iU1p(%8$ -"UAN|Zj^?(%0\&LS< -Qxa7^eGӱ y_8?Y'eˬ2 -@&p쨲t $/D9- w0A9WʷaEYy5zZJf`/%ker(DLb"IދqM~qs^<#ѓ7oE-d'Z=b9ݢ[Ƞ%I H~*g-y㬷uh8Ӑ#ydUR&)3J7" n TLTXɓy2 ҂ ^4^mȱQGYo=#ן G7$V )+o27C46ǀ -CGNlC'=t29c{)J=V ;4s74 2I^Ӹ2 2f)^X<’lPH㎆K@su*M^[' 8{K/a_"Z.| ݀8  Q -0qYO>r?ͭFOqN)e[7]Ɓ ޿[C36Q LwLz/ -031-]i+\)ŔRc6K:ˬ*kB E|wE̦zCW"0 VjU76g/K9mt=j{JpdC轨3WZ_ټJYc%JiS/>k2iкoV~C>'S لom`fbҡ(eH}RWJ-]dMچX?-DwSTؐ&Fe?A&A!3,x @8d T ґVASzm2e)CNڤ2Y Q+8*J LXc=BAEpSg0&@PyS'ԶMJ-fDB놣 C)PYf˽҃yuOfBT)UVz}k‹IޢB)I9]nRki蒯+dʷgf ]TY`t$QPQ`%(yv>@DeJdr(V ' Q ԋNyU}47:R(ޤ[듂(Ў簝PJ@<39$h"^L1KMXgdD)OR|YFL";eR|>IPB&uA'K\*Fb "7f C)A^Q6/Z9v Z5ug)OrTaj5zg^? ">M2 d dFE}J}砪J+ Y A$MJ HLA&A‹Hc[/&XELDK,3 ]uC܈"60Km h[`L! @Eݹ&/n6xqur8A(cuk gZKA$E!JJє锒akZe%M">Bj렔 G2)Vo=ދB/8Zs="JI>J)Z$ '7>o= /A轨7\l^a%2٢rDJiXzL$Shf,y4^4lZ;Nº#XFd|/ -B) L~>zuM -Q@'mxMzp~;17j!+e?ǻX]J(%2LB&A%oi ~? _ -; x+xhA}jd "_ L AE}$`~9,S؂$BnXAep^\|7@ qa;cْl4!I>}"^)F5cd$0轨 ]vV{}AHI~ށB%>/³DCGsA(bSD ø'V~[zfJYl)J;dP5z$f -`2Ȑ:{QKޗmd^mvI$Bf)+XV B=]UOc8{E2ug_Qa@Db]2Đ=’>uǁ16W_AIe)&?L^.c`0'm^DwX(Z:fh]J(%ȘD `h!O-I JAD){O"RzD)y!f.27B/y.\o>熞."O'_n*TYJ"4~gC&P8\kIFoCXPͻ >B -ɣhi S^2 -^T Z|2 .<#ҩK י 1TPT.\mѓX3AkYR"K wR&* -@v!uaCFE=ˣ{@d'x+RB) `{&c p!>9Ʃ ʍp7QPR"K d[)U!.$XΜ Z5HFG- Îy$x`@dhJ(Z "~oyIg$[Pk;]Lz/v&g_9nDD(N)$bNKW?'?)7 DMtpCJdLs=m҄h{:RfW_y>x ᓈM)*uUwovW[*kZaM|tt:ˁ!Ň7L H qf7o'tь1QG/C&rI?h ~z ]F1sD[-=!_>({?cO;{OS⟷ٿl_ǵ?K6Y[+mCL T9Ĝ9 vfZy*čmZg;۠@&rw{d(3*]rlj`kvu:#/9ۣ -yA(E?SãZP0HK|E;.T9N4PN2'n;S_pxsJGlyо$|O/Y_8Ĩۻ!UKf(VK2މ:p`̱؍ȝ`295F t9g,wE)A7uQ2B#q L>ڜ:?!1+m>U[n $Q!^4^Vy l -O"r&OxVW#%!8ΤTdՙ;gxL .ϳa4܄g;s{/b;sy +fN '}|6&f<%?\AA)^{Qc$m/h^JUJ$"ѣ= -&HQ (D c2fJIl{f&.v {/cfT J-fR,DHС8ޭD^r -(@(3dU 'mF>mDB6r< OQ -NBXNӍvAf^◯4Xn$"u:'ȋ9 `ne!FՠtIZ>ȍ6H]2v[ǯ 7aA'NtEF۔ ҕᐳ?Z|2DX"%sB轘ZؔaBnI'SA/\"HfQH!b#AȑD :>|~SeE?L@n%sB{/6[mR_LA2Aּ>'sRz/fX'P%ּ{wU$BPwXHL22NGDڴ6bPfMN'I%z/<9<'J7O"DEIWe.q.ALK!~ͮ>?-"{.8KQ/D9-a'=:Q5(9Xkͮޠ`c<>2-1r`v %Ʉf,y'bsQd|h[$H`vY!JL-]eU05[|+xh@d=q&:RyẌދ~ -] |2c"B+VSrFT =~'_z7"_qˁ$Ћ-x0=|fGg3g[:9"F-[vD cd"6_R[ ⓈX!@O.`%CߤyX'{]}srtᓈlA 2AW^L^ÒwZw. k-y_9>Z6EM@_& Yֆ#9i>['? '_oe3<| G$`&}ދI}2ڊKE5oA>?܍݈l/-!&z,#@ ŠB޹ߧXIQ,y*HLePu ~ֶ>R͓KI`8hDi0ƃ&C|a~QQ|d1?~-pF 7`a -C9>yaAQQ'擣I%&`:z\kp(2 U`Wh1wvQ Iث] Yc 2Ҍ"iщ$z/$2I9`qVCYa({QE!4nO:6zɺ"(%¸.z(ވAVA-JZW$/~zǯ']:ƻt$6ZLfSJ -+f>++=R)b!:& =$/(; jQ0~*g /3Ƌ?BF'IWÂI<pb9Ή''KNvDP)( -/D)/AxPhs|ȂE jkkc)J,y# tqD; -(Hf UC! n|'$B`#[F" 0-#NNKMX򦇻ɏOOnuvQ"E5^g=q7:nI -.(7v]'Kag%OmJ,E DDHN] '轘ە.OktwBw1E b)J$B`>GUsr -/D LZ5(,_D|aM%{֮-^)JҍnǺA${/TNLtK&A'S.}V9ةmhA (:,"_> r ZM"ÇC.]u{ӝwX^(n9*@rH/yTNݒ:%VչHw9奄keQJphAwh$hrԢ {q(S\C}Cvw-PG|(yys(?- cQ-@E @vދs#=JOtA>D>X:wX9틉F޹(.Jh!AqQ;^e'AN%a(`;Ui0O},șzѣQHTD8qjn;ݎwQe{?߳՛W >(.JJ`m\n'w7OnS1i:f LQNp/WIp|+= DNA'AnB^vP4=xѠ+2.rWis,Z_}{+֊D."ʘ(A;K2`}6pU՛]+/Dɱ.Gd;Y@-J7hԿ*d_D?١]IyM:oR 2ɍ0vot~O/YSu7/[U)Z'{sh-2|Rwrsv0VB읏!5/!=?`},OFR妍pg8C1C3N;G/tw++dx{[rIo}ѺMwx*$&Z_ v'D$l?m1A^v9iw(5lrszϯve_^i|.OB~o-am7v!:E#&W7^2P$B2܄S)@~JC\8I_J^3~R΁E nwњWoVܴv(|Uv& .rԢ4:/Z'V? =v9w(qhe_cqğVHaMO"%aޮ`)YؒǩY$ g[/VFaŵ.w˽/yrn>%"S<9_z:$݄,_E>^9yr<=UWVܹr*VB{wϧ$RقXJ7k&zy:KIcZeSr#:@>B.|ON^5(,_ﴝqC<82v`t۳uKrO.x|)JLv?oR"/#9 -Ǜfm7vMs#َ>A%aϤ$רXN^ww8~#KY9WhsspbjZ -Q.^UA|_F1m|^gw  .51UYop1 8C@/T!3ใ,3sߙDyRJ#ڴn-N$S9IltD~G!#t'*z8"z;0ɁkKvr[Jv׬>'z&. hxL!yuK%kK W%o$UZ%2$U"J9,#X 9k/p]<^'v$`g Gί?yb<[O]w(*~*$/D DLlHu9>~ThJIiapIi6 XN3rO.())%>YU3ݒJELIƵ׏.]f-cO]W΢uItC':e' JiTU6) ~"Azkm -Do}v:zK;%l<3{?a܂;q0qd1v'o+nj^\H|rŲ'){' Jߊ:$s.yRDL~ o{f:I 薆CRvŃ䒵sٽUغg)J2T߼/br eyؖuU5| E]kK'q*1m|Э$0{)=C*Ҡ;'s婭6)2BI>4Xk҉l<1tHS\SM _K=?yFYJzʓ= 4>-ˏ֎F9VE ݵ2z*'>vNrD^O33U)\%-.wՠp19ΎH8G#NfH:}qmj% %}O^xcgVm[donv\iEh>y1WntYjuެ֖y;o(%"GOr|G 0;+e,sDrr˻lڅ}47.޾i?=0%ӆ#%IX~Ľ/%ygqͫk.V֥dME.9;9`䊻7\^vὼ,ha@d=qɉoE)9VYhj{vK}bͫnsX2v' >yQŒwwF’ ;'w]z^'/U=#2"4F E,_G}r{vʛ|YXd,E ,`e'?`gp>IP0̡1ˬ2Z.=A${Kuh˪{Ui/>~m?3$%ow_Pg{/>ӧڲ;B/*uawjnqƋk\M߄7Y7.^$KQA) <ި'KIPr4Ѝn^*/=r֝KjM$Nt;yr%~[X"q7\pǽUM{!}޺ ?^|<6IIO/!>x[( OhA vg}߿dR YVװC:/km,L&T/yNnz}?=Wo_[GnϼcRskͮ3 Ƕ_pOBɫ/$_y %NyjvSDsPȤ1ay9i@IbP }2wO&;o(KX@Lw r*E=@T+HQ&,yg~]lE7 %Cgi k-c ?[n涒 -{|ɛɹy[( -xO9MN3d+UjdR>Q} L#ח6>OrU̖O'$fRtQ\1ٳC^gʋJ%|[(O78_z'qVҺ.%o' ŒJ/yʺ/b e!+;9 >fZُ:ـ(}V'}&)dڲw[v4$1'z2h|jK'^j W>=ms邞JKn1% W|]]]kI( ){nxy'JׁUxa-?q6sJs՛}\蘓dpE?і{u鲵U?$߹i ;Ҫ;<9WArCފ7.]_}r٪: W%ɁOVNZLG'MW'|{ڍ_~hY.IZ [(vխ]pOէח#' 'y  -dMNrOM RriD{LmnNi-^_ɞ9irǦ5KI1d -s]FW[뺅Owg|;GWJv՛w^'ɗ+O^[v]\zw _'(ĉ]H^P&d0B)h+p)^ŅRiIIfU:#e)wmZsr=Ut;ؠ ;c9ejx'o]ᓅp  <3W:U-юo_'XQY jtҘz@17).~]T4r_5|rݢLɥk Iz˔r8w˖Shix' & Mt>~^A1`Ԭgyw~7вTaM2>EIhw0V2M34_e9_VB}Z|τ_)Y͓wK!G葜-T,\U]rOz}?e'$cRjO*̃kܯ=|ÕL=t#$Rn(u0>Sd>]Rzy'D7Byjnh.ge4">w׬zc}ɹw̽ Wo]M|8$?ûxO瓨Biэ!'gGS -; =7]T[;sD;6!yj[tr46Oj0%v6<9߽,!8v;α}>4{G:l+xn\n$vNI(^mƻw%yG2]PϬrW%%)rY}k ¦N9 }mtBˇo/e;E˖a-ϫHQ&`Uu[#|㛏.k]E&H/,6ٙzᏎ]&>ɻpL 4l6&|x> @dC)cЦ*N38m{V[͛L_)YrsYϞUNز$n{OC݄2܄I!)O^ -Gd~8!-[dR.amߴG7O֧?H.79Yhuk=MHO#޼7|@F=vN>RA0{U.G~ۜm+O8} ݸ_^G/hvCګϒtwx'L'ݱd?}V8OHq'Mך]g]eeOuUuZ~2"|NNm{(:eVy9"j:~]zsjks#|B_OΟeQߍIw{R7'gɛX_GcdLG,+h+h'ᐳ|'^#>95B ,=HN#Vh!t嬉B@ qp5>΋d܄N?^{zxRgSy2')o7T0eAMkheay? 6E.9dp-wT\r٪ū*z7| %'[l7:e$z޸*i2 IwD#K(_w[L Ө'}m= s覸]lZD jPJǣN>_y |dysC |r5'yI[&mN3Ar0ՅI Х{:N햾,wkǷc{ZWۥ솮z8z yKz܌g)wоE+0BF3 ޵]}džr^2tg˫$R -snt$z,`20Y],s$u{n(z9UR29Ds`Fn-38%$UB<ޒOoup]ЊQˉO.>O8ƻ\UpvCjT=ϞQ:z6.ٕrOwU8-Z;ZҾvO~ yGOMw^Q6]J|>Z_}R+i4iKuKF x8<$b_zpyew2nZo_[.F(:yeisq;L1;=êAp;bqܘqի{|i/X|ɚ;hz>YA/5yc&Q>^~GPM''v|.-0?Yk't6זm Rht^oRu@@:lQV\jcc++ޅ#agO d@vnp[K|rC4ҵf:#Z\EƑsJ[7g:Y0IV׷.}rmO.Zᓱ/]k@hjLo@iqQ83m)pTiˠ(oXGyrcww`ѥf3VBpœ&>rH]E|I? ~ R&Xncs{i9lkB@f]ݞ >P>R+x. -3$=dR\ow֭D,}v35M?}io*>L)4emWmeO<?1D wD9[Bdr㧝|%ҺG/(“w4/&mj϶P_Kܛ{DIđJɋ ;,#}<}c-;˽^MIGLن_mx]|^AYgU(/7K's{7-JXϪWZy6 wGCu:&&ֻ9C ^w-gI]ADnđssfTss{+K=|3eI̔~ܧqc՛}>x+HQ}wt eK'{K;\tly'y& +$?x{Uq"⓷Ts\h'ᓦ -vny$id$יU,yIIb֥|%Z B;t0po(6=R0a_͓aY]LtKGY>~#x,+mD##/(29\?ڡ:T)>`Jzgu•uIF$j;RrUNBOИw>ZSY^mIΦs&R9qsLj8>IAŒV4Z 8_yEk$ndU9'LMh\JrA3#!zyO5]vW{ʻ?|ӻ@Hb,$BIYa=K:-rCȞ| F)pRZ $l e{;a{XdK:H|ر-{ށZkA}@&N`#")z* -K+}r!y1~jvT<\R3Nrʇ7(e\ab\ ]<+y=~E59N'#$ǢHiΎؕ-[K~yuLF͢~rE?bzr!_Гqxg+682udshcR_<ԾF@<~{׍W)Ma?X2ӓ9֟5zux{RÚ;/UOuEOLI~Q#9F8{H]vZz]{;'~A v%JƏv@ιAX}61rzjM' =V3>C oь} $One<ڷi ?LħCvˑ渕&fᚳ\u'g]~Г~Г^|mW$a&IӨ,ŷ,Z %8Dž%Xӵ{Oގ28䏝y֟,6G %R2pM4cɏoť擌fd$hLES>M]<7PmHazr›]xً|AO:~;x,E3YRͤnROɘ{~-m$/vRԀ!7kiݝW,o&/b-?Sl (+pfޯOsm]rAG aqR!I9%yUi9?cE?1=9g$-R%54$%{>].0nR3֘s_Z6/Xwwr9;XMo|۳W$qbs̰ҔN%, ֑5v63ThɉД]k1=k d\-i9}N\㕋;JN[HX2+g᜝w t8bR^&0?<(ۘC84o+֟26p -՜}`zr߽go[y'RS%rHAyg3=y_O - SOZ-&er"3qpΒj/Wbrә)g}v K.Ȼ_q4[M$mi-?W2*5rd[mA{INl CAr_| of$h_{:EGf(lю?ŋ) =?Kd=} -:qp $e$܆'3՞,4e?/䦴f더cxsSޓ_~ۢkqz.ϪKQEy͢VĜɵcyr'> s *[*tܭ];nLv8D:ӗR:οzhx7qamI:fp8F2C #)=\a:}=".l_q׌7Ef(L9!N1>H؟ k' } T=IC_*_n4NX΢4nV[ozœ]bM/_RIDեRCgam0<()zEc\xc"QE_jY!x.oQl,[h~ GTr,MO)KK{M_|0 -ԓk"I/>Y_&bsIFCc6'kˮB6's^]>?xW^uݍL}΅#k/Ĝ_|?t̾ $%+U#SN|ahs[<[c"iJNd-8Uz|YLO۠'lJUd%p4]Q-OFr3u- 3yݷ_~pͣi[-k^ -)w:+v[mXjv׬uM!]5k\tdb'od\3'S]xm&=I'/6l9"vU`s_`(,UޤK=a">7龋ux 7𽠾ˆ۲Rd#*(Ԍȝ~"Kb!Rލa'(e;vQ v6EڢkL6gi31 =vܩTvz#'l+))坩ƶ.Rs]=w/k.1Q{iP+jx!J&<ʖP"UcQG:=ec:#ВVĸ9\&&'EX]xRbi: p\~oW,xbzc8iqN-ƥ eÜiWb]6gL,ޭ}'E{…(t2ڴbuəl$%{% +Ye&$0-. ՙR(5=񇋖n~^reSՖ/ݿSbXʘ;JG~Rnn!*Wz ->ݲ-Wtk[y… 'x[F tUM7 տ-UH=3z\;/榠dz7_jY+Kv j[G-TjʓG=>LQOx4v0rf]~ Г^\O >- HR2i4/ iKH CuAks}`~=Ϋn麋Fi.Bk^J#D)'-l Mj,8>ޛURXR alϕd`GB_~J]zE'b8N\|mƎoyp2^/ l") hv|;D<ֿ!%Y9={.͝W_cLc6w*&rW}-l#1#zW /MRTGOsT!6'tSL;HJ P_I[>&I=1 3~qOnYtᕷ*/#i/6zf'.)saitG1Zapr:J[V2{._N]ӌNafY-nCR -!mYHyϸ:(g_i䤾~`O.+̳y=97۳Vʟ/qU-OvrxHyR9>A,adla's DfJT'.|/V 8 @b)oԵ-R귛T*w/ -/9;^P[|m7]pQxn_KerZf@I_Xцf]9>pQs3Qv=8Ɉt:-IbΓ\]J?SVaD*{Ba>L >x_Oޯ/Wzu|^Jv22p7gT$<;!sssewJI&eHO't1[$"$6K<7S0/yk=|)L/>u>d~/H9b/=Qb#;0yWs'ceZ?bz =%R?I|BX))[8? >Fy) Y嵲v:Cl8;!- !¸!Zvnn#=g^"$'žx`ҧk4[PB){ ,U;"0s9$Y8Mz Nc6>1HjGSHU^6m M&)Wb ax2;:ՆIN\OX-\zҩ'Oj` -CRVT?גPUtR&,r6M2]i -A4$¯&jޖM]~bv/|0`b/) >cr^Hㅲ<4SJTȍdZrJGN -{3q'xdAj?D4`Tb,)Rޱ@iΚKt8+BDD)C9bf2 aI]kϡd.eJzҹX:Uڤx@kIV"ȉX } g !F)2xcN^C' n5[Itkb0!rŠX?đ:% ] -ER!]y%|!ԓ-YLOj zҁ`= IJ3>!|IӤ*+)AȰ;{cz~UxGtќ$') =ŋ'7Hy⒘Izڞ k-6 -(|%lɌd21>ȦZa=Xⶓr$!A`|LQ}6(D2=XD1"9_74!뭹| 5'ܪhÁtM.1:.Pq|yr"zsY3q;C;ٹnʏ7j,'vﬓ%ࣰL$XN>{e6wIٝ/W`tz!S=I&&i2%gBL:j8'K>n;i1p(іJفs}8eht j)^%Dq{0$w#et⨘"Q;⃆̗-#pH!"f"VCԓM*;kKLOX =d'-lS&ڒ)oS0zMas|x94qK ag')V?zanQѓ"i&"m$$`*YWnGRV!?۸Փ5IG8FpH%IJڋ9{`6X>FrrȐƑG9L;HX⇾+p*==ԗl6!!)ocw7$ex?5M._HSf;%cdVN1U --O` D×)oBctrEr0X~<!uŁTG*zKA2g@Tx4C&4}iX >^HS0yK<(%-[rG(KuVhK$bn@F3O<8Y0_nn$eXSJ&)VCIad?'})Fdz K8|`4 w9ֿ)0LεdV} II>箱|Ιl5DҎA ?s/,N˃['BDTR5R޶rsάZ%MsDڔ'2IONt -E@ !I iQVr; z -f )ÅI,y,S/]F xvyR,NI/ 6uLQRv3Jk߃vŔbcW^lӹeI',ccMRSOL+rߦDeO\vSJuaQvx&AbآOMJmMPW~ b{N%ct؟c:#SJ&|x˜ew&9'֧`3LT)N%]d?՜1ĶRlK:=RŊ(%'әbcՓ V?KI.5f-%ӖQQ]OdX =i;Ji?v8B1b&Jy< 8 6, IBRvJ~sn͚כQc[Idda8-o~#۹[Pp$F)w pg:b8F2Dy(S;;`d %ӓ n)绡'MPHE8B:-aҥ+_nY2Ҝ1)6f=w]M]C;ctIiJ_8qTKm\OR!NhŃ%W<  Y !)'snaMcadiľ(HmqP-Ƒ'LPiΒc&)C|Η!|vcDт}v ՗AfA0΅M KR"址xё*$x|z5E)k6o ;ٙIiy|NI,A_28xcIS`+$6ct )'DRv+wʗ}>iwl}1Q(8Q%e[mF|21:dCRb ._bz)S|qRRn@2RRzy)< eGd\$w ;&SI&D|.˟.Fg7 ER./: IV\]=LR SʕRO} b\p"3TavZxX:L7 1"-A<ir˟?jHK&͍ٖQe:Ob8n8X|G n3Q}(-)ezCbE+ =)ГvXv2N8C`;{ 4Q#$ec`׮VI?cp5׵TSI1@!$e[eH )4tKonʄћSTӇy{xحtyR T2XV.16hA|ԐvTEs$_׮;Dֻs:X ۣ5dh|N|I@"bZr\atѳÔ2WfђV:sS|:=lғaHP=dnM",J,I& Dy$ĻDȕZlw0=ٜ5ꪡg']\O -?L'EoݪҤR1/v@k%"JsMd Bz23ГV.cK"8F@\n@B䟝?fH+V/XBSjHɄ%og~*̛cG7$eH;/B+K$7Q:qGۺi@pcz4䱇2uSZ[F}[.O##~ -(с >~:w4ۭ4VHv!#ȕu~TB:WC0yY6ƍޯ- a),EQ}A\{u3X,cO3:qP&p.ECcDF[8׵fQUdWre3<!M׫빑 0­?(2s(rTvh"Pz31œ]t^lBg7yP:?V'ХڵrJ^94d2W}LB- 9ĄhšI9Qjя=iڱ?6A`4h4װ(P.W2d19,8fXy>J *,-P 6Fg$;z< =i3q -/v >Nȓ\FxGjTK+\cUE"&˖ӧbr˅b>l7$ _;D=i:/86 lM#ʖ5dk(ן -'X2 =tcHʘU)/t5Г,G3Q'/?#ȺuRJi*r -208 Zvcq;P{ܖhHNH|Q.P{$5ۡ'X|,wQd(U4|yU!RN grztGo!̾>FQQ6{Lmh/B$zLyԨʓ[dzkȣ<8ŌXR2 Wws"!HQa)xlR6zrI+0`-ovm*f Y6DVCASXrq"KmмBdķonZ!&!)chSc& fuMNtBKU%o4+#?$\`Xf;:S &)D%GmNgߝ  oHX,c#\yGPUxn9/MV 9$& N/,uUD,Otx_)4 _4®6B虐^O=2mo15~"bTEfu+RΚ$e䨪ɽDv$娜*%UN@%'%d~HUTٞý"26UIbҬ3.D5΅ё#7jQJHh-ݤX| !#|*rζ\C'WOf²1:plPrTx-9!{ARFaqk $a9/mYU֥' NNq%r;zw]crߐ#9ɢR>͉zmG5|\cjHϮ|UE'8#89X2aM,с7$e(sMt @' te$"WKe\Bgblq1%„eGNc?BRF-Uq*?V8ZȈ@U9Sn ڦؚ굲M<"*"Yr{.E1K)tHIʶ\ߑM LR҉$%1Ffg = ")UiTYΫ"m_L/q]TzNң}gAeԟȊdҤ9Yj )xd4 @4̓OR:rlD~㎕Ԭ#kG˅_tU?nt<JH@|9ߔCX]7=- Hd$2@iUK%pr42 -Nkaޜ wtӑG{*"JH ڨ=7]!~Q|E4ל2 NԒ){S] vkaX -w|@D)ྔ EXa/n_ww+ %7EsMOʽ"k\b|v\jH{k+m7]Ba)(b;1 rJhtH NH^~g"t@gtͺ"P`L,s(Z"/"2FrEʗ*{HBJ9r@1)I&r=-T:r%Yulir2rp"4]q c ,}Ƅe[&tEbj)WߘڱX$UtǘFUdWܚ%{7"H]CǗJui Z90%31:"J~71I)$PH6Al >6d~DsMwܖ#7ڵrٲēCzrj5K/,ѹcB2Uߖk©֖RP){ nVSL ”2g=کMp`W*2Ө,[FUS+1@aNˈIGtې5>CD5$$xp4@|77UGk֐*>?oY0wt3op*ct%%8)jϹ_J%⺺7 !#-#QU4㐉7q(8zM";z4љD[ph;@R%Q>tXFg7fW]۲"@:;zb=.M]O )H;Q SCҏS,$崄 z -]Œ,h3*4g*uʕM?|22zrr3nXAXF|yrct )3zKVRd@4$%PFtvOґ+5ilMR)D#c)&/Ӡ'h 2D1:}CR{OBy0M)SVݬbF'QadNԒ%{7H+TEZ'+WCL:!wtP/74vo$f蜄I񊐑=T٘]+/JgCCZ#&J>LOt*kpGGd̛H!HR ~D*1Ka;DXٕ'7TY "mk &Ē \%}d'I)TÞ?<VRHhGQ;#9B@@m"`9Pa6hcK!#mXb;G)B|(bup%JRDkR,:;+{ -c"P@mH:!'|dol44Q_pŹll$%fb|&LWBpCEB(BcNRe)(r4{R-??DoR -6XHb -7>psߐZ_[w7pot F.+$%3a)Z‡س?Ii&aϦz&JGN -RDtl9-ǗJui B W;zڞ'rbN(}ߦe[XT+>%)'>Ctv $zr5+1 ,(jpGOΝn!쀤,t)le^3VV7(V d2LWS -oɦb=2eGȹo!;=ֿf|D܂sڱ8$%G|['ћc Uk0]DCXf(MjKڑ&j -q$AE)icl.KR[93v:(09=N(a a5=A1:!)}|7]z`E)uMГD Ǟuwǘ)/,*TbCVB%"J)jxY<5t=)AM4ހ;'xKX&;zoHJX*,$ֹ&QUt &1Ise„ew<ݜ-CRžݤBxX\{u٧k9Dvy+%'a٨]=ct>ޖᏐ'y(|ƬJ Lc@zZ X;6ğ;zW>hHPzF"'EʻQwJ (*09&"8i^ 3΄esXct 5GIxo4/K]$CRFvhQmMA䱥u64-CjaYFbaܷ[*,蜾 '9ww&]uu*W7'zmU,b%ctIDep6Pk$AR[hY?)I2§{ L#,&J>TON(ѳ@8 IY,QHJRnw7d'2%$%QEטyWjIHM,l^Cz3%hN63sBt!ʦtڹD]]ؚz01ɔ9 sGRpG\L sߐZʮM3I9YR 5 ]9Ĉ5͉^ 1 -#[=JI痔Es˧+m<ٍ7ǛTT,[O9OLaIK*ZEB(os4%evoaD٦Emf -BO -N/k<8V;H;-box;cM9\R,{Cs*)rȈgbEL'b' @PKþ~"Kj)rD>U8JNi|ϭt)095ټôL 8AXW4pVBC竍Z?a9XROWr TyPpr+D z KX\GSQk&)^ҁҘ+9@LyԮ~`~~{G,;ƴ -RZ:@ʗ2i3'Rܢl'1.My(MTRQD6,,{ - J&>(ٝ<7 LΗ|"&m@7cᱯ;:s.k)~@Jg:s)iϲO7Ę5(q*WPq;zG5o[ĉvZk6>sJ*dǗIuiN6fKx3X-̄nhct )$>/Z%L#J=% AX@} -djx0yM,^C -Z51zlxMYjO2I0 nP5[/nsT$qXEzLXR}(f&@w,Y1:2Ιvx2̚Fj8ft尽֛j @"S&tE'$,O bNPF)F`b Kl%QEB !J,ibdz'"NٱsGSsq/)ꑬzy $o7)lvSS4[*y e9Ah>\49I7>`T<8u-9SE6=0"-G\@yp|D(sL~.g.]{~ig$ϓ$CSR_OWZՇ.K♜GRk7>`\M,~sKjEҖ}&A~^9* H_r,M`}Ҧ\$]7/y$'%f%͘4+INNv%%%''=Bқ-}J~wKi7'}i%Iuz3<4̙$# 'q CLUY9kR`D =H%Sp2 f*Dږtˑ5ctH*m&)}TUF&#WuJ^)6oHsA%s$9=$ ]$=&' IWvPq6WVًRų<LZ$g=0f-ct!)n>sA{d|4#?uE M>?eJ:팤{=$q&ӣPRh"r:6Jݜ1I #J@}\,{stE6 D*"ЅBX&# Fhd#S9t{]uys2i3$x3gq -[@*m,!!)k2)ɮty`=V^9z==$qׯ-s5DSS3Uo4k)@ЯL$e[pR׼ԤY3I1HdV@"9CPʑE]XUũ9{Se] pb2pGL19'!) B O-k\7Od&iIO!]"ldh( ȱ5*T:Oygna9O`*Wa!ч @ pGRle`IJQ#%&KR9#)ifpr1tU)%uAc! tzmSpr$@M,CQ\ -:XYy<(aTΚYUTC>]aamoL0l=WΗC,R:]uɹNb" XjkmT8 -x;З׌<^g -3-%'+bI Ocz7/z s" 8 -eCeܙOCBRX7'(47+AARR4)D QN\+"&˖>ωlݣv]|J|SC2b2PUx TFt:mg큱96&UEp)4BdC^LԐ.yӒ>Ywf&g']$OOp%PevGLQFoZ1{u+eTN)Co1ŘcrCZRÆ{¾[Lff]XWf˭UO>;.N3P$tlF$%Rؒtl}{sj֡`0*#"_4%s8÷no\4* x=]X_xY}Gn}⮚g~~mՎ]*?Yew?7>sf9]_U&%ERLӞvE/^:\!rDј[?a=ӍO2H&= .+]Tϫ꿯 jqŁ';?|^;ށOSߛ;g3'Bqƙ7o^_R&Tm54G&t тg,eiC4jR7<12Ÿ&٥ϯz1}8tez7roEү`8?㓯3=yG8HwJ3f`cJ*WRR|ECZ E?E)[\PWr%SV?jWK*wod?B/{?yS72XA>I(o<90*_Г#LΣ"&Ry?S/\\m38B7* 3)udC1F*eϳҋ5}j[⻯EO䦙n2E&&?@r 8P  D4B^] - 9{JOkQtp[{otS9QKѥ%Wmnc|a1}~I?.R(h|ӈ1qM T⨢qzr*zRmQdg>It`wt&KP9 @ %1-_ܒ7k.1/tm?}'cU[D4oDޠL4Qc,3A!''-&h'ڱvk06XkrDQ㊢?Z" mћsn}_+ן-?nR}Tx$| ]#|QՓwrct͗+c9MW\O LF:1Ec`7gN\&J|cw/;2I.xgЎw^);"Ȥch*bX# [rE]Lg* 7/r?i%&FpbQþ;/hHF3ȾxV<?/n"J?uOWToPR[ɾݝ+^EI1"Q8vE#X"DI|S -I ѵnrKIIt$A2}f.1~ϐ}?V?jkJybU-7Js:#Ì*'''&]Ib$& J}.;krSPE е굱.^6Hwwk~iH)i&)%}CT<@Yzy8?o>WK*1gRF뵜GOF|%R޾ )S5nT*tEpF䣇:_Fݦ}7͞[_ti]E_T?lxŁH1!Ҽ=^Z|+hӞ;=9m4ʢդ>ܚ2(e:/1n;Ⱦ{:=/z*y9ϛ;>o~}ɕ^<@+_ίܵz^|#|!n&ΗC"a-uFFhz2fzR@LzrB -uyZ]ZM//_!!8 ĈӢcJ'u֞EF/L.\L~r)?[+Tݤb}B/:F3;n 'CII̙VI!S9ҨHy &Qw׮mD!nehiӾ{ 1fCڇoOQ6wŕ{-凶3h -F}7׊12`r'Gד<8iԍw(ADڊUֿAG+qnҐ^=>bɾ٥U3^ʭܽr#\i=E'btГɤ$mtkd˧k)V_pa>|֛bKypm <z3"ơQVTzaߝ5duQ+Tqi*e|kk;/IQ}y(Q@OXV9.,v^+>p79gr 'XÌÊO/<%=빠>K>Ú'Ỹ4%beR/]R{^)fϗkݣ8x#%@Oɘ{Nx}$#J1 T"hrNՒDHކ+cwB݅mی_YʗWzurh>nz #;_.@O(.Zў= y$%'Ip)b:FKz۴烈\@jXK(+؝5o˫vdSˁ'i[[~J&$ڥH#OIΗ"FEU'O{zROZs$΃LΗM[LWOi}ƾ1nEsL^./*C1>gڮWO#Y4MӘ1qԎiU@OaL+9s+9)>(p~Iɚ21aV@#zJG~[Ӫ^̭hzWihӾh)f ldX2U&&aC9MW\.{rcLOa5["w‹#$_)ؽbxh;|ڐ}wn@O -=I[gi)R8[X9w:qp 3)ΚC 6./O)F`̮o{yJE0c"eGFaD#W'[K[wGZ)>r t9,s -Np6fǴ᫓L gOd41 ܌uȏbƚ':̗ݛ+Њd߽@Q2}T"ӍΛ?"t#D#H4' IxWNWK+p"V^9441i/BE3D#9xg(+[tQݖjq?ɾ{cc|{XRF|žc_0Ip=$'IlgK%(&hrOx50ׂ:_w0WafzsΣΗҫ6PiT[jq?Vx6(dC+ /ef Id=ެ%mŤX\Rzr_~ -Zrp4d_mY84%Ⱦ1?NuWm?}W4e&+woaruK!ďߡ"aݦMtYO*\O -fk=]Q|fdΤ>}s)(b |O+%{7|g~}Eu>zgyWF釩Kŷ_,{û@Q1q}7G^h $A5hPϮdtnr|4]qTfJimDڟ)ica\wƜeǸڇoyU/fS$TAn}hӥEq/! 'x}_o;Nǯch~ؕѓ⭩MvS 9D Ilyy5${I -N -2S9(!@FNMe9+{/?տ޵탽{˴e$Ax_ftܱXʙݐACHt'2S:rR ;/Yϫkzj~NmWT{ٽ=;{>:=hO_{oʾ{ %Dѓq4Ez3x%$Q+pW!OdRF{zz -:CH_sT%[dzwogb_W>O=>ܷ/~. .{=Qՠy ->β9K>]>]񺵆tͷn٨tv55mww]L[ncb?:^`û?{}q{=ɐ,8~ %vwc9on{nƔ$:%6rH=(^ -tas۶mzJ*xo^O$/?9"mvfƙ,L|xj$w,t0y(`<Sl*=i7mR5Q[6+|p]t|o5|=*L^nLv]K=LJkKדX%% 1 H4-WDGgYuY>tCA% JI9s`CA%I pi&#'/1~ZzEwbWm| }iûۻ?}KCaf%#-Qu @"zm8A]2޼T 5bvS_Í3J|i|_.VPΠvCGk/nP*|P8Ӗo*BG+U$')_rGM_OC:FWH`/6jsfsa_>| ?_5 {^`J|^h@XRd?ط/$ՓB:2t͚ۖ%;WqM]QA2 -#@'EbFFt pAVnjAzxꞭb0Qy@OO"m}駟~}/*s5_rǍW7XM>A|˟^d ~E:{k/=c5ײԳ{ks&8,/owk,{gN6w } g+] _EU3Qkmg4]i}Cθ_5 =sGv=oW{ {ÃlRݢ8ό KOU8g<(>>'[w[Հ @8 PB -%@Bc܀GI@dl5wclq{l|3wSI73=!}m#:eN}129!1wY\tW]=>k/Lzfx{KGl!\GL 7sǥkbny~D˨OoDr7D-V+_~gRD 0-$nXsH= ,zY,7.6)'^3'ŬƊynn, XX[5IY.+G:՛usG?᧿O?r޿Lsy\W!_̇\VUz9顱&%DBVaUOQ𧼇Uxѕ% -cT FJbSi }E3s F+?(\4T2i!nxKXXm$ E|rMƭxt<0μvʼnOg̷x*Z0}1#5v+1g2hNfeх;<5s%;zQ'qkJ.i՞ -#Uրtuy9{gQΛrLo ]Q틼id4_`^{tB¸ λ庫ͯS{Rud?Y4].2ﱞZǚcxN&S1K2g&W0>9,)Fv^?=ܿrVΛY7p}ˀnqwyḎo%tyok~uO=O:&s0.xНtֱMJ9oP'i`pPaƚ[;XQxLx yʋ$kzYFjNK򘿗.La\>>ǒ?ǦnA⹛0D +,i9n?&X^Ͷl1q~fu$Z{0EcOw]%?uN 6]!//^QHKsSk]`.a3%fn:?C QwC-6,7#pU: Q^ŜܬH} ʊ8+gSObJ!]]Iޤ<  ABJzk8vL7pF˛wv/Ʈ^ȇC̴Df b{Ug`ʒ{|l[*'G|=C{@XZ`7m^N27r[J _{>0s~@-DZeq2fsֳ)9qZ9oEsIDu QŠT϶`Ğ=P߲?D}Ƈ[F̌G(Y6 -V')'sj'ɶx7툅ҕϋrMޤ) 'fߌHL^ov^]#•yEtnƲ}3V/n1O-4^n {f+I$hMVzrqf& dʼnl+ݾܖHoo<9us>j QG%*+cÒܕ~LqO8 +4u}oOٞJOb5fC$ēݏ >aމ"hC!wV,q?y6Z%!,[/+c:Z+|n*S* w%Hp7,7D|mHycճB}Ia3śXG;hH@풧r_*(߲e"v*&>=AKŞO=nj/1Y?j4 -azy SXa_{ɉ܍ ww=3'K@2EuEiAb)*k Hok^-.n.u,l/'ֱ-RqVs]9XQx+MЇ sȸޡLJ粃P^.U›w2jH -QeUGO[ +޾4'Y*2JgNA림w2\(^]}Ըm>9,,@uXM OfGfOod%8 -+--%%K.?3YX]^M rҍN˝ &Nn&B;/LG&| ?J}"E_M=sO_7hF̠=๙U҃>^D}ˠggeCԱZi},na} g_校ۼ^nWAX /wOjմkDCVoQ 5@sΖ8+s#yad}ț 2-~䧕*W͟(g;Nz#^RT d=[|Sq!X/VfUnҗU(~YKx'#[&H~̺7#sR|5$Hd uByXg,\?j2&wtI RMqWQBk`[jv/F|z;x{+!5E9/NYܱ %Oz5pm,_p/U*Y&YUJJ?a> lTr$ybV]*{~_xU99y'77z97f ,E'.7,9D}Ni6ݬ[ď>ډ['$TLUz14Mc&ri\Dt1zVi`:jt,gl謺tOY8gEt7\tҸ<Ւ r*lOࠓ"kb$܉۽RvLNvIFM]җ|<|L/+'kZ]'_Նgq]F|Km2z$kfGD.egQBY͍ qLDm}vsLJ5[Cniy6YIȝx8c'B*'GTT K#6|˕f0O{✏n,w t Zpvit]6 $"JmOIZsEԑEa$#Dq~Mo^orIFeZ/WI+#|Lv5V!:&U]'ݰ M7MY+Q^lR ʝ^䳻Jyk"dn()ĿLsQ v+r33K}~\ H_Ƭu^u wz$'ugƃǎg&.ǙMX%04/cq&zpAlZ{ߖY9Fz&8&ś{ }orPǛwrN`@zv*kC2Wf.?%m>nLaM7X̙ddQ޻ΤZxalMIM+kFq)Q _x|G7yY452(0kQ_&ŃC? {\?͗.ZdZ8{ -o,g}Z>"ҟv #+Jo|uk#4ڝc^X1Kŀ(B!̽tLt IkżQ8[EDy!eN-f3rd^{Rb mU/3xs y{jvՀH,X)[t!Aig/p#+į'ę?{^W{ -#1 }Už̲ _cq? m+2}/I 7&}d!}y(Oeb7~kJ -#XZJ_ZYȀeXQ5 t&={19]ܖFwrN&=p=p#Lw2Lsˑz.ʫ_|A_1XYV&?kՂYe܏}iU{wwĎTQ )If#@#oY?E4@cNVR_ƀXuUޫ-|K%./fk|>kt-akÒM3o^^D>nKM:0%i]y{7nkJ?1qNmbe%inpՀt<*V%keynqY~˘X8-]s\Q8{ka_*wK[K/qw璼ikf+qpp˄EI$V-}BL/~ /ɗkΫr볬 ]g!5]49+`lן q ځ,a_3/NJE\1/gXViN3W_[ Bo,7@R{@ziwj HgN=!b+VC("w]xy03s_Y%yl3GXNZ/8' p 2]9?y}EEEMqW\kOefd -(}/L.^_7@[QY+-e2n)j )u+!-y<32޺![6Ӫ,a>g>//xEnKSQi20/1d<p2P -pH=e".e{ )vn,9ŀM^WvR.f_ծod88]ynj,?ԗYV7pFQrrZrQbU k5S@ACfBLb[ɞGJkQZ#8>u_J >ܨ@f.m16] aw -?aQP2=`ܸ঵+ -NaSǩ_OGb<݋r¡tᢞ~qϮtf[t(03%`SVxQ ߯ Mm -n_(0=GjyOfeQ}ZƮ5[U'6d,1[8߽~SZGbߏ{rf@pz/7y U6@USM=, fW 1{^)?j1#On޸ܜM\Ra ]KlV ~!_\6=s8]H;_/jC9L=0cPIc6bVw.Nm:r٤Y+ubӯyȀ9\VG/kM*}3ImYUEvd͝v]qCx탯ʹjM|(Ο&SԶ5\ubBo߯NC{a֣F6yVAXW_&Iwp -a=`.o:{k-;S[LփE?N ؘqMs*g1rHVvD0P -Iد%Oŝjn1EWƮۍhex.Tql/#SدWM1~*[>ycrS"hm2w7Bc-vwpՉM -ɗ9{{jn1,w-,u,'!$3>vS}-ši<$uW6,VB_c7n1Z7_kĦoN5=ӬͩlTZaB~S*#MRnha?οuk5o}$EdljA#/[7 [ޯ7EԁM[ZGOBØnĭװh0ɹk5#P{մo/ -~"DC}M=>:ȓm-}goM2L~~׿WhVYCJ!vq[+݈4{7<$?rf^:5c=o,!.{kĢ34+Wgwq)n5$?WW תP>n9[YXtqoXV0‘~cn[!2bFjeL*rpoy 0l_{#C*Hfo+#7Qؕ4CH]^U\Lf7C+2IP~{N7 k|ߗ~΃|&6ݠSCFPn?W t0cU1gs H_^H=db)5 -`.NZ:!Ups60YMt׷&5U٧򙜯+ɰBmZ.j{6^hC!B^o3w[4dc P؃r.0q:;C:vcvqh.{u&M^}h đ.q<1g(KxHcpdGT3>Fe}2 Eп0G=Ƽ2{XMgŢ {¥l`cv+֋DпXt(- ^J}^BO N0kA,-6D )8𼇲P6Ʀޠ!]_U]nn [=0mZW8l]ƽ/0:Z+?AZuث? <4CP:T#< c{e:_U(ׇcXH88w?# -GۯA{H|%uD <DϾ?C+56}TkxūeY ]T>HK~rWkbApѳOv{mX7MҘIUcٿ}d"ݵhf'{?٪rQItC4v2b[- !mycNGLb(r/7$k 'R^іaONS~wY7*>_&Jߘ&lw;hfTJ{&1On"Hq2n7~:v?E=ĘBu(T[CI/^^xj'¢)!!^TnD5x:JuܯUWvZbHL=jĦSp잣耸4H]yHWMJ&Ʊ4b&3oS[wǦk:({C=$nr$ݏkovggXZd=||mM%7ZwhKۨh{>ু/jԃj_j |(!8/1?d h3iO=ۥ4%7 o7 [8zaCCDI-ހv'7Bo@3nKLF:/f A}/K 7 B0 r.:s6 6)3߳z1ն6= .G٤)KQh h{D$p) W[[{. n$ǢtLpT!L[ϳZrJ]ý}(Sf;S@_Aƹy( -rGqMYxnkVm\l1Y=JթJ?}߳Ak d++on3L]o[سh*irg.  D33VOI*;C&B$J{vE^Ξchao~Bv Ozyt9< ٽƁ?_OMl2W܅Yb2F"wۨcí+a@b}?ks6gjQo :,zӛ.a`D%m78r>,zaҦr#㻑цEo;뭣Ɓ¢o̔o^[ -J!4|d\ǿDnD LnGJn~%ǁ@Ta3JX|)`\rZSI7@nĭt>J1tF4i.}!7y/%" ĢF,Px%RɭTr#6@q@7,F*G(#܈[|JCJ*7"&{>b/Lf:VnS9p*8t~?FsT ph^nub-q$#P-i.%o6=|Ï?;vq@ezs-j &@e*3*?W SI_;:>I?NaဢX4Mo a5Ŀb"a/1v9ŦY Zh (Cw/e5_nx%P~{tz3%>X箄րX6܃M~\Klh Ebƕ9^-mR`g"? 1)caJԀF'۩wTo˂Ħ"Pvs/_Xawma՛Aztj, $MpB nȶ 59?3 }#hvUobi89Lo8\qw+W }azqI>CߎAo\qe:i+AT@[@7$~Fq0I|xM Ǧ{K7wϫ:b#(q_HaSYoxzK(@3j#1cc8 -h{S#-:W>ǡoJhi N>tzsC'1?ytVo=Q{<<@bSYohM.W&"~Ƴ J66=pz+.iq{dW{I>HaOxFW7P(E恞*P*(%Ej4fxBەőn4~8H!126\JwZrqڀzb*MMn4ǦX&ܶu*^J æz(M2uqx(MJM}%@Ml/Tn"};EC:Il޸=(b]0oRy;0 E׍RBn<(91(MR[{7S%01bHyj-c%-y^urb.kW XtyˍY m:6#ܘھ7$Pf/* " 4n)@},ˍKw"RTǦ^+D2({ܛ7&!84M:`ވܕ@p@a, eZL mAp@YlzMJz/Ԅm^7ټXZrt9 -m@p@IZyj֍twp$P y#\p(E7&yџ*j8P ?)0~LbBp@+ 2!@Z6DaoR`3!8!JXIqg iqSԷo~cOG}>Z n.@nl:zcA+,ƢQ;V$@f,\EP/}@&FEZ&2P_p9q7?Jo$}sz鍽킅Rbӕ4I1ɥ.꧛ވ9hv{mo I%}M;&NkV ~nH?ita.M' 8 Ro|O;ȅC] 8 6XxaOF7~w@,fv Lp ! -/]E  JZ;I ȃMؼsz{ HC{cb2~wz$T_H1 3Q^$¦i7 HECR!7 6-3t՛I84[jCE2{_Uoy d]fSwü`ꍽN`zʍeNp}8¦p=1I 6>HOfa_#: ¦7y 6-誥 ·3dX-rA7 6y7 6n Mrfx-62vl7 \N 5V!MA HM7'i3H p'tt@ fހtXtfW$7 }ӣLr0o@>*; $W1i#8\s zdSA-ʻ۵Go&9K$eǘ81q7 -6=rn& =ia endstream endobj 31 0 obj <>stream -:yǑ bm:wlGyqhq.]ڷBo2jڥ& wHCӵ[ؘ "&0rΗIDL"-c 0H ۣ}{wڱCG&/@b~}wԡ)8I 1LoԿw.c+!w!d= اgfbTכAfc  ԯgNb)7 56zAwazS$ ȍMf<%оcM2xZG  \G^iѴ\3q'tCYm ¼ٱ釣F Ч3p1 - 2I!$ش#&ewpO-cӣōE(S#c֑t)1(M_:Mg8HH6瞒8t@޽DD yȥG mLo{S{2Ґjz)#Л%&&I # Eo?u!Lo;+7܀ % v?84clRܐ@R< )NC?<7ݣkXLԖIi9Էww=wH'}6zSF r+(6pI )Lo'5&~`7QA)=v+IyvRFߗ7^%Lr0o@VN|to짆wM_Orh ?ӛ !@ ѿO̾:$c2ճs8I6 hE?WN cF8\opCSz(L{.q@,?Л7/a߳8TB1wr.7'zVo܅=zt$ԛAb8t|nBԖpP"-:0ɿpH?vۮ) o o@#l:Ror'o@'l몮۷pf#cN;ws!y:a;0gޘ}{7]Po& --AV]th/\bO:.b79}p'^8b< w{+ I KLrN"-Z?[TsCH7$whM_^Ɠ0o@32&UtæΏPor:qvtP 'O IJƃqL2d ~Oo܁-1+4plSF7CeLU Ja$SlAE ӐMo184h {No̻}c0]+܈!Lo7H7<Zc OV]tĦvh . b7%6Џȵ3I0BbVo&qeIzBo :qlp@ -C1x ZaІ3ygmXJi[Nٚ]eA; fmh+1w2 D56m.ReX6 wD7][x 辋 +C~N6mu0o[[p2vI8M L}%qhvN4ep'p]'8ij3HNajx -MLo`SgvEHE+ok!yr%N:6{-1[Ap&ycM_*i0oĢZN tC+:z,Z7iC2:pl?܂{9 -Iq:s7#o -Ya{^݂I#̂%Fj.æUp?<sW8K)c砞 zΟOpGDLib&& ;8Ǣ-IE`5ceLr`g2 tNX8{E8tnX8$8LpSq\JN/ }ax)e疗R%YRzHb@#`:2 "4ҔVvy'`h =~ogah=qG!^dh=pYp&9k8Ew\ҢA7EiL㈘H,:3H Xlv(i1XA=Xl%Y7_Ji_xyzl<$38OJ) "Rp4Zw7Wprax4A:-i4yH$,ǤY& q_b@Gfv1#B @`)9& p(hL2kc8t_ @qh|sSM< MƱ(-II1(Z0}QI-:}J7q_߽I&.܆ f&9& ̈́)qMT84JJ?}&d7 ͅ;ye-@2͆'ynT#J< %@ʏ~סQo -Wp^cL`hLp6qem_4hLu)JZT}!`'WAh9LF{o 7!cw8n-JxB {>!7&{'=_ 'Jo?$Ep#?$g킁 \0wC10pJ$ vDL6=p*L+8ƒE8!:4o7M\lApNk8tσC @顳:C~ao6xݧ7X)ܹK$YK3{9s9νz7 2O!@|JOpp;Q tEH98'ɷz@DrpOP{ 8'l P37Y{C |'_ҽs9&PVS㭹QJŗh@@x N0Ǜ$ߤ#@x e4 %'Wȧ.ڽ+-wr08P""wzxdXW!ހ -}MbFF|7vn%P<ܨ]98sp0߆Ș܍1y2ǧ$ಾES\_7ްzw7$#龻7@;c7 =w@_p3N dpGS}N$t+P{ Ԇ~Ñ/Tpaf4 g;wQ=yYP.X!c`@y鳨=tdgn]9ҙ~ΘAAn#9vp޼37S](JR _~>#P,tg%'i{K="8/SNͥdU Fp_{%̏yȏ lⴓC.(YPQ,|9w`b@m16t=9J|ppΐ_"I93i)?2Iڷ/b9@ëXto ciHGpq d+:.5 l PwU|y;r"Vsz1@gK57YGpv=ɡ%pnl4L|q$J|L'Jг78${g&z '!~x7N~f=]bsLO4(үΔ›Q)\ddo0?rf - ct,+@pf$yʀ/_9bGf|X -_l}8rr'_j-AUoif~tn=R],%_q"p)v՛nYN+y`#3L)CMئB&I;#”  r) 3 ZGސ44 GHANOprFYɠtp%m>hʄ”~'0DoN927KCMR `bt_V\D鷱=d+lҲ{6,Øu9/|/Ks<o1btft2 ȣɜPv}Ҳ{n]JIs奙 #T S})f{c#g_ B6LD鑙ES'@Q)[p]j-S_6C #C^65d89\*s յqnnUr@ɬ;[- :c0ӚcuDpΠn w=sp_`+ du{ 01rlB7VڟBC:dX -?gOBP涋mL=C) -~КVQGKf ytETmTdE8Y?^MՓ$8qJ\02&VQG_<26OGݴoX &9&Lz<9p(dl^K/r3"]0^2܋|% ?~ЛN-"ŖΟc#Ǣ#KS~69>QtG` oca+C o޾5olah/pnix %*O A^+cenGziYJS/ܨ _;8w}=M$>+gWTQ,7ҀHn d]޷ye< Rnp7S -G&ǀx47L:0{[ixgyrlKs4?yx9r4.|)6;08y it ei7 ba(s;SOms{sCE{]-.irms8ITx#L>7/ lHR졥±[y77`p+y3뎟릜%]!⽽^kahfտ>FYNp`koqɽ}'p(O\xMM2H;8fף/<XBB>L~ (fǬ=(x dl /"+D- s(Jf_3IbLl[i;8d馽P CdZ<}l7q}DX1R[i=pZeE\>5g&H0ZNB*AfcJ /#A S -WE>!pr@͍˿r׮1H^ҋ/P6dd2T\ 6#ܥ*#FƏI]ç/঍?/0iml3R -Mڐr#rM7AԱc}m߸᧫V2(&C@S -_Zv׿Xnt8<]>xG~~݆9_u!ԍd~Z@.]Zaz1M †8/xs3ݵ 1qK]17|s|@X -G#_o/-m5YQgM ңKf}]\{Ւ>X" Lƈ0y0psDB9z|(1/ bJS_JCqZ3V6 /+ՆgF/-)6*t5fMsRs"Kc(N4S_>U$P&)+mE GceQk\~']6Tgmm6Լ^Wc5g*cbE Ćx:)>n%ӶZפmЬ7g袲YzGsW`.#-ɘ7+Fkmj}|֛#W?k¦FCAQ]rCF˺ڼx}A`lN6}[Mb[fLQe5E&;Őܬ.]ї9یMٵm%re|v`-Ԣj-2R樾 -C2l27kʲB]_zuEkFV\L6ڠQw>j0#ݽ˴5EZr%}n\bj{RuD93rx G^r0&ϤꯏfΡ@ݗѮh rfENMݘST4 -mIT:VQ -}Vowö^5ݍ{cs"fjZL}wQc02c;ӝ>Rl[)qW6z]P"~`s+[EbZBDQnSMC(ΑXߝ:؝[d2csbm֖ ߥ,mj0QoӒX3@ggT*+ kkʛu9jg8^ng2R' )UUҟѕnHoo.p4$GMΠFh2s\^m1e^#<tNZ`TimYPG>t ~TP:-Jԓ+\e6ZU*SSuԷv)6Z,EI}v^ұ/МߝC40w#d*]Ѣn+ ڢWeO16UeFVk]Uݥ+Փf!r؃&ܙs͚5GPF[SRBY.ĤkY-Ime)U&92ͨn.6W-3[Zcu ZmkIF[\e'yP'+RB兆h&+ٸ^Zҥ5e)v}JSTȭv9QNhlNXƩ]ָ3Ra6%SJ][i[M9efBJwŕJ -"'9h,dRLQZJӔjcIAǪ|٘֘ڣcIi)-2IUV:*q[< = -p*EwmlعU{O쫀mش*0L1YU_{̻˳/T Vɭ -xTs4> -LL*\q3Q5 J,(I endstream endobj 15 0 obj [/ICCBased 19 0 R] endobj 6 0 obj [5 0 R] endobj 32 0 obj <> endobj xref 0 33 0000000000 65535 f -0000000016 00000 n -0000000144 00000 n -0000047649 00000 n -0000000000 00000 f -0000163121 00000 n -0000593503 00000 n -0000047700 00000 n -0000048109 00000 n -0000048283 00000 n -0000163420 00000 n -0000139682 00000 n -0000163307 00000 n -0000049181 00000 n -0000048344 00000 n -0000593468 00000 n -0000048620 00000 n -0000048668 00000 n -0000139717 00000 n -0000160473 00000 n -0000163191 00000 n -0000163222 00000 n -0000163494 00000 n -0000163800 00000 n -0000165099 00000 n -0000187851 00000 n -0000253439 00000 n -0000319027 00000 n -0000384615 00000 n -0000450203 00000 n -0000515791 00000 n -0000581379 00000 n -0000593526 00000 n -trailer <]>> startxref 593722 %%EOF \ No newline at end of file diff --git a/ui/responsive/design/chromeStorePics/promo1400560.png b/ui/responsive/design/chromeStorePics/promo1400560.png deleted file mode 100644 index d3637ecc8..000000000 Binary files a/ui/responsive/design/chromeStorePics/promo1400560.png and /dev/null differ diff --git a/ui/responsive/design/chromeStorePics/promo440280.png b/ui/responsive/design/chromeStorePics/promo440280.png deleted file mode 100644 index c1f92b1c0..000000000 Binary files a/ui/responsive/design/chromeStorePics/promo440280.png and /dev/null differ diff --git a/ui/responsive/design/chromeStorePics/promo920680.png b/ui/responsive/design/chromeStorePics/promo920680.png deleted file mode 100644 index 726bd810a..000000000 Binary files a/ui/responsive/design/chromeStorePics/promo920680.png and /dev/null differ diff --git a/ui/responsive/design/chromeStorePics/screen_dao_accounts.png b/ui/responsive/design/chromeStorePics/screen_dao_accounts.png deleted file mode 100644 index 1a2e8052c..000000000 Binary files a/ui/responsive/design/chromeStorePics/screen_dao_accounts.png and /dev/null differ diff --git a/ui/responsive/design/chromeStorePics/screen_dao_locked.png b/ui/responsive/design/chromeStorePics/screen_dao_locked.png deleted file mode 100644 index 6592c17e4..000000000 Binary files a/ui/responsive/design/chromeStorePics/screen_dao_locked.png and /dev/null differ diff --git a/ui/responsive/design/chromeStorePics/screen_dao_notification.png b/ui/responsive/design/chromeStorePics/screen_dao_notification.png deleted file mode 100644 index baeb2ec39..000000000 Binary files a/ui/responsive/design/chromeStorePics/screen_dao_notification.png and /dev/null differ diff --git a/ui/responsive/design/chromeStorePics/screen_wei_account.png b/ui/responsive/design/chromeStorePics/screen_wei_account.png deleted file mode 100644 index 23301e4bf..000000000 Binary files a/ui/responsive/design/chromeStorePics/screen_wei_account.png and /dev/null differ diff --git a/ui/responsive/design/chromeStorePics/screen_wei_notification.png b/ui/responsive/design/chromeStorePics/screen_wei_notification.png deleted file mode 100644 index 7a763e5df..000000000 Binary files a/ui/responsive/design/chromeStorePics/screen_wei_notification.png and /dev/null differ diff --git a/ui/responsive/design/metamask-logo-eyes.png b/ui/responsive/design/metamask-logo-eyes.png deleted file mode 100644 index c29331b28..000000000 Binary files a/ui/responsive/design/metamask-logo-eyes.png and /dev/null differ diff --git a/ui/responsive/design/wireframes/1st_time_use.png b/ui/responsive/design/wireframes/1st_time_use.png deleted file mode 100644 index c18ced5e2..000000000 Binary files a/ui/responsive/design/wireframes/1st_time_use.png and /dev/null differ diff --git a/ui/responsive/design/wireframes/metamask_wfs_jan_13.pdf b/ui/responsive/design/wireframes/metamask_wfs_jan_13.pdf deleted file mode 100644 index c77c9274a..000000000 Binary files a/ui/responsive/design/wireframes/metamask_wfs_jan_13.pdf and /dev/null differ diff --git a/ui/responsive/design/wireframes/metamask_wfs_jan_13.png b/ui/responsive/design/wireframes/metamask_wfs_jan_13.png deleted file mode 100644 index d71d7bdb4..000000000 Binary files a/ui/responsive/design/wireframes/metamask_wfs_jan_13.png and /dev/null differ diff --git a/ui/responsive/design/wireframes/metamask_wfs_jan_18.pdf b/ui/responsive/design/wireframes/metamask_wfs_jan_18.pdf deleted file mode 100644 index 592ba8532..000000000 Binary files a/ui/responsive/design/wireframes/metamask_wfs_jan_18.pdf and /dev/null differ diff --git a/ui/responsive/example.js b/ui/responsive/example.js deleted file mode 100644 index 4627c0e9c..000000000 --- a/ui/responsive/example.js +++ /dev/null @@ -1,123 +0,0 @@ -const injectCss = require('inject-css') -const MetaMaskUi = require('./index.js') -const MetaMaskUiCss = require('./css.js') -const EventEmitter = require('events').EventEmitter - -// account management - -var identities = { - '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111': { - name: 'Walrus', - img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', - address: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', - balance: 220, - txCount: 4, - }, - '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222': { - name: 'Tardus', - img: 'QmQYaRdrf2EhRhJWaHnts8Meu1mZiXrNib5W1P6cYmXWRL', - address: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222', - balance: 10.005, - txCount: 16, - }, - '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333': { - name: 'Gambler', - img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', - address: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333', - balance: 0.000001, - txCount: 1, - }, -} - -var unapprovedTxs = {} -addUnconfTx({ - from: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222', - to: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', - value: '0x123', -}) -addUnconfTx({ - from: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', - to: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333', - value: '0x0000', - data: '0x000462427bcc9133bb46e88bcbe39cd7ef0e7000', -}) - -function addUnconfTx (txParams) { - var time = (new Date()).getTime() - var id = createRandomId() - unapprovedTxs[id] = { - id: id, - txParams: txParams, - time: time, - } -} - -var isUnlocked = false -var selectedAccount = null - -function getState () { - return { - isUnlocked: isUnlocked, - identities: isUnlocked ? identities : {}, - unapprovedTxs: isUnlocked ? unapprovedTxs : {}, - selectedAccount: selectedAccount, - } -} - -var accountManager = new EventEmitter() - -accountManager.getState = function (cb) { - cb(null, getState()) -} - -accountManager.setLocked = function () { - isUnlocked = false - this._didUpdate() -} - -accountManager.submitPassword = function (password, cb) { - if (password === 'test') { - isUnlocked = true - cb(null, getState()) - this._didUpdate() - } else { - cb(new Error('Bad password -- try "test"')) - } -} - -accountManager.setSelectedAccount = function (address, cb) { - selectedAccount = address - cb(null, getState()) - this._didUpdate() -} - -accountManager.signTransaction = function (txParams, cb) { - alert('signing tx....') -} - -accountManager._didUpdate = function () { - this.emit('update', getState()) -} - -// start app - -var container = document.getElementById('app-content') - -var css = MetaMaskUiCss() -injectCss(css) - -MetaMaskUi({ - container: container, - accountManager: accountManager, -}) - -// util - -function createRandomId () { - // 13 time digits - var datePart = new Date().getTime() * Math.pow(10, 3) - // 3 random digits - var extraPart = Math.floor(Math.random() * Math.pow(10, 3)) - // 16 digits - return datePart + extraPart -} diff --git a/ui/responsive/index.html b/ui/responsive/index.html deleted file mode 100644 index 9dfaefbb3..000000000 --- a/ui/responsive/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - MetaMask - - - - -

- - - - -
- -
- - - diff --git a/ui/responsive/index.js b/ui/responsive/index.js deleted file mode 100644 index a729138d3..000000000 --- a/ui/responsive/index.js +++ /dev/null @@ -1,58 +0,0 @@ -const render = require('react-dom').render -const h = require('react-hyperscript') -const Root = require('./app/root') -const actions = require('./app/actions') -const configureStore = require('./app/store') -const txHelper = require('./lib/tx-helper') -global.log = require('loglevel') - -module.exports = launchMetamaskUi - - -log.setLevel(global.METAMASK_DEBUG ? 'debug' : 'warn') - -function launchMetamaskUi (opts, cb) { - var accountManager = opts.accountManager - actions._setBackgroundConnection(accountManager) - // check if we are unlocked first - accountManager.getState(function (err, metamaskState) { - if (err) return cb(err) - const store = startApp(metamaskState, accountManager, opts) - cb(null, store) - }) -} - -function startApp (metamaskState, accountManager, opts) { - // parse opts - const store = configureStore({ - - // metamaskState represents the cross-tab state - metamask: metamaskState, - - // appState represents the current tab's popup state - appState: {}, - - // Which blockchain we are using: - networkVersion: opts.networkVersion, - }) - - // if unconfirmed txs, start on txConf page - const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.network) - if (unapprovedTxsAll.length > 0) { - store.dispatch(actions.showConfTxPage()) - } - - accountManager.on('update', function (metamaskState) { - store.dispatch(actions.updateMetamaskState(metamaskState)) - }) - - // start app - render( - h(Root, { - // inject initial state - store: store, - } - ), opts.container) - - return store -} diff --git a/ui/responsive/lib/account-link.js b/ui/responsive/lib/account-link.js deleted file mode 100644 index d061d0ad1..000000000 --- a/ui/responsive/lib/account-link.js +++ /dev/null @@ -1,26 +0,0 @@ -module.exports = function (address, network) { - const net = parseInt(network) - let link - switch (net) { - case 1: // main net - link = `http://etherscan.io/address/${address}` - break - case 2: // morden test net - link = `http://morden.etherscan.io/address/${address}` - break - case 3: // ropsten test net - link = `http://ropsten.etherscan.io/address/${address}` - break - case 4: // rinkeby test net - link = `http://rinkeby.etherscan.io/address/${address}` - break - case 42: // kovan test net - link = `http://kovan.etherscan.io/address/${address}` - break - default: - link = '' - break - } - - return link -} diff --git a/ui/responsive/lib/contract-namer.js b/ui/responsive/lib/contract-namer.js deleted file mode 100644 index f05e770cc..000000000 --- a/ui/responsive/lib/contract-namer.js +++ /dev/null @@ -1,33 +0,0 @@ -/* CONTRACT NAMER - * - * Takes an address, - * Returns a nicname if we have one stored, - * otherwise returns null. - */ - -const contractMap = require('eth-contract-metadata') -const ethUtil = require('ethereumjs-util') - -module.exports = function (addr, identities = {}) { - const checksummed = ethUtil.toChecksumAddress(addr) - if (contractMap[checksummed] && contractMap[checksummed].name) { - return contractMap[checksummed].name - } - - const address = addr.toLowerCase() - const ids = hashFromIdentities(identities) - return addrFromHash(address, ids) -} - -function hashFromIdentities (identities) { - const result = {} - for (const key in identities) { - result[key] = identities[key].name - } - return result -} - -function addrFromHash (addr, hash) { - const address = addr.toLowerCase() - return hash[address] || null -} diff --git a/ui/responsive/lib/etherscan-prefix-for-network.js b/ui/responsive/lib/etherscan-prefix-for-network.js deleted file mode 100644 index 2c1904f1c..000000000 --- a/ui/responsive/lib/etherscan-prefix-for-network.js +++ /dev/null @@ -1,21 +0,0 @@ -module.exports = function (network) { - const net = parseInt(network) - let prefix - switch (net) { - case 1: // main net - prefix = '' - break - case 3: // ropsten test net - prefix = 'ropsten.' - break - case 4: // rinkeby test net - prefix = 'rinkeby.' - break - case 42: // kovan test net - prefix = 'kovan.' - break - default: - prefix = '' - } - return prefix -} diff --git a/ui/responsive/lib/explorer-link.js b/ui/responsive/lib/explorer-link.js deleted file mode 100644 index 3b82ecd5f..000000000 --- a/ui/responsive/lib/explorer-link.js +++ /dev/null @@ -1,6 +0,0 @@ -const prefixForNetwork = require('./etherscan-prefix-for-network') - -module.exports = function (hash, network) { - const prefix = prefixForNetwork(network) - return `http://${prefix}etherscan.io/tx/${hash}` -} diff --git a/ui/responsive/lib/icon-factory.js b/ui/responsive/lib/icon-factory.js deleted file mode 100644 index 27a74de66..000000000 --- a/ui/responsive/lib/icon-factory.js +++ /dev/null @@ -1,65 +0,0 @@ -var iconFactory -const isValidAddress = require('ethereumjs-util').isValidAddress -const toChecksumAddress = require('ethereumjs-util').toChecksumAddress -const contractMap = require('eth-contract-metadata') - -module.exports = function (jazzicon) { - if (!iconFactory) { - iconFactory = new IconFactory(jazzicon) - } - return iconFactory -} - -function IconFactory (jazzicon) { - this.jazzicon = jazzicon - this.cache = {} -} - -IconFactory.prototype.iconForAddress = function (address, diameter) { - const addr = toChecksumAddress(address) - if (iconExistsFor(addr)) { - return imageElFor(addr) - } - - return this.generateIdenticonSvg(address, diameter) -} - -// returns svg dom element -IconFactory.prototype.generateIdenticonSvg = function (address, diameter) { - var cacheId = `${address}:${diameter}` - // check cache, lazily generate and populate cache - var identicon = this.cache[cacheId] || (this.cache[cacheId] = this.generateNewIdenticon(address, diameter)) - // create a clean copy so you can modify it - var cleanCopy = identicon.cloneNode(true) - return cleanCopy -} - -// creates a new identicon -IconFactory.prototype.generateNewIdenticon = function (address, diameter) { - var numericRepresentation = jsNumberForAddress(address) - var identicon = this.jazzicon(diameter, numericRepresentation) - return identicon -} - -// util - -function iconExistsFor (address) { - return contractMap[address] && isValidAddress(address) && contractMap[address].logo -} - -function imageElFor (address) { - const contract = contractMap[address] - const fileName = contract.logo - const path = `images/contract/${fileName}` - const img = document.createElement('img') - img.src = path - img.style.width = '75%' - return img -} - -function jsNumberForAddress (address) { - var addr = address.slice(2, 10) - var seed = parseInt(addr, 16) - return seed -} - diff --git a/ui/responsive/lib/lost-accounts-notice.js b/ui/responsive/lib/lost-accounts-notice.js deleted file mode 100644 index 948b13db6..000000000 --- a/ui/responsive/lib/lost-accounts-notice.js +++ /dev/null @@ -1,23 +0,0 @@ -const summary = require('../app/util').addressSummary - -module.exports = function (lostAccounts) { - return { - date: new Date().toDateString(), - title: 'Account Problem Caught', - body: `MetaMask has fixed a bug where some accounts were previously mis-generated. This was a rare issue, but you were affected! - -We have successfully imported the accounts that were mis-generated, but they will no longer be recovered with your normal seed phrase. - -We have marked the affected accounts as "Loose", and recommend you transfer ether and tokens away from those accounts, or export & back them up elsewhere. - -Your affected accounts are: -${lostAccounts.map(acct => ` - ${summary(acct)}`).join('\n')} - -These accounts have been marked as "Loose" so they will be easy to recognize in the account list. - -For more information, please read [our blog post.][1] - -[1]: https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd#.7d8ktj4h3 - `, - } -} diff --git a/ui/responsive/lib/persistent-form.js b/ui/responsive/lib/persistent-form.js deleted file mode 100644 index d4dc20b03..000000000 --- a/ui/responsive/lib/persistent-form.js +++ /dev/null @@ -1,61 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const defaultKey = 'persistent-form-default' -const eventName = 'keyup' - -module.exports = PersistentForm - -function PersistentForm () { - Component.call(this) -} - -inherits(PersistentForm, Component) - -PersistentForm.prototype.componentDidMount = function () { - const fields = document.querySelectorAll('[data-persistent-formid]') - const store = this.getPersistentStore() - - for (var i = 0; i < fields.length; i++) { - const field = fields[i] - const key = field.getAttribute('data-persistent-formid') - const cached = store[key] - if (cached !== undefined) { - field.value = cached - } - - field.addEventListener(eventName, this.persistentFieldDidUpdate.bind(this)) - } -} - -PersistentForm.prototype.getPersistentStore = function () { - let store = window.localStorage[this.persistentFormParentId || defaultKey] - if (store && store !== 'null') { - store = JSON.parse(store) - } else { - store = {} - } - return store -} - -PersistentForm.prototype.setPersistentStore = function (newStore) { - window.localStorage[this.persistentFormParentId || defaultKey] = JSON.stringify(newStore) -} - -PersistentForm.prototype.persistentFieldDidUpdate = function (event) { - const field = event.target - const store = this.getPersistentStore() - const key = field.getAttribute('data-persistent-formid') - const val = field.value - store[key] = val - this.setPersistentStore(store) -} - -PersistentForm.prototype.componentWillUnmount = function () { - const fields = document.querySelectorAll('[data-persistent-formid]') - for (var i = 0; i < fields.length; i++) { - const field = fields[i] - field.removeEventListener(eventName, this.persistentFieldDidUpdate.bind(this)) - } - this.setPersistentStore({}) -} - diff --git a/ui/responsive/lib/tx-helper.js b/ui/responsive/lib/tx-helper.js deleted file mode 100644 index ec19daf64..000000000 --- a/ui/responsive/lib/tx-helper.js +++ /dev/null @@ -1,17 +0,0 @@ -const valuesFor = require('../app/util').valuesFor - -module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, network) { - log.debug('tx-helper called with params:') - log.debug({ unapprovedTxs, unapprovedMsgs, personalMsgs, network }) - - const txValues = network ? valuesFor(unapprovedTxs).filter(txMeta => txMeta.metamaskNetworkId === network) : valuesFor(unapprovedTxs) - log.debug(`tx helper found ${txValues.length} unapproved txs`) - const msgValues = valuesFor(unapprovedMsgs) - log.debug(`tx helper found ${msgValues.length} unsigned messages`) - let allValues = txValues.concat(msgValues) - const personalValues = valuesFor(personalMsgs) - log.debug(`tx helper found ${personalValues.length} unsigned personal messages`) - allValues = allValues.concat(personalValues) - - return allValues.sort(txMeta => txMeta.time) -} -- cgit v1.2.3 From 31e7d8a24bd86d0fe8cd3822431bcc4f1a09c780 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 20 Jul 2017 12:50:42 -0700 Subject: Fix css links --- responsive-ui/css.js | 4 ++-- ui/css.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/responsive-ui/css.js b/responsive-ui/css.js index 7c394a87b..043363cd7 100644 --- a/responsive-ui/css.js +++ b/responsive-ui/css.js @@ -9,8 +9,8 @@ var cssFiles = { 'lib.css': fs.readFileSync(path.join(__dirname, '/app/css/lib.css'), 'utf8'), 'index.css': fs.readFileSync(path.join(__dirname, '/app/css/index.css'), 'utf8'), 'transitions.css': fs.readFileSync(path.join(__dirname, '/app/css/transitions.css'), 'utf8'), - 'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'), - 'react-css': fs.readFileSync(path.join(__dirname, '..', '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'), + 'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'), + 'react-css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'), } function bundleCss () { diff --git a/ui/css.js b/ui/css.js index 7c394a87b..043363cd7 100644 --- a/ui/css.js +++ b/ui/css.js @@ -9,8 +9,8 @@ var cssFiles = { 'lib.css': fs.readFileSync(path.join(__dirname, '/app/css/lib.css'), 'utf8'), 'index.css': fs.readFileSync(path.join(__dirname, '/app/css/index.css'), 'utf8'), 'transitions.css': fs.readFileSync(path.join(__dirname, '/app/css/transitions.css'), 'utf8'), - 'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'), - 'react-css': fs.readFileSync(path.join(__dirname, '..', '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'), + 'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'), + 'react-css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'), } function bundleCss () { -- cgit v1.2.3 From 38dccab12e4140bb085f3ea17e642e55f54d68a1 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 20 Jul 2017 12:54:08 -0700 Subject: Fix reference --- responsive-ui/app/conf-tx.js | 2 +- ui/app/conf-tx.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/responsive-ui/app/conf-tx.js b/responsive-ui/app/conf-tx.js index 63b77ef7f..747d3ce2b 100644 --- a/responsive-ui/app/conf-tx.js +++ b/responsive-ui/app/conf-tx.js @@ -6,7 +6,7 @@ const connect = require('react-redux').connect const actions = require('./actions') const NetworkIndicator = require('./components/network') const txHelper = require('../lib/tx-helper') -const isPopupOrNotification = require('../../../app/scripts/lib/is-popup-or-notification') +const isPopupOrNotification = require('../../app/scripts/lib/is-popup-or-notification') const PendingTx = require('./components/pending-tx') const PendingMsg = require('./components/pending-msg') diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 63b77ef7f..747d3ce2b 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -6,7 +6,7 @@ const connect = require('react-redux').connect const actions = require('./actions') const NetworkIndicator = require('./components/network') const txHelper = require('../lib/tx-helper') -const isPopupOrNotification = require('../../../app/scripts/lib/is-popup-or-notification') +const isPopupOrNotification = require('../../app/scripts/lib/is-popup-or-notification') const PendingTx = require('./components/pending-tx') const PendingMsg = require('./components/pending-msg') -- cgit v1.2.3 From 36c997a55eca801d8043ea812557075b6a98e5e6 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 20 Jul 2017 16:17:01 -0700 Subject: Make main account detail view more responsive --- responsive-ui/app/account-detail.js | 11 +++++++++-- responsive-ui/app/actions.js | 2 +- responsive-ui/app/app.js | 15 +++++++++++++-- responsive-ui/app/components/pending-tx.js | 2 +- responsive-ui/app/components/transaction-list.js | 8 ++++++-- responsive-ui/app/css/index.css | 2 ++ 6 files changed, 32 insertions(+), 8 deletions(-) diff --git a/responsive-ui/app/account-detail.js b/responsive-ui/app/account-detail.js index da1ddf98b..b39252db6 100644 --- a/responsive-ui/app/account-detail.js +++ b/responsive-ui/app/account-detail.js @@ -51,7 +51,12 @@ AccountDetailScreen.prototype.render = function () { return ( - h('.account-detail-section', [ + h('.account-detail-section', { + style: { + height: '100%', + maxWidth: '850px', + }, + }, [ // identicon, label, balance, etc h('.account-data-subsection', { @@ -236,7 +241,9 @@ AccountDetailScreen.prototype.subview = function () { AccountDetailScreen.prototype.tabSections = function () { const { currentAccountTab } = this.props - return h('section.tabSection', [ + return h('section.tabSection', { + style: { height: '100%' }, + }, [ h(TabBar, { tabs: [ diff --git a/responsive-ui/app/actions.js b/responsive-ui/app/actions.js index 2c60448dd..d99291e46 100644 --- a/responsive-ui/app/actions.js +++ b/responsive-ui/app/actions.js @@ -1,4 +1,4 @@ -const getBuyEthUrl = require('../../../app/scripts/lib/buy-eth-url') +const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url') var actions = { _setBackgroundConnection: _setBackgroundConnection, diff --git a/responsive-ui/app/app.js b/responsive-ui/app/app.js index 1cfa2d7a9..d1a20f079 100644 --- a/responsive-ui/app/app.js +++ b/responsive-ui/app/app.js @@ -77,6 +77,8 @@ App.prototype.render = function () { // Windows was showing a vertical scroll bar: overflow: 'hidden', position: 'relative', + height: '100%', + alignItems: 'center', }, }, [ @@ -91,7 +93,12 @@ App.prototype.render = function () { }), // panel content - h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), [ + h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), { + style: { + height: '100%', + maxWidth: '850px', + }, + }, [ h(ReactCSSTransitionGroup, { className: 'css-transition-group', transitionName: 'main', @@ -116,7 +123,11 @@ App.prototype.renderAppBar = function () { return ( - h('div', [ + h('div', { + style: { + width: '100%' + }, + }, [ h('.app-header.flex-row.flex-space-between', { style: { diff --git a/responsive-ui/app/components/pending-tx.js b/responsive-ui/app/components/pending-tx.js index 962680d30..d7d602f31 100644 --- a/responsive-ui/app/components/pending-tx.js +++ b/responsive-ui/app/components/pending-tx.js @@ -6,7 +6,7 @@ const clone = require('clone') const ethUtil = require('ethereumjs-util') const BN = ethUtil.BN -const hexToBn = require('../../../../app/scripts/lib/hex-to-bn') +const hexToBn = require('../../../app/scripts/lib/hex-to-bn') const util = require('../util') const MiniAccountPanel = require('./mini-account-panel') const Copyable = require('./copyable') diff --git a/responsive-ui/app/components/transaction-list.js b/responsive-ui/app/components/transaction-list.js index 3b4ba741e..ae6aaec8c 100644 --- a/responsive-ui/app/components/transaction-list.js +++ b/responsive-ui/app/components/transaction-list.js @@ -24,7 +24,11 @@ TransactionList.prototype.render = function () { return ( - h('section.transaction-list', [ + h('section.transaction-list', { + style: { + height: '100%', + }, + }, [ h('style', ` .transaction-list .transaction-list-item:not(:last-of-type) { @@ -39,7 +43,7 @@ TransactionList.prototype.render = function () { h('.tx-list', { style: { overflowY: 'auto', - height: '300px', + height: '100%', padding: '0 20px', textAlign: 'center', }, diff --git a/responsive-ui/app/css/index.css b/responsive-ui/app/css/index.css index c82c1b21b..2ae92bbd6 100644 --- a/responsive-ui/app/css/index.css +++ b/responsive-ui/app/css/index.css @@ -27,6 +27,7 @@ html, body { .css-transition-group { flex: 1; + height: 100%; } input:focus, textarea:focus { @@ -36,6 +37,7 @@ input:focus, textarea:focus { #app-content { overflow-x: hidden; min-width: 357px; + height: 100%; } button, input[type="submit"] { -- cgit v1.2.3 From 5f61fd242ceaa773994e9e4691ff028f9baadabb Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 20 Jul 2017 16:44:01 -0700 Subject: Prevent buy button from floating --- responsive-ui/app/account-detail.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/responsive-ui/app/account-detail.js b/responsive-ui/app/account-detail.js index b39252db6..f01a4a6f6 100644 --- a/responsive-ui/app/account-detail.js +++ b/responsive-ui/app/account-detail.js @@ -186,12 +186,6 @@ AccountDetailScreen.prototype.render = function () { h('button', { onClick: () => props.dispatch(actions.buyEthView(selected)), - style: { - marginBottom: '20px', - marginRight: '8px', - position: 'absolute', - left: '219px', - }, }, 'BUY'), h('button', { -- cgit v1.2.3 From 60aea4839c447de68e723d171fdb862056538057 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 20 Jul 2017 17:00:00 -0700 Subject: Improve account-detail panel responsiveness --- responsive-ui/app/account-detail.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/responsive-ui/app/account-detail.js b/responsive-ui/app/account-detail.js index f01a4a6f6..95e7c554a 100644 --- a/responsive-ui/app/account-detail.js +++ b/responsive-ui/app/account-detail.js @@ -62,7 +62,7 @@ AccountDetailScreen.prototype.render = function () { h('.account-data-subsection', { style: { margin: '0 20px', - maxWidth: '320px', + flex: '1 0 auto', }, }, [ @@ -87,6 +87,7 @@ AccountDetailScreen.prototype.render = function () { style: { lineHeight: '10px', marginLeft: '15px', + width: '100%', }, }, [ h(EditableLabel, { @@ -184,8 +185,11 @@ AccountDetailScreen.prototype.render = function () { }, }), + h('.flex-grow'), + h('button', { onClick: () => props.dispatch(actions.buyEthView(selected)), + style: { marginRight: '10px' }, }, 'BUY'), h('button', { -- cgit v1.2.3 From d571f5ee701acd87495ad8ed69a74e6c5ca424f3 Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Sun, 23 Jul 2017 21:32:49 -0700 Subject: Add Test Coverage with nyc package and coveralls for github badge --- .gitignore | 5 ++++- README.md | 2 +- package.json | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 85c2d15d6..1806b1932 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,7 @@ test/background.js test/bundle.js test/test-bundle.js -notes.txt \ No newline at end of file +notes.txt + +.coveralls.yml +.nyc_output \ No newline at end of file diff --git a/README.md b/README.md index d7086ae91..9aded09c1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# MetaMask Plugin [![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) +# MetaMask Plugin [![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) [![Coverage Status](https://coveralls.io/repos/github/MetaMask/metamask-extension/badge.svg?branch=master)](https://coveralls.io/github/MetaMask/metamask-extension?branch=master) ## Support diff --git a/package.json b/package.json index 375902d09..ef6e55980 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"", "single-test": "METAMASK_ENV=test mocha --require test/helper.js", "test-integration": "npm run buildMock && npm run buildCiUnits && testem ci -P 2", + "test-coverage": "nyc --reporter=text npm run test-unit", "lint": "gulp lint", "buildCiUnits": "node test/integration/index.js", "watch": "mocha watch --recursive \"test/unit/**/*.js\"", @@ -144,6 +145,7 @@ "brfs": "^1.4.3", "browserify": "^13.0.0", "chai": "^3.5.0", + "coveralls": "^2.13.1", "deep-freeze-strict": "^1.1.1", "del": "^2.2.0", "envify": "^4.0.0", @@ -170,6 +172,7 @@ "mocha-jsdom": "^1.1.0", "mocha-sinon": "^1.1.5", "nock": "^8.0.0", + "nyc": "^11.0.3", "open": "0.0.5", "prompt": "^1.0.0", "qs": "^6.2.0", -- cgit v1.2.3 From 55b7e457c504df0b0dadc848bc631704a950a14d Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Sun, 23 Jul 2017 21:35:21 -0700 Subject: Configure ci build to run tests individually --- circle.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/circle.yml b/circle.yml index 66eed17d7..efeb8ba57 100644 --- a/circle.yml +++ b/circle.yml @@ -5,3 +5,8 @@ dependencies: pre: - "npm i -g testem" - "npm i -g mocha" +test: + override: + - "npm run lint" + - "npm run test-coverage" + - "npm run test-integration" \ No newline at end of file -- cgit v1.2.3 From 24ffb40ec77016259ba4bb1b838298bf119f695e Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Mon, 24 Jul 2017 09:06:40 -0700 Subject: Add coveralls to script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ef6e55980..d90cc5e3b 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"", "single-test": "METAMASK_ENV=test mocha --require test/helper.js", "test-integration": "npm run buildMock && npm run buildCiUnits && testem ci -P 2", - "test-coverage": "nyc --reporter=text npm run test-unit", + "test-coverage": "nyc --reporter=lcov --reporter=text npm run test-unit && cat ./coverage/lcov.info | coveralls && rm -rf ./coverage", "lint": "gulp lint", "buildCiUnits": "node test/integration/index.js", "watch": "mocha watch --recursive \"test/unit/**/*.js\"", -- cgit v1.2.3 From 91edab2b1c0f2ac5056b5870ae352c12efdca3bf Mon Sep 17 00:00:00 2001 From: sdtsui Date: Mon, 24 Jul 2017 10:38:24 -0700 Subject: Add cursor to account menu buttons --- responsive-ui/app/account-detail.js | 1 + 1 file changed, 1 insertion(+) diff --git a/responsive-ui/app/account-detail.js b/responsive-ui/app/account-detail.js index 95e7c554a..05cf98de7 100644 --- a/responsive-ui/app/account-detail.js +++ b/responsive-ui/app/account-detail.js @@ -129,6 +129,7 @@ AccountDetailScreen.prototype.render = function () { style: { marginRight: '8px', marginLeft: 'auto', + cursor: 'pointer', }, selected, network, -- cgit v1.2.3 From a1fab0649035b75604dbee5aa18077c5cd747b3b Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Mon, 24 Jul 2017 13:46:02 -0700 Subject: Simplify the test-coverage script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d90cc5e3b..fe5466b9e 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"", "single-test": "METAMASK_ENV=test mocha --require test/helper.js", "test-integration": "npm run buildMock && npm run buildCiUnits && testem ci -P 2", - "test-coverage": "nyc --reporter=lcov --reporter=text npm run test-unit && cat ./coverage/lcov.info | coveralls && rm -rf ./coverage", + "test-coverage": "nyc npm run test-unit && nyc report --reporter=text-lcov | coveralls", "lint": "gulp lint", "buildCiUnits": "node test/integration/index.js", "watch": "mocha watch --recursive \"test/unit/**/*.js\"", -- cgit v1.2.3 From de286d238270e83f8fdf54bd36625883cc3a2816 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 24 Jul 2017 17:03:30 -0700 Subject: Css tweaks --- responsive-ui/app/account-detail.js | 4 +++- responsive-ui/app/css/lib.css | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/responsive-ui/app/account-detail.js b/responsive-ui/app/account-detail.js index 95e7c554a..ad65bf1d3 100644 --- a/responsive-ui/app/account-detail.js +++ b/responsive-ui/app/account-detail.js @@ -240,7 +240,9 @@ AccountDetailScreen.prototype.tabSections = function () { const { currentAccountTab } = this.props return h('section.tabSection', { - style: { height: '100%' }, + style: { + height: '100%', + }, }, [ h(TabBar, { diff --git a/responsive-ui/app/css/lib.css b/responsive-ui/app/css/lib.css index 910a24ee2..b0ca958a2 100644 --- a/responsive-ui/app/css/lib.css +++ b/responsive-ui/app/css/lib.css @@ -232,6 +232,10 @@ hr.horizontal-line { align-items: center; } +.tabSection { + min-width: 350px; +} + .menu-icon { display: inline-block; height: 9px; -- cgit v1.2.3 From a22adec66fd0c541eb350ea424a6b00d179eedaf Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 24 Jul 2017 17:04:13 -0700 Subject: Replace ui with responsive-ui --- app/home.html | 11 - app/scripts/responsive-core.js | 54 - app/scripts/responsive.js | 30 - responsive-ui/.gitignore | 66 - responsive-ui/app/account-detail.js | 297 --- responsive-ui/app/accounts/import/index.js | 100 - responsive-ui/app/accounts/import/json.js | 100 - responsive-ui/app/accounts/import/private-key.js | 67 - responsive-ui/app/accounts/import/seed.js | 30 - responsive-ui/app/actions.js | 1031 --------- responsive-ui/app/add-token.js | 219 -- responsive-ui/app/app.js | 591 ----- responsive-ui/app/components/account-dropdowns.js | 227 -- responsive-ui/app/components/account-export.js | 122 - responsive-ui/app/components/account-panel.js | 86 - responsive-ui/app/components/balance.js | 89 - responsive-ui/app/components/binary-renderer.js | 46 - .../app/components/bn-as-decimal-input.js | 174 -- responsive-ui/app/components/buy-button-subview.js | 197 -- responsive-ui/app/components/coinbase-form.js | 63 - responsive-ui/app/components/copyButton.js | 59 - responsive-ui/app/components/copyable.js | 46 - responsive-ui/app/components/custom-radio-list.js | 60 - responsive-ui/app/components/dropdown.js | 89 - responsive-ui/app/components/editable-label.js | 56 - responsive-ui/app/components/ens-input.js | 170 -- responsive-ui/app/components/eth-balance.js | 89 - responsive-ui/app/components/fiat-value.js | 63 - .../app/components/hex-as-decimal-input.js | 154 -- responsive-ui/app/components/identicon.js | 72 - responsive-ui/app/components/loading.js | 53 - responsive-ui/app/components/mascot.js | 59 - responsive-ui/app/components/mini-account-panel.js | 74 - responsive-ui/app/components/network.js | 124 - responsive-ui/app/components/notice.js | 126 -- .../app/components/pending-msg-details.js | 50 - responsive-ui/app/components/pending-msg.js | 56 - .../app/components/pending-personal-msg-details.js | 60 - .../app/components/pending-personal-msg.js | 47 - responsive-ui/app/components/pending-tx.js | 480 ---- responsive-ui/app/components/qr-code.js | 79 - responsive-ui/app/components/range-slider.js | 58 - responsive-ui/app/components/shapeshift-form.js | 306 --- responsive-ui/app/components/shift-list-item.js | 204 -- responsive-ui/app/components/tab-bar.js | 36 - responsive-ui/app/components/template.js | 18 - responsive-ui/app/components/token-cell.js | 72 - responsive-ui/app/components/token-list.js | 192 -- responsive-ui/app/components/tooltip.js | 22 - .../app/components/transaction-list-item-icon.js | 68 - .../app/components/transaction-list-item.js | 165 -- responsive-ui/app/components/transaction-list.js | 83 - responsive-ui/app/conf-tx.js | 213 -- responsive-ui/app/config.js | 211 -- responsive-ui/app/conversion.json | 207 -- responsive-ui/app/css/debug.css | 21 - responsive-ui/app/css/fonts.css | 36 - responsive-ui/app/css/index.css | 676 ------ responsive-ui/app/css/lib.css | 272 --- responsive-ui/app/css/reset.css | 48 - responsive-ui/app/css/transitions.css | 42 - responsive-ui/app/first-time/init-menu.js | 179 -- responsive-ui/app/img/identicon-tardigrade.png | Bin 141119 -> 0 bytes responsive-ui/app/img/identicon-walrus.png | Bin 388973 -> 0 bytes responsive-ui/app/info.js | 154 -- .../app/keychains/hd/create-vault-complete.js | 76 - .../app/keychains/hd/recover-seed/confirmation.js | 118 - responsive-ui/app/keychains/hd/restore-vault.js | 152 -- responsive-ui/app/new-keychain.js | 29 - responsive-ui/app/reducers.js | 52 - responsive-ui/app/reducers/app.js | 585 ----- responsive-ui/app/reducers/identities.js | 15 - responsive-ui/app/reducers/metamask.js | 137 -- responsive-ui/app/root.js | 22 - responsive-ui/app/send.js | 288 --- responsive-ui/app/settings.js | 59 - responsive-ui/app/store.js | 21 - responsive-ui/app/template.js | 30 - responsive-ui/app/unlock.js | 118 - responsive-ui/app/util.js | 217 -- responsive-ui/css.js | 29 - responsive-ui/design/00-metamask-SignIn.jpg | Bin 57848 -> 0 bytes responsive-ui/design/01-metamask-SelectAcc.jpg | Bin 76063 -> 0 bytes responsive-ui/design/02-metamask-AccDetails.jpg | Bin 75780 -> 0 bytes .../design/02a-metamask-AccDetails-OverToken.jpg | Bin 121847 -> 0 bytes .../02a-metamask-AccDetails-OverTransaction.jpg | Bin 122075 -> 0 bytes responsive-ui/design/02a-metamask-AccDetails.jpg | Bin 117570 -> 0 bytes .../design/02b-metamask-AccDetails-Send.jpg | Bin 110143 -> 0 bytes responsive-ui/design/03-metamask-Qr.jpg | Bin 66052 -> 0 bytes responsive-ui/design/05-metamask-Menu.jpg | Bin 130264 -> 0 bytes .../chromeStorePics/final_screen_dao_accounts.png | Bin 249708 -> 0 bytes .../chromeStorePics/final_screen_dao_locked.png | Bin 220295 -> 0 bytes .../final_screen_dao_notification.png | Bin 214405 -> 0 bytes .../chromeStorePics/final_screen_wei_account.png | Bin 253382 -> 0 bytes .../final_screen_wei_notification.png | Bin 193865 -> 0 bytes responsive-ui/design/chromeStorePics/icon-128.png | Bin 5770 -> 0 bytes responsive-ui/design/chromeStorePics/icon-64.png | Bin 3573 -> 0 bytes .../design/chromeStorePics/metamask_icon.ai | 2383 -------------------- .../design/chromeStorePics/promo1400560.png | Bin 261644 -> 0 bytes .../design/chromeStorePics/promo440280.png | Bin 57471 -> 0 bytes .../design/chromeStorePics/promo920680.png | Bin 206713 -> 0 bytes .../design/chromeStorePics/screen_dao_accounts.png | Bin 517598 -> 0 bytes .../design/chromeStorePics/screen_dao_locked.png | Bin 287108 -> 0 bytes .../chromeStorePics/screen_dao_notification.png | Bin 296498 -> 0 bytes .../design/chromeStorePics/screen_wei_account.png | Bin 653633 -> 0 bytes .../chromeStorePics/screen_wei_notification.png | Bin 402486 -> 0 bytes responsive-ui/design/metamask-logo-eyes.png | Bin 146076 -> 0 bytes responsive-ui/design/wireframes/1st_time_use.png | Bin 937556 -> 0 bytes .../design/wireframes/metamask_wfs_jan_13.pdf | Bin 452413 -> 0 bytes .../design/wireframes/metamask_wfs_jan_13.png | Bin 419066 -> 0 bytes .../design/wireframes/metamask_wfs_jan_18.pdf | Bin 612778 -> 0 bytes responsive-ui/example.js | 123 - responsive-ui/index.html | 20 - responsive-ui/index.js | 58 - responsive-ui/lib/account-link.js | 26 - responsive-ui/lib/contract-namer.js | 33 - responsive-ui/lib/etherscan-prefix-for-network.js | 21 - responsive-ui/lib/explorer-link.js | 6 - responsive-ui/lib/icon-factory.js | 65 - responsive-ui/lib/lost-accounts-notice.js | 23 - responsive-ui/lib/persistent-form.js | 61 - responsive-ui/lib/tx-helper.js | 17 - test/unit/responsive/components/dropdown-test.js | 4 +- ui/.gitignore | 66 + ui/app/account-detail.js | 120 +- ui/app/accounts/account-list-item.js | 91 - ui/app/accounts/index.js | 164 -- ui/app/add-token.js | 2 +- ui/app/app.js | 272 +-- ui/app/components/account-dropdowns.js | 227 ++ ui/app/components/account-info-link.js | 41 - ui/app/components/drop-menu-item.js | 59 - ui/app/components/dropdown.js | 89 + ui/app/components/editable-label.js | 7 +- ui/app/components/pending-tx.js | 2 +- ui/app/components/transaction-list.js | 8 +- ui/app/css/index.css | 15 +- ui/app/css/lib.css | 4 + ui/app/info.js | 10 +- ui/app/keychains/hd/create-vault-complete.js | 2 - ui/lib/tx-helper.js | 6 +- 141 files changed, 606 insertions(+), 14316 deletions(-) delete mode 100644 app/home.html delete mode 100644 app/scripts/responsive-core.js delete mode 100644 app/scripts/responsive.js delete mode 100644 responsive-ui/.gitignore delete mode 100644 responsive-ui/app/account-detail.js delete mode 100644 responsive-ui/app/accounts/import/index.js delete mode 100644 responsive-ui/app/accounts/import/json.js delete mode 100644 responsive-ui/app/accounts/import/private-key.js delete mode 100644 responsive-ui/app/accounts/import/seed.js delete mode 100644 responsive-ui/app/actions.js delete mode 100644 responsive-ui/app/add-token.js delete mode 100644 responsive-ui/app/app.js delete mode 100644 responsive-ui/app/components/account-dropdowns.js delete mode 100644 responsive-ui/app/components/account-export.js delete mode 100644 responsive-ui/app/components/account-panel.js delete mode 100644 responsive-ui/app/components/balance.js delete mode 100644 responsive-ui/app/components/binary-renderer.js delete mode 100644 responsive-ui/app/components/bn-as-decimal-input.js delete mode 100644 responsive-ui/app/components/buy-button-subview.js delete mode 100644 responsive-ui/app/components/coinbase-form.js delete mode 100644 responsive-ui/app/components/copyButton.js delete mode 100644 responsive-ui/app/components/copyable.js delete mode 100644 responsive-ui/app/components/custom-radio-list.js delete mode 100644 responsive-ui/app/components/dropdown.js delete mode 100644 responsive-ui/app/components/editable-label.js delete mode 100644 responsive-ui/app/components/ens-input.js delete mode 100644 responsive-ui/app/components/eth-balance.js delete mode 100644 responsive-ui/app/components/fiat-value.js delete mode 100644 responsive-ui/app/components/hex-as-decimal-input.js delete mode 100644 responsive-ui/app/components/identicon.js delete mode 100644 responsive-ui/app/components/loading.js delete mode 100644 responsive-ui/app/components/mascot.js delete mode 100644 responsive-ui/app/components/mini-account-panel.js delete mode 100644 responsive-ui/app/components/network.js delete mode 100644 responsive-ui/app/components/notice.js delete mode 100644 responsive-ui/app/components/pending-msg-details.js delete mode 100644 responsive-ui/app/components/pending-msg.js delete mode 100644 responsive-ui/app/components/pending-personal-msg-details.js delete mode 100644 responsive-ui/app/components/pending-personal-msg.js delete mode 100644 responsive-ui/app/components/pending-tx.js delete mode 100644 responsive-ui/app/components/qr-code.js delete mode 100644 responsive-ui/app/components/range-slider.js delete mode 100644 responsive-ui/app/components/shapeshift-form.js delete mode 100644 responsive-ui/app/components/shift-list-item.js delete mode 100644 responsive-ui/app/components/tab-bar.js delete mode 100644 responsive-ui/app/components/template.js delete mode 100644 responsive-ui/app/components/token-cell.js delete mode 100644 responsive-ui/app/components/token-list.js delete mode 100644 responsive-ui/app/components/tooltip.js delete mode 100644 responsive-ui/app/components/transaction-list-item-icon.js delete mode 100644 responsive-ui/app/components/transaction-list-item.js delete mode 100644 responsive-ui/app/components/transaction-list.js delete mode 100644 responsive-ui/app/conf-tx.js delete mode 100644 responsive-ui/app/config.js delete mode 100644 responsive-ui/app/conversion.json delete mode 100644 responsive-ui/app/css/debug.css delete mode 100644 responsive-ui/app/css/fonts.css delete mode 100644 responsive-ui/app/css/index.css delete mode 100644 responsive-ui/app/css/lib.css delete mode 100644 responsive-ui/app/css/reset.css delete mode 100644 responsive-ui/app/css/transitions.css delete mode 100644 responsive-ui/app/first-time/init-menu.js delete mode 100644 responsive-ui/app/img/identicon-tardigrade.png delete mode 100644 responsive-ui/app/img/identicon-walrus.png delete mode 100644 responsive-ui/app/info.js delete mode 100644 responsive-ui/app/keychains/hd/create-vault-complete.js delete mode 100644 responsive-ui/app/keychains/hd/recover-seed/confirmation.js delete mode 100644 responsive-ui/app/keychains/hd/restore-vault.js delete mode 100644 responsive-ui/app/new-keychain.js delete mode 100644 responsive-ui/app/reducers.js delete mode 100644 responsive-ui/app/reducers/app.js delete mode 100644 responsive-ui/app/reducers/identities.js delete mode 100644 responsive-ui/app/reducers/metamask.js delete mode 100644 responsive-ui/app/root.js delete mode 100644 responsive-ui/app/send.js delete mode 100644 responsive-ui/app/settings.js delete mode 100644 responsive-ui/app/store.js delete mode 100644 responsive-ui/app/template.js delete mode 100644 responsive-ui/app/unlock.js delete mode 100644 responsive-ui/app/util.js delete mode 100644 responsive-ui/css.js delete mode 100644 responsive-ui/design/00-metamask-SignIn.jpg delete mode 100644 responsive-ui/design/01-metamask-SelectAcc.jpg delete mode 100644 responsive-ui/design/02-metamask-AccDetails.jpg delete mode 100644 responsive-ui/design/02a-metamask-AccDetails-OverToken.jpg delete mode 100644 responsive-ui/design/02a-metamask-AccDetails-OverTransaction.jpg delete mode 100644 responsive-ui/design/02a-metamask-AccDetails.jpg delete mode 100644 responsive-ui/design/02b-metamask-AccDetails-Send.jpg delete mode 100644 responsive-ui/design/03-metamask-Qr.jpg delete mode 100644 responsive-ui/design/05-metamask-Menu.jpg delete mode 100644 responsive-ui/design/chromeStorePics/final_screen_dao_accounts.png delete mode 100644 responsive-ui/design/chromeStorePics/final_screen_dao_locked.png delete mode 100644 responsive-ui/design/chromeStorePics/final_screen_dao_notification.png delete mode 100644 responsive-ui/design/chromeStorePics/final_screen_wei_account.png delete mode 100644 responsive-ui/design/chromeStorePics/final_screen_wei_notification.png delete mode 100644 responsive-ui/design/chromeStorePics/icon-128.png delete mode 100644 responsive-ui/design/chromeStorePics/icon-64.png delete mode 100644 responsive-ui/design/chromeStorePics/metamask_icon.ai delete mode 100644 responsive-ui/design/chromeStorePics/promo1400560.png delete mode 100644 responsive-ui/design/chromeStorePics/promo440280.png delete mode 100644 responsive-ui/design/chromeStorePics/promo920680.png delete mode 100644 responsive-ui/design/chromeStorePics/screen_dao_accounts.png delete mode 100644 responsive-ui/design/chromeStorePics/screen_dao_locked.png delete mode 100644 responsive-ui/design/chromeStorePics/screen_dao_notification.png delete mode 100644 responsive-ui/design/chromeStorePics/screen_wei_account.png delete mode 100644 responsive-ui/design/chromeStorePics/screen_wei_notification.png delete mode 100644 responsive-ui/design/metamask-logo-eyes.png delete mode 100644 responsive-ui/design/wireframes/1st_time_use.png delete mode 100644 responsive-ui/design/wireframes/metamask_wfs_jan_13.pdf delete mode 100644 responsive-ui/design/wireframes/metamask_wfs_jan_13.png delete mode 100644 responsive-ui/design/wireframes/metamask_wfs_jan_18.pdf delete mode 100644 responsive-ui/example.js delete mode 100644 responsive-ui/index.html delete mode 100644 responsive-ui/index.js delete mode 100644 responsive-ui/lib/account-link.js delete mode 100644 responsive-ui/lib/contract-namer.js delete mode 100644 responsive-ui/lib/etherscan-prefix-for-network.js delete mode 100644 responsive-ui/lib/explorer-link.js delete mode 100644 responsive-ui/lib/icon-factory.js delete mode 100644 responsive-ui/lib/lost-accounts-notice.js delete mode 100644 responsive-ui/lib/persistent-form.js delete mode 100644 responsive-ui/lib/tx-helper.js create mode 100644 ui/.gitignore delete mode 100644 ui/app/accounts/account-list-item.js delete mode 100644 ui/app/accounts/index.js create mode 100644 ui/app/components/account-dropdowns.js delete mode 100644 ui/app/components/account-info-link.js delete mode 100644 ui/app/components/drop-menu-item.js create mode 100644 ui/app/components/dropdown.js diff --git a/app/home.html b/app/home.html deleted file mode 100644 index b7b8adbeb..000000000 --- a/app/home.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - MetaMask Plugin - - -
- - - diff --git a/app/scripts/responsive-core.js b/app/scripts/responsive-core.js deleted file mode 100644 index c3fa6700d..000000000 --- a/app/scripts/responsive-core.js +++ /dev/null @@ -1,54 +0,0 @@ -const EventEmitter = require('events').EventEmitter -const async = require('async') -const Dnode = require('dnode') -const EthQuery = require('eth-query') -const launchMetamaskUi = require('../../responsive-ui') -const StreamProvider = require('web3-stream-provider') -const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex - - -module.exports = initializePopup - - -function initializePopup ({ container, connectionStream }, cb) { - // setup app - async.waterfall([ - (cb) => connectToAccountManager(connectionStream, cb), - (accountManager, cb) => launchMetamaskUi({ container, accountManager }, cb), - ], cb) -} - -function connectToAccountManager (connectionStream, cb) { - // setup communication with background - // setup multiplexing - var mx = setupMultiplex(connectionStream) - // connect features - setupControllerConnection(mx.createStream('controller'), cb) - setupWeb3Connection(mx.createStream('provider')) -} - -function setupWeb3Connection (connectionStream) { - var providerStream = new StreamProvider() - providerStream.pipe(connectionStream).pipe(providerStream) - connectionStream.on('error', console.error.bind(console)) - providerStream.on('error', console.error.bind(console)) - global.ethereumProvider = providerStream - global.ethQuery = new EthQuery(providerStream) -} - -function setupControllerConnection (connectionStream, cb) { - // this is a really sneaky way of adding EventEmitter api - // to a bi-directional dnode instance - var eventEmitter = new EventEmitter() - var accountManagerDnode = Dnode({ - sendUpdate: function (state) { - eventEmitter.emit('update', state) - }, - }) - connectionStream.pipe(accountManagerDnode).pipe(connectionStream) - accountManagerDnode.once('remote', function (accountManager) { - // setup push events - accountManager.on = eventEmitter.on.bind(eventEmitter) - cb(null, accountManager) - }) -} diff --git a/app/scripts/responsive.js b/app/scripts/responsive.js deleted file mode 100644 index 6525b833b..000000000 --- a/app/scripts/responsive.js +++ /dev/null @@ -1,30 +0,0 @@ -const injectCss = require('inject-css') -const startPopup = require('./responsive-core') -const MetaMaskUiCss = require('../../responsive-ui/css') -const PortStream = require('./lib/port-stream.js') -const ExtensionPlatform = require('./platforms/extension') -const extension = require('extensionizer') - -// create platform global -global.platform = new ExtensionPlatform() - -// inject css -const css = MetaMaskUiCss() -injectCss(css) - -// setup stream to background -const extensionPort = extension.runtime.connect({ name: 'ui' }) -const connectionStream = new PortStream(extensionPort) - -// start ui -const container = document.getElementById('app-content') -startPopup({ container, connectionStream }, (err, store) => { - if (err) return displayCriticalError(err) -}) - -function displayCriticalError (err) { - container.innerHTML = '
The MetaMask app failed to load: please open and close MetaMask again to restart.
' - container.style.height = '80px' - log.error(err.stack) - throw err -} diff --git a/responsive-ui/.gitignore b/responsive-ui/.gitignore deleted file mode 100644 index c6b1254b5..000000000 --- a/responsive-ui/.gitignore +++ /dev/null @@ -1,66 +0,0 @@ - -# Created by https://www.gitignore.io/api/osx,node - -### OSX ### -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - - -### Node ### -# Logs -logs -*.log -npm-debug.log* - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directory -# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git -node_modules - -# Optional npm cache directory -.npm - -# Optional REPL history -.node_repl_history - diff --git a/responsive-ui/app/account-detail.js b/responsive-ui/app/account-detail.js deleted file mode 100644 index 18c867153..000000000 --- a/responsive-ui/app/account-detail.js +++ /dev/null @@ -1,297 +0,0 @@ -const inherits = require('util').inherits -const extend = require('xtend') -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') -const valuesFor = require('./util').valuesFor -const Identicon = require('./components/identicon') -const EthBalance = require('./components/eth-balance') -const TransactionList = require('./components/transaction-list') -const ExportAccountView = require('./components/account-export') -const ethUtil = require('ethereumjs-util') -const EditableLabel = require('./components/editable-label') -const TabBar = require('./components/tab-bar') -const TokenList = require('./components/token-list') -const AccountDropdowns = require('./components/account-dropdowns').AccountDropdowns - -module.exports = connect(mapStateToProps)(AccountDetailScreen) - -function mapStateToProps (state) { - return { - metamask: state.metamask, - identities: state.metamask.identities, - accounts: state.metamask.accounts, - address: state.metamask.selectedAddress, - accountDetail: state.appState.accountDetail, - network: state.metamask.network, - unapprovedMsgs: valuesFor(state.metamask.unapprovedMsgs), - shapeShiftTxList: state.metamask.shapeShiftTxList, - transactions: state.metamask.selectedAddressTxList || [], - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - currentAccountTab: state.metamask.currentAccountTab, - tokens: state.metamask.tokens, - } -} - -inherits(AccountDetailScreen, Component) -function AccountDetailScreen () { - Component.call(this) -} - -AccountDetailScreen.prototype.render = function () { - var props = this.props - var selected = props.address || Object.keys(props.accounts)[0] - var checksumAddress = selected && ethUtil.toChecksumAddress(selected) - var identity = props.identities[selected] - var account = props.accounts[selected] - const { network, conversionRate, currentCurrency } = props - - return ( - - h('.account-detail-section', { - style: { - height: '100%', - maxWidth: '850px', - }, - }, [ - - // identicon, label, balance, etc - h('.account-data-subsection', { - style: { - margin: '0 20px', - flex: '1 0 auto', - }, - }, [ - - // header - identicon + nav - h('div', { - style: { - paddingTop: '20px', - display: 'flex', - justifyContent: 'flex-start', - alignItems: 'flex-start', - }, - }, [ - - // large identicon and addresses - h('.identicon-wrapper.select-none', [ - h(Identicon, { - diameter: 62, - address: selected, - }), - ]), - h('flex-column', { - style: { - lineHeight: '10px', - marginLeft: '15px', - width: '100%', - }, - }, [ - h(EditableLabel, { - textValue: identity ? identity.name : '', - state: { - isEditingLabel: false, - }, - saveText: (text) => { - props.dispatch(actions.saveAccountLabel(selected, text)) - }, - }, [ - - // What is shown when not editing + edit text: - h('label.editing-label', [h('.edit-text', 'edit')]), - h( - 'div', - { - style: { - display: 'flex', - justifyContent: 'flex-start', - alignItems: 'center', - }, - }, - [ - h( - 'h2.font-medium.color-forest', - { - name: 'edit', - style: { - }, - }, - [ - identity && identity.name, - ] - ), - h( - AccountDropdowns, - { - style: { - marginRight: '8px', - marginLeft: 'auto', - cursor: 'pointer', - }, - selected, - network, - identities: props.identities, - }, - ), - ] - ), - ]), - h('.flex-row', { - style: { - width: '15em', - justifyContent: 'space-between', - alignItems: 'baseline', - }, - }, [ - - // address - - h('div', { - style: { - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingTop: '3px', - width: '5em', - fontSize: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - marginTop: '10px', - marginBottom: '15px', - color: '#AEAEAE', - }, - }, checksumAddress), - ]), - - // account ballence - - ]), - ]), - h('.flex-row', { - style: { - justifyContent: 'space-between', - alignItems: 'flex-start', - }, - }, [ - - h(EthBalance, { - value: account && account.balance, - conversionRate, - currentCurrency, - style: { - lineHeight: '7px', - marginTop: '10px', - }, - }), - - h('.flex-grow'), - - h('button', { - onClick: () => props.dispatch(actions.buyEthView(selected)), - style: { marginRight: '10px' }, - }, 'BUY'), - - h('button', { - onClick: () => props.dispatch(actions.showSendPage()), - style: { - marginBottom: '20px', - marginRight: '8px', - }, - }, 'SEND'), - - ]), - ]), - - // subview (tx history, pk export confirm, buy eth warning) - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.subview(), - ]), - - ]) - ) -} - -AccountDetailScreen.prototype.subview = function () { - var subview - try { - subview = this.props.accountDetail.subview - } catch (e) { - subview = null - } - - switch (subview) { - case 'transactions': - return this.tabSections() - case 'export': - var state = extend({key: 'export'}, this.props) - return h(ExportAccountView, state) - default: - return this.tabSections() - } -} - -AccountDetailScreen.prototype.tabSections = function () { - const { currentAccountTab } = this.props - - return h('section.tabSection', { - style: { - height: '100%', - }, - }, [ - - h(TabBar, { - tabs: [ - { content: 'Sent', key: 'history' }, - { content: 'Tokens', key: 'tokens' }, - ], - defaultTab: currentAccountTab || 'history', - tabSelected: (key) => { - this.props.dispatch(actions.setCurrentAccountTab(key)) - }, - }), - - this.tabSwitchView(), - ]) -} - -AccountDetailScreen.prototype.tabSwitchView = function () { - const props = this.props - const { address, network } = props - const { currentAccountTab, tokens } = this.props - - switch (currentAccountTab) { - case 'tokens': - return h(TokenList, { - userAddress: address, - network, - tokens, - addToken: () => this.props.dispatch(actions.showAddTokenPage()), - }) - default: - return this.transactionList() - } -} - -AccountDetailScreen.prototype.transactionList = function () { - const {transactions, unapprovedMsgs, address, - network, shapeShiftTxList, conversionRate } = this.props - - return h(TransactionList, { - transactions: transactions.sort((a, b) => b.time - a.time), - network, - unapprovedMsgs, - conversionRate, - address, - shapeShiftTxList, - viewPendingTx: (txId) => { - this.props.dispatch(actions.viewPendingTx(txId)) - }, - }) -} diff --git a/responsive-ui/app/accounts/import/index.js b/responsive-ui/app/accounts/import/index.js deleted file mode 100644 index 97b387229..000000000 --- a/responsive-ui/app/accounts/import/index.js +++ /dev/null @@ -1,100 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../../actions') -import Select from 'react-select' - -// Subviews -const JsonImportView = require('./json.js') -const PrivateKeyImportView = require('./private-key.js') - -const menuItems = [ - 'Private Key', - 'JSON File', -] - -module.exports = connect(mapStateToProps)(AccountImportSubview) - -function mapStateToProps (state) { - return { - menuItems, - } -} - -inherits(AccountImportSubview, Component) -function AccountImportSubview () { - Component.call(this) -} - -AccountImportSubview.prototype.render = function () { - const props = this.props - const state = this.state || {} - const { menuItems } = props - const { type } = state - - return ( - h('div', { - style: { - }, - }, [ - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: (event) => { - props.dispatch(actions.goHome()) - }, - }), - h('h2.page-subtitle', 'Import Accounts'), - ]), - h('div', { - style: { - padding: '10px', - color: 'rgb(174, 174, 174)', - }, - }, [ - - h('h3', { style: { padding: '3px' } }, 'SELECT TYPE'), - - h('style', ` - .has-value.Select--single > .Select-control .Select-value .Select-value-label, .Select-value-label { - color: rgb(174,174,174); - } - `), - - h(Select, { - name: 'import-type-select', - clearable: false, - value: type || menuItems[0], - options: menuItems.map((type) => { - return { - value: type, - label: type, - } - }), - onChange: (opt) => { - this.setState({ type: opt.value }) - }, - }), - ]), - - this.renderImportView(), - ]) - ) -} - -AccountImportSubview.prototype.renderImportView = function () { - const props = this.props - const state = this.state || {} - const { type } = state - const { menuItems } = props - const current = type || menuItems[0] - - switch (current) { - case 'Private Key': - return h(PrivateKeyImportView) - case 'JSON File': - return h(JsonImportView) - default: - return h(JsonImportView) - } -} diff --git a/responsive-ui/app/accounts/import/json.js b/responsive-ui/app/accounts/import/json.js deleted file mode 100644 index 158a3c923..000000000 --- a/responsive-ui/app/accounts/import/json.js +++ /dev/null @@ -1,100 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../../actions') -const FileInput = require('react-simple-file-input').default - -const HELP_LINK = 'https://github.com/MetaMask/faq/blob/master/README.md#q-i-cant-use-the-import-feature-for-uploading-a-json-file-the-window-keeps-closing-when-i-try-to-select-a-file' - -module.exports = connect(mapStateToProps)(JsonImportSubview) - -function mapStateToProps (state) { - return { - error: state.appState.warning, - } -} - -inherits(JsonImportSubview, Component) -function JsonImportSubview () { - Component.call(this) -} - -JsonImportSubview.prototype.render = function () { - const { error } = this.props - - return ( - h('div', { - style: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - padding: '5px 15px 0px 15px', - }, - }, [ - - h('p', 'Used by a variety of different clients'), - h('a.warning', { href: HELP_LINK, target: '_blank' }, 'File import not working? Click here!'), - - h(FileInput, { - readAs: 'text', - onLoad: this.onLoad.bind(this), - style: { - margin: '20px 0px 12px 20px', - fontSize: '15px', - }, - }), - - h('input.large-input.letter-spacey', { - type: 'password', - placeholder: 'Enter password', - id: 'json-password-box', - onKeyPress: this.createKeyringOnEnter.bind(this), - style: { - width: 260, - marginTop: 12, - }, - }), - - h('button.primary', { - onClick: this.createNewKeychain.bind(this), - style: { - margin: 12, - }, - }, 'Import'), - - error ? h('span.error', error) : null, - ]) - ) -} - -JsonImportSubview.prototype.onLoad = function (event, file) { - this.setState({file: file, fileContents: event.target.result}) -} - -JsonImportSubview.prototype.createKeyringOnEnter = function (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.createNewKeychain() - } -} - -JsonImportSubview.prototype.createNewKeychain = function () { - const state = this.state - const { fileContents } = state - - if (!fileContents) { - const message = 'You must select a file to import.' - return this.props.dispatch(actions.displayWarning(message)) - } - - const passwordInput = document.getElementById('json-password-box') - const password = passwordInput.value - - if (!password) { - const message = 'You must enter a password for the selected file.' - return this.props.dispatch(actions.displayWarning(message)) - } - - this.props.dispatch(actions.importNewAccount('JSON File', [ fileContents, password ])) -} diff --git a/responsive-ui/app/accounts/import/private-key.js b/responsive-ui/app/accounts/import/private-key.js deleted file mode 100644 index 68ccee58e..000000000 --- a/responsive-ui/app/accounts/import/private-key.js +++ /dev/null @@ -1,67 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../../actions') - -module.exports = connect(mapStateToProps)(PrivateKeyImportView) - -function mapStateToProps (state) { - return { - error: state.appState.warning, - } -} - -inherits(PrivateKeyImportView, Component) -function PrivateKeyImportView () { - Component.call(this) -} - -PrivateKeyImportView.prototype.render = function () { - const { error } = this.props - - return ( - h('div', { - style: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - padding: '5px 15px 0px 15px', - }, - }, [ - h('span', 'Paste your private key string here'), - - h('input.large-input.letter-spacey', { - type: 'password', - id: 'private-key-box', - onKeyPress: this.createKeyringOnEnter.bind(this), - style: { - width: 260, - marginTop: 12, - }, - }), - - h('button.primary', { - onClick: this.createNewKeychain.bind(this), - style: { - margin: 12, - }, - }, 'Import'), - - error ? h('span.error', error) : null, - ]) - ) -} - -PrivateKeyImportView.prototype.createKeyringOnEnter = function (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.createNewKeychain() - } -} - -PrivateKeyImportView.prototype.createNewKeychain = function () { - const input = document.getElementById('private-key-box') - const privateKey = input.value - this.props.dispatch(actions.importNewAccount('Private Key', [ privateKey ])) -} diff --git a/responsive-ui/app/accounts/import/seed.js b/responsive-ui/app/accounts/import/seed.js deleted file mode 100644 index b4a7c0afa..000000000 --- a/responsive-ui/app/accounts/import/seed.js +++ /dev/null @@ -1,30 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect - -module.exports = connect(mapStateToProps)(SeedImportSubview) - -function mapStateToProps (state) { - return {} -} - -inherits(SeedImportSubview, Component) -function SeedImportSubview () { - Component.call(this) -} - -SeedImportSubview.prototype.render = function () { - return ( - h('div', { - style: { - }, - }, [ - `Paste your seed phrase here!`, - h('textarea'), - h('br'), - h('button', 'Submit'), - ]) - ) -} - diff --git a/responsive-ui/app/actions.js b/responsive-ui/app/actions.js deleted file mode 100644 index d99291e46..000000000 --- a/responsive-ui/app/actions.js +++ /dev/null @@ -1,1031 +0,0 @@ -const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url') - -var actions = { - _setBackgroundConnection: _setBackgroundConnection, - - GO_HOME: 'GO_HOME', - goHome: goHome, - // menu state - getNetworkStatus: 'getNetworkStatus', - // transition state - TRANSITION_FORWARD: 'TRANSITION_FORWARD', - TRANSITION_BACKWARD: 'TRANSITION_BACKWARD', - transitionForward, - transitionBackward, - // remote state - UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE', - updateMetamaskState: updateMetamaskState, - // notices - MARK_NOTICE_READ: 'MARK_NOTICE_READ', - markNoticeRead: markNoticeRead, - SHOW_NOTICE: 'SHOW_NOTICE', - showNotice: showNotice, - CLEAR_NOTICES: 'CLEAR_NOTICES', - clearNotices: clearNotices, - markAccountsFound, - // intialize screen - CREATE_NEW_VAULT_IN_PROGRESS: 'CREATE_NEW_VAULT_IN_PROGRESS', - SHOW_CREATE_VAULT: 'SHOW_CREATE_VAULT', - SHOW_RESTORE_VAULT: 'SHOW_RESTORE_VAULT', - FORGOT_PASSWORD: 'FORGOT_PASSWORD', - forgotPassword: forgotPassword, - SHOW_INIT_MENU: 'SHOW_INIT_MENU', - SHOW_NEW_VAULT_SEED: 'SHOW_NEW_VAULT_SEED', - SHOW_INFO_PAGE: 'SHOW_INFO_PAGE', - SHOW_IMPORT_PAGE: 'SHOW_IMPORT_PAGE', - unlockMetamask: unlockMetamask, - unlockFailed: unlockFailed, - showCreateVault: showCreateVault, - showRestoreVault: showRestoreVault, - showInitializeMenu: showInitializeMenu, - showImportPage, - createNewVaultAndKeychain: createNewVaultAndKeychain, - createNewVaultAndRestore: createNewVaultAndRestore, - createNewVaultInProgress: createNewVaultInProgress, - addNewKeyring, - importNewAccount, - addNewAccount, - NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN', - navigateToNewAccountScreen, - showNewVaultSeed: showNewVaultSeed, - showInfoPage: showInfoPage, - // seed recovery actions - REVEAL_SEED_CONFIRMATION: 'REVEAL_SEED_CONFIRMATION', - revealSeedConfirmation: revealSeedConfirmation, - requestRevealSeed: requestRevealSeed, - // unlock screen - UNLOCK_IN_PROGRESS: 'UNLOCK_IN_PROGRESS', - UNLOCK_FAILED: 'UNLOCK_FAILED', - UNLOCK_METAMASK: 'UNLOCK_METAMASK', - LOCK_METAMASK: 'LOCK_METAMASK', - tryUnlockMetamask: tryUnlockMetamask, - lockMetamask: lockMetamask, - unlockInProgress: unlockInProgress, - // error handling - displayWarning: displayWarning, - DISPLAY_WARNING: 'DISPLAY_WARNING', - HIDE_WARNING: 'HIDE_WARNING', - hideWarning: hideWarning, - // accounts screen - SET_SELECTED_ACCOUNT: 'SET_SELECTED_ACCOUNT', - SHOW_ACCOUNT_DETAIL: 'SHOW_ACCOUNT_DETAIL', - SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE', - SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE', - SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE', - SET_CURRENT_FIAT: 'SET_CURRENT_FIAT', - setCurrentCurrency: setCurrentCurrency, - setCurrentAccountTab, - // account detail screen - SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', - showSendPage: showSendPage, - ADD_TO_ADDRESS_BOOK: 'ADD_TO_ADDRESS_BOOK', - addToAddressBook: addToAddressBook, - REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT', - requestExportAccount: requestExportAccount, - EXPORT_ACCOUNT: 'EXPORT_ACCOUNT', - exportAccount: exportAccount, - SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY', - showPrivateKey: showPrivateKey, - SAVE_ACCOUNT_LABEL: 'SAVE_ACCOUNT_LABEL', - saveAccountLabel: saveAccountLabel, - // tx conf screen - COMPLETED_TX: 'COMPLETED_TX', - TRANSACTION_ERROR: 'TRANSACTION_ERROR', - NEXT_TX: 'NEXT_TX', - PREVIOUS_TX: 'PREV_TX', - signMsg: signMsg, - cancelMsg: cancelMsg, - signPersonalMsg, - cancelPersonalMsg, - sendTx: sendTx, - signTx: signTx, - updateAndApproveTx, - cancelTx: cancelTx, - completedTx: completedTx, - txError: txError, - nextTx: nextTx, - previousTx: previousTx, - viewPendingTx: viewPendingTx, - VIEW_PENDING_TX: 'VIEW_PENDING_TX', - // app messages - confirmSeedWords: confirmSeedWords, - showAccountDetail: showAccountDetail, - BACK_TO_ACCOUNT_DETAIL: 'BACK_TO_ACCOUNT_DETAIL', - backToAccountDetail: backToAccountDetail, - showAccountsPage: showAccountsPage, - showConfTxPage: showConfTxPage, - // config screen - SHOW_CONFIG_PAGE: 'SHOW_CONFIG_PAGE', - SET_RPC_TARGET: 'SET_RPC_TARGET', - SET_DEFAULT_RPC_TARGET: 'SET_DEFAULT_RPC_TARGET', - SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE', - USE_ETHERSCAN_PROVIDER: 'USE_ETHERSCAN_PROVIDER', - useEtherscanProvider: useEtherscanProvider, - showConfigPage, - SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', - showAddTokenPage, - addToken, - setRpcTarget: setRpcTarget, - setDefaultRpcTarget: setDefaultRpcTarget, - setProviderType: setProviderType, - // loading overlay - SHOW_LOADING: 'SHOW_LOADING_INDICATION', - HIDE_LOADING: 'HIDE_LOADING_INDICATION', - showLoadingIndication: showLoadingIndication, - hideLoadingIndication: hideLoadingIndication, - // buy Eth with coinbase - BUY_ETH: 'BUY_ETH', - buyEth: buyEth, - buyEthView: buyEthView, - BUY_ETH_VIEW: 'BUY_ETH_VIEW', - COINBASE_SUBVIEW: 'COINBASE_SUBVIEW', - coinBaseSubview: coinBaseSubview, - SHAPESHIFT_SUBVIEW: 'SHAPESHIFT_SUBVIEW', - shapeShiftSubview: shapeShiftSubview, - PAIR_UPDATE: 'PAIR_UPDATE', - pairUpdate: pairUpdate, - coinShiftRquest: coinShiftRquest, - SHOW_SUB_LOADING_INDICATION: 'SHOW_SUB_LOADING_INDICATION', - showSubLoadingIndication: showSubLoadingIndication, - HIDE_SUB_LOADING_INDICATION: 'HIDE_SUB_LOADING_INDICATION', - hideSubLoadingIndication: hideSubLoadingIndication, -// QR STUFF: - SHOW_QR: 'SHOW_QR', - showQrView: showQrView, - reshowQrCode: reshowQrCode, - SHOW_QR_VIEW: 'SHOW_QR_VIEW', -// FORGOT PASSWORD: - BACK_TO_INIT_MENU: 'BACK_TO_INIT_MENU', - goBackToInitView: goBackToInitView, - RECOVERY_IN_PROGRESS: 'RECOVERY_IN_PROGRESS', - BACK_TO_UNLOCK_VIEW: 'BACK_TO_UNLOCK_VIEW', - backToUnlockView: backToUnlockView, - // SHOWING KEYCHAIN - SHOW_NEW_KEYCHAIN: 'SHOW_NEW_KEYCHAIN', - showNewKeychain: showNewKeychain, - - callBackgroundThenUpdate, - forceUpdateMetamaskState, -} - -module.exports = actions - -var background = null -function _setBackgroundConnection (backgroundConnection) { - background = backgroundConnection -} - -function goHome () { - return { - type: actions.GO_HOME, - } -} - -// async actions - -function tryUnlockMetamask (password) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - dispatch(actions.unlockInProgress()) - log.debug(`background.submitPassword`) - background.submitPassword(password, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - dispatch(actions.unlockFailed(err.message)) - } else { - dispatch(actions.transitionForward()) - forceUpdateMetamaskState(dispatch) - } - }) - } -} - -function transitionForward () { - return { - type: this.TRANSITION_FORWARD, - } -} - -function transitionBackward () { - return { - type: this.TRANSITION_BACKWARD, - } -} - -function confirmSeedWords () { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.clearSeedWordCache`) - background.clearSeedWordCache((err, account) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - - log.info('Seed word cache cleared. ' + account) - dispatch(actions.showAccountDetail(account)) - }) - } -} - -function createNewVaultAndRestore (password, seed) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.createNewVaultAndRestore`) - background.createNewVaultAndRestore(password, seed, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.showAccountsPage()) - }) - } -} - -function createNewVaultAndKeychain (password) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.createNewVaultAndKeychain`) - background.createNewVaultAndKeychain(password, (err) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - log.debug(`background.placeSeedWords`) - background.placeSeedWords((err) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch(actions.hideLoadingIndication()) - forceUpdateMetamaskState(dispatch) - }) - }) - } -} - -function revealSeedConfirmation () { - return { - type: this.REVEAL_SEED_CONFIRMATION, - } -} - -function requestRevealSeed (password) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.submitPassword`) - background.submitPassword(password, (err) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - log.debug(`background.placeSeedWords`) - background.placeSeedWords((err, result) => { - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.hideLoadingIndication()) - dispatch(actions.showNewVaultSeed(result)) - }) - }) - } -} - -function addNewKeyring (type, opts) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.addNewKeyring`) - background.addNewKeyring(type, opts, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.showAccountsPage()) - }) - } -} - -function importNewAccount (strategy, args) { - return (dispatch) => { - dispatch(actions.showLoadingIndication('This may take a while, be patient.')) - log.debug(`background.importAccountWithStrategy`) - background.importAccountWithStrategy(strategy, args, (err) => { - if (err) return dispatch(actions.displayWarning(err.message)) - log.debug(`background.getState`) - background.getState((err, newState) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch(actions.updateMetamaskState(newState)) - dispatch({ - type: actions.SHOW_ACCOUNT_DETAIL, - value: newState.selectedAddress, - }) - }) - }) - } -} - -function navigateToNewAccountScreen () { - return { - type: this.NEW_ACCOUNT_SCREEN, - } -} - -function addNewAccount () { - log.debug(`background.addNewAccount`) - return callBackgroundThenUpdate(background.addNewAccount) -} - -function showInfoPage () { - return { - type: actions.SHOW_INFO_PAGE, - } -} - -function setCurrentCurrency (currencyCode) { - return (dispatch) => { - dispatch(this.showLoadingIndication()) - log.debug(`background.setCurrentCurrency`) - background.setCurrentCurrency(currencyCode, (err, data) => { - dispatch(this.hideLoadingIndication()) - if (err) { - log.error(err.stack) - return dispatch(actions.displayWarning(err.message)) - } - dispatch({ - type: this.SET_CURRENT_FIAT, - value: { - currentCurrency: data.currentCurrency, - conversionRate: data.conversionRate, - conversionDate: data.conversionDate, - }, - }) - }) - } -} - -function signMsg (msgData) { - log.debug('action - signMsg') - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - - log.debug(`actions calling background.signMessage`) - background.signMessage(msgData, (err, newState) => { - log.debug('signMessage called back') - dispatch(actions.updateMetamaskState(newState)) - dispatch(actions.hideLoadingIndication()) - - if (err) log.error(err) - if (err) return dispatch(actions.displayWarning(err.message)) - - dispatch(actions.completedTx(msgData.metamaskId)) - }) - } -} - -function signPersonalMsg (msgData) { - log.debug('action - signPersonalMsg') - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - - log.debug(`actions calling background.signPersonalMessage`) - background.signPersonalMessage(msgData, (err, newState) => { - log.debug('signPersonalMessage called back') - dispatch(actions.updateMetamaskState(newState)) - dispatch(actions.hideLoadingIndication()) - - if (err) log.error(err) - if (err) return dispatch(actions.displayWarning(err.message)) - - dispatch(actions.completedTx(msgData.metamaskId)) - }) - } -} - -function signTx (txData) { - return (dispatch) => { - global.ethQuery.sendTransaction(txData, (err, data) => { - dispatch(actions.hideLoadingIndication()) - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.hideWarning()) - }) - dispatch(this.showConfTxPage()) - } -} - -function sendTx (txData) { - log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`) - return (dispatch) => { - log.debug(`actions calling background.approveTransaction`) - background.approveTransaction(txData.id, (err) => { - if (err) { - dispatch(actions.txError(err)) - return log.error(err.message) - } - dispatch(actions.completedTx(txData.id)) - }) - } -} - -function updateAndApproveTx (txData) { - log.info('actions: updateAndApproveTx: ' + JSON.stringify(txData)) - return (dispatch) => { - log.debug(`actions calling background.updateAndApproveTx`) - background.updateAndApproveTransaction(txData, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - dispatch(actions.txError(err)) - return log.error(err.message) - } - dispatch(actions.completedTx(txData.id)) - }) - } -} - -function completedTx (id) { - return { - type: actions.COMPLETED_TX, - value: id, - } -} - -function txError (err) { - return { - type: actions.TRANSACTION_ERROR, - message: err.message, - } -} - -function cancelMsg (msgData) { - log.debug(`background.cancelMessage`) - background.cancelMessage(msgData.id) - return actions.completedTx(msgData.id) -} - -function cancelPersonalMsg (msgData) { - const id = msgData.id - background.cancelPersonalMessage(id) - return actions.completedTx(id) -} - -function cancelTx (txData) { - log.debug(`background.cancelTransaction`) - background.cancelTransaction(txData.id) - return actions.completedTx(txData.id) -} - -// -// initialize screen -// - -function showCreateVault () { - return { - type: actions.SHOW_CREATE_VAULT, - } -} - -function showRestoreVault () { - return { - type: actions.SHOW_RESTORE_VAULT, - } -} - -function forgotPassword () { - return { - type: actions.FORGOT_PASSWORD, - } -} - -function showInitializeMenu () { - return { - type: actions.SHOW_INIT_MENU, - } -} - -function showImportPage () { - return { - type: actions.SHOW_IMPORT_PAGE, - } -} - -function createNewVaultInProgress () { - return { - type: actions.CREATE_NEW_VAULT_IN_PROGRESS, - } -} - -function showNewVaultSeed (seed) { - return { - type: actions.SHOW_NEW_VAULT_SEED, - value: seed, - } -} - -function backToUnlockView () { - return { - type: actions.BACK_TO_UNLOCK_VIEW, - } -} - -function showNewKeychain () { - return { - type: actions.SHOW_NEW_KEYCHAIN, - } -} - -// -// unlock screen -// - -function unlockInProgress () { - return { - type: actions.UNLOCK_IN_PROGRESS, - } -} - -function unlockFailed (message) { - return { - type: actions.UNLOCK_FAILED, - value: message, - } -} - -function unlockMetamask (account) { - return { - type: actions.UNLOCK_METAMASK, - value: account, - } -} - -function updateMetamaskState (newState) { - return { - type: actions.UPDATE_METAMASK_STATE, - value: newState, - } -} - -function lockMetamask () { - log.debug(`background.setLocked`) - return callBackgroundThenUpdate(background.setLocked) -} - -function setCurrentAccountTab (newTabName) { - log.debug(`background.setCurrentAccountTab: ${newTabName}`) - return callBackgroundThenUpdateNoSpinner(background.setCurrentAccountTab, newTabName) -} - -function showAccountDetail (address) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.setSelectedAddress`) - background.setSelectedAddress(address, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch({ - type: actions.SHOW_ACCOUNT_DETAIL, - value: address, - }) - }) - } -} - -function backToAccountDetail (address) { - return { - type: actions.BACK_TO_ACCOUNT_DETAIL, - value: address, - } -} - -function showAccountsPage () { - return { - type: actions.SHOW_ACCOUNTS_PAGE, - } -} - -function showConfTxPage (transForward = true) { - return { - type: actions.SHOW_CONF_TX_PAGE, - transForward: transForward, - } -} - -function nextTx () { - return { - type: actions.NEXT_TX, - } -} - -function viewPendingTx (txId) { - return { - type: actions.VIEW_PENDING_TX, - value: txId, - } -} - -function previousTx () { - return { - type: actions.PREVIOUS_TX, - } -} - -function showConfigPage (transitionForward = true) { - return { - type: actions.SHOW_CONFIG_PAGE, - value: transitionForward, - } -} - -function showAddTokenPage (transitionForward = true) { - return { - type: actions.SHOW_ADD_TOKEN_PAGE, - value: transitionForward, - } -} - -function addToken (address, symbol, decimals) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - background.addToken(address, symbol, decimals, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - setTimeout(() => { - dispatch(actions.goHome()) - }, 250) - }) - } -} - -function goBackToInitView () { - return { - type: actions.BACK_TO_INIT_MENU, - } -} - -// -// notice -// - -function markNoticeRead (notice) { - return (dispatch) => { - dispatch(this.showLoadingIndication()) - log.debug(`background.markNoticeRead`) - background.markNoticeRead(notice, (err, notice) => { - dispatch(this.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err)) - } - if (notice) { - return dispatch(actions.showNotice(notice)) - } else { - dispatch(this.clearNotices()) - return { - type: actions.SHOW_ACCOUNTS_PAGE, - } - } - }) - } -} - -function showNotice (notice) { - return { - type: actions.SHOW_NOTICE, - value: notice, - } -} - -function clearNotices () { - return { - type: actions.CLEAR_NOTICES, - } -} - -function markAccountsFound () { - log.debug(`background.markAccountsFound`) - return callBackgroundThenUpdate(background.markAccountsFound) -} - -// -// config -// - -// default rpc target refers to localhost:8545 in this instance. -function setDefaultRpcTarget (rpcList) { - log.debug(`background.setDefaultRpcTarget`) - return (dispatch) => { - background.setDefaultRpc((err, result) => { - if (err) { - log.error(err) - return dispatch(self.displayWarning('Had a problem changing networks.')) - } - }) - } -} - -function setRpcTarget (newRpc) { - log.debug(`background.setRpcTarget`) - return (dispatch) => { - background.setCustomRpc(newRpc, (err, result) => { - if (err) { - log.error(err) - return dispatch(self.displayWarning('Had a problem changing networks!')) - } - }) - } -} - -// Calls the addressBookController to add a new address. -function addToAddressBook (recipient, nickname) { - log.debug(`background.addToAddressBook`) - return (dispatch) => { - background.setAddressBook(recipient, nickname, (err, result) => { - if (err) { - log.error(err) - return dispatch(self.displayWarning('Address book failed to update')) - } - }) - } -} - -function setProviderType (type) { - log.debug(`background.setProviderType`) - background.setProviderType(type) - return { - type: actions.SET_PROVIDER_TYPE, - value: type, - } -} - -function useEtherscanProvider () { - log.debug(`background.useEtherscanProvider`) - background.useEtherscanProvider() - return { - type: actions.USE_ETHERSCAN_PROVIDER, - } -} - -function showLoadingIndication (message) { - return { - type: actions.SHOW_LOADING, - value: message, - } -} - -function hideLoadingIndication () { - return { - type: actions.HIDE_LOADING, - } -} - -function showSubLoadingIndication () { - return { - type: actions.SHOW_SUB_LOADING_INDICATION, - } -} - -function hideSubLoadingIndication () { - return { - type: actions.HIDE_SUB_LOADING_INDICATION, - } -} - -function displayWarning (text) { - return { - type: actions.DISPLAY_WARNING, - value: text, - } -} - -function hideWarning () { - return { - type: actions.HIDE_WARNING, - } -} - -function requestExportAccount () { - return { - type: actions.REQUEST_ACCOUNT_EXPORT, - } -} - -function exportAccount (password, address) { - var self = this - - return function (dispatch) { - dispatch(self.showLoadingIndication()) - - log.debug(`background.submitPassword`) - background.submitPassword(password, function (err) { - if (err) { - log.error('Error in submiting password.') - dispatch(self.hideLoadingIndication()) - return dispatch(self.displayWarning('Incorrect Password.')) - } - log.debug(`background.exportAccount`) - background.exportAccount(address, function (err, result) { - dispatch(self.hideLoadingIndication()) - - if (err) { - log.error(err) - return dispatch(self.displayWarning('Had a problem exporting the account.')) - } - - dispatch(self.showPrivateKey(result)) - }) - }) - } -} - -function showPrivateKey (key) { - return { - type: actions.SHOW_PRIVATE_KEY, - value: key, - } -} - -function saveAccountLabel (account, label) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - log.debug(`background.saveAccountLabel`) - background.saveAccountLabel(account, label, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch({ - type: actions.SAVE_ACCOUNT_LABEL, - value: { account, label }, - }) - }) - } -} - -function showSendPage () { - return { - type: actions.SHOW_SEND_PAGE, - } -} - -function buyEth (opts) { - return (dispatch) => { - const url = getBuyEthUrl(opts) - global.platform.openWindow({ url }) - dispatch({ - type: actions.BUY_ETH, - }) - } -} - -function buyEthView (address) { - return { - type: actions.BUY_ETH_VIEW, - value: address, - } -} - -function coinBaseSubview () { - return { - type: actions.COINBASE_SUBVIEW, - } -} - -function pairUpdate (coin) { - return (dispatch) => { - dispatch(actions.showSubLoadingIndication()) - dispatch(actions.hideWarning()) - shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { - dispatch(actions.hideSubLoadingIndication()) - dispatch({ - type: actions.PAIR_UPDATE, - value: { - marketinfo: mktResponse, - }, - }) - }) - } -} - -function shapeShiftSubview (network) { - var pair = 'btc_eth' - - return (dispatch) => { - dispatch(actions.showSubLoadingIndication()) - shapeShiftRequest('marketinfo', {pair}, (mktResponse) => { - shapeShiftRequest('getcoins', {}, (response) => { - dispatch(actions.hideSubLoadingIndication()) - if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) - dispatch({ - type: actions.SHAPESHIFT_SUBVIEW, - value: { - marketinfo: mktResponse, - coinOptions: response, - }, - }) - }) - }) - } -} - -function coinShiftRquest (data, marketData) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - shapeShiftRequest('shift', { method: 'POST', data}, (response) => { - dispatch(actions.hideLoadingIndication()) - if (response.error) return dispatch(actions.displayWarning(response.error)) - var message = ` - Deposit your ${response.depositType} to the address bellow:` - log.debug(`background.createShapeShiftTx`) - background.createShapeShiftTx(response.deposit, response.depositType) - dispatch(actions.showQrView(response.deposit, [message].concat(marketData))) - }) - } -} - -function showQrView (data, message) { - return { - type: actions.SHOW_QR_VIEW, - value: { - message: message, - data: data, - }, - } -} -function reshowQrCode (data, coin) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { - if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) - - var message = [ - `Deposit your ${coin} to the address bellow:`, - `Deposit Limit: ${mktResponse.limit}`, - `Deposit Minimum:${mktResponse.minimum}`, - ] - - dispatch(actions.hideLoadingIndication()) - return dispatch(actions.showQrView(data, message)) - }) - } -} - -function shapeShiftRequest (query, options, cb) { - var queryResponse, method - !options ? options = {} : null - options.method ? method = options.method : method = 'GET' - - var requestListner = function (request) { - queryResponse = JSON.parse(this.responseText) - cb ? cb(queryResponse) : null - return queryResponse - } - - var shapShiftReq = new XMLHttpRequest() - shapShiftReq.addEventListener('load', requestListner) - shapShiftReq.open(method, `https://shapeshift.io/${query}/${options.pair ? options.pair : ''}`, true) - - if (options.method === 'POST') { - var jsonObj = JSON.stringify(options.data) - shapShiftReq.setRequestHeader('Content-Type', 'application/json') - return shapShiftReq.send(jsonObj) - } else { - return shapShiftReq.send() - } -} - -// Call Background Then Update -// -// A function generator for a common pattern wherein: -// We show loading indication. -// We call a background method. -// We hide loading indication. -// If it errored, we show a warning. -// If it didn't, we update the state. -function callBackgroundThenUpdateNoSpinner (method, ...args) { - return (dispatch) => { - method.call(background, ...args, (err) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - forceUpdateMetamaskState(dispatch) - }) - } -} - -function callBackgroundThenUpdate (method, ...args) { - return (dispatch) => { - dispatch(actions.showLoadingIndication()) - method.call(background, ...args, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - forceUpdateMetamaskState(dispatch) - }) - } -} - -function forceUpdateMetamaskState (dispatch) { - log.debug(`background.getState`) - background.getState((err, newState) => { - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - dispatch(actions.updateMetamaskState(newState)) - }) -} diff --git a/responsive-ui/app/add-token.js b/responsive-ui/app/add-token.js deleted file mode 100644 index b303b5c0d..000000000 --- a/responsive-ui/app/add-token.js +++ /dev/null @@ -1,219 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') - -const ethUtil = require('ethereumjs-util') -const abi = require('human-standard-token-abi') -const Eth = require('ethjs-query') -const EthContract = require('ethjs-contract') - -const emptyAddr = '0x0000000000000000000000000000000000000000' - -module.exports = connect(mapStateToProps)(AddTokenScreen) - -function mapStateToProps (state) { - return { - } -} - -inherits(AddTokenScreen, Component) -function AddTokenScreen () { - this.state = { - warning: null, - address: null, - symbol: 'TOKEN', - decimals: 18, - } - Component.call(this) -} - -AddTokenScreen.prototype.render = function () { - const state = this.state - const props = this.props - const { warning, symbol, decimals } = state - - return ( - h('.flex-column.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: (event) => { - props.dispatch(actions.goHome()) - }, - }), - h('h2.page-subtitle', 'Add Token'), - ]), - - h('.error', { - style: { - display: warning ? 'block' : 'none', - padding: '0 20px', - textAlign: 'center', - }, - }, warning), - - // conf view - h('.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-space-around', { - style: { - padding: '20px', - }, - }, [ - - h('div', [ - h('span', { - style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Token Address'), - ]), - - h('section.flex-row.flex-center', [ - h('input#token-address', { - name: 'address', - placeholder: 'Token Address', - onChange: this.tokenAddressDidChange.bind(this), - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - }), - ]), - - h('div', [ - h('span', { - style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Token Sybmol'), - ]), - - h('div', { style: {display: 'flex'} }, [ - h('input#token_symbol', { - placeholder: `Like "ETH"`, - value: symbol, - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - onChange: (event) => { - var element = event.target - var symbol = element.value - this.setState({ symbol }) - }, - }), - ]), - - h('div', [ - h('span', { - style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Decimals of Precision'), - ]), - - h('div', { style: {display: 'flex'} }, [ - h('input#token_decimals', { - value: decimals, - type: 'number', - min: 0, - max: 36, - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - onChange: (event) => { - var element = event.target - var decimals = element.value.trim() - this.setState({ decimals }) - }, - }), - ]), - - h('button', { - style: { - alignSelf: 'center', - }, - onClick: (event) => { - const valid = this.validateInputs() - if (!valid) return - - const { address, symbol, decimals } = this.state - this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) - }, - }, 'Add'), - ]), - ]), - ]) - ) -} - -AddTokenScreen.prototype.componentWillMount = function () { - if (typeof global.ethereumProvider === 'undefined') return - - this.eth = new Eth(global.ethereumProvider) - this.contract = new EthContract(this.eth) - this.TokenContract = this.contract(abi) -} - -AddTokenScreen.prototype.tokenAddressDidChange = function (event) { - const el = event.target - const address = el.value.trim() - if (ethUtil.isValidAddress(address) && address !== emptyAddr) { - this.setState({ address }) - this.attemptToAutoFillTokenParams(address) - } -} - -AddTokenScreen.prototype.validateInputs = function () { - let msg = '' - const state = this.state - const { address, symbol, decimals } = state - - const validAddress = ethUtil.isValidAddress(address) - if (!validAddress) { - msg += 'Address is invalid. ' - } - - const validDecimals = decimals >= 0 && decimals < 36 - if (!validDecimals) { - msg += 'Decimals must be at least 0, and not over 36. ' - } - - const symbolLen = symbol.trim().length - const validSymbol = symbolLen > 0 && symbolLen < 10 - if (!validSymbol) { - msg += 'Symbol must be between 0 and 10 characters.' - } - - const isValid = validAddress && validDecimals - - if (!isValid) { - this.setState({ - warning: msg, - }) - } else { - this.setState({ warning: null }) - } - - return isValid -} - -AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) { - const contract = this.TokenContract.at(address) - - const results = await Promise.all([ - contract.symbol(), - contract.decimals(), - ]) - - const [ symbol, decimals ] = results - if (symbol && decimals) { - console.log('SETTING SYMBOL AND DECIMALS', { symbol, decimals }) - this.setState({ symbol: symbol[0], decimals: decimals[0].toString() }) - } -} - diff --git a/responsive-ui/app/app.js b/responsive-ui/app/app.js deleted file mode 100644 index d1a20f079..000000000 --- a/responsive-ui/app/app.js +++ /dev/null @@ -1,591 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('./actions') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') -// init -const InitializeMenuScreen = require('./first-time/init-menu') -const NewKeyChainScreen = require('./new-keychain') -// unlock -const UnlockScreen = require('./unlock') -// accounts -const AccountDetailScreen = require('./account-detail') -const SendTransactionScreen = require('./send') -const ConfirmTxScreen = require('./conf-tx') -// notice -const NoticeScreen = require('./components/notice') -const generateLostAccountsNotice = require('../lib/lost-accounts-notice') -// other views -const ConfigScreen = require('./config') -const AddTokenScreen = require('./add-token') -const Import = require('./accounts/import') -const InfoScreen = require('./info') -const Loading = require('./components/loading') -const SandwichExpando = require('sandwich-expando') -const Dropdown = require('./components/dropdown').Dropdown -const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem -const NetworkIndicator = require('./components/network') -const BuyView = require('./components/buy-button-subview') -const QrView = require('./components/qr-code') -const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete') -const HDRestoreVaultScreen = require('./keychains/hd/restore-vault') -const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation') - -module.exports = connect(mapStateToProps)(App) - -inherits(App, Component) -function App () { Component.call(this) } - -function mapStateToProps (state) { - return { - // state from plugin - isLoading: state.appState.isLoading, - loadingMessage: state.appState.loadingMessage, - noActiveNotices: state.metamask.noActiveNotices, - isInitialized: state.metamask.isInitialized, - isUnlocked: state.metamask.isUnlocked, - currentView: state.appState.currentView, - activeAddress: state.appState.activeAddress, - transForward: state.appState.transForward, - seedWords: state.metamask.seedWords, - unapprovedTxs: state.metamask.unapprovedTxs, - unapprovedMsgs: state.metamask.unapprovedMsgs, - menuOpen: state.appState.menuOpen, - network: state.metamask.network, - provider: state.metamask.provider, - forgottenPassword: state.appState.forgottenPassword, - lastUnreadNotice: state.metamask.lastUnreadNotice, - lostAccounts: state.metamask.lostAccounts, - frequentRpcList: state.metamask.frequentRpcList || [], - } -} - -App.prototype.render = function () { - var props = this.props - const { isLoading, loadingMessage, transForward, network } = props - const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config' - const loadMessage = loadingMessage || isLoadingNetwork ? - `Connecting to ${this.getNetworkName()}` : null - - log.debug('Main ui render function') - - return ( - - h('.flex-column.flex-grow.full-height', { - style: { - // Windows was showing a vertical scroll bar: - overflow: 'hidden', - position: 'relative', - height: '100%', - alignItems: 'center', - }, - }, [ - - // app bar - this.renderAppBar(), - this.renderNetworkDropdown(), - this.renderDropdown(), - - h(Loading, { - isLoading: isLoading || isLoadingNetwork, - loadingMessage: loadMessage, - }), - - // panel content - h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), { - style: { - height: '100%', - maxWidth: '850px', - }, - }, [ - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.renderPrimary(), - ]), - ]), - ]) - ) -} - -App.prototype.renderAppBar = function () { - if (window.METAMASK_UI_TYPE === 'notification') { - return null - } - - const props = this.props - const state = this.state || {} - const isNetworkMenuOpen = state.isNetworkMenuOpen || false - - return ( - - h('div', { - style: { - width: '100%' - }, - }, [ - - h('.app-header.flex-row.flex-space-between', { - style: { - alignItems: 'center', - visibility: props.isUnlocked ? 'visible' : 'none', - background: props.isUnlocked ? 'white' : 'none', - height: '38px', - position: 'relative', - zIndex: 12, - }, - }, [ - - h('div.left-menu-section', { - style: { - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - }, - }, [ - - // mini logo - h('img', { - height: 24, - width: 24, - src: '/images/icon-128.png', - }), - - h(NetworkIndicator, { - network: this.props.network, - provider: this.props.provider, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen }) - }, - }), - ]), - - // metamask name - props.isUnlocked && h('h1', { - style: { - position: 'relative', - left: '9px', - }, - }, 'MetaMask'), - - props.isUnlocked && h('div', { - style: { - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - }, - }, [ - - // hamburger - props.isUnlocked && h(SandwichExpando, { - width: 16, - barHeight: 2, - padding: 0, - isOpen: state.isMainMenuOpen, - color: 'rgb(247,146,30)', - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - this.setState({ isMainMenuOpen: !state.isMainMenuOpen }) - }, - }), - ]), - ]), - ]) - ) -} - -App.prototype.renderNetworkDropdown = function () { - const props = this.props - const { provider: { type: providerType, rpcTarget: activeNetwork } } = props - const rpcList = props.frequentRpcList - const state = this.state || {} - const isOpen = state.isNetworkMenuOpen - - return h(Dropdown, { - isOpen, - onClickOutside: (event) => { - this.setState({ isNetworkMenuOpen: !isOpen }) - }, - zIndex: 11, - style: { - position: 'absolute', - left: '2px', - top: '36px', - }, - innerStyle: {}, - }, [ - - h( - DropdownMenuItem, - { - closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), - onClick: () => props.dispatch(actions.setProviderType('mainnet')), - }, - [ - h('.menu-icon.diamond'), - 'Main Ethereum Network', - providerType === 'mainnet' ? h('.check', '✓') : null, - ] - ), - - h( - DropdownMenuItem, - { - closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), - onClick: () => props.dispatch(actions.setProviderType('ropsten')), - }, - [ - h('.menu-icon.red-dot'), - 'Ropsten Test Network', - providerType === 'ropsten' ? h('.check', '✓') : null, - ] - ), - - h( - DropdownMenuItem, - { - closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), - onClick: () => props.dispatch(actions.setProviderType('kovan')), - }, - [ - h('.menu-icon.hollow-diamond'), - 'Kovan Test Network', - providerType === 'kovan' ? h('.check', '✓') : null, - ] - ), - - h( - DropdownMenuItem, - { - closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), - onClick: () => props.dispatch(actions.setProviderType('rinkeby')), - }, - [ - h('.menu-icon.golden-square'), - 'Rinkeby Test Network', - providerType === 'rinkeby' ? h('.check', '✓') : null, - ] - ), - - h( - DropdownMenuItem, - { - closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), - onClick: () => props.dispatch(actions.setDefaultRpcTarget(rpcList)), - }, - [ - h('i.fa.fa-question-circle.fa-lg.menu-icon'), - 'Localhost 8545', - activeNetwork === 'http://localhost:8545' ? h('.check', '✓') : null, - ] - ), - - this.renderCustomOption(props.provider), - this.renderCommonRpc(rpcList, props.provider), - - h( - DropdownMenuItem, - { - closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), - onClick: () => this.props.dispatch(actions.showConfigPage()), - }, - [ - h('i.fa.fa-question-circle.fa-lg.menu-icon'), - 'Custom RPC', - activeNetwork === 'custom' ? h('.check', '✓') : null, - ] - ), - - ]) -} - -App.prototype.renderDropdown = function () { - const state = this.state || {} - const isOpen = state.isMainMenuOpen - - return h(Dropdown, { - isOpen: isOpen, - zIndex: 11, - onClickOutside: (event) => { - this.setState({ isMainMenuOpen: !isOpen }) - }, - style: { - position: 'absolute', - right: '2px', - top: '38px', - }, - innerStyle: {}, - }, [ - h(DropdownMenuItem, { - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - onClick: () => { this.props.dispatch(actions.showConfigPage()) }, - }, 'Settings'), - - h(DropdownMenuItem, { - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - onClick: () => { this.props.dispatch(actions.showImportPage()) }, - }, 'Import Account'), - - h(DropdownMenuItem, { - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - onClick: () => { this.props.dispatch(actions.lockMetamask()) }, - }, 'Lock'), - - h(DropdownMenuItem, { - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - onClick: () => { this.props.dispatch(actions.showInfoPage()) }, - }, 'Info/Help'), - ]) -} - -App.prototype.renderBackButton = function (style, justArrow = false) { - var props = this.props - return ( - h('.flex-row', { - key: 'leftArrow', - style: style, - onClick: () => props.dispatch(actions.goBackToInitView()), - }, [ - h('i.fa.fa-arrow-left.cursor-pointer'), - justArrow ? null : h('div.cursor-pointer', { - style: { - marginLeft: '3px', - }, - onClick: () => props.dispatch(actions.goBackToInitView()), - }, 'BACK'), - ]) - ) -} - -App.prototype.renderPrimary = function () { - log.debug('rendering primary') - var props = this.props - - // notices - if (!props.noActiveNotices) { - log.debug('rendering notice screen for unread notices.') - return h(NoticeScreen, { - notice: props.lastUnreadNotice, - key: 'NoticeScreen', - onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)), - }) - } else if (props.lostAccounts && props.lostAccounts.length > 0) { - log.debug('rendering notice screen for lost accounts view.') - return h(NoticeScreen, { - notice: generateLostAccountsNotice(props.lostAccounts), - key: 'LostAccountsNotice', - onConfirm: () => props.dispatch(actions.markAccountsFound()), - }) - } - - if (props.seedWords) { - log.debug('rendering seed words') - return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'}) - } - - // show initialize screen - if (!props.isInitialized || props.forgottenPassword) { - // show current view - log.debug('rendering an initialize screen') - switch (props.currentView.name) { - - case 'restoreVault': - log.debug('rendering restore vault screen') - return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) - - default: - log.debug('rendering menu screen') - return h(InitializeMenuScreen, {key: 'menuScreenInit'}) - } - } - - // show unlock screen - if (!props.isUnlocked) { - switch (props.currentView.name) { - - case 'restoreVault': - log.debug('rendering restore vault screen') - return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) - - case 'config': - log.debug('rendering config screen from unlock screen.') - return h(ConfigScreen, {key: 'config'}) - - default: - log.debug('rendering locked screen') - return h(UnlockScreen, {key: 'locked'}) - } - } - - // show current view - switch (props.currentView.name) { - - case 'accountDetail': - log.debug('rendering account detail screen') - return h(AccountDetailScreen, {key: 'account-detail'}) - - case 'sendTransaction': - log.debug('rendering send tx screen') - return h(SendTransactionScreen, {key: 'send-transaction'}) - - case 'newKeychain': - log.debug('rendering new keychain screen') - return h(NewKeyChainScreen, {key: 'new-keychain'}) - - case 'confTx': - log.debug('rendering confirm tx screen') - return h(ConfirmTxScreen, {key: 'confirm-tx'}) - - case 'add-token': - log.debug('rendering add-token screen from unlock screen.') - return h(AddTokenScreen, {key: 'add-token'}) - - case 'config': - log.debug('rendering config screen') - return h(ConfigScreen, {key: 'config'}) - - case 'import-menu': - log.debug('rendering import screen') - return h(Import, {key: 'import-menu'}) - - case 'reveal-seed-conf': - log.debug('rendering reveal seed confirmation screen') - return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'}) - - case 'info': - log.debug('rendering info screen') - return h(InfoScreen, {key: 'info'}) - - case 'buyEth': - log.debug('rendering buy ether screen') - return h(BuyView, {key: 'buyEthView'}) - - case 'qr': - log.debug('rendering show qr screen') - return h('div', { - style: { - position: 'absolute', - height: '100%', - top: '0px', - left: '0px', - }, - }, [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { - onClick: () => props.dispatch(actions.backToAccountDetail(props.activeAddress)), - style: { - marginLeft: '10px', - marginTop: '50px', - }, - }), - h('div', { - style: { - position: 'absolute', - left: '44px', - width: '285px', - }, - }, [ - h(QrView, {key: 'qr'}), - ]), - ]) - - default: - log.debug('rendering default, account detail screen') - return h(AccountDetailScreen, {key: 'account-detail'}) - } -} - -App.prototype.toggleMetamaskActive = function () { - if (!this.props.isUnlocked) { - // currently inactive: redirect to password box - var passwordBox = document.querySelector('input[type=password]') - if (!passwordBox) return - passwordBox.focus() - } else { - // currently active: deactivate - this.props.dispatch(actions.lockMetamask(false)) - } -} - -App.prototype.renderCustomOption = function (provider) { - const { rpcTarget, type } = provider - if (type !== 'rpc') return null - - // Concatenate long URLs - let label = rpcTarget - if (rpcTarget.length > 31) { - label = label.substr(0, 34) + '...' - } - - switch (rpcTarget) { - - case 'http://localhost:8545': - return null - - default: - return h( - DropdownMenuItem, - { - key: rpcTarget, - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - }, - [ - h('i.fa.fa-question-circle.fa-lg.menu-icon'), - label, - h('.check', '✓'), - ] - ) - } -} - -App.prototype.getNetworkName = function () { - const { provider } = this.props - const providerName = provider.type - - let name - - if (providerName === 'mainnet') { - name = 'Main Ethereum Network' - } else if (providerName === 'ropsten') { - name = 'Ropsten Test Network' - } else if (providerName === 'kovan') { - name = 'Kovan Test Network' - } else if (providerName === 'rinkeby') { - name = 'Rinkeby Test Network' - } else { - name = 'Unknown Private Network' - } - - return name -} - -App.prototype.renderCommonRpc = function (rpcList, provider) { - const { rpcTarget } = provider - const props = this.props - - return rpcList.map((rpc) => { - if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { - return null - } else { - return h( - DropdownMenuItem, - { - key: rpc, - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setRpcTarget(rpc)), - }, - [ - h('i.fa.fa-question-circle.fa-lg.menu-icon'), - rpc, - h('.check', '✓'), - ] - ) - } - }) -} diff --git a/responsive-ui/app/components/account-dropdowns.js b/responsive-ui/app/components/account-dropdowns.js deleted file mode 100644 index d1d319477..000000000 --- a/responsive-ui/app/components/account-dropdowns.js +++ /dev/null @@ -1,227 +0,0 @@ -const Component = require('react').Component -const PropTypes = require('react').PropTypes -const h = require('react-hyperscript') -const actions = require('../actions') -const genAccountLink = require('../../lib/account-link.js') -const connect = require('react-redux').connect -const Dropdown = require('./dropdown').Dropdown -const DropdownMenuItem = require('./dropdown').DropdownMenuItem -const Identicon = require('./identicon') -const ethUtil = require('ethereumjs-util') -const copyToClipboard = require('copy-to-clipboard') - -class AccountDropdowns extends Component { - constructor (props) { - super(props) - this.state = { - accountSelectorActive: false, - optionsMenuActive: false, - } - } - - renderAccounts () { - const { identities, selected } = this.props - - return Object.keys(identities).map((key) => { - const identity = identities[key] - const isSelected = identity.address === selected - - return h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => { - this.props.actions.showAccountDetail(identity.address) - }, - }, - [ - h( - Identicon, - { - address: identity.address, - diameter: 16, - }, - ), - h('span', { style: { marginLeft: '10px' } }, identity.name || ''), - h('span', { style: { marginLeft: '10px' } }, isSelected ? h('.check', '✓') : null), - ] - ) - }) - } - - renderAccountSelector () { - const { actions } = this.props - const { accountSelectorActive } = this.state - - return h( - Dropdown, - { - style: { - marginLeft: '-125px', - minWidth: '180px', - }, - isOpen: accountSelectorActive, - onClickOutside: () => { this.setState({ accountSelectorActive: false }) }, - }, - [ - ...this.renderAccounts(), - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => actions.addNewAccount(), - }, - [ - h( - Identicon, - { - diameter: 16, - }, - ), - h('span', { style: { marginLeft: '10px' } }, 'Create Account'), - ], - ), - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => actions.showImportPage(), - }, - [ - h( - Identicon, - { - diameter: 16, - }, - ), - h('span', { style: { marginLeft: '10px' } }, 'Import Account'), - ] - ), - ] - ) - } - - renderAccountOptions () { - const { actions } = this.props - const { optionsMenuActive } = this.state - - return h( - Dropdown, - { - style: { - marginLeft: '-162px', - minWidth: '180px', - }, - isOpen: optionsMenuActive, - onClickOutside: () => { this.setState({ optionsMenuActive: false }) }, - }, - [ - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => actions.showConfigPage(), - }, - 'Account Settings', - ), - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => { - const { selected, network } = this.props - const url = genAccountLink(selected, network) - global.platform.openWindow({ url }) - }, - }, - 'View account on Etherscan', - ), - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => { - const { selected } = this.props - const checkSumAddress = selected && ethUtil.toChecksumAddress(selected) - copyToClipboard(checkSumAddress) - }, - }, - 'Copy Address to clipboard', - ), - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => { - actions.requestAccountExport() - }, - }, - 'Export Private Key', - ), - ] - ) - } - - render () { - const { style } = this.props - const { optionsMenuActive, accountSelectorActive } = this.state - - return h( - 'span', - { - style: style, - }, - [ - h( - 'i.fa.fa-angle-down', - { - style: {}, - onClick: (event) => { - event.stopPropagation() - this.setState({ - accountSelectorActive: !accountSelectorActive, - optionsMenuActive: false, - }) - }, - }, - this.renderAccountSelector(), - ), - h( - 'i.fa.fa-ellipsis-h', - { - style: { 'marginLeft': '10px'}, - onClick: (event) => { - event.stopPropagation() - this.setState({ - accountSelectorActive: false, - optionsMenuActive: !optionsMenuActive, - }) - }, - }, - this.renderAccountOptions() - ), - ] - ) - } -} - -AccountDropdowns.propTypes = { - identities: PropTypes.objectOf(PropTypes.object), - selected: PropTypes.string, -} - -const mapDispatchToProps = (dispatch) => { - return { - actions: { - showConfigPage: () => dispatch(actions.showConfigPage()), - requestAccountExport: () => dispatch(actions.requestExportAccount()), - showAccountDetail: (address) => dispatch(actions.showAccountDetail(address)), - addNewAccount: () => dispatch(actions.addNewAccount()), - showImportPage: () => dispatch(actions.showImportPage()), - }, - } -} - -module.exports = { - AccountDropdowns: connect(null, mapDispatchToProps)(AccountDropdowns), -} diff --git a/responsive-ui/app/components/account-export.js b/responsive-ui/app/components/account-export.js deleted file mode 100644 index 394d878f7..000000000 --- a/responsive-ui/app/components/account-export.js +++ /dev/null @@ -1,122 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const copyToClipboard = require('copy-to-clipboard') -const actions = require('../actions') -const ethUtil = require('ethereumjs-util') -const connect = require('react-redux').connect - -module.exports = connect(mapStateToProps)(ExportAccountView) - -inherits(ExportAccountView, Component) -function ExportAccountView () { - Component.call(this) -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -ExportAccountView.prototype.render = function () { - var state = this.props - var accountDetail = state.accountDetail - - if (!accountDetail) return h('div') - var accountExport = accountDetail.accountExport - - var notExporting = accountExport === 'none' - var exportRequested = accountExport === 'requested' - var accountExported = accountExport === 'completed' - - if (notExporting) return h('div') - - if (exportRequested) { - var warning = `Export private keys at your own risk.` - return ( - h('div', { - style: { - display: 'inline-block', - textAlign: 'center', - }, - }, - [ - h('div', { - key: 'exporting', - style: { - margin: '0 20px', - }, - }, [ - h('p.error', warning), - h('input#exportAccount.sizing-input', { - type: 'password', - placeholder: 'confirm password', - onKeyPress: this.onExportKeyPress.bind(this), - style: { - position: 'relative', - top: '1.5px', - marginBottom: '7px', - }, - }), - ]), - h('div', { - key: 'buttons', - style: { - margin: '0 20px', - }, - }, - [ - h('button', { - onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }), - style: { - marginRight: '10px', - }, - }, 'Submit'), - h('button', { - onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), - }, 'Cancel'), - ]), - (this.props.warning) && ( - h('span.error', { - style: { - margin: '20px', - }, - }, this.props.warning.split('-')) - ), - ]) - ) - } - - if (accountExported) { - return h('div.privateKey', { - style: { - margin: '0 20px', - }, - }, [ - h('label', 'Your private key (click to copy):'), - h('p.error.cursor-pointer', { - style: { - textOverflow: 'ellipsis', - overflow: 'hidden', - webkitUserSelect: 'text', - width: '100%', - }, - onClick: function (event) { - copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey)) - }, - }, ethUtil.stripHexPrefix(accountDetail.privateKey)), - h('button', { - onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), - }, 'Done'), - ]) - } -} - -ExportAccountView.prototype.onExportKeyPress = function (event) { - if (event.key !== 'Enter') return - event.preventDefault() - - var input = document.getElementById('exportAccount').value - this.props.dispatch(actions.exportAccount(input, this.props.address)) -} diff --git a/responsive-ui/app/components/account-panel.js b/responsive-ui/app/components/account-panel.js deleted file mode 100644 index abaaf8163..000000000 --- a/responsive-ui/app/components/account-panel.js +++ /dev/null @@ -1,86 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const Identicon = require('./identicon') -const formatBalance = require('../util').formatBalance -const addressSummary = require('../util').addressSummary - -module.exports = AccountPanel - - -inherits(AccountPanel, Component) -function AccountPanel () { - Component.call(this) -} - -AccountPanel.prototype.render = function () { - var state = this.props - var identity = state.identity || {} - var account = state.account || {} - var isFauceting = state.isFauceting - - var panelState = { - key: `accountPanel${identity.address}`, - identiconKey: identity.address, - identiconLabel: identity.name || '', - attributes: [ - { - key: 'ADDRESS', - value: addressSummary(identity.address), - }, - balanceOrFaucetingIndication(account, isFauceting), - ], - } - - return ( - - h('.identity-panel.flex-row.flex-space-between', { - style: { - flex: '1 0 auto', - cursor: panelState.onClick ? 'pointer' : undefined, - }, - onClick: panelState.onClick, - }, [ - - // account identicon - h('.identicon-wrapper.flex-column.select-none', [ - h(Identicon, { - address: panelState.identiconKey, - imageify: state.imageifyIdenticons, - }), - h('span.font-small', panelState.identiconLabel.substring(0, 7) + '...'), - ]), - - // account address, balance - h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [ - - panelState.attributes.map((attr) => { - return h('.flex-row.flex-space-between', { - key: '' + Math.round(Math.random() * 1000000), - }, [ - h('label.font-small.no-select', attr.key), - h('span.font-small', attr.value), - ]) - }), - ]), - - ]) - - ) -} - -function balanceOrFaucetingIndication (account, isFauceting) { - // Temporarily deactivating isFauceting indication - // because it shows fauceting for empty restored accounts. - if (/* isFauceting*/ false) { - return { - key: 'Account is auto-funding.', - value: 'Please wait.', - } - } else { - return { - key: 'BALANCE', - value: formatBalance(account.balance), - } - } -} diff --git a/responsive-ui/app/components/balance.js b/responsive-ui/app/components/balance.js deleted file mode 100644 index 57ca84564..000000000 --- a/responsive-ui/app/components/balance.js +++ /dev/null @@ -1,89 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const formatBalance = require('../util').formatBalance -const generateBalanceObject = require('../util').generateBalanceObject -const Tooltip = require('./tooltip.js') -const FiatValue = require('./fiat-value.js') - -module.exports = EthBalanceComponent - -inherits(EthBalanceComponent, Component) -function EthBalanceComponent () { - Component.call(this) -} - -EthBalanceComponent.prototype.render = function () { - var props = this.props - let { value } = props - var style = props.style - var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true - value = value ? formatBalance(value, 6, needsParse) : '...' - var width = props.width - - return ( - - h('.ether-balance.ether-balance-amount', { - style: style, - }, [ - h('div', { - style: { - display: 'inline', - width: width, - }, - }, this.renderBalance(value)), - ]) - - ) -} -EthBalanceComponent.prototype.renderBalance = function (value) { - var props = this.props - if (value === 'None') return value - if (value === '...') return value - var balanceObj = generateBalanceObject(value, props.shorten ? 1 : 3) - var balance - var splitBalance = value.split(' ') - var ethNumber = splitBalance[0] - var ethSuffix = splitBalance[1] - const showFiat = 'showFiat' in props ? props.showFiat : true - - if (props.shorten) { - balance = balanceObj.shortBalance - } else { - balance = balanceObj.balance - } - - var label = balanceObj.label - - return ( - h(Tooltip, { - position: 'bottom', - title: `${ethNumber} ${ethSuffix}`, - }, h('div.flex-column', [ - h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('div', { - style: { - width: '100%', - textAlign: 'right', - }, - }, this.props.incoming ? `+${balance}` : balance), - h('div', { - style: { - color: ' #AEAEAE', - fontSize: '12px', - marginLeft: '5px', - }, - }, label), - ]), - - showFiat ? h(FiatValue, { value: props.value }) : null, - ])) - ) -} diff --git a/responsive-ui/app/components/binary-renderer.js b/responsive-ui/app/components/binary-renderer.js deleted file mode 100644 index 0b6a1f5c2..000000000 --- a/responsive-ui/app/components/binary-renderer.js +++ /dev/null @@ -1,46 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ethUtil = require('ethereumjs-util') -const extend = require('xtend') - -module.exports = BinaryRenderer - -inherits(BinaryRenderer, Component) -function BinaryRenderer () { - Component.call(this) -} - -BinaryRenderer.prototype.render = function () { - const props = this.props - const { value, style } = props - const text = this.hexToText(value) - - const defaultStyle = extend({ - width: '315px', - maxHeight: '210px', - resize: 'none', - border: 'none', - background: 'white', - padding: '3px', - }, style) - - return ( - h('textarea.font-small', { - readOnly: true, - style: defaultStyle, - defaultValue: text, - }) - ) -} - -BinaryRenderer.prototype.hexToText = function (hex) { - try { - const stripped = ethUtil.stripHexPrefix(hex) - const buff = Buffer.from(stripped, 'hex') - return buff.toString('utf8') - } catch (e) { - return hex - } -} - diff --git a/responsive-ui/app/components/bn-as-decimal-input.js b/responsive-ui/app/components/bn-as-decimal-input.js deleted file mode 100644 index f3ace4720..000000000 --- a/responsive-ui/app/components/bn-as-decimal-input.js +++ /dev/null @@ -1,174 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const extend = require('xtend') - -module.exports = BnAsDecimalInput - -inherits(BnAsDecimalInput, Component) -function BnAsDecimalInput () { - this.state = { invalid: null } - Component.call(this) -} - -/* Bn as Decimal Input - * - * A component for allowing easy, decimal editing - * of a passed in bn string value. - * - * On change, calls back its `onChange` function parameter - * and passes it an updated bn string. - */ - -BnAsDecimalInput.prototype.render = function () { - const props = this.props - const state = this.state - - const { value, scale, precision, onChange, min, max } = props - - const suffix = props.suffix - const style = props.style - const valueString = value.toString(10) - const newValue = this.downsize(valueString, scale, precision) - - return ( - h('.flex-column', [ - h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('input.hex-input', { - type: 'number', - step: 'any', - required: true, - min, - max, - style: extend({ - display: 'block', - textAlign: 'right', - backgroundColor: 'transparent', - border: '1px solid #bdbdbd', - - }, style), - value: newValue, - onBlur: (event) => { - this.updateValidity(event) - }, - onChange: (event) => { - this.updateValidity(event) - const value = (event.target.value === '') ? '' : event.target.value - - - const scaledNumber = this.upsize(value, scale, precision) - const precisionBN = new BN(scaledNumber, 10) - onChange(precisionBN, event.target.checkValidity()) - }, - onInvalid: (event) => { - const msg = this.constructWarning() - if (msg === state.invalid) { - return - } - this.setState({ invalid: msg }) - event.preventDefault() - return false - }, - }), - h('div', { - style: { - color: ' #AEAEAE', - fontSize: '12px', - marginLeft: '5px', - marginRight: '6px', - width: '20px', - }, - }, suffix), - ]), - - state.invalid ? h('span.error', { - style: { - position: 'absolute', - right: '0px', - textAlign: 'right', - transform: 'translateY(26px)', - padding: '3px', - background: 'rgba(255,255,255,0.85)', - zIndex: '1', - textTransform: 'capitalize', - border: '2px solid #E20202', - }, - }, state.invalid) : null, - ]) - ) -} - -BnAsDecimalInput.prototype.setValid = function (message) { - this.setState({ invalid: null }) -} - -BnAsDecimalInput.prototype.updateValidity = function (event) { - const target = event.target - const value = this.props.value - const newValue = target.value - - if (value === newValue) { - return - } - - const valid = target.checkValidity() - - if (valid) { - this.setState({ invalid: null }) - } -} - -BnAsDecimalInput.prototype.constructWarning = function () { - const { name, min, max } = this.props - let message = name ? name + ' ' : '' - - if (min && max) { - message += `must be greater than or equal to ${min} and less than or equal to ${max}.` - } else if (min) { - message += `must be greater than or equal to ${min}.` - } else if (max) { - message += `must be less than or equal to ${max}.` - } else { - message += 'Invalid input.' - } - - return message -} - - -BnAsDecimalInput.prototype.downsize = function (number, scale, precision) { - // if there is no scaling, simply return the number - if (scale === 0) { - return Number(number) - } else { - // if the scale is the same as the precision, account for this edge case. - var decimals = (scale === precision) ? -1 : scale - precision - return Number(number.slice(0, -scale) + '.' + number.slice(-scale, decimals)) - } -} - -BnAsDecimalInput.prototype.upsize = function (number, scale, precision) { - var stringArray = number.toString().split('.') - var decimalLength = stringArray[1] ? stringArray[1].length : 0 - var newString = stringArray[0] - - // If there is scaling and decimal parts exist, integrate them in. - if ((scale !== 0) && (decimalLength !== 0)) { - newString += stringArray[1].slice(0, precision) - } - - // Add 0s to account for the upscaling. - for (var i = decimalLength; i < scale; i++) { - newString += '0' - } - return newString -} diff --git a/responsive-ui/app/components/buy-button-subview.js b/responsive-ui/app/components/buy-button-subview.js deleted file mode 100644 index 87084f92d..000000000 --- a/responsive-ui/app/components/buy-button-subview.js +++ /dev/null @@ -1,197 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const connect = require('react-redux').connect -const actions = require('../actions') -const CoinbaseForm = require('./coinbase-form') -const ShapeshiftForm = require('./shapeshift-form') -const Loading = require('./loading') -const AccountPanel = require('./account-panel') -const RadioList = require('./custom-radio-list') - -module.exports = connect(mapStateToProps)(BuyButtonSubview) - -function mapStateToProps (state) { - return { - identity: state.appState.identity, - account: state.metamask.accounts[state.appState.buyView.buyAddress], - warning: state.appState.warning, - buyView: state.appState.buyView, - network: state.metamask.network, - provider: state.metamask.provider, - context: state.appState.currentView.context, - isSubLoading: state.appState.isSubLoading, - } -} - -inherits(BuyButtonSubview, Component) -function BuyButtonSubview () { - Component.call(this) -} - -BuyButtonSubview.prototype.render = function () { - const props = this.props - const isLoading = props.isSubLoading - - return ( - h('.buy-eth-section.flex-column', { - style: { - alignItems: 'center', - }, - }, [ - // back button - h('.flex-row', { - style: { - alignItems: 'center', - justifyContent: 'center', - }, - }, [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { - onClick: this.backButtonContext.bind(this), - style: { - position: 'absolute', - left: '10px', - }, - }), - h('h2.text-transform-uppercase.flex-center', { - style: { - width: '100vw', - background: 'rgb(235, 235, 235)', - color: 'rgb(174, 174, 174)', - paddingTop: '4px', - paddingBottom: '4px', - }, - }, 'Buy Eth'), - ]), - h('div', { - style: { - position: 'absolute', - top: '57vh', - left: '49vw', - }, - }, [ - h(Loading, {isLoading}), - ]), - h('div', { - style: { - width: '80%', - }, - }, [ - h(AccountPanel, { - showFullAddress: true, - identity: props.identity, - account: props.account, - }), - ]), - h('h3.text-transform-uppercase', { - style: { - paddingLeft: '15px', - fontFamily: 'Montserrat Light', - width: '100vw', - background: 'rgb(235, 235, 235)', - color: 'rgb(174, 174, 174)', - paddingTop: '4px', - paddingBottom: '4px', - }, - }, 'Select Service'), - h('.flex-row.selected-exchange', { - style: { - position: 'relative', - right: '35px', - marginTop: '20px', - marginBottom: '20px', - }, - }, [ - h(RadioList, { - defaultFocus: props.buyView.subview, - labels: [ - 'Coinbase', - 'ShapeShift', - ], - subtext: { - 'Coinbase': 'Crypto/FIAT (USA only)', - 'ShapeShift': 'Crypto', - }, - onClick: this.radioHandler.bind(this), - }), - ]), - h('h3.text-transform-uppercase', { - style: { - paddingLeft: '15px', - fontFamily: 'Montserrat Light', - width: '100vw', - background: 'rgb(235, 235, 235)', - color: 'rgb(174, 174, 174)', - paddingTop: '4px', - paddingBottom: '4px', - }, - }, props.buyView.subview), - this.formVersionSubview(), - ]) - ) -} - -BuyButtonSubview.prototype.formVersionSubview = function () { - const network = this.props.network - if (network === '1') { - if (this.props.buyView.formView.coinbase) { - return h(CoinbaseForm, this.props) - } else if (this.props.buyView.formView.shapeshift) { - return h(ShapeshiftForm, this.props) - } - } else { - return h('div.flex-column', { - style: { - alignItems: 'center', - margin: '50px', - }, - }, [ - h('h3.text-transform-uppercase', { - style: { - width: '225px', - marginBottom: '15px', - }, - }, 'In order to access this feature, please switch to the Main Network'), - ((network === '3') || (network === '4') || (network === '42')) ? h('h3.text-transform-uppercase', 'or go to the') : null, - (network === '3') ? h('button.text-transform-uppercase', { - onClick: () => this.props.dispatch(actions.buyEth({ network })), - style: { - marginTop: '15px', - }, - }, 'Ropsten Test Faucet') : null, - (network === '4') ? h('button.text-transform-uppercase', { - onClick: () => this.props.dispatch(actions.buyEth({ network })), - style: { - marginTop: '15px', - }, - }, 'Rinkeby Test Faucet') : null, - (network === '42') ? h('button.text-transform-uppercase', { - onClick: () => this.props.dispatch(actions.buyEth({ network })), - style: { - marginTop: '15px', - }, - }, 'Kovan Test Faucet') : null, - ]) - } -} - -BuyButtonSubview.prototype.navigateTo = function (url) { - global.platform.openWindow({ url }) -} - -BuyButtonSubview.prototype.backButtonContext = function () { - if (this.props.context === 'confTx') { - this.props.dispatch(actions.showConfTxPage(false)) - } else { - this.props.dispatch(actions.goHome()) - } -} - -BuyButtonSubview.prototype.radioHandler = function (event) { - switch (event.target.title) { - case 'Coinbase': - return this.props.dispatch(actions.coinBaseSubview()) - case 'ShapeShift': - return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type)) - } -} diff --git a/responsive-ui/app/components/coinbase-form.js b/responsive-ui/app/components/coinbase-form.js deleted file mode 100644 index f44d86045..000000000 --- a/responsive-ui/app/components/coinbase-form.js +++ /dev/null @@ -1,63 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const connect = require('react-redux').connect -const actions = require('../actions') - -module.exports = connect(mapStateToProps)(CoinbaseForm) - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -inherits(CoinbaseForm, Component) - -function CoinbaseForm () { - Component.call(this) -} - -CoinbaseForm.prototype.render = function () { - var props = this.props - - return h('.flex-column', { - style: { - marginTop: '35px', - padding: '25px', - width: '100%', - }, - }, [ - h('.flex-row', { - style: { - justifyContent: 'space-around', - margin: '33px', - marginTop: '0px', - }, - }, [ - h('button.btn-green', { - onClick: this.toCoinbase.bind(this), - }, 'Continue to Coinbase'), - - h('button.btn-red', { - onClick: () => props.dispatch(actions.backTobuyView(props.accounts.address)), - }, 'Cancel'), - ]), - ]) -} - -CoinbaseForm.prototype.toCoinbase = function () { - const props = this.props - const address = props.buyView.buyAddress - props.dispatch(actions.buyEth({ network: '1', address, amount: 0 })) -} - -CoinbaseForm.prototype.renderLoading = function () { - return h('img', { - style: { - width: '27px', - marginRight: '-27px', - }, - src: 'images/loading.svg', - }) -} diff --git a/responsive-ui/app/components/copyButton.js b/responsive-ui/app/components/copyButton.js deleted file mode 100644 index a25d0719c..000000000 --- a/responsive-ui/app/components/copyButton.js +++ /dev/null @@ -1,59 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const copyToClipboard = require('copy-to-clipboard') - -const Tooltip = require('./tooltip') - -module.exports = CopyButton - -inherits(CopyButton, Component) -function CopyButton () { - Component.call(this) -} - -// As parameters, accepts: -// "value", which is the value to copy (mandatory) -// "title", which is the text to show on hover (optional, defaults to 'Copy') -CopyButton.prototype.render = function () { - const props = this.props - const state = this.state || {} - - const value = props.value - const copied = state.copied - - const message = copied ? 'Copied' : props.title || ' Copy ' - - return h('.copy-button', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - - h(Tooltip, { - title: message, - }, [ - h('i.fa.fa-clipboard.cursor-pointer.color-orange', { - style: { - margin: '5px', - }, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - copyToClipboard(value) - this.debounceRestore() - }, - }), - ]), - - ]) -} - -CopyButton.prototype.debounceRestore = function () { - this.setState({ copied: true }) - clearTimeout(this.timeout) - this.timeout = setTimeout(() => { - this.setState({ copied: false }) - }, 850) -} diff --git a/responsive-ui/app/components/copyable.js b/responsive-ui/app/components/copyable.js deleted file mode 100644 index a4f6f4bc6..000000000 --- a/responsive-ui/app/components/copyable.js +++ /dev/null @@ -1,46 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const Tooltip = require('./tooltip') -const copyToClipboard = require('copy-to-clipboard') - -module.exports = Copyable - -inherits(Copyable, Component) -function Copyable () { - Component.call(this) - this.state = { - copied: false, - } -} - -Copyable.prototype.render = function () { - const props = this.props - const state = this.state - const { value, children } = props - const { copied } = state - - return h(Tooltip, { - title: copied ? 'Copied!' : 'Copy', - position: 'bottom', - }, h('span', { - style: { - cursor: 'pointer', - }, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - copyToClipboard(value) - this.debounceRestore() - }, - }, children)) -} - -Copyable.prototype.debounceRestore = function () { - this.setState({ copied: true }) - clearTimeout(this.timeout) - this.timeout = setTimeout(() => { - this.setState({ copied: false }) - }, 850) -} diff --git a/responsive-ui/app/components/custom-radio-list.js b/responsive-ui/app/components/custom-radio-list.js deleted file mode 100644 index a4c525396..000000000 --- a/responsive-ui/app/components/custom-radio-list.js +++ /dev/null @@ -1,60 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = RadioList - -inherits(RadioList, Component) -function RadioList () { - Component.call(this) -} - -RadioList.prototype.render = function () { - const props = this.props - const activeClass = '.custom-radio-selected' - const inactiveClass = '.custom-radio-inactive' - const { - labels, - defaultFocus, - } = props - - - return ( - h('.flex-row', { - style: { - fontSize: '12px', - }, - }, [ - h('.flex-column.custom-radios', { - style: { - marginRight: '5px', - }, - }, - labels.map((lable, i) => { - let isSelcted = (this.state !== null) - isSelcted = isSelcted ? (this.state.selected === lable) : (defaultFocus === lable) - return h(isSelcted ? activeClass : inactiveClass, { - title: lable, - onClick: (event) => { - this.setState({selected: event.target.title}) - props.onClick(event) - }, - }) - }) - ), - h('.text', {}, - labels.map((lable) => { - if (props.subtext) { - return h('.flex-row', {}, [ - h('.radio-titles', lable), - h('.radio-titles-subtext', `- ${props.subtext[lable]}`), - ]) - } else { - return h('.radio-titles', lable) - } - }) - ), - ]) - ) -} - diff --git a/responsive-ui/app/components/dropdown.js b/responsive-ui/app/components/dropdown.js deleted file mode 100644 index e77b4c40c..000000000 --- a/responsive-ui/app/components/dropdown.js +++ /dev/null @@ -1,89 +0,0 @@ -const Component = require('react').Component -const PropTypes = require('react').PropTypes -const h = require('react-hyperscript') -const MenuDroppo = require('menu-droppo') - -const noop = () => {} - -class Dropdown extends Component { - render () { - const { isOpen, onClickOutside, style, children } = this.props - - return h( - MenuDroppo, - { - isOpen, - zIndex: 11, - onClickOutside, - style, - innerStyle: { - borderRadius: '4px', - padding: '8px 16px', - background: 'rgba(0, 0, 0, 0.8)', - boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', - }, - }, - [ - h( - 'style', - ` - li.dropdown-menu-item:hover { color:rgb(225, 225, 225); } - li.dropdown-menu-item { color: rgb(185, 185, 185); } - ` - ), - ...children, - ] - ) - } -} - -Dropdown.defaultProps = { - isOpen: false, - onClick: noop, -} - -Dropdown.propTypes = { - isOpen: PropTypes.bool.isRequired, - onClick: PropTypes.func.isRequired, - children: PropTypes.node, - style: PropTypes.object.isRequired, -} - -class DropdownMenuItem extends Component { - render () { - const { onClick, closeMenu, children } = this.props - - return h( - 'li.dropdown-menu-item', - { - onClick: () => { - onClick() - closeMenu() - }, - style: { - listStyle: 'none', - padding: '8px 0px 8px 0px', - fontSize: '12px', - fontStyle: 'normal', - fontFamily: 'Montserrat Regular', - cursor: 'pointer', - display: 'flex', - justifyContent: 'flex-start', - alignItems: 'center', - }, - }, - children - ) - } -} - -DropdownMenuItem.propTypes = { - closeMenu: PropTypes.func.isRequired, - onClick: PropTypes.func.isRequired, - children: PropTypes.node, -} - -module.exports = { - Dropdown, - DropdownMenuItem, -} diff --git a/responsive-ui/app/components/editable-label.js b/responsive-ui/app/components/editable-label.js deleted file mode 100644 index 167be7eaf..000000000 --- a/responsive-ui/app/components/editable-label.js +++ /dev/null @@ -1,56 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const findDOMNode = require('react-dom').findDOMNode - -module.exports = EditableLabel - -inherits(EditableLabel, Component) -function EditableLabel () { - Component.call(this) -} - -EditableLabel.prototype.render = function () { - const props = this.props - const state = this.state - - if (state && state.isEditingLabel) { - return h('div.editable-label', [ - h('input.sizing-input', { - defaultValue: props.textValue, - maxLength: '20', - onKeyPress: (event) => { - this.saveIfEnter(event) - }, - }), - h('button.editable-button', { - onClick: () => this.saveText(), - }, 'Save'), - ]) - } else { - return h('div.name-label', { - onClick: (event) => { - const nameAttribute = event.target.getAttribute('name') - // checks for class to handle smaller CTA above the account name - const classAttribute = event.target.getAttribute('class') - if (nameAttribute === 'edit' || classAttribute === 'edit-text') { - this.setState({ isEditingLabel: true }) - } - }, - }, this.props.children) - } -} - -EditableLabel.prototype.saveIfEnter = function (event) { - if (event.key === 'Enter') { - this.saveText() - } -} - -EditableLabel.prototype.saveText = function () { - var container = findDOMNode(this) - var text = container.querySelector('.editable-label input').value - var truncatedText = text.substring(0, 20) - this.props.saveText(truncatedText) - this.setState({ isEditingLabel: false, textLabel: truncatedText }) -} diff --git a/responsive-ui/app/components/ens-input.js b/responsive-ui/app/components/ens-input.js deleted file mode 100644 index 3a33ebf74..000000000 --- a/responsive-ui/app/components/ens-input.js +++ /dev/null @@ -1,170 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const extend = require('xtend') -const debounce = require('debounce') -const copyToClipboard = require('copy-to-clipboard') -const ENS = require('ethjs-ens') -const networkMap = require('ethjs-ens/lib/network-map.json') -const ensRE = /.+\.eth$/ -const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' - - -module.exports = EnsInput - -inherits(EnsInput, Component) -function EnsInput () { - Component.call(this) -} - -EnsInput.prototype.render = function () { - const props = this.props - const opts = extend(props, { - list: 'addresses', - onChange: () => { - const network = this.props.network - const networkHasEnsSupport = getNetworkEnsSupport(network) - if (!networkHasEnsSupport) return - - const recipient = document.querySelector('input[name="address"]').value - if (recipient.match(ensRE) === null) { - return this.setState({ - loadingEns: false, - ensResolution: null, - ensFailure: null, - }) - } - - this.setState({ - loadingEns: true, - }) - this.checkName() - }, - }) - return h('div', { - style: { width: '100%' }, - }, [ - h('input.large-input', opts), - // The address book functionality. - h('datalist#addresses', - [ - // Corresponds to the addresses owned. - Object.keys(props.identities).map((key) => { - const identity = props.identities[key] - return h('option', { - value: identity.address, - label: identity.name, - key: identity.address, - }) - }), - // Corresponds to previously sent-to addresses. - props.addressBook.map((identity) => { - return h('option', { - value: identity.address, - label: identity.name, - key: identity.address, - }) - }), - ]), - this.ensIcon(), - ]) -} - -EnsInput.prototype.componentDidMount = function () { - const network = this.props.network - const networkHasEnsSupport = getNetworkEnsSupport(network) - this.setState({ ensResolution: ZERO_ADDRESS }) - - if (networkHasEnsSupport) { - const provider = global.ethereumProvider - this.ens = new ENS({ provider, network }) - this.checkName = debounce(this.lookupEnsName.bind(this), 200) - } -} - -EnsInput.prototype.lookupEnsName = function () { - const recipient = document.querySelector('input[name="address"]').value - const { ensResolution } = this.state - - log.info(`ENS attempting to resolve name: ${recipient}`) - this.ens.lookup(recipient.trim()) - .then((address) => { - if (address === ZERO_ADDRESS) throw new Error('No address has been set for this name.') - if (address !== ensResolution) { - this.setState({ - loadingEns: false, - ensResolution: address, - nickname: recipient.trim(), - hoverText: address + '\nClick to Copy', - ensFailure: false, - }) - } - }) - .catch((reason) => { - log.error(reason) - return this.setState({ - loadingEns: false, - ensResolution: ZERO_ADDRESS, - ensFailure: true, - hoverText: reason.message, - }) - }) -} - -EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) { - const state = this.state || {} - const ensResolution = state.ensResolution - // If an address is sent without a nickname, meaning not from ENS or from - // the user's own accounts, a default of a one-space string is used. - const nickname = state.nickname || ' ' - if (prevState && ensResolution && this.props.onChange && - ensResolution !== prevState.ensResolution) { - this.props.onChange(ensResolution, nickname) - } -} - -EnsInput.prototype.ensIcon = function (recipient) { - const { hoverText } = this.state || {} - return h('span', { - title: hoverText, - style: { - position: 'absolute', - padding: '9px', - transform: 'translatex(-40px)', - }, - }, this.ensIconContents(recipient)) -} - -EnsInput.prototype.ensIconContents = function (recipient) { - const { loadingEns, ensFailure, ensResolution } = this.state || { ensResolution: ZERO_ADDRESS} - - if (loadingEns) { - return h('img', { - src: 'images/loading.svg', - style: { - width: '30px', - height: '30px', - transform: 'translateY(-6px)', - }, - }) - } - - if (ensFailure) { - return h('i.fa.fa-warning.fa-lg.warning') - } - - if (ensResolution && (ensResolution !== ZERO_ADDRESS)) { - return h('i.fa.fa-check-circle.fa-lg.cursor-pointer', { - style: { color: 'green' }, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - copyToClipboard(ensResolution) - }, - }) - } -} - -function getNetworkEnsSupport (network) { - return Boolean(networkMap[network]) -} diff --git a/responsive-ui/app/components/eth-balance.js b/responsive-ui/app/components/eth-balance.js deleted file mode 100644 index 4f538fd31..000000000 --- a/responsive-ui/app/components/eth-balance.js +++ /dev/null @@ -1,89 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const formatBalance = require('../util').formatBalance -const generateBalanceObject = require('../util').generateBalanceObject -const Tooltip = require('./tooltip.js') -const FiatValue = require('./fiat-value.js') - -module.exports = EthBalanceComponent - -inherits(EthBalanceComponent, Component) -function EthBalanceComponent () { - Component.call(this) -} - -EthBalanceComponent.prototype.render = function () { - var props = this.props - let { value } = props - const { style, width } = props - var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true - value = value ? formatBalance(value, 6, needsParse) : '...' - - return ( - - h('.ether-balance.ether-balance-amount', { - style, - }, [ - h('div', { - style: { - display: 'inline', - width, - }, - }, this.renderBalance(value)), - ]) - - ) -} -EthBalanceComponent.prototype.renderBalance = function (value) { - var props = this.props - const { conversionRate, shorten, incoming, currentCurrency } = props - if (value === 'None') return value - if (value === '...') return value - var balanceObj = generateBalanceObject(value, shorten ? 1 : 3) - var balance - var splitBalance = value.split(' ') - var ethNumber = splitBalance[0] - var ethSuffix = splitBalance[1] - const showFiat = 'showFiat' in props ? props.showFiat : true - - if (shorten) { - balance = balanceObj.shortBalance - } else { - balance = balanceObj.balance - } - - var label = balanceObj.label - - return ( - h(Tooltip, { - position: 'bottom', - title: `${ethNumber} ${ethSuffix}`, - }, h('div.flex-column', [ - h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('div', { - style: { - width: '100%', - textAlign: 'right', - }, - }, incoming ? `+${balance}` : balance), - h('div', { - style: { - color: ' #AEAEAE', - fontSize: '12px', - marginLeft: '5px', - }, - }, label), - ]), - - showFiat ? h(FiatValue, { value: props.value, conversionRate, currentCurrency }) : null, - ])) - ) -} diff --git a/responsive-ui/app/components/fiat-value.js b/responsive-ui/app/components/fiat-value.js deleted file mode 100644 index 8a64a1cfc..000000000 --- a/responsive-ui/app/components/fiat-value.js +++ /dev/null @@ -1,63 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const formatBalance = require('../util').formatBalance - -module.exports = FiatValue - -inherits(FiatValue, Component) -function FiatValue () { - Component.call(this) -} - -FiatValue.prototype.render = function () { - const props = this.props - const { conversionRate, currentCurrency } = props - - const value = formatBalance(props.value, 6) - - if (value === 'None') return value - var fiatDisplayNumber, fiatTooltipNumber - var splitBalance = value.split(' ') - - if (conversionRate !== 0) { - fiatTooltipNumber = Number(splitBalance[0]) * conversionRate - fiatDisplayNumber = fiatTooltipNumber.toFixed(2) - } else { - fiatDisplayNumber = 'N/A' - fiatTooltipNumber = 'Unknown' - } - - return fiatDisplay(fiatDisplayNumber, currentCurrency) -} - -function fiatDisplay (fiatDisplayNumber, fiatSuffix) { - if (fiatDisplayNumber !== 'N/A') { - return h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('div', { - style: { - width: '100%', - textAlign: 'right', - fontSize: '12px', - color: '#333333', - }, - }, fiatDisplayNumber), - h('div', { - style: { - color: '#AEAEAE', - marginLeft: '5px', - fontSize: '12px', - }, - }, fiatSuffix), - ]) - } else { - return h('div') - } -} diff --git a/responsive-ui/app/components/hex-as-decimal-input.js b/responsive-ui/app/components/hex-as-decimal-input.js deleted file mode 100644 index 4a71e9585..000000000 --- a/responsive-ui/app/components/hex-as-decimal-input.js +++ /dev/null @@ -1,154 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const extend = require('xtend') - -module.exports = HexAsDecimalInput - -inherits(HexAsDecimalInput, Component) -function HexAsDecimalInput () { - this.state = { invalid: null } - Component.call(this) -} - -/* Hex as Decimal Input - * - * A component for allowing easy, decimal editing - * of a passed in hex string value. - * - * On change, calls back its `onChange` function parameter - * and passes it an updated hex string. - */ - -HexAsDecimalInput.prototype.render = function () { - const props = this.props - const state = this.state - - const { value, onChange, min, max } = props - - const toEth = props.toEth - const suffix = props.suffix - const decimalValue = decimalize(value, toEth) - const style = props.style - - return ( - h('.flex-column', [ - h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('input.hex-input', { - type: 'number', - required: true, - min: min, - max: max, - style: extend({ - display: 'block', - textAlign: 'right', - backgroundColor: 'transparent', - border: '1px solid #bdbdbd', - - }, style), - value: parseInt(decimalValue), - onBlur: (event) => { - this.updateValidity(event) - }, - onChange: (event) => { - this.updateValidity(event) - const hexString = (event.target.value === '') ? '' : hexify(event.target.value) - onChange(hexString) - }, - onInvalid: (event) => { - const msg = this.constructWarning() - if (msg === state.invalid) { - return - } - this.setState({ invalid: msg }) - event.preventDefault() - return false - }, - }), - h('div', { - style: { - color: ' #AEAEAE', - fontSize: '12px', - marginLeft: '5px', - marginRight: '6px', - width: '20px', - }, - }, suffix), - ]), - - state.invalid ? h('span.error', { - style: { - position: 'absolute', - right: '0px', - textAlign: 'right', - transform: 'translateY(26px)', - padding: '3px', - background: 'rgba(255,255,255,0.85)', - zIndex: '1', - textTransform: 'capitalize', - border: '2px solid #E20202', - }, - }, state.invalid) : null, - ]) - ) -} - -HexAsDecimalInput.prototype.setValid = function (message) { - this.setState({ invalid: null }) -} - -HexAsDecimalInput.prototype.updateValidity = function (event) { - const target = event.target - const value = this.props.value - const newValue = target.value - - if (value === newValue) { - return - } - - const valid = target.checkValidity() - if (valid) { - this.setState({ invalid: null }) - } -} - -HexAsDecimalInput.prototype.constructWarning = function () { - const { name, min, max } = this.props - let message = name ? name + ' ' : '' - - if (min && max) { - message += `must be greater than or equal to ${min} and less than or equal to ${max}.` - } else if (min) { - message += `must be greater than or equal to ${min}.` - } else if (max) { - message += `must be less than or equal to ${max}.` - } else { - message += 'Invalid input.' - } - - return message -} - -function hexify (decimalString) { - const hexBN = new BN(parseInt(decimalString), 10) - return '0x' + hexBN.toString('hex') -} - -function decimalize (input, toEth) { - if (input === '') { - return '' - } else { - const strippedInput = ethUtil.stripHexPrefix(input) - const inputBN = new BN(strippedInput, 'hex') - return inputBN.toString(10) - } -} diff --git a/responsive-ui/app/components/identicon.js b/responsive-ui/app/components/identicon.js deleted file mode 100644 index c754bc6ba..000000000 --- a/responsive-ui/app/components/identicon.js +++ /dev/null @@ -1,72 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const isNode = require('detect-node') -const findDOMNode = require('react-dom').findDOMNode -const jazzicon = require('jazzicon') -const iconFactoryGen = require('../../lib/icon-factory') -const iconFactory = iconFactoryGen(jazzicon) - -module.exports = IdenticonComponent - -inherits(IdenticonComponent, Component) -function IdenticonComponent () { - Component.call(this) - - this.defaultDiameter = 46 -} - -IdenticonComponent.prototype.render = function () { - var props = this.props - var diameter = props.diameter || this.defaultDiameter - return ( - h('div', { - key: 'identicon-' + this.props.address, - style: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - height: diameter, - width: diameter, - borderRadius: diameter / 2, - overflow: 'hidden', - }, - }) - ) -} - -IdenticonComponent.prototype.componentDidMount = function () { - var props = this.props - const { address } = props - - if (!address) return - - var container = findDOMNode(this) - - var diameter = props.diameter || this.defaultDiameter - if (!isNode) { - var img = iconFactory.iconForAddress(address, diameter) - container.appendChild(img) - } -} - -IdenticonComponent.prototype.componentDidUpdate = function () { - var props = this.props - const { address } = props - - if (!address) return - - var container = findDOMNode(this) - - var children = container.children - for (var i = 0; i < children.length; i++) { - container.removeChild(children[i]) - } - - var diameter = props.diameter || this.defaultDiameter - if (!isNode) { - var img = iconFactory.iconForAddress(address, diameter) - container.appendChild(img) - } -} - diff --git a/responsive-ui/app/components/loading.js b/responsive-ui/app/components/loading.js deleted file mode 100644 index 87d6f5d20..000000000 --- a/responsive-ui/app/components/loading.js +++ /dev/null @@ -1,53 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') - - -inherits(LoadingIndicator, Component) -module.exports = LoadingIndicator - -function LoadingIndicator () { - Component.call(this) -} - -LoadingIndicator.prototype.render = function () { - const { isLoading, loadingMessage } = this.props - - return ( - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'loader', - transitionEnterTimeout: 150, - transitionLeaveTimeout: 150, - }, [ - - isLoading ? h('div', { - style: { - zIndex: 10, - position: 'absolute', - flexDirection: 'column', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - height: '100%', - width: '100%', - background: 'rgba(255, 255, 255, 0.8)', - }, - }, [ - h('img', { - src: 'images/loading.svg', - }), - - h('br'), - - showMessageIfAny(loadingMessage), - ]) : null, - ]) - ) -} - -function showMessageIfAny (loadingMessage) { - if (!loadingMessage) return null - return h('span', loadingMessage) -} diff --git a/responsive-ui/app/components/mascot.js b/responsive-ui/app/components/mascot.js deleted file mode 100644 index 973ec2cad..000000000 --- a/responsive-ui/app/components/mascot.js +++ /dev/null @@ -1,59 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const metamaskLogo = require('metamask-logo') -const debounce = require('debounce') - -module.exports = Mascot - -inherits(Mascot, Component) -function Mascot () { - Component.call(this) - this.logo = metamaskLogo({ - followMouse: true, - pxNotRatio: true, - width: 200, - height: 200, - }) - - this.refollowMouse = debounce(this.logo.setFollowMouse.bind(this.logo, true), 1000) - this.unfollowMouse = this.logo.setFollowMouse.bind(this.logo, false) -} - -Mascot.prototype.render = function () { - // this is a bit hacky - // the event emitter is on `this.props` - // and we dont get that until render - this.handleAnimationEvents() - - return h('#metamask-mascot-container', { - style: { zIndex: 0 }, - }) -} - -Mascot.prototype.componentDidMount = function () { - var targetDivId = 'metamask-mascot-container' - var container = document.getElementById(targetDivId) - container.appendChild(this.logo.container) -} - -Mascot.prototype.componentWillUnmount = function () { - this.animations = this.props.animationEventEmitter - this.animations.removeAllListeners() - this.logo.container.remove() - this.logo.stopAnimation() -} - -Mascot.prototype.handleAnimationEvents = function () { - // only setup listeners once - if (this.animations) return - this.animations = this.props.animationEventEmitter - this.animations.on('point', this.lookAt.bind(this)) - this.animations.on('setFollowMouse', this.logo.setFollowMouse.bind(this.logo)) -} - -Mascot.prototype.lookAt = function (target) { - this.unfollowMouse() - this.logo.lookAt(target) - this.refollowMouse() -} diff --git a/responsive-ui/app/components/mini-account-panel.js b/responsive-ui/app/components/mini-account-panel.js deleted file mode 100644 index c09cf5b7a..000000000 --- a/responsive-ui/app/components/mini-account-panel.js +++ /dev/null @@ -1,74 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const Identicon = require('./identicon') - -module.exports = AccountPanel - - -inherits(AccountPanel, Component) -function AccountPanel () { - Component.call(this) -} - -AccountPanel.prototype.render = function () { - var props = this.props - var picOrder = props.picOrder || 'left' - const { imageSeed } = props - - return ( - - h('.identity-panel.flex-row.flex-left', { - style: { - cursor: props.onClick ? 'pointer' : undefined, - }, - onClick: props.onClick, - }, [ - - this.genIcon(imageSeed, picOrder), - - h('div.flex-column.flex-justify-center', { - style: { - lineHeight: '15px', - order: 2, - display: 'flex', - alignItems: picOrder === 'left' ? 'flex-begin' : 'flex-end', - }, - }, this.props.children), - ]) - ) -} - -AccountPanel.prototype.genIcon = function (seed, picOrder) { - const props = this.props - - // When there is no seed value, this is a contract creation. - // We then show the contract icon. - if (!seed) { - return h('.identicon-wrapper.flex-column.select-none', { - style: { - order: picOrder === 'left' ? 1 : 3, - }, - }, [ - h('i.fa.fa-file-text-o.fa-lg', { - style: { - fontSize: '42px', - transform: 'translate(0px, -16px)', - }, - }), - ]) - } - - // If there was a seed, we return an identicon for that address. - return h('.identicon-wrapper.flex-column.select-none', { - style: { - order: picOrder === 'left' ? 1 : 3, - }, - }, [ - h(Identicon, { - address: seed, - imageify: props.imageifyIdenticons, - }), - ]) -} - diff --git a/responsive-ui/app/components/network.js b/responsive-ui/app/components/network.js deleted file mode 100644 index 698a0bbb9..000000000 --- a/responsive-ui/app/components/network.js +++ /dev/null @@ -1,124 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = Network - -inherits(Network, Component) - -function Network () { - Component.call(this) -} - -Network.prototype.render = function () { - const props = this.props - const networkNumber = props.network - let providerName - try { - providerName = props.provider.type - } catch (e) { - providerName = null - } - let iconName, hoverText - - if (networkNumber === 'loading') { - return h('span', { - style: { - display: 'flex', - alignItems: 'center', - flexDirection: 'row', - }, - onClick: (event) => this.props.onClick(event), - }, [ - h('img', { - title: 'Attempting to connect to blockchain.', - style: { - width: '27px', - }, - src: 'images/loading.svg', - }), - h('i.fa.fa-sort-desc'), - ]) - } else if (providerName === 'mainnet') { - hoverText = 'Main Ethereum Network' - iconName = 'ethereum-network' - } else if (providerName === 'ropsten') { - hoverText = 'Ropsten Test Network' - iconName = 'ropsten-test-network' - } else if (parseInt(networkNumber) === 3) { - hoverText = 'Ropsten Test Network' - iconName = 'ropsten-test-network' - } else if (providerName === 'kovan') { - hoverText = 'Kovan Test Network' - iconName = 'kovan-test-network' - } else if (providerName === 'rinkeby') { - hoverText = 'Rinkeby Test Network' - iconName = 'rinkeby-test-network' - } else { - hoverText = 'Unknown Private Network' - iconName = 'unknown-private-network' - } - - return ( - h('#network_component.pointer', { - title: hoverText, - onClick: (event) => this.props.onClick(event), - }, [ - (function () { - switch (iconName) { - case 'ethereum-network': - return h('.network-indicator', [ - h('.menu-icon.diamond'), - h('.network-name', { - style: { - color: '#039396', - }}, - 'Ethereum Main Net'), - ]) - case 'ropsten-test-network': - return h('.network-indicator', [ - h('.menu-icon.red-dot'), - h('.network-name', { - style: { - color: '#ff6666', - }}, - 'Ropsten Test Net'), - ]) - case 'kovan-test-network': - return h('.network-indicator', [ - h('.menu-icon.hollow-diamond'), - h('.network-name', { - style: { - color: '#690496', - }}, - 'Kovan Test Net'), - ]) - case 'rinkeby-test-network': - return h('.network-indicator', [ - h('.menu-icon.golden-square'), - h('.network-name', { - style: { - color: '#e7a218', - }}, - 'Rinkeby Test Net'), - ]) - default: - return h('.network-indicator', [ - h('i.fa.fa-question-circle.fa-lg', { - style: { - margin: '10px', - color: 'rgb(125, 128, 130)', - }, - }), - - h('.network-name', { - style: { - color: '#AEAEAE', - }}, - 'Private Network'), - ]) - } - })(), - ]) - ) -} diff --git a/responsive-ui/app/components/notice.js b/responsive-ui/app/components/notice.js deleted file mode 100644 index d9f0067cd..000000000 --- a/responsive-ui/app/components/notice.js +++ /dev/null @@ -1,126 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const ReactMarkdown = require('react-markdown') -const linker = require('extension-link-enabler') -const findDOMNode = require('react-dom').findDOMNode - -module.exports = Notice - -inherits(Notice, Component) -function Notice () { - Component.call(this) -} - -Notice.prototype.render = function () { - const { notice, onConfirm } = this.props - const { title, date, body } = notice - const state = this.state || { disclaimerDisabled: true } - const disabled = state.disclaimerDisabled - - return ( - h('.flex-column.flex-center.flex-grow', [ - h('h3.flex-center.text-transform-uppercase.terms-header', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - width: '100%', - fontSize: '20px', - textAlign: 'center', - padding: 6, - }, - }, [ - title, - ]), - - h('h5.flex-center.text-transform-uppercase.terms-header', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginBottom: 24, - width: '100%', - fontSize: '20px', - textAlign: 'center', - padding: 6, - }, - }, [ - date, - ]), - - h('style', ` - - .markdown { - overflow-x: hidden; - } - - .markdown h1, .markdown h2, .markdown h3 { - margin: 10px 0; - font-weight: bold; - } - - .markdown strong { - font-weight: bold; - } - .markdown em { - font-style: italic; - } - - .markdown p { - margin: 10px 0; - } - - .markdown a { - color: #df6b0e; - } - - `), - - h('div.markdown', { - onScroll: (e) => { - var object = e.currentTarget - if (object.offsetHeight + object.scrollTop + 100 >= object.scrollHeight) { - this.setState({disclaimerDisabled: false}) - } - }, - style: { - background: 'rgb(235, 235, 235)', - height: '310px', - padding: '6px', - width: '90%', - overflowY: 'scroll', - scroll: 'auto', - }, - }, [ - h(ReactMarkdown, { - className: 'notice-box', - source: body, - skipHtml: true, - }), - ]), - - h('button', { - disabled, - onClick: () => { - this.setState({disclaimerDisabled: true}) - onConfirm() - }, - style: { - marginTop: '18px', - }, - }, 'Accept'), - ]) - ) -} - -Notice.prototype.componentDidMount = function () { - var node = findDOMNode(this) - linker.setupListener(node) - if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) { - this.setState({disclaimerDisabled: false}) - } -} - -Notice.prototype.componentWillUnmount = function () { - var node = findDOMNode(this) - linker.teardownListener(node) -} diff --git a/responsive-ui/app/components/pending-msg-details.js b/responsive-ui/app/components/pending-msg-details.js deleted file mode 100644 index 16308d121..000000000 --- a/responsive-ui/app/components/pending-msg-details.js +++ /dev/null @@ -1,50 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const AccountPanel = require('./account-panel') - -module.exports = PendingMsgDetails - -inherits(PendingMsgDetails, Component) -function PendingMsgDetails () { - Component.call(this) -} - -PendingMsgDetails.prototype.render = function () { - var state = this.props - var msgData = state.txData - - var msgParams = msgData.msgParams || {} - var address = msgParams.from || state.selectedAddress - var identity = state.identities[address] || { address: address } - var account = state.accounts[address] || { address: address } - - return ( - h('div', { - key: msgData.id, - style: { - margin: '10px 20px', - }, - }, [ - - // account that will sign - h(AccountPanel, { - showFullAddress: true, - identity: identity, - account: account, - imageifyIdenticons: state.imageifyIdenticons, - }), - - // message data - h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-row.flex-space-between', [ - h('label.font-small', 'MESSAGE'), - h('span.font-small', msgParams.data), - ]), - ]), - - ]) - ) -} - diff --git a/responsive-ui/app/components/pending-msg.js b/responsive-ui/app/components/pending-msg.js deleted file mode 100644 index b2cac164a..000000000 --- a/responsive-ui/app/components/pending-msg.js +++ /dev/null @@ -1,56 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const PendingTxDetails = require('./pending-msg-details') - -module.exports = PendingMsg - -inherits(PendingMsg, Component) -function PendingMsg () { - Component.call(this) -} - -PendingMsg.prototype.render = function () { - var state = this.props - var msgData = state.txData - - return ( - - h('div', { - key: msgData.id, - }, [ - - // header - h('h3', { - style: { - fontWeight: 'bold', - textAlign: 'center', - }, - }, 'Sign Message'), - - h('.error', { - style: { - margin: '10px', - }, - }, `Signing this message can have - dangerous side effects. Only sign messages from - sites you fully trust with your entire account. - This will be fixed in a future version.`), - - // message details - h(PendingTxDetails, state), - - // sign + cancel - h('.flex-row.flex-space-around', [ - h('button', { - onClick: state.cancelMessage, - }, 'Cancel'), - h('button', { - onClick: state.signMessage, - }, 'Sign'), - ]), - ]) - - ) -} - diff --git a/responsive-ui/app/components/pending-personal-msg-details.js b/responsive-ui/app/components/pending-personal-msg-details.js deleted file mode 100644 index 1050513f2..000000000 --- a/responsive-ui/app/components/pending-personal-msg-details.js +++ /dev/null @@ -1,60 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const AccountPanel = require('./account-panel') -const BinaryRenderer = require('./binary-renderer') - -module.exports = PendingMsgDetails - -inherits(PendingMsgDetails, Component) -function PendingMsgDetails () { - Component.call(this) -} - -PendingMsgDetails.prototype.render = function () { - var state = this.props - var msgData = state.txData - - var msgParams = msgData.msgParams || {} - var address = msgParams.from || state.selectedAddress - var identity = state.identities[address] || { address: address } - var account = state.accounts[address] || { address: address } - - var { data } = msgParams - - return ( - h('div', { - key: msgData.id, - style: { - margin: '10px 20px', - }, - }, [ - - // account that will sign - h(AccountPanel, { - showFullAddress: true, - identity: identity, - account: account, - imageifyIdenticons: state.imageifyIdenticons, - }), - - // message data - h('div', { - style: { - height: '260px', - }, - }, [ - h('label.font-small', { style: { display: 'block' } }, 'MESSAGE'), - h(BinaryRenderer, { - value: data, - style: { - height: '215px', - }, - }), - ]), - - ]) - ) -} - diff --git a/responsive-ui/app/components/pending-personal-msg.js b/responsive-ui/app/components/pending-personal-msg.js deleted file mode 100644 index 4542adb28..000000000 --- a/responsive-ui/app/components/pending-personal-msg.js +++ /dev/null @@ -1,47 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const PendingTxDetails = require('./pending-personal-msg-details') - -module.exports = PendingMsg - -inherits(PendingMsg, Component) -function PendingMsg () { - Component.call(this) -} - -PendingMsg.prototype.render = function () { - var state = this.props - var msgData = state.txData - - return ( - - h('div', { - key: msgData.id, - }, [ - - // header - h('h3', { - style: { - fontWeight: 'bold', - textAlign: 'center', - }, - }, 'Sign Message'), - - // message details - h(PendingTxDetails, state), - - // sign + cancel - h('.flex-row.flex-space-around', [ - h('button', { - onClick: state.cancelPersonalMessage, - }, 'Cancel'), - h('button', { - onClick: state.signPersonalMessage, - }, 'Sign'), - ]), - ]) - - ) -} - diff --git a/responsive-ui/app/components/pending-tx.js b/responsive-ui/app/components/pending-tx.js deleted file mode 100644 index d7d602f31..000000000 --- a/responsive-ui/app/components/pending-tx.js +++ /dev/null @@ -1,480 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const actions = require('../actions') -const clone = require('clone') - -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const hexToBn = require('../../../app/scripts/lib/hex-to-bn') -const util = require('../util') -const MiniAccountPanel = require('./mini-account-panel') -const Copyable = require('./copyable') -const EthBalance = require('./eth-balance') -const addressSummary = util.addressSummary -const nameForAddress = require('../../lib/contract-namer') -const BNInput = require('./bn-as-decimal-input') - -const MIN_GAS_PRICE_GWEI_BN = new BN(2) -const GWEI_FACTOR = new BN(1e9) -const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) -const MIN_GAS_LIMIT_BN = new BN(21000) - -module.exports = PendingTx -inherits(PendingTx, Component) -function PendingTx () { - Component.call(this) - this.state = { - valid: true, - txData: null, - submitting: false, - } -} - -PendingTx.prototype.render = function () { - const props = this.props - const { currentCurrency, blockGasLimit } = props - - const conversionRate = props.conversionRate - const txMeta = this.gatherTxMeta() - const txParams = txMeta.txParams || {} - - // Account Details - const address = txParams.from || props.selectedAddress - const identity = props.identities[address] || { address: address } - const account = props.accounts[address] - const balance = account ? account.balance : '0x0' - - // recipient check - const isValidAddress = !txParams.to || util.isValidAddress(txParams.to) - - // Gas - const gas = txParams.gas - const gasBn = hexToBn(gas) - const gasLimit = new BN(parseInt(blockGasLimit)) - const safeGasLimit = this.bnMultiplyByFraction(gasLimit, 19, 20).toString(10) - - // Gas Price - const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) - const gasPriceBn = hexToBn(gasPrice) - - const txFeeBn = gasBn.mul(gasPriceBn) - const valueBn = hexToBn(txParams.value) - const maxCost = txFeeBn.add(valueBn) - - const dataLength = txParams.data ? (txParams.data.length - 2) / 2 : 0 - - const balanceBn = hexToBn(balance) - const insufficientBalance = balanceBn.lt(maxCost) - - this.inputs = [] - - return ( - - h('div', { - key: txMeta.id, - }, [ - - h('form#pending-tx-form', { - onSubmit: this.onSubmit.bind(this), - - }, [ - - // tx info - h('div', [ - - h('.flex-row.flex-center', { - style: { - maxWidth: '100%', - }, - }, [ - - h(MiniAccountPanel, { - imageSeed: address, - picOrder: 'right', - }, [ - h('span.font-small', { - style: { - fontFamily: 'Montserrat Bold, Montserrat, sans-serif', - }, - }, identity.name), - - h(Copyable, { - value: ethUtil.toChecksumAddress(address), - }, [ - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, addressSummary(address, 6, 4, false)), - ]), - - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, [ - h(EthBalance, { - value: balance, - conversionRate, - currentCurrency, - inline: true, - labelColor: '#F7861C', - }), - ]), - ]), - - forwardCarrat(), - - this.miniAccountPanelForRecipient(), - ]), - - h('style', ` - .table-box { - margin: 7px 0px 0px 0px; - width: 100%; - } - .table-box .row { - margin: 0px; - background: rgb(236,236,236); - display: flex; - justify-content: space-between; - font-family: Montserrat Light, sans-serif; - font-size: 13px; - padding: 5px 25px; - } - .table-box .row .value { - font-family: Montserrat Regular; - } - `), - - h('.table-box', [ - - // Ether Value - // Currently not customizable, but easily modified - // in the way that gas and gasLimit currently are. - h('.row', [ - h('.cell.label', 'Amount'), - h(EthBalance, { value: txParams.value, currentCurrency, conversionRate }), - ]), - - // Gas Limit (customizable) - h('.cell.row', [ - h('.cell.label', 'Gas Limit'), - h('.cell.value', { - }, [ - h(BNInput, { - name: 'Gas Limit', - value: gasBn, - precision: 0, - scale: 0, - // The hard lower limit for gas. - min: MIN_GAS_LIMIT_BN.toString(10), - max: safeGasLimit, - suffix: 'UNITS', - style: { - position: 'relative', - top: '5px', - }, - onChange: this.gasLimitChanged.bind(this), - - ref: (hexInput) => { this.inputs.push(hexInput) }, - }), - ]), - ]), - - // Gas Price (customizable) - h('.cell.row', [ - h('.cell.label', 'Gas Price'), - h('.cell.value', { - }, [ - h(BNInput, { - name: 'Gas Price', - value: gasPriceBn, - precision: 9, - scale: 9, - suffix: 'GWEI', - min: MIN_GAS_PRICE_GWEI_BN.toString(10), - style: { - position: 'relative', - top: '5px', - }, - onChange: this.gasPriceChanged.bind(this), - ref: (hexInput) => { this.inputs.push(hexInput) }, - }), - ]), - ]), - - // Max Transaction Fee (calculated) - h('.cell.row', [ - h('.cell.label', 'Max Transaction Fee'), - h(EthBalance, { value: txFeeBn.toString(16), currentCurrency, conversionRate }), - ]), - - h('.cell.row', { - style: { - fontFamily: 'Montserrat Regular', - background: 'white', - padding: '10px 25px', - }, - }, [ - h('.cell.label', 'Max Total'), - h('.cell.value', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - h(EthBalance, { - value: maxCost.toString(16), - currentCurrency, - conversionRate, - inline: true, - labelColor: 'black', - fontSize: '16px', - }), - ]), - ]), - - // Data size row: - h('.cell.row', { - style: { - background: '#f7f7f7', - paddingBottom: '0px', - }, - }, [ - h('.cell.label'), - h('.cell.value', { - style: { - fontFamily: 'Montserrat Light', - fontSize: '11px', - }, - }, `Data included: ${dataLength} bytes`), - ]), - ]), // End of Table - - ]), - - h('style', ` - .conf-buttons button { - margin-left: 10px; - text-transform: uppercase; - } - `), - - txMeta.simulationFails ? - h('.error', { - style: { - marginLeft: 50, - fontSize: '0.9em', - }, - }, 'Transaction Error. Exception thrown in contract code.') - : null, - - !isValidAddress ? - h('.error', { - style: { - marginLeft: 50, - fontSize: '0.9em', - }, - }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.') - : null, - - insufficientBalance ? - h('span.error', { - style: { - marginLeft: 50, - fontSize: '0.9em', - }, - }, 'Insufficient balance for transaction') - : null, - - // send + cancel - h('.flex-row.flex-space-around.conf-buttons', { - style: { - display: 'flex', - justifyContent: 'flex-end', - margin: '14px 25px', - }, - }, [ - - - insufficientBalance ? - h('button.btn-green', { - onClick: props.buyEth, - }, 'Buy Ether') - : null, - - h('button', { - onClick: (event) => { - this.resetGasFields() - event.preventDefault() - }, - }, 'Reset'), - - // Accept Button - h('input.confirm.btn-green', { - type: 'submit', - value: 'SUBMIT', - style: { marginLeft: '10px' }, - disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, - }), - - h('button.cancel.btn-red', { - onClick: props.cancelTransaction, - }, 'Reject'), - ]), - ]), - ]) - ) -} - -PendingTx.prototype.miniAccountPanelForRecipient = function () { - const props = this.props - const txData = props.txData - const txParams = txData.txParams || {} - const isContractDeploy = !('to' in txParams) - - // If it's not a contract deploy, send to the account - if (!isContractDeploy) { - return h(MiniAccountPanel, { - imageSeed: txParams.to, - picOrder: 'left', - }, [ - - h('span.font-small', { - style: { - fontFamily: 'Montserrat Bold, Montserrat, sans-serif', - }, - }, nameForAddress(txParams.to, props.identities)), - - h(Copyable, { - value: ethUtil.toChecksumAddress(txParams.to), - }, [ - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, addressSummary(txParams.to, 6, 4, false)), - ]), - - ]) - } else { - return h(MiniAccountPanel, { - picOrder: 'left', - }, [ - - h('span.font-small', { - style: { - fontFamily: 'Montserrat Bold, Montserrat, sans-serif', - }, - }, 'New Contract'), - - ]) - } -} - -PendingTx.prototype.gasPriceChanged = function (newBN, valid) { - log.info(`Gas price changed to: ${newBN.toString(10)}`) - const txMeta = this.gatherTxMeta() - txMeta.txParams.gasPrice = '0x' + newBN.toString('hex') - this.setState({ - txData: clone(txMeta), - valid, - }) -} - -PendingTx.prototype.gasLimitChanged = function (newBN, valid) { - log.info(`Gas limit changed to ${newBN.toString(10)}`) - const txMeta = this.gatherTxMeta() - txMeta.txParams.gas = '0x' + newBN.toString('hex') - this.setState({ - txData: clone(txMeta), - valid, - }) -} - -PendingTx.prototype.resetGasFields = function () { - log.debug(`pending-tx resetGasFields`) - - this.inputs.forEach((hexInput) => { - if (hexInput) { - hexInput.setValid() - } - }) - - this.setState({ - txData: null, - valid: true, - }) -} - -PendingTx.prototype.onSubmit = function (event) { - event.preventDefault() - const txMeta = this.gatherTxMeta() - const valid = this.checkValidity() - this.setState({ valid, submitting: true }) - if (valid && this.verifyGasParams()) { - this.props.sendTransaction(txMeta, event) - } else { - this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) - this.setState({ submitting: false }) - } -} - -PendingTx.prototype.checkValidity = function () { - const form = this.getFormEl() - const valid = form.checkValidity() - return valid -} - -PendingTx.prototype.getFormEl = function () { - const form = document.querySelector('form#pending-tx-form') - // Stub out form for unit tests: - if (!form) { - return { checkValidity () { return true } } - } - return form -} - -// After a customizable state value has been updated, -PendingTx.prototype.gatherTxMeta = function () { - log.debug(`pending-tx gatherTxMeta`) - const props = this.props - const state = this.state - const txData = clone(state.txData) || clone(props.txData) - - log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) - return txData -} - -PendingTx.prototype.verifyGasParams = function () { - // We call this in case the gas has not been modified at all - if (!this.state) { return true } - return ( - this._notZeroOrEmptyString(this.state.gas) && - this._notZeroOrEmptyString(this.state.gasPrice) - ) -} - -PendingTx.prototype._notZeroOrEmptyString = function (obj) { - return obj !== '' && obj !== '0x0' -} - -PendingTx.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) { - const numBN = new BN(numerator) - const denomBN = new BN(denominator) - return targetBN.mul(numBN).div(denomBN) -} - -function forwardCarrat () { - return ( - h('img', { - src: 'images/forward-carrat.svg', - style: { - padding: '5px 6px 0px 10px', - height: '37px', - }, - }) - ) -} diff --git a/responsive-ui/app/components/qr-code.js b/responsive-ui/app/components/qr-code.js deleted file mode 100644 index 06b9aed9b..000000000 --- a/responsive-ui/app/components/qr-code.js +++ /dev/null @@ -1,79 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const qrCode = require('qrcode-npm').qrcode -const inherits = require('util').inherits -const connect = require('react-redux').connect -const isHexPrefixed = require('ethereumjs-util').isHexPrefixed -const CopyButton = require('./copyButton') - -module.exports = connect(mapStateToProps)(QrCodeView) - -function mapStateToProps (state) { - return { - Qr: state.appState.Qr, - buyView: state.appState.buyView, - warning: state.appState.warning, - } -} - -inherits(QrCodeView, Component) - -function QrCodeView () { - Component.call(this) -} - -QrCodeView.prototype.render = function () { - const props = this.props - const Qr = props.Qr - const address = `${isHexPrefixed(Qr.data) ? 'ethereum:' : ''}${Qr.data}` - const qrImage = qrCode(4, 'M') - qrImage.addData(address) - qrImage.make() - return h('.main-container.flex-column', { - key: 'qr', - style: { - justifyContent: 'center', - paddingBottom: '45px', - paddingLeft: '45px', - paddingRight: '45px', - alignItems: 'center', - }, - }, [ - Array.isArray(Qr.message) ? h('.message-container', this.renderMultiMessage()) : h('.qr-header', Qr.message), - - this.props.warning ? this.props.warning && h('span.error.flex-center', { - style: { - textAlign: 'center', - width: '229px', - height: '82px', - }, - }, - this.props.warning) : null, - - h('#qr-container.flex-column', { - style: { - marginTop: '25px', - marginBottom: '15px', - }, - dangerouslySetInnerHTML: { - __html: qrImage.createTableTag(4), - }, - }), - h('.flex-row', [ - h('h3.ellip-address', { - style: { - width: '247px', - }, - }, Qr.data), - h(CopyButton, { - value: Qr.data, - }), - ]), - ]) -} - -QrCodeView.prototype.renderMultiMessage = function () { - var Qr = this.props.Qr - var multiMessage = Qr.message.map((message) => h('.qr-message', message)) - return multiMessage -} diff --git a/responsive-ui/app/components/range-slider.js b/responsive-ui/app/components/range-slider.js deleted file mode 100644 index 823f5eb01..000000000 --- a/responsive-ui/app/components/range-slider.js +++ /dev/null @@ -1,58 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = RangeSlider - -inherits(RangeSlider, Component) -function RangeSlider () { - Component.call(this) -} - -RangeSlider.prototype.render = function () { - const state = this.state || {} - const props = this.props - const onInput = props.onInput || function () {} - const name = props.name - const { - min = 0, - max = 100, - increment = 1, - defaultValue = 50, - mirrorInput = false, - } = this.props.options - const {container, input, range} = props.style - - return ( - h('.flex-row', { - style: container, - }, [ - h('input', { - type: 'range', - name: name, - min: min, - max: max, - step: increment, - style: range, - value: state.value || defaultValue, - onChange: mirrorInput ? this.mirrorInputs.bind(this, event) : onInput, - }), - - // Mirrored input for range - mirrorInput ? h('input.large-input', { - type: 'number', - name: `${name}Mirror`, - min: min, - max: max, - value: state.value || defaultValue, - step: increment, - style: input, - onChange: this.mirrorInputs.bind(this, event), - }) : null, - ]) - ) -} - -RangeSlider.prototype.mirrorInputs = function (event) { - this.setState({value: event.target.value}) -} diff --git a/responsive-ui/app/components/shapeshift-form.js b/responsive-ui/app/components/shapeshift-form.js deleted file mode 100644 index e0a720426..000000000 --- a/responsive-ui/app/components/shapeshift-form.js +++ /dev/null @@ -1,306 +0,0 @@ -const PersistentForm = require('../../lib/persistent-form') -const h = require('react-hyperscript') -const inherits = require('util').inherits -const connect = require('react-redux').connect -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') -const actions = require('../actions') -const Qr = require('./qr-code') -const isValidAddress = require('../util').isValidAddress -module.exports = connect(mapStateToProps)(ShapeshiftForm) - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - isSubLoading: state.appState.isSubLoading, - qrRequested: state.appState.qrRequested, - } -} - -inherits(ShapeshiftForm, PersistentForm) - -function ShapeshiftForm () { - PersistentForm.call(this) - this.persistentFormParentId = 'shapeshift-buy-form' -} - -ShapeshiftForm.prototype.render = function () { - return h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.props.qrRequested ? h(Qr, {key: 'qr'}) : this.renderMain(), - ]) -} - -ShapeshiftForm.prototype.renderMain = function () { - const marketinfo = this.props.buyView.formView.marketinfo - const coinOptions = this.props.buyView.formView.coinOptions - var coin = marketinfo.pair.split('_')[0].toUpperCase() - - return h('.flex-column', { - style: { - // marginTop: '10px', - padding: '25px', - paddingTop: '5px', - width: '100%', - minHeight: '215px', - alignItems: 'center', - overflowY: 'auto', - }, - }, [ - h('.flex-row', { - style: { - justifyContent: 'center', - alignItems: 'baseline', - height: '42px', - }, - }, [ - h('img', { - src: coinOptions[coin].image, - width: '25px', - height: '25px', - style: { - marginRight: '5px', - }, - }), - - h('.input-container', [ - h('input#fromCoin.buy-inputs.ex-coins', { - type: 'text', - list: 'coinList', - autoFocus: true, - dataset: { - persistentFormId: 'input-coin', - }, - style: { - boxSizing: 'border-box', - }, - onChange: this.handleLiveInput.bind(this), - defaultValue: 'BTC', - }), - - this.renderCoinList(), - - h('i.fa.fa-pencil-square-o.edit-text', { - style: { - fontSize: '12px', - color: '#F7861C', - position: 'relative', - bottom: '48px', - left: '106px', - }, - }), - ]), - - h('.icon-control', [ - h('i.fa.fa-refresh.fa-4.orange', { - style: { - bottom: '5px', - left: '5px', - color: '#F7861C', - }, - onClick: this.updateCoin.bind(this), - }), - h('i.fa.fa-chevron-right.fa-4.orange', { - style: { - position: 'relative', - bottom: '26px', - left: '10px', - color: '#F7861C', - }, - onClick: this.updateCoin.bind(this), - }), - ]), - - h('#toCoin.ex-coins', marketinfo.pair.split('_')[1].toUpperCase()), - - h('img', { - src: coinOptions[marketinfo.pair.split('_')[1].toUpperCase()].image, - width: '25px', - height: '25px', - style: { - marginLeft: '5px', - }, - }), - ]), - h('.flex-column', { - style: { - alignItems: 'flex-start', - }, - }, [ - this.props.warning ? this.props.warning && h('span.error.flex-center', { - style: { - textAlign: 'center', - width: '229px', - height: '82px', - }, - }, - this.props.warning) : this.renderInfo(), - ]), - - h(this.activeToggle('.input-container'), { - style: { - padding: '10px', - paddingTop: '0px', - width: '100%', - }, - }, [ - - h('div', `${coin} Address:`), - - h('input#fromCoinAddress.buy-inputs', { - type: 'text', - placeholder: `Your ${coin} Refund Address`, - dataset: { - persistentFormId: 'refund-address', - }, - style: { - boxSizing: 'border-box', - width: '227px', - height: '30px', - padding: ' 5px ', - }, - }), - - h('i.fa.fa-pencil-square-o.edit-text', { - style: { - fontSize: '12px', - color: '#F7861C', - position: 'relative', - bottom: '10px', - right: '11px', - }, - }), - h('.flex-row', { - style: { - justifyContent: 'flex-end', - }, - }, [ - h('button', { - onClick: this.shift.bind(this), - style: { - marginTop: '10px', - position: 'relative', - bottom: '40px', - }, - }, - 'Submit'), - ]), - ]), - ]) -} - -ShapeshiftForm.prototype.shift = function () { - var props = this.props - var withdrawal = this.props.buyView.buyAddress - var returnAddress = document.getElementById('fromCoinAddress').value - var pair = this.props.buyView.formView.marketinfo.pair - var data = { - 'withdrawal': withdrawal, - 'pair': pair, - 'returnAddress': returnAddress, - // Public api key - 'apiKey': '803d1f5df2ed1b1476e4b9e6bcd089e34d8874595dda6a23b67d93c56ea9cc2445e98a6748b219b2b6ad654d9f075f1f1db139abfa93158c04e825db122c14b6', - } - var message = [ - `Deposit Limit: ${props.buyView.formView.marketinfo.limit}`, - `Deposit Minimum:${props.buyView.formView.marketinfo.minimum}`, - ] - if (isValidAddress(withdrawal)) { - this.props.dispatch(actions.coinShiftRquest(data, message)) - } -} - -ShapeshiftForm.prototype.renderCoinList = function () { - var list = Object.keys(this.props.buyView.formView.coinOptions).map((item) => { - return h('option', { - value: item, - }, item) - }) - - return h('datalist#coinList', { - onClick: (event) => { - event.preventDefault() - }, - }, list) -} - -ShapeshiftForm.prototype.updateCoin = function (event) { - event.preventDefault() - const props = this.props - var coinOptions = this.props.buyView.formView.coinOptions - var coin = document.getElementById('fromCoin').value - - if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { - var message = 'Not a valid coin' - return props.dispatch(actions.displayWarning(message)) - } else { - return props.dispatch(actions.pairUpdate(coin)) - } -} - -ShapeshiftForm.prototype.handleLiveInput = function () { - const props = this.props - var coinOptions = this.props.buyView.formView.coinOptions - var coin = document.getElementById('fromCoin').value - - if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { - return null - } else { - return props.dispatch(actions.pairUpdate(coin)) - } -} - -ShapeshiftForm.prototype.renderInfo = function () { - const marketinfo = this.props.buyView.formView.marketinfo - const coinOptions = this.props.buyView.formView.coinOptions - var coin = marketinfo.pair.split('_')[0].toUpperCase() - - return h('span', { - style: { - }, - }, [ - h('h3.flex-row.text-transform-uppercase', { - style: { - color: '#868686', - paddingTop: '4px', - justifyContent: 'space-around', - textAlign: 'center', - fontSize: '17px', - }, - }, `Market Info for ${marketinfo.pair.replace('_', ' to ').toUpperCase()}:`), - h('.marketinfo', ['Status : ', `${coinOptions[coin].status}`]), - h('.marketinfo', ['Exchange Rate: ', `${marketinfo.rate}`]), - h('.marketinfo', ['Limit: ', `${marketinfo.limit}`]), - h('.marketinfo', ['Minimum : ', `${marketinfo.minimum}`]), - ]) -} - -ShapeshiftForm.prototype.activeToggle = function (elementType) { - if (!this.props.buyView.formView.response || this.props.warning) return elementType - return `${elementType}.inactive` -} - -ShapeshiftForm.prototype.renderLoading = function () { - return h('span', { - style: { - position: 'absolute', - left: '70px', - bottom: '194px', - background: 'transparent', - width: '229px', - height: '82px', - display: 'flex', - justifyContent: 'center', - }, - }, [ - h('img', { - style: { - width: '60px', - }, - src: 'images/loading.svg', - }), - ]) -} diff --git a/responsive-ui/app/components/shift-list-item.js b/responsive-ui/app/components/shift-list-item.js deleted file mode 100644 index 32bfbeda4..000000000 --- a/responsive-ui/app/components/shift-list-item.js +++ /dev/null @@ -1,204 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const vreme = new (require('vreme')) -const explorerLink = require('../../lib/explorer-link') -const actions = require('../actions') -const addressSummary = require('../util').addressSummary - -const CopyButton = require('./copyButton') -const EthBalance = require('./eth-balance') -const Tooltip = require('./tooltip') - - -module.exports = connect(mapStateToProps)(ShiftListItem) - -function mapStateToProps (state) { - return { - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - } -} - -inherits(ShiftListItem, Component) - -function ShiftListItem () { - Component.call(this) -} - -ShiftListItem.prototype.render = function () { - return ( - h('.transaction-list-item.flex-row', { - style: { - paddingTop: '20px', - paddingBottom: '20px', - justifyContent: 'space-around', - alignItems: 'center', - }, - }, [ - h('div', { - style: { - width: '0px', - position: 'relative', - bottom: '19px', - }, - }, [ - h('img', { - src: 'https://info.shapeshift.io/sites/default/files/logo.png', - style: { - height: '35px', - width: '132px', - position: 'absolute', - clip: 'rect(0px,23px,34px,0px)', - }, - }), - ]), - - this.renderInfo(), - this.renderUtilComponents(), - ]) - ) -} - -function formatDate (date) { - return vreme.format(new Date(date), 'March 16 2014 14:30') -} - -ShiftListItem.prototype.renderUtilComponents = function () { - var props = this.props - const { conversionRate, currentCurrency } = props - - switch (props.response.status) { - case 'no_deposits': - return h('.flex-row', [ - h(CopyButton, { - value: this.props.depositAddress, - }), - h(Tooltip, { - title: 'QR Code', - }, [ - h('i.fa.fa-qrcode.pointer.pop-hover', { - onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType)), - style: { - margin: '5px', - marginLeft: '23px', - marginRight: '12px', - fontSize: '20px', - color: '#F7861C', - }, - }), - ]), - ]) - case 'received': - return h('.flex-row') - - case 'complete': - return h('.flex-row', [ - h(CopyButton, { - value: this.props.response.transaction, - }), - h(EthBalance, { - value: `${props.response.outgoingCoin}`, - conversionRate, - currentCurrency, - width: '55px', - shorten: true, - needsParse: false, - incoming: true, - style: { - fontSize: '15px', - color: '#01888C', - }, - }), - ]) - - case 'failed': - return '' - default: - return '' - } -} - -ShiftListItem.prototype.renderInfo = function () { - var props = this.props - switch (props.response.status) { - case 'no_deposits': - return h('.flex-column', { - style: { - width: '200px', - overflow: 'hidden', - }, - }, [ - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, `${props.depositType} to ETH via ShapeShift`), - h('div', 'No deposits received'), - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, formatDate(props.time)), - ]) - case 'received': - return h('.flex-column', { - style: { - width: '200px', - overflow: 'hidden', - }, - }, [ - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, `${props.depositType} to ETH via ShapeShift`), - h('div', 'Conversion in progress'), - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, formatDate(props.time)), - ]) - case 'complete': - var url = explorerLink(props.response.transaction, parseInt('1')) - - return h('.flex-column.pointer', { - style: { - width: '200px', - overflow: 'hidden', - }, - onClick: () => global.platform.openWindow({ url }), - }, [ - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, 'From ShapeShift'), - h('div', formatDate(props.time)), - h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - width: '100%', - }, - }, addressSummary(props.response.transaction)), - ]) - - case 'failed': - return h('span.error', '(Failed)') - default: - return '' - } -} diff --git a/responsive-ui/app/components/tab-bar.js b/responsive-ui/app/components/tab-bar.js deleted file mode 100644 index 6295e7dd9..000000000 --- a/responsive-ui/app/components/tab-bar.js +++ /dev/null @@ -1,36 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = TabBar - -inherits(TabBar, Component) -function TabBar () { - Component.call(this) -} - -TabBar.prototype.render = function () { - const props = this.props - const state = this.state || {} - const { tabs = [], defaultTab, tabSelected } = props - const { subview = defaultTab } = state - - return ( - h('.flex-row.space-around.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - paddingTop: '4px', - }, - }, tabs.map((tab) => { - const { key, content } = tab - return h(subview === key ? '.activeForm' : '.inactiveForm.pointer', { - onClick: () => { - this.setState({ subview: key }) - tabSelected(key) - }, - }, content) - })) - ) -} - diff --git a/responsive-ui/app/components/template.js b/responsive-ui/app/components/template.js deleted file mode 100644 index b6ed8eaa0..000000000 --- a/responsive-ui/app/components/template.js +++ /dev/null @@ -1,18 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = NewComponent - -inherits(NewComponent, Component) -function NewComponent () { - Component.call(this) -} - -NewComponent.prototype.render = function () { - const props = this.props - - return ( - h('span', props.message) - ) -} diff --git a/responsive-ui/app/components/token-cell.js b/responsive-ui/app/components/token-cell.js deleted file mode 100644 index 19d7139bb..000000000 --- a/responsive-ui/app/components/token-cell.js +++ /dev/null @@ -1,72 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const Identicon = require('./identicon') -const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') - -module.exports = TokenCell - -inherits(TokenCell, Component) -function TokenCell () { - Component.call(this) -} - -TokenCell.prototype.render = function () { - const props = this.props - const { address, symbol, string, network, userAddress } = props - - return ( - h('li.token-cell', { - style: { cursor: network === '1' ? 'pointer' : 'default' }, - onClick: this.view.bind(this, address, userAddress, network), - }, [ - - h(Identicon, { - diameter: 50, - address, - network, - }), - - h('h3', `${string || 0} ${symbol}`), - - h('span', { style: { flex: '1 0 auto' } }), - - /* - h('button', { - onClick: this.send.bind(this, address), - }, 'SEND'), - */ - - ]) - ) -} - -TokenCell.prototype.send = function (address, event) { - event.preventDefault() - event.stopPropagation() - const url = tokenFactoryFor(address) - if (url) { - navigateTo(url) - } -} - -TokenCell.prototype.view = function (address, userAddress, network, event) { - const url = etherscanLinkFor(address, userAddress, network) - if (url) { - navigateTo(url) - } -} - -function navigateTo (url) { - global.platform.openWindow({ url }) -} - -function etherscanLinkFor (tokenAddress, address, network) { - const prefix = prefixForNetwork(network) - return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}` -} - -function tokenFactoryFor (tokenAddress) { - return `https://tokenfactory.surge.sh/#/token/${tokenAddress}` -} - diff --git a/responsive-ui/app/components/token-list.js b/responsive-ui/app/components/token-list.js deleted file mode 100644 index 20cfa897e..000000000 --- a/responsive-ui/app/components/token-list.js +++ /dev/null @@ -1,192 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const TokenTracker = require('eth-token-tracker') -const TokenCell = require('./token-cell.js') -const normalizeAddress = require('eth-sig-util').normalize - -const defaultTokens = [] -const contracts = require('eth-contract-metadata') -for (const address in contracts) { - const contract = contracts[address] - if (contract.erc20) { - contract.address = address - defaultTokens.push(contract) - } -} - -module.exports = TokenList - -inherits(TokenList, Component) -function TokenList () { - this.state = { - tokens: [], - isLoading: true, - network: null, - } - Component.call(this) -} - -TokenList.prototype.render = function () { - const state = this.state - const { tokens, isLoading, error } = state - const { userAddress, network } = this.props - - if (isLoading) { - return this.message('Loading') - } - - if (error) { - log.error(error) - return this.message('There was a problem loading your token balances.') - } - - const tokenViews = tokens.map((tokenData) => { - tokenData.network = network - tokenData.userAddress = userAddress - return h(TokenCell, tokenData) - }) - - return h('div', [ - h('ol', { - style: { - height: '260px', - overflowY: 'auto', - display: 'flex', - flexDirection: 'column', - }, - }, [ - h('style', ` - - li.token-cell { - display: flex; - flex-direction: row; - align-items: center; - padding: 10px; - } - - li.token-cell > h3 { - margin-left: 12px; - } - - li.token-cell:hover { - background: white; - cursor: pointer; - } - - `), - ...tokenViews, - tokenViews.length ? null : this.message('No Tokens Found.'), - ]), - this.addTokenButtonElement(), - ]) -} - -TokenList.prototype.addTokenButtonElement = function () { - return h('div', [ - h('div.footer.hover-white.pointer', { - key: 'reveal-account-bar', - onClick: () => { - this.props.addToken() - }, - style: { - display: 'flex', - height: '40px', - padding: '10px', - justifyContent: 'center', - alignItems: 'center', - }, - }, [ - h('i.fa.fa-plus.fa-lg'), - ]), - ]) -} - -TokenList.prototype.message = function (body) { - return h('div', { - style: { - display: 'flex', - height: '250px', - alignItems: 'center', - justifyContent: 'center', - padding: '30px', - }, - }, body) -} - -TokenList.prototype.componentDidMount = function () { - this.createFreshTokenTracker() -} - -TokenList.prototype.createFreshTokenTracker = function () { - if (this.tracker) { - // Clean up old trackers when refreshing: - this.tracker.stop() - this.tracker.removeListener('update', this.balanceUpdater) - this.tracker.removeListener('error', this.showError) - } - - if (!global.ethereumProvider) return - const { userAddress } = this.props - this.tracker = new TokenTracker({ - userAddress, - provider: global.ethereumProvider, - tokens: uniqueMergeTokens(defaultTokens, this.props.tokens), - pollingInterval: 8000, - }) - - - // Set up listener instances for cleaning up - this.balanceUpdater = this.updateBalances.bind(this) - this.showError = (error) => { - this.setState({ error, isLoading: false }) - } - this.tracker.on('update', this.balanceUpdater) - this.tracker.on('error', this.showError) - - this.tracker.updateBalances() - .then(() => { - this.updateBalances(this.tracker.serialize()) - }) - .catch((reason) => { - log.error(`Problem updating balances`, reason) - this.setState({ isLoading: false }) - }) -} - -TokenList.prototype.componentWillUpdate = function (nextProps) { - if (nextProps.network === 'loading') return - const oldNet = this.props.network - const newNet = nextProps.network - - if (oldNet && newNet && newNet !== oldNet) { - this.setState({ isLoading: true }) - this.createFreshTokenTracker() - } -} - -TokenList.prototype.updateBalances = function (tokens) { - const heldTokens = tokens.filter(token => { - return token.balance !== '0' && token.string !== '0.000' - }) - this.setState({ tokens: heldTokens, isLoading: false }) -} - -TokenList.prototype.componentWillUnmount = function () { - if (!this.tracker) return - this.tracker.stop() -} - -function uniqueMergeTokens (tokensA, tokensB) { - const uniqueAddresses = [] - const result = [] - tokensA.concat(tokensB).forEach((token) => { - const normal = normalizeAddress(token.address) - if (!uniqueAddresses.includes(normal)) { - uniqueAddresses.push(normal) - result.push(token) - } - }) - return result -} - diff --git a/responsive-ui/app/components/tooltip.js b/responsive-ui/app/components/tooltip.js deleted file mode 100644 index edbc074bb..000000000 --- a/responsive-ui/app/components/tooltip.js +++ /dev/null @@ -1,22 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ReactTooltip = require('react-tooltip-component') - -module.exports = Tooltip - -inherits(Tooltip, Component) -function Tooltip () { - Component.call(this) -} - -Tooltip.prototype.render = function () { - const props = this.props - const { position, title, children } = props - - return h(ReactTooltip, { - position: position || 'left', - title, - fixed: false, - }, children) -} diff --git a/responsive-ui/app/components/transaction-list-item-icon.js b/responsive-ui/app/components/transaction-list-item-icon.js deleted file mode 100644 index 431054340..000000000 --- a/responsive-ui/app/components/transaction-list-item-icon.js +++ /dev/null @@ -1,68 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const Tooltip = require('./tooltip') - -const Identicon = require('./identicon') - -module.exports = TransactionIcon - -inherits(TransactionIcon, Component) -function TransactionIcon () { - Component.call(this) -} - -TransactionIcon.prototype.render = function () { - const { transaction, txParams, isMsg } = this.props - switch (transaction.status) { - case 'unapproved': - return h(!isMsg ? '.unapproved-tx-icon' : 'i.fa.fa-certificate.fa-lg') - - case 'rejected': - return h('i.fa.fa-exclamation-triangle.fa-lg.warning', { - style: { - width: '24px', - }, - }) - - case 'failed': - return h('i.fa.fa-exclamation-triangle.fa-lg.error', { - style: { - width: '24px', - }, - }) - - case 'submitted': - return h(Tooltip, { - title: 'Pending', - position: 'bottom', - }, [ - h('i.fa.fa-ellipsis-h', { - style: { - fontSize: '27px', - }, - }), - ]) - } - - if (isMsg) { - return h('i.fa.fa-certificate.fa-lg', { - style: { - width: '24px', - }, - }) - } - - if (txParams.to) { - return h(Identicon, { - diameter: 24, - address: txParams.to || transaction.hash, - }) - } else { - return h('i.fa.fa-file-text-o.fa-lg', { - style: { - width: '24px', - }, - }) - } -} diff --git a/responsive-ui/app/components/transaction-list-item.js b/responsive-ui/app/components/transaction-list-item.js deleted file mode 100644 index dbda66a31..000000000 --- a/responsive-ui/app/components/transaction-list-item.js +++ /dev/null @@ -1,165 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const EthBalance = require('./eth-balance') -const addressSummary = require('../util').addressSummary -const explorerLink = require('../../lib/explorer-link') -const CopyButton = require('./copyButton') -const vreme = new (require('vreme')) -const Tooltip = require('./tooltip') -const numberToBN = require('number-to-bn') - -const TransactionIcon = require('./transaction-list-item-icon') -const ShiftListItem = require('./shift-list-item') -module.exports = TransactionListItem - -inherits(TransactionListItem, Component) -function TransactionListItem () { - Component.call(this) -} - -TransactionListItem.prototype.render = function () { - const { transaction, network, conversionRate, currentCurrency } = this.props - if (transaction.key === 'shapeshift') { - if (network === '1') return h(ShiftListItem, transaction) - } - var date = formatDate(transaction.time) - - let isLinkable = false - const numericNet = parseInt(network) - isLinkable = numericNet === 1 || numericNet === 3 || numericNet === 4 || numericNet === 42 - - var isMsg = ('msgParams' in transaction) - var isTx = ('txParams' in transaction) - var isPending = transaction.status === 'unapproved' - let txParams - if (isTx) { - txParams = transaction.txParams - } else if (isMsg) { - txParams = transaction.msgParams - } - - const nonce = txParams.nonce ? numberToBN(txParams.nonce).toString(10) : '' - - const isClickable = ('hash' in transaction && isLinkable) || isPending - return ( - h(`.transaction-list-item.flex-row.flex-space-between${isClickable ? '.pointer' : ''}`, { - onClick: (event) => { - if (isPending) { - this.props.showTx(transaction.id) - } - event.stopPropagation() - if (!transaction.hash || !isLinkable) return - var url = explorerLink(transaction.hash, parseInt(network)) - global.platform.openWindow({ url }) - }, - style: { - padding: '20px 0', - }, - }, [ - - h('.identicon-wrapper.flex-column.flex-center.select-none', [ - h('.pop-hover', { - onClick: (event) => { - event.stopPropagation() - if (!isTx || isPending) return - var url = `https://metamask.github.io/eth-tx-viz/?tx=${transaction.hash}` - global.platform.openWindow({ url }) - }, - }, [ - h(TransactionIcon, { txParams, transaction, isTx, isMsg }), - ]), - ]), - - h(Tooltip, { - title: 'Transaction Number', - position: 'bottom', - }, [ - h('span', { - style: { - display: 'flex', - cursor: 'normal', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - padding: '10px', - }, - }, nonce), - ]), - - h('.flex-column', {style: {width: '200px', overflow: 'hidden'}}, [ - domainField(txParams), - h('div', date), - recipientField(txParams, transaction, isTx, isMsg), - ]), - - // Places a copy button if tx is successful, else places a placeholder empty div. - transaction.hash ? h(CopyButton, { value: transaction.hash }) : h('div', {style: { display: 'flex', alignItems: 'center', width: '26px' }}), - - isTx ? h(EthBalance, { - value: txParams.value, - conversionRate, - currentCurrency, - width: '55px', - shorten: true, - showFiat: false, - style: {fontSize: '15px'}, - }) : h('.flex-column'), - ]) - ) -} - -function domainField (txParams) { - return h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - overflow: 'hidden', - textOverflow: 'ellipsis', - width: '100%', - }, - }, [ - txParams.origin, - ]) -} - -function recipientField (txParams, transaction, isTx, isMsg) { - let message - - if (isMsg) { - message = 'Signature Requested' - } else if (txParams.to) { - message = addressSummary(txParams.to) - } else { - message = 'Contract Published' - } - - return h('div', { - style: { - fontSize: 'x-small', - color: '#ABA9AA', - }, - }, [ - message, - failIfFailed(transaction), - ]) -} - -function formatDate (date) { - return vreme.format(new Date(date), 'March 16 2014 14:30') -} - -function failIfFailed (transaction) { - if (transaction.status === 'rejected') { - return h('span.error', ' (Rejected)') - } - if (transaction.err) { - return h(Tooltip, { - title: transaction.err.message, - position: 'bottom', - }, [ - h('span.error', ' (Failed)'), - ]) - } -} diff --git a/responsive-ui/app/components/transaction-list.js b/responsive-ui/app/components/transaction-list.js deleted file mode 100644 index ae6aaec8c..000000000 --- a/responsive-ui/app/components/transaction-list.js +++ /dev/null @@ -1,83 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -const TransactionListItem = require('./transaction-list-item') - -module.exports = TransactionList - - -inherits(TransactionList, Component) -function TransactionList () { - Component.call(this) -} - -TransactionList.prototype.render = function () { - const { transactions, network, unapprovedMsgs, conversionRate } = this.props - - var shapeShiftTxList - if (network === '1') { - shapeShiftTxList = this.props.shapeShiftTxList - } - const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList) - .sort((a, b) => b.time - a.time) - - return ( - - h('section.transaction-list', { - style: { - height: '100%', - }, - }, [ - - h('style', ` - .transaction-list .transaction-list-item:not(:last-of-type) { - border-bottom: 1px solid #D4D4D4; - } - .transaction-list .transaction-list-item .ether-balance-label { - display: block !important; - font-size: small; - } - `), - - h('.tx-list', { - style: { - overflowY: 'auto', - height: '100%', - padding: '0 20px', - textAlign: 'center', - }, - }, [ - - txsToRender.length - ? txsToRender.map((transaction, i) => { - let key - switch (transaction.key) { - case 'shapeshift': - const { depositAddress, time } = transaction - key = `shift-tx-${depositAddress}-${time}-${i}` - break - default: - key = `tx-${transaction.id}-${i}` - } - return h(TransactionListItem, { - transaction, i, network, key, - conversionRate, - showTx: (txId) => { - this.props.viewPendingTx(txId) - }, - }) - }) - : h('.flex-center', { - style: { - flexDirection: 'column', - height: '100%', - }, - }, [ - 'No transaction history.', - ]), - ]), - ]) - ) -} - diff --git a/responsive-ui/app/conf-tx.js b/responsive-ui/app/conf-tx.js deleted file mode 100644 index 747d3ce2b..000000000 --- a/responsive-ui/app/conf-tx.js +++ /dev/null @@ -1,213 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') -const NetworkIndicator = require('./components/network') -const txHelper = require('../lib/tx-helper') -const isPopupOrNotification = require('../../app/scripts/lib/is-popup-or-notification') - -const PendingTx = require('./components/pending-tx') -const PendingMsg = require('./components/pending-msg') -const PendingPersonalMsg = require('./components/pending-personal-msg') -const Loading = require('./components/loading') - -module.exports = connect(mapStateToProps)(ConfirmTxScreen) - -function mapStateToProps (state) { - return { - identities: state.metamask.identities, - accounts: state.metamask.accounts, - selectedAddress: state.metamask.selectedAddress, - unapprovedTxs: state.metamask.unapprovedTxs, - unapprovedMsgs: state.metamask.unapprovedMsgs, - unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs, - index: state.appState.currentView.context, - warning: state.appState.warning, - network: state.metamask.network, - provider: state.metamask.provider, - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - blockGasLimit: state.metamask.currentBlockGasLimit, - } -} - -inherits(ConfirmTxScreen, Component) -function ConfirmTxScreen () { - Component.call(this) -} - -ConfirmTxScreen.prototype.render = function () { - const props = this.props - const { network, provider, unapprovedTxs, currentCurrency, - unapprovedMsgs, unapprovedPersonalMsgs, conversionRate, blockGasLimit } = props - - var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) - - var txData = unconfTxList[props.index] || {} - var txParams = txData.params || {} - var isNotification = isPopupOrNotification() === 'notification' - - - log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`) - if (unconfTxList.length === 0) return h(Loading, { isLoading: true }) - - return ( - - h('.flex-column.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - !isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: this.goHome.bind(this), - }) : null, - h('h2.page-subtitle', 'Confirm Transaction'), - isNotification ? h(NetworkIndicator, { - network: network, - provider: provider, - }) : null, - ]), - - h('h3', { - style: { - alignSelf: 'center', - display: unconfTxList.length > 1 ? 'block' : 'none', - }, - }, [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - style: { - display: props.index === 0 ? 'none' : 'inline-block', - }, - onClick: () => props.dispatch(actions.previousTx()), - }), - ` ${props.index + 1} of ${unconfTxList.length} `, - h('i.fa.fa-arrow-right.fa-lg.cursor-pointer', { - style: { - display: props.index + 1 === unconfTxList.length ? 'none' : 'inline-block', - }, - onClick: () => props.dispatch(actions.nextTx()), - }), - ]), - - warningIfExists(props.warning), - - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - - currentTxView({ - // Properties - txData: txData, - key: txData.id, - selectedAddress: props.selectedAddress, - accounts: props.accounts, - identities: props.identities, - conversionRate, - currentCurrency, - blockGasLimit, - // Actions - buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), - sendTransaction: this.sendTransaction.bind(this), - cancelTransaction: this.cancelTransaction.bind(this, txData), - signMessage: this.signMessage.bind(this, txData), - signPersonalMessage: this.signPersonalMessage.bind(this, txData), - cancelMessage: this.cancelMessage.bind(this, txData), - cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), - }), - - ]), - ]) - ) -} - -function currentTxView (opts) { - log.info('rendering current tx view') - const { txData } = opts - const { txParams, msgParams, type } = txData - - if (txParams) { - log.debug('txParams detected, rendering pending tx') - return h(PendingTx, opts) - } else if (msgParams) { - log.debug('msgParams detected, rendering pending msg') - - if (type === 'eth_sign') { - log.debug('rendering eth_sign message') - return h(PendingMsg, opts) - } else if (type === 'personal_sign') { - log.debug('rendering personal_sign message') - return h(PendingPersonalMsg, opts) - } - } -} - -ConfirmTxScreen.prototype.buyEth = function (address, event) { - event.preventDefault() - this.props.dispatch(actions.buyEthView(address)) -} - -ConfirmTxScreen.prototype.sendTransaction = function (txData, event) { - this.stopPropagation(event) - this.props.dispatch(actions.updateAndApproveTx(txData)) -} - -ConfirmTxScreen.prototype.cancelTransaction = function (txData, event) { - this.stopPropagation(event) - event.preventDefault() - this.props.dispatch(actions.cancelTx(txData)) -} - -ConfirmTxScreen.prototype.signMessage = function (msgData, event) { - log.info('conf-tx.js: signing message') - var params = msgData.msgParams - params.metamaskId = msgData.id - this.stopPropagation(event) - this.props.dispatch(actions.signMsg(params)) -} - -ConfirmTxScreen.prototype.stopPropagation = function (event) { - if (event.stopPropagation) { - event.stopPropagation() - } -} - -ConfirmTxScreen.prototype.signPersonalMessage = function (msgData, event) { - log.info('conf-tx.js: signing personal message') - var params = msgData.msgParams - params.metamaskId = msgData.id - this.stopPropagation(event) - this.props.dispatch(actions.signPersonalMsg(params)) -} - -ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) { - log.info('canceling message') - this.stopPropagation(event) - this.props.dispatch(actions.cancelMsg(msgData)) -} - -ConfirmTxScreen.prototype.cancelPersonalMessage = function (msgData, event) { - log.info('canceling personal message') - this.stopPropagation(event) - this.props.dispatch(actions.cancelPersonalMsg(msgData)) -} - -ConfirmTxScreen.prototype.goHome = function (event) { - this.stopPropagation(event) - this.props.dispatch(actions.goHome()) -} - -function warningIfExists (warning) { - if (warning && - // Do not display user rejections on this screen: - warning.indexOf('User denied transaction signature') === -1) { - return h('.error', { - style: { - margin: 'auto', - }, - }, warning) - } -} diff --git a/responsive-ui/app/config.js b/responsive-ui/app/config.js deleted file mode 100644 index 62785c49b..000000000 --- a/responsive-ui/app/config.js +++ /dev/null @@ -1,211 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') -const currencies = require('./conversion.json').rows -const validUrl = require('valid-url') -const copyToClipboard = require('copy-to-clipboard') - -module.exports = connect(mapStateToProps)(ConfigScreen) - -function mapStateToProps (state) { - return { - metamask: state.metamask, - warning: state.appState.warning, - } -} - -inherits(ConfigScreen, Component) -function ConfigScreen () { - Component.call(this) -} - -ConfigScreen.prototype.render = function () { - var state = this.props - var metamaskState = state.metamask - var warning = state.warning - - return ( - h('.flex-column.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: (event) => { - state.dispatch(actions.goHome()) - }, - }), - h('h2.page-subtitle', 'Settings'), - ]), - - h('.error', { - style: { - display: warning ? 'block' : 'none', - padding: '0 20px', - textAlign: 'center', - }, - }, warning), - - // conf view - h('.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-space-around', { - style: { - padding: '20px', - }, - }, [ - - currentProviderDisplay(metamaskState), - - h('div', { style: {display: 'flex'} }, [ - h('input#new_rpc', { - placeholder: 'New RPC URL', - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - onKeyPress (event) { - if (event.key === 'Enter') { - var element = event.target - var newRpc = element.value - rpcValidation(newRpc, state) - } - }, - }), - h('button', { - style: { - alignSelf: 'center', - }, - onClick (event) { - event.preventDefault() - var element = document.querySelector('input#new_rpc') - var newRpc = element.value - rpcValidation(newRpc, state) - }, - }, 'Save'), - ]), - - h('hr.horizontal-line'), - - currentConversionInformation(metamaskState, state), - - h('hr.horizontal-line'), - - h('div', { - style: { - marginTop: '20px', - }, - }, [ - h('p', { - style: { - fontFamily: 'Montserrat Light', - fontSize: '13px', - }, - }, `State logs contain your public account addresses and sent transactions.`), - h('br'), - h('button', { - style: { - alignSelf: 'center', - }, - onClick (event) { - copyToClipboard(window.logState()) - }, - }, 'Copy State Logs'), - ]), - - h('hr.horizontal-line'), - - h('div', { - style: { - marginTop: '20px', - }, - }, [ - h('button', { - style: { - alignSelf: 'center', - }, - onClick (event) { - event.preventDefault() - state.dispatch(actions.revealSeedConfirmation()) - }, - }, 'Reveal Seed Words'), - ]), - - ]), - ]), - ]) - ) -} - -function rpcValidation (newRpc, state) { - if (validUrl.isWebUri(newRpc)) { - state.dispatch(actions.setRpcTarget(newRpc)) - } else { - var appendedRpc = `http://${newRpc}` - if (validUrl.isWebUri(appendedRpc)) { - state.dispatch(actions.displayWarning('URIs require the appropriate HTTP/HTTPS prefix.')) - } else { - state.dispatch(actions.displayWarning('Invalid RPC URI')) - } - } -} - -function currentConversionInformation (metamaskState, state) { - var currentCurrency = metamaskState.currentCurrency - var conversionDate = metamaskState.conversionDate - return h('div', [ - h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, 'Current Conversion'), - h('span', {style: { fontWeight: 'bold', paddingRight: '10px', fontSize: '13px'}}, `Updated ${Date(conversionDate)}`), - h('select#currentCurrency', { - onChange (event) { - event.preventDefault() - var element = document.getElementById('currentCurrency') - var newCurrency = element.value - state.dispatch(actions.setCurrentCurrency(newCurrency)) - }, - defaultValue: currentCurrency, - }, currencies.map((currency) => { - return h('option', {key: currency.code, value: currency.code}, `${currency.code} - ${currency.name}`) - }) - ), - ]) -} - -function currentProviderDisplay (metamaskState) { - var provider = metamaskState.provider - var title, value - - switch (provider.type) { - - case 'mainnet': - title = 'Current Network' - value = 'Main Ethereum Network' - break - - case 'ropsten': - title = 'Current Network' - value = 'Ropsten Test Network' - break - - case 'kovan': - title = 'Current Network' - value = 'Kovan Test Network' - break - - case 'rinkeby': - title = 'Current Network' - value = 'Rinkeby Test Network' - break - - default: - title = 'Current RPC' - value = metamaskState.provider.rpcTarget - } - - return h('div', [ - h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, title), - h('span', value), - ]) -} diff --git a/responsive-ui/app/conversion.json b/responsive-ui/app/conversion.json deleted file mode 100644 index 155ffc4fc..000000000 --- a/responsive-ui/app/conversion.json +++ /dev/null @@ -1,207 +0,0 @@ -{ - "rows": [ - { - "code": "REP", - "name": "Augur", - "statuses": [ - "primary" - ] - }, - { - "code": "BCN", - "name": "Bytecoin", - "statuses": [ - "primary" - ] - }, - { - "code": "BTC", - "name": "Bitcoin", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "BTS", - "name": "BitShares", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "BLK", - "name": "Blackcoin", - "statuses": [ - "primary" - ] - }, - { - "code": "GBP", - "name": "British Pound Sterling", - "statuses": [ - "secondary" - ] - }, - { - "code": "CAD", - "name": "Canadian Dollar", - "statuses": [ - "secondary" - ] - }, - { - "code": "CNY", - "name": "Chinese Yuan", - "statuses": [ - "secondary" - ] - }, - { - "code": "DSH", - "name": "Dashcoin", - "statuses": [ - "primary" - ] - }, - { - "code": "DOGE", - "name": "Dogecoin", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "ETC", - "name": "Ethereum Classic", - "statuses": [ - "primary" - ] - }, - { - "code": "EUR", - "name": "Euro", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "GNO", - "name": "GNO", - "statuses": [ - "primary" - ] - }, - { - "code": "GNT", - "name": "GNT", - "statuses": [ - "primary" - ] - }, - { - "code": "JPY", - "name": "Japanese Yen", - "statuses": [ - "secondary" - ] - }, - { - "code": "LTC", - "name": "Litecoin", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "MAID", - "name": "MaidSafeCoin", - "statuses": [ - "primary" - ] - }, - { - "code": "XEM", - "name": "NEM", - "statuses": [ - "primary" - ] - }, - { - "code": "XLM", - "name": "Stellar", - "statuses": [ - "primary" - ] - }, - { - "code": "XMR", - "name": "Monero", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "XRP", - "name": "Ripple", - "statuses": [ - "primary" - ] - }, - { - "code": "RUR", - "name": "Ruble", - "statuses": [ - "secondary" - ] - }, - { - "code": "STEEM", - "name": "Steem", - "statuses": [ - "primary" - ] - }, - { - "code": "STRAT", - "name": "STRAT", - "statuses": [ - "primary" - ] - }, - { - "code": "UAH", - "name": "Ukrainian Hryvnia", - "statuses": [ - "secondary" - ] - }, - { - "code": "USD", - "name": "US Dollar", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "WAVES", - "name": "WAVES", - "statuses": [ - "primary" - ] - }, - { - "code": "ZEC", - "name": "Zcash", - "statuses": [ - "primary" - ] - } - ] -} diff --git a/responsive-ui/app/css/debug.css b/responsive-ui/app/css/debug.css deleted file mode 100644 index 3e125bcd4..000000000 --- a/responsive-ui/app/css/debug.css +++ /dev/null @@ -1,21 +0,0 @@ -/* -debug / dev -*/ - -#app-content { - border: 2px solid green; -} - -#design-container { - position: absolute; - left: 360px; - top: -42px; - width: calc(100vw - 360px); - height: 100vh; - overflow: scroll; -} - -#design-container img { - width: 2000px; - margin-right: 600px; -} \ No newline at end of file diff --git a/responsive-ui/app/css/fonts.css b/responsive-ui/app/css/fonts.css deleted file mode 100644 index 3b9f581b9..000000000 --- a/responsive-ui/app/css/fonts.css +++ /dev/null @@ -1,36 +0,0 @@ -@import url(https://fonts.googleapis.com/css?family=Roboto:300,500); -@import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css); - -@font-face { - font-family: 'Montserrat Regular'; - src: url('/fonts/Montserrat/Montserrat-Regular.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-Regular.ttf') format('truetype'); - font-weight: normal; - font-style: normal; - font-size: 'small'; - -} - -@font-face { - font-family: 'Montserrat Bold'; - src: url('/fonts/Montserrat/Montserrat-Bold.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-Bold.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'Montserrat Light'; - src: url('/fonts/Montserrat/Montserrat-Light.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-Light.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'Montserrat UltraLight'; - src: url('/fonts/Montserrat/Montserrat-UltraLight.woff') format('woff'); - src: url('/fonts/Montserrat/Montserrat-UltraLight.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} diff --git a/responsive-ui/app/css/index.css b/responsive-ui/app/css/index.css deleted file mode 100644 index 2ae92bbd6..000000000 --- a/responsive-ui/app/css/index.css +++ /dev/null @@ -1,676 +0,0 @@ -/* -faint orange (textfield shades) #FAF6F0 -light orange (button shades): #F5C26D -dark orange (text): #F5A623 -borders/font/any gray: #4A4A4A -*/ - -/* -application specific styles -*/ - -* { - box-sizing: border-box; -} - -html, body { - font-family: 'Montserrat Regular', Arial; - color: #4D4D4D; - font-weight: 300; - line-height: 1.4em; - background: #F7F7F7; - width: 100%; - height: 100%; - margin: 0; - padding: 0; -} - -.css-transition-group { - flex: 1; - height: 100%; -} - -input:focus, textarea:focus { - outline: none; -} - -#app-content { - overflow-x: hidden; - min-width: 357px; - height: 100%; -} - -button, input[type="submit"] { - font-family: 'Montserrat Bold'; - outline: none; - cursor: pointer; - padding: 8px 12px; - border: none; - color: white; - transform-origin: center center; - transition: transform 50ms ease-in; - /* default orange */ - background: rgba(247, 134, 28, 1); - box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); -} - -.btn-green, input[type="submit"].btn-green { - background: rgba(106, 195, 96, 1); - box-shadow: 0px 3px 6px rgba(106, 195, 96, 0.36); -} - -.btn-red { - background: rgba(254, 35, 17, 1); - box-shadow: 0px 3px 6px rgba(254, 35, 17, 0.36); -} - -button[disabled], input[type="submit"][disabled] { - cursor: not-allowed; - background: rgba(197, 197, 197, 1); - box-shadow: 0px 3px 6px rgba(197, 197, 197, 0.36); -} - -button.spaced { - margin: 2px; -} - -button:not([disabled]):hover, input[type="submit"]:not([disabled]):hover { - transform: scale(1.1); -} -button:not([disabled]):active, input[type="submit"]:not([disabled]):active { - transform: scale(0.95); -} - -a { - text-decoration: none; - color: inherit; -} - -a:hover{ - color: #df6b0e; -} - -/* -app -*/ - -.active { - color: #909090; -} - -button.primary { - padding: 8px 12px; - background: #F7861C; - box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); - color: white; - font-size: 1.1em; - font-family: 'Montserrat Regular'; - text-transform: uppercase; -} - -button.btn-thin { - border: 1px solid; - border-color: #4D4D4D; - color: #4D4D4D; - background: rgb(255, 174, 41); - border-radius: 4px; - min-width: 200px; - margin: 12px 0; - padding: 6px; - font-size: 13px; -} - -.app-header { - padding: 6px 8px; -} - -.app-header h1 { - font-family: 'Montserrat Regular'; - text-transform: uppercase; - color: #AEAEAE; -} - -h2.page-subtitle { - font-family: 'Montserrat Regular'; - text-transform: uppercase; - color: #AEAEAE; - font-size: 1em; - margin: 12px; -} - -.app-primary { - -} - -.app-footer { - padding-bottom: 10px; - align-items: center; -} - -.identicon { - height: 46px; - width: 46px; - background-size: cover; - border-radius: 100%; - border: 3px solid gray; -} - -textarea.twelve-word-phrase { - padding: 12px; - width: 300px; - height: 140px; - font-size: 16px; - background: white; - resize: none; -} - -.network-indicator { - display: flex; - align-items: center; - font-size: 0.6em; - -} - -.network-name { - width: 5.2em; - line-height: 9px; - text-rendering: geometricPrecision; -} - -.check { - margin-left: 7px; - color: #F7861C; - flex: 1 0 auto; - display: flex; - justify-content: flex-end; -} -/* -app sections -*/ - -/* initialize */ - -.initialize-screen hr { - width: 60px; - margin: 12px; - border-color: #F7861C; - border-style: solid; -} - -.initialize-screen label { - margin-top: 20px; -} - -.initialize-screen button.create-vault { - margin-top: 40px; -} - -.initialize-screen .warning { - font-size: 14px; - margin: 0 16px; -} - -/* unlock */ -.error { - color: #E20202; -} - -.warning { - color: #FFAE00; -} - -.lock { - width: 50px; - height: 50px; -} - -.lock.locked { - transform: scale(1.5); - opacity: 0.0; - transition: opacity 400ms ease-in, transform 400ms ease-in; -} -.lock.unlocked { - transform: scale(1); - opacity: 1; - transition: opacity 500ms ease-out, transform 500ms ease-out, background 200ms ease-in; -} - -.lock.locked .lock-top { - transform: scaleX(1) translateX(0); - transition: transform 250ms ease-in; -} -.lock.unlocked .lock-top { - transform: scaleX(-1) translateX(-12px); - transition: transform 250ms ease-in; -} -.lock.unlocked:hover { - border-radius: 4px; - background: #e5e5e5; - border: 1px solid #b1b1b1; -} -.lock.unlocked:active { - background: #c3c3c3; -} - -.section-title .fa-arrow-left { - margin: -2px 8px 0px -8px; -} - -.unlock-screen #metamask-mascot-container { - margin-top: 24px; -} - -.unlock-screen h1 { - margin-top: -28px; - margin-bottom: 42px; -} - -.unlock-screen input[type=password] { - width: 260px; - /*height: 36px; - margin-bottom: 24px; - padding: 8px;*/ -} - -.sizing-input{ - font-size: 14px; - height: 30px; - padding-left: 5px; -} -.editable-label{ - display: flex; -} -/* Webkit */ -.unlock-screen input::-webkit-input-placeholder { - text-align: center; - font-size: 1.2em; -} -/* Firefox 18- */ -.unlock-screen input:-moz-placeholder { - text-align: center; - font-size: 1.2em; -} -/* Firefox 19+ */ -.unlock-screen input::-moz-placeholder { - text-align: center; - font-size: 1.2em; -} -/* IE */ -.unlock-screen input:-ms-input-placeholder { - text-align: center; - font-size: 1.2em; -} - -input.large-input, textarea.large-input { - /*margin-bottom: 24px;*/ - padding: 8px; -} - -input.large-input { - height: 36px; -} - -.letter-spacey { - letter-spacing: 0.1em; -} - - - -/* accounts */ - -.accounts-section { - margin: 0 0px; -} - -.accounts-section .horizontal-line { - margin: 0px 18px; -} - -.accounts-list-option { - height: 120px; -} - -.accounts-list-option .identicon-wrapper { - width: 100px; -} - -.unconftx-link { - margin-top: 24px; - cursor: pointer; -} - -.unconftx-link .fa-arrow-right { - margin: 0px -8px 0px 8px; -} - -/* identity panel */ - -.identity-panel { - font-weight: 500; -} - -.identity-panel .identicon-wrapper { - margin: 4px; - margin-top: 8px; - display: flex; - align-items: center; -} - -.identity-panel .identicon-wrapper span { - margin: 0 auto; -} - -.identity-panel .identity-data { - margin: 8px 8px 8px 18px; -} - -.identity-panel i { - margin-top: 32px; - margin-right: 6px; - color: #B9B9B9; -} - -.identity-panel .arrow-right { - padding-left: 18px; - width: 42px; - min-width: 18px; - height: 100%; -} - -.identity-copy.flex-column { - flex: 0.25 0 auto; - justify-content: center; -} - -/* accounts screen */ - -.identity-section { - -} - -.identity-section .identity-panel { - background: #E9E9E9; - border-bottom: 1px solid #B1B1B1; - cursor: pointer; -} - -.identity-section .identity-panel.selected { - background: white; - color: #F3C83E; -} - -.identity-section .identity-panel.selected .identicon { - border-color: orange; -} - -.identity-section .accounts-list-option:hover, -.identity-section .accounts-list-option.selected { - background:white; -} - -/* account detail screen */ - -.account-detail-section { - display: flex; - flex-wrap: wrap; -} -.name-label{ - -} - -.unapproved-tx-icon { - height: 16px; - width: 16px; - background: rgb(47, 174, 244); - border-color: #AEAEAE; - border-radius: 13px; -} - -.edit-text { - height: 100%; - visibility: hidden; -} -.editing-label { - display: flex; - justify-content: flex-start; - margin-left: 50px; - margin-bottom: 2px; - font-size: 11px; - text-rendering: geometricPrecision; - color: #F7861C; -} -.name-label:hover .edit-text { - visibility: visible; -} -/* tx confirm */ - -.unconftx-section input[type=password] { - height: 22px; - padding: 2px; - margin: 12px; - margin-bottom: 24px; - border-radius: 4px; - border: 2px solid #F3C83E; - background: #FAF6F0; -} - -/* Send Screen */ - -.send-screen { - -} - -.send-screen section { - margin: 8px 16px; -} - -.send-screen input { - width: 100%; - font-size: 12px; -} - -/* Ether Balance Widget */ - -.ether-balance-amount { - color: #F7861C; -} - -.ether-balance-label { - color: #ABA9AA; -} - -/* Info screen */ -.info-gray{ - font-family: 'Montserrat Regular'; - text-transform: uppercase; - color: #AEAEAE; -} - -.icon-size{ - width: 20px; -} - -.info{ - font-family: 'Montserrat Regular', Arial; - padding-bottom: 10px; - display: inline-block; - padding-left: 5px; -} - -/* buy eth warning screen */ -.custom-radios { - justify-content: space-around; - align-items: center; -} - - -.custom-radio-selected { - width: 17px; - height: 17px; - border: solid; - border-style: double; - border-radius: 15px; - border-width: 5px; - background: rgba(247, 134, 28, 1); - border-color: #F7F7F7; -} - -.custom-radio-inactive { - width: 14px; - height: 14px; - border: solid; - border-width: 1px; - border-radius: 24px; - border-color: #AEAEAE; -} - -.radio-titles { - color: rgba(247, 134, 28, 1); -} - -.radio-titles-subtext { - -} - -.selected-exchange { - -} - -.buy-radio { - -} - -.eth-warning{ - transition: opacity 400ms ease-in, transform 400ms ease-in; -} - -.buy-subview{ - transition: opacity 400ms ease-in, transform 400ms ease-in; -} - -.input-container:hover .edit-text{ - visibility: visible; -} - -.buy-inputs{ - font-family: 'Montserrat Light'; - font-size: 13px; - height: 20px; - background: transparent; - box-sizing: border-box; - border: solid; - border-color: transparent; - border-width: 0.5px; - border-radius: 2px; - -} -.input-container:hover .buy-inputs{ - box-sizing: inherit; - border: solid; - border-color: #F7861C; - border-width: 0.5px; - border-radius: 2px; -} - -.buy-inputs:focus{ - border: solid; - border-color: #F7861C; - border-width: 0.5px; - border-radius: 2px; -} - -.activeForm { - background: #F7F7F7; - border: none; - border-radius: 8px 8px 0px 0px; - width: 50%; - text-align: center; - padding-bottom: 4px; - -} - -.inactiveForm { - border: none; - border-radius: 8px 8px 0px 0px; - width: 50%; - text-align: center; - padding-bottom: 4px; -} - -.ex-coins { - font-family: 'Montserrat Regular'; - text-transform: uppercase; - text-align: center; - font-size: 33px; - width: 118px; - height: 42px; - padding: 1px; - color: #4D4D4D; -} - -.marketinfo{ - font-family: 'Montserrat light'; - color: #AEAEAE; - font-size: 15px; - line-height: 17px; -} - -#fromCoin::-webkit-calendar-picker-indicator { - display: none; -} - -#coinList { - width: 400px; - height: 500px; - overflow: scroll; -} - -.icon-control .fa-refresh{ - visibility: hidden; -} - -.icon-control:hover .fa-refresh{ - visibility: visible; -} - -.icon-control:hover .fa-chevron-right{ - visibility: hidden; -} - -.inactive { - color: #AEAEAE; -} - -.inactive button{ - background: #AEAEAE; - color: white; -} - -.ellip-address { - overflow: hidden; - text-overflow: ellipsis; - width: 5em; - font-size: 14px; - font-family: "Montserrat Light"; - margin-left: 5px; -} - -.qr-header { - font-size: 25px; - margin-top: 40px; -} - -.qr-message { - font-size: 12px; - color: #F7861C; -} - -div.message-container > div:first-child { - margin-top: 18px; - font-size: 15px; - color: #4D4D4D; -} - -.pop-hover:hover { - transform: scale(1.1); -} diff --git a/responsive-ui/app/css/lib.css b/responsive-ui/app/css/lib.css deleted file mode 100644 index b0ca958a2..000000000 --- a/responsive-ui/app/css/lib.css +++ /dev/null @@ -1,272 +0,0 @@ -/* color */ - -.color-orange { - color: #F7861C; -} - -.color-forest { - color: #0A5448; -} - -/* lib */ - -.full-width { - width: 100%; -} - -.full-height { - height: 100%; -} - -.flex-column { - display: flex; - flex-direction: column; -} - -.space-between { - justify-content: space-between; -} - -.space-around { - justify-content: space-around; -} - -.flex-column-bottom { - display: flex; - flex-direction: column-reverse; -} - -.flex-row { - display: flex; - flex-direction: row; -} - -.flex-space-between { - justify-content: space-between; -} - -.flex-space-around { - justify-content: space-around; -} - -.flex-right { - display: flex; - flex-direction: row; - justify-content: flex-end; -} - -.flex-left { - display: flex; - flex-direction: row; - justify-content: flex-start; -} - -.flex-fixed { - flex: none; -} - -.flex-basis-auto { - flex-basis: auto; -} - -.flex-grow { - flex: 1 1 auto; -} - -.flex-wrap { - flex-wrap: wrap; -} - -.flex-center { - display: flex; - justify-content: center; - align-items: center; -} - -.flex-justify-center { - justify-content: center; -} - -.flex-align-center { - align-items: center; -} - -.flex-self-end { - align-self: flex-end; -} - -.flex-self-stretch { - align-self: stretch; -} - -.flex-vertical { - flex-direction: column; -} - -.z-bump { - z-index: 1; -} - -.select-none { - cursor: inherit; - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; -} - -.pointer { - cursor: pointer; -} -.cursor-pointer { - cursor: pointer; - transform-origin: center center; - transition: transform 50ms ease-in-out; -} -.cursor-pointer:hover { - transform: scale(1.1); -} -.cursor-pointer:active { - transform: scale(0.95); -} - -.cursor-disabled { - cursor: not-allowed; -} - -.margin-bottom-sml { - margin-bottom: 20px; -} - -.margin-bottom-med { - margin-bottom: 40px; -} - -.margin-right-left { - margin: 0 20px; -} - -.bold { - font-weight: bold; -} - -.text-transform-uppercase { - text-transform: uppercase; -} - -.font-small { - font-size: 12px; -} - -.font-medium { - font-size: 1.2em; -} - -hr.horizontal-line { - display: block; - height: 1px; - border: 0; - border-top: 1px solid #ccc; - margin: 1em 0; - padding: 0; -} - -.hover-white:hover { - background: white; -} - -.red-dot { - background: #E91550; - color: white; - border-radius: 10px; -} - -.diamond { - transform: rotate(45deg); - background: #038789; -} - -.hollow-diamond { - transform: rotate(45deg); - border: 3px solid #690496; -} - -.golden-square { - background: #EBB33F; -} - -.pending-dot { - background: red; - left: 14px; - top: 14px; - color: white; - border-radius: 10px; - height: 20px; - min-width: 20px; - position: relative; - display: flex; - align-items: center; - justify-content: center; - padding: 4px; - z-index: 1; -} - -.keyring-label { - z-index: 1; - font-size: 11px; - background: rgba(255,0,0,0.8); - bottom: -47px; - color: white; - border-radius: 10px; - height: 20px; - min-width: 20px; - position: relative; - display: flex; - align-items: center; - justify-content: center; - padding: 4px; -} - -.ether-balance { - display: flex; - align-items: center; -} - -.tabSection { - min-width: 350px; -} - -.menu-icon { - display: inline-block; - height: 9px; - min-width: 9px; - margin: 13px; -} -.ether-icon { - background: rgb(0, 163, 68); - border-radius: 20px; -} -.testnet-icon { - background: #2465E1; -} - -.drop-menu-item { - display: flex; - align-items: center; -} - -.invisible { - visibility: hidden; -} - -.one-line-concat { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.critical-error { - text-align: center; - margin-top: 20px; - color: red; -} diff --git a/responsive-ui/app/css/reset.css b/responsive-ui/app/css/reset.css deleted file mode 100644 index 9ce89e8bc..000000000 --- a/responsive-ui/app/css/reset.css +++ /dev/null @@ -1,48 +0,0 @@ -/* http://meyerweb.com/eric/tools/css/reset/ - v2.0 | 20110126 - License: none (public domain) -*/ - -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; -} \ No newline at end of file diff --git a/responsive-ui/app/css/transitions.css b/responsive-ui/app/css/transitions.css deleted file mode 100644 index 393a944f9..000000000 --- a/responsive-ui/app/css/transitions.css +++ /dev/null @@ -1,42 +0,0 @@ -/* universal */ -.app-primary .main-enter { - position: absolute; - width: 100%; -} - -/* center position */ -.app-primary.from-right .main-enter-active, -.app-primary.from-left .main-enter-active { - overflow-x: hidden; - transform: translateX(0px); - transition: transform 300ms ease-in; -} - -/* exited positions */ -.app-primary.from-left .main-leave-active { - transform: translateX(360px); - transition: transform 300ms ease-in; -} -.app-primary.from-right .main-leave-active { - transform: translateX(-360px); - transition: transform 300ms ease-in; -} - -/* loader transitions */ -.loader-enter, .loader-leave-active { - opacity: 0.0; - transition: opacity 150 ease-in; -} -.loader-enter-active, .loader-leave { - opacity: 1.0; - transition: opacity 150 ease-in; -} - -/* entering positions */ -.app-primary.from-right .main-enter:not(.main-enter-active) { - transform: translateX(360px); -} -.app-primary.from-left .main-enter:not(.main-enter-active) { - transform: translateX(-360px); -} - diff --git a/responsive-ui/app/first-time/init-menu.js b/responsive-ui/app/first-time/init-menu.js deleted file mode 100644 index cc7c51bd3..000000000 --- a/responsive-ui/app/first-time/init-menu.js +++ /dev/null @@ -1,179 +0,0 @@ -const inherits = require('util').inherits -const EventEmitter = require('events').EventEmitter -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const Mascot = require('../components/mascot') -const actions = require('../actions') -const Tooltip = require('../components/tooltip') -const getCaretCoordinates = require('textarea-caret') - -module.exports = connect(mapStateToProps)(InitializeMenuScreen) - -inherits(InitializeMenuScreen, Component) -function InitializeMenuScreen () { - Component.call(this) - this.animationEventEmitter = new EventEmitter() -} - -function mapStateToProps (state) { - return { - // state from plugin - currentView: state.appState.currentView, - warning: state.appState.warning, - } -} - -InitializeMenuScreen.prototype.render = function () { - var state = this.props - - switch (state.currentView.name) { - - default: - return this.renderMenu(state) - - } -} - -// InitializeMenuScreen.prototype.componentDidMount = function(){ -// document.getElementById('password-box').focus() -// } - -InitializeMenuScreen.prototype.renderMenu = function (state) { - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - h(Mascot, { - animationEventEmitter: this.animationEventEmitter, - }), - - h('h1', { - style: { - fontSize: '1.3em', - textTransform: 'uppercase', - color: '#7F8082', - marginBottom: 10, - }, - }, 'MetaMask'), - - - h('div', [ - h('h3', { - style: { - fontSize: '0.8em', - color: '#7F8082', - display: 'inline', - }, - }, 'Encrypt your new DEN'), - - h(Tooltip, { - title: 'Your DEN is your password-encrypted storage within MetaMask.', - }, [ - h('i.fa.fa-question-circle.pointer', { - style: { - fontSize: '18px', - position: 'relative', - color: 'rgb(247, 134, 28)', - top: '2px', - marginLeft: '4px', - }, - }), - ]), - ]), - - h('span.in-progress-notification', state.warning), - - // password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box', - placeholder: 'New Password (min 8 chars)', - onInput: this.inputChanged.bind(this), - style: { - width: 260, - marginTop: 12, - }, - }), - - // confirm password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box-confirm', - placeholder: 'Confirm Password', - onKeyPress: this.createVaultOnEnter.bind(this), - onInput: this.inputChanged.bind(this), - style: { - width: 260, - marginTop: 16, - }, - }), - - - h('button.primary', { - onClick: this.createNewVaultAndKeychain.bind(this), - style: { - margin: 12, - }, - }, 'Create'), - - h('.flex-row.flex-center.flex-grow', [ - h('p.pointer', { - onClick: this.showRestoreVault.bind(this), - style: { - fontSize: '0.8em', - color: 'rgb(247, 134, 28)', - textDecoration: 'underline', - }, - }, 'Import Existing DEN'), - ]), - - ]) - ) -} - -InitializeMenuScreen.prototype.createVaultOnEnter = function (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.createNewVaultAndKeychain() - } -} - -InitializeMenuScreen.prototype.componentDidMount = function () { - document.getElementById('password-box').focus() -} - -InitializeMenuScreen.prototype.showRestoreVault = function () { - this.props.dispatch(actions.showRestoreVault()) -} - -InitializeMenuScreen.prototype.createNewVaultAndKeychain = function () { - var passwordBox = document.getElementById('password-box') - var password = passwordBox.value - var passwordConfirmBox = document.getElementById('password-box-confirm') - var passwordConfirm = passwordConfirmBox.value - - if (password.length < 8) { - this.warning = 'password not long enough' - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - if (password !== passwordConfirm) { - this.warning = 'passwords don\'t match' - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - - this.props.dispatch(actions.createNewVaultAndKeychain(password)) -} - -InitializeMenuScreen.prototype.inputChanged = function (event) { - // tell mascot to look at page action - var element = event.target - var boundingRect = element.getBoundingClientRect() - var coordinates = getCaretCoordinates(element, element.selectionEnd) - this.animationEventEmitter.emit('point', { - x: boundingRect.left + coordinates.left - element.scrollLeft, - y: boundingRect.top + coordinates.top - element.scrollTop, - }) -} diff --git a/responsive-ui/app/img/identicon-tardigrade.png b/responsive-ui/app/img/identicon-tardigrade.png deleted file mode 100644 index 1742a32b8..000000000 Binary files a/responsive-ui/app/img/identicon-tardigrade.png and /dev/null differ diff --git a/responsive-ui/app/img/identicon-walrus.png b/responsive-ui/app/img/identicon-walrus.png deleted file mode 100644 index d58fae912..000000000 Binary files a/responsive-ui/app/img/identicon-walrus.png and /dev/null differ diff --git a/responsive-ui/app/info.js b/responsive-ui/app/info.js deleted file mode 100644 index e8470de97..000000000 --- a/responsive-ui/app/info.js +++ /dev/null @@ -1,154 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') - -module.exports = connect(mapStateToProps)(InfoScreen) - -function mapStateToProps (state) { - return {} -} - -inherits(InfoScreen, Component) -function InfoScreen () { - Component.call(this) -} - -InfoScreen.prototype.render = function () { - const state = this.props - const version = global.platform.getVersion() - - return ( - h('.flex-column.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: (event) => { - state.dispatch(actions.goHome()) - }, - }), - h('h2.page-subtitle', 'Info'), - ]), - - // main view - h('.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-space-around', { - style: { - padding: '20px', - }, - }, [ - // current version number - - h('.info.info-gray', [ - h('div', 'Metamask'), - h('div', { - style: { - marginBottom: '10px', - }, - }, `Version: ${version}`), - ]), - - h('div', { - style: { - marginBottom: '5px', - }}, - [ - h('div', [ - h('a', { - href: 'https://metamask.io/privacy.html', - target: '_blank', - onClick (event) { this.navigateTo(event.target.href) }, - }, [ - h('div.info', 'Privacy Policy'), - ]), - ]), - h('div', [ - h('a', { - href: 'https://metamask.io/terms.html', - target: '_blank', - onClick (event) { this.navigateTo(event.target.href) }, - }, [ - h('div.info', 'Terms of Use'), - ]), - ]), - h('div', [ - h('a', { - href: 'https://metamask.io/attributions.html', - target: '_blank', - onClick (event) { this.navigateTo(event.target.href) }, - }, [ - h('div.info', 'Attributions'), - ]), - ]), - ] - ), - - h('hr', { - style: { - margin: '10px 0 ', - width: '7em', - }, - }), - - h('div', { - style: { - paddingLeft: '30px', - }}, - [ - h('div.fa.fa-github', [ - h('a.info', { - href: 'https://github.com/MetaMask/faq', - target: '_blank', - }, 'Need Help? Read our FAQ!'), - ]), - h('div', [ - h('a', { - href: 'https://metamask.io/', - target: '_blank', - }, [ - h('img.icon-size', { - src: 'images/icon-128.png', - style: { - // IE6-9 - filter: 'grayscale(100%)', - // Microsoft Edge and Firefox 35+ - WebkitFilter: 'grayscale(100%)', - }, - }), - h('div.info', 'Visit our web site'), - ]), - ]), - h('div.fa.fa-slack', [ - h('a.info', { - href: 'http://slack.metamask.io', - target: '_blank', - }, 'Join the conversation on Slack'), - ]), - - h('div.fa.fa-twitter', [ - h('a.info', { - href: 'https://twitter.com/metamask_io', - target: '_blank', - }, 'Follow us on Twitter'), - ]), - - h('div.fa.fa-envelope', [ - h('a.info', { - target: '_blank', - style: { width: '85vw' }, - href: 'mailto:help@metamask.io?subject=Feedback', - }, 'Email us!'), - ]), - ]), - ]), - ]), - ]) - ) -} - -InfoScreen.prototype.navigateTo = function (url) { - global.platform.openWindow({ url }) -} - diff --git a/responsive-ui/app/keychains/hd/create-vault-complete.js b/responsive-ui/app/keychains/hd/create-vault-complete.js deleted file mode 100644 index c32751fff..000000000 --- a/responsive-ui/app/keychains/hd/create-vault-complete.js +++ /dev/null @@ -1,76 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('../../actions') - -module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen) - -inherits(CreateVaultCompleteScreen, Component) -function CreateVaultCompleteScreen () { - Component.call(this) -} - -function mapStateToProps (state) { - return { - seed: state.appState.currentView.seedWords, - cachedSeed: state.metamask.seedWords, - } -} - -CreateVaultCompleteScreen.prototype.render = function () { - var state = this.props - var seed = state.seed || state.cachedSeed || '' - - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - // // subtitle and nav - // h('.section-title.flex-row.flex-center', [ - // h('h2.page-subtitle', 'Vault Created'), - // ]), - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginTop: 36, - marginBottom: 8, - width: '100%', - fontSize: '20px', - padding: 6, - }, - }, [ - 'Vault Created', - ]), - - h('div', { - style: { - fontSize: '1em', - marginTop: '10px', - textAlign: 'center', - }, - }, [ - h('span.error', 'These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret.'), - ]), - - h('textarea.twelve-word-phrase', { - readOnly: true, - value: seed, - }), - - h('button.primary', { - onClick: () => this.confirmSeedWords(), - style: { - margin: '24px', - fontSize: '0.9em', - }, - }, 'I\'ve copied it somewhere safe'), - ]) - ) -} - -CreateVaultCompleteScreen.prototype.confirmSeedWords = function () { - this.props.dispatch(actions.confirmSeedWords()) -} diff --git a/responsive-ui/app/keychains/hd/recover-seed/confirmation.js b/responsive-ui/app/keychains/hd/recover-seed/confirmation.js deleted file mode 100644 index 4ccbec9fc..000000000 --- a/responsive-ui/app/keychains/hd/recover-seed/confirmation.js +++ /dev/null @@ -1,118 +0,0 @@ -const inherits = require('util').inherits - -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('../../../actions') - -module.exports = connect(mapStateToProps)(RevealSeedConfirmation) - -inherits(RevealSeedConfirmation, Component) -function RevealSeedConfirmation () { - Component.call(this) -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -RevealSeedConfirmation.prototype.render = function () { - const props = this.props - - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginBottom: 24, - width: '100%', - fontSize: '20px', - padding: 6, - }, - }, [ - 'Reveal Seed Words', - ]), - - h('.div', { - style: { - display: 'flex', - flexDirection: 'column', - padding: '20px', - justifyContent: 'center', - }, - }, [ - - h('h4', 'Do not recover your seed words in a public place! These words can be used to steal all your accounts.'), - - // confirmation - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box', - placeholder: 'Enter your password to confirm', - onKeyPress: this.checkConfirmation.bind(this), - style: { - width: 260, - marginTop: '12px', - }, - }), - - h('.flex-row.flex-space-between', { - style: { - marginTop: 30, - width: '50%', - }, - }, [ - // cancel - h('button.primary', { - onClick: this.goHome.bind(this), - }, 'CANCEL'), - - // submit - h('button.primary', { - onClick: this.revealSeedWords.bind(this), - }, 'OK'), - - ]), - - (props.warning) && ( - h('span.error', { - style: { - margin: '20px', - }, - }, props.warning.split('-')) - ), - - props.inProgress && ( - h('span.in-progress-notification', 'Generating Seed...') - ), - ]), - ]) - ) -} - -RevealSeedConfirmation.prototype.componentDidMount = function () { - document.getElementById('password-box').focus() -} - -RevealSeedConfirmation.prototype.goHome = function () { - this.props.dispatch(actions.showConfigPage(false)) -} - -// create vault - -RevealSeedConfirmation.prototype.checkConfirmation = function (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.revealSeedWords() - } -} - -RevealSeedConfirmation.prototype.revealSeedWords = function () { - var password = document.getElementById('password-box').value - this.props.dispatch(actions.requestRevealSeed(password)) -} diff --git a/responsive-ui/app/keychains/hd/restore-vault.js b/responsive-ui/app/keychains/hd/restore-vault.js deleted file mode 100644 index 06e51d9b3..000000000 --- a/responsive-ui/app/keychains/hd/restore-vault.js +++ /dev/null @@ -1,152 +0,0 @@ -const inherits = require('util').inherits -const PersistentForm = require('../../../lib/persistent-form') -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('../../actions') - -module.exports = connect(mapStateToProps)(RestoreVaultScreen) - -inherits(RestoreVaultScreen, PersistentForm) -function RestoreVaultScreen () { - PersistentForm.call(this) -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - forgottenPassword: state.appState.forgottenPassword, - } -} - -RestoreVaultScreen.prototype.render = function () { - var state = this.props - this.persistentFormParentId = 'restore-vault-form' - - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginBottom: 24, - width: '100%', - fontSize: '20px', - padding: 6, - }, - }, [ - 'Restore Vault', - ]), - - // wallet seed entry - h('h3', 'Wallet Seed'), - h('textarea.twelve-word-phrase.letter-spacey', { - dataset: { - persistentFormId: 'wallet-seed', - }, - placeholder: 'Enter your secret twelve word phrase here to restore your vault.', - }), - - // password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box', - placeholder: 'New Password (min 8 chars)', - dataset: { - persistentFormId: 'password', - }, - style: { - width: 260, - marginTop: 12, - }, - }), - - // confirm password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box-confirm', - placeholder: 'Confirm Password', - onKeyPress: this.createOnEnter.bind(this), - dataset: { - persistentFormId: 'password-confirmation', - }, - style: { - width: 260, - marginTop: 16, - }, - }), - - (state.warning) && ( - h('span.error.in-progress-notification', state.warning) - ), - - // submit - - h('.flex-row.flex-space-between', { - style: { - marginTop: 30, - width: '50%', - }, - }, [ - - // cancel - h('button.primary', { - onClick: this.showInitializeMenu.bind(this), - }, 'CANCEL'), - - // submit - h('button.primary', { - onClick: this.createNewVaultAndRestore.bind(this), - }, 'OK'), - - ]), - ]) - - ) -} - -RestoreVaultScreen.prototype.showInitializeMenu = function () { - if (this.props.forgottenPassword) { - this.props.dispatch(actions.backToUnlockView()) - } else { - this.props.dispatch(actions.showInitializeMenu()) - } -} - -RestoreVaultScreen.prototype.createOnEnter = function (event) { - if (event.key === 'Enter') { - this.createNewVaultAndRestore() - } -} - -RestoreVaultScreen.prototype.createNewVaultAndRestore = function () { - // check password - var passwordBox = document.getElementById('password-box') - var password = passwordBox.value - var passwordConfirmBox = document.getElementById('password-box-confirm') - var passwordConfirm = passwordConfirmBox.value - if (password.length < 8) { - this.warning = 'Password not long enough' - - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - if (password !== passwordConfirm) { - this.warning = 'Passwords don\'t match' - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - // check seed - var seedBox = document.querySelector('textarea.twelve-word-phrase') - var seed = seedBox.value.trim() - if (seed.split(' ').length !== 12) { - this.warning = 'seed phrases are 12 words long' - this.props.dispatch(actions.displayWarning(this.warning)) - return - } - // submit - this.warning = null - this.props.dispatch(actions.displayWarning(this.warning)) - this.props.dispatch(actions.createNewVaultAndRestore(password, seed)) -} diff --git a/responsive-ui/app/new-keychain.js b/responsive-ui/app/new-keychain.js deleted file mode 100644 index cc9633166..000000000 --- a/responsive-ui/app/new-keychain.js +++ /dev/null @@ -1,29 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect - -module.exports = connect(mapStateToProps)(NewKeychain) - -function mapStateToProps (state) { - return {} -} - -inherits(NewKeychain, Component) -function NewKeychain () { - Component.call(this) -} - -NewKeychain.prototype.render = function () { - // const props = this.props - - return ( - h('div', { - style: { - background: 'blue', - }, - }, [ - h('h1', `Here's a list!!!!`), - ]) - ) -} diff --git a/responsive-ui/app/reducers.js b/responsive-ui/app/reducers.js deleted file mode 100644 index 11efca529..000000000 --- a/responsive-ui/app/reducers.js +++ /dev/null @@ -1,52 +0,0 @@ -const extend = require('xtend') - -// -// Sub-Reducers take in the complete state and return their sub-state -// -const reduceIdentities = require('./reducers/identities') -const reduceMetamask = require('./reducers/metamask') -const reduceApp = require('./reducers/app') - -window.METAMASK_CACHED_LOG_STATE = null - -module.exports = rootReducer - -function rootReducer (state, action) { - // clone - state = extend(state) - - if (action.type === 'GLOBAL_FORCE_UPDATE') { - return action.value - } - - // - // Identities - // - - state.identities = reduceIdentities(state, action) - - // - // MetaMask - // - - state.metamask = reduceMetamask(state, action) - - // - // AppState - // - - state.appState = reduceApp(state, action) - - window.METAMASK_CACHED_LOG_STATE = state - return state -} - -window.logState = function () { - var stateString = JSON.stringify(window.METAMASK_CACHED_LOG_STATE, removeSeedWords, 2) - console.log(stateString) - return stateString -} - -function removeSeedWords (key, value) { - return key === 'seedWords' ? undefined : value -} diff --git a/responsive-ui/app/reducers/app.js b/responsive-ui/app/reducers/app.js deleted file mode 100644 index 2fcc9bfe0..000000000 --- a/responsive-ui/app/reducers/app.js +++ /dev/null @@ -1,585 +0,0 @@ -const extend = require('xtend') -const actions = require('../actions') -const txHelper = require('../../lib/tx-helper') - -module.exports = reduceApp - - -function reduceApp (state, action) { - log.debug('App Reducer got ' + action.type) - // clone and defaults - const selectedAddress = state.metamask.selectedAddress - const hasUnconfActions = checkUnconfActions(state) - let name = 'accounts' - if (selectedAddress) { - name = 'accountDetail' - } - if (hasUnconfActions) { - log.debug('pending txs detected, defaulting to conf-tx view.') - name = 'confTx' - } - - var defaultView = { - name, - detailView: null, - context: selectedAddress, - } - - // confirm seed words - var seedWords = state.metamask.seedWords - var seedConfView = { - name: 'createVaultComplete', - seedWords, - } - - // default state - var appState = extend({ - shouldClose: false, - menuOpen: false, - currentView: seedWords ? seedConfView : defaultView, - accountDetail: { - subview: 'transactions', - }, - transForward: true, // Used to render transition direction - isLoading: false, // Used to display loading indicator - warning: null, // Used to display error text - }, state.appState) - - switch (action.type) { - - // transition methods - - case actions.TRANSITION_FORWARD: - return extend(appState, { - transForward: true, - }) - - case actions.TRANSITION_BACKWARD: - return extend(appState, { - transForward: false, - }) - - // intialize - - case actions.SHOW_CREATE_VAULT: - return extend(appState, { - currentView: { - name: 'createVault', - }, - transForward: true, - warning: null, - }) - - case actions.SHOW_RESTORE_VAULT: - return extend(appState, { - currentView: { - name: 'restoreVault', - }, - transForward: true, - forgottenPassword: true, - }) - - case actions.FORGOT_PASSWORD: - return extend(appState, { - currentView: { - name: 'restoreVault', - }, - transForward: false, - forgottenPassword: true, - }) - - case actions.SHOW_INIT_MENU: - return extend(appState, { - currentView: defaultView, - transForward: false, - }) - - case actions.SHOW_CONFIG_PAGE: - return extend(appState, { - currentView: { - name: 'config', - context: appState.currentView.context, - }, - transForward: action.value, - }) - - case actions.SHOW_ADD_TOKEN_PAGE: - return extend(appState, { - currentView: { - name: 'add-token', - context: appState.currentView.context, - }, - transForward: action.value, - }) - - case actions.SHOW_IMPORT_PAGE: - - return extend(appState, { - currentView: { - name: 'import-menu', - }, - transForward: true, - }) - - case actions.SHOW_INFO_PAGE: - return extend(appState, { - currentView: { - name: 'info', - context: appState.currentView.context, - }, - transForward: true, - }) - - case actions.CREATE_NEW_VAULT_IN_PROGRESS: - return extend(appState, { - currentView: { - name: 'createVault', - inProgress: true, - }, - transForward: true, - isLoading: true, - }) - - case actions.SHOW_NEW_VAULT_SEED: - return extend(appState, { - currentView: { - name: 'createVaultComplete', - seedWords: action.value, - }, - transForward: true, - isLoading: false, - }) - - case actions.NEW_ACCOUNT_SCREEN: - return extend(appState, { - currentView: { - name: 'new-account', - context: appState.currentView.context, - }, - transForward: true, - }) - - case actions.SHOW_SEND_PAGE: - return extend(appState, { - currentView: { - name: 'sendTransaction', - context: appState.currentView.context, - }, - transForward: true, - warning: null, - }) - - case actions.SHOW_NEW_KEYCHAIN: - return extend(appState, { - currentView: { - name: 'newKeychain', - context: appState.currentView.context, - }, - transForward: true, - }) - - // unlock - - case actions.UNLOCK_METAMASK: - return extend(appState, { - forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null, - detailView: {}, - transForward: true, - isLoading: false, - warning: null, - }) - - case actions.LOCK_METAMASK: - return extend(appState, { - currentView: defaultView, - transForward: false, - warning: null, - }) - - case actions.BACK_TO_INIT_MENU: - return extend(appState, { - warning: null, - transForward: false, - forgottenPassword: true, - currentView: { - name: 'InitMenu', - }, - }) - - case actions.BACK_TO_UNLOCK_VIEW: - return extend(appState, { - warning: null, - transForward: true, - forgottenPassword: false, - currentView: { - name: 'UnlockScreen', - }, - }) - // reveal seed words - - case actions.REVEAL_SEED_CONFIRMATION: - return extend(appState, { - currentView: { - name: 'reveal-seed-conf', - }, - transForward: true, - warning: null, - }) - - // accounts - - case actions.SET_SELECTED_ACCOUNT: - return extend(appState, { - activeAddress: action.value, - }) - - case actions.GO_HOME: - return extend(appState, { - currentView: extend(appState.currentView, { - name: 'accountDetail', - }), - accountDetail: { - subview: 'transactions', - accountExport: 'none', - privateKey: '', - }, - transForward: false, - warning: null, - }) - - case actions.SHOW_ACCOUNT_DETAIL: - return extend(appState, { - forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null, - currentView: { - name: 'accountDetail', - context: action.value, - }, - accountDetail: { - subview: 'transactions', - accountExport: 'none', - privateKey: '', - }, - transForward: false, - }) - - case actions.BACK_TO_ACCOUNT_DETAIL: - return extend(appState, { - currentView: { - name: 'accountDetail', - context: action.value, - }, - accountDetail: { - subview: 'transactions', - accountExport: 'none', - privateKey: '', - }, - transForward: false, - }) - - case actions.SHOW_ACCOUNTS_PAGE: - return extend(appState, { - currentView: { - name: seedWords ? 'createVaultComplete' : 'accounts', - seedWords, - }, - transForward: true, - isLoading: false, - warning: null, - scrollToBottom: false, - forgottenPassword: false, - }) - - case actions.SHOW_NOTICE: - return extend(appState, { - transForward: true, - isLoading: false, - }) - - case actions.REVEAL_ACCOUNT: - return extend(appState, { - scrollToBottom: true, - }) - - case actions.SHOW_CONF_TX_PAGE: - return extend(appState, { - currentView: { - name: 'confTx', - context: 0, - }, - transForward: action.transForward, - warning: null, - isLoading: false, - }) - - case actions.SHOW_CONF_MSG_PAGE: - return extend(appState, { - currentView: { - name: hasUnconfActions ? 'confTx' : 'account-detail', - context: 0, - }, - transForward: true, - warning: null, - isLoading: false, - }) - - case actions.COMPLETED_TX: - log.debug('reducing COMPLETED_TX for tx ' + action.value) - const otherUnconfActions = getUnconfActionList(state) - .filter(tx => tx.id !== action.value) - const hasOtherUnconfActions = otherUnconfActions.length > 0 - - if (hasOtherUnconfActions) { - log.debug('reducer detected txs - rendering confTx view') - return extend(appState, { - transForward: false, - currentView: { - name: 'confTx', - context: 0, - }, - warning: null, - }) - } else { - log.debug('attempting to close popup') - return extend(appState, { - // indicate notification should close - shouldClose: true, - transForward: false, - warning: null, - currentView: { - name: 'accountDetail', - context: state.metamask.selectedAddress, - }, - accountDetail: { - subview: 'transactions', - }, - }) - } - - case actions.NEXT_TX: - return extend(appState, { - transForward: true, - currentView: { - name: 'confTx', - context: ++appState.currentView.context, - warning: null, - }, - }) - - case actions.VIEW_PENDING_TX: - const context = indexForPending(state, action.value) - return extend(appState, { - transForward: true, - currentView: { - name: 'confTx', - context, - warning: null, - }, - }) - - case actions.PREVIOUS_TX: - return extend(appState, { - transForward: false, - currentView: { - name: 'confTx', - context: --appState.currentView.context, - warning: null, - }, - }) - - case actions.TRANSACTION_ERROR: - return extend(appState, { - currentView: { - name: 'confTx', - errorMessage: 'There was a problem submitting this transaction.', - }, - }) - - case actions.UNLOCK_FAILED: - return extend(appState, { - warning: action.value || 'Incorrect password. Try again.', - }) - - case actions.SHOW_LOADING: - return extend(appState, { - isLoading: true, - loadingMessage: action.value, - }) - - case actions.HIDE_LOADING: - return extend(appState, { - isLoading: false, - }) - - case actions.SHOW_SUB_LOADING_INDICATION: - return extend(appState, { - isSubLoading: true, - }) - - case actions.HIDE_SUB_LOADING_INDICATION: - return extend(appState, { - isSubLoading: false, - }) - case actions.CLEAR_SEED_WORD_CACHE: - return extend(appState, { - transForward: true, - currentView: {}, - isLoading: false, - accountDetail: { - subview: 'transactions', - accountExport: 'none', - privateKey: '', - }, - }) - - case actions.DISPLAY_WARNING: - return extend(appState, { - warning: action.value, - isLoading: false, - }) - - case actions.HIDE_WARNING: - return extend(appState, { - warning: undefined, - }) - - case actions.REQUEST_ACCOUNT_EXPORT: - return extend(appState, { - transForward: true, - currentView: { - name: 'accountDetail', - context: appState.currentView.context, - }, - accountDetail: { - subview: 'export', - accountExport: 'requested', - }, - }) - - case actions.EXPORT_ACCOUNT: - return extend(appState, { - accountDetail: { - subview: 'export', - accountExport: 'completed', - }, - }) - - case actions.SHOW_PRIVATE_KEY: - return extend(appState, { - accountDetail: { - subview: 'export', - accountExport: 'completed', - privateKey: action.value, - }, - }) - - case actions.BUY_ETH_VIEW: - return extend(appState, { - transForward: true, - currentView: { - name: 'buyEth', - context: appState.currentView.name, - }, - identity: state.metamask.identities[action.value], - buyView: { - subview: 'Coinbase', - amount: '15.00', - buyAddress: action.value, - formView: { - coinbase: true, - shapeshift: false, - }, - }, - }) - - case actions.COINBASE_SUBVIEW: - return extend(appState, { - buyView: { - subview: 'Coinbase', - formView: { - coinbase: true, - shapeshift: false, - }, - buyAddress: appState.buyView.buyAddress, - amount: appState.buyView.amount, - }, - }) - - case actions.SHAPESHIFT_SUBVIEW: - return extend(appState, { - buyView: { - subview: 'ShapeShift', - formView: { - coinbase: false, - shapeshift: true, - marketinfo: action.value.marketinfo, - coinOptions: action.value.coinOptions, - }, - buyAddress: appState.buyView.buyAddress, - amount: appState.buyView.amount, - }, - }) - - case actions.PAIR_UPDATE: - return extend(appState, { - buyView: { - subview: 'ShapeShift', - formView: { - coinbase: false, - shapeshift: true, - marketinfo: action.value.marketinfo, - coinOptions: appState.buyView.formView.coinOptions, - }, - buyAddress: appState.buyView.buyAddress, - amount: appState.buyView.amount, - warning: null, - }, - }) - - case actions.SHOW_QR: - return extend(appState, { - qrRequested: true, - transForward: true, - - Qr: { - message: action.value.message, - data: action.value.data, - }, - }) - - case actions.SHOW_QR_VIEW: - return extend(appState, { - currentView: { - name: 'qr', - context: appState.currentView.context, - }, - transForward: true, - Qr: { - message: action.value.message, - data: action.value.data, - }, - }) - default: - return appState - } -} - -function checkUnconfActions (state) { - const unconfActionList = getUnconfActionList(state) - const hasUnconfActions = unconfActionList.length > 0 - return hasUnconfActions -} - -function getUnconfActionList (state) { - const { unapprovedTxs, unapprovedMsgs, - unapprovedPersonalMsgs, network } = state.metamask - - const unconfActionList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) - return unconfActionList -} - -function indexForPending (state, txId) { - const unconfTxList = getUnconfActionList(state) - const match = unconfTxList.find((tx) => tx.id === txId) - const index = unconfTxList.indexOf(match) - return index -} diff --git a/responsive-ui/app/reducers/identities.js b/responsive-ui/app/reducers/identities.js deleted file mode 100644 index 341a404e7..000000000 --- a/responsive-ui/app/reducers/identities.js +++ /dev/null @@ -1,15 +0,0 @@ -const extend = require('xtend') - -module.exports = reduceIdentities - -function reduceIdentities (state, action) { - // clone + defaults - var idState = extend({ - - }, state.identities) - - switch (action.type) { - default: - return idState - } -} diff --git a/responsive-ui/app/reducers/metamask.js b/responsive-ui/app/reducers/metamask.js deleted file mode 100644 index e0c416c2d..000000000 --- a/responsive-ui/app/reducers/metamask.js +++ /dev/null @@ -1,137 +0,0 @@ -const extend = require('xtend') -const actions = require('../actions') - -module.exports = reduceMetamask - -function reduceMetamask (state, action) { - let newState - - // clone + defaults - var metamaskState = extend({ - isInitialized: false, - isUnlocked: false, - rpcTarget: 'https://rawtestrpc.metamask.io/', - identities: {}, - unapprovedTxs: {}, - noActiveNotices: true, - lastUnreadNotice: undefined, - frequentRpcList: [], - addressBook: [], - }, state.metamask) - - switch (action.type) { - - case actions.SHOW_ACCOUNTS_PAGE: - newState = extend(metamaskState) - delete newState.seedWords - return newState - - case actions.SHOW_NOTICE: - return extend(metamaskState, { - noActiveNotices: false, - lastUnreadNotice: action.value, - }) - - case actions.CLEAR_NOTICES: - return extend(metamaskState, { - noActiveNotices: true, - }) - - case actions.UPDATE_METAMASK_STATE: - return extend(metamaskState, action.value) - - case actions.UNLOCK_METAMASK: - return extend(metamaskState, { - isUnlocked: true, - isInitialized: true, - selectedAddress: action.value, - }) - - case actions.LOCK_METAMASK: - return extend(metamaskState, { - isUnlocked: false, - }) - - case actions.SET_RPC_LIST: - return extend(metamaskState, { - frequentRpcList: action.value, - }) - - case actions.SET_RPC_TARGET: - return extend(metamaskState, { - provider: { - type: 'rpc', - rpcTarget: action.value, - }, - }) - - case actions.SET_PROVIDER_TYPE: - return extend(metamaskState, { - provider: { - type: action.value, - }, - }) - - case actions.COMPLETED_TX: - var stringId = String(action.id) - newState = extend(metamaskState, { - unapprovedTxs: {}, - unapprovedMsgs: {}, - }) - for (const id in metamaskState.unapprovedTxs) { - if (id !== stringId) { - newState.unapprovedTxs[id] = metamaskState.unapprovedTxs[id] - } - } - for (const id in metamaskState.unapprovedMsgs) { - if (id !== stringId) { - newState.unapprovedMsgs[id] = metamaskState.unapprovedMsgs[id] - } - } - return newState - - case actions.SHOW_NEW_VAULT_SEED: - return extend(metamaskState, { - isUnlocked: true, - isInitialized: false, - seedWords: action.value, - }) - - case actions.CLEAR_SEED_WORD_CACHE: - newState = extend(metamaskState, { - isUnlocked: true, - isInitialized: true, - selectedAddress: action.value, - }) - delete newState.seedWords - return newState - - case actions.SHOW_ACCOUNT_DETAIL: - newState = extend(metamaskState, { - isUnlocked: true, - isInitialized: true, - selectedAddress: action.value, - }) - delete newState.seedWords - return newState - - case actions.SAVE_ACCOUNT_LABEL: - const account = action.value.account - const name = action.value.label - var id = {} - id[account] = extend(metamaskState.identities[account], { name }) - var identities = extend(metamaskState.identities, id) - return extend(metamaskState, { identities }) - - case actions.SET_CURRENT_FIAT: - return extend(metamaskState, { - currentCurrency: action.value.currentCurrency, - conversionRate: action.value.conversionRate, - conversionDate: action.value.conversionDate, - }) - - default: - return metamaskState - - } -} diff --git a/responsive-ui/app/root.js b/responsive-ui/app/root.js deleted file mode 100644 index 9e7314b20..000000000 --- a/responsive-ui/app/root.js +++ /dev/null @@ -1,22 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const Provider = require('react-redux').Provider -const h = require('react-hyperscript') -const App = require('./app') - -module.exports = Root - -inherits(Root, Component) -function Root () { Component.call(this) } - -Root.prototype.render = function () { - return ( - - h(Provider, { - store: this.props.store, - }, [ - h(App), - ]) - - ) -} diff --git a/responsive-ui/app/send.js b/responsive-ui/app/send.js deleted file mode 100644 index a21a219eb..000000000 --- a/responsive-ui/app/send.js +++ /dev/null @@ -1,288 +0,0 @@ -const inherits = require('util').inherits -const PersistentForm = require('../lib/persistent-form') -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const Identicon = require('./components/identicon') -const actions = require('./actions') -const util = require('./util') -const numericBalance = require('./util').numericBalance -const addressSummary = require('./util').addressSummary -const isHex = require('./util').isHex -const EthBalance = require('./components/eth-balance') -const EnsInput = require('./components/ens-input') -const ethUtil = require('ethereumjs-util') -module.exports = connect(mapStateToProps)(SendTransactionScreen) - -function mapStateToProps (state) { - var result = { - address: state.metamask.selectedAddress, - accounts: state.metamask.accounts, - identities: state.metamask.identities, - warning: state.appState.warning, - network: state.metamask.network, - addressBook: state.metamask.addressBook, - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - } - - result.error = result.warning && result.warning.split('.')[0] - - result.account = result.accounts[result.address] - result.identity = result.identities[result.address] - result.balance = result.account ? numericBalance(result.account.balance) : null - - return result -} - -inherits(SendTransactionScreen, PersistentForm) -function SendTransactionScreen () { - PersistentForm.call(this) -} - -SendTransactionScreen.prototype.render = function () { - this.persistentFormParentId = 'send-tx-form' - - const props = this.props - const { - address, - account, - identity, - network, - identities, - addressBook, - conversionRate, - currentCurrency, - } = props - - return ( - - h('.send-screen.flex-column.flex-grow', [ - - // - // Sender Profile - // - - h('.account-data-subsection.flex-row.flex-grow', { - style: { - margin: '0 20px', - }, - }, [ - - // header - identicon + nav - h('.flex-row.flex-space-between', { - style: { - marginTop: '15px', - }, - }, [ - // back button - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { - onClick: this.back.bind(this), - }), - - // large identicon - h('.identicon-wrapper.flex-column.flex-center.select-none', [ - h(Identicon, { - diameter: 62, - address: address, - }), - ]), - - // invisible place holder - h('i.fa.fa-users.fa-lg.invisible', { - style: { - marginTop: '28px', - }, - }), - - ]), - - // account label - - h('.flex-column', { - style: { - marginTop: '10px', - alignItems: 'flex-start', - }, - }, [ - h('h2.font-medium.color-forest.flex-center', { - style: { - paddingTop: '8px', - marginBottom: '8px', - }, - }, identity && identity.name), - - // address and getter actions - h('.flex-row.flex-center', { - style: { - marginBottom: '8px', - }, - }, [ - - h('div', { - style: { - lineHeight: '16px', - }, - }, addressSummary(address)), - - ]), - - // balance - h('.flex-row.flex-center', [ - - h(EthBalance, { - value: account && account.balance, - conversionRate, - currentCurrency, - }), - - ]), - ]), - ]), - - // - // Required Fields - // - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginTop: '15px', - marginBottom: '16px', - }, - }, [ - 'Send Transaction', - ]), - - // error message - props.error && h('span.error.flex-center', props.error), - - // 'to' field - h('section.flex-row.flex-center', [ - h(EnsInput, { - name: 'address', - placeholder: 'Recipient Address', - onChange: this.recipientDidChange.bind(this), - network, - identities, - addressBook, - }), - ]), - - // 'amount' and send button - h('section.flex-row.flex-center', [ - - h('input.large-input', { - name: 'amount', - placeholder: 'Amount', - type: 'number', - style: { - marginRight: '6px', - }, - dataset: { - persistentFormId: 'tx-amount', - }, - }), - - h('button.primary', { - onClick: this.onSubmit.bind(this), - style: { - textTransform: 'uppercase', - }, - }, 'Next'), - - ]), - - // - // Optional Fields - // - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginTop: '16px', - marginBottom: '16px', - }, - }, [ - 'Transaction Data (optional)', - ]), - - // 'data' field - h('section.flex-column.flex-center', [ - h('input.large-input', { - name: 'txData', - placeholder: '0x01234', - style: { - width: '100%', - resize: 'none', - }, - dataset: { - persistentFormId: 'tx-data', - }, - }), - ]), - ]) - ) -} - -SendTransactionScreen.prototype.navigateToAccounts = function (event) { - event.stopPropagation() - this.props.dispatch(actions.showAccountsPage()) -} - -SendTransactionScreen.prototype.back = function () { - var address = this.props.address - this.props.dispatch(actions.backToAccountDetail(address)) -} - -SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickname) { - this.setState({ - recipient: recipient, - nickname: nickname, - }) -} - -SendTransactionScreen.prototype.onSubmit = function () { - const state = this.state || {} - const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '') - const nickname = state.nickname || ' ' - const input = document.querySelector('input[name="amount"]').value - const value = util.normalizeEthStringToWei(input) - const txData = document.querySelector('input[name="txData"]').value - const balance = this.props.balance - let message - - if (value.gt(balance)) { - message = 'Insufficient funds.' - return this.props.dispatch(actions.displayWarning(message)) - } - - if (input < 0) { - message = 'Can not send negative amounts of ETH.' - return this.props.dispatch(actions.displayWarning(message)) - } - - if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) { - message = 'Recipient address is invalid.' - return this.props.dispatch(actions.displayWarning(message)) - } - - if (!isHex(ethUtil.stripHexPrefix(txData)) && txData) { - message = 'Transaction data must be hex string.' - return this.props.dispatch(actions.displayWarning(message)) - } - - this.props.dispatch(actions.hideWarning()) - - this.props.dispatch(actions.addToAddressBook(recipient, nickname)) - - var txParams = { - from: this.props.address, - value: '0x' + value.toString(16), - } - - if (recipient) txParams.to = ethUtil.addHexPrefix(recipient) - if (txData) txParams.data = txData - - this.props.dispatch(actions.signTx(txParams)) -} diff --git a/responsive-ui/app/settings.js b/responsive-ui/app/settings.js deleted file mode 100644 index 454cc95e0..000000000 --- a/responsive-ui/app/settings.js +++ /dev/null @@ -1,59 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') - -module.exports = connect(mapStateToProps)(AppSettingsPage) - -function mapStateToProps (state) { - return {} -} - -inherits(AppSettingsPage, Component) -function AppSettingsPage () { - Component.call(this) -} - -AppSettingsPage.prototype.render = function () { - return ( - - h('.account-detail-section.flex-column.flex-grow', [ - - // subtitle and nav - h('.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: this.navigateToAccounts.bind(this), - }), - h('h2.page-subtitle', 'Settings'), - ]), - - h('label', { - htmlFor: 'settings-rpc-endpoint', - }, 'RPC Endpoint:'), - h('input', { - type: 'url', - id: 'settings-rpc-endpoint', - onKeyPress: this.onKeyPress.bind(this), - }), - - ]) - - ) -} - -AppSettingsPage.prototype.componentDidMount = function () { - document.querySelector('input').focus() -} - -AppSettingsPage.prototype.onKeyPress = function (event) { - // get submit event - if (event.key === 'Enter') { - // this.submitPassword(event) - } -} - -AppSettingsPage.prototype.navigateToAccounts = function (event) { - event.stopPropagation() - this.props.dispatch(actions.showAccountsPage()) -} diff --git a/responsive-ui/app/store.js b/responsive-ui/app/store.js deleted file mode 100644 index ba9e58b49..000000000 --- a/responsive-ui/app/store.js +++ /dev/null @@ -1,21 +0,0 @@ -const createStore = require('redux').createStore -const applyMiddleware = require('redux').applyMiddleware -const thunkMiddleware = require('redux-thunk') -const rootReducer = require('./reducers') -const createLogger = require('redux-logger') - -global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' - -module.exports = configureStore - -const loggerMiddleware = createLogger({ - predicate: () => global.METAMASK_DEBUG, -}) - -const middlewares = [thunkMiddleware, loggerMiddleware] - -const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore) - -function configureStore (initialState) { - return createStoreWithMiddleware(rootReducer, initialState) -} diff --git a/responsive-ui/app/template.js b/responsive-ui/app/template.js deleted file mode 100644 index d15b30fd2..000000000 --- a/responsive-ui/app/template.js +++ /dev/null @@ -1,30 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect - -module.exports = connect(mapStateToProps)(COMPONENTNAME) - -function mapStateToProps (state) { - return {} -} - -inherits(COMPONENTNAME, Component) -function COMPONENTNAME () { - Component.call(this) -} - -COMPONENTNAME.prototype.render = function () { - const props = this.props - - return ( - h('div', { - style: { - background: 'blue', - }, - }, [ - `Hello, ${props.sender}`, - ]) - ) -} - diff --git a/responsive-ui/app/unlock.js b/responsive-ui/app/unlock.js deleted file mode 100644 index 1aee3c5d0..000000000 --- a/responsive-ui/app/unlock.js +++ /dev/null @@ -1,118 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('./actions') -const getCaretCoordinates = require('textarea-caret') -const EventEmitter = require('events').EventEmitter - -const Mascot = require('./components/mascot') - -module.exports = connect(mapStateToProps)(UnlockScreen) - -inherits(UnlockScreen, Component) -function UnlockScreen () { - Component.call(this) - this.animationEventEmitter = new EventEmitter() -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -UnlockScreen.prototype.render = function () { - const state = this.props - const warning = state.warning - return ( - h('.flex-column', [ - h('.unlock-screen.flex-column.flex-center.flex-grow', [ - - h(Mascot, { - animationEventEmitter: this.animationEventEmitter, - }), - - h('h1', { - style: { - fontSize: '1.4em', - textTransform: 'uppercase', - color: '#7F8082', - }, - }, 'MetaMask'), - - h('input.large-input', { - type: 'password', - id: 'password-box', - placeholder: 'enter password', - style: { - - }, - onKeyPress: this.onKeyPress.bind(this), - onInput: this.inputChanged.bind(this), - }), - - h('.error', { - style: { - display: warning ? 'block' : 'none', - padding: '0 20px', - textAlign: 'center', - }, - }, warning), - - h('button.primary.cursor-pointer', { - onClick: this.onSubmit.bind(this), - style: { - margin: 10, - }, - }, 'Unlock'), - ]), - - h('.flex-row.flex-center.flex-grow', [ - h('p.pointer', { - onClick: () => this.props.dispatch(actions.forgotPassword()), - style: { - fontSize: '0.8em', - color: 'rgb(247, 134, 28)', - textDecoration: 'underline', - }, - }, 'I forgot my password.'), - ]), - ]) - ) -} - -UnlockScreen.prototype.componentDidMount = function () { - document.getElementById('password-box').focus() -} - -UnlockScreen.prototype.onSubmit = function (event) { - const input = document.getElementById('password-box') - const password = input.value - this.props.dispatch(actions.tryUnlockMetamask(password)) -} - -UnlockScreen.prototype.onKeyPress = function (event) { - if (event.key === 'Enter') { - this.submitPassword(event) - } -} - -UnlockScreen.prototype.submitPassword = function (event) { - var element = event.target - var password = element.value - // reset input - element.value = '' - this.props.dispatch(actions.tryUnlockMetamask(password)) -} - -UnlockScreen.prototype.inputChanged = function (event) { - // tell mascot to look at page action - var element = event.target - var boundingRect = element.getBoundingClientRect() - var coordinates = getCaretCoordinates(element, element.selectionEnd) - this.animationEventEmitter.emit('point', { - x: boundingRect.left + coordinates.left - element.scrollLeft, - y: boundingRect.top + coordinates.top - element.scrollTop, - }) -} diff --git a/responsive-ui/app/util.js b/responsive-ui/app/util.js deleted file mode 100644 index ac3f42c6b..000000000 --- a/responsive-ui/app/util.js +++ /dev/null @@ -1,217 +0,0 @@ -const ethUtil = require('ethereumjs-util') - -var valueTable = { - wei: '1000000000000000000', - kwei: '1000000000000000', - mwei: '1000000000000', - gwei: '1000000000', - szabo: '1000000', - finney: '1000', - ether: '1', - kether: '0.001', - mether: '0.000001', - gether: '0.000000001', - tether: '0.000000000001', -} -var bnTable = {} -for (var currency in valueTable) { - bnTable[currency] = new ethUtil.BN(valueTable[currency], 10) -} - -module.exports = { - valuesFor: valuesFor, - addressSummary: addressSummary, - miniAddressSummary: miniAddressSummary, - isAllOneCase: isAllOneCase, - isValidAddress: isValidAddress, - numericBalance: numericBalance, - parseBalance: parseBalance, - formatBalance: formatBalance, - generateBalanceObject: generateBalanceObject, - dataSize: dataSize, - readableDate: readableDate, - normalizeToWei: normalizeToWei, - normalizeEthStringToWei: normalizeEthStringToWei, - normalizeNumberToWei: normalizeNumberToWei, - valueTable: valueTable, - bnTable: bnTable, - isHex: isHex, -} - -function valuesFor (obj) { - if (!obj) return [] - return Object.keys(obj) - .map(function (key) { return obj[key] }) -} - -function addressSummary (address, firstSegLength = 10, lastSegLength = 4, includeHex = true) { - if (!address) return '' - let checked = ethUtil.toChecksumAddress(address) - if (!includeHex) { - checked = ethUtil.stripHexPrefix(checked) - } - return checked ? checked.slice(0, firstSegLength) + '...' + checked.slice(checked.length - lastSegLength) : '...' -} - -function miniAddressSummary (address) { - if (!address) return '' - var checked = ethUtil.toChecksumAddress(address) - return checked ? checked.slice(0, 4) + '...' + checked.slice(-4) : '...' -} - -function isValidAddress (address) { - var prefixed = ethUtil.addHexPrefix(address) - if (address === '0x0000000000000000000000000000000000000000') return false - return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed) -} - -function isAllOneCase (address) { - if (!address) return true - var lower = address.toLowerCase() - var upper = address.toUpperCase() - return address === lower || address === upper -} - -// Takes wei Hex, returns wei BN, even if input is null -function numericBalance (balance) { - if (!balance) return new ethUtil.BN(0, 16) - var stripped = ethUtil.stripHexPrefix(balance) - return new ethUtil.BN(stripped, 16) -} - -// Takes hex, returns [beforeDecimal, afterDecimal] -function parseBalance (balance) { - var beforeDecimal, afterDecimal - const wei = numericBalance(balance) - var weiString = wei.toString() - const trailingZeros = /0+$/ - - beforeDecimal = weiString.length > 18 ? weiString.slice(0, weiString.length - 18) : '0' - afterDecimal = ('000000000000000000' + wei).slice(-18).replace(trailingZeros, '') - if (afterDecimal === '') { afterDecimal = '0' } - return [beforeDecimal, afterDecimal] -} - -// Takes wei hex, returns an object with three properties. -// Its "formatted" property is what we generally use to render values. -function formatBalance (balance, decimalsToKeep, needsParse = true) { - var parsed = needsParse ? parseBalance(balance) : balance.split('.') - var beforeDecimal = parsed[0] - var afterDecimal = parsed[1] - var formatted = 'None' - if (decimalsToKeep === undefined) { - if (beforeDecimal === '0') { - if (afterDecimal !== '0') { - var sigFigs = afterDecimal.match(/^0*(.{2})/) // default: grabs 2 most significant digits - if (sigFigs) { afterDecimal = sigFigs[0] } - formatted = '0.' + afterDecimal + ' ETH' - } - } else { - formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ' ETH' - } - } else { - afterDecimal += Array(decimalsToKeep).join('0') - formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ' ETH' - } - return formatted -} - - -function generateBalanceObject (formattedBalance, decimalsToKeep = 1) { - var balance = formattedBalance.split(' ')[0] - var label = formattedBalance.split(' ')[1] - var beforeDecimal = balance.split('.')[0] - var afterDecimal = balance.split('.')[1] - var shortBalance = shortenBalance(balance, decimalsToKeep) - - if (beforeDecimal === '0' && afterDecimal.substr(0, 5) === '00000') { - // eslint-disable-next-line eqeqeq - if (afterDecimal == 0) { - balance = '0' - } else { - balance = '<1.0e-5' - } - } else if (beforeDecimal !== '0') { - balance = `${beforeDecimal}.${afterDecimal.slice(0, decimalsToKeep)}` - } - - return { balance, label, shortBalance } -} - -function shortenBalance (balance, decimalsToKeep = 1) { - var truncatedValue - var convertedBalance = parseFloat(balance) - if (convertedBalance > 1000000) { - truncatedValue = (balance / 1000000).toFixed(decimalsToKeep) - return `${truncatedValue}m` - } else if (convertedBalance > 1000) { - truncatedValue = (balance / 1000).toFixed(decimalsToKeep) - return `${truncatedValue}k` - } else if (convertedBalance === 0) { - return '0' - } else if (convertedBalance < 0.001) { - return '<0.001' - } else if (convertedBalance < 1) { - var stringBalance = convertedBalance.toString() - if (stringBalance.split('.')[1].length > 3) { - return convertedBalance.toFixed(3) - } else { - return stringBalance - } - } else { - return convertedBalance.toFixed(decimalsToKeep) - } -} - -function dataSize (data) { - var size = data ? ethUtil.stripHexPrefix(data).length : 0 - return size + ' bytes' -} - -// Takes a BN and an ethereum currency name, -// returns a BN in wei -function normalizeToWei (amount, currency) { - try { - return amount.mul(bnTable.wei).div(bnTable[currency]) - } catch (e) {} - return amount -} - -function normalizeEthStringToWei (str) { - const parts = str.split('.') - let eth = new ethUtil.BN(parts[0], 10).mul(bnTable.wei) - if (parts[1]) { - var decimal = parts[1] - while (decimal.length < 18) { - decimal += '0' - } - const decimalBN = new ethUtil.BN(decimal, 10) - eth = eth.add(decimalBN) - } - return eth -} - -var multiple = new ethUtil.BN('10000', 10) -function normalizeNumberToWei (n, currency) { - var enlarged = n * 10000 - var amount = new ethUtil.BN(String(enlarged), 10) - return normalizeToWei(amount, currency).div(multiple) -} - -function readableDate (ms) { - var date = new Date(ms) - var month = date.getMonth() - var day = date.getDate() - var year = date.getFullYear() - var hours = date.getHours() - var minutes = '0' + date.getMinutes() - var seconds = '0' + date.getSeconds() - - var dateStr = `${month}/${day}/${year}` - var time = `${hours}:${minutes.substr(-2)}:${seconds.substr(-2)}` - return `${dateStr} ${time}` -} - -function isHex (str) { - return Boolean(str.match(/^(0x)?[0-9a-fA-F]+$/)) -} diff --git a/responsive-ui/css.js b/responsive-ui/css.js deleted file mode 100644 index 043363cd7..000000000 --- a/responsive-ui/css.js +++ /dev/null @@ -1,29 +0,0 @@ -const fs = require('fs') -const path = require('path') - -module.exports = bundleCss - -var cssFiles = { - 'fonts.css': fs.readFileSync(path.join(__dirname, '/app/css/fonts.css'), 'utf8'), - 'reset.css': fs.readFileSync(path.join(__dirname, '/app/css/reset.css'), 'utf8'), - 'lib.css': fs.readFileSync(path.join(__dirname, '/app/css/lib.css'), 'utf8'), - 'index.css': fs.readFileSync(path.join(__dirname, '/app/css/index.css'), 'utf8'), - 'transitions.css': fs.readFileSync(path.join(__dirname, '/app/css/transitions.css'), 'utf8'), - 'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'), - 'react-css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'), -} - -function bundleCss () { - var cssBundle = Object.keys(cssFiles).reduce(function (bundle, fileName) { - var fileContent = cssFiles[fileName] - var output = String() - - output += '/*========== ' + fileName + ' ==========*/\n\n' - output += fileContent - output += '\n\n' - - return bundle + output - }, String()) - - return cssBundle -} diff --git a/responsive-ui/design/00-metamask-SignIn.jpg b/responsive-ui/design/00-metamask-SignIn.jpg deleted file mode 100644 index 2becdb032..000000000 Binary files a/responsive-ui/design/00-metamask-SignIn.jpg and /dev/null differ diff --git a/responsive-ui/design/01-metamask-SelectAcc.jpg b/responsive-ui/design/01-metamask-SelectAcc.jpg deleted file mode 100644 index 239091a98..000000000 Binary files a/responsive-ui/design/01-metamask-SelectAcc.jpg and /dev/null differ diff --git a/responsive-ui/design/02-metamask-AccDetails.jpg b/responsive-ui/design/02-metamask-AccDetails.jpg deleted file mode 100644 index d7d408ffc..000000000 Binary files a/responsive-ui/design/02-metamask-AccDetails.jpg and /dev/null differ diff --git a/responsive-ui/design/02a-metamask-AccDetails-OverToken.jpg b/responsive-ui/design/02a-metamask-AccDetails-OverToken.jpg deleted file mode 100644 index f26ff31e8..000000000 Binary files a/responsive-ui/design/02a-metamask-AccDetails-OverToken.jpg and /dev/null differ diff --git a/responsive-ui/design/02a-metamask-AccDetails-OverTransaction.jpg b/responsive-ui/design/02a-metamask-AccDetails-OverTransaction.jpg deleted file mode 100644 index 8a06be6b9..000000000 Binary files a/responsive-ui/design/02a-metamask-AccDetails-OverTransaction.jpg and /dev/null differ diff --git a/responsive-ui/design/02a-metamask-AccDetails.jpg b/responsive-ui/design/02a-metamask-AccDetails.jpg deleted file mode 100644 index c37e0f539..000000000 Binary files a/responsive-ui/design/02a-metamask-AccDetails.jpg and /dev/null differ diff --git a/responsive-ui/design/02b-metamask-AccDetails-Send.jpg b/responsive-ui/design/02b-metamask-AccDetails-Send.jpg deleted file mode 100644 index 10f2d27fd..000000000 Binary files a/responsive-ui/design/02b-metamask-AccDetails-Send.jpg and /dev/null differ diff --git a/responsive-ui/design/03-metamask-Qr.jpg b/responsive-ui/design/03-metamask-Qr.jpg deleted file mode 100644 index 9c09de42f..000000000 Binary files a/responsive-ui/design/03-metamask-Qr.jpg and /dev/null differ diff --git a/responsive-ui/design/05-metamask-Menu.jpg b/responsive-ui/design/05-metamask-Menu.jpg deleted file mode 100644 index 0a43d7b2a..000000000 Binary files a/responsive-ui/design/05-metamask-Menu.jpg and /dev/null differ diff --git a/responsive-ui/design/chromeStorePics/final_screen_dao_accounts.png b/responsive-ui/design/chromeStorePics/final_screen_dao_accounts.png deleted file mode 100644 index 805cc96b6..000000000 Binary files a/responsive-ui/design/chromeStorePics/final_screen_dao_accounts.png and /dev/null differ diff --git a/responsive-ui/design/chromeStorePics/final_screen_dao_locked.png b/responsive-ui/design/chromeStorePics/final_screen_dao_locked.png deleted file mode 100644 index 9d9e33930..000000000 Binary files a/responsive-ui/design/chromeStorePics/final_screen_dao_locked.png and /dev/null differ diff --git a/responsive-ui/design/chromeStorePics/final_screen_dao_notification.png b/responsive-ui/design/chromeStorePics/final_screen_dao_notification.png deleted file mode 100644 index d56a5ce62..000000000 Binary files a/responsive-ui/design/chromeStorePics/final_screen_dao_notification.png and /dev/null differ diff --git a/responsive-ui/design/chromeStorePics/final_screen_wei_account.png b/responsive-ui/design/chromeStorePics/final_screen_wei_account.png deleted file mode 100644 index d503ff301..000000000 Binary files a/responsive-ui/design/chromeStorePics/final_screen_wei_account.png and /dev/null differ diff --git a/responsive-ui/design/chromeStorePics/final_screen_wei_notification.png b/responsive-ui/design/chromeStorePics/final_screen_wei_notification.png deleted file mode 100644 index 3560c51ff..000000000 Binary files a/responsive-ui/design/chromeStorePics/final_screen_wei_notification.png and /dev/null differ diff --git a/responsive-ui/design/chromeStorePics/icon-128.png b/responsive-ui/design/chromeStorePics/icon-128.png deleted file mode 100644 index ae687147d..000000000 Binary files a/responsive-ui/design/chromeStorePics/icon-128.png and /dev/null differ diff --git a/responsive-ui/design/chromeStorePics/icon-64.png b/responsive-ui/design/chromeStorePics/icon-64.png deleted file mode 100644 index 7062cf4f1..000000000 Binary files a/responsive-ui/design/chromeStorePics/icon-64.png and /dev/null differ diff --git a/responsive-ui/design/chromeStorePics/metamask_icon.ai b/responsive-ui/design/chromeStorePics/metamask_icon.ai deleted file mode 100644 index 27400c5a4..000000000 --- a/responsive-ui/design/chromeStorePics/metamask_icon.ai +++ /dev/null @@ -1,2383 +0,0 @@ -%PDF-1.5 % -1 0 obj <>/OCGs[5 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream - - - - - application/pdf - - - metamask_icon - - - Adobe Illustrator CC 2015 (Macintosh) - 2016-06-15T14:23:12-04:00 - 2016-06-15T14:23:12-04:00 - 2016-06-15T14:23:12-04:00 - - - - 240 - 256 - JPEG - /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAADwAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXnP5r/mvB5Tg/RmnAT6/cJyUMKx28bVAkf+ZjT4U+k7UDYuo1HBsObl6bTce5+l5X+Wf5t6jonm KZtfu5rzTNVcG+lkZpHilACLOAamgUBWC/sgUrxAzEwakxl6uRczUaYSj6eYfS9vcQXEEdxA6ywT KskUimqsjCqsCOoIObUG3UkUvxQ7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXln5rfnHb+X1l0bQnWfXCCs0+zR2tfEGoaTwXoO/hmJqNTw7Dm5mm0plvL6fvfO U889xPJcXEjTTzM0ksshLO7saszMdySTUk5qybdsBSzAl7R+Rv5ni0dPKutTqto5ppNw/KqyOwH1 c0BHFuVVJpTpvUUz9Jnr0n4Ov1mnv1D4ve82LrHYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FUn1Pzl5W0yF5r3VLeNI9no4kYfNU5N+GY89XijsZC/mfkHIhpMstxE18h8ynCmqg7iorQ7HMhx 3Yq7FXYq7FXYq7FXjf5v/nG2nPL5e8tXAN9Ro9Rv039A7D04WBp6nUM1Ph7fFXjg6nU16Y83P0ul v1S5PAWZmYsxJYmpJ3JJzWu0axV2KuxV9Ifkr+Zq67py6FrF1y121+G3eUjlcwKtQeRPxyIAeXci jbnkc2mlz8Qo83U6vT8J4h9L1PMxwnYq7FXYq7FXYq7FXYq7FXYq7FXYqlN95s8t2I/0jUYQQaFE b1HHzWPk34Zi5Nbhh9Uh9/3OVi0Waf0xP3fex7UfzX0WEOtjBNdSCoR2AjjPgak8/wDhcwcvbWIf SDL7B+v7HOxdi5T9REftP6vtYre/mf5ouD+5eK0UdoowxPzMnqfhmsydsZpcqj7h+u3aY+x8Eedy 95/VTFdc803n1dptVv5powSVjeRmqx7IhNMxPEy5jRJPx2cwY8WEWAB8N0l8gRXnm/z/AKXazpXT 7WX65NCo5II4PjHqVrXk3FDX+btXNtodLETDqddqiYH7H1LnQPOuxV2KuxV2KuZlVSzEBQKknYAD FXhX5t/nOsyzaB5XuCEDBbzVoXZSSrA8Ld0I2qKM/foNt81+o1X8Mfm7LTaT+KXyeI5r3YuxV2Ku xV2KqtpdXNpdQ3VrI0NzA6yQyoaMroaqwPiCMINboIsUX1j+XH5g6f5w0WOcNHDq0I439irbqwoD IiklvTaux7dK1GbnBmEx5ukz4DjPky3Lmh2KuxV2KuxV2KuxVKb/AM2eW7CoudQhDA0KI3qMD7rH yYZjZdbhh9Uh9/3OVi0Waf0xP3fex6//ADY0OHktnbzXTKaKxpFGw8QTyb/hcwMnbWIfSDL7B+Pg 5+PsXKa4iI/afx8WO3/5ra9OJEtYIbVG2RqNJIo/1iQv/C5gZe2sp+kCP2n8fBz8XYuIVxEy+wfj 4sVvdY1a+FLy8muFBLBZJGZQT4KTQZrMmfJP6pE/F2ePBjh9MQPghMqbXYqler+YLPTgUJ9W57Qq en+se2X4dPKfuaMueMPewW+vrm9uGnuH5Ox2G/FR/KoPQZtYQERQdZOZkbL3L/nG7y8Y7PVPMEqj lOy2VqxBDBEpJKQSPsszINu6nNpoYbGTqdfPcRe1ZnuvdirsVdirsVeF/n1+Y96l1N5O04+lCERt Vn35uXAkWBfBOJVmI+1XjsAeWv1ec3wD4uy0eAVxn4PEM17sXYq7FXYq7FXYq7FU18seZNU8uaxD qumymOeKqsBQh0YUZCGDDceI2O+ESlH6TRYmEZbSFh7bbfmL5mubeO4iv6xSqHQ+lD0YV/kzVy7U 1MTRl9kf1Owj2XpiLEftP61T/Hvmv/lt/wCSUP8AzRkf5W1P877I/qZfyTp/5v2n9bv8e+a/+W3/ AJJQ/wDNGP8AK2p/nfZH9S/yTp/5v2n9bv8AHvmv/lt/5JQ/80Y/ytqf532R/Uv8k6f+b9p/W7/H vmv/AJbf+SUP/NGP8ran+d9kf1L/ACTp/wCb9p/W7/Hvmv8A5bf+SUP/ADRj/K2p/nfZH9S/yTp/ 5v2n9bHtW1/WtUeuoXTz8eiGioCNtkUKv4ZDNqsmX6zbdh0uPF9ApL8ob3Yq7FXYq7FWO695pW0d rWyo9wtRJKd1Q+A8WH3ZmYNLxby5OJn1PDtHmw6SR5JGkclnclmY9STuTmzArZ1xN7uhhlmmSGJS 8sjBI0HUsxoAPpwhiS+zvLGhxaF5e0/SIiGFlAkTOoIDuB8b0JNOb1bN5jhwxAdBknxSJ70zybB2 KuxV2KpX5o12HQfL2oaxNxK2UDyKjHiHkpSOOu9ObkL9OQyT4Yks8cOKQHe+Nr6+u7+8mvLyVp7q 4cyTTOaszNuSc0ZJJsu/AAFBRwJdirsVdirsVdirsVdirKvI2tmC6OmzN+5uDWEmlFkp03/mp9/z zX67BY4hzDm6PNR4T1Z5mpdo7FXYq7FXYqg7laSn33y2PJgVLJIdirsVWu6IjO7BUUEsxNAAOpJO IFqTTD9c81yT1gsGaKIH4px8LtT+Xuo/HNlg0gG8ubrs2qJ2ixzM1w3Yqzz8k/L66z5/s2kAMGmK 2oSAkgkwkCKlO4ldDTwBzJ0sOKfucbVz4YHz2fU+bd0rsVdirsVdirxT/nIzzWi2ll5ZtZ1Mkj/W dRjQnkqoB6KPTajli9D/ACqcwNbk2EQ7DQ49zIvB81zs3Yq7FXYq7FXYq7FXYq7FW0d0dXRirqQV YGhBG4IIxItQXp3ljWjqunc5Cv1qI8J1Xb/Van+UPxrmh1WDw5bcnc6fNxx35pvmO5DsVdirsVQ1 2v2W+gnJwYlD5YxdiqheXttZwGe4cRxjap6k+AHc5KEDI0GM5iIssE1fzBeakeB/dWw6Qqevux7n Nth08Ye91eXPKfuSzL2h2KuxV9If84/eVk03ys+tyEm51lqhSKcIYHdEArv8Zq3uKZtdHjqN97qN bkuVdz1PMtw3Yq7FXYqp3V1b2ltNdXMgit4EaWaVjRVRByZifAAYCaSBez418169Nr/mPUdYl5Vv Z2kjVyCyRVpEhIp9iMKv0Zo8k+KRLv8AHDhiB3JVkGbsVdirsVdirsVdirsVdirsVTPy7q50vU45 2J9B/guFHdD36H7J3/DKNTh8SFdejdgy8Er6PUYpY5okljblHIoZGHQqwqDmhIINF3QNiwuwJdir sVUrlaxH23yUeaCg8tYJfq2t2Wmx/vW5TleUcC/abtv/ACj3OXYsEp8uTVlzRhz5sD1DULm+uGnu GLE/ZX9lR/KozbY8YgKDqsmQyNlDZNg7FXYqiNN0+51HUbXT7UBrm8mjggUmgLysEWp7bnDEWaRK VCy+0tM0+307TbXT7YEW9nDHbwgmp4RKEWp+QzfRFCnnpSs2UThQ7FXYq7FXmX59ebIdL8ovpEMw Goauyx+mrFXW2U8pH2/Zbj6dD15HwOYmryVGupczR4uKd9A+ac1Tt3Yq7FXYq7FXYq7FXYq7FXYq 7FXYqzXyJrSem2lztRgS9sSQAQT8SD3ruPpzV6/Bvxj4ux0Wb+EsxzWuwdirsVadeSlfEEYhDE9b 8zwWLNb24E10pKuK/AhHjTqa9s2ODSme52Dh5tSI7DcsJmmlmkaWVy8jmrOxqTm0AAFB1hJJsrMK HYq7FXYq9S/5x+8qvqXmp9bkIFtoq1CEV5zTo6IBXb4RVq9jTMzR47lfc4WtyVHh730jm0dS7FXY q7FXYq+X/wA+dQmuvzGu4JPsWMFvBF/qtGJ/+JTHNTq5Xk9zudHGsY83nmYrlOxV2KuxV2KuxV2K uxV2KuxV2KtqrMwVQSxNABuSTirN/Lfl9LBVurhQ1626g7iMeA/yvE/R89VqdRx7D6XZ6fT8O55s tUggEdDuM1zmt4pdirsVYZ5s8s+pLJfWS0lNXmhH7fcsv+V4jv8APrs9JqaHDJ1+p01+qLDM2brn Yq7FXYq7FX0n/wA48to48kyR2cwfUPrLyanEdmjZvhiA2rwMaAj35ZtdHXBtzdRrr49+XR6hmW4b sVdirsVdir5I/Ne/+vfmJrs1QeFx6G3/AC7osP8AzLzTag3Mu800axhieUN7sVdirsVdirsVdirs VdirsVdirMPLHl8wAXt4hE5/uYmFCg/mI8f1ZrdVqL9MeTsdNgr1HmyXMJzEXatWOndf1ZVMbswr ZFLsVdiqGu13VvoOTgWJYV5o8vFS9/aL8O7XEQ7eLj28c2ml1H8MnXanT/xBi+Z7guxV2KuxVP8A yLf+abLzPZP5ZDyarI4SO3XdZVO7JKKgenQVYkjiPiqKVFuKUhIcPNqzRiYni5PsKIymJDKFWXiP UCElQ1N6EgEivtm7dCuxV2KuxV4P+Zn5i/m7o189tNbRaNYszi2urVBOsqEkD/SJAw5bV2VG8QNs 12fNlie4Oy0+DFId5eL3FxPczyXFxI01xMzSTSuSzu7GrMzHckk1JzBJt2AFLMCXYq7FXYq7FXYq 7FXYq7FXYqyvyv5fZWF9ex0IobaNvv5kfq/2s1+q1H8Mfi5+mwfxS+DKswHOdiqrbuVkA7Nsf4ZG Q2SEZlTN2KuxVTnTlEfbcfRhid0FBZcwYd5k8uNAz3tkg+rdZYl6oe7KKfZ/V8umy02pv0y5uu1G nr1R5MbzNcN2KuxVnH5Z/mdP5MuXjayhudPu5Fa9cJS6CAEUjkqoIFa8W2/1ak5kYM/B02cbUafx Ou76X8s+ZdL8x6RDqumGU2s32TLG8R5DZl+IUbi1VJQlajrm1hMSFh1GTGYGimmTYOxV2KqN9HZS Wk0d8sT2boVuEnCmIodiHDfDT54DVbpF3s+b/wAyfI/kCy9fU/LvmWzJZix0f1BOQSWJWF4OZXsq q6/N81efFAbxkPc7bBmmdpRPveZZiOY7FXYq7FXYq7FXYq7FXYqybyz5cMhjv7xaRD4oYSPteDN/ k+Hj8uuDqdTXpi5um09+osvzXOwdirsVdiqYIwZA3iMoIZt4pdirsVS9l4sV8DTLg1tEAih6YVYT 5k8vGzY3dsC1qxJdf99knpt+z4ZtNNqOLY83W6jT8O45JBmW4jsVZP8Alp5c07zH5z0/SNQd1tZz I7rH1f0o2l4V/ZDcNzl2CAlMAtOomYQJD65gggt4I7e3jWGCFRHFFGAqIiiiqqjYADYAZugKdGTa /FDsVdirHfNvkDyv5rjUava87iNGSC7iYxzRhvBhs1DuA4I9sqyYYz5tuLNKHJ4v5q/5x58xWBlu NAuE1S1G6270iuQCTtv+7fitN+QJ7LmDk0Uh9O7sMeuifq2eUzQzQTPDMjRTRMUkjcFWVlNCrA7g g5hkU5oNrMCXYq7FXYq7FXYq7FWR+XfLJuAl5eDjBUGKEjdx4nwX9fy64Wo1NemPNzNPpr9UuTMs 1rsXYq7FXYq7FUVavVSh6jcfLK5hkFfIMnYq7FUHdLSWviK/wyyHJgVLJoWuiOjI4DIwKsp3BB2I OINKRbB/MPl19Pb6xb1ezY713MZPY+3gfo+e10+o49j9Tq9Rp+DcckkzKcZmP5P3EsH5k6G8cTTM ZZIyqgkhZIXRm27IrFj7DL9Mf3gcfVC8ZfWWbl0jsVdirsVdirwr87POX5jaTqj6dFIdP0O4VTaX dorK8o6lXuDusgZTVUI+HrUHfX6rLOJrkHZaTFjkL5l4jmvdi7FXYq7FXYq7FXYqn/lzy6t8purr kLZTREG3Mg77/wAvbbMTU6jg2HNy9Pp+Lc8maqqooVQFVRRVGwAHYZqyXZAN4q7FXYq7FXYqvhfj Ip7dD9ORkNkhHZUzdirsVULpKoG/l/jkoFiULlrF2KrJYYpozHKiyRt9pGAIP0HCCQbCCAdiwTzD obabcB4qm0lJ9M7nif5Sf1ZttPn4xvzdXqMPAduT6E/Jz8tofLejx6pqMStrt+iyNzSj20bLtCOY DK9G/edN/h7VO+02DhFnmXn9Vn4zQ+kPSMynEdirsVdirsVS7zBoGl6/pM+l6nCJrScUI6MrD7Lo ezKehyM4CQos4TMTYfKfn3yJq3lDWHs7pWkspCTYX1KJMgp4Vo61oy9vlQ5p82EwNF3WHMMgsMYq MpbnVGKuxVvFXYqnnlvQDfSfWbhSLRDsP9+MOw9h3zF1Oo4BQ5uVp8HEbPJm6IiIqIoVFACqBQAD YADNUTbswKXYq7FXYq7FXYq7FXYqjoX5xg9+h+eUyFFmF+BLsVWyLyRl8RtiChAZewdirsVTjyfZ JeeZ9NidOarOkpUio/dH1Afo45mdnx4s8R5/du4faE+HBI+X37Pds7N4t2KuxV2KuxV2KuxV5Z+Y esC91gWcZBhsKpUb1kahf7qcfozke2dTx5eEcoff1/U9b2PpuDFxHnP7un62K5p3buxV2KuxV1Bi qFukowcdDsfnlkCxKhk2LsVdirsVdirsVdirsVRFo/xFfHcZCYZBE5WydirsVQEq8ZGHv+vLgdmB W4UOxVmH5WQCTzOzn/dFvI4+kqn/ABvm27GiDm90T+h1PbUiMI85D9L17OpeVdirsVdirsVdiqG1 K/i0+wuL2X7ECFyK0qR0Ue7HbKs+UY4GZ6Btw4jkmIjqXh000k0zzSsWkkYu7HqWY1Jzz+UjIknm XvYxEQAOQWYGTsVdirsVdiq2ROaFfHp88INIKAIIJB6jrlrB2FXYq7FXYq7FXYq7FV0bcXDeBwEJ CPylm7FXYqhbtTyDdiKfTlkCxKhk2LsVZ7+UduW1S+uO0cCx/TI4P/MvN32HC5yl3Cvn/Y6PtydQ jHvN/L+16jnSPNuxV2KuxV2KuxVhP5l60IrOPSY6+rccZZj29NSeI+l1r9GaHtzUgQGMc5bn3f2/ c73sTTEzOQ8o7D3/ANn3vOM5d6d2KuxV2KuxV2KuxVCXiFT6iqWB+1Sn8csgejEhBfW4/Bvw/rlv Chr64n8px4Va+uD+T8ceBWvrv+R+P9mHgVv67/kfj/ZjwK19cP8AL+OPArX1yT+UY8KtfXJfBfx/ rjwhU2s5Ge3Ut9rv/DMeYosgr5FLsVUL3l9WZlFWX4hX26/hkoc0FKDdSnwHyGZPCGKhZ6oLy3We CTlGxIBoBupKnt4jLMuA45cMhu1Yc0ckeKPJ6F+UmpSxapdQMapPGrGvjG1BT/kYc2vYs6nKPeL+ X9rqO3IeiMu418/7HsA3GdE807FXYq7FXYq7FXi/mfVP0nrl1dA1i5cIaGo9NPhUj/Wpy+nOF7Qz +LmlLpyHuH4t7jQYPCwxj15n3n8UlWYbmOxV2KuxV2KuxV2KuIB2PTFUDdWaV5caqe/cZbCbAhAy WjCpQ8h4d8tElUCCDQ9ckrWKpZrHmHTtKUeuxeY9II6F6eJBIoMztJoMmf6dh3nk4Os7Rxaceo2e 4c2Far5x1a+5JE31W3bb04z8RHu/X7qZ0ul7IxYtz6pef6v7XltX2zmy7A8EfL9f9id+R9d9WP8A Rc5q8YLW7kjdR1Tfeo6j2+WaztrRcJ8WPI8/1/jr73adh6/iHgy5jl7u78dPcy5RyYL4mmc+9GnN o1HK9iP1ZjzCQisrZOxVp1DoynowIPyOIKscl/dc+ewSvL2p1zNiL5NZNCy888na79QvDa3DgWlw d2Y0CPTZvDfofo8M67tfQ+LDiiPXH7Q8b2Nr/CnwSPol9h7/ANb2PytcvZ6rYSqwX96odu3FzRq/ Qc5fRZTDPEjvr57PT6/GJ4ZA91/J79A3KJT7Z2bxS/FXYq7FXYqk/m7VRpug3UyvwnkX0oN6Hm+1 V91FWzC7Rz+FhkevIe8/i3N7PweLmiOnM/D8U8azhnt3Yq7FXYq7FXYq7FXYq7FXEAih6Yqg54Ch 5L9j9WWRlbAhQeNHFGFcmChJ9b0DXL6ALpN8lt2kVwysfcSLyI+hfpzP0WqwY5XliZfju/b8HB12 DPkjWKQj+O/9nxYTe/l55tilc+gt11Zpo5VPInc7OUcn6M6XD23pSBvw+RH6rDzGXsXUgnbi8wf1 0Uiu9K1SzAa7s5rdTsGljZAfkWAzZYtTjyfTKMvcQ67Jp8kPqiY+8KNrczWtxHcQNwliYMje4yeX HGcTGXIscWWWOQlHmHrei39tqUMVzAQyMKuvdGAqVb3GcDqsEsMjGX9vm+g6bUxzQE4/2eSco3Fw 3gcwyHITAEEAjodxlLJ2KXYqx/X7J5UubeJgj3MThHaoVWcFakgHau+Z2kyiMoyPKJH2OPqMZnjl EcyCEB5f/LjSLBEm1AC+ux1Dbwqd+iftf7KvyGZ2t7dy5DUPRH7fn+p1ej7DxYxc/XL7Pl+tkDoI 3KqAoX7IGwA7UzUg3u7iq2fQWlzGfTbadl4mWJHK+HJQaZ30JcUQe8PAzjwyI7iiskxdirsVdirz L8y9S9fV4rFSeFmnxj/iySjH/heOcp25n4sogP4R9p/ZT1PYmDhxmZ/iP2D9tsPzSO7dirsVdirs VdirsVdirsVdiriARQ9MVQc8BT4hun6ssjK2BDdq1JCP5h+IxmNkhF5WydiqAu9A0O8Ltc2FvLJJ 9uQxrzP+zpy/HMnHrc0KEZyAHnt8nGyaPDO+KEST5b/NRsPLOlaYH/R0RgEn205u6kjvRy1D8snn 12TNXiG68h+hjp9Hjw2MYoHzP6VVlZTRhQ5SC5CJt5l4hCaEdK98hKKQVfIMnYqo3dstxEV2DjdG 8DkoSooKhp8jrW2mqJE3UH+XJZB1ChddLSWviMYcmJfQsESwwRxL9mNQo+QFM9BAfPyV+FDsVdiq jeXdvZ2st1cOEhhUs7HwGQyZIwiZS2AZ48cpyEY7kvD7+7kvL2e7kFHuJGkYDoORrQfLOAzZDOZk ept73FjEICI6ClDK2x2KuxV2KuxV2KuxV2KuxV2KuxVxAIoeh64qhZIjE4dd0B+7LAbY1SKBBAI6 HplbJ2KuxV2KrJI1kWh69jhBpBCElhaM77jscsErYkK8NwGor/a7HxyEopBV8iydiqhcW5kKyRkL Mn2GPT3ByUZVz5IbVDcyQLTizuI2U9mYgZZijcuEdWGSXDEnufQeegPn7sVdirsVYb+ZmqGDS4dP Q/Fdvyk6f3cRBp47tT7s0fbmfhxiA/iP2D9tO67EwcWQzP8ACPtP7LeaZyr1TsVdirsVdirsVdir sVdirsVdirsVWvJGgq7BR2qaYgEoQc2qW4BVVMn4A/x/DLRiKLVIbscAGQjbpWtPbtgMFtEJIj/Z NfbvlZFJtdil2KuxVogEUIqMUIWa3K1ZN18O4yyMkEKkFxyoj9ex8cEoqCr5Bk7FUTpEdv8Apmxe YqkQuYWmZjReIcVJJ2+z3zI0kgMsCeXEPvcfVRJxSA58J+57pnfPBuxV2KuxV5B531M3/mK4INYr b/R46eEZPL/hy2cV2rn8TOe6O3y/bb2fZeHw8A75b/P9lJDmudi7FXYq7FXYq7FXYq7FXYqoSXtq nWQE+A3/AFZIQJRaEk1c7iOP5Fj/AAH9csGHvRaFkv7t+shA8F2/VvlgxgLagSSanqckhVtY+UnI 9F3+nBIqjcrQ7FVRZ5V/aqPA75ExCbVUuwdnFPcZEwTauro32SDkCEt4pdiqhNbhqsmzeHY5KMmJ DoJzXhJs3YnDKPUKCr5Bk7FWd+RvOPp+npOoyfu9ltJ2/Z7CNj4fy+HTpnQ9k9pVWKZ/qn9H6vk8 92r2dd5YD3j9P6/m9CzpXnHYq7FXhnnLS30vzHeW4BWF3M1vQED05PiAX2U1X6M4vX4PDzSHTmPj +Ke00GfxMMT15H4fi0l5N4nMOnMdybxONK7kfHFXVPjirVT44q6p8cVdU+OKuqcKrZEEiFT9B8Di DSoB0ZGKt1GWgpW4q7FWwCTQdT0xVHxR+mgXv1Jysm0L8CuxV2KuxV2KqiXEq96jwORMQm1dbpD9 oFfxGQMCm1VWVhVSD8sjSVssSyDfY9jhBpSFkbsh4S9f2W7HCRfJVbIpdir0fyR5zW4WPStRci5A 421wxr6ngjH+bw8fn16jsvtTjrHk+roe/wAvf9/v58x2n2Zw3kx/T1Hd5+77vdym2b50TsVef/m1 pJktbTVY1FYCYJyAa8X3Qk+CtUf7LNH21guImOmx/H45u97Ez1IwPXcfj8cnmOc49G7FXYq7FXYq 7FXYq7FXYqpTw+otR9odMINKgiCDQ9csS1iqItI6vzPRenzyMiqLyCHYq7FXYq7FXYq7FXYq4Eg1 HXFVVLmRdj8Q9+uRMQm1UXETijinz3GR4SE2vRgopXkg6N1p88iUoTWNf0bRoBNqd3Hao1eAc1Zq UrxQVZqV3oMtw6fJlNQFtWbUQxC5mmMXn5w+TbYqYJbi8J7wRFeP/I4xfhmwx9i6g86j7z+q3Ayd sYByuXuH66ehfl//AM5Q+UtVuk0rzAH0mT4Y7XUp6GGToo9dgW9JjWpY/B1qV79Tp4zEAJkGQeX1 BgZkwFRe3wzRTRJNC6yRSANHIhDKyncEEbEHL2hC6xpcGq6ZcafOSIp1oWHUEEMp+hgDlWfCMkDA 8i24MxxTExzDwG5t5ra5ltpl4zQO0ci9aMh4kfeM4ecDEkHmHuYTEoiQ5FTyLJ2KuxV2KuxV2Kux V2KuxVDXMNayL/shkolKGAJIA6nYZNUwRAihR2yolC7FXYq7FXYq7FW1VmPFQWJ6AbnEC+Skgc0V DpGpzbpbPTxYcR/w1MyIaTLLlEuPPV4o85BEDy3rJNDBT3Lp/A5aOzs3837Q1HtHD/O+wq48pamR UvCPYs38Fy4dk5e+P4+DSe1sXdL7P1q8Xk+cj97cqp/yVLfrK5ZHsiXWX4+xql2vHpH8farR+Tog f3l0zDwVAv6y2Wx7IHWX2Ncu1z0j9quvlLTlIPqzVH+Uv/NOWfyTi75fZ+pq/lbL3R+39b5x/OyS VfzBvrLmWt7JII7ZDT4VeFJW6UrV5Cc2uk00MUKiHV6rUTyyuRYJmS4zsVfU/wDziJp/mFdA1bUJ 72QaC8/oWOnHiUNwqq00+68h8JRBxeh+LkKgHCgvoLFDx/8AM3SBZeYfrMakQ36erWlF9RfhkA8e zH55yna+Dgy8Q5S+/r+v4vV9kZ+PFwnnHb4dP1fBiOat2rsVdirsVdirsVdirsVdiqvaWF9euUtL eW4cdViRnI+fEHJ48Up/SCfcwyZYw+oge9PY/wArfMqQrdskahhX0ORaVK+KqD+GbQdkZjGzQdbL tnCDQstDybIrFJboJKv2k4EkfOrKckOxz1l9jWe2B0j9v7FaLyfbj++uHf8A1AF/XyyyPZEesj+P m1y7Xl0iPv8A1Ky+U9MBqXlYeBZf4KMsHZWLvl+Pg1HtXKekfx8VceW9G/5Z6+/N/wDmrLv5Ow/z ftP62r+Uc3877B+pXTSNLQUFrER/lKGP3tXLY6TEP4R8mqWryn+I/NWis7SI1igjjPiqgfqGWRww jyAHwapZpy5kn4q2WNbsVdirsVdirsVdir5U/O7/AMmdrP8A0bf9QsWZEOTRPmwbJMU28p+XL3zL 5l03QbIH6xqNwkAcKX9NWPxysq78Y0q7ewOKv0B8teXNJ8t6FZ6HpEPoafYpwgjJLHclmZierMzF ifE4WKZYqxn8wtFj1Hy7PMIw11YqZoHrSiggyj6UB28QM13amAZMJPWO/wCv7HY9l6g48wH8Mtv1 fa8XzkXr3Yq7FXYq7FXYqmOm+Xdc1Ir9SspZkbYS8eMe3/FjUT8cvxaXLk+mJP3fNx82qxY/qkB9 /wAubK9I/KjUpZEfVJ0t4OrRRHnL8q04D51ObTB2LMm8hoeXP9X3usz9tQArGLPny/X9zL9N/L3y tYgH6r9akH+7Lk+pX5rtH/wubTF2Zgh0s+e/7PsdVm7Tzz60PLb9v2sghhhhiWKFFjiQUSNAFUD2 A2zPjEAUOTgSkSbPNfhQo3NnaXScLmFJV3oHANK+HhjSQUovPKNhIpNo720gHwgMXSvuG5fgcrOM MxkKQ3fl7zBa1IjW6Qb8o9zT/V+E/cMgcZbBkCXNdCNzHNG8Ug2ZWFCCPHvkKZ2vW4gbo4+nb9eB VTFLsUOxV2KuxV2KuxVpnVBViAPE4q8U89flBq3mfzvf6yt/b2un3Xo8Kh3mAjhSNqpRF6pt8eWx nQa5Qsr7b/nH3yssai51C+llH2mjaKNT/sTHIR/wWPiFfDD178qfyf8AKnli6/Ttrp3p35jMVrcT SPI4jcDm4ViVUsNgwANKjocnC+rXOuj1DJsHYq0yqylWAZWFGU7gg9sVeBa/pbaXrN3YN0gkIQ+K H4kP0qQc4fVYfDySj3H+x7nS5vExxl3j+1L8ob21VmYKoJYmgA3JOICkp5p3kjzRfjlFYPHHt8c9 IhuKggPQn6Bmbi7OzT5Rr37OFl7RwQ5yv3bsp0z8o3qj6nfLQN8cNupNV9pG40/4DNli7E6zl8B+ v9jrM3bnSEfif1ftZlp3lLy5p9Da2EQdSGWRx6jgjuGfkR9GbbFosOP6Yj7/AL3U5dbmyfVI/d9y bZlOK7FXYq7FXYq7FXYq7FVKe1tbheM8KSgdA6huvz+WNKCkWoeSdNnFbVjav4CrqfoJr+OQOMNg yFILvylrlpVolE6AVLQtv16cTRvuyswLYMgSx5b23k9OZWRx1SRSp/GhyBDMFUXUF/aQj5Gv9MaV VW7ganxUJ7EYFVVZWFVII8RviriQBUmgHUnFUNNfINo/iPiemGlQTu7mrmpxVrFWReVfLr3cyXt0 g+poaorb+ow26fyg9a/LxyyEba5zrZneXNDsVdirsVYV538iXeuajBe2Uscb8PSnEpIFFJKsOIap 3pmo7Q7OlmmJRIG1G3cdndpRwwMZAnexShp35S6ZEQ1/eS3J2PCICJfcGvMn6KZDF2JAfVIy+xnl 7bmfpiI/b+plmk+X9H0lWXT7VYOf22BLMfYsxZqfTmzwabHi+gU6vPqcmX6zaYZe0OxV2KuxV2Ku xV2KuxV2KuxV2KuxV2KuxVRu7K1vITDcxiSM9j/AjcYCLSDSQ3vkbTpSWtZHtif2f7xfuJDf8NkD jDMZCkV55O1m3HJEW4UVJMR3FP8AJbifurkDAsxkCXjRtX/5Yrjb/ip/6YOEsuIK40PX5lANpKQO nIcev+tTHgK8YVB5S8wEf7y/8lI/+asPAUcYVU8ma4w3RE9mcfwrj4ZR4gRlr5EvfXT61NGIK/vP TLF6eAqoGSGNByBmccaRRrHGOKIAqqOwAoBlrSuxV2Kv/9k= - - - - proof:pdf - uuid:65E6390686CF11DBA6E2D887CEACB407 - xmp.did:d4d07395-aa96-47c2-a9e5-d0351947bb0c - uuid:c63c1031-e157-9748-9c58-86481308e954 - - uuid:1abccb90-0c26-4942-b156-fd2eb962e3e1 - xmp.did:58fdc1b8-1448-3a44-9e20-282d8ec1cf95 - uuid:65E6390686CF11DBA6E2D887CEACB407 - proof:pdf - - - - - saved - xmp.iid:d4d07395-aa96-47c2-a9e5-d0351947bb0c - 2016-06-15T14:23:10-04:00 - Adobe Illustrator CC 2015 (Macintosh) - / - - - - Web - Document - 1 - True - False - - 128.000000 - 128.000000 - Pixels - - - - Cyan - Magenta - Yellow - Black - - - - - - Default Swatch Group - 0 - - - - White - RGB - PROCESS - 255 - 255 - 255 - - - Black - RGB - PROCESS - 0 - 0 - 0 - - - RGB Red - RGB - PROCESS - 255 - 0 - 0 - - - RGB Yellow - RGB - PROCESS - 255 - 255 - 0 - - - RGB Green - RGB - PROCESS - 0 - 255 - 0 - - - RGB Cyan - RGB - PROCESS - 0 - 255 - 255 - - - RGB Blue - RGB - PROCESS - 0 - 0 - 255 - - - RGB Magenta - RGB - PROCESS - 255 - 0 - 255 - - - R=193 G=39 B=45 - RGB - PROCESS - 193 - 39 - 45 - - - R=237 G=28 B=36 - RGB - PROCESS - 237 - 28 - 36 - - - R=241 G=90 B=36 - RGB - PROCESS - 241 - 90 - 36 - - - R=247 G=147 B=30 - RGB - PROCESS - 247 - 147 - 30 - - - R=251 G=176 B=59 - RGB - PROCESS - 251 - 176 - 59 - - - R=252 G=238 B=33 - RGB - PROCESS - 252 - 238 - 33 - - - R=217 G=224 B=33 - RGB - PROCESS - 217 - 224 - 33 - - - R=140 G=198 B=63 - RGB - PROCESS - 140 - 198 - 63 - - - R=57 G=181 B=74 - RGB - PROCESS - 57 - 181 - 74 - - - R=0 G=146 B=69 - RGB - PROCESS - 0 - 146 - 69 - - - R=0 G=104 B=55 - RGB - PROCESS - 0 - 104 - 55 - - - R=34 G=181 B=115 - RGB - PROCESS - 34 - 181 - 115 - - - R=0 G=169 B=157 - RGB - PROCESS - 0 - 169 - 157 - - - R=41 G=171 B=226 - RGB - PROCESS - 41 - 171 - 226 - - - R=0 G=113 B=188 - RGB - PROCESS - 0 - 113 - 188 - - - R=46 G=49 B=146 - RGB - PROCESS - 46 - 49 - 146 - - - R=27 G=20 B=100 - RGB - PROCESS - 27 - 20 - 100 - - - R=102 G=45 B=145 - RGB - PROCESS - 102 - 45 - 145 - - - R=147 G=39 B=143 - RGB - PROCESS - 147 - 39 - 143 - - - R=158 G=0 B=93 - RGB - PROCESS - 158 - 0 - 93 - - - R=212 G=20 B=90 - RGB - PROCESS - 212 - 20 - 90 - - - R=237 G=30 B=121 - RGB - PROCESS - 237 - 30 - 121 - - - R=199 G=178 B=153 - RGB - PROCESS - 199 - 178 - 153 - - - R=153 G=134 B=117 - RGB - PROCESS - 153 - 134 - 117 - - - R=115 G=99 B=87 - RGB - PROCESS - 115 - 99 - 87 - - - R=83 G=71 B=65 - RGB - PROCESS - 83 - 71 - 65 - - - R=198 G=156 B=109 - RGB - PROCESS - 198 - 156 - 109 - - - R=166 G=124 B=82 - RGB - PROCESS - 166 - 124 - 82 - - - R=140 G=98 B=57 - RGB - PROCESS - 140 - 98 - 57 - - - R=117 G=76 B=36 - RGB - PROCESS - 117 - 76 - 36 - - - R=96 G=56 B=19 - RGB - PROCESS - 96 - 56 - 19 - - - R=66 G=33 B=11 - RGB - PROCESS - 66 - 33 - 11 - - - - - - Grays - 1 - - - - R=0 G=0 B=0 - RGB - PROCESS - 0 - 0 - 0 - - - R=26 G=26 B=26 - RGB - PROCESS - 26 - 26 - 26 - - - R=51 G=51 B=51 - RGB - PROCESS - 51 - 51 - 51 - - - R=77 G=77 B=77 - RGB - PROCESS - 77 - 77 - 77 - - - R=102 G=102 B=102 - RGB - PROCESS - 102 - 102 - 102 - - - R=128 G=128 B=128 - RGB - PROCESS - 128 - 128 - 128 - - - R=153 G=153 B=153 - RGB - PROCESS - 153 - 153 - 153 - - - R=179 G=179 B=179 - RGB - PROCESS - 179 - 179 - 179 - - - R=204 G=204 B=204 - RGB - PROCESS - 204 - 204 - 204 - - - R=230 G=230 B=230 - RGB - PROCESS - 230 - 230 - 230 - - - R=242 G=242 B=242 - RGB - PROCESS - 242 - 242 - 242 - - - - - - Web Color Group - 1 - - - - R=63 G=169 B=245 - RGB - PROCESS - 63 - 169 - 245 - - - R=122 G=201 B=67 - RGB - PROCESS - 122 - 201 - 67 - - - R=255 G=147 B=30 - RGB - PROCESS - 255 - 147 - 30 - - - R=255 G=29 B=37 - RGB - PROCESS - 255 - 29 - 37 - - - R=255 G=123 B=172 - RGB - PROCESS - 255 - 123 - 172 - - - R=189 G=204 B=212 - RGB - PROCESS - 189 - 204 - 212 - - - - - - - Adobe PDF library 15.00 - - - - - - - - - - - - - - - - - - - - - - - - - endstream endobj 3 0 obj <> endobj 7 0 obj <>/Resources<>/ExtGState<>/ProcSet[/PDF/ImageC]/Properties<>/XObject<>>>/Thumb 14 0 R/TrimBox[0.0 0.0 128.0 128.0]/Type/Page>> endobj 8 0 obj <>stream -HwVu6PprqV*234R04S32P4ճT(J -W*w6PH/H+X)Hwr.gK>W /@.ӊ endstream endobj 9 0 obj <> endobj 14 0 obj <>stream -8;W:dYmnJk$j=`^PKX*GV"-/6MPPhMW4o*jFegTA5n:ROqi. -8M?-(/t#IN>re.=TbIMqYWQK1D%b&pOLGa]H?hKs'8Gqa4A/k;[i&\e-=4:h!/H6BW;~> endstream endobj 16 0 obj [/Indexed/DeviceRGB 255 17 0 R] endobj 17 0 obj <>stream -8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 -b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` -E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn -6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( -l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 13 0 obj <>/Filter/FlateDecode/Height 947/Intent/RelativeColorimetric/Length 90241/Name/X/SMask 18 0 R/Subtype/Image/Type/XObject/Width 880>>stream -Hoi@Hy&8_nyA'?6G3+ZHҥYakOj6gיoU GHII_AgK/EcF6LrchI 2$҆ԘU4w$5_7BQUm"Ť>&k2W$%Nib;Iߓuavտ,HJ \u.&1ٌ^₞@Ǥl_Lrs:#ј,32] IJ7d+65i1$Lb#d]G>&Y=g답*_/*:p.uʙcRIf") ˬ#q4Ό=sL&=(P{ HJ+b~n+cSFsm0'&&cܼXI=3zER,D#0)2=r -I Ә}즟 (9?l?ݳ;݃Q~twoo `41)"g476WxzGMݞx7hpqh{ƃn\ w Zᶂ37{M23>)25Eܩo|+>8q/8m y3=??~wL#I\dΕ/doޱ=Hh|d]ү$*wOc} yz<*\@~R/}}FRHxw G]as &lu9x")`m;=-Ƀ -O8Cmȑ{mG.&?){3];,V01o`it4)'ѭU, ]?b<ݳN=;.]Lں*_w6}hL[I$np+bdjlb46[ܩ0k`I{-ɯG_>]zt8rI_K}4јغOinӭng`HUN4ݛ|yRr #+x/>骞SyXAQȃękfUXDvE#&sBe< HQ%\Ωdg'sǟá:>xQ -!K -W<* '%Y%Vmao!ǩkv>w u{=Q<\ȃ*fƸmqY%ŏRpV{ ueW&)!)sE2Jݓ?ҋӆgohԎeɝiFGb}/g. -,%m.7'FX!TՀITV $y9Iפ?_ȼ0;Wi;h9:FQ]itB)`5yIe[j*lraպS"u 3$hۯNT´A}ٷO.ϡqˤ3!"Iڻkha˳J)@) iq1J٦oչrEIWt+>]hlrW9,-nr_}i#eR=椔 5 ](?"aIK9;z>g9d68 =FGY/Հ@@ 9ۋFJT2[̟~>:ekG<Q2B&M)}YƢ}\ekfeJ#.-3 -iHF'>hd,I#_ыTj~Q5cR`n:s e8 P/di]Ҩm +!g֝V=@nI${%3Tj[ԣ`Į;m$XOT4==Aŵ͆ޭ|hˑ"6XWvZY,{&y` wɵs/NٮMDz3z2 F^ęA r۝gB7hu ȲhI})CF -WWzٶ:lgl7ɃHiJ&/ Ӻg.}C'dD|V֪'9TL*4I]6 x74MVK%X T8ENZ!f8ah@&͚-AsׄOQ"2sO-ʃ#db-tgHIFHVj.Y!х@Cdҕ@2ǯeqyJΎC43nw0"D2ȥXiQϖJ'&:?Ed!ªGIKSيb$utõǭ2'}~dbNct`d K񕨈\29juC ڣy 5,ҧ9.~&g(r\&$zkddjd_&n,dk딝]|ڷт$lw)-H](H&, tHU4ѐxIE$\+Kl֓ȁI*?^^O/N*)iit<~O&=۠SZ0LK578hsZ?5ĬeV"k K ->#gV-?}= TjOK<>Nh auOBnY#qkB)fiQ@ 7@Olo-n 9 =)~erŗzC9z9Ilr&JO-NbW3Ӳh adR<+}D?NJiPJG%:?5pn2TI2v֋rBk &l f'<[wm}2I4KtwH r"!hͣ .:17f͝$Wq`Z Z 'R\%n~2/f|i|a*J&r>-fj@eRc}'yGBvr*'r ->|.WB~0ɱ{H ܌232ɤMRe7r!ic/_Ȗm͇OJ!dfH)OtE9#jԝj'C]aպWf+>KctȁL&rK%SͧH&1'ejuQA2p!Ϟ* UKW?-02hZ!nKO.? ZdzѤ٣wrLI*΍Sі2+TI,5N$6ぺ G7 whQXI4:?5ƫhq-Ǿ(v,vHz&.aKiݵdTOph2E ɤ0J>-zBb8,A6Mgd͝$K I,E[ 8ƶ0yTS rlS]|ѩ+&_9Ezb(Jr2h+ɓ)⇻A!_:۪.%ٱ4E3w~ sOq9F$~EH(M"0>"C|)4 {edUŭ߿/|}e.tD _(^u)TJ 8([E;ZgbDR!TY;g$÷=W__h8pü8SqO㠸*5:Mb)r2`Ny@:iXg*-X=zb-osW̟Y[V$J|!h|$p6V2LRwsU""YA(\A[u0#0j>k6NZ *P$Idv`;K?29G3@/)hqGaLH&)#f2ݥ:"@ -c1BuUU!hB -m?IXqBf=O-uS]*pb Lp=a d0 '%}QJ1kv-E&Y%͇ѓ!L6y֯-ZNſw@ME<V -+Qf1XGbu.AL}{;j:1XM;`m)ݒr2??bӥ^"T.4{7V:7cO n]&IIʴ׭]׳L&ټ~e?618qW 6$уS-J+j &HR#Y(u3C"vaVO qˤg/{Nd* w4~8`ਡODT -( ƃ(rVu6z0F|iU6_Up_ |7//y e26kaE9JTh<'| e\xy(7QQ1Z)7#5eoӈ0g+ۨwCxc XSg")-nkEJN[* FAKLLR7R.LBME#@* -~U݊tEEVOt7U콊ؾxԜ'isjf=O[ZO ((A>&]r"т|f0`A|0/2}+58{:!ELTǝuB1HzGQ0g[|Q_[VSor^Gy?lD$g=?ȩՕLN9 -K*RYģpu0%K'*- lpD ID2MnݾbL)OYׯR3OF"R j"iO8%{Pqs ?Hee}-XJNm\-H/}GϩZ&L/u>7&I wQ) /+P.bP"$N55B]={2`[Hnk?-s\粓y*dqP9I,1k[`^A/n3ՕcVna-_s%YSM{b FǠoZnkE%yt$mAs%Ev]JW^xA Xk0v_KӉ i$Fߪ/u( jLIO%k/Sr7B>Y,770ݙs)]ubQ9OΈL12$jn*2*쾊O&iV"sr+ 2LjȞCxҜJ 9:Jʌ H(hxA&xk i&Iu$6ԕj:.E Q\-^*%V@V;RM]dLW<}*w&Kߊ{-7@-&;. -C66 @9TBUfI[#v1r`,f/5n TLֹޤosIwT&\ߍ#UBXJ&uo6TE-EHLu[FUf -x謖Xz{FEr6qiVd>սl -\Uv^dKCR&p6kڄo@)ɛzxZZfBv5nFC `r{Lŷy7g2H&;x@kASYQC,29Wp -c!{)r*Rj!&#8ˁScM}Zi*H&Mf$\P -Ŵ\Id eDҐIЍF1|CeH ldԬ6i.2K8t׎&t(Q+ZfB*R&~?g4|W~ !$1[NIkqS(T['iͧ4*m~@?>KT)ΕJ3t -dEÀ."!|g_F>";,o)%OQ~Z2FBt-Ŵ=ݗJ)Rbd/}0i -3%f_,%.u;}oZ_`>19)ۂ֙Ĥ b- 2z[&;BEz1i6Cӯ!G9htj9'I#8ˁB!=*t-T:zTG\2z;F3}ZgLhHӍָ!fiVL:,N0EwHVsR I !-U֐L1#4zvB`V|u 4i$\"&^Xjbޟ mR q6H JER/-N&w',͌ƹӿ8!fI|1TBS?D%}35fW̊CȊ\!/5n:VC1@#&R&2rÚ!"H OS&c1[pUyuN'v96c9)Ӿ/I W%f<}*Gz{-/0D֧)T!LSXva?:n[ʜUX% *R&~o㹤w-dr>و'r*+*1=9)zKy$Sp$"ӔIWcQ@&~!AZۑ[W *'W|쌰95n}ăF.i(xyB [(58|i+&Yjhz>˜2m^?fA)\('N,1^{,gI&}<Ǿ dTVԀaI$7XUkt sU dl:OOٯ<2w)6E =Lq<{ hK|7%FE=ikA(#cia78Hw:;i'}h%Z^N^7VҺuQIHNcMft+ -0 '0$:HJ\ ;``pPL=NM.H1Eb~ԓvsK`TO=hwѓ7|GOYZaȎL7e Ջ[녤&Ɍ;b1lx"Iw!}9pXfv`7xgV~π\\zπD-/؁_~ɬebJz!QyrTS蕴.}?]$i;o"):F)(&ۂS^/Ǧ1IG )!bZ1 p8ĕT-@Kp -m crE?m}F!e_JRPF -7b1T}<x4zV,&읲yeTJ=q#cz>)Rv:5[QГO"5o) c^mXyTعT=%o-oK2U~c͠B>(h1*h|:6Ll' ޑ4 =ui7eGo{s͈KjDE1!3e00R,I %y)1]5ά>#Vgr|%vVV>&I5lS<v Z ;RLj/1'{uO -ؐsz( o?;I&mT@L>`|wdF[pY!=;Yk AR;o^2Lh ~_ٛ|GdO q/zRqcw_}9~eiq$i[?~$N%Y72zىSEx1,HDlEg3JJy}F}7)?'|(GoŤ*isFGO=`r3R8)p/MM$j٪u=9(&˜|m5(t75zM'O \]]ITٱ3u0Ǜe/$iF1Da*b1Tzz_W8/wzg'=srV~@?];(&0H1ʿ[P=kW˻zBf`d.d*XJZC^mt.'h V QLqr9wTҺ辣QEx=D19-d!}?d!}3Z#)nmDzly_|1^Nd`Q0l9'0NnbX9T0ZdWf˂8d=pJl#&BsԨA7FDqڇJZ*レT=eSH'YTo4>doE|~ $#&1¾W` ڑ1)د6:'P}O࿠ne*Fرs|q -(iC4P+ $ -cT6^b-4je˷O|zS~?_qCjRr̖E˓>jEk.C-n#E<IO{vE5ӄ&EN& ݆)-:< I'%}8HwяV4d=Yv 1|Dh*; <Okr(Ny%*~*Yy&WA4!x2zsY-L"=\Le5ƔRFI%IF\3_87 0>hq x2`+IǙBh#R8-w*Ó1YeVO[x!mh?[ <$|`2s2l嚽O EҌX)#Z!Sр4Yoy?ªf8jO1O_9O"%z dy.nNY2u5UV\Q~ɲ|kxrd'?apK tE7s!m`jqZv[>hZ-%6}az,ڜdKɷGM)،xd"6T\l,&c<'Uf; -w&B gw)#„S]\QVQ>$I_jJX,\^YSd|'zD%{o1!䅇qx';qڈ>
khYӷ@mwGyxbr ~=ͱ{9hsۈ!x2< !f!mf" e!ONYW,B-%'>,;} ^&CE"OtzK68]dGRfɈt}B)ILN-^3d[ɿ/3GlV&77ug#K1P)^I\D}&/o>߭SHh+<1Iy_uA^ -sMzC*d\'\z1zADd& -9$Y"?LtzK_14*Y|!ԯ)7$URyuf۲/ɖ$yhs:ڏa<#<(){;qSdLt&}ZHy$yyLܘ: ew}\yYj|aIQ%#?xCE"Oq1Nb5˵I~t֌ZcEb-%Շ8}@i?qqN~ Ndl'\z¤.o !جlݜ(B],YoSO&w"0fr -L&\Pby?ޓur!m FZKGbrΓͲc)eE+WqytT?w ]]}["֫K5Ez~J+T3YnO26hSEH'㷢MO$ta0?j9VuK'/K~CcζI-OV/KѸmkҡuΦ)"&㸔]LyĂX)cML=yn\Ω۬ crї'ma5r(E=pu7< - [rd{d7.`w(d;wr(M=zRy -7]=!Ij9Cidy!NmSiǯƆX9r R:wP<+y^{᬴$eYn;ﷂ2^%)uũ Kw Eߊ. UxlidW)I5Ip΍y%gMGƔd Z9"~t]utڵɲ2E#{Xtp7 #i4i>f-2x jL?bGœ{y9k -AQש'=FE4b2&al6>` -hB");Is*QY9c"1鲒z2klvy0 7>`%JN dXn; ŘWO8@g,צ)_cvH$q\ѾM_@%Ƈ؛XBRcΜJcRΆ1xZU]è-9NEw'c뜠I=]c fi~>?!NI&ļfb2 Z8,W䥌e|a(!me?MQH'cMsY*+!\VuSLQ5Kp#}֓mj:SHú5\bØC)I0> jYn>_+c t57*pT̛6=nW%4EQ4z2~+ɜEO1$|T9c^Jt,Ύ9AŵK2Ɏ'+{,uCL/ڻAZ" -d֕MW.oBgDtʿv(uX4Kp}Gߓ-8,]o^ѓ[J^c(NY]$eo h9[ƣ:1vt᥌e| q'Kp4 b>HAB t2n{'ͩ(*rGOӡz>(-ɘ-d6=г.k؉NO&37{UGɓ*Ysgzҋ>XA[ &f.oqC󢔱gs.$e/;?n>.0&cT51}<;(*FOkE;a\.S+S=˖&EǗo3rLdA˿}yb/IE\E"*;pIыeCbuZ ){ٻ #=I_cN|k)rd@Q?h.3Ed^ts*g5 xQX욮&VO䩪(Idt1>ðh[[AEX̕ϺܓyK{./T˱^>xL,Mo'svy[/*|OJgC{::EQ1SDLk%>>Ѕ<I t -eo$7-gHIΆ(&-^^rs *BadVؓ-W*j͉IvHʞNLO:W%_31dӨXܸvH^|Tݓ+9/Hrdz_wq\e?@MQ̙ݙ"NuhEA(iK̙}v(AHQ"@D(WDUlMĎ-'Eyf&٦Q|~GV ?|mzR3|}pӬ3 QJoJpd\nc\7n,Aײmɏzs ޡ22JywoK2/X'& ځNJ* pbR3|w) A7ɤbrc )#f dp[M_&g][ه?@O*v'd5Ä-`˨[ GOFntLλ=95fPL -&!&;=@OK1Ŏ=5:2J.778&$k4RJGL*sͽ֨+IN,Iij&}`K闍sEvDRϿd}OIb_93Da`(r\|@O@Moߎ$gi)R~cb]_+$H!n~F]1GL*vZFi2T'{ z\ARFMbz~wb1Qe5+zRb25em-3_~Ȯ) ֧I/_Ox^JIQOyT=F9CW鉣)fʴRڴ1[Ig - &NA@Ot\ᏻNoV0[%dRїbۤARJwu oX7h4b0$m~a'U:냰6G8XГOГWuNVL1҅ Qp00bIe΍{֠ 6 =q(B:w6Ñrޯ[?^ORLRIv+/gRJ:A{MAOzIN0n/{Nac5H$,+ԓr~ ~OWllfC+0+ГwځZWBA9%gѓ-y唋w-GF)Uk(5?C%;=GLj/bIIzYw~<HיմFi1GL*t\۵ŤH5$gP!||5Y,_;$8hdẂHh C~aГʜ` 2$R 2-D=yq{IeMИJ)1 wT2#&:=FI'@L&ƥfZDRʲ2n%%AL)~(_"H1IW0J W'91S;nZk PJk3=) {=^)&/75fPOG3-#&7@.d{LO\ ~OywtALL%h//8IeN@7sbHbۼ1W2\jn} bRn@z4M6 -'?Ztw -٫0T?^Г" [od% I'{}q{LII6WeVgROS8 K@D\>&;sp6"l\Z= SԞ89c-|~ۘlr\iƌU?D'3&Haaőom“ߓ?wP9Ô"BH$&;m5wI'6WvyCi~tw g#̵͎.p9 FЧziۈ$)&$#})r%R+ [=ړ~t; ՛!Qײ^Gzr}\10Ouc+#C͂&(_HP(D -d!թXKtkcZ*yͅ (  w¯<\*Db,$=OVp'1K.:|/lӥ+i&o练Ɏ[UlL=t _wHY{D'C%A)Cyt)8tDzPˠ|!B!DDEg ̓~WF/Vs'A\U/! -.a{0Ç)zfnڛ>< -.ĕ#_uMLzb)ZOVfc+UA)" -4D')58=26L">^&Ư~nc#{Uҭ' T Z; $U:ri -_͒K 쳷x#LJ4K\4^mΔX][XVBf@)5:'7}OV2L=Piϲgc0Yh-8iҧVk6\o'Nq|$T($)y6Aߓg O"Hb1flsarEtku*F?L$ |)> R ѸoB(L7H>IwUhc}[3;/)go2qCJ= RHOY$BkѧJ6)b Fl{h-8թrbVyZgcF;HҢt@ʰߓŤA#v齌3BILODxRzI;e5 Rkw'w9OD)Ý$)Cy|O[X)7PG$E,̿6D\GLJ_VO,Zhֻ\/of>!Ç6A0ߓN(+MC d.ia1^j&VmXuw}q:ZLa\RM24Iaw ! -yš|%0KeX\vIِ)H{fZ;qRC{ /Dny]&OkꉥUS=l'凯Gw%)H3ct:BxO 3$0.e#PɶO -|XZE_\(ZODhғŔA-]%f.>nEѫpE/zT(ЄOJs-M*_*PEJ}{ Pm3\)W>[w*]d:@,wZ$IQ@97,aNEd$KeR,}jV]RDP($)]ߎccC$BhGlkFbRz@>ZVN|H[l$R=y:QE>HiϯFxbJjVV5ܮyR^ -rk'eG!% :W!G{DNhJ\9\wACl -wϱR>"j'3J)_PKwG&) wZtݠVwgc)ßHaO&nr#󬦲l'3yxY}fdKJdARH9E}%TTҌS4<zbtXG٧WgB'WVD1T}gLbi[wǚS$B Lٹ#VLXC+;ݪd #+oIA{0]ceR(d֙F3$Bxt@V IғVm̴dE!)yJ>D*:!Zy1Mz/PBIf0 Ҏɸ; Gځ*z͹6HOI`)\NW~㠧£aITVߓMӬ$B'ctL&ݪ\Ylik>Wccyh)GՋ`Ji\1W݅JHrmL*w@ʡxѲHzCx /+΅cf%&B_$Gc&/ ɴ.>)=yV`pB_5B#:ʹv't,;fs8KUeD 0p1>$Bhg91jILJup6ꭕ7k6$?:L2zקb`};=R=fyq($&jkeMkoxs9["L -UB.t/MO0tx!Dn}~yLҿV]=2f^CQ_uyp%(I͹䈬!I-yBs95kIAQnԩ{Sѯp篧Gm2=d7R&Ӻ4M 54;=NGc2ncRt`AJxw&ӷ4p#BΣ)Óe6y0)%L^_׫rhe{-G^O9&%4j2Q/ -LAHiL"CɇvMå)30PfۿՂXa0)QqVgsIV^UB~l׋ Gސp3 L4RI9&M?V !$)n+Ye6\V0YO'j Sm,u^)dw>R CҠ/eO 5`2 j'#=%JXHPe9zTw?pf}iTnQntwR)OX"pO%tT&Ϗ]3)$7ePf[pr޸||l5pndғ+ĽH9zK6]mŤ zͿR8!>u=oD!h`EE}^:zO)vNVB%Ϩtg13nՅvkj,2Y'@]>';qQ)dzٳbT O? :wu\hvߟ#zٜl]CV rneu&w{LJw'R-FE?!R/DJrI$d<12,REV%U<7g:fg^d`bcꩶ[:+7>VΫq ҴP+}coVD0+ [uBKZ~ RyUs.++3UgD0:=/mCԁ*#iU}$]9e88 ?N!"::-_:kúXQG9zչ'Iy\8R*KƸ/!~Tk4NFIʞ.9])3#ByoL>{cRnn[n7V>)WKRQY#UzO?'s=Рg]duC. R> -'}nMty!׸/0y([v7t%OZ`bsI=P'L'ol3{%RJ }&|DQ5M,$)KBcuq\B銞SV'Ofw57'H#RΘs9'R/)Ȗ1BrTk,pY -}+;B(Ř ɯGE'ts+ mF\wO;KlTȒ_SOcf@=-M//:GnW?qPgE9oO%`^ xm{5Hܞ^Ĕ™M:'Z!2Wƶ4ro+nopEfDjtKГAbk&V=Bj%ܡHIc8gRchJ\#YYZi\\h<]R]Q_^vЮPv7>>i <]NlskT?'P5mIF@OU)xUaT}#F;B@ =~_ `rm,Z="]H ?jjR*~yT TI*{zԢ,HF -W!DdUb;<޵s[#Vۦ-Q|_緍Thwr+PKR*B@E` kR.Itiai^ArȥkP_ڃbDJl2ˣfgIrlX?gw>X<}"W -*e Oh)5ЋPŅ]lxh7&\B{ԭxhvRzE,Y0C>yF UJA)_~D7DAA$;)Q&%AGt)K^y4ν* o{MO8pr\r@x"Bqبrۑ]JƾИk7lJ){'@oJMNJ"\ Fc}IJӴq%M d* ,l(:KU+͌'ᏏGJ)23,I#kLZ 9:F-G'\Aθnqޒ`w.kY=ʆ7 ?>:ϕJ1+4V(*il"K!ʓ2G9.]*ӱU@)[r7]>cےuLDMbV [FQ )zeK W2|2& %n0I]4ukǢYNfh;Afbke2$op푮^J2\2޴ )HR>}yRE*oyd } iAbI_ :KUli -d4<@{d΍b}rJ4E7l;|@*w&$!ϧUke2t-:] -,!߼l]}~ =oz{֤X5Q$>eL)Lҹא/] -Og6*beC>7WH+#bX&8)rXu$|RGmkÛVlR޼lURՙ+ڑB#0UIEKأ9~,bԲ surX`,Pvx#Er TUUnMt)NOsT,pa]~C@0 蓉-#$Z|f—)E\%.rolϛ͂ / ߍbE2yL{L& "t{~@C"a(R -tRuf:NReؚ3CQJXkl cfS,hICc=u0_Wfk>knL1ז^O> ~Q'tz`'#W xV -t`O=?7F{Nvfowvv*QJ*0 -D?ޙa B J_$<z;i{wF#e={\&C[r!7&'kn¼~Ѻ{]2 @ *n{Q^Qw+eǔwT>~',U)+DBGbe!z/E"-|tʌWXbvF<6NHP&?pdrA[_Wm_ -5?&PF1J'3p|R]]9M]9LL2 Q -LrHP<ɤv4ΒV^ZYv?`vFRB(M(  -H4JoէX)Ϣ G)<Ʈ@C*p&̟\q7H&5UQ^Z^u-R)E7?A|^u60H%LϐORКr{$$A@$n|^v$zn₰WSo_Z[sSrdRޛ>||R -% -X3J*%0|,ϙ"g,39!+\JdR"NtgQ^ҊRlr?R)i'a,P * Jycq?DVI1? IM<(.-i[-gb\~{ ֟!ɥOZ:,Ø9{ٵJ:36pVݕII- o޾Ѳcݷ85kk,K(;9g' 8r[a/#<4+, -:VInI(o d^r@ԛ/{w?p_&4(eDRcёD>]+Xkdqj22y{6pdRw S^yK RE)10Kҟ(. E6L, bT!ЕLnTνe%U-V* [}iIX+ٯUBT&C86OʳDP1[]\/&ְUڪKKjn#2LvɩO%JzQΎ~.' τ9+RTDL.tdR>"V+[Wo__w7B/W3 O.9+Sl>t]ӉO"oOB|r -VNͪ^32 X)mP ?sbֺVf{D0/#o`7ΒVm_Z_~ BpSETTzaLtZv2Z)8Jq%S&I?IHd:A7.ɲG=Pkɳ)?(_;YDlgO_!;FJ**e' )2H& zӏz,T=Y]=+eQ/0g"cb|蛲IkF $!YrڪKK4.»5Jj;>i:':nA){;,nXepx}P<4MXyd@=K?IFmr[ŬUT=Nr I!ԡ +b(9qarJans=Iu f'-'Lu,QJ RtRJHEx<'*\aOpw@ӣe nhzuFa·-T_~M2I<L3+vm n a`Vx|€q*&L:t+/,C&MXeUH8mL^UI2IV9{5)uR -ƏA)(#nao$<2cIGG&/Zw$Q ھފ}!iD_~ϾzdÈ40ɴb)+2 dtd}wCxkW 1  >U ~lUH&6(^q10Po=&L_v|ș$+Aer@B6.䉳:/ Jy'Qʹ sx7o}'oLO sSao^<I) -2 -$lWS/`_wt7U6?J)p0g%z*u#"#eDy ڔן8ȈggnW4c[4R~zŰ:,,G L}ʳAHLBoHxVۗ~,9ʛ;`:A)10C|E"Edxvۭ -tbX:OZ` RFyxQh$TIcz78'a0y&'2Yd̟L/b]U/`_w[Q|$wYKRwEWp =NdcTWd@gBDHvrPXx^`aL?$I&Q`OnfY)*͎LL cz$GD<Rѕ۳dIⅉvkLn{yL2U+»=J$FwB! LouM_9[ƵR`#JA }IlVk^ݍ[[$<9Z; z>I)E߆Eܘ&LiÐ/Q/|e0A EߊH&~ȑq?, TUt<d`_3MG*&􏩧w -H\A=xraOjVDEI`NI&Ό8隧ϗ7E69t}y^&+tz“/޽%vp\ԧ">AnB.WC(yS&rt+YHJ W"U|C~KJǬwRŔ!3c[1 FdI2qǐxCo)Bi)LN0&%6$5KL#xG;n䟴T%m8<9%S4o6 I@Kq=ow;Gnۊhх|!`Jd}<v,Yl6I&ozIki[іp뺤u13O*QL#dI'LH;, o+R1 ;ϝ DDX| R&K!R]?y;i'|WKCr0X,>{;P7`Hdt~ìsVBF *`V7'98QN;&Uqk¤Th:0l.0Ϲm>h7JmTEHy R}ӿ_J9pIcT~B{Lh"axov% L"%˛:0Nܮ7Jm7XۊdJY>;)E{d ;L*;[0&|X$Hl٘LD'qYTjd]#mcw%R/oSa~/؉0+ ^&? -\CD}#IgJE>Bm'Rӆ"ixؐcχn' =B3]LItf-"`*E'GI)\R&'vؒNK)¤V3,L*> E}o|| -Dң[+lvu,ޒfZ˭+G_2T$fvS3DJUzDJF+7$; S2[+K}t]aBz1e m л~> R"eΙFc!έ|F W"%FI O)KHrڰ8:3Rƿ$ %R$Z㼩{W"=q t)pmyۊd@}zY:3JJ[F,qB&$Haos\7h=$%L&8ͤj߹4n1ȡ3)틤 % -n^_G,hZm|R0e"<9XiI$O.>j<3!R i^L LrД/15D6ĖmEl6uA]W6<=H"Hk!.I4yIܬEKLj6#k[F|r1 [C YI2ܟ!M]t-u:~lDAVtĥ3LMU߬JI瑒:vT -Q$EmHfDSɼ߽4Z&Q0"v%Ls|'瑒PJV4 h0yd…F e3LV[җZ.)Hm^sH=10 H^;ly,pg/B~\:Ôi| ٘F,& z BF -&H㑒#RʆBl, m+ -L`ڪlѠ6~TK''W"y0i%#gL襔AOI,g~et)AAII(kWu%2?X[E"\NHZ[Ѯ&QPs/Ƞ)_bɆ+Z%\yѿ^% cG_ I҆)щ7dDo·=D >V`%^n_&|l!#Of!!e -D'a?t1<|)zXZgz9x;$"%v)i)юec^$RӔ/7hJd]kUҳg}qOb&3IYJD~Fە30k1.zmE[_=.FZA<#VNɁ.g*|Rܨ=ާ&Ir}dEGwL~F4ƤD&sQ> &nl.]bj` Džؠvuf|c0~̈ Dh_ne6v/r+.& -BcO"N*HsVpIzQ.h% P@ Oq-ko&zcǛwy4;k5$wxPѰ@cl.7x[:qΙPJ0${p!YKVb1`= 5Z-0~),ȸgG)OEI)[K@#$|=+qPODo[kVf'g1sg-gf"p]N@w 3߈%-Y,p|Zyǂiy1ո*DIOu=tNZací( (8s '%$!@6/cAѲ*e}Y>Qc33gC)CB2de 2 ?eGneel LY(Q4:]rD *l= aϼ/_-t쯥,vAh,XM}ow#$D筫3y(% t0b3F+d/O8 &d3cAjBtl/hA;];ICJQJJ d©o-Tz*iʉXF:72'zڬvaf@eJ;}3R=L3/NFL>tZyZb*UVf/9!l{JL@). d]h -V$*#%RJsci$—0R.dL@&@@R+Q,8+δqh_=DXq(%8wr⧟L ڼj,zIQ6ǃj\kNA[fk$^rθ%rď?;s -2 h"V <44^WGúZU6v=JIF. -ẅ́c=M~_ghf]Sɷϩ`6SVOVd-6秋1}ᓈ/U# ??I}G> ;9G'#~,CڹI9=3 z+{ak?qz8gd%$}Ye喱CBN=sXlQOO"~ɫ#旗Y[>}OM ʫߌON 6L_=>~|'ޙOFLYO9ϙ`hg&W$m=4óI@A3+YLYyrAg;5MWځL"~6r+WԻEI0 KVs[ dE-irB ʿy?oGxzt/DhG╯O]Ͼ0&ѽsg#>|Ȩɿ?+V / cAeò1?7|M<\ݷ.I[GK3Sԭ7.J|7ux纇>?<{_}d)*n?&LC+YСLmp$x>}QҘe1LWe^9RgΈ7QdDGp#oU]t;w%ǂF 09IJO"~Jh(^_^Tfm34{ɞ~{xyiIR'k$4; $hY4J D|suIng=UW'#4&+qDO8YuH&UY.q|2ho,J,4҂ک]zTe{lڼOM5ZI'1G+a#5!aa@ʉ͔*ڢGhs'io K0Hr$>,ffQֲm[lE:>"KVF={jٕm (hql!!&$ -g8& Dỷ^/Z,iUJ|β-d@[I$ٿ̀Uո*bw'C7|B4<});B/R^`|2&V溳CI7Rr}Ew `]iiJ @_hF65'7leDőh|[)v9|a6'E̊X @hF I>l'(kYӝeO"=~͌h_VDK#-JZ $zf@lO&F[uΨ \|]T-V~ -$HOkidm'W'sY37%{HVЀT)R04+q`8AW'v_+owQ87+PUOKi 4k ybk:jO"1ƥh_FiEIwsgh@jh)+ T㪨UEKc rI`KЀTOkpʊSK-*|aJȜ7xLO}iz,iU.O|ƂmXmSuF>Er$SMQ\M&> -<}!jqj!!GuW'g!$”P'/\FϩeI+T=FZH3'H~6aF50t -J)H ®A]T*Cp&NlLnfUYT*V%6a50C00D?Փ+os9ؿAtjJ|LGq'l/-~zG7 0OhAW\m5-2V*Z<!Q=dF-V"`R!ZZͿ |;?E*80K2HGܲ,K̡x9Ulu,hOҰUEĵ.d쑭J (h5iI5˾:b-#0o#%E?+IFxl'Qw`4smH*3Ͽ9b#|9.Ī:Id .2}'lY; {oCEM+>#XJ=5k vi-:ӿl-ŗ^ao}^ty`$u-ž+fg+jZб>mHȢ+[wQ>j`"!jU.ZF'GeXM)p_0D[߉+vhh5ֳҵPZyhRUhs|_Wm(ًz%QOC)MMrЫcK3;>;|z2 vA' F;Ͽҳ*G@ΩV,|oakh9> nI4E##ɋD\[4s"%6 *Ap'6Mrg> ^L* uKg>9ڜOя>5,)^I^4sKj[EYӸJSՔ$2;A9+[:hZyt>#kIw.=5-3(zv||Wasrx8qYOIM$Uwъ -k)>%`tsg^. -{0y=k=+78\ɲE*'k_k>1+m;QOD= `7twKKj"T,)'t_S>)yvtp2 v*(lKj"Y+ldTmNwv*'q$me -znh)2ҵ8'VfE">|ڐI0&`@6,{IMd(X\_IO"`ƾa߉'D# `7) ^*R[US$qrأsm`?opW.I]D6rh*s'dJ\^LEgoĬQ솖bsB!΂& -=Sb#VS2H'?]/},6P. -w0iO6si"=[Դ-=ұ7'#_Gp[rHsē%^ lJR -$EFj-3>YM#D?Vx

`t ~9:X%I$i(%@A-#kY>?v|:H$'GG߉ eZ$@QӪ[_O٢+X(z uȅ333dC%H#{0C&iǽ& F,N>y=?})'~fso l?3tb]L$kI;:֙OfVTN|I"qn蓉D>g^mboz%HKnX9`yi[EY2WԔȨc~xLtrId?Βw;}lUנV3'kiJ4'g~/W.$.p}蓉DJ[A`Y/>Cz%QOP -C#-%4 l0VE>љxH$D#=>D += $AYH\4:襑SO|#܊⟞={G.\?}|ٿS+Kڸ'Kz%QOC)JJ6sµ,LT&)Ъ?n8dU%璤䓉D[EJb_yv.`ti- {7͂^z6eBC4G?㙙SHO>u|tr/}A`~ -s}tHOFBx7q!D5z[Z_ϊGG77?EI#^x:{i:<.! |2뭧 oc4Ό lf`̈'苪RJmݒ؀9Ćv~JժR/zҶ `!Y`se睱6Cד< 3 e+vPa>z^dPϦ5%JiB(.vnBn=4f]WyFd~Usd/0_OUOJu"aǰm$%[/MJ(1M*%H Z {X1Ԯ(e4}K1%.ghjR^6'Kj'!f+-ubY?GOb< -8TSsm֕$+F".P(. -Źڬ6:TQߵO"4TJ͕Wr'x(9$ IO= XN=? -+38B0 gS[=%;ˋ/qUb'D}$C*,=\C8/C1ԮԊ@;mx""5r tHoN ekk+k1r#@`>vt[#™ƨ'KfM d'S|%3YBWR/YCDk'(κh -@S8e1[ gY4UrgI(9+ʝ'%ItuKkK8ӤDtT0|vƐ8{bRI6eGX(Z9-A:E1/'|Ŵɲnw4e驒/tDyC=nzu ^<CO / QL#8K&<'A˰ßɋ$;)rOt:D姻e3}lߛDObh{@Rbxi"/žI)(*~]xAp=q S͸CfT>|{1~05$Ia6e?*/W5;glkJ,h,v(uP}J>0:x)[KUW:Rc}?)% - JZ$O|v؟ _ -P 3>o tC, U͂d7; V %gI${r5Tpi`ԓNߛDObZjwW,[\{S󥐏D|~H՗(/)K$ 0"'?nLv$Nv;U 5K$tpvx~Oe=)N#|=!%0#\ vF -sS0Hb<)V:o(Ic\&zb2|1$m$;āko`\}|0O%_߁RȧEr |'Puqn9dԜ;x@߇uZH?Jm K]T{I.;aCk(9 -ji4.;Nꌒi2:dm\xLd>v`n+̿>.ҟTQy$K!߼>^U+qGp)gQ9ݔw6' $惩ڝ ^f{w>Ki8` _~\j07Yf;0,u8l'u:kn 7)0k ;]cwՁEiUQS7b`ޒ*0{򹌽v.ԮOUK)6tۉ-XnF+6bTj&ٓC5S6{;/$_"U2lh/qsH}_  --vY`+Iѩ"[pi4agi.uR1Nɬ[x_zBRamOv KjbgHvPJQImHoT'iWB?ZF|2.u/S(rc*'}JJvfT"xL_?;Hɂ6eEk nU[_NdQaUJZkшvw1qR -5mYlQlDne6$@ڡO?wd[:(Ԛyo5bxpmZ >ū -VkkWX 2.$<y =VCyY_)*=)$OwJRozj?D?@h|8և77_!xK}rBv6!f'up-0mA J~̀|%G||RWqTmήtkC%n'OJɕXB"ÉMRd|Or i/@#5<֊@/wy |rayxU6E)|/Od^msN̸RvIٙ^pN}I-נ nvTSST>rOZq ,|2J}WB)mVJ`ٞ)ia K c=>r 6qvHBgzf;&_%\ai/^3# {a5U{-铚d; $څu&(ڻ(\'u'Q5ݪ7j}($TPR -)w.G#ʛT&){xf LZeG>ӁVͱBY*kR 3]+U73k[)g]'{0v,6~ ASyZɺAF̞{ cmcS°/f@g~R*ӖZ]I]|ׂO*q|rQd*I7ʞr3\mV""2)vY3Jqq Vs~k}b9EM -dKz9A|X|/㬶#/ÀR*b}LԦVӄd.-uךxge#V϶# &O -.KwfZiS痧2.&>)=bxIǫv|'QMMJ)vZRp_cVn-" aLJ pZe&9 -B/Gne^;͓SufuG%A}C<*xKVߜ('5froo? b,*^uZ vš\>k'_27ɼ<ņ$xt{]Y)V“>ʜ D 8Ҏ<'gy'G&zʃp}0c7ӳDo]BGr "$\x7533> -olMze[nw hyɞI>j[IJ)J"`>enX -EZU%RܨCRe]`&Q0,Oo2L~r ?L8vVS>'"+=r!cTVPv D)/n_) -YʙJ* Vلfتy&R'=KWnH'EUvHD7YIpB0/ZpmU-)BǙг[I_xQAvX Sٵ&($U%cں8$Ϲcشݾ`M% &Iv/ɦj*R2MUjkތ1yH3̐tUsJB˵WGWߗs~x < "<{`8LVѢ)QV)U<}BSTG{XØ~!7J~pOsW֗dy%#Qqdd=a딈('lHɟpL/8t y7>S{&JMa$ )38qf R$~,]r@#,O>LM%~[Mp ~a'N -,gGCO֗$Ħ223؍{UQ0!"z^"eT*'TмM9%Tkժ $e:;__r'8j)Iԫ.]k#8O -ϓpC`:Tjϓu4-ZCIOKÅV7~fyuJoyR]eJsDx2O-vGض^DDY? pUΞ*b6IYq oe Ӳ|x9 7}tp΅ƻDJҴ%-4BDe<7JZsOټጭҵ8q $DAiB<8DϜ9lJ.fO'AP `h);ă\A%vy^;ʀ'J RUa2yr ^njtH_@JÃ8؏'{$~rH\3NH?)4ij,2 Y7Os%Ӌ A1d]-k|p"hQ6ɣTaQYVeUjNo2&I <]HIx2dZ$"]E9H fN-OişbJ|NW~1ӷbS.J_rBP.Def>]0ICkަ1td'h4Xzl)<OR#dy xD}V^v[ZE?MP""arO:%[*/bIr΅~Js}},(:A1@7}| ؃3nhҩ"jeU -cA - 4"E(@cC㝣2H!:ovj'+j' *'nb`rZb$"24RrqpUwL%@`_FJYAZG̺>- Iy *Waq;zGh9 @^ ;[qPAC`5OjZU6EU3]i&IW -PJPpL>L:_HIWi͊ -5U -{2-nt IHR2{r,҉B܀1`u s% L^IJwM./?O¦x6yp8"SgQw%aTB6P!J.ԘsFbJ'\ :b҅E&VR]z94x I(3 <)1 )[*nE u$Eg`CTȠMz1+b;jAeF]/~\0B^j-ڃqK)Td9; KյIFCaH*J?!*@v<·=˄ǮjsFӍڬ Y8|vG=EO@b/FѵL~"a2?h.+ ؕt~ }A)4N=05@vI x2[[M<&^9ӳ^:$u두l$,) \ijāa&F=64Մ>?A_xc$L)vK2bs7u x́'SQ?qoLՅEqD;p -4OҋcHJ{2cr2l'5)Ry<8ϡ"dAqx-,On!LPP` ɦTO\RFr!~F 'cA￙c. ݆cʇ_{;:1kڸ9G(j2fV-9iL7j. ޓو[[E '-D ePv|&oo8>3}=y/<2!/#ړgme_ -./g MA~5xR/Ynne@=c$5!“?iHfUφ8Ucl3]\cZ$<%cat3jؙx2b^HHH"dPejHkX5!\;hGЅ(jO45qd=sQ"y$~zfp3kVyD7%s3/iȜLn -B~ri~?5c2 $HCDaAř$""WW$uwjfvvڭڣfv-P rprk췻!NBw߫OP <}- O[|'O#e#g`RJ13C_^SlFI?}I8nf`N$|@ e,ydMUn$9K1|N=@~{ 太7$ŻI_2FB"l3~n@♨z2|Rا:Dx|r])O03[<+$xa0.uN3zގZk`QS}m>@,5:,kQ0P~"@#!񰊿 ^)\3g%݋N0O|?Z}1I -DPÁ2$BGFťs>7gO` 伍r_`bc RJX䨙m^ CWۘZ=u[͂\ mRJݦֶ̗́{CG>0P KqQ>UD .ҐstӢSV6 &a!0ZtЄ~wQ>$bUI`5`SJ`qmN(`فX{VF)y}U|RWT"< b?<ɾRꑙ-O,_2"ƼZYk>sa8 o -r+9g[9mj6FO&@FZ{->9_b uR -'TYXSpmx5t1۪Od%N?`jb9nyƎDwOe$o>9lBރT1S G%įNL&6'$;ۘXMY L+`" |2;[2}r 3ye -/1 1JY+v"X꫟vC0d-1K$0(\.Uiiڗt ĒeBߎ(i&©Y) UjL6E+Ep%L \!@}co1q쳢VLѥϸy-e>La 9;\  :fYJYC=i[IqK=&\CkZn%a|Ju>~W-m(SJTӼ غdÄDl.탄<' ί".2rA Quj2&jBWb̌}2d.p! vGZb0#~6z`^[<3-;iP0Gne䝒_D*(ֻ)2Rh-܆Of ۳ådWዄ7<r7x9KrPTY'~a\ުPY\zllfLt'v"<:Rn|BrKbƊ%3˔[_Dr*#B}ĩR_!/ -]bfi"p~}SL<'(%Dp)"`G~) MĬ5lkz9o'hHpoW|>"WyxbjZꁣڍ&X?B{O¬V'u~` zb3Ta0AC.B@.9ȱjIdªH bAJ' -|;d!I쓻b0[K家т>Uʑjۀ͚Khw+VN-=ƨ_SgM 23Le0/!׽ѫx$#}k.b+9@BRJ.O-cv{e ߛI3㓐-—>UJ`J -Z\z服`/~κMTQ)=p HoJ b!Orw?tTŇC"b3L-E!r[FCWI_g4d`}]yyjIIddV&m?Ttwb`vTJ،96!=i,Ҟ;ƨqi T/8L\KJ`s:=ީ'z PG>9PF -tC9O皇WI=f~"Xu>:63;;n3>Њ<"*%,Z-.;гv`Vrʈ__:\?HO9S[sKf^p)UDxɡ -ꑙ&5Ԩ]U.D*K&NWl3|M7呜OTTO-Fx?֢hG bm f;M)a%5̮$y-5{.@&A)W8üܝj=P+?vu܉pHʷ)Sdœ t ԿE{RV \ܧw(xXEX5 ~OBHO鑚/دۧ;Й1sZiW*AY+,i)jʃ" -< ‹ng:w7}v:ӝo;l _');-T[L)AGuD5KO   wQ@L0Rj$vϜ$ 긣dV_𹒚- #l5VOJܗhr*-:*LW\VA_x#?(fouʊglkԖVRp0 [1Hv}#eQF5 òGRf!C1 HvY CMƜoG-4ƿR9үx'MwL?! -veGT -^txZ`vIr@ 1P^A]t3snZ9zO*O>q*eɍOB,0LfZmq'SVuǖ֡&Rj= =aٳәqG}'O>w`&mI6d`nāRid!`KaS^x{x/c瀵_]=ٲŨpqÙ_pN:XG`z·uIwEr?0OadmjV2D炝CnE:r\IMO"&udV01wJfrc"<Ò6ZDT ĔW1eqN8{մW~~'U'ږ-/xA*hxKrӓ6UGR;@!dFg0 CK-w@5%&ГlJբ>aՏD f7&'Hi NF-iWI77]>RYiyEEqYM -s_Iǘ'JO⾑[q#%O#V\/HRDa)dfhjoeN[CBy9zRM)`w>/ %I LwzÖ ⪟9p_x;ieeHuJni]tEJ Яxő&4'E {BvhZWyڌWM.ozil2rw#> ;YpQuϪ+>&ܿۗM[1gt,1湐Tjג"ela`F-xJI$-d2'1OuHd(0LNeEnrow"jS<$e:K ? (1hNpxI2i)̥]oU+Mdoa 񻸄16>)a?R%]E8)€TI=XVd) %EJVXp.idڌЛL&^ɇ9Fx 2)Fv_|d#΀xcrs̵sXd`6ҟ)fMÊWl!g۴{RۤQ (H=߶:(m/ 6)-DS9H`OJ SĤ -)AdH LXZwyøEKЛL'jjE/ &)6*sę|,$CJ`v1Rk݄'%$zRK='1L2)+wO"JSVs$'IO҆I65Gd 2cnx'udV/8<4_ &5RZCDOrJ8kcY)tFlEA hT9mr^9MO6MM{O -'?K6H2$li0gmN:Bk"%& -X8rKfãÒ2-wsh9Ȓ U6!KR<<^B>aBIk  >Ʀ%W*aKkջ)h7'k'G x>2Â{f*v@ReK쮨L@43Lzj Jyd/nj[C-=/$~$+1N>ۯ+u>?1Է: Z)g,'S(XJYO7!JI_s6R:L\,I9n?'CVOMN)iVK` d3ZA@)gY7QʷtۓIԢa仇>Pܟ&\f4+ѝtj>iyIJrcH>' vvƨb|#~z"!)7O(5SycPJjteO9C5n@)CɢH䓞j5-8 -oH\6_?৖ -AEdR r+TOnגRTY%rwͅJI_O,^cId(ʕɞNM[0r3 v;wŁJI⌎<*D --QO^g*J= \gyhTԕh$y(VC)C;V7wͅJIΰEg|䓥m$|2 eS$`LW)w~RC!c)eJO)[i_)I554<|2䧂!zIݭ3UY3`CR̒$igC(𔲙/oi}rkQ{~C'FOpj|OR4SsՆGk}ROSIVƝT?N+ʱ"Td/ojd畒<]}u(k _m@>YI@&CPnF:zL= nJ9 ؉~82Rc\ʏ5:fe _Nz߶bKK>ڐ}^ʻ=`LE) -ոͩ.;'sRrO^)s2t"CwUuŲ^cN譛g^p9H*XxhyILa55GO ڻZwE6УS(a Ԝ瓿jT 'Hrf= Pŕ&KwR1rعW)ê"ƽr%>'7h$4)*DjDEd@v,7L *%MLո} ,8'AT)ͅo|-fR)],E|2 u'|Hꁻd?F_P)Տ|lwɲw6UJ}Э&mK'i*>83.āe(0 ČWJFm3;ǝ#~}G\J)m-:Yt%8'O_ pLW1Aabx -%RqY(%m~ apink_)%II_)9N2?'Kl[* |2%i/ytTw)2R)eatV?ͻ$tPJ1֓Mm\Њt!܋f˚+ ^\өX2e -LyY&SJ27.H+*1; ܏7JIAѷ~3lGy'=O;kd $JĻ쓝 %f -K@) IcUR#O|\);i(d= pm\ ?5Sy[@_R铕:AbR -۩D֢vV)rmjhg%dKJ *ھ{ @3RTThyYi(/H wg}Ma餔9ZcF^槕R$]?~RM~d2}23RD{S+q.4, _k#e;l˚HX~?+'Tr}0}\`JIP%ftW3"$LD $jlN9~O"2{U7!TQ.AsS%ftŝ -% opV 􁧔RnǙ3T6(ja?!%QO\m&@*(m;Uaq(e |qC?VEuN?,Fc.>rs3LcaH*tԥa ^{2mέ L͕+/ɀՆLSftq_o'ϻ+J ekO>ד>^~oGy2wg |2H%dVy[kzhr)~_f;wՇ,:J -X\H)iN/wp'*>'0+O6d݂5sP"H$jIcR.fC3˱Lh`z֢݇TJV)AWed~/\qHRepU?}m F+`y C_ycc.#1r[2iN}5ՌeszِŃLTr8Fk?#d kn'@R[<04.o)|rS2FOvqs[[ .ᓣ器+5rRJ$ApRH(uВ_MYro_bK;z'G&:DGz`F)iNPm?!R\KJ0}8ߙQJ\d%sSҕ*I>92k`͔8p^|{\!y"?jR1JYgsy5TRS"繝zi<\H|rtBբw].;7E,'I-?^?d AYJ)3إ;Hm[O(#B)vn-(eHbi@t9o (e<^/ﰭ)xGE.߅]/Q U[>>֛ -9qD`dIyPJR%)?cY> ePЅh5n 2$򞬯*`j?(9_dXcyf=7jO@*d7HOv!kRd9n-ҷG^/Fxs:91@~6mwQ$03 =-)AJy%kY~ӺOŘ^6$* N lsUU8p /#_ 'Q~PJK'9v:>үuNj#<2rǷH#-Y~65 ϺF,!?<A$~R۹t#̆\.hǔOӌ -Z֛HRzbۙa[]{Tх$5myS:~ I)3 jRg<$'IK4@Cxg9մ9 |=Y9~?$[PbXh IuŨkZbJKj-5S$OyaadƦPT+j_ȏߋ^4w*q^<}yy]=ܾ/pDΈḙRRkA){)&3rϛN?O'$HFW#ZRfG?-XĒps(eƯ&[ZjBe{#~FF/aX?d;3J igQ~h$8ZR܆z Nn{RC3?>i'~A׆~-o Hy}ٜt6Ccwg=730nyOEd).{7?*:20>쟕'JFZ[SH3I+q~w؛{4r@"= zb-<r{ojڪ(,E)P nդřIJFJkH(-Yj!h"~0qDT|ӞkJ=U -lÆHFO=Ac7RNk%7F֕FWE%Ώy!EL)Cwa)K>˓&e= Q 2@(!$xPCgۏ)v؄4& ~!vپtR3XY0vэQ'gv4 Ő<%{kJ|H; Α{F03ΈԾEiM -hT`HN90/,9cka َqvЅEqH#uۀ,X;: R>R嬢p?|,c ד02󖢨T ?/: IF{rgkazX,E^-)Ja"ŜHF}):^W{)zZ\i|wmL -ifɍIp q!,iT*X6},I[G"c59G6@BOvV E#)qeȿb;$[/p'g"]8WvJ:HJ;5)z֧b3C"Ͽb[S -ӭ?2g+XiT\$$lR_0(LʠRu_Nt)Hȓw8Qw0uL ӾEu=x43ȋ8Dq #9䘱3˵_ʷl ,qe}>l)+K  -JEFYxGðZڼp6//g}C՘(Y0vf8'ILp<B*ZQYK×{A "HILeOw8?^:'Ӿ|EuPG;'Iq;6'QdvTɝ9~dmֿ,,&.I#:YvRRiJQ@1)1ɹYIJ GQ}SC6`ȋ8//ۚ2Gɍ # l0BUq2,qܐ?u&<N";^RHyDR&MTs"巆aΚ}({u12< ߟh1Oz98L - ՘ X>egQQLeNVH Po$dzHa#f#YdEB- ߛCC*ƈi/ ,S;3KuiQFqo(u/ '%h԰՗ۛ()F+eȿb;3 9tl`ܚo*KERXv~KRB^((Cօ]ey R1^lfj_RШ 0/,9ckȵ^|~@,S,DXF@A ʒ7,MH2©VL59^{-iiǮbEB%۞fhHb{r؞DVx,1e79L,]g%_' bO2O"<,}u}<8_"ߗiE޿VO:ug몴S2ރ(+/+=m(x&P=K쳔| TqtDܹZ)Uw3ĤLIkAQbd-!*a^\0ٯ64Πg眝G[1vplK*ef]#!گ -F|Oo6riwȐhsv{ɓN-߳e9,1 Kp5$Lh@֤l ?ehZ)_$񗃺Z'ъ3T`0醴*,޿6툢,1rj]dQnpW?R g=r`0E$+HĞ0og NߵACFؙc}4sȏ;{l[D] -7DH;~аLf -Sf 6D~^#eAqnuMx!ruA<(`W8[eWha dڂƤ=uN2,.ۙ@JH3s@_n#HŐ8*kZd:B0("(.3Fp@*))߃eނe5I K0A)^)fgk|1LC0R3Kl[A9,1^_e4YZi^I7QL)'e+c4"Lwƥe*YT$(77׏ "%z{ 끚KPAI%!&H9j R*wzR*E};S΢Hy97D/oN8Xx˒MOlkx76)O ָ/wҦr.8=)ʅ))=_ j(DD-I N+5qT{{xRֈ@M~e/҃*)OJX_raJ,ϼA>0e@9|ϖ%# `paXa6`N=x?3z6|IHiH -}!ORԤ{6XrK H~P.A^ -㨨%Dx`U@4nrEʙrh߳஻ Re0; F -sr KU+m)7{biƬw"X,wrI 3 ak')jB= D;`)T (?et@T +Hhe/ 斗5~,(YΡoOrkW8:<}g7GQ_ކuC4,AtI0RH)úlgŅ\DWXAIIQL%A8>2IXbe)^0"3iC4f -<&)j#H9`za>(jrٰ,*QjL6t4.~XDʷYѓf(*RJ6N(Er>pCC_k٪AX⼙l0lzUh"0}\@vZhz@|?M&oyH9`V_?2WȆig HiQt ]5^#Ð$=ا 3~ͧ)RCx;< >rw ^K}~F;S-ϰĆFљ`oLJ²)j){^(̐ RRԤ{̴ `>aVWUكqT,[Uo 8/(9)ZykUjzOI֢sJFQn "ay#_? "t94_s]0R4R"BnqD%H=NJ%;dʩ|@yUМGr~#R23D_G\*ᠨD9"j 3i>ib8 qRͲ&kaҡ8m3ug=r`vu&r_}X<o8 pΠHY [-SE][5 -Rv! 6&uW,3t9Fw*ʃ{ٰuK8C0p%<'[i>vw& -s.}93e(;=aÇ.4s@_5 ``V -Y\e0I:T'%ybH͌HٽTۄ<BHD{(jJTPR2ϓ†eF7TKp8I9?ɌO3LL9Г9zi#8οvwIxΜːV6j+5UWizjWEj6UwەY !!`\CUO"c}Z. !m`n1$ߙ, ig`~W3g,j.,Xh#&HE׽^,eD8٤>fugy5sR宺_y(iMF2lnL^UKa+;*[.2cڙ%j>TyYI SKcJg)exJ_7HJ +sZG~58cL2a~ɁeRZXa PҖՄ _"!1aRDgT.c 5!`f#Mt'$>0r`9-E* 9 M;6НH')ZL>oVs* -MȺ6v1zDR>Փ.1|(aKvX.(Xfj"C>1L@d"'Fփ(W+ -J|=jR ? ~cU>H[09Dڄ°fX·82ӫ蒋1vJw[I{-NKnp6D`6KNsKv9g{,Ťu -N8W5@/32WP-;E/jRF- ,RFŃ/Zmҙ _lox `cGvȹ!#M4.cg)p31R'c&SA_ Z>&)Oü<<^HJǓ/3+a^<@߲Q |MwuR߹@7`X˅n(i0")K=S'Zs떚po!)1LWtxC 0V-T-vseGPq)]Z@z&3_x3Rj3yYO‰߽H7`+Ii -Mn-MHx .b[k`&]Oz5^B뿓1̳Tu1̻Ik*i!%)/a3Sw@w!f6rݗy.(JlI-e1$O7LpEsߣ?8I^3 g`A)ڭ•T#ud3"0LKWvӓcL50m fHwq`H)[?f l&}x?gr97 Ai3giۏSwOAWUFu2dmb$xj55LS 3R XoT .1v9&H#l1 {*mD:Wn[3Pk.#eI^{?/w%kI[[f(@r8\ td`%}sO:*+++ŹI~ڞJ <<\pZas``]j/OBlQ+jgc~mc?Go;cֿI5F2Ɨ+VRDm=7M -^jRV͉ao6pj#MNb(ޟ ,sT;T\K=('HJ!!8JngDoi rǘ_u* > r;U:;Pg^\Kô'V>ܨ~{{-Lu0lm JDr!pOw -{DJУj1 o - 娟C_+gO'Z%;0$l$SㄘQJ6)N5@A˹ߐwi^`r9._d 24-g"/=pn6 c:) {7lC+?WYz7@W*CUFUu <@#K@>#sDYR}t*xB;0fzH9@[o'#FM|*7ѩ z.njWMJX:|cKjg̓DJ8;|h9JoSBb)X^K=0j{6p;{mo%Ga kki!5q! ;>KI)s$#iM^# ?Z1̳ſt_X,W"b&)T=ej뱺0j ̀Nj'%R[ŏKJ}aVHZ㗙vJ;=aVe)2}.Ul -΅s#%a-ƲJdeJY>UJYIɁIiVaӽK 0;oU0m)Uc6VcΟ8W4վZ`(LiKmɴIai0iPsؒm|M(%@6`1i2+r8@6͸ϻ+ɒ6vw֫]iw0aDJ*W3e09StM+}E[%djj+bilLY=<&>!Ϭ*cF%>X7ɃaXrj]YORu6fat -`鰼!o@U-@ʁLFV{[De%:BgO<><9P>=aҦ)"0lZw6{`U -+ԗ֒oxػ*b R*ŰJId>a0 Q00`RYç=0l'jY"o߃*j2Mhm`)Z !)؍d~q3$ɟ#ؘqtO6  þ3$Aw]`q"6AQ7Soq%jo~<˚e%ɇHiTr|n=@K(aX4ݻ6iI Y'vaؔ*Fp.c+mnG֊j%˂zWߛi"o338n='/;@JI̜0R e-?iI#0,M 4GheEfh$9Z|%|Z#zihǛ= z/ptDLq9iY!Z9s-誴ZZ!/ů Ib(\fOq=m~0 ıJB7E3YW4-V޺,ݿ z?pd=jo^O&wݭjQ]ptysYГ< ΢EٓqR{d OQ zY2 e>8wI2m$G#i lZ| -bFoUpvhRJ_ß<ǕmNyjȸ+M5*+rgC<=bb?&&ޔ,/ iķQ]3d̓kxa0J1+Ŋ:A]՟&jK@]+ |mm>9s3^,_(yYHĨt5}ؐD -+e]iEOyXfA0Ko0,}jݳnҐ2 9ُ ace(Սbt#h)EZ|tQ-_zh|w۰zsrIjZ|]GcW(;Gq(>f13ƄdΓpa5IJ$*/ &ia'mI aXGK1h^b,]~ּYx8(6T.f\ m:X-=FPbZ4>ѓI({ndaبIb0$- Ű za 4ԍNc̈ۦS˗Kõ? }ЮsQ14z4]rO%pZ ĸlPbИ)7'$Vkf=94tb=x-/#n ȟ#YaNMJ4n#݊q\Kj 7C{9'n6\:Kwe'cυu1&)#z=i/ZN)t?|0 Linw )Z^u 8`ؔ*FG8 ( ]ֈqu%ڌgi Zx1\jxC{|7IDb^ϹC#JI J|蘄FA\/%L`cRIEZ8P>;Ma.;nI g"&1,mb:4:.11hLF(9juW. Zط*pd};]9_t|\<\:`9]&a46q=ԞV{ɡ":HJ Ib0\&p.e} ıs H4R74mk_׹|_|9w~\Y.Уբlp$iFaS\%2mg;wתk8wkQaDQgT ->BxFgydI=i{?~>IEؠud~iZ#L˄>IR@y-= rVrzl/fa1+xr[/|/PHݗEo8x45OD??чD(<<|4&cEGNk(j|٦8)$(ta}i\TŨQb"auTIFA) x;T xl_q_[yN꾄bo[N(`^a!q8ч-nwkQ?ߑFF򤮘LKI<$,/rV|Ơ(j86gA'!ٮ0W% ܞhKŕxr|f\AO*s]oDZrPVԸ莡KɍyraO2L wkI%~bUYg͟c~?}5$ ?+e?@%*Eaq41sCC*-\V|kW? fwJ|mFZv(`(Gt#p("1&0 `R=cSb{¡(jxȴ|,F[֯uUٴ] p:?en\ʄ!W _soR&uWb-qY2"Flo.;XPBԋG &W:GE'%pU8_euјF8,c$oô݀hgbqrts[/)p9;Ǥ*V+ -#*T4{U1^\F@Ejݨ?xU<6|Ѩ'gЏ8+=JulY+IP]* Y4Ba]l FbُC U/}G9RK}Yې1btrЈ9]MPDT1Ӫ>rziE-@Rt:%ȐS}l2GňhdЍU=ǔe[B,t5{uo}w.J=WŁ&a DbD+@c܍oIb'YI -Orx_GȓR, %.4>"Jc,mZ -Ew~=|ts||f|~7>򵞖ګˢ[lFF0Уq0w87OjEL <^*)a2FuⰊ_ua(Ƹ3-* r1?%b7Lb2&;ʑ \Prտ~Xk)/z0$ Fc!DD^6w9t$(RyRK`I6Or}"OR+T`E`zЪ@X\7XG##uEqf(%o O$|Q1^KsFi[끐Wցح~xA4)"O03Ƀ<X<^8,>bU=R86gy>Lj 暧_؅dRܘnۀhteOn}sOY[_{uI)?SWsfHa̭rѭޣ4%&H%gIH`I*ODIqZIS.EQΟc>&ŃfЪ,Ŕs)gShZcV[0ä.!W -^iFrLj.ub0 -2FC1=tGHȓ'SSg 33GL0>v9RZ)9H̳̚zhPhԔXS[`/gSk4N:S1T,uTKݗEOD̍{~0!vƚgE'!3 3|3 }R|?]S…f@>)2HBn.Y%"V"yЍX.}3\%\5T#x+{ B:p0%n8=XO@ąHh^ȓXbR}qxVaS-9VDZgä:.U tAt1b7#aܤݬ+tP{r.5{u -eRG'ychcg\R8E%C'- +&Lr{1Sb$E8oS\>N)BBU~PJ4h[WuhҤp~iRPB;qqbqǹlv9>vHy$&  y?z~>me)+qe~j$RZFJѤSkL=x@)v*bhbbLCI@W+6"~رN6;\ -\8ꁋ3!OSk?'険QI}VBa&5{R<)mM{F-E)[JO - D!H _E*߮h$l4.4/Y*ɥpEKXQX%QLȎ'/~7"m(. -V4 -^~q|zh=ԊtT|bzR'7RJk}>z&[`߾]ѽZ9aExS*)&|/?SQi]=µI45 O$E@cfg`{G=7fJf.VFai%@B͔O(1f]7NŮňnİ5=0)}OJl3 MIaf+1){rf{y nV B1/ױ㿛=ٿP1X"G#]B} 1<VLdP\YM VM̓k)hߪ6ʓa#pn2L -oiГL&chbP\bf»ӈ?b|eoϴTDZFaP0h'ꑹ$Iؓ)bİM_s۶mRxR"0 Up0PYՉ&C© R~wh\JJfa&YV,ͣ14%'ErX9qx[英 {glHu:{64|mN>*F>hhYa*JIكIvGRiaC(7oieR=~̭F32icInTIFYӉڭTGvĎvkцweÑΏC&aN;0(n>ab~ wht24.w#T -=j&Zy 7*ѓ.Vޣf3.W$dɏR~,׈"*&=?BD?o gދtԅ]gB?σ YE[ҟdJK hD7bJjF2L.)~bL6e=pl Ё'C{򓥚T&Yn(Z65?Mz.~wΞ=1:=BsK Ђ!(.\Tbf(F ,zR$ I)ѓh'1\$yzTݎ'j_"j"pt! ~;U(qb2@/Qh%Q=TIYI=3F"a>`$Rf*ɛU[F;sf]z봽n*x[c=d8}؀0'=]Qa:S' -!%Ub#$FOI P0E)yٚ0O -wՀǕ/}e_t8C7#F Vj휜@O$`$ݪ6ГEC^ݩ5-SrcRZ7dSI4`x]}Hi#oV)#1II1^mXX[vRF>C8vXp3AmeO;j vx%0l}Oj -uI >hߪ6'1%rbQuwkV("͑Lpɦ 09nt:),G:ݶqɾ+^;RCðuٺQ~~IZ|_ 1j[H!F^P6.u֬lRKֵ>12m{Ntuϱ%qr#ݝ=T'%X-Ap|{>"nyIꪢP(IQ&u:K`5ׄy)goX#!, n4O3FJaF؀1*|L`lq7v !GolN/[Xƈ[v#c}-) -eYJ'P2)v~MMsc5 sI,b>De?(lH\v8oq42֡.pW$VJޚ˼[K].pˉN.-Jp:.}Q(T}ZT -%쓂Ї5c*%R FF3I"LjC悈@2%%zZS-Y$+㕜az[Eǔ6v'"ީ-CBY{J'5*P2IYϚ[jRw7.]$g+#Y?1 LҀۦ_ݹd+F.;EVI&fE.c}|*`JR$࿌ty&"ɘ:)GPֆRI_a}RC{A\#PhRۜBg -_33̭f"&l_H&MwBhJo{^+HOPؕ>N\Q薶cu}? PP6'Eut Q*UBYP("lă|R1n41')wobC*UmȎ-CrS -)8[\ب+&|l  M!hO'qK]jH*P(I:g:FFTj~mpHR%z掯}h/̚쓒;du|R;kgz+eikwL,p/s d2pcn6klx=J-Lcc3MP@t2$yR({d=!5*B^UUQYRR1mٽ~G^<{̟|mgY,^AffQ5*yS(_FEtn%(&Lv!l2EgUq`j(\Iۢkbٞ.d- l:hr'"ީe%A'Eu $qHryEeٖe堔ۿ\g=z虧BˉglhKӖco*` /F=nW]9szv) *z 7A#L2۠Q_sL&+Kqo[_ّўNH:R/zt>vt%B5Bi,|PGRV(UUe[*|`cowO - r ADPoK8 I(φRedЭ̤yR rŗ^.F&k6|nq#R&ȨF~Q*|LA XX8o]Uet-S~|pkC2lsMǥ/P -(:F4BU] ƀF* ޯ?xgק;p} -8ǃ@B=d9a®36&w֬>Iٸd5Ks̭fuMm!X4>!pprl"&C6l|!:>?[tKNuOT6:랈xh$&b7)BG٣F#Q]UrR Vco{/'^=fwIm( b!S[ωWsn]*JhOf*Rr7oi/p3!!܀B -$$.VH+jsݴ$!^ ,4cO$>Ӻ)Hۍ$mzb+ϙuv$c;Gq{k_w9{*ֹsm6.e۲F6{1N&CxLjƬ1R+Dp GD _fS0%A,O ᓐɎ';N8ucǎ=zȑc>j~ ׿~zgu/5HHbih{hRY#caÝJ@&=頺(;\e(eil3DLUC#7|YgS>Ҋҵ>m![5UUu 녈 -,d"0 0&y9`-Ǽ!ˆ&Rp{GYNԯ3|䓟z)|{?zo]u:Oކ.!샓q1-Fuo|:q>ol3t 7\Ds$%+]UX%*̾zs'8NFWR=np\ZJ -PsmA?!3Ҙ~3C!۵$$ŸG'vr{_k (%& {v9 H0r?aL1ƬQ"65OmE8!En?0H&wK赑Fw?{FP?G"'3thq4RH@%< %mZp<aUPQVַ}1'fsLq\Qq0sܻ 44R\ر4Yu8d쳩Р\aFwSl*]#$ggq4< ?7 -uwcݽ\wқxZ|J^:/A0I8+0IfPC H#}1̵k\,#}9F*&TI[iwQ^?6@W[ɤ"U8_jhCoE$.ؓ=ǾʇAͦBŨWBaRPՉzxp>Z<Ơ"ki2YKQ"|Jٶ|9\2|Uτ›Wᒱ<2i߹`oh>FR=Qīy9 YW -pp*`z<&9Lj-}dr0\C d&)d"lc݄k^^sJ.YX#J l%DXיcu}mf9$~VTU52i\+FP;eF^9raKXe[hbO7.KG1ʂfzzDp4cA3 -M Zx2%d?3<]vK r>ak3Xrf%êG?$%r!!/J VEN\6c+PQHչ?H^@-yL%`2/i "ZDWJfS\r 96ܑ3Í`!0(qت2nRCRc%?ﻥnʹfضT^g3i~#l"RJnS'Tpa$ ,cۈb.WuxRi5!~5/M- -(,⠨8 dk{Wp^Uk}eiH6H,G:g"Ckg[9n9<\Ӆ^B,M$$Y}Э&l'DmP-XgR6ǰǪ0IC#_k?rvw؋;ZGmFRs1USG]XΦP1.gu%JCsh!nIr1vI biẆ=1z%ç{;WMdwE{O&!ﺤn & Q{ ᐤT 'E[o؍aV+UZR"" i6 NHJJp`sN>b'Ƙd}0vubh;{#UbgVV$ò?닳[9gK2ެRIҰG}d%^awXgڌT.yNG!b',*!㉓AR ,eY_dl5:*Yq&:c4/][J5[[H6Bp%^2c,I]uIM_B q񋠎6R~p' 8M/:xEnߝ<H2\RW2aFanfj lC|bR^4]^-hi^} ^W0$ |M#ߘ -s' w a/f8 -?|"tABeQF#m4^D8oMӋ 8A #1 ab/3.,rj}>Q6Y6qx fgan`˽2%w`лc}q<#I[[HښTqhN%y-'(v s0)EXVP<o9H"3`.'yoS)-lkT5+QL;(޸OڲFX6j&Xk,k`K IX3Q'd,_H M:ܽ=y|M÷'˪hH -"Щ$KyǒP=ԖeSլ{j{R2L|}qRO~V -XF6UMNJ$Iӭ-‹Yθo=^?=$^z›OnHAO',yr{Ԑw#2lE3R2dw'YC6JcL VNLD[ޠbQI.kL c$IqSn0B_ѓKK(Ey@vsLxf$uH/k)zw|f6sZ𲚎7)ɳg}u>QL#tu^F% wV[y;턩!<dsW !#1kA V( pT2Źp'] F͙., jMjng;- -/`n3vYZTu+jgwya6WSbF fHJ Z|_2 FE8`nT-e1> -S%̻Յd͊3*eϖeSclj'-U]VcEscqpZx<<%]9y1CM'$ez0OOmmà?L#cwπ*!Z2.*aUұFVf,y W\nVbcin{Eظ+[PW;%{ʥ*2tM491bB$v Ꭾ+5d!"0EXh(kfCrZD8#K`F&t%6E}6d:R2;UƴR}[U닳em'/{)+SI6aWgIhR fqy7'-]_$;aݴ ;2C<-rA ,RѐlDZOM# G3[fk lj{E- '<0 <7] ,?fziWSbCP_H$ rJC^^{9uw)IR8NF8֬DJT3ļZ(jT;5ڲWXVIv  c.IZ- -H1WI+=7 +"dGoO8($y4`0c?Y^&jdђFgeT6jV܅e2l Nfbk ɭ`P%kg]>/qWIضh6) t MRR^'#$u``>lGpf1)h2 |r3*2#C([N^-`˽-p@9b$L)KZ^BsWz着ՔؕS EXM <{𭳷xc0 +0"ꌭSIںsgtЊ Ll,5liA$oZ&<ZIYxIV@tI=A||"8򝺥T !#1DgU*` ېZLF*pOK J,|ǚ_[HnfS;ypxWSkV%qVGs%ɾV/Hy[Dl'AO;i=:ACb0al#KUrkmasCjϖ8/r%78_U7ڶ3hՋJ[cje怱f7؎RloY{osƬc_(@8o}m \;3 0h]34_ +뻉9'+||Ig5ȓr6I`Ħd* ճ뽳_wO^w@`0 w?[H$H#`J&R2%xYVB^X%,z -&\eb׀&`ͿX_Ƙo#b\CގJN'T 8Rҹjq0RnE\.$۳ZV.x%גߠ+ڶ젫To.[~Tg I+|kKpBL^(wѮ:A%TϏ{zM.L´Ur4d f`B)1pH 0e [0͵B*^DWfkIzt&]od^ʬQoL0YA h -X%_z7HJC }H{_|raf8! f0#~-5ogj˰2X9˹R6*%%d+z>ԼTtR Q53 9ҜAL[J_wݧr UvUUv鯺g$O;`0gv.,=Z Zv`Y2t$"–p&[DWf ϟR -.-mSc9sNRD nx[<@Fx^@=֒.k0RN'7?%nҋhzG0 fC)mpH6euA'-LZ>R0[H9ğ-$7z*vlsW1[(Z wZPa/y@OGoH8#OʷF#dCb˧.`0 YӼZ*wgCHm.߲e%lObz}ް^HçL|yMJMR*y3I0Rs۔#~b~!S+]Wj"#1-Hk A j6VIGZjQe!ؒlf7uaK`Y/$KJ&|K.%#X_`2d}0W}i}`&]W8rard?{~} `0:ҬXNGVf@S!!m[CJ& -n \r^¾SxdVZh^ _}H<)*$89C?## 'gwϛx.d$\/⇔۲k$$2{}9@˹R6M%SKaK fd5ՐUԥՈp5_N -#mq<  p`-$[7vL0`07XgAga U'HQA*JGG$RǼo@tǁ>y%F?$i;/Q!}B}fH [(MR=jAy}u.)T,,i-Us9_uፐێQ~FUݖs:@mudW#F^Ģ\ l+W`Qg I y (!ܔeZ 2 _(OA}G~mqܧC?z8#sdϫ|Ey~2! 3@R_,#0XWfr:Pm}[⸞O"3bCHKnƵ鼃-< <[tx~O!ECb0P4Eq:\FLAvu. Bey>kx={ɧ^ lc}yGMz\H PJ,=RƸtfPnidb0Tm쀴6({P.:HqNzl8lyh8@l۠#t§ |u!^1B6,7 6*t`%TUsqqsZ#;*9P$IzNp㡏N(L(ޡ gGoEFb0̇3PYf>[e"fQp}d"t`>q=#?Fnp㡟'py`]vOX koVqI( &&u ^K0@l b^۹uhc;׶4J+lC$Mz5Z'- Ǒ EQW56T@R]v*9lDʏd~VpK%U|nRlrg^2uL#)'(BNrv04gƱl %JL[[,)+n,E;B(/pc긿cl}mlss,1j|Iu)^75iN2$EQu~qΣTr۳`rtKg)1~ab%*.j`>Rkթ0Xr lWb=9Bt}l㞎 ȐBE8FÕD&&V.׵S;#>4_ EQԅ9|$= @聭 -"rͼs1dE3bD v*n5!̳?:5 ݻ$v{i,=F2’+d6N >nzw+v=WqhJo꣎i[H(Ҏ7H wI ASj.S1N7`M7TsbJ_ƕ6# -!Ü.Vimt«~P"Sg]C(]*yg?'Ckr晷,eB%e_ԩii@UʴY &X.n,/l0PP0Ca+.;zgu-,:b߄({gE؞((Tv&tMߑ̓~d;8A#D:GRUbpn5ەv긜RapI0&|hs}G-*"zƝ= JqnL^An5ԏ((TҷgW.&\[,/߄btb1>y@g-65ki8<DT =%; b h\1떹R+bkYJO)=*:REQEQ,Ym=z ms&< /.\2”۱>mJʱ:򆣇;:\ns3>4nһVL%cS3 -EQEQuFLXBʊ0&;|UnL2|TOH*1RbiQEQEQ0GOvcu)((9C7y(JREQEQE` endstream endobj 11 0 obj [/ICCBased 19 0 R] endobj 18 0 obj <>/Filter/FlateDecode/Height 947/Intent/RelativeColorimetric/Length 20505/Name/X/Subtype/Image/Type/XObject/Width 880>>stream -H pT/!&_aBA+ʄbJZDhjhb"SlSF[qvBP -P)-7Bd I&;=&_{{|!;Lsw9W=!ግPڇ#=^v^fV$Ax 6G<=\ k˰n3,- gGX+)G`{[-6t 8W{17# 8nz GJyH&AwB tTV \D06ns'Ab G.⬊FI8 dw AZ-hV7%\( "~,z2YlKA bYqM V&u8minO1"`y;A^[7`:58vԆw^Z"VPQ $A.c'nnJm4RD,8>F75RI8; pћ"AрMV7Q#(Ag^PD ,=A$ۂXtSADXtSZM#%A} '&3>A$ u]NgO Z,;ȘuS+\$Aǥ|Z⚠nƩnc; 6,n>DACƔaw 0CKDSDr * FVFw w? GM!Unٕ#A݊a2, CŮ~ :vW74g58hr׹Z܉&j,v&pu$A\ t_7%܆4R8g ?#n#N{1L9# KM?)Txԯm4Q eS/1uC&n>v3>'$x i} RBsNOuSnk GH1>pRfDݦJuS­LOEne9< -]J{X/-0GOKK;ֺW̷Baפ =R2XG8ciLaԾG\},Rԗ)2NZʄs$\Ed\1f9t]RE?%H7%$\QO>KUsj Á]Sb}X & q kR"D-JS2sO}Uj9(%INtSsR"=Hla|9.X@S0 ~}}?X;8bcG[ʇ +|f|k|njMV Ϙ0"# F‘~$K֦F`__C KsM]f[:pfMeVtmdkP!(2K g\^t4饝T7HAھHL%}eu]EȨ;\߸`@7s%\X:]{x~'a,Z=lcx⼑\†n Nd{pnqY8ҕ2rr(Km{i]ذ xx;?GuGƼ#Mo' _qScm5k?bަTpt57mbtSLg ("dw2{J0ۅ}.6DŽ'<20äFFD@pa=x]aO) 've_ pPY+EЦI7H8cdo x".@״lᛇUrB7mDy=D yrEcU'tf<'_21uMq!~pz7Gqc:{loP%ZH:kծJp_ky6\ME}ߍrAp3j Nk˃iqV(8dݔp$\Bk9~{6& %dngZ8A(S7WSdZg AZ=$7`&&X护M5*~Z${S:ƎVBLJ`Y+TH$\gR|%1닶43hXz4J w @(_R `_ g,]ƜrlY.8.lg;jȾX8w޸`oO:ݔpZDE-#*5D^A;LI56(VX 2"sAVãTl1qVx`L2% 4MTદpuwVUn2"`Cpٯ+ sAGETdTQXRP#m5)hՖg}Ԉ $!)f,Q*>ZEB ַG0ujEq@{usIg0ufs_7ZY_z_W9.Or?KSy/?X?'[Ư -q'+l魌~_$ۘ\zFܠ=F_.LkQG+Y I1ӠMå;o]syk.Z5׽FĖ .?Ǐ}Q$^k0p2E~L4Z ^b7k'7-Xx]#]Z\)߽+k"IJj~ނU7:>"p3{+P֬&Y}4cI,ҝ~eݣV_y{&!DqK;]F>pqO׳UQ+fRn r[mEeZv7a񺷛p^k͖wpR{YP^:y-[W!=AvZk^w7nquEٍ+y*+E/lͼn=XN|qڏۏ}kO*lXZu^{Nj7hk k!9n>JTO2A: 8A ߆xm8 Wgqys޾;[Y5kmx] ~G7K~ t"\ZjU\^y[sΜ ~lm}r~̜~=oT*ZKc2 J -에T{aku)\`f+Qg>q,^yײjv}5}_CC9?׍5py¸<{{n9ZFp699%D@7˚qDuX7MA -0PGDrz-oD]][,Sghۃ,o)ZOXSK[j_Md,o) =doٽ]w:I߭Vym: NQ)#5Pya:1Mda -0D3ܩIF) rv7u2Vysr5NZ_kWG,7.W1n*I'cT/#^ "DA7Yy#H^GN(JPysRn@ͽZWm$Bd8bnPy!X,o*%`b ͵vaDm9*k`U74\U䷌*G`R 7{~oỲIFAJYAj'c*ay#Pvs$^e#'$;#%#7Btu /]PY(n6 }j2%5JoN3o*(SQZ@{`%(n~̼*8_$75vQ89m}3e>F7 ȼ2v3NzeVoNƔ7?NFbW6]l' ׍H)Jt!N7i_o}2M\FtIXV:}w7Q{ER*2(D8d,#YV0 'n u vqﰼ8&2M7ew"ER"IuSQ{9'Jt շ틉@;7•#%,o,Hm7as^N~FNzp┷H~2(k'ÔH5 N;Ɉ&ˊD,o,0[$'o$-#4i'c`ck9-#4O N27BVֹ0qs!NokHv2Q{p.$Zb*7'?`y#`y -Nyʼ0 Nz$ 7k'{7uv!D P´vYsv.Jy3wh9#5P%(-+y#d^oɡ7B"Cd\¼2k'qIYiT!J}q#d!7'a/:$nVަ4A7'FٯX2>c:$Ok -a "TZKX3@-DL~sCBؚJRa#jdSaSB=Pj~>?7}uݼH:ӕ8Ps%!kQ}լ'0@5s7y1zޚ: ym184hR3 ̷݃]0@5O u |d TsZB tS%dN=ycՌyh :z˨oy:I9jJym TFM~V /ɡ@N#bnױMJo St>Ղ9G|hFqK>IEou_yvX' ]0@7[Trߦc44 Is(C= T79-fo1rq},*Z Я ଔIۤ t|b- A|i{o畤79ϑ(rq@3;z(e7M>)Y'QynAy: kN`rb$z - 6 ؿɨEqWMz -AYtQNÎz:wEny gI/#Mɯ7Hq|qAJn$ʅ "o47e !en&3_$Cr[Zd3 RQVtJş y8no ~IئɋݧA"<UnƘ8MsљGm gSD;[߳8s GUd'.z$H>#+?G&/j|_KbyBg*y3]p8r T6mܭϝ8st9 skw?"cb4G1k[[[i-rgClPN^P}ŝ A-)Kt[yS2Dl;/#7\QW{!cA2fSߠ*'<A)QB^AOʓj{_l*9u3Z0694zKɈ=kԦkgX5œC/Duj+du#j{\mT W5,\BBfTuնr5Uˮ&,ZcjnEqD5򑩕\F>>qPS,*@n[1⠆+Uyk8*;傥's$d#RS.[T 9Oᅡ 56܈iOEqin5n:9 :b .lq6rX*! 75F0h3X*!/eu89~2*#6}ن~agoqԼ 8LޛUF;2ʼX>Y0R\£*C# ~}1@)kȄARl)U8t8ڏ(oEi`9sH ay|AS#3'yuwѼRR8^5xo.}?Y9GH }C/>?f69^ȣ5HWPNZ,2χP7 Uk]R'*_U|>{r Ns7RUVT J1Em(-'P6Κ#7%6FO%ɏrsλre֐e}^ݿŵQ,rm[%Hogqn 9U$Mw X~LI&M{.@;*BR"J _-e Ax!mX렯{.z?Xo˛t>g\q WIge9frBnI  Drpx+p77A,w41ތ Kc-6{3 dr+nY1@bUJ?ފYf9M2Vj_}*oR[vz[ `9N}n2}ox,7Ie;K{nR=θJBd^`rΫ"8<v{u.0me`BkX!8}x H3Cn  wzop e#8K!7*K%\X~@[AH}xe 8scs nyv6Pu fJ}WZ;I R@?G?| WJPѡ?z`A,oEoEps8zWhPyEFTj#wŕ!|o){n9@?O%ߙiͷr -pNUo1cpMCp?!;24 J9o[o 8)ur> 8ЩʏG7$8\)An^F=3u|lRbz=,p%s[ p}h{3t~7P$翛h{Ki1rM,oMbqEkoM/gi\ko e+1@[fha7Dݑ&5IPE8sְ x91JhiXGQ;AM.Wo ˹x7[J2byCs%Tz?X^]_417CcN9>$%C3W)B9fbo xD՛+N`R9`LυRd7P4w: J AIz&8CN7PJzMo =@+٥xz3t&PuuB0@-'-gi$.1@-'F˥X\JS7)JU54xoTWʢ-11@/ mn*G2 r斆L^pk鳄%s-2Ny(%-PR;: 7tHr]Ku'羷=Io*Z[L.;0@19Ul[B\xèЁK7P󰼷}DeP&-gh1fsK h e)L_o1@7#d5ԕw4}`~Yq?3;33n(Mj6J1zAŠ5P\`$1i0FBQ"4 mѴTUD:ҨW -5|k҆@1J NPlgƶfҟMTo8NV>P'z  y(ɘos|l]k^.dRgsНӟl Ut*P9bf p\&Q߰c3K"^ʉZK%ԛGk^T "zo>8nT2}Ǡ<7,$پ|Ypɸ͠RƛppgH}{%Uo9y/YkZ9T^gXaYqdQ+5e>(HOnorT,8CYR^LHꅽkzϦP: I0Mȏ\552\)uIheCkW5ToL2.fp=@3 '&r!M)4fQE655[\-G\Ћ{ZѪW -0N-8$_])yts\YOlӉX4bBoޅ tPp ˛…\sZ#N˚5[6iHom^=vh\gSr#7Г\`|tPߗQ߄VMc=-4$aykh؟uP څu4%ݔ|`Yͷě=J=>KzwR1j5L҄~[V\rg ;pmU -tR͡Ƴjlor7|HR̦z 7#ۍ@1J"9!wc,w{{j6PjޛGkNcQ[SԪloOu'70jb.7}{s~L4NZ)͡1IB>5[˺(Ǒ@sB.|m{si٫X'_yoDwb+H>Ų-9 -2/jyi$!C|.M{{ , }{si 7-"zt+XC|>mo.5:h.E<ձ7օj!'3x9܃ Grxu8{r}Ft#7G닩t<u!%ߑi,eAT.¥=_Kn$Hհ7J9#l>״7>2CIǣ_!q \>d\ͣx|ojV QuG1]{NU:3u]oy"7K7=i`bxSmIzhٛCu ` ]y$E򹛂}-I#B٭ш_o.M$XFoڝo݌|I06O5ͣ![$źiNϳ $X'Ro`FXGt!' #ycoN|HvK_~4w^62[o>8>ަ]oj˝^?NQȣ`Gp٩6kכKzIv^\* l$X4m^֬7Do`!ϔHͥI`%: V^9 @qi+X'Nt>7ݤUo.Eo`)՛GeMJx[ ެF>_1[uǿ A5lMeH:f` C.50ӦJij>d+ְrRqfP@&E~߷﹵\}=:sޯk'ΖZޑͦRR -X2q etӴ"ݓ -H9{I5+H7*Po|s -=z +N욾!yۙ7G~2o(1Ņ-ۅ U]Ϸ_tPZ;2"˻dPf^OZޡ%#UC3AW{2ZrC}آqN̼nY"9so۰A$B-Y(5Oe)D1o(7)FoLZMo(7w.Fo!詺ϷHF.aPr;[Ho(9?,Dov^>o) -qOƲJ~/(ȾUzC~R_9PvNɿXXμBkG ׬r -My9 -䝛W -꿖9-gPAwo7Tg["W*k3r-v&_HZ4&"Z{+Dʼ".38{;m`˲E*#aT~E2'pN:*+8;'傼}8'Q)^9CS|K7Tӭ%HF?d;P)N%rk*[fqtJd'Tw[,]XIo } .C?7 n>\PQ($ rNlkI߂d>$~g7TWP -ˇ6ͩߟB=$1(#ѴnҜtO*i=.71o'upL{y4]'|βhzĞG .Y=:$&KaLn٭0YpL9 pB,+kUpW](y"D?-qet@x<({aހt^erӳn2zށ{+[3А -(x@-Sz506{xgF?PP9"Q].Lpe۵g -ƣ .3ug[,< ӧ -V08]55刭4O镅Hj+ h x],ݥg0OXl\6 ˔AzK& Ɉ8(lf\"s8(Ƭs3 .3p@c^w? Pp|.<M8|DE88+'\"'=>}`E24tۧn{M9}dB}|? -PxqK~ h.][ '_Z` 8n$2yzZ`\u\$О5pe} ;]p|;I92'\,=qaT"YJo@>;b9q?prrEj^p@R7\8UNCL{ހV8XFhEpc߀<]w\"fY.D UNIȜW(681_⎂d}Z;[7\"(?&.u7Ʀk~o46X2Ercɸk_\,cW2p@E҂79[8]m U\"S_7}ܶ+RkkF,ڔNܺSǶ5q5舷py[Ge,$83VܫWgE-w%ЩwIZmfTzT1ӂki_Z6 -#Q˙AC?3 -"{QiL- s@7V&7;WǞq̓;Np@wgܚkM'.1孢k\\$=KlT\"6qP uFWc}KnL{^pjYV܋osd#XKȰ v@մ@ .8 +6qs-aJR_E!([>)ɰwȆAy9d 39p96J ^_ޤ7{}ݮNi еjz{ZyNM-[8K^׾jj&WAyIz{ӵSZ-)9(_QUw?啘 -$AQ#+X ->x4 "2h;NA* -% a)Pa HA) ;gY{w?;ݻ;{o4Hz%"ADo)E$8P"HH $9Lo8T98I"/!S9R{K[# #xVVX)Aȏ| NUl@p Po&vPfKo|& ]`M~.48qmX͡BQreI(6Lli-c%ob)IJ)70qDPQvm{uJ:7\* 6b 1X_5؁ L4X vb3{#$@D9Z}hp9L -8'%& {PnE$"4)m778&Lwnoepxo%?L,pn\㨷1Ln+5qtA7X]6%xPrɋ `(9gwre';ZvQ.Yt^T$ m7 -O!_k#5ݯ4cH $JIz j{.i&-y1%L.Vbp=ymAlzov>BQrEP̌"Eme^M?#K\ `4`z8YtЪ2:ʑn/qHw7&yU]T]5)ly=5HN\o'`9rsGn&=/l+洄nYMjJc!YC{쒀,J(`b__4߀vMdd`U,{7aZ~ҏ1r~En$7Ħ7ʯ<൦!x\oou~lQTlaboқ|La(L(pݳ2EFӑ-z]\ш`b;$B^4yV}["l{ e+Oã1 *-{#$@@Szj.l 6a䬮TΔa}ܫ)rQ i"eͿ?Ckĭ4z\*ʡۻgo`pSEo>gz>iJbӄzJ|1.ڛGS:qdT.D64b 6lκpy $Ey&ZI,OMUXjL6rw&ܖͻY{tL..]IyJ -sN/ߗs) K.|"T= Kz3H=7t}wjjC,"[_)ZzD%E_/bL!8Ӂ)R&x-wޏ/0rރn ʜT$2fRUWY.{j0y *674[*%gP[qlo.y &}XIFqcҡ53u tFZYE?W{>,Ɇ&q h!7!6Mh1/ԋE%&*z?M&yI]^ TUoeAP6g.$HlΉEy:i NF6{jwb[B?+fA4D{7bk}RLlN?TEgkaad \` Fr+b"ٶ!k=UŦ euɶ/@6UqM8`} ?Mcp̟@Qr3:-f:^$gvcVL.5`,;q(%Dr)g `yٔPZ fTY.3mqYH\[c)7-w'8W&?,Me"{7c Uqb*7(BT_|fUo)d"SVJ.0q!c~^6h D62t!Z4L $z3HE&Tp -Gmd aҲrJVʂ>l̘ n/Jj{Sư5B҇d[$g$;|gp\-AW-<>nT="8 ydt|C}aLÀkA܄3Q1.r?)xұ[V -h3 d t"=T͖ '[wFeK!) R6V -49{Yx} -m?zΛPQ;Fvw(97uUK6S7M^uJ.*cGׄ .$SF\ˌ_kD0;$c .gPBTs1h3NrMACD2<'jp!eOd(AA7cM(/Va*g_i)Wc֜c(k'ە(KԮzy$ 򏭢3C@$0?!vi! -%QSE@EXݒ?lVC]A Eإ -*hE`/ymiiݖ7ܙs?sWO.X}[2t6BmE`6NBnn l@) '?X>Y e ^#S?YB\ܑnƒskl_m^FPB-/+?faP!E2 .dup}R-Ԩg - Y1T.k'Ql o܅φoKll}aW%rɳ&$Rxؾb11ԁ'lO\)d9ҭ)>Mp]7Z‚G':u΀(q) -u$dlM -'wk S-| O;y] -1ԍ]7T5ھGOݸ$kTF`OV|PčM]tMcY }v,n \fcj{.)ٚUŠrLV$B@u*B7F6y2|rr;+\[ιrptrCx!r](P -g=c(1 fB8P -G]d i9++m'AUdQn3%ilrO8Ӫo+9ETOiSWhrFdw3:{ϣlW .6lY{ex!8t_xW>mΈȀ5{J=WFsLhPv$}_RMפ} -˴{Bzr5x.UI&${C[#Eqx(կa9EP\%wEOwS8q'^ӘD٢#AJSTY+v0ZGM0٪]lQm>Pvy$+^D & H}Bp8o/wo$_u.7Zհ 7D-z VwԼuYa0N@Ye囍 'cmt-ƗYkC#01*SuS,٩p܅[C_qbhjF Y.&L0W7O.(Uf?UʍlE0%ݹ@"qy*`@vyIy%YqpF$Xu{9"yFmNp*{BeQ-f^}szs>^D lZu4']G67TN@]6dA3$6(OY99Q#a9q 9\=Mx$U /> o$Co0:')8\٣!=zЎˋ5~Q6#]1rZ1O/h_ ~]e7yasrvlRB$@Ifs3FJ1!]Cmhykr-!إ?ns෣Ξr ONۯ0BBʆt/6ds;t>u>-.3oJM:h$c۶2kEȫ`KE9 -~&enc*wnjIcw5kΗ@,'k;}Y.Z.\\)+;=V~M+\`I)^*-O0 ! AB\u߃7%˵ .}w[<Ċsdr#Go?"Z-6K1 -$d/:0\}]7> -vTUC:ˉA€e>Ś>stream -HyTSwoɞc [5laQIBHADED2mtFOE.c}08׎8GNg9w߽'0 ֠Jb  - 2y.-;!KZ ^i"L0- @8(r;q7Ly&Qq4j|9 -V)gB0iW8#8wթ8_٥ʨQQj@&A)/g>'Kt;\ ӥ$պFZUn(4T%)뫔0C&Zi8bxEB;Pӓ̹A om?W= -x-[0}y)7ta>jT7@tܛ`q2ʀ&6ZLĄ?_yxg)˔zçLU*uSkSeO4?׸c. R ߁-25 S>ӣVd`rn~Y&+`;A4 A9=-tl`;~p Gp| [`L`< "A YA+Cb(R,*T2B- -ꇆnQt}MA0alSx k&^>0|>_',G!"F$H:R!zFQd?r 9\A&G rQ hE]a4zBgE#H *B=0HIpp0MxJ$D1D, VĭKĻYdE"EI2EBGt4MzNr!YK ?%_&#(0J:EAiQ(()ӔWT6U@P+!~mD eԴ!hӦh/']B/ҏӿ?a0nhF!X8܌kc&5S6lIa2cKMA!E#ƒdV(kel }}Cq9 -N')].uJr - wG xR^[oƜchg`>b$*~ :Eb~,m,-ݖ,Y¬*6X[ݱF=3뭷Y~dó ti zf6~`{v.Ng#{}}jc1X6fm;'_9 r:8q:˜O:ϸ8uJqnv=MmR 4 -n3ܣkGݯz=[==<=GTB(/S,]6*-W:#7*e^YDY}UjAyT`#D="b{ų+ʯ:!kJ4Gmt}uC%K7YVfFY .=b?SƕƩȺy چ k5%4m7lqlioZlG+Zz͹mzy]?uuw|"űNwW&e֥ﺱ*|j5kyݭǯg^ykEklD_p߶7Dmo꿻1ml{Mś nLl<9O[$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! -zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)Km endstream endobj 5 0 obj <> endobj 20 0 obj [/View/Design] endobj 21 0 obj <>>> endobj 12 0 obj <> endobj 10 0 obj <> endobj 22 0 obj <> endobj 23 0 obj <>stream -%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 17.0 %%AI8_CreatorVersion: 19.2.1 %%For: (Zachary Mitton) () %%Title: (metamask_icon) %%CreationDate: 6/15/16 2:23 PM %%Canvassize: 16383 %%BoundingBox: 98 -140 188 -44 %%HiResBoundingBox: 98.7919746568114 -140 188 -44 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 13.0 %AI12_BuildNumber: 147 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBProcessColor: 0 0 0 ([Registration]) %AI3_Cropmarks: 79 -156 207 -28 %AI3_TemplateBox: 180.5 -120.5 180.5 -120.5 %AI3_TileBox: -163 -488 449 304 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 6 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 1 %AI17_Begin_Content_if_version_gt:17 1 %AI9_OpenToView: -39.6666666666679 23.666666666667 3 1419 866 18 0 0 -5 38 0 0 0 1 1 0 1 1 0 1 %AI17_Alternate_Content %AI9_OpenToView: -39.6666666666679 23.666666666667 3 1419 866 18 0 0 -5 38 0 0 0 1 1 0 1 1 0 1 %AI17_End_Versioned_Content %AI5_OpenViewLayers: 7 %%PageOrigin:-220 -420 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 24 0 obj <>stream -%%BoundingBox: 98 -140 188 -44 %%HiResBoundingBox: 98.7919746568114 -140 188 -44 %AI7_Thumbnail: 120 128 8 %%BeginData: 22554 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C45FD24FFA776FD75FFA04A4AA1FD73FFA04A754A75A8FD71FF7C4475 %4A6F4A6FA8FD6FFFA04A754B754B754A75FD6EFF764A6F4A754A6F4A754A %76FD6CFF764A754B754A754B754A754AA1FD69FFA8754A6F4A754A6F4A75 %4A6F4A6F4AA1FD44FFA7C9A075A8FD1EFFA8754A754B754B754B754B754B %754B754ACAFD3FFFCFC9C299C1997476FD1EFFA76F4A754A6F4A754A6F4A %754A6F4A754A4B4AFD3BFFA7C99FC198BB98C198754AA8FD1DFFA8754A75 %4A754B754A754B754A754B754A754B6F76FD37FFC9C99FC198C199C199C1 %99754A75FD1DFFA74B4A754A6F4A754A6F4A754A6F4A754A6F4A754A4A76 %FD31FFA8C9A0C1999998C1999F99C199C1746F4A4A76FD1CFFA1754A754B %754B754B754B754B754B754B754B754B754B6F7CFD2CFFCAC9C89FC198C1 %99C199C199C199C1C1C175754B754ACAFD1BFF7D4A4A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A6FA8FD27FFCAC9A1C2989998C199C199 %C199C199C199C199C16E4B4A754A75A8FD1AFF7C6F4A754B754A754B754A %754B754A754B754A754B754A754B754A75FD24FFC9C99FC199C199C199C1 %99C199C199C199C199C1C1994A754B754A6F76FD1AFF764A4A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A76A8FD1DFFA7C9A0C1 %99BB989998C1999F99C1999F99C1999F99C199C199994A4B4A754A6F4AA8 %FD19FF756F4B754B754B754B754B754B754B754B754B754B754B754B754B %754B754A76FD1AFFCAC299C198C199C199C199C199C199C199C199C199C1 %99C199C199994B754B754B754A7CFD19FF764A4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A99FD04C199C1C1C199C1 %C1C199C1C1C199C1C1C199C1C1C199C198C199C199C199C199C199C199C1 %99C199C199C199C199C199754A754A6F4A754A4A7DFD18FF7C6E4B754A75 %4B754A754B754A754B754A754B754A754B754A754B754A754B754A754BFD %05C1BBC1C1C1BBC1C1C1BBC1C1C1BBC1C1C1BBC1C1C199C199C199C199C1 %99C199C199C199C199C199C199C199C199754B754A754B754A754BFD19FF %754A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A4B74C199C199C199C199C199C199C199C199C199C199C199C199 %9F99C1999F99C1999F99C1999F99C1999F99C1999F99C1994B4A754A6F4A %754A6F4A76FD18FFA14A754B754B754B754B754B754B754B754B754B754B %754B754B754B754B754B754B754B7599C2FD16C199C199C199C199C199C1 %99C199C199C199C199C199C175754B754B754B754B754B75A1FD18FF4A4B %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1 %99C199C199C199C199C199C199C199C199C199C16E4B4A6F4A754A6F4A75 %4A6F4AFD18FFA16F4B754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B75FD16C199C199C199C199C199C199 %C199C199C199C1999F6F754A754B754A754B754A754A7CFD18FF764A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A9999C199C199C199C199C199C199C199C199C199C199C199 %9F99C1999F99C1999F99C1999F99C199994A6F4A6F4A754A6F4A754A6F4A %4A7DFD17FFCA4A754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B7575FD17C199C199C199C199C199C1 %99C199C1C1994B754B754B754B754B754B754B754BFD18FF764A4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A4B74C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1 %99C199C199C199C199C199C199754A6F4A754A6F4A754A6F4A754A6F4A7C %FD18FF754B754A754B754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B7599FD13C1BBC199C199C199C199C1 %99C199C199754A754B754A754B754A754B754A754B75A8FD17FFA04A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A7599C199C199C199C199C199C199C199C199C199 %C199C1999F99C1999F99C199C174754A6F4A754A6F4A754A6F4A754A6F4A %6F75FD18FF4A754B754B754B754B754B754B754B754B754B754B754B754B %754B754B754B754B754B754B754B754B754B754A9FFD14C199C199C199C1 %99C199C175754B754B754B754B754B754B754B754B754AA7FD17FF7D4A4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A4B4AC1C1C199C1BBC199C1BBC199C1BBC199 %C1BBC199C199C199C199C199C16F4B4A754A6F4A754A6F4A754A6F4A754A %6F4A75A8FD17FF764A754A754B754A754B754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B7575FD13C199C1 %99C199C1BBC16F754B754A754B754A754B754A754B754A754B6F75FD17FF %A84A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A4B74C199C199C199C199C199 %C199C199C199C199C199C199C199994A4B4A754A6F4A754A6F4A754A6F4A %754A6F4A754AA1FD17FF76754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %9FFD11C199C199C199994B754B754B754B754B754B754B754B754B754B75 %4B75A8FD16FFA86F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A99C1C1 %99C1BBC199C1BBC199C1BBC199C1BBC199C199994A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A6F4A6F75FD17FFA74A754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A754B754A754B754A754B754A %754B754A754B754AFD13C199754B754A754B754A754B754A754B754A754B %754A754B754ACAFD16FFCA4A6F4A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A4B4AC1BBC199C199C199C199C199C199C199C1996F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A75FD17FF7D6F4B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B7599FD10C19F4A754B754B754B754B754B %754B754B754B754B754B754B6F7CFD17FF764A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %4A6F4A754A6F4A754A7599C1BBC199C1BBC199C1BBC199C1BBC199C1C199 %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754AA8FD17FF75754A %754B754A754B754A754B754A754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A7599C199FD12C1994A754B75 %4A754B754A754B754A754B754A754B754A75FD17FFA8754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A7599C1999999C199C199C199C199C199C199 %C199C199C199994A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A7CFD %17FF75754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754B754B7599C199C199FD13C1 %99994B754B754B754B754B754B754B754B754B754B754A7CFD15FFA8754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A7599C199C199C199C1BBC199C1BB %C199C1BBC199C1BBC199C199C199994A6F4A754A6F4A754A6F4A754A6F4A %754A6F4A754A76A8FD13FFA84A754B754A754B754A754B754A754B754A75 %4B754A754B754A754B754A754B754A754B754A754B754A754B754A754B75 %99C199C199C199C199FD0FC1BBC199C199994B754A754B754A754B754A75 %4B754A754B754A754AFD14FFA14A4A754A6F4A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75 %75C199C1999F99C199C199C199C199C199C199C199C199C199C199C199C1 %99754A6F4A754A6F4A754A6F4A754A6F4A754A4B4AA8FD14FF7C4A754B75 %4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B7599C199C199C199C199C199FD11C199C199C1 %C1754A754B754B754B754B754B754B754B7576FD12FFA8A151754A6F4A75 %4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A4B74C199C199C199C199C199C199C1BBC199C1 %BBC199C1BBC199C1BBC199C199C199C199754A754A6F4A754A6F4A754A6F %4A754A757DFD11FFA14A4A4A754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A754B754A754B754A754B754A7575C199 %C199C199C199C199C199C1BBFD0FC199C199C199C199754A754B754A754B %754A754B754A754A4A75CFFD10FFA8754A4A754A6F4A754A6F4A754A6F4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %4B6EC1999F99C1999F99C1999F99C199C199C199C199C199C199C199C199 %C199C199C1999F99C199754A754A6F4A754A6F4A754A6F4A754A4A4ACAFD %11FFA8754A754B754B754B754B754B754B754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B7575C199C199C199C199C199C199C1 %99C199FD11C199C199C199C199754B754B754B754B754B754B754B754ACA %FD13FFA87C4A4B4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A756EC199C199C199C199C199C199C199 %C199C199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C199754A %6F4A754A6F4A754A6F4A754AA7FD17FF75754A754B754A754B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B756FC199C199C1 %99C199C199C199C199C199C199FD11C199C199C199C199C199754B754A75 %4B754A754B754A76FD13FFA8CA7DA176754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A4B6EC199C1999F99 %C1999F99C1999F99C1999F99C199C199C199C199C199C199C199C199C199 %9F99C1999F99C199C198754A6F4A754A6F4A754A4B4AFD12FFA87C4A4A4A %754B754B754B754B754B754B754B754B754B754B754B754B754B754B754B %754B754B754B7575C199C199C199C199C199C199C199C199C199C199FD11 %C199C199C199C199C199C199754B754B754B754B754A75FD14FFA8754A4A %754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A4B4A9F99C199C199C199C199C199C199C199C199C199C199C199 %C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C199C1994B4A754A %6F4A754A6F4AFD16FFA8A14B754B754A754B754A754B754A754B754A754B %754A754B754A754B754A754B754A7575C199C199C199C199C199C199C199 %C199C199C199C199C199FD11C199C199C199C199C199C199754A754B754A %754B6FA7FD18FF516F4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A4B4AC1999F99C1999F99C1999F99C1999F99C1999F99 %C1999F99C199C199C199C199C199C199C199C199C1999F99C1999F99C199 %9F99C1754B4A754A6F4A6F4AA8FD17FFA1754B754B754B754B754B754B75 %4B754B754B754B754B754B754B754B754B754BC1C1C199C199C199C199C1 %99C199C199C199C199C199C199C199FD11C199C199C199C199C199C199C1 %75754B754B754AA7FD15FFA8A14B4A4A754A6F4A754A6F4A754A6F4A754A %6F4A754A6F4A754A6F4A754A6F4A756E9999C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C1BBC199C1BBC199C1BBC199C199 %C199C199C199C199C199C199C174754A6F4AA1FD17FF4B6F4B754A754B75 %4A754B754A754B754A754B754A754B754A754B754A754B754AC1C1C199C1 %99C199C199C199C199C199C199C199C199C199C199C199FD11C199C199C1 %99C199C199C199C199C175754A76FD18FFCA4B4B4A6F4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A6F4A6F4A9999C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C199C199C199C199C199C1 %99C199C199C1999F99C1999F99C1999F99C199C16E4BA8FD1AFF756F4B75 %4B754B754B754B754B754B754B754B754B754B754B754B756F9F99C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199FD0FC1 %99C199C199C199C199C199C199C199C1A1FD1CFF4B4B4A754A6F4A754A6F %4A754A6F4A754A6F4A754A6F4A754A4B4A9F99C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C1BBC199C1BBC1 %99C1BBC199C199C199C199C199C199C199C199C198CAFD1CFFCF4A754A75 %4B754A754B754A754B754A754B754A754B754A754B9999C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199FD0FC199C1 %99C199C199C199C199C199C199C1CAFD1DFFA84A6F4A754A6F4A754A6F4A %754A754A754A754A754A6F4A99999F99C1999F99C1999F99C1999F99C199 %9F99C1999F99C1999F99C1999F99C1999F99C199C199C199C199C199C199 %C199C199C1999F99C1999F99C1999F99C199FD1FFFC299C1C1C19FFD0FC1 %99C1C1C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199FD11C199C199C199C199C199C199C199C2FD1EFFCABBC1 %99C1C1C199C1C1C199C1C1C199C1C1C199C1C1C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1BBC199C1BBC199C1BBC199C199C199C199C199C199C199C1A0FD1EFF %FD18C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199FD0FC199C199C199C199C199C199C198C9FD1DFF %C9C199C199C199C199C199C199C199C199C199C199C199C199C199C1999F %99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1 %999F99C199C199C199C199C199C199C199C199C1999F99C1999F99C19999 %A1FD1DFFC9BBFD19C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199FD0FC199C199C199C199C199C199C198 %C9FD1DFFC1C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C1 %99C199BBA7FD1CFFC9FD1BC199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199FD0FC199C199C199C199C1 %99C199CFFD1CFFC298C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C1999F99C1999F99C1999F99C1999F99C1999F99C1999F %99C1999F99C1999F99C1999F99C199C199C199C199C199C199C1999F99C1 %999F99C1999F98C1A8FD1CFFFD1EC199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199FD0FC199C199C199C199 %C199C199FD1CFFC9C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C199C199C199C199C199C199C199C199C199C199C199 %C199C1999999C1999999C1999999C1BBC199C1BBC199C1BBC199C1999999 %C19999989999999899A8FD1BFFC2FD1EC1BBC199C199C199C199C199C199 %C1999999C1999999C1999999C199BB99C1999999C1999999FD0DC1999999 %C1999999C1999998C9FD1AFFC998C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199999899999998999999989999 %99989999999899999998BB999998999999989999C199C199C199C199C199 %C199C199C199999899279998999999A0FD1AFFC2FD23C199C199C199C199 %C199C199C199C199C199C199C1999F515299C199C199C199C199FD0DC199 %9999C1992E4BC199C199C2A8FD18FFCAC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C19999989999 %99989999999899999998C175510528057598999999989999C199C1BBC199 %C1BBC199C1BBC199C19999989927286FBB99C198C9FD18FFC9BBFD25C199 %C199C199C1999999C1999999C1BB994B2E0628272E279999C1999999C199 %FD0DC1BBC199BB992E282E99C199C1A0FD18FF9FC199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C1999F99 %C1999998999999989999BB98752706052827280528279998FD0499C199C1 %99C199C199C199C199C199C1989998990528054B98C198A0A9FD16FFCAFD %29C199C199C199C199C1999F7552282E272E272E272E272875C199C199C1 %99FD0FC199C1752E062E51C1BBC199FD17FFC998C1BBC199C1BBC199C1BB %C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C199C199C199C199992706052805280528272805280528989999C199C199 %C199C1BBC199C1BBC199C1BBC199999951057699C199C1999FA8FD16FFFD %2AC199C199C199C199C199A07576272E2728052E2728272E277599C199C1 %99C199FD0DC199C1759FFD04C199C199CAFD15FFA7C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C1999F99C199C1BBC1C1C1999F7575272827280528057598C1 %999F99C199C199C199C199C199C199C199C198BBC1C199C1999F98BBA7FD %15FFC2FD2EC199C199C199FD0BC17576512E27C19FC199C199FD0FC199FD %05C199C199CFFD14FFCA98C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1999F99C1 %999F99C1BBC199C1BBC199FD05C1999F99C199C199C199C199C1BBC199C1 %BBC199C1BBC1999999C199C1BBC198C1CAFD14FFC2FD31C199C199FD11C1 %99C199C199C199FD0DC199C199FD05C199FD14FFA8C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C1999F99C199C199C199C199C199C199C199C199C1 %99C199C1999F99C199C199C199C199C199C199C199C1999999C199C199C1 %CAFD13FFCFFD32C199C199FD15C199C199C199FD0FC1BBC1C1C199FD14FF %A0C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1 %BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C199C199C1BBC199C1BBC199C1 %BBC199C1999999C199C1CAFD13FFC1BBFD33C199C199FD15C199C199C199 %FD0DC199C1C1C1C2FD13FFC998C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1999F99C199C199C199C199C199C199C199C198C2FD13FFFD52C199C1 %99FD0DC199C1A1FD12FFA7C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1 %BBC199C1BBC199C199C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1 %99C199C199C199C199C1BBC199C1BBC199C1BBC198C9FD12FFC2BBFD35C1 %99C199C199C199FD17C199C199FD0DC1C9FD12FF99C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199999899999998C1999999C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %98C2FD11FFC9FD31C199C199BB99C199BB99C199C199C199C199FD15C199 %C199FD0BC1BAC9FD10FFC2BBC199C1BBC199C1BBC199C1BBC199C1BBC199 %C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1FD0499 %98999999989999C199C199C199C199C199C199C199C1BBC199C1BBC199C1 %BBC199C1BBC199C1999F99C1BBC199C1BBC199C1BBC198C9FD0EFFCFBBFD %2BC199C1999999C1999999C1999999C199C199C199C199C199C199C199C1 %99FD11C199C199FD0BC1BAC9FD0DFFA0C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C19999989999 %99989999999899999998FD0499C1999F99C1999F99C1999F99C1999F99C1 %99C199C199C199C199C199C199C199C1999F99C199C199C199C199C199C1 %98C9FD0CFFC299C19FC199C19FC199C19FC199C199C199C199C199C199C1 %99C199C199C199C199C199C199C1999999C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C19FFD0FC199FD %0CC1CFFD0BFFA79999C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C19999989999999899999998999999 %989999C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %BBC199C1BBC199C1BBC199C199C199C1BBC199C1BBC199C199CFFD0BFF99 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C1999999C1999999C1999999C1999999C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C19FC1BBFD09C199 %FD0CC1CFFD0AFFC2989F99C1999F99C1999F99C1999F99C1999F99C1999F %99C1999F99C1999F99C1999F99C199C1FD0499989999999899999998FD04 %99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1 %999F99C199C199C199C199C199C199C199C199C199C199C199CFFD09FFCA %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %FD07C199FD0CC1FD0AFF99C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199999899999998999999 %989999C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C1C1C199C1BBC199C1BBC199FD04C1 %FD09FFC999C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C1999999C1999999C1999999C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C1C1C199FD07C19975754B27A8FD07FFA8C1999F99 %C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C199 %9F99C1999F99C199999899999998FD0499C1999F99C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F %99C199C199C199C14A27F827F805F8F8F852A8FD06FF9FC199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C1BBC175270027F82727272027F82752FD05FFCA98C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C1999998999999989999C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C198BB98C198C199C199C199C2A0A0A0 %C9A127F827F827F827F827F827F8F87DFD05FFC299C199C199C199C199C1 %99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C1999999C199C199C199C199C199C199C199C199C199C199C199C1 %99C199C199C199C199C299C199C2A0C3A0C9A1CAA7CAA7CAA8CAA8CAA8A8 %2727F8272727F8272727F8274BFD06FFA0BB999F99C1999F99C1999F99C1 %999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C19999 %98FD0499C1999F99C1999F99C1999F98C1999998C198BB98C1999F99C199 %A09FA1A1A7A1A8A1A8A1A8A8A8A7A8A7A8A1A8A7A8A1A8A7A8A127F827F8 %27F827F827F827F8A8FD06FFCF99C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C29FC199C2A0C9A0C9A0C9A7CAA7CAA7CAA8 %CAA8CAA8CAA8CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA8CA27272027272720 %272727F852FD08FFC299C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C1989998BB99C199C199 %C2A0A0A0C3A0A7A1A8A7A8A1FD07A8A7A8A7A8A1A8A7A8A1A8A7A8A1A8A7 %A8A1A8A7A8A1A8A7A8A1A8A7A8A127F827F827F827F827F827A8FD08FFA1 %C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C199C198C2A1C9A0C9A1CAA7CAA7CAA8CAA8CAA8CAA7 %CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8 %CAA7CAA8CAA7CAA8A8FD0427F8272727F82752FD09FFCF98C1999F99C199 %9F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999998 %BB98C1A0C9CAFFAFFFA8A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7 %A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1 %CAA127F827F827F827F827F8A8FD0AFFC299C199C199C199C199C199C199 %C199C199C199C199C199C199C199C199C199C199C199C2C9CFFD0AFFA8A8 %A7A8A7CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CA %A8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8A8FD0427F827F827F87DFD0BFF %A8C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1 %989999C9A7CFFD10FFA8A87DA7A1A8A7A8A7CAA7A8A1A8A7A8A1A8A7A8A1 %A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1CAA127F82727 %5252767CA1A8FD0CFF9FC199C199C199C199C199C199C199C199C199C199 %C199C199C199C199C2C9CFFD16FFA8A8A1A8A7A8A7CAA8CAA7CAA8CAA7CA %A8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7A8527D %7DA8A7CAA7A8A8FD0DFFC998C1999F99C1999F99C1999F99C1999F99C199 %9F98BB989999C9A7FD1CFFCFA7A87DA17DA8A1A8A1A8A7A8A1A8A7A8A1A8 %A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A1A77DA8A1A77DA7A1A1 %A7FD0EFFCAC199C199C199C199C199C199C199C199C199C199C2A0C9CAFD %23FFA8CAA1A8A1A8A7A8A7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CA %A7CAA8CAA7CAA7A8A1A8A1A8A1A8A1A8A8FD10FFA0C199C199C199C199C1 %99C199C199BB98C199C9CAFD29FFA8A77DA7A1A77DA8A1A8A1A8A7A8A7CA %A7A8A1A8A7A8A1A8A7A8A1CAA7A87DA8A1A77DA8A1A77DA7A1FD11FFCA98 %C199C199C199C199C199C198C2A0C9CAFD2FFFA8A8A1A7A1A8A1A8A1A8A7 %A8A7CAA8CAA7CAA8CAA7CAA8CAA7A8A1A8A1A8A1A8A1A8A1A8A1FD12FFCA %C198C1999F99C1999998C2A0CAA8FD33FFA8FFA8A87DA77DA77DA77DA77D %A8A1A8A1A8A7A8A1A8A1A77DA7A1A77DA7A1A77DA7A1FD14FFA0C199C199 %C199C1A0FD3DFFA8A8A1A8A1A8A1A8A1A8A1A8A7A8A7A8A1A8A1A8A1A8A1 %A8A1A8A1A8A1FD15FFCA98BB99C2A0CFFD41FFCFA7A8A1A17DA8A1A77DA8 %A1A77DA8A1A77DA8A1A77DA77DA17DCAFD16FFC9A7FD49FFA8A8A1A8A1A7 %A1A8A1A8A1A8A7A8A1FD04A8FFA8FD66FFA8CAA8A8A8FFA8FFA8FFFFFFA8 %FD12FFFF %%EndData endstream endobj 25 0 obj <>stream -%AI12_CompressedDatax$&?d& C;\;fJ-n[[YUZ(xd&,f #+3q98OxOq< ?wo ~7_{ˏ~/&G4M~Vۯ߼LG{4?oqwo^_^ޡݻ篞g/PWnC} 4`e߱=&7Ô?L/ޣvu&3%Co^|O߾yq7/߼W?>y~^|0;qv~c7/o^a|t/_/tqzWw0R<3ٯOaC)?l/Jo|ۿi[Lݘج{K̬̂1??J[x?~W~NguG|˻FѤS7_ܽD/ H1oɦ >Kz3 3y}vgӭw2IM~?WyOtҺ2!Lj}.6(^{_G<Ѽb |aoSl߿DŽkѽ_bٚO1';=I~R4!o;H]cձW?y=5w7_j|ӷR}To?~_^bqQ>mŃߔ?뿏 Bӛ{U>}|CsL!޽zn -!}Ho_wg߾܎Ͽzz_~˧ߩO n_|=w.̙/|ܿ8~_xNuTOX[˷Zߥfi>$_>ksP3|q-y=?F?!]϶j~xx7/~tS/>\?߿cwwI|+r;鳶|0e> loq\߼3L/pba,H=;0?)vU\) ߀%%Ӧ\ \܌x[f`*nU-LDIo^iSi.繜5Jz7ѵ][*FAiUU*'-VmӯVuY[a^^Zd]fU͛V+ebe7g]k;la]/WkdYԬU)۵Z)*և:YeXtMe@CY#չk)7ܲԓŗYUeLIɭ̍zʵؔF2 ssλɝP-Vx>'O[L .C -S -p7v v)esUsSʧ|o-&7 LNyni̕W*^rdNONtemwp|:[l6#ut}>_\ތXwoM7 usnnnn#n1aoz^m7r*U9զL _^*qSªUq 8R$l!z7M9kӪ\ʴ*ySҪU M*vU̪KS>_֣_WENf]Z%. bXv 2ʌd)v64o򬡙R&)$%JR\)vWL%Ϫ?')WLRr)8K R|)%Ѓֵ;zeY eeگed&G/fdndbN2yw|Q^Z^Jd^Fq``2Ϡ[W_t,yP5 j>H73_J F1aoטt@H tr@x#HuN D;x;p{{@}w & -D5CpqnX. K׌ucq7g\DW);2UnC|Ey x;Rٙ-خv8Pz? gx'H˗v؅0ܮH *`Cld!2r.y 9&*w"6`ټ.b/+>P,"eXalX~PdS }bٛ2<duXE3Ϩr;.E9J\d('}(bs=U-l4W=9"}ZӬYAL6%Ts끼VJ{OX 2=ye[3UCwD翇d?Sԇos<;XԵ?ʏOBF7mLE߰s8҆+ H$" (" tAy\( !DʢJBFǭH|! ,DiȪ4$uN"e(rE"R,RQш‘Q ,e$JI OeSB%'ЈjFĥkK(2Qhؔ|J5t[듖|97nI?FյX4̲P,~)uuxIx" ?4 Qs E6< KCvD1l4맕aýE|r.VOoGEq3- -U -ͪLTTJэEUZ*mXM]JY\9UDi%%2r5=Ҵъ %s(It8i4fCT -a);12y?Ӌ+[ @c&*PV5m\86 ŸV+-7bU[#w)Zf{% 0<@jXp1frb=]9It=G9 [>EXy+3KoJO+ï~QQ0:1L<98Ilj> -Ѿ8Jl+u!3)]Hh% \hB#ڸLo{[Z&qk"fÊmIWCv8x}i؎u'^{߇qmCIJϞ=c@09Bk۱iccwt6sM&;XݸCI1TQoZ@,qx)G7?}HH|5FPH!H58RZ$n҇U}|OV#pDnyu:q/#7I0c (0 -+tǵl⟿Z +R>qV#6Cxji7~zQfd @,zv4./_bS;;c4`&# 4؜6%0ySIRм m{=jP]||s|<:@> lm/zr._S  -_rUXBa3|!<4S<0< Sy+De&W@AAr-^uUjKTˈXōuXᒗe?#uFnfVŜzQ27sPf'z3 mdq<9+r>3_;ug{YʪHNEa십]ԕd1U w]<[ )8c(bb<;]=),IȥId,v+POD QuVoʬ*rk$V_ba_U[X.}7IʲC#rOL,lEkY, En%ʦ"ƞD1V] ֯$XJ:vl`{6VY=+2ҡYAnVط6{;,Y ugeV^am|O;>ͩ3/Յ8/+w J.zꓨW|٘X \bU鵧_o(r = wxw<}e|m>rz|/GTy/Ҡ `+0{ط>WG|/\,O_{ُF-I}34Yݵuw}\&vřTi: }S͍2.",W(^QFl~34mٷ!,ԅl#AiEqڐ9,%#s֡%bn#'g=<r9?3:Iq?:?'3{T|ʞ:/yE X0=QnEk Zi:EBm9Ɍ \:H͕}g02x˩yj4>/=?S=w טiąYpo8&joEI'Oo>sQcb J"cTՎb&)xEUܔ&`dgFzv%ś9rJAKqj6Myt a\~ا~+7^ݽJhgyus]j/iR:SnBL%4#Kq _ KX*8^Kz\e>l/ư7TVrǩ0 7U/ޅA[lH8$&qOTqʅZ%.B5!% KJ)@(|GY:>LBaK\NKyUoItfm6ŭݐ\¦?)NUWe6>%BE6Q%J>q唚mWCyZy"obݮuc*qj+}ZX9>p,JͫoD Dޤ$;;mXGƕl_f#2-dpeQSEN[cnm4;h\; Vymff g,Ju}o14Q$:i\-.ns8ič+9m6VkϚ|['hZzvvAuaG`}t`}YhG_:m8GN84hu*, _ CkA|,|aGWuw#my{"Vފݹ(|مavWJů9jZ~,wr Ez_dxZ_KY˻JR EV 襠qwU)y@R޸0fwnxQ4;9LAǦ6 -wz·2_}q|t0>\v,нe| -(Cq7o]ͷ`['sثj[d˧1rQNت8 ETԑ<}>S|efk Hʾ_>Ctu;,0D9,'%CS9 .N[ZcqaO΃q5Ovi7bE@Om>nzw)6>FaCmq$8I?I7 ,BԽMٻ -M^A*V*#9Փi]nLʼnigLìQ&[,_?5@'Q -oDT7 F\'uY6@  ,[eh>O*r.V+÷h#exZ:i0$3QN -ߑ6V<ϒ^XEH92kV'jX\+KdP8SGYр3ɡeNqV1 e'%:U9J1џP:vlu{L[5*F6Ԯ/+B ƪ-; eфaǓJږ߶<' Oo_KM ]"\EkF0EDϤE Ȕθϔog|VUu^v)H! {'xT:UjCITҒآ(ZJ(Z4KY薮lZJa\"6Bi(D9P{i{::6KOyíO0i5*alX!X%RK努Q>E(c4,Zۙ;^o;r%( />1]3HJW.=Cq Q; JliJB%۵C^ײҫjL[ExDZ䤵 nI˴~ԻzMj,v8'2<9> Oo_êD Oس&F$(0SNeb - -0jhMMop-~v*fPj0<м4A͗ƪ]\ /0)zdp[_bݫjZL]kmrkX6ָ՚.XƬuɨ1i=d.LYO^IS)exZ 2< NO' -O' -xm7?a>Ykpi>d8$\09 f‡B'zgFxqO/e⎭cT1;ɸC0tmojj{ (qp͂C@z Nim)x?NvU!qnܺ//rvEi]ŧMi|T3ٷO'ؤ\L+u6u&٦|t,JtѲK]j=?Q`%֜k,pHyTak5u1'@IQAl P8MJ&7nJiQ<YOdF0DG3yvhCx/ew9ˣ/h-UPhnu&rSwOfc[x^$؝lxqq}csnٖIİX#!uDmw2<@7[I%~d<>WMeuv]a v2K%ѳ.ڟsq\\40Ž[Ѭ 7A 7_,~bN|v#n`^E.'[dՊ>R: ^G=,>($&K}id5VZڨpk ,ٴ 0 JJTgb60rOd`WIj>Q)3G^<-g@GpAYj,D9&# n-ƄA2 m2v'}2(W]c~TQ9oXVͨt?{<% rv 3nl=ثcvL !crU8+}Nߓ%@q+%ǭ$"~(2^ -Ma\Qݙ>Y+|Z[1z[gz[1Ԙ۴mJUr5Y|Vnodw`xNRKisl\7P΍||TGYugnKE$%V`t -6>+j::T\Phel銻PnC%oS5 -YSh -fWX1V *:I2SOBI44!eVk?xܓ'cOLj4-oKYuUh% JLfd-ytu<+qeYmˇ_CUVN`7(<ˌpaN ƍ]崂v - 6<žr[D\,-9n^$ D8aw2\0[ԡz*--ZLTFh7@K`nQ@밆#^MnRD_yrwQvpѹჲpb?McCa7Mt;HsM8)4y%qi$!wb-jqo1Pm+?:W]:HC'tJ!v\Y"fzw)n.(RD -K gѢ_ \߹D!˔] bjQ"#ΐSm2a+|;rudȼ]A>Q?ulR0Ԝn`uEss9 +Q37fQ;NLthp) n)n6WZnE*:]yu}; ӕdqYJZ,=*SZ< JӐH~[͎csߥ ÅR$Azae+̬䜽)Ò)SA$ZܬKV׬q:k)=As<2mS/LtMo@] ?}S>wihx9{JgU|>i?)D=:Kb%5ީ xyN$ȫik. ~s>4P}h/=vY@M~ ƽ#j&btan"k9hj/$s.!OE8]O$opLs*~$neqb:<7BB.oN4;rN/+"V}ZC^2TX5wb!xS`*ffs9 : i.!JUfQ?H*F!uV[6Nա$ͲWa⇰0IRnJ:'hp!bBY -`E;p8O -n2 ;aN(FXg `ᡭXbmZbw k7ax-(ÅS[/|um*][Wm!n;2._=11i8cT]#w-ՇI~ݔ÷Q~^/CQJ Rz$-Hi[Ȥ"k?hR{W5qn UVi+~ - -whpC=C~ӷݿOVrbXݽ} ?9DaSt~;X43a{GOl6O2+o:O?1'?O}t+T>:oZv{{~ywo^?ïD{ӛ7/1y)wo>;=WL?ܿ{݋w8w|ȯZ>@ȒgEYkpZtSacBM~˵)\Y$gq81A`EtY5$fL!Lzv#`!1<5ق6$CkG9`g9A&0=޲;z|ŏM?!0}<&|;t> C0/6R[¾3>8bx 7鈹3]Gg #.6ÄJ*1*ɁDݡ ؚ#{6+#Xu<*ln[T8lǀ`!k_&.GN΄bia&P,. G_;3GxbcY16TCG)D[^6Wc,d:ɓGd1IN'LH0%@;K'Y(G(MO#6%] @PbsO' d$S߆d-J4Rwg4udo+qo5!|~lb>Y Vg= !9P`W^éȝGV` jTZYBؓuZuvd1elYSg#n -}[Ϲ0qufۿdf7_/ ʃs7_ٛ? ojõL{آ!TfEbѝ(xL(2f㴸˸$n -,L"C"zJЄ \7T[ʤd + 6vBoeW]!DW1oHwm%H2kdRq'CՎXZ[i;Fx#B޵] yoeNl3_cD9D/*m% -dބSy]- u•HJbcx[ w?* rYrBʟ:3̑nsS.eUdSik3G>fT9i\h?qsiC;+x#@E:PlXJŽKun|ƛy$ 8C(A]Z0Oׄm x{\F0TOEȓHfwaaJ@Cr`%@|R%RU#>Lo;yp"MfP"0=Xpn |9APr- -23A(LOř\'"Dӂ3 -|=g>/ݡR҉Ѩ#3'7=.{&a1[f^qɷb!qVxT,@m gS+ + ~ -gZh+6,QYޚYռ9>sǹ %ґ?l mm]㽃)Lp轑ChM - SI8\Յ<%44.i+഻2>=ρo?Rݵ41?U58?!Yި&2qňLeLf9m#p\ Kdve~e K¶#}0UOe$|xA*4>:Q[# - LTU$ ֎&&4yr($bx(5M0Ɉprwݗ#-F6<_fn.(`wy|nME&ڑFȄ/;b)q&L{30D>fL4Z$.H=%7}+.Xl Vnp4s. .N:іDkP'_C:QӍŪNT)/#*tNN."PtHݼBsEn>nlG؊ TV7SBH`dp,d kEڴqƑ-@#v∣ؔfAR !D^Kh׊309Pd.WjZQJ`t-vߩkNM7nv[y=Anu =U^d*NIq<4Q) -4֣VԻs9)߲zNWQFm;-:ϱ?3RONYϹ4wJ?Y1F7lֵ, F8J\oY}68j @)7TەTEਟ -ic c&N-pdd?ѭLc̍w0SkY~x;(&68`d]@i U}q@3ŭOʨ4|nނ, xʀ1MO#iڸ&H-nqdq]$v0qTɣR;{#OR?WjJ'$q+M[1MFU7$#C tm!NLa\h{:ldneMq @G׾QKr>2k;AMx_}puB^C?1*!jT-tC"GӰ׏-8`Ǹ;EN/ik0C?* RFnvn~Uh:}|KCpބTmOM4{ޭ8Ix!@cnY`LXV@--4%"Epj|E$GΈdɄx%hkQQomh=mCQoOwN $. -4"Fj0Z̝eHӐKaDcJj{ eY[El]lB8y@A;^|Dڋ(@\▃@:usFtΐ"hνIB/>RzH#wm=Y sΈr ̎\DK墿8 W&:nХb%uV7K$3)ގI^c_SroT#xCAa#4[So+8ո{|*&D -l0;ut0ۙ\L2(D/qpoTWF\ n6!XH/@]/"#RBեx9D -1 fEh5X1FHXPIX X|5^ɓIr=t]F3}7Q0Yuoe%D>9tɆ/Ge#* BBu Vѫ!-W'0rǺ~,!^iTvtItu:+! H{D>GYƕn\/ iǩ'+z&;vʈ!5p O| ߩw{$${b[BPO;y,Ͼ YKfa5;-gLP>]yl?w"CuQ*v>ͫ֋БGqE{'{: -豛ɗWi+vwf7V'˒Qv]j.gT쒑hL~^eY݄6:oCت(^@Úd7^_y“-]- T?Q{jԿYgOcV/ɂ3xo]:y_~lukjȣ^}V%/VQ92RඈPūV Xԓ;Nev=ϻJbOӪwCi֠cû3P.v^z:p}:5I /P'U`fxX;Eu} e?HS߼N"7m&E&<0A4۴-fRny|WF$C2KaR`/v&ӋKD?:\ - DLsL^:~"r|ws5mn%n!#\ -얍y09ġxJxI&0!Sl <ཽAz#௑(k$2JS` L%NO;/< &*0yf0 -XOV:GKoe'o/^wDFFWfn -8ݱ ɉ)Z\ɹxf#~2`gWuSq!nH "74w`>`ő<`z*Po1գguΐ!?穋H#o1)g 5k78'*%PbNHj2pWFpJO^6GA0SXb;VF h=Yt՘‡Ob4Q4d1VTGKN,"6ΜF %3a-W3e^\ zr] Ki -/ƺ7EN)(ꦑ6~,VE VSӢRfn~:}t0%GQ Dj[1"FQQb3Q]?h+&@zLYB -,%Mw'Ia$ Q=uYsTB -ݓU g&TQLQLgyiP6Ǝ}]7iR^!FKBᦢ5Jd2Ɲ:qu#U -H)4v-@BN ifSeO>I"Ntl?Us*Oi4K;!ql%1- "%(ѥܹLoSSᕝm( +֌ܪ+sFFWod,QbdB(*dtI$  F`3fP"0 }̼aiටkp=YS‰7-b$'186h^H6 -Gbgy@h <):o^i&망n( -"deA53cK^3C"xfB+DuŅ*MfHV@ cX=dԥ bkug(]ꓚVI`4f !m :Ez8ӿR@LM7P[y=A - D T C׊Vc}jxɠj)BН+e <*%2.Aܖnb;_G韬蓺VɕVv,]ߠwjڵm?cDtp4Gb@ +(•<j"y/R;θ:Q4rS%f6tw"zSv[NC*FJ""9XvZ4OZYե洣ԏ8^/\NMe4c ݲ -X dp> .hۈ~$t$kUeGʀ<K^ԷxQ$d B(^먭ŞBע}*M694O -XΛ -u,_w-/ߢZ {[;=qUZ*Qu஺/[etl mѾQZ7nv`܆EU vY};ڏ %^VDoɻ^taecDX)pÉꮅm3׭8mP d\K 6oѼЋaNt43ҌGS!xzFж^pݴt3zT9BET"C ":] È@:~$@":$:Jz^.8DׁCt|8D8D5lסD0};-^_  $C@"*_ 1CAB~-D *.D*`Pwa*&`P;P P6Cp~` U-.C:Eoha;px( .N8ZH] P"ҷ~~۱tk(M+%X8ag$ ٿQBT'z7[e 'bh3PW$PfOVtE9JF;z␙q@2\!"y҆Dp-T>jhGV,J;#[4oajRAq膂i_-􍚉ick4>ْ`86:~rѴcu{BXX,VR0HP]0OθN*E >eat <~*6Gۑ"ok ԯLpǖǻ*L{U8HJtRFmKr }#B;Ӷ > -|\oa\=y)t3!-m߈7p@y7E:B޵Ҕ=׼{R?G|?Va$T1Oب*Ed\5!soD 4@.3b$nJ{Us8^}]U~9)-ר>M!:4iR#e6ݏiXeiig#[%.hb7Ϡ=[/abrQkG0^3Y'{: Ė: virwb+VUcն$4M?ʼӽP짠<-1$[&mI1_VbFVcF2i@TkʸLŰA[#4S~^Ʀ5u<4O>NJE(پv -s.MCoELIənܶx`;(VLG+qv^BdIkQdX佗ak4Y|X;;iӍ4h|^3\ĺ&qeߩiQfn~:\l(neGQj~ZߊU!n+*Z3Vbk%Iu3f퀴$݇ϢzYTW$Kn_ $\VbE Xnfa}\":*K5*Z z*[^ŕb:Qw3H`P=)I`M]aU +3ryDgQMtz8EECẠ~ -E{,6A2 xA @ɹkAv*«;- dCBDn}'dхt_"1chFZ@*̓dZި\yřLb ----8 "d/]T̴t>PI(v u 4O8oub){4W`Chr)8E h`hK}LZ*hE4i[4gcj#;DKeuk#i[Ni9sR ,i@`-~k9N.mm]+[Tɹ@tWiߣķϩ{@:Kg~LAǴB_y22O8:}UaFE#b)#N( - ,0+\10WsZpyt{˵ jD$}4E} o~߼}wwᗿynovw_^߿~ֿgx۷o^_:7_m]iFϻ/ٛw n޽}qR0Y߾y|YڛgWqn^QпOw_޿./GEQ ɺQ1FF>gg|p4smYn^۬Ù߉|@c5eD!'1ծ뀑V+Fky>٩* ~ y#ogclJ)+J&H4 -f`E -ZT:УRS9@3%O,#/hF1CvU@uyPk%dK0^;:#x#vbΜ%gǤ_YR'$MhAܖFJÒI_G5@T0{7+\Τ7710 0@,n&V 0RUBn>GRW9E&?Y#Њ -lJFIVE [VW}-^pG|L8Mo F&ЃP=c0a`oM }8&\&)x,l'PX&8gc`RIM$h8t8*sȘV<$XzUAtDTTK{K(f@Z`@Nb}Koiٗ.F|ȀGԈ4тk ɳ`KZ9M5XXI3m"3'!Dk:$$'5A:ы;I0ђ#Hs9虙x3$f -TىVl K+nKv b@LjHE# -&4240=#%sM9I $Q,*WNXYI*J GFO%]POH(ߢTseѲWT<0ƬF&v -FB&?iRa}4J[1Ez.GLLĞ=ܻͻqt[ C5>N]d`Q8nvr-[2o5ȓo";fQlk=V1s!&imI*VpdJ11GA[.T¨heg4UȔ(1qK2g ] D`~K+U։SSZcIӚ\ܬu>B|59F$x8X3=,'(SOHc\|(6sZ3ʕqpǒ:Uh!v%,z8)Js [1I9(zdŅvj>i"eې4NRcy ;j@6WIF})Gbc-I * ̜!+(+oe#BC]Ѧ K*`R/}Wq 9W|.<(8"d譙PɕLU+Y/|d^+¬[I,$ےB L -W aҏe - -/AxC!A]U8VwC !oOsᓗCj%\B[.|&HpxQ3t+d`X)dVǩ -4+GNIeΥ3I bDI `xV4HE=sJeg %h>g8H\;nZne3ٙYL4tR9%\UصءIT|>T~>T*YIxYq;gn0+)["|=8:W4nDL_BQI>lC$;w$tд;#/%~ԥbœoG_0;hAr^9\o'“ -QWT &?H8 5`RoF9Hl Iev#Y B\j'ADUÒCTG|z!VXV."=X>w 8Fi$RJ[JW|_poe-v8GO| !v6*,W`-eIMu_HBRg_T]$:߆6P8|!= -IEmla˴Rvg *t-#dD| n1w81ge/$R`i qE8+e^2e[5ע>)~-~(i"^Yш*W#CJ '~L#?ϸKOՀhJu38IL/EQYɭ@E'R]3D"8Y!.=cVL7I/n[s .O99s}?B8AMQ2z݋RYajQ2u?Oύ}$)%M*CE[Br3:[!Q䗩rd(0-.J9*&rcDC +R1cţߑt0w7E%IKMj7؄^hqIj!HBT (M)j@%$q)dFF! TSF&7DaSGT'S= k -!#.ZUzZi)Vy ~TD܄tYx w &:GX~i+;$2d9&Ee'i! VAY)VQAEṑHTjŭQaW]2Ĭ HdffT7G2ydVgVZ*=IHyt 뢹rq=*7 ~&\jYP0ⓖE -j/P݉2L8zt?PsiFOIqK&W'5!4ĥj(YQ( -XվUPdlV@Dw<%ߜbF=EQ30YUJY9A}7 -jO*ijI[9p>;2U%Dc&ƴ0'NˎcyB *¥$ @k[ _K@(w˚fIP},PǼ(gn$:PIc㘊O0gT@t;)-",$U 1=ȗN/m3x6i^dMf., &b HdIЊQHğ5\j"Gs`c~\|P$=Dz8N4jsM$PJq^GY׈Ҡ4ptkO -} -%EEWg8չƩA]xnY!gŐIYW)¨{D*$q F'wc2xL=h+)WXQ/ Gf[4%XCJ損Pa^04yB -3ٙĊL$-8hRdwrzjsKlWMxI)v3d>J3CnYvܼhv 9z|`1m -`N_7y:x5uQO`̚($C#Q&fMΆuEXRQ`L{Y2gI@a!cհѧK-Kdr08OOQ[ptNL]6,J`I#ĕu=ADQL/@^I|ſGc!qʦL{>ggf0.JR]rOԸR]w 5{i, X]ź G+)%k\i"5(&)TJ' P^f/E Qԍ:5Kk5gPﲑFL55[!_DhXW] ă'$^υyt!D8 -YOlOEa)J2jBޘgA^($p$轕dpeՊłd ec9d_ -PHEw21hH#u;(DMv,ٓ-  ;IdgzW{8C@_al=0` eC<+ܟkR<0pg3"L2bgCh49r0GGu'x,Z0y2jLղ=;(fnzӸWl88@-aF`"Z̵I6$py30ܚ8oDXr82Ռ~Zq&nMDt 0ng=ܞq) l  {Fn^ -4Zc[,kl\bI+(5Sgw!9~qpT40c/[FbʞaRE7kg~D8ࡉ1`V aKb%J*c9dм(=L N7"Lbu9%6W`_ -2Ar+Pj#؂\͍i&YS~IY6e mCۨjk!atZ=x9[m5{z`fyv>C',|du2M JOjߑfI b಴^˥ǐ%0~VjA`Xl_nZCu$ (f_Ŝr}A'V e쳄mgB+ |%v1_#NjJم10tfW -'-L#!<؍IMMΪn0ǟ` cu - n-K#!!?FT 4ISiAKXZ^!TztAwJ6:7~:*GCb!1; 8[]>mS*|)겍@ .(W <:; :զ3 -h8qML(=\2)@xYȫ3{!n ؿ? -mD=ߞ+#QZ)N,czA-\7t6lN B{7qes P)|ïMCcK-8>4 34Lh=^Q HW,7)ӣ3?P8 -!FRd% Hi&v * r*Y6zELS "XG}v `ͿMA7R-̫{f4-?BEf/3~bpGhvcPS|]rߘd 2J9|J6 -m!Dr< K9o)ζ !~H㓃6-$ж|PN *>q<QRpQU?9/amK,?hca&ť6htKwO- G -U!|j l_p$7G:j'Rdq! U~~־ Em;np A*&<8'cQiTr[LmG@^J).bf_yXz|+ǞaV'%Sf'\<]1)fv=%LVe%wb$T*돐Cʉ뱉|^@r|,9Ff g*;7;v-n9,Vl(Z5420 2( νD͗8Q)XE4}<ZM7Jh]5y曲71RddYl=yEpw_L!;!N?P Gk6H~)H$(Ңa~=ЋorUO _7t~o }\vS)uIMaSw }҆߇ORޞ @1Bʰ'p PےZ(<A[lg ym <FumI ! ~mm.?pٓ~4袽H 22w΍kDSRꢺP)a戀~>$4B;>P_]{|WG(tϐf(r>Rǜgˎڵko -nSVfNMԴG<qҘ<35\Ҏ]5ۖt0ɰB;/1'YV4%AN$ErrR:u`+R_.5luG&Bv.̯iUsh_jә!f?PzE2U|\WFU:wX#= -ψ5vW=JEאd;3]助Q2ztN+_`}{"}4*T*QГB~_d)봳4Fړ{f) $yضi9ؿ`W@hMZ,[P B0@Z,a$|3Y4st(?~N!$s0 ͵ -ku{aR9'tv5e -K'S>!1=@tPWfl;Mu8Z]oTXó.?@i d30P^6@(AG`Se'.`+V]ˠ^B|,r:ҢcI]^_T6 tzU{9S5L-H AFce5GiH x7)G~9ї{>&0 -?:2˴elX,&6IGt&s۠qJr6:Z$Q6D0N{+X POM#; -g +2㧢Ѓ^Nω*0sʤ>G VrljIfmP%YU, yB h;+bPi,hvC%5x,=V_Kdt5첷f0L6OL:l[Wk"qƼre̋899C!)Bqpu»!^ҵ HAbB?Ww7 lgHH BW3|[(@"UZK!餍m=V̥Wx`p}{]$B~EjQf9!jBt<ފI \=)GW(fKQŲg.+sC?#Du>zoh.0C0i˔ē&鈀¤|3nJ `.GVk>nX9l -@0*'7v ?b՘ᒏZa6`P"̎垔]Ur'2Q ΨWDH B%}C[7c"))C_Fgl9B s^op"T6 XEoIdtĄ N"'L|[R=8;by!MRZLk&}9EoB1ws44&䩡4"t}n U (as!Sf6CeU3<  3ޖ,8aEv~@3ڑ*BDzL%gƟS mA92@E h1tzE-h 6= |^qD*bT`[BDˋM9W._ Cgz#_iYn!T{Z^ =xYahT<RQ{%"'M86P,vKnr-XJTi2sh2Gaɳj͇Gy6N -]l뫜%[U>C 8L޷8d8(s7QF (a6Ķ)2Li(X -G8x^+g+)}ǯxeQ@!= - X{3Y=aYLRIN+v\)3a +i, -MH^ƒd_w)sC~IPLzfW$dm&*n뫜ThN*m6RT)RŠc U>1n=PKG{ah̼r #moaN#Yl粄~ 术I_3gX Xus]|fAJ̗վ -8bʋң9rK֚FHU[O]Ad xހ?7L|' 8e%1`KQ,S -JytwFWr-ԼgT9ǁ77 MVDFO%a=WV8s{9DUpfB)R|N9E.6݊'5&%5*G*عJ pm}jHg/!I̼޸rqޕ9TVr(zD+="F$qG9mFpU,T·T3s1He>w#,fY\5Whߘ ˆYȽ ^(T=z sUU(K5ڟ 0F9RՆ ă蜉p{nakS-V'q˂Iɧ] -o}un`׵\YZHiQסostqRj{6aΟȡ1K mFvʫRBP%D-Ά9 xg - &\[SYg?Q] {I>xu/e|ڱOBɪXq*3o-D:ET]p?BtuG41E,4 Ż}d()#գKv66o -OX@(X8bZgw@C!'AQ{`w+9qVr6%}L -u I)#Eq&GUӒJCxzC>s4fYHx{DdǒԱ p\lwMgn0.PQV<S72=rj륯pTխQd:35k3;wPgRlx _lBGR׼:TbOqZno6A&F?FyP+Ytg3dk5^l4xSy`áͤWbw}5m:OX:If\i,o%6H2_57b;s7Z~C 8 Or;UMg,{2rfc:Sm?VXY0;ܾOX@u2M,6ª3e3xc 9YxAEIA`YZh`aQ+I?exBMZ0d 8ܾ !I{D>2@T:sYvpDvF>"'oz3ι0\2;zc@fC!!.(zUsF)WcќpCG`GRs^9(-J\s -7\%`7Ľ?#O MMV>żyH|+D3Sry,$GyЀ@@ceJ% =5tn+C'P|oUa$4{,g0 t _}8PHG PbΛS$@Ǣ*_A%$I)rmD1׎ʶFe^;^F‘|ȫ|7B<쉀.-- -AQe|(UXjno*I!CuԐEVAd\d[V%$M-V;C <36لdi$s̳8ȅ(ߧ5y- dnѽW "^m1$d,_zDD9Ƕ>4#XhD;uܦ$Ks9MGjToRn_Ds??O?ӿOO?OOO޾ͭ0'Ϸ`<១6`V1g ܪn17Zr7ŭa~|f{ 1S=V!osT^!<!# +I#*6(g}|щ|BȺb+[)>$;fqZvBIUsB0W0HQLslT Rg/?/zKԾ{#'A =fy_ݧo(?[.&ʧ}! 틐09 -a__$Z_ )Y=LٞG}okzJWGz)o#z>HBd|垦2oC?W-O˷<#Iʀ~6Ĵxm]<4O.)s6Wl[KG'xoξH}'Wݻp+Avos.~tqs6=| s~wB~Xryw6gj˓rmŎ"0ۂ} xf;'BOu>=hKPE:t{o/u1#D.;q2]N jendYG!,aq[m L1}QRP.C̲|>-tyahJG8402~~'woy| DQ?% 1QSnduh5OhZRQ9 -+t/ksG55x/FQ6J7lq((Π4~8/moct: .? /d`!_>+/Bnz1uNi[s!˰!Afy[ _~V۶ܱ[Y9nQ2L^ї!#W~G/ݮ5(}O%0"(߲oyX3Đ|C7!Dn4\m&ᔿ NJa !X?YEŒf]"j7Fp;,=/u[{7.OG<[|0.Sy۶jpaEnZOnS -mҝZ<'Y yF~FIDpFżvp<})?m-)e 4fZjOmIm]SlScⴎne$FV厍 +m 4uΧ6~gq!?k,ݚ=Mˈ}?\]T8o/ND۰H|7QA1n{j+5[+53 یfӃKmip7bt-P>Z~rf /jk8Lp҄\vYExI4$WmhNsy fDa _3J.oh B}l2{噿A=͝9E d;9smsFn,ݤDK:fIJ+@mD͉2y!кn2xOX[S2k\<#c[ݕ]Se6JGy)'Pd+S^~ɇ% e 6G.nrx.ܞȔK{Y!_a 2 -(=NZK*#69ON/scu/scdiS^V遼>H,a Q{HlpEJ|0#Ƕ1R.^8|)YnIak4OgA*&-yY v'v>T 7{Aέ.,̂r=*f *Qxdw'`V@ Gƿ棞Km{V.d9+`b|:g2=%Ǜ/BmVC.L{w>zYQ0wŲlg^\ko>"ۇVx:?Qo -c؋1.qZ{Ɓ6C&im\#f>o2/r/l6kqiO{[rp҇R}brE -1mu0~[2Xtч-TbW~ǖ;meq3d-yɟ?R.TIb%ݦgY+ Du}ڻ=5S$Vk&zʴ=]=htoGX,2_޳Oωg*QYJT=oWWGjCUw.iWb Neۻf4>we8&]8MH,W?e9?e5iQ쀄˛+ TtU~1{`_y1h>q žËm^="㵔7Wb/UCl4,U39\UiT 9\}w%Ai:yȵtܭy{k4Frկ*`r0nW,ίle6'=25R(1\ί=`˄bRsˣA|<`amg_֯4E=C/XzHgH hl^HG4ڗW|r􂅜-kg/X@8Yx?` B4Zxsei+?&@L`9윆)96G?{Bd1@x.;}ZVlWe9>+=1}8R@=9u 7Lvί/|< k畈A,l?6zw  Fv]t;AWͨн_ikwY -v| p5!)ެ_(cթm8iƑOW!)Zcێ"Ηe+ۈʲInåi6V$JyWQ\aTBv|WL2'(k#_{ۆӒNEMaU!<`zZÈtz}:ӻ{2N?RC&u^;: #Ӎz ~{֙s>3fX@t:TIo۾B:)"Mڛ i3=oR^ádlz2}4=m:m)sQt;.(^^x~ۈ9ú9)ܛkԏnP0IZGn'6Ed6NқRnflV|->Ufa$.kOe\@ -G/϶-۾/qa։/`<:NBd,#'xrv+R$SwMf[l{lX ;ŶyMeU32l->L2 @1I)ļTn'L#-@n'LP`敍y8acNB&pGSj+Lo|`N8 O4 %ˉd:e9r%I W٥W{s1. wTbP4^xذe9 -G78oKBt^'EmzIF&KXe!II"M4֥"Drm/Me3,ߍ7_3 -=U YD[".s^Z)& 4rS0(50x&6T0)o6JlL('F 0%׷ ﺝ0mcu\ }ZRUn(Pb! ezү oڑN+Ey(4+΀2yxb~E2I3.ٺfSs.3SuVggOOLɟ!cqeC\R)paĔVM o -$ϮgH( ?ζD:oLF@ΗñIb+d64M /ME+P2 RAV49_\2P6ΧexT7CgDɥsiѦ -z>&jkҷϥY}^A -lWKl3K)᩼yXAA a[Wvݎ]fTɱS2:*;-%+]ێvқdDZmsZN$I= &;e2e0˪ƒm7~HS6-&"֛"wɷsmE=MD+ vƞ&މtܖ\GҍJ.ȔN\-"ZULe9%އnC9j=!QZBf%_ml!,Ů o1zಷ}!oٮG+jl]^KxZba9mHx?OBJgߙ ڮ-6< {yeslvO`6"<59K63+.!I͏O#B/%4o#B#FfWq[! B0A)4 $$ +tֈ*H̨k 6/] v E(YUm@{θ/ ׷Dc/6HugSj&-|y ^aDRf_NO -6S/R¿cT ?0hpj4 M͂//>3ı,Wr[y42i!:޼ec/h%ؠoAb7\~}Y2Pg-N:IG!U*@ܯUFg N9nS@!n h?[(voK@Rbv{J6Vy4N/G@j,#@@L:6}s8ZFrT0EX`Cɂ2{8;# Pn'@`բ\ޜgo'@ I/Hhu9N`u(*< r: ]oF|Vumu'o6J5g/@Q9yWmHr rU9&4QQx31&01ڪ Г) -9<t4)}gJ A+y6q2^~@E6䜁J})"ڰzO?(Z[h05WEWRX|xsӇĐv@vP@;|y\S9( -v3}hw!\y;y:RpiwG-FQ$>AiO; f|8@iy -6QdDZ$]w']ZsIߑ{Q j - ݟ&xv~ q(/jESSyQrlx}7.?."R7Z}EsD}iI҆@`?w7ට׋93t{GMmO?qJ=nj㯚E7Zy_(۝gYj۪f+}T $J,cۏoޛ MpWX) oqs]p| -TR͎%^=M]oӷjo U.7e樟ooTŀYl_pܺ6o&n@nﶞBr[O6аsCZicWm6~UDF6[=BhZ=՚^vTXH-g٦ZG.-u3 ,Y=TDht7|V%73sdKY[|7+Wnqq --j~0>f{ːYe=O2U5[ɲu͒l˹n'XdU1a2C/Tebʝ0@N|l+14(=djzui%,`}v{ ߬EEeU']:#ܼBb4꛻WE( k#"#Miu)vBC/.F/O׹eP()#pcqAW͸X.@)%C!&.,4@X@֗-BZs`xi/-c%gemLlY2-i\Klr/*y^f(zx]lvbwU,Р8C+xcj∐J۪|UQe"}}2@x.8D3m? :{[yOd!0"b{MEvh[L)W7g)f!5Mּ}G4 -uh&5ET!KT,~_O\.m51ށAhcmtiH/z;;QKcۆN^}@Q/x7 vX˵)̄GT&Md/6-O'}l0n+jmT5(l>=mԗrA z8 Qu58IuP]hI5_&H k28tFw[ roH*1;Lo~Gcin#SFdRK{352)_2Ůuys.b۱h@2*%џhE R yTߨ>i:@n!͆d 7!Tr+ng! /C!1~&okj>]Igz8 Yѐ,H0bϗO?Yv?Âm|4Rz=}ˋGOKZ%6={d,_~(K Ӟ/ӕCǻʷ$H/?x4r4inx켎N[r_%,O|,Lto wNQN 4,s/xѯq((۝S6))#oޗ Zownb@6WgDyXp=/7e[S`ӭ8c!LjJ -7MA'K( ۉ3_1 h_2) :Y%,iC:Oq7D/G]_Nb=@ $k5sכ"D"ߌ\EK /&46v'[xfc䑊chsi2JLI:5e99K)`9ˁS vOj3yӰ1Cx\DB( <ޗ`zb^<ʄc*fZGѹ@P0!Ӛ+5+ v:{_P;>whK90%ud`]թO.ѩa"ۊJ - LHxNb0,sjnWEaV֜9)DtM(6gQ{*hK4{U6^DI-1JصME˯sN -V4C}H~s#W0h.sZD&?B:?UXiZ m0!NBwcPv@ -TbOd[3FՊ+=DRי+u3Ϝ[)w@3x8JhL-hZG(uA) x%f }ZQKѹa7 Cjb.uv4v[XVb:R X| 8Eu˅WS͜7Mm:B(:7RfYYWG:gxsc¢Y@vM}*0^Fo -# ܄zMъYV!:{ac=ؗ5}y9eS,v"88@flf"jQ̕ED,Cp0dZђmM} K\qs:A"LeY5̶ׁs8ey+¥oq$Ґl;AdMwU~j+l.фeuVQ1ΐ–*1Ѓ,: 5$M̼ayhSZA)ex$@V9PGCc.huzE^8P0L8Or7A|ʯ͸ (/z7=m+J8U Ĥ@:|=20UNXD;e)+SkPg/ΡUE³Nyuy(dqI#z 7Efb'ZKN}K\j*h@>7,-k -.E[Afx^nFVQIh86|ƟcyϲR6icTPqtaijI#zkVRsa=?Cʤ%8L\{47T6^qj}h"JM`V%qmN=$@F!DFK%hT?$x!P1W^Sy/t b -BZB6 36tB(qj_JϑG/B'e~5~+եL%NglfHtng[̙6h>8ה)3a'ё0#z!'Luѷ)';f[4 uTʪL -&E"1@Ys'8˹B䮴jk_]%W_6J IRZ,u B&9 V5Wp9K[[[ZFVs ۩]M{mP?4|yI"lCN~|Jo}Q|S}hb(?y,1h]aŋ M!wH:PJq'!He1vumel~MO!=!?ΑqKGK -3 zOPBpc'Oj%`E6!h2&Q&ufv"b;ח]*\HhEi6v;z=[z-9#v-" -%MGZ`v;)T <k"AZT)}R5%gD (T!!=c_=fdpWBFD-݅4R0;J3J``\IV3km{e?Xu6WĄi TLQ-?SH_ˢDmEW2&,ނA.clӌӹ]p=Ѱ1Bz&AΑ ]!2yg];\Hy> s/ ø%46©hbj2.޾9l5-tZ7g:(q*m:VBYw}.C,'3D u6kSaT<(Y~2RC&ZI!s* 3G%Vi dԔC_5=z?ap*:=b_ PH& c:NmɽT`$ǖ *3maWBѰ &f)W̯F nyh8$Ա!k)la iFMkG#$H4VpTL{׼RRlPA ӎe8ڭ;ؼĚ'5ch5WL^{̢3|XD: X {bY-[|ܱm9.3 }cFԂ5$[bO8J 2챾 6DZsrQFNa)KfeN>SLLV6^9Q4"9Kf$-ܑkxaWk,uv e%Ѳm#9sY229dd< 5va͹#d@;זfe4{ uWIk_!}E)oV3!ORfFx \S) S/"yORRcjY;0*fDB (&6:%!G~/۹ 2g~%LIۿ ,1 Y^.Y:A+{/$HvX>ƕ9^AR,(Yβ! =5*݈vrɫ5i1nmeц̝Cns  ➿|ϯVZ`2~ o^LjQl"  Z+PG;6{F{ߔy=n,ߔs(_,uك,% 2as+P54$1Odkc#iIwntLىTsnCr!+>-,]r@!)\ -^C19+lIoy -4FDbe =;~[pp5WaBqrd (0]e/~1vm^젅@'U G8fSpud< tC- xm~l\E_#@ 8tSFaD/3vqaȸBp%36 J(m瘘q̰G6%|o60RkFJX,wfڢ /x N#;ٗ>qz<=J Kݒ9ߏoiA8CMuW.^{Q/YMsޅl~Y/˒# $Jit65Sև M(F:-1z9 EQ(cH&4Cl~\; -bP~lhґ Z#;[ ͊pm/,:%f'4h5H͋G,NC:l؇(5ۙFh -Bj hP3N -dM#/P\p7DHq F +4| gJyk52=c -{n=qXF$_q[V(Ʀ@e}CUz =PitSAfɴ ]MzT4=ۻvMM|)i`XXIc9!r;Iٱ\RfWt*_Q#-`m` 6ъ]W?.HNF:='K,M{=1b|FiLzhRSئxm`uyΦ~#d8֍yhi)Z2(.kTˏ/IcXU]pz:uiP/\b8WerP\bs:L!;ju?֒Qb2~),h|K:-IoX6Vu @)YhjXX, e8 8;Nbߊj"^pCH4\案,Z%:/dXu몐C.:G2J/BlQ GhXbAE@ OD\?`FsK_` ̀<"v=pzQKᕗ/BI4Vt6qL5?P`[SwҔ~PͰ-]%xLQ.-]UBҘq*SE I& -q IqԍzCPt+eBSABOz!yI/D"]R)pQ;Y AFRW  @mHGcc5RH|hv }9KIN0nA-دVH K },x8y)\&I]3\.g& wDD`;aiL; -mR!gotF:U DZؼ:;˾#[_sgb@.L?DCbK43o/]jU&ӒHzﲂ>?=Y+#dK/rx+ ,^k=_,JP4g%hS_%HMxU ഗgfIPIЃJ8\x̀?mq?̺۫x[ul7:2߯T -Bn/\qgXSpҁ ܄[ 4{|?M0"q^F 抌8{^%f'}QiXom=0缊ױ,o=b%zr̀ܶC-S "=ܨ5yhaJ/ҕEbd>A5{tC.rudp 뛨:Q.]ak]ݤU^Wh/Iy`^#(8yr-X06񎖾[`YK"LW*X7k/-%A(` -3@ pa3a.@${.0N {W6Q:4{B8Gyd&.Z!hEdE "<-5)D%jAL/rU ^b9" ߏb`T)o,/r:uDƽvp`0-9 elO%=Q)@DQ9o]2ywz)1q'9I`CX#C45JFklm]N$:'^W ]G#&p6zJl*#??Jwp! wHA ¹sZ'B8x}=HkrDԋpo) '6'Q*J@'r2X - -g{BHkKi&-Ez\ %@O9GӤ}5>>}C=qu0ˁ_wwr6EqTjD;=:7nmai_uVi9Ic|ǜH0QF~T&BqW丹KCCȏ1ѵ6<%;>6Sb#B pf:IΜp}A⇸"#X=9/9Rt D19M=Q\+,w(M@mr՜דșRS$\;I-zkpTqiVZ?|+{ ڱv"@`ZPߤ"[yAt8#H¤HU\{}G\R&aaVYRBNN_jо5KKUo?k) 3a;֩6{"ͽZեo9X}N+)$gϺ:(>ǽWJ odQDe-!ʽ(g&8mW@2zIǸkQemG* {=E8k\a9L J 4JZCf_xxHbפpQl8Kqe@}eO&lc= -fsii1v)\ 'ÿ^KSD'g,pD%CP A:"DRo`g{QuJzj*9]05ԃ i.2 "|\ 6+s=<sT3cM4rHc+Ňnؕ;jdRZnn B[a$ʪTwJToo~cZ;|Qfp998+Cru/F|"P 4lt+ 5Ab|Mj7RRhg!/7|]dr\AQSjo<?mޚXGdUd~N~]rpQb?¬M5ucIw)7f^1yga\oq {Ѵi{c _"[Kw=PC6* -x=1F_CvcRhd-gT'jm$+9/SH_W9ToĂVB -2 /`0@xTJxgTH/ϠLC4}V0 $$kX0\,ZXWk %I!@LwEHP\η>LK٫uv5x|B05Nޏ[4`BDIL9^0t -?dd4d|n:DtN߱q-GZN)HTk) W04JU* fZ 﫻Mֱ8sVu5O5-Dzᣌ(vP9)T6Tx.'/-/w$`ok՝Pv0(0^!uƂi -zWi+&## mQ2  S8=b8 m؅3@r^ŝ]=䒣$:Q׾@^014~]ԃDi l]%'U`0I,#;uG=r%w|0[t@'G*w63OuZpʁhQCW -> ԡ3˭l7I|m -JT ) )F^dEIRU=b9_EJ#C1#EzөqB(Hs9ԢMΔ(%7nzF+qbr6b؂Mλ0 !܊@ɍT:/M -ra5qQ9!J)7ԫHwSe{\g* gIᠺK`f(MH~ KT* e~ ı FӔ@ !Bg^& -ux5x EZ xY#4!A,# |"u5n#7 JAU}r)^[xbU=13e -OJCDA BG8Z;0J(N5䝖9b-'n;Jj׫# ^Fw"ʾ^%SsX)HW=lTS[D9Pt2 D2Ca"јiK&Fg`])qLKVa!%u l){$9.`1b sߥPx!(=ٗ#*`זej1&0Շ"PA:ɘOjgTϱ%BSEےװc#PΌSdlb|?eV&NƣC̽S#(F7YyQ3be={(0JmQ9+_\,UkZptw2l#NNXPcՄ72.Nw;[;)?+aQKHAWO=.fJ Xcy]2)/e !$2֯$gX4P/28 +\Ӎ{U0]n8Sݏ *IT'bS,C#ߢ =lm5t=RO5 [=8xK n\F[Z`(o%o+=|q,Ӏ7~1n:Y 3ڵ;@(݋~U0Fc.6@1UXfa9a(@>p w^oD&bp2ɋ.qc&ˢkZ)INV#MuU7(XÅ#NтH~D{Rr2֠ghjկd(qP 1@N1:m6U[2=닿c)s!ș(rw -4yeMrj5@qhcP/Pj(!C.SI*\d 4oe.PسW,݆ɘ%V=epnWg"2+p 9j\eD!i5ӞVP#z JT -5vE4Lft{V,cnwE* 6l9#SSh9$VXC\b) ۪`Y[/Yu 9YA\šnb'jI h/i/6D@iJC@%Z=VĉæK?{CWC` W\=(G%B[JI;9UzٰH҈[bInh`Mw;ȬyUK͓ ۸p^-.+D|JINw !жSʝuZ\1@yc!M2 -xطh^wCe [= -ɏW)YMDRyD$Ujsn$y.f߱Y/f&`xq-dӫ|sM`\ u +P{''"@l'D m -L"ќ mاEm=NFI -w(!Mj챙}ǜc-~SX@ܯrN9f;ЃPF/[vHlzmj܉`v<´B'Tdz s{b/K~'qwsT!]OL~2#eȓ v"Kb^LOF(wF H-Tԋ<$F؂ĽĴ\Pn)e}S jX2|()F'`2B{ /&O*k\6cͻ-Rpa+6K*lIQ\"2BSV^bi(aϳ% -M\V)!d!B'h|ԍ(B -,MQִ*R4!M,K x !rH<$@ H Li"S4zc9);{ړ]\0?])%{}ZIa9w@j 잤"v* Xl[B}!֦^uGT77hœޙ%ǜ2n QW7)7Ȏ,5mpa\~EOxzOM1gfFL]b$d`ثN[|[3LA5Pkcdh SDʂ6L]PGp rZu"9@^M A!Wuu1EPI7" Hc`Tv2X6&c¸ė-5Gf{%S=AUrKҰ5X-vlrRu*zH -e_iZ0{ -;kAje_) 'E\3P3q7BzJo4K[k)hk3$':oIQ(!r\!:!L[1^x\1@ -M!"(cT|˱H {#K=?Cz~I%Yy„f+K<lyC< Z쁎M+oLjFSWU~!+].TrZaՇJ79+A-Rryf4?!R)S:c'"fllxmBb!بÉe4("rRt.D1rG}$?_uYgKH:sSIpfBV҄5[e!d^L1`6^J̛,n->.=X=e10&Rg>b--`|;`>40VIA(KHݣ+1iڥWWy -+Ib˩;B}Jh;O/ i~eRBdrR8cHA=X$4Y\L>("D7y+.mT>,Hނ<$#ju{"eɷp`a%׌ƜAOD7=X&޹n$O.qb{؃D iRoRl6Cկ @YU.`Qd6`{e""R>AiWsXAm2^D!9jrZ@&QHx|`7%_Jx(z)~,ȭ>vw5{Vv|s8`Q4h[lO[A( dO_ijFA^΃+2݋Mu5]D endstream endobj 26 0 obj <>stream -hFux(cŻ,ыqyh -.GQSC -ѫ!dPPfD2ӍWUЈw[H8K{hzH#0'V%s_DnQ|?$D *#U..yr(mȺּqmvU~WMfd)~u[%ggKe$pKN!p1Wﲩu͎>?ţD^T_ߟߨ>_#Mn1أ#ܤ#0sJ\ -Tct9\\"$H"=lRW| 9 iŃS] lB*.=R|>U=h \<pᤗLxt!\>GF$ H/$b51h3Ov~?`Co9K7-,K"j5w])r#[IC ~DS[Esx#U1tuEg:ԁsEzv` -d] n*T_:Je)CI[uŅeZ5KZ"@R]ZVw{NhA:tW?(r9$ӂ7y^MӖKboMh -v9ٛ+?M)dD_uO瑒90{q8v_m#DdӤf"qdTPv\ud4|*b/K=O769X7+!m)M0%)$Ԭ~xuٮPOz ~c"8h; Y%l&f;G6lI{~hb iyCuKۖ"`{u c:6|*PK!tʧ !2O&tUwa7Cߣ n &QUa*3w$W߯XlmA -i;=&GaށRµ%܊%ރfGjǯ5} Xa@vuS +ᐰ1PWy-_C}7t`TdҘn6#-#*2^Z=qlȿi3砺L!OfA -͍M-8ŐKƍ)I;JΑ}\.=]տE~+4PKUWmpqKAJD'Cu86 ^J'Vq}bH0RVXGJy -{L-,;B/;@=W3^RMKy1Mצ* ۏgGoT/9lFNR E]RWWЯ-S@}Џ(;b!g)YéԜ)t&5?o7ﳐ̘jL%I-,űw,>ʗF+@s-I3iLŎ܂Xmܾpąo<(ry>v>gqwv2P:z,)v\Cn%n&܏A@Ra;6qSu Bo6 _ʤp5}NnpGaYInvA@Φ9_hh7!Hmz($*PP|) MޏHua]o/NS@BmdvK/E) -yu^඗覙 .p0ir0dUzQ`(lY; 1x#p63Y4]0ʤnId̃+23ҟ)*e~j6꯳k)?s3Ik2ih{5~'Z;F#Ě:/ɭ2ЇkAɟ!g'wG[/d[^ 23[-%} ȗHKݰ_ -~)>Ct<8 jΰ(H*ֶv͎vP$G.d~UWKc -|? -oHDiQ`)Y_awxupKTff>">B5 8Ԝ.ݗFcQ>`EjR; Bg1|RmCe!Vodz=$.a)lm#(RRA7s^vۄi/9X -)׋ͬ`MA5}8=  [/4`C* )_K)ч4^a@7Wvܱg\J䙺@a=[m 6Z|pE0X,k2YԞ&Gi\?.;|vX[x= -E1_ :Pv:k;W/$M;~ZF"E K嶄5;vaCTn5WXA] m$hv 95B@\"pm{ldEA{0-%C6GwĞՕ؏:\-Q!P~`x6<QRaD6AηgZ$x0Ym]>p :X2X+y5*Uռb9IcR B&'QtbTj{PQi0 CH]I+Ps= ˫!VYby˓{\7Ce )!9P?ڟ&ܾv4&i.&'aY5*,o2cBC,ޛ_ғ<Òk\хԀّV H6+Lb{*S}G-I"SjztSM2yR=Xa?cbg#/l$ow\crb!I5޽%xL^e(DBJsUGoF2ILn]譒6%'݃\}q$UCυ(RYdKMU"!s|PfBC+UQJi#_7T 'ЎIV :Rm^\ujiKCV+|q$e4aaAY#;pipR=Hh3z-;=YZ+I YK(uڊHd¼١o"&aQ ڑk!@ҟMCT "qpgG.7G2Zz`o ;OK9Apj?19-iP={ 0+|HQwP9o3y`Ҏ wz *xWS;]w(ˠr.zڦ ^+&} OP%q9@IPE,Nרw4硦I1S@v`BbRn%Z@0'эHCaf`XƢ_D*?x{(j~27 121X$3 O*zQBHrPL(Ӑm p |Zo2i(-qgd`=r$&E,ɉQS{%P[(#N7l F!գ bbJ!F携ZW* VŒ#k80\H!Q%8L4ນ\ 47K+헓:P%_ZPLg)YVݸ}o#YN@O5U"w(9=RAdO5zF1n>I-,T!3B76cp<@!5Y{/H4/z}I&b鹯jI-Q2*ôH8KJ}B8= k, Yl"ZCa2Lw[r~"!2nfj)-5HcJg]m{u&恹;Q,fڹ`-~gb+/C&L]r~‘ʕP M܄eSU=CkC{oT\J%U(d!aIn;W/9(W{Kh;x!# #\W(C0Kþ#sYSp@hc"/P۪8.;)&W\CE(l%9 -*sEKV3Q).I/i -|ĥdlVFf6\n=|Gy'45%tX;wnb$(.&:n *r1U"K%!H.^*ݳac?]1N#29NҼ‰"/!ds4unxr A T:@mAv"{.tkuMUP(2~oq؂ 5J:jVgH4xDlSĞQ۸Uسt>%(SIJÍ6O\(&BZaE @3RzĮ")CbJS6ݸ?`*jJJ#䀷 -̻t}7݁ᑯ-xɺsUlw  HX a?=7z-#jnFC,0 -8A`b0G`K/R1)w\Sy>K -bwd~k[ Pq VyAt1$DHR}Pcn⒟j@[3w,)S3-۪ʑ!?qNh@=zP̶ |OgVKa/G-D^ 9~8?ؕO x*L+(&q5X'wU_R:(G(5NJ9Yt@?~il?ǵj`I2XZ>YLoaKPpo&EK{\$鶷Q;<4!^OEF?_)0ՒK"XU~HsEgnpv! Gq nlWµK`n ܠ|Yg[J+0JkP 5{lDM_%]7<صʡ8R߃b #+h\Sbc}ΕN7،Y%n({s.Bݷ2L@}v0 `J%$1fACfP1 Qo$ofk9KR ̠ab'i>eiq>l04k7GȎ~pwh/_.,{sʛ4Y 鰽~vAq~9'`wn%U E%$#mw!q>V вO8WLE (\,?!KdƁ/Y+A OZͨ~([ -XͣJF ePlIHC()PV>}ciuT -ʉ.1&t 'Lɴ-Ety/$Y@`k?R=T~( ,TmѪʯF{Ԭk&5T~+*$upqX+|G-CY"G3w/_d!/PɄr9m'2G -B)1aj^($ !@*%(OGWFL[RM-;=J TD2zh,|y -/P+Hx!v`^cAЀ%P[#Ģg 36:S9Fpa7Xo ' 0ako%m<\C=;ƃ6krƬ 񊬪0C301N<-}|<(RR+~!Ȇ렰"o:5K<$K:0FEBAA}^xÍW|@L`3RÓBG8_v-> ^ H4=3(S$D롪fHI6Nľ_^OOWDvA2ܑlх9!A'*UGqF^{C!b{ϩnRlCI9$րfy3Bl!E)M;@iiD$`S0vr-I@ek2lBs4^%xUBRd,VjP·\$N.K-2Oe-VB (XHBH9_ag8_[5M՞ȤP6ܞe-!xjI m^uMN52Q΋%NB9$>㼬+*jCN/K^IW g( - lO͌(e \l<}K.&e' wqx{`uJh|5ǶTK?6/0]KMrv$1` 7idx -Ri/}R fA噀.sDX0(&uґwMhb,F̻yF s4A:ɥ 1GJC6{ 2ʜ!Ez6Kꏑ:v -͚d*fk/p#0m *GdjF*%({Q4 8Ƶ[k,v* t٦â̧ol^` CBTc|Wā:`ԉ^s0fߗFj(0j!дD420PR&YxOC@jIz4@"i;Iդg-7R(HrI逮 N QOSULlj<%!t_@N$@('7d_;=f&T*%Ca%E%SXE匨LSvn_ImkC?.m. ~X?`Z\O -^t|v%ugK*k8#s tt] -Rl䰊 _CV8Cᤴ:h%qu?__0CQZ$MwV?љւE{їn@ޥyb݈.rDPc3mwܢfT>A)dk0/G{29Wܷ&n= -ZhT"ہLOszqe_Y/  *ɰ-GPQwݾ;9HĪ7 X9DjՔ훹2̓¨I\sl<.Y,w"{ΗS -ؿ\/r] XROjd:4*pxu}TQϢ@א\-G^ bCXBô8L) ;(,\5вw!`o^i,u(eB +ZԂ'"Ԙd n g$̂ȄDP;17-RJYz$1D,~%Fi⑭A]3ܡ?7T/_1$"98*⮴_r{_WK88D@J`+x?gWs\߯YPbCYv~l'mˢ$D]JCR\RN -xmsG"IC%v7-Edဧd$ ʟO|eH%(g9";A}$f;dogDo"{ߐ$ -T Lq?DG६׶(#%}î_UF=jo?K#kELlL@>T@# -1{Ű{I|ҫ1͆Uң J0jΣF!Tk|(Š$RLg͈7 zS=*$u*@ %էiu䡁]ڋ I6(SY)b;w<|3pHW cU0&9~H2FLTLFTZ)c'M{HHm=+u!? $J$"f&G 6>[w? !Q\QE')TX^7pHz;@w[$4Jr{ŷ6wrMk-HE'- )Z+)nRf^Zj5ɠ%bFr:L :if=-Xޛ$NQ`?uu?j֎ $Z,42t`M@zz}U]BeNG1vmVU=ˣoI,4?F"uW?5@ ],='8zcbN'}}YM!u]8SVqF\Z21DQ a^IP; WfX9SʋI1ō`ygǐ=U|{x;gx9铧- -)A&Uv4;+I2\a9N|q I@de]y]VnWI ' ^쾉gtfDZ)Vċ!Lwzln -[gѐT QAf@I(E_+,9=q3K΀87zU_ C1א%9i ʁX+/2뼔'cO]xŘM,*)tqV%&0ij 2qd4?3Ե kPޯL}~34+S`v -ȝYRl4w4% ySG%uz+߁҆W'1 q>n\Qaر'E8◙,G2=x5C"`_qeD~IK"fm)'5}n'5kl#e^tˏJ0ebŧG<.F8(]!3u<yF Ug.FN܆]16RV -@V빃.+H({T;f$[Pi/]&$U׮eU-#c J'v@T2XPviQ4z &=B} ʋ Q﷟M-Il?$A޹.C{T>85^F//Vvl&Tgs&E6 -!]Ԁ!Kn1%>?^.O4ChcƙtOaYÁɦ6et,S.%Bd剅ap~$,ݻdf"aGIP"w-< -Vk/MKBN@"s:T}ܕLS-faG~IGl8д$yi~~r -ݓVN6c{om~اzXf?dT&/R.b_%~5EZ`FSUbp;'q(uɲ,~TTo2MA-$DrX{,\Ҝ8-9}%lCfڙ%}z8:OboG$2]ΐd'A>Ȅjp`aT8b[6|`َMA;> -ET^W?юs]g'P~0qoaGꦀlRA3`IV|Fp,N<J!HshNDjͰRWj:cJEyHt$Ԋ"?ïMe ~3Lb PM{ -E]<5R幈ZSQՖwJXsИe|[cI(X tJ5'CAfPP [l, 3a,C Q| A9'%6-C0lM!YQ}QM7vU׳xcX}u9IbKNtոTW+A}áA2TA* ,Pbf`w/r֪̒\/B++ꥃځ$,[rR#xlIk)gmӞ |6 O#~ 8CGf&X*i-Hb4{fLM3H|zij3ّ1CaT-'DnǛ|c"][+?2N`HOt ץ3ќʆk4%MB*7֛} Jpy'jyn$sf*JQ a!jz1n r@3 &H q@$@3ULiS3ڗ/um_K-ݟXi{ -]+nah2A8t$[en2 BK%.kg(PIӁn*_xEұ W )?uɶCZdJu|^(8fQwGO%bR 2Qe'4)Chv,ě/C( C mbp @"~J%EMHctKTxhxN`dC -Hpٽ QW`Kع~¥ƾj!=fZDKOLY88ntqZ9m-knj^%L_vH) Rv$~_ʗ ׀ޖT@KL_ho/<%XE> f埭[M)b]LnC@ 7"AdžbqXjv|['6i1"qvQK+2˦ -BV -40[ J# 0 .t:ܫ)s Jt1&}ZuIfD*K)ME4>b&/*'☣lX
s.5pu;Y1R[y\{dV\0- 1:MPma~{Mo Y_`V$mɇp -+f]uײc ,"tPYM]Yʬn@y_:ph{]1J`cC!ֽ+J5ʒ{62@+`F;x@ۙ+6"!U@eԲdw -.;m^K:VJev|8{iA=_̣jxD,j)[KeIG67L-L3nޑBr`&SRW>`bnb'ThZq:cJ])(-PtIqn_Ot:DX)pMLF83iVj(oj}"qBL"6(+)V.aDW(h^TIB JډO~bcng#ܔrhT~]j|A({ڭ #Nb*.w2ETͨy<ɳnNbĊNmOVj=w֞9KUu+hjʅ`J⬆yUR@E$V<|nV/•Ϋ\Xu2[O8=@uiɆ}Vjp'RCRFXkﰠ"m%IQ-W("Qa -=G}:bEWa"n#2+5x!Kxfs?r9#:ň*Bʴ~0Ywlazx d<f[k9YV!F 8nq:@BogwT/ovsAK ʣ*mH& T>/FR|0bH{:Wߵry9S"s%/:()uK*dtg-W&=W`}̭r ܦҥ| 8J( ̏$R -$u,u^]yB}jB(b!Tn[} xBz5zX.ϸ* -CNőudY~"A܎[]iGدs:Dwأ &[eͶ@|=>h<;8^W0]ቼ9h(Qm()9 BEut&OCrDp_kF&L/I6S4eDz-+nK;o[)N3)o?Oo7o?7?~W~?wo_ =~'w?t}O_?n7ww?[j~|lS~ x͇WW귿Sο?ovʻ̑#㷿m_\oRq+/u{ko_W_6Ibq"~{!yYb 퇓L9o_$pw rIXZEYu%d;D!KG Zį Qû{^_}! ;=SE#*j|э7q|ظ||~su`HsXq*dyr#d ~ -wGJ?uBqʛ~pRqNtsx`3wD9~FWϗ|X%H )8,*eZ=/'` ^ |WC+C4ƙ 58A8-?=eh pv{jJ4x~'􍞍D<_/b9وN4ӈ[ st:<&3>Ύ+ GZ< -2ʖ?O+h\IiΩ|D4q ĝz En. Fˢq8X7y_ Qx^xb+U9+=#-uij^ -NPO5Ϗw'xyibYqiTUNSW [6^> k|ncw^A=;~f<8Wk'1{`jqĸrN %Kgh1C[ _ @U>0٫zvdU/55daL373Zk;ss;I?}ICco|>b 9<+9 {QU+LDi1֕8o~ξ1()w^A`ިE^k\"ƙOA I:o=!'1 5rN< -HٯUO*P\A1&12\3H=X8i+x)ΉFE[#*M& '8sŁ3h( _7`+3L76s>s\?)zXqa ~W]^}bm6_m{%q8CWϛw(;_w!T4kFعz7tsyi U||^_1c8|e79yL%zs#'qS4"2v(_ -ԝ<'ڮ◰;F8"xHD#hl2cѲohDcobfSkzij'#P_~- j)Y{Uc_?.׻95]9pz/aM4fј>l'YZNWl n9TpǷ &kjmB9mnbcl7;UF9|PhuA#)ϙJ?gs=ƹvD}'%WnV$[h9.7K73iO+4W+rNGNHŧ%z\ֽ\g >}DJ!oaBܾeEh^ y)϶ʟKcȠdg}|%ϔZ(4휠?DdPN^s;>2N$˒["ª -p=k}+ɸ|\@zyظgA)9a;)ۻ:2=zyDL]2,vO(R;A:Cɷ6X4^H_2|1;J j>ypc.o$lONPy邲3crx3{Oc ly/WS (@dڟ/R&V5VFDU+bIgX=x_9Zjفb~LNPW_$lFd/ +˵w<Ӳxv}ˍٜ#yVmą P"n#{yEL5q~yYk[Wl'Z։FLJAԒnrc;  LJ}/ o :^FMgcN4QŤu Z8YryLEWR6"쨈O{u裝v{sWzYcjGXep4SF;\Nl (l|V;.kn,R\h#$(|HgW}G'oyZ)g:kW{[FI)_"N ]9 u-f|6.,ˏ##kq.9/,CS̬S`؞}0jMAK)Ol<)೾ )O}`F㨙.( Y#f>1`?_؅Iگ^Y9аcOQߥl_!+O سc|2Tc\]40Yj%'w;6~4`AP':nZ7's4 oF{k kE.@Qys^9zoyhfSoQŬ}YKɾbFEۈ)NrSvO3hgnH0/|x9@try9g/4ڍ̜j PU8H3 -"w*iiVk.0x^iyȭ-?+<IW ֿ udew]q[/йfc;1o}v6%>69dN(<V{'n7{?gsԅGE˷<$fGg3}Ŵg@9끷wqs20%{w ެZb09cV-.(?tVghgKT!l}i9`PxR,w*;:+#y fF+y#X NNsͧp+Ϲ9|Z<\$UN4A -E!jYES̩2} |FvL4=b :Hrn<]½+u/ZS0YFZDQ$i8MMjq_Q}QO3S O ߍQB30\R n(23 W9\!ٕ-͑''Fz]}9^տSA6'"#|2EĉM Oj'gE*S/AK$ s-u]gA3O\IchHzWc@X,, u?_1\jL +I뙪{}q%ڕtu2 d}ؕ29ۙ E=\{բmmfX=AC_jG^W~r'/F=MOLyb1Ȭ+skM*CaO姜p9}(g5WR͚+Y65nq"QQ x61Ո|JN=*U3{]~K&֥ḽTc04#y۽1+* f>r\×8՜F>RǞC^{ԨOw{yaRȑ?@9hZjvqW't[w#ժ'rC7.~k;f6g:A ΓBVoD=d:6sF98"%D.p?D\ +]Ɖ#e[3.C|I:Ʈ+{*h泍ƽQ8th;״6E|PYON䏽ڕC~y+ZY]m4WΗyϼvzwkƔ됕ƉhȀI 2xGh\'i9UvrOj^4]zw8uZyH}hHa_sU6aq'+\'x?^l_'ONWlɵ5#XrB9ih~h;Nq1HcɞP^+dv6$m}ngD~W"l/0'Oczʅ|$5?4+! :)I,#",s=n\+aW,ܫHcgOeZ>=PD5j)ܠOԺѳא~=TCKz -}y·8A + P܋EΠo=_ש-@ -ٶg˙ IS,9U;(OkYN)2w,lA9ܘ;1NPj\s'fAcs̼HuhM:9oL)A=._g}sIm3K$lLSBi<2=|&~>|6TI_˾9U3D3xwBA8T!c 8HrYgx'ȒCwhl9fkJn+?8T!dt G'xmLJ߄un5+$I.! JN|sإ;'י<",d*?nwוdW䥮@IY=SW*~V3U*V ^~™2E. '*]#V+N0F6gV&g Ȱd]\tN%#2ΚJ~I -/bH렽ƻhH o 'ȓH}}MۜąF` rRdJ4bCWT8 n|YpvE/p, GCu͚[Yя9Uצy]OQPߣ,_29I9=iNk Npbe r"^'Z/vئ Q, -\g'H(t'yo -/z_ -A{LXQ'xr^{E?6f}*nqZJ^؍xa  |r'YVߣ3G}#>gBIh*}۩(KK!U5qGWő4o{.FrqEF].~nwg=@ٞ߯@RZjU,|UBL^:xU[qH__xc9id"dMBF)F}=*A@?(Sc- "*S}U5jQI25ā*&f"kRn,lӇb6P0fj`>@(5 * -~Wf*Oz@fߧ -O IH _7pZpZh) G_JW7NЫfd~:8;X;L4d2+ +LK^„_VPL;XU$Fը)bս 4M=8D|XK*v fP( J&0cUQdkZDb*fPPGG |jQTi5UL#`HhBBx*2x_}ʄ #U23o /w"/f&%sՍYCxS58F(Xg*KU6 (lõTTW . n6|@M2vBTLQ4zv -TW9a&bh( -3&S/㭎Û75$DfVi FJeB 蘚è5jd-Jn͸QFFRME]Z -ex U9 J -h'PUɍLj,rtu5Hkh F 5h5D苾T%-S3S5k`5ve*mIlR9Iwv3hоf q/՗]ƒT`WyMՁpLP_k*bGa]8AZ6iz&Qo&̔ %UFPGIQo ʽp/!/S5LT(' e* 2T2S~Ѓ$Tc\Bi -EhJ ! -,[+z.*k[Ruؾ-̭>T:a+YpH d - F}K-?=q]jR`5)tF33C͔n ʣ(XQjl,\+SF_ƚ1|Ш+o'eve`"RTsvezFaߪۨ:0cpMf+L]E>*؎u[eb(rYF!,E3]δj/Q|#Fd⪌j8b)^UVt1& -jjbҗo~0R\Hc.M|^hNdF8\}3HbY$j P4ZC3bQ1M57>AmJE$K4ePUK#-S5 -)@ß;Дǀfqqje :T[̠Q8}@]P[MkMRoi nXP*Ə؁NDk w"a,kjG*pPz}}B.<+B,RS+ XLjNJdh;1D(Ǘd*K' Ȍ 4Lq1*:h" w6C#Z31L_(R(3!V4GakM,gXtԆ7Im F]4&}MT oZ*dh,V."JPLT¬oTfFMe7 獪53AOV6*qҾB؀f 2Uh43KUs5jeB'hD.Dt|h:MdgUzP^ #ԵXEoYZHKM?TqbJkuڨSO#@su)]h4ƌBH 8!.ŝWxUeGv&}Pc!j U_j"W׫˪kM]KbyOo(ȝ-7Ԝڢbth>(T!IOJbj;)5]"WL 5S#"p@?fX .;\bR"4DEjMJMl c.13c?(ri&⻙諪Iy$lH•BBJ -܁N-y{+5ybۯn4EɮrPTO`/^+s0 J=Vb5Չ:xMZ ◉F7c1rPW[6S: oX_fj`*l+>0եLUUDui,V3PRj7i XD#7r,W>| Raȵ,TIdLF#S#qbwQ_R3d* D:|o)DC0cB'QK)˚bmj3[%VxR>e8S h6GHŚ3&j+5*1U:OۧvPTb,͚R6ҭZ柺$n,VѓKY}Xoa VF+fm45F3 /lV'^.>mЁz}^A]3FA=/uiH]D@1QCՈC+5\Z_VZȋثQ,m R4QR?UVjd"tWMl&aϫ:Eoz.nvt.`7XdY曑H5JHSoÍFu<ʸQ@¾zu+3ygbZB9f1TepU+F?dMs\eo&d )ԁ!k5447)LS/>O* S;5vS_7WhYlgNJ/U 妿cx۟NqfuNJFO EDXEu^'`pLR'j֔YA]*0B G,, -<";U.'UE6U vڤ9鏵܄󐄍,Z5xZLфXPXRG j*U,Uo&$jND|CaCDn \PPal"f5RO}Q2SUVH':kՇbu }\cm]5 -%!j%mi ͌LjPa:UQ)̇E<̇e=Cn!/Nf="1Ssfl #>&w\C#١Cў9ĺ|JpoyIJa>4k,=$ W8 -G_]Uu&u/$)$3SIL`p9\\d>~;L#2#A oOIΆ"NŇN3,ds.C3^1C( \B4.n2KmÅgRG2ICA6lJtS7TtLD6(g48ԆGsrFπ9a<'S^a||t:(m$2 "y#XǚՖ3h.uc͸kmHl$Vr~r61p -AkޡB)fnZ:cC.:H X*Y.~{ :x 0Ȇ׆{ t@#~=Ϗ{  o!OhSaY!rWۆCzGuO…Gs^QC8A|h8exx!?ޟLGg2e3Q"yC3Cr){6d҉LK|b4>,{5RLlh^?L`t(< Cxx&8k4%.yL6"&g,^MjB}M<sSo*b&n*?dEr6 bz,:R>}6U?U4k%%?2à6HmzXX+7{g S}*}j+l 8 ]P~֤}? Q6P{@-l s曁8@GuH~}S1ѥQ4W)TNoNg˦dž#uiKa\oPxɇ5;t I e.y6K3@+v9F1ly „qS<#܃׆,D:*b?7`;C>x"p\Xx.,c < 6dtHf.:Pt$0lXh>aX +K&$k4 tUJ .f<'-Q6䐏>I3l<:s<:TytT$:at&\ǦM7MerF.YHɒ3\ڮ9l$h,|O#4*@IkDMlzM2d68g4M8L=t@eLZcHn"dz>IHH`l(7Ⱦh33\zC]؆~ Yy݌*8,$r.`Dz>tTo«FWp klxx6bR6éȢq Z\ropЩ; Lm& 4-n9Dt% -,۾Y#[nV5_r}lꯋ)؏ +2WLyt@2yX:c{&ml*B:}]9.@EŠR_Dd;ZRzjɃ1_0kXk`R!'S"10; , -X_dc0yc{V`>D4{_)j{& -N,b맂|bܨ:p5!'٭ Gxv`~ |LȾ0ȾƘ2<]48,p CCg<oa{]DdCXSl]7l-35&rM9ÄeϠa;gC::q{)hgH!{G9"WIe~ڂ dž}J#K!΁lցSXc=$t@H'g6(kq®³=iCܑMG\` 6X_`P$YhS|H{(}u~:dP;Zrh- ^n!l ;iac |ֶ qjӮhm{Oݢ2ap6q$4GRw&k{ ^_b'*:ޙwcM33* ]_qr'+Gw!K4 Aa|`b|8|(KW nQ6U0oP't0O."} &8i; -k#ԟDؔʇ l3E1g#^<Zd}=Ekn]pt)5A=裢sFT'Vǫ)7!Q!y#51鉮] ~!kCGb\]x`&03pt $w`Xk XǢ߇.lXQ. j{!UKN;kqAoHCκjä[=Il΄Gki=t@ -qK[O#NAQHv"#yn#c,Z atC:;a\`hFށl4hXW%OK蒳23"KlQ}Q|ˈ)x "\){ߑQi#?a -ߨǃ[=)jMb>Cx yH@-:Xe,멣illyLk!7G .p_%!Y ̧V`3[璥eĶ'+_  Μx[KE PE `'^%+ЅʉIȁlv=Y&yS?Sac ftL}* -4F]*=)Ej>K_0I0>F6B:슱| 7U|)wO`s"2v$)L ~-? :0 ||#AF"=9L^1OF>it%ljLN7(K!1 -THH KttRt̟wcK6=& 0_`' Ɇ˩Tb4Ld$举2yc:u *8o4l^9)z;+ooҋ&>sK£K ii$Z-/a$ox53 |v&wuTX805ȒLjt. *sɂSK# s;Q1֔2 -|z`l |X/$H߂L8<%bĄmj55Wt%ws d81[2(=O~=HIYGN"w@;$qNG!?9Ħ8$\ph> +|b y> {4x%N~p} A5_ | hL2wys ?tM陜#@'UWLR9AF?P >'KD%cEM3*\7w84]ވ{';$\Z,)x%ٔ'M Et:W}w%[`vh>` #r5kT1]?SemD3Ll|-Ͼ:`%S3b=К A)<&q I( 5Im_%T ܣ 1oC\>;{2'>"_ -a.tXT|K,ɣp| #ȉm\\_G82;|3Y;1iۿ~&oga[2T fd9sGŝa} _Hڄg|0_Ll0:'!y`r/fvʧaΤψ:O0Uxl)Y~Έ,8|\ Rc+'CLIBeźԽhY0b{`bgI x 1)&6;>OR l~:{c,8z߈!v ~5OLL؁^ c5+U~g3Ph٦3y^;VՈc>Wgl9omVwF^uۘLc{2NM ؆-8O_6o/~ :'tq)Dٔz=L\C+#,{B؇>PgzOP+*ܴ@r z0`AG <(u!@;!ǻsgC HwqH^<]trF-z.zlR46񈬝HbKd L`XqQV[BܓFv֓cp{^O؇W5N=>@8b; I ̡7LNA,֥;F@\ o8X3*=aHY^CσG*=pż-4u4Z2j ۏx%'2W )g Cy) ݔcI稁ce Nɻ#E|Im3!V\@V^7%ko|M]FU}rf[0!oyRS_c ^(>o37 ;s#~gZm  x).J8?1 wćN6א"Sn9O`.!=m-r~re6CF|ŏ9&%̰ΛJ!Gy(rH9 &'s8ँr -JAl)? O~9'=Jyk@)|o9> p!UBe~s >|=릵~)YFoK6n$kqC<,riH3IGDv{C쳏ΟI5zG=L5[+ 1e鍳;G6gdˋdRtX't91"nQ5 J>ZT\TRC;)53 kHPhIm}FP.h.vn뮅.Ą'n~MW\1>@`Q6K7j|ڜhl/dۋDӣ"| H_yE〗:_>?qΡytCI>,.cOQx!CF#A>97uk,$+VlXg%q<p'poQxhh8aָ|"!|Aiĥݹƛ 26{W1x[z۳ duTE 3 -fy \|蚩\%L\`(٠D@ š|tdr7_P[=uqK@Er,U XuV5cϛ(`>B)\jLC+OS{,a~){/ko.o[3 wVgA7PswArB [3DWl :tZ,1aI#s !k/=g4. `lSgStʈڷ`va+0uwVэ7W,`6~ -wDE}*2"ͧ -PY @ -]yr@Α2r<9r:Ta*(8}G|02G5=xG|+|uڷ*Ύww_n >{/j*=|tx(:o|jWfLM:6I,9 ekolg?o -:j^G^1fZ3}U: 0q<)T!.Dpn#B -y㍯ĶD@H2t,yz`:|^\RtS1U~l  Oe -醻+54v UxZJ?(>42O,cVT\1_-֤l*Ϡn: y p#Kت٦udErGbq$vo'ax7+`-gPaa5/8p@H*jϛMW#%c(>:'yfGT]2!PrGW0av^IiCLRfr.B'"R1 l>Z`' `b l6>_Ŝ6uU{}Www-[w5>_trfgZ?"aP V(k GHn[rw!.:i@\@r1tT,0 A.@7Zzٻ2uS3.0dD8 ]1ȖbumdVߓqт|X} 4s7DϤ=%+pΑAx C3'z2+jҩ\֮K2˜du[9<[sB!`c͝g$aW8>Y$S{@J\V8Upt1ؤzm3K)jn{fe ')KW^2񟄚i%nFdb@D9ς|#0هQ53w/9}v&^ʹ̤kو3)zp@kQ Y1>8H?1! 1?%}c3t mi1Z>D`0Kfm Flaq*bts%\ܽN;0 M gF8[k6-p,~Wx @vAo@TxwU;šg7GS|/BmA~Y1x*RۧI -|=[fL9FkocgĊ w&jnن/XĤ_8/4 ,hD 4csGn8W|ΤFMfGs%Wl.Fp?bS s;cۏGw!{ -u~4*G|K6>3Vx VW@.OP:q*UxސekAax ұ!7pE叧c&A^ -]p@5egK&DnƴfC'>/[Qi3Z{YpÌi}l}{ cKȊD2= qTďͦlyh!]tl)6=YEzdIh yw!ܽuCh~hz*!q9Te)Z6HlzmLu| 8< q)פxJ\CqwdۗdyG81Gn=:iL*|E9/`񊠢QDtD2atEMbw[lvF~[~%J}O;>oT}^M~XC{eÝvxMQ-ѕWM~ ƌޏd}NӍ];٭ͷM7٪+@ʘZ<+{߀?# 7HM0D؞n{jj~jx&!ko\s2ySwʛ/M'l g9ٶ[@θ̽ /z ~ҩgE'׍-W+l{hԳR[xF+j~rrM4=[#7k#:\a;y͓Ne</NPP^A:mrv7{ˎ>" -oWLZF}I:=;;d '++SW̸;Ozk9Uu+6jxR^zYlYf{讣{Nd/kɂKlʑyl%pL+E`)?A1PCo'C%zCsHƷM%wLWiYPˉsK'/,[mn:O~b^?O!/S޾aμu&O|`S9CO^9([:5|Tcj{y% -N.,Xm@Oײ'WKGO Hm:)Y`3$oA#})MyЧmeO>pa>ufw=#k_[^.w?>(4,~BdE`ExF_?hwZ{Zyo^@ sT ck'eڟlnQ${0TKZ{9>eP4?_h}fw=6+Dɂ>4qOƕ=r+でs;ֳ>#_6ˏEq'>E~g~aOw~lw7ஒѳj{.>oxknzF`kvtU[t9!j{Rm_k[[%˷?!Iǜm{n}mNg}(OgK^3 y59|zM+5ͬuY^UXwzW(~.eg0ݧ$ݯ ko?S}xǾ~|0RקgkwS8r䏿leοv=P{x+w0C|zinP=VpW^n{J뗒߈tzY*߮F";Go{aBc.jTyuk:o>_>e>?Ϣ|~E..p3I{OOsy[i/Ͻwy~R\Yw6yEO_ǏNV*?,v];G:w=b~b= otu\Hcrs/_n>DxNjpÏx<[W_dm]ihX"sa>LUU|Q`dpr'#mn͍SY~g7(LsGcYR1FhcO?r=w;Xy'#[}RqM{^xÝ}͝x~Aq͊c)dsO;ww3_])q~ɮ:חޝd?Ieov??{۽\lӽ=A몃Ww5Ty_F`Noo l~n]CGBU&t@WK6?̨(~XWSNl z߲Y9׳rd>X M2yu='/^w}wXl>ӿLY߳gGWŽ,O7wMq-.Zn>ۻ֢rثOVl9Mu7un\K%&pzяYnOW;>;Qp]PwȮҪ{eKގ/?›]!W:".މ,Vp^XMwËn܈*>z3fBY*G͕(`?tf;:Qx_~6'zkCՆ:edI5J27_o󘗏Sϓ Ϸ oe)({{z2?m+ K;rf4mw|y{%JW]^\RYp/`GtEVx < SQ%GкkPVByԻ9-n2E7]rGor.ݸdJk%.?yUwn9w+l)9w6?RʜOs&?NeĠF'{^A|ݻk%/ _)._Uy?q[zAۋ5IݹǗ.ρ%ߣy:w+쵸Sb܈-\lgq;Uy#ԋq?(}/ -F4$Y*nVwV_K̹]G|ocuW+Oy2kVl\ W}}ᝌzwlćq/nw7?۫kSAG{y;}mq~y~t;}+0~a+|ݍ,s/dQ%W5=py}u^7we[oqvOtTͨTz%,nYÆ2=7^*tϻr'Y=9C2&Z*=STY?©_;\^+wxwYUGH}p>ލ[~m>1}Kxh[ n%޾x+^b󫽥V vӛ^n^~^}/c/V HHQsǽܦ{]뉰ב`]̽Ʋu"ws]g샻 GZ:J"VT~r\IjOW).vx}ՑYMuO*J#_a^w!yJ=UURd2{W?E/~J>m*1 zWX͛n^kXz3īdNȎ]/bݞIWwR|%G.%ᅵPzr\鶫N/}wk֝쪓WcK/\.jG:!y -u+qoA./o[y;ģ#"Sn岿{hRo<ݻ|Ktuګ$+%F_}'16[)14^!7VxDBLoMԉQk{k,*v.|Ϗeʫg7_L.-SOW[M;ŮOT!\TSx3rjն;nR8ϦGkFwz-}ݣ[Fn=8~cU=?o8?2c_W>M2Kod/%zgw'&"*$>#п&HJIfѓ,5|gW<5]gso_e3]שg9nVuV\N-k\RR)ҋeSʿ[ː>.=})Njq7J,/Zʷ |X埽/V%8[.W$sI-'Yc淋!exֱk"gOד Jt轴$Z렖i㶡iK YD wWJV.+YZq!jZYmSʶ_H.;x>̅ңK!ZŗWvfTqݜO3Ny\QCOB -+*ֿ򭭻dޤyHGgJ ''ZmJaF%LHD(Y1{crxⅸIeWː)>{%(N^)A6_-9p5ZJӶ29?b;ĕXkuSn a ~ a"~m_?v}߻n0oZ ,z2n͞[>/ޤ*sx|Ϊ{EmޓD%"( Q9CUb2DI3JT$&0ۆVYQn[y}κsLsyyS#59b1l ⦳ȸ/=q1W]įJX/B 1 }9~E߯وh4# ,;E+sV4<_|b~ -'|bA峅.4nVP0T۲yƦCӰo0sI㜰ؽss꣈ wmN\ b?aK?AZP_s)F4A^u%ν _4n9[sڎa6BV;bď?߃gM.B(2{=\u3\'?MpeÄ=/nq)!GW=\F޶r]otRAӶMUJ4̭/O?:t~Smo$9Qhtᔊ*:V"#y3 [\,vba//}!v}#< -jq|ohSAw=kiL~.g6ѰB8`dcDd5Ҝ 5ll4 \Wr(^yKo6+ -p&cOa_ٍw`zzvóip͙!£i|hel+?SGNEc qd 2̌!Syt\d7>=3g4eA8rڭ6 ؊^ ёOTQ %p{qŒ_]+y{xǷ -~n;s;r-,? O2ײFFQx!n}D4FLt쐩4vds?sPd#BYEcWvJ4}IVL?sEd [ c \!ye眩D(0aY,m2['I.9hj@%t'vJ}Y5(g^OKԵr]]8ܦ8 ^iz9vNˡ ׻_Y/ =D*S sd= 8l8bL}<2ױ\7푥J4q2lƠi+45M\7 VRmW_ZbIa轰}9w [8%]ϗ]5[럴^u£[YdR2T r,$8ziXg} 2לDlcmf&z6Do1z&23Yۅ 9hX4eEP&T!V&>ptI½͵.n:W yunf}7evݺy>7)v d8)C 0՘G%cenmv8aMb, q|2&:9XX屁c`jHX<$8-6Iu˚l9sill~vۇ]?<|s'P%5x?D6Kq\p?YOpFy8!WBfc}&gxX,ǭDVNhdΠ {#FsWS_Pxkg' {rIXZ~xxigjn_(z)&//#K(ՋWBMaƫ&m:$;/K,'Q%SGhC:C&O\4y qKpN)Bf3h<9朄T9L+_ƂWK>9V~-|"Eow%Qo{9#qOvy~Skm!^oPsֆ-2jۯ7z~ks&c@bhdNGA_jYd!36aAg";ϵȖ@#4ʯh/ȹi'5ZTzKwQ3E\8!a}S33a {) -zr88wλEĈ = ާ<}GU|]1>cDwdn+F]֢9T)MZ0fC}ժ3x|\K-zh8z\y%W5-ۣ"pes疟ƻ/B׏)܏W|WM+Ƨ-Wmÿo_>[ʏ!?V:q/Zqyw -*:)4L5!0ӌGN¹4Z& -F6hG^ќh=vGhb-Ԗ$tU]qWpy(BɍjVʤ?.L믳}^+ -bM !'9&w_U1>KnJٝ_p߼(.chpÖ⡚fΏ&[/R6{yDo -\. D <UhYlҺkF. 3=~$b:eOd!iz߅5"/'!uk!ثx߸zmK0qޞB }~˽mQ&?`G1Zh[E;XF06Mƾ@(& d~#Ċn \iKN\|J^:ݷtK!o֒m?x1-7SgSg3ʏ]S(o] -yt?Mэr`/e=eCRU-t_1i ':z4@56_&$:+*9mף>vNjOdn$D梄՚uEy%~Ml ->'%ܓC%r)Ԟ_e5L-7rŹqܙag?ٝʕ;P'u&M_I?8f̞+CK -53N $B -1??,þ{C'Ox|x䭗ɵw?m -{ChH}v~7Ƿ炓j4z_|R 7b" J !JAt@?ۊisTեd ی'I$FktR qmB4AԜތs>-v}$u"o*B>{̃w#sSW?+<)R>}PP:|}RJY%)n6WVa}в =爐{dmc i\]WKTGR/$Ԯoܨ֛&&76SMvl 4a+훈'0[jX p~^|_?G-o`*E`Q1a+v~0irbc8
 s0^qQd;?':&G K-Y;ڃzfU)}^~ionsv?З>F0ަ=Un/XzջcFx^pkOv'm۟-~\":"20hr'4zZ`id^(4~n䆜1eO,̕ъlHG#dٕTi'"GasVFo+ іhɢHע]xJJn̓T*8t;J qoy7Ln (aR`UQ{QҚ33̞tJ 9cdWxz#++)!2U:Sf)m4_zqtE{mQ;ߺG^r_d_IlASyޚ竂n^S^^;UPYz>Exl:c2Tc2 -1F&#q=Py"wiS[~y*93'Kn6r[ĒDz4ʿ*OHhYh̅(+4Sr#sMBVe֌;//}GԱ$ݯWzf+쫕Cr;zI:ͩ,&~2f?jshd7a-`ץ~{m^(< g':#m>eퟐ퉖Μ\(lR>hhG"dMlv%-]})̩4g -1˨+L^d!þPyvlӥo[#S'+.}k2gKҽuƲ/mŗ ȋO =O'L=:#ya;! Ia̼|?>'wtrY 3u=/b禎lmXBГ.yJ6^4vC}40d۟9GUJ'o22635GgBqpOD/nEɏ=Q*> va_u~{>$KCґrrYң?^sЀWB7B͏jZ%Bb% -PiHRG -WDNi=\6̢=<r"Ua!I=SBg@opxq_Z~kWYN]c;'YTj4J_7'g졷 uٰM{'SelŇ~u Ӫy4(e -(;e&Ӂ/ZLd5KT[bb;z@KOZnZTkog#=f|i -Sk%aZ4M1FϿhG蔊Ѳ]8ü @3tDMUJ担|3$뽻 4 T*o\LnXo"zA}퓝/w `l:=9X.'u?]}h:]kJ嵎dw(zm(}*M݃WUdju?q,XyU<u\ uru\}EDX$ V1vhyD/RCR&kguOyl(Lqz~,ӌj2_]r`2ĊXu -۪PšJzp s^+:c q` -hR=Oq qdI>+iVYB/rcӨ`TW*]Aa>"%S5t#Gf:}]L2jgj3WOa;dzi[L#zCy$;^L{b|Ͼ .|E{/Ճ>^.W*Dϟ QՀ-zSݾR1ÜywBOim>2?G~,DoЖ٠VHӖk:\HxlTr!' TX'*Rʌ 3*:C KRu@+,V|`}Hӫ}=AאI2⳷3}]'E]=U}Ӄ̎Ѓ4w``zg15g8AS"$ZH.-//du-ù펽oi+$UjeTe^de6nD5.bd>{!eν -a/OfpLtv[IctDt߃A61 >,2 /k;>:4dOOI;AԲJ -I;IO%d :L]]@7:8*lJ3SD БsʨtUAdmph=6bNL>^twbw?>Wj- ҽO{KO%o%~pOc䗞O?]}-~neՏjϔMzOQ uFTj1~*񚽬T:BZvd,99̱G+p<%8HUDd'dbSGf*K7ٺCv̮<|jh.1S/nOeC7t7VKfC)Wr` -6̭lf9I6łhl`ŋvLds[,dF:!SN(#z;ے)[5ρ>|ШԆ>i*૾C'Rj Lq0_߹Jvqv}pu?[m<0 A{ -k=D kXS<]/CeJt{gf@w↬B;j޲  XW7Ug<[9~o)􏃞?hGJxiPڀ -B*h' #.b4o2hSrE}4MKcGJkJ5A94U3/}މ=Jq]Z(}GG|cpl֋>Mw L땪W>™tc9\&i,޷~ЋKWoI,Rƅ7Q=dGR!pfk}Y{1FdV& f)\%AS/]&4 -t4h{Hwum3/hu2C&˒)q,LVٔj4q}وIb̬5 %4n*Jw^:W5jpzb./@cM).̾OÏܾR@3NP^[s @{ 4GOie0g@ÌLR>Wny [ @l`AHA]&U[;.qr_yDhE&81z}B&@!6Z _ +MW:$IdXӆhǍ|YD+t|S㧓C߈{= -<ܑңD˛C^^^9*QB_>d _̑D톃/K֔tu峖 o\kGLP&k° ClX6hO jFb@+ -%ϦךkRF@V~v[zF <ХM%B sGxǏL!аgpNY<6$P[,xMm4@j}hi|.zluŞ/ϯ8Ezо!ц?樬=rg Ч5<2!ؗ`Rp|$U*: j?Yr?tIӴD o\zq.sHy4oe!j Ӡ_ -tXRCi5LAnH?M '{^'^ѧdGtq͆&js@ Ult6ԱWf00W_q4𺁯Aw\wd{Bnl :媓wbh qw]Al}Y-Gsms{9cLb!꾸 UVvIOO!]1H18YzǁIPu& Y\b^hcȵ_Z8o|.5`lեlUԘ\r0(Z*&@Yyh6hm*vߓлd֕Bc5 tdYdwaXԙh1F1<+U/:Vp``- ؝_{)7lv86ے5_=w_z*)1]W Q^mL-Cу oYLbk7(D/'SzdSsTlي3'#NNQ\-ѕ==|i.X`}: -w`{_yc*poNXhD&hA\$ܬUFJw0ҁyHW8MYߗkY7cHW lY!ccA[Pl є M戦m>T޴k -H- n̨cK h甠1To|4ĵr#/714.?l<x$zvgq&, g(udpR%CG\iUBXmkl -†'}@lv̍16N]m X6Ԍ|Z^=Edt'|m`gU; wqwv['y \:+(T eVms k18FDKGQk4D" -P6xuAYm K8+{"ȑ^sb=>zXچ,0AwA;ЬhAgx80>T1yR$d }a}Kv -E3I.0C4YcEJ`gEdTewMמWLSxFtnWf8P̬02 -YqG=?? -4lieFM}ӂ:}B uSy*?ux! ME4mX0[_c 1YTVGh sqGoЖ2ja;$uţ=HbLtx& D(yes.!?w/Βr -5Ov$X#( -Pw /#Q:ty|dH&|q+lB{»זk>OVh8 |N^&aMdv7V -Z\J1_f\]gIa PՃ}#U; [7׊YiIXN8f; { R`gAbgV YK?Y,c5!Rj)0wA_r – gB; z~>񊒮ɠM}k4_<Tg< tLXLcvw}+Y`<NX`Zo1m Dz x$#r$"ՠv>0rI> P A*JCYi*6 8P)ĝv@Xe=)7Wwv!恝{% u`g16`+W(R -GŦGOf8~ do -0F΁k>kz3U^50*[}ȩ@ 7uLf!FB;96η]T9 1yzAYeu[s_.\Lvr"=ӨŶt$쬜V`gi\T$sy;@7&,%𓁝U8R;+eATr^`m}oo@N,0ejV uFՀK9"`jdXx<='^? Bv൯2RQH~$-G#wB զ7x]X4f 6 zlt*[-mKJy'ŵ ^Kߊԙ̏@#=6]GOvV:*7G^{^\6Z* QmW̬3;{JPr![z ,|}:2g$*M.; ֗9@J*) -X^fm;K^l`WsaqX7QwZ 9'ܘvk]rqUtp"wb9':pbuH\R!BJ!a=KAStL 15م5O\4X/am}ChK`mVwJۈPT+ǵ*0p`}s)\m<̡1ԭ'ya ׄ}' `F!ly|㥥+}`8?sm[&Ģi|-'OܯS u.]5_W6 -Ø0 ٺp_r>r}1Nb7=\\nxвfȚXϪY֋ /Y|K FV󕹭V$~-3ͧZ|eݓI5_k"µ{.ɥF\\ïߖt݃+mjp>8>GbDi5fb="ԉjpEj#k+K'$!Vhr!iڰX[`TEL2”s[LX2j q=( 6D0uc4KW:]PyVKvNToÂeB.6O0€$:0 g{|F}䥇aMD^;Y-Q l2U^90@oqh E%,6n>Ol3 ͇gÚ#Аk$YO;?m!u] -,2C[s|6 XiYx";s8S- c]Qfu{&=Z8UsֳƖrgHB֬gk6\){  SxոﹻJ.Mڗt=vp]Qy|77Orc'\T -dnz3"ENK|o -{ݸܑHzQQg&UQgpNM}bhu,R$1c8/"X)T#,O{mo"`S9Z֢بX-`m|>lCfst(ZM(=ty鄏B}"p`mz3pϊ0a LEVwYpnIѮiXz -&5{9\RÆ&iS}=ޗDl25>:kRqt4-HZu`/ oxj!+ze߅ͤ1XĤ}3C.%O/6"yJv8eQ5';zvxb.\C=/ǩ0Up\!>L' 8o?=[ϖ9[B>!y(57=cw> n/g-a^OžG =Y[ g%>% q֮ >ܛЀb,>{2`-8El:-$h4*pNpo\sZ9a{iMyϑWkprFyR!ab 97eityŰ>#/ߟ .n `rtį+}I9`r0?X[l u6d˫=hVd Q/',.zjUl|vw -ɝ]St-rplgyߪ|TEA%Wpl>y}%p3a"nDzTx}r&C^%1Ϛ޲ϖH^ZL5_^T66Kl\|{H -vOWHo2VX8媶c$M3ue*dLσk|iJ)y4&Pu< rf bL^9Y -'A<-aϖnifP;˨73 ~Ŝ6V@[XPnBXM>p^a0ǹ-LJO#+`:;cT| -"A\-d꿜9!b>O~(aʌȽRM?1i8߆m,:+tP'66EW] B/ڂRuL qas6uM&vZӆ|r]u|Vo -97~alE%j'\Jg_p_s53Qq`L(3$csOǧt|:>Oǧt|:>Oǧt|:>Oǧt|:>Oǧt|:>SBC X/ދגuIS:'%E%G'ć$[;·X?/$=")h^} eֶ'ΰuޜs%!ֶ/jl-JߔF 20-,~jqH2~J7]"sӷmpk]kmE3 Y; -D[#td Vyr A?T!CkmMw7?DRe4-(Tk4@K'@]WcVǟG."!||(.yW/cԁJƿ -Zj}4A)2QLVSחʩ(Ϭ52|=Ӷ*Y*AAZe -zD+쇋MաSk)񊆳K'9;.>MzIa8h6B6@L0 4(uZg2tPEt&cru٤}Te7a>PC4-KVmf2iOGkk@CBm#zfɫQ6q9zlB.9z37[Ht/.[C_$3=-ԗ*X`{ ؞w2h3>\|.](6VwL֤֗$)GǍEjqY]\\Pp>I<ߔ)>QCL֤C¯QQ/;Qk T<Ĕ -~+?esF@?W~:b*\-R#K3 -t$ s=|=Й z{AIlLHb="QC~4(AS@Oe2XIׁVUF7em&gx|t kI4U$C/#KC)a,_}b.NBo7uM {DYV=3Db4ep0gB scd>vej’~fU)>zHR#bo/+S*MeF<sZDg "'A %c-zOd<~>ItXLD, YAGiB/Hpu&b6:AlNӂa[sE -{%SL@tz@CC\m :nRĪˡ'*_ -^ zDW^+}!EL.h.ȓFoRvM8*ʺl'm2G=-gPUsl :'*J -4**ߤظ|2u1hkZ -u#Ub|]^;tEh#@i ,Ķ}Чђ#m=dԙF%sWMzx..G)gN6+xm#Vv-sAS@YY{2 -2=S|b!ѓz]WbHbzz9&_ G|*A$[Mtב た}p=@O]ʪԀF㸰&GjdM.4Ct@c~RDCNa z - -ob>n˦A/5s*"M/-+nӡöDG.,Yþ9yh:o6~x 誱DI6xq‰@lqzwGXs¼ĞAk 3ȅ@f S7v0D%qfDi}hEJ S¶>$l3g>Pv=2cea$Ea5H<_a^n%¼=9чh5C#-l=lA4؊ö Z"gMmﱂUҏm (@4\5Ұ/ۗ ~ДekoWնOs 4fD100PE7 -bآHw#9g;p{{~~b˜s}c>umh- -]'8΢0k0k&Ff(.<} ;>4(hs)S^*]=Kc)Et[@n Z:&:U?y{"k7yÆ̞ h"XcZsW/~^rA9lEӄo,a l!f& c>qJ 4 @' -h3g.O`R|PIQ6{DC8@ /A8mt Ԭ11&%w6?d5C|9p5hB5nM-P`h@YT_@Ƙ2 -{`dV *жx&fpFqAӁު瓷~ե6Ocq|)EБf_:6hnxtIQ/+1{:, -_%>j -Z1Tоחc?O0p, ŶA -!FY/{-`/~S$b|.#n[ը_dlˍ(f7t阒TJ^ -]Is!p%#+&ԩjԄ 媠2K\X 40.{bej e@\:+S~Ih"?({9^ ʟf#ӁFhV[ -&C^=aЁ]xgccg}&9HsWNbC O" w}2`=rmH/FM\SCg@-/ ߀^aMx(O`|S.sqOzH>W~DQˀvz}VKAxu5޸ -TCE<97Z=fND~e;G AA Z#rg -WAW[Ș?~uõ'M X 럻NDUng2~(D@&EF|!Zq< Qqx/#c`MHۏ~ \ze.=BnGF7Ħx?=&ȧ R*G4t`͝FӦNx.x*-}4(?)1C:͐?R+^/So:? b]!>8քt'Z -̛ iZM=|koN/ Y 9-ƄZvL!u =>a\e어~|j{hPӠw-:ksi%QM]4OD539T}:5y@+kTטpTb^oev Ʒ0f+ޖ+2wH=*,М?i;r|+ }^䞤$vMx"j -_>G/71W"DcކhX-I/:OΆ_ Is0_4Gcb蹘">hlq+s1yBk˱^DaJ(Pxdvj =?99"KyJ|V~HKq`n9guT`%п~P<X!va:h9b=D < fC:)1WCsu^k\}o.52b 뒥Do19ȯb4%GIנۏZf- ~*G endstream endobj 27 0 obj <>stream -A<(wQnPaE~)m[1ᝤO ?.?`ln/ G ➠ -ixq[;\w>hA_qܝyWɔ.:C{&QW:|u"E#=Iw zc >75,wcZ௨fB|(x< p^пuB3ZМ1a,A{LhӄEGGKkܟAxu2<5t!!>ycr&iDz(N:ZkAzX| -Ό|"F1J". .N)d Qmýwxr8l\Mo!9!fzLX8?Z=&n~y)[D5%1s$ -pK#%spŐX\xd,\Fo(xB( F zFJ/xU\W,* +:mt%~+5<&1QWѥ-l55+*ŰCu%8j=<>/ :q2SV:wT˄!7W Þm^q0T;a脢 -56TN)S3^nDyk)P -+\\YJ=[sa]_ -csX W1لJ-J|z^Y9)_|)DejMx?فoϵ^U2` xxLmqzH@r9m$ g4p@l6qTCoEW.)3QwVJҗ@UuhOH6({sFOIxBh)d TkI۱'$xpGm}оW0|1g UuQ2qS7(J*xL96h}"}u.x2x3Wb iG\]=/{|111!= ->Xa)J TQg+UuORTa|' -?|'/S!-r q5rQj&Z^XK4#nPO^<:Q@eAK&%|.p2Ή@NmQxFxP{.+P}Gu^|axZX.9b}eX;eE!h3宒d,5! (a}%g'^Q1"ģ/<{7ǹx!!vY?W4 '^1hre_k􄄱%3K6)xck:ڄv2JNnPf# -|yf.o=:/x]#q>ĕl:Xq(F2w4wO@\%_P'`?^5t-P%YK_ŜR&ὲ8RCTNOO^vɫS<؇P,b+nd4kחaOH N9π^ 셡mf[53!倣D:BD -~X}9Gdg{@?bjhh5Ox -Ç|O̎xޣ {rր rX=XU'+}kp=pԥiT`s!p^rЫF]_ { ^ ~veGxc{Tp1b'L@v0.XA>^{瘹xOehZ]]k&Z2j/_{")o3"rU-~o?gb^n!eyp2%7V=}.񾽄J/ڧ'| ?.eYKg0Xr0h.nm `]qJ랰uƘ>qi4<hPKY@<'g)yaiL%apfB6v%'1;e7N!֗%qK U Vo "50f k؋~ B }~ q_LQk )85C<\\'Ea6'_y&6֤ ASӎ>BiBkAb )OɔJekrF~(~_lƾAVVgawCg'3[Ԡx\Ϻ+>+ e;/74_6ƹ7Z1{Oxr6<EпZ}X煞ǡ -7YZĚS(cx$ar}b1zEh,@a)l:FM{m[c{Q?2@M`||hO[1yq,yf$ 2 HGm~fQ\ {s+WKaC6 ݃YPS9kmE<ʭװO=y楀GqbO*XS\Y [b5lDVYل+{ - zK//lh&K.Q,#lk(pҗ #=ScRy[i/ -iLX&h[`DzTӸ֡蓉jӂoYkՎFe?$55):p2jj>}ذ}CJ%B5k+aXGEx3EC# {@z!P{G@ށ'LZ>*[ +#aen/EMgg^ѩmDMz෎\pX< +JuUqP>a/!=ɐrxcJͅL@ -R.`VX*l -4v͞OFn":@_.OfLhwLl!̥Lr>A feV3j!oփZP״|zmFdÊB 8DV]d Һ\񧙴K<|3wa\|)]\o^iHWa N+j/z'"YXiI{fҌ$J]TwIiڏr.W*j&UE|J%Jj?] \-a!x9Wa8?/(A}dn07؜6i浙STXKѩh!yoUш;`m{]|QHtY,ɭ9dW|N҄Io&ӻTѡ1DŽ^q 1F_fjz0bS_zެ25Tj ->6 忤&eX >(W{]z]G=Gq}֋@7K(a_vFxK[N rO>l#77!_| -K[ĥ&]0yn֬dhຂQ"閰,Imj'̬%>/6JŷEVJr5k2٫ڬU[FGož[<-"Jo}ТSY_T;x~(H^g!󒸿6PC|9M5f>}\%zGo# uvtEmKUz(_=(>..l8+"ީ11-;ye3R2 uz'_[sKR;=caf>C+ەoVKoi,BF&}'i25C|E@? -ߴwgJG}y[vt~)>üd~>FqzX*dʔuYJJ}$}Ee5E&!3>هbqn){eZuȫjV飊#ҧe;- }ˈޡ'F4 -R7jw?걷H*O>ꢨ>)0ے-8Ǽ:T|8Tv^ .jWD  tA!Je>\<$h,y59Sa˼=LeҢP 4q.Gxakx&Hޔ_4wQ_?ZͿ]MG3C>Y -bz%jI3[H{UǥolK\n_b'~QsBrI$j7XJ&496_ a­b+q5OŃ%O_|د}@x4xkV4Wy`\x嘣d.⾦#ĚtJڛڎ~Z2ٗi*FK2'W=GK,~hmR$.mn>)*n=')T9T\o7e_vӯ;Q;2F4P+nXӯLgjz-n%X6Y']mLh͒0=_Oi"mJ66] S>ne - 1kˏ4-?,PǫhaU軩N|!H=mcDN7s:^NNjLO?0Ѹ{]k4|?髊s&CD[V 2h $ބ<1Uu9*̲9%ZpWNԱ[1`qsΗ2aRY5]'K+_<9/yq~2Z^DZoϼy'ƽU–7qgwQΪ㤠1gSj%vs19D&.7M za?9^O\M-wIp Ip N<ߜh F"L5H-@5Pès)[e}?C@ӊ*6h`W&[- W6m6b/n+ueIR7IiI }ud5ȧg GK;JEjK#.4&&z&y&&WzJZ麶"]BS}ܢV9Gf8߬p@]' xP#T}aAfJ5l(~pL7D dsv+ɖגN6JtTş-x%j~#+~-gium1q]녃=ŲCy2Ǟ)<ŔA8ɐ+zP#+?kX۝CO=HjJKkrZy!":bpQmXa]XIuHeuH)&><1jkTPoeCZq@YדH3'z>sy+=8(ѐI.I3Ҏ ަ agC_*TJ ?IKLz+#N愴齷*Ĥm[Mpr@O+:$EwVMKDLH^CdQMXauF7k~Mh~M*4R+]eer7We@SAA)'4 +YƩ -Ĺ%oϚ4G6d }j&Qaɛ BT_~1zT:WGE -[-k }`MnV)'Zo%\mW!Zr;Gf$KLuuOlLa;\u܍wo -LuYJh^G;>+v-q-:ޞ% loVW]Z7X˫anD_Fq/+v/u -M* (O;{\^s|^#w(OAeE^E2(_II>IѹQzo9=לO6I -ʶlaޙ6 -λ׆^qe]jLjr{5E a GPvpnjqH~l$ر*"6'&=jG}eT䨳)Ǜ"=oͺEJzC$%ͭ~Gn]GfMQg2SO7g%p7`C=Ϣ}Ȟln7ѷ#ef}"9 #j#c]#_6xg~ۢdMTǤMQbCcd]"~urƺ=XqB~̌rg~pV*O{'勓^ '֍jZfӤ8$.n5=r/ѱ:*%;j[mcl2@Xֵۉ*]% g2"m+cnuC9y<ܬ0ڼ+'BsR_KMx≶tGpܘmYqcccbT7;^-g_ܡ;S'8{V1~JkewLZ[${7"J"N6eDw܈`<[էèa;ߍJ%{7;o]sd;ÝC}})!, 嬒JAQrGZ-;guJSLQӥ' v9 vQM^ɢePMDSwyR~y=|1}6y^b}ƭ{5[U[ eq)[QiP_ڝJ"_zn -/ -="C /#p13VkU~n,E񡥾 ob߻ɲn.o -Kߞ5Zinh:rX5WX8\O(silt(WMطODh4n;w)s9,[ -dJK iks7+V([ -}>3vUqBAV[gKwYo=b -:-v /( {1"-:+ֻ2 ѭ2$!Wm4jSt)b0C>7@k8ޘm1Y鞂N;%/rE!S;n -Xip"BIa1b1CE$"1PA,DfD5 W|j<&N w+gc"D/lt5u{}s*0.WTZG7(/F]-=.v|!J[Ǒ8G.t/|Z&knuY~iI\p9o-j ۰SXTXf9nzFN󚂶lW"&(b1@%>r?^z,'yx˄"b/JW?*727*BoYzgԕ{nQ/ -\"s \"rbEQoK}*ٯ䯟\/DǼjF5bhts.Nqb(sJ_8t#rj7L F N$FMD? fNYMrU@ucsB9갸9zlVGTasշQYܢP +r 5yYSwΑŞQmQ_[>X7;GΝ/{=w]Bi t DgO@4ET|?\_{h.{Pk~[?e;zAf-#d.Zb^BX$aoc+Bg';QrkGh9-CVGGqx!5&-}Kt"7nMj_uF){R*K( r bƄixnwyuwpMc~H9_$jĂebEb8q؞<Ϝ[I/|e<^ž-pM(+pK)-vNz^[RX[EPѕnhr:m__P6,/c:{cѻa3I8MC9ii#/&&/"YMT8@Tv!v}=B[8!Ԅ(hぼM(D=E25ɯZXt)&2aKwiK/cC\ ?U/`N@Y IqbM;YӶ3 -S''ZGL -ߏ@(GJ,xf%?[n3oxJ(`{/<Pk}\5Ǖ2׌JJRD]ʹǿ \/JĔIh)7ČˉiVƬ!H̜Xa? gb#}I{Fq+ש导 9b,~~?1?QqBb696s6%,Q'-2$f-sԉ bN(˱e[n AQz\,#hw -~AX{Ǩ7Qo5F~[i'<9g84b42!75v91k:b̽Ē5bS sR̥$1s1CQ=~o/T;A6kwٲ<50Ts n Ǿ.rP3P_c;PXST'Y5fp0F=<{CM@N!F"[BP”5h fMM(NML_&Ebbẓ -m_b{;ps[?rG] <#5SuCT[S<̹ZǴ*z䷥NKr{_еxMELG ͫihM^OLDc8 F-"f_m#0k+OU&ļĂ}}bO fOv6C@_X&2{܇}|yYd֬uv-`DTZ6N E(G949]N]bEĴq PnD䕄tiJkL Ă '{bmm^5p*<]*0 ȊWztm%OK">Tل~oK.vHL*q0}a:fC?~+ a1hơ6,|LP<5:llgPN',&g 4&h-o i7KۨZm3 k>Ee(wƼBXhtscBgCRoC:J甎 -G%Ejp[&/q(LDׂ/%-t*Ĭj(W( -3Q L4\;k71g^b -1w1oM,xXJmt+!y/~Wm zy ,L/*t~M/~9mV7F WŻh℅u ն1BQT4N -VsP=^XaM,fF,Y#CCr5耚;{;”Ă4`XzXHcGo 7[SV z9 H󭰓uч:Dl̊M}'TӵӞ(ڈԲ2ې -b}Axm#Ly28ᯀA _N1ah>*SGDL@Xs[xR1lwf읶=k;.=ǯ|j.b-j;k;-Re(lc|$95w-t=QZb!j9SA4C|2V5;e-pF !r};r1<ގ^wuy#x<~o-Foӣb¾ܚ}n"flnf-S t8n'r.VDfBٹ2,ƀ6$!$r 9+g1xR ӍjZnR?Sea$zݔ}T+>>&.h9 8#qtA?f H#2!m;A,հ!<{6SIJBb+0lǣvݗUvg˙_; }~w훵;=j:7zA G*8SAiցPzYYKc{];`{I?U!!AjI *m7RXh6n11gVb&#bIbA]'7E R3ω>=u!3s?mx6Yh~x:9WC39X0WdM󠑺6;vC39#ŢFܑ;Ӓ-$!L؋ zy+6LE*q0O>&qEqt \obW#Oܙ)~3GΉ5q;U˸MݜQڈ2 -HK7hxm|m>Pi4q.`O{ o턶:Q>F2QAd1W|*l9w0wIZT1z!1g ؔG8 aIˉ[f$Am AI_9KOa j9R#usz`3Nq adC R7+8L?rּ6B7ϭ9J-ԵG,?db5Wc$}'ٛtImLuLnc2@]hP1vQuEyq\byԉ B,XCӳ!5|wݷZ% '/=<_(8:V偢+Ni*p#^:X 4 zm 9(EGi9QWHv5z-^^-eDALxvNUI6͌-j>!~_z(k`7u2CQ]A*AsY,ۋy~LE5c:7i%}s(_ Qݥ|s ܠe{mvMb Fz9iLnM>ԛ+>TpBS1xvl4 яԳvg$y1f 'Nﻤ̣v1׿K_7^\ gd7Ulzg6-~b z)n&/m)[8)1)-򔾨=%i [ydf~2d BQam w0 -WT#VX-o[ш_vqBDvvkBPF<%MZ؍7sMY*.<\#-M\< NJلƾmY8GlOϳth 1HВ&w S4tjfTuYΒWOZÿU*%SwI~4kY#T-Gi\z8IɴY_Ѓnǹ:,jS6ZoeNڏFޭBSMl3$:3q!˳91{_mG~M]`*zu)u|cftOu˥mLHمcgG^ L1ίH`xy nO[풫O,^EJ,Ey*eb)`0E^|t<@y?gi^Q0 -( uai  RgRb6I3ihsSi:f7tG5Iv;-6-e?tt1n*5x~olxqP=;Z9vKٓU V3ݶ²o|Z!ϔh>[N?Y͗ۗ"DS%Lf.8M5?4RTқ]Wބmٟ5yY]{ً[Vp/m 6uy($E.b?a}c@%Ulᅽ^aֽ00C߷^6kp;59}_9 ]t_r:Fk?U;> (+?"7O\SE|Kg]lyfB-a62W{waJfû?ԌU6+]o1U9qF}.+ e 3r)M1 3C -Sٺضd9gVB;hx $};bݥ%_#>wiF>էE?\Camg)shxoga&75ɇy׾lQa~ &VIm· lgagSחsUܧЦ_Jli.ٍ\nHSݩ:BdAf*}xhfn1srBtqгM´Tj j/r4 $n5r=Hz{oUV!ww'xTUq!n4hhwq~ߨ1ҡTR3#}N<8y/bŻ#p׿uݯ7}/w|y|F|W>ᘣp6ac4cYݴqf]7k{׌3Y'dnų -1[\9&cf W_:q/<7./5ܜc\zᕁn\GeB^T>fXrr6kz{^>s3g[꣊p_R4g>/:/wsEgbTw(ڝan^Irz-_o[_nn6HO ^=Zlp/wb^g>E"&ՌᓪFN,]x_/Žg ݟ} Ӟ=_(; yX`~P?)u٣{<<+?WY<~ftyf~Q IօZ{XrI:c5[ܺv#ظr!:NdX!G%ڪ ŎČ -I,Dh%'y矩…)|rF{k($U3j Own|ܐG}oߋ݋4)L:ۙ\: ']{eiG*5޹ 7i6ݩqٸOs@`9KmxK4l4hW>930g8rmpOoV/kDc%iW8Jɫ,Jx&K/׵<\wWOpز.u7rmO7qO7}?^u~P7;~oxpVx{?ׁ? ,&nm<0{͞O܇ ckFe B4׏f0MgU.܊p7w<.<ˆ-!tx~ )&Yue&<w?{~aNCb_c,| je%ڞn G2l􃿹pL\}˵=?n:t͎{=4-,qY7{f]vi\{hvcnޒC';B{3Z֟sZ{@sb\tP>4[ڑbYgh%/6`?ՇgQO~7}Jd__&b~Mx[2;Gzmį;0ܡhcTI*mWI޾ɖ;6k6/ޠٵiKCkt>Ֆѹ#4[mp%h'78I7CqjppE -07's{f-6A |fxth&ld8 sRũ{(/]g|QW|y>if՚=[kKcYIp4:aL%d~SDv/soX0JN 757nۤBjTxn6X|Zvl<⤘44uRY=rbVDҒLiGQvS|P3 Ny<sШJƜ4 [K\G{"줬X/ݩ;^no]/H%B:B7;!ɤa͝~4x^ J "Ts?r;7WVrW'W0r ȑCڑ:\j=l?Yw+:hfFI%3M)Ngrk GZ>V4 [+OҸQfB*]l=n]_!6oI{f8Nrp0>ҍo}Gӌ>+7~q>,]} -&Z8~g]ybfm}Dr'K'wD.g>ېnd\1f c*F!q4Ux`taEΟM7 ͸pEfʘb!s,/D+qRJ~t꣄BB_;Z, nq@Hs1 \.\7ۡ!uN#VQs?]x!1`$4\3=WՋ_3_P#@3*?=_,tJhπ ³:?+w/{uݯ6a@L&6{:y .,pwY- t`&l\d`w}h>,k8{r[ۥ<556 :"s>]l J.i!fZL4]yXKhɈ=0T\w\q~i"}UO_0$8J\Ik Ah7_}A-fU{,b.<}㭥zV>_iy;]4׸N7c -Feu0ӟMl |Z>EiavЊbr0 qpäˡKM%U…ߴQ(0 B)o-jxfĢ3gQWFZB۟;\b@.j.2<;12Gz)V4I&'p{%␉q2)1oZ:x_B[.==zu,4gB"ҌJt!>=_o_X6]K;BbXfR`xS0oĠa!bfD>tO1ut< 5'Qfr3}@_9ZNHHV~fx6۷A7QjuTmiU_Zf'}'xVϙF҆lxo֨Ո/ʟœjx -~̗Z,>vNb+GWŧ5C_o}~͆y5hth WVriC*!A/?g*'<@6G?[c8l8R+/-&‚}!y=|9w'KkՀ;z0i{]E┱<@Le1=lN>0īdM(OYʱ -Fl-eLEJ%rh`UU]YV[].0Đ(kԉo+,o5B-^7|gŞ;A3'CS9L1x{*-cTh -)NBD> - )1K/lM,.gp֗'AW`1zWN"L~phեc1-BRbtLU'=)9.{Qx)7W.e=ssb.\mRF NN`۟m3xm$jB;.4[4炒p\D+WO<;5lXVͼ-:GșBBhCK8‰jF-}3P4}">yAqg7{_SѩbHe˱eR%|rݽuЊG 4h%)7bF$1k2U.^mkzty+XJr=Zy 9S)(P_FҊ>0 - -:bW8s\ ~c#/8P3]9JT-4;pg^{;'h2z*,8B|(褋%7]z)('cv1T`Aq34z zۂ7+ҟ,91G9&Bs~eT5 ZHs/Z0 bڰ{ Q}F.W^bE|B=4Ÿ|YʲCnރ g 'g=A`챘1 -||O.' 9:&v]ӝ·Q󂙅 -g _֛rz+5#t~M脢'B׹,VC׮BwwBZ9^?luZseE|3XP:[>fTwq ӳ{U|i|;/ooJEP,ҷ;x~z_RLjE@j@p=CfA$1b}:;[+ˍsJt(%ydhXj5fؿxZzu1_h `^Pw9z[k]-:CBs/JIn1t9ӉSw`w=w{Mw7Z塾foxs<!( -qY1JA!6Xwj\HAhBYu?ګx81`gʷ'X--b9 Q;0NHS`D9#HSl`@$cWX!qtq f$TrSE93R:0;z)]~-fvyiL wyÆ{W>˧knhbgMI`g R?ם屟\*$Vm 6c .jV"$U9A\xyYھag{5hX%iY|`^K梁-E#Pk#XH\H.$|&hz;kH-U< -:IK.;1+u'rde ,g,CLZjytf|5^w%Ьy4Fژ)0e;JJ3꧊CO2j<3IN CR#lggM;J߰Ӈ3:)WNUY<#\[vL"bZ8Ʃ73_oG]{g-HsL -jطl5xH\{n~m4u_nw̓5Ӄj\zrZ$?Tkk*f'a=\0X쉅즩Z kŅB]cy2{5w#vNR!&zz5,#>Sl !6J`09(e8|4Xħj4MRlwt:0~H>yr}7)(gehޠW --n)Q`|3 rRl[5<FU3bfL⏌,b&7G;KyBUk -'/L,#}-_ Uck]h+,q1f";dabICo_:^- mb!h|>ŧ* 3`K# Ycƣ؉|krk V"b'k<@:E%vD?CjĖk?X\R*/h{h51V>X6t6E)>;b {)@ʝU -yY. "j;׬V-*I `T]_F}T&>Bb$#m#=Yuo1q!,7aO:덧(+/-;r-;Klb#2βpZT̥&6!bgO;+2+;+ ;+abTYގ충9 X`b>&@gu̞jfR!g _qe}l]K%"ێd!Vқ&G?\%&9` /j5"~]g5y`0˩N#"~Ei.?'JW6UA-$2\7Q-/,n'%ZFN%[q}/\d쬤Y6FbgY\^!~ZujOAl<[S=vt鱷|x,KCZaZrŖO7˝v n[ 

jNg/JhܣZzy1j114͖kS6#X-kKYlwWNdˆe'Yq!y߰_sJ%]sʳKܚs[ӋwWJ/,RbXr:ţV&nLQ3~CrÑ լ -0-pĎF{` 28Au"sx.ri,_]}ᚊEbXN 4#vR MJ'=[{LO6D;5L>Y>H cPp|)3DYxy=ȥ (FF]{{b|;K>oe1ߐ,Zeﻸk\=XsԆk'-\FX}_R=n+'RCJD!(J:= cy6z_lZ?\K|'VcQQvyVG'yj41RKFGuU K5$9I_E,qxVy{z]l[0h0B<5(#i!y -]쎩`nȅ31cB}D+QrLaĀaT[ bGڋI#S.=g&߈"q` Ả%w| -4RTՐ\> Xf5} /*rٵ<>t/.FOCj(v?!DZ굲WĨɮD/pސE䏠~¿Ć;nm"^[|\hw}PbR8ҔYcxU1l# k61Y= @ 8CڋK똍mPB= ; W=bkTd|mG<;wFÞZ~m)L)}Ih>2<z` ^3|>Kص/陃nRpp\nTR錡˩a 6Cz4:Q# C~%%X=9L̈R3 |+_"&Qō׀99Z(18 --\OebE#F!6!e+Ezjzۘ%kƞ1Ä]_[!oƻܒ\C%{7Ǖ2 kK{9$F<k$֍Ź7s_+JמbMAWoZ?ߪ #9,磜/PrOK0yo[%;YOׂXj5㐳ag/{¹{]o>-P" QV8^88Ea+DL;6L' A\YNh.ǜT=3gR~zz؃DΨ9hHxmkџQק_\ %׏ř1"VIǒ~I -XQv_A`m v|lGd 2j/A>Bj0 f;/'Og.sC9|Nj|ۣMyJ9%UB~=ҁzb}%:q|]~EzJIb֘o2яM9r4#?I.'[ -PK[L81ř2?e*6Nƫ+Ď϶J>a)qX,+sZ>^O5'cI|͙\ǧr?XYT1<|L1"F)C|8!)k&V6G>C.Mk_RSJb9)c5yFK{k5:N< ]\wIoJ)6d`LkuLY92VEܢGZ:XMYAWLX5.\3#O#X賳5$k'ȋ`Z%49Z>'~(OT$o,mnGNxeاK+sBv6]ˋ\bvDžDE&;oė9]KYbM]\ٿ3ط8/f?|˴~Mՙ}36$8$}QhM~ɺ V,]bWZzuKW-__/%//9d/G{opY;ǘŞd ~p~ٮ$wg^q^Lo?^:W-_vyڕoO`?IZ:̡W[ykً\4]lAsOg>wYgVhCf,k92Aڀ+/5|f}=ƃ30Ƙ{].ޚ}{4@22"rm)VZσ>,ôI>ݼ w4ZlQ߿ߗ~[ -1ヒ!ebH "cv4cԕdzs0L-Gc8zqLC~t`?c`<'i˪129"Ö$(g%4FDIxI4 ~#HAX!I"Dgؑ4B`#żьS!Gi! -#c!$ n!V8 oǒ $tKBBU5DㇱbWWKO.0dwO(b&[AZ{l߃P\#I(}70Cl$9qUcZ'!8{!Rt8>dMh&>> L8>O!fKG2CF2̞Af^Z?3w/U%Fps<_`^Qaggה?0wگx)d]B[.V0 -ݞ/荡ti0>-cv@9Vdk9rW=Z(́h6Ys i^_3Hu@H`)-$k545B/Y Վqs={5+I5NjFdCN3 Ng$9R'؏hÇ$Z-@"jCfDlGLHIӘIe:l'n1&dgH*sdI2udw -򃐁}B,H+ ˲c3G`Ҙql -|<%(Æ$NȕT$g -[CFqb`$$f E12o(=HԐxti'Fe19"И^r8sT6cQ4IE1y#tv/=qḡ"mi$ѐ>]/%&QFxAVRzd)"K4߳ݍ=֛!R+G[0v/,9Cxwф1FZdL`"[f^QU>ȵ0~#D~`-2{YH6:Cd)0AO(FLՐH Kh Ok&Ռ5Ė;vjX FQ)$:XcUJQ1Hf`T[Bc@ y -18 -n]Ըj -55z=$?Jo2!فaf_agwO[ 0S,oC^6&Yak #R4^sH/`P1rCՁWY?]{5n`l&8ǖ󍱄<ɠK2Ga)HPbG7BjT MHZ9 c-9'44<ӆ|Xb  Ҩ4ojڱcZ;S!)Q/,oRKNχ/2$8B -K -[!a4UM-.-'׎ ɐA0cgs1hcXCo$^ !~$𞰖0_\L#wWa csB>CR9YmS!S1#,fwW-͆4hh!c*'94ÅӓcGbT|*m;Duq?s0&sh4bA aq!4Ot0J_s/],d 9 |Pcd]4d2'1̱\ L"G@zJ:raRtq>ɾYI̗h([> ?>ZȠ$-cM .cFÓ(T#hLw8#X,d9avgHð&6C.IQ)' pIB%qɵH)`P:䏘mGQe6@XiSŎj^3wiWKLbl+ -9N$S3;.4ј"ls! *|bCc I#E鞊1SdHCr$i|9̾CVͷ'$FщY`8$r =9:df*I2_˳B3 A#l1L ʷvx >K5mdW2xaI6P*o.jFZo{mK75pՅ4U LPlyEtQ੅PR)UȵWa4]A CF`e<,~d _#KŐyJa>B5%ȃho$&hc&l}l -˅l;|Wl(\K>Ƌc,~f %foZht_JQgA#//,62UkWGbV>H QHy@SH#_LH'fB^ebR:KUWcXe/8s8^dm0 -AՂYRt|*d7h<׶~j-lAV2[dE 3-*j[0ײK$f BHxkɅ@FY~L##VbF`ĬI3:&Yst bL=Zc9rRYMfd;(urH(q5cX@|TYA~YvrjI9tt;$ņb"\㍐kn,;|@A3|jH!b54F>', ߢ 9,,Sl%uh-XARnK\N -Mz f\P@֜<7dX;X;(Ll⭰HJMXnQdVC[&|b֣t>qG+$!dkc``Q@G:ctKj $xmpsPsC}gg!&\F) RlH'efE[b,).Ʌ$[C2ZVd-ћ"xxK/J`z '$t)$}8$ V{< gi}eVO:mqӟun&Sc-% 7R+JG. -=R` 1#7άCǧ@FИrjT-O5d  S o5EYd 9NYn )q*ǒe1  `ggK5W$\0~'|"YPv.dd@מy|'5-p Ŕ8RQ x 2ڧ4q B>?j H5pc$ւzY\p}&[~Vִ~^ -2²m%SȐqDql- 4p(c8joVx|4޲JYC::9%|R-i1%|',)Q/2}Ȩ;X|(!#H|fd5.$Sqgc?_j`5Qyꌄ1oB;Ÿ⑈E|ېHA -{rzJe'cvtߐ -f05[AiɇBT9BPS:Uk<=C5j!$Q;咞Y|->J,~&?5PJ {_AxOK5wWBP+X > X"OIқ& ?B҆Cv\hb|+I1Yh7G- =.w<)w\Iŕ)KIK@:S~>+Kţukˀ]pכufWXS`u?Iȇ&Gdhm2G)Afu dѷCd?5$ A$Sn-GW_F0daqc*G}ͥL9(% s8lC Wfvߙ^7A-=g@rQH$.o$OZ?Yǝ/_ ca^j9FkEXOP~)GȩOIE_R2z}襰|!Bob*cLrC,e~j}Ce@1ɓ$QL5B>bj#_ GA6egL䦫-rON횄\ qB*Hnxz1o⢜:Qlxg96໤bB#b^a`cwW_:TjuHא2EBFΗXNbBB zv1ʿ !DysV4_H"{%ᴚ@B(ѩvX;_3!!'.rNjxG ߏ/w$W[X]yz$!w\fn] "'5+8=טlwm"!c;o[>D2xlxgD8gj:7pdgaF:rnIU^" 9Q#?ʅÿ=θIk!)XaoGݕﬠ׆CÙRo_$5fCOg%'B/Fa}`.m9|H0`!p@֮<Ѝ'( '@M5}Q,[lVo,%fB.\t=/ OCҋ{] g ߀+O{BEh'%7s&Ր<0 =,=7@*ug GA6#L.Cq\NkNN[c@D1evi*rbk%BP>tB΄z {TD[l!#7N籫4 Bo A2r,{ !pEB䛐,ҁٍ1@Xk/tNGHlRR?ow[A|N{?aV1,a8 +ȃ=:$MQ =<547CèrEC46kvl٧BV]P^ME {X=ȁIZ+[GQ $7PBׅd=^ԟŕ5@b -9p3|I#HPMw5ҡF7䡤ߴiEppJQnQ}f9Jݵ3!{ - $7Z=Q[JWWH_A~{(dKf+=o=p\BB_l $qGDL P.l -!!rMH_oւoS=a{lXI=GL=:^-<7Ov JީYrFda=Q kK8>[0EԍE]juԿ@x"CM Jn* d>!/+4|FEAƝԪ!{w{j\vոxJS| r/GK?cMDյ8[K{W?\FJ'Ylȶ8sHߺb "w$Dp!!.W25[O<Ξ\Ӑ4ـPצ `X3q梠o6|O Θ;)jQLHn,O_-E6}`tZq$ѻ 3-;uoٽABjg7e'?^vxp6xC􇒭|7HfeS.5Nb\sf\wkZvnX젳=^ ml씺܎޳PJ([˵}hX|ePuk1K NENG9!?0[jYj{5zlף\MnW:q7\|g8R*o.R \l tіޢ7S "z &^XOG ?Y i|HU~R,G@߄`5[j8rM6eyz$0bAtDTp(w"r QR[Pg!]΄_cu*I -K]'>z=`{ݔ?v=?ƹ`sk߇4zJBG+p`;'RQxj${QwhO䂵 TY*&w=jMK|rR_턽|m~‹jYW#3쌈#@7Vйb%~ -!cgpEReWrf`O/aذGv/qV >yjLHʴQX}jRsc$2կWH+Q_}c3&&A/ -}|jTǑG[n./}?SxT&±B`iov)t>&5|V9raR88OU`O{YJBӁ/G=obr|D-^lldXx~pЄoci$՜["1P84ߛ*մsŶϷ(w _`N߱vO]9LZc1[$yS ?ADӳpF@)q<孥rFdj3^P gguYƊ+KAo>D+Sa{CQଂLʁoPa,J#D 32,!rԌh zƩj٩8+ሜ S}߷Bd1;>؈ $ɷ&*>=84*WuSn RVu‘3scq&„#bO; Q;A؝7B*Ȅ,8pCtqvk]/| YJ氚a0.$ - }TԔӎc r] p&c{~bp*}c!| 1@wNb etΠ\jrFQyo<,2NeD>{nE/<7*,"tI.΁gXE} -[YccbBE?`&g|/壇^%$u'HEf P^B =O;cP=(\UN͝.~ҹ?ԛ/ =Q)*WjtOqcDtv]3 :'WZ.u=i<u*͟l#=n6K|.<Gr`pw6:r}11;SQJuE <KpK=(\V?Mr -y'sWgz<]uvooOO߂u_xtJW#}K87=Z6t<'v|&WbvoS^fR~s)jE߼ꉾ};C&`q3bh%KgSу^ mƂ.~,?lv|,XX=LPE {+;ω+k]^:=-=!)yd΀>:7*h;l\.P:̨.-9E$A 5}I^tߔ.ou$$ D9Zx.]Ev2E;P"ydQv(>ٯ%!LvCm1ÍpeI9ї-- ';u3A<&,i7뭒z"i(&|˚/H_tHwTY*r2TvJ|^|v -P1<~ZCktN!jvz)7nm -•]N_\f&zxTNX苐x:,uģ!~mI:J}& &+O϶xoW_h9Yg/骼,.V?B"wTK:]t^A=SZ(2֣Etrj#>:F }&cN!jN?2o~5 ->S0[=CR5UoOU!N5Β=#bAbbP  {4͊l8GGG[„=[>;=7x'}" -P٨.Ci(>*~Tkх=ɻjG/%ɍ:d[u >ؓB0ap ^Q43[wTٴۢYQEq6(ۄU:qև9N¤]ogOPhE(CK_ %:tT̕lϊ MpV>x-f2@G-hqiO^Z skbY#꒗fUG8O,.ou2( WoDeg$%Β:wE$EEW=6ě^ fh5vŵ35ҷW|?a+?Od谨9@W!^/[C. *0*~|%>|tG[%"m&?5NA.~3z۸&zbcA^ZXc-)h1ޯ4-3'u'س>za#zrRIq%V{8'a^uڈkj\E^Ҷ}%1G?u^O={?zUh>PQ lp"^Q坧z/1V/uJoJ:לИw5=XgnHQ9:'l8hG|&REymA1 ߆b"^ǝd]E2`gV=6aws}FC|)e) y%&K˞qv]εfgvǻr%U#k:6J]Ck -ػqh}]$õWqdˈ3c~#$~3{pV!,|#?n7RYddM9a÷#d_'qJW_uWrFpP1ߏu\K̬IHM -iz=#զ~KMsg+D3Īa֌䣝qC?Ev8?K>\GElC+o{w`ך.* _+~igZS# s~Ɣmm7aOG1IcR=OT>4V875y'4fJ:.M]g͇Ke-/G_6G߬퍽TRRs9|sBj`>I>1g[6HtAJV6kydu'ɪIѮڲ]Zen1a1תdwb"+[K.D׸DU48E;ETŸk**\T\ LoJ0~!i{"Ú8%xw ȇ"ѣ :#-%{A2FvP$ÍOՑȀ;H:\e6X+#;z`>` ǥm$ufe> !͗3϶\}', p p߭x玘zunF׭NcQsdQs:טB82k㲫ҫ}jCކs+UFhFD{}w*|ZJBͻbD[O sG+CZ6Q]W_,% !5L]y y0dPDkM8y+l¢x¸ mq)^ 9۶ ٷ-4ů981Kv-n18r,L|VWP!K=ڝ+BwH.oN'qu+Юオ}#"BS/7=SXU`rbI@|v_[S,ȚiG#cXvw2#BC;o]-Z/Ug}#qBǫZ1^1w\cJZ2[${[3OfObCj/'$W^+u?՜{9#h{L:.ܲL2!*i b!k˶m9'sS(H|k5,}ՙ} ;s;r̤9f>zPr^wsLiSEkƐm'"s2=m'mI"|Xo^bdobROM-ɯY^h3!6fHQ+ssVO}Ǿ|0`Fmx]+qYwSc3b/^IК) [nQSd%t˕4Ϻ )7}qjN>yt:ڲ8ަ0=-3)(ӫ!:xWN[Eǝo:ڦA]WnC{bbBOb@Sp -=|s72cxotQdle /aj|d0n*Cbڮ܈!G O0Z{--sʅ>9b1# 1'. (ǢWп=kuy)r8l,UY`>Ps>_;n.4w}Ga~>sofisl``Ftvfkf`1s{Ɗ~ެT/{2跕.QOKcҪ}+/ՆFH͎,bBtx2РkkTBCp8C>- W/z7 L3;0tAakдRo /^Z0L$0LT*+UMLA('i$.icty__g`uW p+b]hRج~|e/^+{\!{S}'cw7ŞљU>q5IL}._΄Ɉ;١FYn^-jb~jm~J^?oڶV,Z )` "{WRw9 ϊIo?)M׀[E`,* %> ſ /X|).?[ػE>/zy;;e^%+]/׆$S_{́/;^p笆v XxVl+L:S97ɳAz`2+7>Ncf׃;Y7F裬C!:'76'%Q~5aO"“m>zSP)K,=֕KBT\xtg 7Pܛœf6c OJi񋀊 -0Uq9 L̞֪yG(ie6Qo=`"ńXxӑaۮȠ} -}e #נ*en17agrV|%xdPTǭlsgsTvՙ,``M@yFOfM7K,jv*gVs{[ۡvuW#Qnvl -}|_.,:P}e+{#-#]Ω -o_~)hT銪pQS~%L@ez2~1i+=X~:`+.Δ? cIH|{Ť"Ībw)Şy%-} R8z\LXIn_~ gqipUplgmsVhx`2TiKU$` wLVlx",[]}\%Y\B /0+/wm|#Qfs,\~ٶ0Nۅ83(spL3g9V97y$``vx֊8qLUm0w&~<8Yb-[^ My[/oJ[ߔ>Tx$Ը% 7$46Tx#Y3tSmϘ&wYcyW9U7oo3Tm _&K7ف5`]=} ՚eNGᦼ@>X;S}[lWG2sݍY]u]ͮ+=`-XN߶͟mx,?(+ - +8f(,-SNe`m[9;}j@䀹[`;X kdO[,uZ%,;X.::]hL9̽`{=1===-g̾9S|CE -@uئg'È={± `ެ`R ,v <xZ^`3X},v1`=S՟0kǙPߢg/=Ooz]TjsɉŰNhre!E_]Tì3gӿ\ 0z)X6q1ˀRAڦ -ϐ۷̞ L\}#<f@,`oKx l-+qRFv'#_~'y VQ-CsFvzԴ -DN1x8Z\p{PXTnbJuAC0­p3 } -[nTj%:@uX9PXŽ{9j5VfmZ.G'}gkKCosOmpKlrK_S֒d wҸ)?cxXXCY}5ٻτ εsys`R.XKڂ`0l:@~Oո->f1W٪p]L&..)'NP݂10TwIcSTT:yڮm<"/9ͲP q5u=-6`XXK (m9%,YO%E`i -6`g -[#{&~fԴ>0QƆ;ߊkʉcךRbڛˉ'Rbc2.D 8G& 1Skưi@ٜ"r/g(-9|8U:S69K 25zx rh"gjO=fVOۜA}xUMsA50k52&ӼOIKIУӭ2Tϡ嚏QZ=5-v3# Y|m3#Dy``:CAX;U6Q` l *M(nr#jŽI;'p/өh^ɚ ~2YZ*FFװu1"62bP΂ڕ sgڨ.ƚ4$7hw{(lՁ.1C~}*̇gYjXy -lz"$ێ]W0e@%Zff-}!Mw(lX=vוxUN9ءuV˜:P_J| Z/bQlj^Dj<#?8iP0=nN=lNJmo;N8%8Dazi\+X3v+u?Xwl<` m`j~' vb!r>Bͬ/թc AæfsEoN?\ ;J/} o0ۍ+!5e$F}|o*N;hq3a폣Too]DjMx%KRUm0 Ser0oNb تkva~`OU]:]V1gDzaָ?ڜ83Gu3\ ;#1X3з bڄ1*f U'<gX׍=w϶f뾓b8'yYvK|>NnS-TVn<eZiwP*zWcmk1{5mY~gLb̫>,F4@y}l׫btm99%U>CFczL^cn` >Jt#ժND"r2_|"j:`!`KZ"pQ5 ~+̛m*< XSW;h4M~X ->łs>1ֆ-42c^3c)a\Lz*`x <킚߭/GƉ3c'/p1?qYx4J *`/c/wwLDJŒW!^s*m] }|F -Wr+r>l*-S:zXl2vRuz[_U9BU.s}U׌VrÍdprW84xSpAܓGg΍zO1{̨1Z7ł|X8) v>lEnM_Q/,JGܑߴ-Gm 苅 FЙ܉KVd)Ø~졂4_N=T˨~d&FÌ)bo>Q_#/M߭ v8gLZK=Oud:%[ >?xskǍo?bԹ'g+/Jޣzn7d">映"0=`h"IB}Dr>7MQ1OxD$eӱKlgc.]*i -0V9/ZA_W._ 1?jÏhr^&&KFO[}%ԃ1v!m!߻ɕyOK.?4&mruCaBGl?}8(k%X4p6,h{K!l$} ?XΚ,\ڃǬ`X_Akt# eмY+=-0 -382;c%_q -yc{ѱf.ĢMG= lT!#c K1%Kh}:d˭DffEƮvzAp^ٓSEM؋Wx7kt''uh׃YbORR s"JN#AU٠9[g񽥢{6v,%&! x1ss -^\2g *sl|Dd=-} x\^(5'| nݎ'<&Ly6E4{]%w7 iry$090Z/c I)h tv5m@ؽB,;'wSl+z}7Xb,j"`y*QveNKD{0[M"a?tG-I~7!U'c rt4m+ 4y/ ~0_N0~![|4u3NnYO_z'(o=VcOV$`umĖ -V6g=T,NyNCҏͰ|,.?b,8qO&L w#~|B*vu<3Sٲ1bjvެf=`txwC{n}uf[f012͌Q)cy1%chtI1K{ -`E@s.`q ysbh"/·89}^Y@VJ܃Uɓ>HŒ}Xz"b3i7G}eq^❥er+ImRZZB27碕!W>6EO:nOkNu,s6w}f#s]ǯe,9^9 M1l6ڣ4B{.óԅ<'RqHOMF{ȫM7>\ň!Ҧr}~9\+\H2[8hʕ<:,z*6YP׆ ´d֨6STx7~V^,ZauoF&`Ƶ0f,8=97c6F>Jjj\˓!vإ $@W]tEC@_\ -]d.7[I|=T^yAbAg-{,BaGul "82IxuIQY``\HQE7b>)sqeZ&׿$Un^SskR7IH_S]0$vn&/:FNr;]+Vw#8p[qh/bIU~ kio -!},+y=rϘG&C<Ca-m]Ig>7-AG5AMAt:  By]OmEJ.>Lzy/.͉Zo@Uc SiL -]Aӕ)y"?*WοM@ #C4aN7T13W:=Fgᙕ¬=Āޮ ٤_" 폧._Y<^hK>z-͢GtmC<_O\~:A<7?bxh*-r86xg7t隊vBPxVrB8e)jt - M ־C@k~`le9\&9✺4Md}\ mE/SwKr{ -}m3LIXtH7.i0-auRǠtY :$OڨMO9B&h|6IK~7\g -OLDbx"C[>X  +0a\ xR^2]qψ :ho\ڪk8)W|| -~>l:~A6C | ~auaOJ<ͨ؃a鰈WM1e.3;oṟ%OVW/%1n庂`rn"8o{a1&1cs weذS{ω|sy+蓎Y([IEqY6ʯLҗ"nB{$nk䯡N*4 Y!8?M=}xPQ j ->SwpՎHG84.QO7b)M}A=vYM\A4!u -{ɷ>Ľoq\tԹ8^p칈xwDOGۍh -7bHŽ{NM"2a<Y짏 +\U#25=\<_mh0m0~:jYt7|X*2z~?>tTIDUœ &fb!wVbVeY{y7 u9`۪`Ì`M`СC@߈c -pz<< tVFA_mZD?`~\Tf!<]11{DprzGrFl YFy&EL,FPIBv[\,6E Ó>#W RJM]V[C_Ve2+gģOx1)cl6-\ jFo -҈Dlx%i:Hw q/.X=L9h?AWV}0,$eoaZ>!Q|JIevO{\6y -b_lƣn$  -8DA?E twey"v,p mz3g%CG8=}Xo܉ 1a^82?8wؑ߭e”=G{JL%jeIM`DP_h ڛo-_5iӄ>9yÚ(߁)X`çКWl%rZ+3wYsM̜5C]GR^h kK{IӊHeX9*N13'q]ѳćƙv/%KI 1pU<0w)rhP's; >W*jijZ5D7ĝ6 wP&xh;B -r[K9HϘXݵ[bah*p9(cCjxlvaGKT448`@@:HK 0,$;ET.$ 69ݺXVoe2KX(=Lj . aq0 1Ckm%4BXtH8S^ga}AB_AfQ3Y§Ĺ I)8H醰:S ǃI_j:'tCxHi3!71\FVn(@5bF!|c5y1}1b=[{iNGQ]Af>4gÌB3='Q> Pނ]?DZtwM`-j2D^[l149| .<8FxcmuU%> LhY:(Ѿ) Gq^' lt )`LXC銛VD,g_[ƲT㥼t` -::tgr )rGW|ዳw˭}GǚF%]̻"^9*߮'Ѿ| =(50| k0*QqZ<nktb)IFHXrRgqVѐk % - UӤC,;DĊB%O:E:Eq[rZjqx3!zQ0'~~K,C {"6*&bk978j E\عigL$s Ro9q𰣒1B - N2 XG `q4P>S *ˈڅtP -` Ⱥnˌr8!j>X-Xjʻ8৽' l0?ucJaJn1~Wd'oBBHXˑ6cQ !SyvʎyfbvTld.@1( pSJH)hϨ9H sÊ۹Gd<<V.csӉ.c1SD!V{*xu97ҨGdTS̻{f'_oZ<s3'ӛ5R z7Q,[%'>=T#+af}Q1Щh -wIj#~#gnf V{}Xj`. sH-!&7O#~)bgay6 -@mcvHn6Ғzo=.K^_1wOL0:+ظ\gcG*΢}He> ӫK8Ehq\9HY* -[tɜA"oАm,)r6`ycJSO6-]tИ4&"}e5O!R|=F*CҕdI2H Q:\Xg@|c+{s=XKƖ> $Y\ZJ!'¹Q䓴P9WU(ӤS+mbs2WX41dΰ!7h-:)AظKW#ĥC2f2;>H/RgмGkN-Df8gSOj=H(Xݫֈ{ <'m, pg~|1o˷J ZM񈢍#a&ZրHwKCDLH"hw|򰼑#m/n_]I .&rMC63Ȩ., WĻ>=iQjga0/?lbj By.>eIQ*u_x(ֲ0".>0~KҠc\g JTgOLΕnD_g6Doq06Wj6g=&#m -Vf/Dgd{igX;sO m!y1j6"E蜨)kױH_^pAaJoAၹ+a9먘EiuڈKeu"#ڪc˝ǢgH ---_y5q[kuCwm̮+'^@k|suLüuIV9 -圬^1Eby؊X6Sc.WΎA96EڍY,طig#,{M{GX {jg'al|HpJBSBeR -m(eV1vMlT"gBLo{rF:[0NiH5rrj:h7XAyZ=,L'&Ҷ?YܬPR^34w؝ YGc{.j|HyNBQ⓮jgE/ s 刴Ez&cN 酡8$;?OH;e:NꖾPb'3{4Pt t,%^/qw"kߑ/wx8~~-sbV#&OB[=qrJӴJ:[7ew;߹͚;/}44n)E^ًϏAk£R”QGYB etTn.sQC~Յ1Lh'tgcO64/]KdBDh}Q-tιB`jg (%hD.G -8|_lQ|X;v/u/>7q|4f=b抽 It=w i|sA(o\bٓ]DֲmIu:VXܔC2&7R4PNE=&FXm,%hZ@<jN:$ -p'7,YTy-=\N󠝥e(jƃjSVS,%֣g,?va--%ہy\8&rO4c@={+G-bt>L%=<Ǖ%r/ET4E1[  e%?@Pne -@FϾ -k-E\Arrۀ>xPm|F t ' -hsn1e 6簇1R|4hR\IC|.e4V¾-T; ,E;˳jg#]\$b!CokJRY-wQ}ke|SKCW@3G {8_!ԯgȻ94)uKSK*k2ԗ[8R9'8>f0Or},jg5$TtͰp  cn-wvKJ1RґRhXCC3ŌˋqgARt b㨛(C z9./1Qzx-b=fHAcEV^߂=wq)WdktZ!iGKFP gPu --&aErY{EqN~,8[M6MN4pǘol|{+]!U |2rlTĈ cb{@_|#zb\ejU{~HBS}Kc$kTg5Oa=%U_R⩹"UfpҰ*CS/,pDw-`z, :N}zu{"U]pgRۄ?ESG`|MXʛ OҙA?N@GLvFƚ>Ƅu u)6|Enhg)gť>z"T6\,9_NzVrrMzޣ%d[1[IW卧X}h9 i亣K"˾ZUR|﩮-\vE U~qŵ҆佒Oτt>$şڹh ɇg!z.mP}'c\#,!OBI&֌Bo5=ttP%'=#k-Lo.Cf$_f 4P7 QSqB'Y9=&{E5<8j[zxmxL:>˹51|"49-|ŰqЬ}.S DNI5 -$n xEqz>:~tSNBMC:,)׫5^2sը j|A 8%| 'vyu!B igˊ!yB+MI>2'jgEm#s* -XMy߲oa4ubT>l.Rbc̫КUx E|mН"sK"|YJHBq䅡v9a]J8;,yla}ݙ n6Gک*ؽukH> _$'cIr}uQGМ9r'CBLM5CAw#q2%a|؜;Q d Rjyc'A -&Q99M9@rYԢ8{gmhm~rI'g9+{AGkbѩTB`tPF/vu֬!j3СVa փ-@  ,.Bž򼻋f1Ƥ3灥Dd} /ңkBĿ+-gu&+ -+EaT&2﹤8I޲xœ'P޿{ϸMTn]XS__ĕ^Op3˥XD}.0+ޗ2{ ' q+EVçҵik[-佈xU{2P NgSܲG/{44UpMXc{l&Vd.2G1tP (z X$F#ܔQW>#F#'E{gĞ% K_ Nвw qy14~ 3y6]G$ 2 y]romrr Vs@K%qF=J S~j})T=k~חb m;jȷXQ'ϿOk=#)RO_.ϻDsszQ9g~\1T{HB' -$GAP6|bGf&I5ko/Л|p;{ aGu>3|M 3 9.;p[yb~Զ1MV-;K@O lI{'aŢW.rgoH?9WF8WSxL]h:S=aW ̧tnQN>7꼢fBp(8zA8sX{F'EA-d3/8=uR=1BuɽҵR&V<5BE6F{` -^G Ov6)f&c+tA_#{  .ƥ㳩&8f>d"ӸI 3gs^#aKOTcM:;|aaWu*OeF;}80GhFM_z|=_z|=_z|=_z|=_z|=?wl[gZۤzbK]::sW{Z`ur<mo^\oko[y,7m,NכE^:o΂Ezsm򤅍mK K-~yK}?o9,Z^p΢K9xz1}{7qYgCLg޴z#cֻyr-4.zi%u&n$_ͣ8oƑH((,&M ĀLM3%4"2Fj$ck!h(l=%?s- gAX:D;Dm) YMQ  ùClܵ fF޽I])C92Uf]Z6鰕Hp [݋UbzK.AEϨ!i _.6sRGHc!z iPIIJ7;p[߱"7sIkĴ Icë%(JX%![ȟIrQzx Pzŏ%:dNm+FaЦNA9hUdC+lhЏgE34BpncɈؾ~G/]dQCA;^ 60[S8b#Ж)4(W|QKa M݆&2ƌۦf |ؘKM̘[h [as>׈ Ra<]I@XyFF.9kh/rJJb/% >Br#|0|#HT9G~hK&!%8$Irǂp¼__J'6/"+&p2n_\P9chs#vCJ@pw.%"NmGAl CYchs9)JŠИL4|ż&1o@,#RoqAG2Cri&lͷqGZ\i  ,>62l<hic(5bd2=dmė{Ak8|?m9p*e l'qڀ݉($#w 2^9F1%kN;_BP/H|AկCŹtn7;`!62.\Jxfmtcȧ_Ļ$ 21v}u㳅sN]Dw A%q d\=I"QFo2|g] N/EB8ئZ tMdhs-o3h8qO0 ;/4BHRD$-n|*>6cIr-HY~Ͱۣ _X!O3j$3NÜr|% -7]4I%zM(ß 1gft6A&=S чs3A䎆`v8\P7x(@ M y@FϹG@\ LJ - !qWpj$V >J$ߠ;#(IEqj;1r(R/=R=j[[>Ch,`~Hd@G,//s[+麴I$$opO)a HMf/9,D~GEULIG"~\ qh6Om3JA>m$gtBȑx` H\VnNs=\O'3 -0;$։[ -!w&(3Me$WQXi\sc-;k$qqh L|b2ĸ30An{īNvnd{x3n7|AhNB^M;(<{"_wh4!}Yil's֣/i@UXħ\{& oF {@MBrq HT!S|b0 nǜɵ F j+l #T]dApqze,Pl'>hj2zf@I#Sk:()%q |M rw@|{b{k.Nkn `+Qrۇ`a h<%&$0GuWC鏹ۤ> >y!qYyk#`n٭ ex;M)?6O| - 3 %sH}$6#9%c&hPL<|خ_ob"_\ v>1eV;{9F|,$E:DyQ_k)K3gFܱ#lQdIQ\#u?LJlW9U]l*\w{>5 -NQk!#w%U6R>"?w4!le֟(Y,rE=! .v_tPܴV([ "2FWB!/D D\K^=C?=O8˺XƠ02Į0)9㞾"rN?ԍMb Hu{[퍜dU W=3(/& Bz$!ul4y~Gq[`_&+C;#i$Ԩzh)hQ @v֣,S∢o}2Ph,|O 34Q8p%<'~s`њ>jHzd}Se@"-Z -$_jJOS}G)J$͡b@hxd*)yuנ"+Mkll"(A rf7uM=CQ_D9?1B4v9|Kx!N gf%/VqY sa". -~cs+!LG۝ELFrx޷ք1{PBIF޳Hax_.C[S-*E՛ S삷 Z br:ag0-wF!?ڔD쐒s03;RQ -pB )qRA]=r,% /Ty:͛k 5|5 g OςOT[ʥ^%æÇ(HmC )#ʿ>w +ȹ9\܉[ 4Ԯ$=|@q$ Bnl -+I)Lrׂ5bӵAƀH&q +.&%17o 'ٔƼsş:0ST4Éy [!<1XO --@I <@p)B.,>Bm$W #Dآ?|}12E|9oB 8s'_| }F endstream endobj 28 0 obj <>stream -vbw3$_[ZwLe<+`%nB]T '69Wr2*v@6ݴV+9@:0GJ".3PO" $-.fiLui" yC;lNIs5Ki@Bqs"PQTy]LyJk*[*v!}ߚIl"gXk/bQ:'~ v!ܡB*+z.ĂBqZ\C9; @r/U(It\)9^)w @ k ,UX-|l؆/f]׭@ 6q Wu"k?{lM,4 ֳyjB[`A.KV.ifvۤy5>Q@<ĺ k.=.etJ:@yw_꟢+_bJרA~ؚ|кG -~ -B-$֣Lݽɷ*3?.J׊7~viQA/i$=.0R?]]G*ݨIdۗM !%;RA2P7djwqG +/}-Zͦ_OQ^<=Ct+Q?1 -9=<ȩ!2/+ kE.خibjo1eHޯ<ȇ)/BuFYܐY&ⰨZ8@,WĨa  LIsz!j!H - 37Vty̩؅f[ja %\]Co輸״>ݫ/ƛkЦ_ܴqDg ?@a!-2/󮯠~Ne\\e_[ H)|}~Ajh^uTJ_lm/),oRVXH:M؊ '|ܡ)T{]- -cUO@D -~G|*v#v!؅۟bY+-I.քIgjs- 5CGh5J<:+\{m(ui1$!p,[յBr47%#*1I#!N -dW3b\Xq.`1o%jR>%͍Mk7271&#]dK.QòP@Xɽ7'./(ݳ7Ori\TZ˸X"}F?!mG~of*vzzlF /b*ő9 v Em͂' ^(F{>˕ չ'u$%ǞUW@suqs*T;D1S k^j7b|!E}m~JeuPRϾ~jri.~<7œWb3E}XQl#[O uD2)%)Q2!,!")LH{{XVEkڵlU_Ԟٌ[ v#B;yaSKJ89b\)tcxBᕓ|a* "5AWJ,kȦݞۡkB Z*rtI$iRX_<׸՘ l9w(#DDׁw& 8h8YLH@l=bh$Mȉ1M{&Aػ;Ufh1B~ǥ#/oI^p){X==BKg*DuuE -HQ -B}&tEiXPG fA\whӇ}ػsT_:yj*_a0ಘj}3̇@ &y_:*rJ~9{ օOPX{b͏qc:{{[KabDuYh#Oxܑ`*=!fpP>f0t֢8lCJtOKz/䭨J3(K(^+g@$؇|fpz6kXcbBE?r" a͔^ +z(Z3l4<*z:ֹ"&Q5 S'@~a /o[ qJaw05pΙ!h+ NACr. j -O +QWL )~2,ɧf#g0UkQ "Maj=G˄ki] CMqniX+7"=f 1~Y/`5 an/b_o5-v% U8н)b]96M/KEg|3GVD>] -H}#t+}&M?~w -;Fݣ{QPGY:쩷qڒj>!>tQI4/ ɉ33S7b_R|%dOLP&oa/|ՅKbN) 46W&uGԌ#K'PD♢Olt5jDܰ/z~u!byPaOXGb`lهSL  !Nce&YEJ.;CYq -I#4.*;כE-+ݧEQ$f-:*ɏ-F #ŝɧKk>b([VN"U9Ц4/(^.}V.B+kW4:8-{Fh3;7 9*KнA̒өBy|iZ -:qkyܺ\̻ -/{P{RQx3bʕؗ(~F4$؞z~cȕ[ϕ<_u< D\yJbem?ski]O9?SQٹ+n_KΤ#3h-=>~ _ClŹl%kmkʶMbuPٲq a'kEY*a1C;BnQ'Y7p^[^tHS8y*7Yd6wV+KNp"c~e$?a'#űO&*[^+73/G54[{9j~5X~g^v߬W~gRm.ߡ<ޠqUo6 h\!HqR8Zɗ_`_|V-3^Z;ުunז*H~Pofv1_ڀ??b]|쉩f H͸E!J_ -7_;J5;k-XG.[>W.|W^VlpLv["2P3:2U{,>`ky -&[޹-TtmduK۬īOW[\dE~{J]ʑVŞ{ ;m[m;ݹoݸ?;(nVq7Kٵ_ܣWn統qb# k[q0],SδY'[xkPDuI8G_cYTp鹵㟌#ӝsT_mG];\.ŕwؤVT|!{'N 쭶m\s- cKͼNn˗Vqf6P˰I}9W?m~ -;u{_ݣ,xK97G3]_ZTe,ߖ%7:'UwX N͇~ҕ&g6Q4XeۍX7LD7^ߴTvSyo6۟/?^ [_pEHjSU]˳3En:][SyV7IζIr?ݸkmeUXfuQ|p={ꣂJz[u$KvN@[vO'n'nVZ{Vj3z孇w,*^-H8o״ؿ7m^|z(U1U|+k^&^Yvk/Q}u:\}{⶗ײ>g((+zqa -MVv)/t9+e<9˿qgv]w^(|ۘ!tI/c|٘tn87S٥ -3gܫ!m//dkNeIㅦUJ݆mst+ -Ol- oJ)^ZdTtgo>فlw3,}鱿b"~FBٙ|꽅\kCsOv(C[V>)K?)r/QQŲ6LY[$\xc-Yf\yʝs?ʕOj÷Thr6I|e6swkQQ䓤'IE%yQOʔ51绬W4dT63e͊N|X՜L͛{Wkg7{K-v?n&~j2ƥ@G.GJ]ٚRwyÿ965ڽb}&{\a;RE)wTJ_ar#m*l+ת΋zМYY>$ =)}o?lNmxZK~kI?Lm`femhVrm\[Ky⇦Wnq\q-cYpgߊ⹮O\/YoP۵[-Yxſ||$M\~x&z0mx`qK/&gUۃTwaM)%ͱeګm{y9[^˹_Ϸu$ǧ#37qD'>[Ӯz!_o>I'Ri2B> -. ˎjHU}bR-?n{p(yUytaMU˽$ΚLnZyŻRuL:Vyutk(D&'?VY?n>sEQR_6pފB[kgkI#Vo_x3]Mksק}۲ -> /r*Qw= u:^ޖTќ$D}1  8]{䯾Zw&( u^mDc,SGz-z"=|->hcnk]'dץ4D&%/߄0ovNS򣸂{Q9{Ed<"U\V[HMcn#F.R$=N>H9|KPhWJ2?[y?^wzYK`㹊?^v0{.w9̼Ć܂Y"rܛKLG7x2avҽe7ݒnn}7gnw{5F7F%Ȩ[ڒ6TFݿoj}79v( -|UUST(,Ϸ5L!Qhn vOBo[Rw78ӻ9yu6oؽ:,i|YHSF\ٮ@u+uv<ݞ4Px7?'6 O~\%-oaʁqkW}GCrL}BnyM={aLLC|_g[֮ N Q_{yNhoCޏȱ}Ib{ yoW'c۞Oq } -mn{#Zry]hSnJi*$ސxh_v^^e2xv7uV40,YYd53oя%yKL@voPTZLYneߎPJ̉&*Ox&U}.*H+,AXr",Z!׈~wڱN -F>/6VYk(231{2]f3<3~FYFd6KUaZW̔㘱1.3Dft/=f\)9qW2Mݘ5#7][OmݟݬF޾q&0V_ua99Qy1 Ea wr1 SߏxE/Pܦ6=)d.̠dӁ?l -<4bQ ]äz~-68۽ Udѓ=F2:f ӏOOHfp=fR3'tU^.8~C[F_wcJU1\l{%Ltxq$71)nTV;j*oGf Rx&,c;3n?({U޾;[/}x3ѹ4Nt/^qckSY157G5WK3e0L/O\h~yf UOZ߷:әy DEc3wWyԷ9rGgݎ{;2k߭7ò -Qyzhq27ՄO κ[ېX''s螎$?ۓU񏖎̴1ӈ#sCJ^fyL58zhҫ+qt>CALoA䧑衳91u?;([]{SYogT ϪE|L{yo.E>Jɇmy~:&2yUGG"֔)&vY}c0zGkKϿ79C|Cia;|߮5?]~#\3*gwRu6$t\t8Ք$;ueMb dcO_Ⱦv;|[!Y-5A9]2O5 tuﺝ-~{rc;f(Q_n}fDcf/_uoR|~8Z|9od_{^p9WoܹsfhV8uh}ZnJm\Nr]Lve=ɻՇk3k#p{#VjΝ9`?z4עvz,7}:z%ÌfjOfƎZLYb]{痁֯%\Q|ح, y%^YqoSVj<AHfM=X;Ihͻj/^i}Əz{?_z{;ӟܡ`:{{3df$fhYhM̌!7W˿DpĄSm凫mjc._YJ| 0>[oۼ2ӻ=3n /`dFk Jg3zB˙1ÿgF\[̌Йnj36`3 $]L^YjY@4n֣I.ҀN\*$ٝ"+:ͣЊ !{67!ZS׽d`Fi1C{"? &OXK&0#{Mc 71p3z:imf%2_5Fx}#(Z|Ko=Ww'aPyw䝸uk|Hݫg]?&|D|/#|?(7d_Č]Č3V=3~)3f3f)3jzfwqS9faC`ΪsW2$gQr'65fٹ5j3s{E bf=u^PFߺ.sJќp9C ~/ =>ghd03Rs434fй?1w0ّ3vȌ`FO3'0#Ggf\fE/7붵i=VWTx)!5 -Iϻq/PaPM~y5= *&5{rb՘S6 zy{=!Ìό85lݡ[ƌ5|3bOMbMs]q̢m't55K?u? zjD[dd= *|X1y Kĥ}kV010-!y5̹ZsAd >gk[YBs5IcV%nwk}f:_LW3m.jHȕٷ5^{nFȈ{ϰu{R"vQ{/(vAXkU ґARl ņ]cXc&{[v.9g=?Hu͙k˘?i^wȹpin\/F_4tӢw[=>3,g=n7{D*{A Q䜆{'!۶ߌ3,&Hj:̰K9iv,3nI3s53ֽ4qaFx2Ťq=cxcX!p5?:;\*|x j 74]!uÛytRÅJ#CLc)͵#eJb#N Ϗ$9f@r~VNLQ7GI@{|ǩ(f8f"fL`#3ʷuإՒÆ $}uMF0Lc5ؠ3Y:f @f_fP7f(OfЌf2jYGMl|d0d-,[ϿE=^[uu}C'[vF>S{mA^wڿUM> -'yָ\Ha=qƌr! N|Rf`?IkÇ,b ]8 reV0ffjfL_qxV3Z.8mˮfy4(8UQ~aM_^.zhK%[7~ S|l :N+I_=/ a뚫ٌ_ϫ'Y^ҒfYC~d #x(fg3%;QŌq rT@̛ : -f5CÌsKa0ӄ&yG&꾱aR  ˣgkķm길ڦ'C~c͛9_ʮk^9=29́ |ؙNec87\aɥ ! 6?o~oZ<{/4L\~C/D?ܐe95OvRk7kxy混}^$߽mZaȌ 7=ُL೙Q̔S>3seytzy97-j=kf]Ͳӆ-=hpr}:}a}C b w5"gy%sm -A,cx"'~be.3/_ҘA$  d/Kfqcf7qg<ۆ9n-~on~fo.zm×6"_ !ޥK?]- {Wƫc+/K| -lI+HsP쏡W:1O`SUC{XL=*f%LU3SӘic<3~3a'3e媍楞5?ykaW5?^TKo~ Mrs RAP2&JK_]꣭//lUpmsJew6޺VVwF;7U=<#j 3l\f fW<•1|fa\k~jA4[borU?f~J_ - *_ : _^4x|2/+W}yʢէ A7oc_hHS_i T0bDXnvC^v.M'tO>6Pzh:w ^Sf)a6cZۦ ɳHg3f8/}ot)5+jH -! \M!@ξ5}kS4H M/*ˎ:(:sVGb, C*N8ߟw-|NRob&Wwn -z~p^5ռЇTg -n5NO>C\}wFq \?˒>&Ù;QÌLf\zѡ~۰'bs;,h|O6o_^1(6R$0UJ3Wo9+|\.=~<:GaòLc 76֖ 5̓}^|/.]}FF޷Kxp{qw!:S9U{F_B=H̘2eK -Y~A!!k]_J_ޕo3wL&0),+okEtd=i U]rસh f8凿y}%rb݇_b2>΄r/nwZ-8C^B6qUPVfE)̨!4Ft?ҌkBx6rPs) K|fг}Tuo7ޟYpu/ſkūn<-Nr9mQ-2j<_^ʖkUlnR_[ +B3]B 1͵_>Nb@2j(`0f2\鲳⃕o$Of^=ѱBx9XoR&pLUyc'3K&f1!pBaE}Ob|aw+G/ޗKNn>'7 -eb6 ?U[osq yBnuǣܾ[sT&*:mCx`~k9'}Rf k!({仞EJAO}b z]|^>G00؝Obqkm vBI?u< -DBh:݉a7BˋlWwv?Uu g& NV?\mS}e J-avcK̖.U0^J4Qoa3~:Շqx-^ޣE#'{z2n1>Z3}IhJmHIǘв ҕG?c_k?[eF]?2W'V[~w]~*}%^̭=0+>_>:l<{5ߘvn`O\At?tAWC?]CE[+w2?jz>[?}QO)GΝϸ,1')^6tUbM|pO#/KG -jakTe0@*h&2[5P;7!O%r~TRuO˄S q.cS -i\և.¹w*c=]jy"#GS -OZ -Ɓm}N5gjUflH_i̿R{uü X K2էWkk?-w;nw;y?|G;_Q(:X,^/Kkw:G-ɏw]\:PxDn[77"^:{i*mHE}ƞ\6D+#-vUunKѽ3H4np9`ৠbbbM.N򚾚83+_ nRVם=L_ 6}6S+_~x48cت0g:~_{cq;1`r)N#A~]%G:ZV?)5 zN#W^cG0_a5&Bqp+Q'<{'z[ ]ݸ=O/" 373k9$}n;/hW%w^*DU4wZu7OߖqVR2$[}tg o_'S!PQ/u43efHs8/2sdS.2\O3u],!sTK]o+$YB+ KmzՌɶM GJGZA\Ep>N`Bfu?9 끻̵gW!܍Ta/I%G 5'|B푉$OUvջ*;S{zy!h; PERWat{^b()@w?;`!`7G&kvޘ#z$| - ?yɟ~)+렎+P^`S6󻆂GR1w$[!G'з"t --2k#$2P#kOLn.ڟ.|'Z΃ >N> "Tվ7B-=o?y!?UއM̷yz L8ꃱk3T-Pu:+0=~3~42f:p,f匏 ܛ6Iox{*o_% O4aA6X`|1"4q[/͕:,;%[ ? U{`btgZ -nޟ7ߝk(¯~Y8*H>"B}^ ؚS\:^/PW7Sƾl2,ӔKhZg> 3}8feR7 4"׮?AvS讁C^3A -zE Cn880B(4H(mu`۞,}4Ui$xbF+hJOgRf g FUǨw};}p;%'wn1'2TXmVYAA&9 -L=uH8{?+V v NLޟ #>|r-_d;R,;n4 }8|V;y;B>H?+Q` vE NDo"ׄK_.hNm˖t WUfb{Zb5 6˜rZnVw ;d.[,2YЛإ TZnF2i5kN1%eDdY3SU0P;8:B#E뢳,;G76_m_w4h~U}R 4#OT꽯T\Ɵ~-h)㺸s$أMbluEd>.)6}2oPyW}_-uv~6P:U|n>,# |Pe0W.3K\2lzkeo};U'ư[퐋z.fk0 FVG#{}CΦ`CW zxuעCa}DU~ ,:hqLw JH%H|i^<+&Q~BF;!Pb'$C!TT`,fۣzupC*1jՒ: Š]^h31$E8|V{N:x_hP`=.{ <}o}}b΀a&A&Z*iq@nX~ AAM jh(\BEJ` ;2Ü-ͯ7mWiͱpc$3\x\}8 VPHQGyILcDC`)`ɈۉTsM8k\}d2e;2|~&}.ObNK =yȇ(XKǟ+t/Xrz*+/z 즏a~1[xnJfy tq3zĺDceX"pF 2{*op唝[jVtKu|;yTzt`*qMg Gߪc?~Z u^&9eG3AH#zbA'YSUklj -,㉯Xg'740  1p'XaKġ12J9UeyA,nxH sad t(3*C>·K/@`xa6.3:|jk6$Ƅ mcJvybV{&8H$Q4{!MC>x!gA!+)g19JRfd3:e\T?Xa,NcI`F i5^j)r$ IUƐ})BRéyჩvv=6Ki> ->xKOِgcM叟W%O5<8BH(e+Mw Rj`65?nȟ?~4QEduI&>vuV\䐚dCRT35ṁ2jo>GwTk>Bمe-#ɧg*1xkqdMfJYCNkbNļ: -GƙI*} Pˌk*K/+T< z`i6J$Ò(tmrAJI4Nll\sp2=ڔ5} } ;r30Cnva`j{\2 g'O"#Ė[q+-4 \T9ka棐Hθ̟:p3Hgg kk*ΨA'>؏~,fRٶ (ܫP_4\ˋےaGj ] 'X[@AF=t4qZF-MщyRRfFJ-WZ/.7~8v-D&n:_l/v ͵]Ƿ: aڲ[t -X|ڽwUoUlX)9>Z{2t=|W~4w=q,cĘ -K1В#OE\iT" XqQØa- l"q^T1uX>O?s_xڟ ;\-DMVT1,Cp|cˁNN7_+}\|뼫麭~"4%#tEuC5'fחɭ7܉mМ6BŮQ$M<*[hbm{IݯnuR𠅊4?fXF k;.چ[mC/g-\P m-w]׈ԵJfШ֟whivTs\ m-qg-CRlzgvwj-ɟGP^ߑ -+^Gw!w= -Jztऋkov"RTYp6G}`|xJ!F1"n7x|dOzit}tY`|PjwKSzqt1sO)ŭ56d\B5]}_] t=X.7̧ڀ/B?B+X{Jw:xΘPo91};wܛOl6;n/@ ,u=``O-_."7t5#츹PP\Ih}D㡏rzwp95UfXsn<1SsG箠i7>R`IeU5]_+:^,*ǕNoBH)u}5֨uH uD)mS?HB3؝J6VS1U/U/5NJ[O&izoX6w BF|zj[hIhgq[I^vR{Aev(^x) LA8_]A]f 0P[E$ނO+ h niJ+l֝ThldZPltY=,nqcD}`q2|nA] )5e b[.V$ڋI^W9@aq|:kuxhJE5.+Gf#6kRK kз>V"P#qWj qY?A9uzCPJ~ڪ3Q=$1N1Cʹ}(s9{>zG\g;S]a`*j/qݱܮG37ϝKםBu;_wnt1Xh>zGb=e<_UI$ 1SoNTb+D%!.Rݬ~J;0ɳӦkHc\nEW/M=|➯}v@;kE:<ڲgŠqlu1V8Lٰ s2m)Sy12ЀȨ nw -6NWI(fm?Yܚ^עC_,?`3˕LNӶ,uS=lO}=DvTg(i(X]vwԇ\G;jPmBhSPmxʥ3lO,m˩9Yb[Кkjȩ|ZS>I."zA_^|o|4^g#~ 4ëĚ}ƳΚ;JϽxZ/%1r;?o=u'_P@!H P{$^oGkNL\pUd)X2usBI MDmZAs=^l]$W=TsMEЎ&Yhg58{a}PʀvQ*g :]0!m|YEyjgE;/7NK\mIurR uбorK_G|!WMLsz`VþDbtk ~'Mwɝןދ4hhjwd3ͱ&%!C7+ 3R!F,tJ;7!ϕsۇ"Aۏ1W !F*!vbJuXWZEtu=zmkM:/.$ t~,MԒֈ5 -kD@="%I[kɲD{GCS6os- .|uv[$'~`v͆S5Mm#]w<k`qKvyOIwܐb PSd+¾U9 --TAH[ۇ O4ZN$fv֎;λXKS; V?Yέ^쿵Jz3E|ѧ#1w׮C+6l8rxv/E^a ^u|h0frM.Wɫ3]==+v,Ifq=)Jq+i0J)#ֳZb%J e(/0*FGrԗmtTqP-ʃcǞԬ܁} 7@암 -#@;8!G~ץM/eSjF9"\H4$ .媮1@];Y˭%Rwd6u8BBn<9SM ׶o!PJwo.es.G$ߺL_i.*$qHp홠%OLɃ|ۅjgQOvU,?EmTnMv/A?Y%=YJS;+c‰Ԓ9/㋶; $eT8 -CVp|t3PdKz8ǻhɵYQ}^modBr!&_Γj5Z'ħCM}4'5yd~/<'P1j}ScuDox_a@w O!RWmn?OabLCN/+IܖOKj9ja󉛼?#=.BO?,T;4MYgmje_5jm+6 \>&8[zv1-x.bz\5'f-8XׇŘB :RY+=wh ߻i6Ξj8ǭZխrU_]?pMŊG16B-Fƈ4?SdTaFbx..w`h'KLvdܫX>_JE,t&Ju{~HA.8+hAc$kTgkn4q6[5xzttٛS}fĝfǧkH - oh -P<3ߐPMh\&@  hzCy yq_EΏXj:MhX4Nn -:_z~_۲D'tO2:e;Fʫ*>DH̷})sH`w>wvD|H{GKT; Z!XZO쳲9_hghgɛ?#6]A[I69\}h uyW;FQ=cgH[/ΣZO_HhUuOn=jOLt>]wj.{!*L=8kwyEtm+\Dj,Zk;>EnDre$*Rh9|ɫmͲev|j D.z(HIuضcauc`]`T4RW4]6hnGDŽ3t;W_&XEè %MɸA5Vۈ4G8:^zmD4坣-m!\Wԣ/ljRe ʮ3r=ۤFCOul;z2GJjg9XזryߎZ BzxT Ry5x WA|r۠;EXCj;K5 ܵOM&ȇCO]]uLRϢo ]_.#3|Ձ%9rt^7)8Z37]Xn֯4HNM5Ww9BwK$gc &Q 1KKzQ-OY5Nз%IT}f9s ,֢83hmzjj?.|0kbLK62k*(^/4ZFBg k%;<&$mСpiӅܒΣ=t? -c&q4ڵyϗiRJk!i2`_xECfCtk!՝>1Cl|횽㩦94oIGs>@Su4gg󻞸sM]oqeV4&?8 ~&S7Օ:{BĿMpOO:y&TS*Ze< y2ŚDe6 DC^{&~ @ۿ\$n8sK\$x {= -b%g6DΊ>%^B h֫nth ^Xh=X NL -D2*8'VVȫ]s=zNӬ?0kQRbK5HF嶺i<73 -bi+M`O_`ɂ%yh'[aRnx4~ u0lz/4ȠWwd*[ NFw_NxZ/v^C=ԪnAo{~g!|f=%XT㱛v5tbpj}Wѽ'Xk -BrǝlMf{:$vv7nFb={6ɑG`͝=c,3Vts@NH̦A2?e췪;Vz0zləb%Ҧ.O/+F -v:r͟|bE0Ǥ.G~ \۵\ӕE,)gYhKᰇdJCW[i VZ$wzh2l\ָ)MRVQ_.$>u8]#57p/S1AjA.;$oJ Զ1Mm yZs@O li˻w -5[vFIu=GЂ`O={%>EnMg'T[:XͩOp&yŚ C1ǥzO:WЊ:;c rJyl@Əgb -ϕk{zb0yNYُ+%94;N$6Vv8hQ2ܧ]g: u:?l8>^w` B7/EsrmtE#h\>:jsMjdD1=7{i|؊N2N%_J_H{ AGn,tzHH#+mtx?x?x?x?x?ر CSCmDon>sUR#SlmOwKI]9 'fEVB>?mh$BR"DfM8M1HD -f&DǰbJ.4DJ4|L9B1oIy=U͆0#-yU&:F 9Hc$@ϽrX:lWAG(g~^T)=J*h*U؊u:{!w.V)К2;)&(:]L9Ow@>5chcA`VE)UF~J-ڒ] R%9k(x :G՚xN);(t*Ĕ[r1lX -K _ioA紸_Pxo,R0l 2jHe֬h u }y;!F{FN7tIBTve.ZV767R[7;Yr;k27k;b˨a?R\aiJ0 [k7];+HvL tP*II+4g'R2tA+4UmABW< c"'Q"dlR`NJ}SKl5QTy5r\b9\ >`*F&$^򺪃55Ǧ26 |K9:@"BjH;JaڢxyrӍE` -z(BrZGƂ:N%Ѡ(P%b"3Ȝ7RrzU|?Zň^+A( !ڈ9[YJo/EPRL RX[l"iJGblVo2+Vd?hYJq.e6>Je.T}v@)]p6.12%Oǒbqd*;tR^z:Ny5]Bd -U쟠pjnZJq9Nn"k%>LthG*+167:a3~Z*)9]=357ukS+iRl^1ϭ]{kc@D;:Fj [_ۉ~ɅrQptNt#qS7A+12.59n&muo]WAwd,a!> 2o.7!`W -_ŀĊy&_ϰ2y=I|x2ā^f6t og?BdSh->dyi._-tGюq~e֚ څ&D?^ 3֠c&ؒ I6PI #ٖ#hRJ72*̳@7r:`[r,O2p0yI+E䰌O_{&2'/hCЄkjߤ]s`|.c(XbT"R9hV'g @1@<ۄqZԕ[BG,: -7JX#04':R5Mݶ f cpB>C)9 TLF_:`kvYWw.a.IN*9vܖM0Wrs,ѥG}* 6pIdVP"0|tz|FPWcH\XY` j*eO"1FE1@`m7c@BPu#ƐSIU7ҘjӫS (S͠#>s(} TVd(mI#:wi# Lc %9tGE?SO1)qݱ bDϤݔB,p@)$jᠭ̈/'ׂW~_ŔRQ -:![ImYHm lz%w%6B8}բ~5c.{Km@bWµ\~/H|ՀiICtn7om >/nh: -WD;J9̓N,9K5 -t&~TttkWs:AC(OƓ2k_d>js՗_#0K 5smi%GA7lt IY\2?(1BrB:O)> -RRGI㕶GR1y?:Ca3"Dώ||ڵLrHUv@% ǧMUj6R_If? - ;2*̑RfK/eݣA?]\T24%m#ړSy,.1]PT[IJOA͋pmI݂-Y*a_X!O)H2RXВVxcXm!ف$Pyig뚣Xb 1r偱b͇A hĬ{pP(kwA^ r+R!qy"(Qy-Z28KL 09-Su-4R0HcIs;?~O*$ s -Y.oEIUw9 - 5#~>s eGaQLR3ǙfI㡨zC傓iGd -$J1 1I% 2˜'ZOiUu_F:k]ɒZ1ZB|7!u{J&uJcC aBL&$/f_u}(~Bh)dF$xe-*RL6!$Z!iJaz dz#Œ:] UԸdU 5": -6 vY$y`RR?l0qb$i&l,m#y5T (5Plۤ>-}$qӏ!Yf(3͠4+EKE)X 8 e K@VH]Dj: Í(QO\Al72H렼sP@㣲1ِUtuKNRG| u#Q_ -E9pjFRゾ  y՟o E cq -*B7aÇS>E|lQ:%kOEA &z/m2@ZA4ԇ-+@*U'A[}$Xjsj{uChӉor1U=X뙔P4JS{9|sw~˨cӵy @3c+kn"Ua5g)o`BLnoDžm_/4Uk [{r&[n̗?pۿtVHJqpԥZ%OJi~Lu'&Kg de.؞zU u?ǯ|COѹIC͕\>΁i)"]\ -"ȍK/ -&lФ!_85wg9(_?0N ]SsɸiD`R#LNŴMt -Kק@~>&s6^W1ǟ`Ҵu'IX 㝳FVn]R~(M<@$ԫr㑼7_[VEд&v~,u4u7A -7,L$='s9Fs!^i{˩D٫DfҖG9E d6Cs\ Sl@"La 92!p^ćm@MHz-O~ek\!eGb.GQ0lSy? H ԭvޣ nR`,FSX<8gWa"sMIrӫ`2O0} _1d?M[c^[n_ ^7)jZR̝Wri\8۫Ӌ|׀O$#>]'?GGqݒ>a_e /TErDi9s1S4%qL ftFc^c6LMJ*:(HI*c4@%0P -ܹqƱ+ -MM( -0>~J:`9U̟MVh5NY0U0(Lb5Hj3d ("30W)D|}+s'_gu#ȿN -hyqGSEy_7|!ryg%_(򺱂ly&N "c(BSQk,Diط;85XERrNqh)؇ل1 $l"k^VJ9u -C><|(àHe[<%0.:LӃ\s<Ԏ/$cw߬1ɐdpN q!5PCgS2!0W2?m@ B|fNi1'Ĥ!5qt(I!Җ߿=&+,BFa˹rg.#bv}A%Z&sUq ؆NvWJGMuːǛN 2N__^o -{OˡF\r-㔡LvoQWs2#$kPǮi- )tݾ7r 1ǾK};En?NF0(xZN Z? @ wh2?/5sQ&F2A&q~4N wc<!}w&L5ǡ17L}&f3R#`^.؆ʏ}=2~/ss -gMgȩo B\ jRdl,AMA=8L8T j6_jWFM|EvkcrA5}⓹ -TȨ]tPzP+߅njףUDD6Pt7U\p7P3%uLrS@܊(ALNcnMx&vO5 蕀<p~ \>|Xxr&9OP&ڎEK3OQ ǀ?jĘ-~S.E4e;IM{Am#m]N B2]@'[%-/rmr&a8 -rXb@ɳab*W+?9^MYM0chg ԁ )>ۥD. V0@z;s} N-gPH'URJAe9< { qW5!585l72+́یw9;n/D#J$tQ T\$2s?S?p~(XS>~q46PdR7"(ya? x#zOʡ:ڿSۣ@=r1{!o5,;_q̄|!(q ^ƀc&`ryaR%ngJN.>S[A  x0 t\AOsjz2'shn`'\ 5,N>ZJ|P=}ey"8ld8n/\'s?DR aO7PaO6 MQ]Mf Ȥ~PCYa_~-a8BG -"r; ykǩAP%;85̟BO+"(BSv^*+_ qa$evTS.Ԅq"Kw1E;;׭G8i$qNhϤv둉ꐗ\,Kwhya5%ym2a4x9䏀NCX^-=}mUA&R!2`;y>`2'_rTA[5 1Ǹz(N*Z&ra HP:;~6A(uHsiX$T*˽[N[=Lr.l՟jGK95;`)[DB ƚRRx(ȵ ).0B=n)KWr~&m9%(~BT#H<4ca f(IT8_U*(+3a*:= K&zejvAМD^QJ8Gfkj%P!؏Isݸ[y))(7Y'u wD׈ox]`8vp=^Q1EELBP g*8nĠl~gS *@_^F=_(+Am 0͡x\Ӗ0J5X] -ypȭH4{8;6qo#y)9eI=reo^V=-=^ĒҔ#QjSoly -LŇ)\=0Fl63.JD`nqVI(nI ɟ'J:ba;Q>i+ %q^A A7-c95SiL|:X&ub(ڑݼ*%ujBߏ.C_0=\MبMlՄ@Uj!s$U=p(uUL |;Ŏa:آ~NY/y)q2#z(s>\_5x7 jG[=?8.`WjSB ؠF +>(IκNrGLj/`}s8fہZ>ܔf]ڋ˫CDS"I >:Mz{K7=*9d_hMoBPO 6q| r;`(RImI72!*Y+95]+A>6+Wr]'Q%@Q<:8quyͩʻ`/5Џpw-r+l{R~W9N뙶y*UkhGhd_A03#ldxߛ+9'ԇ8$[G+r%}܌Nifo.n-h*t:i<r- Cل0jf"GE+BV~X}Z#sdƐdcx Cj\T?_>xx -`v3]e^ j\rSA}jC^k2W P0O.MNxP>8#\vQsA rRg|UKz#`f*=P>r= -\C!py芝3oN \}೾Z8F*(JdKКAIU{mK)1D攚rpAvJ05۠dīDXF.oQO<20p (K=ozz\L/J3?\׀rkK1g -.ܤ|W೸ w6 -xm\\O7uA!f.=G>1uh>~hA8l&uP1ҕE[wq/=S$8r~ W.;[!zrofTKuq `l{g59cB y~״E\^jr|aqsΏ:ԚFm u.Ŝ2[A7'F-x:Ԥ -ꍢ~S5c_E.N -l IOJUȫpkrį )`=\82 ҲHoр\(햺qMV>+1e;D6ryi|6_6s}nZ1nmxEX#v뗅y!8Th*[W[Ctw -iq땄"ԮἋ[!;Se "|ru 9h:J.s#zcu (G꩷8=rKӊ3&Nُ%[qP(aӁymp@y z7Sz})L@ơ{:b*:J^ Tc9>}+5􉚺/x)PX|=g5K@ K╳ )P0+0`%pqSO`/L`fP}]ʠj$.FqJ!(sFx 8<C%Uױܳ*NI@Г >/8rKq^I0uLB.n圹n&n~ҡn n~f0"_3%>r'}"~2m@%TN'őE22$cwC~Fr3eqջA3 }:Oou@sIͪ&6%"J4d=O2]| ׬ -v&񼳊˥rY+GR*z* -aԴJkg4Di HxDLNk2] zG=zf>D?޽֝Dw-yLaIVZt_wY5̽&^Z qv&K66 UNF44zt@ z 谢"^90o'@>x7߬jiV!*Pp^/qk1u\M5|!" +.|_dIun]AW,i>rpYdU)|CH&% -3Y -퀠Z\uF|*z2}'K]$51_"yYBj YJ _])ӡc߳ xkU6֑6-u\bIg15m(jǹR(xB#}G7a&E>'2y/ ysDy@|1$0Z$N> -O?SL¿/D$W^h)iVlHkc@, -GdG([$k1YlĵiT40'j_h%{Q~F|T+iK3ZIzJԬ'N6bҺ'A͊~9%/VD~_+;Α/$tN |+&|"* ]rAa>X2'lfkXKEÆ~{Kj> x49_q)Wo]C!_|{I]E?^#]M_3M< #zUvue-c"k" ]Jl%/kNH6&VIF8$bL":_UM1Q͂(W2X+Sws k͐nN -( -.qN9𛓤f咞_i{7Uq 3AK!a}DҀ4YAI1A~%o2k)6_i>).nYZTi--7ʿ*~|AmI^9U%$cIi~YIAYLMRv -.Gl&kB{Ⱥd}U뢾Fo悒?̈_,_R>^ 6xJ?UZv=|j*>^u[<"*z2VQ'~`i,,4T4ʜ~&a~ϻU8$iQv "!hO˼jTT\V;ɚ*w{f·Q^E=%-+u,\d6`on;1g.F] -;y5#t /qrwCH~&vL[@&|Pn 1z v ^,0NB 1k`і'7-><`pQU軹CV]bHC=SDQ -b %uNݍИ|Qi}'5n3)h"fuy^?37w[gFm"b޻yx j:$T`H~NdFSag¥nuem!7|F7 XsX2㻶0ktEYyӎD !U/Ѳ*/ˎQGڟFVZ\$t vV'LO~7>&w' šoG:'ؓOեWLTo6NaQoɖr -(15b-FHTI>>~scB0'GA“d8?-| ( ؕb7[Ӵ:4MKS!o|Ԗj=iZQ"-p=1OR/\˗_O<$G><9"ZQ+Pa:Xr16ֽ7έ769ң7QR|k;oNpzxpKxZ<>vC^or0^>.GyQ% 'H /I7e7S̭fF/ k|v93\sjq'K;% -k~%*~56exI][S?kx8`wns.R=.^ J9i'xxל5& -0+wx9=`0ioGw n v _e'/*h -|hX?YZ>]bsXrsX|KGOS?`EĒ]sf%Ůf E!ⷍgDJ$I(yvHZxQRc/ҚZw -Djyyk\z ~-iXr1>D8" C$_vg4:EFԺƞiI`:꜏vdE6Ƹ{GF:oWÕUiӫ+baY%Rc홡->&YuUQ~III} - yUla^^偑NUqn>ޱTgVodɯ3:#<-̮.W\\`coYCP,퓍|3}2ϴIgmDe]k`zUir>$)~͕Y/=Qg/?9? -]Vz24l}|crYOEs1@W%zAE/~9dTz)=tsFYz_y.~՚v;q7̢ܤ=nOzOOΗZ#^7z)01\5cz Tᵍ¯J[8|+ ~oF ?ND#&fYS֠u[U38n G5oZ54K(1EAn~=O6:dsGoKUpc7761@g5^H - xMӱMşj0nuan,{3Y\Rjy]e4a:zHϵɛЪcwGhf -YC-U&^tCbhMK:EN1M.Mcj_u -9,#LnTqg ۷߽#hn;S˅h%Y;lO;3kyεawss߸Dw)wI(-v{QSP./ ˥/wyx`RVMu yCE5+˞gcO)fѤa3>M>i. дKѤKDhʘ5hLJ wEĄPpRӁ\M)rcBa_طɛ.ƜӪ9$;{H+ò]zR4Kqeo_`MJY79wӶ#̩[ QqT|F4sp $4Ca=v=e=;k/ZVo?mq4w]DY4kPDsp?[Gj'h~`C)OyZ-kLqK oJ"*"kl#jbkJb` ^$M=<8{C?:9 -)M]}J4sZ|vSw#ii{[a-EOzh;e+*FÇnx}YQ.!\{}R[CB[KxiԲ{_ZϦ*FS1Cj>s-@N״Qьk6U}c4Bs6*g}h7ZDdhLaw3TKٍToKn^!1k{! qam]mvy竣C4߲fOo_ =DJHiB9O{!1+?{7n=[ܙ{тEZ8Z --rFK5Uk4_iVCԞ+yQWy!A]z_=9/) 8gˮpyHWWKg^op)c߿) _l38ۜߧ+[f[[}#&B3cX)5G,'В=hQZZUQ;n]vFqIiKI}CtWvk+s ~Vb:Smtm|Y]l\kɏkV=_d 5xgstşҤ^)6`;i>]#ZHiZ9_xր ;oU+M6lqݳ;8:j16\eYc[gJ\wcBG=-,ok?x-}_RG u:W L|Y7{7;g/-)hQB&Gr6Z`ܩk1o37g;Ɣ

9٘Zdܛ둧nĘ:Ն$UXFFFrH8?1ϣ:.e4}Z4{>Zٗs]]xR |wfN5eSU>E=]U7Ğ3Ğ})0{1Z|89`'] ^)-.t1CN4g,d4j.7eZZ>VheZc3|ۑޏvOգlV[=o7Z*oN kղUogjd˵kYmzVhƊ X`|:"0 +cgʠet !!$"ynw稊. [|` -ܞ_,54s&RZa;h qo*n󫙸;_5O%u_%.)*G׋Y'oT:qg4?a7a| k1xR͊t|J60(ZAXT}^F俩30?nT;}n*L݇gǘV7ת؊R -pU Tֆ>gx-As窠M&hvvh׉;MQ-dWi`OϜ_+lMg?~gi}gyڟYwVj=Hr3bt| m7Ҡ2nxskBu;ou^Ƥb }:FIf=I?'}ڪu!vܒ8߽M6lkh#{Oܙ*aۮ% ~e`}g%؝efnVwum<1$ET"|9=l{Zmڏ-:e5wg\|c|fU32=y;D%1IjTb۔ySASD9E9dgmBm5 KK.?lS,&@K8~m̝ٓƿg>/e 20x׳֤=+cm[|^bA'G֊7C3[SYz<"J6/*q;y}i97FshW>2_^)KoE$yhhąGoE Whۉ;÷]+Ҩdwyqs2qC%׬Tn{íh`?=*1*`y YS%8_] oе1e6Rٱ9?aٖD9ag^+KN|Ǭ5kdر82?t4Dx7]R49E}!"d'9I0)a>ݰH񒬠 apq&VQS ΅mx-ъ\>}؟v9MVqmf|m S 랣]Q |BVe>?ōDvx.<%mt6;29&n>ݹA;?thG[Ͽ5Fm"2}aDVC,='W2&tiz%+,6+-r=%n2[Dz~"d"6}.wo3'D?@kϠg]R_t"vk`t4ȄoLq D%{Gk h9h璹h7\+ }Eo${O3 c( 0KVߡ)JҢr7Q]M ]uA\tHm?zW=`U{Ԫ`ULq8YqỏdP?9JIOd|]K @/cuP CV8kqɍ{M.EMٽ[ W6͝:c-mg:3EzvTRQa'x _p=_5> i{5Zcժh9r hVV W;_OYzgбFΩ3Mn,4Iorw /v FN_J6qy1bc795:=AGCiދz8^Dj +P.Qs1[ȈǛ1oi6|.*G/7ϪV8[1-)vU_Ԅ*+!ysMu=w^ 54c26CWl!uǂɚM~&|t!uai[Ct#^^t ztKpx)hnCFHDfǹd,6 -%  k<"t+ᗳZN_\ܬg^%eKw2Wg3ϝdR>*f7l04TȚʼMn]]0W]x6H6{ 3?XkENvn6]rd5-920a m#*D=χOٯ{F - -=m_]SGf ŃZwvd,2pԛ9|"6?kj|;s1u}[ߴmQnH)lCfvQ -~\V~ЛnV>([W1ngwjFzAP50xbMY?S6<8⌢ɁӊzhǪh߶mHgȃF M}_vs |oIϔ<ϤeR{o%x*|0v߲HUfSNaJsO(yA[$ J?\"yrPZY}wl{AB"BTϋ,bH[3c/eO:1^! |J)})T3kD;TѾf(LnRuk,!x&OxN5],uMT”&U8sͥ k_*xD`0OgRܳ.61J<3q"%f0O虵8nq _UMJ7RTn|al3ԤowSgæ|A&DL^ -b,:4LMeڶtڳAsGu ϲ,=%i0~?֊fJtQ@eRNs}\J4ѳk okfšģ|o􏪂_Qz((}^  ymG1Lgq.3zj1tQgtQg.M6fϖI*-lG.7&)To=Rx5Yީƈ133`n1}pEEI:db0j/Ң3 dwn j2>vK^z0Ǣݩg繈hk/Wԓ ugm"ېZNN'#gf{[ zg6MWvYA85L_b#/*&6sDuU#h44SB#>C #9]&AⵂD%=c@MWfa1KF蒒z͘DN_̿>8C|YEx~+}Vw̴}=+51k}D[pQZVOX?CZ4)8f3pمj4^?؈3 -+D/\:ӌ:j=8=\t `, :MyTArN*KR}: wEϜV=6{YzJ׆g(y6Jj2jwo% +7Êכ؆N;1],89EC-n S,'2zHAjQyM2:"nWNκ߽ԵDxVN U{K0[O(w'\ -ZR!̹?4<0KsHKQ<(ȟ`[rŗ_:xn1O{f-'N8!ιq<`LZ>#Wqe}ԭ}ѭ۾eD=s„kjpY^){!oOZOCy_.LP'RL2R~RN[U"j7mK֣]6#]#_6LgD!-3FVSa xFf-¬?P 87*Lb~2YfvgQKMF;b]3}19fbn -9uRQv#?j_02zCsl;u nYAߴV}K'n3I)cY=a.'L2rmhOЮ0^r -i;-qD^]9t0/!8=L?}y,Hq+QqMۃDG-2Z&J̋/Lɳᛐ\AD duGBAj^"Lj6&Rh/y|9EuDof=89tD?q3f#`$8Ҟ+Z/qO9IFe:P 6ŦIrXFG#idi4-cq1w !JrJ!ޔ۾~㾵֩3#Ygʌ8֜}Z{U^{-:֞O: cG6rEӟtyw+Jų ۶ț56t'[x5Nóx~-nM_3wwUج=0XâIQkHqZvT9tm_uSfͼe}B_kؐ۲]+O_}<栤/m&vM?u6-{آ89nظ{]L1f`}vc'7x5ũO0pK*_F3?,?9tߑ+Bkw5 <:^-{,]gƆ)/oL {o/oX9 dP6熚2㉹|>kJLԲ1f*KG^CYpS#8{C < -;}19+1F`ɡdhSL\^ZKrŮo+O=lT^YENXXc*'9iim=6ۄ2g7}^1NA1 -<Ű#_.dž_6ǰTwFÓWk^csu lyP?1?Ǝ`tspmПnz0&Ptk;alI1cR,m=C_qFR{?y[~ 3|^;M|Ow>" ~;ػHwn, Ƽ7:'vN#~%AsvBbbn&1Z |k"o}˟FѸvB1xijh~z -<.g07H >L3,gמ^ŨY\ywo[m28F1s 'LĘ w,;aɚp qºֲ7--Ro]:)rӗ`q\w*-%'-.C5>cɄ(~sWP,U-Dv-J~RNgenOd[?e"|y:?cO/r$!<l)m?'UW֕]R6lDr `>ĺk[oZNSmrk΢iK=cEa19pQn7~7M-ηK1僯{}{n| -a:#gBKws[,1uЦ@9vx﹯c{<׹xSCm=;&W>aѲI  ladʱ Xڣc r?#rזs0.#yP1wdKWc:aZg/XwbF->bC P.X̧<]7H紆ۓoOiy12ʆ?TB˞\x[xc01!ԾD!ջ,rF]su0m1l?x1QE-wNE`Y|̉:Vdu%AGж`V /8Eao<{%7a3<ÏR̻U9;1w_OZ~Mr׻<ц?PG~EKL9=1k8; .{'EuהW+eoZPV߶|y`VR4$߮kOb@kf/hbgR^y m -<F:]K/_6o{;l3j0,pV0w^}ESz@hkNܜDxI*a|aLZ_,زH7 u\ZBG^AqAtMxU9= -˅9B .gjdb|C-b1F`3B~[ k`F_,GwD_]( -aMZbT\`ǦSz'aG(&1u ;N\Bx=Nw7e;XG|3>QNW| #0V39rwql+S)sȡĞ.9GḌ?3Gy|Jh*r7tzd2i` ߾/}G/yBlۼYs`>ʂK6cɷE;'N&ͫ S]&_%RJ2.:.l_2tY'cRe[ΎݽskF0gT_e -c1o}A~x 9zt@?V7ܹ eR[=8NQ'o|˛bDWZ~?;b? ?{߭<8I,оo¸_K֓)04yal0@_{Sc\8ͧ7<盞|s]Ƶ;.| x*j2d7={ȺiOs3BG1?|o pI1tpKIw)>\% Xm+N 'T07B c~3%][W* $:&aɔwٔ9"vÚ?S"kȇ3N{kSk[|x2dMsv&I0w"ƥwNj^EMky>~1`,(k>1x[ wN}'3û߿bQ_A o9p}W ?v䓺nZr崁~Bÿ -i]\Xzo9e=sw(Tq!|37pYC4uq& keOTC"+]Haog͋9p]6 |O`/BOF}Y$뇹f}SOo8&B+{qyƸMwmfg{\s=&Fvv`/UFƺ ox -{[Ӣ2?rugkn ozm -o>zŷ{ڦ7}?>F3]m`b 19HbwEknQc̭ 4m|pgE]OnΟ b!|x=9z,[kjȝzT pMķqK&@mZe-yq)ފMgE{9O}tkt7S.;7޴ _̱dʃ jbq>:-ʧuw?L64Wykšsmg?Y?m lhc=Ywt+)"7qá+Qco\zu${0It E׽xը+`"#z!-_s"{M{?Pm'&OWrpdD21.'`^^ӟϏmyNjqq ?9) }k>Bq1ߑ0(/._36W1TO m}cnn|pi>X\Tv|须ŜsOgŸ0r=7ֲևA}ok$7mga\U'㼋e֝rcb\nʝGsG?|#1;kZe©BK&b<N'Se ai4|+#s@,t&<3M?]9V(qצ)o]ټt7n\s:TJ#wtsW~9_,wWḙʴ= sGҫNrg=rq2̝TVWLyp u,?Eϝu΢xFĪIt/\yFq۵rCqɘDzqݣc,U _{OGyxV~Vcjߡ4jU>5G n:w>`ĝq_qFߋ/(0f/;|iâ'Dw}l7keBm Gznd Pan@]'O89l8r/xVl7.ۡk9}> -yk7̾rg± A3weF2vaS _'|jL8qʓ}'G'FW<sqS~5.{b䟡QS m˘n==}Պ\C964GsJ1wxgNoQ%ꤸM=%Zs*Pn .𷦏\N 3IFT9-bBy}ϑ&c2̝6rgrg5{x/MKFm 8w1g٨!m\%Go#ҺDʽ囗=t.xh6cnS>5ڸ'͛_&j=S.^*DbX̍GAv@p^]>s%b\=HYm$1q/ZVn#g/0'vxFhOY%#j֗׸s6<}AGrT\;\_1ѧmVK~ǺN[rgQ'ʝ;kn̝u|g/Xݔ)wV#OYYYi#w!-c΋>}1`.ea~s+Py;"wMT3P,kDsܕ1ec,{"zcć^ ?/:m5:zO -[-MD|fa21rɸ700﴿ 8?[` -=NCy eoˀ3wr=wVM;eotXf󙾆l;ƽƕ{79[=(Ϡ| 0/ Ų||y+Qgw%OkaC~~ƒGؖo_yIc~0e oc)`]~Q -rg)w'7Fѣ_܆>hP.uWXh$rIF,\_œ_Sb_$ ?ҵG~ 4ny:]Z}uu<?(o̦<,ʎ(&xk<|>(oI0}xоOo5s\v}Xfd᜺r7ݳr -ܿHЦВ'B mQv1Wl<N ֻNh|:ڷ+>s O?ڵC.8ȸW燻7ߘ>2}~@.rqKHF\< vz2\]o3qcQ~5{.FӼiضoO9pp}S)l-PSw#`7(' -]wq| u}<"`x̋y~B;ߚNcsˬ"YtU_ߌ3|_oءOǏ6yj`zK#==}`Sp_*K;ς |y3 -=4<5/XAZs4ʝBp=N/κW˝ybhO -2qI9[P>=!|ƃQGv#xWtV0ߖS>n< u=浘|`ַ+RWf#P/zXǽz-wVh׏x}Qd[, {?R~oh_qB{\iz.N`_6rgmw6 -zc=vWhقzWwQcƘ#YhjiXއ6;s4l}@?`{쁷7ٷ+oڛT=TmF^[bKhߡ)^W"ygDWxGУ^^/zR̳kx,GYQC\qkb^yMk%ͣ#W5> -׸ބ׮#>7ܽ<\yr{Rto_O?-l|賿 p&5;&O}_M91-|n{u"{B́z{|?TwL]o Lzkϣ.p)w?vA{='=Ǣ#}0:hD_A9U@۴}XA7$#`އ<59-<aO:ۍg羸sp\z]ʯWV @zΊDK~$p -?DJ{qh$pSgYˉ0 -{c;_p}~y"zE畱?aܫ\_KM'ֈ>@[^z-7{OwG74x,فc昖xυ;5y%G3ҝ"\os -u Z0șG3V[ iﰡ}~]郭wPc1Ȳ x{~&c_s7{/9xx}E V1C}'G{f -C{htG= [0fÝ+N9f.A_HSb(w-DW+lG?q.ȿQ[&# &|S+v9)0+C9c`[\k9xwYk&?Ə0)8sbqXtn=3OP/16uHaP?x.mt0+}['pn?8UkLu-7N1Kό:*;tog*)F4uL*3:ƣ1}>nnδ/̴,B T`*39E5_Zf"?zxObNwDgSj1v xjEgg rOw7\;@4=F6IhpcVA"d lŮೣ>ʒNqIw%tc/ݚZluYNtGW')`D;Htڄ힙 X| (&k/ X&cRz&uҴۻ@tǵ]ŵ]еƝ XF%dV%&RqXLƭZ1S6QOmVbi2 *޳uZRHU{{zΪAq,FksL2us6 RL-ޓ >3Jw7{*v[7SqUȝB_ښNg,[<-jW -4Qǰ+Z;1{ Лw^ޕLt:-cY(ΞL|HZKRs >N1UV)J[l!2v"udgYL[ӎ>CC{ vJǚqū-qіCs, 8dGD(jD;;"ls◙dfqG"|cd܏I.tE([Jrx,Ht/J %KO%*t.vGãQI8UөDbm:?"_.`V[d*|Lmu-1"%u;ޖuNENww-Nҋ1[/ws~Vȹ۸i#i.OCu8vڤX'9R[L/U#rp,@"8F1Z[ &"%se$S)Gm& -;U2VIOwhiq7.+>)EJ;(Ջ㝝/JȖ_pԑw5T$ 8J[̂xW$cɻd.v+]97:G}vġ*[%sq\Bq&^o8DZTq>|8?3[^2wE w)Uc;ܧA:f?ڳ"|Y{z=R}qwdJg"pCk<պ AVxwr2iGz Lw:Јvgdg"[w{赵%3ɥ gBۓT!>ש1LɎQ)wxqiufs%$یc73+WǙq8S-v:|rLgZǍ8bb8:ΰLjqq\Bu)F:ng -E>UƵWj|`JR,`xBQ=J/Fcۆq2r^, -\ڟ#%t 4._;_q#&o%Umv7[IrnrZys[)q7Wwscf6j.qa19˾gII$'cIN"u#:vmЮ/Vb5%'Rux_saGi,mt8DK> 'n70 :;c< =~7.YRXlK';ͱl]xfVVbsٔd DGz~-*RH:"?%0I_ck~`4vn`4 -bEZ]Zk{ K/8^`[2?=9O[2,AvE }gG--%g;Yܑ8JIEꔒ$ASǐ=8vsυ,%4wv&Y;3 Q*{7K9<{sv-s-ξg)ݦd{{oO: wWnvLº/Ш#חH˜bJ.Z+Z1xcs: /yP"ԍpUl"tD]ѱJRndq٪nuZ cۆqժr^oq-;z},sPڇms!AXE>W{݉uqZke# sDZP,3:/WF+0SeɶGwD!>Y㣎ej!=ʨV\= L[,}[ -QOi/(e9"G Xȥw.֙xNOY];؝i_&idn Tgʌc7ގL|iًqxObNwDgs "Ԩ#gh"hij-lҡ*0LuO `~&ޝpãx -&5^/DqLڹN*n4%7Ҁȹє{hJ#қd80%pDp:]=ոB+B$F %9Tí$nJtU -Cxػ;>stream -TU喻Snn;2h#iæ՗W0^ަ* z:+X˪{Y<U^"D,{*T*.`,FԔcQYT(x0 @/x/HP=+{y K*i6+ -'qJVD p) 멀j*^xlI -k%hUYI)W0:YU"C0%Ydh-v1/Z h&{Ut40hO!~*̪pmOAXVÊQeC -r}LD@?V!2+(B֒,,9emMig\S f]SļPה~,Q\&|66yH 2pf -;lH49c_Qe9 zJe_Yx_ne$mx9c6Ҭyױ8C@gDQK8.ׇ<$ 1G/,J؍l=< 5p@aXu;{$l Bo%` 0T />5$zh69m p`׿c?LqBeu+#CyLA p86nUËmhJ54DmhЙu 9AC9ă=0dVqUTT\xRFp`[f*c  V]Lpm.<:}CTZ>UÊ:&8XMpv :O\u1c) ->4&8XMp)_E"8 I]T6E(F@p E!Ʊ N&-(dK<ʪxU9 S5HNeC"#`".fPqя mi]\ ۠DaINC҂#y4^-Ia?1[765T6 -QnSIADGnH9i_c/)rЀօmQtqel3xB?TNhHRA$ FNY=1GKs]c~91nj;U!z*hDN7WdkoLM9Aup xUcal -s,#^ -Ί飡yMtM0CJ7jP?-ztƢk|q?V-ngdc־]o0eǻU9reN#{kSx -JW .UÄC0`[(8cenxfkxX鮶c:Z[Y ΘY Dt0QGƄRp@nL'.fE ӵdѪۂHV]j@fUTMNKT9b`oԚK2wOEEKy$MaWUUvԧ3fA(l/陾0OۀicKAI<9*# ysu6О0bӠAۖnI4WSL_*l6t6\8%S̲Fb;mF xgT+&" M ˵Ba8T$Yڨq8VX;-˕c@ $J ,%s(8ƙ[1F駫*kۖn9!Z2QmF.sW( - -I"f{Cw0lm+0mxU_j.'\p"065mk]~2?fmM0 G.(cb2\cvn~a8[qAMdʐ<)me\$NN Y!Zs`٬mS<\8'usvbtYbt 2:N>Z ɪWoX(V5g1%_/F5A);G]%)<#s:.Ж+" -s 9˲Xr$lapV*X0h?s~ cgN%B~#G^"k~ -!9TDZM~ cؽey[B]NF>tTcYԘY  v0@( J^CbZ>Ä~gxeE`x=:#ʁ*P +'Jt^-̳LRb%S¼;F䄀8ɲIsY^EIJ4b`xJv#fbFYW -)xRADgNez'`#YTAY@S@,kkmEEx0H ?b=Qz`B{Е5si;hYN%g%{#On`Nif|bqC>Ca䚡m dK,MR>Oh!hG),U=xG7 Ab>֝9S- "\wJWh{Y1c>jd S@vvf2RzXAjӒ4#6uе֝QjK :*lΩJcfrԡH@\E*'NwR*TK!(O¸WyZ hS۩<]t+-Pe,kJ3\UZͣw?FE 5|tMiCӄh s ˈ{ Py΂LeEr -V,-gbc,|:򨄤J٤=j`0pWTlȤHf"$ˣLY""$;d/˼b2]J^MѪ)fLAYj`f,8 KZN<ڐe 6AZ[,Jx`qY7v&Mf6N1 dRBz wۦBd&f1O^۽N&fhI6YP̊ d&Ȣ fTlbo>/&'51 ͉ỉCvfь3)afh3cT,2+Z`/bsZ2p0 E>v+.vu̢12kCӱheC ɀQqoɄdAN_Y42n@Ll:m:ra׿,drcHW,*1T(J" 1Id:FK- -(X &z{B԰+\ 3Ne, - -E"C'2:Y~oȄ$AemN_㭶̢vQu(9q0lJX4bHQ|As_е_uXߪ2Xq~܏g ǡ~f+zyjz Omw1-߇7ei' }5gKDaSD+--`cKmhes?i)aƎ\o$| -m!nPɼ+wC@vh[+Pa{ndoRy,6K;~Ɂ`:M* uCmt(1Qo`JI tMI7j(֐jdfMv fy۰AT ߲jltsCby[:@:M޶ Z l-dowRO6Ⓑ·1ml!(5D7XP6aED7ֱdZ XMlp![H6Z:F`kUȦl%Cc6,`ɦfv(M66Cl,eéj9u;(E·,u6Cؒ)YdʶT?rf3!lX,eʦfQv(W66 az )[MuH[[6}N!moY*oqipN7,N7, N7,N73jd[dj'ddJj'd[ejce\Aӱ4K94K 5K+SM| LɁrӾ%ݑ8Kd"I ^jJ 9`Y̐zr8\ e//iA"FXM9^xg"\J$쵃 Rv3j ŮBҞ7(Y:o%04e[CaU{CvU$ONXx:02Nu7A.eDQB_0byé^*dЖNqHlAjLnΆ .K`~"@*6/I6粄QEmEdm7uhiRێܦ <|*k[;Nr^U; `(;Q'-'hɸ1o D-5m#;lU]noQE3x:aCjL'k;5ŴӿcBݨnۺcQgM[o3k+:X Κ/͹Wd"u^!cA}_{ -" -M, -'[]F7^@xȽXsjZ=L{pGPpMY -_;o>_>#en1 -0Gi^=$).<%"y\(I*KNAu d=>f%xQk A;WD`cZͶ5+Ũu5y<3iTA3NEsgt*:a^f'(ZNLVX p}5FB€p^TH8aL -2-@ 2NQ/8Z H B;bqK -*I[ASEܚWQ x@VD{P0'\4`ڀC?>>i-BcU F5ګ{myTCC0߮pyLN -F`i$$4x}{M*i}f00[^ZnYX9YèZ Ψr<1|FS^Lu<3,jHșL/1kB&ƾ\rmYQY[I =#-^LK)rvYZx`*PXɏY5C0$ VPm,soVy5鬿Bg'F</ff,Jh.x^~PW&#cs<``wzF#s)(,6Ġ3fw -[ƽ$dn#ĵh -qkm6 - nwp@Ud"Y2. 2,b\b!R[sXWi+~`G_iy:) gB]v1mV!-:S{ԨHZU-LOs@*bU4Urә;[cg~KjFt5Bcߦxsf m!:B8jq<Ѯ!9 @rYUǼYNwղ8. +JԶVɑKEW!G x#W5i>`˱灑ㅖ5;OieN8m>&h ԬY?y4A@]ʢ<ys\53Y[d>l] -ڿ0[5ZUIF96xՏOCCHAe%UBS0'YPǪxT + .a%Eai` ~jf7·X%ڵw0JuԊwJ^p8 %F -}fJ55vCby;onOks=0YND:'Ak]t1v8+inʬ'TٿXg# jZuSTw7u/z% -*e2: d7]~Xtu%<*0OaF֍axFm^$W4hFEErC.E~Kږ;r-ggA3(nCyT\dd6{2^O}mz<S 1Kd?,z -(0' |CuqTEb4 NYEǻ[SlM)"@@2#^ދӌ^+z4M@Gsў90  Lt_/)>3n@OLěy/ tz%K#ӫ]_s)(?Cӣ=A5kmuO${ |xj$[ŻKsC[/5ڕ @=OAVc:}Dk/~yL؋?~6&tvYޓL^`^*Kcc&0őNYzj df( {jmo,AĬdOW*ާNAn 5Sk#fTjsfװޱ(īJ6 `DN'bţr+Z\W h8+NH&y$%SR=DwpIJMIp̾`,:-gao]I闱(ŭ򺳱g#D /32/q"uxNdJ,:;A#]PV5%GZLD=;9SQN\u% -FLLuJJ(~qL*՛,L3ĸȊ,'rxJ2'<+`MV׏gluH9, |=%1ϓlk,'2"fP;)YxPv䘮H eו&!X ֖PSA5Ɏ'zJA&p*.ELdI6܍y;x]l@à|"1*z#qzCT<栣QH>L3\Z~dPyOYQ00yeEa)ωfplM_xLj w^yXI Β,q"O Gxm N^I|&gHd3MM3{xn g̵U]]]QY#IhWV0i7dН!:bOSA:(n! UU  KrDr+@8m`$Hja _O $KRV_g?5ݮ&W),K7hIl|;87&Qz~UW\zÏZtWP&>Z4ׂNj4O3n/jdNЙ7# g)(,CBX$tbz.%<\ZB&[ -{)-ޮ7ioU)\aoKp`ED3KcܮMoxs_?𻙻b+a75a+tKs-OGuxDwpbo3@&Gvtmw+j\dN)5$ϨAgӠH_-t/!MD#j@H*H|l E>'ڡ8(aH2;@1?Yz@'fvx=jZ)_lj38g`j/kؿDkc,N;d<6@r S Y >Y-F1Y_'J0~S\ڞ#o/M`PmG'8Ym}40!ht7/8t~7z>lGVh$Jl(?dsBn;~MCA޼1H)nH6~@K#G)A&}Lnϔ?*)e+գJo2Z{\`Y$flxKxIdkbcSx|M2>D0{/djDR]1Am. -$>qO{QZ- G $ 9ހ$Z?  :1*\P"p'SVIobE-rQ*Xy'2e*Ea -0B/Tx}~pP>剡"GHHQE } pV0.za% N!|=b(g.'h>@т>@!D~tm!+~%̓BR Ä (]r_hIP"`c^ ; -Q,H~PBXԠ4"RP^@lU/':CGY& –&٪%sAm8F fs;ESh<Zl%:I oFD"b!C@;oF`KzX0Gz lȢp 0w?&3~?~:lC˒`XypYJˏFJQUK>V⠱SQ>T@#&*>$Rf]籑^4@]A IDgVBܱk @sAw0u5"PHa%(.E |>vjILSGQ5 `!u "$89v((_u h/Ddc@瓬nQfo,<`;!2 E剬#̇YP , z5I@gM$aI$5$]){a Pҗ9I`rXyQ&~v~%t9 x>DXbu -"Pa]hJ'G'`!nF p2xdG`s kL֥BD|$^ - !Ų>^䌁KPddRay bfȣM4P`ASH惩Ьc.V!-0C/'{8N˺D#x`"+F恁0 k0)-A"Q 9RJR -nǷ/XieNz}X3'Ë5Ff8h:ou!itGz -!}.6 -.k_VB Xh%NX1ȋ5ʢ]ހL"H\l_+S -k -bO/%&,, -''] >0hO.X^A\{)I첸ע~Jh}D=.Hc CN;kЕ0`w/:HcФt{H"rJHy0TVؚ PG*U ҋ - ݠcGOXHLGHKZ Cl|[lܞ, /&A`]c4whe ڏ bn&xy!V@[990E~q)hPO`p{qi!G -p8 tX.^bZJ6\ud*À!P^Bν0x,QlXA]PJ!OqLF+-r޲%^ K*7KGcꫨv0 U_q=)zX.!\%l3.rpk~mQg[+t׈%)kUZsޟ\_Ld lO6%I!4$"G]gm3`̐<$!X/~ ĬZ@'FGaXI\d-?D3w x2#X:ebpxcY0N -g͂, v[4Z  e&1' B$5$9iOlet0 ?ߔG<*0} 9 0ؘi J|*gسNăUÉZQR0 (г`Pb_MC ЀwX~ N<|_`B[ŀ〢ulBZ| /~Kp1>_84֤ž sT`|->$ϕAe*"Z"|;@=kYAP/hM6A -QSdr/F_ p%Y!GoSz{ۮ2 V >""(e)rCiD^K((Fe9f.\Jq.#6ș h;0QvȸgB&@ha{eZCx[|0<` B -h9D^'! esHځoEȥ89R2 $ *Aps0%O{-k.+v{02oڟĩDG|w$ ʨ`t׫fm]^ekmyBz=XFl*׮eg0;[{8 4ƨ*"v4oS-&1]o6#hmzx6Ϳig_]|~߄7&{*p$0|krǮ~=-؟.~^+phŶ9f?eVjGo L7vٶL;T60 Rg^4fGC-9tVC@06`h>>~_RTZ_uVzK59 ~{%ٿCJpC:Vv? t76n1@M CII1 Xn Q+35{EqPu<wꀎ-LZ 7IOM lO-#)^n58hng?j;F~a;%Qϯnv|vٓ(|gbc[,NHOSQ6#\eΆu@yCJ]DAڏuPhZ?)9`+͂q:S5Jčxfӳݾct*!o` ֺxNj78.`G\͸cd?1B[D~S?dgJ۞9CA),W   -XDSeHd`Kd75Z,Rr }_h(y4*ϋQ2oM2b4ȵzvs;. A6D ~X@X|n;a _b{-yytyc<"QFLq4t}h,Xvwjୋ -h~~a.R[ NHv@jbۉD^aF;| S?xB!O)vQMzoZ$(H}`_ sIo`49_ -Mg5PgtG7p5 o7 x7[3o{G]1oN!v"Q uKfM'*5@ÒM,|5Ih^pKbw1.1?!/b0Uk#ҖX=|}|[p6ŎҟLTf-YQyPmٛvtV51}M3hXۮQ -Fi$fbAS%(%!9;ux /X3` -gՑ]w :1u?LMyd_6EP.*}DAC:?b6QPr?Ba -L|vaj8Ww.V_C.:(h<>n&L%)jZytZLL -mJ)a3iRmъOD,5p&\[pE.!Xwg>=WnBor^,Nz?Fr@v~a5^ߞ7WU}*mi=zSt{Q\/C*Aq{Z xҌfsCGm8)M.Uex3AViB6r݋X[zr{#ۖͲ(2fx6S)@@D3F$cZv8:5r -o{(ˏh7x#JI`U eȬxg q9ֽoNIWJ"%@dc|0QCz.cÉA}o 6d ) ĭ|\?.,Nqʩ81OyVa V q=]Ah7t)ྙ, -% )I]jw6 O/pyѬ*pԴ߻ %5A(8h -?=Bt!X+&uX̛TpgFqPumЙ}zQ#}=g{IvMu>H4mGՕ}Yч Դ_X_gS_3fMB |g&=Z3e3I;eKzlɛҥI1a;ݔJ*m| -_qlB7]g*s%=eqM㘛<ԸΎw# ?aR~ώm!4;wxRe0$w뽛%;D}xgNf<|u<1+Wgҹ*7yX'l$[tiJ͢#S)Ȕxg^,(* g|M|ӣygS;||aS vjO8R0t$]taXܘ›\M}vP Z"@.nHfq.An]xO\n}zzJL<\y/Ftu:0ӥ>mJ+znl7vvx^bo!k]klzB3淐L댔31tѪx"r:Eg4:{)9Im\OcClH|yP{}QcY oЫѤ4|RaUOM ;sbbWF351:>~|}2J1&-Ƽ72VmEmw(g|j^bWJScw0n9I>DoL&}8X^M}oe )roĸaMJ)j35wL4Ѧi3,ͺaġfy1}/,a1stdKZj;K{fXi2;˲\x;XTguOV:HޚɆaaqS.m'Kz%9<)GƔ1]w;[=ڞc+f&@}-~)尻/;}s{k랽uE>6ysP#bwviw&g%vu䴾N*[wF\gf9yyoƹ.9wL1 WUZ׮xmW\sw}:L #RqnԲݭrEqވU=I]O5z]0U 5!9qef Zt?x -|c"{6lS#*㚡>Z|Ef ZTlYPTC=9 -L^*,^[>ۇ7x֘d4ƽIoWϳO9|c;nFηw~ܾیޟKLGO&igh=yGȏ RI_9JϠԨT &ͭM!3?FFX3duB^2.ꌦЬMߘbrp:mمrx.ZюV?O}"^mGuIEc-zߛ-c))1gE}^?W`c|rw]uSv =՜'`xjŇ&,v&=N|ZzӤ%2LdiCldΐlS}oT9jԸkOaŪ۝~kOGcL2`e_u̾d-]6c_r-\vProA%mDʧkyiŤlbYx| -ܶh;P(6ӷ5՗])mSSSaIgFZ,4gT3;>Ag^~2򚮺+p5%~e5jC}?Ƈ@vAVOy?z+๫/ yFj4uWG3м4k!k9ُA|)m3?ڠxu,?7Bt4/V:w"y,:r?-cx\vlqe-|] yyq6u" -#Wg_*|Ė} ׹_Bbž4u}x}ʷna ΢$NAAMO%PcLD>s6:NKd㦗ihM1͔=Mݾ=Bj.֜vvtn1v&>VojTo {ܹ?ׇ^Zni˨gn. 9ovF] F3ջ]De0 8_)BJs7,[gj.hv+sqvQ &rfv[֪GTw%!ɋn*[@,7-ْOe# {9j<볧]y^O#8vajR|^Q8=>_<ʗ\~ɫDѪBU8K'SAyE!U3{TE;C{Te튩$TFReXF U&Esճ\)J`l -X :{׾iG-ޓ5W]6&+]1sZ-YϽ3ҏ ms.T~TÀfJD|إ?'an{<1eبONx7jf v[&cYSyӟoI߳˙X˻D:bMҾ1D/ٙ krۅy-ڹtZs +1=~k3hK)4NN)L -aNWΨU}"XI&Qv4~wLI?%'h5o~]2I6}39H4ӟSzc?>kD5ǍW-41%};Kgzy')Qͦ?' I65@ԇ3@kYJTR6mX >~~pܵz_bt7D?\MDYjʩ74>"wi"b|Diӑ\R?lj`9n;`*Xt*s"9&؄?qɝ "gfFXD(k5ol)]gew̩:k$a!8,f&̎  j砖_}s|TJݘ&L'˝%]Ìad(*emE8?SXuc: F5g3i?]8n2rbiƞŪN4OF8u8Tb^hgEPE"[7l$GZyni'IX_=N 7j\<|8…$OKQLqOx$rCH9I>Cw~ dfg>=]5b㉕w^ΘA<"sr}%F鴦Noܘ-k~e=ooIDĕetzdPnoL' -'"~B>vz# ljp&;`SW0 w!&e1P_rzF0Τ1z,WÉ Lh`%1ߘ4pPw}a`: ţ:ŝ $pK%>%,өG+ C-PiTgV$~|I(;Vr䈞njW*Z'FYivq'&u(n|^uj̵9DBbONƒeR[I/䘊gǤ ii^dž씜+-1u:@ڪK~4 <,4f-&Xc0I9Q鄲:W]F\x8Iͺ`efUϜk p;С'.k -׳x[!޲X.%G*]7>\B(;{]zpB[$i'd?IٶBR<^U7"5Emp]Y좃'Z{* ,%?qN4&Rs5ᬾXYvg%po<}TLb3LsW< Hk-O>_HT0t"ctɝ.|23@ȟX֢LTK:Y<#"TѴr=yl_":ssC5ɹm` -8) +ar"'_~W⏺ 6# ܧSsCj spݰwZ !ts*;?81;.GH}vlfuUIYN% C-ڢ=tLF O/G㵙`~(o܋ܮ:ڊ sR!ߘN,oG_Ҿ]yw)bLx}W7|wHqK7 \Mߗ{Xj@;ճwnFGU0 _9O:_0tqk`yct~i[0c 0j{:*^"h+f -:Q|D=jy>@sW̗7 Y[O:䐝h~.#YT*#Zs6z֎CSI.u:mqcb!?2] ?ſ]IcMO|R69L<0Xb`ߝ XJLiu7ےx3L$ؖ8ׄ|kGr܉{m7YL7&9'[*']x)ۑQM@:GRfc#n0wcWr0:E͊ǖ4ccc*J8\mޓx˸ߟ a':z_ցMKȢ# -/Ims<,0霉lK M1/JZv~G]B,\? dϾ{gOӹi&Ffcm8k- ȹ|E2)=tpp.npgJ&f_G3> =<*;3{e~W - -nfNg˫.MXӨda::1JCʗr]Xz"& iiZgEOwbJ] :[:|;ȠYFu qYhȣh䂉+mdhs\4R~b!rH,z#+a@5D.h)'C1gz <6 bc+_/cG9<^g[Hθd"8Rj2,r-tݎGYw֐_wh 3Ps,AZ!EP=3C(FI/$v{ZV|\£y cc_KlE"|7 !o&<C/ޚXi0fE`o7qhN/*>m -HX m_` w4 BZ^Vo>ҭǷVKSӗyXxMJiXcٞ9=<85)<;$ [ {OI.FJwkL1AT4=:c_ xAl$;$N(89㜊34DRLc51v]fh84CT^K.\d3y'<}~Flb߃W&њX'lإXr,ݶx)cDii?0e[DThbXIKqu$Џ՗ON”}gm:3mdf1ƒyhJl|θ:Ix?:'fzgJ(Vp"ީ{K?4fN`tR Cj҅E-RjZKKٹ_OaOϒQ!X#$ASA> -c.LәXjkg}wÅjN0Q38iQ$&0`+?VO oRwGªs8a 5wA,]=7cG(.&((+U\1T`~s\æϡl3.S/Ql[I^GvY7&7[{prxPMa5-v{ 5C{*~YSf/ OgV#G|O&;>_$ -1k=Qq9{/D"!~5Ir!&vzJ9uzyQcߘtvgϾVfLf6HVrL [bʻ6kg0Θ}fbh&1@߁#әn.Ÿ5;i![AhC[hx>(=+L@#:vG@kOOg73؝YqO=K'# g{uDHٮdx,Rc%R\(m0 NȎg86{Sh.O5eK=ǚ(b?#6p!VF`mjx_ T&К"/7ggNfoߦ xfFRh.4c2zWFto;tTž˲PGx¼Y~Ju:D6HKg'R@oLD-+3V`*)9="hI t0{qYEE9f^YQf\$j1њ\e6b-v倦-"n@aWhiR@ƻM/5֝ݿ<۬@[Dhc@oLc}&" 5=f㑨4MQ/d2z2~RZh%y>I} (/cYmpI,WEk%K୩<ٲ( 4po.c6%^4wD\1P#-Pu: -V t6G9;hދ tnw1@KTpB4 ,Dйݳk J(5n -%D@h}ߢNt q.W}4o8a{HuHi\Np~z!A2o#^#FMnXjz -Z!*4@OD <}Zv(8|~*mہ9yܱOǖGhI}bbc%ri|+m{tO_ӡb?L](s_e[Tby6֕{Pze)_Ib(˽m֙ v٧D?}Z<:h 2l>͆3PlzEEiW>X,Pl(E07Y@O(eޘ?kDVZ6(ZȦh -fu<ǟK-势eZ҆n橪?IkKúDԩ4a s#.ZhyXMYG2csъOE,{IedY4qcYYs |p-\t6E@%=QtS[\̬zjFPr~︘XȠ=؏avNFP3 xDo2?!B/I -y B[qR;G1AZ%5?3/1>Nv|7<_C>I ->k̟gX -gV-~$VugJYʽ~]OyIqq)O%EeK(zlQcka' eͬ葦]U+ ,3dp#WҴtb[nUx:bxp޻VF\&H"vFbQjn37b4PZ$%aw{ |a3rOiirnȞђ8qoӵ#z'㠎tgΤt/]/u):Е=Aq. t?/&[dfJR O(zD_$/ypB>'Y, 2N +rJ_q9%ÜUb`35UK7k7hzZÜPNK>+^wEY]Ysh1%y8u7&m3^af fpeR4,\mytXi UkLPuQvb$ߪ1޷H1D0l/}lMX䥜A9VRASɧNE lUڪL>}s؋̣-6:Yq-ԉNjY5 mEBArOSl8) m=,{"QQ< -]\ᓳ$SnymT@<LP,A #)>dHA1]@(-ђ{ۛղVP8 ,H~[A=!ϱkSdm;KA.#OZ۱R"%.`/ u^cp."pKz%ZR,(ɊQ -Ɋ"$ˢЂqC04Bf0I%T7N^A>pli+Nܘt"(Ȣx yrSiLAlJ9FT.Mkhc2>Z ޻G"/v",,ﭗЃMyh|^:+~F4zS=ݘ8xG#M9Fw Hٲ@SC|[ Oա* ? -~ )}]rO )m'ռ [g!oE]%X47oRYSVy7:a퉳ts/G|M?뽓/љ`:%*`iZ)+; )|zcR_ r_'cD\N&RЗ@%nnhxOD0JdzD;zX%ڍ$%iUZ_h0kR\/.bl??h~vIiscJV[6Y[sGt_Ɩ/y12K:3NBg-UB$+f \-K <(0k&9 ޏ6^~~{qE;75%vpgfu!ρ 6_]?yLw?ZY؅6l_e+`Qg?_tZ !K-} p?T/'UPYb cm(Ѕ}b ~t$$$8])H:a[)ҩa'jQ:%s(=$"\5sгQ\iH$8GuySPK)G_A1Qɧlz|暌QHaqwm ݜ=Z31\*F(\gb |wk,b-7&4r42t,毬$= npnsuV7s%]T7cOny _]VЉ*]C\Ae/tՂW)WRC\A'~PC\A'A rBU5ttZjq?X -g: -:l *j-/_ $Jvрd7mV/NM_HKZ:_Zm:v֊tUK1sR :S6>S<>Qrh'zd*U"WJ(I̡\U4IdD ܞ -Wc ׇdǫ:.n4 3! bN9iĘ-v۶zIjnOZfAU3*u&L"/wlԗZ6^U))WڷƪCu%}.Cgjy`# -Iرɚ]U`ȳDń_*q zJ.mE:-tR^NԅLsP#KtAuICqu58{'Oٛ5;{rsх(0ϧS5}k ur4i*qS2(QUwJ5r7*e<񀔏S>Iա>U&,w*R{w E+R{J߯T~NE*7*RQ+RQ/Qv % Dԫl.nPT -'-~+fF)z)B)W?(A%pQA)t|LQ2 ~RT6WUˉB{,Vq&z"Ȩ3a.vsWѸt:/r)w^,{=GQ p^8iljF-}eM=l5:˴!zbBPbRncŎEGT3G=CU5KJ}1ԒS{.:>4]Yj??Nzɍ/"Mwzr;tߋ\[M'j:N*&(^/?n|5T,^:'tpkVUIurB7龩ڧ9_SM'UK1j:X߫]j:)㆟;;tRt2ARn5qzcj:Ȇa5+;UM'g[n5vNԕxOEk~N:(\M'["ʁj:) ^Neg䗪oTIlV5Z%TIsuv]ut-^TX}u(.oW'o]haNg* 2!QMa -2UrHP* -4.'ܘJbU.+$H!+apDZLݑŝ#͕#s۲.5ws4߹NvZ%Uri+Ӕ |gsl2t͝jDq6Ew?掭}SNѦ \yII^gQMlriO]tAj2:<+F5ihQ0O\_PH"Cԑ 9Y [`CSe,u6~Ofa  -J%\s6t?9 -:Ӗѭ،e߯T>|+(p87tT/̮o@E%dz-;LSa결SQgr11V0.YR6Hz߫RrKU]fP+zr9ԣW*SN'_oI\vU> &Ey?^uQxۋRV)l??N8.WQC!U;62li(d wJ; %+{ou7)U>`WnS'vSON7|*p'KR{Ew]ÝSQ k_f:S7sn:t+W>?Brι|Cn^z -SGVTtv.v"&(΋eL7eLZ,Ѯi1-eLAN]E)dT趟VeȪeUj)bDWb~UELrDDM{aT~a(qXS7j\SnSŐrtW]I)ou~h}׎T0U=ܔf+o}04T=׸Jj\2# h|NFƍ)}h4 r5\ݗ}z)KLfb'A]TPwcZ?T%-z𶇏)ɢ2<.WGL&W* Ƣnc%rGYB=vz:x@i; c>#U9ڬw/ )7&D`s2OR&6|s V\4g R@oR t`%4y -2=w>qE{#}v!ێ__I|C =:B}&arڪZhU͕%Eumɴ-0}|dl!],)>+*>_.}$>ْ3{+9V(':5qN)"hMDIZBœؔB%I>Au>}my%M Z!KG՗1]Mݡ-n:>fV))Yy^Rlyɧѹ٣MRCլτ\~v65ƶ\J̥ɚ|XȔ,n]ߠ5^_ngoeqBVhՂ% --V6] ,-𸾝T`֏)$Qs~'ִqWVe\v=h 7򇬪BZ]"ķV`Z*uY>ɰgyڋ>lHe"쨴 +== ^nb7;"J{vj]G~]6T-B|ד#( ?Z^[zY]&/kN >co,5@XNG-%HSPeC,ÎecNK'$N>)׺S'>$ylk$,tGT'=V%F|q%? -Oml`y`]qq< SZ8 I7S{a8XV/Fc,ð0stҫ001ٜ-m"3A6qԇA6Db#-lw}rvj 3&JB{Yss\X'L?[mZ?}c>1 $dz^g[☏ͭc>Y?99W J 6Wab߿dwi 5ޥ ۸8%b9Rh汐5&7- +dXn?ow](]zfq.(]}HV[VQ|1SMc+.[Giء=C>fѶi WyuX6-?=:WqRXWƆJDjy]I|${F{Yr_9>VI[| -;bW%2S/-Vy2/=o&>m0 Od5lUkv]'*{ 5Pj[Re=@ tRf´LCkYŕy*YV<[]}z]7S+y)2=˧k\9,=tW$VEN[tl2_*IY J5V*V3HwJ2"->yZfl[ -iGfmUJ&ӗ $f¸R㽤l}܃ftζijk.'|$0~V'Nݛ)+7)0lםm2N’q>esoMn붷+c yg~;Pݮl h u.}C7:f0XqKY|38u3\xW񒹌ij!,z&-qt11S–⩤KR>%5H͖T67|0IW}i q?1V(Iؔ8T^218?1VT0Ky5K ;.`d c,M(5olKyKJjWiFY_X7AUԧ=_:в 9;˦ه ť'rhEkQcM=$pTaDr/؇ q☴zٜFıeG#FжrՕLQ&}]{lEƆ#J͖U,[V' Ok[#0|--rCnۉ&pvYYcR-4-Rqz_M%1*T0g0ÜU7g}^qLS͑O,*JuZOiOʕJmEw=E%MM-g9ti:UCmM}³A#z.4&?*>ROџ -T>u|Kg.9zgˮ]7#kx9mQ~p仧ɇ*qՓOgq; 9wb$~>=2yhjzUqwU␔P5CLDC1RXQ[hhX RZ} I L)&G \O2I^_˃t'*K!*&M,7' -= 0p1>XM2%iZ)cUb&XpMU=Ĥ))ɯiU{p6eJV7I̘DU'7SMAnk)I͐䍾-HJ'!//cUmIa#C'DG~:@~Z5Ր5̔1֍1q8TܜK!)rf""]5{ˤURk 0aW4W![I{x] X.&ӥ$DXss( )rfXa!VdBUoMa'rzf~$@Jws_ .Rʝ㻗[1" [&kRgfݴ#Y -.&;kEfa$ד(ʊS  `RiWQ3f2TފS2VżaZA DxQL!1 -B SLFǙ,dGҶ?W(+2baeô, T] Q4쎲t7%^]vV_nO|W qBpڋ'Ԑ+s$۱U?.%Â3sKR+\q&dU䑐zO1I$I#Ҩ&xe@$DۥU7I {0 D䐊s#;.V <`yJWכGUנ*lnu\4IA"&oTO_O#(I ݲ*&F/9S3$)Tu3!{FcÝD #T@<MxK_IRLJy-,haNفD/ŪdU%uXpItoCO* 7oǨxHI8hdI{<A!yqSU2y7h̝nx!2yFa _ NZft|?pC.lV@rpJ iqaU;qƜ2frv{ӽ+qHJL__kڟ* ikfuPwox.i(5#dbL㾾Wg0'r -JKfX&m^9I͡n1wG L+v957=0BJCZ$/"GidR1I_Qd{X) caDR4w=I^{5E19,̜ eƉR?P!kS+) fLI219?{Z=]J"f# Hɰ[Q:?,'MyDS3q=A-׺LFRFH* FHe5`_>S0i}^ذ]2|n4涃47Z;68$%,DN9HHiDͲo&HN#;_uȟ: #&1F`{ Ӡɪjr2dzZv'7@*7CRy}H"vSGM" 5yȪd?l>\'Wn,0'LW,[ נb矫z -aHKުf4V@],υC):[jHFo6IW T|fh0s/F/La؈Q!0I!rHujn*^?ԻtIiOjqfh},zC/*^b2htYH 4itlkO-UZei)nwN7ug22br21Iv?kNVar2hd psg&wwGG-PLpk_o.RT@y&܌GyN) 6EQLLFOzq{,jx""襘ԥ~Xa !otϵHy۟i)iz(&o_߿C4y)_CZ`3oB"wsI#@nY9q~ַ#!ުFٗ4diȄdߞj\4`rVOjq X$MFif2B 9ު$pK]uz8D^f7zw7>d&)/2ÀI냀*O80G`4De$sgcznz0B"G۲.~UcV:jԋ}+F"oU3brD}KTʕ'%kuBG2,mH`L$)$%n-Vf|D Gr9FIMA.70g'9|*29Ƭ`C&jFY?씩;Bo  i6V3E,`*m&!Y*7dR -ZNJÌ(#]rHF1[UMׇ͏YS3#19Tq/Ywշc -uv.0]S1?|TE{ I5 -cJ\L~ea"42a:u<;{Π'AլHIC 9^cn}O wE-C)8و4&){=3`rLCG/n`EBGbՊ#(џ yyȱ}~= _g!E7"PͨD 57d]e5߻.tY#hgpAogoDNiJO QpiɋGi7&% V54r\u+틯u_WÚgЌFoߎO19ɿh>}۩r 9o"by!&#!p@pZU8%y;1w-u͘U8ftMUe %L@zhb^ΜܜKQ kyyyPjn$Qc.%iL\yжQy4K䕈hX{C!IoňHXjxŷ{QŦ5ƍǻ=mr8̀ɟ>%Zr哑5q$Qisal&tRQsj&xŸ6:\WO D:ތ!zH$MNvaʕOAHrTa.gV{j+ɓ4ӽ7RäբtS桏DFD@Z{yfY LtWNմvnh0HȤ"1` ɑgp| oT,1"YzO;[1OQc$ϾyK~0 Twjfj1.&;՝IT]9C^CR8>5/U[%>^`Z$`j7z>$ȎOLɏtL&&gP;t$B/vp0hzUT7 aȺ<8J<[oFw7y7kW.V7E5=[1V3hHnɬ?0,`&pEpBcnp -RiqDf$q\<0-y:4շb4&GvNAẌ09tQr$5fDcnţ{TN#ra=BY8TVU]8'V578m9̺&|v0םsm}ceG f/hhbE^!E3jOd^R,QPߗ'p !@f^uN*0N3T#Om2~Ivt(a2i[U͘ljH"FwÚ]АA&&G4W-2,`J# W $:@!U砣ssx -3rvU֪HSwotŨ'hۭp#ϾMz*&#\/XVFE`fy[Z#zI!$R]y#R"~Vf\$ˬ&J%)}K=`2"i1v1fI+*^+$X# 5p1W" f$Vn-X߀tnL9|I-#$mkoxH8t<̀_wȟoI,&ͷS2Mel)c81Vvz tQ1)t"m,wL7 Ez@S$4;~_>(S֖А`j4&$%`r,]O70 ,bz Q^Qec8~= ;AX .C'$b{}/Sͫ!'yjݩ,VQ~Ƙuߺ,6ſ۔]-?zUvoD*9!ѪirV9āZOeߘ{,*nX&.&h/NCB譸?sU9?~9Lwf;='+eo$OqYzg춆[= 4`2"j?>!>5ʕ'Hkz0$G$w&xb9RjYzL{}|k !4&_>Ue}$'K`AOc$iAY(<&Ɗ?C@Rx2SvK0iL]!/YveyriGEELj`JKG=o_x@n8d1E%8{r壚'wgqQWxIAKUA0Uo]E'ouKГGΰ&#SfX+[CF@G  -'#O`V/`탞gC09rZv嘘TdI'L!8:xI'AL? |F"CIpv_<( -4=ؚZQ - .r eTg@3OI'@4+tJArf /6Z}{=wwP˛Г`J4˕Y|L3YncideaB>JA.vT$rv 0sxW= Nk,`jwIUqEѳ0AŚt8 :A孞_G$T8M=z?q)阗O&9<5Z} 8e DC.}l=JAF.&!)A''䟷˟!J2ʕ''U#j sלQlQ(ՌgQT5Ѭ06Cr\yRFT0thI } A -ϳ&}V \n -%8Iߕ4 QnA++ `7t1Y*.ުA͖8y 1`3DLjh8&ӣzvk8I!Ch'A0A^/!1Y RbW9I+YE=qX8}2qSj}sWBF)9uKIROolxn԰e{L!8|M)>m $3^'& jn)&$]&8$f{XLZ4X$Xƾ] 8h+dߌ#twW(c s>so@|&&*V5-JFoDqP8zcH rG"#yp| ߻"$B(G\>V=)B-8݀ 8i9JW"ϡ¹Ih zYfTNIɶ0YQL7Һ.sa(L&M+ܟkܿ,pEUs\!?_=v;3`21JA0 -=v` -na(4-|U5c)N5VaNG}]4IK S6n4IQv;0Q:|W5[J=`2*S'INjI(-o4dIH}|`c'rN%B(s|@w/J/r;8r UPD8o= /Asis;=j<`*8PqxdbSTWghqe'#?^܃2A 8sjaj0:w$ -u_>#3wL6x0N:q֫n+^oB(s]&]/QȾRcժ#!oI4Nn) 8;M]ހ LBB3rXUpLi%uu\0szS@p+ܟ}c@~ルnPi$l.o0ȁZZPГ' $&;(G<=yb2HͰ=\Oةf3qzt`9DR^t}퓰R:d1AOf+SҋYr&xu8"'>UN%ha~&g'y$zA|v^gTd6(!zv#bJpGtzք^ovviΔ,`oNy{ TS8G& QVJ8bQ~UX){ћ|êZFoD !K='쎅\y -^M(ΆV-X'w> vĜ?-PQBOf5e!\bTf6]O2!n' uB~0dGP^(=wg:i꿡_;qрI犬 ?K4Br=a;Ѱ"P<׻6a&$3x_]/SL)1o,*L=Wd OSUtg7*9>]&k¸ܿ,!1!|2-v&p"y͈&S'yD&փn n6Xja E(G $WDL~UP!Iev񓀳9.aPCFPBOfyaU9ߕBG攙Oe,rR d123M1@FE7 P$/z<%~z2&뗏{`U͇U$0. &:qzRГ$Mכٲl?|ҰʅUDLI#{X$o1enz`l^Ü&tz_G$R6GcГ (&٩&_#`2S/CTD : Q=" &+ݟ:y2 ==~7Tʯո:J'{Mpv;=`DqGBo5}(sc4^19ɯv~ħrLiw氠Qv`zR -V 5 -mG}z*߼,@*I#`gO˟!Ji' =Aq:xQBpti$$Q|$;DL~iXWv'n1 ň⎅&(G |ɡ4~V\!bS Q7XuB.sl>>A4IjPg[2$e>Wg=GQL'yRIc搰}d 8<޷;x@BJN:0KҧNM)8LgMָ@03٥I޹GrAB.1%19o?\'7onASDnB7NnTcAp2\J' W )s4z2b:Ld,yI՛=T葓AVkG&19UIܦB$Ĥca^z5EEx+ބ99W7 &$_K=(4>brv0.vf>|"&F^na7Qݶ0 zsIgC zwG&0^݂99Er)ov#%gbaU(Wp!vGhIIByDm H `T>"镮AOPLɿ9FB:Ae'Q)ȱqLFQyc.E )o=yU/ڮвDLkKeb=\T DZ^Sh&A:r?xsГ9<&i}'t(WԌh8z,:.  &?#CbN\ј=9$QߚwA<xC-I"Oo؍ &@Of?vPN)&!C~Nq%avTn" I^ja'SgP2I>5[ $q˳4D"4ŭiEX{^\醊&G427f\T ǙdND  W&-9Г$yh?&mٚ9z,'s k> NEQ^&A9,+j>VLaUY3[nVܦvw1 `Zs?} X)w{!&bz#_Ej{ۥ `2#mJ'9L]U\1'A0s>=#$)9<`rD&q&3ŒQaﱈnn\$_:}eݺ5oƜ'k7ې}qea Y=DvH9 = GބOyQ2y~]gR=I_w M"z$2NתVQLN-LkȚ0락|cI0ʯtH;Io(p{sw ʘ;Z^Ü&\fGP3޷;;e5ۮHI2j򏞔>sHz4VbҙЍ[ ΠNg띕$Wm1!0Gww `2s zMZ5NL뮅B0N`>ˬ%$eN=˕P寞Iop2`/8NyyZ'm .#,7M=Ĝ#Ys^O -v~ׅ &Dzs 8 znŭ[A (!Q7$e6'vKRLLF<%}VDd&S32qZ/0N:EZ U -g7:<#޷;H4Q^>Sij)II A8叨dDo&Y[ҵӓ<`Y_-&2J!1TpgwTYl .5V;Cun"$eUno -D$Q -੔1{%Vv2 -=ul!ġm-T6Ua -$j^o$dBJ3`{ 4L\Ǫh8ʘ;d$עL:G}*<`rXIzfʳTL*KqIGj8 '9YW;&b_>鋱rqg5VƼ3L#'L /GEtFE٭CO:FEI 7%R pej2+emLIђ;[ɍZ8쓎n )Ă hKqo!;K8m!4`2" k/n+2i&z,:6VaH30QfodL|Sf73Iu)82q 'C.vCO heGU`|pD۵4J(WjaogMr;O#a^tU`qԌ~<sed?)}|]S/z=qn -= DDGn^'@ZFPrK^߁!aA@O:9 =iΠ1='#ov*g>_2SOq xqҡQDN 8Rc}AM!vXt"ISa>DQf8|FfPY!`2IC 89UhD'ox. ^ Lqy8#9FBg7ʘ; z r H*]9b9W'I^} 饦4=qT -rt=Yn'AL (3v19y(Wde̅(āqiH0x?qS$OyNa(FzCRf&_ -%/WNOpwS LG8LO* L%KGo=CMaLތ٣'idDV9/^ &_Bi8٭,6 -F? A.F'}>Ĝ#{l'}0-W4`Q3Pg7N_A<]dL#:TPvrI^arD!Y7I%x>Vf},,rV&2l f);9(OD޿,XJ0I䏟>4z,Y`TW˿ո~.r3# R\mߎr 슄8ĤsB'A 4^>2ޙ R՜lJ+rnEﱈL$bg6a-0fyt~G,!,c{UIDO9i!z?J^iHχ 5x\chdv4<ɋvn֍7a\M0k1UG5-v"`҉CO: zŰ1FsV-zgovwC^ Xha솧i`WAyv)yUp/NOɟ˟9! &0!#'{ȊRq8 (?_7 e&PQM?ͲoP4"쓎^v#rbY)Ḿ$LPoN[r>@%_FEA  @!"65Mv>"'y7=U4`f5v_71'vx 턥a7 H]\a"]V9o]!wR*A&_? `stǢ` @"Ham?`VM<`?)}ZOg;NcDnj 41;iFT8DϏD'Ŀ'b;,`e3=QQhULOzuݲʱ$fwr$$Y_o5]094I$-9viQsu"awo5 F6 )^o{$k,`˕CLS-Dq'~$'AGSt8MqZΔ$q C.&_ɓ҈6IwzE h:x8| *+;GE'Z,mAd !jdTUhDE[x@J8EuII.((N_YF/0=Y;Oh’ɁZ.J7)Y&z(m[cC(\^V!)HV%fV{qIɖ(> ’I=$PLr2mou ju|s.)mcBF2t; zk;4 DuAc&'-ODyǏ,~1v.)%{6y缧M_[ ےX\%JdbDZ-ﲭ}Ip_=mI:ik7i$y['qӦf'M#.qQINw`\`}p\o;s?{YB m {QƼ˰ \&ɗ<-e4d/VR\ cl LJR۳R -m#8Sx7WU~& I܄= NҗVfRS{(x<qRt 47*b'qZ`Ad=O6IF~`0\&J,yq18M{4R(+-wԔt"o JibX s`DOFGnvX{z>{GTQ2tR>WR~Jivm{^(e᠖Z|1 07b"I[ݒx*$u*nH`{Ձ[kmQݷFbkd1s߼hZhbv\LGŔR(Yu"o=c 89Z.Ko KY1^kMQ -B\K8L[ -;EKf[ҚWrQʿyȎnG6/sfX0+=XL-bm&Tu? ߦJYX|Xe_w 2G7ZvN1B&f4 #ʫu6d)M'bͻE=P}T&O2(3-ybgK#)OV^ MWJK#PJ > 0.?Eyp#sF[VqMQδĔtSєsX}C4J5}9U_rRʯ+m7(є ߦ yo(-ѳ3iIF}2{Qbo20kttwoj|2B^ .we؞;&66[ҼXCL)tsADs.܁zNd?2[F!,3!Rǯ 쒚LC{)M'⓸sC>h/2A{/Vn?h./·AҜI]0׻bJ~2܄S9fYN' W2(R0zwiq{-XnL/lOʢe&eӎ@) L-ma N勵N#Ey a@ rcV27J龕HJ]ڂb L1bJ>9V;[V@~ᦌ.}/I(eUJ%^Kyz]B$]-p蒯^}?ȁbE v̷cI/Wò eLUX~1 Z2b;_{0)kw1\hTy90wTRmQKWg.A#L}JM$D|)ݪAӼ+=,Rb;Cu伶Jd߷Kjɤ$M - -g'+]a>۠|V_v+eJo}2vVc0WeE0.XǾe)OVXkkRY<5RKmls9+W{1 xqrSJuԽEPvVI(fH7:.9ܜ:32F1A/:} qHU;$?Z_bRzH2->z-h Hl@&ދ(DEh ?}j(b<"ƘN 9 IL Kˤ"WZ=;YE_/X67!_qgUK`; RK-jqv<̉&8{PXB!l2NJ˽rkYJ3œ5&2Y늠wGd-jۑQYxr"'+9'ypP_ՠ|1*]j./Fdp ҒA`;Cum82e))~~!ڞ;e;aCμLJb(9Pc1d^oZQ0343Yb9^z;K{1ƋFzLo!\&UddIł3KLFtj8%c;O8>&%zr9ɼ v^}b),ZP,0b 8Ė'xE"wC XT)2 =L 2G/֡ bLEe4y|g&Z&O^ b;{u%iCwے9IrYR>"$ojLBRv d2"ZC&OBfons r=wD Q*puR &g>9)ZI%N:I&5ދЪA+eGJ0(DL8<ӈw[xJ,=`;ߨR&uɬ@RSR\)OVXkR Ks="fWE/~BG6 tR6Ljbָښ-dQ/`v5b{&o`;C*'Ah(ICe^l/ZX|1=:R<|a s *^k2i.vVHDe{&_\Ldr2re75TtWj|tf)[4`d>GQQQhJP5VIHf&=XGEˤz/f-/WJ2dKo`d~HrZצZX Q{k^d# bָ.i/k͕4A)#[ ' Fes9d-n%}xi|SD ?<UJw+6b&ATҌ(6J̢~5no$40#%ů?l'sk {r{q(fGx]SJwѶ\l_׎OHhd,I*6[8 /k0d :IHQDD~gᗛnr`JGc@#\f`; *4S L:ZDVd`bvr,7K>y[]YP}Y)Ld轘:sh e JGw XUmN{-XƢ"XeznOXFY2+Y{&+!HͳP8%WRr$cg%Лb%Лm܈hM!,TZɟ*scI/}pșv΂ދYJ=QlA)s~6UFpT'RKC <$Ho=@@,0MJ/R.5W3(gZ,u.}RK?+eD)ܷ<; -ƀ=a.uW.wZp1* UJ?2#ViWپJd)mSe['CX=yy -zcB&3s rQUFQxFɾؽÕLP܁+hn'3ZLjyn.7 -ɀmAd.Wb`&XՠɾXr8F/M7UrOffT%DDgi)_t@v2IG|Lf$~e8S9FI}rU }g ?}M^g/"D'jBIB;S_5OBs -xs_.|%o!ƞUeaR)ELlnK^QTTFml]n)9O2iE2%OtK[o(O ݳ/H[0I%T턐4,E]-w:JJLrKF2EK3q+r'3.s kL &X6:4cW7XouiU`2<֯&;Ȥ@—qP_iɧGʼnB&s),!ڨ?G<-d2_r;y --D-!EiѪA3iwUy[X6in d2"nSeS-֖>ZBn#%Y9d&b O1Ĕ-t/%V v endstream endobj 30 0 obj <>stream -dI"![xi n/9Őנbvi΃# Wj+R?Gs{A.w-up;i{h[adv[e,yl#uVA)uCI۳Њ7@3èIq΀ދFC~B?WUo!gb(e/sC&+&ov6Jrr?6xſO- ~}\)@PyGǔRC#IXLcT|#%AX%B2Yb!2 p _a@&b[(RQ{1u2YaIȤ HT.moAL2en@>.3)[ՠܧ?|B)SxsAȤB;HE^z}yM d-uVY-\j/]f9_AB>xR&ctRuk2iig zi5VwYl.+.i(6f[& Й~cJ)a˨Zm4g?/BX +dO;..]&f]^$_(LbR_5/Warj,QJL@&s ^RtN&`@A'|;|u /IuPѷzjB}!d2gKA^4nrK'r1mPs6rS <9I}g N!E(~SeEe]uZ$Vx8+d2W(W/S2%_khfOk ʹkۡSwA /Bq Rkkі k| d$|B zh$5SL:[}e6w' .Ox=C!t ]Lu3|j2Q -Np<>R?$vM%,U`5* ?QʘL 4dŠ&SAe^4w}3SJtc C-Ehy+z "{Q*7jwȇBLdd2nދF2!¯Æe)_҅ovKկBIGNgS*yZU%> @jd5jbH ?WʷHU*2X-jzf< #27yh9H _hhLPv@KE_eCFNRz*Al %Jq#oDl=k+VGU -ypO@p<נ`;mM{;L*ee+Ƀq/xI]4EKoo`iU}N^flk 4LZEeV:^bڨÔ_qVʶ -[%r>W߼ֺG5Pz+rp8dS5&߆C *d2({Y; -zk20BWJ0l52 CEE3'.4yj,RnmHׯi@/ukg*L+t[EEjZ싥R6F9ϒ֊hIJ:x=UrB+~{:iJ)*:~砪|qU-̙IV,ɜ Zg {!~62k.H0)LQ`aKNaX5!ěa{ۭ -Le&ODPRhD̢zMI|VM2mC r6@&MGyMBE#`UL! - _CcJa^rP - MTFΐK2 8X_by~݃ty 2iz{8얮I/qJ!{5f%((A)*J龕mh @eGBfd/c8c JyABRJ.*ͬL%v %h[&7Iw3=m!z/PPj *Rz -e>ɖ->RjUF+7*3AȤI!pYw.z^%XJ٨|>[,|OXJ!&視7:EJ}eM)i2 4%%NyTKpPGW؎98D \ V\#VVpR]>qp x2Y\Œ1Q0{ՠx!tL!(e7< ̥ z*̛y HLzL~cUL^4ᦂ+³*OynͿG|c#drPLphx2%W[_b!<@~_鮶^)Y_qynGVڞ5ѦA$*Е<d -{QAas#6~=X*nWN[aGs>nWz?*:;&dɤ[/OILt; l b [ꪲ6xL{ Rl'$JTW *!΀)b[[EaNK/z/ɕQ!UϝRs9=g™G/ -½2ǀiGnmSYgFowK9 VH.2YH@@PVCE?t׻#Is;/. -Zj z!` -%o?PX-LA,yƋ/ƔRPGhC'oFfm]QC_iU1p(%8$ -"UAN|Zj^?(%0\&LS< -Qxa7^eGӱ y_8?Y'eˬ2 -@&p쨲t $/D9- w0A9WʷaEYy5zZJf`/%ker(DLb"IދqM~qs^<#ѓ7oE-d'Z=b9ݢ[Ƞ%I H~*g-y㬷uh8Ӑ#ydUR&)3J7" n TLTXɓy2 ҂ ^4^mȱQGYo=#ן G7$V )+o27C46ǀ -CGNlC'=t29c{)J=V ;4s74 2I^Ӹ2 2f)^X<’lPH㎆K@su*M^[' 8{K/a_"Z.| ݀8  Q -0qYO>r?ͭFOqN)e[7]Ɓ ޿[C36Q LwLz/ -031-]i+\)ŔRc6K:ˬ*kB E|wE̦zCW"0 VjU76g/K9mt=j{JpdC轨3WZ_ټJYc%JiS/>k2iкoV~C>'S لom`fbҡ(eH}RWJ-]dMچX?-DwSTؐ&Fe?A&A!3,x @8d T ґVASzm2e)CNڤ2Y Q+8*J LXc=BAEpSg0&@PyS'ԶMJ-fDB놣 C)PYf˽҃yuOfBT)UVz}k‹IޢB)I9]nRki蒯+dʷgf ]TY`t$QPQ`%(yv>@DeJdr(V ' Q ԋNyU}47:R(ޤ[듂(Ў簝PJ@<39$h"^L1KMXgdD)OR|YFL";eR|>IPB&uA'K\*Fb "7f C)A^Q6/Z9v Z5ug)OrTaj5zg^? ">M2 d dFE}J}砪J+ Y A$MJ HLA&A‹Hc[/&XELDK,3 ]uC܈"60Km h[`L! @Eݹ&/n6xqur8A(cuk gZKA$E!JJє锒akZe%M">Bj렔 G2)Vo=ދB/8Zs="JI>J)Z$ '7>o= /A轨7\l^a%2٢rDJiXzL$Shf,y4^4lZ;Nº#XFd|/ -B) L~>zuM -Q@'mxMzp~;17j!+e?ǻX]J(%2LB&A%oi ~? _ -; x+xhA}jd "_ L AE}$`~9,S؂$BnXAep^\|7@ qa;cْl4!I>}"^)F5cd$0轨 ]vV{}AHI~ށB%>/³DCGsA(bSD ø'V~[zfJYl)J;dP5z$f -`2Ȑ:{QKޗmd^mvI$Bf)+XV B=]UOc8{E2ug_Qa@Db]2Đ=’>uǁ16W_AIe)&?L^.c`0'm^DwX(Z:fh]J(%ȘD `h!O-I JAD){O"RzD)y!f.27B/y.\o>熞."O'_n*TYJ"4~gC&P8\kIFoCXPͻ >B -ɣhi S^2 -^T Z|2 .<#ҩK י 1TPT.\mѓX3AkYR"K wR&* -@v!uaCFE=ˣ{@d'x+RB) `{&c p!>9Ʃ ʍp7QPR"K d[)U!.$XΜ Z5HFG- Îy$x`@dhJ(Z "~oyIg$[Pk;]Lz/v&g_9nDD(N)$bNKW?'?)7 DMtpCJdLs=m҄h{:RfW_y>x ᓈM)*uUwovW[*kZaM|tt:ˁ!Ň7L H qf7o'tь1QG/C&rI?h ~z ]F1sD[-=!_>({?cO;{OS⟷ٿl_ǵ?K6Y[+mCL T9Ĝ9 vfZy*čmZg;۠@&rw{d(3*]rlj`kvu:#/9ۣ -yA(E?SãZP0HK|E;.T9N4PN2'n;S_pxsJGlyо$|O/Y_8Ĩۻ!UKf(VK2މ:p`̱؍ȝ`295F t9g,wE)A7uQ2B#q L>ڜ:?!1+m>U[n $Q!^4^Vy l -O"r&OxVW#%!8ΤTdՙ;gxL .ϳa4܄g;s{/b;sy +fN '}|6&f<%?\AA)^{Qc$m/h^JUJ$"ѣ= -&HQ (D c2fJIl{f&.v {/cfT J-fR,DHС8ޭD^r -(@(3dU 'mF>mDB6r< OQ -NBXNӍvAf^◯4Xn$"u:'ȋ9 `ne!FՠtIZ>ȍ6H]2v[ǯ 7aA'NtEF۔ ҕᐳ?Z|2DX"%sB轘ZؔaBnI'SA/\"HfQH!b#AȑD :>|~SeE?L@n%sB{/6[mR_LA2Aּ>'sRz/fX'P%ּ{wU$BPwXHL22NGDڴ6bPfMN'I%z/<9<'J7O"DEIWe.q.ALK!~ͮ>?-"{.8KQ/D9-a'=:Q5(9Xkͮޠ`c<>2-1r`v %Ʉf,y'bsQd|h[$H`vY!JL-]eU05[|+xh@d=q&:RyẌދ~ -] |2c"B+VSrFT =~'_z7"_qˁ$Ћ-x0=|fGg3g[:9"F-[vD cd"6_R[ ⓈX!@O.`%CߤyX'{]}srtᓈlA 2AW^L^ÒwZw. k-y_9>Z6EM@_& Yֆ#9i>['? '_oe3<| G$`&}ދI}2ڊKE5oA>?܍݈l/-!&z,#@ ŠB޹ߧXIQ,y*HLePu ~ֶ>R͓KI`8hDi0ƃ&C|a~QQ|d1?~-pF 7`a -C9>yaAQQ'擣I%&`:z\kp(2 U`Wh1wvQ Iث] Yc 2Ҍ"iщ$z/$2I9`qVCYa({QE!4nO:6zɺ"(%¸.z(ވAVA-JZW$/~zǯ']:ƻt$6ZLfSJ -+f>++=R)b!:& =$/(; jQ0~*g /3Ƌ?BF'IWÂI<pb9Ή''KNvDP)( -/D)/AxPhs|ȂE jkkc)J,y# tqD; -(Hf UC! n|'$B`#[F" 0-#NNKMX򦇻ɏOOnuvQ"E5^g=q7:nI -.(7v]'Kag%OmJ,E DDHN] '轘ە.OktwBw1E b)J$B`>GUsr -/D LZ5(,_D|aM%{֮-^)JҍnǺA${/TNLtK&A'S.}V9ةmhA (:,"_> r ZM"ÇC.]u{ӝwX^(n9*@rH/yTNݒ:%VչHw9奄keQJphAwh$hrԢ {q(S\C}Cvw-PG|(yys(?- cQ-@E @vދs#=JOtA>D>X:wX9틉F޹(.Jh!AqQ;^e'AN%a(`;Ui0O},șzѣQHTD8qjn;ݎwQe{?߳՛W >(.JJ`m\n'w7OnS1i:f LQNp/WIp|+= DNA'AnB^vP4=xѠ+2.rWis,Z_}{+֊D."ʘ(A;K2`}6pU՛]+/Dɱ.Gd;Y@-J7hԿ*d_D?١]IyM:oR 2ɍ0vot~O/YSu7/[U)Z'{sh-2|Rwrsv0VB읏!5/!=?`},OFR妍pg8C1C3N;G/tw++dx{[rIo}ѺMwx*$&Z_ v'D$l?m1A^v9iw(5lrszϯve_^i|.OB~o-am7v!:E#&W7^2P$B2܄S)@~JC\8I_J^3~R΁E nwњWoVܴv(|Uv& .rԢ4:/Z'V? =v9w(qhe_cqğVHaMO"%aޮ`)YؒǩY$ g[/VFaŵ.w˽/yrn>%"S<9_z:$݄,_E>^9yr<=UWVܹr*VB{wϧ$RقXJ7k&zy:KIcZeSr#:@>B.|ON^5(,_ﴝqC<82v`t۳uKrO.x|)JLv?oR"/#9 -Ǜfm7vMs#َ>A%aϤ$רXN^ww8~#KY9WhsspbjZ -Q.^UA|_F1m|^gw  .51UYop1 8C@/T!3ใ,3sߙDyRJ#ڴn-N$S9IltD~G!#t'*z8"z;0ɁkKvr[Jv׬>'z&. hxL!yuK%kK W%o$UZ%2$U"J9,#X 9k/p]<^'v$`g Gί?yb<[O]w(*~*$/D DLlHu9>~ThJIiapIi6 XN3rO.())%>YU3ݒJELIƵ׏.]f-cO]W΢uItC':e' JiTU6) ~"Azkm -Do}v:zK;%l<3{?a܂;q0qd1v'o+nj^\H|rŲ'){' Jߊ:$s.yRDL~ o{f:I 薆CRvŃ䒵sٽUغg)J2T߼/br eyؖuU5| E]kK'q*1m|Э$0{)=C*Ҡ;'s婭6)2BI>4Xk҉l<1tHS\SM _K=?yFYJzʓ= 4>-ˏ֎F9VE ݵ2z*'>vNrD^O33U)\%-.wՠp19ΎH8G#NfH:}qmj% %}O^xcgVm[donv\iEh>y1WntYjuެ֖y;o(%"GOr|G 0;+e,sDrr˻lڅ}47.޾i?=0%ӆ#%IX~Ľ/%ygqͫk.V֥dME.9;9`䊻7\^vὼ,ha@d=qɉoE)9VYhj{vK}bͫnsX2v' >yQŒwwF’ ;'w]z^'/U=#2"4F E,_G}r{vʛ|YXd,E ,`e'?`gp>IP0̡1ˬ2Z.=A${Kuh˪{Ui/>~m?3$%ow_Pg{/>ӧڲ;B/*uawjnqƋk\M߄7Y7.^$KQA) <ި'KIPr4Ѝn^*/=r֝KjM$Nt;yr%~[X"q7\pǽUM{!}޺ ?^|<6IIO/!>x[( OhA vg}߿dR YVװC:/km,L&T/yNnz}?=Wo_[GnϼcRskͮ3 Ƕ_pOBɫ/$_y %NyjvSDsPȤ1ay9i@IbP }2wO&;o(KX@Lw r*E=@T+HQ&,yg~]lE7 %Cgi k-c ?[n涒 -{|ɛɹy[( -xO9MN3d+UjdR>Q} L#ח6>OrU̖O'$fRtQ\1ٳC^gʋJ%|[(O78_z'qVҺ.%o' ŒJ/yʺ/b e!+;9 >fZُ:ـ(}V'}&)dڲw[v4$1'z2h|jK'^j W>=ms邞JKn1% W|]]]kI( ){nxy'JׁUxa-?q6sJs՛}\蘓dpE?і{u鲵U?$߹i ;Ҫ;<9WArCފ7.]_}r٪: W%ɁOVNZLG'MW'|{ڍ_~hY.IZ [(vխ]pOէח#' 'y  -dMNrOM RriD{LmnNi-^_ɞ9irǦ5KI1d -s]FW[뺅Owg|;GWJv՛w^'ɗ+O^[v]\zw _'(ĉ]H^P&d0B)h+p)^ŅRiIIfU:#e)wmZsr=Ut;ؠ ;c9ejx'o]ᓅp  <3W:U-юo_'XQY jtҘz@17).~]T4r_5|rݢLɥk Iz˔r8w˖Shix' & Mt>~^A1`Ԭgyw~7вTaM2>EIhw0V2M34_e9_VB}Z|τ_)Y͓wK!G葜-T,\U]rOz}?e'$cRjO*̃kܯ=|ÕL=t#$Rn(u0>Sd>]Rzy'D7Byjnh.ge4">w׬zc}ɹw̽ Wo]M|8$?ûxO瓨Biэ!'gGS -; =7]T[;sD;6!yj[tr46Oj0%v6<9߽,!8v;α}>4{G:l+xn\n$vNI(^mƻw%yG2]PϬrW%%)rY}k ¦N9 }mtBˇo/e;E˖a-ϫHQ&`Uu[#|㛏.k]E&H/,6ٙzᏎ]&>ɻpL 4l6&|x> @dC)cЦ*N38m{V[͛L_)YrsYϞUNز$n{OC݄2܄I!)O^ -Gd~8!-[dR.amߴG7O֧?H.79Yhuk=MHO#޼7|@F=vN>RA0{U.G~ۜm+O8} ݸ_^G/hvCګϒtwx'L'ݱd?}V8OHq'Mך]g]eeOuUuZ~2"|NNm{(:eVy9"j:~]zsjks#|B_OΟeQߍIw{R7'gɛX_GcdLG,+h+h'ᐳ|'^#>95B ,=HN#Vh!t嬉B@ qp5>΋d܄N?^{zxRgSy2')o7T0eAMkheay? 6E.9dp-wT\r٪ū*z7| %'[l7:e$z޸*i2 IwD#K(_w[L Ө'}m= s覸]lZD jPJǣN>_y |dysC |r5'yI[&mN3Ar0ՅI Х{:N햾,wkǷc{ZWۥ솮z8z yKz܌g)wоE+0BF3 ޵]}džr^2tg˫$R -snt$z,`20Y],s$u{n(z9UR29Ds`Fn-38%$UB<ޒOoup]ЊQˉO.>O8ƻ\UpvCjT=ϞQ:z6.ٕrOwU8-Z;ZҾvO~ yGOMw^Q6]J|>Z_}R+i4iKuKF x8<$b_zpyew2nZo_[.F(:yeisq;L1;=êAp;bqܘqի{|i/X|ɚ;hz>YA/5yc&Q>^~GPM''v|.-0?Yk't6זm Rht^oRu@@:lQV\jcc++ޅ#agO d@vnp[K|rC4ҵf:#Z\EƑsJ[7g:Y0IV׷.}rmO.Zᓱ/]k@hjLo@iqQ83m)pTiˠ(oXGyrcww`ѥf3VBpœ&>rH]E|I? ~ R&Xncs{i9lkB@f]ݞ >P>R+x. -3$=dR\ow֭D,}v35M?}io*>L)4emWmeO<?1D wD9[Bdr㧝|%ҺG/(“w4/&mj϶P_Kܛ{DIđJɋ ;,#}<}c-;˽^MIGLن_mx]|^AYgU(/7K's{7-JXϪWZy6 wGCu:&&ֻ9C ^w-gI]ADnđssfTss{+K=|3eI̔~ܧqc՛}>x+HQ}wt eK'{K;\tly'y& +$?x{Uq"⓷Ts\h'ᓦ -vny$id$יU,yIIb֥|%Z B;t0po(6=R0a_͓aY]LtKGY>~#x,+mD##/(29\?ڡ:T)>`Jzgu•uIF$j;RrUNBOИw>ZSY^mIΦs&R9qsLj8>IAŒV4Z 8_yEk$ndU9'LMh\JrA3#!zyO5]vW{ʻ?|ӻ@Hb,$BIYa=K:-rCȞ| F)pRZ $l e{;a{XdK:H|ر-{ށZkA}@&N`#")z* -K+}r!y1~jvT<\R3Nrʇ7(e\ab\ ]<+y=~E59N'#$ǢHiΎؕ-[K~yuLF͢~rE?bzr!_Гqxg+682udshcR_<ԾF@<~{׍W)Ma?X2ӓ9֟5zux{RÚ;/UOuEOLI~Q#9F8{H]vZz]{;'~A v%JƏv@ιAX}61rzjM' =V3>C oь} $One<ڷi ?LħCvˑ渕&fᚳ\u'g]~Г~Г^|mW$a&IӨ,ŷ,Z %8Dž%Xӵ{Oގ28䏝y֟,6G %R2pM4cɏoť擌fd$hLES>M]<7PmHazr›]xً|AO:~;x,E3YRͤnROɘ{~-m$/vRԀ!7kiݝW,o&/b-?Sl (+pfޯOsm]rAG aqR!I9%yUi9?cE?1=9g$-R%54$%{>].0nR3֘s_Z6/Xwwr9;XMo|۳W$qbs̰ҔN%, ֑5v63ThɉД]k1=k d\-i9}N\㕋;JN[HX2+g᜝w t8bR^&0?<(ۘC84o+֟26p -՜}`zr߽go[y'RS%rHAyg3=y_O - SOZ-&er"3qpΒj/Wbrә)g}v K.Ȼ_q4[M$mi-?W2*5rd[mA{INl CAr_| of$h_{:EGf(lю?ŋ) =?Kd=} -:qp $e$܆'3՞,4e?/䦴f더cxsSޓ_~ۢkqz.ϪKQEy͢VĜɵcyr'> s *[*tܭ];nLv8D:ӗR:οzhx7qamI:fp8F2C #)=\a:}=".l_q׌7Ef(L9!N1>H؟ k' } T=IC_*_n4NX΢4nV[ozœ]bM/_RIDեRCgam0<()zEc\xc"QE_jY!x.oQl,[h~ GTr,MO)KK{M_|0 -ԓk"I/>Y_&bsIFCc6'kˮB6's^]>?xW^uݍL}΅#k/Ĝ_|?t̾ $%+U#SN|ahs[<[c"iJNd-8Uz|YLO۠'lJUd%p4]Q-OFr3u- 3yݷ_~pͣi[-k^ -)w:+v[mXjv׬uM!]5k\tdb'od\3'S]xm&=I'/6l9"vU`s_`(,UޤK=a">7龋ux 7𽠾ˆ۲Rd#*(Ԍȝ~"Kb!Rލa'(e;vQ v6EڢkL6gi31 =vܩTvz#'l+))坩ƶ.Rs]=w/k.1Q{iP+jx!J&<ʖP"UcQG:=ec:#ВVĸ9\&&'EX]xRbi: p\~oW,xbzc8iqN-ƥ eÜiWb]6gL,ޭ}'E{…(t2ڴbuəl$%{% +Ye&$0-. ՙR(5=񇋖n~^reSՖ/ݿSbXʘ;JG~Rnn!*Wz ->ݲ-Wtk[y… 'x[F tUM7 տ-UH=3z\;/榠dz7_jY+Kv j[G-TjʓG=>LQOx4v0rf]~ Г^\O >- HR2i4/ iKH CuAks}`~=Ϋn麋Fi.Bk^J#D)'-l Mj,8>ޛURXR alϕd`GB_~J]zE'b8N\|mƎoyp2^/ l") hv|;D<ֿ!%Y9={.͝W_cLc6w*&rW}-l#1#zW /MRTGOsT!6'tSL;HJ P_I[>&I=1 3~qOnYtᕷ*/#i/6zf'.)saitG1Zapr:J[V2{._N]ӌNafY-nCR -!mYHyϸ:(g_i䤾~`O.+̳y=97۳Vʟ/qU-OvrxHyR9>A,adla's DfJT'.|/V 8 @b)oԵ-R귛T*w/ -/9;^P[|m7]pQxn_KerZf@I_Xцf]9>pQs3Qv=8Ɉt:-IbΓ\]J?SVaD*{Ba>L >x_Oޯ/Wzu|^Jv22p7gT$<;!sssewJI&eHO't1[$"$6K<7S0/yk=|)L/>u>d~/H9b/=Qb#;0yWs'ceZ?bz =%R?I|BX))[8? >Fy) Y嵲v:Cl8;!- !¸!Zvnn#=g^"$'žx`ҧk4[PB){ ,U;"0s9$Y8Mz Nc6>1HjGSHU^6m M&)Wb ax2;:ՆIN\OX-\zҩ'Oj` -CRVT?גPUtR&,r6M2]i -A4$¯&jޖM]~bv/|0`b/) >cr^Hㅲ<4SJTȍdZrJGN -{3q'xdAj?D4`Tb,)Rޱ@iΚKt8+BDD)C9bf2 aI]kϡd.eJzҹX:Uڤx@kIV"ȉX } g !F)2xcN^C' n5[Itkb0!rŠX?đ:% ] -ER!]y%|!ԓ-YLOj zҁ`= IJ3>!|IӤ*+)AȰ;{cz~UxGtќ$') =ŋ'7Hy⒘Izڞ k-6 -(|%lɌd21>ȦZa=Xⶓr$!A`|LQ}6(D2=XD1"9_74!뭹| 5'ܪhÁtM.1:.Pq|yr"zsY3q;C;ٹnʏ7j,'vﬓ%ࣰL$XN>{e6wIٝ/W`tz!S=I&&i2%gBL:j8'K>n;i1p(іJفs}8eht j)^%Dq{0$w#et⨘"Q;⃆̗-#pH!"f"VCԓM*;kKLOX =d'-lS&ڒ)oS0zMas|x94qK ag')V?zanQѓ"i&"m$$`*YWnGRV!?۸Փ5IG8FpH%IJڋ9{`6X>FrrȐƑG9L;HX⇾+p*==ԗl6!!)ocw7$ex?5M._HSf;%cdVN1U --O` D×)oBctrEr0X~<!uŁTG*zKA2g@Tx4C&4}iX >^HS0yK<(%-[rG(KuVhK$bn@F3O<8Y0_nn$eXSJ&)VCIad?'})Fdz K8|`4 w9ֿ)0LεdV} II>箱|Ιl5DҎA ?s/,N˃['BDTR5R޶rsάZ%MsDڔ'2IONt -E@ !I iQVr; z -f )ÅI,y,S/]F xvyR,NI/ 6uLQRv3Jk߃vŔbcW^lӹeI',ccMRSOL+rߦDeO\vSJuaQvx&AbآOMJmMPW~ b{N%ct؟c:#SJ&|x˜ew&9'֧`3LT)N%]d?՜1ĶRlK:=RŊ(%'әbcՓ V?KI.5f-%ӖQQ]OdX =i;Ji?v8B1b&Jy< 8 6, IBRvJ~sn͚כQc[Idda8-o~#۹[Pp$F)w pg:b8F2Dy(S;;`d %ӓ n)绡'MPHE8B:-aҥ+_nY2Ҝ1)6f=w]M]C;ctIiJ_8qTKm\OR!NhŃ%W<  Y !)'snaMcadiľ(HmqP-Ƒ'LPiΒc&)C|Η!|vcDт}v ՗AfA0΅M KR"址xё*$x|z5E)k6o ;ٙIiy|NI,A_28xcIS`+$6ct )'DRv+wʗ}>iwl}1Q(8Q%e[mF|21:dCRb ._bz)S|qRRn@2RRzy)< eGd\$w ;&SI&D|.˟.Fg7 ER./: IV\]=LR SʕRO} b\p"3TavZxX:L7 1"-A<ir˟?jHK&͍ٖQe:Ob8n8X|G n3Q}(-)ezCbE+ =)ГvXv2N8C`;{ 4Q#$ec`׮VI?cp5׵TSI1@!$e[eH )4tKonʄћSTӇy{xحtyR T2XV.16hA|ԐvTEs$_׮;Dֻs:X ۣ5dh|N|I@"bZr\atѳÔ2WfђV:sS|:=lғaHP=dnM",J,I& Dy$ĻDȕZlw0=ٜ5ꪡg']\O -?L'EoݪҤR1/v@k%"JsMd Bz23ГV.cK"8F@\n@B䟝?fH+V/XBSjHɄ%og~*̛cG7$eH;/B+K$7Q:qGۺi@pcz4䱇2uSZ[F}[.O##~ -(с >~:w4ۭ4VHv!#ȕu~TB:WC0yY6ƍޯ- a),EQ}A\{u3X,cO3:qP&p.ECcDF[8׵fQUdWre3<!M׫빑 0­?(2s(rTvh"Pz31œ]t^lBg7yP:?V'ХڵrJ^94d2W}LB- 9ĄhšI9Qjя=iڱ?6A`4h4װ(P.W2d19,8fXy>J *,-P 6Fg$;z< =i3q -/v >Nȓ\FxGjTK+\cUE"&˖ӧbr˅b>l7$ _;D=i:/86 lM#ʖ5dk(ן -'X2 =tcHʘU)/t5Г,G3Q'/?#ȺuRJi*r -208 Zvcq;P{ܖhHNH|Q.P{$5ۡ'X|,wQd(U4|yU!RN grztGo!̾>FQQ6{Lmh/B$zLyԨʓ[dzkȣ<8ŌXR2 Wws"!HQa)xlR6zrI+0`-ovm*f Y6DVCASXrq"KmмBdķonZ!&!)chSc& fuMNtBKU%o4+#?$\`Xf;:S &)D%GmNgߝ  oHX,c#\yGPUxn9/MV 9$& N/,uUD,Otx_)4 _4®6B虐^O=2mo15~"bTEfu+RΚ$e䨪ɽDv$娜*%UN@%'%d~HUTٞý"26UIbҬ3.D5΅ё#7jQJHh-ݤX| !#|*rζ\C'WOf²1:plPrTx-9!{ARFaqk $a9/mYU֥' NNq%r;zw]crߐ#9ɢR>͉zmG5|\cjHϮ|UE'8#89X2aM,с7$e(sMt @' te$"WKe\Bgblq1%„eGNc?BRF-Uq*?V8ZȈ@U9Sn ڦؚ굲M<"*"Yr{.E1K)tHIʶ\ߑM LR҉$%1Ffg = ")UiTYΫ"m_L/q]TzNң}gAeԟȊdҤ9Yj )xd4 @4̓OR:rlD~㎕Ԭ#kG˅_tU?nt<JH@|9ߔCX]7=- Hd$2@iUK%pr42 -Nkaޜ wtӑG{*"JH ڨ=7]!~Q|E4ל2 NԒ){S] vkaX -w|@D)ྔ EXa/n_ww+ %7EsMOʽ"k\b|v\jH{k+m7]Ba)(b;1 rJhtH NH^~g"t@gtͺ"P`L,s(Z"/"2FrEʗ*{HBJ9r@1)I&r=-T:r%Yulir2rp"4]q c ,}Ƅe[&tEbj)WߘڱX$UtǘFUdWܚ%{7"H]CǗJui Z90%31:"J~71I)$PH6Al >6d~DsMwܖ#7ڵrٲēCzrj5K/,ѹcB2Uߖk©֖RP){ nVSL ”2g=کMp`W*2Ө,[FUS+1@aNˈIGtې5>CD5$$xp4@|77UGk֐*>?oY0wt3op*ct%%8)jϹ_J%⺺7 !#-#QU4㐉7q(8zM";z4љD[ph;@R%Q>tXFg7fW]۲"@:;zb=.M]O )H;Q SCҏS,$崄 z -]Œ,h3*4g*uʕM?|22zrr3nXAXF|yrct )3zKVRd@4$%PFtvOґ+5ilMR)D#c)&/Ӡ'h 2D1:}CR{OBy0M)SVݬbF'QadNԒ%{7H+TEZ'+WCL:!wtP/74vo$f蜄I񊐑=T٘]+/JgCCZ#&J>LOt*kpGGd̛H!HR ~D*1Ka;DXٕ'7TY "mk &Ē \%}d'I)TÞ?<VRHhGQ;#9B@@m"`9Pa6hcK!#mXb;G)B|(bup%JRDkR,:;+{ -c"P@mH:!'|dol44Q_pŹll$%fb|&LWBpCEB(BcNRe)(r4{R-??DoR -6XHb -7>psߐZ_[w7pot F.+$%3a)Z‡س?Ii&aϦz&JGN -RDtl9-ǗJui B W;zڞ'rbN(}ߦe[XT+>%)'>Ctv $zr5+1 ,(jpGOΝn!쀤,t)le^3VV7(V d2LWS -oɦb=2eGȹo!;=ֿf|D܂sڱ8$%G|['ћc Uk0]DCXf(MjKڑ&j -q$AE)icl.KR[93v:(09=N(a a5=A1:!)}|7]z`E)uMГD Ǟuwǘ)/,*TbCVB%"J)jxY<5t=)AM4ހ;'xKX&;zoHJX*,$ֹ&QUt &1Ise„ew<ݜ-CRžݤBxX\{u٧k9Dvy+%'a٨]=ct>ޖᏐ'y(|ƬJ Lc@zZ X;6ğ;zW>hHPzF"'EʻQwJ (*09&"8i^ 3΄esXct 5GIxo4/K]$CRFvhQmMA䱥u64-CjaYFbaܷ[*,蜾 '9ww&]uu*W7'zmU,b%ctIDep6Pk$AR[hY?)I2§{ L#,&J>TON(ѳ@8 IY,QHJRnw7d'2%$%QEטyWjIHM,l^Cz3%hN63sBt!ʦtڹD]]ؚz01ɔ9 sGRpG\L sߐZʮM3I9YR 5 ]9Ĉ5͉^ 1 -#[=JI痔Es˧+m<ٍ7ǛTT,[O9OLaIK*ZEB(os4%evoaD٦Emf -BO -N/k<8V;H;-box;cM9\R,{Cs*)rȈgbEL'b' @PKþ~"Kj)rD>U8JNi|ϭt)095ټôL 8AXW4pVBC竍Z?a9XROWr TyPpr+D z KX\GSQk&)^ҁҘ+9@LyԮ~`~~{G,;ƴ -RZ:@ʗ2i3'Rܢl'1.My(MTRQD6,,{ - J&>(ٝ<7 LΗ|"&m@7cᱯ;:s.k)~@Jg:s)iϲO7Ę5(q*WPq;zG5o[ĉvZk6>sJ*dǗIuiN6fKx3X-̄nhct )$>/Z%L#J=% AX@} -djx0yM,^C -Z51zlxMYjO2I0 nP5[/nsT$qXEzLXR}(f&@w,Y1:2Ιvx2̚Fj8ft尽֛j @"S&tE'$,O bNPF)F`b Kl%QEB !J,ibdz'"NٱsGSsq/)ꑬzy $o7)lvSS4[*y e9Ah>\49I7>`T<8u-9SE6=0"-G\@yp|D(sL~.g.]{~ig$ϓ$CSR_OWZՇ.K♜GRk7>`\M,~sKjEҖ}&A~^9* H_r,M`}Ҧ\$]7/y$'%f%͘4+INNv%%%''=Bқ-}J~wKi7'}i%Iuz3<4̙$# 'q CLUY9kR`D =H%Sp2 f*Dږtˑ5ctH*m&)}TUF&#WuJ^)6oHsA%s$9=$ ]$=&' IWvPq6WVًRų<LZ$g=0f-ct!)n>sA{d|4#?uE M>?eJ:팤{=$q&ӣPRh"r:6Jݜ1I #J@}\,{stE6 D*"ЅBX&# Fhd#S9t{]uys2i3$x3gq -[@*m,!!)k2)ɮty`=V^9z==$qׯ-s5DSS3Uo4k)@ЯL$e[pR׼ԤY3I1HdV@"9CPʑE]XUũ9{Se] pb2pGL19'!) B O-k\7Od&iIO!]"ldh( ȱ5*T:Oygna9O`*Wa!ч @ pGRle`IJQ#%&KR9#)ifpr1tU)%uAc! tzmSpr$@M,CQ\ -:XYy<(aTΚYUTC>]aamoL0l=WΗC,R:]uɹNb" XjkmT8 -x;З׌<^g -3-%'+bI Ocz7/z s" 8 -eCeܙOCBRX7'(47+AARR4)D QN\+"&˖>ωlݣv]|J|SC2b2PUx TFt:mg큱96&UEp)4BdC^LԐ.yӒ>Ywf&g']$OOp%PevGLQFoZ1{u+eTN)Co1ŘcrCZRÆ{¾[Lff]XWf˭UO>;.N3P$tlF$%Rؒtl}{sj֡`0*#"_4%s8÷no\4* x=]X_xY}Gn}⮚g~~mՎ]*?Yew?7>sf9]_U&%ERLӞvE/^:\!rDј[?a=ӍO2H&= .+]Tϫ꿯 jqŁ';?|^;ށOSߛ;g3'Bqƙ7o^_R&Tm54G&t тg,eiC4jR7<12Ÿ&٥ϯz1}8tez7roEү`8?㓯3=yG8HwJ3f`cJ*WRR|ECZ E?E)[\PWr%SV?jWK*wod?B/{?yS72XA>I(o<90*_Г#LΣ"&Ry?S/\\m38B7* 3)udC1F*eϳҋ5}j[⻯EO䦙n2E&&?@r 8P  D4B^] - 9{JOkQtp[{otS9QKѥ%Wmnc|a1}~I?.R(h|ӈ1qM T⨢qzr*zRmQdg>It`wt&KP9 @ %1-_ܒ7k.1/tm?}'cU[D4oDޠL4Qc,3A!''-&h'ڱvk06XkrDQ㊢?Z" mћsn}_+ן-?nR}Tx$| ]#|QՓwrct͗+c9MW\O LF:1Ec`7gN\&J|cw/;2I.xgЎw^);"Ȥch*bX# [rE]Lg* 7/r?i%&FpbQþ;/hHF3ȾxV<?/n"J?uOWToPR[ɾݝ+^EI1"Q8vE#X"DI|S -I ѵnrKIIt$A2}f.1~ϐ}?V?jkJybU-7Js:#Ì*'''&]Ib$& J}.;krSPE е굱.^6Hwwk~iH)i&)%}CT<@Yzy8?o>WK*1gRF뵜GOF|%R޾ )S5nT*tEpF䣇:_Fݦ}7͞[_ti]E_T?lxŁH1!Ҽ=^Z|+hӞ;=9m4ʢդ>ܚ2(e:/1n;Ⱦ{:=/z*y9ϛ;>o~}ɕ^<@+_ίܵz^|#|!n&ΗC"a-uFFhz2fzR@LzrB -uyZ]ZM//_!!8 ĈӢcJ'u֞EF/L.\L~r)?[+Tݤb}B/:F3;n 'CII̙VI!S9ҨHy &Qw׮mD!nehiӾ{ 1fCڇoOQ6wŕ{-凶3h -F}7׊12`r'Gד<8iԍw(ADڊUֿAG+qnҐ^=>bɾ٥U3^ʭܽr#\i=E'btГɤ$mtkd˧k)V_pa>|֛bKypm <z3"ơQVTzaߝ5duQ+Tqi*e|kk;/IQ}y(Q@OXV9.,v^+>p79gr 'XÌÊO/<%=빠>K>Ú'Ỹ4%beR/]R{^)fϗkݣ8x#%@Oɘ{Nx}$#J1 T"hrNՒDHކ+cwB݅mی_YʗWzurh>nz #;_.@O(.Zў= y$%'Ip)b:FKz۴烈\@jXK(+؝5o˫vdSˁ'i[[~J&$ڥH#OIΗ"FEU'O{zROZs$΃LΗM[LWOi}ƾ1nEsL^./*C1>gڮWO#Y4MӘ1qԎiU@OaL+9s+9)>(p~Iɚ21aV@#zJG~[Ӫ^̭hzWihӾh)f ldX2U&&aC9MW\.{rcLOa5["w‹#$_)ؽbxh;|ڐ}wn@O -=I[gi)R8[X9w:qp 3)ΚC 6./O)F`̮o{yJE0c"eGFaD#W'[K[wGZ)>r t9,s -Np6fǴ᫓L gOd41 ܌uȏbƚ':̗ݛ+Њd߽@Q2}T"ӍΛ?"t#D#H4' IxWNWK+p"V^9441i/BE3D#9xg(+[tQݖjq?ɾ{cc|{XRF|žc_0Ip=$'IlgK%(&hrOx50ׂ:_w0WafzsΣΗҫ6PiT[jq?Vx6(dC+ /ef Id=ެ%mŤX\Rzr_~ -Zrp4d_mY84%Ⱦ1?NuWm?}W4e&+woaruK!ďߡ"aݦMtYO*\O -fk=]Q|fdΤ>}s)(b |O+%{7|g~}Eu>zgyWF釩Kŷ_,{û@Q1q}7G^h $A5hPϮdtnr|4]qTfJimDڟ)ica\wƜeǸڇoyU/fS$TAn}hӥEq/! 'x}_o;Nǯch~ؕѓ⭩MvS 9D Ilyy5${I -N -2S9(!@FNMe9+{/?տ޵탽{˴e$Ax_ftܱXʙݐACHt'2S:rR ;/Yϫkzj~NmWT{ٽ=;{>:=hO_{oʾ{ %Dѓq4Ez3x%$Q+pW!OdRF{zz -:CH_sT%[dzwogb_W>O=>ܷ/~. .{=Qՠy ->β9K>]>]񺵆tͷn٨tv55mww]L[ncb?:^`û?{}q{=ɐ,8~ %vwc9on{nƔ$:%6rH=(^ -tas۶mzJ*xo^O$/?9"mvfƙ,L|xj$w,t0y(`<Sl*=i7mR5Q[6+|p]t|o5|=*L^nLv]K=LJkKדX%% 1 H4-WDGgYuY>tCA% JI9s`CA%I pi&#'/1~ZzEwbWm| }iûۻ?}KCaf%#-Qu @"zm8A]2޼T 5bvS_Í3J|i|_.VPΠvCGk/nP*|P8Ӗo*BG+U$')_rGM_OC:FWH`/6jsfsa_>| ?_5 {^`J|^h@XRd?ط/$ՓB:2t͚ۖ%;WqM]QA2 -#@'EbFFt pAVnjAzxꞭb0Qy@OO"m}駟~}/*s5_rǍW7XM>A|˟^d ~E:{k/=c5ײԳ{ks&8,/owk,{gN6w } g+] _EU3Qkmg4]i}Cθ_5 =sGv=oW{ {ÃlRݢ8ό KOU8g<(>>'[w[Հ @8 PB -%@Bc܀GI@dl5wclq{l|3wSI73=!}m#:eN}129!1wY\tW]=>k/Lzfx{KGl!\GL 7sǥkbny~D˨OoDr7D-V+_~gRD 0-$nXsH= ,zY,7.6)'^3'ŬƊynn, XX[5IY.+G:՛usG?᧿O?r޿Lsy\W!_̇\VUz9顱&%DBVaUOQ𧼇Uxѕ% -cT FJbSi }E3s F+?(\4T2i!nxKXXm$ E|rMƭxt<0μvʼnOg̷x*Z0}1#5v+1g2hNfeх;<5s%;zQ'qkJ.i՞ -#Uրtuy9{gQΛrLo ]Q틼id4_`^{tB¸ λ庫ͯS{Rud?Y4].2ﱞZǚcxN&S1K2g&W0>9,)Fv^?=ܿrVΛY7p}ˀnqwyḎo%tyok~uO=O:&s0.xНtֱMJ9oP'i`pPaƚ[;XQxLx yʋ$kzYFjNK򘿗.La\>>ǒ?ǦnA⹛0D +,i9n?&X^Ͷl1q~fu$Z{0EcOw]%?uN 6]!//^QHKsSk]`.a3%fn:?C QwC-6,7#pU: Q^ŜܬH} ʊ8+gSObJ!]]Iޤ<  ABJzk8vL7pF˛wv/Ʈ^ȇC̴Df b{Ug`ʒ{|l[*'G|=C{@XZ`7m^N27r[J _{>0s~@-DZeq2fsֳ)9qZ9oEsIDu QŠT϶`Ğ=P߲?D}Ƈ[F̌G(Y6 -V')'sj'ɶx7툅ҕϋrMޤ) 'fߌHL^ov^]#•yEtnƲ}3V/n1O-4^n {f+I$hMVzrqf& dʼnl+ݾܖHoo<9us>j QG%*+cÒܕ~LqO8 +4u}oOٞJOb5fC$ēݏ >aމ"hC!wV,q?y6Z%!,[/+c:Z+|n*S* w%Hp7,7D|mHycճB}Ia3śXG;hH@풧r_*(߲e"v*&>=AKŞO=nj/1Y?j4 -azy SXa_{ɉ܍ ww=3'K@2EuEiAb)*k Hok^-.n.u,l/'ֱ-RqVs]9XQx+MЇ sȸޡLJ粃P^.U›w2jH -QeUGO[ +޾4'Y*2JgNA림w2\(^]}Ըm>9,,@uXM OfGfOod%8 -+--%%K.?3YX]^M rҍN˝ &Nn&B;/LG&| ?J}"E_M=sO_7hF̠=๙U҃>^D}ˠggeCԱZi},na} g_校ۼ^nWAX /wOjմkDCVoQ 5@sΖ8+s#yad}ț 2-~䧕*W͟(g;Nz#^RT d=[|Sq!X/VfUnҗU(~YKx'#[&H~̺7#sR|5$Hd uByXg,\?j2&wtI RMqWQBk`[jv/F|z;x{+!5E9/NYܱ %Oz5pm,_p/U*Y&YUJJ?a> lTr$ybV]*{~_xU99y'77z97f ,E'.7,9D}Ni6ݬ[ď>ډ['$TLUz14Mc&ri\Dt1zVi`:jt,gl謺tOY8gEt7\tҸ<Ւ r*lOࠓ"kb$܉۽RvLNvIFM]җ|<|L/+'kZ]'_Նgq]F|Km2z$kfGD.egQBY͍ qLDm}vsLJ5[Cniy6YIȝx8c'B*'GTT K#6|˕f0O{✏n,w t Zpvit]6 $"JmOIZsEԑEa$#Dq~Mo^orIFeZ/WI+#|Lv5V!:&U]'ݰ M7MY+Q^lR ʝ^䳻Jyk"dn()ĿLsQ v+r33K}~\ H_Ƭu^u wz$'ugƃǎg&.ǙMX%04/cq&zpAlZ{ߖY9Fz&8&ś{ }orPǛwrN`@zv*kC2Wf.?%m>nLaM7X̙ddQ޻ΤZxalMIM+kFq)Q _x|G7yY452(0kQ_&ŃC? {\?͗.ZdZ8{ -o,g}Z>"ҟv #+Jo|uk#4ڝc^X1Kŀ(B!̽tLt IkżQ8[EDy!eN-f3rd^{Rb mU/3xs y{jvՀH,X)[t!Aig/p#+į'ę?{^W{ -#1 }Už̲ _cq? m+2}/I 7&}d!}y(Oeb7~kJ -#XZJ_ZYȀeXQ5 t&={19]ܖFwrN&=p=p#Lw2Lsˑz.ʫ_|A_1XYV&?kՂYe܏}iU{wwĎTQ )If#@#oY?E4@cNVR_ƀXuUޫ-|K%./fk|>kt-akÒM3o^^D>nKM:0%i]y{7nkJ?1qNmbe%inpՀt<*V%keynqY~˘X8-]s\Q8{ka_*wK[K/qw璼ikf+qpp˄EI$V-}BL/~ /ɗkΫr볬 ]g!5]49+`lן q ځ,a_3/NJE\1/gXViN3W_[ Bo,7@R{@ziwj HgN=!b+VC("w]xy03s_Y%yl3GXNZ/8' p 2]9?y}EEEMqW\kOefd -(}/L.^_7@[QY+-e2n)j )u+!-y<32޺![6Ӫ,a>g>//xEnKSQi20/1d<p2P -pH=e".e{ )vn,9ŀM^WvR.f_ծod88]ynj,?ԗYV7pFQrrZrQbU k5S@ACfBLb[ɞGJkQZ#8>u_J >ܨ@f.m16] aw -?aQP2=`ܸ঵+ -NaSǩ_OGb<݋r¡tᢞ~qϮtf[t(03%`SVxQ ߯ Mm -n_(0=GjyOfeQ}ZƮ5[U'6d,1[8߽~SZGbߏ{rf@pz/7y U6@USM=, fW 1{^)?j1#On޸ܜM\Ra ]KlV ~!_\6=s8]H;_/jC9L=0cPIc6bVw.Nm:r٤Y+ubӯyȀ9\VG/kM*}3ImYUEvd͝v]qCx탯ʹjM|(Ο&SԶ5\ubBo߯NC{a֣F6yVAXW_&Iwp -a=`.o:{k-;S[LփE?N ؘqMs*g1rHVvD0P -Iد%Oŝjn1EWƮۍhex.Tql/#SدWM1~*[>ycrS"hm2w7Bc-vwpՉM -ɗ9{{jn1,w-,u,'!$3>vS}-ši<$uW6,VB_c7n1Z7_kĦoN5=ӬͩlTZaB~S*#MRnha?οuk5o}$EdljA#/[7 [ޯ7EԁM[ZGOBØnĭװh0ɹk5#P{մo/ -~"DC}M=>:ȓm-}goM2L~~׿WhVYCJ!vq[+݈4{7<$?rf^:5c=o,!.{kĢ34+Wgwq)n5$?WW תP>n9[YXtqoXV0‘~cn[!2bFjeL*rpoy 0l_{#C*Hfo+#7Qؕ4CH]^U\Lf7C+2IP~{N7 k|ߗ~΃|&6ݠSCFPn?W t0cU1gs H_^H=db)5 -`.NZ:!Ups60YMt׷&5U٧򙜯+ɰBmZ.j{6^hC!B^o3w[4dc P؃r.0q:;C:vcvqh.{u&M^}h đ.q<1g(KxHcpdGT3>Fe}2 Eп0G=Ƽ2{XMgŢ {¥l`cv+֋DпXt(- ^J}^BO N0kA,-6D )8𼇲P6Ʀޠ!]_U]nn [=0mZW8l]ƽ/0:Z+?AZuث? <4CP:T#< c{e:_U(ׇcXH88w?# -GۯA{H|%uD <DϾ?C+56}TkxūeY ]T>HK~rWkbApѳOv{mX7MҘIUcٿ}d"ݵhf'{?٪rQItC4v2b[- !mycNGLb(r/7$k 'R^іaONS~wY7*>_&Jߘ&lw;hfTJ{&1On"Hq2n7~:v?E=ĘBu(T[CI/^^xj'¢)!!^TnD5x:JuܯUWvZbHL=jĦSp잣耸4H]yHWMJ&Ʊ4b&3oS[wǦk:({C=$nr$ݏkovggXZd=||mM%7ZwhKۨh{>ু/jԃj_j |(!8/1?d h3iO=ۥ4%7 o7 [8zaCCDI-ހv'7Bo@3nKLF:/f A}/K 7 B0 r.:s6 6)3߳z1ն6= .G٤)KQh h{D$p) W[[{. n$ǢtLpT!L[ϳZrJ]ý}(Sf;S@_Aƹy( -rGqMYxnkVm\l1Y=JթJ?}߳Ak d++on3L]o[سh*irg.  D33VOI*;C&B$J{vE^Ξchao~Bv Ozyt9< ٽƁ?_OMl2W܅Yb2F"wۨcí+a@b}?ks6gjQo :,zӛ.a`D%m78r>,zaҦr#㻑цEo;뭣Ɓ¢o̔o^[ -J!4|d\ǿDnD LnGJn~%ǁ@Ta3JX|)`\rZSI7@nĭt>J1tF4i.}!7y/%" ĢF,Px%RɭTr#6@q@7,F*G(#܈[|JCJ*7"&{>b/Lf:VnS9p*8t~?FsT ph^nub-q$#P-i.%o6=|Ï?;vq@ezs-j &@e*3*?W SI_;:>I?NaဢX4Mo a5Ŀb"a/1v9ŦY Zh (Cw/e5_nx%P~{tz3%>X箄րX6܃M~\Klh Ebƕ9^-mR`g"? 1)caJԀF'۩wTo˂Ħ"Pvs/_Xawma՛Aztj, $MpB nȶ 59?3 }#hvUobi89Lo8\qw+W }azqI>CߎAo\qe:i+AT@[@7$~Fq0I|xM Ǧ{K7wϫ:b#(q_HaSYoxzK(@3j#1cc8 -h{S#-:W>ǡoJhi N>tzsC'1?ytVo=Q{<<@bSYohM.W&"~Ƴ J66=pz+.iq{dW{I>HaOxFW7P(E恞*P*(%Ej4fxBەőn4~8H!126\JwZrqڀzb*MMn4ǦX&ܶu*^J æz(M2uqx(MJM}%@Ml/Tn"};EC:Il޸=(b]0oRy;0 E׍RBn<(91(MR[{7S%01bHyj-c%-y^urb.kW XtyˍY m:6#ܘھ7$Pf/* " 4n)@},ˍKw"RTǦ^+D2({ܛ7&!84M:`ވܕ@p@a, eZL mAp@YlzMJz/Ԅm^7ټXZrt9 -m@p@IZyj֍twp$P y#\p(E7&yџ*j8P ?)0~LbBp@+ 2!@Z6DaoR`3!8!JXIqg iqSԷo~cOG}>Z n.@nl:zcA+,ƢQ;V$@f,\EP/}@&FEZ&2P_p9q7?Jo$}sz鍽킅Rbӕ4I1ɥ.꧛ވ9hv{mo I%}M;&NkV ~nH?ita.M' 8 Ro|O;ȅC] 8 6XxaOF7~w@,fv Lp ! -/]E  JZ;I ȃMؼsz{ HC{cb2~wz$T_H1 3Q^$¦i7 HECR!7 6-3t՛I84[jCE2{_Uoy d]fSwü`ꍽN`zʍeNp}8¦p=1I 6>HOfa_#: ¦7y 6-誥 ·3dX-rA7 6y7 6n Mrfx-62vl7 \N 5V!MA HM7'i3H p'tt@ fހtXtfW$7 }ӣLr0o@>*; $W1i#8\s zdSA-ʻ۵Go&9K$eǘ81q7 -6=rn& =ia endstream endobj 31 0 obj <>stream -:yǑ bm:wlGyqhq.]ڷBo2jڥ& wHCӵ[ؘ "&0rΗIDL"-c 0H ۣ}{wڱCG&/@b~}wԡ)8I 1LoԿw.c+!w!d= اgfbTכAfc  ԯgNb)7 56zAwazS$ ȍMf<%оcM2xZG  \G^iѴ\3q'tCYm ¼ٱ釣F Ч3p1 - 2I!$ش#&ewpO-cӣōE(S#c֑t)1(M_:Mg8HH6瞒8t@޽DD yȥG mLo{S{2Ґjz)#Л%&&I # Eo?u!Lo;+7܀ % v?84clRܐ@R< )NC?<7ݣkXLԖIi9Էww=wH'}6zSF r+(6pI )Lo'5&~`7QA)=v+IyvRFߗ7^%Lr0o@VN|to짆wM_Orh ?ӛ !@ ѿO̾:$c2ճs8I6 hE?WN cF8\opCSz(L{.q@,?Л7/a߳8TB1wr.7'zVo܅=zt$ԛAb8t|nBԖpP"-:0ɿpH?vۮ) o o@#l:Ror'o@'l몮۷pf#cN;ws!y:a;0gޘ}{7]Po& --AV]th/\bO:.b79}p'^8b< w{+ I KLrN"-Z?[TsCH7$whM_^Ɠ0o@32&UtæΏPor:qvtP 'O IJƃqL2d ~Oo܁-1+4plSF7CeLU Ja$SlAE ӐMo184h {No̻}c0]+܈!Lo7H7<Zc OV]tĦvh . b7%6Џȵ3I0BbVo&qeIzBo :qlp@ -C1x ZaІ3ygmXJi[Nٚ]eA; fmh+1w2 D56m.ReX6 wD7][x 辋 +C~N6mu0o[[p2vI8M L}%qhvN4ep'p]'8ij3HNajx -MLo`SgvEHE+ok!yr%N:6{-1[Ap&ycM_*i0oĢZN tC+:z,Z7iC2:pl?܂{9 -Iq:s7#o -Ya{^݂I#̂%Fj.æUp?<sW8K)c砞 zΟOpGDLib&& ;8Ǣ-IE`5ceLr`g2 tNX8{E8tnX8$8LpSq\JN/ }ax)e疗R%YRzHb@#`:2 "4ҔVvy'`h =~ogah=qG!^dh=pYp&9k8Ew\ҢA7EiL㈘H,:3H Xlv(i1XA=Xl%Y7_Ji_xyzl<$38OJ) "Rp4Zw7Wprax4A:-i4yH$,ǤY& q_b@Gfv1#B @`)9& p(hL2kc8t_ @qh|sSM< MƱ(-II1(Z0}QI-:}J7q_߽I&.܆ f&9& ̈́)qMT84JJ?}&d7 ͅ;ye-@2͆'ynT#J< %@ʏ~סQo -Wp^cL`hLp6qem_4hLu)JZT}!`'WAh9LF{o 7!cw8n-JxB {>!7&{'=_ 'Jo?$Ep#?$g킁 \0wC10pJ$ vDL6=p*L+8ƒE8!:4o7M\lApNk8tσC @顳:C~ao6xݧ7X)ܹK$YK3{9s9νz7 2O!@|JOpp;Q tEH98'ɷz@DrpOP{ 8'l P37Y{C |'_ҽs9&PVS㭹QJŗh@@x N0Ǜ$ߤ#@x e4 %'Wȧ.ڽ+-wr08P""wzxdXW!ހ -}MbFF|7vn%P<ܨ]98sp0߆Ș܍1y2ǧ$ಾES\_7ްzw7$#龻7@;c7 =w@_p3N dpGS}N$t+P{ Ԇ~Ñ/Tpaf4 g;wQ=yYP.X!c`@y鳨=tdgn]9ҙ~ΘAAn#9vp޼37S](JR _~>#P,tg%'i{K="8/SNͥdU Fp_{%̏yȏ lⴓC.(YPQ,|9w`b@m16t=9J|ppΐ_"I93i)?2Iڷ/b9@ëXto ciHGpq d+:.5 l PwU|y;r"Vsz1@gK57YGpv=ɡ%pnl4L|q$J|L'Jг78${g&z '!~x7N~f=]bsLO4(үΔ›Q)\ddo0?rf - ct,+@pf$yʀ/_9bGf|X -_l}8rr'_j-AUoif~tn=R],%_q"p)v՛nYN+y`#3L)CMئB&I;#”  r) 3 ZGސ44 GHANOprFYɠtp%m>hʄ”~'0DoN927KCMR `bt_V\D鷱=d+lҲ{6,Øu9/|/Ks<o1btft2 ȣɜPv}Ҳ{n]JIs奙 #T S})f{c#g_ B6LD鑙ES'@Q)[p]j-S_6C #C^65d89\*s յqnnUr@ɬ;[- :c0ӚcuDpΠn w=sp_`+ du{ 01rlB7VڟBC:dX -?gOBP涋mL=C) -~КVQGKf ytETmTdE8Y?^MՓ$8qJ\02&VQG_<26OGݴoX &9&Lz<9p(dl^K/r3"]0^2܋|% ?~ЛN-"ŖΟc#Ǣ#KS~69>QtG` oca+C o޾5olah/pnix %*O A^+cenGziYJS/ܨ _;8w}=M$>+gWTQ,7ҀHn d]޷ye< Rnp7S -G&ǀx47L:0{[ixgyrlKs4?yx9r4.|)6;08y it ei7 ba(s;SOms{sCE{]-.irms8ITx#L>7/ lHR졥±[y77`p+y3뎟릜%]!⽽^kahfտ>FYNp`koqɽ}'p(O\xMM2H;8fף/<XBB>L~ (fǬ=(x dl /"+D- s(Jf_3IbLl[i;8d馽P CdZ<}l7q}DX1R[i=pZeE\>5g&H0ZNB*AfcJ /#A S -WE>!pr@͍˿r׮1H^ҋ/P6dd2T\ 6#ܥ*#FƏI]ç/঍?/0iml3R -Mڐr#rM7AԱc}m߸᧫V2(&C@S -_Zv׿Xnt8<]>xG~~݆9_u!ԍd~Z@.]Zaz1M †8/xs3ݵ 1qK]17|s|@X -G#_o/-m5YQgM ңKf}]\{Ւ>X" Lƈ0y0psDB9z|(1/ bJS_JCqZ3V6 /+ՆgF/-)6*t5fMsRs"Kc(N4S_>U$P&)+mE GceQk\~']6Tgmm6Լ^Wc5g*cbE Ćx:)>n%ӶZפmЬ7g袲YzGsW`.#-ɘ7+Fkmj}|֛#W?k¦FCAQ]rCF˺ڼx}A`lN6}[Mb[fLQe5E&;Őܬ.]ї9یMٵm%re|v`-Ԣj-2R樾 -C2l27kʲB]_zuEkFV\L6ڠQw>j0#ݽ˴5EZr%}n\bj{RuD93rx G^r0&ϤꯏfΡ@ݗѮh rfENMݘST4 -mIT:VQ -}Vowö^5ݍ{cs"fjZL}wQc02c;ӝ>Rl[)qW6z]P"~`s+[EbZBDQnSMC(ΑXߝ:؝[d2csbm֖ ߥ,mj0QoӒX3@ggT*+ kkʛu9jg8^ng2R' )UUҟѕnHoo.p4$GMΠFh2s\^m1e^#<tNZ`TimYPG>t ~TP:-Jԓ+\e6ZU*SSuԷv)6Z,EI}v^ұ/МߝC40w#d*]Ѣn+ ڢWeO16UeFVk]Uݥ+Փf!r؃&ܙs͚5GPF[SRBY.ĤkY-Ime)U&92ͨn.6W-3[Zcu ZmkIF[\e'yP'+RB兆h&+ٸ^Zҥ5e)v}JSTȭv9QNhlNXƩ]ָ3Ra6%SJ][i[M9efBJwŕJ -"'9h,dRLQZJӔjcIAǪ|٘֘ڣcIi)-2IUV:*q[< = -p*EwmlعU{O쫀mش*0L1YU_{̻˳/T Vɭ -xTs4> -LL*\q3Q5 J,(I endstream endobj 15 0 obj [/ICCBased 19 0 R] endobj 6 0 obj [5 0 R] endobj 32 0 obj <> endobj xref 0 33 0000000000 65535 f -0000000016 00000 n -0000000144 00000 n -0000047649 00000 n -0000000000 00000 f -0000163121 00000 n -0000593503 00000 n -0000047700 00000 n -0000048109 00000 n -0000048283 00000 n -0000163420 00000 n -0000139682 00000 n -0000163307 00000 n -0000049181 00000 n -0000048344 00000 n -0000593468 00000 n -0000048620 00000 n -0000048668 00000 n -0000139717 00000 n -0000160473 00000 n -0000163191 00000 n -0000163222 00000 n -0000163494 00000 n -0000163800 00000 n -0000165099 00000 n -0000187851 00000 n -0000253439 00000 n -0000319027 00000 n -0000384615 00000 n -0000450203 00000 n -0000515791 00000 n -0000581379 00000 n -0000593526 00000 n -trailer <]>> startxref 593722 %%EOF \ No newline at end of file diff --git a/responsive-ui/design/chromeStorePics/promo1400560.png b/responsive-ui/design/chromeStorePics/promo1400560.png deleted file mode 100644 index d3637ecc8..000000000 Binary files a/responsive-ui/design/chromeStorePics/promo1400560.png and /dev/null differ diff --git a/responsive-ui/design/chromeStorePics/promo440280.png b/responsive-ui/design/chromeStorePics/promo440280.png deleted file mode 100644 index c1f92b1c0..000000000 Binary files a/responsive-ui/design/chromeStorePics/promo440280.png and /dev/null differ diff --git a/responsive-ui/design/chromeStorePics/promo920680.png b/responsive-ui/design/chromeStorePics/promo920680.png deleted file mode 100644 index 726bd810a..000000000 Binary files a/responsive-ui/design/chromeStorePics/promo920680.png and /dev/null differ diff --git a/responsive-ui/design/chromeStorePics/screen_dao_accounts.png b/responsive-ui/design/chromeStorePics/screen_dao_accounts.png deleted file mode 100644 index 1a2e8052c..000000000 Binary files a/responsive-ui/design/chromeStorePics/screen_dao_accounts.png and /dev/null differ diff --git a/responsive-ui/design/chromeStorePics/screen_dao_locked.png b/responsive-ui/design/chromeStorePics/screen_dao_locked.png deleted file mode 100644 index 6592c17e4..000000000 Binary files a/responsive-ui/design/chromeStorePics/screen_dao_locked.png and /dev/null differ diff --git a/responsive-ui/design/chromeStorePics/screen_dao_notification.png b/responsive-ui/design/chromeStorePics/screen_dao_notification.png deleted file mode 100644 index baeb2ec39..000000000 Binary files a/responsive-ui/design/chromeStorePics/screen_dao_notification.png and /dev/null differ diff --git a/responsive-ui/design/chromeStorePics/screen_wei_account.png b/responsive-ui/design/chromeStorePics/screen_wei_account.png deleted file mode 100644 index 23301e4bf..000000000 Binary files a/responsive-ui/design/chromeStorePics/screen_wei_account.png and /dev/null differ diff --git a/responsive-ui/design/chromeStorePics/screen_wei_notification.png b/responsive-ui/design/chromeStorePics/screen_wei_notification.png deleted file mode 100644 index 7a763e5df..000000000 Binary files a/responsive-ui/design/chromeStorePics/screen_wei_notification.png and /dev/null differ diff --git a/responsive-ui/design/metamask-logo-eyes.png b/responsive-ui/design/metamask-logo-eyes.png deleted file mode 100644 index c29331b28..000000000 Binary files a/responsive-ui/design/metamask-logo-eyes.png and /dev/null differ diff --git a/responsive-ui/design/wireframes/1st_time_use.png b/responsive-ui/design/wireframes/1st_time_use.png deleted file mode 100644 index c18ced5e2..000000000 Binary files a/responsive-ui/design/wireframes/1st_time_use.png and /dev/null differ diff --git a/responsive-ui/design/wireframes/metamask_wfs_jan_13.pdf b/responsive-ui/design/wireframes/metamask_wfs_jan_13.pdf deleted file mode 100644 index c77c9274a..000000000 Binary files a/responsive-ui/design/wireframes/metamask_wfs_jan_13.pdf and /dev/null differ diff --git a/responsive-ui/design/wireframes/metamask_wfs_jan_13.png b/responsive-ui/design/wireframes/metamask_wfs_jan_13.png deleted file mode 100644 index d71d7bdb4..000000000 Binary files a/responsive-ui/design/wireframes/metamask_wfs_jan_13.png and /dev/null differ diff --git a/responsive-ui/design/wireframes/metamask_wfs_jan_18.pdf b/responsive-ui/design/wireframes/metamask_wfs_jan_18.pdf deleted file mode 100644 index 592ba8532..000000000 Binary files a/responsive-ui/design/wireframes/metamask_wfs_jan_18.pdf and /dev/null differ diff --git a/responsive-ui/example.js b/responsive-ui/example.js deleted file mode 100644 index 4627c0e9c..000000000 --- a/responsive-ui/example.js +++ /dev/null @@ -1,123 +0,0 @@ -const injectCss = require('inject-css') -const MetaMaskUi = require('./index.js') -const MetaMaskUiCss = require('./css.js') -const EventEmitter = require('events').EventEmitter - -// account management - -var identities = { - '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111': { - name: 'Walrus', - img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', - address: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', - balance: 220, - txCount: 4, - }, - '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222': { - name: 'Tardus', - img: 'QmQYaRdrf2EhRhJWaHnts8Meu1mZiXrNib5W1P6cYmXWRL', - address: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222', - balance: 10.005, - txCount: 16, - }, - '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333': { - name: 'Gambler', - img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', - address: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333', - balance: 0.000001, - txCount: 1, - }, -} - -var unapprovedTxs = {} -addUnconfTx({ - from: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222', - to: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', - value: '0x123', -}) -addUnconfTx({ - from: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', - to: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333', - value: '0x0000', - data: '0x000462427bcc9133bb46e88bcbe39cd7ef0e7000', -}) - -function addUnconfTx (txParams) { - var time = (new Date()).getTime() - var id = createRandomId() - unapprovedTxs[id] = { - id: id, - txParams: txParams, - time: time, - } -} - -var isUnlocked = false -var selectedAccount = null - -function getState () { - return { - isUnlocked: isUnlocked, - identities: isUnlocked ? identities : {}, - unapprovedTxs: isUnlocked ? unapprovedTxs : {}, - selectedAccount: selectedAccount, - } -} - -var accountManager = new EventEmitter() - -accountManager.getState = function (cb) { - cb(null, getState()) -} - -accountManager.setLocked = function () { - isUnlocked = false - this._didUpdate() -} - -accountManager.submitPassword = function (password, cb) { - if (password === 'test') { - isUnlocked = true - cb(null, getState()) - this._didUpdate() - } else { - cb(new Error('Bad password -- try "test"')) - } -} - -accountManager.setSelectedAccount = function (address, cb) { - selectedAccount = address - cb(null, getState()) - this._didUpdate() -} - -accountManager.signTransaction = function (txParams, cb) { - alert('signing tx....') -} - -accountManager._didUpdate = function () { - this.emit('update', getState()) -} - -// start app - -var container = document.getElementById('app-content') - -var css = MetaMaskUiCss() -injectCss(css) - -MetaMaskUi({ - container: container, - accountManager: accountManager, -}) - -// util - -function createRandomId () { - // 13 time digits - var datePart = new Date().getTime() * Math.pow(10, 3) - // 3 random digits - var extraPart = Math.floor(Math.random() * Math.pow(10, 3)) - // 16 digits - return datePart + extraPart -} diff --git a/responsive-ui/index.html b/responsive-ui/index.html deleted file mode 100644 index 9dfaefbb3..000000000 --- a/responsive-ui/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - MetaMask - - - - -

- - - - -
- -
- - - diff --git a/responsive-ui/index.js b/responsive-ui/index.js deleted file mode 100644 index a729138d3..000000000 --- a/responsive-ui/index.js +++ /dev/null @@ -1,58 +0,0 @@ -const render = require('react-dom').render -const h = require('react-hyperscript') -const Root = require('./app/root') -const actions = require('./app/actions') -const configureStore = require('./app/store') -const txHelper = require('./lib/tx-helper') -global.log = require('loglevel') - -module.exports = launchMetamaskUi - - -log.setLevel(global.METAMASK_DEBUG ? 'debug' : 'warn') - -function launchMetamaskUi (opts, cb) { - var accountManager = opts.accountManager - actions._setBackgroundConnection(accountManager) - // check if we are unlocked first - accountManager.getState(function (err, metamaskState) { - if (err) return cb(err) - const store = startApp(metamaskState, accountManager, opts) - cb(null, store) - }) -} - -function startApp (metamaskState, accountManager, opts) { - // parse opts - const store = configureStore({ - - // metamaskState represents the cross-tab state - metamask: metamaskState, - - // appState represents the current tab's popup state - appState: {}, - - // Which blockchain we are using: - networkVersion: opts.networkVersion, - }) - - // if unconfirmed txs, start on txConf page - const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.network) - if (unapprovedTxsAll.length > 0) { - store.dispatch(actions.showConfTxPage()) - } - - accountManager.on('update', function (metamaskState) { - store.dispatch(actions.updateMetamaskState(metamaskState)) - }) - - // start app - render( - h(Root, { - // inject initial state - store: store, - } - ), opts.container) - - return store -} diff --git a/responsive-ui/lib/account-link.js b/responsive-ui/lib/account-link.js deleted file mode 100644 index d061d0ad1..000000000 --- a/responsive-ui/lib/account-link.js +++ /dev/null @@ -1,26 +0,0 @@ -module.exports = function (address, network) { - const net = parseInt(network) - let link - switch (net) { - case 1: // main net - link = `http://etherscan.io/address/${address}` - break - case 2: // morden test net - link = `http://morden.etherscan.io/address/${address}` - break - case 3: // ropsten test net - link = `http://ropsten.etherscan.io/address/${address}` - break - case 4: // rinkeby test net - link = `http://rinkeby.etherscan.io/address/${address}` - break - case 42: // kovan test net - link = `http://kovan.etherscan.io/address/${address}` - break - default: - link = '' - break - } - - return link -} diff --git a/responsive-ui/lib/contract-namer.js b/responsive-ui/lib/contract-namer.js deleted file mode 100644 index f05e770cc..000000000 --- a/responsive-ui/lib/contract-namer.js +++ /dev/null @@ -1,33 +0,0 @@ -/* CONTRACT NAMER - * - * Takes an address, - * Returns a nicname if we have one stored, - * otherwise returns null. - */ - -const contractMap = require('eth-contract-metadata') -const ethUtil = require('ethereumjs-util') - -module.exports = function (addr, identities = {}) { - const checksummed = ethUtil.toChecksumAddress(addr) - if (contractMap[checksummed] && contractMap[checksummed].name) { - return contractMap[checksummed].name - } - - const address = addr.toLowerCase() - const ids = hashFromIdentities(identities) - return addrFromHash(address, ids) -} - -function hashFromIdentities (identities) { - const result = {} - for (const key in identities) { - result[key] = identities[key].name - } - return result -} - -function addrFromHash (addr, hash) { - const address = addr.toLowerCase() - return hash[address] || null -} diff --git a/responsive-ui/lib/etherscan-prefix-for-network.js b/responsive-ui/lib/etherscan-prefix-for-network.js deleted file mode 100644 index 2c1904f1c..000000000 --- a/responsive-ui/lib/etherscan-prefix-for-network.js +++ /dev/null @@ -1,21 +0,0 @@ -module.exports = function (network) { - const net = parseInt(network) - let prefix - switch (net) { - case 1: // main net - prefix = '' - break - case 3: // ropsten test net - prefix = 'ropsten.' - break - case 4: // rinkeby test net - prefix = 'rinkeby.' - break - case 42: // kovan test net - prefix = 'kovan.' - break - default: - prefix = '' - } - return prefix -} diff --git a/responsive-ui/lib/explorer-link.js b/responsive-ui/lib/explorer-link.js deleted file mode 100644 index 3b82ecd5f..000000000 --- a/responsive-ui/lib/explorer-link.js +++ /dev/null @@ -1,6 +0,0 @@ -const prefixForNetwork = require('./etherscan-prefix-for-network') - -module.exports = function (hash, network) { - const prefix = prefixForNetwork(network) - return `http://${prefix}etherscan.io/tx/${hash}` -} diff --git a/responsive-ui/lib/icon-factory.js b/responsive-ui/lib/icon-factory.js deleted file mode 100644 index 27a74de66..000000000 --- a/responsive-ui/lib/icon-factory.js +++ /dev/null @@ -1,65 +0,0 @@ -var iconFactory -const isValidAddress = require('ethereumjs-util').isValidAddress -const toChecksumAddress = require('ethereumjs-util').toChecksumAddress -const contractMap = require('eth-contract-metadata') - -module.exports = function (jazzicon) { - if (!iconFactory) { - iconFactory = new IconFactory(jazzicon) - } - return iconFactory -} - -function IconFactory (jazzicon) { - this.jazzicon = jazzicon - this.cache = {} -} - -IconFactory.prototype.iconForAddress = function (address, diameter) { - const addr = toChecksumAddress(address) - if (iconExistsFor(addr)) { - return imageElFor(addr) - } - - return this.generateIdenticonSvg(address, diameter) -} - -// returns svg dom element -IconFactory.prototype.generateIdenticonSvg = function (address, diameter) { - var cacheId = `${address}:${diameter}` - // check cache, lazily generate and populate cache - var identicon = this.cache[cacheId] || (this.cache[cacheId] = this.generateNewIdenticon(address, diameter)) - // create a clean copy so you can modify it - var cleanCopy = identicon.cloneNode(true) - return cleanCopy -} - -// creates a new identicon -IconFactory.prototype.generateNewIdenticon = function (address, diameter) { - var numericRepresentation = jsNumberForAddress(address) - var identicon = this.jazzicon(diameter, numericRepresentation) - return identicon -} - -// util - -function iconExistsFor (address) { - return contractMap[address] && isValidAddress(address) && contractMap[address].logo -} - -function imageElFor (address) { - const contract = contractMap[address] - const fileName = contract.logo - const path = `images/contract/${fileName}` - const img = document.createElement('img') - img.src = path - img.style.width = '75%' - return img -} - -function jsNumberForAddress (address) { - var addr = address.slice(2, 10) - var seed = parseInt(addr, 16) - return seed -} - diff --git a/responsive-ui/lib/lost-accounts-notice.js b/responsive-ui/lib/lost-accounts-notice.js deleted file mode 100644 index 948b13db6..000000000 --- a/responsive-ui/lib/lost-accounts-notice.js +++ /dev/null @@ -1,23 +0,0 @@ -const summary = require('../app/util').addressSummary - -module.exports = function (lostAccounts) { - return { - date: new Date().toDateString(), - title: 'Account Problem Caught', - body: `MetaMask has fixed a bug where some accounts were previously mis-generated. This was a rare issue, but you were affected! - -We have successfully imported the accounts that were mis-generated, but they will no longer be recovered with your normal seed phrase. - -We have marked the affected accounts as "Loose", and recommend you transfer ether and tokens away from those accounts, or export & back them up elsewhere. - -Your affected accounts are: -${lostAccounts.map(acct => ` - ${summary(acct)}`).join('\n')} - -These accounts have been marked as "Loose" so they will be easy to recognize in the account list. - -For more information, please read [our blog post.][1] - -[1]: https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd#.7d8ktj4h3 - `, - } -} diff --git a/responsive-ui/lib/persistent-form.js b/responsive-ui/lib/persistent-form.js deleted file mode 100644 index d4dc20b03..000000000 --- a/responsive-ui/lib/persistent-form.js +++ /dev/null @@ -1,61 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const defaultKey = 'persistent-form-default' -const eventName = 'keyup' - -module.exports = PersistentForm - -function PersistentForm () { - Component.call(this) -} - -inherits(PersistentForm, Component) - -PersistentForm.prototype.componentDidMount = function () { - const fields = document.querySelectorAll('[data-persistent-formid]') - const store = this.getPersistentStore() - - for (var i = 0; i < fields.length; i++) { - const field = fields[i] - const key = field.getAttribute('data-persistent-formid') - const cached = store[key] - if (cached !== undefined) { - field.value = cached - } - - field.addEventListener(eventName, this.persistentFieldDidUpdate.bind(this)) - } -} - -PersistentForm.prototype.getPersistentStore = function () { - let store = window.localStorage[this.persistentFormParentId || defaultKey] - if (store && store !== 'null') { - store = JSON.parse(store) - } else { - store = {} - } - return store -} - -PersistentForm.prototype.setPersistentStore = function (newStore) { - window.localStorage[this.persistentFormParentId || defaultKey] = JSON.stringify(newStore) -} - -PersistentForm.prototype.persistentFieldDidUpdate = function (event) { - const field = event.target - const store = this.getPersistentStore() - const key = field.getAttribute('data-persistent-formid') - const val = field.value - store[key] = val - this.setPersistentStore(store) -} - -PersistentForm.prototype.componentWillUnmount = function () { - const fields = document.querySelectorAll('[data-persistent-formid]') - for (var i = 0; i < fields.length; i++) { - const field = fields[i] - field.removeEventListener(eventName, this.persistentFieldDidUpdate.bind(this)) - } - this.setPersistentStore({}) -} - diff --git a/responsive-ui/lib/tx-helper.js b/responsive-ui/lib/tx-helper.js deleted file mode 100644 index ec19daf64..000000000 --- a/responsive-ui/lib/tx-helper.js +++ /dev/null @@ -1,17 +0,0 @@ -const valuesFor = require('../app/util').valuesFor - -module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, network) { - log.debug('tx-helper called with params:') - log.debug({ unapprovedTxs, unapprovedMsgs, personalMsgs, network }) - - const txValues = network ? valuesFor(unapprovedTxs).filter(txMeta => txMeta.metamaskNetworkId === network) : valuesFor(unapprovedTxs) - log.debug(`tx helper found ${txValues.length} unapproved txs`) - const msgValues = valuesFor(unapprovedMsgs) - log.debug(`tx helper found ${msgValues.length} unsigned messages`) - let allValues = txValues.concat(msgValues) - const personalValues = valuesFor(personalMsgs) - log.debug(`tx helper found ${personalValues.length} unsigned personal messages`) - allValues = allValues.concat(personalValues) - - return allValues.sort(txMeta => txMeta.time) -} diff --git a/test/unit/responsive/components/dropdown-test.js b/test/unit/responsive/components/dropdown-test.js index 0472c541b..3ad2c390e 100644 --- a/test/unit/responsive/components/dropdown-test.js +++ b/test/unit/responsive/components/dropdown-test.js @@ -5,8 +5,8 @@ const h = require('react-hyperscript'); const ReactTestUtils = require('react-addons-test-utils'); const sinon = require('sinon'); const path = require('path'); -const Dropdown = require(path.join(__dirname, '..', '..', '..', '..', 'responsive-ui', 'app', 'components', 'dropdown.js')).Dropdown; -const DropdownMenuItem = require(path.join(__dirname, '..', '..', '..', '..', 'responsive-ui', 'app', 'components', 'dropdown.js')).DropdownMenuItem; +const Dropdown = require(path.join(__dirname, '..', '..', '..', '..', 'ui', 'app', 'components', 'dropdown.js')).Dropdown; +const DropdownMenuItem = require(path.join(__dirname, '..', '..', '..', '..', 'ui', 'app', 'components', 'dropdown.js')).DropdownMenuItem; describe('Dropdown components', function () { let onClickOutside; diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 000000000..c6b1254b5 --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,66 @@ + +# Created by https://www.gitignore.io/api/osx,node + +### OSX ### +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### Node ### +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git +node_modules + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index bed05a7fb..18c867153 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -3,21 +3,18 @@ const extend = require('xtend') const Component = require('react').Component const h = require('react-hyperscript') const connect = require('react-redux').connect -const CopyButton = require('./components/copyButton') -const AccountInfoLink = require('./components/account-info-link') const actions = require('./actions') const ReactCSSTransitionGroup = require('react-addons-css-transition-group') const valuesFor = require('./util').valuesFor - const Identicon = require('./components/identicon') const EthBalance = require('./components/eth-balance') const TransactionList = require('./components/transaction-list') const ExportAccountView = require('./components/account-export') const ethUtil = require('ethereumjs-util') const EditableLabel = require('./components/editable-label') -const Tooltip = require('./components/tooltip') const TabBar = require('./components/tab-bar') const TokenList = require('./components/token-list') +const AccountDropdowns = require('./components/account-dropdowns').AccountDropdowns module.exports = connect(mapStateToProps)(AccountDetailScreen) @@ -54,12 +51,18 @@ AccountDetailScreen.prototype.render = function () { return ( - h('.account-detail-section', [ + h('.account-detail-section', { + style: { + height: '100%', + maxWidth: '850px', + }, + }, [ // identicon, label, balance, etc h('.account-data-subsection', { style: { margin: '0 20px', + flex: '1 0 auto', }, }, [ @@ -84,6 +87,7 @@ AccountDetailScreen.prototype.render = function () { style: { lineHeight: '10px', marginLeft: '15px', + width: '100%', }, }, [ h(EditableLabel, { @@ -98,7 +102,42 @@ AccountDetailScreen.prototype.render = function () { // What is shown when not editing + edit text: h('label.editing-label', [h('.edit-text', 'edit')]), - h('h2.font-medium.color-forest', {name: 'edit'}, identity && identity.name), + h( + 'div', + { + style: { + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'center', + }, + }, + [ + h( + 'h2.font-medium.color-forest', + { + name: 'edit', + style: { + }, + }, + [ + identity && identity.name, + ] + ), + h( + AccountDropdowns, + { + style: { + marginRight: '8px', + marginLeft: 'auto', + cursor: 'pointer', + }, + selected, + network, + identities: props.identities, + }, + ), + ] + ), ]), h('.flex-row', { style: { @@ -124,56 +163,6 @@ AccountDetailScreen.prototype.render = function () { color: '#AEAEAE', }, }, checksumAddress), - - // copy and export - - h('.flex-row', { - style: { - justifyContent: 'flex-end', - }, - }, [ - - h(AccountInfoLink, { selected, network }), - - h(CopyButton, { - value: checksumAddress, - }), - - h(Tooltip, { - title: 'QR Code', - }, [ - h('i.fa.fa-qrcode.pointer.pop-hover', { - onClick: () => props.dispatch(actions.showQrView(selected, identity ? identity.name : '')), - style: { - fontSize: '18px', - position: 'relative', - color: 'rgb(247, 134, 28)', - top: '5px', - marginLeft: '3px', - marginRight: '3px', - }, - }), - ]), - - h(Tooltip, { - title: 'Export Private Key', - }, [ - h('div', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - h('img.cursor-pointer.color-orange', { - src: 'images/key-32.png', - onClick: () => this.requestAccountExport(selected), - style: { - height: '19px', - }, - }), - ]), - ]), - ]), ]), // account ballence @@ -197,14 +186,11 @@ AccountDetailScreen.prototype.render = function () { }, }), + h('.flex-grow'), + h('button', { onClick: () => props.dispatch(actions.buyEthView(selected)), - style: { - marginBottom: '20px', - marginRight: '8px', - position: 'absolute', - left: '219px', - }, + style: { marginRight: '10px' }, }, 'BUY'), h('button', { @@ -254,7 +240,11 @@ AccountDetailScreen.prototype.subview = function () { AccountDetailScreen.prototype.tabSections = function () { const { currentAccountTab } = this.props - return h('section.tabSection', [ + return h('section.tabSection', { + style: { + height: '100%', + }, + }, [ h(TabBar, { tabs: [ @@ -305,7 +295,3 @@ AccountDetailScreen.prototype.transactionList = function () { }, }) } - -AccountDetailScreen.prototype.requestAccountExport = function () { - this.props.dispatch(actions.requestExportAccount()) -} diff --git a/ui/app/accounts/account-list-item.js b/ui/app/accounts/account-list-item.js deleted file mode 100644 index 10a0b6cc7..000000000 --- a/ui/app/accounts/account-list-item.js +++ /dev/null @@ -1,91 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const ethUtil = require('ethereumjs-util') - -const EthBalance = require('../components/eth-balance') -const CopyButton = require('../components/copyButton') -const Identicon = require('../components/identicon') - -module.exports = AccountListItem - -inherits(AccountListItem, Component) -function AccountListItem () { - Component.call(this) -} - -AccountListItem.prototype.render = function () { - const { identity, selectedAddress, accounts, onShowDetail, - conversionRate, currentCurrency } = this.props - - const checksumAddress = identity && identity.address && ethUtil.toChecksumAddress(identity.address) - const isSelected = selectedAddress === identity.address - const account = accounts[identity.address] - const selectedClass = isSelected ? '.selected' : '' - - return ( - h(`.accounts-list-option.flex-row.flex-space-between.pointer.hover-white${selectedClass}`, { - key: `account-panel-${identity.address}`, - onClick: (event) => onShowDetail(identity.address, event), - }, [ - - h('.identicon-wrapper.flex-column.flex-center.select-none', [ - this.pendingOrNot(), - this.indicateIfLoose(), - h(Identicon, { - address: identity.address, - imageify: true, - }), - ]), - - // account address, balance - h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', { - style: { - width: '200px', - }, - }, [ - h('span', identity.name), - h('span.font-small', { - style: { - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - }, checksumAddress), - h(EthBalance, { - value: account && account.balance, - currentCurrency, - conversionRate, - style: { - lineHeight: '7px', - marginTop: '10px', - }, - }), - ]), - - // copy button - h('.identity-copy.flex-column', { - style: { - margin: '0 20px', - }, - }, [ - h(CopyButton, { - value: checksumAddress, - }), - ]), - ]) - ) -} - -AccountListItem.prototype.indicateIfLoose = function () { - try { // Sometimes keyrings aren't loaded yet: - const type = this.props.keyring.type - const isLoose = type !== 'HD Key Tree' - return isLoose ? h('.keyring-label', 'LOOSE') : null - } catch (e) { return } -} - -AccountListItem.prototype.pendingOrNot = function () { - const pending = this.props.pending - if (pending.length === 0) return null - return h('.pending-dot', pending.length) -} diff --git a/ui/app/accounts/index.js b/ui/app/accounts/index.js deleted file mode 100644 index ac2615cd7..000000000 --- a/ui/app/accounts/index.js +++ /dev/null @@ -1,164 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../actions') -const valuesFor = require('../util').valuesFor -const findDOMNode = require('react-dom').findDOMNode -const AccountListItem = require('./account-list-item') - -module.exports = connect(mapStateToProps)(AccountsScreen) - -function mapStateToProps (state) { - const pendingTxs = valuesFor(state.metamask.unapprovedTxs) - .filter(txMeta => txMeta.metamaskNetworkId === state.metamask.network) - const pendingMsgs = valuesFor(state.metamask.unapprovedMsgs) - const pending = pendingTxs.concat(pendingMsgs) - - return { - accounts: state.metamask.accounts, - identities: state.metamask.identities, - unapprovedTxs: state.metamask.unapprovedTxs, - selectedAddress: state.metamask.selectedAddress, - scrollToBottom: state.appState.scrollToBottom, - pending, - keyrings: state.metamask.keyrings, - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - } -} - -inherits(AccountsScreen, Component) -function AccountsScreen () { - Component.call(this) -} - -AccountsScreen.prototype.render = function () { - const props = this.props - const { keyrings, conversionRate, currentCurrency } = props - const identityList = valuesFor(props.identities) - const unapprovedTxList = valuesFor(props.unapprovedTxs) - - return ( - - h('.accounts-section.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: this.goHome.bind(this), - }), - h('h2.page-subtitle', 'Select Account'), - ]), - - h('hr.horizontal-line'), - - // identity selection - h('section.identity-section', { - style: { - height: '418px', - overflowY: 'auto', - overflowX: 'hidden', - }, - }, - [ - identityList.map((identity) => { - const pending = this.props.pending.filter((txOrMsg) => { - if ('txParams' in txOrMsg) { - return txOrMsg.txParams.from === identity.address - } else if ('msgParams' in txOrMsg) { - return txOrMsg.msgParams.from === identity.address - } else { - return false - } - }) - - const simpleAddress = identity.address.substring(2).toLowerCase() - const keyring = keyrings.find((kr) => { - return kr.accounts.includes(simpleAddress) || - kr.accounts.includes(identity.address) - }) - - return h(AccountListItem, { - key: `acct-panel-${identity.address}`, - identity, - selectedAddress: this.props.selectedAddress, - conversionRate, - currentCurrency, - accounts: this.props.accounts, - onShowDetail: this.onShowDetail.bind(this), - pending, - keyring, - }) - }), - - h('hr.horizontal-line'), - h('div.footer.hover-white.pointer', { - key: 'reveal-account-bar', - onClick: () => { - this.addNewAccount() - }, - style: { - display: 'flex', - height: '40px', - padding: '10px', - justifyContent: 'center', - alignItems: 'center', - }, - }, [ - h('i.fa.fa-plus.fa-lg', {key: ''}), - ]), - h('hr.horizontal-line'), - ]), - - unapprovedTxList.length ? ( - - h('.unconftx-link.flex-row.flex-center', { - onClick: this.navigateToConfTx.bind(this), - }, [ - h('span', 'Unconfirmed Txs'), - h('i.fa.fa-arrow-right.fa-lg'), - ]) - - ) : ( - null - ), - ]) - ) -} - -// If a new account was revealed, scroll to the bottom -AccountsScreen.prototype.componentDidUpdate = function () { - const scrollToBottom = this.props.scrollToBottom - - if (scrollToBottom) { - var container = findDOMNode(this) - var scrollable = container.querySelector('.identity-section') - scrollable.scrollTop = scrollable.scrollHeight - } -} - -AccountsScreen.prototype.navigateToConfTx = function () { - event.stopPropagation() - this.props.dispatch(actions.showConfTxPage()) -} - -AccountsScreen.prototype.onShowDetail = function (address, event) { - event.stopPropagation() - this.props.dispatch(actions.showAccountDetail(address)) -} - -AccountsScreen.prototype.addNewAccount = function () { - this.props.dispatch(actions.addNewAccount(0)) -} - -/* An optional view proposed in this design: - * https://consensys.quip.com/zZVrAysM5znY -AccountsScreen.prototype.addNewAccount = function () { - this.props.dispatch(actions.navigateToNewAccountScreen()) -} -*/ - -AccountsScreen.prototype.goHome = function () { - this.props.dispatch(actions.goHome()) -} diff --git a/ui/app/add-token.js b/ui/app/add-token.js index 15ef7a852..b303b5c0d 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -86,7 +86,7 @@ AddTokenScreen.prototype.render = function () { h('div', [ h('span', { style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Token Symbol'), + }, 'Token Sybmol'), ]), h('div', { style: {display: 'flex'} }, [ diff --git a/ui/app/app.js b/ui/app/app.js index 1a63002e1..d1a20f079 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -10,7 +10,6 @@ const NewKeyChainScreen = require('./new-keychain') // unlock const UnlockScreen = require('./unlock') // accounts -const AccountsScreen = require('./accounts') const AccountDetailScreen = require('./account-detail') const SendTransactionScreen = require('./send') const ConfirmTxScreen = require('./conf-tx') @@ -24,10 +23,9 @@ const Import = require('./accounts/import') const InfoScreen = require('./info') const Loading = require('./components/loading') const SandwichExpando = require('sandwich-expando') -const MenuDroppo = require('menu-droppo') -const DropMenuItem = require('./components/drop-menu-item') +const Dropdown = require('./components/dropdown').Dropdown +const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem const NetworkIndicator = require('./components/network') -const Tooltip = require('./components/tooltip') const BuyView = require('./components/buy-button-subview') const QrView = require('./components/qr-code') const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete') @@ -79,6 +77,8 @@ App.prototype.render = function () { // Windows was showing a vertical scroll bar: overflow: 'hidden', position: 'relative', + height: '100%', + alignItems: 'center', }, }, [ @@ -95,8 +95,8 @@ App.prototype.render = function () { // panel content h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), { style: { - height: '380px', - width: '360px', + height: '100%', + maxWidth: '850px', }, }, [ h(ReactCSSTransitionGroup, { @@ -123,14 +123,18 @@ App.prototype.renderAppBar = function () { return ( - h('div', [ + h('div', { + style: { + width: '100%' + }, + }, [ h('.app-header.flex-row.flex-space-between', { style: { alignItems: 'center', visibility: props.isUnlocked ? 'visible' : 'none', background: props.isUnlocked ? 'white' : 'none', - height: '36px', + height: '38px', position: 'relative', zIndex: 12, }, @@ -178,21 +182,6 @@ App.prototype.renderAppBar = function () { }, }, [ - // small accounts nav - props.isUnlocked && h(Tooltip, { title: 'Switch Accounts' }, [ - h('img.cursor-pointer.color-orange', { - src: 'images/switch_acc.svg', - style: { - width: '23.5px', - marginRight: '8px', - }, - onClick: (event) => { - event.stopPropagation() - this.props.dispatch(actions.showAccountsPage()) - }, - }), - ]), - // hamburger props.isUnlocked && h(SandwichExpando, { width: 16, @@ -214,11 +203,12 @@ App.prototype.renderAppBar = function () { App.prototype.renderNetworkDropdown = function () { const props = this.props + const { provider: { type: providerType, rpcTarget: activeNetwork } } = props const rpcList = props.frequentRpcList const state = this.state || {} const isOpen = state.isNetworkMenuOpen - return h(MenuDroppo, { + return h(Dropdown, { isOpen, onClickOutside: (event) => { this.setState({ isNetworkMenuOpen: !isOpen }) @@ -226,72 +216,92 @@ App.prototype.renderNetworkDropdown = function () { zIndex: 11, style: { position: 'absolute', - left: 0, + left: '2px', top: '36px', }, - innerStyle: { - background: 'white', - boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', - }, - }, [ // DROP MENU ITEMS - h('style', ` - .drop-menu-item:hover { background:rgb(235, 235, 235); } - .drop-menu-item i { margin: 11px; } - `), - - h(DropMenuItem, { - label: 'Main Ethereum Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setProviderType('mainnet')), - icon: h('.menu-icon.diamond'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Ropsten Test Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setProviderType('ropsten')), - icon: h('.menu-icon.red-dot'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Kovan Test Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false}), - action: () => props.dispatch(actions.setProviderType('kovan')), - icon: h('.menu-icon.hollow-diamond'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Rinkeby Test Network', - closeMenu: () => this.setState({ isNetworkMenuOpen: false}), - action: () => props.dispatch(actions.setProviderType('rinkeby')), - icon: h('.menu-icon.golden-square'), - activeNetworkRender: props.network, - provider: props.provider, - }), - - h(DropMenuItem, { - label: 'Localhost 8545', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setDefaultRpcTarget(rpcList)), - icon: h('i.fa.fa-question-circle.fa-lg'), - activeNetworkRender: props.provider.rpcTarget, - }), + innerStyle: {}, + }, [ + + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setProviderType('mainnet')), + }, + [ + h('.menu-icon.diamond'), + 'Main Ethereum Network', + providerType === 'mainnet' ? h('.check', '✓') : null, + ] + ), + + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setProviderType('ropsten')), + }, + [ + h('.menu-icon.red-dot'), + 'Ropsten Test Network', + providerType === 'ropsten' ? h('.check', '✓') : null, + ] + ), + + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setProviderType('kovan')), + }, + [ + h('.menu-icon.hollow-diamond'), + 'Kovan Test Network', + providerType === 'kovan' ? h('.check', '✓') : null, + ] + ), + + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setProviderType('rinkeby')), + }, + [ + h('.menu-icon.golden-square'), + 'Rinkeby Test Network', + providerType === 'rinkeby' ? h('.check', '✓') : null, + ] + ), + + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => props.dispatch(actions.setDefaultRpcTarget(rpcList)), + }, + [ + h('i.fa.fa-question-circle.fa-lg.menu-icon'), + 'Localhost 8545', + activeNetwork === 'http://localhost:8545' ? h('.check', '✓') : null, + ] + ), this.renderCustomOption(props.provider), this.renderCommonRpc(rpcList, props.provider), - h(DropMenuItem, { - label: 'Custom RPC', - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => this.props.dispatch(actions.showConfigPage()), - icon: h('i.fa.fa-question-circle.fa-lg'), - }), + h( + DropdownMenuItem, + { + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => this.props.dispatch(actions.showConfigPage()), + }, + [ + h('i.fa.fa-question-circle.fa-lg.menu-icon'), + 'Custom RPC', + activeNetwork === 'custom' ? h('.check', '✓') : null, + ] + ), ]) } @@ -300,7 +310,7 @@ App.prototype.renderDropdown = function () { const state = this.state || {} const isOpen = state.isMainMenuOpen - return h(MenuDroppo, { + return h(Dropdown, { isOpen: isOpen, zIndex: 11, onClickOutside: (event) => { @@ -308,46 +318,30 @@ App.prototype.renderDropdown = function () { }, style: { position: 'absolute', - right: 0, - top: '36px', + right: '2px', + top: '38px', }, - innerStyle: { - background: 'white', - boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', - }, - }, [ // DROP MENU ITEMS - h('style', ` - .drop-menu-item:hover { background:rgb(235, 235, 235); } - .drop-menu-item i { margin: 11px; } - `), - - h(DropMenuItem, { - label: 'Settings', + innerStyle: {}, + }, [ + h(DropdownMenuItem, { closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.showConfigPage()), - icon: h('i.fa.fa-gear.fa-lg'), - }), + onClick: () => { this.props.dispatch(actions.showConfigPage()) }, + }, 'Settings'), - h(DropMenuItem, { - label: 'Import Account', + h(DropdownMenuItem, { closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.showImportPage()), - icon: h('i.fa.fa-arrow-circle-o-up.fa-lg'), - }), + onClick: () => { this.props.dispatch(actions.showImportPage()) }, + }, 'Import Account'), - h(DropMenuItem, { - label: 'Lock', + h(DropdownMenuItem, { closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.lockMetamask()), - icon: h('i.fa.fa-lock.fa-lg'), - }), + onClick: () => { this.props.dispatch(actions.lockMetamask()) }, + }, 'Lock'), - h(DropMenuItem, { - label: 'Info/Help', + h(DropdownMenuItem, { closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - action: () => this.props.dispatch(actions.showInfoPage()), - icon: h('i.fa.fa-question.fa-lg'), - }), + onClick: () => { this.props.dispatch(actions.showInfoPage()) }, + }, 'Info/Help'), ]) } @@ -433,10 +427,6 @@ App.prototype.renderPrimary = function () { // show current view switch (props.currentView.name) { - case 'accounts': - log.debug('rendering accounts screen') - return h(AccountsScreen, {key: 'accounts'}) - case 'accountDetail': log.debug('rendering account detail screen') return h(AccountDetailScreen, {key: 'account-detail'}) @@ -539,13 +529,18 @@ App.prototype.renderCustomOption = function (provider) { return null default: - return h(DropMenuItem, { - label, - key: rpcTarget, - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - icon: h('i.fa.fa-question-circle.fa-lg'), - activeNetworkRender: 'custom', - }) + return h( + DropdownMenuItem, + { + key: rpcTarget, + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + }, + [ + h('i.fa.fa-question-circle.fa-lg.menu-icon'), + label, + h('.check', '✓'), + ] + ) } } @@ -578,14 +573,19 @@ App.prototype.renderCommonRpc = function (rpcList, provider) { if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { return null } else { - return h(DropMenuItem, { - label: rpc, - key: rpc, - closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setRpcTarget(rpc)), - icon: h('i.fa.fa-question-circle.fa-lg'), - activeNetworkRender: rpc, - }) + return h( + DropdownMenuItem, + { + key: rpc, + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setRpcTarget(rpc)), + }, + [ + h('i.fa.fa-question-circle.fa-lg.menu-icon'), + rpc, + h('.check', '✓'), + ] + ) } }) } diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js new file mode 100644 index 000000000..d1d319477 --- /dev/null +++ b/ui/app/components/account-dropdowns.js @@ -0,0 +1,227 @@ +const Component = require('react').Component +const PropTypes = require('react').PropTypes +const h = require('react-hyperscript') +const actions = require('../actions') +const genAccountLink = require('../../lib/account-link.js') +const connect = require('react-redux').connect +const Dropdown = require('./dropdown').Dropdown +const DropdownMenuItem = require('./dropdown').DropdownMenuItem +const Identicon = require('./identicon') +const ethUtil = require('ethereumjs-util') +const copyToClipboard = require('copy-to-clipboard') + +class AccountDropdowns extends Component { + constructor (props) { + super(props) + this.state = { + accountSelectorActive: false, + optionsMenuActive: false, + } + } + + renderAccounts () { + const { identities, selected } = this.props + + return Object.keys(identities).map((key) => { + const identity = identities[key] + const isSelected = identity.address === selected + + return h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + this.props.actions.showAccountDetail(identity.address) + }, + }, + [ + h( + Identicon, + { + address: identity.address, + diameter: 16, + }, + ), + h('span', { style: { marginLeft: '10px' } }, identity.name || ''), + h('span', { style: { marginLeft: '10px' } }, isSelected ? h('.check', '✓') : null), + ] + ) + }) + } + + renderAccountSelector () { + const { actions } = this.props + const { accountSelectorActive } = this.state + + return h( + Dropdown, + { + style: { + marginLeft: '-125px', + minWidth: '180px', + }, + isOpen: accountSelectorActive, + onClickOutside: () => { this.setState({ accountSelectorActive: false }) }, + }, + [ + ...this.renderAccounts(), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => actions.addNewAccount(), + }, + [ + h( + Identicon, + { + diameter: 16, + }, + ), + h('span', { style: { marginLeft: '10px' } }, 'Create Account'), + ], + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => actions.showImportPage(), + }, + [ + h( + Identicon, + { + diameter: 16, + }, + ), + h('span', { style: { marginLeft: '10px' } }, 'Import Account'), + ] + ), + ] + ) + } + + renderAccountOptions () { + const { actions } = this.props + const { optionsMenuActive } = this.state + + return h( + Dropdown, + { + style: { + marginLeft: '-162px', + minWidth: '180px', + }, + isOpen: optionsMenuActive, + onClickOutside: () => { this.setState({ optionsMenuActive: false }) }, + }, + [ + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => actions.showConfigPage(), + }, + 'Account Settings', + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + const { selected, network } = this.props + const url = genAccountLink(selected, network) + global.platform.openWindow({ url }) + }, + }, + 'View account on Etherscan', + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + const { selected } = this.props + const checkSumAddress = selected && ethUtil.toChecksumAddress(selected) + copyToClipboard(checkSumAddress) + }, + }, + 'Copy Address to clipboard', + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + actions.requestAccountExport() + }, + }, + 'Export Private Key', + ), + ] + ) + } + + render () { + const { style } = this.props + const { optionsMenuActive, accountSelectorActive } = this.state + + return h( + 'span', + { + style: style, + }, + [ + h( + 'i.fa.fa-angle-down', + { + style: {}, + onClick: (event) => { + event.stopPropagation() + this.setState({ + accountSelectorActive: !accountSelectorActive, + optionsMenuActive: false, + }) + }, + }, + this.renderAccountSelector(), + ), + h( + 'i.fa.fa-ellipsis-h', + { + style: { 'marginLeft': '10px'}, + onClick: (event) => { + event.stopPropagation() + this.setState({ + accountSelectorActive: false, + optionsMenuActive: !optionsMenuActive, + }) + }, + }, + this.renderAccountOptions() + ), + ] + ) + } +} + +AccountDropdowns.propTypes = { + identities: PropTypes.objectOf(PropTypes.object), + selected: PropTypes.string, +} + +const mapDispatchToProps = (dispatch) => { + return { + actions: { + showConfigPage: () => dispatch(actions.showConfigPage()), + requestAccountExport: () => dispatch(actions.requestExportAccount()), + showAccountDetail: (address) => dispatch(actions.showAccountDetail(address)), + addNewAccount: () => dispatch(actions.addNewAccount()), + showImportPage: () => dispatch(actions.showImportPage()), + }, + } +} + +module.exports = { + AccountDropdowns: connect(null, mapDispatchToProps)(AccountDropdowns), +} diff --git a/ui/app/components/account-info-link.js b/ui/app/components/account-info-link.js deleted file mode 100644 index 6526ab502..000000000 --- a/ui/app/components/account-info-link.js +++ /dev/null @@ -1,41 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const Tooltip = require('./tooltip') -const genAccountLink = require('../../lib/account-link') - -module.exports = AccountInfoLink - -inherits(AccountInfoLink, Component) -function AccountInfoLink () { - Component.call(this) -} - -AccountInfoLink.prototype.render = function () { - const { selected, network } = this.props - const title = 'View account on Etherscan' - const url = genAccountLink(selected, network) - - if (!url) { - return null - } - - return h('.account-info-link', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - - h(Tooltip, { - title, - }, [ - h('i.fa.fa-info-circle.cursor-pointer.color-orange', { - style: { - margin: '5px', - }, - onClick () { global.platform.openWindow({ url }) }, - }), - ]), - ]) -} diff --git a/ui/app/components/drop-menu-item.js b/ui/app/components/drop-menu-item.js deleted file mode 100644 index e42948209..000000000 --- a/ui/app/components/drop-menu-item.js +++ /dev/null @@ -1,59 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits - -module.exports = DropMenuItem - -inherits(DropMenuItem, Component) -function DropMenuItem () { - Component.call(this) -} - -DropMenuItem.prototype.render = function () { - return h('li.drop-menu-item', { - onClick: () => { - this.props.closeMenu() - this.props.action() - }, - style: { - listStyle: 'none', - padding: '6px 16px 6px 5px', - fontFamily: 'Montserrat Regular', - color: 'rgb(125, 128, 130)', - cursor: 'pointer', - display: 'flex', - justifyContent: 'flex-start', - }, - }, [ - this.props.icon, - this.props.label, - this.activeNetworkRender(), - ]) -} - -DropMenuItem.prototype.activeNetworkRender = function () { - const activeNetwork = this.props.activeNetworkRender - const { provider } = this.props - const providerType = provider ? provider.type : null - if (activeNetwork === undefined) return - - switch (this.props.label) { - case 'Main Ethereum Network': - if (providerType === 'mainnet') return h('.check', '✓') - break - case 'Ropsten Test Network': - if (providerType === 'ropsten') return h('.check', '✓') - break - case 'Kovan Test Network': - if (providerType === 'kovan') return h('.check', '✓') - break - case 'Rinkeby Test Network': - if (providerType === 'rinkeby') return h('.check', '✓') - break - case 'Localhost 8545': - if (activeNetwork === 'http://localhost:8545') return h('.check', '✓') - break - default: - if (activeNetwork === 'custom') return h('.check', '✓') - } -} diff --git a/ui/app/components/dropdown.js b/ui/app/components/dropdown.js new file mode 100644 index 000000000..e77b4c40c --- /dev/null +++ b/ui/app/components/dropdown.js @@ -0,0 +1,89 @@ +const Component = require('react').Component +const PropTypes = require('react').PropTypes +const h = require('react-hyperscript') +const MenuDroppo = require('menu-droppo') + +const noop = () => {} + +class Dropdown extends Component { + render () { + const { isOpen, onClickOutside, style, children } = this.props + + return h( + MenuDroppo, + { + isOpen, + zIndex: 11, + onClickOutside, + style, + innerStyle: { + borderRadius: '4px', + padding: '8px 16px', + background: 'rgba(0, 0, 0, 0.8)', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + }, + }, + [ + h( + 'style', + ` + li.dropdown-menu-item:hover { color:rgb(225, 225, 225); } + li.dropdown-menu-item { color: rgb(185, 185, 185); } + ` + ), + ...children, + ] + ) + } +} + +Dropdown.defaultProps = { + isOpen: false, + onClick: noop, +} + +Dropdown.propTypes = { + isOpen: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, + children: PropTypes.node, + style: PropTypes.object.isRequired, +} + +class DropdownMenuItem extends Component { + render () { + const { onClick, closeMenu, children } = this.props + + return h( + 'li.dropdown-menu-item', + { + onClick: () => { + onClick() + closeMenu() + }, + style: { + listStyle: 'none', + padding: '8px 0px 8px 0px', + fontSize: '12px', + fontStyle: 'normal', + fontFamily: 'Montserrat Regular', + cursor: 'pointer', + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'center', + }, + }, + children + ) + } +} + +DropdownMenuItem.propTypes = { + closeMenu: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, + children: PropTypes.node, +} + +module.exports = { + Dropdown, + DropdownMenuItem, +} diff --git a/ui/app/components/editable-label.js b/ui/app/components/editable-label.js index 41936f5e0..167be7eaf 100644 --- a/ui/app/components/editable-label.js +++ b/ui/app/components/editable-label.js @@ -30,7 +30,12 @@ EditableLabel.prototype.render = function () { } else { return h('div.name-label', { onClick: (event) => { - this.setState({ isEditingLabel: true }) + const nameAttribute = event.target.getAttribute('name') + // checks for class to handle smaller CTA above the account name + const classAttribute = event.target.getAttribute('class') + if (nameAttribute === 'edit' || classAttribute === 'edit-text') { + this.setState({ isEditingLabel: true }) + } }, }, this.props.children) } diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 5324ccd64..d7d602f31 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -15,7 +15,7 @@ const addressSummary = util.addressSummary const nameForAddress = require('../../lib/contract-namer') const BNInput = require('./bn-as-decimal-input') -const MIN_GAS_PRICE_GWEI_BN = new BN(1) +const MIN_GAS_PRICE_GWEI_BN = new BN(2) const GWEI_FACTOR = new BN(1e9) const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) const MIN_GAS_LIMIT_BN = new BN(21000) diff --git a/ui/app/components/transaction-list.js b/ui/app/components/transaction-list.js index 3b4ba741e..ae6aaec8c 100644 --- a/ui/app/components/transaction-list.js +++ b/ui/app/components/transaction-list.js @@ -24,7 +24,11 @@ TransactionList.prototype.render = function () { return ( - h('section.transaction-list', [ + h('section.transaction-list', { + style: { + height: '100%', + }, + }, [ h('style', ` .transaction-list .transaction-list-item:not(:last-of-type) { @@ -39,7 +43,7 @@ TransactionList.prototype.render = function () { h('.tx-list', { style: { overflowY: 'auto', - height: '300px', + height: '100%', padding: '0 20px', textAlign: 'center', }, diff --git a/ui/app/css/index.css b/ui/app/css/index.css index 808aafb4c..2ae92bbd6 100644 --- a/ui/app/css/index.css +++ b/ui/app/css/index.css @@ -19,6 +19,15 @@ html, body { font-weight: 300; line-height: 1.4em; background: #F7F7F7; + width: 100%; + height: 100%; + margin: 0; + padding: 0; +} + +.css-transition-group { + flex: 1; + height: 100%; } input:focus, textarea:focus { @@ -28,8 +37,7 @@ input:focus, textarea:focus { #app-content { overflow-x: hidden; min-width: 357px; - width: 360px; - height: 500px; + height: 100%; } button, input[type="submit"] { @@ -403,7 +411,8 @@ input.large-input { /* account detail screen */ .account-detail-section { - + display: flex; + flex-wrap: wrap; } .name-label{ diff --git a/ui/app/css/lib.css b/ui/app/css/lib.css index 910a24ee2..b0ca958a2 100644 --- a/ui/app/css/lib.css +++ b/ui/app/css/lib.css @@ -232,6 +232,10 @@ hr.horizontal-line { align-items: center; } +.tabSection { + min-width: 350px; +} + .menu-icon { display: inline-block; height: 9px; diff --git a/ui/app/info.js b/ui/app/info.js index cb2e41f5b..e8470de97 100644 --- a/ui/app/info.js +++ b/ui/app/info.js @@ -97,17 +97,11 @@ InfoScreen.prototype.render = function () { paddingLeft: '30px', }}, [ - h('div.fa.fa-support', [ - h('a.info', { - href: 'http://metamask.consensyssupport.happyfox.com', - target: '_blank', - }, 'Visit our Support Center'), - ]), h('div.fa.fa-github', [ h('a.info', { - href: 'https://github.com/MetaMask/metamask-extension/issues/new', + href: 'https://github.com/MetaMask/faq', target: '_blank', - }, 'Found a bug? Report it!'), + }, 'Need Help? Read our FAQ!'), ]), h('div', [ h('a', { diff --git a/ui/app/keychains/hd/create-vault-complete.js b/ui/app/keychains/hd/create-vault-complete.js index a318a9b50..c32751fff 100644 --- a/ui/app/keychains/hd/create-vault-complete.js +++ b/ui/app/keychains/hd/create-vault-complete.js @@ -47,8 +47,6 @@ CreateVaultCompleteScreen.prototype.render = function () { h('div', { style: { - width: '360px', - height: '78px', fontSize: '1em', marginTop: '10px', textAlign: 'center', diff --git a/ui/lib/tx-helper.js b/ui/lib/tx-helper.js index afc62e7b6..ec19daf64 100644 --- a/ui/lib/tx-helper.js +++ b/ui/lib/tx-helper.js @@ -12,10 +12,6 @@ module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, network) const personalValues = valuesFor(personalMsgs) log.debug(`tx helper found ${personalValues.length} unsigned personal messages`) allValues = allValues.concat(personalValues) - allValues = allValues.sort((a, b) => { - return a.time > b.time - }) - return allValues + return allValues.sort(txMeta => txMeta.time) } - -- cgit v1.2.3 From 7a04643d8ea8a47cc159fc69c941e588e8b873cc Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 24 Jul 2017 17:27:27 -0700 Subject: Restore sort order fix --- ui/app/app.js | 2 +- ui/lib/tx-helper.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index d1a20f079..973cb756c 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -125,7 +125,7 @@ App.prototype.renderAppBar = function () { h('div', { style: { - width: '100%' + width: '100%', }, }, [ diff --git a/ui/lib/tx-helper.js b/ui/lib/tx-helper.js index ec19daf64..5def23e51 100644 --- a/ui/lib/tx-helper.js +++ b/ui/lib/tx-helper.js @@ -12,6 +12,9 @@ module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, network) const personalValues = valuesFor(personalMsgs) log.debug(`tx helper found ${personalValues.length} unsigned personal messages`) allValues = allValues.concat(personalValues) + allValues = allValues.sort((a, b) => { + return a.time > b.time + }) - return allValues.sort(txMeta => txMeta.time) + return allValues } -- cgit v1.2.3 From 77d91ec36f452ce47676b5ede2aa1b3d068d2634 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 25 Jul 2017 11:57:03 -0700 Subject: prov-eng - bump to ignore json parse errors --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 375902d09..9b582d3c9 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,7 @@ "valid-url": "^1.0.9", "vreme": "^3.0.2", "web3": "0.19.1", - "web3-provider-engine": "^13.2.8", + "web3-provider-engine": "^13.2.9", "web3-stream-provider": "^3.0.1", "xtend": "^4.0.1" }, -- cgit v1.2.3 From ab01358a480243c9073ac06dc4f510a6089567d0 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 25 Jul 2017 16:08:31 -0400 Subject: Add stack traces both in errors and as a way to track txMetas --- CHANGELOG.md | 2 ++ app/scripts/controllers/transactions.js | 30 ++++++++++++++++++++++-------- app/scripts/lib/util.js | 8 ++++++++ package.json | 1 + 4 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 app/scripts/lib/util.js diff --git a/CHANGELOG.md b/CHANGELOG.md index bf18bb361..eeeda9d68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Include stack traces in txMeta's to better understand the life cycle of transactions + ## 3.9.1 2017-7-19 - No longer automatically request 1 ropsten ether for the first account in a new vault. diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 5f3d84ebe..4d1a18df7 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -1,10 +1,12 @@ const EventEmitter = require('events') const async = require('async') const extend = require('xtend') +const clone = require('deep-clone') const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') const pify = require('pify') const TxProviderUtil = require('../lib/tx-utils') +const getStack = require('../lib/util').getStack const createId = require('../lib/random-id') const NonceTracker = require('../lib/nonce-tracker') @@ -117,9 +119,14 @@ module.exports = class TransactionController extends EventEmitter { // updateTx (txMeta) { + const txMetaForHistory = clone(txMeta) + txMetaForHistory.stack = getStack() var txId = txMeta.id var txList = this.getFullTxList() var index = txList.findIndex(txData => txData.id === txId) + if (!txMeta.history) txMeta.history = [] + txMeta.history.push(txMetaForHistory) + txList[index] = txMeta this._saveTxList(txList) this.emit('update') @@ -134,7 +141,7 @@ module.exports = class TransactionController extends EventEmitter { } addUnapprovedTransaction (txParams, done) { - let txMeta + let txMeta = {} async.waterfall([ // validate (cb) => this.txProviderUtils.validateTxParams(txParams, cb), @@ -146,6 +153,7 @@ module.exports = class TransactionController extends EventEmitter { status: 'unapproved', metamaskNetworkId: this.getNetwork(), txParams: txParams, + history: [], } cb() }, @@ -165,6 +173,7 @@ module.exports = class TransactionController extends EventEmitter { txParams.value = txParams.value || '0x0' if (!txParams.gasPrice) { this.query.gasPrice((err, gasPrice) => { + if (err) return cb(err) // set gasPrice txParams.gasPrice = gasPrice @@ -201,6 +210,7 @@ module.exports = class TransactionController extends EventEmitter { nonceLock.releaseLock() } catch (err) { this.setTxStatusFailed(txId, { + stack: err.stack || err.message, errCode: err.errCode || err, message: err.message || 'Transaction failed during approval', }) @@ -364,11 +374,11 @@ module.exports = class TransactionController extends EventEmitter { var txId = txMeta.id if (!txHash) { - const errReason = { + return this.setTxStatusFailed(txId, { + stack: 'checkForTxInBlock: custom tx-controller error message Line# 368', errCode: 'No hash was provided', message: 'We had an error while submitting this transaction, please try again.', - } - return this.setTxStatusFailed(txId, errReason) + }) } block.transactions.forEach((tx) => { @@ -452,6 +462,7 @@ module.exports = class TransactionController extends EventEmitter { if (isKnownTx) return // encountered real error - transition to error state this.setTxStatusFailed(txMeta.id, { + stack: err.stack || err.message, errCode: err.errCode || err, message: err.message, }) @@ -466,7 +477,10 @@ module.exports = class TransactionController extends EventEmitter { // if the value of the transaction is greater then the balance, fail. if (!this.txProviderUtils.sufficientBalance(txMeta.txParams, balance)) { const message = 'Insufficient balance.' - this.setTxStatusFailed(txMeta.id, { message }) + this.setTxStatusFailed(txMeta.id, { + stack: '_resubnitTx: custom tx-controller error line# 472', + message, + }) cb() return log.error(message) } @@ -501,11 +515,11 @@ module.exports = class TransactionController extends EventEmitter { // extra check in case there was an uncaught error during the // signature and submission process if (!txHash) { - const errReason = { + this.setTxStatusFailed(txId, { + stack: '_checkPendingTxs: custom tx-controller error message Line# 510', errCode: 'No hash was provided', message: 'We had an error while submitting this transaction, please try again.', - } - this.setTxStatusFailed(txId, errReason) + }) return } // get latest transaction status diff --git a/app/scripts/lib/util.js b/app/scripts/lib/util.js new file mode 100644 index 000000000..bddd60ee8 --- /dev/null +++ b/app/scripts/lib/util.js @@ -0,0 +1,8 @@ +module.exports = { + getStack, +} + +function getStack () { + const stack = new Error('Stack trace generator - not an error').stack + return stack +} diff --git a/package.json b/package.json index 375902d09..f18f84727 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "clone": "^1.0.2", "copy-to-clipboard": "^2.0.0", "debounce": "^1.0.0", + "deep-clone": "^3.0.2", "deep-extend": "^0.4.1", "detect-node": "^2.0.3", "disc": "^1.3.2", -- cgit v1.2.3 From 5b9a6bd367173330d8bcfd973278eeba6f31ec06 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 25 Jul 2017 13:16:46 -0700 Subject: tx cont - remove old cb from async fn --- app/scripts/controllers/transactions.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 5f3d84ebe..fc91bdf4d 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -458,7 +458,7 @@ module.exports = class TransactionController extends EventEmitter { })) } - async _resubmitTx (txMeta, cb) { + async _resubmitTx (txMeta) { const address = txMeta.txParams.from const balance = this.ethStore.getState().accounts[address].balance if (!('retryCount' in txMeta)) txMeta.retryCount = 0 @@ -467,17 +467,17 @@ module.exports = class TransactionController extends EventEmitter { if (!this.txProviderUtils.sufficientBalance(txMeta.txParams, balance)) { const message = 'Insufficient balance.' this.setTxStatusFailed(txMeta.id, { message }) - cb() - return log.error(message) + log.error(message) + return } // Only auto-submit already-signed txs: - if (!('rawTx' in txMeta)) return cb() + if (!('rawTx' in txMeta)) return // Increment a try counter. txMeta.retryCount++ const rawTx = txMeta.rawTx - return await this.txProviderUtils.publishTransaction(rawTx, cb) + return await this.txProviderUtils.publishTransaction(rawTx) } // checks the network for signed txs and -- cgit v1.2.3 From 4445ba15694fc6ef7f9c4e3a2de5de5428e28b3a Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 25 Jul 2017 14:36:19 -0700 Subject: tx cont - add argument for provider constructor --- app/scripts/controllers/network.js | 5 +++-- test/unit/network-contoller-test.js | 36 +++++++++++++++++++++--------------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js index c07f13b8d..0a3e5e26b 100644 --- a/app/scripts/controllers/network.js +++ b/app/scripts/controllers/network.js @@ -28,9 +28,9 @@ module.exports = class NetworkController extends EventEmitter { this._provider = provider } - initializeProvider (opts) { + initializeProvider (opts, providerContructor = MetaMaskProvider) { this.providerInit = opts - this._provider = MetaMaskProvider(opts) + this._provider = providerContructor(opts) this._proxy = new Proxy(this._provider, { get: (obj, name) => { if (name === 'on') return this._on.bind(this) @@ -38,6 +38,7 @@ module.exports = class NetworkController extends EventEmitter { }, set: (obj, name, value) => { this._provider[name] = value + return value }, }) this.provider.on('block', this._logBlock.bind(this)) diff --git a/test/unit/network-contoller-test.js b/test/unit/network-contoller-test.js index 0c7ee9d70..87c2ee7a3 100644 --- a/test/unit/network-contoller-test.js +++ b/test/unit/network-contoller-test.js @@ -3,6 +3,9 @@ const NetworkController = require('../../app/scripts/controllers/network') describe('# Network Controller', function () { let networkController + const networkControllerProviderInit = { + getAccounts: () => {}, + } beforeEach(function () { networkController = new NetworkController({ @@ -10,26 +13,13 @@ describe('# Network Controller', function () { type: 'rinkeby', }, }) - // stub out provider - networkController._provider = new Proxy({}, { - get: (obj, name) => { - return () => {} - }, - }) - networkController.providerInit = { - getAccounts: () => {}, - } - networkController.ethQuery = new Proxy({}, { - get: (obj, name) => { - return () => {} - }, - }) + networkController.initializeProvider(networkControllerProviderInit, dummyProviderConstructor) }) describe('network', function () { describe('#provider', function () { it('provider should be updatable without reassignment', function () { - networkController.initializeProvider(networkController.providerInit) + networkController.initializeProvider(networkControllerProviderInit, dummyProviderConstructor) const provider = networkController.provider networkController._provider = {test: true} assert.ok(provider.test) @@ -75,3 +65,19 @@ describe('# Network Controller', function () { }) }) }) + +function dummyProviderConstructor() { + return { + // provider + sendAsync: noop, + // block tracker + start: noop, + stop: noop, + on: noop, + addListener: noop, + once: noop, + removeAllListeners: noop, + } +} + +function noop() {} \ No newline at end of file -- cgit v1.2.3 From 5ec73c0e652f5389d14d00abed3977324043a824 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 25 Jul 2017 14:39:17 -0700 Subject: tx cont - fix test to use async api --- test/unit/tx-controller-test.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index 7b86cfe14..31908569a 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -343,13 +343,17 @@ describe('Transaction Controller', function () { // Adding the fake tx: txController.addTx(clone(txMeta)) - txController._resubmitTx(txMeta, function (err) { - assert.ifError(err, 'should not throw an error') + txController._resubmitTx(txMeta) + .then(() => { const updatedMeta = txController.getTx(txMeta.id) assert.notEqual(updatedMeta.status, txMeta.status, 'status changed.') assert.equal(updatedMeta.status, 'failed', 'tx set to failed.') done() }) + .catch((err) => { + assert.ifError(err, 'should not throw an error') + done() + }) }) }) }) -- cgit v1.2.3 From e0a626da3b026fd7dd258e8accf771353c4ea38e Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 25 Jul 2017 18:02:21 -0400 Subject: remove line numbers --- app/scripts/controllers/transactions.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 4d1a18df7..d96e2236a 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -375,7 +375,7 @@ module.exports = class TransactionController extends EventEmitter { if (!txHash) { return this.setTxStatusFailed(txId, { - stack: 'checkForTxInBlock: custom tx-controller error message Line# 368', + stack: 'checkForTxInBlock: custom tx-controller error me', errCode: 'No hash was provided', message: 'We had an error while submitting this transaction, please try again.', }) @@ -478,7 +478,7 @@ module.exports = class TransactionController extends EventEmitter { if (!this.txProviderUtils.sufficientBalance(txMeta.txParams, balance)) { const message = 'Insufficient balance.' this.setTxStatusFailed(txMeta.id, { - stack: '_resubnitTx: custom tx-controller error line# 472', + stack: '_resubnitTx: custom tx-controller ', message, }) cb() @@ -516,7 +516,7 @@ module.exports = class TransactionController extends EventEmitter { // signature and submission process if (!txHash) { this.setTxStatusFailed(txId, { - stack: '_checkPendingTxs: custom tx-controller error message Line# 510', + stack: '_checkPendingTxs: custom tx-controller error message', errCode: 'No hash was provided', message: 'We had an error while submitting this transaction, please try again.', }) -- cgit v1.2.3 From 1df833bee8a51c304491a3045138560e8c3f2b52 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 25 Jul 2017 18:21:40 -0400 Subject: use clone --- app/scripts/controllers/transactions.js | 2 +- package.json | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index d96e2236a..4de2b7db3 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -1,7 +1,7 @@ const EventEmitter = require('events') const async = require('async') const extend = require('xtend') -const clone = require('deep-clone') +const clone = require('clone') const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') const pify = require('pify') diff --git a/package.json b/package.json index f18f84727..375902d09 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "clone": "^1.0.2", "copy-to-clipboard": "^2.0.0", "debounce": "^1.0.0", - "deep-clone": "^3.0.2", "deep-extend": "^0.4.1", "detect-node": "^2.0.3", "disc": "^1.3.2", -- cgit v1.2.3 From b81f8831505b1ebb1d58a474d52b068d42879d56 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 25 Jul 2017 18:23:17 -0400 Subject: fix stack wording --- app/scripts/controllers/transactions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 4de2b7db3..5485dc723 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -375,7 +375,7 @@ module.exports = class TransactionController extends EventEmitter { if (!txHash) { return this.setTxStatusFailed(txId, { - stack: 'checkForTxInBlock: custom tx-controller error me', + stack: 'checkForTxInBlock: custom tx-controller error message', errCode: 'No hash was provided', message: 'We had an error while submitting this transaction, please try again.', }) @@ -478,7 +478,7 @@ module.exports = class TransactionController extends EventEmitter { if (!this.txProviderUtils.sufficientBalance(txMeta.txParams, balance)) { const message = 'Insufficient balance.' this.setTxStatusFailed(txMeta.id, { - stack: '_resubnitTx: custom tx-controller ', + stack: '_resubnitTx: custom tx-controller error', message, }) cb() -- cgit v1.2.3 From ba88f7b8dd32b6ffdb46e70b8c9fbd563bb53b69 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 25 Jul 2017 18:29:02 -0400 Subject: fix typo --- app/scripts/controllers/transactions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 5485dc723..263424518 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -478,7 +478,7 @@ module.exports = class TransactionController extends EventEmitter { if (!this.txProviderUtils.sufficientBalance(txMeta.txParams, balance)) { const message = 'Insufficient balance.' this.setTxStatusFailed(txMeta.id, { - stack: '_resubnitTx: custom tx-controller error', + stack: '_resubmitTx: custom tx-controller error', message, }) cb() -- cgit v1.2.3 From eb1566349723804e688f95a06f1208767e6d1938 Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Tue, 25 Jul 2017 16:33:52 -0700 Subject: One script runs for Ci build --- circle.yml | 4 +--- package.json | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/circle.yml b/circle.yml index efeb8ba57..2ea60bb9d 100644 --- a/circle.yml +++ b/circle.yml @@ -7,6 +7,4 @@ dependencies: - "npm i -g mocha" test: override: - - "npm run lint" - - "npm run test-coverage" - - "npm run test-integration" \ No newline at end of file + - "npm run ci" \ No newline at end of file diff --git a/package.json b/package.json index fe5466b9e..6d5227356 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "single-test": "METAMASK_ENV=test mocha --require test/helper.js", "test-integration": "npm run buildMock && npm run buildCiUnits && testem ci -P 2", "test-coverage": "nyc npm run test-unit && nyc report --reporter=text-lcov | coveralls", + "ci": "npm run lint && npm run test-coverage && npm run test-integration", "lint": "gulp lint", "buildCiUnits": "node test/integration/index.js", "watch": "mocha watch --recursive \"test/unit/**/*.js\"", -- cgit v1.2.3 From 3575bd49ab4f3ec879a3e04b4a6fa35fe9c67c6e Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Tue, 25 Jul 2017 16:59:08 -0700 Subject: Run coveralls on all branches --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9aded09c1..27b19c91c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# MetaMask Plugin [![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) [![Coverage Status](https://coveralls.io/repos/github/MetaMask/metamask-extension/badge.svg?branch=master)](https://coveralls.io/github/MetaMask/metamask-extension?branch=master) +# MetaMask Plugin [![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) [![Coverage Status](https://coveralls.io/repos/github/MetaMask/metamask-extension/badge.svg?branch=master)](https://coveralls.io/github/MetaMask/metamask-extension) ## Support -- cgit v1.2.3 From 4d218ac57a5db8c4d3d446fbfaa5ef8488c2a6d5 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 25 Jul 2017 17:39:40 -0700 Subject: Remove responsive folder from gulp --- gulpfile.js | 1 - 1 file changed, 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index f0a28e273..53de7a7d9 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -175,7 +175,6 @@ const jsFiles = [ 'blacklister', 'background', 'popup', - 'responsive' ] // bundle tasks -- cgit v1.2.3 From 1eb089b0e76336719bf4c627755a7aa789280808 Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Tue, 25 Jul 2017 18:03:47 -0700 Subject: Coveralls badge directs to master branch --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 27b19c91c..9aded09c1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# MetaMask Plugin [![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) [![Coverage Status](https://coveralls.io/repos/github/MetaMask/metamask-extension/badge.svg?branch=master)](https://coveralls.io/github/MetaMask/metamask-extension) +# MetaMask Plugin [![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) [![Coverage Status](https://coveralls.io/repos/github/MetaMask/metamask-extension/badge.svg?branch=master)](https://coveralls.io/github/MetaMask/metamask-extension?branch=master) ## Support -- cgit v1.2.3 From 0ea6749dbc923a6e796b1de4bbd301d931739b9d Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 25 Jul 2017 18:22:31 -0700 Subject: Lots of flex rearrangement on account detail view Includes removal of ReactCssTransitionGroup for a simpler UI refactor. --- package.json | 2 -- ui/app/account-detail.js | 25 +++--------------- ui/app/app.js | 31 ++++++---------------- ui/app/components/loading.js | 47 ++++++++++++++-------------------- ui/app/components/shapeshift-form.js | 10 +------- ui/app/components/tab-bar.js | 1 + ui/app/components/token-list.js | 40 +++++++++++++++++++++-------- ui/app/components/transaction-list.js | 12 +++++---- ui/app/conf-tx.js | 48 ++++++++++++++--------------------- ui/app/css/index.css | 46 ++++++++++++++++++++++++++++----- ui/app/css/lib.css | 1 + 11 files changed, 129 insertions(+), 134 deletions(-) diff --git a/package.json b/package.json index a95f2c75f..c1d83107b 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ "dist": "npm run clear && npm install && gulp dist", "test": "npm run lint && npm run test-unit && npm run test-integration", "test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"", - "test-responsive": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/responsive/**/*.js\"", "single-test": "METAMASK_ENV=test mocha --require test/helper.js", "test-integration": "npm run buildMock && npm run buildCiUnits && testem ci -P 2", "lint": "gulp lint", @@ -106,7 +105,6 @@ "pumpify": "^1.3.4", "qrcode-npm": "0.0.3", "react": "^15.0.2", - "react-addons-css-transition-group": "^15.0.2", "react-dom": "^15.5.4", "react-hyperscript": "^2.2.2", "react-markdown": "^2.3.0", diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index 18c867153..24561c32e 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -4,7 +4,6 @@ const Component = require('react').Component const h = require('react-hyperscript') const connect = require('react-redux').connect const actions = require('./actions') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') const valuesFor = require('./util').valuesFor const Identicon = require('./components/identicon') const EthBalance = require('./components/eth-balance') @@ -51,14 +50,9 @@ AccountDetailScreen.prototype.render = function () { return ( - h('.account-detail-section', { - style: { - height: '100%', - maxWidth: '850px', - }, - }, [ + h('.account-detail-section.full-flex-height', [ - // identicon, label, balance, etc + // identicon, label, balance, etc h('.account-data-subsection', { style: { margin: '0 20px', @@ -205,14 +199,7 @@ AccountDetailScreen.prototype.render = function () { ]), // subview (tx history, pk export confirm, buy eth warning) - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.subview(), - ]), + this.subview(), ]) ) @@ -240,11 +227,7 @@ AccountDetailScreen.prototype.subview = function () { AccountDetailScreen.prototype.tabSections = function () { const { currentAccountTab } = this.props - return h('section.tabSection', { - style: { - height: '100%', - }, - }, [ + return h('section.tabSection.full-flex-height.grow-tenx', [ h(TabBar, { tabs: [ diff --git a/ui/app/app.js b/ui/app/app.js index 973cb756c..6da48b9b6 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -3,7 +3,6 @@ const Component = require('react').Component const connect = require('react-redux').connect const h = require('react-hyperscript') const actions = require('./actions') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') // init const InitializeMenuScreen = require('./first-time/init-menu') const NewKeyChainScreen = require('./new-keychain') @@ -67,17 +66,15 @@ App.prototype.render = function () { const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config' const loadMessage = loadingMessage || isLoadingNetwork ? `Connecting to ${this.getNetworkName()}` : null - log.debug('Main ui render function') return ( - h('.flex-column.flex-grow.full-height', { + h('.flex-column.full-height', { style: { // Windows was showing a vertical scroll bar: overflow: 'hidden', position: 'relative', - height: '100%', alignItems: 'center', }, }, [ @@ -93,20 +90,12 @@ App.prototype.render = function () { }), // panel content - h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), { + h('.app-primary' + (transForward ? '.from-right' : '.from-left'), { style: { - height: '100%', maxWidth: '850px', }, }, [ - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.renderPrimary(), - ]), + this.renderPrimary(), ]), ]) ) @@ -123,10 +112,8 @@ App.prototype.renderAppBar = function () { return ( - h('div', { - style: { - width: '100%', - }, + h('.full-width', { + height: '38px', }, [ h('.app-header.flex-row.flex-space-between', { @@ -328,11 +315,6 @@ App.prototype.renderDropdown = function () { onClick: () => { this.props.dispatch(actions.showConfigPage()) }, }, 'Settings'), - h(DropdownMenuItem, { - closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), - onClick: () => { this.props.dispatch(actions.showImportPage()) }, - }, 'Import Account'), - h(DropdownMenuItem, { closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), onClick: () => { this.props.dispatch(actions.lockMetamask()) }, @@ -515,6 +497,8 @@ App.prototype.toggleMetamaskActive = function () { App.prototype.renderCustomOption = function (provider) { const { rpcTarget, type } = provider + const props = this.props + if (type !== 'rpc') return null // Concatenate long URLs @@ -533,6 +517,7 @@ App.prototype.renderCustomOption = function (provider) { DropdownMenuItem, { key: rpcTarget, + onClick: () => props.dispatch(actions.setCustomRpc(rpcTarget)), closeMenu: () => this.setState({ isNetworkMenuOpen: false }), }, [ diff --git a/ui/app/components/loading.js b/ui/app/components/loading.js index 87d6f5d20..92d17ccd6 100644 --- a/ui/app/components/loading.js +++ b/ui/app/components/loading.js @@ -1,7 +1,6 @@ const inherits = require('util').inherits const Component = require('react').Component const h = require('react-hyperscript') -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') inherits(LoadingIndicator, Component) @@ -15,35 +14,27 @@ LoadingIndicator.prototype.render = function () { const { isLoading, loadingMessage } = this.props return ( - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'loader', - transitionEnterTimeout: 150, - transitionLeaveTimeout: 150, + isLoading ? h('div', { + style: { + zIndex: 10, + position: 'absolute', + flexDirection: 'column', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '100%', + width: '100%', + background: 'rgba(255, 255, 255, 0.8)', + }, }, [ + h('img', { + src: 'images/loading.svg', + }), - isLoading ? h('div', { - style: { - zIndex: 10, - position: 'absolute', - flexDirection: 'column', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - height: '100%', - width: '100%', - background: 'rgba(255, 255, 255, 0.8)', - }, - }, [ - h('img', { - src: 'images/loading.svg', - }), - - h('br'), - - showMessageIfAny(loadingMessage), - ]) : null, - ]) + h('br'), + + showMessageIfAny(loadingMessage), + ]) : null ) } diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js index e0a720426..76a265d63 100644 --- a/ui/app/components/shapeshift-form.js +++ b/ui/app/components/shapeshift-form.js @@ -2,7 +2,6 @@ const PersistentForm = require('../../lib/persistent-form') const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') const actions = require('../actions') const Qr = require('./qr-code') const isValidAddress = require('../util').isValidAddress @@ -24,14 +23,7 @@ function ShapeshiftForm () { } ShapeshiftForm.prototype.render = function () { - return h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - this.props.qrRequested ? h(Qr, {key: 'qr'}) : this.renderMain(), - ]) + return this.props.qrRequested ? h(Qr, {key: 'qr'}) : this.renderMain() } ShapeshiftForm.prototype.renderMain = function () { diff --git a/ui/app/components/tab-bar.js b/ui/app/components/tab-bar.js index 6295e7dd9..bef444a48 100644 --- a/ui/app/components/tab-bar.js +++ b/ui/app/components/tab-bar.js @@ -21,6 +21,7 @@ TabBar.prototype.render = function () { background: '#EBEBEB', color: '#AEAEAE', paddingTop: '4px', + minHeight: '30px', }, }, tabs.map((tab) => { const { key, content } = tab diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index 20cfa897e..79ec3f351 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -47,10 +47,11 @@ TokenList.prototype.render = function () { return h(TokenCell, tokenData) }) - return h('div', [ - h('ol', { + return h('.full-flex-height', [ + this.renderTokenStatusBar(), + + h('ol.full-flex-height.flex-column', { style: { - height: '260px', overflowY: 'auto', display: 'flex', flexDirection: 'column', @@ -63,6 +64,7 @@ TokenList.prototype.render = function () { flex-direction: row; align-items: center; padding: 10px; + min-height: 50px; } li.token-cell > h3 { @@ -76,17 +78,35 @@ TokenList.prototype.render = function () { `), ...tokenViews, - tokenViews.length ? null : this.message('No Tokens Found.'), + h('.flex-grow'), ]), - this.addTokenButtonElement(), ]) } -TokenList.prototype.addTokenButtonElement = function () { - return h('div', [ - h('div.footer.hover-white.pointer', { +TokenList.prototype.renderTokenStatusBar = function () { + const { tokens } = this.state + + let msg + if (tokens.length > 0) { + msg = `You own ${tokens.length} tokens` + } else { + msg = `No tokens found` + } + + return h('div', { + style: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + minHeight: '70px', + padding: '10px', + }, + }, [ + h('span', msg), + h('button', { key: 'reveal-account-bar', - onClick: () => { + onClick: (event) => { + event.preventDefault() this.props.addToken() }, style: { @@ -97,7 +117,7 @@ TokenList.prototype.addTokenButtonElement = function () { alignItems: 'center', }, }, [ - h('i.fa.fa-plus.fa-lg'), + 'ADD TOKEN', ]), ]) } diff --git a/ui/app/components/transaction-list.js b/ui/app/components/transaction-list.js index ae6aaec8c..192931486 100644 --- a/ui/app/components/transaction-list.js +++ b/ui/app/components/transaction-list.js @@ -24,9 +24,9 @@ TransactionList.prototype.render = function () { return ( - h('section.transaction-list', { + h('section.transaction-list.full-flex-height', { style: { - height: '100%', + justifyContent: 'center', }, }, [ @@ -68,13 +68,15 @@ TransactionList.prototype.render = function () { }, }) }) - : h('.flex-center', { + : h('.flex-center.full-flex-height', { style: { flexDirection: 'column', - height: '100%', + justifyContent: 'center', }, }, [ - 'No transaction history.', + h('p', { + marginTop: '50px', + }, 'No transaction history.'), ]), ]), ]) diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 747d3ce2b..34727ff78 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -1,6 +1,5 @@ const inherits = require('util').inherits const Component = require('react').Component -const ReactCSSTransitionGroup = require('react-addons-css-transition-group') const h = require('react-hyperscript') const connect = require('react-redux').connect const actions = require('./actions') @@ -92,34 +91,25 @@ ConfirmTxScreen.prototype.render = function () { warningIfExists(props.warning), - h(ReactCSSTransitionGroup, { - className: 'css-transition-group', - transitionName: 'main', - transitionEnterTimeout: 300, - transitionLeaveTimeout: 300, - }, [ - - currentTxView({ - // Properties - txData: txData, - key: txData.id, - selectedAddress: props.selectedAddress, - accounts: props.accounts, - identities: props.identities, - conversionRate, - currentCurrency, - blockGasLimit, - // Actions - buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), - sendTransaction: this.sendTransaction.bind(this), - cancelTransaction: this.cancelTransaction.bind(this, txData), - signMessage: this.signMessage.bind(this, txData), - signPersonalMessage: this.signPersonalMessage.bind(this, txData), - cancelMessage: this.cancelMessage.bind(this, txData), - cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), - }), - - ]), + currentTxView({ + // Properties + txData: txData, + key: txData.id, + selectedAddress: props.selectedAddress, + accounts: props.accounts, + identities: props.identities, + conversionRate, + currentCurrency, + blockGasLimit, + // Actions + buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), + sendTransaction: this.sendTransaction.bind(this), + cancelTransaction: this.cancelTransaction.bind(this, txData), + signMessage: this.signMessage.bind(this, txData), + signPersonalMessage: this.signPersonalMessage.bind(this, txData), + cancelMessage: this.cancelMessage.bind(this, txData), + cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), + }), ]) ) } diff --git a/ui/app/css/index.css b/ui/app/css/index.css index 2ae92bbd6..00d4bea93 100644 --- a/ui/app/css/index.css +++ b/ui/app/css/index.css @@ -25,19 +25,48 @@ html, body { padding: 0; } -.css-transition-group { - flex: 1; - height: 100%; +html { + min-height: 400px; +} + +.app-root { + overflow: hidden; + position: relative +} + +.app-primary { + display: flex; } input:focus, textarea:focus { outline: none; } +.full-size { + height: 100%; + width: 100%; +} + +.full-width { + width: 100%; +} + +.full-height { + height: 100%; +} + +.full-flex-height { + display: flex; + flex: 1 1 auto; + flex-direction: column; +} + #app-content { overflow-x: hidden; min-width: 357px; height: 100%; + display: flex; + flex-direction: column; } button, input[type="submit"] { @@ -138,10 +167,6 @@ h2.page-subtitle { margin: 12px; } -.app-primary { - -} - .app-footer { padding-bottom: 10px; align-items: center; @@ -413,7 +438,14 @@ input.large-input { .account-detail-section { display: flex; flex-wrap: wrap; + overflow-y: auto; + flex-direction: inherit; } + +.grow-tenx { + flex-grow: 10; +} + .name-label{ } diff --git a/ui/app/css/lib.css b/ui/app/css/lib.css index b0ca958a2..98570859a 100644 --- a/ui/app/css/lib.css +++ b/ui/app/css/lib.css @@ -270,3 +270,4 @@ hr.horizontal-line { margin-top: 20px; color: red; } + -- cgit v1.2.3 From f16802e2d4f7e917e894e3ec38a716255f6b0942 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 26 Jul 2017 10:15:35 -0700 Subject: nonce-tracker - validation - add validation failing value to error message --- app/scripts/lib/nonce-tracker.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js index b76dac4e8..4bba1f1a8 100644 --- a/app/scripts/lib/nonce-tracker.js +++ b/app/scripts/lib/nonce-tracker.js @@ -31,12 +31,12 @@ class NonceTracker { const currentBlock = await this._getCurrentBlock() const pendingTransactions = this.getPendingTransactions(address) const pendingCount = pendingTransactions.length - assert(Number.isInteger(pendingCount), 'nonce-tracker - pendingCount is an integer') + assert(Number.isInteger(pendingCount), `nonce-tracker - pendingCount is not an integer - got: "${pendingCount}"`) const baseCountHex = await this._getTxCount(address, currentBlock) const baseCount = parseInt(baseCountHex, 16) - assert(Number.isInteger(baseCount), 'nonce-tracker - baseCount is an integer') + assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: "${baseCount}"`) const nextNonce = baseCount + pendingCount - assert(Number.isInteger(nextNonce), 'nonce-tracker - nextNonce is an integer') + assert(Number.isInteger(nextNonce), `nonce-tracker - nextNonce is not an integer - got: "${nextNonce}"`) // return next nonce and release cb return { nextNonce, releaseLock } } -- cgit v1.2.3 From 39d28922de31fa26b50eca5c7719ae9feefae770 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 26 Jul 2017 10:16:08 -0700 Subject: nonce-tracker - validation - add validation failing value type to error message --- app/scripts/lib/nonce-tracker.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js index 4bba1f1a8..81b500550 100644 --- a/app/scripts/lib/nonce-tracker.js +++ b/app/scripts/lib/nonce-tracker.js @@ -31,12 +31,12 @@ class NonceTracker { const currentBlock = await this._getCurrentBlock() const pendingTransactions = this.getPendingTransactions(address) const pendingCount = pendingTransactions.length - assert(Number.isInteger(pendingCount), `nonce-tracker - pendingCount is not an integer - got: "${pendingCount}"`) + assert(Number.isInteger(pendingCount), `nonce-tracker - pendingCount is not an integer - got: (${typeof pendingCount}) "${pendingCount}"`) const baseCountHex = await this._getTxCount(address, currentBlock) const baseCount = parseInt(baseCountHex, 16) - assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: "${baseCount}"`) + assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`) const nextNonce = baseCount + pendingCount - assert(Number.isInteger(nextNonce), `nonce-tracker - nextNonce is not an integer - got: "${nextNonce}"`) + assert(Number.isInteger(nextNonce), `nonce-tracker - nextNonce is not an integer - got: (${typeof nextNonce}) "${nextNonce}"`) // return next nonce and release cb return { nextNonce, releaseLock } } -- cgit v1.2.3 From 0ef90fb1f0f1a1bf4a7efd90df7b8f8c66fc07d5 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 26 Jul 2017 10:40:08 -0700 Subject: tx controller + nonce tracker - record nonce components on txMeta --- app/scripts/controllers/transactions.js | 4 ++++ app/scripts/lib/nonce-tracker.js | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 7b2e4e314..32795a9f2 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -200,8 +200,12 @@ module.exports = class TransactionController extends EventEmitter { // get next nonce const txMeta = this.getTx(txId) const fromAddress = txMeta.txParams.from + // wait for a nonce nonceLock = await this.nonceTracker.getNonceLock(fromAddress) + // add nonce to txParams txMeta.txParams.nonce = nonceLock.nextNonce + // add nonce debugging information to txMeta + txMeta.nonceDetails = nonceLock.nonceDetails this.updateTx(txMeta) // sign transaction const rawTx = await this.signTransaction(txId) diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js index 81b500550..c0746bd87 100644 --- a/app/scripts/lib/nonce-tracker.js +++ b/app/scripts/lib/nonce-tracker.js @@ -37,8 +37,11 @@ class NonceTracker { assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`) const nextNonce = baseCount + pendingCount assert(Number.isInteger(nextNonce), `nonce-tracker - nextNonce is not an integer - got: (${typeof nextNonce}) "${nextNonce}"`) - // return next nonce and release cb - return { nextNonce, releaseLock } + // collect the numbers used to calculate the nonce for debugging + const blockNumber = currentBlock.number + const nonceDetails = { blockNumber, baseCount, pendingCount } + // return nonce and release cb + return { nextNonce, nonceDetails, releaseLock } } async _getCurrentBlock () { -- cgit v1.2.3 From 9d69951decc0cfb0e3dc8bd40bd98f4d1c524dc1 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 26 Jul 2017 10:50:06 -0700 Subject: logState - dont redundantly log to console --- ui/app/reducers.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/app/reducers.js b/ui/app/reducers.js index 11efca529..36045772f 100644 --- a/ui/app/reducers.js +++ b/ui/app/reducers.js @@ -43,7 +43,6 @@ function rootReducer (state, action) { window.logState = function () { var stateString = JSON.stringify(window.METAMASK_CACHED_LOG_STATE, removeSeedWords, 2) - console.log(stateString) return stateString } -- cgit v1.2.3 From 7e2e4948a6ce5856338406de49cbad6a9931d72b Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 26 Jul 2017 10:57:47 -0700 Subject: tx cont - dont recursively store history --- app/scripts/controllers/transactions.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index d6b2b555e..8f53ffa8c 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -119,14 +119,20 @@ module.exports = class TransactionController extends EventEmitter { // updateTx (txMeta) { + // create txMeta snapshot for history const txMetaForHistory = clone(txMeta) + // dont include previous history in this snapshot + delete txMetaForHistory.history + // add stack to help understand why tx was updated txMetaForHistory.stack = getStack() - var txId = txMeta.id - var txList = this.getFullTxList() - var index = txList.findIndex(txData => txData.id === txId) + // add snapshot to tx history if (!txMeta.history) txMeta.history = [] txMeta.history.push(txMetaForHistory) + // update the tx + var txId = txMeta.id + var txList = this.getFullTxList() + var index = txList.findIndex(txData => txData.id === txId) txList[index] = txMeta this._saveTxList(txList) this.emit('update') -- cgit v1.2.3 From b15a2baaf3cf7b4850c427857e935b238d1e5cc2 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 26 Jul 2017 11:09:02 -0700 Subject: nonce-tracker - add raw baseNonceHex to nonceDetails --- app/scripts/lib/nonce-tracker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js index c0746bd87..e33073ac1 100644 --- a/app/scripts/lib/nonce-tracker.js +++ b/app/scripts/lib/nonce-tracker.js @@ -39,7 +39,7 @@ class NonceTracker { assert(Number.isInteger(nextNonce), `nonce-tracker - nextNonce is not an integer - got: (${typeof nextNonce}) "${nextNonce}"`) // collect the numbers used to calculate the nonce for debugging const blockNumber = currentBlock.number - const nonceDetails = { blockNumber, baseCount, pendingCount } + const nonceDetails = { blockNumber, baseCount, baseCountHex, pendingCount } // return nonce and release cb return { nextNonce, nonceDetails, releaseLock } } -- cgit v1.2.3 From 35a128db1e6ecba9076ec145c9d2334f623703b7 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 26 Jul 2017 11:37:00 -0700 Subject: nonce-tracker - hotfix for provider proxying --- app/scripts/controllers/transactions.js | 1 - app/scripts/lib/nonce-tracker.js | 15 +++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 8f53ffa8c..f71659042 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -24,7 +24,6 @@ module.exports = class TransactionController extends EventEmitter { this.blockTracker = opts.blockTracker this.nonceTracker = new NonceTracker({ provider: this.provider, - blockTracker: this.provider._blockTracker, getPendingTransactions: (address) => { return this.getFilteredTxList({ from: address, diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js index e33073ac1..8328e81ec 100644 --- a/app/scripts/lib/nonce-tracker.js +++ b/app/scripts/lib/nonce-tracker.js @@ -4,8 +4,8 @@ const Mutex = require('await-semaphore').Mutex class NonceTracker { - constructor ({ blockTracker, provider, getPendingTransactions }) { - this.blockTracker = blockTracker + constructor ({ provider, getPendingTransactions }) { + this.provider = provider this.ethQuery = new EthQuery(provider) this.getPendingTransactions = getPendingTransactions this.lockMap = {} @@ -45,10 +45,11 @@ class NonceTracker { } async _getCurrentBlock () { - const currentBlock = this.blockTracker.getCurrentBlock() + const blockTracker = this._getBlockTracker() + const currentBlock = blockTracker.getCurrentBlock() if (currentBlock) return currentBlock return await Promise((reject, resolve) => { - this.blockTracker.once('latest', resolve) + blockTracker.once('latest', resolve) }) } @@ -82,6 +83,12 @@ class NonceTracker { return mutex } + // this is a hotfix for the fact that the blockTracker will + // change when the network changes + _getBlockTracker () { + return this.provider._blockTracker + } + } module.exports = NonceTracker -- cgit v1.2.3 From 55a55141d08f7827cb23f4213a6d88351803fbff Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 26 Jul 2017 11:43:37 -0700 Subject: nonce-tracker - fix test --- test/unit/nonce-tracker-test.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/unit/nonce-tracker-test.js b/test/unit/nonce-tracker-test.js index 16cd6d008..b0283e159 100644 --- a/test/unit/nonce-tracker-test.js +++ b/test/unit/nonce-tracker-test.js @@ -18,11 +18,13 @@ describe('Nonce Tracker', function () { getPendingTransactions = () => pendingTxs - provider = { sendAsync: (_, cb) => { cb(undefined, {result: '0x0'}) } } - nonceTracker = new NonceTracker({ - blockTracker: { + provider = { + sendAsync: (_, cb) => { cb(undefined, {result: '0x0'}) }, + _blockTracker: { getCurrentBlock: () => '0x11b568', }, + } + nonceTracker = new NonceTracker({ provider, getPendingTransactions, }) -- cgit v1.2.3 From 3d8ebf2265d167923f3b913bac3b9cc4d37fa052 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 26 Jul 2017 12:10:42 -0700 Subject: Begin implementing live-updating blacklist, not working yet --- app/scripts/background.js | 27 +++++++++++++++++++++++++++ app/scripts/blacklister.js | 19 +++++++++++-------- app/scripts/controllers/infura.js | 14 ++++++++++++++ 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index e8987394f..c9505b237 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -90,6 +90,10 @@ function setupController (initState) { extension.runtime.onConnect.addListener(connectRemote) function connectRemote (remotePort) { + if (remotePort.name === 'blacklister') { + return setupBlacklist(connectRemote) + } + var isMetaMaskInternalProcess = remotePort.name === 'popup' || remotePort.name === 'notification' var portStream = new PortStream(remotePort) if (isMetaMaskInternalProcess) { @@ -135,6 +139,29 @@ function setupController (initState) { return Promise.resolve() } +// Listen for new pages and return if blacklisted: +function setupBlacklist (port) { + console.log('Blacklist connection established') + const handler = handleNewPageLoad.bind(port) + port.onMessage.addListener(handler) + setTimeout(() => { + port.onMessage.removeListener(handler) + }, 30000) +} + +function handleNewPageLoad (message) { + const { pageLoaded } = message + console.log('blaclist message received', message.pageLoaded) + if (!pageLoaded || !global.metamaskController) return + + const state = global.metamaskController.getState() + const { blacklist } = state.metamask + + if (blacklist && blacklist.includes(pageLoaded)) { + this.postMessage({ 'blacklist': pageLoaded }) + } +} + // // Etc... // diff --git a/app/scripts/blacklister.js b/app/scripts/blacklister.js index a45265a75..f5572c11a 100644 --- a/app/scripts/blacklister.js +++ b/app/scripts/blacklister.js @@ -1,13 +1,16 @@ -const blacklistedDomains = require('etheraddresslookup/blacklists/domains.json') +const extension = require('extensionizer') +console.log('blacklister content script loaded.') -function detectBlacklistedDomain() { - var strCurrentTab = window.location.hostname - if (blacklistedDomains && blacklistedDomains.includes(strCurrentTab)) { +const port = extension.runtime.connect({ name: 'blacklister' }) +port.postMessage({ 'pageLoaded': window.location.hostname }) +port.onMessage.addListener(redirectIfBlacklisted) + +function redirectIfBlacklisted (response) { + const { blacklist } = response + console.log('blacklister contentscript received blacklist response') + const host = window.location.hostname + if (blacklist && blacklist === host) { window.location.href = 'https://metamask.io/phishing.html' } } -window.addEventListener('load', function() { - detectBlacklistedDomain() -}) - diff --git a/app/scripts/controllers/infura.js b/app/scripts/controllers/infura.js index b34b0bc03..97b2ab7e3 100644 --- a/app/scripts/controllers/infura.js +++ b/app/scripts/controllers/infura.js @@ -1,5 +1,6 @@ const ObservableStore = require('obs-store') const extend = require('xtend') +const recentBlacklist = require('etheraddresslookup/blacklists/domains.json') // every ten minutes const POLLING_INTERVAL = 300000 @@ -9,6 +10,7 @@ class InfuraController { constructor (opts = {}) { const initState = extend({ infuraNetworkStatus: {}, + blacklist: recentBlacklist, }, opts.initState) this.store = new ObservableStore(initState) } @@ -30,12 +32,24 @@ class InfuraController { }) } + updateLocalBlacklist () { + return fetch('https://api.infura.io/v1/blacklist') + .then(response => response.json()) + .then((parsedResponse) => { + this.store.updateState({ + blacklist: parsedResponse, + }) + return parsedResponse + }) + } + scheduleInfuraNetworkCheck () { if (this.conversionInterval) { clearInterval(this.conversionInterval) } this.conversionInterval = setInterval(() => { this.checkInfuraNetworkStatus() + this.updateLocalBlacklist() }, POLLING_INTERVAL) } } -- cgit v1.2.3 From 45737532333e01b7f01d4631a6ff8880d8c03e87 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 26 Jul 2017 12:29:56 -0700 Subject: Bump version of menu-droppo, no longer needs css transitions --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c1d83107b..2af3e9ec2 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "inject-css": "^0.1.1", "jazzicon": "^1.2.0", "loglevel": "^1.4.1", - "menu-droppo": "1.1.6", + "menu-droppo": "2.0.0", "metamask-logo": "^2.1.2", "mississippi": "^1.2.0", "mkdirp": "^0.5.1", -- cgit v1.2.3 From 0fdbb8096259d622ff92f4ebb0b83a135c2f705c Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 26 Jul 2017 12:31:08 -0700 Subject: Remove Account Settings item from optionsMenu, unnecessary --- ui/app/components/account-dropdowns.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index d1d319477..ec223ea29 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -116,14 +116,6 @@ class AccountDropdowns extends Component { onClickOutside: () => { this.setState({ optionsMenuActive: false }) }, }, [ - h( - DropdownMenuItem, - { - closeMenu: () => {}, - onClick: () => actions.showConfigPage(), - }, - 'Account Settings', - ), h( DropdownMenuItem, { -- cgit v1.2.3 From 4c0f827946dfb6ea115d628480a332d6f732ca20 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 26 Jul 2017 12:43:18 -0700 Subject: Make account selection dropdown menu scrollable when too large for view --- ui/app/components/account-dropdowns.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index ec223ea29..61f32f713 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -59,6 +59,8 @@ class AccountDropdowns extends Component { style: { marginLeft: '-125px', minWidth: '180px', + overflowY: 'auto', + maxHeight: '300px', }, isOpen: accountSelectorActive, onClickOutside: () => { this.setState({ accountSelectorActive: false }) }, -- cgit v1.2.3 From 8006d798ee8d1993ef4b06cce25480f0aea6c4f4 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 26 Jul 2017 13:02:08 -0700 Subject: Re-center network dropdown --- ui/app/app.js | 4 +++- ui/app/components/dropdown.js | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index 6da48b9b6..4ecc6d6d5 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -206,7 +206,9 @@ App.prototype.renderNetworkDropdown = function () { left: '2px', top: '36px', }, - innerStyle: {}, + innerStyle: { + padding: '2px 16px 2px 0px', + }, }, [ h( diff --git a/ui/app/components/dropdown.js b/ui/app/components/dropdown.js index e77b4c40c..70ed388f4 100644 --- a/ui/app/components/dropdown.js +++ b/ui/app/components/dropdown.js @@ -7,7 +7,7 @@ const noop = () => {} class Dropdown extends Component { render () { - const { isOpen, onClickOutside, style, children } = this.props + const { isOpen, onClickOutside, style, innerStyle, children } = this.props return h( MenuDroppo, @@ -21,6 +21,7 @@ class Dropdown extends Component { padding: '8px 16px', background: 'rgba(0, 0, 0, 0.8)', boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + ...innerStyle, }, }, [ -- cgit v1.2.3 From b50c10f373c30642e083fb87724fc5db2ffff1e9 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 26 Jul 2017 14:15:24 -0700 Subject: Version 3.9.2 --- CHANGELOG.md | 3 +++ app/manifest.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eeeda9d68..c5f727586 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Current Master +## 3.9.2 2017-7-26 + +- Fix bugs that could sometimes result in failed transactions after switching networks. - Include stack traces in txMeta's to better understand the life cycle of transactions ## 3.9.1 2017-7-19 diff --git a/app/manifest.json b/app/manifest.json index eadd99590..55e1eb5b1 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.9.1", + "version": "3.9.2", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From 8fc0a025f62722e9cced11e22600e0093e64e4f5 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 26 Jul 2017 14:36:22 -0700 Subject: Set initial scale for mobile. --- app/popup.html | 3 ++- ui/app/components/transaction-list.js | 4 +++- ui/app/css/index.css | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/popup.html b/app/popup.html index 6d85a9811..2504a2512 100644 --- a/app/popup.html +++ b/app/popup.html @@ -2,10 +2,11 @@ + MetaMask Plugin
- \ No newline at end of file + diff --git a/ui/app/components/transaction-list.js b/ui/app/components/transaction-list.js index 192931486..69b72614c 100644 --- a/ui/app/components/transaction-list.js +++ b/ui/app/components/transaction-list.js @@ -75,7 +75,9 @@ TransactionList.prototype.render = function () { }, }, [ h('p', { - marginTop: '50px', + style: { + marginTop: '50px', + }, }, 'No transaction history.'), ]), ]), diff --git a/ui/app/css/index.css b/ui/app/css/index.css index 00d4bea93..05bdb33af 100644 --- a/ui/app/css/index.css +++ b/ui/app/css/index.css @@ -26,7 +26,7 @@ html, body { } html { - min-height: 400px; + min-height: 500px; } .app-root { -- cgit v1.2.3 From 8e2da52e641ac625bf09efbec804e65e0fc0c753 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 26 Jul 2017 14:37:26 -0700 Subject: Adjust mobile scale for smaller devices --- app/popup.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/popup.html b/app/popup.html index 2504a2512..a8f213d53 100644 --- a/app/popup.html +++ b/app/popup.html @@ -2,7 +2,7 @@ - + MetaMask Plugin -- cgit v1.2.3 From bc65484e1bd15fb6ccc9b94f23c991a170b8d39c Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 26 Jul 2017 15:10:50 -0700 Subject: Remove object spread syntax --- ui/app/components/dropdown.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/ui/app/components/dropdown.js b/ui/app/components/dropdown.js index 70ed388f4..759800fd6 100644 --- a/ui/app/components/dropdown.js +++ b/ui/app/components/dropdown.js @@ -2,6 +2,7 @@ const Component = require('react').Component const PropTypes = require('react').PropTypes const h = require('react-hyperscript') const MenuDroppo = require('menu-droppo') +const extend = require('xtend') const noop = () => {} @@ -9,6 +10,13 @@ class Dropdown extends Component { render () { const { isOpen, onClickOutside, style, innerStyle, children } = this.props + const innerStyleDefaults = extend({ + borderRadius: '4px', + padding: '8px 16px', + background: 'rgba(0, 0, 0, 0.8)', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + }, innerStyle) + return h( MenuDroppo, { @@ -16,13 +24,7 @@ class Dropdown extends Component { zIndex: 11, onClickOutside, style, - innerStyle: { - borderRadius: '4px', - padding: '8px 16px', - background: 'rgba(0, 0, 0, 0.8)', - boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', - ...innerStyle, - }, + innerStyle: innerStyleDefaults, }, [ h( -- cgit v1.2.3 From 6a9d40c558564763ee06deb4861238b1b06e1f00 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 26 Jul 2017 15:24:57 -0700 Subject: Add test for blacklister. --- test/unit/blacklister-test.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 test/unit/blacklister-test.js diff --git a/test/unit/blacklister-test.js b/test/unit/blacklister-test.js new file mode 100644 index 000000000..d9290795c --- /dev/null +++ b/test/unit/blacklister-test.js @@ -0,0 +1,24 @@ +const assert = require('assert') +const Blacklister = require('../../app/scripts/blacklister') + + +describe('blacklister', function () { + describe('#isPhish', function () { + it('should not flag whitelisted values', function () { + var result = Blacklister('www.metamask.io') + assert(!result) + }) + it('should flag explicit values', function () { + var result = Blacklister('metamask.com') + assert(result) + }) + it('should flag levenshtein values', function () { + var result = Blacklister('metmask.io') + assert(result) + }) + it('should not flag not-even-close values', function () { + var result = Blacklister('example.com') + assert(!result) + }) + }) +}) -- cgit v1.2.3 From 66f6d5a4e06c6938ae22bd2cb4696f6ade900df2 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 26 Jul 2017 15:25:30 -0700 Subject: Add levenshtein logic to blacklister. --- app/scripts/blacklister.js | 40 ++++++++++++++++++++++++++++++++-------- package.json | 1 + 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/app/scripts/blacklister.js b/app/scripts/blacklister.js index a45265a75..f4b95a31f 100644 --- a/app/scripts/blacklister.js +++ b/app/scripts/blacklister.js @@ -1,13 +1,37 @@ -const blacklistedDomains = require('etheraddresslookup/blacklists/domains.json') +const levenshtein = require('fast-levenshtein') +const blacklistedMetaMaskDomains = ['metamask.com'] +const blacklistedDomains = require('etheraddresslookup/blacklists/domains.json').concat(blacklistedMetaMaskDomains) +const whitelistedMetaMaskDomains = ['metamask.io', 'www.metamask.io'] +const whitelistedDomains = require('etheraddresslookup/whitelists/domains.json').concat(whitelistedMetaMaskDomains) +const LEVENSHTEIN_TOLERANCE = 4 +const LEVENSHTEIN_CHECKS = ['myetherwallet', 'myetheroll', 'ledgerwallet', 'metamask'] -function detectBlacklistedDomain() { - var strCurrentTab = window.location.hostname - if (blacklistedDomains && blacklistedDomains.includes(strCurrentTab)) { - window.location.href = 'https://metamask.io/phishing.html' - } +function isPhish(hostname) { + var strCurrentTab = hostname + + // check if the domain is part of the whitelist. + if (whitelistedDomains && whitelistedDomains.includes(strCurrentTab)) { return false } + + // check if the domain is part of the blacklist. + var isBlacklisted = blacklistedDomains && blacklistedDomains.includes(strCurrentTab) + + // check for similar values. + var levenshteinMatched = false + var levenshteinForm = strCurrentTab.replace(/\./g, '') + LEVENSHTEIN_CHECKS.forEach((element) => { + if (levenshtein.get(element, levenshteinForm) < LEVENSHTEIN_TOLERANCE) { + levenshteinMatched = true + } + }) + + return isBlacklisted || levenshteinMatched } -window.addEventListener('load', function() { - detectBlacklistedDomain() +window.addEventListener('load', function () { + var hostnameToCheck = window.location.hostname + if (isPhish(hostnameToCheck)) { + window.location.href = 'https://metamask.io/phishing.html' + } }) +module.exports = isPhish diff --git a/package.json b/package.json index dcd25cda6..10afc8228 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "express": "^4.14.0", "extension-link-enabler": "^1.0.0", "extensionizer": "^1.0.0", + "fast-levenshtein": "^2.0.6", "gulp-eslint": "^2.0.0", "hat": "0.0.3", "idb-global": "^1.0.0", -- cgit v1.2.3 From a6395436653f274b39bbf642a4d502879bd6eb92 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 26 Jul 2017 15:29:13 -0700 Subject: Changelog bump --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eeeda9d68..46de6fd97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Current Master - Include stack traces in txMeta's to better understand the life cycle of transactions +- Enhance blacklister functionality to include levenshtein logic. ## 3.9.1 2017-7-19 -- cgit v1.2.3 From c071591adb7b5190c21769114f3c98fd2a5b5ec8 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 26 Jul 2017 15:30:41 -0700 Subject: Fix custom provider indication --- ui/app/actions.js | 2 +- ui/app/app.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index d99291e46..fca34d624 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -719,7 +719,7 @@ function setDefaultRpcTarget (rpcList) { } function setRpcTarget (newRpc) { - log.debug(`background.setRpcTarget`) + log.debug(`background.setRpcTarget: ${newRpc}`) return (dispatch) => { background.setCustomRpc(newRpc, (err, result) => { if (err) { diff --git a/ui/app/app.js b/ui/app/app.js index 4ecc6d6d5..f67335797 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -519,7 +519,7 @@ App.prototype.renderCustomOption = function (provider) { DropdownMenuItem, { key: rpcTarget, - onClick: () => props.dispatch(actions.setCustomRpc(rpcTarget)), + onClick: () => props.dispatch(actions.setRpcTarget(rpcTarget)), closeMenu: () => this.setState({ isNetworkMenuOpen: false }), }, [ @@ -553,24 +553,24 @@ App.prototype.getNetworkName = function () { } App.prototype.renderCommonRpc = function (rpcList, provider) { - const { rpcTarget } = provider const props = this.props + const rpcTarget = provider.rpcTarget return rpcList.map((rpc) => { if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { return null } else { + return h( DropdownMenuItem, { - key: rpc, closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - action: () => props.dispatch(actions.setRpcTarget(rpc)), + onClick: () => props.dispatch(actions.setRpcTarget(rpc)), }, [ h('i.fa.fa-question-circle.fa-lg.menu-icon'), rpc, - h('.check', '✓'), + rpcTarget === rpc ? h('.check', '✓') : null, ] ) } -- cgit v1.2.3 From aa282b4e3a55d090f27e37cacf850aa5298cfe27 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 26 Jul 2017 15:31:16 -0700 Subject: Give credit where it is due --- CHANGELOG.md | 2 +- app/scripts/blacklister.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46de6fd97..bb57870fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## Current Master - Include stack traces in txMeta's to better understand the life cycle of transactions -- Enhance blacklister functionality to include levenshtein logic. +- Enhance blacklister functionality to include levenshtein logic. (credit to @sogoiii and @409H for their help!) ## 3.9.1 2017-7-19 diff --git a/app/scripts/blacklister.js b/app/scripts/blacklister.js index f4b95a31f..9337599cc 100644 --- a/app/scripts/blacklister.js +++ b/app/scripts/blacklister.js @@ -6,6 +6,9 @@ const whitelistedDomains = require('etheraddresslookup/whitelists/domains.json') const LEVENSHTEIN_TOLERANCE = 4 const LEVENSHTEIN_CHECKS = ['myetherwallet', 'myetheroll', 'ledgerwallet', 'metamask'] + +// credit to @sogoiii and @409H for their help! +// Return a boolean on whether or not a phish is detected. function isPhish(hostname) { var strCurrentTab = hostname @@ -30,6 +33,7 @@ function isPhish(hostname) { window.addEventListener('load', function () { var hostnameToCheck = window.location.hostname if (isPhish(hostnameToCheck)) { + // redirect to our phishing warning page. window.location.href = 'https://metamask.io/phishing.html' } }) -- cgit v1.2.3 From 8b1726cc550d4a5b142a2a525ce6b94713dc04e0 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 26 Jul 2017 16:30:54 -0700 Subject: Live update blacklist from Infura --- CHANGELOG.md | 2 ++ app/manifest.json | 9 +++++++-- app/scripts/background.js | 14 ++++++-------- app/scripts/blacklister.js | 4 +--- app/scripts/inpage.js | 1 + 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5f727586..ba8bdd16c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Continuously update blacklist for known phishing sites in background. + ## 3.9.2 2017-7-26 - Fix bugs that could sometimes result in failed transactions after switching networks. diff --git a/app/manifest.json b/app/manifest.json index 55e1eb5b1..edc4d7162 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -55,8 +55,13 @@ }, { "run_at": "document_start", - "matches": ["http://*/*", "https://*/*"], - "js": ["scripts/blacklister.js"] + "matches": [ + "file://*/*", + "http://*/*", + "https://*/*" + ], + "js": ["scripts/blacklister.js"], + "all_frames": true } ], "permissions": [ diff --git a/app/scripts/background.js b/app/scripts/background.js index c9505b237..01bb39186 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -91,7 +91,7 @@ function setupController (initState) { extension.runtime.onConnect.addListener(connectRemote) function connectRemote (remotePort) { if (remotePort.name === 'blacklister') { - return setupBlacklist(connectRemote) + return checkBlacklist(remotePort) } var isMetaMaskInternalProcess = remotePort.name === 'popup' || remotePort.name === 'notification' @@ -140,25 +140,23 @@ function setupController (initState) { } // Listen for new pages and return if blacklisted: -function setupBlacklist (port) { - console.log('Blacklist connection established') - const handler = handleNewPageLoad.bind(port) +function checkBlacklist (port) { + const handler = handleNewPageLoad.bind(null, port) port.onMessage.addListener(handler) setTimeout(() => { port.onMessage.removeListener(handler) }, 30000) } -function handleNewPageLoad (message) { +function handleNewPageLoad (port, message) { const { pageLoaded } = message - console.log('blaclist message received', message.pageLoaded) if (!pageLoaded || !global.metamaskController) return const state = global.metamaskController.getState() - const { blacklist } = state.metamask + const { blacklist } = state if (blacklist && blacklist.includes(pageLoaded)) { - this.postMessage({ 'blacklist': pageLoaded }) + port.postMessage({ 'blacklist': pageLoaded }) } } diff --git a/app/scripts/blacklister.js b/app/scripts/blacklister.js index f5572c11a..37751b595 100644 --- a/app/scripts/blacklister.js +++ b/app/scripts/blacklister.js @@ -1,13 +1,11 @@ const extension = require('extensionizer') -console.log('blacklister content script loaded.') -const port = extension.runtime.connect({ name: 'blacklister' }) +var port = extension.runtime.connect({name: 'blacklister'}) port.postMessage({ 'pageLoaded': window.location.hostname }) port.onMessage.addListener(redirectIfBlacklisted) function redirectIfBlacklisted (response) { const { blacklist } = response - console.log('blacklister contentscript received blacklist response') const host = window.location.hostname if (blacklist && blacklist === host) { window.location.href = 'https://metamask.io/phishing.html' diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index ec764535e..9e98c044b 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -65,3 +65,4 @@ function restoreContextAfterImports () { console.warn('MetaMask - global.define could not be overwritten.') } } + -- cgit v1.2.3 From eb92d65c4dc764f3a0ad1055fef2ed7cabc04a6f Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 26 Jul 2017 16:57:58 -0700 Subject: Add Levenshtein item to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 823b131be..66c95a0c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Current Master - Continuously update blacklist for known phishing sites in background. +- Automatically detect suspicious URLs too similar to common phishing targets, and blacklist them. ## 3.9.2 2017-7-26 -- cgit v1.2.3 From 33432afb0c15b22f925d9ad11ad34aa50f12d101 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 09:14:52 -0700 Subject: Bump menu-droppo to 2.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2af3e9ec2..219653a98 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "inject-css": "^0.1.1", "jazzicon": "^1.2.0", "loglevel": "^1.4.1", - "menu-droppo": "2.0.0", + "menu-droppo": "2.0.1", "metamask-logo": "^2.1.2", "mississippi": "^1.2.0", "mkdirp": "^0.5.1", -- cgit v1.2.3 From 26ce530f6f32637977774fd266e2d183d521c53c Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 09:17:00 -0700 Subject: Remove deprecated responsive dev docs --- README.md | 1 - docs/responsive-ui-dev.md | 11 ----------- 2 files changed, 12 deletions(-) delete mode 100644 docs/responsive-ui-dev.md diff --git a/README.md b/README.md index 2323a710e..d7086ae91 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,6 @@ To write tests that will be run in the browser using QUnit, add your test files - [Publishing Guide](./docs/publishing.md) - [How to develop an in-browser mocked UI](./docs/ui-mock-mode.md) - [How to live reload on local dependency changes](./docs/developing-on-deps.md) -- [How to Edit our New Responsive UI](./docs/responsive-ui-dev.md) - [How to add new networks to the Provider Menu](./docs/adding-new-networks.md) - [How to manage notices that appear when the app starts up](./docs/notices.md) - [How to generate a visualization of this repository's development](./docs/development-visualization.md) diff --git a/docs/responsive-ui-dev.md b/docs/responsive-ui-dev.md deleted file mode 100644 index 280c78020..000000000 --- a/docs/responsive-ui-dev.md +++ /dev/null @@ -1,11 +0,0 @@ -# Developing our Responsive UI - -To allow parallel development of a new responsive version of our interface, we have forked our `ui` folder into two sub-folders: - -- ui/classic (our original extension UI, fixed dimensions) -- ui/responsive (our new, responsive UI) - -To visit this new responsive ui while in development mode (`npm start`) simply visit: - -[chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/home.html](chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/home.html) - -- cgit v1.2.3 From 21bde25780d4b228a273a75fa3e04cfe96b410e6 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 09:19:03 -0700 Subject: Fix spelling --- ui/app/add-token.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/add-token.js b/ui/app/add-token.js index b303b5c0d..15ef7a852 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -86,7 +86,7 @@ AddTokenScreen.prototype.render = function () { h('div', [ h('span', { style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Token Sybmol'), + }, 'Token Symbol'), ]), h('div', { style: {display: 'flex'} }, [ -- cgit v1.2.3 From 1d9fea86546026aa09f6e040a9b0ef551d5659fa Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 13:57:01 -0700 Subject: Fix info page formatting --- ui/app/info.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/ui/app/info.js b/ui/app/info.js index e8470de97..38576b284 100644 --- a/ui/app/info.js +++ b/ui/app/info.js @@ -20,7 +20,11 @@ InfoScreen.prototype.render = function () { const version = global.platform.getVersion() return ( - h('.flex-column.flex-grow', [ + h('.flex-column.flex-grow', { + style: { + maxWidth: '400px', + }, + }, [ // subtitle and nav h('.section-title.flex-row.flex-center', [ @@ -103,6 +107,7 @@ InfoScreen.prototype.render = function () { target: '_blank', }, 'Need Help? Read our FAQ!'), ]), + h('div', [ h('a', { href: 'https://metamask.io/', @@ -120,6 +125,7 @@ InfoScreen.prototype.render = function () { h('div.info', 'Visit our web site'), ]), ]), + h('div.fa.fa-slack', [ h('a.info', { href: 'http://slack.metamask.io', @@ -127,11 +133,13 @@ InfoScreen.prototype.render = function () { }, 'Join the conversation on Slack'), ]), - h('div.fa.fa-twitter', [ - h('a.info', { - href: 'https://twitter.com/metamask_io', - target: '_blank', - }, 'Follow us on Twitter'), + h('div', [ + h('.fa.fa-twitter', [ + h('a.info', { + href: 'https://twitter.com/metamask_io', + target: '_blank', + }, 'Follow us on Twitter'), + ]) ]), h('div.fa.fa-envelope', [ -- cgit v1.2.3 From e73c7b907a111a12bcae3c04da9c3c99931f9e72 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 13:58:17 -0700 Subject: Restore support link --- ui/app/info.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/app/info.js b/ui/app/info.js index 38576b284..899841c83 100644 --- a/ui/app/info.js +++ b/ui/app/info.js @@ -101,11 +101,11 @@ InfoScreen.prototype.render = function () { paddingLeft: '30px', }}, [ - h('div.fa.fa-github', [ + h('div.fa.fa-support', [ h('a.info', { - href: 'https://github.com/MetaMask/faq', + href: 'http://metamask.consensyssupport.happyfox.com', target: '_blank', - }, 'Need Help? Read our FAQ!'), + }, 'Visit our Support Center'), ]), h('div', [ @@ -139,7 +139,7 @@ InfoScreen.prototype.render = function () { href: 'https://twitter.com/metamask_io', target: '_blank', }, 'Follow us on Twitter'), - ]) + ]), ]), h('div.fa.fa-envelope', [ -- cgit v1.2.3 From f884477abbf427ac390e682fd6380c6a826f0745 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 14:07:36 -0700 Subject: Remove unused parameter --- ui/app/actions.js | 2 +- ui/app/app.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index fca34d624..0a9d347aa 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -706,7 +706,7 @@ function markAccountsFound () { // // default rpc target refers to localhost:8545 in this instance. -function setDefaultRpcTarget (rpcList) { +function setDefaultRpcTarget () { log.debug(`background.setDefaultRpcTarget`) return (dispatch) => { background.setDefaultRpc((err, result) => { diff --git a/ui/app/app.js b/ui/app/app.js index f67335797..aadaa6f03 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -267,7 +267,7 @@ App.prototype.renderNetworkDropdown = function () { DropdownMenuItem, { closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), - onClick: () => props.dispatch(actions.setDefaultRpcTarget(rpcList)), + onClick: () => props.dispatch(actions.setDefaultRpcTarget()), }, [ h('i.fa.fa-question-circle.fa-lg.menu-icon'), -- cgit v1.2.3 From 56496f1ea08c1f4f2786e3d1be11026adf0f5900 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 14:13:53 -0700 Subject: Fix loading indication placement --- ui/app/components/loading.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/loading.js b/ui/app/components/loading.js index 92d17ccd6..933321983 100644 --- a/ui/app/components/loading.js +++ b/ui/app/components/loading.js @@ -14,7 +14,7 @@ LoadingIndicator.prototype.render = function () { const { isLoading, loadingMessage } = this.props return ( - isLoading ? h('div', { + isLoading ? h('.full-flex-height', { style: { zIndex: 10, position: 'absolute', -- cgit v1.2.3 From 9a1cf2a0d424c54c760fd18644a2fad7c582959a Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 14:15:56 -0700 Subject: Ensure loading indication is full screen --- ui/app/components/loading.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/app/components/loading.js b/ui/app/components/loading.js index 933321983..163792584 100644 --- a/ui/app/components/loading.js +++ b/ui/app/components/loading.js @@ -16,6 +16,7 @@ LoadingIndicator.prototype.render = function () { return ( isLoading ? h('.full-flex-height', { style: { + left: '0px', zIndex: 10, position: 'absolute', flexDirection: 'column', -- cgit v1.2.3 From 65bd178b647ae4938e240ba5b070d7ea30a179cc Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 14:32:18 -0700 Subject: Fix viewport width to 1 --- app/popup.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/popup.html b/app/popup.html index a8f213d53..b2fcc09d8 100644 --- a/app/popup.html +++ b/app/popup.html @@ -2,7 +2,7 @@ - + MetaMask Plugin -- cgit v1.2.3 From f795f30a67619093e8c0756ca7e3446786fe6e6d Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 14:37:27 -0700 Subject: Disable user zoom in mobile mode --- app/popup.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/popup.html b/app/popup.html index b2fcc09d8..eeca58331 100644 --- a/app/popup.html +++ b/app/popup.html @@ -2,7 +2,7 @@ - + MetaMask Plugin -- cgit v1.2.3 From 8ba32d5ea8cbd30b85cade9fccaaa6c0f3f5cd04 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 14:58:51 -0700 Subject: Set min gas price to 1 gwei --- ui/app/components/pending-tx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index d7d602f31..5324ccd64 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -15,7 +15,7 @@ const addressSummary = util.addressSummary const nameForAddress = require('../../lib/contract-namer') const BNInput = require('./bn-as-decimal-input') -const MIN_GAS_PRICE_GWEI_BN = new BN(2) +const MIN_GAS_PRICE_GWEI_BN = new BN(1) const GWEI_FACTOR = new BN(1e9) const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) const MIN_GAS_LIMIT_BN = new BN(21000) -- cgit v1.2.3 From 7c71ee1babcaad19dbe7db6c5abfefe2f9654781 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 15:16:42 -0700 Subject: Do not blacklist files --- app/manifest.json | 1 - 1 file changed, 1 deletion(-) diff --git a/app/manifest.json b/app/manifest.json index edc4d7162..591a07d0d 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -56,7 +56,6 @@ { "run_at": "document_start", "matches": [ - "file://*/*", "http://*/*", "https://*/*" ], -- cgit v1.2.3 From 6c25f9950b53179ffb34f3baf1f6aa2b5d665720 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 15:25:00 -0700 Subject: Bump changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66c95a0c3..600df2955 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Replace account scren with an account drop-down menu. +- Replace confusing buttons with an new account-specific drop-down menu. - Continuously update blacklist for known phishing sites in background. - Automatically detect suspicious URLs too similar to common phishing targets, and blacklist them. -- cgit v1.2.3 From 87c26eb5bc30941a85de98f885d4eb9a752cf9d6 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 18:07:42 -0700 Subject: Correct token pluralization for one token --- ui/app/components/token-list.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index 79ec3f351..5ea31ae8d 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -87,7 +87,9 @@ TokenList.prototype.renderTokenStatusBar = function () { const { tokens } = this.state let msg - if (tokens.length > 0) { + if (tokens.length === 1) { + msg = `You own 1 token` + } else if (tokens.length === 1) { msg = `You own ${tokens.length} tokens` } else { msg = `No tokens found` -- cgit v1.2.3 From a25c4f34c0063a0f3d3b1989d9303cb75952d443 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 18:32:32 -0700 Subject: Limit max width of seed word conf screen --- ui/app/keychains/hd/recover-seed/confirmation.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ui/app/keychains/hd/recover-seed/confirmation.js b/ui/app/keychains/hd/recover-seed/confirmation.js index 4ccbec9fc..4335186a5 100644 --- a/ui/app/keychains/hd/recover-seed/confirmation.js +++ b/ui/app/keychains/hd/recover-seed/confirmation.js @@ -23,7 +23,9 @@ RevealSeedConfirmation.prototype.render = function () { return ( - h('.initialize-screen.flex-column.flex-center.flex-grow', [ + h('.initialize-screen.flex-column.flex-center.flex-grow', { + style: { maxWidth: '420px' }, + }, [ h('h3.flex-center.text-transform-uppercase', { style: { @@ -61,7 +63,7 @@ RevealSeedConfirmation.prototype.render = function () { }, }), - h('.flex-row.flex-space-between', { + h('.flex-row.flex-start', { style: { marginTop: 30, width: '50%', @@ -74,6 +76,7 @@ RevealSeedConfirmation.prototype.render = function () { // submit h('button.primary', { + style: { marginLeft: '10px' }, onClick: this.revealSeedWords.bind(this), }, 'OK'), -- cgit v1.2.3 From d1828b6dc98c95a284350f0d22bf4b6be08ebacd Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 18:43:09 -0700 Subject: Fix react warning --- ui/app/app.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ui/app/app.js b/ui/app/app.js index aadaa6f03..b251baefd 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -214,6 +214,7 @@ App.prototype.renderNetworkDropdown = function () { h( DropdownMenuItem, { + key: 'main', closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), onClick: () => props.dispatch(actions.setProviderType('mainnet')), }, @@ -227,6 +228,7 @@ App.prototype.renderNetworkDropdown = function () { h( DropdownMenuItem, { + key: 'ropsten', closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), onClick: () => props.dispatch(actions.setProviderType('ropsten')), }, @@ -240,6 +242,7 @@ App.prototype.renderNetworkDropdown = function () { h( DropdownMenuItem, { + key: 'kovan', closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), onClick: () => props.dispatch(actions.setProviderType('kovan')), }, @@ -253,6 +256,7 @@ App.prototype.renderNetworkDropdown = function () { h( DropdownMenuItem, { + key: 'rinkeby', closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), onClick: () => props.dispatch(actions.setProviderType('rinkeby')), }, @@ -266,6 +270,7 @@ App.prototype.renderNetworkDropdown = function () { h( DropdownMenuItem, { + key: 'default', closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), onClick: () => props.dispatch(actions.setDefaultRpcTarget()), }, @@ -564,6 +569,7 @@ App.prototype.renderCommonRpc = function (rpcList, provider) { return h( DropdownMenuItem, { + key: `common${rpc}`, closeMenu: () => this.setState({ isNetworkMenuOpen: false }), onClick: () => props.dispatch(actions.setRpcTarget(rpc)), }, -- cgit v1.2.3 From 9ac0a18f3b345bb8236febe626d3b6f6bed75ae3 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 27 Jul 2017 18:43:18 -0700 Subject: Correct viewport param --- app/popup.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/popup.html b/app/popup.html index eeca58331..cfb4b00a0 100644 --- a/app/popup.html +++ b/app/popup.html @@ -2,7 +2,7 @@ - + MetaMask Plugin -- cgit v1.2.3 From 651fec5112ee77eed995db80621d2ae6e799e8cf Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 28 Jul 2017 11:06:39 -0700 Subject: Create distinct responsive 'home.html' file, hard-code popup.html size Because firefox was having inconsistent sizing, made a second html file for forcing the view to a certain size. Still allows us to develop a responsive interface via the `home.html` file, which shares all the same react JS & CSS as popup.html. --- app/home.html | 12 ++++++++++++ app/popup.html | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 app/home.html diff --git a/app/home.html b/app/home.html new file mode 100644 index 000000000..cfb4b00a0 --- /dev/null +++ b/app/home.html @@ -0,0 +1,12 @@ + + + + + + MetaMask Plugin + + +
+ + + diff --git a/app/popup.html b/app/popup.html index cfb4b00a0..d09b09315 100644 --- a/app/popup.html +++ b/app/popup.html @@ -5,7 +5,7 @@ MetaMask Plugin - +
-- cgit v1.2.3 From 24d375aaf1717f1ae743fbe6e83eb50f0e6a4b95 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Fri, 28 Jul 2017 15:31:03 -0700 Subject: Fix dropdown toggle behavior - account dropdowns --- ui/app/components/account-dropdowns.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index 61f32f713..2813f4752 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -17,6 +17,8 @@ class AccountDropdowns extends Component { accountSelectorActive: false, optionsMenuActive: false, } + this.accountSelectorToggleClassName = 'fa-angle-down'; + this.optionsMenuToggleClassName = 'fa-ellipsis-h'; } renderAccounts () { @@ -63,7 +65,13 @@ class AccountDropdowns extends Component { maxHeight: '300px', }, isOpen: accountSelectorActive, - onClickOutside: () => { this.setState({ accountSelectorActive: false }) }, + onClickOutside: (event) => { + const { classList } = event.target + const isNotToggleElement = !classList.contains(this.accountSelectorToggleClassName) + if (accountSelectorActive && isNotToggleElement) { + this.setState({ accountSelectorActive: false }) + } + }, }, [ ...this.renderAccounts(), @@ -115,7 +123,13 @@ class AccountDropdowns extends Component { minWidth: '180px', }, isOpen: optionsMenuActive, - onClickOutside: () => { this.setState({ optionsMenuActive: false }) }, + onClickOutside: () => { + const { classList } = event.target + const isNotToggleElement = !classList.contains(this.optionsMenuToggleClassName) + if (optionsMenuActive && isNotToggleElement) { + this.setState({ optionsMenuActive: false }) + } + }, }, [ h( -- cgit v1.2.3 From 34834c108dafd1de75f26a09d78feb8949ef5e56 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Fri, 28 Jul 2017 15:40:26 -0700 Subject: Fix dropdown toggle behavior - settings --- ui/app/app.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/app/app.js b/ui/app/app.js index b251baefd..f293e89bd 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -308,7 +308,11 @@ App.prototype.renderDropdown = function () { isOpen: isOpen, zIndex: 11, onClickOutside: (event) => { - this.setState({ isMainMenuOpen: !isOpen }) + const { classList } = event.target + const isNotToggleElement = !classList.contains('sandwich-expando') + if (isNotToggleElement) { + this.setState({ isMainMenuOpen: false }) + } }, style: { position: 'absolute', -- cgit v1.2.3 From 4044b58b5a7133caeefd0f3c0a16478387fe7247 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Fri, 28 Jul 2017 15:55:55 -0700 Subject: Fix dropdown behavior - network --- ui/app/app.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ui/app/app.js b/ui/app/app.js index f293e89bd..8fad0f7d6 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -198,7 +198,17 @@ App.prototype.renderNetworkDropdown = function () { return h(Dropdown, { isOpen, onClickOutside: (event) => { - this.setState({ isNetworkMenuOpen: !isOpen }) + const { classList } = event.target + const isNotToggleElement = [ + classList.contains('menu-icon'), + classList.contains('network-name'), + classList.contains('network-indicator'), + ].filter(bool => bool).length === 0; + // classes from three constituent nodes of the toggle element + + if (isNotToggleElement) { + this.setState({ isNetworkMenuOpen: false }) + } }, zIndex: 11, style: { -- cgit v1.2.3 From 8e1713e1e00be89988824c0666837187f96d073c Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sat, 29 Jul 2017 12:07:46 -0700 Subject: [WIP] Propose more scaleable color management --- ui/app/css/colors.css | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 ui/app/css/colors.css diff --git a/ui/app/css/colors.css b/ui/app/css/colors.css new file mode 100644 index 000000000..ec2287192 --- /dev/null +++ b/ui/app/css/colors.css @@ -0,0 +1,2 @@ +$gallery: #EFEFEF; +$alabaster: #F7F7F7; -- cgit v1.2.3 From 4d7295b05fd3b4189bb2e4c604e53b349d5618a0 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sat, 29 Jul 2017 12:20:19 -0700 Subject: Add for use in main header --- ui/app/css/colors.css | 1 + ui/app/css/index.css | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/app/css/colors.css b/ui/app/css/colors.css index ec2287192..7ac93595f 100644 --- a/ui/app/css/colors.css +++ b/ui/app/css/colors.css @@ -1,2 +1,3 @@ $gallery: #EFEFEF; $alabaster: #F7F7F7; +$shark: #22232C; \ No newline at end of file diff --git a/ui/app/css/index.css b/ui/app/css/index.css index 05bdb33af..3cedb1d8e 100644 --- a/ui/app/css/index.css +++ b/ui/app/css/index.css @@ -151,12 +151,13 @@ button.btn-thin { .app-header { padding: 6px 8px; + // background: #EFEFEF; // $gallery } .app-header h1 { font-family: 'Montserrat Regular'; text-transform: uppercase; - color: #AEAEAE; + color: #22232C; // $shark } h2.page-subtitle { -- cgit v1.2.3 From 8c5be547228c9801ab616beb5054b888509463f9 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sat, 29 Jul 2017 12:21:10 -0700 Subject: Rearrange header components: closer to redesigned UI --- ui/app/app.js | 49 ++++++++++++++++++------------------------------- 1 file changed, 18 insertions(+), 31 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index 8fad0f7d6..6537e2f56 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -82,7 +82,7 @@ App.prototype.render = function () { // app bar this.renderAppBar(), this.renderNetworkDropdown(), - this.renderDropdown(), + // this.renderDropdown(), h(Loading, { isLoading: isLoading || isLoadingNetwork, @@ -120,7 +120,7 @@ App.prototype.renderAppBar = function () { style: { alignItems: 'center', visibility: props.isUnlocked ? 'visible' : 'none', - background: props.isUnlocked ? 'white' : 'none', + background: '#EFEFEF', // $gallery height: '38px', position: 'relative', zIndex: 12, @@ -134,7 +134,6 @@ App.prototype.renderAppBar = function () { alignItems: 'center', }, }, [ - // mini logo h('img', { height: 24, @@ -142,46 +141,34 @@ App.prototype.renderAppBar = function () { src: '/images/icon-128.png', }), - h(NetworkIndicator, { - network: this.props.network, - provider: this.props.provider, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen }) + // metamask name + h('h1', { + style: { + position: 'relative', + left: '9px', }, - }), - ]), + }, 'MetaMask'), - // metamask name - props.isUnlocked && h('h1', { - style: { - position: 'relative', - left: '9px', - }, - }, 'MetaMask'), + ]), - props.isUnlocked && h('div', { + h('div', { style: { display: 'flex', flexDirection: 'row', alignItems: 'center', }, }, [ - - // hamburger - props.isUnlocked && h(SandwichExpando, { - width: 16, - barHeight: 2, - padding: 0, - isOpen: state.isMainMenuOpen, - color: 'rgb(247,146,30)', + // Network Indicator + h(NetworkIndicator, { + network: this.props.network, + provider: this.props.provider, onClick: (event) => { event.preventDefault() event.stopPropagation() - this.setState({ isMainMenuOpen: !state.isMainMenuOpen }) + this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen }) }, }), + ]), ]), ]) @@ -213,8 +200,8 @@ App.prototype.renderNetworkDropdown = function () { zIndex: 11, style: { position: 'absolute', - left: '2px', - top: '36px', + right: '2px', + top: '38px', }, innerStyle: { padding: '2px 16px 2px 0px', -- cgit v1.2.3 From c0483fc230ec1893f15c6f8994f63e318474846e Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sat, 29 Jul 2017 13:31:53 -0700 Subject: Add token logo to send screen --- ui/app/app.js | 10 ++++++++++ ui/app/send.js | 24 ++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index 6537e2f56..2eb037460 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -171,6 +171,16 @@ App.prototype.renderAppBar = function () { ]), ]), + + h('.app-header', { + style: { + visibility: props.isUnlocked ? 'visible' : 'none', + background: '#EFEFEF', // $gallery + height: '38px', + position: 'relative', + zIndex: 12, + }, + }) ]) ) } diff --git a/ui/app/send.js b/ui/app/send.js index a21a219eb..1235da223 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -11,6 +11,9 @@ const isHex = require('./util').isHex const EthBalance = require('./components/eth-balance') const EnsInput = require('./components/ens-input') const ethUtil = require('ethereumjs-util') + +const ARAGON = '960b236A07cf122663c4303350609A66A7B288C0' + module.exports = connect(mapStateToProps)(SendTransactionScreen) function mapStateToProps (state) { @@ -58,16 +61,33 @@ SendTransactionScreen.prototype.render = function () { h('.send-screen.flex-column.flex-grow', [ + h('div', { + style: { + position: 'fixed', + zIndex: 15, // token-icon-z-index + marginTop: '-55px', + marginLeft: '20%', + } + }, [ + h(Identicon, { + address: ARAGON, + diameter: 76, + }), + ]), + // // Sender Profile // - h('.account-data-subsection.flex-row.flex-grow', { style: { margin: '0 20px', + width: '335px', + background: 'white', // $white + marginTop: '-15px', + zIndex: 13, // $send-screen-z-index + display: 'flex', }, }, [ - // header - identicon + nav h('.flex-row.flex-space-between', { style: { -- cgit v1.2.3 From 2a5f2c7f4041006daf5bda4d51117b4fe9544e98 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sat, 29 Jul 2017 17:38:29 -0700 Subject: Add responsive container; add send token copy --- ui/app/components/ens-input.js | 7 +++ ui/app/send.js | 139 ++++++++++++++--------------------------- 2 files changed, 53 insertions(+), 93 deletions(-) diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js index 3a33ebf74..93c07599d 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/ens-input.js @@ -44,6 +44,13 @@ EnsInput.prototype.render = function () { return h('div', { style: { width: '100%' }, }, [ + h('span', { + style: { + textAlign: 'left', + } + }, [ + 'To:' + ]), h('input.large-input', opts), // The address book functionality. h('datalist#addresses', diff --git a/ui/app/send.js b/ui/app/send.js index 1235da223..66ba21e3e 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -59,14 +59,17 @@ SendTransactionScreen.prototype.render = function () { return ( - h('.send-screen.flex-column.flex-grow', [ - - h('div', { + h('.send-screen.flex-column.flex-grow', { + style: { + background: '#FFFFFF', // $background-white + marginLeft: '3.5%', + marginRight: '3.5%', + } + }, [ + h('section.flex-center.flex-row', { style: { - position: 'fixed', - zIndex: 15, // token-icon-z-index - marginTop: '-55px', - marginLeft: '20%', + zIndex: 15, // $token-icon-z-index + marginTop: '-35px', } }, [ h(Identicon, { @@ -76,102 +79,52 @@ SendTransactionScreen.prototype.render = function () { ]), // - // Sender Profile + // Required Fields // - h('.account-data-subsection.flex-row.flex-grow', { + + h('h3.flex-center', { style: { - margin: '0 20px', - width: '335px', - background: 'white', // $white marginTop: '-15px', - zIndex: 13, // $send-screen-z-index - display: 'flex', + fontSize: '20px', }, }, [ - // header - identicon + nav - h('.flex-row.flex-space-between', { - style: { - marginTop: '15px', - }, - }, [ - // back button - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { - onClick: this.back.bind(this), - }), - - // large identicon - h('.identicon-wrapper.flex-column.flex-center.select-none', [ - h(Identicon, { - diameter: 62, - address: address, - }), - ]), - - // invisible place holder - h('i.fa.fa-users.fa-lg.invisible', { - style: { - marginTop: '28px', - }, - }), - - ]), - - // account label - - h('.flex-column', { - style: { - marginTop: '10px', - alignItems: 'flex-start', - }, - }, [ - h('h2.font-medium.color-forest.flex-center', { - style: { - paddingTop: '8px', - marginBottom: '8px', - }, - }, identity && identity.name), - - // address and getter actions - h('.flex-row.flex-center', { - style: { - marginBottom: '8px', - }, - }, [ - - h('div', { - style: { - lineHeight: '16px', - }, - }, addressSummary(address)), - - ]), - - // balance - h('.flex-row.flex-center', [ - - h(EthBalance, { - value: account && account.balance, - conversionRate, - currentCurrency, - }), - - ]), - ]), + 'Send Tokens', ]), - // - // Required Fields - // + h('h3.flex-center', { + style: { + textAlign: 'center', + fontSize: '16px', + }, + }, [ + 'Send Tokens to anyone with an Ethereum account', + ]), - h('h3.flex-center.text-transform-uppercase', { + h('h3.flex-center', { style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginTop: '15px', - marginBottom: '16px', + textAlign: 'center', + fontSize: '16px', + }, + }, [ + 'Your Aragon Token Balance is:', + ]), + + h('h3.flex-center', { + style: { + textAlign: 'center', + fontSize: '43px', + }, + }, [ + '2.34', + ]), + + h('h3.flex-center', { + style: { + textAlign: 'center', + fontSize: '16px', }, }, [ - 'Send Transaction', + 'ANT', ]), // error message @@ -194,7 +147,7 @@ SendTransactionScreen.prototype.render = function () { h('input.large-input', { name: 'amount', - placeholder: 'Amount', + placeholder: '0', type: 'number', style: { marginRight: '6px', -- cgit v1.2.3 From fec3e64d630d17d035df43203bbfbf061930cd61 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sat, 29 Jul 2017 18:07:58 -0700 Subject: Add high-level css layout for Send Token --- ui/app/send.js | 101 ++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 78 insertions(+), 23 deletions(-) diff --git a/ui/app/send.js b/ui/app/send.js index 66ba21e3e..bd4cf4ee1 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -143,7 +143,17 @@ SendTransactionScreen.prototype.render = function () { ]), // 'amount' and send button - h('section.flex-row.flex-center', [ + h('section.flex-column.flex-center', [ + + h('div.flex-row.flex-center', { + style: { + width: '100%', + justifyContent: 'space-between', + } + },[ + h('span', { style: {} }, ['Amount']), + h('span', { style: {} }, ['Token <> USD']), + ]), h('input.large-input', { name: 'amount', @@ -157,43 +167,88 @@ SendTransactionScreen.prototype.render = function () { }, }), - h('button.primary', { - onClick: this.onSubmit.bind(this), + ]), + + h('section.flex-column.flex-center', [ + + h('div.flex-row.flex-center', { style: { - textTransform: 'uppercase', + width: '100%', + justifyContent: 'space-between', + } + },[ + h('span', { style: {} }, ['Gas Fee:']), + h('span', { style: {} }, ['What\'s this?']), + ]), + + h('input.large-input', { + name: 'Gas Fee', + placeholder: '0', + type: 'number', + style: { + marginRight: '6px', }, - }, 'Next'), + // dataset: { + // persistentFormId: 'tx-amount', + // }, + }), ]), // // Optional Fields // - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginTop: '16px', - marginBottom: '16px', - }, - }, [ - 'Transaction Data (optional)', - ]), - // 'data' field h('section.flex-column.flex-center', [ - h('input.large-input', { - name: 'txData', - placeholder: '0x01234', + + h('div.flex-row.flex-center', { style: { width: '100%', - resize: 'none', - }, - dataset: { - persistentFormId: 'tx-data', + justifyContent: 'flex-start', + } + },[ + h('span', { style: {} }, ['Transaction Memo (optional)']), + h('span', { style: {} }, ['What\'s this?']), + ]), + + h('input.large-input', { + name: 'memo', + placeholder: '', + type: 'string', + style: { + marginRight: '6px', }, }), + ]), + + + + // h('h3.flex-center.text-transform-uppercase', { + // style: { + // background: '#EBEBEB', + // color: '#AEAEAE', + // marginTop: '16px', + // marginBottom: '16px', + // }, + // }, [ + // 'Transaction Data (optional)', + // ]), + + // // 'data' field + // h('section.flex-column.flex-center', [ + // h('input.large-input', { + // name: 'txData', + // placeholder: '0x01234', + // style: { + // width: '100%', + // resize: 'none', + // }, + // dataset: { + // persistentFormId: 'tx-data', + // }, + // }), + // ]), ]) ) } -- cgit v1.2.3 From 970988167982a79c131331a7585512b5e53c9a95 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sat, 29 Jul 2017 18:54:21 -0700 Subject: Center send token content; hook up 'Next' and 'Cancel' buttons --- ui/app/app.js | 20 +-- ui/app/css/index.css | 6 +- ui/app/send.js | 350 ++++++++++++++++++++++++++++----------------------- 3 files changed, 203 insertions(+), 173 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index 2eb037460..0f26f8add 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -172,15 +172,17 @@ App.prototype.renderAppBar = function () { ]), ]), - h('.app-header', { - style: { - visibility: props.isUnlocked ? 'visible' : 'none', - background: '#EFEFEF', // $gallery - height: '38px', - position: 'relative', - zIndex: 12, - }, - }) + // extra app-header space + + // h('.app-header', { + // style: { + // visibility: props.isUnlocked ? 'visible' : 'none', + // background: '#EFEFEF', // $gallery + // height: '38px', + // position: 'relative', + // zIndex: 12, + // }, + // }) ]) ) } diff --git a/ui/app/css/index.css b/ui/app/css/index.css index 3cedb1d8e..d45966fc0 100644 --- a/ui/app/css/index.css +++ b/ui/app/css/index.css @@ -489,12 +489,8 @@ input.large-input { /* Send Screen */ -.send-screen { - -} - .send-screen section { - margin: 8px 16px; + margin: 4px 16px; } .send-screen input { diff --git a/ui/app/send.js b/ui/app/send.js index bd4cf4ee1..513c2462f 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -59,197 +59,229 @@ SendTransactionScreen.prototype.render = function () { return ( - h('.send-screen.flex-column.flex-grow', { + h('div.flex-column.flex-grow', { style: { - background: '#FFFFFF', // $background-white - marginLeft: '3.5%', - marginRight: '3.5%', - } + // overflow: 'scroll', + minWidth: '355px', // TODO: maxWidth TBD, use home.html + }, }, [ - h('section.flex-center.flex-row', { - style: { - zIndex: 15, // $token-icon-z-index - marginTop: '-35px', - } - }, [ - h(Identicon, { - address: ARAGON, - diameter: 76, - }), - ]), - - // - // Required Fields - // - - h('h3.flex-center', { - style: { - marginTop: '-15px', - fontSize: '20px', - }, - }, [ - 'Send Tokens', - ]), - - h('h3.flex-center', { - style: { - textAlign: 'center', - fontSize: '16px', - }, - }, [ - 'Send Tokens to anyone with an Ethereum account', - ]), - h('h3.flex-center', { + // Main Send token Card + h('div.send-screen.flex-column.flex-grow', { style: { - textAlign: 'center', - fontSize: '16px', - }, - }, [ - 'Your Aragon Token Balance is:', - ]), - - h('h3.flex-center', { - style: { - textAlign: 'center', - fontSize: '43px', - }, - }, [ - '2.34', - ]), - - h('h3.flex-center', { - style: { - textAlign: 'center', - fontSize: '16px', - }, + marginLeft: '3.5%', + marginRight: '3.5%', + background: '#FFFFFF', // $background-white + } }, [ - 'ANT', - ]), - - // error message - props.error && h('span.error.flex-center', props.error), - - // 'to' field - h('section.flex-row.flex-center', [ - h(EnsInput, { - name: 'address', - placeholder: 'Recipient Address', - onChange: this.recipientDidChange.bind(this), - network, - identities, - addressBook, - }), - ]), - - // 'amount' and send button - h('section.flex-column.flex-center', [ - - h('div.flex-row.flex-center', { + h('section.flex-center.flex-row', { style: { - width: '100%', - justifyContent: 'space-between', + zIndex: 15, // $token-icon-z-index + marginTop: '-35px', } - },[ - h('span', { style: {} }, ['Amount']), - h('span', { style: {} }, ['Token <> USD']), + }, [ + h(Identicon, { + address: ARAGON, + diameter: 76, + }), ]), - h('input.large-input', { - name: 'amount', - placeholder: '0', - type: 'number', + // + // Required Fields + // + + h('h3.flex-center', { style: { - marginRight: '6px', + marginTop: '-15px', + fontSize: '16px', }, - dataset: { - persistentFormId: 'tx-amount', + }, [ + 'Send Tokens', + ]), + + h('h3.flex-center', { + style: { + textAlign: 'center', + fontSize: '12px', }, - }), + }, [ + 'Send Tokens to anyone with an Ethereum account', + ]), - ]), + h('h3.flex-center', { + style: { + textAlign: 'center', + fontSize: '12px', + }, + }, [ + 'Your Aragon Token Balance is:', + ]), - h('section.flex-column.flex-center', [ + h('h3.flex-center', { + style: { + textAlign: 'center', + fontSize: '36px', + }, + }, [ + '2.34', + ]), - h('div.flex-row.flex-center', { + h('h3.flex-center', { style: { - width: '100%', - justifyContent: 'space-between', - } - },[ - h('span', { style: {} }, ['Gas Fee:']), - h('span', { style: {} }, ['What\'s this?']), + textAlign: 'center', + fontSize: '12px', + }, + }, [ + 'ANT', ]), - h('input.large-input', { - name: 'Gas Fee', - placeholder: '0', - type: 'number', + // error message + props.error && h('span.error.flex-center', props.error), + + // 'to' field + h('section.flex-row.flex-center', { style: { - marginRight: '6px', + fontSize: '12px', }, - // dataset: { - // persistentFormId: 'tx-amount', - // }, - }), + }, [ + h(EnsInput, { + name: 'address', + placeholder: 'Recipient Address', + onChange: this.recipientDidChange.bind(this), + network, + identities, + addressBook, + }), + ]), - ]), + // 'amount' and send button + h('section.flex-column.flex-center', [ + + h('div.flex-row.flex-center', { + style: { + fontSize: '12px', + width: '100%', + justifyContent: 'space-between', + } + },[ + h('span', { style: {} }, ['Amount']), + h('span', { style: {} }, ['Token <> USD']), + ]), + + h('input.large-input', { + name: 'amount', + placeholder: '0', + type: 'number', + style: { + marginRight: '6px', + fontSize: '12px', + }, + dataset: { + persistentFormId: 'tx-amount', + }, + }), - // - // Optional Fields - // + ]), - h('section.flex-column.flex-center', [ + h('section.flex-column.flex-center', [ + + h('div.flex-row.flex-center', { + style: { + fontSize: '12px', + width: '100%', + justifyContent: 'space-between', + } + },[ + h('span', { style: {} }, ['Gas Fee:']), + h('span', { style: { fontSize: '8px' } }, ['What\'s this?']), + ]), + + h('input.large-input', { + name: 'Gas Fee', + placeholder: '0', + type: 'number', + style: { + fontSize: '12px', + marginRight: '6px', + }, + // dataset: { + // persistentFormId: 'tx-amount', + // }, + }), - h('div.flex-row.flex-center', { - style: { - width: '100%', - justifyContent: 'flex-start', - } - },[ - h('span', { style: {} }, ['Transaction Memo (optional)']), - h('span', { style: {} }, ['What\'s this?']), ]), - h('input.large-input', { - name: 'memo', - placeholder: '', - type: 'string', - style: { - marginRight: '6px', - }, - }), + // + // Optional Fields + // + + h('section.flex-column.flex-center', [ + + h('div.flex-row.flex-center', { + style: { + fontSize: '12px', + width: '100%', + justifyContent: 'flex-start', + } + },[ + h('span', { style: {} }, ['Transaction Memo (optional)']), + h('span', { style: { fontSize: '8px' } }, ['What\'s this?']), + ]), + + h('input.large-input', { + name: 'memo', + placeholder: '', + type: 'string', + style: { + marginRight: '6px', + }, + }), + ]), + // h('h3.flex-center.text-transform-uppercase', { + // style: { + // background: '#EBEBEB', + // color: '#AEAEAE', + // marginTop: '16px', + // marginBottom: '16px', + // }, + // }, [ + // 'Transaction Data (optional)', + // ]), + + // // 'data' field + // h('section.flex-column.flex-center', [ + // h('input.large-input', { + // name: 'txData', + // placeholder: '0x01234', + // style: { + // width: '100%', + // resize: 'none', + // }, + // dataset: { + // persistentFormId: 'tx-data', + // }, + // }), + // ]), ]), + // Buttons underneath card + h('button.primary', { + onClick: this.onSubmit.bind(this), + style: { + textTransform: 'uppercase', + }, + }, 'Next'), - // h('h3.flex-center.text-transform-uppercase', { - // style: { - // background: '#EBEBEB', - // color: '#AEAEAE', - // marginTop: '16px', - // marginBottom: '16px', - // }, - // }, [ - // 'Transaction Data (optional)', - // ]), - - // // 'data' field - // h('section.flex-column.flex-center', [ - // h('input.large-input', { - // name: 'txData', - // placeholder: '0x01234', - // style: { - // width: '100%', - // resize: 'none', - // }, - // dataset: { - // persistentFormId: 'tx-data', - // }, - // }), - // ]), + h('button.primary', { + onClick: this.back.bind(this), + style: { + textTransform: 'uppercase', + }, + }, 'Cancel'), ]) + ) } -- cgit v1.2.3 From 25259cc2cdadce71514d2e37cc9e0ea4bce21f40 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sat, 29 Jul 2017 19:37:08 -0700 Subject: Center action buttons, add minor style adjustments --- ui/app/css/index.css | 98 +++++++++++++++++++++++++++++++--------------------- ui/app/send.js | 48 +++++++++++++++---------- 2 files changed, 88 insertions(+), 58 deletions(-) diff --git a/ui/app/css/index.css b/ui/app/css/index.css index d45966fc0..f4783a446 100644 --- a/ui/app/css/index.css +++ b/ui/app/css/index.css @@ -69,46 +69,48 @@ input:focus, textarea:focus { flex-direction: column; } -button, input[type="submit"] { - font-family: 'Montserrat Bold'; - outline: none; - cursor: pointer; - padding: 8px 12px; - border: none; - color: white; - transform-origin: center center; - transition: transform 50ms ease-in; - /* default orange */ - background: rgba(247, 134, 28, 1); - box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); -} - -.btn-green, input[type="submit"].btn-green { - background: rgba(106, 195, 96, 1); - box-shadow: 0px 3px 6px rgba(106, 195, 96, 0.36); -} - -.btn-red { - background: rgba(254, 35, 17, 1); - box-shadow: 0px 3px 6px rgba(254, 35, 17, 0.36); -} - -button[disabled], input[type="submit"][disabled] { - cursor: not-allowed; - background: rgba(197, 197, 197, 1); - box-shadow: 0px 3px 6px rgba(197, 197, 197, 0.36); -} - -button.spaced { - margin: 2px; -} - -button:not([disabled]):hover, input[type="submit"]:not([disabled]):hover { - transform: scale(1.1); -} -button:not([disabled]):active, input[type="submit"]:not([disabled]):active { - transform: scale(0.95); -} +// TODO: remove/refactor for new design + +// button, input[type="submit"] { +// font-family: 'Montserrat Bold'; +// outline: none; +// cursor: pointer; +// padding: 8px 12px; +// border: none; +// color: white; +// transform-origin: center center; +// transition: transform 50ms ease-in; +// /* default orange */ +// background: rgba(247, 134, 28, 1); +// box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); +// } + +// .btn-green, input[type="submit"].btn-green { +// background: rgba(106, 195, 96, 1); +// box-shadow: 0px 3px 6px rgba(106, 195, 96, 0.36); +// } + +// .btn-red { +// background: rgba(254, 35, 17, 1); +// box-shadow: 0px 3px 6px rgba(254, 35, 17, 0.36); +// } + +// button[disabled], input[type="submit"][disabled] { +// cursor: not-allowed; +// background: rgba(197, 197, 197, 1); +// box-shadow: 0px 3px 6px rgba(197, 197, 197, 0.36); +// } + +// button.spaced { +// margin: 2px; +// } + +// button:not([disabled]):hover, input[type="submit"]:not([disabled]):hover { +// transform: scale(1.1); +// } +// button:not([disabled]):active, input[type="submit"]:not([disabled]):active { +// transform: scale(0.95); +// } a { text-decoration: none; @@ -137,6 +139,22 @@ button.primary { text-transform: uppercase; } +button.light { + padding: 8px 12px; + // background: #FFFFFF; // $bg-white + box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); + color: #585D67; // TODO: make reusable light button color + font-size: 1.1em; + font-family: 'Montserrat Regular'; + text-transform: uppercase; + text-align: center; + line-height: 20px; + border-radius: 2px; + border: 1px solid #979797; // #TODO: make reusable light border color + opacity: 0.5; +} + +// TODO: cleanup: not used anywhere button.btn-thin { border: 1px solid; border-color: #4D4D4D; diff --git a/ui/app/send.js b/ui/app/send.js index 513c2462f..a24989e56 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -72,6 +72,7 @@ SendTransactionScreen.prototype.render = function () { marginLeft: '3.5%', marginRight: '3.5%', background: '#FFFFFF', // $background-white + boxShadow: '0 2px 4px 0 rgba(0,0,0,0.08)', } }, [ h('section.flex-center.flex-row', { @@ -92,7 +93,7 @@ SendTransactionScreen.prototype.render = function () { h('h3.flex-center', { style: { - marginTop: '-15px', + marginTop: '-18px', fontSize: '16px', }, }, [ @@ -111,6 +112,7 @@ SendTransactionScreen.prototype.render = function () { h('h3.flex-center', { style: { textAlign: 'center', + marginTop: '2px', fontSize: '12px', }, }, [ @@ -121,6 +123,7 @@ SendTransactionScreen.prototype.render = function () { style: { textAlign: 'center', fontSize: '36px', + marginTop: '8px', }, }, [ '2.34', @@ -130,6 +133,7 @@ SendTransactionScreen.prototype.render = function () { style: { textAlign: 'center', fontSize: '12px', + marginTop: '4px', }, }, [ 'ANT', @@ -156,7 +160,6 @@ SendTransactionScreen.prototype.render = function () { // 'amount' and send button h('section.flex-column.flex-center', [ - h('div.flex-row.flex-center', { style: { fontSize: '12px', @@ -184,7 +187,6 @@ SendTransactionScreen.prototype.render = function () { ]), h('section.flex-column.flex-center', [ - h('div.flex-row.flex-center', { style: { fontSize: '12px', @@ -215,8 +217,11 @@ SendTransactionScreen.prototype.render = function () { // Optional Fields // - h('section.flex-column.flex-center', [ - + h('section.flex-column.flex-center', { + style: { + marginBottom: '10px', + }, + }, [ h('div.flex-row.flex-center', { style: { fontSize: '12px', @@ -225,7 +230,6 @@ SendTransactionScreen.prototype.render = function () { } },[ h('span', { style: {} }, ['Transaction Memo (optional)']), - h('span', { style: { fontSize: '8px' } }, ['What\'s this?']), ]), h('input.large-input', { @@ -266,20 +270,28 @@ SendTransactionScreen.prototype.render = function () { ]), // Buttons underneath card + h('section.flex-column.flex-center', [ - h('button.primary', { - onClick: this.onSubmit.bind(this), - style: { - textTransform: 'uppercase', - }, - }, 'Next'), + h('button.light', { + onClick: this.onSubmit.bind(this), + style: { + marginTop: '8px', + width: '8em', + background: '#FFFFFF' + }, + }, 'Next'), - h('button.primary', { - onClick: this.back.bind(this), - style: { - textTransform: 'uppercase', - }, - }, 'Cancel'), + h('button.light', { + onClick: this.back.bind(this), + style: { + background: '#F7F7F7', // $alabaster + border: 'none', + opacity: 1, + width: '8em', + }, + }, 'Cancel'), + + ]), ]) ) -- cgit v1.2.3 From 8ace4710ffa1ca56e59e5888fd076fecfae8a911 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sat, 29 Jul 2017 19:46:59 -0700 Subject: Clean up send screen --- ui/app/send.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ui/app/send.js b/ui/app/send.js index a24989e56..873db8473 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -320,7 +320,11 @@ SendTransactionScreen.prototype.onSubmit = function () { const nickname = state.nickname || ' ' const input = document.querySelector('input[name="amount"]').value const value = util.normalizeEthStringToWei(input) - const txData = document.querySelector('input[name="txData"]').value + // TODO: check with team on whether txData is removed completely. + const txData = false; + // Must replace with memo data. + // const txData = document.querySelector('input[name="txData"]').value + const balance = this.props.balance let message @@ -339,7 +343,7 @@ SendTransactionScreen.prototype.onSubmit = function () { return this.props.dispatch(actions.displayWarning(message)) } - if (!isHex(ethUtil.stripHexPrefix(txData)) && txData) { + if (txData && !isHex(ethUtil.stripHexPrefix(txData))) { message = 'Transaction data must be hex string.' return this.props.dispatch(actions.displayWarning(message)) } -- cgit v1.2.3 From 35ff4c195c4ff91a90b7572f07060b0213898f22 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sat, 29 Jul 2017 20:55:55 -0700 Subject: [WIP] Isolate form logic from rest of confirmation UI --- ui/app/components/pending-tx.js | 49 +++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 5324ccd64..2414a9759 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -75,11 +75,8 @@ PendingTx.prototype.render = function () { key: txMeta.id, }, [ - h('form#pending-tx-form', { - onSubmit: this.onSubmit.bind(this), - + h('div', { }, [ - // tx info h('div', [ @@ -305,24 +302,32 @@ PendingTx.prototype.render = function () { }, 'Buy Ether') : null, - h('button', { - onClick: (event) => { - this.resetGasFields() - event.preventDefault() - }, - }, 'Reset'), - - // Accept Button - h('input.confirm.btn-green', { - type: 'submit', - value: 'SUBMIT', - style: { marginLeft: '10px' }, - disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, - }), - - h('button.cancel.btn-red', { - onClick: props.cancelTransaction, - }, 'Reject'), + + h('form#pending-tx-form', { + onSubmit: this.onSubmit.bind(this), + }, [ + // Reset Button + h('button', { + onClick: (event) => { + this.resetGasFields() + event.preventDefault() + }, + }, 'Reset'), + + // Accept Button + h('input.confirm.btn-green', { + type: 'submit', + value: 'SUBMIT', + style: { marginLeft: '10px' }, + disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, + }), + + // Cancel Button + h('button.cancel.btn-red', { + onClick: props.cancelTransaction, + }, 'Reject'), + ]), + ]), ]), ]) -- cgit v1.2.3 From 65327dd11f5c56ae8a60a4a8f399d1b4832285c1 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sat, 29 Jul 2017 22:17:56 -0700 Subject: Cleanup unnecessary pending tx-switching logic --- ui/app/conf-tx.js | 79 +++++++++++++------------------------------------------ 1 file changed, 19 insertions(+), 60 deletions(-) diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 34727ff78..4a8c616e2 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -52,66 +52,25 @@ ConfirmTxScreen.prototype.render = function () { log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`) if (unconfTxList.length === 0) return h(Loading, { isLoading: true }) - return ( - - h('.flex-column.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - !isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: this.goHome.bind(this), - }) : null, - h('h2.page-subtitle', 'Confirm Transaction'), - isNotification ? h(NetworkIndicator, { - network: network, - provider: provider, - }) : null, - ]), - - h('h3', { - style: { - alignSelf: 'center', - display: unconfTxList.length > 1 ? 'block' : 'none', - }, - }, [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - style: { - display: props.index === 0 ? 'none' : 'inline-block', - }, - onClick: () => props.dispatch(actions.previousTx()), - }), - ` ${props.index + 1} of ${unconfTxList.length} `, - h('i.fa.fa-arrow-right.fa-lg.cursor-pointer', { - style: { - display: props.index + 1 === unconfTxList.length ? 'none' : 'inline-block', - }, - onClick: () => props.dispatch(actions.nextTx()), - }), - ]), - - warningIfExists(props.warning), - - currentTxView({ - // Properties - txData: txData, - key: txData.id, - selectedAddress: props.selectedAddress, - accounts: props.accounts, - identities: props.identities, - conversionRate, - currentCurrency, - blockGasLimit, - // Actions - buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), - sendTransaction: this.sendTransaction.bind(this), - cancelTransaction: this.cancelTransaction.bind(this, txData), - signMessage: this.signMessage.bind(this, txData), - signPersonalMessage: this.signPersonalMessage.bind(this, txData), - cancelMessage: this.cancelMessage.bind(this, txData), - cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), - }), - ]) - ) + return currentTxView({ + // Properties + txData: txData, + key: txData.id, + selectedAddress: props.selectedAddress, + accounts: props.accounts, + identities: props.identities, + conversionRate, + currentCurrency, + blockGasLimit, + // Actions + buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), + sendTransaction: this.sendTransaction.bind(this), + cancelTransaction: this.cancelTransaction.bind(this, txData), + signMessage: this.signMessage.bind(this, txData), + signPersonalMessage: this.signPersonalMessage.bind(this, txData), + cancelMessage: this.cancelMessage.bind(this, txData), + cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), + }) } function currentTxView (opts) { -- cgit v1.2.3 From abc78a1bf9b83d35bf1ac4453d8886f11675d41d Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sat, 29 Jul 2017 22:18:26 -0700 Subject: Add content boxes to pendingTx, prep for reusability --- ui/app/components/pending-tx.js | 509 ++++++++++++++-------------------------- 1 file changed, 179 insertions(+), 330 deletions(-) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 2414a9759..8031547d4 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -20,6 +20,11 @@ const GWEI_FACTOR = new BN(1e9) const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) const MIN_GAS_LIMIT_BN = new BN(21000) + +// Faked, for Icon +const Identicon = require('./identicon') +const ARAGON = '960b236A07cf122663c4303350609A66A7B288C0' + module.exports = PendingTx inherits(PendingTx, Component) function PendingTx () { @@ -31,6 +36,24 @@ function PendingTx () { } } +const sectionDivider = h('div', { + style: { + height:'1px', + background:'#E7E7E7', + }, +}), + +// Next: create separate react components +const contentDivider = h('div', { + style: { + marginLeft: '16px', + marginRight: '16px', + height:'1px', + background:'#E7E7E7', + }, +}) + + PendingTx.prototype.render = function () { const props = this.props const { currentCurrency, blockGasLimit } = props @@ -70,349 +93,187 @@ PendingTx.prototype.render = function () { this.inputs = [] return ( - - h('div', { - key: txMeta.id, + h('div.flex-column.flex-grow', { + style: { + // overflow: 'scroll', + minWidth: '355px', // TODO: maxWidth TBD, use home.html + }, }, [ - h('div', { + // Main Send token Card + h('div.send-screen.flex-column.flex-grow', { + style: { + marginLeft: '3.5%', + marginRight: '3.5%', + background: '#FFFFFF', // $background-white + boxShadow: '0 2px 4px 0 rgba(0,0,0,0.08)', + } }, [ - // tx info - h('div', [ - - h('.flex-row.flex-center', { - style: { - maxWidth: '100%', - }, - }, [ - - h(MiniAccountPanel, { - imageSeed: address, - picOrder: 'right', - }, [ - h('span.font-small', { - style: { - fontFamily: 'Montserrat Bold, Montserrat, sans-serif', - }, - }, identity.name), - - h(Copyable, { - value: ethUtil.toChecksumAddress(address), - }, [ - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, addressSummary(address, 6, 4, false)), - ]), - - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, [ - h(EthBalance, { - value: balance, - conversionRate, - currentCurrency, - inline: true, - labelColor: '#F7861C', - }), - ]), - ]), - - forwardCarrat(), - - this.miniAccountPanelForRecipient(), - ]), - - h('style', ` - .table-box { - margin: 7px 0px 0px 0px; - width: 100%; - } - .table-box .row { - margin: 0px; - background: rgb(236,236,236); - display: flex; - justify-content: space-between; - font-family: Montserrat Light, sans-serif; - font-size: 13px; - padding: 5px 25px; - } - .table-box .row .value { - font-family: Montserrat Regular; - } - `), - - h('.table-box', [ - - // Ether Value - // Currently not customizable, but easily modified - // in the way that gas and gasLimit currently are. - h('.row', [ - h('.cell.label', 'Amount'), - h(EthBalance, { value: txParams.value, currentCurrency, conversionRate }), - ]), - - // Gas Limit (customizable) - h('.cell.row', [ - h('.cell.label', 'Gas Limit'), - h('.cell.value', { - }, [ - h(BNInput, { - name: 'Gas Limit', - value: gasBn, - precision: 0, - scale: 0, - // The hard lower limit for gas. - min: MIN_GAS_LIMIT_BN.toString(10), - max: safeGasLimit, - suffix: 'UNITS', - style: { - position: 'relative', - top: '5px', - }, - onChange: this.gasLimitChanged.bind(this), - - ref: (hexInput) => { this.inputs.push(hexInput) }, - }), - ]), - ]), - - // Gas Price (customizable) - h('.cell.row', [ - h('.cell.label', 'Gas Price'), - h('.cell.value', { - }, [ - h(BNInput, { - name: 'Gas Price', - value: gasPriceBn, - precision: 9, - scale: 9, - suffix: 'GWEI', - min: MIN_GAS_PRICE_GWEI_BN.toString(10), - style: { - position: 'relative', - top: '5px', - }, - onChange: this.gasPriceChanged.bind(this), - ref: (hexInput) => { this.inputs.push(hexInput) }, - }), - ]), - ]), - - // Max Transaction Fee (calculated) - h('.cell.row', [ - h('.cell.label', 'Max Transaction Fee'), - h(EthBalance, { value: txFeeBn.toString(16), currentCurrency, conversionRate }), - ]), - - h('.cell.row', { - style: { - fontFamily: 'Montserrat Regular', - background: 'white', - padding: '10px 25px', - }, - }, [ - h('.cell.label', 'Max Total'), - h('.cell.value', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - h(EthBalance, { - value: maxCost.toString(16), - currentCurrency, - conversionRate, - inline: true, - labelColor: 'black', - fontSize: '16px', - }), - ]), - ]), - - // Data size row: - h('.cell.row', { - style: { - background: '#f7f7f7', - paddingBottom: '0px', - }, - }, [ - h('.cell.label'), - h('.cell.value', { - style: { - fontFamily: 'Montserrat Light', - fontSize: '11px', - }, - }, `Data included: ${dataLength} bytes`), - ]), - ]), // End of Table - + h('section.flex-center.flex-row', { + style: { + zIndex: 15, // $token-icon-z-index + marginTop: '-35px', + } + }, [ + h(Identicon, { + address: ARAGON, + diameter: 76, + }), ]), - h('style', ` - .conf-buttons button { - margin-left: 10px; - text-transform: uppercase; - } - `), + // + // Required Fields + // - txMeta.simulationFails ? - h('.error', { - style: { - marginLeft: 50, - fontSize: '0.9em', - }, - }, 'Transaction Error. Exception thrown in contract code.') - : null, + h('h3.flex-center', { + style: { + marginTop: '-18px', + fontSize: '16px', + }, + }, [ + 'Confirm Transaction', + ]), - !isValidAddress ? - h('.error', { - style: { - marginLeft: 50, - fontSize: '0.9em', - }, - }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.') - : null, + h('h3.flex-center', { + style: { + textAlign: 'center', + fontSize: '12px', + }, + }, [ + 'You\'re sending to Recipient ...5924', + ]), - insufficientBalance ? - h('span.error', { - style: { - marginLeft: 50, - fontSize: '0.9em', - }, - }, 'Insufficient balance for transaction') - : null, + h('h3.flex-center', { + style: { + textAlign: 'center', + fontSize: '36px', + marginTop: '8px', + }, + }, [ + '0.24', + ]), - // send + cancel - h('.flex-row.flex-space-around.conf-buttons', { + h('h3.flex-center', { style: { - display: 'flex', - justifyContent: 'flex-end', - margin: '14px 25px', + textAlign: 'center', + fontSize: '12px', + marginTop: '4px', }, }, [ + 'ANT', + ]), + // error message + props.error && h('span.error.flex-center', props.error), - insufficientBalance ? - h('button.btn-green', { - onClick: props.buyEth, - }, 'Buy Ether') - : null, + sectionDivider, + h('section.flex-row.flex-center', { - h('form#pending-tx-form', { - onSubmit: this.onSubmit.bind(this), + }, [ + h('div', { + style: { + width: '50%', + } }, [ - // Reset Button - h('button', { - onClick: (event) => { - this.resetGasFields() - event.preventDefault() - }, - }, 'Reset'), - - // Accept Button - h('input.confirm.btn-green', { - type: 'submit', - value: 'SUBMIT', - style: { marginLeft: '10px' }, - disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, - }), - - // Cancel Button - h('button.cancel.btn-red', { - onClick: props.cancelTransaction, - }, 'Reject'), + h('span', { + style: { + textAlign: 'left', + fontSize: '12px', + } + }, [ + 'From' + ]) ]), - ]), - ]), - ]) - ) -} - -PendingTx.prototype.miniAccountPanelForRecipient = function () { - const props = this.props - const txData = props.txData - const txParams = txData.txParams || {} - const isContractDeploy = !('to' in txParams) - - // If it's not a contract deploy, send to the account - if (!isContractDeploy) { - return h(MiniAccountPanel, { - imageSeed: txParams.to, - picOrder: 'left', - }, [ - - h('span.font-small', { - style: { - fontFamily: 'Montserrat Bold, Montserrat, sans-serif', - }, - }, nameForAddress(txParams.to, props.identities)), - - h(Copyable, { - value: ethUtil.toChecksumAddress(txParams.to), - }, [ - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, addressSummary(txParams.to, 6, 4, false)), - ]), - - ]) - } else { - return h(MiniAccountPanel, { - picOrder: 'left', - }, [ + h('div', { + style: { + width: '50%', + } + },[ + h('div', { + style: { + textAlign: 'left', + fontSize: '10px', + marginBottom: '-10px', + }, + }, 'Aragon Token'), - h('span.font-small', { - style: { - fontFamily: 'Montserrat Bold, Montserrat, sans-serif', - }, - }, 'New Contract'), + h('div', { + style: { + textAlign: 'left', + fontSize: '8px', + }, + }, 'Your Balance 2.34 ANT') + ]) + ]), - ]) - } -} + contentDivider, -PendingTx.prototype.gasPriceChanged = function (newBN, valid) { - log.info(`Gas price changed to: ${newBN.toString(10)}`) - const txMeta = this.gatherTxMeta() - txMeta.txParams.gasPrice = '0x' + newBN.toString('hex') - this.setState({ - txData: clone(txMeta), - valid, - }) -} + h('form#pending-tx-form', { + onSubmit: this.onSubmit.bind(this), + }, [ + // Reset Button + h('button', { + onClick: (event) => { + this.resetGasFields() + event.preventDefault() + }, + }, 'Reset'), + + // Accept Button + h('input.confirm.btn-green', { + type: 'submit', + value: 'SUBMIT', + style: { marginLeft: '10px' }, + disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, + }), + + // Cancel Button + h('button.cancel.btn-red', { + onClick: props.cancelTransaction, + }, 'Reject'), + ]), -PendingTx.prototype.gasLimitChanged = function (newBN, valid) { - log.info(`Gas limit changed to ${newBN.toString(10)}`) - const txMeta = this.gatherTxMeta() - txMeta.txParams.gas = '0x' + newBN.toString('hex') - this.setState({ - txData: clone(txMeta), - valid, - }) + ]) // end of main container + ]) // end of minwidth wrapper + ) } -PendingTx.prototype.resetGasFields = function () { - log.debug(`pending-tx resetGasFields`) - - this.inputs.forEach((hexInput) => { - if (hexInput) { - hexInput.setValid() - } - }) - - this.setState({ - txData: null, - valid: true, - }) -} +// PendingTx.prototype.gasPriceChanged = function (newBN, valid) { +// log.info(`Gas price changed to: ${newBN.toString(10)}`) +// const txMeta = this.gatherTxMeta() +// txMeta.txParams.gasPrice = '0x' + newBN.toString('hex') +// this.setState({ +// txData: clone(txMeta), +// valid, +// }) +// } + +// PendingTx.prototype.gasLimitChanged = function (newBN, valid) { +// log.info(`Gas limit changed to ${newBN.toString(10)}`) +// const txMeta = this.gatherTxMeta() +// txMeta.txParams.gas = '0x' + newBN.toString('hex') +// this.setState({ +// txData: clone(txMeta), +// valid, +// }) +// } + +// PendingTx.prototype.resetGasFields = function () { +// log.debug(`pending-tx resetGasFields`) + +// this.inputs.forEach((hexInput) => { +// if (hexInput) { +// hexInput.setValid() +// } +// }) + +// this.setState({ +// txData: null, +// valid: true, +// }) +// } PendingTx.prototype.onSubmit = function (event) { event.preventDefault() @@ -471,15 +332,3 @@ PendingTx.prototype.bnMultiplyByFraction = function (targetBN, numerator, denomi const denomBN = new BN(denominator) return targetBN.mul(numBN).div(denomBN) } - -function forwardCarrat () { - return ( - h('img', { - src: 'images/forward-carrat.svg', - style: { - padding: '5px 6px 0px 10px', - height: '37px', - }, - }) - ) -} -- cgit v1.2.3 From 4880ee26d589ea7bbb1f0d532646fa818d4eaae4 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sat, 29 Jul 2017 22:20:29 -0700 Subject: Add note to self, for future code cleanup --- ui/app/components/pending-tx.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 8031547d4..f77374ef8 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -36,6 +36,13 @@ function PendingTx () { } } +// Next: create separate react components +// roughly 5 components: +// heroIcon +// numericDisplay (contains symbol + currency) +// divider +// contentBox +// actionButtons const sectionDivider = h('div', { style: { height:'1px', @@ -43,7 +50,6 @@ const sectionDivider = h('div', { }, }), -// Next: create separate react components const contentDivider = h('div', { style: { marginLeft: '16px', -- cgit v1.2.3 From 689f60d1ce811f542e70da523bcb89b12242440d Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sat, 29 Jul 2017 22:44:39 -0700 Subject: Add rounded background to total token, with minor styling tweaks --- ui/app/components/pending-tx.js | 192 +++++++++++++++++++++++++++++++++++----- ui/app/css/colors.css | 3 +- ui/app/css/index.css | 10 +-- ui/app/send.js | 4 +- 4 files changed, 180 insertions(+), 29 deletions(-) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index f77374ef8..4b06f71b0 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -25,17 +25,6 @@ const MIN_GAS_LIMIT_BN = new BN(21000) const Identicon = require('./identicon') const ARAGON = '960b236A07cf122663c4303350609A66A7B288C0' -module.exports = PendingTx -inherits(PendingTx, Component) -function PendingTx () { - Component.call(this) - this.state = { - valid: true, - txData: null, - submitting: false, - } -} - // Next: create separate react components // roughly 5 components: // heroIcon @@ -48,7 +37,7 @@ const sectionDivider = h('div', { height:'1px', background:'#E7E7E7', }, -}), +}) const contentDivider = h('div', { style: { @@ -59,6 +48,16 @@ const contentDivider = h('div', { }, }) +module.exports = PendingTx +inherits(PendingTx, Component) +function PendingTx () { + Component.call(this) + this.state = { + valid: true, + txData: null, + submitting: false, + } +} PendingTx.prototype.render = function () { const props = this.props @@ -175,7 +174,6 @@ PendingTx.prototype.render = function () { sectionDivider, h('section.flex-row.flex-center', { - }, [ h('div', { style: { @@ -216,27 +214,179 @@ PendingTx.prototype.render = function () { contentDivider, + h('section.flex-row.flex-center', { + }, [ + h('div', { + style: { + width: '50%', + } + }, [ + h('span', { + style: { + textAlign: 'left', + fontSize: '12px', + } + }, [ + 'To' + ]) + ]), + + h('div', { + style: { + width: '50%', + } + },[ + h('div', { + style: { + textAlign: 'left', + fontSize: '10px', + marginBottom: '-10px', + }, + }, 'Ethereum Address'), + + h('div', { + style: { + textAlign: 'left', + fontSize: '8px', + }, + }, '...5924') + ]) + ]), + + contentDivider, + + h('section.flex-row.flex-center', { + }, [ + h('div', { + style: { + width: '50%', + } + }, [ + h('span', { + style: { + textAlign: 'left', + fontSize: '12px', + } + }, [ + 'Gas Fee' + ]) + ]), + + h('div', { + style: { + width: '50%', + } + },[ + h('div', { + style: { + textAlign: 'left', + fontSize: '10px', + marginBottom: '-10px', + }, + }, '$0.04 USD'), + + h('div', { + style: { + textAlign: 'left', + fontSize: '8px', + }, + }, '0.001575 ETH') + ]) + ]), + + contentDivider, + + h('section.flex-row.flex-center', { + style: { + backgroundColor: '#F6F6F6', // $wild-sand + borderRadius: '8px', + marginLeft: '10px', + marginRight: '10px', + paddingLeft: '6px', + paddingRight: '6px', + } + }, [ + h('div', { + style: { + width: '50%', + } + }, [ + h('div', { + style: { + textAlign: 'left', + fontSize: '12px', + } + }, [ + 'Total Tokens' + ]), + + h('div', { + style: { + textAlign: 'left', + fontSize: '8px', + } + }, [ + 'Total Gas' + ]) + + ]), + + h('div', { + style: { + width: '50%', + } + },[ + h('div', { + style: { + textAlign: 'left', + fontSize: '10px', + marginBottom: '-10px', + }, + }, '0.24 ANT (127.00 USD)'), + + h('div', { + style: { + textAlign: 'left', + fontSize: '8px', + }, + }, '0.249 ETH') + ]) + ]), + + sectionDivider, + h('form#pending-tx-form', { onSubmit: this.onSubmit.bind(this), }, [ // Reset Button - h('button', { - onClick: (event) => { - this.resetGasFields() - event.preventDefault() - }, - }, 'Reset'), + // h('button', { + // onClick: (event) => { + // this.resetGasFields() + // event.preventDefault() + // }, + // }, 'Reset'), // Accept Button h('input.confirm.btn-green', { type: 'submit', value: 'SUBMIT', - style: { marginLeft: '10px' }, + style: { + color: '#FFFFFF', + fontSize: '12px', + lineHeight: '20px', + textAlign: 'center', + }, disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, }), // Cancel Button - h('button.cancel.btn-red', { + h('button.cancel.btn-light', { + style: { + background: '#F7F7F7', // $alabaster + border: 'none', + opacity: 1, + width: '8em', + }, onClick: props.cancelTransaction, }, 'Reject'), ]), diff --git a/ui/app/css/colors.css b/ui/app/css/colors.css index 7ac93595f..650c81120 100644 --- a/ui/app/css/colors.css +++ b/ui/app/css/colors.css @@ -1,3 +1,4 @@ $gallery: #EFEFEF; $alabaster: #F7F7F7; -$shark: #22232C; \ No newline at end of file +$shark: #22232C; +$wild-sand: #F6F6F6; \ No newline at end of file diff --git a/ui/app/css/index.css b/ui/app/css/index.css index f4783a446..dc8cea695 100644 --- a/ui/app/css/index.css +++ b/ui/app/css/index.css @@ -85,10 +85,10 @@ input:focus, textarea:focus { // box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); // } -// .btn-green, input[type="submit"].btn-green { -// background: rgba(106, 195, 96, 1); -// box-shadow: 0px 3px 6px rgba(106, 195, 96, 0.36); -// } +.btn-green, input[type="submit"].btn-green { + border-radius: 2px; + background-color: #02C9B1; +} // .btn-red { // background: rgba(254, 35, 17, 1); @@ -139,7 +139,7 @@ button.primary { text-transform: uppercase; } -button.light { +.btn-light { padding: 8px 12px; // background: #FFFFFF; // $bg-white box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); diff --git a/ui/app/send.js b/ui/app/send.js index 873db8473..de9e64ad1 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -272,7 +272,7 @@ SendTransactionScreen.prototype.render = function () { // Buttons underneath card h('section.flex-column.flex-center', [ - h('button.light', { + h('button.btn-light', { onClick: this.onSubmit.bind(this), style: { marginTop: '8px', @@ -281,7 +281,7 @@ SendTransactionScreen.prototype.render = function () { }, }, 'Next'), - h('button.light', { + h('button.btn-light', { onClick: this.back.bind(this), style: { background: '#F7F7F7', // $alabaster -- cgit v1.2.3 From 6a5e73e67386b063786eb36efe1ee6544f8017bb Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sat, 29 Jul 2017 23:00:44 -0700 Subject: Enhance button and input css reset to protect from user agent stylesheet --- ui/app/css/reset.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/app/css/reset.css b/ui/app/css/reset.css index 9ce89e8bc..da94e6212 100644 --- a/ui/app/css/reset.css +++ b/ui/app/css/reset.css @@ -45,4 +45,8 @@ q:before, q:after { table { border-collapse: collapse; border-spacing: 0; +} + +input, button { + border-style: none; } \ No newline at end of file -- cgit v1.2.3 From f368f371c29699f277b8c91ad8a6284a3b451223 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sat, 29 Jul 2017 23:02:04 -0700 Subject: Simplify btn-green colors --- ui/app/components/pending-tx.js | 1 + ui/app/css/index.css | 5 ++--- ui/app/css/reset.css | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 4b06f71b0..eae9046a8 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -372,6 +372,7 @@ PendingTx.prototype.render = function () { value: 'SUBMIT', style: { color: '#FFFFFF', + borderRadius: '2px'; fontSize: '12px', lineHeight: '20px', textAlign: 'center', diff --git a/ui/app/css/index.css b/ui/app/css/index.css index dc8cea695..3c397dcff 100644 --- a/ui/app/css/index.css +++ b/ui/app/css/index.css @@ -85,9 +85,8 @@ input:focus, textarea:focus { // box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); // } -.btn-green, input[type="submit"].btn-green { - border-radius: 2px; - background-color: #02C9B1; +.btn-green { + background-color: #02C9B1; // TODO: reusable color in colors.css } // .btn-red { diff --git a/ui/app/css/reset.css b/ui/app/css/reset.css index da94e6212..146be1e15 100644 --- a/ui/app/css/reset.css +++ b/ui/app/css/reset.css @@ -49,4 +49,4 @@ table { input, button { border-style: none; -} \ No newline at end of file +} -- cgit v1.2.3 From 9373ba9f381f390be7e8d88057c0c6f1a97a27f8 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sat, 29 Jul 2017 23:09:49 -0700 Subject: Move action buttons out of send token container, tweak styles --- ui/app/components/pending-tx.js | 78 +++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index eae9046a8..ede561bd2 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -304,6 +304,7 @@ PendingTx.prototype.render = function () { marginRight: '10px', paddingLeft: '6px', paddingRight: '6px', + marginBottom: '10px', } }, [ h('div', { @@ -315,6 +316,7 @@ PendingTx.prototype.render = function () { style: { textAlign: 'left', fontSize: '12px', + marginBottom: '-10px', } }, [ 'Total Tokens' @@ -353,46 +355,46 @@ PendingTx.prototype.render = function () { ]) ]), - sectionDivider, - - h('form#pending-tx-form', { - onSubmit: this.onSubmit.bind(this), - }, [ - // Reset Button - // h('button', { - // onClick: (event) => { - // this.resetGasFields() - // event.preventDefault() - // }, - // }, 'Reset'), - - // Accept Button - h('input.confirm.btn-green', { - type: 'submit', - value: 'SUBMIT', - style: { - color: '#FFFFFF', - borderRadius: '2px'; - fontSize: '12px', - lineHeight: '20px', - textAlign: 'center', - }, - disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, - }), + ]), // end of container - // Cancel Button - h('button.cancel.btn-light', { - style: { - background: '#F7F7F7', // $alabaster - border: 'none', - opacity: 1, - width: '8em', - }, - onClick: props.cancelTransaction, - }, 'Reject'), - ]), + h('form#pending-tx-form.flex-column.flex-center', { + onSubmit: this.onSubmit.bind(this), + }, [ + // Reset Button + // h('button', { + // onClick: (event) => { + // this.resetGasFields() + // event.preventDefault() + // }, + // }, 'Reset'), + + // Accept Button + h('input.confirm.btn-green', { + type: 'submit', + value: 'SUBMIT', + style: { + marginTop: '8px', + width: '8em', + color: '#FFFFFF', + borderRadius: '2px', + fontSize: '12px', + lineHeight: '20px', + textAlign: 'center', + }, + disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, + }), - ]) // end of main container + // Cancel Button + h('button.cancel.btn-light', { + style: { + background: '#F7F7F7', // $alabaster + border: 'none', + opacity: 1, + width: '8em', + }, + onClick: props.cancelTransaction, + }, 'Reject'), + ]), ]) // end of minwidth wrapper ) } -- cgit v1.2.3 From 97cb25c9f17b5cbf66f4e9a769bcdbdd39b4c9b5 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sat, 29 Jul 2017 23:11:11 -0700 Subject: Adjust copy in send token confirmation screen --- ui/app/components/pending-tx.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index ede561bd2..1fa9db4ef 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -371,7 +371,7 @@ PendingTx.prototype.render = function () { // Accept Button h('input.confirm.btn-green', { type: 'submit', - value: 'SUBMIT', + value: 'CONFIRM', style: { marginTop: '8px', width: '8em', @@ -393,7 +393,7 @@ PendingTx.prototype.render = function () { width: '8em', }, onClick: props.cancelTransaction, - }, 'Reject'), + }, 'CANCEL'), ]), ]) // end of minwidth wrapper ) -- cgit v1.2.3 From a7ab69b940e91aea4362c3c0bf9e9f3efb7c76c9 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sun, 30 Jul 2017 16:47:47 -0700 Subject: Adjust button styles for Send Token screen --- ui/app/components/pending-tx.js | 1 + ui/app/css/reset.css | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 1fa9db4ef..1c47440f2 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -380,6 +380,7 @@ PendingTx.prototype.render = function () { fontSize: '12px', lineHeight: '20px', textAlign: 'center', + borderStyle: 'none', }, disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, }), diff --git a/ui/app/css/reset.css b/ui/app/css/reset.css index 146be1e15..fef74825d 100644 --- a/ui/app/css/reset.css +++ b/ui/app/css/reset.css @@ -47,6 +47,6 @@ table { border-spacing: 0; } -input, button { +button { border-style: none; } -- cgit v1.2.3 From dd3766242dcdfd334f79367793d2b27bfcc36eb6 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sun, 30 Jul 2017 19:52:27 -0700 Subject: Adjust dimensions of popup.html and app bar to match --- app/notification.html | 2 +- app/popup.html | 2 +- ui/app/app.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/notification.html b/app/notification.html index cc485da7f..be38f4aa3 100644 --- a/app/notification.html +++ b/app/notification.html @@ -9,7 +9,7 @@ } - +
diff --git a/app/popup.html b/app/popup.html index d09b09315..471468b13 100644 --- a/app/popup.html +++ b/app/popup.html @@ -5,7 +5,7 @@ MetaMask Plugin - +
diff --git a/ui/app/app.js b/ui/app/app.js index 0f26f8add..7f844560c 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -121,7 +121,7 @@ App.prototype.renderAppBar = function () { alignItems: 'center', visibility: props.isUnlocked ? 'visible' : 'none', background: '#EFEFEF', // $gallery - height: '38px', + height: '11%', position: 'relative', zIndex: 12, }, -- cgit v1.2.3 From 7ea38523ea8231fa75916f48803abb0eb593f9a2 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sun, 30 Jul 2017 19:56:11 -0700 Subject: Set font-size on body of popup.html, for responsiveness --- app/popup.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/popup.html b/app/popup.html index 471468b13..a742686c5 100644 --- a/app/popup.html +++ b/app/popup.html @@ -5,7 +5,7 @@ MetaMask Plugin - +
-- cgit v1.2.3 From b15575b4530d4c1830af0471e9fb5bc1ee689ee3 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sun, 30 Jul 2017 20:02:42 -0700 Subject: Remove old header space --- ui/app/app.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index 7f844560c..4d70f9df9 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -172,17 +172,6 @@ App.prototype.renderAppBar = function () { ]), ]), - // extra app-header space - - // h('.app-header', { - // style: { - // visibility: props.isUnlocked ? 'visible' : 'none', - // background: '#EFEFEF', // $gallery - // height: '38px', - // position: 'relative', - // zIndex: 12, - // }, - // }) ]) ) } -- cgit v1.2.3 From ca1a4b309676c3d10473acf4869b398d4ed50fb7 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sun, 30 Jul 2017 20:42:12 -0700 Subject: Add layout for main container elements --- ui/app/app.js | 20 ++++++++------------ ui/app/main-container.js | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 12 deletions(-) create mode 100644 ui/app/main-container.js diff --git a/ui/app/app.js b/ui/app/app.js index 4d70f9df9..4f877bc51 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -9,7 +9,7 @@ const NewKeyChainScreen = require('./new-keychain') // unlock const UnlockScreen = require('./unlock') // accounts -const AccountDetailScreen = require('./account-detail') +const MainContainer = require('./main-container') const SendTransactionScreen = require('./send') const ConfirmTxScreen = require('./conf-tx') // notice @@ -90,13 +90,7 @@ App.prototype.render = function () { }), // panel content - h('.app-primary' + (transForward ? '.from-right' : '.from-left'), { - style: { - maxWidth: '850px', - }, - }, [ - this.renderPrimary(), - ]), + this.renderPrimary(), ]) ) } @@ -121,7 +115,7 @@ App.prototype.renderAppBar = function () { alignItems: 'center', visibility: props.isUnlocked ? 'visible' : 'none', background: '#EFEFEF', // $gallery - height: '11%', + height: '11vh', position: 'relative', zIndex: 12, }, @@ -132,6 +126,7 @@ App.prototype.renderAppBar = function () { display: 'flex', flexDirection: 'row', alignItems: 'center', + marginBottom: '1.8em', }, }, [ // mini logo @@ -156,6 +151,7 @@ App.prototype.renderAppBar = function () { display: 'flex', flexDirection: 'row', alignItems: 'center', + marginBottom: '1.8em', }, }, [ // Network Indicator @@ -419,8 +415,8 @@ App.prototype.renderPrimary = function () { switch (props.currentView.name) { case 'accountDetail': - log.debug('rendering account detail screen') - return h(AccountDetailScreen, {key: 'account-detail'}) + log.debug('rendering main container') + return h(MainContainer, {key: 'account-detail'}) case 'sendTransaction': log.debug('rendering send tx screen') @@ -488,7 +484,7 @@ App.prototype.renderPrimary = function () { default: log.debug('rendering default, account detail screen') - return h(AccountDetailScreen, {key: 'account-detail'}) + return h(MainContainer, {key: 'account-detail'}) } } diff --git a/ui/app/main-container.js b/ui/app/main-container.js new file mode 100644 index 000000000..ca68ba6b0 --- /dev/null +++ b/ui/app/main-container.js @@ -0,0 +1,41 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = MainContainer + +inherits(MainContainer, Component) +function MainContainer () { + Component.call(this) +} + +MainContainer.prototype.render = function () { + console.log("rendering MainContainer..."); + return h('div.flex-row', { + style: { + position: 'absolute', + marginTop: '6vh', + width: '98%', + zIndex: 20, + } + }, [ + h('div.wallet-view', { + style: { + flexGrow: 1, + height: '82vh', + background: 'blue', + } + }, [ + ]), + + h('div.tx-view', { + style: { + flexGrow: 2, + height: '82vh', + background: 'green', + } + }, [ + ]), + ]) +} + -- cgit v1.2.3 From cbd53d4601f1af5aa4337e86ea8875606406e803 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sun, 30 Jul 2017 21:25:32 -0700 Subject: Add containers for wallet view and dropdown UI --- ui/app/components/wallet-view.js | 81 ++++++++++++++++++++++++++++++++++++++++ ui/app/main-container.js | 6 +-- 2 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 ui/app/components/wallet-view.js diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js new file mode 100644 index 000000000..fed227d6b --- /dev/null +++ b/ui/app/components/wallet-view.js @@ -0,0 +1,81 @@ +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Identicon = require('./identicon') +const AccountDropdowns = require('./account-dropdowns').AccountDropdowns + +module.exports = connect(mapStateToProps)(WalletView) + +function mapStateToProps (state) { + return { + network: state.metamask.network, + } +} + + +inherits(WalletView, Component) +function WalletView () { + Component.call(this) +} + +const noop = () => {} + +WalletView.prototype.render = function () { + const selected = '0x82df11beb942BEeeD58d466fCb0F0791365C7684' + const { network } = this.props + + return h('div.wallet-view.flex-column', { + style: { + flexGrow: 1, + height: '82vh', + background: '#FAFAFA', + } + }, [ + + h('div.flex-row.flex-center', { + style: { + // marginLeft: '5px', + // marginRight: '5px', + // marginTop: '10px', + // alignItems: 'center', + } + }, [ + + h('.identicon-wrapper.select-none', [ + h(Identicon, { + diameter: 24, + address: selected, + }), + ]), + + h('span', { + style: { + fontSize: '1.5em', + marginLeft: '5px', + } + }, [ + 'Account 1' + ]), + + h( + AccountDropdowns, + { + style: { + marginRight: '8px', + marginLeft: 'auto', + cursor: 'pointer', + }, + selected, + network, + identities: {}, + }, + ), + + ]) + + // wallet display 1 + // token display 1 + + ]) +} diff --git a/ui/app/main-container.js b/ui/app/main-container.js index ca68ba6b0..c1f7db0d8 100644 --- a/ui/app/main-container.js +++ b/ui/app/main-container.js @@ -1,6 +1,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits +const WalletView = require('./components/wallet-view') module.exports = MainContainer @@ -19,11 +20,10 @@ MainContainer.prototype.render = function () { zIndex: 20, } }, [ - h('div.wallet-view', { + h(WalletView, { style: { flexGrow: 1, height: '82vh', - background: 'blue', } }, [ ]), @@ -32,7 +32,7 @@ MainContainer.prototype.render = function () { style: { flexGrow: 2, height: '82vh', - background: 'green', + background: '#FFFFFF', } }, [ ]), -- cgit v1.2.3 From 4cdfd00f58082a26b8835a7c17ed4c964ec24c9e Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sun, 30 Jul 2017 21:35:27 -0700 Subject: Add box shadow to main container --- ui/app/main-container.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/app/main-container.js b/ui/app/main-container.js index c1f7db0d8..f64009539 100644 --- a/ui/app/main-container.js +++ b/ui/app/main-container.js @@ -18,6 +18,7 @@ MainContainer.prototype.render = function () { marginTop: '6vh', width: '98%', zIndex: 20, + boxShadow: '0 0 7px 0 rgba(0,0,0,0.08)', } }, [ h(WalletView, { -- cgit v1.2.3 From ddbf5613b3b7cf7b7b637f33cac87f5bbe69e7a7 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sun, 30 Jul 2017 21:35:41 -0700 Subject: Add note for later on isolating components --- ui/app/components/wallet-view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index fed227d6b..3a08705be 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -67,7 +67,7 @@ WalletView.prototype.render = function () { cursor: 'pointer', }, selected, - network, + network, // TODO: this prop could be in the account dropdown container identities: {}, }, ), -- cgit v1.2.3 From 0ca50dfb1b990dadc702b178fad7e6434b71167a Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sun, 30 Jul 2017 21:53:13 -0700 Subject: Center account name and dropdowns --- ui/app/components/wallet-view.js | 9 ++++----- ui/app/main-container.js | 6 +++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index 3a08705be..f425ec3d1 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -35,10 +35,9 @@ WalletView.prototype.render = function () { h('div.flex-row.flex-center', { style: { - // marginLeft: '5px', - // marginRight: '5px', - // marginTop: '10px', - // alignItems: 'center', + marginLeft: '35px', + marginRight: '35px', + marginTop: '35px', } }, [ @@ -52,7 +51,7 @@ WalletView.prototype.render = function () { h('span', { style: { fontSize: '1.5em', - marginLeft: '5px', + marginLeft: '10px', // TODO: switch all units for this component to em } }, [ 'Account 1' diff --git a/ui/app/main-container.js b/ui/app/main-container.js index f64009539..f891402ac 100644 --- a/ui/app/main-container.js +++ b/ui/app/main-container.js @@ -23,15 +23,15 @@ MainContainer.prototype.render = function () { }, [ h(WalletView, { style: { - flexGrow: 1, - height: '82vh', + // width: '33.33%', + // height: '82vh', } }, [ ]), h('div.tx-view', { style: { - flexGrow: 2, + width: '66.66%', height: '82vh', background: '#FFFFFF', } -- cgit v1.2.3 From 610d6da8aee2c32bb142b1ff93f6a0a685adf46c Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sun, 30 Jul 2017 22:10:59 -0700 Subject: Add hyperscript for wallet display component --- ui/app/components/wallet-view.js | 52 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index f425ec3d1..c06c4133b 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -29,10 +29,11 @@ WalletView.prototype.render = function () { style: { flexGrow: 1, height: '82vh', - background: '#FAFAFA', + background: '#FAFAFA', // TODO: add to reusable colors } }, [ + // TODO: Separate component: wallet account details h('div.flex-row.flex-center', { style: { marginLeft: '35px', @@ -71,10 +72,53 @@ WalletView.prototype.render = function () { }, ), - ]) + ]), - // wallet display 1 - // token display 1 + // TODO: Separate component: wallet contents + h('div.flex-column', { + style: { + marginLeft: '35px', + marginTop: '15px', + alignItems: 'flex-start', + } + }, [ + + h('span', { + style: { + fontSize: '1.1em', + }, + }, 'Wallet'), + + h('span', { + style: { + fontSize: '1.8em', + margin: '10px 0px', + }, + }, '1001.124 ETH'), + + h('span', { + style: { + fontSize: '1.3em', + }, + }, '$300,000.00 USD'), + + h('div', { + style: { + position: 'absolute', + marginLeft: '-35px', + height: '6em', + width: '4px', + background: '#D8D8D8', // TODO: add to resuable colors + } + }, [ + ]) + ]), + + // Buy Buttons + + + + // Wallet contents ]) } -- cgit v1.2.3 From 7d4927c975554b091d72f6c24e7dd9e824f32548 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sun, 30 Jul 2017 22:25:20 -0700 Subject: Add layout for Buy and Send buttons --- ui/app/components/wallet-view.js | 45 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index c06c4133b..b61b53447 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -115,7 +115,50 @@ WalletView.prototype.render = function () { ]), // Buy Buttons - + // for index.css + // + // TODO: move into a class + // div.wallet-btn { + // border: 1px solid rgb(91, 93, 103); + // border-radius: 2px; + // height: 30px; + // width: 75px; + // font-size: 0.8em; + // text-align: center; + // line-height: 25px; + // } + + h('div.flex-row', { + style: { + marginLeft: '35px', + marginTop: '10px', + } + }, [ + h('div', { + style: { + border: '1px solid rgb(91, 93, 103)', + borderRadius: '2px', + height: '30px', + width: '75px', + fontSize: '0.8em', + textAlign: 'center', + lineHeight: '25px', + } + }, 'BUY'), + h('div.wallet-btn', { + style: { + border: '1px solid rgb(91, 93, 103)', + borderRadius: '2px', + height: '30px', + width: '75px', + fontSize: '0.8em', + textAlign: 'center', + lineHeight: '25px', + // spacing... + marginLeft: '15px', + } + }, 'SEND'), + ]), // Wallet contents -- cgit v1.2.3 From 0c1aea97c74e6ac0c263a654510faca73a2dc949 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Sun, 30 Jul 2017 22:30:55 -0700 Subject: Isolate wallet-content-display component --- ui/app/components/wallet-content-display.js | 54 +++++++++++++++++++++++++++++ ui/app/components/wallet-view.js | 10 ++++-- 2 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 ui/app/components/wallet-content-display.js diff --git a/ui/app/components/wallet-content-display.js b/ui/app/components/wallet-content-display.js new file mode 100644 index 000000000..f1db09ec8 --- /dev/null +++ b/ui/app/components/wallet-content-display.js @@ -0,0 +1,54 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = WalletContentDisplay + +inherits(WalletContentDisplay, Component) +function WalletContentDisplay () { + Component.call(this) +} + +WalletContentDisplay.prototype.render = function () { + const { title, amount, fiatValue, active } = this.props + + return h('div.flex-column', { + style: { + marginLeft: '35px', + marginTop: '15px', + alignItems: 'flex-start', + } + }, [ + + h('span', { + style: { + fontSize: '1.1em', + }, + }, title), + + h('span', { + style: { + fontSize: '1.8em', + margin: '10px 0px', + }, + }, amount), + + h('span', { + style: { + fontSize: '1.3em', + }, + }, fiatValue), + + active && h('div', { + style: { + position: 'absolute', + marginLeft: '-35px', + height: '6em', + width: '4px', + background: '#D8D8D8', // TODO: add to resuable colors + } + }, [ + ]) + ]) +} + diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index b61b53447..1c3f3b7f9 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -4,6 +4,7 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const Identicon = require('./identicon') const AccountDropdowns = require('./account-dropdowns').AccountDropdowns +const Content = require('./wallet-content-display') module.exports = connect(mapStateToProps)(WalletView) @@ -74,7 +75,7 @@ WalletView.prototype.render = function () { ]), - // TODO: Separate component: wallet contents + // TODO: Separate component: wallet-content-account h('div.flex-column', { style: { marginLeft: '35px', @@ -160,8 +161,13 @@ WalletView.prototype.render = function () { }, 'SEND'), ]), - // Wallet contents + h(Content, { + title: "Total Token Balance", + amount: "45.439 ETH", + fiatValue: "$13,000.00 USD", + active: false, + }) ]) } -- cgit v1.2.3 From 3797b9921fc227c1bcf9681cffa73588cc7afb44 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Mon, 31 Jul 2017 20:22:15 -0700 Subject: Adjust popup size to 545x450; refactor wallet view to fit --- app/popup.html | 2 +- ui/app/components/wallet-content-display.js | 14 +++--- ui/app/components/wallet-view.js | 74 ++++++++--------------------- 3 files changed, 29 insertions(+), 61 deletions(-) diff --git a/app/popup.html b/app/popup.html index a742686c5..de0e100a5 100644 --- a/app/popup.html +++ b/app/popup.html @@ -5,7 +5,7 @@ MetaMask Plugin - +
diff --git a/ui/app/components/wallet-content-display.js b/ui/app/components/wallet-content-display.js index f1db09ec8..3baffad69 100644 --- a/ui/app/components/wallet-content-display.js +++ b/ui/app/components/wallet-content-display.js @@ -10,13 +10,14 @@ function WalletContentDisplay () { } WalletContentDisplay.prototype.render = function () { - const { title, amount, fiatValue, active } = this.props + const { title, amount, fiatValue, active, style } = this.props + // TODO: Separate component: wallet-content-account return h('div.flex-column', { style: { - marginLeft: '35px', - marginTop: '15px', + marginLeft: '1.3em', alignItems: 'flex-start', + ...style, } }, [ @@ -29,7 +30,7 @@ WalletContentDisplay.prototype.render = function () { h('span', { style: { fontSize: '1.8em', - margin: '10px 0px', + margin: '0.4em 0em', }, }, amount), @@ -42,13 +43,14 @@ WalletContentDisplay.prototype.render = function () { active && h('div', { style: { position: 'absolute', - marginLeft: '-35px', + marginLeft: '-1.3em', height: '6em', - width: '4px', + width: '0.3em', background: '#D8D8D8', // TODO: add to resuable colors } }, [ ]) ]) + } diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index 1c3f3b7f9..0c5bd5c4f 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -37,9 +37,7 @@ WalletView.prototype.render = function () { // TODO: Separate component: wallet account details h('div.flex-row.flex-center', { style: { - marginLeft: '35px', - marginRight: '35px', - marginTop: '35px', + margin: '1.8em 1.3em', } }, [ @@ -52,8 +50,8 @@ WalletView.prototype.render = function () { h('span', { style: { - fontSize: '1.5em', - marginLeft: '10px', // TODO: switch all units for this component to em + fontSize: '1.2em', + marginLeft: '0.6em', // TODO: switch all units for this component to em } }, [ 'Account 1' @@ -63,7 +61,6 @@ WalletView.prototype.render = function () { AccountDropdowns, { style: { - marginRight: '8px', marginLeft: 'auto', cursor: 'pointer', }, @@ -75,49 +72,15 @@ WalletView.prototype.render = function () { ]), - // TODO: Separate component: wallet-content-account - h('div.flex-column', { - style: { - marginLeft: '35px', - marginTop: '15px', - alignItems: 'flex-start', - } - }, [ - - h('span', { - style: { - fontSize: '1.1em', - }, - }, 'Wallet'), - - h('span', { - style: { - fontSize: '1.8em', - margin: '10px 0px', - }, - }, '1001.124 ETH'), - - h('span', { - style: { - fontSize: '1.3em', - }, - }, '$300,000.00 USD'), - - h('div', { - style: { - position: 'absolute', - marginLeft: '-35px', - height: '6em', - width: '4px', - background: '#D8D8D8', // TODO: add to resuable colors - } - }, [ - ]) - ]), + h(Content, { + title: 'Wallet', + amount: '1001.124 ETH', + fiatValue: '$300,000.00 USD', + active: true, + }), // Buy Buttons // for index.css - // // TODO: move into a class // div.wallet-btn { // border: 1px solid rgb(91, 93, 103); @@ -131,32 +94,32 @@ WalletView.prototype.render = function () { h('div.flex-row', { style: { - marginLeft: '35px', - marginTop: '10px', + marginLeft: '1.3em', + marginTop: '0.8em', } }, [ h('div', { style: { border: '1px solid rgb(91, 93, 103)', borderRadius: '2px', - height: '30px', - width: '75px', + height: '28px', + width: '70px', fontSize: '0.8em', textAlign: 'center', lineHeight: '25px', + marginLeft: '.6em', } }, 'BUY'), h('div.wallet-btn', { style: { border: '1px solid rgb(91, 93, 103)', borderRadius: '2px', - height: '30px', - width: '75px', + height: '28px', + width: '70px', fontSize: '0.8em', textAlign: 'center', lineHeight: '25px', - // spacing... - marginLeft: '15px', + marginLeft: '.6em', } }, 'SEND'), ]), @@ -167,6 +130,9 @@ WalletView.prototype.render = function () { amount: "45.439 ETH", fiatValue: "$13,000.00 USD", active: false, + style: { + marginTop: '1.3em', + } }) ]) -- cgit v1.2.3 From fce6041dbeb3384badef50467912ab47e51053ff Mon Sep 17 00:00:00 2001 From: sdtsui Date: Mon, 31 Jul 2017 20:44:32 -0700 Subject: Add new fonts from @cjeria: DIN Next and DIN OT --- app/fonts/DIN Next/DIN Next W01 Bold.otf | Bin 0 -> 106032 bytes app/fonts/DIN Next/DIN Next W01 Regular.otf | Bin 0 -> 106580 bytes app/fonts/DIN Next/DIN Next W10 Black.otf | Bin 0 -> 105972 bytes app/fonts/DIN Next/DIN Next W10 Italic.otf | Bin 0 -> 115984 bytes app/fonts/DIN Next/DIN Next W10 Light.otf | Bin 0 -> 108672 bytes app/fonts/DIN Next/DIN Next W10 Medium.otf | Bin 0 -> 105684 bytes app/fonts/DIN_OT/DINOT-2.otf | Bin 0 -> 44144 bytes app/fonts/DIN_OT/DINOT-Bold 2.otf | Bin 0 -> 45564 bytes app/fonts/DIN_OT/DINOT-BoldItalic.otf | Bin 0 -> 49684 bytes app/fonts/DIN_OT/DINOT-Italic 2.otf | Bin 0 -> 47956 bytes app/fonts/DIN_OT/DINOT-Medium 2.otf | Bin 0 -> 44652 bytes app/fonts/DIN_OT/DINOT-MediumItalic 2.otf | Bin 0 -> 47732 bytes 12 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/fonts/DIN Next/DIN Next W01 Bold.otf create mode 100644 app/fonts/DIN Next/DIN Next W01 Regular.otf create mode 100644 app/fonts/DIN Next/DIN Next W10 Black.otf create mode 100644 app/fonts/DIN Next/DIN Next W10 Italic.otf create mode 100644 app/fonts/DIN Next/DIN Next W10 Light.otf create mode 100644 app/fonts/DIN Next/DIN Next W10 Medium.otf create mode 100644 app/fonts/DIN_OT/DINOT-2.otf create mode 100644 app/fonts/DIN_OT/DINOT-Bold 2.otf create mode 100644 app/fonts/DIN_OT/DINOT-BoldItalic.otf create mode 100644 app/fonts/DIN_OT/DINOT-Italic 2.otf create mode 100644 app/fonts/DIN_OT/DINOT-Medium 2.otf create mode 100644 app/fonts/DIN_OT/DINOT-MediumItalic 2.otf diff --git a/app/fonts/DIN Next/DIN Next W01 Bold.otf b/app/fonts/DIN Next/DIN Next W01 Bold.otf new file mode 100644 index 000000000..2b78d1ff4 Binary files /dev/null and b/app/fonts/DIN Next/DIN Next W01 Bold.otf differ diff --git a/app/fonts/DIN Next/DIN Next W01 Regular.otf b/app/fonts/DIN Next/DIN Next W01 Regular.otf new file mode 100644 index 000000000..09f6ee297 Binary files /dev/null and b/app/fonts/DIN Next/DIN Next W01 Regular.otf differ diff --git a/app/fonts/DIN Next/DIN Next W10 Black.otf b/app/fonts/DIN Next/DIN Next W10 Black.otf new file mode 100644 index 000000000..08eb73373 Binary files /dev/null and b/app/fonts/DIN Next/DIN Next W10 Black.otf differ diff --git a/app/fonts/DIN Next/DIN Next W10 Italic.otf b/app/fonts/DIN Next/DIN Next W10 Italic.otf new file mode 100644 index 000000000..73f2b9e8c Binary files /dev/null and b/app/fonts/DIN Next/DIN Next W10 Italic.otf differ diff --git a/app/fonts/DIN Next/DIN Next W10 Light.otf b/app/fonts/DIN Next/DIN Next W10 Light.otf new file mode 100644 index 000000000..700450e49 Binary files /dev/null and b/app/fonts/DIN Next/DIN Next W10 Light.otf differ diff --git a/app/fonts/DIN Next/DIN Next W10 Medium.otf b/app/fonts/DIN Next/DIN Next W10 Medium.otf new file mode 100644 index 000000000..b73f2e43f Binary files /dev/null and b/app/fonts/DIN Next/DIN Next W10 Medium.otf differ diff --git a/app/fonts/DIN_OT/DINOT-2.otf b/app/fonts/DIN_OT/DINOT-2.otf new file mode 100644 index 000000000..4a5e13127 Binary files /dev/null and b/app/fonts/DIN_OT/DINOT-2.otf differ diff --git a/app/fonts/DIN_OT/DINOT-Bold 2.otf b/app/fonts/DIN_OT/DINOT-Bold 2.otf new file mode 100644 index 000000000..6ed5b6c3d Binary files /dev/null and b/app/fonts/DIN_OT/DINOT-Bold 2.otf differ diff --git a/app/fonts/DIN_OT/DINOT-BoldItalic.otf b/app/fonts/DIN_OT/DINOT-BoldItalic.otf new file mode 100644 index 000000000..148c90588 Binary files /dev/null and b/app/fonts/DIN_OT/DINOT-BoldItalic.otf differ diff --git a/app/fonts/DIN_OT/DINOT-Italic 2.otf b/app/fonts/DIN_OT/DINOT-Italic 2.otf new file mode 100644 index 000000000..e365e77ab Binary files /dev/null and b/app/fonts/DIN_OT/DINOT-Italic 2.otf differ diff --git a/app/fonts/DIN_OT/DINOT-Medium 2.otf b/app/fonts/DIN_OT/DINOT-Medium 2.otf new file mode 100644 index 000000000..a87a2df37 Binary files /dev/null and b/app/fonts/DIN_OT/DINOT-Medium 2.otf differ diff --git a/app/fonts/DIN_OT/DINOT-MediumItalic 2.otf b/app/fonts/DIN_OT/DINOT-MediumItalic 2.otf new file mode 100644 index 000000000..14eddfc76 Binary files /dev/null and b/app/fonts/DIN_OT/DINOT-MediumItalic 2.otf differ -- cgit v1.2.3 From 2d5c3394f493868a2eb5706e8b42127252c9b746 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Mon, 31 Jul 2017 20:46:56 -0700 Subject: Use DIN OT in wallet view --- ui/app/css/fonts.css | 16 +++++++++++----- ui/app/main-container.js | 3 ++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/ui/app/css/fonts.css b/ui/app/css/fonts.css index 3b9f581b9..2afaa26e1 100644 --- a/ui/app/css/fonts.css +++ b/ui/app/css/fonts.css @@ -8,7 +8,6 @@ font-weight: normal; font-style: normal; font-size: 'small'; - } @font-face { @@ -23,14 +22,21 @@ font-family: 'Montserrat Light'; src: url('/fonts/Montserrat/Montserrat-Light.woff') format('woff'); src: url('/fonts/Montserrat/Montserrat-Light.ttf') format('truetype'); - font-weight: normal; - font-style: normal; + font-weight: normal; + font-style: normal; } @font-face { font-family: 'Montserrat UltraLight'; src: url('/fonts/Montserrat/Montserrat-UltraLight.woff') format('woff'); src: url('/fonts/Montserrat/Montserrat-UltraLight.ttf') format('truetype'); - font-weight: normal; - font-style: normal; + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'DIN OT'; + src: url('/fonts/DIN_OT/DINOT-2.otf') format('opentype'); + font-weight: normal; + font-style: normal; } diff --git a/ui/app/main-container.js b/ui/app/main-container.js index f891402ac..cc61860b6 100644 --- a/ui/app/main-container.js +++ b/ui/app/main-container.js @@ -11,7 +11,7 @@ function MainContainer () { } MainContainer.prototype.render = function () { - console.log("rendering MainContainer..."); + return h('div.flex-row', { style: { position: 'absolute', @@ -19,6 +19,7 @@ MainContainer.prototype.render = function () { width: '98%', zIndex: 20, boxShadow: '0 0 7px 0 rgba(0,0,0,0.08)', + fontFamily: 'DIN OT', } }, [ h(WalletView, { -- cgit v1.2.3 From 92bd783e0c61e05772cc494a386bb5f21e9dbbb3 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Mon, 31 Jul 2017 20:54:04 -0700 Subject: Adjust button styles to left align with wallet text --- ui/app/components/wallet-view.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index 0c5bd5c4f..e61346290 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -102,23 +102,20 @@ WalletView.prototype.render = function () { style: { border: '1px solid rgb(91, 93, 103)', borderRadius: '2px', - height: '28px', - width: '70px', + height: '20px', + width: '50px', fontSize: '0.8em', textAlign: 'center', - lineHeight: '25px', - marginLeft: '.6em', } }, 'BUY'), h('div.wallet-btn', { style: { border: '1px solid rgb(91, 93, 103)', borderRadius: '2px', - height: '28px', - width: '70px', + height: '20px', + width: '50px', fontSize: '0.8em', textAlign: 'center', - lineHeight: '25px', marginLeft: '.6em', } }, 'SEND'), -- cgit v1.2.3 From c876428044c8e6eec300ceeb0d7ab0c44e68f8d3 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Mon, 31 Jul 2017 21:16:07 -0700 Subject: Add TxView, use width percentages instead of flex-grow for layout --- ui/app/components/tx-view.js | 53 ++++++++++++++++++++++++++++++++++++++++ ui/app/components/wallet-view.js | 2 +- ui/app/main-container.js | 11 ++++++--- 3 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 ui/app/components/tx-view.js diff --git a/ui/app/components/tx-view.js b/ui/app/components/tx-view.js new file mode 100644 index 000000000..b10589035 --- /dev/null +++ b/ui/app/components/tx-view.js @@ -0,0 +1,53 @@ +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const inherits = require('util').inherits +// const Identicon = require('./identicon') +// const AccountDropdowns = require('./account-dropdowns').AccountDropdowns +// const Content = require('./wallet-content-display') + +module.exports = connect()(TxView) + +// function mapStateToProps (state) { +// return { +// network: state.metamask.network, +// } +// } + +inherits(TxView, Component) +function TxView () { + Component.call(this) +} + +TxView.prototype.render = function () { + return h('div.tx-view.flex-column', { + style: { + width: '66.666%', + height: '82vh', + background: '#FFFFFF', + } + }, [ + h('div.flex-row', { + }, [ + // tab + h('div.flex-column', { + + }, [ + h('div', {}, 'Transactions'), + h('div', { + style: { + height: '0.5em', + color: 'black', + width: '100%', + } + }) + ]), + + // tab2 + ]) + ]) + // column + // tab row + // divider + // item +} diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index e61346290..b8ea633db 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -28,7 +28,7 @@ WalletView.prototype.render = function () { return h('div.wallet-view.flex-column', { style: { - flexGrow: 1, + width: '33.333%', height: '82vh', background: '#FAFAFA', // TODO: add to reusable colors } diff --git a/ui/app/main-container.js b/ui/app/main-container.js index cc61860b6..88028f8eb 100644 --- a/ui/app/main-container.js +++ b/ui/app/main-container.js @@ -2,6 +2,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits const WalletView = require('./components/wallet-view') +const TxView = require('./components/tx-view') module.exports = MainContainer @@ -20,6 +21,7 @@ MainContainer.prototype.render = function () { zIndex: 20, boxShadow: '0 0 7px 0 rgba(0,0,0,0.08)', fontFamily: 'DIN OT', + display: 'flex', } }, [ h(WalletView, { @@ -30,11 +32,12 @@ MainContainer.prototype.render = function () { }, [ ]), - h('div.tx-view', { + h(TxView, { style: { - width: '66.66%', - height: '82vh', - background: '#FFFFFF', + // flexGrow: 2 + // width: '66.66%', + // height: '82vh', + // background: '#FFFFFF', } }, [ ]), -- cgit v1.2.3 From c7ace5b23d911512495edbd4f0ecb8e0190bc537 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Mon, 31 Jul 2017 21:27:37 -0700 Subject: Add hyperscript for tx-view tabs --- ui/app/components/tx-view.js | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/ui/app/components/tx-view.js b/ui/app/components/tx-view.js index b10589035..06ee3bfc6 100644 --- a/ui/app/components/tx-view.js +++ b/ui/app/components/tx-view.js @@ -13,6 +13,15 @@ module.exports = connect()(TxView) // network: state.metamask.network, // } // } +// +const contentDivider = h('div', { + style: { + marginLeft: '1.3em', + marginRight: '1.3em', + height:'1px', + background:'#E7E7E7', // TODO: make custom color + }, +}) inherits(TxView, Component) function TxView () { @@ -28,23 +37,32 @@ TxView.prototype.render = function () { } }, [ h('div.flex-row', { + style: { + margin: '1.8em 1.3em', + } }, [ - // tab - h('div.flex-column', { + // tx-view-tab.js + h('div.flex-row', { }, [ - h('div', {}, 'Transactions'), + h('div', { style: { - height: '0.5em', - color: 'black', - width: '100%', + borderBottom: '0.07em solid black', + paddingBottom: '0.015em', } - }) - ]), + }, 'TRANSACTIONS'), + + h('div', { + style: { + marginLeft: '2em', + } + }, 'TOKENS'), - // tab2 + ]), ]) + + h('') ]) // column // tab row -- cgit v1.2.3 From ce06fbd36debac144b4f4bf1d3948b35332e9c41 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Mon, 31 Jul 2017 21:34:37 -0700 Subject: Add tx-view content divider component --- ui/app/components/tx-view.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ui/app/components/tx-view.js b/ui/app/components/tx-view.js index 06ee3bfc6..1bc828c20 100644 --- a/ui/app/components/tx-view.js +++ b/ui/app/components/tx-view.js @@ -13,7 +13,7 @@ module.exports = connect()(TxView) // network: state.metamask.network, // } // } -// + const contentDivider = h('div', { style: { marginLeft: '1.3em', @@ -38,7 +38,7 @@ TxView.prototype.render = function () { }, [ h('div.flex-row', { style: { - margin: '1.8em 1.3em', + margin: '1.8em 1.3em 0.8em 1.3em', } }, [ @@ -55,14 +55,15 @@ TxView.prototype.render = function () { h('div', { style: { - marginLeft: '2em', + marginLeft: '1.25em', } }, 'TOKENS'), ]), - ]) + ]), + + contentDivider, - h('') ]) // column // tab row -- cgit v1.2.3 From caab0b61ccb20a19fd97d4177698e9675d0b5451 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Mon, 31 Jul 2017 22:00:18 -0700 Subject: Add rough layout for tx list items --- ui/app/components/tx-view.js | 60 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/ui/app/components/tx-view.js b/ui/app/components/tx-view.js index 1bc828c20..c32e9edcb 100644 --- a/ui/app/components/tx-view.js +++ b/ui/app/components/tx-view.js @@ -64,9 +64,69 @@ TxView.prototype.render = function () { contentDivider, + this.renderTransactionListItem(), + + contentDivider, + + this.renderTransactionListItem(), + + contentDivider, + ]) // column // tab row // divider // item } + +TxView.prototype.renderTransactionListItem = function () { + return h('div.flex-column', { + style: { + alignItems: 'stretch', + margin: '0.6em 1.3em 0.6em 1.3em', + } + }, [ + + h('div', { + style: { + flexGrow: 1, + marginTop: '0.3em', + } + }, 'Jul 01, 2017'), + + h('div.flex-row', { + style: { + alignItems: 'stretch', + } + }, [ + + h('div', { + style: { + flexGrow: 1, + } + }, 'icon'), + + h('div', { + style: { + flexGrow: 3, + } + }, 'Hash'), + + h('div', { + style: { + flexGrow: 5, + } + }, 'Status'), + + h('div', { + style: { + flexGrow: 2, + } + }, 'Details'), + + ]) + + ]) +} + + -- cgit v1.2.3 From 9cc461a6c2348ffd1a884e0ca92d74294cce6b4e Mon Sep 17 00:00:00 2001 From: sdtsui Date: Mon, 31 Jul 2017 23:07:25 -0700 Subject: Reset popup to 350x500, old form factor as advised by @Zanibas --- app/popup.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/popup.html b/app/popup.html index de0e100a5..fddf01841 100644 --- a/app/popup.html +++ b/app/popup.html @@ -5,7 +5,7 @@ MetaMask Plugin - +
-- cgit v1.2.3 From a7fc5126502a9c69aaa727178997ea4ed703c2d6 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Mon, 31 Jul 2017 23:07:58 -0700 Subject: Implement mobile-friendly responsive layout with flex wrap --- ui/app/app.js | 4 +++- ui/app/components/tx-view.js | 6 +++++- ui/app/components/wallet-view.js | 5 ++++- ui/app/main-container.js | 5 ++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index 4f877bc51..021ef5f27 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -73,7 +73,9 @@ App.prototype.render = function () { h('.flex-column.full-height', { style: { // Windows was showing a vertical scroll bar: - overflow: 'hidden', + overflowX: 'hidden', + // TODO: check with dev who committed L75, see if this still happens, and whether auto is enough + // overflowY: 'auto', position: 'relative', alignItems: 'center', }, diff --git a/ui/app/components/tx-view.js b/ui/app/components/tx-view.js index c32e9edcb..bcd30e6d8 100644 --- a/ui/app/components/tx-view.js +++ b/ui/app/components/tx-view.js @@ -31,7 +31,11 @@ function TxView () { TxView.prototype.render = function () { return h('div.tx-view.flex-column', { style: { - width: '66.666%', + // width: '66.666%', + flexGrow: 2, + flexShrink: 0, + flexBasis: '230px', // .666*345 + // flexBasis: '400px', height: '82vh', background: '#FFFFFF', } diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index b8ea633db..60c2cb5c6 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -28,7 +28,10 @@ WalletView.prototype.render = function () { return h('div.wallet-view.flex-column', { style: { - width: '33.333%', + // width: '33.333%', + flexGrow: 1, + flexShrink: 0, + flexBasis: '230px', // .333*345 height: '82vh', background: '#FAFAFA', // TODO: add to reusable colors } diff --git a/ui/app/main-container.js b/ui/app/main-container.js index 88028f8eb..ae62a0e0c 100644 --- a/ui/app/main-container.js +++ b/ui/app/main-container.js @@ -13,7 +13,7 @@ function MainContainer () { MainContainer.prototype.render = function () { - return h('div.flex-row', { + return h('div', { style: { position: 'absolute', marginTop: '6vh', @@ -22,6 +22,9 @@ MainContainer.prototype.render = function () { boxShadow: '0 0 7px 0 rgba(0,0,0,0.08)', fontFamily: 'DIN OT', display: 'flex', + flexWrap: 'wrap', + alignItems: 'stretch', + overflowY: 'scroll', } }, [ h(WalletView, { -- cgit v1.2.3 From 6f4bee45997862b3ca52785b9d62489969f070f5 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Mon, 31 Jul 2017 23:21:11 -0700 Subject: Hook up send button to Send Token screen --- ui/app/components/wallet-view.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index 60c2cb5c6..091a5cd7c 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -5,8 +5,9 @@ const inherits = require('util').inherits const Identicon = require('./identicon') const AccountDropdowns = require('./account-dropdowns').AccountDropdowns const Content = require('./wallet-content-display') +const actions = require('../actions') -module.exports = connect(mapStateToProps)(WalletView) +module.exports = connect(mapStateToProps, mapDispatchToProps)(WalletView) function mapStateToProps (state) { return { @@ -14,6 +15,12 @@ function mapStateToProps (state) { } } +function mapDispatchToProps (dispatch) { + return { + showSendPage: () => {dispatch(actions.showSendPage())}, + } +} + inherits(WalletView, Component) function WalletView () { @@ -112,6 +119,10 @@ WalletView.prototype.render = function () { } }, 'BUY'), h('div.wallet-btn', { + onClick: () => { + console.log("SHOW"); + this.props.showSendPage(); + }, style: { border: '1px solid rgb(91, 93, 103)', borderRadius: '2px', -- cgit v1.2.3 From 4115c25d8f2e186a575de7904a91b3717da5e800 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 1 Aug 2017 13:21:02 -0700 Subject: lint fix --- ui/app/app.js | 2 +- ui/app/components/account-dropdowns.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index 8fad0f7d6..297a2f621 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -203,7 +203,7 @@ App.prototype.renderNetworkDropdown = function () { classList.contains('menu-icon'), classList.contains('network-name'), classList.contains('network-indicator'), - ].filter(bool => bool).length === 0; + ].filter(bool => bool).length === 0 // classes from three constituent nodes of the toggle element if (isNotToggleElement) { diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index 2813f4752..4ef9a5c14 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -17,8 +17,8 @@ class AccountDropdowns extends Component { accountSelectorActive: false, optionsMenuActive: false, } - this.accountSelectorToggleClassName = 'fa-angle-down'; - this.optionsMenuToggleClassName = 'fa-ellipsis-h'; + this.accountSelectorToggleClassName = 'fa-angle-down' + this.optionsMenuToggleClassName = 'fa-ellipsis-h' } renderAccounts () { -- cgit v1.2.3 From 9d345e744d61d94879f3c01a263aa9cf73afa36b Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 1 Aug 2017 16:58:40 -0700 Subject: blacklist - clearer test format --- test/unit/blacklister-test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/unit/blacklister-test.js b/test/unit/blacklister-test.js index 1badc2c8f..47e9b3c6b 100644 --- a/test/unit/blacklister-test.js +++ b/test/unit/blacklister-test.js @@ -5,19 +5,19 @@ describe('blacklister', function () { describe('#isPhish', function () { it('should not flag whitelisted values', function () { var result = isPhish({ hostname: 'www.metamask.io' }) - assert(!result) + assert.equal(result, false) }) it('should flag explicit values', function () { var result = isPhish({ hostname: 'metamask.com' }) - assert(result) + assert.equal(result, true) }) it('should flag levenshtein values', function () { - var result = isPhish({ hostname: 'metmask.com' }) - assert(result) + var result = isPhish({ hostname: 'metmask.io' }) + assert.equal(result, true) }) it('should not flag not-even-close values', function () { var result = isPhish({ hostname: 'example.com' }) - assert(!result) + assert.equal(result, false) }) }) }) -- cgit v1.2.3 From 9eb13aee0046405bb304a2e31dbf7712e8b1da21 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 1 Aug 2017 17:05:36 -0700 Subject: blacklist - add tests for metamask subdomains --- test/unit/blacklister-test.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/unit/blacklister-test.js b/test/unit/blacklister-test.js index 47e9b3c6b..ce110491c 100644 --- a/test/unit/blacklister-test.js +++ b/test/unit/blacklister-test.js @@ -19,6 +19,18 @@ describe('blacklister', function () { var result = isPhish({ hostname: 'example.com' }) assert.equal(result, false) }) + it('should not flag the ropsten faucet domains', function () { + var result = isPhish({ hostname: 'faucet.metamask.io' }) + assert.equal(result, false) + }) + it('should not flag the mascara domain', function () { + var result = isPhish({ hostname: 'zero.metamask.io' }) + assert.equal(result, false) + }) + it('should not flag the mascara-faucet domain', function () { + var result = isPhish({ hostname: 'zero-faucet.metamask.io' }) + assert.equal(result, false) + }) }) }) -- cgit v1.2.3 From 432f516ab005dd2b4eb4b2e8766ed30216386d98 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 26 Jul 2017 14:56:52 -0400 Subject: make addUnapprovedTransaction async function and use promise based ethQuery --- app/scripts/controllers/transactions.js | 296 +++++++++++++++----------------- app/scripts/lib/tx-utils.js | 61 ++----- app/scripts/metamask-controller.js | 29 ++-- package.json | 1 + test/unit/tx-controller-test.js | 72 +++++--- test/unit/tx-utils-test.js | 1 + 6 files changed, 219 insertions(+), 241 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 5f2d75b47..d3e852ef9 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -1,10 +1,11 @@ const EventEmitter = require('events') const async = require('async') const extend = require('xtend') +const pify = require('pify') const clone = require('clone') const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') -const pify = require('pify') +const EthQuery = require('ethjs-query'); const TxProviderUtil = require('../lib/tx-utils') const getStack = require('../lib/util').getStack const createId = require('../lib/random-id') @@ -33,7 +34,7 @@ module.exports = class TransactionController extends EventEmitter { }) }, }) - this.query = opts.ethQuery + this.query = new EthQuery(this.provider) this.txProviderUtils = new TxProviderUtil(this.query) this.blockTracker.on('rawBlock', this.checkForTxInBlock.bind(this)) // this is a little messy but until ethstore has been either @@ -62,13 +63,6 @@ module.exports = class TransactionController extends EventEmitter { return this.preferencesStore.getState().selectedAddress } - // Returns the tx list - getTxList () { - const network = this.getNetwork() - const fullTxList = this.getFullTxList() - return fullTxList.filter(txMeta => txMeta.metamaskNetworkId === network) - } - // Returns the number of txs for the current network. getTxCount () { return this.getTxList().length @@ -79,6 +73,50 @@ module.exports = class TransactionController extends EventEmitter { return this.store.getState().transactions } + get unapprovedTxCount () { + return Object.keys(this.getUnapprovedTxList()).length + } + + get pendingTxCount () { + return this.getTxsByMetaData('status', 'signed').length + } + + // Returns the tx list + getTxList () { + const network = this.getNetwork() + const fullTxList = this.getFullTxList() + return this.getTxsByMetaData('metamaskNetworkId', network, fullTxList) + } + + // gets tx by Id and returns it + getTx (txId) { + const txList = this.getTxList() + const txMeta = txList.find(txData => txData.id === txId) + return txMeta + } + getUnapprovedTxList () { + let txList = this.getTxList() + return txList.filter((txMeta) => txMeta.status === 'unapproved') + .reduce((result, tx) => { + result[tx.id] = tx + return result + }, {}) + } + + updateTx (txMeta) { + const txMetaForHistory = clone(txMeta) + txMetaForHistory.stack = getStack() + const txId = txMeta.id + const txList = this.getFullTxList() + const index = txList.findIndex(txData => txData.id === txId) + if (!txMeta.history) txMeta.history = [] + txMeta.history.push(txMetaForHistory) + + txList[index] = txMeta + this._saveTxList(txList) + this.emit('update') + } + // Adds a tx to the txlist addTx (txMeta) { const txCount = this.getTxCount() @@ -92,7 +130,7 @@ module.exports = class TransactionController extends EventEmitter { // or rejected tx's. // not tx's that are pending or unapproved if (txCount > txHistoryLimit - 1) { - var index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId)) + let index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId)) fullTxList.splice(index, 1) } fullTxList.push(txMeta) @@ -110,86 +148,40 @@ module.exports = class TransactionController extends EventEmitter { this.emit(`${txMeta.id}:unapproved`, txMeta) } - // gets tx by Id and returns it - getTx (txId, cb) { - var txList = this.getTxList() - var txMeta = txList.find(txData => txData.id === txId) - return cb ? cb(txMeta) : txMeta - } - - // - updateTx (txMeta) { - const txMetaForHistory = clone(txMeta) - txMetaForHistory.stack = getStack() - var txId = txMeta.id - var txList = this.getFullTxList() - var index = txList.findIndex(txData => txData.id === txId) - if (!txMeta.history) txMeta.history = [] - txMeta.history.push(txMetaForHistory) - - txList[index] = txMeta - this._saveTxList(txList) - this.emit('update') - } - - get unapprovedTxCount () { - return Object.keys(this.getUnapprovedTxList()).length - } - - get pendingTxCount () { - return this.getTxsByMetaData('status', 'signed').length - } - - addUnapprovedTransaction (txParams, done) { - let txMeta = {} - async.waterfall([ - // validate - (cb) => this.txProviderUtils.validateTxParams(txParams, cb), - // construct txMeta - (cb) => { - txMeta = { - id: createId(), - time: (new Date()).getTime(), - status: 'unapproved', - metamaskNetworkId: this.getNetwork(), - txParams: txParams, - history: [], - } - cb() - }, - // add default tx params - (cb) => this.addTxDefaults(txMeta, cb), - // save txMeta - (cb) => { - this.addTx(txMeta) - cb(null, txMeta) - }, - ], done) + async addUnapprovedTransaction (txParams) { + // validate + await this.txProviderUtils.validateTxParams(txParams) + // construct txMeta + const txMeta = { + id: createId(), + time: (new Date()).getTime(), + status: 'unapproved', + metamaskNetworkId: this.getNetwork(), + txParams: txParams, + history: [], + } + // add default tx params + await this.addTxDefaults(txMeta), + // save txMeta + this.addTx(txMeta) + return txMeta } - addTxDefaults (txMeta, cb) { + async addTxDefaults (txMeta) { const txParams = txMeta.txParams // ensure value txParams.value = txParams.value || '0x0' if (!txParams.gasPrice) { - this.query.gasPrice((err, gasPrice) => { - - if (err) return cb(err) - // set gasPrice - txParams.gasPrice = gasPrice - }) + gassPrice = await this.query.gasPrice() + txParams.gasPrice = gasPrice } // set gasLimit - this.txProviderUtils.analyzeGasUsage(txMeta, cb) + return await this.txProviderUtils.analyzeGasUsage(txMeta) } - getUnapprovedTxList () { - var txList = this.getTxList() - return txList.filter((txMeta) => txMeta.status === 'unapproved') - .reduce((result, tx) => { - result[tx.id] = tx - return result - }, {}) + async updateAndApproveTransaction (txMeta) { + this.updateTx(txMeta) + await this.approveTransaction(txMeta.id) } async approveTransaction (txId) { @@ -221,26 +213,6 @@ module.exports = class TransactionController extends EventEmitter { } } - cancelTransaction (txId, cb = warn) { - this.setTxStatusRejected(txId) - cb() - } - - async updateAndApproveTransaction (txMeta) { - this.updateTx(txMeta) - await this.approveTransaction(txMeta.id) - } - - getChainId () { - const networkState = this.networkStore.getState() - const getChainId = parseInt(networkState) - if (Number.isNaN(getChainId)) { - return 0 - } else { - return getChainId - } - } - async signTransaction (txId) { const txMeta = this.getTx(txId) const txParams = txMeta.txParams @@ -265,6 +237,22 @@ module.exports = class TransactionController extends EventEmitter { }) } + cancelTransaction (txId) { + this.setTxStatusRejected(txId) + return Promise.resolve() + } + + + getChainId () { + const networkState = this.networkStore.getState() + const getChainId = parseInt(networkState) + if (Number.isNaN(getChainId)) { + return 0 + } else { + return getChainId + } + } + // receives a txHash records the tx as signed setTxHash (txId, txHash) { // Add the tx hash to the persisted meta-tx object @@ -275,7 +263,7 @@ module.exports = class TransactionController extends EventEmitter { /* Takes an object of fields to search for eg: - var thingsToLookFor = { + let thingsToLookFor = { to: '0x0..', from: '0x0..', status: 'signed', @@ -298,7 +286,7 @@ module.exports = class TransactionController extends EventEmitter { and that have been 'confirmed' */ getFilteredTxList (opts) { - var filteredTxList + let filteredTxList Object.keys(opts).forEach((key) => { filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList) }) @@ -359,7 +347,7 @@ module.exports = class TransactionController extends EventEmitter { // merges txParams obj onto txData.txParams // use extend to ensure that all fields are filled updateTxParams (txId, txParams) { - var txMeta = this.getTx(txId) + const txMeta = this.getTx(txId) txMeta.txParams = extend(txMeta.txParams, txParams) this.updateTx(txMeta) } @@ -367,20 +355,19 @@ module.exports = class TransactionController extends EventEmitter { // checks if a signed tx is in a block and // if included sets the tx status as 'confirmed' checkForTxInBlock (block) { - var signedTxList = this.getFilteredTxList({status: 'submitted'}) + const signedTxList = this.getFilteredTxList({status: 'submitted'}) if (!signedTxList.length) return signedTxList.forEach((txMeta) => { - var txHash = txMeta.hash - var txId = txMeta.id + const txHash = txMeta.hash + const txId = txMeta.id if (!txHash) { - return this.setTxStatusFailed(txId, { - stack: 'checkForTxInBlock: custom tx-controller error message', - errCode: 'No hash was provided', - message: 'We had an error while submitting this transaction, please try again.', - }) + const noTxHash = new Error('We had an error while submitting this transaction, please try again.') + noTxHash.name = 'NoTxHashError' + this.setTxStatusFailed(noTxHash) } + block.transactions.forEach((tx) => { if (tx.hash === txHash) this.setTxStatusConfirmed(txId) }) @@ -398,6 +385,39 @@ module.exports = class TransactionController extends EventEmitter { if (diff > 1) this._checkPendingTxs() } + resubmitPendingTxs () { + const pending = this.getTxsByMetaData('status', 'submitted') + // only try resubmitting if their are transactions to resubmit + if (!pending.length) return + pending.forEach((txMeta) => this._resubmitTx(txMeta).catch((err) => { + /* + Dont marked as failed if the error is a "known" transaction warning + "there is already a transaction with the same sender-nonce + but higher/same gas price" + */ + const errorMessage = err.message.toLowerCase() + const isKnownTx = ( + // geth + errorMessage.includes('replacement transaction underpriced') + || errorMessage.includes('known transaction') + // parity + || errorMessage.includes('gas price too low to replace') + || errorMessage.includes('transaction with the same hash was already imported') + // other + || errorMessage.includes('gateway timeout') + || errorMessage.includes('nonce too low') + ) + // ignore resubmit warnings, return early + if (isKnownTx) return + // encountered real error - transition to error state + this.setTxStatusFailed(txMeta.id, { + stack: err.stack || err.message, + errCode: err.errCode || err, + message: err.message, + }) + })) + } + // PRIVATE METHODS // Should find the tx in the tx list and @@ -411,7 +431,7 @@ module.exports = class TransactionController extends EventEmitter { // - `'confirmed'` the tx has been included in a block. // - `'failed'` the tx failed for some reason, included on tx data. _setTxStatus (txId, status) { - var txMeta = this.getTx(txId) + const txMeta = this.getTx(txId) txMeta.status = status this.emit(`${txMeta.id}:${status}`, txId) if (status === 'submitted' || status === 'rejected') { @@ -436,39 +456,6 @@ module.exports = class TransactionController extends EventEmitter { this.memStore.updateState({ unapprovedTxs, selectedAddressTxList }) } - resubmitPendingTxs () { - const pending = this.getTxsByMetaData('status', 'submitted') - // only try resubmitting if their are transactions to resubmit - if (!pending.length) return - pending.forEach((txMeta) => this._resubmitTx(txMeta).catch((err) => { - /* - Dont marked as failed if the error is a "known" transaction warning - "there is already a transaction with the same sender-nonce - but higher/same gas price" - */ - const errorMessage = err.message.toLowerCase() - const isKnownTx = ( - // geth - errorMessage.includes('replacement transaction underpriced') - || errorMessage.includes('known transaction') - // parity - || errorMessage.includes('gas price too low to replace') - || errorMessage.includes('transaction with the same hash was already imported') - // other - || errorMessage.includes('gateway timeout') - || errorMessage.includes('nonce too low') - ) - // ignore resubmit warnings, return early - if (isKnownTx) return - // encountered real error - transition to error state - this.setTxStatusFailed(txMeta.id, { - stack: err.stack || err.message, - errCode: err.errCode || err, - message: err.message, - }) - })) - } - async _resubmitTx (txMeta) { const address = txMeta.txParams.from const balance = this.ethStore.getState().accounts[address].balance @@ -515,17 +502,14 @@ module.exports = class TransactionController extends EventEmitter { // extra check in case there was an uncaught error during the // signature and submission process if (!txHash) { - this.setTxStatusFailed(txId, { - stack: '_checkPendingTxs: custom tx-controller error message', - errCode: 'No hash was provided', - message: 'We had an error while submitting this transaction, please try again.', - }) - return + const noTxHash = new Error('We had an error while submitting this transaction, please try again.') + noTxHash.name = 'NoTxHashError' + this.setTxStatusFailed(noTxHash) } // get latest transaction status let txParams try { - txParams = await pify((cb) => this.query.getTransactionByHash(txHash, cb))() + txParams = await this.query.getTransactionByHash(txHash) if (!txParams) return if (txParams.blockNumber) { this.setTxStatusConfirmed(txId) @@ -538,12 +522,8 @@ module.exports = class TransactionController extends EventEmitter { message: 'There was a problem loading this transaction.', } this.updateTx(txMeta) - log.error(err) + throw err } } } - -} - - -const warn = () => log.warn('warn was used no cb provided') +} \ No newline at end of file diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js index 8f6943937..43928feaf 100644 --- a/app/scripts/lib/tx-utils.js +++ b/app/scripts/lib/tx-utils.js @@ -10,24 +10,18 @@ its passed ethquery and used to do things like calculate gas of a tx. */ -module.exports = class txProviderUtils { - +module.exports = class txProvideUtils { constructor (ethQuery) { this.query = ethQuery } - analyzeGasUsage (txMeta, cb) { - var self = this - this.query.getBlockByNumber('latest', true, (err, block) => { - if (err) return cb(err) - async.waterfall([ - self.estimateTxGas.bind(self, txMeta, block.gasLimit), - self.setTxGas.bind(self, txMeta, block.gasLimit), - ], cb) - }) + async analyzeGasUsage (txMeta) { + const block = await this.query.getBlockByNumber('latest', true) + const estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit) + this.setTxGas(txMeta, block.gasLimit, estimatedGasHex) } - estimateTxGas (txMeta, blockGasLimitHex, cb) { + async estimateTxGas (txMeta, blockGasLimitHex) { const txParams = txMeta.txParams // check if gasLimit is already specified txMeta.gasLimitSpecified = Boolean(txParams.gas) @@ -38,10 +32,10 @@ module.exports = class txProviderUtils { txParams.gas = bnToHex(saferGasLimitBN) } // run tx, see if it will OOG - this.query.estimateGas(txParams, cb) + return await this.query.estimateGas(txParams) } - setTxGas (txMeta, blockGasLimitHex, estimatedGasHex, cb) { + setTxGas (txMeta, blockGasLimitHex, estimatedGasHex) { txMeta.estimatedGas = estimatedGasHex const txParams = txMeta.txParams @@ -49,14 +43,12 @@ module.exports = class txProviderUtils { // use original specified amount if (txMeta.gasLimitSpecified) { txMeta.estimatedGas = txParams.gas - cb() return } // if gasLimit not originally specified, // try adding an additional gas buffer to our estimation for safety const recommendedGasHex = this.addGasBuffer(txMeta.estimatedGas, blockGasLimitHex) txParams.gas = recommendedGasHex - cb() return } @@ -74,22 +66,6 @@ module.exports = class txProviderUtils { return bnToHex(upperGasLimitBn) } - fillInTxParams (txParams, cb) { - const fromAddress = txParams.from - const reqs = {} - - if (isUndef(txParams.gas)) reqs.gas = (cb) => this.query.estimateGas(txParams, cb) - if (isUndef(txParams.gasPrice)) reqs.gasPrice = (cb) => this.query.gasPrice(cb) - if (isUndef(txParams.nonce)) reqs.nonce = (cb) => this.query.getTransactionCount(fromAddress, 'pending', cb) - - async.parallel(reqs, function (err, result) { - if (err) return cb(err) - // write results to txParams obj - Object.assign(txParams, result) - cb() - }) - } - // builds ethTx from txParams object buildEthTxFromParams (txParams) { // normalize values @@ -107,20 +83,17 @@ module.exports = class txProviderUtils { } publishTransaction (rawTx) { - return new Promise((resolve, reject) => { - this.query.sendRawTransaction(rawTx, (err, ress) => { - if (err) reject(err) - else resolve(ress) - }) - }) + return this.query.sendRawTransaction(rawTx) } - validateTxParams (txParams, cb) { - if (('value' in txParams) && txParams.value.indexOf('-') === 0) { - cb(new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)) - } else { - cb() - } + validateTxParams (txParams) { + return new Promise ((resolve, reject) => { + if (('value' in txParams) && txParams.value.indexOf('-') === 0) { + reject(new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)) + } else { + resolve() + } + }) } sufficientBalance (txParams, hexBalance) { diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 11dcde2c1..f7c92e618 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -195,7 +195,7 @@ module.exports = class MetamaskController extends EventEmitter { cb(null, result) }, // tx signing - processTransaction: (txParams, cb) => this.newUnapprovedTransaction(txParams, cb), + processTransaction: nodeify(this.newUnapprovedTransaction, this), // old style msg signing processMessage: this.newUnsignedMessage.bind(this), @@ -308,7 +308,7 @@ module.exports = class MetamaskController extends EventEmitter { exportAccount: nodeify(keyringController.exportAccount, keyringController), // txController - cancelTransaction: txController.cancelTransaction.bind(txController), + cancelTransaction: nodeify(txController.cancelTransaction, txController), updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController), // messageManager @@ -440,22 +440,21 @@ module.exports = class MetamaskController extends EventEmitter { // Identity Management // - newUnapprovedTransaction (txParams, cb) { + async newUnapprovedTransaction (txParams) { log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) - const self = this - self.txController.addUnapprovedTransaction(txParams, (err, txMeta) => { - if (err) return cb(err) - self.sendUpdate() - self.opts.showUnapprovedTx(txMeta) - // listen for tx completion (success, fail) - self.txController.once(`${txMeta.id}:finished`, (completedTx) => { + const txMeta = await this.txController.addUnapprovedTransaction(txParams) + this.sendUpdate() + this.opts.showUnapprovedTx(txMeta) + // listen for tx completion (success, fail) + return new Promise ((resolve, reject) => { + this.txController.once(`${txMeta.id}:finished`, (completedTx) => { switch (completedTx.status) { case 'submitted': - return cb(null, completedTx.hash) + return reoslve(completedTx.hash) case 'rejected': - return cb(new Error('MetaMask Tx Signature: User denied transaction signature.')) + return reject(new Error('MetaMask Tx Signature: User denied transaction signature.')) default: - return cb(new Error(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(completedTx.txParams)}`)) + return reject(new Error(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(completedTx.txParams)}`)) } }) }) @@ -646,6 +645,4 @@ module.exports = class MetamaskController extends EventEmitter { return Promise.resolve(rpcTarget) }) } - - -} +} \ No newline at end of file diff --git a/package.json b/package.json index dcd25cda6..94232d46d 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", "ethjs-ens": "^2.0.0", + "ethjs-query": "^0.2.6", "express": "^4.14.0", "extension-link-enabler": "^1.0.0", "extensionizer": "^1.0.0", diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index 31908569a..6e42e8e68 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -1,7 +1,6 @@ const assert = require('assert') const ethUtil = require('ethereumjs-util') const EthTx = require('ethereumjs-tx') -const EthQuery = require('eth-query') const ObservableStore = require('obs-store') const clone = require('clone') const sinon = require('sinon') @@ -11,7 +10,7 @@ const currentNetworkId = 42 const otherNetworkId = 36 const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex') -describe('Transaction Controller', function () { +describe.only('Transaction Controller', function () { let txController beforeEach(function () { @@ -20,7 +19,6 @@ describe('Transaction Controller', function () { txHistoryLimit: 10, blockTracker: { getCurrentBlock: noop, on: noop, once: noop }, provider: { sendAsync: noop }, - ethQuery: new EthQuery({ sendAsync: noop }), ethStore: { getState: noop }, signTransaction: (ethTx) => new Promise((resolve) => { ethTx.sign(privKey) @@ -28,24 +26,59 @@ describe('Transaction Controller', function () { }), }) txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop }) + const queryStubResult = {} + txController.query = new Proxy({}, { + get: (_, key) => { + if (key === 'stubResult') { + return function (method, ...args) { + queryStubResult[method] = args + } + } else { + returnValue = queryStubResult[key] + return () => Promise.resolve(...returnValue) + } + }, + }) + }) + + describe('#addUnapprovedTransaction', function () { + it('should add an unapproved transaction and return a valid txMeta', function (done) { + const addTxDefaultsStub = sinon.stub(txController, 'addTxDefaults').callsFake(() => Promise.resolve) + txController.addUnapprovedTransaction({}) + .then((txMeta) => { + assert(('id' in txMeta), 'should have a id') + assert(('time' in txMeta), 'should have a time stamp') + assert(('metamaskNetworkId' in txMeta), 'should have a metamaskNetworkId') + assert(('txParams' in txMeta), 'should have a txParams') + assert(('history' in txMeta), 'should have a history') + + const memTxMeta = txController.getTx(txMeta.id) + assert.deepEqual(txMeta, memTxMeta, `txMeta should be stored in txController after adding it\n expected: ${txMeta} \n got: ${memTxMeta}`) + addTxDefaultsStub.restore() + done() + }).catch(done) + }) }) describe('#validateTxParams', function () { - it('returns null for positive values', function () { + it('does not throw for positive values', function (done) { var sample = { value: '0x01', } - txController.txProviderUtils.validateTxParams(sample, (err) => { - assert.equal(err, null, 'no error') + txController.txProviderUtils.validateTxParams(sample).then(() => { + done() }) }) - it('returns error for negative values', function () { + it('returns error for negative values', function (done) { var sample = { value: '-0x01', } - txController.txProviderUtils.validateTxParams(sample, (err) => { + txController.txProviderUtils.validateTxParams(sample) + .then(() => done('expected to thrown on negativity values but didn\'t')) + .catch((err) => { assert.ok(err, 'error') + done() }) }) }) @@ -56,9 +89,6 @@ describe('Transaction Controller', function () { assert.ok(Array.isArray(result)) assert.equal(result.length, 0) }) - it('should also return transactions from local storage if any', function () { - - }) }) describe('#addTx', function () { @@ -276,16 +306,15 @@ describe('Transaction Controller', function () { txController.addTx(txMeta) - const estimateStub = sinon.stub(txController.txProviderUtils.query, 'estimateGas') - .callsArgWithAsync(1, null, wrongValue) - - const priceStub = sinon.stub(txController.txProviderUtils.query, 'gasPrice') - .callsArgWithAsync(0, null, wrongValue) + txController.query.stubResult('estimateGas', wrongValue) + txController.query.stubResult('gasPrice', wrongValue) + const signStub = sinon.stub(txController, 'signTransaction').callsFake(() => Promise.resolve()) - const signStub = sinon.stub(txController, 'signTransaction', () => Promise.resolve()) - - const pubStub = sinon.stub(txController.txProviderUtils, 'publishTransaction', () => Promise.resolve(originalValue)) + const pubStub = sinon.stub(txController, 'publishTransaction').callsFake(() => { + txController.setTxHash('1', originalValue) + txController.setTxStatusSubmitted('1') + }) txController.approveTransaction(txMeta.id).then(() => { const result = txController.getTx(txMeta.id) @@ -294,9 +323,6 @@ describe('Transaction Controller', function () { assert.equal(params.gas, originalValue, 'gas unmodified') assert.equal(params.gasPrice, originalValue, 'gas price unmodified') assert.equal(result.hash, originalValue, `hash was set \n got: ${result.hash} \n expected: ${originalValue}`) - - estimateStub.restore() - priceStub.restore() signStub.restore() pubStub.restore() done() @@ -352,7 +378,7 @@ describe('Transaction Controller', function () { }) .catch((err) => { assert.ifError(err, 'should not throw an error') - done() + done(err) }) }) }) diff --git a/test/unit/tx-utils-test.js b/test/unit/tx-utils-test.js index a43bcfb35..43807922a 100644 --- a/test/unit/tx-utils-test.js +++ b/test/unit/tx-utils-test.js @@ -1,4 +1,5 @@ const assert = require('assert') +const sinon = require('sinon') const ethUtil = require('ethereumjs-util') const BN = ethUtil.BN -- cgit v1.2.3 From 21e76484d628d7861b09f013116121101e67334c Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 2 Aug 2017 11:34:45 -0400 Subject: add test for addTxDefaults --- test/unit/tx-controller-test.js | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index 6e42e8e68..fc70da9a2 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -5,12 +5,13 @@ const ObservableStore = require('obs-store') const clone = require('clone') const sinon = require('sinon') const TransactionController = require('../../app/scripts/controllers/transactions') +const TxProvideUtils = require('../../app/scripts/lib/tx-utils') const noop = () => true const currentNetworkId = 42 const otherNetworkId = 36 const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex') -describe.only('Transaction Controller', function () { +describe('Transaction Controller', function () { let txController beforeEach(function () { @@ -26,19 +27,19 @@ describe.only('Transaction Controller', function () { }), }) txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop }) - const queryStubResult = {} txController.query = new Proxy({}, { - get: (_, key) => { + get: (queryStubResult, key) => { if (key === 'stubResult') { return function (method, ...args) { queryStubResult[method] = args } } else { - returnValue = queryStubResult[key] - return () => Promise.resolve(...returnValue) + const returnValues = queryStubResult[key] + return () => Promise.resolve(...returnValues) } }, }) + txController.txProviderUtils = new TxProvideUtils(txController.query) }) describe('#addUnapprovedTransaction', function () { @@ -60,6 +61,30 @@ describe.only('Transaction Controller', function () { }) }) + describe('#addTxDefaults', function () { + it('should add the tx defaults if their are none', function (done) { + let txMeta = { + 'txParams': { + 'from':'0xc684832530fcbddae4b4230a47e991ddcec2831d', + 'to':'0xc684832530fcbddae4b4230a47e991ddcec2831d', + }, + } + + txController.query.stubResult('gasPrice', '0x4a817c800') + txController.query.stubResult('getBlockByNumber', { gasLimit: '0x47b784' }) + txController.query.stubResult('estimateGas', '0x5209') + + txController.addTxDefaults(txMeta) + .then((txMetaWithDefaults) => { + assert(txMetaWithDefaults.txParams.value, '0x0','should have added 0x0 as the value') + assert(txMetaWithDefaults.txParams.gasPrice, 'should have added the gas price') + assert(txMetaWithDefaults.txParams.gas, 'should have added the gas field') + done() + }) + .catch(done) + }) + }) + describe('#validateTxParams', function () { it('does not throw for positive values', function (done) { var sample = { -- cgit v1.2.3 From 3a4726018e43157909f8c04e03f33cee2584795a Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 2 Aug 2017 11:35:35 -0400 Subject: fix addTxDefaults --- app/scripts/controllers/transactions.js | 2 +- app/scripts/lib/tx-utils.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index d3e852ef9..43dfb9360 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -172,7 +172,7 @@ module.exports = class TransactionController extends EventEmitter { // ensure value txParams.value = txParams.value || '0x0' if (!txParams.gasPrice) { - gassPrice = await this.query.gasPrice() + const gasPrice = await this.query.gasPrice() txParams.gasPrice = gasPrice } // set gasLimit diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js index 43928feaf..5b9fe0167 100644 --- a/app/scripts/lib/tx-utils.js +++ b/app/scripts/lib/tx-utils.js @@ -19,6 +19,7 @@ module.exports = class txProvideUtils { const block = await this.query.getBlockByNumber('latest', true) const estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit) this.setTxGas(txMeta, block.gasLimit, estimatedGasHex) + return txMeta } async estimateTxGas (txMeta, blockGasLimitHex) { @@ -32,7 +33,7 @@ module.exports = class txProvideUtils { txParams.gas = bnToHex(saferGasLimitBN) } // run tx, see if it will OOG - return await this.query.estimateGas(txParams) + return this.query.estimateGas(txParams) } setTxGas (txMeta, blockGasLimitHex, estimatedGasHex) { -- cgit v1.2.3 From ece9200c72cd0b53ee237263933b2b9a24ae5802 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 2 Aug 2017 11:36:07 -0400 Subject: fix spelling mistake --- app/scripts/metamask-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index f7c92e618..af4ea32e3 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -450,7 +450,7 @@ module.exports = class MetamaskController extends EventEmitter { this.txController.once(`${txMeta.id}:finished`, (completedTx) => { switch (completedTx.status) { case 'submitted': - return reoslve(completedTx.hash) + return resolve(completedTx.hash) case 'rejected': return reject(new Error('MetaMask Tx Signature: User denied transaction signature.')) default: -- cgit v1.2.3 From 25bc15ba175091513f6d281e9a16f3643fe4b18d Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 2 Aug 2017 11:47:13 -0400 Subject: lint fixes --- app/scripts/controllers/transactions.js | 6 ++---- app/scripts/lib/tx-utils.js | 7 +------ app/scripts/metamask-controller.js | 2 +- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 8855dfd5b..0c63a5647 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -1,11 +1,9 @@ const EventEmitter = require('events') -const async = require('async') const extend = require('xtend') -const pify = require('pify') const clone = require('clone') const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') -const EthQuery = require('ethjs-query'); +const EthQuery = require('ethjs-query') const TxProviderUtil = require('../lib/tx-utils') const getStack = require('../lib/util').getStack const createId = require('../lib/random-id') @@ -168,7 +166,7 @@ module.exports = class TransactionController extends EventEmitter { history: [], } // add default tx params - await this.addTxDefaults(txMeta), + await this.addTxDefaults(txMeta) // save txMeta this.addTx(txMeta) return txMeta diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js index 5b9fe0167..24ea2d763 100644 --- a/app/scripts/lib/tx-utils.js +++ b/app/scripts/lib/tx-utils.js @@ -1,4 +1,3 @@ -const async = require('async') const ethUtil = require('ethereumjs-util') const Transaction = require('ethereumjs-tx') const normalize = require('eth-sig-util').normalize @@ -88,7 +87,7 @@ module.exports = class txProvideUtils { } validateTxParams (txParams) { - return new Promise ((resolve, reject) => { + return new Promise((resolve, reject) => { if (('value' in txParams) && txParams.value.indexOf('-') === 0) { reject(new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)) } else { @@ -111,10 +110,6 @@ module.exports = class txProvideUtils { // util -function isUndef (value) { - return value === undefined -} - function bnToHex (inputBn) { return ethUtil.addHexPrefix(inputBn.toString(16)) } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index af4ea32e3..46a01c900 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -446,7 +446,7 @@ module.exports = class MetamaskController extends EventEmitter { this.sendUpdate() this.opts.showUnapprovedTx(txMeta) // listen for tx completion (success, fail) - return new Promise ((resolve, reject) => { + return new Promise((resolve, reject) => { this.txController.once(`${txMeta.id}:finished`, (completedTx) => { switch (completedTx.status) { case 'submitted': -- cgit v1.2.3 From 41c585c796a9049c2413036e7b23bf07330daa82 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 2 Aug 2017 12:18:29 -0700 Subject: Make wallet view visible iff vw above 575px --- ui/app/components/wallet-view.js | 2 +- ui/app/css/index.css | 24 +++++++++++++++++++++++- ui/app/css/lib.css | 1 - ui/app/main-container.js | 3 +-- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index 091a5cd7c..97c881483 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -33,7 +33,7 @@ WalletView.prototype.render = function () { const selected = '0x82df11beb942BEeeD58d466fCb0F0791365C7684' const { network } = this.props - return h('div.wallet-view.flex-column', { + return h('div.wallet-view.flex-column.lap-visible', { style: { // width: '33.333%', flexGrow: 1, diff --git a/ui/app/css/index.css b/ui/app/css/index.css index 3c397dcff..b027792fb 100644 --- a/ui/app/css/index.css +++ b/ui/app/css/index.css @@ -718,5 +718,27 @@ div.message-container > div:first-child { } .pop-hover:hover { - transform: scale(1.1); + transform: scale(1.1); +} + +@media screen and (min-width: 576px) { + .lap-visible { + display: none; + } + + .phone-visible { + display: flex; + } + +} + +@media screen and (max-width: 575px) { + .lap-visible { + display: flex; + } + + .phone-visible { + display: none; + } + } diff --git a/ui/app/css/lib.css b/ui/app/css/lib.css index 98570859a..b0ca958a2 100644 --- a/ui/app/css/lib.css +++ b/ui/app/css/lib.css @@ -270,4 +270,3 @@ hr.horizontal-line { margin-top: 20px; color: red; } - diff --git a/ui/app/main-container.js b/ui/app/main-container.js index ae62a0e0c..592f331b5 100644 --- a/ui/app/main-container.js +++ b/ui/app/main-container.js @@ -27,10 +27,9 @@ MainContainer.prototype.render = function () { overflowY: 'scroll', } }, [ + h(WalletView, { style: { - // width: '33.33%', - // height: '82vh', } }, [ ]), -- cgit v1.2.3 From 22b03c62e650182951dce25a5ce9de982782a7fa Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 2 Aug 2017 12:29:07 -0700 Subject: Add burger icon and phone-visible media queries --- ui/app/components/tx-view.js | 5 +++++ ui/app/css/index.css | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/ui/app/components/tx-view.js b/ui/app/components/tx-view.js index bcd30e6d8..c5c6484cc 100644 --- a/ui/app/components/tx-view.js +++ b/ui/app/components/tx-view.js @@ -40,6 +40,11 @@ TxView.prototype.render = function () { background: '#FFFFFF', } }, [ + + h('div.phone-visible.fa.fa-bars', { + + }, []), + h('div.flex-row', { style: { margin: '1.8em 1.3em 0.8em 1.3em', diff --git a/ui/app/css/index.css b/ui/app/css/index.css index b027792fb..9e63c9e55 100644 --- a/ui/app/css/index.css +++ b/ui/app/css/index.css @@ -723,22 +723,22 @@ div.message-container > div:first-child { @media screen and (min-width: 576px) { .lap-visible { - display: none; + display: flex; } .phone-visible { - display: flex; + display: none; } } @media screen and (max-width: 575px) { .lap-visible { - display: flex; + display: none; } .phone-visible { - display: none; + display: flex; } } -- cgit v1.2.3 From 96d3b2f35ff9cd4bb45ad04feaf30d2fb4fc2740 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 2 Aug 2017 13:03:30 -0700 Subject: Add dependejncy: react-burger-menu --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 29db4dace..c1b228272 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,7 @@ "pumpify": "^1.3.4", "qrcode-npm": "0.0.3", "react": "^15.0.2", + "react-burger-menu": "^2.1.4", "react-dom": "^15.5.4", "react-hyperscript": "^2.2.2", "react-markdown": "^2.3.0", -- cgit v1.2.3 From 7767f9f7ad7321d88a0b738d2c272961cc1ce286 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 2 Aug 2017 13:03:36 -0700 Subject: Hook up responsive sidebar --- ui/app/actions.js | 3 +++ ui/app/components/wallet-view.js | 4 ++-- ui/app/main-container.js | 12 +++++++++++- ui/app/reducers/app.js | 11 +++++++++++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index 0a9d347aa..0083543b4 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -5,6 +5,9 @@ var actions = { GO_HOME: 'GO_HOME', goHome: goHome, + // sidebar state + SIDEBAR_OPEN: 'UI_SIDEBAR_OPEN', + SIDEBAR_CLOSE: 'UI_SIDEBAR_CLOSE', // menu state getNetworkStatus: 'getNetworkStatus', // transition state diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index 97c881483..63335dd93 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -31,9 +31,9 @@ const noop = () => {} WalletView.prototype.render = function () { const selected = '0x82df11beb942BEeeD58d466fCb0F0791365C7684' - const { network } = this.props + const { network, responsiveDisplayClassname } = this.props - return h('div.wallet-view.flex-column.lap-visible', { + return h('div.wallet-view.flex-column' + (responsiveDisplayClassname || ''), { style: { // width: '33.333%', flexGrow: 1, diff --git a/ui/app/main-container.js b/ui/app/main-container.js index 592f331b5..fb768c386 100644 --- a/ui/app/main-container.js +++ b/ui/app/main-container.js @@ -3,6 +3,7 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const WalletView = require('./components/wallet-view') const TxView = require('./components/tx-view') +const SlideoutMenu = require('react-burger-menu').slide module.exports = MainContainer @@ -28,9 +29,18 @@ MainContainer.prototype.render = function () { } }, [ + h(SlideoutMenu, { + isOpen: true, + }, [ + h(WalletView, { + responsiveDisplayClassname: '.phone-visible' + }), + ]), + h(WalletView, { style: { - } + }, + responsiveDisplayClassname: '.lap-visible', }, [ ]), diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 2fcc9bfe0..bf1de4577 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -36,6 +36,7 @@ function reduceApp (state, action) { var appState = extend({ shouldClose: false, menuOpen: false, + sidebarOpen: false, currentView: seedWords ? seedConfView : defaultView, accountDetail: { subview: 'transactions', @@ -46,6 +47,16 @@ function reduceApp (state, action) { }, state.appState) switch (action.type) { + // sidebar methods + case actions.SIDEBAR_OPEN: + return extend(appState, { + sidebarOpen: true, + }) + + case actions.SIDEBAR_CLOSE: + return extend(appState, { + sidebarOpen: false, + }) // transition methods -- cgit v1.2.3 From dfa10763e36f745d82fb62adc4ac42773d266da4 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 2 Aug 2017 13:32:02 -0700 Subject: Integrate slideout menu with tx view --- ui/app/actions.js | 15 +++++++++++++++ ui/app/app.js | 1 + ui/app/components/tx-view.js | 36 +++++++++++++++++++++++++++++------- ui/app/main-container.js | 10 +--------- 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index 0083543b4..d3d6c165e 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -8,6 +8,8 @@ var actions = { // sidebar state SIDEBAR_OPEN: 'UI_SIDEBAR_OPEN', SIDEBAR_CLOSE: 'UI_SIDEBAR_CLOSE', + showSidebar: showSidebar, + hideSidebar: hideSidebar, // menu state getNetworkStatus: 'getNetworkStatus', // transition state @@ -763,6 +765,19 @@ function useEtherscanProvider () { } } +function showSidebar () { + return { + type: actions.SIDEBAR_OPEN, + } +} + +function hideSidebar () { + return { + type: actions.SIDEBAR_CLOSE, + } +} + + function showLoadingIndication (message) { return { type: actions.SHOW_LOADING, diff --git a/ui/app/app.js b/ui/app/app.js index 021ef5f27..2a07b57d3 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -39,6 +39,7 @@ function App () { Component.call(this) } function mapStateToProps (state) { return { // state from plugin + sidebarOpen: state.appState.sidebarOpen, isLoading: state.appState.isLoading, loadingMessage: state.appState.loadingMessage, noActiveNotices: state.metamask.noActiveNotices, diff --git a/ui/app/components/tx-view.js b/ui/app/components/tx-view.js index c5c6484cc..b72abb084 100644 --- a/ui/app/components/tx-view.js +++ b/ui/app/components/tx-view.js @@ -2,17 +2,29 @@ const Component = require('react').Component const connect = require('react-redux').connect const h = require('react-hyperscript') const inherits = require('util').inherits +const actions = require('../actions') +// slideout menu +const SlideoutMenu = require('react-burger-menu').slide +const WalletView = require('./wallet-view') + // const Identicon = require('./identicon') // const AccountDropdowns = require('./account-dropdowns').AccountDropdowns // const Content = require('./wallet-content-display') -module.exports = connect()(TxView) +module.exports = connect(mapStateToProps, mapDispatchToProps)(TxView) + +function mapStateToProps (state) { + return { + sidebarOpen: state.appState.sidebarOpen, + } +} -// function mapStateToProps (state) { -// return { -// network: state.metamask.network, -// } -// } +function mapDispatchToProps (dispatch) { + return { + showSidebar: () => {dispatch(actions.showSidebar())}, + hideSidebar: () => {dispatch(actions.hideSidebar())}, + } +} const contentDivider = h('div', { style: { @@ -40,9 +52,19 @@ TxView.prototype.render = function () { background: '#FFFFFF', } }, [ + // slideout - move to separate render func + h(SlideoutMenu, { + isOpen: this.props.sidebarOpen, + }, [ + h(WalletView, { + responsiveDisplayClassname: '.phone-visible' + }), + ]), h('div.phone-visible.fa.fa-bars', { - + onClick: () => { + this.props.sidebarOpen ? this.props.hideSidebar() : this.props.showSidebar() + } }, []), h('div.flex-row', { diff --git a/ui/app/main-container.js b/ui/app/main-container.js index fb768c386..870b3e7f0 100644 --- a/ui/app/main-container.js +++ b/ui/app/main-container.js @@ -1,8 +1,8 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits -const WalletView = require('./components/wallet-view') const TxView = require('./components/tx-view') +const WalletView = require('./components/wallet-view') const SlideoutMenu = require('react-burger-menu').slide module.exports = MainContainer @@ -29,14 +29,6 @@ MainContainer.prototype.render = function () { } }, [ - h(SlideoutMenu, { - isOpen: true, - }, [ - h(WalletView, { - responsiveDisplayClassname: '.phone-visible' - }), - ]), - h(WalletView, { style: { }, -- cgit v1.2.3 From 9ebdc343aa32c36bdff9debcecc3c75485939e2a Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 2 Aug 2017 14:17:58 -0700 Subject: Implement custom sidebar, with close button --- ui/app/app.js | 68 +++++++++++++++++++++++++++++++++++++++- ui/app/components/tx-view.js | 13 ++------ ui/app/components/wallet-view.js | 14 +++++++-- 3 files changed, 82 insertions(+), 13 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index 2a07b57d3..7cf000d76 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -15,6 +15,11 @@ const ConfirmTxScreen = require('./conf-tx') // notice const NoticeScreen = require('./components/notice') const generateLostAccountsNotice = require('../lib/lost-accounts-notice') + +// slideout menu +const WalletView = require('./components/wallet-view') +const SlideoutMenu = require('react-burger-menu').slide + // other views const ConfigScreen = require('./config') const AddTokenScreen = require('./add-token') @@ -63,7 +68,7 @@ function mapStateToProps (state) { App.prototype.render = function () { var props = this.props - const { isLoading, loadingMessage, transForward, network } = props + const { isLoading, loadingMessage, transForward, network, sidebarOpen } = props const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config' const loadMessage = loadingMessage || isLoadingNetwork ? `Connecting to ${this.getNetworkName()}` : null @@ -82,8 +87,23 @@ App.prototype.render = function () { }, }, [ + // app bar this.renderAppBar(), + + // slideout - move to separate render func + this.renderSidebar(), + // h('div.phone-visible', {} ,[ + // h(SlideoutMenu, { + // isOpen: false, + // }, [ + // h(WalletView, { + // responsiveDisplayClassname: '.phone-visible', + // }), + // ]), + // ]) + + // network dropdown this.renderNetworkDropdown(), // this.renderDropdown(), @@ -98,6 +118,52 @@ App.prototype.render = function () { ) } +App.prototype.renderSidebar = function() { + if (!this.props.sidebarOpen) { + return null; + } + + return h('div.phone-visible', {}, [ + // content + h(WalletView, { + responsiveDisplayClassname: '.phone-visible', + style: { + zIndex: 26, + position: 'fixed', + top: '6%', + left: '0px', + right: '0px', + bottom: '0px', + opacity: '1', + visibility: 'visible', + transition: 'transform 0.3s ease-out', + willChange: 'transform', + transform: 'translateX(0%)', + overflowY: 'auto', + boxShadow: 'rgba(0, 0, 0, 0.15) 2px 2px 4px', + width: '85%', + height: '100%', + }, + }), + + // overlay + h('div', { + style: { + zIndex: 25, + position: 'fixed', + top: '6%', + left: '0px', + right: '0px', + bottom: '0px', + opacity: '1', + visibility: 'visible', + transition: 'opacity 0.3s ease-out, visibility 0.3s ease-out', + backgroundColor: 'rgba(0, 0, 0, 0.3)', + } + }, []) + ]) +} + App.prototype.renderAppBar = function () { if (window.METAMASK_UI_TYPE === 'notification') { return null diff --git a/ui/app/components/tx-view.js b/ui/app/components/tx-view.js index b72abb084..2aaa32395 100644 --- a/ui/app/components/tx-view.js +++ b/ui/app/components/tx-view.js @@ -52,20 +52,13 @@ TxView.prototype.render = function () { background: '#FFFFFF', } }, [ - // slideout - move to separate render func - h(SlideoutMenu, { - isOpen: this.props.sidebarOpen, - }, [ - h(WalletView, { - responsiveDisplayClassname: '.phone-visible' - }), - ]), - h('div.phone-visible.fa.fa-bars', { onClick: () => { + console.log("click received") this.props.sidebarOpen ? this.props.hideSidebar() : this.props.showSidebar() } - }, []), + }, [ + ]), h('div.flex-row', { style: { diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index 63335dd93..2a626a930 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -12,16 +12,17 @@ module.exports = connect(mapStateToProps, mapDispatchToProps)(WalletView) function mapStateToProps (state) { return { network: state.metamask.network, + sidebarOpen: state.appState.sidebarOpen, } } function mapDispatchToProps (dispatch) { return { showSendPage: () => {dispatch(actions.showSendPage())}, + hideSidebar: () => {dispatch(actions.hideSidebar())}, } } - inherits(WalletView, Component) function WalletView () { Component.call(this) @@ -31,7 +32,7 @@ const noop = () => {} WalletView.prototype.render = function () { const selected = '0x82df11beb942BEeeD58d466fCb0F0791365C7684' - const { network, responsiveDisplayClassname } = this.props + const { network, responsiveDisplayClassname, style } = this.props return h('div.wallet-view.flex-column' + (responsiveDisplayClassname || ''), { style: { @@ -41,9 +42,18 @@ WalletView.prototype.render = function () { flexBasis: '230px', // .333*345 height: '82vh', background: '#FAFAFA', // TODO: add to reusable colors + ...style, } }, [ + h('div.phone-visible.fa.fa-bars', { + onClick: () => { + console.log("click received-inwalletview") + this.props.hideSidebar() + } + }, [ + ]), + // TODO: Separate component: wallet account details h('div.flex-row.flex-center', { style: { -- cgit v1.2.3 From 84aba21ae9f00d5d82d8087de6938fa9526bd3d4 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 2 Aug 2017 14:19:13 -0700 Subject: Add notes for overlay click action --- ui/app/app.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/app/app.js b/ui/app/app.js index 7cf000d76..3279e0333 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -147,6 +147,7 @@ App.prototype.renderSidebar = function() { }), // overlay + // TODO: add onClick for overlay to close sidebar h('div', { style: { zIndex: 25, -- cgit v1.2.3 From aea5735b29c87d0a9aad3bfa86d854ed9b20bdf7 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 2 Aug 2017 14:25:28 -0700 Subject: obj-multiplex - missing name error + prefer const over var --- app/scripts/lib/obj-multiplex.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/scripts/lib/obj-multiplex.js b/app/scripts/lib/obj-multiplex.js index bd114c394..0034febe0 100644 --- a/app/scripts/lib/obj-multiplex.js +++ b/app/scripts/lib/obj-multiplex.js @@ -5,12 +5,16 @@ module.exports = ObjectMultiplex function ObjectMultiplex (opts) { opts = opts || {} // create multiplexer - var mx = through.obj(function (chunk, enc, cb) { - var name = chunk.name - var data = chunk.data - var substream = mx.streams[name] + const mx = through.obj(function (chunk, enc, cb) { + const name = chunk.name + const data = chunk.data + if (!name) { + console.warn(`ObjectMultiplex - Malformed chunk without name "${chunk}"`) + return cb() + } + const substream = mx.streams[name] if (!substream) { - console.warn(`orphaned data for stream "${name}"`) + console.warn(`ObjectMultiplex - orphaned data for stream "${name}"`) } else { if (substream.push) substream.push(data) } @@ -19,7 +23,7 @@ function ObjectMultiplex (opts) { mx.streams = {} // create substreams mx.createStream = function (name) { - var substream = mx.streams[name] = through.obj(function (chunk, enc, cb) { + const substream = mx.streams[name] = through.obj(function (chunk, enc, cb) { mx.push({ name: name, data: chunk, -- cgit v1.2.3 From ecaa235b5e3331defab75dad72593951fdf37790 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 2 Aug 2017 14:26:10 -0700 Subject: phishing detection - move phishing detection into contentscript and metamask controller --- app/manifest.json | 9 --------- app/scripts/background.js | 34 ++++------------------------------ app/scripts/blacklister.js | 14 -------------- app/scripts/contentscript.js | 22 ++++++++++++++++------ app/scripts/lib/inpage-provider.js | 3 +++ app/scripts/lib/is-phish.js | 6 +++--- app/scripts/metamask-controller.js | 26 ++++++++++++++++++++++++-- gulpfile.js | 1 - package.json | 2 +- test/unit/blacklister-test.js | 36 ------------------------------------ test/unit/phishing-detection-test.js | 36 ++++++++++++++++++++++++++++++++++++ 11 files changed, 87 insertions(+), 102 deletions(-) delete mode 100644 app/scripts/blacklister.js delete mode 100644 test/unit/blacklister-test.js create mode 100644 test/unit/phishing-detection-test.js diff --git a/app/manifest.json b/app/manifest.json index 591a07d0d..1eaf6f26a 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -52,15 +52,6 @@ ], "run_at": "document_start", "all_frames": true - }, - { - "run_at": "document_start", - "matches": [ - "http://*/*", - "https://*/*" - ], - "js": ["scripts/blacklister.js"], - "all_frames": true } ], "permissions": [ diff --git a/app/scripts/background.js b/app/scripts/background.js index bc0fbdc37..a235afff3 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -11,7 +11,6 @@ const NotificationManager = require('./lib/notification-manager.js') const MetamaskController = require('./metamask-controller') const extension = require('extensionizer') const firstTimeState = require('./first-time-state') -const isPhish = require('./lib/is-phish') const STORAGE_KEY = 'metamask-config' const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' @@ -91,16 +90,12 @@ function setupController (initState) { extension.runtime.onConnect.addListener(connectRemote) function connectRemote (remotePort) { - if (remotePort.name === 'blacklister') { - return checkBlacklist(remotePort) - } - - var isMetaMaskInternalProcess = remotePort.name === 'popup' || remotePort.name === 'notification' - var portStream = new PortStream(remotePort) + const isMetaMaskInternalProcess = remotePort.name === 'popup' || remotePort.name === 'notification' + const portStream = new PortStream(remotePort) if (isMetaMaskInternalProcess) { // communication with popup popupIsOpen = popupIsOpen || (remotePort.name === 'popup') - controller.setupTrustedCommunication(portStream, 'MetaMask', remotePort.name) + controller.setupTrustedCommunication(portStream, 'MetaMask') // record popup as closed if (remotePort.name === 'popup') { endOfStream(portStream, () => { @@ -109,7 +104,7 @@ function setupController (initState) { } } else { // communication with page - var originDomain = urlUtil.parse(remotePort.sender.url).hostname + const originDomain = urlUtil.parse(remotePort.sender.url).hostname controller.setupUntrustedCommunication(portStream, originDomain) } } @@ -140,27 +135,6 @@ function setupController (initState) { return Promise.resolve() } -// Listen for new pages and return if blacklisted: -function checkBlacklist (port) { - const handler = handleNewPageLoad.bind(null, port) - port.onMessage.addListener(handler) - setTimeout(() => { - port.onMessage.removeListener(handler) - }, 30000) -} - -function handleNewPageLoad (port, message) { - const { pageLoaded } = message - if (!pageLoaded || !global.metamaskController) return - - const state = global.metamaskController.getState() - const updatedBlacklist = state.blacklist - - if (isPhish({ updatedBlacklist, hostname: pageLoaded })) { - port.postMessage({ 'blacklist': pageLoaded }) - } -} - // // Etc... // diff --git a/app/scripts/blacklister.js b/app/scripts/blacklister.js deleted file mode 100644 index 37751b595..000000000 --- a/app/scripts/blacklister.js +++ /dev/null @@ -1,14 +0,0 @@ -const extension = require('extensionizer') - -var port = extension.runtime.connect({name: 'blacklister'}) -port.postMessage({ 'pageLoaded': window.location.hostname }) -port.onMessage.addListener(redirectIfBlacklisted) - -function redirectIfBlacklisted (response) { - const { blacklist } = response - const host = window.location.hostname - if (blacklist && blacklist === host) { - window.location.href = 'https://metamask.io/phishing.html' - } -} - diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 291b922e8..6fde0edcd 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -37,28 +37,33 @@ function setupInjection () { function setupStreams () { // setup communication to page and plugin - var pageStream = new LocalMessageDuplexStream({ + const pageStream = new LocalMessageDuplexStream({ name: 'contentscript', target: 'inpage', }) pageStream.on('error', console.error) - var pluginPort = extension.runtime.connect({name: 'contentscript'}) - var pluginStream = new PortStream(pluginPort) + const pluginPort = extension.runtime.connect({ name: 'contentscript' }) + const pluginStream = new PortStream(pluginPort) pluginStream.on('error', console.error) // forward communication plugin->inpage pageStream.pipe(pluginStream).pipe(pageStream) // setup local multistream channels - var mx = ObjectMultiplex() + const mx = ObjectMultiplex() mx.on('error', console.error) mx.pipe(pageStream).pipe(mx) + mx.pipe(pluginStream).pipe(mx) // connect ping stream - var pongStream = new PongStream({ objectMode: true }) + const pongStream = new PongStream({ objectMode: true }) pongStream.pipe(mx.createStream('pingpong')).pipe(pongStream) - // ignore unused channels (handled by background) + // connect phishing warning stream + const phishingStream = mx.createStream('phishing') + phishingStream.once('data', redirectToPhishingWarning) + + // ignore unused channels (handled by background, inpage) mx.ignoreStream('provider') mx.ignoreStream('publicConfig') } @@ -88,3 +93,8 @@ function suffixCheck () { } return true } + +function redirectToPhishingWarning () { + console.log('MetaMask - redirecting to phishing warning') + window.location.href = 'https://metamask.io/phishing.html' +} diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js index 8b8623974..fd032a673 100644 --- a/app/scripts/lib/inpage-provider.js +++ b/app/scripts/lib/inpage-provider.js @@ -26,6 +26,9 @@ function MetamaskInpageProvider (connectionStream) { (err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err) ) + // ignore phishing warning message (handled elsewhere) + multiStream.ignoreStream('phishing') + // connect to async provider const asyncProvider = self.asyncProvider = new StreamProvider() pipe( diff --git a/app/scripts/lib/is-phish.js b/app/scripts/lib/is-phish.js index 68c09e4ac..21c68b0d6 100644 --- a/app/scripts/lib/is-phish.js +++ b/app/scripts/lib/is-phish.js @@ -9,15 +9,15 @@ const LEVENSHTEIN_CHECKS = ['myetherwallet', 'myetheroll', 'ledgerwallet', 'meta // credit to @sogoiii and @409H for their help! // Return a boolean on whether or not a phish is detected. -function isPhish({ hostname, updatedBlacklist = null }) { +function isPhish({ hostname, blacklist }) { var strCurrentTab = hostname // check if the domain is part of the whitelist. if (whitelistedDomains && whitelistedDomains.includes(strCurrentTab)) { return false } // Allow updating of blacklist: - if (updatedBlacklist) { - blacklistedDomains = blacklistedDomains.concat(updatedBlacklist) + if (blacklist) { + blacklistedDomains = blacklistedDomains.concat(blacklist) } // check if the domain is part of the blacklist. diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 11dcde2c1..28c35a13d 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -23,6 +23,7 @@ const ConfigManager = require('./lib/config-manager') const nodeify = require('./lib/nodeify') const accountImporter = require('./account-import-strategies') const getBuyEthUrl = require('./lib/buy-eth-url') +const checkForPhishing = require('./lib/is-phish') const debounce = require('debounce') const version = require('../manifest.json').version @@ -326,8 +327,15 @@ module.exports = class MetamaskController extends EventEmitter { } setupUntrustedCommunication (connectionStream, originDomain) { + // Check if new connection is blacklisted + if (this.isHostBlacklisted(originDomain)) { + console.log('MetaMask - sending phishing warning for', originDomain) + this.sendPhishingWarning(connectionStream, originDomain) + return + } + // setup multiplexing - var mx = setupMultiplex(connectionStream) + const mx = setupMultiplex(connectionStream) // connect features this.setupProviderConnection(mx.createStream('provider'), originDomain) this.setupPublicConfig(mx.createStream('publicConfig')) @@ -335,12 +343,26 @@ module.exports = class MetamaskController extends EventEmitter { setupTrustedCommunication (connectionStream, originDomain) { // setup multiplexing - var mx = setupMultiplex(connectionStream) + const mx = setupMultiplex(connectionStream) // connect features this.setupControllerConnection(mx.createStream('controller')) this.setupProviderConnection(mx.createStream('provider'), originDomain) } + // Check if a domain is on our blacklist + isHostBlacklisted (hostname) { + if (!hostname) return false + const { blacklist } = this.getState().blacklist + return checkForPhishing({ blacklist, hostname }) + } + + sendPhishingWarning (connectionStream, hostname) { + const mx = setupMultiplex(connectionStream) + const phishingStream = mx.createStream('phishing') + // phishingStream.write(true) + phishingStream.write({ hostname }) + } + setupControllerConnection (outStream) { const api = this.getApi() const dnode = Dnode(api) diff --git a/gulpfile.js b/gulpfile.js index 53de7a7d9..cc723704a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -172,7 +172,6 @@ gulp.task('default', ['lint'], function () { const jsFiles = [ 'inpage', 'contentscript', - 'blacklister', 'background', 'popup', ] diff --git a/package.json b/package.json index 10afc8228..40bc78c6f 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "sw-stream": "^2.0.0", "textarea-caret": "^3.0.1", "three.js": "^0.73.2", - "through2": "^2.0.1", + "through2": "^2.0.3", "valid-url": "^1.0.9", "vreme": "^3.0.2", "web3": "0.19.1", diff --git a/test/unit/blacklister-test.js b/test/unit/blacklister-test.js deleted file mode 100644 index ce110491c..000000000 --- a/test/unit/blacklister-test.js +++ /dev/null @@ -1,36 +0,0 @@ -const assert = require('assert') -const isPhish = require('../../app/scripts/lib/is-phish') - -describe('blacklister', function () { - describe('#isPhish', function () { - it('should not flag whitelisted values', function () { - var result = isPhish({ hostname: 'www.metamask.io' }) - assert.equal(result, false) - }) - it('should flag explicit values', function () { - var result = isPhish({ hostname: 'metamask.com' }) - assert.equal(result, true) - }) - it('should flag levenshtein values', function () { - var result = isPhish({ hostname: 'metmask.io' }) - assert.equal(result, true) - }) - it('should not flag not-even-close values', function () { - var result = isPhish({ hostname: 'example.com' }) - assert.equal(result, false) - }) - it('should not flag the ropsten faucet domains', function () { - var result = isPhish({ hostname: 'faucet.metamask.io' }) - assert.equal(result, false) - }) - it('should not flag the mascara domain', function () { - var result = isPhish({ hostname: 'zero.metamask.io' }) - assert.equal(result, false) - }) - it('should not flag the mascara-faucet domain', function () { - var result = isPhish({ hostname: 'zero-faucet.metamask.io' }) - assert.equal(result, false) - }) - }) -}) - diff --git a/test/unit/phishing-detection-test.js b/test/unit/phishing-detection-test.js new file mode 100644 index 000000000..affcb16c2 --- /dev/null +++ b/test/unit/phishing-detection-test.js @@ -0,0 +1,36 @@ +const assert = require('assert') +const isPhish = require('../../app/scripts/lib/is-phish') + +describe('phishing detection test', function () { + describe('#isPhish', function () { + it('should not flag whitelisted values', function () { + var result = isPhish({ hostname: 'www.metamask.io' }) + assert.equal(result, false) + }) + it('should flag explicit values', function () { + var result = isPhish({ hostname: 'metamask.com' }) + assert.equal(result, true) + }) + it('should flag levenshtein values', function () { + var result = isPhish({ hostname: 'metmask.io' }) + assert.equal(result, true) + }) + it('should not flag not-even-close values', function () { + var result = isPhish({ hostname: 'example.com' }) + assert.equal(result, false) + }) + it('should not flag the ropsten faucet domains', function () { + var result = isPhish({ hostname: 'faucet.metamask.io' }) + assert.equal(result, false) + }) + it('should not flag the mascara domain', function () { + var result = isPhish({ hostname: 'zero.metamask.io' }) + assert.equal(result, false) + }) + it('should not flag the mascara-faucet domain', function () { + var result = isPhish({ hostname: 'zero-faucet.metamask.io' }) + assert.equal(result, false) + }) + }) +}) + -- cgit v1.2.3 From 8c6f01b91094564df59d6d95b6f43b811e711824 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 2 Aug 2017 15:54:59 -0700 Subject: blacklist controller - breakout from metamask and infura controllers --- app/scripts/controllers/blacklist.js | 50 ++++++++++++++++++++++++++++++++++ app/scripts/controllers/infura.js | 16 +---------- app/scripts/lib/is-phish.js | 29 +++++--------------- app/scripts/metamask-controller.js | 19 ++++++------- test/unit/blacklist-controller-test.js | 41 ++++++++++++++++++++++++++++ test/unit/phishing-detection-test.js | 36 ------------------------ 6 files changed, 108 insertions(+), 83 deletions(-) create mode 100644 app/scripts/controllers/blacklist.js create mode 100644 test/unit/blacklist-controller-test.js delete mode 100644 test/unit/phishing-detection-test.js diff --git a/app/scripts/controllers/blacklist.js b/app/scripts/controllers/blacklist.js new file mode 100644 index 000000000..11e26d5b2 --- /dev/null +++ b/app/scripts/controllers/blacklist.js @@ -0,0 +1,50 @@ +const ObservableStore = require('obs-store') +const extend = require('xtend') +const communityBlacklistedDomains = require('etheraddresslookup/blacklists/domains.json') +const communityWhitelistedDomains = require('etheraddresslookup/whitelists/domains.json') +const checkForPhishing = require('../lib/is-phish') + +// compute phishing lists +const PHISHING_BLACKLIST = communityBlacklistedDomains.concat(['metamask.com']) +const PHISHING_WHITELIST = communityWhitelistedDomains.concat(['metamask.io', 'www.metamask.io']) +const PHISHING_FUZZYLIST = ['myetherwallet', 'myetheroll', 'ledgerwallet', 'metamask'] +// every ten minutes +const POLLING_INTERVAL = 10 * 60 * 1000 + +class BlacklistController { + + constructor (opts = {}) { + const initState = extend({ + phishing: PHISHING_BLACKLIST, + }, opts.initState) + this.store = new ObservableStore(initState) + // polling references + this._phishingUpdateIntervalRef = null + } + + // + // PUBLIC METHODS + // + + checkForPhishing (hostname) { + if (!hostname) return false + const { blacklist } = this.store.getState() + return checkForPhishing({ hostname, blacklist, whitelist: PHISHING_WHITELIST, fuzzylist: PHISHING_FUZZYLIST }) + } + + async updatePhishingList () { + const response = await fetch('https://api.infura.io/v1/blacklist') + const phishing = await response.json() + this.store.updateState({ phishing }) + return phishing + } + + scheduleUpdates () { + if (this._phishingUpdateIntervalRef) return + this._phishingUpdateIntervalRef = setInterval(() => { + this.updatePhishingList() + }, POLLING_INTERVAL) + } +} + +module.exports = BlacklistController diff --git a/app/scripts/controllers/infura.js b/app/scripts/controllers/infura.js index 97b2ab7e3..10adb1004 100644 --- a/app/scripts/controllers/infura.js +++ b/app/scripts/controllers/infura.js @@ -1,16 +1,14 @@ const ObservableStore = require('obs-store') const extend = require('xtend') -const recentBlacklist = require('etheraddresslookup/blacklists/domains.json') // every ten minutes -const POLLING_INTERVAL = 300000 +const POLLING_INTERVAL = 10 * 60 * 1000 class InfuraController { constructor (opts = {}) { const initState = extend({ infuraNetworkStatus: {}, - blacklist: recentBlacklist, }, opts.initState) this.store = new ObservableStore(initState) } @@ -32,24 +30,12 @@ class InfuraController { }) } - updateLocalBlacklist () { - return fetch('https://api.infura.io/v1/blacklist') - .then(response => response.json()) - .then((parsedResponse) => { - this.store.updateState({ - blacklist: parsedResponse, - }) - return parsedResponse - }) - } - scheduleInfuraNetworkCheck () { if (this.conversionInterval) { clearInterval(this.conversionInterval) } this.conversionInterval = setInterval(() => { this.checkInfuraNetworkStatus() - this.updateLocalBlacklist() }, POLLING_INTERVAL) } } diff --git a/app/scripts/lib/is-phish.js b/app/scripts/lib/is-phish.js index 21c68b0d6..ce51c353d 100644 --- a/app/scripts/lib/is-phish.js +++ b/app/scripts/lib/is-phish.js @@ -1,38 +1,23 @@ const levenshtein = require('fast-levenshtein') -const blacklistedMetaMaskDomains = ['metamask.com'] -let blacklistedDomains = require('etheraddresslookup/blacklists/domains.json').concat(blacklistedMetaMaskDomains) -const whitelistedMetaMaskDomains = ['metamask.io', 'www.metamask.io'] -const whitelistedDomains = require('etheraddresslookup/whitelists/domains.json').concat(whitelistedMetaMaskDomains) const LEVENSHTEIN_TOLERANCE = 4 -const LEVENSHTEIN_CHECKS = ['myetherwallet', 'myetheroll', 'ledgerwallet', 'metamask'] - // credit to @sogoiii and @409H for their help! // Return a boolean on whether or not a phish is detected. -function isPhish({ hostname, blacklist }) { - var strCurrentTab = hostname +function isPhish({ hostname, blacklist, whitelist, fuzzylist }) { // check if the domain is part of the whitelist. - if (whitelistedDomains && whitelistedDomains.includes(strCurrentTab)) { return false } - - // Allow updating of blacklist: - if (blacklist) { - blacklistedDomains = blacklistedDomains.concat(blacklist) - } + if (whitelist && whitelist.includes(hostname)) return false // check if the domain is part of the blacklist. - const isBlacklisted = blacklistedDomains && blacklistedDomains.includes(strCurrentTab) + if (blacklist && blacklist.includes(hostname)) return true // check for similar values. - let levenshteinMatched = false - var levenshteinForm = strCurrentTab.replace(/\./g, '') - LEVENSHTEIN_CHECKS.forEach((element) => { - if (levenshtein.get(element, levenshteinForm) <= LEVENSHTEIN_TOLERANCE) { - levenshteinMatched = true - } + const levenshteinForm = hostname.replace(/\./g, '') + const levenshteinMatched = fuzzylist.some((element) => { + return levenshtein.get(element, levenshteinForm) <= LEVENSHTEIN_TOLERANCE }) - return isBlacklisted || levenshteinMatched + return levenshteinMatched } module.exports = isPhish diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 28c35a13d..6d6cb85ab 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -16,6 +16,7 @@ const NoticeController = require('./notice-controller') const ShapeShiftController = require('./controllers/shapeshift') const AddressBookController = require('./controllers/address-book') const InfuraController = require('./controllers/infura') +const BlacklistController = require('./controllers/blacklist') const MessageManager = require('./lib/message-manager') const PersonalMessageManager = require('./lib/personal-message-manager') const TransactionController = require('./controllers/transactions') @@ -23,7 +24,6 @@ const ConfigManager = require('./lib/config-manager') const nodeify = require('./lib/nodeify') const accountImporter = require('./account-import-strategies') const getBuyEthUrl = require('./lib/buy-eth-url') -const checkForPhishing = require('./lib/is-phish') const debounce = require('debounce') const version = require('../manifest.json').version @@ -70,6 +70,10 @@ module.exports = class MetamaskController extends EventEmitter { }) this.infuraController.scheduleInfuraNetworkCheck() + this.blacklistController = new BlacklistController({ + initState: initState.BlacklistController, + }) + this.blacklistController.scheduleUpdates() // rpc provider this.provider = this.initializeProvider() @@ -152,6 +156,9 @@ module.exports = class MetamaskController extends EventEmitter { this.networkController.store.subscribe((state) => { this.store.updateState({ NetworkController: state }) }) + this.blacklistController.store.subscribe((state) => { + this.store.updateState({ BlacklistController: state }) + }) this.infuraController.store.subscribe((state) => { this.store.updateState({ InfuraController: state }) }) @@ -328,7 +335,7 @@ module.exports = class MetamaskController extends EventEmitter { setupUntrustedCommunication (connectionStream, originDomain) { // Check if new connection is blacklisted - if (this.isHostBlacklisted(originDomain)) { + if (this.blacklistController.checkForPhishing(originDomain)) { console.log('MetaMask - sending phishing warning for', originDomain) this.sendPhishingWarning(connectionStream, originDomain) return @@ -349,17 +356,9 @@ module.exports = class MetamaskController extends EventEmitter { this.setupProviderConnection(mx.createStream('provider'), originDomain) } - // Check if a domain is on our blacklist - isHostBlacklisted (hostname) { - if (!hostname) return false - const { blacklist } = this.getState().blacklist - return checkForPhishing({ blacklist, hostname }) - } - sendPhishingWarning (connectionStream, hostname) { const mx = setupMultiplex(connectionStream) const phishingStream = mx.createStream('phishing') - // phishingStream.write(true) phishingStream.write({ hostname }) } diff --git a/test/unit/blacklist-controller-test.js b/test/unit/blacklist-controller-test.js new file mode 100644 index 000000000..a9260466f --- /dev/null +++ b/test/unit/blacklist-controller-test.js @@ -0,0 +1,41 @@ +const assert = require('assert') +const BlacklistController = require('../../app/scripts/controllers/blacklist') + +describe('blacklist controller', function () { + let blacklistController + + before(() => { + blacklistController = new BlacklistController() + }) + + describe('checkForPhishing', function () { + it('should not flag whitelisted values', function () { + const result = blacklistController.checkForPhishing('www.metamask.io') + assert.equal(result, false) + }) + it('should flag explicit values', function () { + const result = blacklistController.checkForPhishing('metamask.com') + assert.equal(result, true) + }) + it('should flag levenshtein values', function () { + const result = blacklistController.checkForPhishing('metmask.io') + assert.equal(result, true) + }) + it('should not flag not-even-close values', function () { + const result = blacklistController.checkForPhishing('example.com') + assert.equal(result, false) + }) + it('should not flag the ropsten faucet domains', function () { + const result = blacklistController.checkForPhishing('faucet.metamask.io') + assert.equal(result, false) + }) + it('should not flag the mascara domain', function () { + const result = blacklistController.checkForPhishing('zero.metamask.io') + assert.equal(result, false) + }) + it('should not flag the mascara-faucet domain', function () { + const result = blacklistController.checkForPhishing('zero-faucet.metamask.io') + assert.equal(result, false) + }) + }) +}) \ No newline at end of file diff --git a/test/unit/phishing-detection-test.js b/test/unit/phishing-detection-test.js deleted file mode 100644 index affcb16c2..000000000 --- a/test/unit/phishing-detection-test.js +++ /dev/null @@ -1,36 +0,0 @@ -const assert = require('assert') -const isPhish = require('../../app/scripts/lib/is-phish') - -describe('phishing detection test', function () { - describe('#isPhish', function () { - it('should not flag whitelisted values', function () { - var result = isPhish({ hostname: 'www.metamask.io' }) - assert.equal(result, false) - }) - it('should flag explicit values', function () { - var result = isPhish({ hostname: 'metamask.com' }) - assert.equal(result, true) - }) - it('should flag levenshtein values', function () { - var result = isPhish({ hostname: 'metmask.io' }) - assert.equal(result, true) - }) - it('should not flag not-even-close values', function () { - var result = isPhish({ hostname: 'example.com' }) - assert.equal(result, false) - }) - it('should not flag the ropsten faucet domains', function () { - var result = isPhish({ hostname: 'faucet.metamask.io' }) - assert.equal(result, false) - }) - it('should not flag the mascara domain', function () { - var result = isPhish({ hostname: 'zero.metamask.io' }) - assert.equal(result, false) - }) - it('should not flag the mascara-faucet domain', function () { - var result = isPhish({ hostname: 'zero-faucet.metamask.io' }) - assert.equal(result, false) - }) - }) -}) - -- cgit v1.2.3 From b80c7e417bfa3adf338170472ba4c4c6733e8402 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 2 Aug 2017 18:58:05 -0400 Subject: move newUnapprovedTransaction to transactions.js --- app/scripts/background.js | 2 +- app/scripts/controllers/transactions.js | 63 ++++++++++++++++++++---------- app/scripts/lib/tx-utils.js | 12 ++---- app/scripts/metamask-controller.js | 23 +---------- test/unit/tx-controller-test.js | 69 +++++++++++++++++++++++++++++++-- 5 files changed, 115 insertions(+), 54 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index bc0fbdc37..f995c390e 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -126,7 +126,7 @@ function setupController (initState) { // plugin badge text function updateBadge () { var label = '' - var unapprovedTxCount = controller.txController.unapprovedTxCount + var unapprovedTxCount = controller.txController.getUnapprovedTxCount() var unapprovedMsgCount = controller.messageManager.unapprovedMsgCount var unapprovedPersonalMsgs = controller.personalMessageManager.unapprovedPersonalMsgCount var count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgs diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 0c63a5647..720323e41 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -70,11 +70,11 @@ module.exports = class TransactionController extends EventEmitter { return this.store.getState().transactions } - get unapprovedTxCount () { + getUnapprovedTxCount () { return Object.keys(this.getUnapprovedTxList()).length } - get pendingTxCount () { + getPendingTxCount () { return this.getTxsByMetaData('status', 'signed').length } @@ -92,7 +92,7 @@ module.exports = class TransactionController extends EventEmitter { return txMeta } getUnapprovedTxList () { - let txList = this.getTxList() + const txList = this.getTxList() return txList.filter((txMeta) => txMeta.status === 'unapproved') .reduce((result, tx) => { result[tx.id] = tx @@ -135,7 +135,7 @@ module.exports = class TransactionController extends EventEmitter { // or rejected tx's. // not tx's that are pending or unapproved if (txCount > txHistoryLimit - 1) { - let index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId)) + const index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId)) fullTxList.splice(index, 1) } fullTxList.push(txMeta) @@ -153,6 +153,25 @@ module.exports = class TransactionController extends EventEmitter { this.emit(`${txMeta.id}:unapproved`, txMeta) } + async newUnapprovedTransaction (txParams) { + log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) + const txMeta = await this.addUnapprovedTransaction(txParams) + this.emit('newUnaprovedTx', txMeta) + // listen for tx completion (success, fail) + return new Promise((resolve, reject) => { + this.once(`${txMeta.id}:finished`, (completedTx) => { + switch (completedTx.status) { + case 'submitted': + return resolve(completedTx.hash) + case 'rejected': + return reject(new Error('MetaMask Tx Signature: User denied transaction signature.')) + default: + return reject(new Error(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(completedTx.txParams)}`)) + } + }) + }) + } + async addUnapprovedTransaction (txParams) { // validate await this.txProviderUtils.validateTxParams(txParams) @@ -229,10 +248,9 @@ module.exports = class TransactionController extends EventEmitter { // add network/chain id txParams.chainId = this.getChainId() const ethTx = this.txProviderUtils.buildEthTxFromParams(txParams) - const rawTx = await this.signEthTx(ethTx, fromAddress).then(() => { - this.setTxStatusSigned(txMeta.id) - return ethUtil.bufferToHex(ethTx.serialize()) - }) + await this.signEthTx(ethTx, fromAddress) + this.setTxStatusSigned(txMeta.id) + const rawTx = ethUtil.bufferToHex(ethTx.serialize()) return rawTx } @@ -240,15 +258,13 @@ module.exports = class TransactionController extends EventEmitter { const txMeta = this.getTx(txId) txMeta.rawTx = rawTx this.updateTx(txMeta) - await this.txProviderUtils.publishTransaction(rawTx).then((txHash) => { - this.setTxHash(txId, txHash) - this.setTxStatusSubmitted(txId) - }) + const txHash = await this.txProviderUtils.publishTransaction(rawTx) + this.setTxHash(txId, txHash) + this.setTxStatusSubmitted(txId) } - cancelTransaction (txId) { + async cancelTransaction (txId) { this.setTxStatusRejected(txId) - return Promise.resolve() } @@ -371,9 +387,9 @@ module.exports = class TransactionController extends EventEmitter { const txId = txMeta.id if (!txHash) { - const noTxHash = new Error('We had an error while submitting this transaction, please try again.') - noTxHash.name = 'NoTxHashError' - this.setTxStatusFailed(noTxHash) + const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') + noTxHashErr.name = 'NoTxHashError' + this.setTxStatusFailed(txId, noTxHashErr) } @@ -427,7 +443,12 @@ module.exports = class TransactionController extends EventEmitter { })) } - // PRIVATE METHODS + +/* _____________________________________ +| | +| PRIVATE METHODS | +|______________________________________*/ + // Should find the tx in the tx list and // update it. @@ -511,9 +532,9 @@ module.exports = class TransactionController extends EventEmitter { // extra check in case there was an uncaught error during the // signature and submission process if (!txHash) { - const noTxHash = new Error('We had an error while submitting this transaction, please try again.') - noTxHash.name = 'NoTxHashError' - this.setTxStatusFailed(noTxHash) + const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') + noTxHashErr.name = 'NoTxHashError' + this.setTxStatusFailed(txId, noTxHashErr) } // get latest transaction status let txParams diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js index 24ea2d763..0e5b6a999 100644 --- a/app/scripts/lib/tx-utils.js +++ b/app/scripts/lib/tx-utils.js @@ -86,14 +86,10 @@ module.exports = class txProvideUtils { return this.query.sendRawTransaction(rawTx) } - validateTxParams (txParams) { - return new Promise((resolve, reject) => { - if (('value' in txParams) && txParams.value.indexOf('-') === 0) { - reject(new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)) - } else { - resolve() - } - }) + async validateTxParams (txParams) { + if (('value' in txParams) && txParams.value.indexOf('-') === 0) { + throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`) + } } sufficientBalance (txParams, hexBalance) { diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 46a01c900..794ca1a9b 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -108,6 +108,7 @@ module.exports = class MetamaskController extends EventEmitter { ethQuery: this.ethQuery, ethStore: this.ethStore, }) + this.txController.on('newUnaprovedTx', opts.showUnapprovedTx.bind(opts)) // notices this.noticeController = new NoticeController({ @@ -195,7 +196,7 @@ module.exports = class MetamaskController extends EventEmitter { cb(null, result) }, // tx signing - processTransaction: nodeify(this.newUnapprovedTransaction, this), + processTransaction: nodeify(async (txParams) => await this.txController.newUnapprovedTransaction(txParams), this), // old style msg signing processMessage: this.newUnsignedMessage.bind(this), @@ -440,26 +441,6 @@ module.exports = class MetamaskController extends EventEmitter { // Identity Management // - async newUnapprovedTransaction (txParams) { - log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) - const txMeta = await this.txController.addUnapprovedTransaction(txParams) - this.sendUpdate() - this.opts.showUnapprovedTx(txMeta) - // listen for tx completion (success, fail) - return new Promise((resolve, reject) => { - this.txController.once(`${txMeta.id}:finished`, (completedTx) => { - switch (completedTx.status) { - case 'submitted': - return resolve(completedTx.hash) - case 'rejected': - return reject(new Error('MetaMask Tx Signature: User denied transaction signature.')) - default: - return reject(new Error(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(completedTx.txParams)}`)) - } - }) - }) - } - newUnsignedMessage (msgParams, cb) { const msgId = this.messageManager.addUnapprovedMessage(msgParams) this.sendUpdate() diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index fc70da9a2..c7743a7c6 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -42,6 +42,70 @@ describe('Transaction Controller', function () { txController.txProviderUtils = new TxProvideUtils(txController.query) }) + describe('#newUnapprovedTransaction', function () { + let stub, txMeta, txParams + beforeEach(function () { + txParams = { + 'from':'0xc684832530fcbddae4b4230a47e991ddcec2831d', + 'to':'0xc684832530fcbddae4b4230a47e991ddcec2831d', + }, + txMeta = { + status: 'unapproved', + id: 1, + metamaskNetworkId: currentNetworkId, + txParams, + } + txController._saveTxList([txMeta]) + stub = sinon.stub(txController, 'addUnapprovedTransaction').returns(Promise.resolve(txMeta)) + }) + + afterEach(function () { + stub.restore() + }) + + it('should emit newUnaprovedTx event and pass txMeta as the first argument', function (done) { + txController.once('newUnaprovedTx', (txMetaFromEmit) => { + assert(txMetaFromEmit, 'txMeta is falsey') + assert.equal(txMetaFromEmit.id, 1, 'the right txMeta was passed') + done() + }) + txController.newUnapprovedTransaction(txParams) + .catch(done) + }) + + it('should resolve when finished and status is submitted and resolve with the hash', function (done) { + txController.once('newUnaprovedTx', (txMetaFromEmit) => { + setTimeout(() => { + console.log('HELLLO') + txController.setTxHash(txMetaFromEmit.id, '0x0') + txController.setTxStatusSubmitted(txMetaFromEmit.id) + }, 10) + }) + + txController.newUnapprovedTransaction(txParams) + .then((hash) => { + assert(hash, 'newUnapprovedTransaction needs to return the hash') + done() + }) + .catch(done) + }) + + it('should reject when finished and status is rejected', function (done) { + txController.once('newUnaprovedTx', (txMetaFromEmit) => { + setTimeout(() => { + console.log('HELLLO') + txController.setTxStatusRejected(txMetaFromEmit.id) + }, 10) + }) + + txController.newUnapprovedTransaction(txParams) + .catch((err) => { + if (err.message === 'MetaMask Tx Signature: User denied transaction signature.') done() + else done(err) + }) + }) + }) + describe('#addUnapprovedTransaction', function () { it('should add an unapproved transaction and return a valid txMeta', function (done) { const addTxDefaultsStub = sinon.stub(txController, 'addTxDefaults').callsFake(() => Promise.resolve) @@ -92,7 +156,7 @@ describe('Transaction Controller', function () { } txController.txProviderUtils.validateTxParams(sample).then(() => { done() - }) + }).catch(done) }) it('returns error for negative values', function (done) { @@ -407,5 +471,4 @@ describe('Transaction Controller', function () { }) }) }) -}) - +}) \ No newline at end of file -- cgit v1.2.3 From 0808eb22562ac9a64b00cb5aa54856ac8c1ea18c Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 2 Aug 2017 19:07:18 -0400 Subject: fix test --- test/unit/tx-controller-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index c7743a7c6..f290088a1 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -108,7 +108,7 @@ describe('Transaction Controller', function () { describe('#addUnapprovedTransaction', function () { it('should add an unapproved transaction and return a valid txMeta', function (done) { - const addTxDefaultsStub = sinon.stub(txController, 'addTxDefaults').callsFake(() => Promise.resolve) + const addTxDefaultsStub = sinon.stub(txController, 'addTxDefaults').callsFake(() => Promise.resolve()) txController.addUnapprovedTransaction({}) .then((txMeta) => { assert(('id' in txMeta), 'should have a id') -- cgit v1.2.3 From 340dbe75fca95373b861372d89ecdb246ad0e681 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 2 Aug 2017 19:09:37 -0400 Subject: use async with #publishTransaction --- app/scripts/lib/tx-utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js index 0e5b6a999..3687a9652 100644 --- a/app/scripts/lib/tx-utils.js +++ b/app/scripts/lib/tx-utils.js @@ -82,8 +82,8 @@ module.exports = class txProvideUtils { return ethTx } - publishTransaction (rawTx) { - return this.query.sendRawTransaction(rawTx) + async publishTransaction (rawTx) { + return await this.query.sendRawTransaction(rawTx) } async validateTxParams (txParams) { -- cgit v1.2.3 From 5ac4c2de6fa3a83709eeefc71d5afc0857a28445 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 2 Aug 2017 19:11:51 -0400 Subject: remove unused sinon --- test/unit/tx-utils-test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/unit/tx-utils-test.js b/test/unit/tx-utils-test.js index 43807922a..a43bcfb35 100644 --- a/test/unit/tx-utils-test.js +++ b/test/unit/tx-utils-test.js @@ -1,5 +1,4 @@ const assert = require('assert') -const sinon = require('sinon') const ethUtil = require('ethereumjs-util') const BN = ethUtil.BN -- cgit v1.2.3 From b471afcdb34cd121b9d3e3cccb3883943ba8aef5 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 2 Aug 2017 19:24:34 -0400 Subject: use error for #approveTransaction when setting failed --- app/scripts/controllers/transactions.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 720323e41..d4f32e049 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -229,11 +229,8 @@ module.exports = class TransactionController extends EventEmitter { // must set transaction to submitted/failed before releasing lock nonceLock.releaseLock() } catch (err) { - this.setTxStatusFailed(txId, { - stack: err.stack || err.message, - errCode: err.errCode || err, - message: err.message || 'Transaction failed during approval', - }) + if(!err.message) err.message = 'Transaction failed during approval' + this.setTxStatusFailed(txId, err) // must set transaction to submitted/failed before releasing lock if (nonceLock) nonceLock.releaseLock() // continue with error chain -- cgit v1.2.3 From 8a39ef03c298f846171173c38912d3386d688af2 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 2 Aug 2017 17:49:04 -0700 Subject: Hook up css animation --- package.json | 2 + ui/app/app.js | 109 ++++++++++++++++++++++++++++----------------- ui/app/css/transitions.css | 5 +++ 3 files changed, 75 insertions(+), 41 deletions(-) diff --git a/package.json b/package.json index c1b228272..98d0708a4 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,7 @@ "pumpify": "^1.3.4", "qrcode-npm": "0.0.3", "react": "^15.0.2", + "react-addons-css-transition-group": "^15.6.0", "react-burger-menu": "^2.1.4", "react-dom": "^15.5.4", "react-hyperscript": "^2.2.2", @@ -116,6 +117,7 @@ "react-select": "^1.0.0-rc.2", "react-simple-file-input": "^1.0.0", "react-tooltip-component": "^0.3.0", + "react-transition-group": "^2.2.0", "readable-stream": "^2.1.2", "redux": "^3.0.5", "redux-logger": "^2.10.2", diff --git a/ui/app/app.js b/ui/app/app.js index 3279e0333..9bda4966d 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -35,6 +35,7 @@ const QrView = require('./components/qr-code') const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete') const HDRestoreVaultScreen = require('./keychains/hd/restore-vault') const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation') +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') module.exports = connect(mapStateToProps)(App) @@ -119,49 +120,75 @@ App.prototype.render = function () { } App.prototype.renderSidebar = function() { - if (!this.props.sidebarOpen) { - return null; - } + // if (!this.props.sidebarOpen) { + // return null; + // } - return h('div.phone-visible', {}, [ - // content - h(WalletView, { - responsiveDisplayClassname: '.phone-visible', - style: { - zIndex: 26, - position: 'fixed', - top: '6%', - left: '0px', - right: '0px', - bottom: '0px', - opacity: '1', - visibility: 'visible', - transition: 'transform 0.3s ease-out', - willChange: 'transform', - transform: 'translateX(0%)', - overflowY: 'auto', - boxShadow: 'rgba(0, 0, 0, 0.15) 2px 2px 4px', - width: '85%', - height: '100%', - }, - }), - - // overlay - // TODO: add onClick for overlay to close sidebar - h('div', { - style: { - zIndex: 25, - position: 'fixed', - top: '6%', - left: '0px', - right: '0px', - bottom: '0px', - opacity: '1', - visibility: 'visible', - transition: 'opacity 0.3s ease-out, visibility 0.3s ease-out', - backgroundColor: 'rgba(0, 0, 0, 0.3)', + return h('div', { + }, [ + h('style', ` + .sidebar-enter { + transition: transform 500ms ease-in-out; + transform: translateX(-100%); + } + .sidebar-enter.sidebar-enter-active { + transition: transform 500ms ease-in-out; + transform: translateX(0%); + } + .sidebar-leave { + transition: transform 500ms ease-in-out; + transform: translateX(0%); } - }, []) + .sidebar-leave.sidebar-leave-active { + transition: transform 500ms ease-in-out; + transform: translateX(-100%); + } + `), + + h(ReactCSSTransitionGroup, { + transitionName: 'sidebar', + transitionEnterTimeout: 500, + transitionLeaveTimeout: 500, + }, [ + // content + this.props.sidebarOpen ? h(WalletView, { + responsiveDisplayClassname: '.sidebar', + style: { + zIndex: 26, + position: 'fixed', + top: '6%', + left: '0px', + right: '0px', + bottom: '0px', + opacity: '1', + visibility: 'visible', + // transition: 'transform 1s ease-in', + willChange: 'transform', + // transform: 'translateX(300px)', + overflowY: 'auto', + boxShadow: 'rgba(0, 0, 0, 0.15) 2px 2px 4px', + width: '85%', + height: '100%', + }, + }) : undefined, + + // overlay + // TODO: add onClick for overlay to close sidebar + this.props.sidebarOpen ? h('div', { + style: { + zIndex: 25, + position: 'fixed', + top: '6%', + left: '0px', + right: '0px', + bottom: '0px', + opacity: '1', + visibility: 'visible', + // transition: 'opacity 0.3s ease-out, visibility 0.3s ease-out', + backgroundColor: 'rgba(0, 0, 0, 0.3)', + } + }, []) : undefined, + ]) ]) } diff --git a/ui/app/css/transitions.css b/ui/app/css/transitions.css index 393a944f9..5f9f31ed7 100644 --- a/ui/app/css/transitions.css +++ b/ui/app/css/transitions.css @@ -22,6 +22,11 @@ transition: transform 300ms ease-in; } +.sidebar.from-left { + transform: translateX(-320px); + transition: transform 300ms ease-in; +} + /* loader transitions */ .loader-enter, .loader-leave-active { opacity: 0.0; -- cgit v1.2.3 From c312f341199b8d05e7e78c4203d7953bdf5a184e Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 2 Aug 2017 17:50:27 -0700 Subject: Move overlay out of transition area --- ui/app/app.js | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index 9bda4966d..8552282ad 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -172,23 +172,24 @@ App.prototype.renderSidebar = function() { }, }) : undefined, - // overlay - // TODO: add onClick for overlay to close sidebar - this.props.sidebarOpen ? h('div', { - style: { - zIndex: 25, - position: 'fixed', - top: '6%', - left: '0px', - right: '0px', - bottom: '0px', - opacity: '1', - visibility: 'visible', - // transition: 'opacity 0.3s ease-out, visibility 0.3s ease-out', - backgroundColor: 'rgba(0, 0, 0, 0.3)', - } - }, []) : undefined, - ]) + ]), + + // overlay + // TODO: add onClick for overlay to close sidebar + this.props.sidebarOpen ? h('div', { + style: { + zIndex: 25, + position: 'fixed', + top: '6%', + left: '0px', + right: '0px', + bottom: '0px', + opacity: '1', + visibility: 'visible', + // transition: 'opacity 0.3s ease-out, visibility 0.3s ease-out', + backgroundColor: 'rgba(0, 0, 0, 0.3)', + } + }, []) : undefined, ]) } -- cgit v1.2.3 From 46da924d485cf10be2965f4126609aa55707bfb5 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 2 Aug 2017 18:24:30 -0700 Subject: Add Ethereum Logo --- app/images/eth_logo.svg | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 app/images/eth_logo.svg diff --git a/app/images/eth_logo.svg b/app/images/eth_logo.svg new file mode 100644 index 000000000..894bd70dd --- /dev/null +++ b/app/images/eth_logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file -- cgit v1.2.3 From 49e37137042ba2f23dd48db92365f468f9d59e13 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 2 Aug 2017 18:24:48 -0700 Subject: Cleanup send, move send token into separate func to make room for send ETH --- ui/app/app.js | 1 + ui/app/send.js | 254 +++++++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 220 insertions(+), 35 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index 8552282ad..53801ed52 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -202,6 +202,7 @@ App.prototype.renderAppBar = function () { const state = this.state || {} const isNetworkMenuOpen = state.isNetworkMenuOpen || false + console.log("___rendering app header;;;") return ( h('.full-width', { diff --git a/ui/app/send.js b/ui/app/send.js index de9e64ad1..ab527019f 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -61,7 +61,6 @@ SendTransactionScreen.prototype.render = function () { h('div.flex-column.flex-grow', { style: { - // overflow: 'scroll', minWidth: '355px', // TODO: maxWidth TBD, use home.html }, }, [ @@ -87,10 +86,6 @@ SendTransactionScreen.prototype.render = function () { }), ]), - // - // Required Fields - // - h('h3.flex-center', { style: { marginTop: '-18px', @@ -213,10 +208,6 @@ SendTransactionScreen.prototype.render = function () { ]), - // - // Optional Fields - // - h('section.flex-column.flex-center', { style: { marginBottom: '10px', @@ -241,32 +232,6 @@ SendTransactionScreen.prototype.render = function () { }, }), ]), - - // h('h3.flex-center.text-transform-uppercase', { - // style: { - // background: '#EBEBEB', - // color: '#AEAEAE', - // marginTop: '16px', - // marginBottom: '16px', - // }, - // }, [ - // 'Transaction Data (optional)', - // ]), - - // // 'data' field - // h('section.flex-column.flex-center', [ - // h('input.large-input', { - // name: 'txData', - // placeholder: '0x01234', - // style: { - // width: '100%', - // resize: 'none', - // }, - // dataset: { - // persistentFormId: 'tx-data', - // }, - // }), - // ]), ]), // Buttons underneath card @@ -290,7 +255,226 @@ SendTransactionScreen.prototype.render = function () { width: '8em', }, }, 'Cancel'), + ]), + ]) + + ) +} + +// WIP - hyperscript for renderSendToken - hook up later +SendTransactionScreen.prototype.renderSendToken = function () { + this.persistentFormParentId = 'send-tx-form' + + const props = this.props + const { + address, + account, + identity, + network, + identities, + addressBook, + conversionRate, + currentCurrency, + } = props + + return ( + + h('div.flex-column.flex-grow', { + style: { + minWidth: '355px', // TODO: maxWidth TBD, use home.html + }, + }, [ + + // Main Send token Card + h('div.send-screen.flex-column.flex-grow', { + style: { + marginLeft: '3.5%', + marginRight: '3.5%', + background: '#FFFFFF', // $background-white + boxShadow: '0 2px 4px 0 rgba(0,0,0,0.08)', + } + }, [ + h('section.flex-center.flex-row', { + style: { + zIndex: 15, // $token-icon-z-index + marginTop: '-35px', + } + }, [ + h(Identicon, { + address: ARAGON, + diameter: 76, + }), + ]), + + h('h3.flex-center', { + style: { + marginTop: '-18px', + fontSize: '16px', + }, + }, [ + 'Send Tokens', + ]), + + h('h3.flex-center', { + style: { + textAlign: 'center', + fontSize: '12px', + }, + }, [ + 'Send Tokens to anyone with an Ethereum account', + ]), + + h('h3.flex-center', { + style: { + textAlign: 'center', + marginTop: '2px', + fontSize: '12px', + }, + }, [ + 'Your Aragon Token Balance is:', + ]), + + h('h3.flex-center', { + style: { + textAlign: 'center', + fontSize: '36px', + marginTop: '8px', + }, + }, [ + '2.34', + ]), + + h('h3.flex-center', { + style: { + textAlign: 'center', + fontSize: '12px', + marginTop: '4px', + }, + }, [ + 'ANT', + ]), + + // error message + props.error && h('span.error.flex-center', props.error), + + // 'to' field + h('section.flex-row.flex-center', { + style: { + fontSize: '12px', + }, + }, [ + h(EnsInput, { + name: 'address', + placeholder: 'Recipient Address', + onChange: this.recipientDidChange.bind(this), + network, + identities, + addressBook, + }), + ]), + + // 'amount' and send button + h('section.flex-column.flex-center', [ + h('div.flex-row.flex-center', { + style: { + fontSize: '12px', + width: '100%', + justifyContent: 'space-between', + } + },[ + h('span', { style: {} }, ['Amount']), + h('span', { style: {} }, ['Token <> USD']), + ]), + + h('input.large-input', { + name: 'amount', + placeholder: '0', + type: 'number', + style: { + marginRight: '6px', + fontSize: '12px', + }, + dataset: { + persistentFormId: 'tx-amount', + }, + }), + + ]), + + h('section.flex-column.flex-center', [ + h('div.flex-row.flex-center', { + style: { + fontSize: '12px', + width: '100%', + justifyContent: 'space-between', + } + },[ + h('span', { style: {} }, ['Gas Fee:']), + h('span', { style: { fontSize: '8px' } }, ['What\'s this?']), + ]), + + h('input.large-input', { + name: 'Gas Fee', + placeholder: '0', + type: 'number', + style: { + fontSize: '12px', + marginRight: '6px', + }, + // dataset: { + // persistentFormId: 'tx-amount', + // }, + }), + + ]), + + h('section.flex-column.flex-center', { + style: { + marginBottom: '10px', + }, + }, [ + h('div.flex-row.flex-center', { + style: { + fontSize: '12px', + width: '100%', + justifyContent: 'flex-start', + } + },[ + h('span', { style: {} }, ['Transaction Memo (optional)']), + ]), + h('input.large-input', { + name: 'memo', + placeholder: '', + type: 'string', + style: { + marginRight: '6px', + }, + }), + ]), + ]), + + // Buttons underneath card + h('section.flex-column.flex-center', [ + + h('button.btn-light', { + onClick: this.onSubmit.bind(this), + style: { + marginTop: '8px', + width: '8em', + background: '#FFFFFF' + }, + }, 'Next'), + + h('button.btn-light', { + onClick: this.back.bind(this), + style: { + background: '#F7F7F7', // $alabaster + border: 'none', + opacity: 1, + width: '8em', + }, + }, 'Cancel'), ]), ]) -- cgit v1.2.3 From 61b4b1f947230a8d5157fab27ee8ec82e0826e02 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 2 Aug 2017 19:13:17 -0700 Subject: Ensure app-header is rendered in responsive layout --- app/scripts/lib/is-popup-or-notification.js | 2 +- ui/app/app.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/scripts/lib/is-popup-or-notification.js b/app/scripts/lib/is-popup-or-notification.js index 693fa8751..73a812d5f 100644 --- a/app/scripts/lib/is-popup-or-notification.js +++ b/app/scripts/lib/is-popup-or-notification.js @@ -1,6 +1,6 @@ module.exports = function isPopupOrNotification () { const url = window.location.href - if (url.match(/popup.html$/)) { + if (url.match(/popup.html$/) || url.match(/home.html$/)) { return 'popup' } else { return 'notification' diff --git a/ui/app/app.js b/ui/app/app.js index 53801ed52..7a855813f 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -214,7 +214,8 @@ App.prototype.renderAppBar = function () { alignItems: 'center', visibility: props.isUnlocked ? 'visible' : 'none', background: '#EFEFEF', // $gallery - height: '11vh', + height: '12vh', + maxHeight: '60px', position: 'relative', zIndex: 12, }, -- cgit v1.2.3 From dd4586ee84ea0e6a74ad4cd6b6f058169ddd9129 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 2 Aug 2017 19:26:46 -0700 Subject: Adjust sidebar transition using @cjeria\'s feedback --- app/scripts/lib/environment-type.js | 10 ++++++++++ ui/app/app.js | 16 ++++++---------- 2 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 app/scripts/lib/environment-type.js diff --git a/app/scripts/lib/environment-type.js b/app/scripts/lib/environment-type.js new file mode 100644 index 000000000..7966926eb --- /dev/null +++ b/app/scripts/lib/environment-type.js @@ -0,0 +1,10 @@ +module.exports = function environmentType () { + const url = window.location.href + if (url.match(/popup.html$/)) { + return 'popup' + } else if (url.match(/home.html$/)) { + return 'responsive' + } else { + return 'notification' + } +} diff --git a/ui/app/app.js b/ui/app/app.js index 7a855813f..21eb44b8b 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -128,27 +128,27 @@ App.prototype.renderSidebar = function() { }, [ h('style', ` .sidebar-enter { - transition: transform 500ms ease-in-out; + transition: transform 300ms ease-in-out; transform: translateX(-100%); } .sidebar-enter.sidebar-enter-active { - transition: transform 500ms ease-in-out; + transition: transform 300ms ease-in-out; transform: translateX(0%); } .sidebar-leave { - transition: transform 500ms ease-in-out; + transition: transform 200ms ease-out; transform: translateX(0%); } .sidebar-leave.sidebar-leave-active { - transition: transform 500ms ease-in-out; + transition: transform 200ms ease-out; transform: translateX(-100%); } `), h(ReactCSSTransitionGroup, { transitionName: 'sidebar', - transitionEnterTimeout: 500, - transitionLeaveTimeout: 500, + transitionEnterTimeout: 300, + transitionLeaveTimeout: 200, }, [ // content this.props.sidebarOpen ? h(WalletView, { @@ -162,9 +162,7 @@ App.prototype.renderSidebar = function() { bottom: '0px', opacity: '1', visibility: 'visible', - // transition: 'transform 1s ease-in', willChange: 'transform', - // transform: 'translateX(300px)', overflowY: 'auto', boxShadow: 'rgba(0, 0, 0, 0.15) 2px 2px 4px', width: '85%', @@ -186,7 +184,6 @@ App.prototype.renderSidebar = function() { bottom: '0px', opacity: '1', visibility: 'visible', - // transition: 'opacity 0.3s ease-out, visibility 0.3s ease-out', backgroundColor: 'rgba(0, 0, 0, 0.3)', } }, []) : undefined, @@ -202,7 +199,6 @@ App.prototype.renderAppBar = function () { const state = this.state || {} const isNetworkMenuOpen = state.isNetworkMenuOpen || false - console.log("___rendering app header;;;") return ( h('.full-width', { -- cgit v1.2.3 From 41250f9769d3224e0b42821058cd6445fa7efaca Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 2 Aug 2017 19:48:33 -0700 Subject: Adjust header spacing for 500px and 900px heights --- ui/app/app.js | 17 ++++------------- ui/app/main-container.js | 2 +- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index 21eb44b8b..19d80a728 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -88,21 +88,11 @@ App.prototype.render = function () { }, }, [ - // app bar this.renderAppBar(), - // slideout - move to separate render func + // sidebar this.renderSidebar(), - // h('div.phone-visible', {} ,[ - // h(SlideoutMenu, { - // isOpen: false, - // }, [ - // h(WalletView, { - // responsiveDisplayClassname: '.phone-visible', - // }), - // ]), - // ]) // network dropdown this.renderNetworkDropdown(), @@ -113,7 +103,7 @@ App.prototype.render = function () { loadingMessage: loadMessage, }), - // panel content + // content this.renderPrimary(), ]) ) @@ -202,7 +192,7 @@ App.prototype.renderAppBar = function () { return ( h('.full-width', { - height: '38px', + style: {} }, [ h('.app-header.flex-row.flex-space-between', { @@ -210,6 +200,7 @@ App.prototype.renderAppBar = function () { alignItems: 'center', visibility: props.isUnlocked ? 'visible' : 'none', background: '#EFEFEF', // $gallery + paddingTop: '1.5vh', height: '12vh', maxHeight: '60px', position: 'relative', diff --git a/ui/app/main-container.js b/ui/app/main-container.js index 870b3e7f0..62a8bdb7b 100644 --- a/ui/app/main-container.js +++ b/ui/app/main-container.js @@ -17,7 +17,7 @@ MainContainer.prototype.render = function () { return h('div', { style: { position: 'absolute', - marginTop: '6vh', + marginTop: '35px', width: '98%', zIndex: 20, boxShadow: '0 0 7px 0 rgba(0,0,0,0.08)', -- cgit v1.2.3 From b70a399faa30c478ffffb5607cfe36001745adc7 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 2 Aug 2017 21:53:40 -0700 Subject: Isolate container component, add refactor notes --- ui/app/account-and-transaction-details.js | 39 +++++++++++++++++++++++++++++++ ui/app/main-container.js | 37 ++++++++++++++--------------- 2 files changed, 57 insertions(+), 19 deletions(-) create mode 100644 ui/app/account-and-transaction-details.js diff --git a/ui/app/account-and-transaction-details.js b/ui/app/account-and-transaction-details.js new file mode 100644 index 000000000..9386b2314 --- /dev/null +++ b/ui/app/account-and-transaction-details.js @@ -0,0 +1,39 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +// Main Views +const TxView = require('./components/tx-view') +const WalletView = require('./components/wallet-view') + +module.exports = AccountAndTransactionDetails + +inherits(AccountAndTransactionDetails, Component) +function AccountAndTransactionDetails () { + Component.call(this) +} + +AccountAndTransactionDetails.prototype.render = function () { + console.log('atdR') + return h('div', { + style: { + display: 'flex', + flex: '1 0 auto', + }, + }, [ + // wallet + h(WalletView, { + style: { + }, + responsiveDisplayClassname: '.lap-visible', + }, [ + ]), + + // transaction + h(TxView, { + style: { + } + }, [ + ]), + ]) +} + diff --git a/ui/app/main-container.js b/ui/app/main-container.js index 62a8bdb7b..5194c2343 100644 --- a/ui/app/main-container.js +++ b/ui/app/main-container.js @@ -4,6 +4,7 @@ const inherits = require('util').inherits const TxView = require('./components/tx-view') const WalletView = require('./components/wallet-view') const SlideoutMenu = require('react-burger-menu').slide +const AccountAndTransactionDetails = require('./account-and-transaction-details') module.exports = MainContainer @@ -14,6 +15,22 @@ function MainContainer () { MainContainer.prototype.render = function () { + // 1. Fixing Mobile View: flush container + // media query for mobile view: + // position: absolute; + // margin-top: 35px; + // width: 100%; + // + // 2. Fix responsive sizing - smaller + // https://puu.sh/x0gDA/5ff3b734eb.png + // + // 3. summarize: + // switch statement goes inside MainContainer, + // or a method in renderPrimary + // - pass resulting h() to MainContainer + // - error checking in separate func + // - router in separate func + return h('div', { style: { position: 'absolute', @@ -27,24 +44,6 @@ MainContainer.prototype.render = function () { alignItems: 'stretch', overflowY: 'scroll', } - }, [ - - h(WalletView, { - style: { - }, - responsiveDisplayClassname: '.lap-visible', - }, [ - ]), - - h(TxView, { - style: { - // flexGrow: 2 - // width: '66.66%', - // height: '82vh', - // background: '#FFFFFF', - } - }, [ - ]), - ]) + }, [h(AccountAndTransactionDetails, {}, [])]) } -- cgit v1.2.3 From ff7ba83a6c62abb42c0141d4cd5a53a7870a9199 Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 2 Aug 2017 22:09:12 -0700 Subject: Add note for styling buttons --- ui/app/main-container.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/app/main-container.js b/ui/app/main-container.js index 5194c2343..84d8c5435 100644 --- a/ui/app/main-container.js +++ b/ui/app/main-container.js @@ -30,6 +30,8 @@ MainContainer.prototype.render = function () { // - pass resulting h() to MainContainer // - error checking in separate func // - router in separate func + // + // 4. style all buttons as - + +
\ No newline at end of file diff --git a/mascara/src/lib/setup-iframe.js b/mascara/src/lib/setup-iframe.js deleted file mode 100644 index dcf404574..000000000 --- a/mascara/src/lib/setup-iframe.js +++ /dev/null @@ -1,19 +0,0 @@ -const Iframe = require('iframe') -const createIframeStream = require('iframe-stream').IframeStream - -module.exports = setupIframe - - -function setupIframe(opts) { - opts = opts || {} - var frame = Iframe({ - src: opts.zeroClientProvider || 'https://zero.metamask.io/', - container: opts.container || document.head, - sandboxAttributes: opts.sandboxAttributes || ['allow-scripts', 'allow-popups'], - }) - var iframe = frame.iframe - iframe.style.setProperty('display', 'none') - var iframeStream = createIframeStream(iframe) - - return iframeStream -} diff --git a/mascara/src/lib/setup-provider.js b/mascara/src/lib/setup-provider.js deleted file mode 100644 index 62335b18d..000000000 --- a/mascara/src/lib/setup-provider.js +++ /dev/null @@ -1,22 +0,0 @@ -const setupIframe = require('./setup-iframe.js') -const MetamaskInpageProvider = require('../../../app/scripts/lib/inpage-provider.js') - -module.exports = getProvider - - -function getProvider(opts){ - if (global.web3) { - console.log('MetaMask ZeroClient - using environmental web3 provider') - return global.web3.currentProvider - } - console.log('MetaMask ZeroClient - injecting zero-client iframe!') - var iframeStream = setupIframe({ - zeroClientProvider: opts.mascaraUrl, - sandboxAttributes: ['allow-scripts', 'allow-popups', 'allow-same-origin'], - container: document.body, - }) - - var inpageProvider = new MetamaskInpageProvider(iframeStream) - return inpageProvider - -} diff --git a/mascara/src/mascara.js b/mascara/src/mascara.js index 1655d1f64..0af6f532f 100644 --- a/mascara/src/mascara.js +++ b/mascara/src/mascara.js @@ -1,47 +1 @@ -const Web3 = require('web3') -const setupProvider = require('./lib/setup-provider.js') -const setupDappAutoReload = require('../../app/scripts/lib/auto-reload.js') -const MASCARA_ORIGIN = process.env.MASCARA_ORIGIN || 'http://localhost:9001' -console.log('MASCARA_ORIGIN:', MASCARA_ORIGIN) - -// -// setup web3 -// - -const provider = setupProvider({ - mascaraUrl: MASCARA_ORIGIN + '/proxy/', -}) -instrumentForUserInteractionTriggers(provider) - -const web3 = new Web3(provider) -setupDappAutoReload(web3, provider.publicConfigStore) -// -// ui stuff -// - -let shouldPop = false -window.addEventListener('click', maybeTriggerPopup) - -// -// util -// - -function maybeTriggerPopup(){ - if (!shouldPop) return - shouldPop = false - window.open(MASCARA_ORIGIN, '', 'width=360 height=500') - console.log('opening window...') -} - -function instrumentForUserInteractionTriggers(provider){ - const _super = provider.sendAsync.bind(provider) - provider.sendAsync = function(payload, cb){ - if (payload.method === 'eth_sendTransaction') { - console.log('saw send') - shouldPop = true - } - _super(payload, cb) - } -} - - +global.metamask = require('metamascara') diff --git a/package.json b/package.json index 14e7f100f..4da6d99a7 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "json-rpc-engine": "^3.1.0", "json-rpc-middleware-stream": "^1.0.0", "loglevel": "^1.4.1", + "metamascara": "^1.1.1", "metamask-logo": "^2.1.2", "mississippi": "^1.2.0", "mkdirp": "^0.5.1", -- cgit v1.2.3 From 51b40adecd5586e6ede75362fcfc4756a8ec0062 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 18 Sep 2017 22:42:04 -0700 Subject: v3.10.2 published `v3.10.2` as an emergency rollback to `v3.10.0` --- app/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/manifest.json b/app/manifest.json index 8febf91aa..67fb543b9 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.10.1", + "version": "3.10.2", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From 0424ab3e48596ec682cc58992879da377dc9dc55 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 18 Sep 2017 22:44:40 -0700 Subject: v3.10.2 - changelog add changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 464cbe43c..c4366db45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ - Fix bug that would sometimes display transactions as failed that could be successfully mined. +## 3.10.2 2017-9-18 + +rollback to 3.10.0 due to bug + ## 3.10.1 2017-9-18 - Add ability to export private keys as a file. -- cgit v1.2.3 From bfd75107f11995e0636def0e1efa6dd14b3ee8a6 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 19 Sep 2017 10:45:32 -0700 Subject: add context to platform to not have X-Metamask-Origin in mascara --- app/scripts/background.js | 1 + app/scripts/metamask-controller.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index 1b96d68b5..a271db263 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -20,6 +20,7 @@ window.log = log log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn') const platform = new ExtensionPlatform() +platform.context = 'extension' const notificationManager = new NotificationManager() global.METAMASK_NOTIFIER = notificationManager diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index fef16c3a9..3a2f3878c 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -196,7 +196,7 @@ module.exports = class MetamaskController extends EventEmitter { }, // rpc data source rpcUrl: this.networkController.getCurrentRpcAddress(), - originHttpHeaderKey: 'X-Metamask-Origin', + originHttpHeaderKey: this.platform.context === 'extension' ? 'X-Metamask-Origin' : undefined, // account mgmt getAccounts: (cb) => { const isUnlocked = this.keyringController.memStore.getState().isUnlocked -- cgit v1.2.3 From d2ded61cc9ae9a9354d947c24929f74e8139a2bc Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 19 Sep 2017 10:54:41 -0700 Subject: deps - bump json-rpc-engine --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 3f9d9c538..986e6e187 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "iframe-stream": "^3.0.0", "inject-css": "^0.1.1", "jazzicon": "^1.2.0", - "json-rpc-engine": "^3.1.0", + "json-rpc-engine": "^3.2.0", "json-rpc-middleware-stream": "^1.0.0", "loglevel": "^1.4.1", "metamask-logo": "^2.1.2", @@ -174,7 +174,6 @@ "jsdom": "^11.1.0", "jsdom-global": "^3.0.2", "jshint-stylish": "~2.2.1", - "json-rpc-engine": "^3.0.1", "karma": "^1.7.1", "karma-chrome-launcher": "^2.2.0", "karma-cli": "^1.0.1", -- cgit v1.2.3 From b979c6a2f3856525faaff0de94a3e97e322d18b6 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 19 Sep 2017 11:22:55 -0700 Subject: deps - bump json-rpc-middleware-stream --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 986e6e187..7d5f01f2d 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "inject-css": "^0.1.1", "jazzicon": "^1.2.0", "json-rpc-engine": "^3.2.0", - "json-rpc-middleware-stream": "^1.0.0", + "json-rpc-middleware-stream": "^1.0.1", "loglevel": "^1.4.1", "metamask-logo": "^2.1.2", "mississippi": "^1.2.0", -- cgit v1.2.3 From 3a3e1511e5cbeab1b94bb4052050c67eb3635ecc Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 19 Sep 2017 11:30:06 -0700 Subject: changelog - add note on filter fixes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4366db45..3ff062cf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Current Master +- Fix bug where metamask-dapp connections are lost on rpc error - Fix bug that would sometimes display transactions as failed that could be successfully mined. ## 3.10.2 2017-9-18 -- cgit v1.2.3 From 4e57cdf8b35c0bde571fb108b76d70ca251644f7 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 19 Sep 2017 10:53:56 -0700 Subject: stub platform --- mock-dev.js | 1 + test/unit/metamask-controller-test.js | 1 + 2 files changed, 2 insertions(+) diff --git a/mock-dev.js b/mock-dev.js index a47f1ed4d..0a3eb12ce 100644 --- a/mock-dev.js +++ b/mock-dev.js @@ -62,6 +62,7 @@ const controller = new MetamaskController({ showUnconfirmedMessage: noop, unlockAccountMessage: noop, showUnapprovedTx: noop, + platform: {}, // initial state initState: firstTimeState, }) diff --git a/test/unit/metamask-controller-test.js b/test/unit/metamask-controller-test.js index 5ee0a6c84..ef6cae758 100644 --- a/test/unit/metamask-controller-test.js +++ b/test/unit/metamask-controller-test.js @@ -10,6 +10,7 @@ describe('MetaMaskController', function () { showUnconfirmedMessage: noop, unlockAccountMessage: noop, showUnapprovedTx: noop, + platform: {}, // initial state initState: clone(firstTimeState), }) -- cgit v1.2.3 From 566ffee8cd4c2d25e5399a9f6fb4d9ed1c8cd564 Mon Sep 17 00:00:00 2001 From: Dan Date: Mon, 18 Sep 2017 22:05:52 -0230 Subject: Ensure conversion util does not return insignificant trailing zeroes. --- ui/app/conversion-util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js index b440aea7f..29e5ce668 100644 --- a/ui/app/conversion-util.js +++ b/ui/app/conversion-util.js @@ -51,7 +51,7 @@ const toSpecifiedDenomination = { } const baseChange = { hex: n => n.toString(16), - dec: n => n.toString(10), + dec: n => Number(n).toString(10), BN: n => new BN(n.toString(16)), } -- cgit v1.2.3 From daeb7f6ad3aefae7c643a753320edf178e9bc9bb Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 19 Sep 2017 12:58:51 -0700 Subject: update metamascara --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a7f52abee..f23f31d7c 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "json-rpc-engine": "^3.1.0", "json-rpc-middleware-stream": "^1.0.0", "loglevel": "^1.4.1", - "metamascara": "^1.1.1", + "metamascara": "^1.2.1", "metamask-logo": "^2.1.2", "mississippi": "^1.2.0", "mkdirp": "^0.5.1", -- cgit v1.2.3 From 418f01411e3c41505d2011767e571af9401c3a2d Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 19 Sep 2017 16:48:42 -0700 Subject: mascara: turn off background --- mascara/src/background.js | 3 +-- package.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/mascara/src/background.js b/mascara/src/background.js index d9dbf593a..5ba865ad8 100644 --- a/mascara/src/background.js +++ b/mascara/src/background.js @@ -19,8 +19,7 @@ const migrations = require('../../app/scripts/migrations/') const firstTimeState = require('../../app/scripts/first-time-state') const STORAGE_KEY = 'metamask-config' -// const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' -const METAMASK_DEBUG = true +const METAMASK_DEBUG = process.env.METAMASK_DEBUG let popupIsOpen = false let connectedClientCount = 0 diff --git a/package.json b/package.json index f23f31d7c..c897af8a5 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "ui": "npm run test:flat:build:states && beefy ui-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./", "mock": "beefy mock-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./", "watch": "mocha watch --recursive \"test/unit/**/*.js\"", - "mascara": "node ./mascara/example/server", + "mascara": "METAMASK_DEBUG=true node ./mascara/example/server", "dist": "npm run dist:clear && npm install && gulp dist", "dist:clear": "rm -rf node_modules/eth-contract-metadata && rm -rf node_modules/eth-phishing-detect", "test": "npm run lint && npm run test:coverage && npm run test:integration", -- cgit v1.2.3 From e7f1fc44361829f05a713218f8b1837a8574c2f2 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Tue, 19 Sep 2017 18:49:35 -0700 Subject: Buy Modal Styling --- ui/app/components/modals/modal.js | 7 ++++--- ui/app/css/itcss/components/modal.scss | 20 +++++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index 477dcbe86..04a2f5f40 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -21,12 +21,13 @@ const MODALS = { mobileModalStyle: { width: '95%', top: isPopupOrNotification() === 'popup' ? '48vh' : '36.5vh', - boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + boxShadow: '0 0 7px 0 rgba(0,0,0,0.08)', }, laptopModalStyle: { width: '66%', + maxWidth: '550px', top: 'calc(30% + 10px)', - boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + boxShadow: '0 0 7px 0 rgba(0,0,0,0.08)', }, }, @@ -64,7 +65,7 @@ const MODALS = { }, contentStyle: { borderRadius: '4px', - } + }, }, NEW_ACCOUNT: { diff --git a/ui/app/css/itcss/components/modal.scss b/ui/app/css/itcss/components/modal.scss index e9698ce5b..c85e61ae2 100644 --- a/ui/app/css/itcss/components/modal.scss +++ b/ui/app/css/itcss/components/modal.scss @@ -9,6 +9,12 @@ justify-content: center; text-align: center; font-family: 'DIN OT'; + padding: 0 16px; +} + +.buy-modal-content-option { + cursor: pointer; + color: #5B5D67; } @media screen and (max-width: 575px) { @@ -44,7 +50,6 @@ border-radius: 6px; border: 1px solid $black; padding: 0% 7%; - justify-content: space-around; div.buy-modal-content-option-title { font-size: 20px; @@ -76,29 +81,30 @@ .buy-modal-content-options { flex-direction: row; - margin: 20px 0; + margin: 20px 0 60px; } div.buy-modal-content-option { display: flex; flex-direction: column; width: 20vw; - height: 18vw; + height: 120px; text-align: center; border-radius: 6px; border: 1px solid $black; - margin: 0 .5vw; - justify-content: space-around; + margin: 0 8px; + padding: 18px 0; div.buy-modal-content-option-title { font-size: 20px; + margin-bottom: 12px; @media screen and (max-width: 679px) { font-size: 14px; } @media screen and (min-width: 1281px) { - font-size: 26px; + font-size: 20px; } } @@ -121,7 +127,7 @@ } @media screen and (min-width: 1281px) { - font-size: 20px; + font-size: 16px; padding: 0; } } -- cgit v1.2.3 From 0204aa2001af25da01ba61aed32f36eac47079a1 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Tue, 19 Sep 2017 21:18:36 -0700 Subject: Add Add Token UI; Add Fuzzy search for tokens --- package.json | 5 +- ui/app/add-token.js | 348 ++++++++++++++++++----------- ui/app/css/itcss/components/add-token.scss | 173 ++++++++++++++ ui/app/css/itcss/components/index.scss | 2 + yarn.lock | 70 ++++-- 5 files changed, 458 insertions(+), 140 deletions(-) create mode 100644 ui/app/css/itcss/components/add-token.scss diff --git a/package.json b/package.json index b615cab20..c6bd437f1 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "end-of-stream": "^1.1.0", "ensnare": "^1.0.0", "eth-bin-to-ops": "^1.0.1", - "eth-contract-metadata": "^1.1.4", + "eth-contract-metadata": "^1.1.5", "eth-hd-keyring": "^1.1.1", "eth-json-rpc-filters": "^1.1.0", "eth-phishing-detect": "^1.1.4", @@ -93,6 +93,7 @@ "extensionizer": "^1.0.0", "fast-json-patch": "^2.0.4", "fast-levenshtein": "^2.0.6", + "fuse.js": "^3.1.0", "gulp": "github:gulpjs/gulp#4.0", "gulp-autoprefixer": "^4.0.0", "gulp-eslint": "^4.0.0", @@ -212,8 +213,8 @@ "react-addons-test-utils": "^15.5.1", "react-test-renderer": "^15.5.4", "react-testutils-additions": "^15.2.0", - "stylelint-config-standard": "^17.0.0", "sinon": "^3.2.0", + "stylelint-config-standard": "^17.0.0", "tape": "^4.5.1", "testem": "^1.10.3", "uglifyify": "^4.0.2", diff --git a/ui/app/add-token.js b/ui/app/add-token.js index 4374ee586..dbba8e4f1 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -2,8 +2,20 @@ const inherits = require('util').inherits const Component = require('react').Component const h = require('react-hyperscript') const connect = require('react-redux').connect -const actions = require('./actions') -const Tooltip = require('./components/tooltip.js') +const Fuse = require('fuse.js') +const contractMap = require('eth-contract-metadata') +const contractList = Object.entries(contractMap).map(([ _, tokenData]) => tokenData) +const fuse = new Fuse(contractList, { + shouldSort: true, + threshold: 0.3, + location: 0, + distance: 100, + maxPatternLength: 32, + minMatchCharLength: 1, + keys: ['address', 'name', 'symbol'], +}) +// const actions = require('./actions') +// const Tooltip = require('./components/tooltip.js') const ethUtil = require('ethereumjs-util') @@ -24,146 +36,232 @@ function mapStateToProps (state) { inherits(AddTokenScreen, Component) function AddTokenScreen () { this.state = { - warning: null, - address: null, - symbol: 'TOKEN', - decimals: 18, + // warning: null, + // address: null, + // symbol: 'TOKEN', + // decimals: 18, + searchQuery: '', + isCollapsed: true, } Component.call(this) } -AddTokenScreen.prototype.render = function () { - const state = this.state - const props = this.props - const { warning, symbol, decimals } = state - - return ( - h('.flex-column.flex-grow', [ - - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: (event) => { - props.dispatch(actions.goHome()) - }, - }), - h('h2.page-subtitle', 'Add Token'), +AddTokenScreen.prototype.renderCustomForm = function () { + return !this.state.isCollapsed && ( + h('div.add-token__add-custom-form', [ + h('div.add-token__add-custom-field', [ + h('div.add-token__add-custom-label', 'Token Address'), + h('input.add-token__add-custom-input', { type: 'text' }), ]), + h('div.add-token__add-custom-field', [ + h('div.add-token__add-custom-label', 'Token Symbol'), + h('input.add-token__add-custom-input', { type: 'text', disabled: true }), + ]), + h('div.add-token__add-custom-field', [ + h('div.add-token__add-custom-label', 'Decimals of Precision'), + h('input.add-token__add-custom-input', { type: 'text', disabled: true }), + ]), + ]) + ) +} - h('.error', { - style: { - display: warning ? 'block' : 'none', - padding: '0 20px', - textAlign: 'center', - }, - }, warning), - - // conf view - h('.flex-column.flex-justify-center.flex-grow.select-none', [ - h('.flex-space-around', { - style: { - padding: '20px', - }, - }, [ - - h('div', [ - h(Tooltip, { - position: 'top', - title: 'The contract of the actual token contract. Click for more info.', - }, [ - h('a', { - style: { fontWeight: 'bold', paddingRight: '10px'}, - href: 'https://consensyssupport.happyfox.com/staff/kb/article/24-what-is-a-token-contract-address', - target: '_blank', - }, [ - h('span', 'Token Contract Address '), - h('i.fa.fa-question-circle'), - ]), - ]), - ]), - - h('section.flex-row.flex-center', [ - h('input#token-address', { - name: 'address', - placeholder: 'Token Contract Address', - onChange: this.tokenAddressDidChange.bind(this), - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - }), - ]), - - h('div', [ - h('span', { - style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Token Symbol'), - ]), - - h('div', { style: {display: 'flex'} }, [ - h('input#token_symbol', { - placeholder: `Like "ETH"`, - value: symbol, - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - onChange: (event) => { - var element = event.target - var symbol = element.value - this.setState({ symbol }) - }, - }), +AddTokenScreen.prototype.renderTokenList = function () { + const { searchQuery = '' } = this.state + const results = searchQuery + ? fuse.search(searchQuery) || [] + : contractList + + return Array(6).fill(undefined) + .map((_, i) => { + const { logo, symbol, name } = results[i] || {} + console.log({ i, logo, symbol, name }) + return Boolean(logo || symbol || name) && ( + h('div.add-token__token-wrapper', [ + h('div.add-token__token-icon', { + style: { + backgroundImage: `url(images/contract/${logo})`, + }, + }), + h('div.add-token__token-data', [ + h('div.add-token__token-symbol', symbol), + h('div.add-token__token-name', name), ]), + ]) + ) + }) +} - h('div', [ - h('span', { - style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Decimals of Precision'), - ]), +AddTokenScreen.prototype.render = function () { + const { isCollapsed } = this.state - h('div', { style: {display: 'flex'} }, [ - h('input#token_decimals', { - value: decimals, - type: 'number', - min: 0, - max: 36, - style: { - width: 'inherit', - flex: '1 0 auto', - height: '30px', - margin: '8px', - }, - onChange: (event) => { - var element = event.target - var decimals = element.value.trim() - this.setState({ decimals }) - }, + return ( + h('div.add-token', [ + h('div.add-token__wrapper', [ + h('div.add-token__title-container', [ + h('div.add-token__title', 'Add Token'), + h('div.add-token__description', 'Keep track of the tokens you’ve bought with your MetaMask account. If you bought tokens using a different account, those tokens will not appear here.'), + h('div.add-token__description', 'Search for tokens or select from our list of popular tokens.'), + ]), + h('div.add-token__content-container', [ + h('div.add-token__input-container', [ + h('input.add-token__input', { + type: 'text', + placeholder: 'Search', + onChange: e => this.setState({ searchQuery: e.target.value }), }), ]), - - h('button', { - style: { - alignSelf: 'center', - }, - onClick: (event) => { - const valid = this.validateInputs() - if (!valid) return - - const { address, symbol, decimals } = this.state - this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) - }, - }, 'Add'), + h( + 'div.add-token__token-icons-container', + this.renderTokenList(), + ), + ]), + h('div.add-token__footers', [ + h('div.add-token__add-custom', { + onClick: () => this.setState({ isCollapsed: !isCollapsed }), + }, 'Add custom token'), + this.renderCustomForm(), ]), ]), + h('div.add-token__buttons', [ + h('button.btn-secondary', 'Next'), + h('button.btn-tertiary', 'Cancel'), + ]), ]) ) } +// AddTokenScreen.prototype.render = function () { +// const state = this.state +// const props = this.props +// const { warning, symbol, decimals } = state + +// return ( +// h('.flex-column.flex-grow', [ + +// // subtitle and nav +// h('.section-title.flex-row.flex-center', [ +// h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { +// onClick: (event) => { +// props.dispatch(actions.goHome()) +// }, +// }), +// h('h2.page-subtitle', 'Add Token'), +// ]), + +// h('.error', { +// style: { +// display: warning ? 'block' : 'none', +// padding: '0 20px', +// textAlign: 'center', +// }, +// }, warning), + +// // conf view +// h('.flex-column.flex-justify-center.flex-grow.select-none', [ +// h('.flex-space-around', { +// style: { +// padding: '20px', +// }, +// }, [ + +// h('div', [ +// h(Tooltip, { +// position: 'top', +// title: 'The contract of the actual token contract. Click for more info.', +// }, [ +// h('a', { +// style: { fontWeight: 'bold', paddingRight: '10px'}, +// href: 'https://consensyssupport.happyfox.com/staff/kb/article/24-what-is-a-token-contract-address', +// target: '_blank', +// }, [ +// h('span', 'Token Contract Address '), +// h('i.fa.fa-question-circle'), +// ]), +// ]), +// ]), + +// h('section.flex-row.flex-center', [ +// h('input#token-address', { +// name: 'address', +// placeholder: 'Token Contract Address', +// onChange: this.tokenAddressDidChange.bind(this), +// style: { +// width: 'inherit', +// flex: '1 0 auto', +// height: '30px', +// margin: '8px', +// }, +// }), +// ]), + +// h('div', [ +// h('span', { +// style: { fontWeight: 'bold', paddingRight: '10px'}, +// }, 'Token Symbol'), +// ]), + +// h('div', { style: {display: 'flex'} }, [ +// h('input#token_symbol', { +// placeholder: `Like "ETH"`, +// value: symbol, +// style: { +// width: 'inherit', +// flex: '1 0 auto', +// height: '30px', +// margin: '8px', +// }, +// onChange: (event) => { +// var element = event.target +// var symbol = element.value +// this.setState({ symbol }) +// }, +// }), +// ]), + +// h('div', [ +// h('span', { +// style: { fontWeight: 'bold', paddingRight: '10px'}, +// }, 'Decimals of Precision'), +// ]), + +// h('div', { style: {display: 'flex'} }, [ +// h('input#token_decimals', { +// value: decimals, +// type: 'number', +// min: 0, +// max: 36, +// style: { +// width: 'inherit', +// flex: '1 0 auto', +// height: '30px', +// margin: '8px', +// }, +// onChange: (event) => { +// var element = event.target +// var decimals = element.value.trim() +// this.setState({ decimals }) +// }, +// }), +// ]), + +// h('button', { +// style: { +// alignSelf: 'center', +// }, +// onClick: (event) => { +// const valid = this.validateInputs() +// if (!valid) return + +// const { address, symbol, decimals } = this.state +// this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) +// }, +// }, 'Add'), +// ]), +// ]), +// ]) +// ) +// } + AddTokenScreen.prototype.componentWillMount = function () { if (typeof global.ethereumProvider === 'undefined') return diff --git a/ui/app/css/itcss/components/add-token.scss b/ui/app/css/itcss/components/add-token.scss new file mode 100644 index 000000000..db1d0dc18 --- /dev/null +++ b/ui/app/css/itcss/components/add-token.scss @@ -0,0 +1,173 @@ +.add-token { + width: 498px; + display: flex; + flex-flow: column nowrap; + align-items: center; + position: relative; + top: -36px; + z-index: 12; + font-family: 'DIN Next Light'; + + @media screen and (max-width: $break-small) { + top: 0; + width: 100%; + + &__wrapper { + box-shadow: none !important; + } + + &__footers { + border-bottom: 1px solid $gallery; + } + } + + &__wrapper { + background-color: $white; + box-shadow: 0 2px 4px 0 rgba($black, .08); + display: flex; + flex-flow: column nowrap; + align-items: center; + flex: 0 0 auto; + } + + &__title-container { + display: flex; + flex-flow: column nowrap; + align-items: center; + padding: 30px 60px 12px; + border-bottom: 1px solid $gallery; + flex: 0 0 auto; + } + + &__title { + color: $scorpion; + font-size: 20px; + line-height: 26px; + text-align: center; + font-weight: 600; + margin-bottom: 12px; + } + + &__description { + text-align: center; + } + + &__description + &__description { + margin-top: 24px; + } + + &__content-container { + width: 100%; + border-bottom: 1px solid $gallery; + } + + &__input-container { + padding: 11px 0; + width: 263px; + margin: 0 auto; + } + + &__input { + width: 100%; + border: 2px solid $gallery; + border-radius: 4px; + padding: 5px 15px; + font-size: 14px; + line-height: 19px; + + &::placeholder { + color: $silver; + } + } + + &__footers { + width: 100%; + } + + &__add-custom { + color: $scorpion; + font-size: 18px; + line-height: 24px; + text-align: center; + padding: 11px 0 19px; + font-weight: 600; + cursor: pointer; + } + + &__add-custom-form { + display: flex; + flex-flow: column nowrap; + margin: 8px 0 51px; + } + + &__add-custom-field { + width: 290px; + margin: 0 auto; + } + + &__add-custom-label { + font-size: 16px; + line-height: 21px; + margin-bottom: 8px; + } + + &__add-custom-input { + width: 100%; + border: 1px solid $silver; + padding: 5px 15px; + font-size: 14px; + line-height: 19px; + + &::placeholder { + color: $silver; + } + } + + &__add-custom-field + &__add-custom-field { + margin-top: 21px; + } + + &__buttons { + display: flex; + flex-flow: column nowrap; + margin: 30px 0 51px; + flex: 0 0 auto; + } + + &__token-icons-container { + display: flex; + flex-flow: row wrap; + } + + &__token-wrapper { + display: flex; + flex-flow: row nowrap; + flex: 0 0 50%; + align-items: center; + padding: 24px 0 24px 24px; + } + + &__token-name { + font-size: 14px; + line-height: 19px; + } + + &__token-symbol { + font-size: 22px; + line-height: 29px; + font-weight: 600; + } + + &__token-icon { + width: 60px; + height: 60px; + background-repeat: no-repeat; + background-size: contain; + background-position: center; + border-radius: 50%; + background-color: $white; + box-shadow: 0 2px 4px 0 rgba($black, .24); + margin-right: 12px; + flex: 0 0 auto; + } +} diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss index 63ac8bd47..9b3690099 100644 --- a/ui/app/css/itcss/components/index.scss +++ b/ui/app/css/itcss/components/index.scss @@ -27,3 +27,5 @@ @import './sections.scss'; @import './token-list.scss'; + +@import './add-token.scss'; diff --git a/yarn.lock b/yarn.lock index aed3a9da8..078ab75cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1950,6 +1950,12 @@ cli-width@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" +cli@0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/cli/-/cli-0.4.3.tgz#e6819c8d5faa957f64f98f66a8506268c1d1f17d" + dependencies: + glob ">= 3.1.4" + client-sw-ready-event@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/client-sw-ready-event/-/client-sw-ready-event-3.3.0.tgz#988d1045562b0c228e33d9247a6dd3ed7b276fe3" @@ -2063,7 +2069,7 @@ colors@1.0.3, colors@1.0.x: version "1.0.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" -colors@^1.1.0, colors@^1.1.2: +colors@>=0.6.x, colors@^1.1.0, colors@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" @@ -3370,7 +3376,7 @@ eth-block-tracker@^2.0.1, eth-block-tracker@^2.1.2: pify "^2.3.0" tape "^4.6.3" -eth-contract-metadata@^1.1.4: +eth-contract-metadata@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/eth-contract-metadata/-/eth-contract-metadata-1.1.5.tgz#301f51b0460b8dd044997dc05870751fb7f4cfcb" @@ -4211,6 +4217,20 @@ functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" +fuse.js@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.1.0.tgz#9062146c471552189b0f678b4f5a155731ae3b3c" + +fuse@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/fuse/-/fuse-0.4.0.tgz#2c38eaf888abb0a9ba7960cfe3339d1f3f53f6e6" + dependencies: + colors ">=0.6.x" + jshint "0.9.x" + optimist ">=0.3.5" + uglify-js ">=2.2.x" + underscore ">=1.4.x" + gather-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/gather-stream/-/gather-stream-1.0.0.tgz#b33994af457a8115700d410f317733cbe7a0904b" @@ -4346,24 +4366,24 @@ glob@7.1.1: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^5.0.15, glob@^5.0.3, glob@~5.0.0: - version "5.0.15" - resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" +"glob@>= 3.1.4", glob@^7.0.0, glob@^7.0.3, glob@^7.0.4, glob@^7.0.5, glob@^7.0.6, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1, glob@~7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: + fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "2 || 3" + minimatch "^3.0.4" once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.0.4, glob@^7.0.5, glob@^7.0.6, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1, glob@~7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" +glob@^5.0.15, glob@^5.0.3, glob@~5.0.0: + version "5.0.15" + resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" dependencies: - fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.0.4" + minimatch "2 || 3" once "^1.3.0" path-is-absolute "^1.0.0" @@ -5521,6 +5541,13 @@ jshint-stylish@~2.2.1: string-length "^1.0.0" text-table "^0.2.0" +jshint@0.9.x: + version "0.9.1" + resolved "https://registry.yarnpkg.com/jshint/-/jshint-0.9.1.tgz#ff32ec7f09f84001f7498eeafd63c9e4fbb2dc0e" + dependencies: + cli "0.4.3" + minimatch "0.0.x" + jsmin@1.x: version "1.0.1" resolved "https://registry.yarnpkg.com/jsmin/-/jsmin-1.0.1.tgz#e7bd0dcd6496c3bf4863235bf461a3d98aa3b98c" @@ -6236,6 +6263,10 @@ lru-cache@^4.0.1: pseudomap "^1.0.2" yallist "^2.1.2" +lru-cache@~1.0.2: + version "1.0.6" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-1.0.6.tgz#aa50f97047422ac72543bda177a9c9d018d98452" + lru-queue@0.1: version "0.1.0" resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" @@ -6487,6 +6518,12 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" +minimatch@0.0.x: + version "0.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.0.5.tgz#96bb490bbd3ba6836bbfac111adf75301b1584de" + dependencies: + lru-cache "~1.0.2" + "minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -7074,7 +7111,7 @@ opener@^1.3.0: version "1.4.3" resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8" -optimist@^0.6.1: +optimist@>=0.3.5, optimist@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" dependencies: @@ -9697,6 +9734,13 @@ uglify-js@1.x: version "1.3.5" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-1.3.5.tgz#4b5bfff9186effbaa888e4c9e94bd9fc4c94929d" +uglify-js@>=2.2.x: + version "3.1.1" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.1.1.tgz#e7144307281a1bc38a9a20715090b546c9f44791" + dependencies: + commander "~2.11.0" + source-map "~0.5.1" + uglify-js@^2.6, uglify-js@^2.8.27: version "2.8.29" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" @@ -9736,7 +9780,7 @@ unc-path-regex@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" -underscore@>=1.8.3, underscore@^1.6.0: +underscore@>=1.4.x, underscore@>=1.8.3, underscore@^1.6.0: version "1.8.3" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" -- cgit v1.2.3 From 04da22db0863a9a361a0f414d9cc37bf3bb3a392 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Wed, 20 Sep 2017 22:57:36 -0700 Subject: Add Token UI - hover/select state; fetch token address --- ui/app/add-token.js | 85 ++++++++++++++++++++++++------ ui/app/css/itcss/components/add-token.scss | 15 +++++- ui/app/css/itcss/settings/variables.scss | 1 + 3 files changed, 82 insertions(+), 19 deletions(-) diff --git a/ui/app/add-token.js b/ui/app/add-token.js index dbba8e4f1..622cf2bc2 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -1,5 +1,6 @@ const inherits = require('util').inherits const Component = require('react').Component +const classnames = require('classnames') const h = require('react-hyperscript') const connect = require('react-redux').connect const Fuse = require('fuse.js') @@ -7,14 +8,14 @@ const contractMap = require('eth-contract-metadata') const contractList = Object.entries(contractMap).map(([ _, tokenData]) => tokenData) const fuse = new Fuse(contractList, { shouldSort: true, - threshold: 0.3, + threshold: 0.45, location: 0, distance: 100, maxPatternLength: 32, minMatchCharLength: 1, keys: ['address', 'name', 'symbol'], }) -// const actions = require('./actions') +const actions = require('./actions') // const Tooltip = require('./components/tooltip.js') @@ -25,7 +26,7 @@ const EthContract = require('ethjs-contract') const emptyAddr = '0x0000000000000000000000000000000000000000' -module.exports = connect(mapStateToProps)(AddTokenScreen) +module.exports = connect(mapStateToProps, mapDispatchToProps)(AddTokenScreen) function mapStateToProps (state) { return { @@ -33,6 +34,12 @@ function mapStateToProps (state) { } } +function mapDispatchToProps (dispatch) { + return { + goHome: () => dispatch(actions.goHome()), + } +} + inherits(AddTokenScreen, Component) function AddTokenScreen () { this.state = { @@ -40,33 +47,63 @@ function AddTokenScreen () { // address: null, // symbol: 'TOKEN', // decimals: 18, + customAddress: '', + customSymbol: '', + customDecimals: 0, searchQuery: '', isCollapsed: true, + selectedToken: {}, } + this.tokenAddressDidChange = this.tokenAddressDidChange.bind(this) Component.call(this) } +AddTokenScreen.prototype.toggleToken = function (symbol) { + const { selectedToken } = this.state + const { [symbol]: isSelected } = selectedToken + this.setState({ + selectedToken: { + ...selectedToken, + [symbol]: !isSelected, + }, + }) +} + AddTokenScreen.prototype.renderCustomForm = function () { + const { customAddress, customSymbol, customDecimals } = this.state + return !this.state.isCollapsed && ( h('div.add-token__add-custom-form', [ h('div.add-token__add-custom-field', [ h('div.add-token__add-custom-label', 'Token Address'), - h('input.add-token__add-custom-input', { type: 'text' }), + h('input.add-token__add-custom-input', { + type: 'text', + onChange: this.tokenAddressDidChange, + value: customAddress, + }), ]), h('div.add-token__add-custom-field', [ h('div.add-token__add-custom-label', 'Token Symbol'), - h('input.add-token__add-custom-input', { type: 'text', disabled: true }), + h('input.add-token__add-custom-input', { + type: 'text', + value: customSymbol, + disabled: true, + }), ]), h('div.add-token__add-custom-field', [ h('div.add-token__add-custom-label', 'Decimals of Precision'), - h('input.add-token__add-custom-input', { type: 'text', disabled: true }), + h('input.add-token__add-custom-input', { + type: 'number', + value: customDecimals, + disabled: true, + }), ]), ]) ) } AddTokenScreen.prototype.renderTokenList = function () { - const { searchQuery = '' } = this.state + const { searchQuery = '', selectedToken } = this.state const results = searchQuery ? fuse.search(searchQuery) || [] : contractList @@ -74,9 +111,13 @@ AddTokenScreen.prototype.renderTokenList = function () { return Array(6).fill(undefined) .map((_, i) => { const { logo, symbol, name } = results[i] || {} - console.log({ i, logo, symbol, name }) return Boolean(logo || symbol || name) && ( - h('div.add-token__token-wrapper', [ + h('div.add-token__token-wrapper', { + className: classnames('add-token__token-wrapper', { + 'add-token__token-wrapper--selected': selectedToken[symbol], + }), + onClick: () => this.toggleToken(symbol), + }, [ h('div.add-token__token-icon', { style: { backgroundImage: `url(images/contract/${logo})`, @@ -93,6 +134,7 @@ AddTokenScreen.prototype.renderTokenList = function () { AddTokenScreen.prototype.render = function () { const { isCollapsed } = this.state + const { goHome } = this.props return ( h('div.add-token', [ @@ -124,7 +166,9 @@ AddTokenScreen.prototype.render = function () { ]), h('div.add-token__buttons', [ h('button.btn-secondary', 'Next'), - h('button.btn-tertiary', 'Cancel'), + h('button.btn-tertiary', { + onClick: goHome, + }, 'Cancel'), ]), ]) ) @@ -270,12 +314,16 @@ AddTokenScreen.prototype.componentWillMount = function () { this.TokenContract = this.contract(abi) } -AddTokenScreen.prototype.tokenAddressDidChange = function (event) { - const el = event.target - const address = el.value.trim() - if (ethUtil.isValidAddress(address) && address !== emptyAddr) { - this.setState({ address }) - this.attemptToAutoFillTokenParams(address) +AddTokenScreen.prototype.tokenAddressDidChange = function (e) { + const customAddress = e.target.value.trim() + this.setState({ customAddress }) + if (ethUtil.isValidAddress(customAddress) && customAddress !== emptyAddr) { + this.attemptToAutoFillTokenParams(customAddress) + } else { + this.setState({ + customSymbol: '', + customDecimals: 0, + }) } } @@ -330,6 +378,9 @@ AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) const [ symbol, decimals ] = results if (symbol && decimals) { - this.setState({ symbol: symbol[0], decimals: decimals[0].toString() }) + this.setState({ + customSymbol: symbol[0], + customDecimals: decimals[0].toString(), + }) } } diff --git a/ui/app/css/itcss/components/add-token.scss b/ui/app/css/itcss/components/add-token.scss index db1d0dc18..ebfdf7b11 100644 --- a/ui/app/css/itcss/components/add-token.scss +++ b/ui/app/css/itcss/components/add-token.scss @@ -140,11 +140,22 @@ } &__token-wrapper { + transition: 200ms ease-in-out; display: flex; flex-flow: row nowrap; - flex: 0 0 50%; + flex: 0 0 45%; align-items: center; - padding: 24px 0 24px 24px; + padding: 12px; + margin: 2.5%; + box-sizing: border-box; + border-radius: 10px; + cursor: pointer; + border: 2px solid transparent; + + &:hover, + &--selected { + border: 2px solid $malibu-blue; + } } &__token-name { diff --git a/ui/app/css/itcss/settings/variables.scss b/ui/app/css/itcss/settings/variables.scss index 624b301d1..103a7ffe0 100644 --- a/ui/app/css/itcss/settings/variables.scss +++ b/ui/app/css/itcss/settings/variables.scss @@ -38,6 +38,7 @@ $crimson: #e91550; $blue-lagoon: #038789; $purple: #690496; $tulip-tree: #ebb33f; +$malibu-blue: #7ac9fd; /* Z-Indicies -- cgit v1.2.3 From 7256894f51e3ef3073d7c5d8ab7b745ee5d36d2b Mon Sep 17 00:00:00 2001 From: Alex Lunyov Date: Thu, 21 Sep 2017 15:13:53 +0800 Subject: Fix CORS issues in FireFox --- app/manifest.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/manifest.json b/app/manifest.json index 67fb543b9..05e8d30de 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -57,7 +57,11 @@ "permissions": [ "storage", "clipboardWrite", - "http://localhost:8545/" + "http://localhost:8545/", + "https://rinkeby.infura.io/metamask/", + "https://mainnet.infura.io/metamask/", + "https://ropsten.infura.io/metamask/", + "https://kovan.infura.io/metamask/" ], "web_accessible_resources": [ "scripts/inpage.js" -- cgit v1.2.3 From 14b9d16eced03026da051604091833b5d19ab01e Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 21 Sep 2017 11:12:04 -0700 Subject: platforms: put context for extension in platform extension class --- app/scripts/background.js | 1 - app/scripts/metamask-controller.js | 2 +- app/scripts/platforms/extension.js | 3 +++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index a271db263..1b96d68b5 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -20,7 +20,6 @@ window.log = log log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn') const platform = new ExtensionPlatform() -platform.context = 'extension' const notificationManager = new NotificationManager() global.METAMASK_NOTIFIER = notificationManager diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 3a2f3878c..4d149545a 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -196,7 +196,7 @@ module.exports = class MetamaskController extends EventEmitter { }, // rpc data source rpcUrl: this.networkController.getCurrentRpcAddress(), - originHttpHeaderKey: this.platform.context === 'extension' ? 'X-Metamask-Origin' : undefined, + originHttpHeaderKey: this.platform.isExtension ? 'X-Metamask-Origin' : undefined, // account mgmt getAccounts: (cb) => { const isUnlocked = this.keyringController.memStore.getState().isUnlocked diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index 00c2aa275..d97138207 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -5,6 +5,9 @@ class ExtensionPlatform { // // Public // + get isExtension () { + return true + } reload () { extension.runtime.reload() -- cgit v1.2.3 From 8a874824a8e8dc5e16289f7753b13f96497379cd Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 21 Sep 2017 11:45:33 -0700 Subject: Version 3.10.3 --- CHANGELOG.md | 2 ++ app/manifest.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ff062cf8..e692c58dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +## 3.10.3 2017-9-21 + - Fix bug where metamask-dapp connections are lost on rpc error - Fix bug that would sometimes display transactions as failed that could be successfully mined. diff --git a/app/manifest.json b/app/manifest.json index 67fb543b9..fd07f15a9 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.10.2", + "version": "3.10.3", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From e9043f22dfa7856e3360b312ce480e71f36d9381 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 21 Sep 2017 15:47:25 -0700 Subject: Allow custom encryptor to be passed to MetaMaskController and KeyringControllers. --- app/scripts/keyring-controller.js | 2 +- app/scripts/metamask-controller.js | 3 ++- test/unit/keyring-controller-test.js | 5 +---- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js index fd57fac70..adfa4a813 100644 --- a/app/scripts/keyring-controller.js +++ b/app/scripts/keyring-controller.js @@ -36,7 +36,7 @@ class KeyringController extends EventEmitter { identities: {}, }) this.ethStore = opts.ethStore - this.encryptor = encryptor + this.encryptor = opts.encryptor || encryptor this.keyrings = [] this.getNetwork = opts.getNetwork } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index fef16c3a9..42248827f 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -95,6 +95,7 @@ module.exports = class MetamaskController extends EventEmitter { initState: initState.KeyringController, ethStore: this.ethStore, getNetwork: this.networkController.getNetworkState.bind(this.networkController), + encryptor: opts.encryptor || undefined, }) this.keyringController.on('newAccount', (address) => { this.preferencesController.setSelectedAddress(address) @@ -674,4 +675,4 @@ module.exports = class MetamaskController extends EventEmitter { return Promise.resolve(rpcTarget) }) } -} \ No newline at end of file +} diff --git a/test/unit/keyring-controller-test.js b/test/unit/keyring-controller-test.js index 2d9a53723..8d0d75f12 100644 --- a/test/unit/keyring-controller-test.js +++ b/test/unit/keyring-controller-test.js @@ -27,12 +27,9 @@ describe('KeyringController', function () { ethStore: { addAccount (acct) { accounts.push(ethUtil.addHexPrefix(acct)) }, }, + encryptor: mockEncryptor, }) - // Stub out the browser crypto for a mock encryptor. - // Browser crypto is tested in the integration test suite. - keyringController.encryptor = mockEncryptor - keyringController.createNewVaultAndKeychain(password) .then(function (newState) { newState -- cgit v1.2.3 From b25d4d5cfbf9a49e2669188198abd7377e697206 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 21 Sep 2017 15:56:44 -0700 Subject: Add platform docs including encryptor param --- docs/porting_to_new_environment.md | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/docs/porting_to_new_environment.md b/docs/porting_to_new_environment.md index 85670efa7..c6336b9f9 100644 --- a/docs/porting_to_new_environment.md +++ b/docs/porting_to_new_environment.md @@ -6,10 +6,31 @@ MetaMask has been under continuous development for nearly two years now, and we The core functionality of MetaMask all lives in what we call [The MetaMask Controller](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js). Our goal for this file is for it to eventually be its own javascript module that can be imported into any JS-compatible context, allowing it to fully manage an app's relationship to Ethereum. -The MM Controller exposes most of its functionality via two methods: +#### Constructor -- [metamask.getState()](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js#L241) - This method returns a javascript object representing the current MetaMask state. This includes things like known accounts, sent transactions, current exchange rates, and more! The controller is also an event emitter, so you can subscribe to state updates via `metamask.on('update', handleStateUpdate)`. State examples available [here](https://github.com/MetaMask/metamask-extension/tree/master/development/states) under the `metamask` key. (Warning: some are outdated) -- [metamask.getApi()](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js#L274-L335) - Returns a JavaScript object filled with callback functions representing every operation our user interface ever performs. Everything from creating new accounts, changing the current network, to sending a transaction, is provided via these API methods. We export this external API on an object because it allows us to easily expose this API over a port using [dnode](https://www.npmjs.com/package/dnode), which is how our WebExtension's UI works! +When calling `new MetaMask(opts)`, many platform-specific options are configured. The keys on `opts` are as follows: + +- initState: The last emitted state, used for restoring persistent state between sessions. +- platform: The `platform` object defines a variety of platform-specific functions, including opening the confirmation view, and customizing the encryption method. + +##### Platform Options + +The `platform` object has a variety of options: + +- reload (function) - Will be called when MetaMask would like to reload its own context. +- openWindow ({ url }) - Will be called when MetaMask would like to open a web page. It will be passed a single `options` object with a `url` key, with a string value. +- getVersion() - Should return the current MetaMask version, as described in the current `CHANGELOG.md` or `app/manifest.json`. +- encryptor - An object that includes two methods: + - encrypt(password, object) - returns a Promise of a string that is ready for storage. + - decrypt(password, encryptedString) - Accepts the encrypted output of `encrypt` and returns a Promise of a restored `object` as it was encrypted. + +#### [metamask.getState()](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js#L241) + +This method returns a javascript object representing the current MetaMask state. This includes things like known accounts, sent transactions, current exchange rates, and more! The controller is also an event emitter, so you can subscribe to state updates via `metamask.on('update', handleStateUpdate)`. State examples available [here](https://github.com/MetaMask/metamask-extension/tree/master/development/states) under the `metamask` key. (Warning: some are outdated) + +#### [metamask.getApi()](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js#L274-L335) + +Returns a JavaScript object filled with callback functions representing every operation our user interface ever performs. Everything from creating new accounts, changing the current network, to sending a transaction, is provided via these API methods. We export this external API on an object because it allows us to easily expose this API over a port using [dnode](https://www.npmjs.com/package/dnode), which is how our WebExtension's UI works! ### The UI @@ -62,4 +83,4 @@ If streams seem new and confusing to you, that's ok, they can seem strange at fi ## Conclusion -I hope this has been helpful to you! If you have any other questionsm, or points you think need clarification in this guide, please [open an issue on our GitHub](https://github.com/MetaMask/metamask-plugin/issues/new)! \ No newline at end of file +I hope this has been helpful to you! If you have any other questionsm, or points you think need clarification in this guide, please [open an issue on our GitHub](https://github.com/MetaMask/metamask-plugin/issues/new)! -- cgit v1.2.3 From 97810acb5355be575914a2e1e57a1cacd9fbccfa Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 20 Sep 2017 20:34:03 -0230 Subject: Handles errors with to field and renders warnings from backend in send token. --- ui/app/components/send-token/index.js | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/ui/app/components/send-token/index.js b/ui/app/components/send-token/index.js index 72fb593be..7adbf48dc 100644 --- a/ui/app/components/send-token/index.js +++ b/ui/app/components/send-token/index.js @@ -6,6 +6,7 @@ const classnames = require('classnames') const inherits = require('util').inherits const actions = require('../../actions') const selectors = require('../../selectors') +const { isValidAddress } = require('../../util') // const BalanceComponent = require('./balance-component') const Identicon = require('../identicon') @@ -14,12 +15,12 @@ const CurrencyToggle = require('../send/currency-toggle') const GasTooltip = require('../send/gas-tooltip') const GasFeeDisplay = require('../send/gas-fee-display') - module.exports = connect(mapStateToProps, mapDispatchToProps)(SendTokenScreen) function mapStateToProps (state) { // const sidebarOpen = state.appState.sidebarOpen + const { warning } = state.appState const identities = state.metamask.identities const addressBook = state.metamask.addressBook const conversionRate = state.metamask.conversionRate @@ -34,6 +35,7 @@ function mapStateToProps (state) { const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {} // const checksumAddress = selectedAddress && ethUtil.toChecksumAddress(selectedAddress) // const identity = identities[selectedAddress] + return { // sidebarOpen, selectedAddress, @@ -45,6 +47,7 @@ function mapStateToProps (state) { tokenExchangeRate, currentBlockGasLimit, selectedToken, + warning, // selectedToken: selectors.getSelectedToken(state), // identity, // network, @@ -106,13 +109,6 @@ SendTokenScreen.prototype.validate = function () { const gasLimit = parseInt(hexGasLimit, 16) / 1000000000 const amount = Number(stringAmount) - if (to && amount && gasPrice && gasLimit) { - return { - isValid: true, - errors: {}, - } - } - const errors = { to: !to ? 'Required' : null, amount: !amount ? 'Required' : null, @@ -120,9 +116,14 @@ SendTokenScreen.prototype.validate = function () { gasLimit: !gasLimit ? 'Gas Limit Required' : null, } + if(to && !isValidAddress(to)) { + errors.to = 'Invalid address' + } + + const isValid = Object.entries(errors).every(([key, value]) => value === null) return { - isValid: false, - errors, + isValid, + errors: isValid ? {} : errors, } } @@ -145,7 +146,6 @@ SendTokenScreen.prototype.submit = function () { } = this.props const { nickname = ' ' } = identities[to] || {} - const { isValid, errors } = this.validate() if (!isValid) { @@ -340,6 +340,7 @@ SendTokenScreen.prototype.render = function () { const { selectedTokenAddress, selectedToken, + warning, } = this.props return h('div.send-token', [ @@ -359,6 +360,11 @@ SendTokenScreen.prototype.render = function () { this.renderAmountInput(), this.renderGasInput(), this.renderMemoInput(), + warning && h('div.send-screen-input-wrapper--error', {}, + h('div.send-screen-input-wrapper__error-message', [ + warning, + ]) + ), ]), this.renderButtons(), ]) -- cgit v1.2.3 From 4fa79ffc6eccce1c758f719062d2db6abb65489c Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 20 Sep 2017 13:15:47 -0230 Subject: Clean up code in send.js --- ui/app/send.js | 427 ++++++++++++++++++++++++++------------------------------- 1 file changed, 193 insertions(+), 234 deletions(-) diff --git a/ui/app/send.js b/ui/app/send.js index bfc569b7d..481682bc8 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -18,12 +18,10 @@ const { signTx, } = require('./actions') const { stripHexPrefix, addHexPrefix } = require('ethereumjs-util') -const { isHex, numericBalance } = require('./util') +const { isHex, numericBalance, isValidAddress } = require('./util') const { conversionUtil } = require('./conversion-util') const BigNumber = require('bignumber.js') -const ARAGON = '960b236A07cf122663c4303350609A66A7B288C0' - module.exports = connect(mapStateToProps)(SendTransactionScreen) function mapStateToProps (state) { @@ -81,9 +79,180 @@ function SendTransactionScreen () { this.back = this.back.bind(this) this.closeTooltip = this.closeTooltip.bind(this) this.onSubmit = this.onSubmit.bind(this) - this.recipientDidChange = this.recipientDidChange.bind(this) this.setActiveCurrency = this.setActiveCurrency.bind(this) this.toggleTooltip = this.toggleTooltip.bind(this) + + this.renderFromInput = this.renderFromInput.bind(this) + this.renderToInput = this.renderToInput.bind(this) + this.renderAmountInput = this.renderAmountInput.bind(this) + this.renderGasInput = this.renderGasInput.bind(this) + this.renderMemoInput = this.renderMemoInput.bind(this) +} + +SendTransactionScreen.prototype.renderFromInput = function (from, identities) { + return h('div.send-screen-input-wrapper', [ + + h('div', 'From:'), + + h('input.large-input.send-screen-input', { + list: 'accounts', + placeholder: 'Account', + value: from, + onChange: (event) => { + this.setState({ + newTx: { + ...this.state.newTx, + from: event.target.value, + }, + }) + }, + }), + + h('datalist#accounts', [ + Object.entries(identities).map(([key, { address, name }]) => { + return h('option', { + value: address, + label: name, + key: address, + }) + }), + ]), + + ]) +} + +SendTransactionScreen.prototype.renderToInput = function (to, identities, addressBook) { + return h('div.send-screen-input-wrapper', [ + + h('div', 'To:'), + + h('input.large-input.send-screen-input', { + name: 'address', + list: 'addresses', + placeholder: 'Address', + value: to, + onChange: (event) => { + this.setState({ + newTx: { + ...this.state.newTx, + to: event.target.value, + }, + }) + }, + }), + + h('datalist#addresses', [ + // Corresponds to the addresses owned. + ...Object.entries(identities).map(([key, { address, name }]) => { + return h('option', { + value: address, + label: name, + key: address, + }) + }), + // Corresponds to previously sent-to addresses. + ...addressBook.map(({ address, name }) => { + return h('option', { + value: address, + label: name, + key: address, + }) + }), + ]), + + ]) +} + +SendTransactionScreen.prototype.renderAmountInput = function (activeCurrency) { + return h('div.send-screen-input-wrapper', [ + + h('div.send-screen-amount-labels', [ + h('span', 'Amount'), + h(CurrencyToggle, { + activeCurrency, + onClick: (newCurrency) => this.setActiveCurrency(newCurrency), + }), // holding on icon from design + ]), + + h('input.large-input.send-screen-input', { + placeholder: `0 ${activeCurrency}`, + type: 'number', + onChange: (event) => { + this.setState({ + newTx: Object.assign( + this.state.newTx, + { + amount: event.target.value, + } + ), + }) + }, + }), + + ]) +} + +SendTransactionScreen.prototype.renderGasInput = function (gasPrice, gas, activeCurrency, conversionRate, blockGasLimit) { + return h('div.send-screen-input-wrapper', [ + this.state.tooltipIsOpen && h(GasTooltip, { + className: 'send-tooltip', + gasPrice, + gasLimit: gas, + onClose: this.closeTooltip, + onFeeChange: ({gasLimit, gasPrice}) => { + this.setState({ + newTx: { + ...this.state.newTx, + gas: gasLimit, + gasPrice, + }, + }) + }, + }), + + h('div.send-screen-gas-labels', [ + h('span', [ + h('i.fa.fa-bolt'), + 'Gas fee:', + ]), + h('span', 'What\'s this?'), + ]), + + // TODO: handle loading time when switching to USD + h('div.large-input.send-screen-gas-input', {}, [ + h(GasFeeDisplay, { + activeCurrency, + conversionRate, + gas, + gasPrice, + blockGasLimit, + }), + h('div.send-screen-gas-input-customize', { + onClick: this.toggleTooltip, + }, [ + 'Customize', + ]), + ]), + + ]) +} + +SendTransactionScreen.prototype.renderMemoInput = function () { + return h('div.send-screen-input-wrapper', [ + h('div', 'Transaction memo (optional)'), + h('input.large-input.send-screen-input', { + onChange: () => { + this.setState({ + newTx: Object.assign( + this.state.newTx, + { + memo: event.target.value, + } + ), + }) + }, + }), + ]) } SendTransactionScreen.prototype.render = function () { @@ -93,16 +262,13 @@ SendTransactionScreen.prototype.render = function () { const { // selectedIdentity, // network, - // identities, - // addressBook, + identities, + addressBook, conversionRate, } = props const { blockGasLimit, newTx, activeCurrency } = this.state const { gas, gasPrice } = newTx - // console.log(`activeCurrency`, activeCurrency) - // console.log({ selectedIdentity, identities }) - // console.log('SendTransactionScreen state:', this.state) return ( @@ -116,199 +282,16 @@ SendTransactionScreen.prototype.render = function () { h('div.send-screen__subtitle', 'Send Ethereum to anyone with an Ethereum account'), - h('div.send-screen-input-wrapper', [ - - h('div', 'From:'), - - h('input.large-input.send-screen-input', { - list: 'accounts', - placeholder: 'Account', - value: this.state.newTx.from, - onChange: (event) => { - console.log('event', event.target.value) - this.setState({ - newTx: { - ...this.state.newTx, - from: event.target.value, - }, - }) - }, - }), - - h('datalist#accounts', [ - Object.keys(props.identities).map((key) => { - const identity = props.identities[key] - return h('option', { - value: identity.address, - label: identity.name, - key: identity.address, - }) - }), - ]), - - ]), - - h('div.send-screen-input-wrapper', [ - - h('div', 'To:'), - - h('input.large-input.send-screen-input', { - name: 'address', - list: 'addresses', - placeholder: 'Address', - value: this.state.newTx.to, - onChange: (event) => { - console.log('event', event.target.value) - this.setState({ - newTx: { - ...this.state.newTx, - to: event.target.value, - }, - }) - }, - }), - - h('datalist#addresses', [ - // Corresponds to the addresses owned. - Object.entries(props.identities).map(([key, { address, name }]) => { - return h('option', { - value: address, - label: name, - key: address, - }) - }), - // Corresponds to previously sent-to addresses. - props.addressBook.map(({ address, name }) => { - return h('option', { - value: address, - label: name, - key: address, - }) - }), - ]), - - // h(EnsInput, { - // name: 'address', - // placeholder: 'Recipient Address', - // value: this.state.newTx.to, - // onChange: (event) => { - // this.setState({ - // newTx: Object.assign( - // this.state.newTx, - // { - // to: event.target.value, - // } - // ), - // }) - // }, - // network, - // identities, - // addressBook, - // }), - - ]), - - h('div.send-screen-input-wrapper', [ - - h('div.send-screen-amount-labels', [ - h('span', 'Amount'), - h(CurrencyToggle, { - activeCurrency, - onClick: (newCurrency) => this.setActiveCurrency(newCurrency), - }), // holding on icon from design - ]), - - h('input.large-input.send-screen-input', { - placeholder: `0 ${activeCurrency}`, - type: 'number', - onChange: (event) => { - this.setState({ - newTx: Object.assign( - this.state.newTx, - { - amount: event.target.value, - } - ), - }) - }, - }), - - ]), - - h('div.send-screen-input-wrapper', [ - this.state.tooltipIsOpen && h(GasTooltip, { - className: 'send-tooltip', - gasPrice, - gasLimit: gas, - onClose: this.closeTooltip, - onFeeChange: ({gasLimit, gasPrice}) => { - this.setState({ - newTx: { - ...this.state.newTx, - gas: gasLimit, - gasPrice, - }, - }) - }, - }), - - h('div.send-screen-gas-labels', [ - h('span', [ - h('i.fa.fa-bolt'), - 'Gas fee:', - ]), - h('span', 'What\'s this?'), - ]), - - // TODO: handle loading time when switching to USD - h('div.large-input.send-screen-gas-input', {}, [ - h(GasFeeDisplay, { - activeCurrency, - conversionRate, - gas, - gasPrice, - blockGasLimit, - }), - h('div.send-screen-gas-input-customize', { - onClick: this.toggleTooltip, - }, [ - 'Customize', - ]), - ]), - - ]), - - h('div.send-screen-input-wrapper', [ - h('div', 'Transaction memo (optional)'), - h('input.large-input.send-screen-input', { - onChange: () => { - this.setState({ - newTx: Object.assign( - this.state.newTx, - { - memo: event.target.value, - } - ), - }) - }, - }), - ]), - - h('div.send-screen-input-wrapper', {}, [ - h('div', {}, ['Data (optional)']), - h('input.large-input.send-screen-input', { - onChange: () => { - this.setState({ - newTx: Object.assign( - this.state.newTx, - { - txData: event.target.value, - } - ), - }) - }, - }), - ]), + this.renderFromInput(this.state.newTx.from, identities), + + this.renderToInput(this.state.newTx.to, identities, addressBook), + + this.renderAmountInput(activeCurrency), + + this.renderGasInput(gasPrice, gas, activeCurrency, conversionRate, blockGasLimit), + + this.renderMemoInput(), + ]), // Buttons underneath card @@ -337,41 +320,21 @@ SendTransactionScreen.prototype.setActiveCurrency = function (newCurrency) { this.setState({ activeCurrency: newCurrency }) } -SendTransactionScreen.prototype.navigateToAccounts = function (event) { - event.stopPropagation() - this.props.dispatch(showAccountsPage()) -} - SendTransactionScreen.prototype.back = function () { var address = this.props.address this.props.dispatch(backToAccountDetail(address)) } -SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickname) { - this.setState({ - recipient: recipient, - nickname: nickname, - }) -} - SendTransactionScreen.prototype.onSubmit = function (event) { event.preventDefault() + const { warning } = this.props const state = this.state || {} - // const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '') const recipient = state.newTx.to - const nickname = state.nickname || ' ' - // const input = document.querySelector('input[name="amount"]').value - // const input = state.newTx.value - // const value = util.normalizeEthStringToWei(input) - - // https://consensys.slack.com/archives/G1L7H42BT/p1503439134000169?thread_ts=1503438076.000411&cid=G1L7H42BT - // From @kumavis: "not needed for MVP but we will end up adding it again so consider just adding it now" - const txData = false - // Must replace with memo data. - // const txData = document.querySelector('input[name="txData"]').value + // TODO: convert this to hex when created and include it in send + const txData = state.newTx.memo let message @@ -385,14 +348,9 @@ SendTransactionScreen.prototype.onSubmit = function (event) { // return this.props.dispatch(actions.displayWarning(message)) // } - if ((util.isInvalidChecksumAddress(recipient))) { - message = 'Recipient address checksum is invalid.' - return this.props.dispatch(actions.displayWarning(message)) - } - - if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) { + if (!isValidAddress(recipient) && !recipient) { message = 'Recipient address is invalid.' - return this.props.dispatch(actions.displayWarning(message)) + return this.props.dispatch(displayWarning(message)) } if (txData && !isHex(stripHexPrefix(txData))) { @@ -424,7 +382,6 @@ SendTransactionScreen.prototype.onSubmit = function (event) { value: sendAmount, - // New: gas will now be specified on this step gas: this.state.newTx.gas, gasPrice: this.state.newTx.gasPrice, } @@ -432,5 +389,7 @@ SendTransactionScreen.prototype.onSubmit = function (event) { if (recipient) txParams.to = addHexPrefix(recipient) if (txData) txParams.data = txData - this.props.dispatch(signTx(txParams)) + if (!warning) { + this.props.dispatch(signTx(txParams)) + } } -- cgit v1.2.3 From 39afbea7aaf17cfee5d7fc11299cb82e657edd7e Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 20 Sep 2017 11:26:20 -0230 Subject: Confirm screen shows amount plus gas in total field --- ui/app/components/pending-tx.js | 25 +++++++++++++++++++------ ui/app/conversion-util.js | 10 ++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 18b622925..a8fac4c28 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -14,7 +14,7 @@ const ethUtil = require('ethereumjs-util') const BN = ethUtil.BN const hexToBn = require('../../../app/scripts/lib/hex-to-bn') const util = require('../util') -const { conversionUtil } = require('../conversion-util') +const { conversionUtil, addCurrencies } = require('../conversion-util') const MIN_GAS_PRICE_GWEI_BN = new BN(1) const GWEI_FACTOR = new BN(1e9) @@ -67,7 +67,7 @@ PendingTx.prototype.componentWillMount = function () { this.props.setCurrentCurrencyToUSD() } -PendingTx.prototype.getTotal = function () { +PendingTx.prototype.getAmount = function () { const { conversionRate } = this.props const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} @@ -186,7 +186,16 @@ PendingTx.prototype.getData = function () { const { name, params = [] } = decodedData || {} const { type, value } = params[0] || {} const { USD: gasFeeInUSD, ETH: gasFeeInETH } = this.getGasFee() - const { USD: totalInUSD, ETH: totalInETH } = this.getTotal() + const { USD: amountInUSD, ETH: amountInETH } = this.getAmount() + + const totalInUSD = addCurrencies(gasFeeInUSD, amountInUSD, { + toNumericBase: 'dec', + numberOfDecimals: 2, + }) + const totalInETH = addCurrencies(gasFeeInETH, amountInETH, { + toNumericBase: 'dec', + numberOfDecimals: 6, + }) if (name === 'transfer' && type === 'address') { return { @@ -201,8 +210,8 @@ PendingTx.prototype.getData = function () { memo: txParams.memo || '', gasFeeInUSD, gasFeeInETH, - totalInUSD, - totalInETH, + amountInUSD, + amountInETH, } } else { return { @@ -217,6 +226,8 @@ PendingTx.prototype.getData = function () { memo: txParams.memo || '', gasFeeInUSD, gasFeeInETH, + amountInUSD, + amountInETH, totalInUSD, totalInETH, } @@ -243,6 +254,8 @@ PendingTx.prototype.render = function () { memo, gasFeeInUSD, gasFeeInETH, + amountInUSD, + amountInETH, totalInUSD, totalInETH, } = this.getData() @@ -307,7 +320,7 @@ PendingTx.prototype.render = function () { `You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`, ]), - h('h3.flex-center.confirm-screen-send-amount', [`$${totalInUSD}`]), + h('h3.flex-center.confirm-screen-send-amount', [`$${amountInUSD}`]), h('h3.flex-center.confirm-screen-send-amount-currency', [ 'USD' ]), h('div.flex-center.confirm-memo-wrapper', [ h('h3.confirm-screen-send-memo', [ memo ]), diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js index 29e5ce668..0ede77487 100644 --- a/ui/app/conversion-util.js +++ b/ui/app/conversion-util.js @@ -114,6 +114,16 @@ const conversionUtil = (value, { value, }); +const addCurrencies = (a, b, { toNumericBase, numberOfDecimals }) => { + const value = (new BigNumber(a)).add(b); + return converter({ + value, + toNumericBase, + numberOfDecimals, + }) +} + module.exports = { conversionUtil, + addCurrencies, } \ No newline at end of file -- cgit v1.2.3 From 24fd16b1bee31352ef7f364804eb5f06c08c3bf6 Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 19 Sep 2017 22:26:10 -0230 Subject: Abstract account modal. --- ui/app/components/modals/account-details-modal.js | 45 ++++------------- .../components/modals/account-modal-container.js | 55 +++++++++++++++++++++ ui/app/components/qr-code.js | 2 +- ui/app/css/itcss/components/modal.scss | 57 ++++++++++++---------- 4 files changed, 96 insertions(+), 63 deletions(-) create mode 100644 ui/app/components/modals/account-modal-container.js diff --git a/ui/app/components/modals/account-details-modal.js b/ui/app/components/modals/account-details-modal.js index ec7e4b500..6c2eba7bd 100644 --- a/ui/app/components/modals/account-details-modal.js +++ b/ui/app/components/modals/account-details-modal.js @@ -3,25 +3,21 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect const actions = require('../../actions') +const AccountModalContainer = require('./account-modal-container') const { getSelectedIdentity, getSelectedAddress } = require('../../selectors') const genAccountLink = require('../../../lib/account-link.js') -const Identicon = require('../identicon') const QrView = require('../qr-code') function mapStateToProps (state) { return { network: state.metamask.network, - address: state.metamask.selectedAddress, - selectedAddress: getSelectedAddress(state), selectedIdentity: getSelectedIdentity(state), } } function mapDispatchToProps (dispatch) { return { - hideModal: () => { - dispatch(actions.hideModal()) - }, + // Is this supposed to be used somewhere? showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)), } } @@ -34,49 +30,28 @@ function AccountDetailsModal () { module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountDetailsModal) // Not yet pixel perfect todos: - // fonts of qr-header and close button + // fonts of qr-header AccountDetailsModal.prototype.render = function () { const { selectedIdentity, network } = this.props + const { name, address } = selectedIdentity - return h('div', { style: { borderRadius: '4px' }}, [ - h('div.account-details-modal-wrapper', [ - - h('div', [ - - // Needs a border; requires changes to svg - h(Identicon, { - address: selectedIdentity.address, - diameter: 64, - style: {}, - }), - - ]), - - h('div.account-details-modal-close', { - onClick: this.props.hideModal, - }), - + return h(AccountModalContainer, {}, [ h(QrView, { Qr: { - message: this.props.selectedIdentity.name, - data: this.props.selectedIdentity.address, + message: name, + data: address, }, }), - // divider - h('div.account-details-modal-divider'), + h('div.account-modal-divider'), h('button.btn-clear', { - onClick: () => { - const url = genAccountLink(selectedIdentity.address, network) - global.platform.openWindow({ url }) - }, + onClick: () => global.platform.openWindow({ url: genAccountLink(address, network) }), }, [ 'View account on Etherscan' ]), // Holding on redesign for Export Private Key functionality h('button.btn-clear', [ 'Export private key' ]), - - ]), + ]) } diff --git a/ui/app/components/modals/account-modal-container.js b/ui/app/components/modals/account-modal-container.js new file mode 100644 index 000000000..69650ca15 --- /dev/null +++ b/ui/app/components/modals/account-modal-container.js @@ -0,0 +1,55 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const actions = require('../../actions') +const { getSelectedIdentity } = require('../../selectors') +const Identicon = require('../identicon') + +function mapStateToProps (state) { + return { + selectedIdentity: getSelectedIdentity(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + hideModal: () => { + dispatch(actions.hideModal()) + }, + } +} + +inherits(AccountModalContainer, Component) +function AccountModalContainer () { + Component.call(this) +} + +module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountModalContainer) + +AccountModalContainer.prototype.render = function () { + const { selectedIdentity, children } = this.props + console.log(`children`, children); + return h('div', { style: { borderRadius: '4px' }}, [ + h('div.account-modal-container', [ + + h('div', [ + + // Needs a border; requires changes to svg + h(Identicon, { + address: selectedIdentity.address, + diameter: 64, + style: {}, + }), + + ]), + + h('div.account-modal-close', { + onClick: this.props.hideModal, + }), + + ...children, + + ]), + ]) +} diff --git a/ui/app/components/qr-code.js b/ui/app/components/qr-code.js index 4257c1a15..dca5c8c47 100644 --- a/ui/app/components/qr-code.js +++ b/ui/app/components/qr-code.js @@ -52,7 +52,7 @@ QrCodeView.prototype.render = function () { width: '247px', }, value: Qr.data, - readonly: true, + readOnly: true, }), // h(CopyButton, { // value: Qr.data, diff --git a/ui/app/css/itcss/components/modal.scss b/ui/app/css/itcss/components/modal.scss index c85e61ae2..9f6ce54f5 100644 --- a/ui/app/css/itcss/components/modal.scss +++ b/ui/app/css/itcss/components/modal.scss @@ -175,8 +175,8 @@ font-size: 18px; } -// Account Details Modal -.account-details-modal-wrapper { +// Account Modal Container +.account-modal-container { display: flex; flex-direction: column; justify-content: flex-start; @@ -192,16 +192,38 @@ } } -.account-details-modal-wrapper .qr-header { +.account-modal-close::after { + content: '\00D7'; + font-size: 40px; + color: $dusty-gray; + position: absolute; + top: 10px; + right: 12px; + cursor: pointer; +} + +.account-modal-container .identicon { + position: relative; + left: 0; + right: 0; + margin: 0 auto; + top: -32px; + margin-bottom: -32px; +} + + +// Account Details Modal + +.account-modal-container .qr-header { margin-top: 9px; font-size: 20px; } -.account-details-modal-wrapper .qr-wrapper { +.account-modal-container .qr-wrapper { margin-top: 5px; } -.account-details-modal-wrapper .ellip-address-wrapper { +.account-modal-container .ellip-address-wrapper { display: flex; justify-content: center; border: 1px solid $alto; @@ -211,14 +233,14 @@ width: 286px; } -.account-details-modal-wrapper .qr-ellip-address { +.account-modal-container .qr-ellip-address { width: 254px; border: none; font-family: 'Montserrat Light'; font-size: 14px; } -.account-details-modal-wrapper .btn-clear { +.account-modal-container .btn-clear { min-height: 28px; font-size: 14px; border-color: $curious-blue; @@ -233,32 +255,13 @@ font-family: 'Montserrat Light'; } -.account-details-modal-divider { +.account-modal-divider { width: 100%; height: 1px; margin: 19px 0 8px 0; background-color: $alto; } -.account-details-modal-wrapper .identicon { - position: relative; - left: 0; - right: 0; - margin: 0 auto; - top: -32px; - margin-bottom: -32px; -} - -.account-details-modal-close::after { - content: '\00D7'; - font-size: 40px; - color: $dusty-gray; - position: absolute; - top: 10px; - right: 12px; - cursor: pointer; -} - // New Account Modal .new-account-modal-wrapper { display: flex; -- cgit v1.2.3 From 0a5ae395099f9cadef5e446a0d591d00be4685e5 Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 21 Sep 2017 17:37:30 -0700 Subject: bug - fix event emitter mem leak warning --- app/scripts/contentscript.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 90a0f1f22..b4708189e 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -42,16 +42,21 @@ function setupStreams () { name: 'contentscript', target: 'inpage', }) - pageStream.on('error', console.error) const pluginPort = extension.runtime.connect({ name: 'contentscript' }) const pluginStream = new PortStream(pluginPort) - pluginStream.on('error', console.error) // forward communication plugin->inpage - pageStream.pipe(pluginStream).pipe(pageStream) + pump( + pageStream, + pluginStream, + pageStream, + (err) => logStreamDisconnectWarning('MetaMask Contentscript Forwarding', err) + ) // setup local multistream channels const mux = new ObjectMultiplex() + mux.setMaxListeners(25) + pump( mux, pageStream, -- cgit v1.2.3 From 3ec2f534632426876c28b22c58cbbf14b4904d97 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Thu, 21 Sep 2017 18:44:52 -0700 Subject: Integrate Add Token --- ui/app/actions.js | 39 ++- ui/app/add-token.js | 428 +++++++++++++---------------- ui/app/components/token-balance.js | 14 +- ui/app/components/tx-list.js | 2 +- ui/app/css/itcss/components/add-token.scss | 95 ++++++- ui/app/css/itcss/components/buttons.scss | 10 +- 6 files changed, 333 insertions(+), 255 deletions(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index 678c68a6a..1231fc296 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -147,6 +147,7 @@ var actions = { SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', showAddTokenPage, addToken, + addTokens, setRpcTarget: setRpcTarget, setDefaultRpcTarget: setDefaultRpcTarget, setProviderType: setProviderType, @@ -700,18 +701,40 @@ function showAddTokenPage () { function addToken (address, symbol, decimals) { return (dispatch) => { dispatch(actions.showLoadingIndication()) - background.addToken(address, symbol, decimals, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - setTimeout(() => { - dispatch(actions.goHome()) - }, 250) + return new Promise((resolve, reject) => { + background.addToken(address, symbol, decimals, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + dispatch(actions.displayWarning(err.message)) + reject(err) + } + resolve() + // setTimeout(() => { + // dispatch(actions.goHome()) + // }, 250) + }) }) } } +function addTokens (tokens) { + return dispatch => { + if (Array.isArray(tokens)) { + return Promise.all(tokens.map(({ address, symbol, decimals }) => ( + dispatch(addToken(address, symbol, decimals)) + ))) + } else { + return Promise.all( + Object + .entries(tokens) + .map(([_, { address, symbol, decimals }]) => ( + dispatch(addToken(address, symbol, decimals)) + )) + ) + } + } +} + function goBackToInitView () { return { type: actions.BACK_TO_INIT_MENU, diff --git a/ui/app/add-token.js b/ui/app/add-token.js index 622cf2bc2..f723ff07c 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -5,6 +5,8 @@ const h = require('react-hyperscript') const connect = require('react-redux').connect const Fuse = require('fuse.js') const contractMap = require('eth-contract-metadata') +const TokenBalance = require('./components/token-balance') +const Identicon = require('./components/identicon') const contractList = Object.entries(contractMap).map(([ _, tokenData]) => tokenData) const fuse = new Fuse(contractList, { shouldSort: true, @@ -16,9 +18,6 @@ const fuse = new Fuse(contractList, { keys: ['address', 'name', 'symbol'], }) const actions = require('./actions') -// const Tooltip = require('./components/tooltip.js') - - const ethUtil = require('ethereumjs-util') const abi = require('human-standard-token-abi') const Eth = require('ethjs-query') @@ -37,86 +36,193 @@ function mapStateToProps (state) { function mapDispatchToProps (dispatch) { return { goHome: () => dispatch(actions.goHome()), + addTokens: tokens => dispatch(actions.addTokens(tokens)), } } inherits(AddTokenScreen, Component) function AddTokenScreen () { this.state = { - // warning: null, - // address: null, - // symbol: 'TOKEN', - // decimals: 18, + isShowingConfirmation: false, customAddress: '', customSymbol: '', customDecimals: 0, searchQuery: '', isCollapsed: true, - selectedToken: {}, + selectedTokens: {}, + errors: {}, } this.tokenAddressDidChange = this.tokenAddressDidChange.bind(this) + this.onNext = this.onNext.bind(this) Component.call(this) } -AddTokenScreen.prototype.toggleToken = function (symbol) { - const { selectedToken } = this.state - const { [symbol]: isSelected } = selectedToken +AddTokenScreen.prototype.componentWillMount = function () { + if (typeof global.ethereumProvider === 'undefined') return + + this.eth = new Eth(global.ethereumProvider) + this.contract = new EthContract(this.eth) + this.TokenContract = this.contract(abi) +} + +AddTokenScreen.prototype.toggleToken = function (address, token) { + const { selectedTokens, errors } = this.state + const { [address]: selectedToken } = selectedTokens this.setState({ - selectedToken: { - ...selectedToken, - [symbol]: !isSelected, + selectedTokens: { + ...selectedTokens, + [address]: selectedToken ? null : token, + }, + errors: { + ...errors, + tokenSelector: null, }, }) } +AddTokenScreen.prototype.onNext = function () { + const { isValid, errors } = this.validate() + + return !isValid + ? this.setState({ errors }) + : this.setState({ isShowingConfirmation: true }) +} + +AddTokenScreen.prototype.tokenAddressDidChange = function (e) { + const customAddress = e.target.value.trim() + this.setState({ customAddress }) + if (ethUtil.isValidAddress(customAddress) && customAddress !== emptyAddr) { + this.attemptToAutoFillTokenParams(customAddress) + } else { + this.setState({ + customSymbol: '', + customDecimals: 0, + }) + } +} + +AddTokenScreen.prototype.validate = function () { + const errors = {} + const identitiesList = Object.keys(this.props.identities) + const { customAddress, customSymbol, customDecimals, selectedTokens } = this.state + const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase() + + if (customAddress) { + const validAddress = ethUtil.isValidAddress(customAddress) + if (!validAddress) { + errors.customAddress = 'Address is invalid. ' + } + + const validDecimals = customDecimals >= 0 && customDecimals < 36 + if (!validDecimals) { + errors.customDecimals = 'Decimals must be at least 0, and not over 36.' + } + + const symbolLen = customSymbol.trim().length + const validSymbol = symbolLen > 0 && symbolLen < 10 + if (!validSymbol) { + errors.customSymbol = 'Symbol must be between 0 and 10 characters.' + } + + const ownAddress = identitiesList.includes(standardAddress) + if (ownAddress) { + errors.customAddress = 'Personal address detected. Input the token contract address.' + } + } else if ( + Object.entries(selectedTokens) + .reduce((isEmpty, [ symbol, isSelected ]) => ( + isEmpty && !isSelected + ), true) + ) { + errors.tokenSelector = 'Must select at least 1 token.' + } + + return { + isValid: !Object.keys(errors).length, + errors, + } +} + +AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) { + const contract = this.TokenContract.at(address) + + const results = await Promise.all([ + contract.symbol(), + contract.decimals(), + ]) + + const [ symbol, decimals ] = results + if (symbol && decimals) { + this.setState({ + customSymbol: symbol[0], + customDecimals: decimals[0].toString(), + }) + } +} + AddTokenScreen.prototype.renderCustomForm = function () { - const { customAddress, customSymbol, customDecimals } = this.state + const { customAddress, customSymbol, customDecimals, errors } = this.state return !this.state.isCollapsed && ( h('div.add-token__add-custom-form', [ - h('div.add-token__add-custom-field', [ + h('div', { + className: classnames('add-token__add-custom-field', { + 'add-token__add-custom-field--error': errors.customAddress, + }), + }, [ h('div.add-token__add-custom-label', 'Token Address'), h('input.add-token__add-custom-input', { type: 'text', onChange: this.tokenAddressDidChange, value: customAddress, }), + h('div.add-token__add-custom-error-message', errors.customAddress), ]), - h('div.add-token__add-custom-field', [ + h('div', { + className: classnames('add-token__add-custom-field', { + 'add-token__add-custom-field--error': errors.customSymbol, + }), + }, [ h('div.add-token__add-custom-label', 'Token Symbol'), h('input.add-token__add-custom-input', { type: 'text', value: customSymbol, disabled: true, }), + h('div.add-token__add-custom-error-message', errors.customSymbol), ]), - h('div.add-token__add-custom-field', [ + h('div', { + className: classnames('add-token__add-custom-field', { + 'add-token__add-custom-field--error': errors.customDecimals, + }), + }, [ h('div.add-token__add-custom-label', 'Decimals of Precision'), h('input.add-token__add-custom-input', { type: 'number', value: customDecimals, disabled: true, }), + h('div.add-token__add-custom-error-message', errors.customDecimals), ]), ]) ) } AddTokenScreen.prototype.renderTokenList = function () { - const { searchQuery = '', selectedToken } = this.state + const { searchQuery = '', selectedTokens } = this.state const results = searchQuery ? fuse.search(searchQuery) || [] : contractList return Array(6).fill(undefined) .map((_, i) => { - const { logo, symbol, name } = results[i] || {} + const { logo, symbol, name, address } = results[i] || {} return Boolean(logo || symbol || name) && ( h('div.add-token__token-wrapper', { className: classnames('add-token__token-wrapper', { - 'add-token__token-wrapper--selected': selectedToken[symbol], + 'add-token__token-wrapper--selected': selectedTokens[address], }), - onClick: () => this.toggleToken(symbol), + onClick: () => this.toggleToken(address, results[i]), }, [ h('div.add-token__token-icon', { style: { @@ -132,11 +238,69 @@ AddTokenScreen.prototype.renderTokenList = function () { }) } +AddTokenScreen.prototype.renderConfirmation = function () { + const { + customAddress: address, + customSymbol: symbol, + customDecimals: decimals, + selectedTokens, + } = this.state + + const { addTokens, goHome } = this.props + + const customToken = { + address, + symbol, + decimals, + } + + const tokens = address && symbol && decimals + ? { ...selectedTokens, [address]: customToken } + : selectedTokens + + return ( + h('div.add-token', [ + h('div.add-token__wrapper', [ + h('div.add-token__title-container.add-token__confirmation-title', [ + h('div.add-token__title', 'Add Token'), + h('div.add-token__description', 'Would you like to add these tokens?'), + ]), + h('div.add-token__content-container.add-token__confirmation-content', [ + h('div.add-token__description.add-token__confirmation-description', 'Your balances'), + h('div.add-token__confirmation-token-list', + Object.entries(tokens) + .map(([ address, token ]) => ( + h('span.add-token__confirmation-token-list-item', [ + h(Identicon, { + className: 'add-token__confirmation-token-icon', + diameter: 75, + address, + }), + h(TokenBalance, { token }), + ]) + )) + ), + ]), + ]), + h('div.add-token__buttons', [ + h('button.btn-secondary', { + onClick: () => addTokens(tokens).then(goHome), + }, 'Add Tokens'), + h('button.btn-tertiary', { + onClick: () => this.setState({ isShowingConfirmation: false }), + }, 'Back'), + ]), + ]) + ) +} + AddTokenScreen.prototype.render = function () { - const { isCollapsed } = this.state + const { isCollapsed, errors, isShowingConfirmation } = this.state const { goHome } = this.props - return ( + return isShowingConfirmation + ? this.renderConfirmation() + : ( h('div.add-token', [ h('div.add-token__wrapper', [ h('div.add-token__title-container', [ @@ -151,6 +315,7 @@ AddTokenScreen.prototype.render = function () { placeholder: 'Search', onChange: e => this.setState({ searchQuery: e.target.value }), }), + h('div.add-token__search-input-error-message', errors.tokenSelector), ]), h( 'div.add-token__token-icons-container', @@ -165,7 +330,9 @@ AddTokenScreen.prototype.render = function () { ]), ]), h('div.add-token__buttons', [ - h('button.btn-secondary', 'Next'), + h('button.btn-secondary', { + onClick: this.onNext, + }, 'Next'), h('button.btn-tertiary', { onClick: goHome, }, 'Cancel'), @@ -173,214 +340,3 @@ AddTokenScreen.prototype.render = function () { ]) ) } - -// AddTokenScreen.prototype.render = function () { -// const state = this.state -// const props = this.props -// const { warning, symbol, decimals } = state - -// return ( -// h('.flex-column.flex-grow', [ - -// // subtitle and nav -// h('.section-title.flex-row.flex-center', [ -// h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { -// onClick: (event) => { -// props.dispatch(actions.goHome()) -// }, -// }), -// h('h2.page-subtitle', 'Add Token'), -// ]), - -// h('.error', { -// style: { -// display: warning ? 'block' : 'none', -// padding: '0 20px', -// textAlign: 'center', -// }, -// }, warning), - -// // conf view -// h('.flex-column.flex-justify-center.flex-grow.select-none', [ -// h('.flex-space-around', { -// style: { -// padding: '20px', -// }, -// }, [ - -// h('div', [ -// h(Tooltip, { -// position: 'top', -// title: 'The contract of the actual token contract. Click for more info.', -// }, [ -// h('a', { -// style: { fontWeight: 'bold', paddingRight: '10px'}, -// href: 'https://consensyssupport.happyfox.com/staff/kb/article/24-what-is-a-token-contract-address', -// target: '_blank', -// }, [ -// h('span', 'Token Contract Address '), -// h('i.fa.fa-question-circle'), -// ]), -// ]), -// ]), - -// h('section.flex-row.flex-center', [ -// h('input#token-address', { -// name: 'address', -// placeholder: 'Token Contract Address', -// onChange: this.tokenAddressDidChange.bind(this), -// style: { -// width: 'inherit', -// flex: '1 0 auto', -// height: '30px', -// margin: '8px', -// }, -// }), -// ]), - -// h('div', [ -// h('span', { -// style: { fontWeight: 'bold', paddingRight: '10px'}, -// }, 'Token Symbol'), -// ]), - -// h('div', { style: {display: 'flex'} }, [ -// h('input#token_symbol', { -// placeholder: `Like "ETH"`, -// value: symbol, -// style: { -// width: 'inherit', -// flex: '1 0 auto', -// height: '30px', -// margin: '8px', -// }, -// onChange: (event) => { -// var element = event.target -// var symbol = element.value -// this.setState({ symbol }) -// }, -// }), -// ]), - -// h('div', [ -// h('span', { -// style: { fontWeight: 'bold', paddingRight: '10px'}, -// }, 'Decimals of Precision'), -// ]), - -// h('div', { style: {display: 'flex'} }, [ -// h('input#token_decimals', { -// value: decimals, -// type: 'number', -// min: 0, -// max: 36, -// style: { -// width: 'inherit', -// flex: '1 0 auto', -// height: '30px', -// margin: '8px', -// }, -// onChange: (event) => { -// var element = event.target -// var decimals = element.value.trim() -// this.setState({ decimals }) -// }, -// }), -// ]), - -// h('button', { -// style: { -// alignSelf: 'center', -// }, -// onClick: (event) => { -// const valid = this.validateInputs() -// if (!valid) return - -// const { address, symbol, decimals } = this.state -// this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) -// }, -// }, 'Add'), -// ]), -// ]), -// ]) -// ) -// } - -AddTokenScreen.prototype.componentWillMount = function () { - if (typeof global.ethereumProvider === 'undefined') return - - this.eth = new Eth(global.ethereumProvider) - this.contract = new EthContract(this.eth) - this.TokenContract = this.contract(abi) -} - -AddTokenScreen.prototype.tokenAddressDidChange = function (e) { - const customAddress = e.target.value.trim() - this.setState({ customAddress }) - if (ethUtil.isValidAddress(customAddress) && customAddress !== emptyAddr) { - this.attemptToAutoFillTokenParams(customAddress) - } else { - this.setState({ - customSymbol: '', - customDecimals: 0, - }) - } -} - -AddTokenScreen.prototype.validateInputs = function () { - let msg = '' - const state = this.state - const identitiesList = Object.keys(this.props.identities) - const { address, symbol, decimals } = state - const standardAddress = ethUtil.addHexPrefix(address).toLowerCase() - - const validAddress = ethUtil.isValidAddress(address) - if (!validAddress) { - msg += 'Address is invalid. ' - } - - const validDecimals = decimals >= 0 && decimals < 36 - if (!validDecimals) { - msg += 'Decimals must be at least 0, and not over 36. ' - } - - const symbolLen = symbol.trim().length - const validSymbol = symbolLen > 0 && symbolLen < 10 - if (!validSymbol) { - msg += 'Symbol must be between 0 and 10 characters.' - } - - const ownAddress = identitiesList.includes(standardAddress) - if (ownAddress) { - msg = 'Personal address detected. Input the token contract address.' - } - - const isValid = validAddress && validDecimals && !ownAddress - - if (!isValid) { - this.setState({ - warning: msg, - }) - } else { - this.setState({ warning: null }) - } - - return isValid -} - -AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) { - const contract = this.TokenContract.at(address) - - const results = await Promise.all([ - contract.symbol(), - contract.decimals(), - ]) - - const [ symbol, decimals ] = results - if (symbol && decimals) { - this.setState({ - customSymbol: symbol[0], - customDecimals: decimals[0].toString(), - }) - } -} diff --git a/ui/app/components/token-balance.js b/ui/app/components/token-balance.js index 3a923eb9d..0342c1da9 100644 --- a/ui/app/components/token-balance.js +++ b/ui/app/components/token-balance.js @@ -17,7 +17,8 @@ module.exports = connect(mapStateToProps)(TokenBalance) inherits(TokenBalance, Component) function TokenBalance () { this.state = { - balance: '', + string: '', + symbol: '', isLoading: true, error: null, } @@ -26,11 +27,14 @@ function TokenBalance () { TokenBalance.prototype.render = function () { const state = this.state - const { balance, isLoading } = state + const { symbol, string, balanceOnly, isLoading } = state return isLoading ? h('span', '') - : h('span', balance) + : h('span.token-balance', [ + h('span.token-balance__amount', string), + !balanceOnly && h('span.token-balance__symbol', symbol), + ]) } TokenBalance.prototype.componentDidMount = function () { @@ -93,10 +97,10 @@ TokenBalance.prototype.componentDidUpdate = function (nextProps) { TokenBalance.prototype.updateBalance = function (tokens = []) { const [{ string, symbol }] = tokens - const { balanceOnly } = this.props this.setState({ - balance: balanceOnly ? string : `${string} ${symbol}`, + string, + symbol, isLoading: false, }) } diff --git a/ui/app/components/tx-list.js b/ui/app/components/tx-list.js index 7a147e942..f817d03a9 100644 --- a/ui/app/components/tx-list.js +++ b/ui/app/components/tx-list.js @@ -77,7 +77,7 @@ TxList.prototype.renderTransactionListItem = function (transaction, conversionRa const { showConfTxPage } = this.props const opts = { - key: transActionId, + key: transActionId || transactionHash, txParams: transaction.txParams, transactionStatus, transActionId, diff --git a/ui/app/css/itcss/components/add-token.scss b/ui/app/css/itcss/components/add-token.scss index ebfdf7b11..d5d1aab71 100644 --- a/ui/app/css/itcss/components/add-token.scss +++ b/ui/app/css/itcss/components/add-token.scss @@ -56,6 +56,10 @@ margin-top: 24px; } + &__confirmation-description { + margin: 12px 0; + } + &__content-container { width: 100%; border-bottom: 1px solid $gallery; @@ -65,6 +69,18 @@ padding: 11px 0; width: 263px; margin: 0 auto; + position: relative; + } + + &__search-input-error-message { + position: absolute; + bottom: -10px; + font-size: 12px; + width: 100%; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + color: $red; } &__input { @@ -89,9 +105,13 @@ font-size: 18px; line-height: 24px; text-align: center; - padding: 11px 0 19px; + padding: 12px 0; font-weight: 600; cursor: pointer; + + &:hover { + background-color: $gallery; + } } &__add-custom-form { @@ -103,6 +123,24 @@ &__add-custom-field { width: 290px; margin: 0 auto; + position: relative; + + &--error { + .add-token__add-custom-input { + border-color: $red; + } + } + } + + &__add-custom-error-message { + position: absolute; + bottom: -21px; + font-size: 12px; + width: 100%; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + color: $red; } &__add-custom-label { @@ -152,9 +190,12 @@ cursor: pointer; border: 2px solid transparent; - &:hover, + &:hover { + border: 2px solid rgba($malibu-blue, .5); + } + &--selected { - border: 2px solid $malibu-blue; + border: 2px solid $malibu-blue !important; } } @@ -181,4 +222,52 @@ margin-right: 12px; flex: 0 0 auto; } + + &__confirmation-token-list { + display: flex; + flex-flow: column nowrap; + + .token-balance { + display: flex; + flex-flow: row nowrap; + align-items: flex-start; + + &__amount { + color: $scorpion; + font-size: 43px; + font-weight: 300; + line-height: 43px; + margin-right: 8px; + } + + &__symbol { + color: $scorpion; + font-size: 16px; + line-height: 24px; + } + } + } + + &__confirmation-title { + padding: 30px 120px 12px; + } + + &__confirmation-content { + padding-bottom: 60px; + } + + &__confirmation-token-list-item { + display: flex; + flex-flow: row nowrap; + padding: 0 120px; + align-items: center; + } + + &__confirmation-token-list-item + &__confirmation-token-list-item { + margin-top: 30px; + } + + &__confirmation-token-icon { + margin-right: 18px; + } } diff --git a/ui/app/css/itcss/components/buttons.scss b/ui/app/css/itcss/components/buttons.scss index 0946cdbbb..2c5e6cf57 100644 --- a/ui/app/css/itcss/components/buttons.scss +++ b/ui/app/css/itcss/components/buttons.scss @@ -30,8 +30,9 @@ button.btn-clear { button[disabled], input[type="submit"][disabled] { cursor: not-allowed; - background: rgba(197, 197, 197, 1); - box-shadow: 0 3px 6px rgba(197, 197, 197, .36); + opacity: .5; + // background: rgba(197, 197, 197, 1); + // box-shadow: 0 3px 6px rgba(197, 197, 197, .36); } // button.spaced { @@ -90,6 +91,11 @@ button.btn-thin { font-size: 16px; line-height: 24px; padding: 16px 42px; + + &[disabled] { + background-color: $white !important; + opacity: .5; + } } .btn-tertiary { -- cgit v1.2.3 From bfaacd311806b9ff902f0b5178469b5ef9133358 Mon Sep 17 00:00:00 2001 From: Alex Lunyov Date: Fri, 22 Sep 2017 12:14:04 +0800 Subject: Wildcard for infura.io permissions, added permission for cryptonator api --- app/manifest.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/manifest.json b/app/manifest.json index 05e8d30de..83a967a26 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -58,10 +58,8 @@ "storage", "clipboardWrite", "http://localhost:8545/", - "https://rinkeby.infura.io/metamask/", - "https://mainnet.infura.io/metamask/", - "https://ropsten.infura.io/metamask/", - "https://kovan.infura.io/metamask/" + "https://*.infura.io/", + "https://api.cryptonator.com" ], "web_accessible_resources": [ "scripts/inpage.js" -- cgit v1.2.3 From 83cda2b82e082a5a9b2ee35a9b6d55be43b0d788 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Thu, 21 Sep 2017 22:25:16 -0700 Subject: Refactor Confirmation Tx to render different screen --- ui/app/components/pending-tx.js | 569 --------------------- ui/app/components/pending-tx/confirm-send-ether.js | 446 ++++++++++++++++ ui/app/components/pending-tx/index.js | 103 ++++ 3 files changed, 549 insertions(+), 569 deletions(-) delete mode 100644 ui/app/components/pending-tx.js create mode 100644 ui/app/components/pending-tx/confirm-send-ether.js create mode 100644 ui/app/components/pending-tx/index.js diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js deleted file mode 100644 index a8fac4c28..000000000 --- a/ui/app/components/pending-tx.js +++ /dev/null @@ -1,569 +0,0 @@ -const Component = require('react').Component -const { connect } = require('react-redux') -const h = require('react-hyperscript') -const abi = require('human-standard-token-abi') -const abiDecoder = require('abi-decoder') -abiDecoder.addABI(abi) -const inherits = require('util').inherits -const actions = require('../actions') -const clone = require('clone') -const FiatValue = require('./fiat-value') -const Identicon = require('./identicon') - -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const hexToBn = require('../../../app/scripts/lib/hex-to-bn') -const util = require('../util') -const { conversionUtil, addCurrencies } = require('../conversion-util') - -const MIN_GAS_PRICE_GWEI_BN = new BN(1) -const GWEI_FACTOR = new BN(1e9) -const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) - -// Next: create separate react components -// roughly 5 components: -// heroIcon -// numericDisplay (contains symbol + currency) -// divider -// contentBox -// actionButtons - -module.exports = connect(mapStateToProps, mapDispatchToProps)(PendingTx) - -function mapStateToProps (state) { - const { - conversionRate, - identities, - } = state.metamask - const accounts = state.metamask.accounts - const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] - return { - conversionRate, - identities, - selectedAddress, - } -} - -function mapDispatchToProps (dispatch) { - return { - setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('USD')), - backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)), - cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })), - } -} - -inherits(PendingTx, Component) -function PendingTx () { - Component.call(this) - this.state = { - valid: true, - txData: null, - submitting: false, - } - this.onSubmit = this.onSubmit.bind(this) -} - -PendingTx.prototype.componentWillMount = function () { - this.props.setCurrentCurrencyToUSD() -} - -PendingTx.prototype.getAmount = function () { - const { conversionRate } = this.props - const txMeta = this.gatherTxMeta() - const txParams = txMeta.txParams || {} - const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data) - const { params = [] } = decodedData || {} - const { name, value } = params[1] || {} - const amountBn = name === '_value' - ? value - : txParams.value - - if (name === '_value') { - const token = util.getContractAtAddress(txParams.to) - token.symbol().then(symbol => console.log({symbol})) - console.log({txParams, txMeta, decodedData, token}) - const USD = conversionUtil(amountBn, { - fromNumericBase: 'hex', - toNumericBase: 'dec', - fromCurrency: 'ETH', - toCurrency: 'USD', - numberOfDecimals: 2, - fromDenomination: 'WEI', - conversionRate, - }) - const ETH = conversionUtil(amountBn, { - fromNumericBase: 'hex', - toNumericBase: 'dec', - fromCurrency: 'ETH', - toCurrency: 'ETH', - fromDenomination: 'WEI', - conversionRate, - numberOfDecimals: 6, - }) - return { - USD, - ETH, - } - } else { - const USD = conversionUtil(amountBn, { - fromNumericBase: 'hex', - toNumericBase: 'dec', - fromCurrency: 'ETH', - toCurrency: 'USD', - numberOfDecimals: 2, - fromDenomination: 'WEI', - conversionRate, - }) - const ETH = conversionUtil(amountBn, { - fromNumericBase: 'hex', - toNumericBase: 'dec', - fromCurrency: 'ETH', - toCurrency: 'ETH', - fromDenomination: 'WEI', - conversionRate, - numberOfDecimals: 6, - }) - - return { - USD, - ETH, - } - } - -} - -PendingTx.prototype.getGasFee = function () { - const { conversionRate } = this.props - const txMeta = this.gatherTxMeta() - const txParams = txMeta.txParams || {} - - // Gas - const gas = txParams.gas - const gasBn = hexToBn(gas) - - // From latest master -// const gasLimit = new BN(parseInt(blockGasLimit)) -// const safeGasLimitBN = this.bnMultiplyByFraction(gasLimit, 19, 20) -// const saferGasLimitBN = this.bnMultiplyByFraction(gasLimit, 18, 20) -// const safeGasLimit = safeGasLimitBN.toString(10) - - // Gas Price - const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) - const gasPriceBn = hexToBn(gasPrice) - - const txFeeBn = gasBn.mul(gasPriceBn) - - const USD = conversionUtil(txFeeBn, { - fromNumericBase: 'BN', - toNumericBase: 'dec', - fromDenomination: 'WEI', - fromCurrency: 'ETH', - toCurrency: 'USD', - numberOfDecimals: 2, - conversionRate, - }) - const ETH = conversionUtil(txFeeBn, { - fromNumericBase: 'BN', - toNumericBase: 'dec', - fromDenomination: 'WEI', - fromCurrency: 'ETH', - toCurrency: 'ETH', - numberOfDecimals: 6, - conversionRate, - }) - - return { - USD, - ETH, - } -} - -PendingTx.prototype.getData = function () { - const { identities } = this.props - const txMeta = this.gatherTxMeta() - const txParams = txMeta.txParams || {} - const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data) - const { name, params = [] } = decodedData || {} - const { type, value } = params[0] || {} - const { USD: gasFeeInUSD, ETH: gasFeeInETH } = this.getGasFee() - const { USD: amountInUSD, ETH: amountInETH } = this.getAmount() - - const totalInUSD = addCurrencies(gasFeeInUSD, amountInUSD, { - toNumericBase: 'dec', - numberOfDecimals: 2, - }) - const totalInETH = addCurrencies(gasFeeInETH, amountInETH, { - toNumericBase: 'dec', - numberOfDecimals: 6, - }) - - if (name === 'transfer' && type === 'address') { - return { - from: { - address: txParams.from, - name: identities[txParams.from].name, - }, - to: { - address: value, - name: identities[value] ? identities[value].name : 'New Recipient', - }, - memo: txParams.memo || '', - gasFeeInUSD, - gasFeeInETH, - amountInUSD, - amountInETH, - } - } else { - return { - from: { - address: txParams.from, - name: identities[txParams.from].name, - }, - to: { - address: txParams.to, - name: identities[txParams.to] ? identities[txParams.to].name : 'New Recipient', - }, - memo: txParams.memo || '', - gasFeeInUSD, - gasFeeInETH, - amountInUSD, - amountInETH, - totalInUSD, - totalInETH, - } - } -} - -PendingTx.prototype.render = function () { - const { backToAccountDetail, selectedAddress } = this.props - const txMeta = this.gatherTxMeta() - const txParams = txMeta.txParams || {} - - // recipient check - // const isValidAddress = !txParams.to || util.isValidAddress(txParams.to) - - const { - from: { - address: fromAddress, - name: fromName, - }, - to: { - address: toAddress, - name: toName, - }, - memo, - gasFeeInUSD, - gasFeeInETH, - amountInUSD, - amountInETH, - totalInUSD, - totalInETH, - } = this.getData() - - // This is from the latest master - // It handles some of the errors that we are not currently handling - // Leaving as comments fo reference - - // const balanceBn = hexToBn(balance) - // const insufficientBalance = balanceBn.lt(maxCost) - // const buyDisabled = insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting - // const showRejectAll = props.unconfTxListLength > 1 -// const dangerousGasLimit = gasBn.gte(saferGasLimitBN) -// const gasLimitSpecified = txMeta.gasLimitSpecified - - this.inputs = [] - - return ( - h('div.flex-column.flex-grow.confirm-screen-container', { - style: { minWidth: '355px' }, - }, [ - // Main Send token Card - h('div.confirm-screen-wrapper.flex-column.flex-grow', [ - h('h3.flex-center.confirm-screen-header', [ - h('button.confirm-screen-back-button', { - onClick: () => backToAccountDetail(selectedAddress), - }, 'BACK'), - h('div.confirm-screen-title', 'Confirm Transaction'), - ]), - h('div.flex-row.flex-center.confirm-screen-identicons', [ - h('div.confirm-screen-account-wrapper', [ - h( - Identicon, - { - address: fromAddress, - diameter: 100, - }, - ), - h('span.confirm-screen-account-name', fromName), - h('span.confirm-screen-account-number', fromAddress.slice(fromAddress.length - 4)), - ]), - h('i.fa.fa-arrow-right.fa-lg'), - h('div.confirm-screen-account-wrapper', [ - h( - Identicon, - { - address: txParams.to, - diameter: 100, - }, - ), - h('span.confirm-screen-account-name', toName), - h('span.confirm-screen-account-number', toAddress.slice(toAddress.length - 4)), - ]), - ]), - - h('h3.flex-center.confirm-screen-sending-to-message', { - style: { - textAlign: 'center', - fontSize: '16px', - }, - }, [ - `You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`, - ]), - - h('h3.flex-center.confirm-screen-send-amount', [`$${amountInUSD}`]), - h('h3.flex-center.confirm-screen-send-amount-currency', [ 'USD' ]), - h('div.flex-center.confirm-memo-wrapper', [ - h('h3.confirm-screen-send-memo', [ memo ]), - ]), - - h('div.confirm-screen-rows', [ - h('section.flex-row.flex-center.confirm-screen-row', [ - h('span.confirm-screen-label.confirm-screen-section-column', [ 'From' ]), - h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', fromName), - h('div.confirm-screen-row-detail', `...${fromAddress.slice(fromAddress.length - 4)}`), - ]), - ]), - - h('section.flex-row.flex-center.confirm-screen-row', [ - h('span.confirm-screen-label.confirm-screen-section-column', [ 'To' ]), - h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', toName), - h('div.confirm-screen-row-detail', `...${toAddress.slice(toAddress.length - 4)}`), - ]), - ]), - - h('section.flex-row.flex-center.confirm-screen-row', [ - h('span.confirm-screen-label.confirm-screen-section-column', [ 'Gas Fee' ]), - h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', `$${gasFeeInUSD} USD`), - - h('div.confirm-screen-row-detail', `${gasFeeInETH} ETH`), - ]), - ]), - - - h('section.flex-row.flex-center.confirm-screen-total-box ', [ - h('div.confirm-screen-section-column', [ - h('span.confirm-screen-label', [ 'Total ' ]), - h('div.confirm-screen-total-box__subtitle', [ 'Amount + Gas' ]), - ]), - - h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', `$${totalInUSD} USD`), - h('div.confirm-screen-row-detail', `${totalInETH} ETH`), - ]), - ]), - ]), - -// These are latest errors handling from master -// Leaving as comments as reference when we start implementing error handling -// h('style', ` -// .conf-buttons button { -// margin-left: 10px; -// text-transform: uppercase; -// } -// `), - -// txMeta.simulationFails ? -// h('.error', { -// style: { -// marginLeft: 50, -// fontSize: '0.9em', -// }, -// }, 'Transaction Error. Exception thrown in contract code.') -// : null, - -// !isValidAddress ? -// h('.error', { -// style: { -// marginLeft: 50, -// fontSize: '0.9em', -// }, -// }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.') -// : null, - -// insufficientBalance ? -// h('span.error', { -// style: { -// marginLeft: 50, -// fontSize: '0.9em', -// }, -// }, 'Insufficient balance for transaction') -// : null, - -// // send + cancel -// h('.flex-row.flex-space-around.conf-buttons', { -// style: { -// display: 'flex', -// justifyContent: 'flex-end', -// margin: '14px 25px', -// }, -// }, [ -// h('button', { -// onClick: (event) => { -// this.resetGasFields() -// event.preventDefault() -// }, -// }, 'Reset'), - -// // Accept Button or Buy Button -// insufficientBalance ? h('button.btn-green', { onClick: props.buyEth }, 'Buy Ether') : -// h('input.confirm.btn-green', { -// type: 'submit', -// value: 'SUBMIT', -// style: { marginLeft: '10px' }, -// disabled: buyDisabled, -// }), - -// h('button.cancel.btn-red', { -// onClick: props.cancelTransaction, -// }, 'Reject'), -// ]), -// showRejectAll ? h('.flex-row.flex-space-around.conf-buttons', { -// style: { -// display: 'flex', -// justifyContent: 'flex-end', -// margin: '14px 25px', -// }, -// }, [ -// h('button.cancel.btn-red', { -// onClick: props.cancelAllTransactions, -// }, 'Reject All'), -// ]) : null, -// ]), -// ]) -// ) -// } - ]), - - h('form#pending-tx-form.flex-column.flex-center', { - onSubmit: this.onSubmit, - }, [ - // Reset Button - // h('button', { - // onClick: (event) => { - // this.resetGasFields() - // event.preventDefault() - // }, - // }, 'Reset'), - - // Accept Button - h('button.confirm-screen-confirm-button', ['CONFIRM']), - - // Cancel Button - h('div.cancel.btn-light.confirm-screen-cancel-button', { - onClick: (event) => this.cancel(event, txMeta), - }, 'CANCEL'), - ]), - ]) - ) -} - -// PendingTx.prototype.gasPriceChanged = function (newBN, valid) { -// log.info(`Gas price changed to: ${newBN.toString(10)}`) -// const txMeta = this.gatherTxMeta() -// txMeta.txParams.gasPrice = '0x' + newBN.toString('hex') -// this.setState({ -// txData: clone(txMeta), -// valid, -// }) -// } - -// PendingTx.prototype.gasLimitChanged = function (newBN, valid) { -// log.info(`Gas limit changed to ${newBN.toString(10)}`) -// const txMeta = this.gatherTxMeta() -// txMeta.txParams.gas = '0x' + newBN.toString('hex') -// this.setState({ -// txData: clone(txMeta), -// valid, -// }) -// } - -// PendingTx.prototype.resetGasFields = function () { -// log.debug(`pending-tx resetGasFields`) - -// this.inputs.forEach((hexInput) => { -// if (hexInput) { -// hexInput.setValid() -// } -// }) - -// this.setState({ -// txData: null, -// valid: true, -// }) -// } - -PendingTx.prototype.onSubmit = function (event) { - event.preventDefault() - const txMeta = this.gatherTxMeta() - const valid = this.checkValidity() - this.setState({ valid, submitting: true }) - - if (valid && this.verifyGasParams()) { - this.props.sendTransaction(txMeta, event) - } else { - this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) - this.setState({ submitting: false }) - } -} - -PendingTx.prototype.cancel = function (event, txMeta) { - event.preventDefault() - this.props.cancelTransaction(txMeta) -} - -PendingTx.prototype.checkValidity = function () { - const form = this.getFormEl() - const valid = form.checkValidity() - return valid -} - -PendingTx.prototype.getFormEl = function () { - const form = document.querySelector('form#pending-tx-form') - // Stub out form for unit tests: - if (!form) { - return { checkValidity () { return true } } - } - return form -} - -// After a customizable state value has been updated, -PendingTx.prototype.gatherTxMeta = function () { - const props = this.props - const state = this.state - const txData = clone(state.txData) || clone(props.txData) - - // log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) - return txData -} - -PendingTx.prototype.verifyGasParams = function () { - // We call this in case the gas has not been modified at all - if (!this.state) { return true } - return ( - this._notZeroOrEmptyString(this.state.gas) && - this._notZeroOrEmptyString(this.state.gasPrice) - ) -} - -PendingTx.prototype._notZeroOrEmptyString = function (obj) { - return obj !== '' && obj !== '0x0' -} - -PendingTx.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) { - const numBN = new BN(numerator) - const denomBN = new BN(denominator) - return targetBN.mul(numBN).div(denomBN) -} diff --git a/ui/app/components/pending-tx/confirm-send-ether.js b/ui/app/components/pending-tx/confirm-send-ether.js new file mode 100644 index 000000000..29c6d349c --- /dev/null +++ b/ui/app/components/pending-tx/confirm-send-ether.js @@ -0,0 +1,446 @@ +const Component = require('react').Component +const { connect } = require('react-redux') +const h = require('react-hyperscript') +const inherits = require('util').inherits +const actions = require('../../actions') +const clone = require('clone') +const Identicon = require('../identicon') +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const hexToBn = require('../../../../app/scripts/lib/hex-to-bn') +const { conversionUtil, addCurrencies } = require('../../conversion-util') + +const MIN_GAS_PRICE_GWEI_BN = new BN(1) +const GWEI_FACTOR = new BN(1e9) +const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) + +module.exports = connect(mapStateToProps, mapDispatchToProps)(ConfirmSendEther) + +function mapStateToProps (state) { + const { + conversionRate, + identities, + } = state.metamask + const accounts = state.metamask.accounts + const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] + return { + conversionRate, + identities, + selectedAddress, + } +} + +function mapDispatchToProps (dispatch) { + return { + setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('USD')), + backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)), + cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })), + } +} + +inherits(ConfirmSendEther, Component) +function ConfirmSendEther () { + Component.call(this) + this.state = {} + this.onSubmit = this.onSubmit.bind(this) +} + +ConfirmSendEther.prototype.getAmount = function () { + const { conversionRate } = this.props + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} + + const USD = conversionUtil(txParams.value, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromCurrency: 'ETH', + toCurrency: 'USD', + numberOfDecimals: 2, + fromDenomination: 'WEI', + conversionRate, + }) + const ETH = conversionUtil(txParams.value, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromCurrency: 'ETH', + toCurrency: 'ETH', + fromDenomination: 'WEI', + conversionRate, + numberOfDecimals: 6, + }) + + return { + USD, + ETH, + } + +} + +ConfirmSendEther.prototype.getGasFee = function () { + const { conversionRate } = this.props + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} + + // Gas + const gas = txParams.gas + const gasBn = hexToBn(gas) + + // From latest master +// const gasLimit = new BN(parseInt(blockGasLimit)) +// const safeGasLimitBN = this.bnMultiplyByFraction(gasLimit, 19, 20) +// const saferGasLimitBN = this.bnMultiplyByFraction(gasLimit, 18, 20) +// const safeGasLimit = safeGasLimitBN.toString(10) + + // Gas Price + const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) + const gasPriceBn = hexToBn(gasPrice) + + const txFeeBn = gasBn.mul(gasPriceBn) + + const USD = conversionUtil(txFeeBn, { + fromNumericBase: 'BN', + toNumericBase: 'dec', + fromDenomination: 'WEI', + fromCurrency: 'ETH', + toCurrency: 'USD', + numberOfDecimals: 2, + conversionRate, + }) + const ETH = conversionUtil(txFeeBn, { + fromNumericBase: 'BN', + toNumericBase: 'dec', + fromDenomination: 'WEI', + fromCurrency: 'ETH', + toCurrency: 'ETH', + numberOfDecimals: 6, + conversionRate, + }) + + return { + USD, + ETH, + } +} + +ConfirmSendEther.prototype.getData = function () { + const { identities } = this.props + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} + const { USD: gasFeeInUSD, ETH: gasFeeInETH } = this.getGasFee() + const { USD: amountInUSD, ETH: amountInETH } = this.getAmount() + + const totalInUSD = addCurrencies(gasFeeInUSD, amountInUSD, { + toNumericBase: 'dec', + numberOfDecimals: 2, + }) + const totalInETH = addCurrencies(gasFeeInETH, amountInETH, { + toNumericBase: 'dec', + numberOfDecimals: 6, + }) + + return { + from: { + address: txParams.from, + name: identities[txParams.from].name, + }, + to: { + address: txParams.to, + name: identities[txParams.to] ? identities[txParams.to].name : 'New Recipient', + }, + memo: txParams.memo || '', + gasFeeInUSD, + gasFeeInETH, + amountInUSD, + amountInETH, + totalInUSD, + totalInETH, + } +} + +ConfirmSendEther.prototype.render = function () { + const { backToAccountDetail, selectedAddress } = this.props + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} + + const { + from: { + address: fromAddress, + name: fromName, + }, + to: { + address: toAddress, + name: toName, + }, + memo, + gasFeeInUSD, + gasFeeInETH, + amountInUSD, + totalInUSD, + totalInETH, + } = this.getData() + + // This is from the latest master + // It handles some of the errors that we are not currently handling + // Leaving as comments fo reference + + // const balanceBn = hexToBn(balance) + // const insufficientBalance = balanceBn.lt(maxCost) + // const buyDisabled = insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting + // const showRejectAll = props.unconfTxListLength > 1 +// const dangerousGasLimit = gasBn.gte(saferGasLimitBN) +// const gasLimitSpecified = txMeta.gasLimitSpecified + + this.inputs = [] + + return ( + h('div.flex-column.flex-grow.confirm-screen-container', { + style: { minWidth: '355px' }, + }, [ + // Main Send token Card + h('div.confirm-screen-wrapper.flex-column.flex-grow', [ + h('h3.flex-center.confirm-screen-header', [ + h('button.confirm-screen-back-button', { + onClick: () => backToAccountDetail(selectedAddress), + }, 'BACK'), + h('div.confirm-screen-title', 'Confirm Transaction'), + ]), + h('div.flex-row.flex-center.confirm-screen-identicons', [ + h('div.confirm-screen-account-wrapper', [ + h( + Identicon, + { + address: fromAddress, + diameter: 100, + }, + ), + h('span.confirm-screen-account-name', fromName), + h('span.confirm-screen-account-number', fromAddress.slice(fromAddress.length - 4)), + ]), + h('i.fa.fa-arrow-right.fa-lg'), + h('div.confirm-screen-account-wrapper', [ + h( + Identicon, + { + address: txParams.to, + diameter: 100, + }, + ), + h('span.confirm-screen-account-name', toName), + h('span.confirm-screen-account-number', toAddress.slice(toAddress.length - 4)), + ]), + ]), + + h('h3.flex-center.confirm-screen-sending-to-message', { + style: { + textAlign: 'center', + fontSize: '16px', + }, + }, [ + `You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`, + ]), + + h('h3.flex-center.confirm-screen-send-amount', [`$${amountInUSD}`]), + h('h3.flex-center.confirm-screen-send-amount-currency', [ 'USD' ]), + h('div.flex-center.confirm-memo-wrapper', [ + h('h3.confirm-screen-send-memo', [ memo ]), + ]), + + h('div.confirm-screen-rows', [ + h('section.flex-row.flex-center.confirm-screen-row', [ + h('span.confirm-screen-label.confirm-screen-section-column', [ 'From' ]), + h('div.confirm-screen-section-column', [ + h('div.confirm-screen-row-info', fromName), + h('div.confirm-screen-row-detail', `...${fromAddress.slice(fromAddress.length - 4)}`), + ]), + ]), + + h('section.flex-row.flex-center.confirm-screen-row', [ + h('span.confirm-screen-label.confirm-screen-section-column', [ 'To' ]), + h('div.confirm-screen-section-column', [ + h('div.confirm-screen-row-info', toName), + h('div.confirm-screen-row-detail', `...${toAddress.slice(toAddress.length - 4)}`), + ]), + ]), + + h('section.flex-row.flex-center.confirm-screen-row', [ + h('span.confirm-screen-label.confirm-screen-section-column', [ 'Gas Fee' ]), + h('div.confirm-screen-section-column', [ + h('div.confirm-screen-row-info', `$${gasFeeInUSD} USD`), + + h('div.confirm-screen-row-detail', `${gasFeeInETH} ETH`), + ]), + ]), + + + h('section.flex-row.flex-center.confirm-screen-total-box ', [ + h('div.confirm-screen-section-column', [ + h('span.confirm-screen-label', [ 'Total ' ]), + h('div.confirm-screen-total-box__subtitle', [ 'Amount + Gas' ]), + ]), + + h('div.confirm-screen-section-column', [ + h('div.confirm-screen-row-info', `$${totalInUSD} USD`), + h('div.confirm-screen-row-detail', `${totalInETH} ETH`), + ]), + ]), + ]), + +// These are latest errors handling from master +// Leaving as comments as reference when we start implementing error handling +// h('style', ` +// .conf-buttons button { +// margin-left: 10px; +// text-transform: uppercase; +// } +// `), + +// txMeta.simulationFails ? +// h('.error', { +// style: { +// marginLeft: 50, +// fontSize: '0.9em', +// }, +// }, 'Transaction Error. Exception thrown in contract code.') +// : null, + +// !isValidAddress ? +// h('.error', { +// style: { +// marginLeft: 50, +// fontSize: '0.9em', +// }, +// }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.') +// : null, + +// insufficientBalance ? +// h('span.error', { +// style: { +// marginLeft: 50, +// fontSize: '0.9em', +// }, +// }, 'Insufficient balance for transaction') +// : null, + +// // send + cancel +// h('.flex-row.flex-space-around.conf-buttons', { +// style: { +// display: 'flex', +// justifyContent: 'flex-end', +// margin: '14px 25px', +// }, +// }, [ +// h('button', { +// onClick: (event) => { +// this.resetGasFields() +// event.preventDefault() +// }, +// }, 'Reset'), + +// // Accept Button or Buy Button +// insufficientBalance ? h('button.btn-green', { onClick: props.buyEth }, 'Buy Ether') : +// h('input.confirm.btn-green', { +// type: 'submit', +// value: 'SUBMIT', +// style: { marginLeft: '10px' }, +// disabled: buyDisabled, +// }), + +// h('button.cancel.btn-red', { +// onClick: props.cancelTransaction, +// }, 'Reject'), +// ]), +// showRejectAll ? h('.flex-row.flex-space-around.conf-buttons', { +// style: { +// display: 'flex', +// justifyContent: 'flex-end', +// margin: '14px 25px', +// }, +// }, [ +// h('button.cancel.btn-red', { +// onClick: props.cancelAllTransactions, +// }, 'Reject All'), +// ]) : null, +// ]), +// ]) +// ) +// } + ]), + + h('form#pending-tx-form.flex-column.flex-center', { + onSubmit: this.onSubmit, + }, [ + + // Accept Button + h('button.confirm-screen-confirm-button', ['CONFIRM']), + + // Cancel Button + h('div.cancel.btn-light.confirm-screen-cancel-button', { + onClick: (event) => this.cancel(event, txMeta), + }, 'CANCEL'), + ]), + ]) + ) +} + +ConfirmSendEther.prototype.onSubmit = function (event) { + event.preventDefault() + const txMeta = this.gatherTxMeta() + const valid = this.checkValidity() + this.setState({ valid, submitting: true }) + + if (valid && this.verifyGasParams()) { + this.props.sendTransaction(txMeta, event) + } else { + this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) + this.setState({ submitting: false }) + } +} + +ConfirmSendEther.prototype.cancel = function (event, txMeta) { + event.preventDefault() + this.props.cancelTransaction(txMeta) +} + +ConfirmSendEther.prototype.checkValidity = function () { + const form = this.getFormEl() + const valid = form.checkValidity() + return valid +} + +ConfirmSendEther.prototype.getFormEl = function () { + const form = document.querySelector('form#pending-tx-form') + // Stub out form for unit tests: + if (!form) { + return { checkValidity () { return true } } + } + return form +} + +// After a customizable state value has been updated, +ConfirmSendEther.prototype.gatherTxMeta = function () { + const props = this.props + const state = this.state + const txData = clone(state.txData) || clone(props.txData) + + // log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) + return txData +} + +ConfirmSendEther.prototype.verifyGasParams = function () { + // We call this in case the gas has not been modified at all + if (!this.state) { return true } + return ( + this._notZeroOrEmptyString(this.state.gas) && + this._notZeroOrEmptyString(this.state.gasPrice) + ) +} + +ConfirmSendEther.prototype._notZeroOrEmptyString = function (obj) { + return obj !== '' && obj !== '0x0' +} + +ConfirmSendEther.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) { + const numBN = new BN(numerator) + const denomBN = new BN(denominator) + return targetBN.mul(numBN).div(denomBN) +} diff --git a/ui/app/components/pending-tx/index.js b/ui/app/components/pending-tx/index.js new file mode 100644 index 000000000..3797b5642 --- /dev/null +++ b/ui/app/components/pending-tx/index.js @@ -0,0 +1,103 @@ +const Component = require('react').Component +const { connect } = require('react-redux') +const h = require('react-hyperscript') +const clone = require('clone') +const abi = require('human-standard-token-abi') +const abiDecoder = require('abi-decoder') +abiDecoder.addABI(abi) +const inherits = require('util').inherits +const actions = require('../../actions') +const util = require('../../util') +const ConfirmSendEther = require('./confirm-send-ether') + +const TX_TYPES = { + DEPLOY_CONTRACT: 'deploy_contract', + SEND_ETHER: 'send_ether', + SEND_TOKEN: 'send_token', +} + +module.exports = connect(mapStateToProps, mapDispatchToProps)(PendingTx) + +function mapStateToProps (state) { + const { + conversionRate, + identities, + } = state.metamask + const accounts = state.metamask.accounts + const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] + return { + conversionRate, + identities, + selectedAddress, + } +} + +function mapDispatchToProps (dispatch) { + return { + setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('USD')), + backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)), + cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })), + } +} + +inherits(PendingTx, Component) +function PendingTx () { + Component.call(this) + this.state = { + isFetching: true, + transactionType: '', + } +} + +PendingTx.prototype.componentWillMount = function () { + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} + + this.props.setCurrentCurrencyToUSD() + + if (txParams.to) { + const token = util.getContractAtAddress(txParams.to) + token + .symbol() + .then(result => { + const symbol = result[0] || null + this.setState({ + transactionType: symbol ? TX_TYPES.SEND_TOKEN : TX_TYPES.SEND_ETHER, + isFetching: false, + }) + }) + .catch(() => this.setState({ + transactionType: TX_TYPES.SEND_ETHER, + isFetching: false, + })) + } else { + this.setState({ + transactionType: TX_TYPES.DEPLOY_CONTRACT, + isFetching: false, + }) + } +} + +PendingTx.prototype.gatherTxMeta = function () { + const props = this.props + const state = this.state + const txData = clone(state.txData) || clone(props.txData) + + return txData +} + +PendingTx.prototype.render = function () { + const { isFetching, transactionType } = this.state + + if (isFetching) { + return h('noscript') + } + + + switch (transactionType) { + case TX_TYPES.SEND_ETHER: + return h(ConfirmSendEther, { txData: this.gatherTxMeta() }) + default: + return h('noscript') + } +} -- cgit v1.2.3 From e9b7fd901862f57a92614b19f7f2caa969d4a282 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 22 Sep 2017 10:41:14 -0700 Subject: Patch security update --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0d02f1df5..bcfb6c1ac 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "redux": "^3.0.5", "redux-logger": "^3.0.6", "redux-thunk": "^2.2.0", - "request-promise": "^4.1.1", + "request-promise": "^4.2.1", "sandwich-expando": "^1.0.5", "semaphore": "^1.0.5", "sw-stream": "^2.0.0", -- cgit v1.2.3 From 14bdc5a78c8529742754d69b8e45693b06b380fe Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 20 Sep 2017 15:07:12 -0230 Subject: Client side error handling for from, to and amount fields in send.js --- ui/app/conversion-util.js | 13 +++ ui/app/css/itcss/components/send.scss | 15 +++ ui/app/send.js | 195 ++++++++++++++++++++++++++-------- ui/app/util.js | 5 + 4 files changed, 184 insertions(+), 44 deletions(-) diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js index 0ede77487..7e02fe2bd 100644 --- a/ui/app/conversion-util.js +++ b/ui/app/conversion-util.js @@ -123,7 +123,20 @@ const addCurrencies = (a, b, { toNumericBase, numberOfDecimals }) => { }) } +const conversionGreaterThan = ( + { value, fromNumericBase }, + { value: compareToValue, fromNumericBase: compareToBase }, +) => { + const firstValue = converter({ value, fromNumericBase }) + const secondValue = converter({ + value: compareToValue, + fromNumericBase: compareToBase, + }) + return firstValue.gt(secondValue) +} + module.exports = { conversionUtil, addCurrencies, + conversionGreaterThan, } \ No newline at end of file diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index 2d6374aa2..84f678130 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -104,6 +104,16 @@ color: $red; } } + + .send-screen-input-wrapper__error-message { + display: block; + position: absolute; + bottom: 4px; + font-size: 12px; + line-height: 12px; + left: 8px; + color: $red; + } } .send-screen-input { @@ -295,6 +305,11 @@ width: 163px; text-align: center; } + + &__send-button__disabled { + opacity: 0.5; + cursor: auto; + } } .send-token { diff --git a/ui/app/send.js b/ui/app/send.js index 481682bc8..16fe470be 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -18,8 +18,8 @@ const { signTx, } = require('./actions') const { stripHexPrefix, addHexPrefix } = require('ethereumjs-util') -const { isHex, numericBalance, isValidAddress } = require('./util') -const { conversionUtil } = require('./conversion-util') +const { isHex, numericBalance, isValidAddress, allNull } = require('./util') +const { conversionUtil, conversionGreaterThan } = require('./conversion-util') const BigNumber = require('bignumber.js') module.exports = connect(mapStateToProps)(SendTransactionScreen) @@ -51,7 +51,7 @@ function mapStateToProps (state) { error: warning && warning.split('.')[0], account, identity: identities[address], - balance: account ? numericBalance(account.balance) : null, + balance: account ? account.balance : null, } } @@ -65,8 +65,8 @@ function SendTransactionScreen () { newTx: { from: '', to: '', - // these values are hardcoded, so "Next" can be clicked - amount: '0x0', // see L544 + amount: 0, + amountToSend: '0x0', gasPrice: '0x5d21dba00', gas: '0x7b0d', txData: null, @@ -74,6 +74,8 @@ function SendTransactionScreen () { }, activeCurrency: 'USD', tooltipIsOpen: false, + errors: {}, + isValid: false, } this.back = this.back.bind(this) @@ -81,12 +83,26 @@ function SendTransactionScreen () { this.onSubmit = this.onSubmit.bind(this) this.setActiveCurrency = this.setActiveCurrency.bind(this) this.toggleTooltip = this.toggleTooltip.bind(this) + this.validate = this.validate.bind(this) + this.getAmountToSend = this.getAmountToSend.bind(this) + this.setErrorsFor = this.setErrorsFor.bind(this) + this.clearErrorsFor = this.clearErrorsFor.bind(this) this.renderFromInput = this.renderFromInput.bind(this) this.renderToInput = this.renderToInput.bind(this) this.renderAmountInput = this.renderAmountInput.bind(this) this.renderGasInput = this.renderGasInput.bind(this) this.renderMemoInput = this.renderMemoInput.bind(this) + this.renderErrorMessage = this.renderErrorMessage.bind(this) +} + +SendTransactionScreen.prototype.renderErrorMessage = function(errorType, warning) { + const { errors } = this.state + const errorMessage = errors[errorType]; + + return errorMessage || warning + ? h('div.send-screen-input-wrapper__error-message', [ errorMessage || warning ]) + : null } SendTransactionScreen.prototype.renderFromInput = function (from, identities) { @@ -106,6 +122,8 @@ SendTransactionScreen.prototype.renderFromInput = function (from, identities) { }, }) }, + onBlur: () => this.setErrorsFor('from'), + onFocus: () => this.clearErrorsFor('from'), }), h('datalist#accounts', [ @@ -118,6 +136,8 @@ SendTransactionScreen.prototype.renderFromInput = function (from, identities) { }), ]), + this.renderErrorMessage('from'), + ]) } @@ -139,6 +159,8 @@ SendTransactionScreen.prototype.renderToInput = function (to, identities, addres }, }) }, + onBlur: () => this.setErrorsFor('to'), + onFocus: () => this.clearErrorsFor('to'), }), h('datalist#addresses', [ @@ -160,6 +182,8 @@ SendTransactionScreen.prototype.renderToInput = function (to, identities, addres }), ]), + this.renderErrorMessage('to'), + ]) } @@ -183,12 +207,17 @@ SendTransactionScreen.prototype.renderAmountInput = function (activeCurrency) { this.state.newTx, { amount: event.target.value, + amountToSend: this.getAmountToSend(event.target.value), } ), }) }, + onBlur: () => this.setErrorsFor('amount'), + onFocus: () => this.clearErrorsFor('amount'), }), + this.renderErrorMessage('amount'), + ]) } @@ -260,14 +289,13 @@ SendTransactionScreen.prototype.render = function () { const props = this.props const { - // selectedIdentity, - // network, + warning, identities, addressBook, conversionRate, } = props - const { blockGasLimit, newTx, activeCurrency } = this.state + const { blockGasLimit, newTx, activeCurrency, isValid } = this.state const { gas, gasPrice } = newTx return ( @@ -292,12 +320,15 @@ SendTransactionScreen.prototype.render = function () { this.renderMemoInput(), + this.renderErrorMessage(null, warning), + ]), // Buttons underneath card h('section.flex-column.flex-center', [ h('button.btn-secondary.send-screen__send-button', { - onClick: (event) => this.onSubmit(event), + className: !isValid && 'send-screen__send-button__disabled', + onClick: (event) => isValid && this.onSubmit(event), }, 'Next'), h('button.btn-tertiary.send-screen__cancel-button', { onClick: this.back, @@ -325,62 +356,140 @@ SendTransactionScreen.prototype.back = function () { this.props.dispatch(backToAccountDetail(address)) } -SendTransactionScreen.prototype.onSubmit = function (event) { - event.preventDefault() - const { warning } = this.props - const state = this.state || {} +SendTransactionScreen.prototype.validate = function (balance, amountToSend, { to, from }) { + const sufficientBalance = conversionGreaterThan( + { + value: balance, + fromNumericBase: 'hex', + }, + { + value: amountToSend, + fromNumericBase: 'hex', + }, + ) - const recipient = state.newTx.to - const nickname = state.nickname || ' ' + const amountLessThanZero = conversionGreaterThan( + { + value: 0, + fromNumericBase: 'dec', + }, + { + value: amountToSend, + fromNumericBase: 'hex', + }, + ) - // TODO: convert this to hex when created and include it in send - const txData = state.newTx.memo + const errors = {} - let message + if (!sufficientBalance) { + errors.amount = 'Insufficient funds.' + } - // if (value.gt(balance)) { - // message = 'Insufficient funds.' - // return this.props.dispatch(actions.displayWarning(message)) - // } + if (amountLessThanZero) { + errors.amount = 'Can not send negative amounts of ETH.' + } - // if (input < 0) { - // message = 'Can not send negative amounts of ETH.' - // return this.props.dispatch(actions.displayWarning(message)) - // } + if (!from) { + errors.from = 'Required' + } - if (!isValidAddress(recipient) && !recipient) { - message = 'Recipient address is invalid.' - return this.props.dispatch(displayWarning(message)) + if (from && !isValidAddress(from)) { + errors.from = 'Sender address is invalid.' } - if (txData && !isHex(stripHexPrefix(txData))) { - message = 'Transaction data must be hex string.' - return this.props.dispatch(displayWarning(message)) + if (!to) { + errors.to = 'Required' } - this.props.dispatch(hideWarning()) + if (to && !isValidAddress(to)) { + errors.to = 'Recipient address is invalid.' + } - this.props.dispatch(addToAddressBook(recipient, nickname)) + // if (txData && !isHex(stripHexPrefix(txData))) { + // message = 'Transaction data must be hex string.' + // return this.props.dispatch(displayWarning(message)) + // } + + return { + isValid: allNull(errors), + errors, + } +} + +SendTransactionScreen.prototype.getAmountToSend = function (amount) { + const { activeCurrency } = this.state + const { conversionRate } = this.props // TODO: need a clean way to integrate this into conversionUtil - const sendConversionRate = state.activeCurrency === 'ETH' - ? this.props.conversionRate - : new BigNumber(1.0).div(this.props.conversionRate) + const sendConversionRate = activeCurrency === 'ETH' + ? conversionRate + : new BigNumber(1.0).div(conversionRate) - const sendAmount = conversionUtil(this.state.newTx.amount, { + return conversionUtil(amount, { fromNumericBase: 'dec', toNumericBase: 'hex', - fromCurrency: state.activeCurrency, + fromCurrency: activeCurrency, toCurrency: 'ETH', toDenomination: 'WEI', conversionRate: sendConversionRate, }) - +} + +SendTransactionScreen.prototype.setErrorsFor = function (field) { + const { balance } = this.props + const { newTx, errors: previousErrors } = this.state + const { amountToSend } = newTx + + const { + isValid, + errors: newErrors + } = this.validate(balance, amountToSend, newTx) + + const nextErrors = Object.assign({}, previousErrors, { + [field]: newErrors[field] || null + }) + + if (!isValid) { + this.setState({ + errors: nextErrors, + isValid, + }) + } +} + +SendTransactionScreen.prototype.clearErrorsFor = function (field) { + const { errors: previousErrors } = this.state + const nextErrors = Object.assign({}, previousErrors, { + [field]: null + }) + + this.setState({ + errors: nextErrors, + isValid: allNull(nextErrors), + }) +} + +SendTransactionScreen.prototype.onSubmit = function (event) { + event.preventDefault() + const { warning, balance, amountToSend } = this.props + const state = this.state || {} + + const recipient = state.newTx.to + const sender = state.newTx.from + const nickname = state.nickname || ' ' + + // TODO: convert this to hex when created and include it in send + const txData = state.newTx.memo + + this.props.dispatch(hideWarning()) + + this.props.dispatch(addToAddressBook(recipient, nickname)) + var txParams = { from: this.state.newTx.from, to: this.state.newTx.to, - value: sendAmount, + value: amountToSend, gas: this.state.newTx.gas, gasPrice: this.state.newTx.gasPrice, @@ -389,7 +498,5 @@ SendTransactionScreen.prototype.onSubmit = function (event) { if (recipient) txParams.to = addHexPrefix(recipient) if (txData) txParams.data = txData - if (!warning) { - this.props.dispatch(signTx(txParams)) - } + this.props.dispatch(signTx(txParams)) } diff --git a/ui/app/util.js b/ui/app/util.js index 7aace1b3c..82a5f9f29 100644 --- a/ui/app/util.js +++ b/ui/app/util.js @@ -55,6 +55,7 @@ module.exports = { getContractAtAddress, exportAsFile: exportAsFile, isInvalidChecksumAddress, + allNull, } function valuesFor (obj) { @@ -273,3 +274,7 @@ function exportAsFile (filename, data) { document.body.removeChild(elem) } } + +function allNull (obj) { + return Object.entries(obj).every(([key, value]) => value === null) +} -- cgit v1.2.3 From fe37dd7ecd142bbb0b51d62abfdc0a25240aef42 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 22 Sep 2017 01:56:10 -0230 Subject: Open account details modal on buy -> direct deposit. --- ui/app/components/modals/buy-options-modal.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ui/app/components/modals/buy-options-modal.js b/ui/app/components/modals/buy-options-modal.js index 79bbc798b..a8033d288 100644 --- a/ui/app/components/modals/buy-options-modal.js +++ b/ui/app/components/modals/buy-options-modal.js @@ -19,6 +19,9 @@ function mapDispatchToProps (dispatch) { hideModal: () => { dispatch(actions.hideModal()) }, + showAccountDetailModal: () => { + dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' })) + }, } } @@ -59,7 +62,9 @@ BuyOptions.prototype.render = function () { h('div.buy-modal-content-option-subtitle', {}, 'Trade any digital asset for any other'), ]), - h('div.buy-modal-content-option', {}, [ + h('div.buy-modal-content-option', { + onClick: () => this.goToAccountDetailsModal() + }, [ h('div.buy-modal-content-option-title', {}, 'Direct Deposit'), h('div.buy-modal-content-option-subtitle', {}, 'Deposit from another account'), ]), @@ -75,3 +80,8 @@ BuyOptions.prototype.render = function () { ]), ]) } + +BuyOptions.prototype.goToAccountDetailsModal = function () { + this.props.hideModal() + this.props.showAccountDetailModal() +} -- cgit v1.2.3 From 4c971ebfd1fad18368ec418c36c4d05a6bb37e6d Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 22 Sep 2017 13:25:08 -0700 Subject: Define encryptor in constructor params instead of platform object --- app/scripts/metamask-controller.js | 2 +- docs/porting_to_new_environment.md | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index e4fa3518f..42248827f 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -95,7 +95,7 @@ module.exports = class MetamaskController extends EventEmitter { initState: initState.KeyringController, ethStore: this.ethStore, getNetwork: this.networkController.getNetworkState.bind(this.networkController), - encryptor: opts.platform.encryptor || undefined, + encryptor: opts.encryptor || undefined, }) this.keyringController.on('newAccount', (address) => { this.preferencesController.setSelectedAddress(address) diff --git a/docs/porting_to_new_environment.md b/docs/porting_to_new_environment.md index c6336b9f9..729a28e5d 100644 --- a/docs/porting_to_new_environment.md +++ b/docs/porting_to_new_environment.md @@ -11,7 +11,16 @@ The core functionality of MetaMask all lives in what we call [The MetaMask Contr When calling `new MetaMask(opts)`, many platform-specific options are configured. The keys on `opts` are as follows: - initState: The last emitted state, used for restoring persistent state between sessions. -- platform: The `platform` object defines a variety of platform-specific functions, including opening the confirmation view, and customizing the encryption method. +- platform: The `platform` object defines a variety of platform-specific functions, including opening the confirmation view, and opening web sites. +- encryptor - An object that provides access to the desired encryption methods. + +##### Encryptor + +An object that provides two simple methods, which can encrypt in any format you prefer. This parameter is optional, and will default to the browser-native WebCrypto API. + +- encrypt(password, object) - returns a Promise of a string that is ready for storage. +- decrypt(password, encryptedString) - Accepts the encrypted output of `encrypt` and returns a Promise of a restored `object` as it was encrypted. + ##### Platform Options @@ -20,9 +29,6 @@ The `platform` object has a variety of options: - reload (function) - Will be called when MetaMask would like to reload its own context. - openWindow ({ url }) - Will be called when MetaMask would like to open a web page. It will be passed a single `options` object with a `url` key, with a string value. - getVersion() - Should return the current MetaMask version, as described in the current `CHANGELOG.md` or `app/manifest.json`. -- encryptor - An object that includes two methods: - - encrypt(password, object) - returns a Promise of a string that is ready for storage. - - decrypt(password, encryptedString) - Accepts the encrypted output of `encrypt` and returns a Promise of a restored `object` as it was encrypted. #### [metamask.getState()](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js#L241) -- cgit v1.2.3 From 08b36b9b582853b411a19e48b407c70596381f61 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 22 Sep 2017 13:29:13 -0700 Subject: Allow metamaskController to define keyring types --- app/scripts/keyring-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js index adfa4a813..4454f0b63 100644 --- a/app/scripts/keyring-controller.js +++ b/app/scripts/keyring-controller.js @@ -27,7 +27,7 @@ class KeyringController extends EventEmitter { constructor (opts) { super() const initState = opts.initState || {} - this.keyringTypes = keyringTypes + this.keyringTypes = opts.keyringTypes || keyringTypes this.store = new ObservableStore(initState) this.memStore = new ObservableStore({ isUnlocked: false, -- cgit v1.2.3 From 977405fc7d89256a911e73b83a6678235fa1cfb8 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 22 Sep 2017 13:33:53 -0700 Subject: Remove dead code from eth-store --- app/scripts/lib/eth-store.js | 44 +------------------------------------------- 1 file changed, 1 insertion(+), 43 deletions(-) diff --git a/app/scripts/lib/eth-store.js b/app/scripts/lib/eth-store.js index ebba98f5c..ff22eca4a 100644 --- a/app/scripts/lib/eth-store.js +++ b/app/scripts/lib/eth-store.js @@ -18,10 +18,6 @@ class EthereumStore extends ObservableStore { constructor (opts = {}) { super({ accounts: {}, - transactions: {}, - currentBlockNumber: '0', - currentBlockHash: '', - currentBlockGasLimit: '', }) this._provider = opts.provider this._query = new EthQuery(this._provider) @@ -50,21 +46,6 @@ class EthereumStore extends ObservableStore { this.updateState({ accounts }) } - addTransaction (txHash) { - const transactions = this.getState().transactions - transactions[txHash] = {} - this.updateState({ transactions }) - if (!this._currentBlockNumber) return - this._updateTransaction(this._currentBlockNumber, txHash, noop) - } - - removeTransaction (txHash) { - const transactions = this.getState().transactions - delete transactions[txHash] - this.updateState({ transactions }) - } - - // // private // @@ -72,12 +53,9 @@ class EthereumStore extends ObservableStore { _updateForBlock (block) { const blockNumber = '0x' + block.number.toString('hex') this._currentBlockNumber = blockNumber - this.updateState({ currentBlockNumber: parseInt(blockNumber) }) - this.updateState({ currentBlockHash: `0x${block.hash.toString('hex')}`}) - this.updateState({ currentBlockGasLimit: `0x${block.gasLimit.toString('hex')}` }) + async.parallel([ this._updateAccounts.bind(this), - this._updateTransactions.bind(this, blockNumber), ], (err) => { if (err) return console.error(err) this.emit('block', this.getState()) @@ -104,26 +82,6 @@ class EthereumStore extends ObservableStore { }) } - _updateTransactions (block, cb = noop) { - const transactions = this.getState().transactions - const txHashes = Object.keys(transactions) - async.each(txHashes, this._updateTransaction.bind(this, block), cb) - } - - _updateTransaction (block, txHash, cb = noop) { - // would use the block here to determine how many confirmations the tx has - const transactions = this.getState().transactions - this._query.getTransaction(txHash, (err, result) => { - if (err) return cb(err) - // only populate if the entry is still present - if (transactions[txHash]) { - transactions[txHash] = result - this.updateState({ transactions }) - } - cb(null, result) - }) - } - _getAccount (address, cb = noop) { const query = this._query async.parallel({ -- cgit v1.2.3 From 2ca2df183265edb048ee1f0e6bd2437cab7c4efe Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 22 Sep 2017 13:58:46 -0700 Subject: deps - bump eth-block-tracker --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 0d02f1df5..a9268060d 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "end-of-stream": "^1.1.0", "ensnare": "^1.0.0", "eth-bin-to-ops": "^1.0.1", + "eth-block-tracker": "^2.2.0", "eth-contract-metadata": "^1.1.4", "eth-hd-keyring": "^1.1.1", "eth-json-rpc-filters": "^1.1.0", -- cgit v1.2.3 From 11c8c07bfc6677e347873f02ae8c401f8d6c4dcf Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 22 Sep 2017 13:59:25 -0700 Subject: Refactor eth-store into account-tracker EthStore was only being used for tracking account balances and nonces now, so I removed its block-tracking duties, renamed it account-tracker, and removed it as a dependency from `KeyringController`, so that KRC can go live on without a hard dep on it. --- app/scripts/controllers/balances.js | 64 ------------------- app/scripts/controllers/computed-balances.js | 64 +++++++++++++++++++ app/scripts/controllers/transactions.js | 6 +- app/scripts/lib/account-tracker.js | 96 ++++++++++++++++++++++++++++ app/scripts/lib/eth-store.js | 96 ---------------------------- app/scripts/metamask-controller.js | 26 +++++--- 6 files changed, 179 insertions(+), 173 deletions(-) delete mode 100644 app/scripts/controllers/balances.js create mode 100644 app/scripts/controllers/computed-balances.js create mode 100644 app/scripts/lib/account-tracker.js delete mode 100644 app/scripts/lib/eth-store.js diff --git a/app/scripts/controllers/balances.js b/app/scripts/controllers/balances.js deleted file mode 100644 index 89c2ca95d..000000000 --- a/app/scripts/controllers/balances.js +++ /dev/null @@ -1,64 +0,0 @@ -const ObservableStore = require('obs-store') -const extend = require('xtend') -const BalanceController = require('./balance') - -class BalancesController { - - constructor (opts = {}) { - const { ethStore, txController } = opts - this.ethStore = ethStore - this.txController = txController - - const initState = extend({ - computedBalances: {}, - }, opts.initState) - this.store = new ObservableStore(initState) - this.balances = {} - - this._initBalanceUpdating() - } - - updateAllBalances () { - for (let address in this.balances) { - this.balances[address].updateBalance() - } - } - - _initBalanceUpdating () { - const store = this.ethStore.getState() - this.addAnyAccountsFromStore(store) - this.ethStore.subscribe(this.addAnyAccountsFromStore.bind(this)) - } - - addAnyAccountsFromStore(store) { - const balances = store.accounts - - for (let address in balances) { - this.trackAddressIfNotAlready(address) - } - } - - trackAddressIfNotAlready (address) { - const state = this.store.getState() - if (!(address in state.computedBalances)) { - this.trackAddress(address) - } - } - - trackAddress (address) { - let updater = new BalanceController({ - address, - ethStore: this.ethStore, - txController: this.txController, - }) - updater.store.subscribe((accountBalance) => { - let newState = this.store.getState() - newState.computedBalances[address] = accountBalance - this.store.updateState(newState) - }) - this.balances[address] = updater - updater.updateBalance() - } -} - -module.exports = BalancesController diff --git a/app/scripts/controllers/computed-balances.js b/app/scripts/controllers/computed-balances.js new file mode 100644 index 000000000..a85eb5590 --- /dev/null +++ b/app/scripts/controllers/computed-balances.js @@ -0,0 +1,64 @@ +const ObservableStore = require('obs-store') +const extend = require('xtend') +const BalanceController = require('./balance') + +class ComputedbalancesController { + + constructor (opts = {}) { + const { ethStore, txController } = opts + this.ethStore = ethStore + this.txController = txController + + const initState = extend({ + computedBalances: {}, + }, opts.initState) + this.store = new ObservableStore(initState) + this.balances = {} + + this._initBalanceUpdating() + } + + updateAllBalances () { + for (let address in this.balances) { + this.balances[address].updateBalance() + } + } + + _initBalanceUpdating () { + const store = this.ethStore.getState() + this.addAnyAccountsFromStore(store) + this.ethStore.subscribe(this.addAnyAccountsFromStore.bind(this)) + } + + addAnyAccountsFromStore(store) { + const balances = store.accounts + + for (let address in balances) { + this.trackAddressIfNotAlready(address) + } + } + + trackAddressIfNotAlready (address) { + const state = this.store.getState() + if (!(address in state.computedBalances)) { + this.trackAddress(address) + } + } + + trackAddress (address) { + let updater = new BalanceController({ + address, + ethStore: this.ethStore, + txController: this.txController, + }) + updater.store.subscribe((accountBalance) => { + let newState = this.store.getState() + newState.computedBalances[address] = accountBalance + this.store.updateState(newState) + }) + this.balances[address] = updater + updater.updateBalance() + } +} + +module.exports = ComputedbalancesController diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 59a3f5329..2aff4e5ff 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -22,7 +22,7 @@ module.exports = class TransactionController extends EventEmitter { this.provider = opts.provider this.blockTracker = opts.blockTracker this.signEthTx = opts.signTransaction - this.ethStore = opts.ethStore + this.accountTracker = opts.accountTracker this.nonceTracker = new NonceTracker({ provider: this.provider, @@ -52,7 +52,7 @@ module.exports = class TransactionController extends EventEmitter { provider: this.provider, nonceTracker: this.nonceTracker, getBalance: (address) => { - const account = this.ethStore.getState().accounts[address] + const account = this.accountTracker.getState().accounts[address] if (!account) return return account.balance }, @@ -73,7 +73,7 @@ module.exports = class TransactionController extends EventEmitter { this.blockTracker.on('rawBlock', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker)) // this is a little messy but until ethstore has been either // removed or redone this is to guard against the race condition - // where ethStore hasent been populated by the results yet + // where accountTracker hasent been populated by the results yet this.blockTracker.once('latest', () => { this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker)) }) diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js new file mode 100644 index 000000000..bf949597b --- /dev/null +++ b/app/scripts/lib/account-tracker.js @@ -0,0 +1,96 @@ +/* Account Tracker + * + * This module is responsible for tracking any number of accounts + * and caching their current balances & transaction counts. + * + * It also tracks transaction hashes, and checks their inclusion status + * on each new block. + */ + +const async = require('async') +const EthQuery = require('eth-query') +const ObservableStore = require('obs-store') +function noop () {} + + +class EthereumStore extends ObservableStore { + + constructor (opts = {}) { + super({ + accounts: {}, + }) + this._provider = opts.provider + this._query = new EthQuery(this._provider) + this._blockTracker = opts.blockTracker + // subscribe to latest block + this._blockTracker.on('block', this._updateForBlock.bind(this)) + // blockTracker.currentBlock may be null + this._currentBlockNumber = this._blockTracker.currentBlock + } + + // + // public + // + + addAccount (address) { + const accounts = this.getState().accounts + accounts[address] = {} + this.updateState({ accounts }) + if (!this._currentBlockNumber) return + this._updateAccount(address) + } + + removeAccount (address) { + const accounts = this.getState().accounts + delete accounts[address] + this.updateState({ accounts }) + } + + // + // private + // + + _updateForBlock (block) { + const blockNumber = '0x' + block.number.toString('hex') + this._currentBlockNumber = blockNumber + + async.parallel([ + this._updateAccounts.bind(this), + ], (err) => { + if (err) return console.error(err) + this.emit('block', this.getState()) + }) + } + + _updateAccounts (cb = noop) { + const accounts = this.getState().accounts + const addresses = Object.keys(accounts) + async.each(addresses, this._updateAccount.bind(this), cb) + } + + _updateAccount (address, cb = noop) { + const accounts = this.getState().accounts + this._getAccount(address, (err, result) => { + if (err) return cb(err) + result.address = address + // only populate if the entry is still present + if (accounts[address]) { + accounts[address] = result + this.updateState({ accounts }) + } + cb(null, result) + }) + } + + _getAccount (address, cb = noop) { + const query = this._query + async.parallel({ + balance: query.getBalance.bind(query, address), + nonce: query.getTransactionCount.bind(query, address), + code: query.getCode.bind(query, address), + }, cb) + } + +} + +module.exports = EthereumStore diff --git a/app/scripts/lib/eth-store.js b/app/scripts/lib/eth-store.js deleted file mode 100644 index ff22eca4a..000000000 --- a/app/scripts/lib/eth-store.js +++ /dev/null @@ -1,96 +0,0 @@ -/* Ethereum Store - * - * This module is responsible for tracking any number of accounts - * and caching their current balances & transaction counts. - * - * It also tracks transaction hashes, and checks their inclusion status - * on each new block. - */ - -const async = require('async') -const EthQuery = require('eth-query') -const ObservableStore = require('obs-store') -function noop () {} - - -class EthereumStore extends ObservableStore { - - constructor (opts = {}) { - super({ - accounts: {}, - }) - this._provider = opts.provider - this._query = new EthQuery(this._provider) - this._blockTracker = opts.blockTracker - // subscribe to latest block - this._blockTracker.on('block', this._updateForBlock.bind(this)) - // blockTracker.currentBlock may be null - this._currentBlockNumber = this._blockTracker.currentBlock - } - - // - // public - // - - addAccount (address) { - const accounts = this.getState().accounts - accounts[address] = {} - this.updateState({ accounts }) - if (!this._currentBlockNumber) return - this._updateAccount(address) - } - - removeAccount (address) { - const accounts = this.getState().accounts - delete accounts[address] - this.updateState({ accounts }) - } - - // - // private - // - - _updateForBlock (block) { - const blockNumber = '0x' + block.number.toString('hex') - this._currentBlockNumber = blockNumber - - async.parallel([ - this._updateAccounts.bind(this), - ], (err) => { - if (err) return console.error(err) - this.emit('block', this.getState()) - }) - } - - _updateAccounts (cb = noop) { - const accounts = this.getState().accounts - const addresses = Object.keys(accounts) - async.each(addresses, this._updateAccount.bind(this), cb) - } - - _updateAccount (address, cb = noop) { - const accounts = this.getState().accounts - this._getAccount(address, (err, result) => { - if (err) return cb(err) - result.address = address - // only populate if the entry is still present - if (accounts[address]) { - accounts[address] = result - this.updateState({ accounts }) - } - cb(null, result) - }) - } - - _getAccount (address, cb = noop) { - const query = this._query - async.parallel({ - balance: query.getBalance.bind(query, address), - nonce: query.getTransactionCount.bind(query, address), - code: query.getCode.bind(query, address), - }, cb) - } - -} - -module.exports = EthereumStore diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 02c06ead2..b1cfe1a2d 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -4,7 +4,7 @@ const promiseToCallback = require('promise-to-callback') const pipe = require('pump') const Dnode = require('dnode') const ObservableStore = require('obs-store') -const EthStore = require('./lib/eth-store') +const AccountTracker = require('./lib/account-tracker') const EthQuery = require('eth-query') const streamIntoProvider = require('web3-stream-provider/handler') const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex @@ -81,19 +81,25 @@ module.exports = class MetamaskController extends EventEmitter { // eth data query tools this.ethQuery = new EthQuery(this.provider) - this.ethStore = new EthStore({ - provider: this.provider, - blockTracker: this.provider, - }) // key mgmt this.keyringController = new KeyringController({ initState: initState.KeyringController, - ethStore: this.ethStore, + accountTracker: this.accountTracker, getNetwork: this.networkController.getNetworkState.bind(this.networkController), }) + + // account tracker watches balances, nonces, and any code at their address. + this.accountTracker = new AccountTracker({ + provider: this.provider, + blockTracker: this.provider, + }) this.keyringController.on('newAccount', (address) => { this.preferencesController.setSelectedAddress(address) + this.accountTracker.addAccount(address) + }) + this.keyringController.on('removedAccount', (address) => { + this.accountTracker.removeAccount(address) }) // address book controller @@ -112,13 +118,13 @@ module.exports = class MetamaskController extends EventEmitter { provider: this.provider, blockTracker: this.provider, ethQuery: this.ethQuery, - ethStore: this.ethStore, + accountTracker: this.accountTracker, }) this.txController.on('newUnaprovedTx', opts.showUnapprovedTx.bind(opts)) // computed balances (accounting for pending transactions) this.balancesController = new BalancesController({ - ethStore: this.ethStore, + accountTracker: this.accountTracker, txController: this.txController, }) this.networkController.on('networkDidChange', () => { @@ -177,7 +183,7 @@ module.exports = class MetamaskController extends EventEmitter { // manual mem state subscriptions this.networkController.store.subscribe(this.sendUpdate.bind(this)) - this.ethStore.subscribe(this.sendUpdate.bind(this)) + this.accountTracker.subscribe(this.sendUpdate.bind(this)) this.txController.memStore.subscribe(this.sendUpdate.bind(this)) this.balancesController.store.subscribe(this.sendUpdate.bind(this)) this.messageManager.memStore.subscribe(this.sendUpdate.bind(this)) @@ -260,7 +266,7 @@ module.exports = class MetamaskController extends EventEmitter { isInitialized, }, this.networkController.store.getState(), - this.ethStore.getState(), + this.accountTracker.getState(), this.txController.memStore.getState(), this.messageManager.memStore.getState(), this.personalMessageManager.memStore.getState(), -- cgit v1.2.3 From d2a747e57e591af5beb2e7cef7fc73a363d9c742 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 22 Sep 2017 14:06:54 -0700 Subject: Fix computed-balances controller reference --- app/scripts/metamask-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b1cfe1a2d..34d60d253 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -20,7 +20,7 @@ const BlacklistController = require('./controllers/blacklist') const MessageManager = require('./lib/message-manager') const PersonalMessageManager = require('./lib/personal-message-manager') const TransactionController = require('./controllers/transactions') -const BalancesController = require('./controllers/balances') +const BalancesController = require('./controllers/computed-balances') const ConfigManager = require('./lib/config-manager') const nodeify = require('./lib/nodeify') const accountImporter = require('./account-import-strategies') -- cgit v1.2.3 From f01b0a818ba67c2549f14382056534768a255e5b Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 22 Sep 2017 14:13:56 -0700 Subject: Fix account-tracker references --- app/scripts/controllers/balance.js | 6 +++--- app/scripts/controllers/computed-balances.js | 10 +++++----- app/scripts/keyring-controller.js | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/scripts/controllers/balance.js b/app/scripts/controllers/balance.js index b4e72e751..ddeb06cf9 100644 --- a/app/scripts/controllers/balance.js +++ b/app/scripts/controllers/balance.js @@ -5,9 +5,9 @@ const BN = require('ethereumjs-util').BN class BalanceController { constructor (opts = {}) { - const { address, ethStore, txController } = opts + const { address, accountTracker, txController } = opts this.address = address - this.ethStore = ethStore + this.accountTracker = accountTracker this.txController = txController const initState = { @@ -39,7 +39,7 @@ class BalanceController { } _getBalance () { - const store = this.ethStore.getState() + const store = this.accountTracker.getState() const balances = store.accounts const entry = balances[this.address] const balance = entry.balance diff --git a/app/scripts/controllers/computed-balances.js b/app/scripts/controllers/computed-balances.js index a85eb5590..576746164 100644 --- a/app/scripts/controllers/computed-balances.js +++ b/app/scripts/controllers/computed-balances.js @@ -5,8 +5,8 @@ const BalanceController = require('./balance') class ComputedbalancesController { constructor (opts = {}) { - const { ethStore, txController } = opts - this.ethStore = ethStore + const { accountTracker, txController } = opts + this.accountTracker = accountTracker this.txController = txController const initState = extend({ @@ -25,9 +25,9 @@ class ComputedbalancesController { } _initBalanceUpdating () { - const store = this.ethStore.getState() + const store = this.accountTracker.getState() this.addAnyAccountsFromStore(store) - this.ethStore.subscribe(this.addAnyAccountsFromStore.bind(this)) + this.accountTracker.subscribe(this.addAnyAccountsFromStore.bind(this)) } addAnyAccountsFromStore(store) { @@ -48,7 +48,7 @@ class ComputedbalancesController { trackAddress (address) { let updater = new BalanceController({ address, - ethStore: this.ethStore, + accountTracker: this.accountTracker, txController: this.txController, }) updater.store.subscribe((accountBalance) => { diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js index fd57fac70..fa470dd89 100644 --- a/app/scripts/keyring-controller.js +++ b/app/scripts/keyring-controller.js @@ -35,7 +35,7 @@ class KeyringController extends EventEmitter { keyrings: [], identities: {}, }) - this.ethStore = opts.ethStore + this.accountTracker = opts.accountTracker this.encryptor = encryptor this.keyrings = [] this.getNetwork = opts.getNetwork @@ -338,7 +338,7 @@ class KeyringController extends EventEmitter { // // Initializes the provided account array // Gives them numerically incremented nicknames, - // and adds them to the ethStore for regular balance checking. + // and adds them to the accountTracker for regular balance checking. setupAccounts (accounts) { return this.getAccounts() .then((loadedAccounts) => { @@ -361,7 +361,7 @@ class KeyringController extends EventEmitter { throw new Error('Problem loading account.') } const address = normalizeAddress(account) - this.ethStore.addAccount(address) + this.accountTracker.addAccount(address) return this.createNickname(address) } @@ -567,12 +567,12 @@ class KeyringController extends EventEmitter { clearKeyrings () { let accounts try { - accounts = Object.keys(this.ethStore.getState()) + accounts = Object.keys(this.accountTracker.getState()) } catch (e) { accounts = [] } accounts.forEach((address) => { - this.ethStore.removeAccount(address) + this.accountTracker.removeAccount(address) }) // clear keyrings from memory -- cgit v1.2.3 From 128cf40f91df6d78e2d5ca87608fe16e91a510fa Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 22 Sep 2017 14:16:19 -0700 Subject: Fix accont-tracker merge bug --- app/scripts/metamask-controller.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 2fc5b4204..1dd09d0ad 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -86,6 +86,10 @@ module.exports = class MetamaskController extends EventEmitter { // eth data query tools this.ethQuery = new EthQuery(this.provider) + this.accountTracker = new AccountTracker({ + provider: this.provider, + blockTracker: this.blockTracker, + }) // key mgmt this.keyringController = new KeyringController({ -- cgit v1.2.3 From f128240e7f877280fa59bf22f2ea8285bb467022 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 22 Sep 2017 14:19:14 -0700 Subject: Fix test references --- test/unit/keyring-controller-test.js | 2 +- test/unit/tx-controller-test.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unit/keyring-controller-test.js b/test/unit/keyring-controller-test.js index 2d9a53723..34c314639 100644 --- a/test/unit/keyring-controller-test.js +++ b/test/unit/keyring-controller-test.js @@ -24,7 +24,7 @@ describe('KeyringController', function () { getTxList: () => [], getUnapprovedTxList: () => [], }, - ethStore: { + accountTracker: { addAccount (acct) { accounts.push(ethUtil.addHexPrefix(acct)) }, }, }) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index 7bb193242..7b875db66 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -27,7 +27,7 @@ describe('Transaction Controller', function () { networkStore: new ObservableStore(currentNetworkId), txHistoryLimit: 10, blockTracker: { getCurrentBlock: noop, on: noop, once: noop }, - ethStore: { getState: noop }, + accountTracker: { getState: noop }, signTransaction: (ethTx) => new Promise((resolve) => { ethTx.sign(privKey) resolve() @@ -431,4 +431,4 @@ describe('Transaction Controller', function () { }).catch(done) }) }) -}) \ No newline at end of file +}) -- cgit v1.2.3 From 15195bca75f84859b4d4efce9b8155504750c99d Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 22 Sep 2017 14:20:44 -0700 Subject: deps - bump provider engine for block tracker --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a9268060d..38ed28eef 100644 --- a/package.json +++ b/package.json @@ -139,7 +139,7 @@ "valid-url": "^1.0.9", "vreme": "^3.0.2", "web3": "^0.20.1", - "web3-provider-engine": "^13.2.9", + "web3-provider-engine": "^13.2.10", "web3-stream-provider": "^3.0.1", "xtend": "^4.0.1" }, -- cgit v1.2.3 From dd45592641500504d0d804316e6625e5b89718f9 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 22 Sep 2017 14:22:07 -0700 Subject: metamask - use provider-engines block tracker --- app/scripts/metamask-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index fef16c3a9..53fb27476 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -81,7 +81,7 @@ module.exports = class MetamaskController extends EventEmitter { // rpc provider this.provider = this.initializeProvider() - this.blockTracker = this.provider + this.blockTracker = this.provider._blockTracker // eth data query tools this.ethQuery = new EthQuery(this.provider) -- cgit v1.2.3 From 48867d95fe1a064683f96ad60fd36c893500f62c Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 19 Sep 2017 22:46:51 -0230 Subject: ReadOnlyInput component. --- ui/app/components/qr-code.js | 18 ++++++------------ ui/app/components/readonly-input.js | 27 +++++++++++++++++++++++++++ ui/app/css/itcss/components/modal.scss | 2 +- 3 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 ui/app/components/readonly-input.js diff --git a/ui/app/components/qr-code.js b/ui/app/components/qr-code.js index dca5c8c47..cc723df14 100644 --- a/ui/app/components/qr-code.js +++ b/ui/app/components/qr-code.js @@ -4,6 +4,7 @@ const qrCode = require('qrcode-npm').qrcode const inherits = require('util').inherits const connect = require('react-redux').connect const isHexPrefixed = require('ethereumjs-util').isHexPrefixed +const ReadOnlyInput = require('./readonly-input') module.exports = connect(mapStateToProps)(QrCodeView) @@ -46,18 +47,11 @@ QrCodeView.prototype.render = function () { __html: qrImage.createTableTag(4), }, }), - h('.div.ellip-address-wrapper', [ - h('input.qr-ellip-address', { - style: { - width: '247px', - }, - value: Qr.data, - readOnly: true, - }), - // h(CopyButton, { - // value: Qr.data, - // }), - ]), + h(ReadOnlyInput, { + wrapperClass: 'ellip-address-wrapper', + inputClass: 'qr-ellip-address', + value: Qr.data, + }), ]) } diff --git a/ui/app/components/readonly-input.js b/ui/app/components/readonly-input.js new file mode 100644 index 000000000..b688e0722 --- /dev/null +++ b/ui/app/components/readonly-input.js @@ -0,0 +1,27 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = ReadOnlyInput + +inherits(ReadOnlyInput, Component) +function ReadOnlyInput () { + Component.call(this) +} + +ReadOnlyInput.prototype.render = function () { + const { + wrapperClass, + inputClass, + value, + } = this.props + + return h('div', {className: wrapperClass}, [ + h('input', { + className: inputClass, + value, + readOnly: true, + }), + ]) +} + diff --git a/ui/app/css/itcss/components/modal.scss b/ui/app/css/itcss/components/modal.scss index 9f6ce54f5..5e3f9cc08 100644 --- a/ui/app/css/itcss/components/modal.scss +++ b/ui/app/css/itcss/components/modal.scss @@ -234,7 +234,7 @@ } .account-modal-container .qr-ellip-address { - width: 254px; + width: 247px; border: none; font-family: 'Montserrat Light'; font-size: 14px; -- cgit v1.2.3 From e325e5e2f5f9d09ade29bc86ec160b6da0eb6b71 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 22 Sep 2017 17:31:32 -0230 Subject: Default class params to empty string in readonly-input --- ui/app/components/readonly-input.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/app/components/readonly-input.js b/ui/app/components/readonly-input.js index b688e0722..934cbefae 100644 --- a/ui/app/components/readonly-input.js +++ b/ui/app/components/readonly-input.js @@ -11,8 +11,8 @@ function ReadOnlyInput () { ReadOnlyInput.prototype.render = function () { const { - wrapperClass, - inputClass, + wrapperClass = '', + inputClass = '', value, } = this.props -- cgit v1.2.3 From a1d72a59fe5b03363820d6e1ac2c383ec15ccbcb Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 22 Sep 2017 04:29:27 -0230 Subject: New account modal now allows for addition of account name. --- ui/app/actions.js | 20 ++++++++++++++++++-- ui/app/components/modals/new-account-modal.js | 20 ++++++++++++++++---- ui/app/css/itcss/components/modal.scss | 2 +- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index 1231fc296..a6730db7f 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -349,9 +349,25 @@ function navigateToNewAccountScreen () { } } -function addNewAccount () { +function addNewAccount (oldIdentities) { log.debug(`background.addNewAccount`) - return callBackgroundThenUpdate(background.addNewAccount) + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + return new Promise((resolve, reject) => { + background.addNewAccount((err, { identities: newIdentities}) => { + if (err) { + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + const newAccountAddress = Object.keys(newIdentities).find(address => !oldIdentities[address]) + + dispatch(actions.hideLoadingIndication()) + + forceUpdateMetamaskState(dispatch) + return resolve(newAccountAddress) + }) + }); + } } function showInfoPage () { diff --git a/ui/app/components/modals/new-account-modal.js b/ui/app/components/modals/new-account-modal.js index 910f3c0ca..1adc9e7c7 100644 --- a/ui/app/components/modals/new-account-modal.js +++ b/ui/app/components/modals/new-account-modal.js @@ -8,6 +8,7 @@ function mapStateToProps (state) { return { network: state.metamask.network, address: state.metamask.selectedAddress, + identities: state.metamask.identities, } } @@ -19,9 +20,12 @@ function mapDispatchToProps (dispatch) { hideModal: () => { dispatch(actions.hideModal()) }, - createAccount: () => { - dispatch(actions.addNewAccount()) - dispatch(actions.hideModal()) + createAccount: (identities, newAccountName) => { + dispatch(actions.addNewAccount(identities)) + .then((newAccountAddress) => { + dispatch(actions.saveAccountLabel(newAccountAddress, newAccountName)) + dispatch(actions.hideModal()) + }) }, } } @@ -29,11 +33,18 @@ function mapDispatchToProps (dispatch) { inherits(NewAccountModal, Component) function NewAccountModal () { Component.call(this) + + this.state = { + newAccountName: '' + } } module.exports = connect(mapStateToProps, mapDispatchToProps)(NewAccountModal) NewAccountModal.prototype.render = function () { + const { identities } = this.props + const { newAccountName } = this.state + return h('div', {}, [ h('div.new-account-modal-wrapper', { }, [ @@ -52,6 +63,7 @@ NewAccountModal.prototype.render = function () { h('div.new-account-input-wrapper', {}, [ h('input.new-account-input', { placeholder: 'E.g. My new account', + onChange: (event) => this.setState({ newAccountName: event.target.value }) }, []), ]), @@ -65,7 +77,7 @@ NewAccountModal.prototype.render = function () { h('div.new-account-modal-content.button', {}, [ h('button.btn-clear', { - onClick: this.props.createAccount + onClick: () => this.props.createAccount(identities, newAccountName) }, [ 'SAVE', ]), diff --git a/ui/app/css/itcss/components/modal.scss b/ui/app/css/itcss/components/modal.scss index 5e3f9cc08..c0a5aa1ef 100644 --- a/ui/app/css/itcss/components/modal.scss +++ b/ui/app/css/itcss/components/modal.scss @@ -325,7 +325,7 @@ border: 1px solid $alto; width: 100%; font-size: 1em; - color: $alto; + color: $dusty-gray; font-family: Montserrat Light; font-size: 17px; margin: 0 60px; -- cgit v1.2.3 From 13f22ff6b087f3865f84a0672a9013ada88be61a Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 22 Sep 2017 18:30:00 -0230 Subject: get identities from getState() instead of passing from caller, only set new account label if label set. --- ui/app/actions.js | 5 +++-- ui/app/components/modals/new-account-modal.js | 12 ++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index a6730db7f..0a2b4a636 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -349,9 +349,10 @@ function navigateToNewAccountScreen () { } } -function addNewAccount (oldIdentities) { +function addNewAccount () { log.debug(`background.addNewAccount`) - return (dispatch) => { + return (dispatch, getState) => { + const oldIdentities = getState().metamask.identities dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { background.addNewAccount((err, { identities: newIdentities}) => { diff --git a/ui/app/components/modals/new-account-modal.js b/ui/app/components/modals/new-account-modal.js index 1adc9e7c7..25beb6745 100644 --- a/ui/app/components/modals/new-account-modal.js +++ b/ui/app/components/modals/new-account-modal.js @@ -8,7 +8,6 @@ function mapStateToProps (state) { return { network: state.metamask.network, address: state.metamask.selectedAddress, - identities: state.metamask.identities, } } @@ -20,10 +19,12 @@ function mapDispatchToProps (dispatch) { hideModal: () => { dispatch(actions.hideModal()) }, - createAccount: (identities, newAccountName) => { - dispatch(actions.addNewAccount(identities)) + createAccount: (newAccountName) => { + dispatch(actions.addNewAccount()) .then((newAccountAddress) => { - dispatch(actions.saveAccountLabel(newAccountAddress, newAccountName)) + if (newAccountName) { + dispatch(actions.saveAccountLabel(newAccountAddress, newAccountName)) + } dispatch(actions.hideModal()) }) }, @@ -42,7 +43,6 @@ function NewAccountModal () { module.exports = connect(mapStateToProps, mapDispatchToProps)(NewAccountModal) NewAccountModal.prototype.render = function () { - const { identities } = this.props const { newAccountName } = this.state return h('div', {}, [ @@ -77,7 +77,7 @@ NewAccountModal.prototype.render = function () { h('div.new-account-modal-content.button', {}, [ h('button.btn-clear', { - onClick: () => this.props.createAccount(identities, newAccountName) + onClick: () => this.props.createAccount(newAccountName) }, [ 'SAVE', ]), -- cgit v1.2.3 From e1077836ce916e2bd788451e3f365324024a1c0c Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Fri, 22 Sep 2017 14:34:56 -0700 Subject: Add Confirm Send token screen --- ui/app/components/pending-tx/confirm-send-ether.js | 2 +- ui/app/components/pending-tx/confirm-send-token.js | 394 +++++++++++++++++++++ ui/app/components/pending-tx/index.js | 73 +++- ui/app/components/send-token/index.js | 18 +- ui/app/css/itcss/components/send.scss | 6 + ui/app/reducers/app.js | 60 ++-- 6 files changed, 489 insertions(+), 64 deletions(-) create mode 100644 ui/app/components/pending-tx/confirm-send-token.js diff --git a/ui/app/components/pending-tx/confirm-send-ether.js b/ui/app/components/pending-tx/confirm-send-ether.js index 29c6d349c..b03ec0552 100644 --- a/ui/app/components/pending-tx/confirm-send-ether.js +++ b/ui/app/components/pending-tx/confirm-send-ether.js @@ -49,7 +49,7 @@ ConfirmSendEther.prototype.getAmount = function () { const { conversionRate } = this.props const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} - + console.log(txParams) const USD = conversionUtil(txParams.value, { fromNumericBase: 'hex', toNumericBase: 'dec', diff --git a/ui/app/components/pending-tx/confirm-send-token.js b/ui/app/components/pending-tx/confirm-send-token.js new file mode 100644 index 000000000..384ac92cc --- /dev/null +++ b/ui/app/components/pending-tx/confirm-send-token.js @@ -0,0 +1,394 @@ +const Component = require('react').Component +const { connect } = require('react-redux') +const h = require('react-hyperscript') +const inherits = require('util').inherits +const abi = require('human-standard-token-abi') +const abiDecoder = require('abi-decoder') +abiDecoder.addABI(abi) +const actions = require('../../actions') +const clone = require('clone') +const Identicon = require('../identicon') +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const hexToBn = require('../../../../app/scripts/lib/hex-to-bn') +const { conversionUtil } = require('../../conversion-util') + +const MIN_GAS_PRICE_GWEI_BN = new BN(1) +const GWEI_FACTOR = new BN(1e9) +const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) + +module.exports = connect(mapStateToProps, mapDispatchToProps)(ConfirmSendToken) + +function mapStateToProps (state, ownProps) { + const { token: { symbol }, txData } = ownProps + const { txParams } = txData || {} + const tokenData = txParams.data && abiDecoder.decodeMethod(txParams.data) + const { + conversionRate, + identities, + } = state.metamask + const accounts = state.metamask.accounts + const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] + const tokenExchangeRates = state.metamask.tokenExchangeRates + const pair = `${symbol.toLowerCase()}_eth` + const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {} + + return { + conversionRate, + identities, + selectedAddress, + tokenExchangeRate, + tokenData: tokenData || {}, + } +} + +function mapDispatchToProps (dispatch, ownProps) { + const { token: { symbol } } = ownProps + + return { + backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)), + cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })), + updateTokenExchangeRate: () => dispatch(actions.updateTokenExchangeRate(symbol)), + } +} + +inherits(ConfirmSendToken, Component) +function ConfirmSendToken () { + Component.call(this) + this.state = {} + this.onSubmit = this.onSubmit.bind(this) +} + +ConfirmSendToken.prototype.componentWillMount = function () { + this.props.updateTokenExchangeRate() +} + +ConfirmSendToken.prototype.getAmount = function () { + const { conversionRate, tokenExchangeRate, token, tokenData } = this.props + const { params = [] } = tokenData + const { value } = params[1] || {} + const { decimals } = token + const multiplier = Math.pow(10, Number(decimals || 0)) + const sendTokenAmount = Number(value / multiplier) + + return { + fiat: tokenExchangeRate + ? +(sendTokenAmount * tokenExchangeRate * conversionRate).toFixed(2) + : null, + token: +sendTokenAmount.toFixed(decimals), + } + +} + +ConfirmSendToken.prototype.getGasFee = function () { + const { conversionRate, tokenExchangeRate, token } = this.props + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} + const { decimals } = token + + // Gas + const gas = txParams.gas + const gasBn = hexToBn(gas) + + // Gas Price + const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) + const gasPriceBn = hexToBn(gasPrice) + const txFeeBn = gasBn.mul(gasPriceBn) + + + const USD = conversionUtil(txFeeBn, { + fromNumericBase: 'BN', + toNumericBase: 'dec', + fromDenomination: 'WEI', + fromCurrency: 'ETH', + toCurrency: 'USD', + numberOfDecimals: 2, + conversionRate, + }) + const ETH = conversionUtil(txFeeBn, { + fromNumericBase: 'BN', + toNumericBase: 'dec', + fromDenomination: 'WEI', + fromCurrency: 'ETH', + toCurrency: 'ETH', + numberOfDecimals: 6, + conversionRate, + }) + + return { + fiat: +Number(USD).toFixed(2), + eth: ETH, + token: tokenExchangeRate + ? +(ETH * tokenExchangeRate).toFixed(decimals) + : null, + } +} + +ConfirmSendToken.prototype.getData = function () { + const { identities } = this.props + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} + + return { + from: { + address: txParams.from, + name: identities[txParams.from].name, + }, + to: { + address: txParams.to, + name: identities[txParams.to] ? identities[txParams.to].name : 'New Recipient', + }, + memo: txParams.memo || '', + } +} + +ConfirmSendToken.prototype.renderHeroAmount = function () { + const { token: { symbol } } = this.props + const { fiat: fiatAmount, token: tokenAmount } = this.getAmount() + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} + const { memo = '' } = txParams + + return fiatAmount + ? ( + h('div.confirm-send-token__hero-amount-wrapper', [ + h('h3.flex-center.confirm-screen-send-amount', `$${fiatAmount}`), + h('h3.flex-center.confirm-screen-send-amount-currency', 'USD'), + h('div.flex-center.confirm-memo-wrapper', [ + h('h3.confirm-screen-send-memo', memo), + ]), + ]) + ) + : ( + h('div.confirm-send-token__hero-amount-wrapper', [ + h('h3.flex-center.confirm-screen-send-amount', tokenAmount), + h('h3.flex-center.confirm-screen-send-amount-currency', symbol), + h('div.flex-center.confirm-memo-wrapper', [ + h('h3.confirm-screen-send-memo', memo), + ]), + ]) + ) +} + +ConfirmSendToken.prototype.renderGasFee = function () { + const { token: { symbol } } = this.props + const { fiat: fiatGas, token: tokenGas, eth: ethGas } = this.getGasFee() + + return ( + h('section.flex-row.flex-center.confirm-screen-row', [ + h('span.confirm-screen-label.confirm-screen-section-column', [ 'Gas Fee' ]), + h('div.confirm-screen-section-column', [ + h('div.confirm-screen-row-info', `$${fiatGas} USD`), + + h( + 'div.confirm-screen-row-detail', + tokenGas ? `${tokenGas} ${symbol}` : `${ethGas} ETH` + ), + ]), + ]) + ) +} + +ConfirmSendToken.prototype.renderTotalPlusGas = function () { + const { token: { symbol } } = this.props + const { fiat: fiatAmount, token: tokenAmount } = this.getAmount() + const { fiat: fiatGas, token: tokenGas } = this.getGasFee() + + return fiatAmount && fiatGas + ? ( + h('section.flex-row.flex-center.confirm-screen-total-box ', [ + h('div.confirm-screen-section-column', [ + h('span.confirm-screen-label', [ 'Total ' ]), + h('div.confirm-screen-total-box__subtitle', [ 'Amount + Gas' ]), + ]), + + h('div.confirm-screen-section-column', [ + h('div.confirm-screen-row-info', `$${fiatAmount + fiatGas} USD`), + h('div.confirm-screen-row-detail', `${tokenAmount + tokenGas} ${symbol}`), + ]), + ]) + ) + : ( + h('section.flex-row.flex-center.confirm-screen-total-box ', [ + h('div.confirm-screen-section-column', [ + h('span.confirm-screen-label', [ 'Total ' ]), + h('div.confirm-screen-total-box__subtitle', [ 'Amount + Gas' ]), + ]), + + h('div.confirm-screen-section-column', [ + h('div.confirm-screen-row-info', `${tokenAmount} ${symbol}`), + h('div.confirm-screen-row-detail', `+ ${fiatGas} USD Gas`), + ]), + ]) + ) +} + +ConfirmSendToken.prototype.render = function () { + const { backToAccountDetail, selectedAddress } = this.props + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} + + const { + from: { + address: fromAddress, + name: fromName, + }, + to: { + address: toAddress, + name: toName, + }, + } = this.getData() + + this.inputs = [] + + return ( + h('div.flex-column.flex-grow.confirm-screen-container', { + style: { minWidth: '355px' }, + }, [ + // Main Send token Card + h('div.confirm-screen-wrapper.flex-column.flex-grow', [ + h('h3.flex-center.confirm-screen-header', [ + h('button.confirm-screen-back-button', { + onClick: () => backToAccountDetail(selectedAddress), + }, 'BACK'), + h('div.confirm-screen-title', 'Confirm Transaction'), + ]), + h('div.flex-row.flex-center.confirm-screen-identicons', [ + h('div.confirm-screen-account-wrapper', [ + h( + Identicon, + { + address: fromAddress, + diameter: 100, + }, + ), + h('span.confirm-screen-account-name', fromName), + h('span.confirm-screen-account-number', fromAddress.slice(fromAddress.length - 4)), + ]), + h('i.fa.fa-arrow-right.fa-lg'), + h('div.confirm-screen-account-wrapper', [ + h( + Identicon, + { + address: txParams.to, + diameter: 100, + }, + ), + h('span.confirm-screen-account-name', toName), + h('span.confirm-screen-account-number', toAddress.slice(toAddress.length - 4)), + ]), + ]), + + h('h3.flex-center.confirm-screen-sending-to-message', { + style: { + textAlign: 'center', + fontSize: '16px', + }, + }, [ + `You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`, + ]), + + this.renderHeroAmount(), + + h('div.confirm-screen-rows', [ + h('section.flex-row.flex-center.confirm-screen-row', [ + h('span.confirm-screen-label.confirm-screen-section-column', [ 'From' ]), + h('div.confirm-screen-section-column', [ + h('div.confirm-screen-row-info', fromName), + h('div.confirm-screen-row-detail', `...${fromAddress.slice(fromAddress.length - 4)}`), + ]), + ]), + + h('section.flex-row.flex-center.confirm-screen-row', [ + h('span.confirm-screen-label.confirm-screen-section-column', [ 'To' ]), + h('div.confirm-screen-section-column', [ + h('div.confirm-screen-row-info', toName), + h('div.confirm-screen-row-detail', `...${toAddress.slice(toAddress.length - 4)}`), + ]), + ]), + + this.renderGasFee(), + + this.renderTotalPlusGas(), + + ]), + ]), + + h('form#pending-tx-form.flex-column.flex-center', { + onSubmit: this.onSubmit, + }, [ + + // Accept Button + h('button.confirm-screen-confirm-button', ['CONFIRM']), + + // Cancel Button + h('div.cancel.btn-light.confirm-screen-cancel-button', { + onClick: (event) => this.cancel(event, txMeta), + }, 'CANCEL'), + ]), + ]) + ) +} + +ConfirmSendToken.prototype.onSubmit = function (event) { + event.preventDefault() + const txMeta = this.gatherTxMeta() + const valid = this.checkValidity() + this.setState({ valid, submitting: true }) + + if (valid && this.verifyGasParams()) { + this.props.sendTransaction(txMeta, event) + } else { + this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) + this.setState({ submitting: false }) + } +} + +ConfirmSendToken.prototype.cancel = function (event, txMeta) { + event.preventDefault() + this.props.cancelTransaction(txMeta) +} + +ConfirmSendToken.prototype.checkValidity = function () { + const form = this.getFormEl() + const valid = form.checkValidity() + return valid +} + +ConfirmSendToken.prototype.getFormEl = function () { + const form = document.querySelector('form#pending-tx-form') + // Stub out form for unit tests: + if (!form) { + return { checkValidity () { return true } } + } + return form +} + +// After a customizable state value has been updated, +ConfirmSendToken.prototype.gatherTxMeta = function () { + const props = this.props + const state = this.state + const txData = clone(state.txData) || clone(props.txData) + + // log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) + return txData +} + +ConfirmSendToken.prototype.verifyGasParams = function () { + // We call this in case the gas has not been modified at all + if (!this.state) { return true } + return ( + this._notZeroOrEmptyString(this.state.gas) && + this._notZeroOrEmptyString(this.state.gasPrice) + ) +} + +ConfirmSendToken.prototype._notZeroOrEmptyString = function (obj) { + return obj !== '' && obj !== '0x0' +} + +ConfirmSendToken.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) { + const numBN = new BN(numerator) + const denomBN = new BN(denominator) + return targetBN.mul(numBN).div(denomBN) +} diff --git a/ui/app/components/pending-tx/index.js b/ui/app/components/pending-tx/index.js index 3797b5642..915319958 100644 --- a/ui/app/components/pending-tx/index.js +++ b/ui/app/components/pending-tx/index.js @@ -9,6 +9,7 @@ const inherits = require('util').inherits const actions = require('../../actions') const util = require('../../util') const ConfirmSendEther = require('./confirm-send-ether') +const ConfirmSendToken = require('./confirm-send-token') const TX_TYPES = { DEPLOY_CONTRACT: 'deploy_contract', @@ -46,33 +47,51 @@ function PendingTx () { this.state = { isFetching: true, transactionType: '', + tokenAddress: '', + tokenSymbol: '', + tokenDecimals: '', } } -PendingTx.prototype.componentWillMount = function () { +PendingTx.prototype.componentWillMount = async function () { const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} this.props.setCurrentCurrencyToUSD() - if (txParams.to) { + if (!txParams.to) { + return this.setState({ + transactionType: TX_TYPES.DEPLOY_CONTRACT, + isFetching: false, + }) + } + + try { const token = util.getContractAtAddress(txParams.to) - token - .symbol() - .then(result => { - const symbol = result[0] || null + const results = await Promise.all([ + token.symbol(), + token.decimals(), + ]) + + const [ symbol, decimals ] = results + + if (symbol[0] && decimals[0]) { this.setState({ - transactionType: symbol ? TX_TYPES.SEND_TOKEN : TX_TYPES.SEND_ETHER, + transactionType: TX_TYPES.SEND_TOKEN, + tokenAddress: txParams.to, + tokenSymbol: symbol[0], + tokenDecimals: decimals[0], isFetching: false, }) - }) - .catch(() => this.setState({ - transactionType: TX_TYPES.SEND_ETHER, - isFetching: false, - })) - } else { + } else { + this.setState({ + transactionType: TX_TYPES.SEND_ETHER, + isFetching: false, + }) + } + } catch (e) { this.setState({ - transactionType: TX_TYPES.DEPLOY_CONTRACT, + transactionType: TX_TYPES.SEND_ETHER, isFetching: false, }) } @@ -87,16 +106,36 @@ PendingTx.prototype.gatherTxMeta = function () { } PendingTx.prototype.render = function () { - const { isFetching, transactionType } = this.state + const { + isFetching, + transactionType, + tokenAddress, + tokenSymbol, + tokenDecimals, + } = this.state + + const { sendTransaction } = this.props if (isFetching) { return h('noscript') } - switch (transactionType) { case TX_TYPES.SEND_ETHER: - return h(ConfirmSendEther, { txData: this.gatherTxMeta() }) + return h(ConfirmSendEther, { + txData: this.gatherTxMeta(), + sendTransaction, + }) + case TX_TYPES.SEND_TOKEN: + return h(ConfirmSendToken, { + txData: this.gatherTxMeta(), + sendTransaction, + token: { + address: tokenAddress, + symbol: tokenSymbol, + decimals: tokenDecimals, + }, + }) default: return h('noscript') } diff --git a/ui/app/components/send-token/index.js b/ui/app/components/send-token/index.js index 7adbf48dc..dd8ca6b9d 100644 --- a/ui/app/components/send-token/index.js +++ b/ui/app/components/send-token/index.js @@ -1,7 +1,6 @@ const Component = require('react').Component const connect = require('react-redux').connect const h = require('react-hyperscript') -const { addHexPrefix } = require('ethereumjs-util') const classnames = require('classnames') const inherits = require('util').inherits const actions = require('../../actions') @@ -26,20 +25,15 @@ function mapStateToProps (state) { const conversionRate = state.metamask.conversionRate const currentBlockGasLimit = state.metamask.currentBlockGasLimit const accounts = state.metamask.accounts - // const network = state.metamask.network const selectedTokenAddress = state.metamask.selectedTokenAddress const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] const selectedToken = selectors.getSelectedToken(state) const tokenExchangeRates = state.metamask.tokenExchangeRates const pair = `${selectedToken.symbol.toLowerCase()}_eth` const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {} - // const checksumAddress = selectedAddress && ethUtil.toChecksumAddress(selectedAddress) - // const identity = identities[selectedAddress] return { - // sidebarOpen, selectedAddress, - // checksumAddress, selectedTokenAddress, identities, addressBook, @@ -48,9 +42,6 @@ function mapStateToProps (state) { currentBlockGasLimit, selectedToken, warning, - // selectedToken: selectors.getSelectedToken(state), - // identity, - // network, } } @@ -66,11 +57,6 @@ function mapDispatchToProps (dispatch) { dispatch(actions.signTokenTx(tokenAddress, toAddress, amount, txData)) ), updateTokenExchangeRate: token => dispatch(actions.updateTokenExchangeRate(token)), - // showSidebar: () => { dispatch(actions.showSidebar()) }, - // hideSidebar: () => { dispatch(actions.hideSidebar()) }, - // showModal: (payload) => { dispatch(actions.showModal(payload)) }, - // showSendPage: () => { dispatch(actions.showSendPage()) }, - // showSendTokenPage: () => { dispatch(actions.showSendTokenPage()) }, } } @@ -116,7 +102,7 @@ SendTokenScreen.prototype.validate = function () { gasLimit: !gasLimit ? 'Gas Limit Required' : null, } - if(to && !isValidAddress(to)) { + if (to && !isValidAddress(to)) { errors.to = 'Invalid address' } @@ -360,7 +346,7 @@ SendTokenScreen.prototype.render = function () { this.renderAmountInput(), this.renderGasInput(), this.renderMemoInput(), - warning && h('div.send-screen-input-wrapper--error', {}, + warning && h('div.send-screen-input-wrapper--error', h('div.send-screen-input-wrapper__error-message', [ warning, ]) diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index 84f678130..5691baebe 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -387,3 +387,9 @@ } } } + +.confirm-send-token { + &__hero-amount-wrapper { + width: 100%; + } +} diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index fbabad0ef..c64046518 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -380,36 +380,36 @@ function reduceApp (state, action) { case actions.COMPLETED_TX: log.debug('reducing COMPLETED_TX for tx ' + action.value) - const otherUnconfActions = getUnconfActionList(state) - .filter(tx => tx.id !== action.value) - const hasOtherUnconfActions = otherUnconfActions.length > 0 - - if (hasOtherUnconfActions) { - log.debug('reducer detected txs - rendering confTx view') - return extend(appState, { - transForward: false, - currentView: { - name: 'confTx', - context: 0, - }, - warning: null, - }) - } else { - log.debug('attempting to close popup') - return extend(appState, { - // indicate notification should close - shouldClose: true, - transForward: false, - warning: null, - currentView: { - name: 'accountDetail', - context: state.metamask.selectedAddress, - }, - accountDetail: { - subview: 'transactions', - }, - }) - } + // const otherUnconfActions = getUnconfActionList(state) + // .filter(tx => tx.id !== action.value) + // const hasOtherUnconfActions = otherUnconfActions.length > 0 + + // if (hasOtherUnconfActions) { + // log.debug('reducer detected txs - rendering confTx view') + // return extend(appState, { + // transForward: false, + // currentView: { + // name: 'confTx', + // context: 0, + // }, + // warning: null, + // }) + // } else { + log.debug('attempting to close popup') + return extend(appState, { + // indicate notification should close + shouldClose: true, + transForward: false, + warning: null, + currentView: { + name: 'accountDetail', + context: state.metamask.selectedAddress, + }, + accountDetail: { + subview: 'transactions', + }, + }) + // } case actions.NEXT_TX: return extend(appState, { -- cgit v1.2.3 From 443b1a8eb7883b6799692ddc24d0555d49bd1787 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 22 Sep 2017 14:38:40 -0700 Subject: Remove keyring controller from project --- app/scripts/keyring-controller.js | 594 ------------------------------- app/scripts/metamask-controller.js | 2 +- app/scripts/migrations/_multi-keyring.js | 2 +- package.json | 5 +- test/unit/keyring-controller-test.js | 164 --------- 5 files changed, 3 insertions(+), 764 deletions(-) delete mode 100644 app/scripts/keyring-controller.js delete mode 100644 test/unit/keyring-controller-test.js diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js deleted file mode 100644 index fb60a5b3e..000000000 --- a/app/scripts/keyring-controller.js +++ /dev/null @@ -1,594 +0,0 @@ -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const bip39 = require('bip39') -const EventEmitter = require('events').EventEmitter -const ObservableStore = require('obs-store') -const filter = require('promise-filter') -const encryptor = require('browser-passworder') -const sigUtil = require('eth-sig-util') -const normalizeAddress = sigUtil.normalize -// Keyrings: -const SimpleKeyring = require('eth-simple-keyring') -const HdKeyring = require('eth-hd-keyring') -const keyringTypes = [ - SimpleKeyring, - HdKeyring, -] - -class KeyringController extends EventEmitter { - - // PUBLIC METHODS - // - // THE FIRST SECTION OF METHODS ARE PUBLIC-FACING, - // MEANING THEY ARE USED BY CONSUMERS OF THIS CLASS. - // - // THEIR SURFACE AREA SHOULD BE CHANGED WITH GREAT CARE. - - constructor (opts) { - super() - const initState = opts.initState || {} - this.keyringTypes = opts.keyringTypes || keyringTypes - this.store = new ObservableStore(initState) - this.memStore = new ObservableStore({ - isUnlocked: false, - keyringTypes: this.keyringTypes.map(krt => krt.type), - keyrings: [], - identities: {}, - }) - this.encryptor = opts.encryptor || encryptor - this.keyrings = [] - this.getNetwork = opts.getNetwork - } - - // Full Update - // returns Promise( @object state ) - // - // Emits the `update` event and - // returns a Promise that resolves to the current state. - // - // Frequently used to end asynchronous chains in this class, - // indicating consumers can often either listen for updates, - // or accept a state-resolving promise to consume their results. - // - // Not all methods end with this, that might be a nice refactor. - fullUpdate () { - this.emit('update') - return Promise.resolve(this.memStore.getState()) - } - - // Create New Vault And Keychain - // @string password - The password to encrypt the vault with - // - // returns Promise( @object state ) - // - // Destroys any old encrypted storage, - // creates a new encrypted store with the given password, - // randomly creates a new HD wallet with 1 account, - // faucets that account on the testnet. - createNewVaultAndKeychain (password) { - return this.persistAllKeyrings(password) - .then(this.createFirstKeyTree.bind(this)) - .then(this.fullUpdate.bind(this)) - } - - // CreateNewVaultAndRestore - // @string password - The password to encrypt the vault with - // @string seed - The BIP44-compliant seed phrase. - // - // returns Promise( @object state ) - // - // Destroys any old encrypted storage, - // creates a new encrypted store with the given password, - // creates a new HD wallet from the given seed with 1 account. - createNewVaultAndRestore (password, seed) { - if (typeof password !== 'string') { - return Promise.reject('Password must be text.') - } - - if (!bip39.validateMnemonic(seed)) { - return Promise.reject(new Error('Seed phrase is invalid.')) - } - - this.clearKeyrings() - - return this.persistAllKeyrings(password) - .then(() => { - return this.addNewKeyring('HD Key Tree', { - mnemonic: seed, - numberOfAccounts: 1, - }) - }) - .then((firstKeyring) => { - return firstKeyring.getAccounts() - }) - .then((accounts) => { - const firstAccount = accounts[0] - if (!firstAccount) throw new Error('KeyringController - First Account not found.') - const hexAccount = normalizeAddress(firstAccount) - this.emit('newAccount', hexAccount) - return this.setupAccounts(accounts) - }) - .then(this.persistAllKeyrings.bind(this, password)) - .then(this.fullUpdate.bind(this)) - } - - // Set Locked - // returns Promise( @object state ) - // - // This method deallocates all secrets, and effectively locks metamask. - setLocked () { - // set locked - this.password = null - this.memStore.updateState({ isUnlocked: false }) - // remove keyrings - this.keyrings = [] - this._updateMemStoreKeyrings() - return this.fullUpdate() - } - - // Submit Password - // @string password - // - // returns Promise( @object state ) - // - // Attempts to decrypt the current vault and load its keyrings - // into memory. - // - // Temporarily also migrates any old-style vaults first, as well. - // (Pre MetaMask 3.0.0) - submitPassword (password) { - return this.unlockKeyrings(password) - .then((keyrings) => { - this.keyrings = keyrings - return this.fullUpdate() - }) - } - - // Add New Keyring - // @string type - // @object opts - // - // returns Promise( @Keyring keyring ) - // - // Adds a new Keyring of the given `type` to the vault - // and the current decrypted Keyrings array. - // - // All Keyring classes implement a unique `type` string, - // and this is used to retrieve them from the keyringTypes array. - addNewKeyring (type, opts) { - const Keyring = this.getKeyringClassForType(type) - const keyring = new Keyring(opts) - return keyring.deserialize(opts) - .then(() => { - return keyring.getAccounts() - }) - .then((accounts) => { - return this.checkForDuplicate(type, accounts) - }) - .then((checkedAccounts) => { - this.keyrings.push(keyring) - return this.setupAccounts(checkedAccounts) - }) - .then(() => this.persistAllKeyrings()) - .then(() => this._updateMemStoreKeyrings()) - .then(() => this.fullUpdate()) - .then(() => { - return keyring - }) - } - - // For now just checks for simple key pairs - // but in the future - // should possibly add HD and other types - // - checkForDuplicate (type, newAccount) { - return this.getAccounts() - .then((accounts) => { - switch (type) { - case 'Simple Key Pair': - const isNotIncluded = !accounts.find((key) => key === newAccount[0] || key === ethUtil.stripHexPrefix(newAccount[0])) - return (isNotIncluded) ? Promise.resolve(newAccount) : Promise.reject(new Error('The account you\'re are trying to import is a duplicate')) - default: - return Promise.resolve(newAccount) - } - }) - } - - - // Add New Account - // @number keyRingNum - // - // returns Promise( @object state ) - // - // Calls the `addAccounts` method on the Keyring - // in the kryings array at index `keyringNum`, - // and then saves those changes. - addNewAccount (selectedKeyring) { - return selectedKeyring.addAccounts(1) - .then(this.setupAccounts.bind(this)) - .then(this.persistAllKeyrings.bind(this)) - .then(this._updateMemStoreKeyrings.bind(this)) - .then(this.fullUpdate.bind(this)) - } - - // Save Account Label - // @string account - // @string label - // - // returns Promise( @string label ) - // - // Persists a nickname equal to `label` for the specified account. - saveAccountLabel (account, label) { - try { - const hexAddress = normalizeAddress(account) - // update state on diskStore - const state = this.store.getState() - const walletNicknames = state.walletNicknames || {} - walletNicknames[hexAddress] = label - this.store.updateState({ walletNicknames }) - // update state on memStore - const identities = this.memStore.getState().identities - identities[hexAddress].name = label - this.memStore.updateState({ identities }) - return Promise.resolve(label) - } catch (err) { - return Promise.reject(err) - } - } - - // Export Account - // @string address - // - // returns Promise( @string privateKey ) - // - // Requests the private key from the keyring controlling - // the specified address. - // - // Returns a Promise that may resolve with the private key string. - exportAccount (address) { - try { - return this.getKeyringForAccount(address) - .then((keyring) => { - return keyring.exportAccount(normalizeAddress(address)) - }) - } catch (e) { - return Promise.reject(e) - } - } - - - // SIGNING METHODS - // - // This method signs tx and returns a promise for - // TX Manager to update the state after signing - - signTransaction (ethTx, _fromAddress) { - const fromAddress = normalizeAddress(_fromAddress) - return this.getKeyringForAccount(fromAddress) - .then((keyring) => { - return keyring.signTransaction(fromAddress, ethTx) - }) - } - - // Sign Message - // @object msgParams - // - // returns Promise(@buffer rawSig) - // - // Attempts to sign the provided @object msgParams. - signMessage (msgParams) { - const address = normalizeAddress(msgParams.from) - return this.getKeyringForAccount(address) - .then((keyring) => { - return keyring.signMessage(address, msgParams.data) - }) - } - - // Sign Personal Message - // @object msgParams - // - // returns Promise(@buffer rawSig) - // - // Attempts to sign the provided @object msgParams. - // Prefixes the hash before signing as per the new geth behavior. - signPersonalMessage (msgParams) { - const address = normalizeAddress(msgParams.from) - return this.getKeyringForAccount(address) - .then((keyring) => { - return keyring.signPersonalMessage(address, msgParams.data) - }) - } - - // PRIVATE METHODS - // - // THESE METHODS ARE ONLY USED INTERNALLY TO THE KEYRING-CONTROLLER - // AND SO MAY BE CHANGED MORE LIBERALLY THAN THE ABOVE METHODS. - - // Create First Key Tree - // returns @Promise - // - // Clears the vault, - // creates a new one, - // creates a random new HD Keyring with 1 account, - // makes that account the selected account, - // faucets that account on testnet, - // puts the current seed words into the state tree. - createFirstKeyTree () { - this.clearKeyrings() - return this.addNewKeyring('HD Key Tree', { numberOfAccounts: 1 }) - .then((keyring) => { - return keyring.getAccounts() - }) - .then((accounts) => { - const firstAccount = accounts[0] - if (!firstAccount) throw new Error('KeyringController - No account found on keychain.') - const hexAccount = normalizeAddress(firstAccount) - this.emit('newAccount', hexAccount) - this.emit('newVault', hexAccount) - return this.setupAccounts(accounts) - }) - .then(this.persistAllKeyrings.bind(this)) - } - - // Setup Accounts - // @array accounts - // - // returns @Promise(@object account) - // - // Initializes the provided account array - // Gives them numerically incremented nicknames, - // and adds them to the accountTracker for regular balance checking. - setupAccounts (accounts) { - return this.getAccounts() - .then((loadedAccounts) => { - const arr = accounts || loadedAccounts - return Promise.all(arr.map((account) => { - return this.getBalanceAndNickname(account) - })) - }) - } - - // Get Balance And Nickname - // @string account - // - // returns Promise( @string label ) - // - // Takes an account address and an iterator representing - // the current number of named accounts. - getBalanceAndNickname (account) { - if (!account) { - throw new Error('Problem loading account.') - } - const address = normalizeAddress(account) - this.accountTracker.addAccount(address) - return this.createNickname(address) - } - - // Create Nickname - // @string address - // - // returns Promise( @string label ) - // - // Takes an address, and assigns it an incremented nickname, persisting it. - createNickname (address) { - const hexAddress = normalizeAddress(address) - const identities = this.memStore.getState().identities - const currentIdentityCount = Object.keys(identities).length + 1 - const nicknames = this.store.getState().walletNicknames || {} - const existingNickname = nicknames[hexAddress] - const name = existingNickname || `Account ${currentIdentityCount}` - identities[hexAddress] = { - address: hexAddress, - name, - } - this.memStore.updateState({ identities }) - return this.saveAccountLabel(hexAddress, name) - } - - // Persist All Keyrings - // @password string - // - // returns Promise - // - // Iterates the current `keyrings` array, - // serializes each one into a serialized array, - // encrypts that array with the provided `password`, - // and persists that encrypted string to storage. - persistAllKeyrings (password = this.password) { - if (typeof password === 'string') { - this.password = password - this.memStore.updateState({ isUnlocked: true }) - } - return Promise.all(this.keyrings.map((keyring) => { - return Promise.all([keyring.type, keyring.serialize()]) - .then((serializedKeyringArray) => { - // Label the output values on each serialized Keyring: - return { - type: serializedKeyringArray[0], - data: serializedKeyringArray[1], - } - }) - })) - .then((serializedKeyrings) => { - return this.encryptor.encrypt(this.password, serializedKeyrings) - }) - .then((encryptedString) => { - this.store.updateState({ vault: encryptedString }) - return true - }) - } - - // Unlock Keyrings - // @string password - // - // returns Promise( @array keyrings ) - // - // Attempts to unlock the persisted encrypted storage, - // initializing the persisted keyrings to RAM. - unlockKeyrings (password) { - const encryptedVault = this.store.getState().vault - if (!encryptedVault) { - throw new Error('Cannot unlock without a previous vault.') - } - - return this.encryptor.decrypt(password, encryptedVault) - .then((vault) => { - this.password = password - this.memStore.updateState({ isUnlocked: true }) - vault.forEach(this.restoreKeyring.bind(this)) - return this.keyrings - }) - } - - // Restore Keyring - // @object serialized - // - // returns Promise( @Keyring deserialized ) - // - // Attempts to initialize a new keyring from the provided - // serialized payload. - // - // On success, returns the resulting @Keyring instance. - restoreKeyring (serialized) { - const { type, data } = serialized - - const Keyring = this.getKeyringClassForType(type) - const keyring = new Keyring() - return keyring.deserialize(data) - .then(() => { - return keyring.getAccounts() - }) - .then((accounts) => { - return this.setupAccounts(accounts) - }) - .then(() => { - this.keyrings.push(keyring) - this._updateMemStoreKeyrings() - return keyring - }) - } - - // Get Keyring Class For Type - // @string type - // - // Returns @class Keyring - // - // Searches the current `keyringTypes` array - // for a Keyring class whose unique `type` property - // matches the provided `type`, - // returning it if it exists. - getKeyringClassForType (type) { - return this.keyringTypes.find(kr => kr.type === type) - } - - getKeyringsByType (type) { - return this.keyrings.filter((keyring) => keyring.type === type) - } - - // Get Accounts - // returns Promise( @Array[ @string accounts ] ) - // - // Returns the public addresses of all current accounts - // managed by all currently unlocked keyrings. - getAccounts () { - const keyrings = this.keyrings || [] - return Promise.all(keyrings.map(kr => kr.getAccounts())) - .then((keyringArrays) => { - return keyringArrays.reduce((res, arr) => { - return res.concat(arr) - }, []) - }) - } - - // Get Keyring For Account - // @string address - // - // returns Promise(@Keyring keyring) - // - // Returns the currently initialized keyring that manages - // the specified `address` if one exists. - getKeyringForAccount (address) { - const hexed = normalizeAddress(address) - log.debug(`KeyringController - getKeyringForAccount: ${hexed}`) - - return Promise.all(this.keyrings.map((keyring) => { - return Promise.all([ - keyring, - keyring.getAccounts(), - ]) - })) - .then(filter((candidate) => { - const accounts = candidate[1].map(normalizeAddress) - return accounts.includes(hexed) - })) - .then((winners) => { - if (winners && winners.length > 0) { - return winners[0][0] - } else { - throw new Error('No keyring found for the requested account.') - } - }) - } - - // Display For Keyring - // @Keyring keyring - // - // returns Promise( @Object { type:String, accounts:Array } ) - // - // Is used for adding the current keyrings to the state object. - displayForKeyring (keyring) { - return keyring.getAccounts() - .then((accounts) => { - return { - type: keyring.type, - accounts: accounts, - } - }) - } - - // Add Gas Buffer - // @string gas (as hexadecimal value) - // - // returns @string bufferedGas (as hexadecimal value) - // - // Adds a healthy buffer of gas to an initial gas estimate. - addGasBuffer (gas) { - const gasBuffer = new BN('100000', 10) - const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16) - const correct = bnGas.add(gasBuffer) - return ethUtil.addHexPrefix(correct.toString(16)) - } - - // Clear Keyrings - // - // Deallocates all currently managed keyrings and accounts. - // Used before initializing a new vault. - clearKeyrings () { - let accounts - try { - accounts = Object.keys(this.accountTracker.getState()) - } catch (e) { - accounts = [] - } - accounts.forEach((address) => { - this.accountTracker.removeAccount(address) - }) - - // clear keyrings from memory - this.keyrings = [] - this.memStore.updateState({ - keyrings: [], - identities: {}, - }) - } - - _updateMemStoreKeyrings () { - Promise.all(this.keyrings.map(this.displayForKeyring)) - .then((keyrings) => { - this.memStore.updateState({ keyrings }) - }) - } - -} - -module.exports = KeyringController diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 30e511e19..ebe6b65a8 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -14,7 +14,7 @@ const createOriginMiddleware = require('./lib/createOriginMiddleware') const createLoggerMiddleware = require('./lib/createLoggerMiddleware') const createProviderMiddleware = require('./lib/createProviderMiddleware') const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex -const KeyringController = require('./keyring-controller') +const KeyringController = require('eth-keyring-controller') const NetworkController = require('./controllers/network') const PreferencesController = require('./controllers/preferences') const CurrencyController = require('./controllers/currency') diff --git a/app/scripts/migrations/_multi-keyring.js b/app/scripts/migrations/_multi-keyring.js index 253aa3d9d..7a4578ea7 100644 --- a/app/scripts/migrations/_multi-keyring.js +++ b/app/scripts/migrations/_multi-keyring.js @@ -10,7 +10,7 @@ which we dont have access to at the time of this writing. const ObservableStore = require('obs-store') const ConfigManager = require('../../app/scripts/lib/config-manager') const IdentityStoreMigrator = require('../../app/scripts/lib/idStore-migrator') -const KeyringController = require('../../app/scripts/lib/keyring-controller') +const KeyringController = require('eth-keyring-controller') const password = 'obviously not correct' diff --git a/package.json b/package.json index bcfb6c1ac..8526455e7 100644 --- a/package.json +++ b/package.json @@ -53,10 +53,8 @@ "async": "^2.5.0", "await-semaphore": "^0.1.1", "babel-runtime": "^6.23.0", - "bip39": "^2.2.0", "bluebird": "^3.5.0", "bn.js": "^4.11.7", - "browser-passworder": "^2.0.3", "browserify-derequire": "^0.9.4", "client-sw-ready-event": "^3.3.0", "clone": "^2.1.1", @@ -70,12 +68,11 @@ "ensnare": "^1.0.0", "eth-bin-to-ops": "^1.0.1", "eth-contract-metadata": "^1.1.4", - "eth-hd-keyring": "^1.1.1", "eth-json-rpc-filters": "^1.1.0", + "eth-keyring-controller": "^1.0.1", "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", "eth-sig-util": "^1.2.2", - "eth-simple-keyring": "^1.1.1", "eth-token-tracker": "^1.1.3", "ethereumjs-tx": "^1.3.0", "ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", diff --git a/test/unit/keyring-controller-test.js b/test/unit/keyring-controller-test.js deleted file mode 100644 index 135edf365..000000000 --- a/test/unit/keyring-controller-test.js +++ /dev/null @@ -1,164 +0,0 @@ -const assert = require('assert') -const KeyringController = require('../../app/scripts/keyring-controller') -const configManagerGen = require('../lib/mock-config-manager') -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const mockEncryptor = require('../lib/mock-encryptor') -const sinon = require('sinon') - -describe('KeyringController', function () { - let keyringController - const password = 'password123' - const seedWords = 'puzzle seed penalty soldier say clay field arctic metal hen cage runway' - const addresses = ['eF35cA8EbB9669A35c31b5F6f249A9941a812AC1'.toLowerCase()] - const accounts = [] - // let originalKeystore - - beforeEach(function (done) { - this.sinon = sinon.sandbox.create() - window.localStorage = {} // Hacking localStorage support into JSDom - - keyringController = new KeyringController({ - configManager: configManagerGen(), - txManager: { - getTxList: () => [], - getUnapprovedTxList: () => [], - }, - accountTracker: { - addAccount (acct) { accounts.push(ethUtil.addHexPrefix(acct)) }, - }, - encryptor: mockEncryptor, - }) - - keyringController.createNewVaultAndKeychain(password) - .then(function (newState) { - newState - done() - }) - .catch((err) => { - done(err) - }) - }) - - afterEach(function () { - // Cleanup mocks - this.sinon.restore() - }) - - describe('#createNewVaultAndKeychain', function () { - this.timeout(10000) - - it('should set a vault on the configManager', function (done) { - keyringController.store.updateState({ vault: null }) - assert(!keyringController.store.getState().vault, 'no previous vault') - keyringController.createNewVaultAndKeychain(password) - .then(() => { - const vault = keyringController.store.getState().vault - assert(vault, 'vault created') - done() - }) - .catch((reason) => { - done(reason) - }) - }) - }) - - describe('#restoreKeyring', function () { - it(`should pass a keyring's serialized data back to the correct type.`, function (done) { - const mockSerialized = { - type: 'HD Key Tree', - data: { - mnemonic: seedWords, - numberOfAccounts: 1, - }, - } - const mock = this.sinon.mock(keyringController) - - mock.expects('getBalanceAndNickname') - .exactly(1) - - keyringController.restoreKeyring(mockSerialized) - .then((keyring) => { - assert.equal(keyring.wallets.length, 1, 'one wallet restored') - return keyring.getAccounts() - }) - .then((accounts) => { - assert.equal(accounts[0], addresses[0]) - mock.verify() - done() - }) - .catch((reason) => { - done(reason) - }) - }) - }) - - describe('#createNickname', function () { - it('should add the address to the identities hash', function () { - const fakeAddress = '0x12345678' - keyringController.createNickname(fakeAddress) - const identities = keyringController.memStore.getState().identities - const identity = identities[fakeAddress] - assert.equal(identity.address, fakeAddress) - }) - }) - - describe('#saveAccountLabel', function () { - it('sets the nickname', function (done) { - const account = addresses[0] - var nick = 'Test nickname' - const identities = keyringController.memStore.getState().identities - identities[ethUtil.addHexPrefix(account)] = {} - keyringController.memStore.updateState({ identities }) - keyringController.saveAccountLabel(account, nick) - .then((label) => { - try { - assert.equal(label, nick) - const persisted = keyringController.store.getState().walletNicknames[account] - assert.equal(persisted, nick) - done() - } catch (err) { - done() - } - }) - .catch((reason) => { - done(reason) - }) - }) - }) - - describe('#getAccounts', function () { - it('returns the result of getAccounts for each keyring', function (done) { - keyringController.keyrings = [ - { getAccounts () { return Promise.resolve([1, 2, 3]) } }, - { getAccounts () { return Promise.resolve([4, 5, 6]) } }, - ] - - keyringController.getAccounts() - .then((result) => { - assert.deepEqual(result, [1, 2, 3, 4, 5, 6]) - done() - }) - }) - }) - - describe('#addGasBuffer', function () { - it('adds 100k gas buffer to estimates', function () { - const gas = '0x04ee59' // Actual estimated gas example - const tooBigOutput = '0x80674f9' // Actual bad output - const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16) - const correctBuffer = new BN('100000', 10) - const correct = bnGas.add(correctBuffer) - - // const tooBig = new BN(tooBigOutput, 16) - const result = keyringController.addGasBuffer(gas) - const bnResult = new BN(ethUtil.stripHexPrefix(result), 16) - - assert.equal(result.indexOf('0x'), 0, 'included hex prefix') - assert(bnResult.gt(bnGas), 'Estimate increased in value.') - assert.equal(bnResult.sub(bnGas).toString(10), '100000', 'added 100k gas') - assert.equal(result, '0x' + correct.toString(16), 'Added the right amount') - assert.notEqual(result, tooBigOutput, 'not that bad estimate') - }) - }) -}) -- cgit v1.2.3 From 8ad74cf93a8c0170f260507694f6a05eafa56c4f Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 22 Sep 2017 15:16:42 -0700 Subject: deps - bump filter deps and add random missing deps --- package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 38ed28eef..018c10483 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "eth-block-tracker": "^2.2.0", "eth-contract-metadata": "^1.1.4", "eth-hd-keyring": "^1.1.1", - "eth-json-rpc-filters": "^1.1.0", + "eth-json-rpc-filters": "^1.2.1", "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", "eth-sig-util": "^1.2.2", @@ -81,6 +81,7 @@ "ethereumjs-tx": "^1.3.0", "ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", + "ethjs-contract": "^0.1.9", "ethjs-ens": "^2.0.0", "ethjs-query": "^0.2.9", "express": "^4.14.0", @@ -91,6 +92,7 @@ "gulp": "github:gulpjs/gulp#4.0", "gulp-eslint": "^4.0.0", "hat": "0.0.3", + "human-standard-token-abi": "^1.0.2", "idb-global": "^2.1.0", "identicon.js": "^2.3.1", "iframe": "^1.0.0", @@ -139,7 +141,7 @@ "valid-url": "^1.0.9", "vreme": "^3.0.2", "web3": "^0.20.1", - "web3-provider-engine": "^13.2.10", + "web3-provider-engine": "^13.2.12", "web3-stream-provider": "^3.0.1", "xtend": "^4.0.1" }, -- cgit v1.2.3 From d4578836c99bfcdb66a6c989a7ecf79a896a2dc8 Mon Sep 17 00:00:00 2001 From: tmashuang Date: Fri, 22 Sep 2017 16:15:18 -0700 Subject: Most of transaction controller tests --- test/unit/tx-controller-test.js | 192 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 180 insertions(+), 12 deletions(-) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index 47bfe66f8..e1e4ae8fe 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -7,12 +7,13 @@ const sinon = require('sinon') const TransactionController = require('../../app/scripts/controllers/transactions') const TxGasUtils = require('../../app/scripts/lib/tx-gas-utils') const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper') +const TxStateManager = require('../../app/scripts/lib/tx-state-manager') +const { createStubedProvider } = require('../stub/provider') const noop = () => true const currentNetworkId = 42 const otherNetworkId = 36 const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex') -const { createStubedProvider } = require('../stub/provider') describe('Transaction Controller', function () { @@ -34,11 +35,11 @@ describe('Transaction Controller', function () { }), }) txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop }) - txController.txProviderUtils = new TxGasUtils(txController.provider) + txController.txProviderUtils = new TxGasUtils(txController.provider) }) describe('#getState', function () { - it('should return a state object with the right keys and datat types', function (){ + it('should return a state object with the right keys and datat types', function () { const exposedState = txController.getState() assert('unapprovedTxs' in exposedState, 'state should have the key unapprovedTxs') assert('selectedAddressTxList' in exposedState, 'state should have the key selectedAddressTxList') @@ -71,13 +72,32 @@ describe('Transaction Controller', function () { }) }) + describe('#getConfirmedTransactions', function () { + let address + beforeEach(function () { + address = '0xc684832530fcbddae4b4230a47e991ddcec2831d' + const txParams = { + 'from': address, + 'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d', + } + txController.txStateManager._saveTxList([ + {id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams}, + {id: 2, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams}, + {id: 3, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams}, + ]) + }) + it('should return the number of confirmed txs', function () { + assert.equal(txController.nonceTracker.getConfirmedTransactions(address).length, 3) + }) + }) + describe('#newUnapprovedTransaction', function () { let stub, txMeta, txParams beforeEach(function () { txParams = { - 'from':'0xc684832530fcbddae4b4230a47e991ddcec2831d', - 'to':'0xc684832530fcbddae4b4230a47e991ddcec2831d', + 'from': '0xc684832530fcbddae4b4230a47e991ddcec2831d', + 'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d', }, txMeta = { status: 'unapproved', @@ -157,10 +177,10 @@ describe('Transaction Controller', function () { describe('#addTxDefaults', function () { it('should add the tx defaults if their are none', function (done) { - let txMeta = { + const txMeta = { 'txParams': { - 'from':'0xc684832530fcbddae4b4230a47e991ddcec2831d', - 'to':'0xc684832530fcbddae4b4230a47e991ddcec2831d', + 'from': '0xc684832530fcbddae4b4230a47e991ddcec2831d', + 'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d', }, } providerResultStub.eth_gasPrice = '4a817c800' @@ -168,7 +188,7 @@ describe('Transaction Controller', function () { providerResultStub.eth_estimateGas = '5209' txController.addTxDefaults(txMeta) .then((txMetaWithDefaults) => { - assert(txMetaWithDefaults.txParams.value, '0x0','should have added 0x0 as the value') + assert(txMetaWithDefaults.txParams.value, '0x0', 'should have added 0x0 as the value') assert(txMetaWithDefaults.txParams.gasPrice, 'should have added the gas price') assert(txMetaWithDefaults.txParams.gas, 'should have added the gas field') done() @@ -177,6 +197,16 @@ describe('Transaction Controller', function () { }) }) + xdescribe('#updateAndApprovedTransaction', function () { + it('should update txMeta and approve status for Tx', async function () { + txController.txStateManager.addTx({ id: 0, status: 'unapproved', txParams: { from: '0x1678a085c290ebd122dc42cba69373b5953b831d', nonce: '0x1', value: '0xfffff' }, metamaskNetworkId: currentNetworkId }) + const txMeta = txController.txStateManager.getTx(0) + txMeta.value = '0xffffe' + provider.eth_sendRawTransaction = 0x7f9fade1c0d57a7af66ab4ead79fade1c0d57a7af66ab4ead7c2c2eb7b11a91385 + await txController.updateAndApproveTransaction(txMeta) + }) + }) + describe('#validateTxParams', function () { it('does not throw for positive values', function (done) { var sample = { @@ -205,9 +235,8 @@ describe('Transaction Controller', function () { const txMeta = { id: '1', status: 'unapproved', - id: 1, metamaskNetworkId: currentNetworkId, - txParams: {} + txParams: {}, } const eventNames = ['updateBadge', '1:unapproved'] @@ -286,4 +315,143 @@ describe('Transaction Controller', function () { }).catch(done) }) }) -}) \ No newline at end of file + + describe('#getChainId', function () { + it('returns 0 when the chainId is NaN', async function () { + txController.networkStore = new ObservableStore('hello') + assert.equal(txController.getChainId(), 0) + }) + }) + + describe('#publishTransaction', async function () { + beforeEach(function () { + const txMeta = [ + { id: 0, status: 'unapproved', txParams: { from: '0x1678a085c290ebd122dc42cba69373b5953b831d', nonce: '0x1', value: '0xfffff' }, rawTx: 'f8498080808080801ca00255b75b550cf112e18fd699f27d043d85348c29d7e8fd234799db890f7c272da0238121aed40c3141e63aae5335aaa3c9711d4907f28071f81f8d055b9f8435e0', metamaskNetworkId: currentNetworkId }, + ] + txController.txStateManager.addTx(txMeta) + }) + + it('should update rawTx of a transaction', async function () { + txController.publishTransaction(0, 'f84c01808080830fffff801ca0e23554ce6f82186402dcdcfff377793fdfa8a872a5670c0ffc002c9cd2f6827aa02a83dfce20aef4064a55320bda47394043af3cbfa63c4e029c54ba6281dcc70e') + }) + }) + + describe('#cancelTransaction', function () { + beforeEach(function () { + const txMetas = [ + { id: 0, status: 'unapproved', txParams: { }, metamaskNetworkId: currentNetworkId }, + { id: 1, status: 'rejected', txParams: { }, metamaskNetworkId: currentNetworkId }, + { id: 2, status: 'approved', txParams: { }, metamaskNetworkId: currentNetworkId }, + { id: 3, status: 'signed', txParams: { }, metamaskNetworkId: currentNetworkId }, + { id: 4, status: 'submitted', txParams: { }, metamaskNetworkId: currentNetworkId }, + { id: 5, status: 'confirmed', txParams: { }, metamaskNetworkId: currentNetworkId }, + { id: 6, status: 'failed', txParams: { }, metamaskNetworkId: currentNetworkId }, + ] + txMetas.forEach((txMeta) => txController.txStateManager.addTx(txMeta)) + }) + + it('should set the transaction to rejected from unapproved', async function () { + await txController.cancelTransaction(0) + assert(txController.txStateManager.getTx(0).status, 'rejected') + }) + + it('should set the transaction to rejected from rejected', async function () { + await txController.cancelTransaction(1) + assert(txController.txStateManager.getTx(1).status, 'rejected') + }) + + it('should set the transaction to rejected from approved', async function () { + await txController.cancelTransaction(2) + assert(txController.txStateManager.getTx(2).status, 'rejected') + }) + + it('should set the transaction to rejected from signed', async function () { + await txController.cancelTransaction(3) + assert(txController.txStateManager.getTx(3).status, 'rejected') + }) + + it('should set the transaction to rejected from submitted', async function () { + await txController.cancelTransaction(4) + assert(txController.txStateManager.getTx(4).status, 'rejected') + }) + + it('should set the transaction to rejected from confirmed', async function () { + await txController.cancelTransaction(5) + assert(txController.txStateManager.getTx(5).status, 'rejected') + }) + + it('should set the transaction to rejected from failed', async function () { + await txController.cancelTransaction(6) + assert(txController.txStateManager.getTx(6).status, 'rejected') + }) + + }) + + describe('#publishTransaction', function () { + let replaceRawTx, rawTx, hash, txMeta + beforeEach(function () { + rawTx = 'f86c808504a817c800827b0d940c62bb85faa3311a998d3aba8098c1235c564966880de0b6b3a7640000802aa08ff665feb887a25d4099e40e11f0fef93ee9608f404bd3f853dd9e84ed3317a6a02ec9d3d1d6e176d4d2593dd760e74ccac753e6a0ea0d00cc9789d0d7ff1f471d' + replaceRawTx = 'f84c01808080830fffff801ca0e23554ce6f82186402dcdcfff377793fdfa8a872a5670c0ffc002c9cd2f6827aa02a83dfce20aef4064a55320bda47394043af3cbfa63c4e029c54ba6281dcc70e' + txMeta = { + id: 1, + status: 'approved', + txParams: {}, + rawTx, + hash, + metamaskNetworkId: currentNetworkId, + } + providerResultStub.eth_sendRawTransaction = '0x2a5523c6fa98b47b7d9b6c8320179785150b42a16bcff36b398c5062b65657e8' + }) + it('should publish a tx, updates the rawTx when provided a one', async function () { + txController.txStateManager.addTx(txMeta) + await txController.publishTransaction(txMeta.id, replaceRawTx) + txController.setTxHash(1, '0x2a5523c6fa98b47b7d9b6c8320179785150b42a16bcff36b398c5062b65657e8') + assert.equal(txController.txStateManager.getTx(1).rawTx, replaceRawTx) + assert.notEqual(txController.txStateManager.getTx(1).rawTx, rawTx) + }) + }) + + describe('#getBalance', function () { + it('gets balance', function () { + sinon.stub(txController.ethStore, 'getState').callsFake(() => { + return { + accounts: { + '0x1678a085c290ebd122dc42cba69373b5953b831d': { + address: '0x1678a085c290ebd122dc42cba69373b5953b831d', + balance: '0x00000000000000056bc75e2d63100000', + code: '0x', + nonce: '0x0', + }, + '0xc684832530fcbddae4b4230a47e991ddcec2831d': { + address: '0xc684832530fcbddae4b4230a47e991ddcec2831d', + balance: '0x0', + code: '0x', + nonce: '0x0', + }, + }, + } + }) + assert.equal(txController.pendingTxTracker.getBalance('0x1678a085c290ebd122dc42cba69373b5953b831d'), '0x00000000000000056bc75e2d63100000') + assert.equal(txController.pendingTxTracker.getBalance('0xc684832530fcbddae4b4230a47e991ddcec2831d'), '0x0') + }) + }) + + describe('#getPendingTransactions', function () { + beforeEach(function () { + txController.txStateManager._saveTxList([ + { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, + { id: 2, status: 'rejected', metamaskNetworkId: currentNetworkId, txParams: {} }, + { id: 3, status: 'approved', metamaskNetworkId: currentNetworkId, txParams: {} }, + { id: 4, status: 'signed', metamaskNetworkId: currentNetworkId, txParams: {} }, + { id: 5, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {} }, + { id: 6, status: 'confimed', metamaskNetworkId: currentNetworkId, txParams: {} }, + { id: 7, status: 'failed', metamaskNetworkId: currentNetworkId, txParams: {} }, + ]) + }) + it('should show only submitted transactions as pending transasction', function () { + assert(txController.pendingTxTracker.getPendingTransactions().length, 1) + assert(txController.pendingTxTracker.getPendingTransactions()[0].status, 'submitted') + }) + }) + +}) -- cgit v1.2.3 From 4979f5902fc15a7bb65e1e7420e699282d40c8b7 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Fri, 22 Sep 2017 20:04:58 -0700 Subject: bump metamascara version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c897af8a5..6c0f8ce67 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "json-rpc-engine": "^3.1.0", "json-rpc-middleware-stream": "^1.0.0", "loglevel": "^1.4.1", - "metamascara": "^1.2.1", + "metamascara": "^1.3.1", "metamask-logo": "^2.1.2", "mississippi": "^1.2.0", "mkdirp": "^0.5.1", -- cgit v1.2.3 From 0eeba3771c2396c12de3f254dbfaae957344411d Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 20 Sep 2017 05:35:27 -0230 Subject: Exports private key modal opens from dropdown. --- .../dropdowns/components/account-dropdowns.js | 8 +-- .../components/modals/export-private-key-modal.js | 41 ++++++++++++ ui/app/components/modals/modal.js | 42 ++++++++----- ui/app/css/itcss/components/modal.scss | 73 +++++++++++----------- 4 files changed, 110 insertions(+), 54 deletions(-) create mode 100644 ui/app/components/modals/export-private-key-modal.js diff --git a/ui/app/components/dropdowns/components/account-dropdowns.js b/ui/app/components/dropdowns/components/account-dropdowns.js index e2d3d6d64..76f186a3f 100644 --- a/ui/app/components/dropdowns/components/account-dropdowns.js +++ b/ui/app/components/dropdowns/components/account-dropdowns.js @@ -337,9 +337,7 @@ class AccountDropdowns extends Component { DropdownMenuItem, { closeMenu: () => {}, - onClick: () => { - actions.requestAccountExport() - }, + onClick: () => this.props.actions.showExportPrivateKeyModal(), style: Object.assign( dropdownMenuItemStyle, menuItemStyles, @@ -429,7 +427,6 @@ const mapDispatchToProps = (dispatch) => { return { actions: { showConfigPage: () => dispatch(actions.showConfigPage()), - requestAccountExport: () => dispatch(actions.requestExportAccount()), showAccountDetail: (address) => dispatch(actions.showAccountDetail(address)), showAccountDetailModal: () => { dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' })) @@ -443,6 +440,9 @@ const mapDispatchToProps = (dispatch) => { showNewAccountModal: () => { dispatch(actions.showModal({ name: 'NEW_ACCOUNT' })) }, + showExportPrivateKeyModal: () => { + dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' })) + }, showAddTokenPage: () => { dispatch(actions.showAddTokenPage()) }, diff --git a/ui/app/components/modals/export-private-key-modal.js b/ui/app/components/modals/export-private-key-modal.js new file mode 100644 index 000000000..bbcd25e0d --- /dev/null +++ b/ui/app/components/modals/export-private-key-modal.js @@ -0,0 +1,41 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const actions = require('../../actions') +const AccountModalContainer = require('./account-modal-container') +const { getSelectedIdentity } = require('../../selectors') +const ReadOnlyInput = require('../readonly-input') + +function mapStateToProps (state) { + return { + network: state.metamask.network, + selectedIdentity: getSelectedIdentity(state), + } +} + +inherits(ExportPrivateKeyModal, Component) +function ExportPrivateKeyModal () { + Component.call(this) +} + +module.exports = connect(mapStateToProps)(ExportPrivateKeyModal) + +ExportPrivateKeyModal.prototype.render = function () { + const { selectedIdentity, network } = this.props + const { name, address } = selectedIdentity + + return h(AccountModalContainer, {}, [ + + h('span.account-name', name), + + h(ReadOnlyInput, { + wrapperClass: 'ellip-address-wrapper', + inputClass: 'ellip-address', + value: address, + }), + + h('div.account-modal-divider'), + + ]) +} diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index 04a2f5f40..138efc3ea 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -11,8 +11,27 @@ const isPopupOrNotification = require('../../../../app/scripts/lib/is-popup-or-n const BuyOptions = require('./buy-options-modal') const AccountDetailsModal = require('./account-details-modal') const EditAccountNameModal = require('./edit-account-name-modal') +const ExportPrivateKeyModal = require('./export-private-key-modal') const NewAccountModal = require('./new-account-modal') +const accountModalStyle = { + mobileModalStyle: { + width: '95%', + top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + borderRadius: '4px', + }, + laptopModalStyle: { + width: '360px', + top: 'calc(33% + 45px)', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + borderRadius: '4px', + }, + contentStyle: { + borderRadius: '4px', + }, +} + const MODALS = { BUY: { contents: [ @@ -51,21 +70,14 @@ const MODALS = { contents: [ h(AccountDetailsModal, {}, []), ], - mobileModalStyle: { - width: '95%', - top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh', - boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', - borderRadius: '4px', - }, - laptopModalStyle: { - width: '360px', - top: 'calc(33% + 45px)', - boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', - borderRadius: '4px', - }, - contentStyle: { - borderRadius: '4px', - }, + ...accountModalStyle, + }, + + EXPORT_PRIVATE_KEY: { + contents: [ + h(ExportPrivateKeyModal, {}, []), + ], + ...accountModalStyle, }, NEW_ACCOUNT: { diff --git a/ui/app/css/itcss/components/modal.scss b/ui/app/css/itcss/components/modal.scss index c0a5aa1ef..0afd778e9 100644 --- a/ui/app/css/itcss/components/modal.scss +++ b/ui/app/css/itcss/components/modal.scss @@ -214,45 +214,48 @@ // Account Details Modal -.account-modal-container .qr-header { - margin-top: 9px; - font-size: 20px; -} +.account-modal-container { -.account-modal-container .qr-wrapper { - margin-top: 5px; -} + .qr-header { + margin-top: 9px; + font-size: 20px; + } -.account-modal-container .ellip-address-wrapper { - display: flex; - justify-content: center; - border: 1px solid $alto; - padding: 5px 10px; - font-family: 'Montserrat Light'; - margin-top: 7px; - width: 286px; -} + .qr-wrapper { + margin-top: 5px; + } -.account-modal-container .qr-ellip-address { - width: 247px; - border: none; - font-family: 'Montserrat Light'; - font-size: 14px; -} + .ellip-address-wrapper { + display: flex; + justify-content: center; + border: 1px solid $alto; + padding: 5px 10px; + font-family: 'Montserrat Light'; + margin-top: 7px; + width: 286px; + } -.account-modal-container .btn-clear { - min-height: 28px; - font-size: 14px; - border-color: $curious-blue; - color: $curious-blue; - border-radius: 2px; - flex-basis: 100%; - width: 75%; - margin-top: 17px; - padding: 10px 22px; - height: 44px; - width: 235px; - font-family: 'Montserrat Light'; + .qr-ellip-address, .ellip-address { + width: 247px; + border: none; + font-family: 'Montserrat Light'; + font-size: 14px; + } + + .btn-clear { + min-height: 28px; + font-size: 14px; + border-color: $curious-blue; + color: $curious-blue; + border-radius: 2px; + flex-basis: 100%; + width: 75%; + margin-top: 17px; + padding: 10px 22px; + height: 44px; + width: 235px; + font-family: 'Montserrat Light'; + } } .account-modal-divider { -- cgit v1.2.3 From 20fea3f1dee33455842d9c2ac7e620d3321b6011 Mon Sep 17 00:00:00 2001 From: tmashuang Date: Mon, 25 Sep 2017 10:47:36 -0700 Subject: Remove pending updateAndApprovedTransaction test --- test/unit/tx-controller-test.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index e1e4ae8fe..f6b41f2bb 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -197,16 +197,6 @@ describe('Transaction Controller', function () { }) }) - xdescribe('#updateAndApprovedTransaction', function () { - it('should update txMeta and approve status for Tx', async function () { - txController.txStateManager.addTx({ id: 0, status: 'unapproved', txParams: { from: '0x1678a085c290ebd122dc42cba69373b5953b831d', nonce: '0x1', value: '0xfffff' }, metamaskNetworkId: currentNetworkId }) - const txMeta = txController.txStateManager.getTx(0) - txMeta.value = '0xffffe' - provider.eth_sendRawTransaction = 0x7f9fade1c0d57a7af66ab4ead79fade1c0d57a7af66ab4ead7c2c2eb7b11a91385 - await txController.updateAndApproveTransaction(txMeta) - }) - }) - describe('#validateTxParams', function () { it('does not throw for positive values', function (done) { var sample = { -- cgit v1.2.3 From 40f1d0868401662c42f6a031549c9b023427ccef Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 25 Sep 2017 11:42:08 -0700 Subject: Made some requested changes --- app/scripts/controllers/balance.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/scripts/controllers/balance.js b/app/scripts/controllers/balance.js index ddeb06cf9..840b7abc3 100644 --- a/app/scripts/controllers/balance.js +++ b/app/scripts/controllers/balance.js @@ -16,7 +16,7 @@ class BalanceController { this.store = new ObservableStore(initState) this.balanceCalc = new PendingBalanceCalculator({ - getBalance: () => Promise.resolve(this._getBalance()), + getBalance: () => this._getBalance(), getPendingTransactions: this._getPendingTransactions.bind(this), }) @@ -35,24 +35,24 @@ class BalanceController { this.txController.on('submitted', update) this.txController.on('confirmed', update) this.txController.on('failed', update) + this.accountTracker.subscribe(update) this.txController.blockTracker.on('block', update) } - _getBalance () { - const store = this.accountTracker.getState() - const balances = store.accounts - const entry = balances[this.address] + async _getBalance () { + const { accounts } = this.accountTracker.getState() + const entry = accounts[this.address] const balance = entry.balance return balance ? new BN(balance.substring(2), 16) : undefined } - _getPendingTransactions () { + async _getPendingTransactions () { const pending = this.txController.getFilteredTxList({ from: this.address, status: 'submitted', err: undefined, }) - return Promise.resolve(pending) + return pending } } -- cgit v1.2.3 From 8cd7329c91b047ef15c81b164075ea6c1d15b0df Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 25 Sep 2017 14:36:49 -0700 Subject: Implemented feedback --- app/scripts/controllers/balance.js | 4 ++-- app/scripts/lib/pending-balance-calculator.js | 8 +++----- test/unit/pending-balance-test.js | 20 +++++++------------- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/app/scripts/controllers/balance.js b/app/scripts/controllers/balance.js index 840b7abc3..9b2566852 100644 --- a/app/scripts/controllers/balance.js +++ b/app/scripts/controllers/balance.js @@ -20,7 +20,7 @@ class BalanceController { getPendingTransactions: this._getPendingTransactions.bind(this), }) - this.registerUpdates() + this._registerUpdates() } async updateBalance () { @@ -30,7 +30,7 @@ class BalanceController { }) } - registerUpdates () { + _registerUpdates () { const update = this.updateBalance.bind(this) this.txController.on('submitted', update) this.txController.on('confirmed', update) diff --git a/app/scripts/lib/pending-balance-calculator.js b/app/scripts/lib/pending-balance-calculator.js index c66bffbbb..cea642f1a 100644 --- a/app/scripts/lib/pending-balance-calculator.js +++ b/app/scripts/lib/pending-balance-calculator.js @@ -19,19 +19,17 @@ class PendingBalanceCalculator { this.getPendingTransactions(), ]) - const balance = results[0] - const pending = results[1] - + const [ balance, pending ] = results if (!balance) return undefined const pendingValue = pending.reduce((total, tx) => { - return total.add(this.valueFor(tx)) + return total.add(this.calculateMaxCost(tx)) }, new BN(0)) return `0x${balance.sub(pendingValue).toString(16)}` } - valueFor (tx) { + calculateMaxCost (tx) { const txValue = tx.txParams.value const value = this.hexToBn(txValue) const gasPrice = this.hexToBn(tx.txParams.gasPrice) diff --git a/test/unit/pending-balance-test.js b/test/unit/pending-balance-test.js index dde30fecc..5048d487b 100644 --- a/test/unit/pending-balance-test.js +++ b/test/unit/pending-balance-test.js @@ -11,7 +11,7 @@ const ether = '0x' + etherBn.toString(16) describe('PendingBalanceCalculator', function () { let balanceCalculator - describe('#valueFor(tx)', function () { + describe('#calculateMaxCost(tx)', function () { it('returns a BN for a given tx value', function () { const txGen = new MockTxGen() pendingTxs = txGen.generate({ @@ -24,7 +24,7 @@ describe('PendingBalanceCalculator', function () { }, { count: 1 }) const balanceCalculator = generateBalanceCalcWith([], zeroBn) - const result = balanceCalculator.valueFor(pendingTxs[0]) + const result = balanceCalculator.calculateMaxCost(pendingTxs[0]) assert.equal(result.toString(), etherBn.toString(), 'computes one ether') }) @@ -40,8 +40,8 @@ describe('PendingBalanceCalculator', function () { }, { count: 1 }) const balanceCalculator = generateBalanceCalcWith([], zeroBn) - const result = balanceCalculator.valueFor(pendingTxs[0]) - assert.equal(result.toString(), '6', 'computes one ether') + const result = balanceCalculator.calculateMaxCost(pendingTxs[0]) + assert.equal(result.toString(), '6', 'computes 6 wei of gas') }) }) @@ -82,15 +82,9 @@ describe('PendingBalanceCalculator', function () { }) function generateBalanceCalcWith (transactions, providerStub = zeroBn) { - const getPendingTransactions = () => Promise.resolve(transactions) - const getBalance = () => Promise.resolve(providerStub) - providerResultStub.result = providerStub - const provider = { - sendAsync: (_, cb) => { cb(undefined, providerResultStub) }, - _blockTracker: { - getCurrentBlock: () => '0x11b568', - }, - } + const getPendingTransactions = async () => transactions + const getBalance = async () => providerStub + return new PendingBalanceCalculator({ getBalance, getPendingTransactions, -- cgit v1.2.3 From 674aac83ce49d21606be7be7afdf1b6a8ceb386f Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 25 Sep 2017 14:39:22 -0700 Subject: Make blockTracker an independent param --- app/scripts/controllers/balance.js | 5 +++-- app/scripts/controllers/computed-balances.js | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/scripts/controllers/balance.js b/app/scripts/controllers/balance.js index 9b2566852..ab0cfe907 100644 --- a/app/scripts/controllers/balance.js +++ b/app/scripts/controllers/balance.js @@ -5,10 +5,11 @@ const BN = require('ethereumjs-util').BN class BalanceController { constructor (opts = {}) { - const { address, accountTracker, txController } = opts + const { address, accountTracker, txController, blockTracker } = opts this.address = address this.accountTracker = accountTracker this.txController = txController + this.blockTracker = blockTracker const initState = { ethBalance: undefined, @@ -36,7 +37,7 @@ class BalanceController { this.txController.on('confirmed', update) this.txController.on('failed', update) this.accountTracker.subscribe(update) - this.txController.blockTracker.on('block', update) + this.blockTracker.on('block', update) } async _getBalance () { diff --git a/app/scripts/controllers/computed-balances.js b/app/scripts/controllers/computed-balances.js index 576746164..2b27d128d 100644 --- a/app/scripts/controllers/computed-balances.js +++ b/app/scripts/controllers/computed-balances.js @@ -5,9 +5,10 @@ const BalanceController = require('./balance') class ComputedbalancesController { constructor (opts = {}) { - const { accountTracker, txController } = opts + const { accountTracker, txController, blockTracker } = opts this.accountTracker = accountTracker this.txController = txController + this.blockTracker = blockTracker const initState = extend({ computedBalances: {}, @@ -50,6 +51,7 @@ class ComputedbalancesController { address, accountTracker: this.accountTracker, txController: this.txController, + blockTracker: this.blockTracker, }) updater.store.subscribe((accountBalance) => { let newState = this.store.getState() -- cgit v1.2.3 From feed9a5a17f89ee319dc5558634e8c5c07b2ce65 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 25 Sep 2017 14:45:28 -0700 Subject: Add mock random value generator --- test/lib/mock-encryptor.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/lib/mock-encryptor.js b/test/lib/mock-encryptor.js index cdf13c507..ef229a82f 100644 --- a/test/lib/mock-encryptor.js +++ b/test/lib/mock-encryptor.js @@ -29,4 +29,8 @@ module.exports = { return 'WHADDASALT!' }, + getRandomValues () { + return 'SOO RANDO!!!1' + } + } -- cgit v1.2.3 From 1968d61431183300298f3ea17b5092865cb915bf Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 25 Sep 2017 15:23:37 -0700 Subject: Make encryptor configurable for keyring-controller --- app/scripts/keyring-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js index 2a1af6e29..34e008ec4 100644 --- a/app/scripts/keyring-controller.js +++ b/app/scripts/keyring-controller.js @@ -37,7 +37,7 @@ class KeyringController extends EventEmitter { }) this.accountTracker = opts.accountTracker - this.encryptor = encryptor + this.encryptor = opts.encryptor || encryptor this.keyrings = [] this.getNetwork = opts.getNetwork } -- cgit v1.2.3 From 171d38c8dbbe78c206e57c5d838ab672ef61f23b Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 20 Sep 2017 20:34:03 -0230 Subject: Handles errors with to field and renders warnings from backend in send token. --- ui/app/components/send-token/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/send-token/index.js b/ui/app/components/send-token/index.js index dd8ca6b9d..8dd277f1d 100644 --- a/ui/app/components/send-token/index.js +++ b/ui/app/components/send-token/index.js @@ -346,7 +346,7 @@ SendTokenScreen.prototype.render = function () { this.renderAmountInput(), this.renderGasInput(), this.renderMemoInput(), - warning && h('div.send-screen-input-wrapper--error', + warning && h('div.send-screen-input-wrapper--error', {}, h('div.send-screen-input-wrapper__error-message', [ warning, ]) -- cgit v1.2.3 From a1d87b821b7e0a444257065d284b13f98e4d3173 Mon Sep 17 00:00:00 2001 From: Dan Date: Mon, 25 Sep 2017 21:45:51 -0230 Subject: Update send token to handle errors onBlur events and prevent clicking send until all errors handled --- ui/app/components/send-token/index.js | 71 +++++++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 12 deletions(-) diff --git a/ui/app/components/send-token/index.js b/ui/app/components/send-token/index.js index 8dd277f1d..166088898 100644 --- a/ui/app/components/send-token/index.js +++ b/ui/app/components/send-token/index.js @@ -5,7 +5,7 @@ const classnames = require('classnames') const inherits = require('util').inherits const actions = require('../../actions') const selectors = require('../../selectors') -const { isValidAddress } = require('../../util') +const { isValidAddress, allNull } = require('../../util') // const BalanceComponent = require('./balance-component') const Identicon = require('../identicon') @@ -65,7 +65,8 @@ function SendTokenScreen () { Component.call(this) this.state = { to: '', - amount: '', + amount: '0x0', + amountToSend: '0x0', selectedCurrency: 'USD', isGasTooltipOpen: false, gasPrice: '0x5d21dba00', @@ -113,6 +114,46 @@ SendTokenScreen.prototype.validate = function () { } } +SendTokenScreen.prototype.setErrorsFor = function (field) { + const { balance, selectedToken } = this.props + const { errors: previousErrors } = this.state + + const { + isValid, + errors: newErrors + } = this.validate() + + const nextErrors = Object.assign({}, previousErrors, { + [field]: newErrors[field] || null + }) + + if (!isValid) { + this.setState({ + errors: nextErrors, + isValid, + }) + } +} + +SendTokenScreen.prototype.clearErrorsFor = function (field) { + const { errors: previousErrors } = this.state + const nextErrors = Object.assign({}, previousErrors, { + [field]: null + }) + + this.setState({ + errors: nextErrors, + isValid: allNull(nextErrors), + }) +} + +SendTokenScreen.prototype.getAmountToSend = function (amount, selectedToken) { + const { decimals } = selectedToken || {} + const multiplier = Math.pow(10, Number(decimals || 0)) + const sendAmount = Number(amount * multiplier).toString(16) + return sendAmount +} + SendTokenScreen.prototype.submit = function () { const { to, @@ -132,11 +173,6 @@ SendTokenScreen.prototype.submit = function () { } = this.props const { nickname = ' ' } = identities[to] || {} - const { isValid, errors } = this.validate() - - if (!isValid) { - return this.setState({ errors }) - } hideWarning() addToAddressBook(to, nickname) @@ -148,9 +184,7 @@ SendTokenScreen.prototype.submit = function () { gasPrice: gasPrice, } - const { decimals } = selectedToken || {} - const multiplier = Math.pow(10, Number(decimals || 0)) - const sendAmount = Number(amount * multiplier).toString(16) + const sendAmount = this.getAmountToSend(amount, selectedToken) signTokenTx(selectedTokenAddress, to, sendAmount, txParams) } @@ -181,6 +215,8 @@ SendTokenScreen.prototype.renderToAddressInput = function () { to: e.target.value, errors: {}, }), + onBlur: () => this.setErrorsFor('to'), + onFocus: () => this.clearErrorsFor('to'), }), h('datalist#addresses', [ // Corresponds to the addresses owned. @@ -234,8 +270,9 @@ SendTokenScreen.prototype.renderAmountInput = function () { value: amount, onChange: e => this.setState({ amount: e.target.value, - errors: {}, }), + onBlur: () => this.setErrorsFor('amount'), + onFocus: () => this.clearErrorsFor('amount'), }), h('div.send-screen-input-wrapper__error-message', [ errorMessage ]), ]) @@ -272,6 +309,14 @@ SendTokenScreen.prototype.renderGasInput = function () { onFeeChange: ({ gasLimit, gasPrice }) => { this.setState({ gasLimit, gasPrice, errors: {} }) }, + onBlur: () => { + this.setErrorsFor('gasLimit') + this.setErrorsFor('gasPrice') + }, + onFocus: () => { + this.clearErrorsFor('gasLimit') + this.clearErrorsFor('gasPrice') + }, }), h('div.send-screen-gas-labels', {}, [ @@ -311,10 +356,12 @@ SendTokenScreen.prototype.renderMemoInput = function () { SendTokenScreen.prototype.renderButtons = function () { const { selectedAddress, backToAccountDetail } = this.props + const { isValid } = this.validate() return h('div.send-token__button-group', [ h('button.send-token__button-next.btn-secondary', { - onClick: () => this.submit(), + className: !isValid && 'send-screen__send-button__disabled', + onClick: () => isValid && this.submit(), }, ['Next']), h('button.send-token__button-cancel.btn-tertiary', { onClick: () => backToAccountDetail(selectedAddress), -- cgit v1.2.3 From 88c4226bf1dca8647a45f3921396daaa88bbf939 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 22 Sep 2017 01:45:26 -0230 Subject: Estimate gasPrice and gasLimit in send screen. --- ui/app/actions.js | 57 +++++++++++++++++++++++++++++++++++++++++++++ ui/app/reducers/metamask.js | 22 +++++++++++++++++ ui/app/send.js | 54 ++++++++++++++++++++++++++++++++++++------ 3 files changed, 126 insertions(+), 7 deletions(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index 0a2b4a636..a43809fc0 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -129,6 +129,17 @@ var actions = { cancelAllTx: cancelAllTx, viewPendingTx: viewPendingTx, VIEW_PENDING_TX: 'VIEW_PENDING_TX', + // send screen + estimateGas, + updateGasEstimate, + UPDATE_GAS_ESTIMATE: 'UPDATE_GAS_ESTIMATE', + updateGasPrice, + UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE', + getGasPrice, + CLEAR_GAS_ESTIMATE: 'CLEAR_GAS_ESTIMATE', + CLEAR_GAS_PRICE: 'CLEAR_GAS_PRICE', + clearGasEstimate, + clearGasPrice, // app messages confirmSeedWords: confirmSeedWords, showAccountDetail: showAccountDetail, @@ -449,6 +460,26 @@ function signTx (txData) { } } +function estimateGas ({ to, amount }) { + return (dispatch) => { + global.ethQuery.estimateGas({ to, amount }, (err, data) => { + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.hideWarning()) + dispatch(actions.updateGasEstimate(data)) + }) + } +} + +function getGasPrice () { + return (dispatch) => { + global.ethQuery.gasPrice((err, data) => { + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.hideWarning()) + dispatch(actions.updateGasPrice(data)) + }) + } +} + function sendTx (txData) { log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`) return (dispatch) => { @@ -506,6 +537,32 @@ function txError (err) { } } +function updateGasEstimate (gas) { + return { + type: actions.UPDATE_GAS_ESTIMATE, + value: gas, + } +} + +function clearGasEstimate () { + return { + type: actions.CLEAR_GAS_ESTIMATE, + } +} + +function updateGasPrice (gasPrice) { + return { + type: actions.UPDATE_GAS_PRICE, + value: gasPrice, + } +} + +function clearGasPrice () { + return { + type: actions.CLEAR_GAS_PRICE, + } +} + function cancelMsg (msgData) { log.debug(`background.cancelMessage`) background.cancelMessage(msgData.id) diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index cdc98d05e..e78f51f3a 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -19,6 +19,8 @@ function reduceMetamask (state, action) { addressBook: [], selectedTokenAddress: null, tokenExchangeRates: {}, + estimatedGas: null, + blockGasPrice: null, }, state.metamask) switch (action.type) { @@ -74,6 +76,26 @@ function reduceMetamask (state, action) { }, }) + case actions.UPDATE_GAS_ESTIMATE: + return extend(metamaskState, { + estimatedGas: action.value, + }) + + case actions.UPDATE_GAS_PRICE: + return extend(metamaskState, { + blockGasPrice: action.value, + }) + + case actions.CLEAR_GAS_ESTIMATE: + return extend(metamaskState, { + estimatedGas: null, + }) + + case actions.CLEAR_GAS_PRICE: + return extend(metamaskState, { + blockGasPrice: null, + }) + case actions.COMPLETED_TX: var stringId = String(action.id) newState = extend(metamaskState, { diff --git a/ui/app/send.js b/ui/app/send.js index 16fe470be..4ce7fc475 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -16,6 +16,10 @@ const { hideWarning, addToAddressBook, signTx, + estimateGas, + getGasPrice, + clearGasEstimate, + clearGasPrice, } = require('./actions') const { stripHexPrefix, addHexPrefix } = require('ethereumjs-util') const { isHex, numericBalance, isValidAddress, allNull } = require('./util') @@ -33,6 +37,8 @@ function mapStateToProps (state) { addressBook, conversionRate, currentBlockGasLimit: blockGasLimit, + estimatedGas, + blockGasPrice, } = state.metamask const { warning } = state.appState const selectedIdentity = getSelectedIdentity(state) @@ -46,6 +52,8 @@ function mapStateToProps (state) { addressBook, conversionRate, blockGasLimit, + blockGasPrice, + estimatedGas, warning, selectedIdentity, error: warning && warning.split('.')[0], @@ -67,8 +75,11 @@ function SendTransactionScreen () { to: '', amount: 0, amountToSend: '0x0', - gasPrice: '0x5d21dba00', - gas: '0x7b0d', + gasPrice: null, + gas: null, + amount: '0x0', + gasPrice: null, + gas: null, txData: null, memo: '', }, @@ -87,6 +98,7 @@ function SendTransactionScreen () { this.getAmountToSend = this.getAmountToSend.bind(this) this.setErrorsFor = this.setErrorsFor.bind(this) this.clearErrorsFor = this.clearErrorsFor.bind(this) + this.estimateGasAndPrice = this.estimateGasAndPrice.bind(this) this.renderFromInput = this.renderFromInput.bind(this) this.renderToInput = this.renderToInput.bind(this) @@ -96,6 +108,11 @@ function SendTransactionScreen () { this.renderErrorMessage = this.renderErrorMessage.bind(this) } +SendTransactionScreen.prototype.componentWillMount = function() { + this.props.dispatch(clearGasEstimate()) + this.props.dispatch(clearGasPrice()) +} + SendTransactionScreen.prototype.renderErrorMessage = function(errorType, warning) { const { errors } = this.state const errorMessage = errors[errorType]; @@ -159,7 +176,10 @@ SendTransactionScreen.prototype.renderToInput = function (to, identities, addres }, }) }, - onBlur: () => this.setErrorsFor('to'), + onBlur: () => { + this.setErrorsFor('to') + this.estimateGasAndPrice() + }, onFocus: () => this.clearErrorsFor('to'), }), @@ -212,7 +232,10 @@ SendTransactionScreen.prototype.renderAmountInput = function (activeCurrency) { ), }) }, - onBlur: () => this.setErrorsFor('amount'), + onBlur: () => { + this.setErrorsFor('amount') + this.estimateGasAndPrice() + }, onFocus: () => this.clearErrorsFor('amount'), }), @@ -293,6 +316,8 @@ SendTransactionScreen.prototype.render = function () { identities, addressBook, conversionRate, + estimatedGas, + blockGasPrice, } = props const { blockGasLimit, newTx, activeCurrency, isValid } = this.state @@ -316,7 +341,13 @@ SendTransactionScreen.prototype.render = function () { this.renderAmountInput(activeCurrency), - this.renderGasInput(gasPrice, gas, activeCurrency, conversionRate, blockGasLimit), + this.renderGasInput( + gasPrice || blockGasPrice || '0x0', + gas || estimatedGas || '0x0', + activeCurrency, + conversionRate, + blockGasLimit + ), this.renderMemoInput(), @@ -351,6 +382,15 @@ SendTransactionScreen.prototype.setActiveCurrency = function (newCurrency) { this.setState({ activeCurrency: newCurrency }) } +SendTransactionScreen.prototype.estimateGasAndPrice = function () { + const { errors, sendAmount, newTx } = this.state + + if (!errors.to && !errors.amount && newTx.amount > 0) { + this.props.dispatch(getGasPrice()) + this.props.dispatch(estimateGas({ to: newTx.to, amount: sendAmount })) + } +} + SendTransactionScreen.prototype.back = function () { var address = this.props.address this.props.dispatch(backToAccountDetail(address)) @@ -471,7 +511,7 @@ SendTransactionScreen.prototype.clearErrorsFor = function (field) { SendTransactionScreen.prototype.onSubmit = function (event) { event.preventDefault() - const { warning, balance, amountToSend } = this.props + const { warning, balance } = this.props const state = this.state || {} const recipient = state.newTx.to @@ -489,7 +529,7 @@ SendTransactionScreen.prototype.onSubmit = function (event) { from: this.state.newTx.from, to: this.state.newTx.to, - value: amountToSend, + value: this.state.newTx.amountToSend, gas: this.state.newTx.gas, gasPrice: this.state.newTx.gasPrice, -- cgit v1.2.3 From 79bcb88db3946260c832402d97e0c800cdeba5a9 Mon Sep 17 00:00:00 2001 From: Dan Date: Mon, 25 Sep 2017 21:00:32 -0230 Subject: Refactor to store estimated gas and price in local state, return promise from actions. --- ui/app/actions.js | 60 ++++++++++++++------------------------------- ui/app/reducers/metamask.js | 22 ----------------- ui/app/send.js | 37 +++++++++++++++------------- 3 files changed, 38 insertions(+), 81 deletions(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index a43809fc0..a0dbbbf11 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -131,15 +131,7 @@ var actions = { VIEW_PENDING_TX: 'VIEW_PENDING_TX', // send screen estimateGas, - updateGasEstimate, - UPDATE_GAS_ESTIMATE: 'UPDATE_GAS_ESTIMATE', - updateGasPrice, - UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE', getGasPrice, - CLEAR_GAS_ESTIMATE: 'CLEAR_GAS_ESTIMATE', - CLEAR_GAS_PRICE: 'CLEAR_GAS_PRICE', - clearGasEstimate, - clearGasPrice, // app messages confirmSeedWords: confirmSeedWords, showAccountDetail: showAccountDetail, @@ -462,20 +454,30 @@ function signTx (txData) { function estimateGas ({ to, amount }) { return (dispatch) => { - global.ethQuery.estimateGas({ to, amount }, (err, data) => { - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.hideWarning()) - dispatch(actions.updateGasEstimate(data)) + return new Promise((resolve, reject) => { + global.ethQuery.estimateGas({ to, amount }, (err, data) => { + if (err) { + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + dispatch(actions.hideWarning()) + return resolve(data) + }) }) } } function getGasPrice () { return (dispatch) => { - global.ethQuery.gasPrice((err, data) => { - if (err) return dispatch(actions.displayWarning(err.message)) - dispatch(actions.hideWarning()) - dispatch(actions.updateGasPrice(data)) + return new Promise((resolve, reject) => { + global.ethQuery.gasPrice((err, data) => { + if (err) { + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + dispatch(actions.hideWarning()) + return resolve(data) + }) }) } } @@ -537,32 +539,6 @@ function txError (err) { } } -function updateGasEstimate (gas) { - return { - type: actions.UPDATE_GAS_ESTIMATE, - value: gas, - } -} - -function clearGasEstimate () { - return { - type: actions.CLEAR_GAS_ESTIMATE, - } -} - -function updateGasPrice (gasPrice) { - return { - type: actions.UPDATE_GAS_PRICE, - value: gasPrice, - } -} - -function clearGasPrice () { - return { - type: actions.CLEAR_GAS_PRICE, - } -} - function cancelMsg (msgData) { log.debug(`background.cancelMessage`) background.cancelMessage(msgData.id) diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index e78f51f3a..cdc98d05e 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -19,8 +19,6 @@ function reduceMetamask (state, action) { addressBook: [], selectedTokenAddress: null, tokenExchangeRates: {}, - estimatedGas: null, - blockGasPrice: null, }, state.metamask) switch (action.type) { @@ -76,26 +74,6 @@ function reduceMetamask (state, action) { }, }) - case actions.UPDATE_GAS_ESTIMATE: - return extend(metamaskState, { - estimatedGas: action.value, - }) - - case actions.UPDATE_GAS_PRICE: - return extend(metamaskState, { - blockGasPrice: action.value, - }) - - case actions.CLEAR_GAS_ESTIMATE: - return extend(metamaskState, { - estimatedGas: null, - }) - - case actions.CLEAR_GAS_PRICE: - return extend(metamaskState, { - blockGasPrice: null, - }) - case actions.COMPLETED_TX: var stringId = String(action.id) newState = extend(metamaskState, { diff --git a/ui/app/send.js b/ui/app/send.js index 4ce7fc475..033692910 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -18,8 +18,6 @@ const { signTx, estimateGas, getGasPrice, - clearGasEstimate, - clearGasPrice, } = require('./actions') const { stripHexPrefix, addHexPrefix } = require('ethereumjs-util') const { isHex, numericBalance, isValidAddress, allNull } = require('./util') @@ -52,8 +50,6 @@ function mapStateToProps (state) { addressBook, conversionRate, blockGasLimit, - blockGasPrice, - estimatedGas, warning, selectedIdentity, error: warning && warning.split('.')[0], @@ -73,16 +69,15 @@ function SendTransactionScreen () { newTx: { from: '', to: '', - amount: 0, amountToSend: '0x0', gasPrice: null, gas: null, amount: '0x0', - gasPrice: null, - gas: null, txData: null, memo: '', }, + blockGasPrice: null, + estimatedGas: null, activeCurrency: 'USD', tooltipIsOpen: false, errors: {}, @@ -108,11 +103,6 @@ function SendTransactionScreen () { this.renderErrorMessage = this.renderErrorMessage.bind(this) } -SendTransactionScreen.prototype.componentWillMount = function() { - this.props.dispatch(clearGasEstimate()) - this.props.dispatch(clearGasPrice()) -} - SendTransactionScreen.prototype.renderErrorMessage = function(errorType, warning) { const { errors } = this.state const errorMessage = errors[errorType]; @@ -316,11 +306,16 @@ SendTransactionScreen.prototype.render = function () { identities, addressBook, conversionRate, - estimatedGas, - blockGasPrice, } = props - const { blockGasLimit, newTx, activeCurrency, isValid } = this.state + const { + blockGasLimit, + newTx, + activeCurrency, + isValid, + blockGasPrice, + estimatedGas, + } = this.state const { gas, gasPrice } = newTx return ( @@ -386,8 +381,16 @@ SendTransactionScreen.prototype.estimateGasAndPrice = function () { const { errors, sendAmount, newTx } = this.state if (!errors.to && !errors.amount && newTx.amount > 0) { - this.props.dispatch(getGasPrice()) - this.props.dispatch(estimateGas({ to: newTx.to, amount: sendAmount })) + Promise.all([ + this.props.dispatch(getGasPrice()), + this.props.dispatch(estimateGas({ to: newTx.to, amount: sendAmount })), + ]) + .then(([blockGasPrice, estimatedGas]) => { + this.setState({ + blockGasPrice, + estimatedGas, + }) + }) } } -- cgit v1.2.3 From fc3e071ad699f6c3586a20f12e06e6f456b0eea3 Mon Sep 17 00:00:00 2001 From: Dan Date: Mon, 25 Sep 2017 22:09:10 -0230 Subject: Send token now estimates gas and price. --- ui/app/components/send-token/index.js | 45 ++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/ui/app/components/send-token/index.js b/ui/app/components/send-token/index.js index 166088898..e4f13a874 100644 --- a/ui/app/components/send-token/index.js +++ b/ui/app/components/send-token/index.js @@ -57,6 +57,9 @@ function mapDispatchToProps (dispatch) { dispatch(actions.signTokenTx(tokenAddress, toAddress, amount, txData)) ), updateTokenExchangeRate: token => dispatch(actions.updateTokenExchangeRate(token)), + estimateGas: ({ to, amount }) => dispatch(actions.estimateGas({ to, amount })), + getGasPrice: () => dispatch(actions.getGasPrice()), + } } @@ -69,8 +72,8 @@ function SendTokenScreen () { amountToSend: '0x0', selectedCurrency: 'USD', isGasTooltipOpen: false, - gasPrice: '0x5d21dba00', - gasLimit: '0x7b0d', + gasPrice: null, + gasLimit: null, errors: {}, } } @@ -84,6 +87,24 @@ SendTokenScreen.prototype.componentWillMount = function () { updateTokenExchangeRate(symbol) } +SendTokenScreen.prototype.estimateGasAndPrice = function () { + const { selectedToken } = this.props + const { errors, amount, to } = this.state + + if (!errors.to && !errors.amount && amount > 0) { + Promise.all([ + this.props.getGasPrice(), + this.props.estimateGas({ to, amount: this.getAmountToSend(amount, selectedToken) }), + ]) + .then(([blockGasPrice, estimatedGas]) => { + this.setState({ + blockGasPrice, + estimatedGas, + }) + }) + } +} + SendTokenScreen.prototype.validate = function () { const { to, @@ -215,7 +236,10 @@ SendTokenScreen.prototype.renderToAddressInput = function () { to: e.target.value, errors: {}, }), - onBlur: () => this.setErrorsFor('to'), + onBlur: () => { + this.setErrorsFor('to') + this.estimateGasAndPrice() + }, onFocus: () => this.clearErrorsFor('to'), }), h('datalist#addresses', [ @@ -271,7 +295,10 @@ SendTokenScreen.prototype.renderAmountInput = function () { onChange: e => this.setState({ amount: e.target.value, }), - onBlur: () => this.setErrorsFor('amount'), + onBlur: () => { + this.setErrorsFor('amount') + this.estimateGasAndPrice() + }, onFocus: () => this.clearErrorsFor('amount'), }), h('div.send-screen-input-wrapper__error-message', [ errorMessage ]), @@ -283,6 +310,8 @@ SendTokenScreen.prototype.renderGasInput = function () { isGasTooltipOpen, gasPrice, gasLimit, + blockGasPrice, + estimatedGas, selectedCurrency, errors: { gasPrice: gasPriceErrorMessage, @@ -303,8 +332,8 @@ SendTokenScreen.prototype.renderGasInput = function () { }, [ isGasTooltipOpen && h(GasTooltip, { className: 'send-tooltip', - gasPrice, - gasLimit, + gasPrice: gasPrice || blockGasPrice || '0x0', + gasLimit: gasLimit || estimatedGas || '0x0', onClose: () => this.setState({ isGasTooltipOpen: false }), onFeeChange: ({ gasLimit, gasPrice }) => { this.setState({ gasLimit, gasPrice, errors: {} }) @@ -327,9 +356,9 @@ SendTokenScreen.prototype.renderGasInput = function () { h(GasFeeDisplay, { conversionRate, tokenExchangeRate, - gasPrice, + gasPrice: gasPrice || blockGasPrice || '0x0', activeCurrency: selectedCurrency, - gas: gasLimit, + gas: gasLimit || estimatedGas || '0x0', blockGasLimit: currentBlockGasLimit, }), h( -- cgit v1.2.3 From 2c474b0d6e487652cf16e224e19deb0bf68abedb Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 20 Sep 2017 05:36:31 -0230 Subject: Export private key modal body ui. --- .../components/modals/export-private-key-modal.js | 22 +++++++ ui/app/css/itcss/components/modal.scss | 69 ++++++++++++++++++++++ ui/app/css/itcss/components/sections.scss | 2 +- 3 files changed, 92 insertions(+), 1 deletion(-) diff --git a/ui/app/components/modals/export-private-key-modal.js b/ui/app/components/modals/export-private-key-modal.js index bbcd25e0d..28b988f5a 100644 --- a/ui/app/components/modals/export-private-key-modal.js +++ b/ui/app/components/modals/export-private-key-modal.js @@ -37,5 +37,27 @@ ExportPrivateKeyModal.prototype.render = function () { h('div.account-modal-divider'), + h('span.modal-body-title', 'Download Private Keys'), + + h('div.private-key-password', {}, [ + h('span.private-key-password-label', 'Type Your Password'), + + h('input.private-key-password-input', { + type: 'password', + placeholder: 'Type password', + }), + ]), + + h('div.private-key-password-warning', `Warning: Never disclose this key. + Anyone with your private keys can take steal any assets held in your + account.` + ), + + h('div.export-private-key-buttons', {}, [ + h('button.btn-clear.btn-cancel', {}, 'Cancel'), + + h('button.btn-clear', 'Download'), + ]), + ]) } diff --git a/ui/app/css/itcss/components/modal.scss b/ui/app/css/itcss/components/modal.scss index 0afd778e9..9cdfdec8f 100644 --- a/ui/app/css/itcss/components/modal.scss +++ b/ui/app/css/itcss/components/modal.scss @@ -265,6 +265,75 @@ background-color: $alto; } +// Export Private Key Modal + +.account-modal-container .account-name { + margin-top: 9px; + font-size: 20px; +} + +.account-modal-container .modal-body-title { + margin-top: 16px; + margin-bottom: 16px; + font-size: 18px; +} + +.private-key-password { + display: flex; + flex-direction: column; +} + +.private-key-password-label { + color: $scorpion; + font-size: 14px; + line-height: 18px; + margin-bottom: 10px; +} + +.private-key-password-input { + padding: 10px 0 13px 17px; + font-size: 16px; + line-height: 21px; + width: 291px; + height: 44px; +} + +.private-key-password::-webkit-input-placeholder { + color: $dusty-gray; + font-family: 'Montserrat UltraLight'; +} + +.private-key-password-warning { + border-radius: 8px; + background-color: #FFF6F6; + font-size: 12px; + font-weight: 500; + line-height: 15px; + color: $crimson; + width: 292px; + padding: 9px 15px; + margin-top: 18px; + font-family: 'Montserrat Regular'; +} + +.export-private-key-buttons { + display: flex; + flex-direction: row; + justify-content: center; + + .btn-clear { + width: 141px; + height: 54px; + } + + .btn-cancel { + margin-right: 15px; + border-color: $dusty-gray; + color: $scorpion; + } +} + + // New Account Modal .new-account-modal-wrapper { display: flex; diff --git a/ui/app/css/itcss/components/sections.scss b/ui/app/css/itcss/components/sections.scss index 44ec3e862..5c32976a7 100644 --- a/ui/app/css/itcss/components/sections.scss +++ b/ui/app/css/itcss/components/sections.scss @@ -446,7 +446,7 @@ textarea.twelve-word-phrase { color: $white; } -.qr-ellip-address { +.qr-ellip-address, .ellip-address { overflow: hidden; text-overflow: ellipsis; } -- cgit v1.2.3 From eae40e054418c195310224194d9435ccfbf14e46 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 22 Sep 2017 05:58:43 -0230 Subject: Able to change selections in to and from fields of send and send token. --- ui/app/components/send-token/index.js | 1 + ui/app/send.js | 20 ++++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/ui/app/components/send-token/index.js b/ui/app/components/send-token/index.js index dd8ca6b9d..cc77c2699 100644 --- a/ui/app/components/send-token/index.js +++ b/ui/app/components/send-token/index.js @@ -181,6 +181,7 @@ SendTokenScreen.prototype.renderToAddressInput = function () { to: e.target.value, errors: {}, }), + onFocus: () => to && this.setState({ to: '' }), }), h('datalist#addresses', [ // Corresponds to the addresses owned. diff --git a/ui/app/send.js b/ui/app/send.js index 16fe470be..8fab8a384 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -123,7 +123,15 @@ SendTransactionScreen.prototype.renderFromInput = function (from, identities) { }) }, onBlur: () => this.setErrorsFor('from'), - onFocus: () => this.clearErrorsFor('from'), + onFocus: () => { + this.clearErrorsFor('from'), + this.state.newTx.from && this.setState({ + newTx: { + ...this.state.newTx, + from: '', + }, + }) + }, }), h('datalist#accounts', [ @@ -160,7 +168,15 @@ SendTransactionScreen.prototype.renderToInput = function (to, identities, addres }) }, onBlur: () => this.setErrorsFor('to'), - onFocus: () => this.clearErrorsFor('to'), + onFocus: () => { + this.clearErrorsFor('to') + this.state.newTx.to && this.setState({ + newTx: { + ...this.state.newTx, + to: '', + }, + }) + }, }), h('datalist#addresses', [ -- cgit v1.2.3 From 56697ea9a4399ecaccf33ae3ae1a42283bdc9dc7 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 22 Sep 2017 18:47:05 -0230 Subject: Select all in to and from of send screens, instead of clearing on focus. --- ui/app/components/send-token/index.js | 2 +- ui/app/send.js | 18 ++++-------------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/ui/app/components/send-token/index.js b/ui/app/components/send-token/index.js index cc77c2699..60fe2ac8b 100644 --- a/ui/app/components/send-token/index.js +++ b/ui/app/components/send-token/index.js @@ -181,7 +181,7 @@ SendTokenScreen.prototype.renderToAddressInput = function () { to: e.target.value, errors: {}, }), - onFocus: () => to && this.setState({ to: '' }), + onFocus: event => to && event.target.select(), }), h('datalist#addresses', [ // Corresponds to the addresses owned. diff --git a/ui/app/send.js b/ui/app/send.js index 8fab8a384..ac1ee0d84 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -123,14 +123,9 @@ SendTransactionScreen.prototype.renderFromInput = function (from, identities) { }) }, onBlur: () => this.setErrorsFor('from'), - onFocus: () => { + onFocus: event => { this.clearErrorsFor('from'), - this.state.newTx.from && this.setState({ - newTx: { - ...this.state.newTx, - from: '', - }, - }) + this.state.newTx.from && event.target.select() }, }), @@ -168,14 +163,9 @@ SendTransactionScreen.prototype.renderToInput = function (to, identities, addres }) }, onBlur: () => this.setErrorsFor('to'), - onFocus: () => { + onFocus: event => { this.clearErrorsFor('to') - this.state.newTx.to && this.setState({ - newTx: { - ...this.state.newTx, - to: '', - }, - }) + this.state.newTx.to && event.target.select() }, }), -- cgit v1.2.3 From 5f6ec6aa982101bed57d9a8766330af71a274183 Mon Sep 17 00:00:00 2001 From: Dan Date: Mon, 25 Sep 2017 20:35:14 -0230 Subject: Remove unnecessary trailing comma. --- ui/app/send.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/send.js b/ui/app/send.js index ac1ee0d84..6c701f982 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -124,7 +124,7 @@ SendTransactionScreen.prototype.renderFromInput = function (from, identities) { }, onBlur: () => this.setErrorsFor('from'), onFocus: event => { - this.clearErrorsFor('from'), + this.clearErrorsFor('from') this.state.newTx.from && event.target.select() }, }), -- cgit v1.2.3 From ff6f7b52e4b5397daef74695894e0cdad22fe273 Mon Sep 17 00:00:00 2001 From: tmashuang Date: Mon, 25 Sep 2017 19:47:03 -0700 Subject: Clean up transactionController tests --- test/unit/tx-controller-test.js | 124 +++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 71 deletions(-) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index f6b41f2bb..13056417c 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -2,12 +2,9 @@ const assert = require('assert') const ethUtil = require('ethereumjs-util') const EthTx = require('ethereumjs-tx') const ObservableStore = require('obs-store') -const clone = require('clone') const sinon = require('sinon') const TransactionController = require('../../app/scripts/controllers/transactions') const TxGasUtils = require('../../app/scripts/lib/tx-gas-utils') -const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper') -const TxStateManager = require('../../app/scripts/lib/tx-state-manager') const { createStubedProvider } = require('../stub/provider') const noop = () => true @@ -17,7 +14,7 @@ const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f3 describe('Transaction Controller', function () { - let txController, engine, provider, providerResultStub + let txController, provider, providerResultStub beforeEach(function () { providerResultStub = {} @@ -81,11 +78,18 @@ describe('Transaction Controller', function () { 'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d', } txController.txStateManager._saveTxList([ + {id: 0, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams}, {id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams}, {id: 2, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams}, - {id: 3, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams}, + {id: 3, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams}, + {id: 4, status: 'rejected', metamaskNetworkId: currentNetworkId, txParams}, + {id: 5, status: 'approved', metamaskNetworkId: currentNetworkId, txParams}, + {id: 6, status: 'signed', metamaskNetworkId: currentNetworkId, txParams}, + {id: 7, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams}, + {id: 8, status: 'failed', metamaskNetworkId: currentNetworkId, txParams}, ]) }) + it('should return the number of confirmed txs', function () { assert.equal(txController.nonceTracker.getConfirmedTransactions(address).length, 3) }) @@ -98,7 +102,7 @@ describe('Transaction Controller', function () { txParams = { 'from': '0xc684832530fcbddae4b4230a47e991ddcec2831d', 'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d', - }, + } txMeta = { status: 'unapproved', id: 1, @@ -306,98 +310,76 @@ describe('Transaction Controller', function () { }) }) - describe('#getChainId', function () { - it('returns 0 when the chainId is NaN', async function () { - txController.networkStore = new ObservableStore('hello') - assert.equal(txController.getChainId(), 0) - }) - }) - - describe('#publishTransaction', async function () { + describe('#updateAndApproveTransaction', function () { + let txMeta beforeEach(function () { - const txMeta = [ - { id: 0, status: 'unapproved', txParams: { from: '0x1678a085c290ebd122dc42cba69373b5953b831d', nonce: '0x1', value: '0xfffff' }, rawTx: 'f8498080808080801ca00255b75b550cf112e18fd699f27d043d85348c29d7e8fd234799db890f7c272da0238121aed40c3141e63aae5335aaa3c9711d4907f28071f81f8d055b9f8435e0', metamaskNetworkId: currentNetworkId }, - ] + txMeta = { + id: 1, + status: 'unapproved', + txParams: { + from: '0xc684832530fcbddae4b4230a47e991ddcec2831d', + to: '0x1678a085c290ebd122dc42cba69373b5953b831d', + gasPrice: '0x77359400', + gas: '0x7b0d', + nonce: '0x4b', + }, + metamaskNetworkId: currentNetworkId, + } + }) + it('should update and approve transactions', function () { txController.txStateManager.addTx(txMeta) + txController.updateAndApproveTransaction(txMeta) + const tx = txController.txStateManager.getTx(1) + assert.equal(tx.status, 'approved') }) + }) - it('should update rawTx of a transaction', async function () { - txController.publishTransaction(0, 'f84c01808080830fffff801ca0e23554ce6f82186402dcdcfff377793fdfa8a872a5670c0ffc002c9cd2f6827aa02a83dfce20aef4064a55320bda47394043af3cbfa63c4e029c54ba6281dcc70e') + describe('#getChainId', function () { + it('returns 0 when the chainId is NaN', function () { + txController.networkStore = new ObservableStore(NaN) + assert.equal(txController.getChainId(), 0) }) }) describe('#cancelTransaction', function () { beforeEach(function () { - const txMetas = [ - { id: 0, status: 'unapproved', txParams: { }, metamaskNetworkId: currentNetworkId }, - { id: 1, status: 'rejected', txParams: { }, metamaskNetworkId: currentNetworkId }, - { id: 2, status: 'approved', txParams: { }, metamaskNetworkId: currentNetworkId }, - { id: 3, status: 'signed', txParams: { }, metamaskNetworkId: currentNetworkId }, - { id: 4, status: 'submitted', txParams: { }, metamaskNetworkId: currentNetworkId }, - { id: 5, status: 'confirmed', txParams: { }, metamaskNetworkId: currentNetworkId }, - { id: 6, status: 'failed', txParams: { }, metamaskNetworkId: currentNetworkId }, - ] - txMetas.forEach((txMeta) => txController.txStateManager.addTx(txMeta)) + txController.txStateManager._saveTxList([ + { id: 0, status: 'unapproved', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] }, + { id: 1, status: 'rejected', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] }, + { id: 2, status: 'approved', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] }, + { id: 3, status: 'signed', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] }, + { id: 4, status: 'submitted', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] }, + { id: 5, status: 'confirmed', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] }, + { id: 6, status: 'failed', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] }, + ]) }) it('should set the transaction to rejected from unapproved', async function () { await txController.cancelTransaction(0) - assert(txController.txStateManager.getTx(0).status, 'rejected') - }) - - it('should set the transaction to rejected from rejected', async function () { - await txController.cancelTransaction(1) - assert(txController.txStateManager.getTx(1).status, 'rejected') - }) - - it('should set the transaction to rejected from approved', async function () { - await txController.cancelTransaction(2) - assert(txController.txStateManager.getTx(2).status, 'rejected') - }) - - it('should set the transaction to rejected from signed', async function () { - await txController.cancelTransaction(3) - assert(txController.txStateManager.getTx(3).status, 'rejected') - }) - - it('should set the transaction to rejected from submitted', async function () { - await txController.cancelTransaction(4) - assert(txController.txStateManager.getTx(4).status, 'rejected') - }) - - it('should set the transaction to rejected from confirmed', async function () { - await txController.cancelTransaction(5) - assert(txController.txStateManager.getTx(5).status, 'rejected') - }) - - it('should set the transaction to rejected from failed', async function () { - await txController.cancelTransaction(6) - assert(txController.txStateManager.getTx(6).status, 'rejected') + assert.equal(txController.txStateManager.getTx(0).status, 'rejected') }) }) describe('#publishTransaction', function () { - let replaceRawTx, rawTx, hash, txMeta + let hash, txMeta beforeEach(function () { - rawTx = 'f86c808504a817c800827b0d940c62bb85faa3311a998d3aba8098c1235c564966880de0b6b3a7640000802aa08ff665feb887a25d4099e40e11f0fef93ee9608f404bd3f853dd9e84ed3317a6a02ec9d3d1d6e176d4d2593dd760e74ccac753e6a0ea0d00cc9789d0d7ff1f471d' - replaceRawTx = 'f84c01808080830fffff801ca0e23554ce6f82186402dcdcfff377793fdfa8a872a5670c0ffc002c9cd2f6827aa02a83dfce20aef4064a55320bda47394043af3cbfa63c4e029c54ba6281dcc70e' + hash = '0x2a5523c6fa98b47b7d9b6c8320179785150b42a16bcff36b398c5062b65657e8' txMeta = { id: 1, - status: 'approved', + status: 'unapproved', txParams: {}, - rawTx, - hash, metamaskNetworkId: currentNetworkId, } - providerResultStub.eth_sendRawTransaction = '0x2a5523c6fa98b47b7d9b6c8320179785150b42a16bcff36b398c5062b65657e8' + providerResultStub.eth_sendRawTransaction = hash }) + it('should publish a tx, updates the rawTx when provided a one', async function () { txController.txStateManager.addTx(txMeta) - await txController.publishTransaction(txMeta.id, replaceRawTx) - txController.setTxHash(1, '0x2a5523c6fa98b47b7d9b6c8320179785150b42a16bcff36b398c5062b65657e8') - assert.equal(txController.txStateManager.getTx(1).rawTx, replaceRawTx) - assert.notEqual(txController.txStateManager.getTx(1).rawTx, rawTx) + await txController.publishTransaction(txMeta.id) + const publishedTx = txController.txStateManager.getTx(1) + assert.equal(publishedTx.hash, hash) + assert.equal(publishedTx.status, 'submitted') }) }) -- cgit v1.2.3 From e52d52b22ed00b1a761bf57cff54c7276c6450a7 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 26 Sep 2017 08:22:48 +0000 Subject: chore(package): update sinon to version 4.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bcfb6c1ac..09490096b 100644 --- a/package.json +++ b/package.json @@ -196,7 +196,7 @@ "react-addons-test-utils": "^15.5.1", "react-test-renderer": "^15.5.4", "react-testutils-additions": "^15.2.0", - "sinon": "^3.2.0", + "sinon": "^4.0.0", "tape": "^4.5.1", "testem": "^1.10.3", "uglifyify": "^4.0.2", -- cgit v1.2.3 From 7102772c3a4a73d594ccc20e760defa2999f2d3f Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 20 Sep 2017 10:02:18 -0230 Subject: Connect export key modal to state and enable actions. --- .../components/modals/export-private-key-modal.js | 85 ++++++++++++++++++---- ui/app/components/readonly-input.js | 6 +- ui/app/css/itcss/components/modal.scss | 28 ++++++- 3 files changed, 104 insertions(+), 15 deletions(-) diff --git a/ui/app/components/modals/export-private-key-modal.js b/ui/app/components/modals/export-private-key-modal.js index 28b988f5a..b1d551781 100644 --- a/ui/app/components/modals/export-private-key-modal.js +++ b/ui/app/components/modals/export-private-key-modal.js @@ -2,6 +2,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect +const ethUtil = require('ethereumjs-util') const actions = require('../../actions') const AccountModalContainer = require('./account-modal-container') const { getSelectedIdentity } = require('../../selectors') @@ -9,20 +10,83 @@ const ReadOnlyInput = require('../readonly-input') function mapStateToProps (state) { return { + warning: state.appState.warning, + privateKey: state.appState.accountDetail.privateKey, network: state.metamask.network, selectedIdentity: getSelectedIdentity(state), } } +function mapDispatchToProps (dispatch) { + return { + exportAccount: (password, address) => dispatch(actions.exportAccount(password, address)), + hideModal: () => dispatch(actions.hideModal()), + } +} + inherits(ExportPrivateKeyModal, Component) function ExportPrivateKeyModal () { Component.call(this) + + this.state = { + password: '' + } +} + +module.exports = connect(mapStateToProps, mapDispatchToProps)(ExportPrivateKeyModal) + +ExportPrivateKeyModal.prototype.renderPasswordLabel = function (privateKey) { + return h('span.private-key-password-label', privateKey + ? 'This is your private key (click to copy)' + : 'Type Your Password' + ) +} + +ExportPrivateKeyModal.prototype.renderPasswordInput = function (privateKey) { + const plainKey = privateKey && ethUtil.stripHexPrefix(privateKey) + + return privateKey + ? h(ReadOnlyInput, { + wrapperClass: 'private-key-password-display-wrapper', + inputClass: 'private-key-password-display-textarea', + textarea: true, + value: plainKey, + }) + : h('input.private-key-password-input', { + type: 'password', + placeholder: 'Type password', + onChange: event => this.setState({ password: event.target.value }) + }) +} + +ExportPrivateKeyModal.prototype.renderButton = function (className, onClick, label) { + return h('button', { + className, + onClick, + }, label) } -module.exports = connect(mapStateToProps)(ExportPrivateKeyModal) +ExportPrivateKeyModal.prototype.renderButtons = function (privateKey, password, address) { + const { hideModal, exportAccount } = this.props + + return h('div.export-private-key-buttons', {}, [ + !privateKey && this.renderButton('btn-clear btn-cancel', () => hideModal(), 'Cancel'), + + (privateKey + ? this.renderButton('btn-clear', () => hideModal(), 'Done') + : this.renderButton('btn-clear', () => exportAccount(this.state.password, address), 'Download') + ), + + ]) +} ExportPrivateKeyModal.prototype.render = function () { - const { selectedIdentity, network } = this.props + const { + selectedIdentity, + network, + privateKey, + warning, + } = this.props const { name, address } = selectedIdentity return h(AccountModalContainer, {}, [ @@ -31,7 +95,7 @@ ExportPrivateKeyModal.prototype.render = function () { h(ReadOnlyInput, { wrapperClass: 'ellip-address-wrapper', - inputClass: 'ellip-address', + inputClass: 'qr-ellip-address ellip-address', value: address, }), @@ -40,12 +104,11 @@ ExportPrivateKeyModal.prototype.render = function () { h('span.modal-body-title', 'Download Private Keys'), h('div.private-key-password', {}, [ - h('span.private-key-password-label', 'Type Your Password'), + this.renderPasswordLabel(privateKey), + + this.renderPasswordInput(privateKey), - h('input.private-key-password-input', { - type: 'password', - placeholder: 'Type password', - }), + !warning ? null : h('span.private-key-password-error', warning), ]), h('div.private-key-password-warning', `Warning: Never disclose this key. @@ -53,11 +116,7 @@ ExportPrivateKeyModal.prototype.render = function () { account.` ), - h('div.export-private-key-buttons', {}, [ - h('button.btn-clear.btn-cancel', {}, 'Cancel'), - - h('button.btn-clear', 'Download'), - ]), + this.renderButtons(privateKey, this.state.password, address), ]) } diff --git a/ui/app/components/readonly-input.js b/ui/app/components/readonly-input.js index 934cbefae..33b93b5a0 100644 --- a/ui/app/components/readonly-input.js +++ b/ui/app/components/readonly-input.js @@ -14,13 +14,17 @@ ReadOnlyInput.prototype.render = function () { wrapperClass = '', inputClass = '', value, + textarea, } = this.props + const inputType = textarea ? 'textarea' : 'input' + return h('div', {className: wrapperClass}, [ - h('input', { + h(inputType, { className: inputClass, value, readOnly: true, + onFocus: event => event.target.select(), }), ]) } diff --git a/ui/app/css/itcss/components/modal.scss b/ui/app/css/itcss/components/modal.scss index 9cdfdec8f..00b6111f7 100644 --- a/ui/app/css/itcss/components/modal.scss +++ b/ui/app/css/itcss/components/modal.scss @@ -283,13 +283,18 @@ flex-direction: column; } -.private-key-password-label { +.private-key-password-label, .private-key-password-error { color: $scorpion; font-size: 14px; line-height: 18px; margin-bottom: 10px; } +.private-key-password-error { + color: $crimson; + margin-bottom: 0; +} + .private-key-password-input { padding: 10px 0 13px 17px; font-size: 16px; @@ -333,6 +338,27 @@ } } +.private-key-password-display-wrapper { + height: 80px; + width: 291px; + border: 1px solid $silver; + border-radius: 2px; +} + +.private-key-password-display-textarea { + color: $crimson; + font-family: "DIN OT"; + font-size: 16px; + line-height: 21px; + border: none; + height: 75px; + width: 253px; + overflow: hidden; + resize: none; + padding: 9px 13px 8px; + text-transform: uppercase; +} + // New Account Modal .new-account-modal-wrapper { -- cgit v1.2.3 From bdd1e58e552b50ab5d060a1671d29cf3c2fdf7fa Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Tue, 26 Sep 2017 09:15:14 -0700 Subject: Return null if transaction.key is shapeshift --- ui/app/components/tx-list.js | 7 ++++- yarn.lock | 64 +++++++++----------------------------------- 2 files changed, 18 insertions(+), 53 deletions(-) diff --git a/ui/app/components/tx-list.js b/ui/app/components/tx-list.js index f817d03a9..ef5cfa245 100644 --- a/ui/app/components/tx-list.js +++ b/ui/app/components/tx-list.js @@ -47,7 +47,6 @@ TxList.prototype.render = function () { TxList.prototype.renderTransaction = function () { const { txsToRender, conversionRate } = this.props - return txsToRender.length ? txsToRender.map((transaction, i) => this.renderTransactionListItem(transaction, conversionRate)) : [h('div.tx-list-item.tx-list-item--empty', [ 'No Transactions' ])] @@ -55,6 +54,12 @@ TxList.prototype.renderTransaction = function () { // TODO: Consider moving TxListItem into a separate component TxList.prototype.renderTransactionListItem = function (transaction, conversionRate) { + // console.log({transaction}) + // refer to transaction-list.js:line 58 + if (transaction.key === 'shapeshift') { + return null + } + const props = { dateString: formatDate(transaction.time), address: transaction.txParams.to, diff --git a/yarn.lock b/yarn.lock index 078ab75cf..c93751afc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1950,12 +1950,6 @@ cli-width@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" -cli@0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/cli/-/cli-0.4.3.tgz#e6819c8d5faa957f64f98f66a8506268c1d1f17d" - dependencies: - glob ">= 3.1.4" - client-sw-ready-event@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/client-sw-ready-event/-/client-sw-ready-event-3.3.0.tgz#988d1045562b0c228e33d9247a6dd3ed7b276fe3" @@ -2069,7 +2063,7 @@ colors@1.0.3, colors@1.0.x: version "1.0.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" -colors@>=0.6.x, colors@^1.1.0, colors@^1.1.2: +colors@^1.1.0, colors@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" @@ -4221,16 +4215,6 @@ fuse.js@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.1.0.tgz#9062146c471552189b0f678b4f5a155731ae3b3c" -fuse@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/fuse/-/fuse-0.4.0.tgz#2c38eaf888abb0a9ba7960cfe3339d1f3f53f6e6" - dependencies: - colors ">=0.6.x" - jshint "0.9.x" - optimist ">=0.3.5" - uglify-js ">=2.2.x" - underscore ">=1.4.x" - gather-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/gather-stream/-/gather-stream-1.0.0.tgz#b33994af457a8115700d410f317733cbe7a0904b" @@ -4366,24 +4350,24 @@ glob@7.1.1: once "^1.3.0" path-is-absolute "^1.0.0" -"glob@>= 3.1.4", glob@^7.0.0, glob@^7.0.3, glob@^7.0.4, glob@^7.0.5, glob@^7.0.6, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1, glob@~7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" +glob@^5.0.15, glob@^5.0.3, glob@~5.0.0: + version "5.0.15" + resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" dependencies: - fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.0.4" + minimatch "2 || 3" once "^1.3.0" path-is-absolute "^1.0.0" -glob@^5.0.15, glob@^5.0.3, glob@~5.0.0: - version "5.0.15" - resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" +glob@^7.0.0, glob@^7.0.3, glob@^7.0.4, glob@^7.0.5, glob@^7.0.6, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1, glob@~7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: + fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "2 || 3" + minimatch "^3.0.4" once "^1.3.0" path-is-absolute "^1.0.0" @@ -5541,13 +5525,6 @@ jshint-stylish@~2.2.1: string-length "^1.0.0" text-table "^0.2.0" -jshint@0.9.x: - version "0.9.1" - resolved "https://registry.yarnpkg.com/jshint/-/jshint-0.9.1.tgz#ff32ec7f09f84001f7498eeafd63c9e4fbb2dc0e" - dependencies: - cli "0.4.3" - minimatch "0.0.x" - jsmin@1.x: version "1.0.1" resolved "https://registry.yarnpkg.com/jsmin/-/jsmin-1.0.1.tgz#e7bd0dcd6496c3bf4863235bf461a3d98aa3b98c" @@ -6263,10 +6240,6 @@ lru-cache@^4.0.1: pseudomap "^1.0.2" yallist "^2.1.2" -lru-cache@~1.0.2: - version "1.0.6" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-1.0.6.tgz#aa50f97047422ac72543bda177a9c9d018d98452" - lru-queue@0.1: version "0.1.0" resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" @@ -6518,12 +6491,6 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" -minimatch@0.0.x: - version "0.0.5" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.0.5.tgz#96bb490bbd3ba6836bbfac111adf75301b1584de" - dependencies: - lru-cache "~1.0.2" - "minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -7111,7 +7078,7 @@ opener@^1.3.0: version "1.4.3" resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8" -optimist@>=0.3.5, optimist@^0.6.1: +optimist@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" dependencies: @@ -9734,13 +9701,6 @@ uglify-js@1.x: version "1.3.5" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-1.3.5.tgz#4b5bfff9186effbaa888e4c9e94bd9fc4c94929d" -uglify-js@>=2.2.x: - version "3.1.1" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.1.1.tgz#e7144307281a1bc38a9a20715090b546c9f44791" - dependencies: - commander "~2.11.0" - source-map "~0.5.1" - uglify-js@^2.6, uglify-js@^2.8.27: version "2.8.29" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" @@ -9780,7 +9740,7 @@ unc-path-regex@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" -underscore@>=1.4.x, underscore@>=1.8.3, underscore@^1.6.0: +underscore@>=1.8.3, underscore@^1.6.0: version "1.8.3" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" -- cgit v1.2.3 From b46cb3ecb5a3a1b4a197f69960f932d69287aa62 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 26 Sep 2017 09:23:46 -0700 Subject: Fix token precision bug Had fixed this before in the dependency, but hadn't merged in that version bump yet :( Fixes #2162 --- CHANGELOG.md | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e692c58dc..f04136d78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Fix bug that could mis-render token balances when very small. (Not actually included in 3.9.9) + ## 3.10.3 2017-9-21 - Fix bug where metamask-dapp connections are lost on rpc error diff --git a/package.json b/package.json index bcfb6c1ac..5599f2c20 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "eth-query": "^2.1.2", "eth-sig-util": "^1.2.2", "eth-simple-keyring": "^1.1.1", - "eth-token-tracker": "^1.1.3", + "eth-token-tracker": "^1.1.4", "ethereumjs-tx": "^1.3.0", "ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", -- cgit v1.2.3 From 5d300f146a679ba1a639ee9a568e8452c886c736 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 26 Sep 2017 09:38:43 -0700 Subject: Add computed balance to mock state --- development/states/first-time.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/development/states/first-time.json b/development/states/first-time.json index 683a61fdf..b2cc8ef8f 100644 --- a/development/states/first-time.json +++ b/development/states/first-time.json @@ -4,6 +4,7 @@ "isUnlocked": false, "rpcTarget": "https://rawtestrpc.metamask.io/", "identities": {}, + "computedBalances": {}, "frequentRpcList": [], "unapprovedTxs": {}, "currentCurrency": "USD", @@ -48,5 +49,6 @@ "isLoading": false, "warning": null }, - "identities": {} + "identities": {}, + "computedBalances": {} } -- cgit v1.2.3 From 57b5f15265be2ae39ddf538915f8bd57538760b6 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 26 Sep 2017 10:01:16 -0700 Subject: Remove slack link --- CHANGELOG.md | 2 ++ ui/app/info.js | 7 ------- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e692c58dc..334fd4f00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Remove Slack link from info page, since it is a big phishing target. + ## 3.10.3 2017-9-21 - Fix bug where metamask-dapp connections are lost on rpc error diff --git a/ui/app/info.js b/ui/app/info.js index 4c7d4cb4c..24c211c1f 100644 --- a/ui/app/info.js +++ b/ui/app/info.js @@ -126,13 +126,6 @@ InfoScreen.prototype.render = function () { ]), ]), - h('div.fa.fa-slack', [ - h('a.info', { - href: 'http://slack.metamask.io', - target: '_blank', - }, 'Join the conversation on Slack'), - ]), - h('div', [ h('.fa.fa-twitter', [ h('a.info', { -- cgit v1.2.3 From 31e9dcf47063e1be7337d7d06b4c721e0f619951 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Tue, 26 Sep 2017 10:02:49 -0700 Subject: Modify tests for new API. --- test/unit/currency-controller-test.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/unit/currency-controller-test.js b/test/unit/currency-controller-test.js index 5eeaf9bcc..63ab60f9e 100644 --- a/test/unit/currency-controller-test.js +++ b/test/unit/currency-controller-test.js @@ -15,11 +15,11 @@ describe('currency-controller', function () { describe('currency conversions', function () { describe('#setCurrentCurrency', function () { it('should return USD as default', function () { - assert.equal(currencyController.getCurrentCurrency(), 'USD') + assert.equal(currencyController.getCurrentCurrency(), 'usd') }) it('should be able to set to other currency', function () { - assert.equal(currencyController.getCurrentCurrency(), 'USD') + assert.equal(currencyController.getCurrentCurrency(), 'usd') currencyController.setCurrentCurrency('JPY') var result = currencyController.getCurrentCurrency() assert.equal(result, 'JPY') @@ -36,12 +36,12 @@ describe('currency-controller', function () { describe('#updateConversionRate', function () { it('should retrieve an update for ETH to USD and set it in memory', function (done) { this.timeout(15000) - nock('https://api.cryptonator.com') - .get('/api/ticker/eth-USD') - .reply(200, '{"ticker":{"base":"ETH","target":"USD","price":"11.02456145","volume":"44948.91745289","change":"-0.01472534"},"timestamp":1472072136,"success":true,"error":""}') + nock('https://api.infura.io') + .get('/v1/ticker/ethusd') + .reply(200, '{"base": "ETH", "quote": "USD", "bid": 288.45, "ask": 288.46, "volume": 112888.17569277, "exchange": "bitfinex", "total_volume": 272175.00106721005, "num_exchanges": 8, "timestamp": 1506444677}') assert.equal(currencyController.getConversionRate(), 0) - currencyController.setCurrentCurrency('USD') + currencyController.setCurrentCurrency('usd') currencyController.updateConversionRate() .then(function () { var result = currencyController.getConversionRate() @@ -57,14 +57,14 @@ describe('currency-controller', function () { this.timeout(15000) assert.equal(currencyController.getConversionRate(), 0) - nock('https://api.cryptonator.com') - .get('/api/ticker/eth-JPY') - .reply(200, '{"ticker":{"base":"ETH","target":"JPY","price":"11.02456145","volume":"44948.91745289","change":"-0.01472534"},"timestamp":1472072136,"success":true,"error":""}') + nock('https://api.infura.io') + .get('/v1/ticker/ethjpy') + .reply(200, '{"base": "ETH", "quote": "JPY", "bid": 32300.0, "ask": 32400.0, "volume": 247.4616071, "exchange": "kraken", "total_volume": 247.4616071, "num_exchanges": 1, "timestamp": 1506444676}') var promise = new Promise( function (resolve, reject) { - currencyController.setCurrentCurrency('JPY') + currencyController.setCurrentCurrency('jpy') currencyController.updateConversionRate().then(function () { resolve() }) -- cgit v1.2.3 From 88ddedfb5a293cce4f2716729f412f633dff4053 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Tue, 26 Sep 2017 10:09:50 -0700 Subject: Account for undefined currencies. --- ui/app/components/fiat-value.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/app/components/fiat-value.js b/ui/app/components/fiat-value.js index d76b80ab2..d69f41d11 100644 --- a/ui/app/components/fiat-value.js +++ b/ui/app/components/fiat-value.js @@ -13,6 +13,7 @@ function FiatValue () { FiatValue.prototype.render = function () { const props = this.props const { conversionRate, currentCurrency } = props + const renderedCurrency = currentCurrency || '' const value = formatBalance(props.value, 6) @@ -28,7 +29,7 @@ FiatValue.prototype.render = function () { fiatTooltipNumber = 'Unknown' } - return fiatDisplay(fiatDisplayNumber, currentCurrency.toUpperCase()) + return fiatDisplay(fiatDisplayNumber, renderedCurrency.toUpperCase()) } function fiatDisplay (fiatDisplayNumber, fiatSuffix) { -- cgit v1.2.3 From 7b199e215d9767cf059420df750670140d5bc958 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Tue, 26 Sep 2017 10:11:18 -0700 Subject: Polish names on currency list. --- ui/app/infura-conversion.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ui/app/infura-conversion.json b/ui/app/infura-conversion.json index 3103e72a0..f4c6f9e34 100644 --- a/ui/app/infura-conversion.json +++ b/ui/app/infura-conversion.json @@ -85,7 +85,7 @@ }, "quote": { "code": "cad", - "name": "Canadian dollar" + "name": "Canadian Dollar" } }, { @@ -184,7 +184,7 @@ }, "quote": { "code": "gbp", - "name": "Pound sterling" + "name": "Pound Sterling" } }, { @@ -239,7 +239,7 @@ }, "quote": { "code": "jpy", - "name": "Japanese yen" + "name": "Japanese Yen" } }, { @@ -415,7 +415,7 @@ }, "quote": { "code": "rub", - "name": "Russian ruble" + "name": "Russian Ruble" } }, { @@ -514,7 +514,7 @@ }, "quote": { "code": "uah", - "name": "Ukrainian hryvnia" + "name": "Ukrainian Hryvnia" } }, { @@ -525,7 +525,7 @@ }, "quote": { "code": "usd", - "name": "United States dollar" + "name": "United States Dollar" } }, { @@ -558,7 +558,7 @@ }, "quote": { "code": "xlm", - "name": "Stellar lumen" + "name": "Stellar Lumen" } }, { -- cgit v1.2.3 From accee142821884925f6fb8d7c84b5a0390feaf1c Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Tue, 26 Sep 2017 10:35:13 -0700 Subject: Remove old conversion list. --- ui/app/conversion.json | 207 ------------------------------------------------- 1 file changed, 207 deletions(-) delete mode 100644 ui/app/conversion.json diff --git a/ui/app/conversion.json b/ui/app/conversion.json deleted file mode 100644 index 155ffc4fc..000000000 --- a/ui/app/conversion.json +++ /dev/null @@ -1,207 +0,0 @@ -{ - "rows": [ - { - "code": "REP", - "name": "Augur", - "statuses": [ - "primary" - ] - }, - { - "code": "BCN", - "name": "Bytecoin", - "statuses": [ - "primary" - ] - }, - { - "code": "BTC", - "name": "Bitcoin", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "BTS", - "name": "BitShares", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "BLK", - "name": "Blackcoin", - "statuses": [ - "primary" - ] - }, - { - "code": "GBP", - "name": "British Pound Sterling", - "statuses": [ - "secondary" - ] - }, - { - "code": "CAD", - "name": "Canadian Dollar", - "statuses": [ - "secondary" - ] - }, - { - "code": "CNY", - "name": "Chinese Yuan", - "statuses": [ - "secondary" - ] - }, - { - "code": "DSH", - "name": "Dashcoin", - "statuses": [ - "primary" - ] - }, - { - "code": "DOGE", - "name": "Dogecoin", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "ETC", - "name": "Ethereum Classic", - "statuses": [ - "primary" - ] - }, - { - "code": "EUR", - "name": "Euro", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "GNO", - "name": "GNO", - "statuses": [ - "primary" - ] - }, - { - "code": "GNT", - "name": "GNT", - "statuses": [ - "primary" - ] - }, - { - "code": "JPY", - "name": "Japanese Yen", - "statuses": [ - "secondary" - ] - }, - { - "code": "LTC", - "name": "Litecoin", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "MAID", - "name": "MaidSafeCoin", - "statuses": [ - "primary" - ] - }, - { - "code": "XEM", - "name": "NEM", - "statuses": [ - "primary" - ] - }, - { - "code": "XLM", - "name": "Stellar", - "statuses": [ - "primary" - ] - }, - { - "code": "XMR", - "name": "Monero", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "XRP", - "name": "Ripple", - "statuses": [ - "primary" - ] - }, - { - "code": "RUR", - "name": "Ruble", - "statuses": [ - "secondary" - ] - }, - { - "code": "STEEM", - "name": "Steem", - "statuses": [ - "primary" - ] - }, - { - "code": "STRAT", - "name": "STRAT", - "statuses": [ - "primary" - ] - }, - { - "code": "UAH", - "name": "Ukrainian Hryvnia", - "statuses": [ - "secondary" - ] - }, - { - "code": "USD", - "name": "US Dollar", - "statuses": [ - "primary", - "secondary" - ] - }, - { - "code": "WAVES", - "name": "WAVES", - "statuses": [ - "primary" - ] - }, - { - "code": "ZEC", - "name": "Zcash", - "statuses": [ - "primary" - ] - } - ] -} -- cgit v1.2.3 From 9e3648c668aed1f3e632efe1693d6a2e0aa76617 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 26 Sep 2017 11:33:36 -0700 Subject: Pass blocktracker to balances controller --- app/scripts/metamask-controller.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 30e511e19..cca796678 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -136,6 +136,7 @@ module.exports = class MetamaskController extends EventEmitter { this.balancesController = new BalancesController({ accountTracker: this.accountTracker, txController: this.txController, + blockTracker: this.blockTracker, }) this.networkController.on('networkDidChange', () => { this.balancesController.updateAllBalances() -- cgit v1.2.3 From 3bedcd3582519c7afbb8164b40acca4b96eab4bf Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 26 Sep 2017 13:36:41 -0700 Subject: Restore blockGasLimit to account-tracker --- app/scripts/lib/account-tracker.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index bf949597b..3df5fbc9d 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -18,6 +18,7 @@ class EthereumStore extends ObservableStore { constructor (opts = {}) { super({ accounts: {}, + currentBlockGasLimit: '', }) this._provider = opts.provider this._query = new EthQuery(this._provider) @@ -54,6 +55,8 @@ class EthereumStore extends ObservableStore { const blockNumber = '0x' + block.number.toString('hex') this._currentBlockNumber = blockNumber + this.updateState({ currentBlockGasLimit: `0x${block.gasLimit.toString('hex')}` }) + async.parallel([ this._updateAccounts.bind(this), ], (err) => { -- cgit v1.2.3 From b654eb9b1f37cd3757e4614bb048884ab89d2986 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 26 Sep 2017 11:52:57 -0700 Subject: wrap block tracker in events proxy --- app/scripts/controllers/network.js | 22 ++++++++-------------- app/scripts/lib/events-proxy.js | 31 +++++++++++++++++++++++++++++++ test/unit/network-contoller-test.js | 1 + 3 files changed, 40 insertions(+), 14 deletions(-) create mode 100644 app/scripts/lib/events-proxy.js diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js index 0a3e5e26b..6daedbb67 100644 --- a/app/scripts/controllers/network.js +++ b/app/scripts/controllers/network.js @@ -4,6 +4,7 @@ const ObservableStore = require('obs-store') const ComposedStore = require('obs-store/lib/composed') const extend = require('xtend') const EthQuery = require('eth-query') +const createEventEmitterProxy = require('../lib/events-proxy.js') const RPC_ADDRESS_LIST = require('../config.js').network const DEFAULT_RPC = RPC_ADDRESS_LIST['rinkeby'] @@ -31,16 +32,8 @@ module.exports = class NetworkController extends EventEmitter { initializeProvider (opts, providerContructor = MetaMaskProvider) { this.providerInit = opts this._provider = providerContructor(opts) - this._proxy = new Proxy(this._provider, { - get: (obj, name) => { - if (name === 'on') return this._on.bind(this) - return this._provider[name] - }, - set: (obj, name, value) => { - this._provider[name] = value - return value - }, - }) + this._proxy = createEventEmitterProxy(this._provider) + this.provider._blockTracker = createEventEmitterProxy(this._provider._blockTracker) this.provider.on('block', this._logBlock.bind(this)) this.provider.on('error', this.verifyNetwork.bind(this)) this.ethQuery = new EthQuery(this.provider) @@ -55,11 +48,12 @@ module.exports = class NetworkController extends EventEmitter { this._provider.removeAllListeners() this._provider.stop() - this.provider = MetaMaskProvider(newInit) + this._provider = MetaMaskProvider(newInit) // apply the listners created by other controllers - Object.keys(this._providerListeners).forEach((key) => { - this._providerListeners[key].forEach((handler) => this._provider.addListener(key, handler)) - }) + const blockTrackerHandlers = this.provider._blockTracker.proxyEventHandlers + this.provider.setTarget(this._provider) + + this.provider._blockTracker = createEventEmitterProxy(this._provider._blockTracker, blockTrackerHandlers) this.emit('networkDidChange') } diff --git a/app/scripts/lib/events-proxy.js b/app/scripts/lib/events-proxy.js new file mode 100644 index 000000000..d1199a278 --- /dev/null +++ b/app/scripts/lib/events-proxy.js @@ -0,0 +1,31 @@ +module.exports = function createEventEmitterProxy(eventEmitter, listeners) { + let target = eventEmitter + const eventHandlers = listeners || {} + const proxy = new Proxy({}, { + get: (obj, name) => { + // intercept listeners + if (name === 'on') return addListener + if (name === 'setTarget') return setTarget + if (name === 'proxyEventHandlers') return eventHandlers + return target[name] + }, + set: (obj, name, value) => { + target[name] = value + return true + }, + }) + function setTarget (eventEmitter) { + target = eventEmitter + // migrate listeners + Object.keys(eventHandlers).forEach((name) => { + eventHandlers[name].forEach((handler) => target.on(name, handler)) + }) + } + function addListener (name, handler) { + if (!eventHandlers[name]) eventHandlers[name] = [] + eventHandlers[name].push(handler) + target.on(name, handler) + } + if (listeners) proxy.setTarget(eventEmitter) + return proxy +} \ No newline at end of file diff --git a/test/unit/network-contoller-test.js b/test/unit/network-contoller-test.js index 87c2ee7a3..853e4e457 100644 --- a/test/unit/network-contoller-test.js +++ b/test/unit/network-contoller-test.js @@ -71,6 +71,7 @@ function dummyProviderConstructor() { // provider sendAsync: noop, // block tracker + _blockTracker: {}, start: noop, stop: noop, on: noop, -- cgit v1.2.3 From 2ed8d579daa4d33c38891ac77d1415fcd237a187 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 26 Sep 2017 13:37:13 -0700 Subject: listen for the blocke event on the block tracker instead of rawBlock on the provider --- app/scripts/controllers/network.js | 1 - app/scripts/controllers/transactions.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js index 6daedbb67..00bdca2c3 100644 --- a/app/scripts/controllers/network.js +++ b/app/scripts/controllers/network.js @@ -52,7 +52,6 @@ module.exports = class NetworkController extends EventEmitter { // apply the listners created by other controllers const blockTrackerHandlers = this.provider._blockTracker.proxyEventHandlers this.provider.setTarget(this._provider) - this.provider._blockTracker = createEventEmitterProxy(this._provider._blockTracker, blockTrackerHandlers) this.emit('networkDidChange') } diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index fb3be6073..4e52a3c14 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -70,7 +70,7 @@ module.exports = class TransactionController extends EventEmitter { this.pendingTxTracker.on('txFailed', this.setTxStatusFailed.bind(this)) this.pendingTxTracker.on('txConfirmed', this.setTxStatusConfirmed.bind(this)) - this.blockTracker.on('rawBlock', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker)) + this.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker)) // this is a little messy but until ethstore has been either // removed or redone this is to guard against the race condition // where ethStore hasent been populated by the results yet -- cgit v1.2.3 From 4f887c6a62ebcecfb578d55fffd3c9f7a1bd088a Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 26 Sep 2017 13:44:45 -0700 Subject: fix test --- test/unit/network-contoller-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/network-contoller-test.js b/test/unit/network-contoller-test.js index 853e4e457..c1fdaf032 100644 --- a/test/unit/network-contoller-test.js +++ b/test/unit/network-contoller-test.js @@ -21,7 +21,7 @@ describe('# Network Controller', function () { it('provider should be updatable without reassignment', function () { networkController.initializeProvider(networkControllerProviderInit, dummyProviderConstructor) const provider = networkController.provider - networkController._provider = {test: true} + networkController.provider.setTarget({test: true, on: () => {}}) assert.ok(provider.test) }) }) -- cgit v1.2.3 From 9d1cb0f76dce203299200940f21e868f6a5efef3 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 26 Sep 2017 13:56:09 -0700 Subject: network contoller - clean up unused code --- app/scripts/controllers/network.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js index 00bdca2c3..dc9978043 100644 --- a/app/scripts/controllers/network.js +++ b/app/scripts/controllers/network.js @@ -114,10 +114,4 @@ module.exports = class NetworkController extends EventEmitter { log.info(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`) this.verifyNetwork() } - - _on (event, handler) { - if (!this._providerListeners[event]) this._providerListeners[event] = [] - this._providerListeners[event].push(handler) - this._provider.on(event, handler) - } } -- cgit v1.2.3 From 2eca5455c0c80d99b10c7d56858f84e605494fba Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 26 Sep 2017 14:15:16 -0700 Subject: Move obs store into account-tracker instead of inheriting --- app/scripts/controllers/balance.js | 4 ++-- app/scripts/controllers/computed-balances.js | 6 +++--- app/scripts/lib/account-tracker.js | 31 ++++++++++++++++------------ app/scripts/metamask-controller.js | 4 ++-- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/app/scripts/controllers/balance.js b/app/scripts/controllers/balance.js index ab0cfe907..964dff0df 100644 --- a/app/scripts/controllers/balance.js +++ b/app/scripts/controllers/balance.js @@ -36,12 +36,12 @@ class BalanceController { this.txController.on('submitted', update) this.txController.on('confirmed', update) this.txController.on('failed', update) - this.accountTracker.subscribe(update) + this.accountTracker.store.subscribe(update) this.blockTracker.on('block', update) } async _getBalance () { - const { accounts } = this.accountTracker.getState() + const { accounts } = this.accountTracker.store.getState() const entry = accounts[this.address] const balance = entry.balance return balance ? new BN(balance.substring(2), 16) : undefined diff --git a/app/scripts/controllers/computed-balances.js b/app/scripts/controllers/computed-balances.js index 2b27d128d..2479e1b3a 100644 --- a/app/scripts/controllers/computed-balances.js +++ b/app/scripts/controllers/computed-balances.js @@ -20,15 +20,15 @@ class ComputedbalancesController { } updateAllBalances () { - for (let address in this.balances) { + for (let address in this.accountTracker.store.getState().accounts) { this.balances[address].updateBalance() } } _initBalanceUpdating () { - const store = this.accountTracker.getState() + const store = this.accountTracker.store.getState() this.addAnyAccountsFromStore(store) - this.accountTracker.subscribe(this.addAnyAccountsFromStore.bind(this)) + this.accountTracker.store.subscribe(this.addAnyAccountsFromStore.bind(this)) } addAnyAccountsFromStore(store) { diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index 3df5fbc9d..e2892b1ce 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -10,16 +10,21 @@ const async = require('async') const EthQuery = require('eth-query') const ObservableStore = require('obs-store') +const EventEmitter = require('events').EventEmitter function noop () {} -class EthereumStore extends ObservableStore { +class AccountTracker extends EventEmitter { constructor (opts = {}) { - super({ + super() + + const initState = { accounts: {}, currentBlockGasLimit: '', - }) + } + this.store = new ObservableStore(initState) + this._provider = opts.provider this._query = new EthQuery(this._provider) this._blockTracker = opts.blockTracker @@ -34,17 +39,17 @@ class EthereumStore extends ObservableStore { // addAccount (address) { - const accounts = this.getState().accounts + const accounts = this.store.getState().accounts accounts[address] = {} - this.updateState({ accounts }) + this.store.updateState({ accounts }) if (!this._currentBlockNumber) return this._updateAccount(address) } removeAccount (address) { - const accounts = this.getState().accounts + const accounts = this.store.getState().accounts delete accounts[address] - this.updateState({ accounts }) + this.store.updateState({ accounts }) } // @@ -55,31 +60,31 @@ class EthereumStore extends ObservableStore { const blockNumber = '0x' + block.number.toString('hex') this._currentBlockNumber = blockNumber - this.updateState({ currentBlockGasLimit: `0x${block.gasLimit.toString('hex')}` }) + this.store.updateState({ currentBlockGasLimit: `0x${block.gasLimit.toString('hex')}` }) async.parallel([ this._updateAccounts.bind(this), ], (err) => { if (err) return console.error(err) - this.emit('block', this.getState()) + this.emit('block', this.store.getState()) }) } _updateAccounts (cb = noop) { - const accounts = this.getState().accounts + const accounts = this.store.getState().accounts const addresses = Object.keys(accounts) async.each(addresses, this._updateAccount.bind(this), cb) } _updateAccount (address, cb = noop) { - const accounts = this.getState().accounts this._getAccount(address, (err, result) => { if (err) return cb(err) result.address = address + const accounts = this.store.getState().accounts // only populate if the entry is still present if (accounts[address]) { accounts[address] = result - this.updateState({ accounts }) + this.store.updateState({ accounts }) } cb(null, result) }) @@ -96,4 +101,4 @@ class EthereumStore extends ObservableStore { } -module.exports = EthereumStore +module.exports = AccountTracker diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index cca796678..a86d8d37b 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -194,7 +194,7 @@ module.exports = class MetamaskController extends EventEmitter { // manual mem state subscriptions this.networkController.store.subscribe(this.sendUpdate.bind(this)) - this.accountTracker.subscribe(this.sendUpdate.bind(this)) + this.accountTracker.store.subscribe(this.sendUpdate.bind(this)) this.txController.memStore.subscribe(this.sendUpdate.bind(this)) this.balancesController.store.subscribe(this.sendUpdate.bind(this)) this.messageManager.memStore.subscribe(this.sendUpdate.bind(this)) @@ -277,7 +277,7 @@ module.exports = class MetamaskController extends EventEmitter { isInitialized, }, this.networkController.store.getState(), - this.accountTracker.getState(), + this.accountTracker.store.getState(), this.txController.memStore.getState(), this.messageManager.memStore.getState(), this.personalMessageManager.memStore.getState(), -- cgit v1.2.3 From 651098c70d21edbca98a96ef2a8800d164035638 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 26 Sep 2017 14:30:29 -0700 Subject: Remove duplicate instantiation of account-tracker --- app/scripts/metamask-controller.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index a86d8d37b..0f850b7f5 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -86,6 +86,7 @@ module.exports = class MetamaskController extends EventEmitter { // eth data query tools this.ethQuery = new EthQuery(this.provider) + // account tracker watches balances, nonces, and any code at their address. this.accountTracker = new AccountTracker({ provider: this.provider, blockTracker: this.blockTracker, @@ -99,11 +100,6 @@ module.exports = class MetamaskController extends EventEmitter { encryptor: opts.encryptor || undefined, }) - // account tracker watches balances, nonces, and any code at their address. - this.accountTracker = new AccountTracker({ - provider: this.provider, - blockTracker: this.provider, - }) this.keyringController.on('newAccount', (address) => { this.preferencesController.setSelectedAddress(address) this.accountTracker.addAccount(address) -- cgit v1.2.3 From 9fd545811226c16e11e4f2dc100a348e07f094bf Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 26 Sep 2017 16:52:08 -0700 Subject: transactions: lint fixes and reveal status-update event for balance controller --- app/scripts/background.js | 2 +- app/scripts/controllers/balance.js | 15 ++++++++++++--- app/scripts/controllers/transactions.js | 5 +++-- app/scripts/lib/tx-state-manager.js | 12 ++++++------ app/scripts/metamask-controller.js | 2 +- test/unit/tx-controller-test.js | 2 +- 6 files changed, 24 insertions(+), 14 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index 1b96d68b5..195881e15 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -114,7 +114,7 @@ function setupController (initState) { // updateBadge() - controller.txController.on('updateBadge', updateBadge) + controller.txController.on('update:badge', updateBadge) controller.messageManager.on('updateBadge', updateBadge) controller.personalMessageManager.on('updateBadge', updateBadge) diff --git a/app/scripts/controllers/balance.js b/app/scripts/controllers/balance.js index 964dff0df..4fa4c78fe 100644 --- a/app/scripts/controllers/balance.js +++ b/app/scripts/controllers/balance.js @@ -33,9 +33,18 @@ class BalanceController { _registerUpdates () { const update = this.updateBalance.bind(this) - this.txController.on('submitted', update) - this.txController.on('confirmed', update) - this.txController.on('failed', update) + + this.txController.on('tx:status-update', (txId, status) => { + switch (status) { + case 'submitted': + case 'confirmed': + case 'failed': + update() + return + default: + return + } + }) this.accountTracker.store.subscribe(update) this.blockTracker.on('block', update) } diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index de5fa5b20..1b647a4ed 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -43,7 +43,8 @@ module.exports = class TransactionController extends EventEmitter { txHistoryLimit: opts.txHistoryLimit, getNetwork: this.getNetwork.bind(this), }) - + this.store = this.txStateManager.store + this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update')) this.nonceTracker = new NonceTracker({ provider: this.provider, getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager), @@ -69,7 +70,7 @@ module.exports = class TransactionController extends EventEmitter { getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager), }) - this.txStateManager.store.subscribe(() => this.emit('updateBadge')) + this.txStateManager.store.subscribe(() => this.emit('update:badge')) this.pendingTxTracker.on('txWarning', this.txStateManager.updateTx.bind(this.txStateManager)) this.pendingTxTracker.on('txFailed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager)) diff --git a/app/scripts/lib/tx-state-manager.js b/app/scripts/lib/tx-state-manager.js index d7b76fe22..abb9d7910 100644 --- a/app/scripts/lib/tx-state-manager.js +++ b/app/scripts/lib/tx-state-manager.js @@ -5,7 +5,7 @@ const ethUtil = require('ethereumjs-util') const txStateHistoryHelper = require('./tx-state-history-helper') module.exports = class TransactionStateManger extends EventEmitter { - constructor ({initState, txHistoryLimit, getNetwork}) { + constructor ({ initState, txHistoryLimit, getNetwork }) { super() this.store = new ObservableStore( @@ -15,7 +15,8 @@ module.exports = class TransactionStateManger extends EventEmitter { this.txHistoryLimit = txHistoryLimit this.getNetwork = getNetwork } - // Returns the number of txs for the current network. + + // Returns the number of txs for the current network. getTxCount () { return this.getTxList().length } @@ -31,7 +32,6 @@ module.exports = class TransactionStateManger extends EventEmitter { } // Returns the tx list - getUnapprovedTxList () { const txList = this.getTxsByMetaData('status', 'unapproved') return txList.reduce((result, tx) => { @@ -69,7 +69,7 @@ module.exports = class TransactionStateManger extends EventEmitter { // or rejected tx's. // not tx's that are pending or unapproved if (txCount > txHistoryLimit - 1) { - const index = transactions.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected'))) + const index = transactions.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected') transactions.splice(index, 1) } transactions.push(txMeta) @@ -229,12 +229,12 @@ module.exports = class TransactionStateManger extends EventEmitter { const txMeta = this.getTx(txId) txMeta.status = status this.emit(`${txMeta.id}:${status}`, txId) - this.emit(`${status}`, txId) + this.emit(`tx:status-update`, txId, status) if (status === 'submitted' || status === 'rejected') { this.emit(`${txMeta.id}:finished`, txMeta) } this.updateTx(txMeta) - this.emit('updateBadge') + this.emit('update:badge') } // Saves the new/updated txList. diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 96b34507f..0f850b7f5 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -157,7 +157,7 @@ module.exports = class MetamaskController extends EventEmitter { this.publicConfigStore = this.initPublicConfigStore() // manual disk state subscriptions - this.txController.txStateManager.store.subscribe((state) => { + this.txController.store.subscribe((state) => { this.store.updateState({ TransactionController: state }) }) this.keyringController.store.subscribe((state) => { diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index 8d45fb9b5..c1612a30c 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -210,7 +210,7 @@ describe('Transaction Controller', function () { txParams: {} } - const eventNames = ['updateBadge', '1:unapproved'] + const eventNames = ['update:badge', '1:unapproved'] const listeners = [] eventNames.forEach((eventName) => { listeners.push(new Promise((resolve) => { -- cgit v1.2.3 From 80c98b16531db4c6a13a52df800967af2bc4a676 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 26 Sep 2017 16:55:11 -0700 Subject: transactions: make evnt names pretty and eaiser to read --- app/scripts/controllers/transactions.js | 6 +++--- app/scripts/lib/pending-tx-tracker.js | 16 ++++++++-------- test/unit/pending-tx-test.js | 18 +++++++++--------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 1b647a4ed..ec1524968 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -72,9 +72,9 @@ module.exports = class TransactionController extends EventEmitter { this.txStateManager.store.subscribe(() => this.emit('update:badge')) - this.pendingTxTracker.on('txWarning', this.txStateManager.updateTx.bind(this.txStateManager)) - this.pendingTxTracker.on('txFailed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager)) - this.pendingTxTracker.on('txConfirmed', this.txStateManager.setTxStatusConfirmed.bind(this.txStateManager)) + this.pendingTxTracker.on('tx:warning', this.txStateManager.updateTx.bind(this.txStateManager)) + this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager)) + this.pendingTxTracker.on('tx:confirmed', this.txStateManager.setTxStatusConfirmed.bind(this.txStateManager)) this.blockTracker.on('rawBlock', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker)) // this is a little messy but until ethstore has been either diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js index 4541221d5..31cf9babb 100644 --- a/app/scripts/lib/pending-tx-tracker.js +++ b/app/scripts/lib/pending-tx-tracker.js @@ -42,13 +42,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter { if (!txHash) { const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') noTxHashErr.name = 'NoTxHashError' - this.emit('txFailed', txId, noTxHashErr) + this.emit('tx:failed', txId, noTxHashErr) return } block.transactions.forEach((tx) => { - if (tx.hash === txHash) this.emit('txConfirmed', txId) + if (tx.hash === txHash) this.emit('tx:confirmed', txId) }) }) } @@ -94,7 +94,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter { // ignore resubmit warnings, return early if (isKnownTx) return // encountered real error - transition to error state - this.emit('txFailed', txMeta.id, err) + this.emit('tx:failed', txMeta.id, err) })) } @@ -106,13 +106,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter { if (txMeta.retryCount > this.retryLimit) { const err = new Error(`Gave up submitting after ${this.retryLimit} blocks un-mined.`) - return this.emit('txFailed', txMeta.id, err) + return this.emit('tx:failed', txMeta.id, err) } // if the value of the transaction is greater then the balance, fail. if (!sufficientBalance(txMeta.txParams, balance)) { const insufficientFundsError = new Error('Insufficient balance during rebroadcast.') - this.emit('txFailed', txMeta.id, insufficientFundsError) + this.emit('tx:failed', txMeta.id, insufficientFundsError) log.error(insufficientFundsError) return } @@ -136,7 +136,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter { if (!txHash) { const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') noTxHashErr.name = 'NoTxHashError' - this.emit('txFailed', txId, noTxHashErr) + this.emit('tx:failed', txId, noTxHashErr) return } // get latest transaction status @@ -145,14 +145,14 @@ module.exports = class PendingTransactionTracker extends EventEmitter { txParams = await this.query.getTransactionByHash(txHash) if (!txParams) return if (txParams.blockNumber) { - this.emit('txConfirmed', txId) + this.emit('tx:confirmed', txId) } } catch (err) { txMeta.warning = { error: err, message: 'There was a problem loading this transaction.', } - this.emit('txWarning', txMeta) + this.emit('tx:warning', txMeta) throw err } } diff --git a/test/unit/pending-tx-test.js b/test/unit/pending-tx-test.js index 8c6d287f8..2865a30e6 100644 --- a/test/unit/pending-tx-test.js +++ b/test/unit/pending-tx-test.js @@ -62,7 +62,7 @@ describe('PendingTransactionTracker', function () { it('should emit \'txFailed\' if the txMeta does not have a hash', function (done) { const block = Proxy.revocable({}, {}).revoke() pendingTxTracker.getPendingTransactions = () => [txMetaNoHash] - pendingTxTracker.once('txFailed', (txId, err) => { + pendingTxTracker.once('tx:failed', (txId, err) => { assert(txId, txMetaNoHash.id, 'should pass txId') done() }) @@ -71,11 +71,11 @@ describe('PendingTransactionTracker', function () { it('should emit \'txConfirmed\' if the tx is in the block', function (done) { const block = { transactions: [txMeta]} pendingTxTracker.getPendingTransactions = () => [txMeta] - pendingTxTracker.once('txConfirmed', (txId) => { + pendingTxTracker.once('tx:confirmed', (txId) => { assert(txId, txMeta.id, 'should pass txId') done() }) - pendingTxTracker.once('txFailed', (_, err) => { done(err) }) + pendingTxTracker.once('tx:failed', (_, err) => { done(err) }) pendingTxTracker.checkForTxInBlock(block) }) }) @@ -108,7 +108,7 @@ describe('PendingTransactionTracker', function () { describe('#_checkPendingTx', function () { it('should emit \'txFailed\' if the txMeta does not have a hash', function (done) { - pendingTxTracker.once('txFailed', (txId, err) => { + pendingTxTracker.once('tx:failed', (txId, err) => { assert(txId, txMetaNoHash.id, 'should pass txId') done() }) @@ -122,11 +122,11 @@ describe('PendingTransactionTracker', function () { it('should emit \'txConfirmed\'', function (done) { providerResultStub.eth_getTransactionByHash = {blockNumber: '0x01'} - pendingTxTracker.once('txConfirmed', (txId) => { + pendingTxTracker.once('tx:confirmed', (txId) => { assert(txId, txMeta.id, 'should pass txId') done() }) - pendingTxTracker.once('txFailed', (_, err) => { done(err) }) + pendingTxTracker.once('tx:failed', (_, err) => { done(err) }) pendingTxTracker._checkPendingTx(txMeta) }) }) @@ -188,7 +188,7 @@ describe('PendingTransactionTracker', function () { ] const enoughForAllErrors = txList.concat(txList) - pendingTxTracker.on('txFailed', (_, err) => done(err)) + pendingTxTracker.on('tx:failed', (_, err) => done(err)) pendingTxTracker.getPendingTransactions = () => enoughForAllErrors pendingTxTracker._resubmitTx = async (tx) => { @@ -202,7 +202,7 @@ describe('PendingTransactionTracker', function () { pendingTxTracker.resubmitPendingTxs() }) it('should emit \'txFailed\' if it encountered a real error', function (done) { - pendingTxTracker.once('txFailed', (id, err) => err.message === 'im some real error' ? txList[id - 1].resolve() : done(err)) + pendingTxTracker.once('tx:failed', (id, err) => err.message === 'im some real error' ? txList[id - 1].resolve() : done(err)) pendingTxTracker.getPendingTransactions = () => txList pendingTxTracker._resubmitTx = async (tx) => { throw new TypeError('im some real error') } @@ -226,7 +226,7 @@ describe('PendingTransactionTracker', function () { // Stubbing out current account state: // Adding the fake tx: - pendingTxTracker.once('txFailed', (txId, err) => { + pendingTxTracker.once('tx:failed', (txId, err) => { assert(err, 'Should have a error') done() }) -- cgit v1.2.3 From 508696f71d6b5b14c708a3229039f9f915ce48ce Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 26 Sep 2017 18:11:51 -0700 Subject: transactions: reveal #getFilteredTxList from txStateManage and fix accountTracker.store reference --- app/scripts/controllers/transactions.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index ec1524968..6ea2933dc 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -62,7 +62,7 @@ module.exports = class TransactionController extends EventEmitter { nonceTracker: this.nonceTracker, retryLimit: 3500, // Retry 3500 blocks, or about 1 day. getBalance: (address) => { - const account = this.accountTracker.getState().accounts[address] + const account = this.accountTracker.store.getState().accounts[address] if (!account) return return account.balance }, @@ -111,6 +111,10 @@ module.exports = class TransactionController extends EventEmitter { return this.txStateManager.getPendingTransactions(account).length } + getFilteredTxList (opts) { + return this.txStateManager.getFilteredTxList(opts) + } + getChainId () { const networkState = this.networkStore.getState() const getChainId = parseInt(networkState) -- cgit v1.2.3 From b05a6f89cb2c4de1e53d96f8cac01653a8165555 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 26 Sep 2017 18:19:44 -0700 Subject: fix tests --- test/unit/tx-controller-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index dd8b48684..9ffba31ee 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -25,7 +25,7 @@ describe('Transaction Controller', function () { networkStore: new ObservableStore(currentNetworkId), txHistoryLimit: 10, blockTracker: { getCurrentBlock: noop, on: noop, once: noop }, - accountTracker: { getState: noop }, + accountTracker: { store: {getState: noop} }, signTransaction: (ethTx) => new Promise((resolve) => { ethTx.sign(privKey) resolve() @@ -385,7 +385,7 @@ describe('Transaction Controller', function () { describe('#getBalance', function () { it('gets balance', function () { - sinon.stub(txController.ethStore, 'getState').callsFake(() => { + sinon.stub(txController.accountTracker.store, 'getState').callsFake(() => { return { accounts: { '0x1678a085c290ebd122dc42cba69373b5953b831d': { -- cgit v1.2.3 From 25c2865076784f3e5346f7e34cbf80b9fe210ade Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 26 Sep 2017 21:33:36 -0230 Subject: Restore notification functionality --- ui/app/reducers/app.js | 5 +++++ ui/index.js | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index c64046518..6d805521b 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -15,6 +15,11 @@ function reduceApp (state, action) { name = 'accountDetail' } + if (hasUnconfActions) { + log.debug('pending txs detected, defaulting to conf-tx view.') + name = 'confTx' + } + var defaultView = { name, detailView: null, diff --git a/ui/index.js b/ui/index.js index f748c1ea2..1afc08683 100644 --- a/ui/index.js +++ b/ui/index.js @@ -36,6 +36,15 @@ function startApp (metamaskState, accountManager, opts) { networkVersion: opts.networkVersion, }) + // if unconfirmed txs, start on txConf page + const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.network) + const numberOfUnapprivedTx = unapprovedTxsAll.length + if (numberOfUnapprivedTx > 0) { + store.dispatch(actions.showConfTxPage({ + id: unapprovedTxsAll[numberOfUnapprivedTx - 1].id, + })) + } + accountManager.on('update', function (metamaskState) { store.dispatch(actions.updateMetamaskState(metamaskState)) }) -- cgit v1.2.3 From 541b69dda9a5ddbb0ea4e4c0df805e886f53645c Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 26 Sep 2017 21:51:45 -0230 Subject: Gets gas and price estimates when send components mount. --- ui/app/actions.js | 4 ++-- ui/app/components/send-token/index.js | 32 ++++++++++++-------------------- ui/app/send.js | 33 +++++++++++++-------------------- 3 files changed, 27 insertions(+), 42 deletions(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index a0dbbbf11..63d22238b 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -452,10 +452,10 @@ function signTx (txData) { } } -function estimateGas ({ to, amount }) { +function estimateGas () { return (dispatch) => { return new Promise((resolve, reject) => { - global.ethQuery.estimateGas({ to, amount }, (err, data) => { + global.ethQuery.estimateGas({}, (err, data) => { if (err) { dispatch(actions.displayWarning(err.message)) return reject(err) diff --git a/ui/app/components/send-token/index.js b/ui/app/components/send-token/index.js index 379f63883..02423a348 100644 --- a/ui/app/components/send-token/index.js +++ b/ui/app/components/send-token/index.js @@ -57,9 +57,8 @@ function mapDispatchToProps (dispatch) { dispatch(actions.signTokenTx(tokenAddress, toAddress, amount, txData)) ), updateTokenExchangeRate: token => dispatch(actions.updateTokenExchangeRate(token)), - estimateGas: ({ to, amount }) => dispatch(actions.estimateGas({ to, amount })), + estimateGas: () => dispatch(actions.estimateGas()), getGasPrice: () => dispatch(actions.getGasPrice()), - } } @@ -82,27 +81,22 @@ SendTokenScreen.prototype.componentWillMount = function () { const { updateTokenExchangeRate, selectedToken: { symbol }, + getGasPrice, + estimateGas, } = this.props updateTokenExchangeRate(symbol) -} -SendTokenScreen.prototype.estimateGasAndPrice = function () { - const { selectedToken } = this.props - const { errors, amount, to } = this.state - - if (!errors.to && !errors.amount && amount > 0) { - Promise.all([ - this.props.getGasPrice(), - this.props.estimateGas({ to, amount: this.getAmountToSend(amount, selectedToken) }), - ]) - .then(([blockGasPrice, estimatedGas]) => { - this.setState({ - blockGasPrice, - estimatedGas, - }) + Promise.all([ + getGasPrice(), + estimateGas(), + ]) + .then(([blockGasPrice, estimatedGas]) => { + this.setState({ + blockGasPrice, + estimatedGas, }) - } + }) } SendTokenScreen.prototype.validate = function () { @@ -238,7 +232,6 @@ SendTokenScreen.prototype.renderToAddressInput = function () { }), onBlur: () => { this.setErrorsFor('to') - this.estimateGasAndPrice() }, onFocus: event => { if (to) event.target.select() @@ -300,7 +293,6 @@ SendTokenScreen.prototype.renderAmountInput = function () { }), onBlur: () => { this.setErrorsFor('amount') - this.estimateGasAndPrice() }, onFocus: () => this.clearErrorsFor('amount'), }), diff --git a/ui/app/send.js b/ui/app/send.js index 8791e9124..dc7e7c8ec 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -93,7 +93,6 @@ function SendTransactionScreen () { this.getAmountToSend = this.getAmountToSend.bind(this) this.setErrorsFor = this.setErrorsFor.bind(this) this.clearErrorsFor = this.clearErrorsFor.bind(this) - this.estimateGasAndPrice = this.estimateGasAndPrice.bind(this) this.renderFromInput = this.renderFromInput.bind(this) this.renderToInput = this.renderToInput.bind(this) @@ -103,6 +102,19 @@ function SendTransactionScreen () { this.renderErrorMessage = this.renderErrorMessage.bind(this) } +SendTransactionScreen.prototype.componentWillMount = function () { + Promise.all([ + this.props.dispatch(getGasPrice()), + this.props.dispatch(estimateGas()), + ]) + .then(([blockGasPrice, estimatedGas]) => { + this.setState({ + blockGasPrice, + estimatedGas, + }) + }) +} + SendTransactionScreen.prototype.renderErrorMessage = function(errorType, warning) { const { errors } = this.state const errorMessage = errors[errorType]; @@ -171,7 +183,6 @@ SendTransactionScreen.prototype.renderToInput = function (to, identities, addres }, onBlur: () => { this.setErrorsFor('to') - this.estimateGasAndPrice() }, onFocus: event => { this.clearErrorsFor('to') @@ -230,7 +241,6 @@ SendTransactionScreen.prototype.renderAmountInput = function (activeCurrency) { }, onBlur: () => { this.setErrorsFor('amount') - this.estimateGasAndPrice() }, onFocus: () => this.clearErrorsFor('amount'), }), @@ -383,23 +393,6 @@ SendTransactionScreen.prototype.setActiveCurrency = function (newCurrency) { this.setState({ activeCurrency: newCurrency }) } -SendTransactionScreen.prototype.estimateGasAndPrice = function () { - const { errors, sendAmount, newTx } = this.state - - if (!errors.to && !errors.amount && newTx.amount > 0) { - Promise.all([ - this.props.dispatch(getGasPrice()), - this.props.dispatch(estimateGas({ to: newTx.to, amount: sendAmount })), - ]) - .then(([blockGasPrice, estimatedGas]) => { - this.setState({ - blockGasPrice, - estimatedGas, - }) - }) - } -} - SendTransactionScreen.prototype.back = function () { var address = this.props.address this.props.dispatch(backToAccountDetail(address)) -- cgit v1.2.3 From 39365f2cc419ee824988e6dad4e8a75e650ad1cc Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 26 Sep 2017 23:03:03 -0230 Subject: Update the correct values in state when estimates are received. --- ui/app/components/send-token/index.js | 14 ++++++-------- ui/app/send.js | 19 +++++++++---------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/ui/app/components/send-token/index.js b/ui/app/components/send-token/index.js index 02423a348..8a827e951 100644 --- a/ui/app/components/send-token/index.js +++ b/ui/app/components/send-token/index.js @@ -93,8 +93,8 @@ SendTokenScreen.prototype.componentWillMount = function () { ]) .then(([blockGasPrice, estimatedGas]) => { this.setState({ - blockGasPrice, - estimatedGas, + gasPrice: blockGasPrice, + gasLimit: estimatedGas, }) }) } @@ -305,8 +305,6 @@ SendTokenScreen.prototype.renderGasInput = function () { isGasTooltipOpen, gasPrice, gasLimit, - blockGasPrice, - estimatedGas, selectedCurrency, errors: { gasPrice: gasPriceErrorMessage, @@ -327,8 +325,8 @@ SendTokenScreen.prototype.renderGasInput = function () { }, [ isGasTooltipOpen && h(GasTooltip, { className: 'send-tooltip', - gasPrice: gasPrice || blockGasPrice || '0x0', - gasLimit: gasLimit || estimatedGas || '0x0', + gasPrice: gasPrice || '0x0', + gasLimit: gasLimit || '0x0', onClose: () => this.setState({ isGasTooltipOpen: false }), onFeeChange: ({ gasLimit, gasPrice }) => { this.setState({ gasLimit, gasPrice, errors: {} }) @@ -351,9 +349,9 @@ SendTokenScreen.prototype.renderGasInput = function () { h(GasFeeDisplay, { conversionRate, tokenExchangeRate, - gasPrice: gasPrice || blockGasPrice || '0x0', + gasPrice: gasPrice || '0x0', activeCurrency: selectedCurrency, - gas: gasLimit || estimatedGas || '0x0', + gas: gasLimit || '0x0', blockGasLimit: currentBlockGasLimit, }), h( diff --git a/ui/app/send.js b/ui/app/send.js index dc7e7c8ec..4d2a5f48d 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -35,8 +35,6 @@ function mapStateToProps (state) { addressBook, conversionRate, currentBlockGasLimit: blockGasLimit, - estimatedGas, - blockGasPrice, } = state.metamask const { warning } = state.appState const selectedIdentity = getSelectedIdentity(state) @@ -76,8 +74,6 @@ function SendTransactionScreen () { txData: null, memo: '', }, - blockGasPrice: null, - estimatedGas: null, activeCurrency: 'USD', tooltipIsOpen: false, errors: {}, @@ -103,14 +99,19 @@ function SendTransactionScreen () { } SendTransactionScreen.prototype.componentWillMount = function () { + const { newTx } = this.state + Promise.all([ this.props.dispatch(getGasPrice()), this.props.dispatch(estimateGas()), ]) .then(([blockGasPrice, estimatedGas]) => { this.setState({ - blockGasPrice, - estimatedGas, + newTx: { + ...newTx, + gasPrice: blockGasPrice, + gas: estimatedGas, + }, }) }) } @@ -329,8 +330,6 @@ SendTransactionScreen.prototype.render = function () { newTx, activeCurrency, isValid, - blockGasPrice, - estimatedGas, } = this.state const { gas, gasPrice } = newTx @@ -353,8 +352,8 @@ SendTransactionScreen.prototype.render = function () { this.renderAmountInput(activeCurrency), this.renderGasInput( - gasPrice || blockGasPrice || '0x0', - gas || estimatedGas || '0x0', + gasPrice || '0x0', + gas || '0x0', activeCurrency, conversionRate, blockGasLimit -- cgit v1.2.3 From c77029ea90560b4210f9204e99314d30f9b59989 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Tue, 26 Sep 2017 19:21:04 -0700 Subject: Implement Confirm Deploy Contract screen --- .../pending-tx/confirm-deploy-contract.js | 344 +++++++++++++++++++++ ui/app/components/pending-tx/index.js | 6 + ui/app/css/itcss/components/confirm.scss | 7 +- ui/app/css/itcss/components/send.scss | 2 +- 4 files changed, 357 insertions(+), 2 deletions(-) create mode 100644 ui/app/components/pending-tx/confirm-deploy-contract.js diff --git a/ui/app/components/pending-tx/confirm-deploy-contract.js b/ui/app/components/pending-tx/confirm-deploy-contract.js new file mode 100644 index 000000000..89a0389d7 --- /dev/null +++ b/ui/app/components/pending-tx/confirm-deploy-contract.js @@ -0,0 +1,344 @@ +const Component = require('react').Component +const { connect } = require('react-redux') +const h = require('react-hyperscript') +const inherits = require('util').inherits +const actions = require('../../actions') +const clone = require('clone') +const Identicon = require('../identicon') +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const hexToBn = require('../../../../app/scripts/lib/hex-to-bn') +const { conversionUtil } = require('../../conversion-util') + +const MIN_GAS_PRICE_GWEI_BN = new BN(1) +const GWEI_FACTOR = new BN(1e9) +const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) + + +module.exports = connect(mapStateToProps, mapDispatchToProps)(ConfirmDeployContract) + +function mapStateToProps (state) { + const { + conversionRate, + identities, + } = state.metamask + const accounts = state.metamask.accounts + const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] + return { + conversionRate, + identities, + selectedAddress, + } +} + +function mapDispatchToProps (dispatch) { + return { + setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('USD')), + backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)), + cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })), + } +} + + +inherits(ConfirmDeployContract, Component) +function ConfirmDeployContract () { + Component.call(this) + this.state = {} + this.onSubmit = this.onSubmit.bind(this) +} + +ConfirmDeployContract.prototype.onSubmit = function (event) { + event.preventDefault() + const txMeta = this.gatherTxMeta() + const valid = this.checkValidity() + this.setState({ valid, submitting: true }) + + if (valid && this.verifyGasParams()) { + this.props.sendTransaction(txMeta, event) + } else { + this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) + this.setState({ submitting: false }) + } +} + +ConfirmDeployContract.prototype.cancel = function (event, txMeta) { + event.preventDefault() + this.props.cancelTransaction(txMeta) +} + +ConfirmDeployContract.prototype.checkValidity = function () { + const form = this.getFormEl() + const valid = form.checkValidity() + return valid +} + +ConfirmDeployContract.prototype.getFormEl = function () { + const form = document.querySelector('form#pending-tx-form') + // Stub out form for unit tests: + if (!form) { + return { checkValidity () { return true } } + } + return form +} + +// After a customizable state value has been updated, +ConfirmDeployContract.prototype.gatherTxMeta = function () { + const props = this.props + const state = this.state + const txData = clone(state.txData) || clone(props.txData) + + // log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) + return txData +} + +ConfirmDeployContract.prototype.verifyGasParams = function () { + // We call this in case the gas has not been modified at all + if (!this.state) { return true } + return ( + this._notZeroOrEmptyString(this.state.gas) && + this._notZeroOrEmptyString(this.state.gasPrice) + ) +} + +ConfirmDeployContract.prototype._notZeroOrEmptyString = function (obj) { + return obj !== '' && obj !== '0x0' +} + +ConfirmDeployContract.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) { + const numBN = new BN(numerator) + const denomBN = new BN(denominator) + return targetBN.mul(numBN).div(denomBN) +} + +ConfirmDeployContract.prototype.getData = function () { + const { identities } = this.props + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} + + return { + from: { + address: txParams.from, + name: identities[txParams.from].name, + }, + memo: txParams.memo || '', + } +} + +ConfirmDeployContract.prototype.getAmount = function () { + const { conversionRate } = this.props + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} + + const USD = conversionUtil(txParams.value, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromCurrency: 'ETH', + toCurrency: 'USD', + numberOfDecimals: 2, + fromDenomination: 'WEI', + conversionRate, + }) + const ETH = conversionUtil(txParams.value, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromCurrency: 'ETH', + toCurrency: 'ETH', + fromDenomination: 'WEI', + conversionRate, + numberOfDecimals: 6, + }) + + return { + fiat: Number(USD), + token: Number(ETH), + } + +} + +ConfirmDeployContract.prototype.getGasFee = function () { + const { conversionRate } = this.props + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} + + // Gas + const gas = txParams.gas + const gasBn = hexToBn(gas) + + // Gas Price + const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) + const gasPriceBn = hexToBn(gasPrice) + + const txFeeBn = gasBn.mul(gasPriceBn) + + const USD = conversionUtil(txFeeBn, { + fromNumericBase: 'BN', + toNumericBase: 'dec', + fromDenomination: 'WEI', + fromCurrency: 'ETH', + toCurrency: 'USD', + numberOfDecimals: 2, + conversionRate, + }) + const ETH = conversionUtil(txFeeBn, { + fromNumericBase: 'BN', + toNumericBase: 'dec', + fromDenomination: 'WEI', + fromCurrency: 'ETH', + toCurrency: 'ETH', + numberOfDecimals: 6, + conversionRate, + }) + + return { + fiat: Number(USD), + eth: Number(ETH), + } +} +ConfirmDeployContract.prototype.renderGasFee = function () { + const { fiat: fiatGas, eth: ethGas } = this.getGasFee() + + return ( + h('section.flex-row.flex-center.confirm-screen-row', [ + h('span.confirm-screen-label.confirm-screen-section-column', [ 'Gas Fee' ]), + h('div.confirm-screen-section-column', [ + h('div.confirm-screen-row-info', `$${fiatGas} USD`), + + h( + 'div.confirm-screen-row-detail', + `${ethGas} ETH` + ), + ]), + ]) + ) +} + +ConfirmDeployContract.prototype.renderHeroAmount = function () { + const { fiat: fiatAmount } = this.getAmount() + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} + const { memo = '' } = txParams + + return ( + h('div.confirm-send-token__hero-amount-wrapper', [ + h('h3.flex-center.confirm-screen-send-amount', `$${fiatAmount}`), + h('h3.flex-center.confirm-screen-send-amount-currency', 'USD'), + h('div.flex-center.confirm-memo-wrapper', [ + h('h3.confirm-screen-send-memo', memo), + ]), + ]) + ) +} + +ConfirmDeployContract.prototype.renderTotalPlusGas = function () { + const { fiat: fiatAmount, token: tokenAmount } = this.getAmount() + const { fiat: fiatGas, eth: ethGas } = this.getGasFee() + + return ( + h('section.flex-row.flex-center.confirm-screen-total-box ', [ + h('div.confirm-screen-section-column', [ + h('span.confirm-screen-label', [ 'Total ' ]), + h('div.confirm-screen-total-box__subtitle', [ 'Amount + Gas' ]), + ]), + + h('div.confirm-screen-section-column', [ + h('div.confirm-screen-row-info', `$${fiatAmount + fiatGas} USD`), + h('div.confirm-screen-row-detail', `${tokenAmount + ethGas} ETH`), + ]), + ]) + ) +} + +ConfirmDeployContract.prototype.render = function () { + const { backToAccountDetail, selectedAddress } = this.props + const txMeta = this.gatherTxMeta() + + const { + from: { + address: fromAddress, + name: fromName, + }, + } = this.getData() + + this.inputs = [] + + return ( + h('div.flex-column.flex-grow.confirm-screen-container', { + style: { minWidth: '355px' }, + }, [ + // Main Send token Card + h('div.confirm-screen-wrapper.flex-column.flex-grow', [ + h('h3.flex-center.confirm-screen-header', [ + h('button.confirm-screen-back-button', { + onClick: () => backToAccountDetail(selectedAddress), + }, 'BACK'), + h('div.confirm-screen-title', 'Confirm Transaction'), + ]), + h('div.flex-row.flex-center.confirm-screen-identicons', [ + h('div.confirm-screen-account-wrapper', [ + h( + Identicon, + { + address: fromAddress, + diameter: 100, + }, + ), + h('span.confirm-screen-account-name', fromName), + h('span.confirm-screen-account-number', fromAddress.slice(fromAddress.length - 4)), + ]), + h('i.fa.fa-arrow-right.fa-lg'), + h('div.confirm-screen-account-wrapper', [ + h('i.fa.fa-file-text-o'), + h('span.confirm-screen-account-name', 'New Contract'), + h('span.confirm-screen-account-number', ' '), + ]), + ]), + + h('h3.flex-center.confirm-screen-sending-to-message', { + style: { + textAlign: 'center', + fontSize: '16px', + }, + }, [ + `You're deploying a new contract.`, + ]), + + this.renderHeroAmount(), + + h('div.confirm-screen-rows', [ + h('section.flex-row.flex-center.confirm-screen-row', [ + h('span.confirm-screen-label.confirm-screen-section-column', [ 'From' ]), + h('div.confirm-screen-section-column', [ + h('div.confirm-screen-row-info', fromName), + h('div.confirm-screen-row-detail', `...${fromAddress.slice(fromAddress.length - 4)}`), + ]), + ]), + + h('section.flex-row.flex-center.confirm-screen-row', [ + h('span.confirm-screen-label.confirm-screen-section-column', [ 'To' ]), + h('div.confirm-screen-section-column', [ + h('div.confirm-screen-row-info', 'New Contract'), + ]), + ]), + + this.renderGasFee(), + + this.renderTotalPlusGas(), + + ]), + ]), + + h('form#pending-tx-form.flex-column.flex-center', { + onSubmit: this.onSubmit, + }, [ + + // Accept Button + h('button.confirm-screen-confirm-button', ['CONFIRM']), + + // Cancel Button + h('div.cancel.btn-light.confirm-screen-cancel-button', { + onClick: (event) => this.cancel(event, txMeta), + }, 'CANCEL'), + ]), + ]) + ) +} diff --git a/ui/app/components/pending-tx/index.js b/ui/app/components/pending-tx/index.js index 915319958..b7dd50ff1 100644 --- a/ui/app/components/pending-tx/index.js +++ b/ui/app/components/pending-tx/index.js @@ -10,6 +10,7 @@ const actions = require('../../actions') const util = require('../../util') const ConfirmSendEther = require('./confirm-send-ether') const ConfirmSendToken = require('./confirm-send-token') +const ConfirmDeployContract = require('./confirm-deploy-contract') const TX_TYPES = { DEPLOY_CONTRACT: 'deploy_contract', @@ -136,6 +137,11 @@ PendingTx.prototype.render = function () { decimals: tokenDecimals, }, }) + case TX_TYPES.DEPLOY_CONTRACT: + return h(ConfirmDeployContract, { + txData: this.gatherTxMeta(), + sendTransaction, + }) default: return h('noscript') } diff --git a/ui/app/css/itcss/components/confirm.scss b/ui/app/css/itcss/components/confirm.scss index e8169ffea..36d1bdd9a 100644 --- a/ui/app/css/itcss/components/confirm.scss +++ b/ui/app/css/itcss/components/confirm.scss @@ -108,15 +108,20 @@ line-height: 16px; color: $dusty-gray; text-align: center; + height: 16px; } .confirm-screen-identicons { margin-top: 24px; - i { + i.fa-arrow-right { align-self: start; margin: 42px 14px 0; } + + i.fa-file-text-o { + font-size: 100px; + } } .confirm-screen-sending-to-message { diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index 5691baebe..03e0fac1d 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -307,7 +307,7 @@ } &__send-button__disabled { - opacity: 0.5; + opacity: .5; cursor: auto; } } -- cgit v1.2.3 From 0a94ec41d3a2877ed7cfd3c8f9e9f9d725659183 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 26 Sep 2017 22:42:59 -0700 Subject: pending-tx - move incrementing of the retryCount on the txMeta outside pending-tx-tracker --- app/scripts/controllers/transactions.js | 5 +++++ app/scripts/lib/pending-tx-tracker.js | 3 +-- test/unit/tx-controller-test.js | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 6ea2933dc..3cd107031 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -75,6 +75,11 @@ module.exports = class TransactionController extends EventEmitter { this.pendingTxTracker.on('tx:warning', this.txStateManager.updateTx.bind(this.txStateManager)) this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager)) this.pendingTxTracker.on('tx:confirmed', this.txStateManager.setTxStatusConfirmed.bind(this.txStateManager)) + this.pendingTxTracker.on('tx:retry', (txMeta) => { + if (!('retryCount' in txMeta)) txMeta.retryCount = 0 + txMeta.retryCount++ + this.txStateManager.updateTx(txMeta) + }) this.blockTracker.on('rawBlock', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker)) // this is a little messy but until ethstore has been either diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js index 31cf9babb..b07a6bd39 100644 --- a/app/scripts/lib/pending-tx-tracker.js +++ b/app/scripts/lib/pending-tx-tracker.js @@ -102,7 +102,6 @@ module.exports = class PendingTransactionTracker extends EventEmitter { const address = txMeta.txParams.from const balance = this.getBalance(address) if (balance === undefined) return - if (!('retryCount' in txMeta)) txMeta.retryCount = 0 if (txMeta.retryCount > this.retryLimit) { const err = new Error(`Gave up submitting after ${this.retryLimit} blocks un-mined.`) @@ -124,7 +123,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter { const txHash = await this.publishTransaction(rawTx) // Increment successful tries: - txMeta.retryCount++ + this.emit('tx:retry', txMeta) return txHash } diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index 9ffba31ee..66772ff88 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -25,7 +25,7 @@ describe('Transaction Controller', function () { networkStore: new ObservableStore(currentNetworkId), txHistoryLimit: 10, blockTracker: { getCurrentBlock: noop, on: noop, once: noop }, - accountTracker: { store: {getState: noop} }, + accountTracker: { store: { getState: noop } }, signTransaction: (ethTx) => new Promise((resolve) => { ethTx.sign(privKey) resolve() -- cgit v1.2.3 From d77e0aff4df1f41b30653c761ce2ca19cfc19efb Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 27 Sep 2017 11:15:22 -0700 Subject: Version 3.10.4 --- CHANGELOG.md | 4 ++++ app/manifest.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f04136d78..eedfa89c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,11 @@ ## Current Master +## 3.10.4 2017-9-27 + - Fix bug that could mis-render token balances when very small. (Not actually included in 3.9.9) +- Fix memory leak warning. +- Fix bug where new event filters would not include historical events. ## 3.10.3 2017-9-21 diff --git a/app/manifest.json b/app/manifest.json index fd07f15a9..8812f4eea 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.10.3", + "version": "3.10.4", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From 734490c58c25587a247e48eea086880bcb6a14fe Mon Sep 17 00:00:00 2001 From: tmashuang Date: Wed, 27 Sep 2017 11:16:38 -0700 Subject: Add AUD, HKD, SGD, IDR, PHP to currency conversion list --- CHANGELOG.md | 2 ++ ui/app/infura-conversion.json | 55 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ece1cf75..0d9dbd1d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Added AUD, HKD, SGD, IDR, PHP to currency conversion list + ## 3.10.3 2017-9-21 - Fix bug where metamask-dapp connections are lost on rpc error diff --git a/ui/app/infura-conversion.json b/ui/app/infura-conversion.json index f4c6f9e34..9a96fe069 100644 --- a/ui/app/infura-conversion.json +++ b/ui/app/infura-conversion.json @@ -1,5 +1,60 @@ { "objects": [ + { + "symbol": "ethaud", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "aud", + "name": "Australian Dollar" + } + }, + { + "symbol": "ethhkd", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "hkd", + "name": "Hong Kong Dollar" + } + }, + { + "symbol": "ethsgd", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "sgd", + "name": "Singapore Dollar" + } + }, + { + "symbol": "ethidr", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "idr", + "name": "Indonesian Rupiah" + } + }, + { + "symbol": "ethphp", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "php", + "name": "Philippine Peso" + } + }, { "symbol": "eth1st", "base": { -- cgit v1.2.3 From 8d3fec42d0a49c2e0fe7ef3dc504533deb223d95 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 27 Sep 2017 12:09:32 -0700 Subject: Fix bug where block gas limit was incorrectly parsed. --- CHANGELOG.md | 2 ++ app/scripts/lib/account-tracker.js | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eedfa89c5..4898d27fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Fix block gas limit estimation. + ## 3.10.4 2017-9-27 - Fix bug that could mis-render token balances when very small. (Not actually included in 3.9.9) diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index e2892b1ce..07fc32b10 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -11,6 +11,7 @@ const async = require('async') const EthQuery = require('eth-query') const ObservableStore = require('obs-store') const EventEmitter = require('events').EventEmitter +const ethUtil = require('ethereumjs-util') function noop () {} @@ -59,8 +60,9 @@ class AccountTracker extends EventEmitter { _updateForBlock (block) { const blockNumber = '0x' + block.number.toString('hex') this._currentBlockNumber = blockNumber + const currentBlockGasLimit = ethUtil.addHexPrefix(block.gasLimit.toString()) - this.store.updateState({ currentBlockGasLimit: `0x${block.gasLimit.toString('hex')}` }) + this.store.updateState({ currentBlockGasLimit }) async.parallel([ this._updateAccounts.bind(this), -- cgit v1.2.3 From a453eb132d1aa97923df8900f8290a1b3ca1dee3 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 27 Sep 2017 12:10:25 -0700 Subject: Version 3.10.5 --- CHANGELOG.md | 2 ++ app/manifest.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4898d27fc..3ad9888fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +## 3.10.5 2017-9-27 + - Fix block gas limit estimation. ## 3.10.4 2017-9-27 diff --git a/app/manifest.json b/app/manifest.json index 8812f4eea..4d02cd334 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.10.4", + "version": "3.10.5", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From 1983e161c658979872bad66f8dd5e9b2c7a616b5 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 27 Sep 2017 12:29:09 -0700 Subject: Fix accountTracker store references --- app/scripts/controllers/transactions.js | 2 +- app/scripts/keyring-controller.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 4cd307b07..87521c76b 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -52,7 +52,7 @@ module.exports = class TransactionController extends EventEmitter { provider: this.provider, nonceTracker: this.nonceTracker, getBalance: (address) => { - const account = this.accountTracker.getState().accounts[address] + const account = this.accountTracker.store.getState().accounts[address] if (!account) return return account.balance }, diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js index 34e008ec4..1a1904621 100644 --- a/app/scripts/keyring-controller.js +++ b/app/scripts/keyring-controller.js @@ -568,7 +568,7 @@ class KeyringController extends EventEmitter { clearKeyrings () { let accounts try { - accounts = Object.keys(this.accountTracker.getState()) + accounts = Object.keys(this.accountTracker.store.getState()) } catch (e) { accounts = [] } -- cgit v1.2.3 From 89e690fc794a7cf0af541dbb0c1fd58d73bac368 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 27 Sep 2017 12:33:00 -0700 Subject: account-tracker - use new block-tracker block format --- app/scripts/lib/account-tracker.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index e2892b1ce..e550c2758 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -57,10 +57,8 @@ class AccountTracker extends EventEmitter { // _updateForBlock (block) { - const blockNumber = '0x' + block.number.toString('hex') - this._currentBlockNumber = blockNumber - - this.store.updateState({ currentBlockGasLimit: `0x${block.gasLimit.toString('hex')}` }) + this._currentBlockNumber = block.number + this.store.updateState({ currentBlockGasLimit: block.gasLimit }) async.parallel([ this._updateAccounts.bind(this), -- cgit v1.2.3 From b41aad6d1ae894ab89380b1c7159da8545ad935b Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 27 Sep 2017 12:33:46 -0700 Subject: style - small whitespace nitpick --- app/scripts/lib/pending-tx-tracker.js | 2 +- test/unit/pending-tx-test.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js index 44e9d50fa..8da1253a2 100644 --- a/app/scripts/lib/pending-tx-tracker.js +++ b/app/scripts/lib/pending-tx-tracker.js @@ -55,7 +55,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter { }) } - queryPendingTxs ({oldBlock, newBlock}) { + queryPendingTxs ({ oldBlock, newBlock }) { // check pending transactions on start if (!oldBlock) { this._checkPendingTxs() diff --git a/test/unit/pending-tx-test.js b/test/unit/pending-tx-test.js index 8c6d287f8..7937afa46 100644 --- a/test/unit/pending-tx-test.js +++ b/test/unit/pending-tx-test.js @@ -84,14 +84,14 @@ describe('PendingTransactionTracker', function () { let newBlock, oldBlock newBlock = { number: '0x01' } pendingTxTracker._checkPendingTxs = done - pendingTxTracker.queryPendingTxs({oldBlock, newBlock}) + pendingTxTracker.queryPendingTxs({ oldBlock, newBlock }) }) it('should call #_checkPendingTxs if oldBlock and the newBlock have a diff of greater then 1', function (done) { let newBlock, oldBlock oldBlock = { number: '0x01' } newBlock = { number: '0x03' } pendingTxTracker._checkPendingTxs = done - pendingTxTracker.queryPendingTxs({oldBlock, newBlock}) + pendingTxTracker.queryPendingTxs({ oldBlock, newBlock }) }) it('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less', function (done) { let newBlock, oldBlock @@ -101,7 +101,7 @@ describe('PendingTransactionTracker', function () { const err = new Error('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less') done(err) } - pendingTxTracker.queryPendingTxs({oldBlock, newBlock}) + pendingTxTracker.queryPendingTxs({ oldBlock, newBlock }) done() }) }) -- cgit v1.2.3 From 7e9c6e96a1c0c7ef864bf9b5b04421369de71023 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 27 Sep 2017 14:10:17 -0700 Subject: metamask - improve comment --- app/scripts/metamask-controller.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index dc39ad13e..5b3161bc6 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -232,8 +232,7 @@ module.exports = class MetamaskController extends EventEmitter { processTransaction: nodeify(async (txParams) => await this.txController.newUnapprovedTransaction(txParams), this), // old style msg signing processMessage: this.newUnsignedMessage.bind(this), - - // new style msg signing + // personal_sign msg signing processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), }) } -- cgit v1.2.3 From c781e11c7a4b8c6d0c74cc741af4d805d8bac00f Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 27 Sep 2017 14:10:58 -0700 Subject: network - remove getter/setter --- app/scripts/controllers/network.js | 37 ++++++++++++++----------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js index dc9978043..253a365e2 100644 --- a/app/scripts/controllers/network.js +++ b/app/scripts/controllers/network.js @@ -11,8 +11,8 @@ const DEFAULT_RPC = RPC_ADDRESS_LIST['rinkeby'] module.exports = class NetworkController extends EventEmitter { constructor (config) { super() - this.networkStore = new ObservableStore('loading') config.provider.rpcTarget = this.getRpcAddressForType(config.provider.type, config.provider) + this.networkStore = new ObservableStore('loading') this.providerStore = new ObservableStore(config.provider) this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore }) this._providerListeners = {} @@ -21,42 +21,33 @@ module.exports = class NetworkController extends EventEmitter { this.providerStore.subscribe((state) => this.switchNetwork({rpcUrl: state.rpcTarget})) } - get provider () { - return this._proxy - } - - set provider (provider) { - this._provider = provider - } - initializeProvider (opts, providerContructor = MetaMaskProvider) { - this.providerInit = opts + this._providerInit = opts this._provider = providerContructor(opts) this._proxy = createEventEmitterProxy(this._provider) - this.provider._blockTracker = createEventEmitterProxy(this._provider._blockTracker) - this.provider.on('block', this._logBlock.bind(this)) - this.provider.on('error', this.verifyNetwork.bind(this)) - this.ethQuery = new EthQuery(this.provider) + this._proxy._blockTracker = createEventEmitterProxy(this._provider._blockTracker) + this._proxy.on('block', this._logBlock.bind(this)) + this._proxy.on('error', this.verifyNetwork.bind(this)) + this.ethQuery = new EthQuery(this._proxy) this.lookupNetwork() - return this.provider + return this._proxy } switchNetwork (providerInit) { this.setNetworkState('loading') - const newInit = extend(this.providerInit, providerInit) - this.providerInit = newInit + const newInit = extend(this._providerInit, providerInit) + this._providerInit = newInit - this._provider.removeAllListeners() - this._provider.stop() + this._proxy.removeAllListeners() + this._proxy.stop() this._provider = MetaMaskProvider(newInit) // apply the listners created by other controllers - const blockTrackerHandlers = this.provider._blockTracker.proxyEventHandlers - this.provider.setTarget(this._provider) - this.provider._blockTracker = createEventEmitterProxy(this._provider._blockTracker, blockTrackerHandlers) + const blockTrackerHandlers = this._proxy._blockTracker.proxyEventHandlers + this._proxy.setTarget(this._provider) + this._proxy._blockTracker = createEventEmitterProxy(this._provider._blockTracker, blockTrackerHandlers) this.emit('networkDidChange') } - verifyNetwork () { // Check network when restoring connectivity: if (this.isNetworkLoading()) this.lookupNetwork() -- cgit v1.2.3 From 7d499df8e32e85f5e4ed5c51f20496028d1dcdbb Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 27 Sep 2017 14:12:45 -0700 Subject: account-tracker - remove unused import --- app/scripts/lib/account-tracker.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index a108d0d4f..cdc21282d 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -11,7 +11,6 @@ const async = require('async') const EthQuery = require('eth-query') const ObservableStore = require('obs-store') const EventEmitter = require('events').EventEmitter -const ethUtil = require('ethereumjs-util') function noop () {} -- cgit v1.2.3 From 96ebbde634c5f89793f72a2316a0c9aa2a38bc4b Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 27 Sep 2017 14:43:23 -0700 Subject: Fix Account Selection Do not select accounts on restore, only on creation and deliberate selection. Fixes #2164 --- app/scripts/controllers/preferences.js | 2 +- app/scripts/keyring-controller.js | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index e45224593..bc4848421 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -22,7 +22,7 @@ class PreferencesController { }) } - getSelectedAddress (_address) { + getSelectedAddress () { return this.store.getState().selectedAddress } diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js index 1a1904621..b5c51fd03 100644 --- a/app/scripts/keyring-controller.js +++ b/app/scripts/keyring-controller.js @@ -107,7 +107,6 @@ class KeyringController extends EventEmitter { const firstAccount = accounts[0] if (!firstAccount) throw new Error('KeyringController - First Account not found.') const hexAccount = normalizeAddress(firstAccount) - this.emit('newAccount', hexAccount) return this.setupAccounts(accounts) }) .then(this.persistAllKeyrings.bind(this, password)) @@ -207,6 +206,12 @@ class KeyringController extends EventEmitter { // and then saves those changes. addNewAccount (selectedKeyring) { return selectedKeyring.addAccounts(1) + .then((accounts) => { + accounts.forEach((hexAccount) => { + this.emit('newAccount', hexAccount) + }) + return accounts + }) .then(this.setupAccounts.bind(this)) .then(this.persistAllKeyrings.bind(this)) .then(this._updateMemStoreKeyrings.bind(this)) @@ -325,7 +330,6 @@ class KeyringController extends EventEmitter { const firstAccount = accounts[0] if (!firstAccount) throw new Error('KeyringController - No account found on keychain.') const hexAccount = normalizeAddress(firstAccount) - this.emit('newAccount', hexAccount) this.emit('newVault', hexAccount) return this.setupAccounts(accounts) }) -- cgit v1.2.3 From f2d9b75e94ba52ce34faff0640f494028d037246 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 27 Sep 2017 14:44:13 -0700 Subject: network controller - refactor to use _setProvider --- app/scripts/controllers/network.js | 45 +++++++++++++++++++++++-------------- test/unit/network-contoller-test.js | 6 ++--- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js index 253a365e2..1ed9f7eca 100644 --- a/app/scripts/controllers/network.js +++ b/app/scripts/controllers/network.js @@ -15,17 +15,16 @@ module.exports = class NetworkController extends EventEmitter { this.networkStore = new ObservableStore('loading') this.providerStore = new ObservableStore(config.provider) this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore }) - this._providerListeners = {} + this._proxy = createEventEmitterProxy() this.on('networkDidChange', this.lookupNetwork) - this.providerStore.subscribe((state) => this.switchNetwork({rpcUrl: state.rpcTarget})) + this.providerStore.subscribe((state) => this.switchNetwork({ rpcUrl: state.rpcTarget })) } initializeProvider (opts, providerContructor = MetaMaskProvider) { - this._providerInit = opts - this._provider = providerContructor(opts) - this._proxy = createEventEmitterProxy(this._provider) - this._proxy._blockTracker = createEventEmitterProxy(this._provider._blockTracker) + this._baseProviderParams = opts + const provider = providerContructor(opts) + this._setProvider(provider) this._proxy.on('block', this._logBlock.bind(this)) this._proxy.on('error', this.verifyNetwork.bind(this)) this.ethQuery = new EthQuery(this._proxy) @@ -33,21 +32,33 @@ module.exports = class NetworkController extends EventEmitter { return this._proxy } - switchNetwork (providerInit) { + switchNetwork (opts) { this.setNetworkState('loading') - const newInit = extend(this._providerInit, providerInit) - this._providerInit = newInit - - this._proxy.removeAllListeners() - this._proxy.stop() - this._provider = MetaMaskProvider(newInit) - // apply the listners created by other controllers - const blockTrackerHandlers = this._proxy._blockTracker.proxyEventHandlers - this._proxy.setTarget(this._provider) - this._proxy._blockTracker = createEventEmitterProxy(this._provider._blockTracker, blockTrackerHandlers) + const providerParams = extend(this._baseProviderParams, opts) + this._baseProviderParams = providerParams + const provider = MetaMaskProvider(providerParams) + this._setProvider(provider) this.emit('networkDidChange') } + _setProvider (provider) { + // collect old block tracker events + const oldProvider = this._provider + let blockTrackerHandlers + if (oldProvider) { + // capture old block handlers + blockTrackerHandlers = oldProvider._blockTracker.proxyEventHandlers + // tear down + oldProvider.removeAllListeners() + oldProvider.stop() + } + // override block tracler + provider._blockTracker = createEventEmitterProxy(provider._blockTracker, blockTrackerHandlers) + // set as new provider + this._provider = provider + this._proxy.setTarget(provider) + } + verifyNetwork () { // Check network when restoring connectivity: if (this.isNetworkLoading()) this.lookupNetwork() diff --git a/test/unit/network-contoller-test.js b/test/unit/network-contoller-test.js index c1fdaf032..0b3b5adeb 100644 --- a/test/unit/network-contoller-test.js +++ b/test/unit/network-contoller-test.js @@ -20,9 +20,9 @@ describe('# Network Controller', function () { describe('#provider', function () { it('provider should be updatable without reassignment', function () { networkController.initializeProvider(networkControllerProviderInit, dummyProviderConstructor) - const provider = networkController.provider - networkController.provider.setTarget({test: true, on: () => {}}) - assert.ok(provider.test) + const proxy = networkController._proxy + proxy.setTarget({ test: true, on: () => {} }) + assert.ok(proxy.test) }) }) describe('#getNetworkState', function () { -- cgit v1.2.3 From ea12be2c1bc488cfa5f85ced5472ec1515ddb00f Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 27 Sep 2017 14:44:44 -0700 Subject: Bump changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ad9888fd..5b54ed1a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Current Master +- Fix bug where newly created accounts were not selected. +- Fix bug where selected account was not persisted between lockings. + ## 3.10.5 2017-9-27 - Fix block gas limit estimation. -- cgit v1.2.3 From 06b5dd2096b6bfcdf7d9ebf7c9bb1e40c8aed2e0 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 27 Sep 2017 14:44:54 -0700 Subject: network controller - move _setProvider to bottom --- app/scripts/controllers/network.js | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js index 1ed9f7eca..2a17cdae8 100644 --- a/app/scripts/controllers/network.js +++ b/app/scripts/controllers/network.js @@ -41,24 +41,6 @@ module.exports = class NetworkController extends EventEmitter { this.emit('networkDidChange') } - _setProvider (provider) { - // collect old block tracker events - const oldProvider = this._provider - let blockTrackerHandlers - if (oldProvider) { - // capture old block handlers - blockTrackerHandlers = oldProvider._blockTracker.proxyEventHandlers - // tear down - oldProvider.removeAllListeners() - oldProvider.stop() - } - // override block tracler - provider._blockTracker = createEventEmitterProxy(provider._blockTracker, blockTrackerHandlers) - // set as new provider - this._provider = provider - this._proxy.setTarget(provider) - } - verifyNetwork () { // Check network when restoring connectivity: if (this.isNetworkLoading()) this.lookupNetwork() @@ -112,6 +94,24 @@ module.exports = class NetworkController extends EventEmitter { return provider && provider.rpcTarget ? provider.rpcTarget : DEFAULT_RPC } + _setProvider (provider) { + // collect old block tracker events + const oldProvider = this._provider + let blockTrackerHandlers + if (oldProvider) { + // capture old block handlers + blockTrackerHandlers = oldProvider._blockTracker.proxyEventHandlers + // tear down + oldProvider.removeAllListeners() + oldProvider.stop() + } + // override block tracler + provider._blockTracker = createEventEmitterProxy(provider._blockTracker, blockTrackerHandlers) + // set as new provider + this._provider = provider + this._proxy.setTarget(provider) + } + _logBlock (block) { log.info(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`) this.verifyNetwork() -- cgit v1.2.3 From aefd17ef9418148c1a35dbf72da7991366649f5c Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 27 Sep 2017 14:45:24 -0700 Subject: Remove dead reference --- app/scripts/keyring-controller.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js index b5c51fd03..4627b850e 100644 --- a/app/scripts/keyring-controller.js +++ b/app/scripts/keyring-controller.js @@ -106,7 +106,6 @@ class KeyringController extends EventEmitter { .then((accounts) => { const firstAccount = accounts[0] if (!firstAccount) throw new Error('KeyringController - First Account not found.') - const hexAccount = normalizeAddress(firstAccount) return this.setupAccounts(accounts) }) .then(this.persistAllKeyrings.bind(this, password)) -- cgit v1.2.3 From c0d7d447647d779deafb8b09a3f7a1d523c322b6 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Wed, 27 Sep 2017 21:54:46 +0000 Subject: fix(package): update eth-keyring-controller to version 2.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c160cbfde..052d20a22 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "eth-contract-metadata": "^1.1.4", "eth-hd-keyring": "^1.1.1", "eth-json-rpc-filters": "^1.2.1", - "eth-keyring-controller": "^1.0.1", + "eth-keyring-controller": "^2.0.0", "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", "eth-sig-util": "^1.2.2", -- cgit v1.2.3 From a246770866d242404ce275517c9223fc922e4312 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 27 Sep 2017 14:55:34 -0700 Subject: Commit to the eth-keyring-controller module --- app/scripts/keyring-controller.js | 599 -------------------------------------- package.json | 2 +- 2 files changed, 1 insertion(+), 600 deletions(-) delete mode 100644 app/scripts/keyring-controller.js diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js deleted file mode 100644 index 4627b850e..000000000 --- a/app/scripts/keyring-controller.js +++ /dev/null @@ -1,599 +0,0 @@ -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const bip39 = require('bip39') -const EventEmitter = require('events').EventEmitter -const ObservableStore = require('obs-store') -const filter = require('promise-filter') -const encryptor = require('browser-passworder') -const sigUtil = require('eth-sig-util') -const normalizeAddress = sigUtil.normalize -// Keyrings: -const SimpleKeyring = require('eth-simple-keyring') -const HdKeyring = require('eth-hd-keyring') -const keyringTypes = [ - SimpleKeyring, - HdKeyring, -] - -class KeyringController extends EventEmitter { - - // PUBLIC METHODS - // - // THE FIRST SECTION OF METHODS ARE PUBLIC-FACING, - // MEANING THEY ARE USED BY CONSUMERS OF THIS CLASS. - // - // THEIR SURFACE AREA SHOULD BE CHANGED WITH GREAT CARE. - - constructor (opts) { - super() - const initState = opts.initState || {} - this.keyringTypes = keyringTypes - this.store = new ObservableStore(initState) - this.memStore = new ObservableStore({ - isUnlocked: false, - keyringTypes: this.keyringTypes.map(krt => krt.type), - keyrings: [], - identities: {}, - }) - - this.accountTracker = opts.accountTracker - this.encryptor = opts.encryptor || encryptor - this.keyrings = [] - this.getNetwork = opts.getNetwork - } - - // Full Update - // returns Promise( @object state ) - // - // Emits the `update` event and - // returns a Promise that resolves to the current state. - // - // Frequently used to end asynchronous chains in this class, - // indicating consumers can often either listen for updates, - // or accept a state-resolving promise to consume their results. - // - // Not all methods end with this, that might be a nice refactor. - fullUpdate () { - this.emit('update') - return Promise.resolve(this.memStore.getState()) - } - - // Create New Vault And Keychain - // @string password - The password to encrypt the vault with - // - // returns Promise( @object state ) - // - // Destroys any old encrypted storage, - // creates a new encrypted store with the given password, - // randomly creates a new HD wallet with 1 account, - // faucets that account on the testnet. - createNewVaultAndKeychain (password) { - return this.persistAllKeyrings(password) - .then(this.createFirstKeyTree.bind(this)) - .then(this.fullUpdate.bind(this)) - } - - // CreateNewVaultAndRestore - // @string password - The password to encrypt the vault with - // @string seed - The BIP44-compliant seed phrase. - // - // returns Promise( @object state ) - // - // Destroys any old encrypted storage, - // creates a new encrypted store with the given password, - // creates a new HD wallet from the given seed with 1 account. - createNewVaultAndRestore (password, seed) { - if (typeof password !== 'string') { - return Promise.reject('Password must be text.') - } - - if (!bip39.validateMnemonic(seed)) { - return Promise.reject(new Error('Seed phrase is invalid.')) - } - - this.clearKeyrings() - - return this.persistAllKeyrings(password) - .then(() => { - return this.addNewKeyring('HD Key Tree', { - mnemonic: seed, - numberOfAccounts: 1, - }) - }) - .then((firstKeyring) => { - return firstKeyring.getAccounts() - }) - .then((accounts) => { - const firstAccount = accounts[0] - if (!firstAccount) throw new Error('KeyringController - First Account not found.') - return this.setupAccounts(accounts) - }) - .then(this.persistAllKeyrings.bind(this, password)) - .then(this.fullUpdate.bind(this)) - } - - // Set Locked - // returns Promise( @object state ) - // - // This method deallocates all secrets, and effectively locks metamask. - setLocked () { - // set locked - this.password = null - this.memStore.updateState({ isUnlocked: false }) - // remove keyrings - this.keyrings = [] - this._updateMemStoreKeyrings() - return this.fullUpdate() - } - - // Submit Password - // @string password - // - // returns Promise( @object state ) - // - // Attempts to decrypt the current vault and load its keyrings - // into memory. - // - // Temporarily also migrates any old-style vaults first, as well. - // (Pre MetaMask 3.0.0) - submitPassword (password) { - return this.unlockKeyrings(password) - .then((keyrings) => { - this.keyrings = keyrings - return this.fullUpdate() - }) - } - - // Add New Keyring - // @string type - // @object opts - // - // returns Promise( @Keyring keyring ) - // - // Adds a new Keyring of the given `type` to the vault - // and the current decrypted Keyrings array. - // - // All Keyring classes implement a unique `type` string, - // and this is used to retrieve them from the keyringTypes array. - addNewKeyring (type, opts) { - const Keyring = this.getKeyringClassForType(type) - const keyring = new Keyring(opts) - return keyring.deserialize(opts) - .then(() => { - return keyring.getAccounts() - }) - .then((accounts) => { - return this.checkForDuplicate(type, accounts) - }) - .then((checkedAccounts) => { - this.keyrings.push(keyring) - return this.setupAccounts(checkedAccounts) - }) - .then(() => this.persistAllKeyrings()) - .then(() => this._updateMemStoreKeyrings()) - .then(() => this.fullUpdate()) - .then(() => { - return keyring - }) - } - - // For now just checks for simple key pairs - // but in the future - // should possibly add HD and other types - // - checkForDuplicate (type, newAccount) { - return this.getAccounts() - .then((accounts) => { - switch (type) { - case 'Simple Key Pair': - const isNotIncluded = !accounts.find((key) => key === newAccount[0] || key === ethUtil.stripHexPrefix(newAccount[0])) - return (isNotIncluded) ? Promise.resolve(newAccount) : Promise.reject(new Error('The account you\'re are trying to import is a duplicate')) - default: - return Promise.resolve(newAccount) - } - }) - } - - - // Add New Account - // @number keyRingNum - // - // returns Promise( @object state ) - // - // Calls the `addAccounts` method on the Keyring - // in the kryings array at index `keyringNum`, - // and then saves those changes. - addNewAccount (selectedKeyring) { - return selectedKeyring.addAccounts(1) - .then((accounts) => { - accounts.forEach((hexAccount) => { - this.emit('newAccount', hexAccount) - }) - return accounts - }) - .then(this.setupAccounts.bind(this)) - .then(this.persistAllKeyrings.bind(this)) - .then(this._updateMemStoreKeyrings.bind(this)) - .then(this.fullUpdate.bind(this)) - } - - // Save Account Label - // @string account - // @string label - // - // returns Promise( @string label ) - // - // Persists a nickname equal to `label` for the specified account. - saveAccountLabel (account, label) { - try { - const hexAddress = normalizeAddress(account) - // update state on diskStore - const state = this.store.getState() - const walletNicknames = state.walletNicknames || {} - walletNicknames[hexAddress] = label - this.store.updateState({ walletNicknames }) - // update state on memStore - const identities = this.memStore.getState().identities - identities[hexAddress].name = label - this.memStore.updateState({ identities }) - return Promise.resolve(label) - } catch (err) { - return Promise.reject(err) - } - } - - // Export Account - // @string address - // - // returns Promise( @string privateKey ) - // - // Requests the private key from the keyring controlling - // the specified address. - // - // Returns a Promise that may resolve with the private key string. - exportAccount (address) { - try { - return this.getKeyringForAccount(address) - .then((keyring) => { - return keyring.exportAccount(normalizeAddress(address)) - }) - } catch (e) { - return Promise.reject(e) - } - } - - - // SIGNING METHODS - // - // This method signs tx and returns a promise for - // TX Manager to update the state after signing - - signTransaction (ethTx, _fromAddress) { - const fromAddress = normalizeAddress(_fromAddress) - return this.getKeyringForAccount(fromAddress) - .then((keyring) => { - return keyring.signTransaction(fromAddress, ethTx) - }) - } - - // Sign Message - // @object msgParams - // - // returns Promise(@buffer rawSig) - // - // Attempts to sign the provided @object msgParams. - signMessage (msgParams) { - const address = normalizeAddress(msgParams.from) - return this.getKeyringForAccount(address) - .then((keyring) => { - return keyring.signMessage(address, msgParams.data) - }) - } - - // Sign Personal Message - // @object msgParams - // - // returns Promise(@buffer rawSig) - // - // Attempts to sign the provided @object msgParams. - // Prefixes the hash before signing as per the new geth behavior. - signPersonalMessage (msgParams) { - const address = normalizeAddress(msgParams.from) - return this.getKeyringForAccount(address) - .then((keyring) => { - return keyring.signPersonalMessage(address, msgParams.data) - }) - } - - // PRIVATE METHODS - // - // THESE METHODS ARE ONLY USED INTERNALLY TO THE KEYRING-CONTROLLER - // AND SO MAY BE CHANGED MORE LIBERALLY THAN THE ABOVE METHODS. - - // Create First Key Tree - // returns @Promise - // - // Clears the vault, - // creates a new one, - // creates a random new HD Keyring with 1 account, - // makes that account the selected account, - // faucets that account on testnet, - // puts the current seed words into the state tree. - createFirstKeyTree () { - this.clearKeyrings() - return this.addNewKeyring('HD Key Tree', { numberOfAccounts: 1 }) - .then((keyring) => { - return keyring.getAccounts() - }) - .then((accounts) => { - const firstAccount = accounts[0] - if (!firstAccount) throw new Error('KeyringController - No account found on keychain.') - const hexAccount = normalizeAddress(firstAccount) - this.emit('newVault', hexAccount) - return this.setupAccounts(accounts) - }) - .then(this.persistAllKeyrings.bind(this)) - } - - // Setup Accounts - // @array accounts - // - // returns @Promise(@object account) - // - // Initializes the provided account array - // Gives them numerically incremented nicknames, - // and adds them to the accountTracker for regular balance checking. - setupAccounts (accounts) { - return this.getAccounts() - .then((loadedAccounts) => { - const arr = accounts || loadedAccounts - return Promise.all(arr.map((account) => { - return this.getBalanceAndNickname(account) - })) - }) - } - - // Get Balance And Nickname - // @string account - // - // returns Promise( @string label ) - // - // Takes an account address and an iterator representing - // the current number of named accounts. - getBalanceAndNickname (account) { - if (!account) { - throw new Error('Problem loading account.') - } - const address = normalizeAddress(account) - this.accountTracker.addAccount(address) - return this.createNickname(address) - } - - // Create Nickname - // @string address - // - // returns Promise( @string label ) - // - // Takes an address, and assigns it an incremented nickname, persisting it. - createNickname (address) { - const hexAddress = normalizeAddress(address) - const identities = this.memStore.getState().identities - const currentIdentityCount = Object.keys(identities).length + 1 - const nicknames = this.store.getState().walletNicknames || {} - const existingNickname = nicknames[hexAddress] - const name = existingNickname || `Account ${currentIdentityCount}` - identities[hexAddress] = { - address: hexAddress, - name, - } - this.memStore.updateState({ identities }) - return this.saveAccountLabel(hexAddress, name) - } - - // Persist All Keyrings - // @password string - // - // returns Promise - // - // Iterates the current `keyrings` array, - // serializes each one into a serialized array, - // encrypts that array with the provided `password`, - // and persists that encrypted string to storage. - persistAllKeyrings (password = this.password) { - if (typeof password === 'string') { - this.password = password - this.memStore.updateState({ isUnlocked: true }) - } - return Promise.all(this.keyrings.map((keyring) => { - return Promise.all([keyring.type, keyring.serialize()]) - .then((serializedKeyringArray) => { - // Label the output values on each serialized Keyring: - return { - type: serializedKeyringArray[0], - data: serializedKeyringArray[1], - } - }) - })) - .then((serializedKeyrings) => { - return this.encryptor.encrypt(this.password, serializedKeyrings) - }) - .then((encryptedString) => { - this.store.updateState({ vault: encryptedString }) - return true - }) - } - - // Unlock Keyrings - // @string password - // - // returns Promise( @array keyrings ) - // - // Attempts to unlock the persisted encrypted storage, - // initializing the persisted keyrings to RAM. - unlockKeyrings (password) { - const encryptedVault = this.store.getState().vault - if (!encryptedVault) { - throw new Error('Cannot unlock without a previous vault.') - } - - return this.encryptor.decrypt(password, encryptedVault) - .then((vault) => { - this.password = password - this.memStore.updateState({ isUnlocked: true }) - vault.forEach(this.restoreKeyring.bind(this)) - return this.keyrings - }) - } - - // Restore Keyring - // @object serialized - // - // returns Promise( @Keyring deserialized ) - // - // Attempts to initialize a new keyring from the provided - // serialized payload. - // - // On success, returns the resulting @Keyring instance. - restoreKeyring (serialized) { - const { type, data } = serialized - - const Keyring = this.getKeyringClassForType(type) - const keyring = new Keyring() - return keyring.deserialize(data) - .then(() => { - return keyring.getAccounts() - }) - .then((accounts) => { - return this.setupAccounts(accounts) - }) - .then(() => { - this.keyrings.push(keyring) - this._updateMemStoreKeyrings() - return keyring - }) - } - - // Get Keyring Class For Type - // @string type - // - // Returns @class Keyring - // - // Searches the current `keyringTypes` array - // for a Keyring class whose unique `type` property - // matches the provided `type`, - // returning it if it exists. - getKeyringClassForType (type) { - return this.keyringTypes.find(kr => kr.type === type) - } - - getKeyringsByType (type) { - return this.keyrings.filter((keyring) => keyring.type === type) - } - - // Get Accounts - // returns Promise( @Array[ @string accounts ] ) - // - // Returns the public addresses of all current accounts - // managed by all currently unlocked keyrings. - getAccounts () { - const keyrings = this.keyrings || [] - return Promise.all(keyrings.map(kr => kr.getAccounts())) - .then((keyringArrays) => { - return keyringArrays.reduce((res, arr) => { - return res.concat(arr) - }, []) - }) - } - - // Get Keyring For Account - // @string address - // - // returns Promise(@Keyring keyring) - // - // Returns the currently initialized keyring that manages - // the specified `address` if one exists. - getKeyringForAccount (address) { - const hexed = normalizeAddress(address) - log.debug(`KeyringController - getKeyringForAccount: ${hexed}`) - - return Promise.all(this.keyrings.map((keyring) => { - return Promise.all([ - keyring, - keyring.getAccounts(), - ]) - })) - .then(filter((candidate) => { - const accounts = candidate[1].map(normalizeAddress) - return accounts.includes(hexed) - })) - .then((winners) => { - if (winners && winners.length > 0) { - return winners[0][0] - } else { - throw new Error('No keyring found for the requested account.') - } - }) - } - - // Display For Keyring - // @Keyring keyring - // - // returns Promise( @Object { type:String, accounts:Array } ) - // - // Is used for adding the current keyrings to the state object. - displayForKeyring (keyring) { - return keyring.getAccounts() - .then((accounts) => { - return { - type: keyring.type, - accounts: accounts, - } - }) - } - - // Add Gas Buffer - // @string gas (as hexadecimal value) - // - // returns @string bufferedGas (as hexadecimal value) - // - // Adds a healthy buffer of gas to an initial gas estimate. - addGasBuffer (gas) { - const gasBuffer = new BN('100000', 10) - const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16) - const correct = bnGas.add(gasBuffer) - return ethUtil.addHexPrefix(correct.toString(16)) - } - - // Clear Keyrings - // - // Deallocates all currently managed keyrings and accounts. - // Used before initializing a new vault. - clearKeyrings () { - let accounts - try { - accounts = Object.keys(this.accountTracker.store.getState()) - } catch (e) { - accounts = [] - } - accounts.forEach((address) => { - this.accountTracker.removeAccount(address) - }) - - // clear keyrings from memory - this.keyrings = [] - this.memStore.updateState({ - keyrings: [], - identities: {}, - }) - } - - _updateMemStoreKeyrings () { - Promise.all(this.keyrings.map(this.displayForKeyring)) - .then((keyrings) => { - this.memStore.updateState({ keyrings }) - }) - } - -} - -module.exports = KeyringController diff --git a/package.json b/package.json index c160cbfde..052d20a22 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "eth-contract-metadata": "^1.1.4", "eth-hd-keyring": "^1.1.1", "eth-json-rpc-filters": "^1.2.1", - "eth-keyring-controller": "^1.0.1", + "eth-keyring-controller": "^2.0.0", "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", "eth-sig-util": "^1.2.2", -- cgit v1.2.3 From cd0f44e2d6e1c54dc929ad35df08c04422e55b32 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 27 Sep 2017 14:59:10 -0700 Subject: deps - bump express for security fix --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c160cbfde..60d1ad1eb 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "ethjs-contract": "^0.1.9", "ethjs-ens": "^2.0.0", "ethjs-query": "^0.2.9", - "express": "^4.14.0", + "express": "^4.15.5", "extension-link-enabler": "^1.0.0", "extensionizer": "^1.0.0", "fast-json-patch": "^2.0.4", -- cgit v1.2.3 From b473d440a36d7715582431ff0024d16e4da3bc36 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 27 Sep 2017 15:14:40 -0700 Subject: Version 3.10.6 --- CHANGELOG.md | 2 ++ app/manifest.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b54ed1a6..599f340f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +## 3.10.6 2017-9-27 + - Fix bug where newly created accounts were not selected. - Fix bug where selected account was not persisted between lockings. diff --git a/app/manifest.json b/app/manifest.json index 4d02cd334..639f3fb4b 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.10.5", + "version": "3.10.6", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From b24e16d346d0e71b140a659815a32be9ce7cd117 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 27 Sep 2017 16:14:58 -0700 Subject: re-enabled x-metamask-origin for mascara --- app/scripts/metamask-controller.js | 2 +- app/scripts/platforms/extension.js | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 4d149545a..fef16c3a9 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -196,7 +196,7 @@ module.exports = class MetamaskController extends EventEmitter { }, // rpc data source rpcUrl: this.networkController.getCurrentRpcAddress(), - originHttpHeaderKey: this.platform.isExtension ? 'X-Metamask-Origin' : undefined, + originHttpHeaderKey: 'X-Metamask-Origin', // account mgmt getAccounts: (cb) => { const isUnlocked = this.keyringController.memStore.getState().isUnlocked diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index d97138207..0afe04b74 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -5,10 +5,6 @@ class ExtensionPlatform { // // Public // - get isExtension () { - return true - } - reload () { extension.runtime.reload() } -- cgit v1.2.3 From 01816e1b2216e0cf849ec3d67f01b1e571d69fa4 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 22 Sep 2017 17:27:18 -0230 Subject: Adds a back button to export private key modal; connects account details to same modal. --- ui/app/components/modals/account-details-modal.js | 17 +++++++++++++++-- ui/app/components/modals/account-modal-container.js | 19 +++++++++++++++++-- ui/app/components/modals/export-private-key-modal.js | 10 +++++++++- ui/app/css/itcss/components/modal.scss | 15 +++++++++++++++ ui/app/reducers/app.js | 7 ++++++- 5 files changed, 62 insertions(+), 6 deletions(-) diff --git a/ui/app/components/modals/account-details-modal.js b/ui/app/components/modals/account-details-modal.js index 6c2eba7bd..37a62e1c0 100644 --- a/ui/app/components/modals/account-details-modal.js +++ b/ui/app/components/modals/account-details-modal.js @@ -19,6 +19,10 @@ function mapDispatchToProps (dispatch) { return { // Is this supposed to be used somewhere? showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)), + showExportPrivateKeyModal: () => { + dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' })) + }, + hideModal: () => dispatch(actions.hideModal()), } } @@ -33,7 +37,12 @@ module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountDetailsModa // fonts of qr-header AccountDetailsModal.prototype.render = function () { - const { selectedIdentity, network } = this.props + const { + selectedIdentity, + network, + showExportPrivateKeyModal, + hideModal, + } = this.props const { name, address } = selectedIdentity return h(AccountModalContainer, {}, [ @@ -51,7 +60,11 @@ AccountDetailsModal.prototype.render = function () { }, [ 'View account on Etherscan' ]), // Holding on redesign for Export Private Key functionality - h('button.btn-clear', [ 'Export private key' ]), + h('button.btn-clear', { + onClick: () => { + showExportPrivateKeyModal() + }, + }, [ 'Export private key' ]), ]) } diff --git a/ui/app/components/modals/account-modal-container.js b/ui/app/components/modals/account-modal-container.js index 69650ca15..3cad72067 100644 --- a/ui/app/components/modals/account-modal-container.js +++ b/ui/app/components/modals/account-modal-container.js @@ -28,8 +28,13 @@ function AccountModalContainer () { module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountModalContainer) AccountModalContainer.prototype.render = function () { - const { selectedIdentity, children } = this.props - console.log(`children`, children); + const { + selectedIdentity, + children, + showBackButton = false, + backButtonAction, + } = this.props + return h('div', { style: { borderRadius: '4px' }}, [ h('div.account-modal-container', [ @@ -44,6 +49,16 @@ AccountModalContainer.prototype.render = function () { ]), + showBackButton && h('div.account-modal-back', { + onClick: backButtonAction, + }, [ + + h('i.fa.fa-angle-left.fa-lg'), + + h('span.account-modal-back__text', ' Back'), + + ]), + h('div.account-modal-close', { onClick: this.props.hideModal, }), diff --git a/ui/app/components/modals/export-private-key-modal.js b/ui/app/components/modals/export-private-key-modal.js index b1d551781..4bb34f8c6 100644 --- a/ui/app/components/modals/export-private-key-modal.js +++ b/ui/app/components/modals/export-private-key-modal.js @@ -14,12 +14,14 @@ function mapStateToProps (state) { privateKey: state.appState.accountDetail.privateKey, network: state.metamask.network, selectedIdentity: getSelectedIdentity(state), + previousModalState: state.appState.modal.previousModalState.name, } } function mapDispatchToProps (dispatch) { return { exportAccount: (password, address) => dispatch(actions.exportAccount(password, address)), + showAccountDetailModal: () => dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' })), hideModal: () => dispatch(actions.hideModal()), } } @@ -86,10 +88,16 @@ ExportPrivateKeyModal.prototype.render = function () { network, privateKey, warning, + showAccountDetailModal, + hideModal, + previousModalState, } = this.props const { name, address } = selectedIdentity - return h(AccountModalContainer, {}, [ + return h(AccountModalContainer, { + showBackButton: previousModalState === 'ACCOUNT_DETAILS', + backButtonAction: () => showAccountDetailModal(), + }, [ h('span.account-name', name), diff --git a/ui/app/css/itcss/components/modal.scss b/ui/app/css/itcss/components/modal.scss index 00b6111f7..fd61ad4f4 100644 --- a/ui/app/css/itcss/components/modal.scss +++ b/ui/app/css/itcss/components/modal.scss @@ -192,6 +192,21 @@ } } +.account-modal-back { + color: $dusty-gray; + position: absolute; + top: 13px; + left: 17px; + cursor: pointer; + + &__text { + margin-top: 2px; + font-family: 'DIN OT'; + font-size: 14px; + line-height: 18px; + } +} + .account-modal-close::after { content: '\00D7'; font-size: 40px; diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 6d805521b..4f10d9857 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -42,6 +42,9 @@ function reduceApp (state, action) { modalState: { name: null, }, + previousModalState: { + name: null, + } }, sidebarOpen: false, networkDropdownOpen: false, @@ -87,6 +90,7 @@ function reduceApp (state, action) { state.appState.modal, { open: true }, { modalState: action.payload }, + { previousModalState: appState.modal.modalState}, ), }) @@ -95,7 +99,8 @@ function reduceApp (state, action) { modal: Object.assign( state.appState.modal, { open: false }, - { modalState: action.payload || state.appState.modal.modalState }, + { modalState: { name: null } }, + { previousModalState: appState.modal.modalState}, ), }) -- cgit v1.2.3 From 10345a12c2f812fabbcd9950da14beaa03cb2502 Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 26 Sep 2017 20:33:33 -0230 Subject: Keep privateKey out of state and clear it after closing export private key modal. --- ui/app/actions.js | 39 ++++++++++++++-------- .../components/modals/export-private-key-modal.js | 21 ++++++++---- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index 63d22238b..0b860ee63 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -106,6 +106,7 @@ var actions = { exportAccount: exportAccount, SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY', showPrivateKey: showPrivateKey, + exportAccountComplete, SAVE_ACCOUNT_LABEL: 'SAVE_ACCOUNT_LABEL', saveAccountLabel: saveAccountLabel, // tx conf screen @@ -984,27 +985,39 @@ function exportAccount (password, address) { dispatch(self.showLoadingIndication()) log.debug(`background.submitPassword`) - background.submitPassword(password, function (err) { - if (err) { - log.error('Error in submiting password.') - dispatch(self.hideLoadingIndication()) - return dispatch(self.displayWarning('Incorrect Password.')) - } - log.debug(`background.exportAccount`) - background.exportAccount(address, function (err, result) { - dispatch(self.hideLoadingIndication()) - + return new Promise((resolve, reject) => { + background.submitPassword(password, function (err) { if (err) { - log.error(err) - return dispatch(self.displayWarning('Had a problem exporting the account.')) + log.error('Error in submiting password.') + dispatch(self.hideLoadingIndication()) + dispatch(self.displayWarning('Incorrect Password.')) + return reject(err) } + log.debug(`background.exportAccount`) + return background.exportAccount(address, function (err, result) { + dispatch(self.hideLoadingIndication()) + + if (err) { + log.error(err) + dispatch(self.displayWarning('Had a problem exporting the account.')) + return reject(err) + } - dispatch(self.showPrivateKey(result)) + dispatch(self.exportAccountComplete()) + + return resolve(result) + }) }) }) } } +function exportAccountComplete() { + return { + type: actions.EXPORT_ACCOUNT, + } +} + function showPrivateKey (key) { return { type: actions.SHOW_PRIVATE_KEY, diff --git a/ui/app/components/modals/export-private-key-modal.js b/ui/app/components/modals/export-private-key-modal.js index 4bb34f8c6..ddc7f1352 100644 --- a/ui/app/components/modals/export-private-key-modal.js +++ b/ui/app/components/modals/export-private-key-modal.js @@ -31,12 +31,20 @@ function ExportPrivateKeyModal () { Component.call(this) this.state = { - password: '' + password: '', + privateKey: null, } } module.exports = connect(mapStateToProps, mapDispatchToProps)(ExportPrivateKeyModal) +ExportPrivateKeyModal.prototype.exportAccountAndGetPrivateKey = function (password, address) { + const { exportAccount } = this.props + + exportAccount(password, address) + .then(privateKey => this.setState({ privateKey })) +} + ExportPrivateKeyModal.prototype.renderPasswordLabel = function (privateKey) { return h('span.private-key-password-label', privateKey ? 'This is your private key (click to copy)' @@ -68,15 +76,13 @@ ExportPrivateKeyModal.prototype.renderButton = function (className, onClick, lab }, label) } -ExportPrivateKeyModal.prototype.renderButtons = function (privateKey, password, address) { - const { hideModal, exportAccount } = this.props - +ExportPrivateKeyModal.prototype.renderButtons = function (privateKey, password, address, hideModal) { return h('div.export-private-key-buttons', {}, [ !privateKey && this.renderButton('btn-clear btn-cancel', () => hideModal(), 'Cancel'), (privateKey ? this.renderButton('btn-clear', () => hideModal(), 'Done') - : this.renderButton('btn-clear', () => exportAccount(this.state.password, address), 'Download') + : this.renderButton('btn-clear', () => this.exportAccountAndGetPrivateKey(this.state.password, address), 'Download') ), ]) @@ -86,7 +92,6 @@ ExportPrivateKeyModal.prototype.render = function () { const { selectedIdentity, network, - privateKey, warning, showAccountDetailModal, hideModal, @@ -94,6 +99,8 @@ ExportPrivateKeyModal.prototype.render = function () { } = this.props const { name, address } = selectedIdentity + const { privateKey } = this.state + return h(AccountModalContainer, { showBackButton: previousModalState === 'ACCOUNT_DETAILS', backButtonAction: () => showAccountDetailModal(), @@ -124,7 +131,7 @@ ExportPrivateKeyModal.prototype.render = function () { account.` ), - this.renderButtons(privateKey, this.state.password, address), + this.renderButtons(privateKey, this.state.password, address, hideModal), ]) } -- cgit v1.2.3 From c2ccd6e90ed15245ed4e9e2965f2f5c9f293f115 Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 27 Sep 2017 21:45:27 -0230 Subject: Makes styling fixes to account dropdown. --- .../dropdowns/components/account-dropdowns.js | 17 ++++++++++------- ui/app/css/itcss/tools/utilities.scss | 5 ++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/ui/app/components/dropdowns/components/account-dropdowns.js b/ui/app/components/dropdowns/components/account-dropdowns.js index 76f186a3f..3e31412c6 100644 --- a/ui/app/components/dropdowns/components/account-dropdowns.js +++ b/ui/app/components/dropdowns/components/account-dropdowns.js @@ -51,6 +51,7 @@ class AccountDropdowns extends Component { { marginTop: index === 0 ? '5px' : '', fontSize: '24px', + width: '260px', }, menuItemStyles, ), @@ -92,6 +93,7 @@ class AccountDropdowns extends Component { alignItems: 'flex-start', justifyContent: 'center', marginLeft: '10px', + position: 'relative', }, }, [ this.indicateIfLoose(keyring), @@ -104,7 +106,6 @@ class AccountDropdowns extends Component { textOverflow: 'ellipsis', }, }, identity.name || ''), - h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, isSelected ? h('.check', '✓') : null), h('span.account-dropdown-balance', { style: { @@ -202,17 +203,19 @@ class AccountDropdowns extends Component { }, [ h( - Identicon, + 'div', { style: { - marginLeft: '10px', + marginLeft: '8px', + fontFamily: 'Montserrat UltraLight', + fontSize: '30px', }, - diameter: 32, }, + '+' ), h('span', { style: { - marginLeft: '20px', + marginLeft: '14px', fontFamily: 'DIN OT', fontSize: '16px', lineHeight: '23px', @@ -232,13 +235,13 @@ class AccountDropdowns extends Component { }, [ h( - Identicon, + 'div', { style: { marginLeft: '10px', }, - diameter: 32, }, + String.fromCharCode(10515) ), h('span', { style: { diff --git a/ui/app/css/itcss/tools/utilities.scss b/ui/app/css/itcss/tools/utilities.scss index 9f1caa732..4a55303b9 100644 --- a/ui/app/css/itcss/tools/utilities.scss +++ b/ui/app/css/itcss/tools/utilities.scss @@ -239,9 +239,8 @@ hr.horizontal-line { height: 20px; min-width: 20px; position: absolute; - display: flex; - align-items: center; - justify-content: center; + top: 0px; + right: 5px; padding: 4px; } -- cgit v1.2.3 From deee689426f0b6236093128b47be81faf56d6b75 Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 27 Sep 2017 22:26:44 -0230 Subject: Use font-awesome icons for create and import account. --- .../components/dropdowns/components/account-dropdowns.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/ui/app/components/dropdowns/components/account-dropdowns.js b/ui/app/components/dropdowns/components/account-dropdowns.js index 3e31412c6..d53d2a81b 100644 --- a/ui/app/components/dropdowns/components/account-dropdowns.js +++ b/ui/app/components/dropdowns/components/account-dropdowns.js @@ -203,15 +203,12 @@ class AccountDropdowns extends Component { }, [ h( - 'div', + 'i.fa.fa-plus.fa-lg', { style: { marginLeft: '8px', - fontFamily: 'Montserrat UltraLight', - fontSize: '30px', }, - }, - '+' + } ), h('span', { style: { @@ -235,13 +232,12 @@ class AccountDropdowns extends Component { }, [ h( - 'div', + 'i.fa.fa-download.fa-lg', { style: { - marginLeft: '10px', + marginLeft: '8px', }, - }, - String.fromCharCode(10515) + } ), h('span', { style: { -- cgit v1.2.3 From f69cf1670ff49637281388a6b4a71b377dfb087f Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Thu, 28 Sep 2017 17:02:58 +0000 Subject: chore(package): update coveralls to version 3.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b9a38ac71..5a96a819c 100644 --- a/package.json +++ b/package.json @@ -158,7 +158,7 @@ "brfs": "^1.4.3", "browserify": "^14.4.0", "chai": "^4.1.0", - "coveralls": "^2.13.1", + "coveralls": "^3.0.0", "deep-freeze-strict": "^1.1.1", "del": "^3.0.0", "envify": "^4.0.0", -- cgit v1.2.3 From c74c1fe87f05c85abd44e0519c76cbcfcd50bee8 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Thu, 28 Sep 2017 15:07:31 -0700 Subject: Update yarn.lock --- yarn.lock | 365 ++++++++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 249 insertions(+), 116 deletions(-) diff --git a/yarn.lock b/yarn.lock index c93751afc..4be3a3684 100644 --- a/yarn.lock +++ b/yarn.lock @@ -76,7 +76,7 @@ accepts@1.3.3: mime-types "~2.1.11" negotiator "0.6.1" -accepts@~1.3.3: +accepts@~1.3.3, accepts@~1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" dependencies: @@ -397,6 +397,12 @@ async-eventemitter@^0.2.2: dependencies: async "^2.4.0" +"async-eventemitter@github:ahultgren/async-eventemitter#fa06e39e56786ba541c180061dbf2c0a5bbf951c": + version "0.2.3" + resolved "https://codeload.github.com/ahultgren/async-eventemitter/tar.gz/fa06e39e56786ba541c180061dbf2c0a5bbf951c" + dependencies: + async "^2.4.0" + async-foreach@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" @@ -1345,7 +1351,7 @@ bindings@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.0.tgz#b346f6ecf6a95f5a815c5839fc7cdb22502f1ed7" -bip39@^2.2.0: +bip39@^2.2.0, bip39@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/bip39/-/bip39-2.4.0.tgz#a0b8adbf163f53495f00f05d9ede7c25369ccf13" dependencies: @@ -1397,10 +1403,25 @@ bn.js@4.11.6: version "4.11.6" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215" -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.3, bn.js@^4.11.7, bn.js@^4.4.0, bn.js@^4.8.0: +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.10.0, bn.js@^4.11.3, bn.js@^4.11.7, bn.js@^4.4.0, bn.js@^4.8.0: version "4.11.8" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" +body-parser@1.18.2: + version "1.18.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454" + dependencies: + bytes "3.0.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.1" + http-errors "~1.6.2" + iconv-lite "0.4.19" + on-finished "~2.3.0" + qs "6.5.1" + raw-body "2.3.2" + type-is "~1.6.15" + body-parser@^1.16.1: version "1.18.1" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.1.tgz#9c1629370bcfd42917f30641a2dcbe2ec50d4c26" @@ -1701,21 +1722,6 @@ buffer@^5.0.2: base64-js "^1.0.2" ieee754 "^1.1.4" -build@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/build/-/build-0.1.4.tgz#707fe026ffceddcacbfdcdf356eafda64f151046" - dependencies: - cssmin "0.3.x" - jsmin "1.x" - jxLoader "*" - moo-server "*" - promised-io "*" - timespan "2.x" - uglify-js "1.x" - walker "1.x" - winston "*" - wrench "1.3.x" - builtin-modules@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -2431,10 +2437,6 @@ cssauron@^1.1.0: dependencies: through X.X.X -cssmin@0.3.x: - version "0.3.2" - resolved "https://registry.yarnpkg.com/cssmin/-/cssmin-0.3.2.tgz#ddce4c547b510ae0d594a8f1fbf8aaf8e2c5c00d" - cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": version "0.3.2" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.2.tgz#b8036170c79f07a90ff2f16e22284027a243848b" @@ -2527,6 +2529,12 @@ debug@2.6.8, debug@^2.1.0, debug@^2.2.0, debug@^2.6.0, debug@^2.6.3, debug@^2.6. dependencies: ms "2.0.0" +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + debug@^3.0.0, debug@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/debug/-/debug-3.0.1.tgz#0564c612b521dc92d9f2988f0549e34f9c98db64" @@ -3341,6 +3349,10 @@ etag@~1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051" +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + eth-bin-to-ops@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/eth-bin-to-ops/-/eth-bin-to-ops-1.0.1.tgz#4d2703b9878825bc38c6259910e90b4db005c7de" @@ -3359,7 +3371,7 @@ eth-block-tracker@^1.0.7: pify "^2.3.0" tape "^4.6.3" -eth-block-tracker@^2.0.1, eth-block-tracker@^2.1.2: +eth-block-tracker@^2.1.2: version "2.1.3" resolved "https://registry.yarnpkg.com/eth-block-tracker/-/eth-block-tracker-2.1.3.tgz#ef24ab415f18445bd5c0ef49b9ac248f847e34f9" dependencies: @@ -3370,6 +3382,17 @@ eth-block-tracker@^2.0.1, eth-block-tracker@^2.1.2: pify "^2.3.0" tape "^4.6.3" +eth-block-tracker@^2.2.0, eth-block-tracker@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/eth-block-tracker/-/eth-block-tracker-2.2.2.tgz#b3d72cd82ba5ee37471d22bac4f56387ee4137cf" + dependencies: + async-eventemitter ahultgren/async-eventemitter#fa06e39e56786ba541c180061dbf2c0a5bbf951c + babelify "^7.3.0" + eth-query "^2.1.0" + ethjs-util "^0.1.3" + pify "^2.3.0" + tape "^4.6.3" + eth-contract-metadata@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/eth-contract-metadata/-/eth-contract-metadata-1.1.5.tgz#301f51b0460b8dd044997dc05870751fb7f4cfcb" @@ -3381,7 +3404,7 @@ eth-ens-namehash@^1.0.2: idna-uts46 "^1.0.1" js-sha3 "^0.5.7" -eth-hd-keyring@^1.1.1: +eth-hd-keyring@^1.1.1, eth-hd-keyring@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/eth-hd-keyring/-/eth-hd-keyring-1.2.0.tgz#40bcc7ea877ef5c746f54c0c87a6b39ceb5edde3" dependencies: @@ -3391,9 +3414,9 @@ eth-hd-keyring@^1.1.1: ethereumjs-wallet "^0.6.0" events "^1.1.1" -eth-json-rpc-filters@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/eth-json-rpc-filters/-/eth-json-rpc-filters-1.1.0.tgz#fb2cb1d45c825f40d92c29ece5ff91a296c8f9a1" +eth-json-rpc-filters@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/eth-json-rpc-filters/-/eth-json-rpc-filters-1.2.1.tgz#96e1714272a0f7d6d8efef7af8d764988f73ffc1" dependencies: await-semaphore "^0.1.1" eth-json-rpc-middleware "^1.0.0" @@ -3414,6 +3437,21 @@ eth-json-rpc-middleware@^1.0.0, eth-json-rpc-middleware@^1.2.7: promise-to-callback "^1.0.0" tape "^4.6.3" +eth-keyring-controller@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eth-keyring-controller/-/eth-keyring-controller-2.1.0.tgz#46b2c1597d9471aab5e4f792dc109084ed196f2d" + dependencies: + bip39 "^2.4.0" + bluebird "^3.5.0" + browser-passworder "^2.0.3" + eth-hd-keyring "^1.2.0" + eth-sig-util "^1.2.2" + eth-simple-keyring "^1.1.1" + ethereumjs-util "^5.1.2" + loglevel "^1.5.0" + obs-store "^2.4.1" + promise-filter "^1.1.0" + eth-phishing-detect@^1.1.4: version "1.1.11" resolved "https://registry.yarnpkg.com/eth-phishing-detect/-/eth-phishing-detect-1.1.11.tgz#e29c38b84abed3d41df4131c56d6a41308c3e56d" @@ -3427,12 +3465,19 @@ eth-query@^2.1.0, eth-query@^2.1.2: json-rpc-random-id "^1.0.0" xtend "^4.0.1" -eth-sig-util@^1.1.0, eth-sig-util@^1.2.1, eth-sig-util@^1.2.2: +eth-sig-util@^1.1.0, eth-sig-util@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/eth-sig-util/-/eth-sig-util-1.2.2.tgz#7e982f5f8d94e79027d8c69e6006cdbd2f57942f" dependencies: ethereumjs-util "^5.1.1" +eth-sig-util@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eth-sig-util/-/eth-sig-util-1.3.0.tgz#14c1c02367a4264dbfeae611b4dc7f8d9d6ee4ba" + dependencies: + ethereumjs-abi "git+https://github.com/ethereumjs/ethereumjs-abi.git" + ethereumjs-util "^5.1.1" + eth-simple-keyring@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/eth-simple-keyring/-/eth-simple-keyring-1.1.1.tgz#6dd75d7cc6edea7c788cf19ef9431c830cd961ae" @@ -3442,9 +3487,9 @@ eth-simple-keyring@^1.1.1: ethereumjs-wallet "^0.6.0" events "^1.1.1" -eth-token-tracker@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/eth-token-tracker/-/eth-token-tracker-1.1.3.tgz#c9951a214a9a30bdfc251133d1ba920743a59848" +eth-token-tracker@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/eth-token-tracker/-/eth-token-tracker-1.1.4.tgz#29ff2457d66bfa3b8ee490e83ff40fd0cf2cec41" dependencies: deep-equal "^1.0.1" eth-block-tracker "^1.0.7" @@ -3465,6 +3510,13 @@ ethereum-ens-network-map@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/ethereum-ens-network-map/-/ethereum-ens-network-map-1.0.0.tgz#43cd7669ce950a789e151001118d4d65f210eeb7" +"ethereumjs-abi@git+https://github.com/ethereumjs/ethereumjs-abi.git": + version "0.6.4" + resolved "git+https://github.com/ethereumjs/ethereumjs-abi.git#ee6ded67235a98f3ef4ae2a338aee70a9f68fe20" + dependencies: + bn.js "^4.10.0" + ethereumjs-util "^4.3.0" + ethereumjs-account@^2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/ethereumjs-account/-/ethereumjs-account-2.0.4.tgz#f8c30231bcb707f4514d8a052c1f9da103624d47" @@ -3489,7 +3541,7 @@ ethereumjs-tx@^1.2.0, ethereumjs-tx@^1.2.2, ethereumjs-tx@^1.3.0, ethereumjs-tx@ ethereum-common "^0.0.18" ethereumjs-util "^5.0.0" -ethereumjs-util@4.5.0, ethereumjs-util@^4.0.0, ethereumjs-util@^4.0.1, ethereumjs-util@^4.4.0: +ethereumjs-util@4.5.0, ethereumjs-util@^4.0.0, ethereumjs-util@^4.0.1, ethereumjs-util@^4.3.0, ethereumjs-util@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-4.5.0.tgz#3e9428b317eebda3d7260d854fddda954b1f1bc6" dependencies: @@ -3499,7 +3551,7 @@ ethereumjs-util@4.5.0, ethereumjs-util@^4.0.0, ethereumjs-util@^4.0.1, ethereumj rlp "^2.0.0" secp256k1 "^3.0.1" -ethereumjs-util@^5.0.0, ethereumjs-util@^5.1.1: +ethereumjs-util@^5.0.0, ethereumjs-util@^5.1.1, ethereumjs-util@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-5.1.2.tgz#25ba0215cbb4c2f0b108a6f96af2a2e62e45921f" dependencies: @@ -3767,7 +3819,7 @@ expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" -express@^4.10.7, express@^4.14.0: +express@^4.10.7: version "4.15.4" resolved "https://registry.yarnpkg.com/express/-/express-4.15.4.tgz#032e2253489cf8fce02666beca3d11ed7a2daed1" dependencies: @@ -3800,6 +3852,41 @@ express@^4.10.7, express@^4.14.0: utils-merge "1.0.0" vary "~1.1.1" +express@^4.15.5: + version "4.16.0" + resolved "https://registry.yarnpkg.com/express/-/express-4.16.0.tgz#b519638e4eb58e7178c81b498ef22f798cb2e255" + dependencies: + accepts "~1.3.4" + array-flatten "1.1.1" + body-parser "1.18.2" + content-disposition "0.5.2" + content-type "~1.0.4" + cookie "0.3.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.1" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.1.0" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.2" + path-to-regexp "0.1.7" + proxy-addr "~2.0.2" + qs "6.5.1" + range-parser "~1.2.0" + safe-buffer "5.1.1" + send "0.16.0" + serve-static "1.13.0" + setprototypeof "1.1.0" + statuses "~1.3.1" + type-is "~1.6.15" + utils-merge "1.0.1" + vary "~1.1.2" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -3953,6 +4040,18 @@ finalhandler@1.0.4, finalhandler@~1.0.4: statuses "~1.3.1" unpipe "~1.0.0" +finalhandler@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5" + dependencies: + debug "2.6.9" + encodeurl "~1.0.1" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.2" + statuses "~1.3.1" + unpipe "~1.0.0" + find-cache-dir@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-0.1.1.tgz#c8defae57c8a52a8a784f9e31c57c742e993a0b9" @@ -4116,10 +4215,18 @@ forwarded@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.1.tgz#8a4e30c640b05395399a3549c730257728048961" +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + fresh@0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + from2@^2.1.0: version "2.3.0" resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" @@ -5070,6 +5177,10 @@ ipaddr.js@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.4.0.tgz#296aca878a821816e5b85d0a285a99bcff4582f0" +ipaddr.js@1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.5.2.tgz#d4b505bde9946987ccf0fc58d9010ff9607e3fa0" + irregular-plurals@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-1.3.0.tgz#7af06931bdf74be33dcf585a13e06fccc16caecf" @@ -5450,10 +5561,6 @@ js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" -js-yaml@0.3.x: - version "0.3.7" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-0.3.7.tgz#d739d8ee86461e54b354d6a7d7d1f2ad9a167f62" - js-yaml@3.6.1: version "3.6.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.6.1.tgz#6e5fe67d8b205ce4d22fad05b7781e8dadcc4b30" @@ -5525,10 +5632,6 @@ jshint-stylish@~2.2.1: string-length "^1.0.0" text-table "^0.2.0" -jsmin@1.x: - version "1.0.1" - resolved "https://registry.yarnpkg.com/jsmin/-/jsmin-1.0.1.tgz#e7bd0dcd6496c3bf4863235bf461a3d98aa3b98c" - json-loader@^0.5.4: version "0.5.7" resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d" @@ -5541,6 +5644,15 @@ json-rpc-engine@^3.0.1, json-rpc-engine@^3.1.0: babel-preset-env "^1.3.2" babelify "^7.3.0" +json-rpc-engine@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/json-rpc-engine/-/json-rpc-engine-3.2.0.tgz#d34dff106c8339c337a894da801f73b1f77b1bc8" + dependencies: + async "^2.0.1" + babel-preset-env "^1.3.2" + babelify "^7.3.0" + json-rpc-error "^2.0.0" + json-rpc-error@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/json-rpc-error/-/json-rpc-error-2.0.0.tgz#a7af9c202838b5e905c7250e547f1aff77258a02" @@ -5556,6 +5668,16 @@ json-rpc-middleware-stream@^1.0.0: json-rpc-engine "^3.0.1" readable-stream "^2.3.3" +json-rpc-middleware-stream@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-rpc-middleware-stream/-/json-rpc-middleware-stream-1.0.1.tgz#c9b8a005c80af32e6df8bb88e6bdd1300484a4ed" + dependencies: + end-of-stream "^1.4.0" + eth-block-tracker "^2.1.2" + ethjs-query "^0.2.9" + json-rpc-engine "^3.0.1" + readable-stream "^2.3.3" + json-rpc-random-id@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/json-rpc-random-id/-/json-rpc-random-id-1.0.1.tgz#ba49d96aded1444dbb8da3d203748acbbcdec8c8" @@ -5644,15 +5766,6 @@ just-extend@^1.1.22: version "1.1.22" resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.22.tgz#3330af756cab6a542700c64b2e4e4aa062d52fff" -jxLoader@*: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jxLoader/-/jxLoader-0.1.1.tgz#0134ea5144e533b594fc1ff25ff194e235c53ecd" - dependencies: - js-yaml "0.3.x" - moo-server "1.3.x" - promised-io "*" - walker "1.x" - karma-chrome-launcher@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz#cf1b9d07136cc18fe239327d24654c3dbc368acf" @@ -6194,7 +6307,7 @@ log4js@^0.6.31: readable-stream "~1.0.2" semver "~4.3.3" -loglevel@^1.4.1: +loglevel@^1.4.1, loglevel@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.5.0.tgz#3863984a2c326b986fbb965f378758a6dc8a4324" @@ -6256,12 +6369,6 @@ make-iterator@^1.0.0: dependencies: kind-of "^3.1.0" -makeerror@1.0.x: - version "1.0.11" - resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" - dependencies: - tmpl "1.0.x" - map-async@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/map-async/-/map-async-0.1.1.tgz#c897c0449f85864c74b5a3f196edb42156431745" @@ -6404,6 +6511,18 @@ mersenne-twister@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/mersenne-twister/-/mersenne-twister-1.1.0.tgz#f916618ee43d7179efcf641bec4531eb9670978a" +metamascara@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/metamascara/-/metamascara-1.3.1.tgz#a84d6f20ef4ba401ce44eba120857ee1d680747b" + dependencies: + iframe "^1.0.0" + iframe-stream "^3.0.0" + json-rpc-engine "^3.1.0" + json-rpc-middleware-stream "^1.0.0" + obj-multiplex "^1.0.0" + obs-store "^2.4.1" + pump "^1.0.2" + metamask-logo@^2.1.2: version "2.1.3" resolved "https://registry.yarnpkg.com/metamask-logo/-/metamask-logo-2.1.3.tgz#175ce57ae50c7344b3b1dc32d2fd0b08e3978fd0" @@ -6454,6 +6573,10 @@ mime@1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" +mime@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" + mime@^1.3.4: version "1.4.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.0.tgz#69e9e0db51d44f2a3b56e48b7817d7d137f1a343" @@ -6596,10 +6719,6 @@ module-deps@^4.0.8: through2 "^2.0.0" xtend "^4.0.0" -moo-server@*, moo-server@1.3.x: - version "1.3.0" - resolved "https://registry.yarnpkg.com/moo-server/-/moo-server-1.3.0.tgz#5dc79569565a10d6efed5439491e69d2392e58f1" - ms@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" @@ -6681,7 +6800,7 @@ next-tick@1: version "1.0.0" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" -nise@^1.0.1: +nise@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/nise/-/nise-1.1.0.tgz#37e41b9bf0041ccb83d1bf03e79440bbc0db10ad" dependencies: @@ -7254,7 +7373,7 @@ parseuri@0.0.5: dependencies: better-assert "~1.0.0" -parseurl@~1.3.0, parseurl@~1.3.1: +parseurl@~1.3.0, parseurl@~1.3.1, parseurl@~1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" @@ -7624,10 +7743,6 @@ promise@^8.0.1: dependencies: asap "~2.0.3" -promised-io@*: - version "0.3.5" - resolved "https://registry.yarnpkg.com/promised-io/-/promised-io-0.3.5.tgz#4ad217bb3658bcaae9946b17a8668ecd851e1356" - prompt@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/prompt/-/prompt-1.0.0.tgz#8e57123c396ab988897fb327fd3aedc3e735e4fe" @@ -7661,6 +7776,13 @@ proxy-addr@~1.1.5: forwarded "~0.1.0" ipaddr.js "1.4.0" +proxy-addr@~2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec" + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.5.2" + prr@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" @@ -8217,14 +8339,14 @@ request-promise-native@^1.0.3: stealthy-require "^1.1.0" tough-cookie ">=2.3.0" -request-promise@^4.1.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-4.2.1.tgz#7eec56c89317a822cbfea99b039ce543c2e15f67" +request-promise@^4.2.1: + version "4.2.2" + resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-4.2.2.tgz#d1ea46d654a6ee4f8ee6a4fea1018c22911904b4" dependencies: bluebird "^3.5.0" request-promise-core "1.1.1" stealthy-require "^1.1.0" - tough-cookie ">=2.3.0" + tough-cookie ">=2.3.3" request@2, request@^2.67.0, request@^2.79.0, request@^2.81.0: version "2.81.0" @@ -8398,7 +8520,7 @@ rx-lite@*, rx-lite@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" @@ -8519,6 +8641,24 @@ send@0.15.4: range-parser "~1.2.0" statuses "~1.3.1" +send@0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.16.0.tgz#16338dbb9a2ede4ad57b48420ec3b82d8e80a57b" + dependencies: + debug "2.6.9" + depd "~1.1.1" + destroy "~1.0.4" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.6.2" + mime "1.4.1" + ms "2.0.0" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.3.1" + serve-static@1.12.4: version "1.12.4" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.4.tgz#9b6aa98eeb7253c4eedc4c1f6fdbca609901a961" @@ -8528,6 +8668,15 @@ serve-static@1.12.4: parseurl "~1.3.1" send "0.15.4" +serve-static@1.13.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.0.tgz#810c91db800e94ba287eae6b4e06caab9fdc16f1" + dependencies: + encodeurl "~1.0.1" + escape-html "~1.0.3" + parseurl "~1.3.2" + send "0.16.0" + set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -8544,6 +8693,10 @@ setprototypeof@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + sha.js@^2.4.0, sha.js@^2.4.8, sha.js@~2.4.4: version "2.4.8" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.8.tgz#37068c2c476b6baf402d14a49c67f597921f634f" @@ -8606,17 +8759,16 @@ simple-get@^1.4.2: unzip-response "^1.0.0" xtend "^4.0.0" -sinon@^3.2.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-3.3.0.tgz#9132111b4bbe13c749c2848210864250165069b1" +sinon@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-4.0.0.tgz#a54a5f0237aa1dd2215e5e81c89b42b50c4fdb6b" dependencies: - build "^0.1.4" diff "^3.1.0" formatio "1.2.0" lodash.get "^4.4.2" lolex "^2.1.2" native-promise-only "^0.8.1" - nise "^1.0.1" + nise "^1.1.0" path-to-regexp "^1.7.0" samsam "^1.1.3" text-encoding "0.6.4" @@ -9542,10 +9694,6 @@ timers-ext@^0.1.2: es5-ext "~0.10.14" next-tick "1" -timespan@2.x: - version "2.3.0" - resolved "https://registry.yarnpkg.com/timespan/-/timespan-2.3.0.tgz#4902ce040bd13d845c8f59b27e9d59bad6f39929" - tmp@0.0.31, tmp@^0.0.31: version "0.0.31" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" @@ -9558,10 +9706,6 @@ tmp@0.0.x: dependencies: os-tmpdir "~1.0.2" -tmpl@1.0.x: - version "1.0.4" - resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" - to-absolute-glob@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-0.1.1.tgz#1cdfa472a9ef50c239ee66999b662ca0eb39937f" @@ -9598,6 +9742,12 @@ tough-cookie@>=2.3.0, tough-cookie@^2.3.2, tough-cookie@~2.3.0: dependencies: punycode "^1.4.1" +tough-cookie@>=2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" + dependencies: + punycode "^1.4.1" + tr46@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" @@ -9697,10 +9847,6 @@ uglify-es@^3.0.15: commander "~2.11.0" source-map "~0.5.1" -uglify-js@1.x: - version "1.3.5" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-1.3.5.tgz#4b5bfff9186effbaa888e4c9e94bd9fc4c94929d" - uglify-js@^2.6, uglify-js@^2.8.27: version "2.8.29" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" @@ -9836,6 +9982,10 @@ utils-merge@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + uuid@^2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" @@ -9873,6 +10023,10 @@ vary@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" @@ -9971,12 +10125,6 @@ vreme@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/vreme/-/vreme-3.0.2.tgz#4721376b449457fefde8a849d3340933b90b5686" -walker@1.x: - version "1.0.7" - resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" - dependencies: - makeerror "1.0.x" - warning@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c" @@ -10010,14 +10158,14 @@ weak@^1.0.0: bindings "^1.2.1" nan "^2.0.5" -web3-provider-engine@^13.2.9: - version "13.2.9" - resolved "https://registry.yarnpkg.com/web3-provider-engine/-/web3-provider-engine-13.2.9.tgz#1db7d6e1df4665c3e63f972c4d67087427200ed4" +web3-provider-engine@^13.2.12: + version "13.3.0" + resolved "https://registry.yarnpkg.com/web3-provider-engine/-/web3-provider-engine-13.3.0.tgz#77c07b9ca2c529c48ad8fdfbb9d8ef9c3637d37e" dependencies: async "^2.5.0" clone "^2.0.0" - eth-block-tracker "^2.0.1" - eth-sig-util "^1.2.1" + eth-block-tracker "^2.2.2" + eth-sig-util "^1.3.0" ethereumjs-block "^1.2.2" ethereumjs-tx "^1.2.0" ethereumjs-util "^5.1.1" @@ -10162,17 +10310,6 @@ window-size@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075" -winston@*: - version "2.3.1" - resolved "https://registry.yarnpkg.com/winston/-/winston-2.3.1.tgz#0b48420d978c01804cf0230b648861598225a119" - dependencies: - async "~1.0.0" - colors "1.0.x" - cycle "1.0.x" - eyes "0.1.x" - isstream "0.1.x" - stack-trace "0.0.x" - winston@2.1.x: version "2.1.1" resolved "https://registry.yarnpkg.com/winston/-/winston-2.1.1.tgz#3c9349d196207fd1bdff9d4bc43ef72510e3a12e" @@ -10215,10 +10352,6 @@ wreck@^6.3.0: boom "2.x.x" hoek "2.x.x" -wrench@1.3.x: - version "1.3.9" - resolved "https://registry.yarnpkg.com/wrench/-/wrench-1.3.9.tgz#6f13ec35145317eb292ca5f6531391b244111411" - write-file-atomic@^1.1.4: version "1.3.4" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.4.tgz#f807a4f0b1d9e913ae7a48112e6cc3af1991b45f" -- cgit v1.2.3 From a4838b1c575f08f9a83457222737075bab374936 Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 27 Sep 2017 22:11:46 -0230 Subject: Close mobile sidebar when selecting 'Add token' from account options dropdown. --- ui/app/components/dropdowns/components/account-dropdowns.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/app/components/dropdowns/components/account-dropdowns.js b/ui/app/components/dropdowns/components/account-dropdowns.js index d53d2a81b..fc60c6005 100644 --- a/ui/app/components/dropdowns/components/account-dropdowns.js +++ b/ui/app/components/dropdowns/components/account-dropdowns.js @@ -349,6 +349,7 @@ class AccountDropdowns extends Component { { closeMenu: () => {}, onClick: () => { + actions.hideSidebar() actions.showAddTokenPage() }, style: Object.assign( @@ -425,6 +426,7 @@ AccountDropdowns.propTypes = { const mapDispatchToProps = (dispatch) => { return { actions: { + hideSidebar: () => dispatch(actions.hideSidebar()), showConfigPage: () => dispatch(actions.showConfigPage()), showAccountDetail: (address) => dispatch(actions.showAccountDetail(address)), showAccountDetailModal: () => { -- cgit v1.2.3 From b55a40c7f144645a29569294996893cb1b519779 Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 28 Sep 2017 09:25:04 -0230 Subject: Close sidebar on token selection. --- ui/app/components/token-cell.js | 11 +++++++++-- ui/app/components/wallet-view.js | 13 +++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js index dc1c7f46f..e87d2c859 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/token-cell.js @@ -15,6 +15,7 @@ function mapStateToProps (state) { userAddress: selectors.getSelectedAddress(state), tokenExchangeRates: state.metamask.tokenExchangeRates, ethToUSDRate: state.metamask.conversionRate, + sidebarOpen: state.appState.sidebarOpen, } } @@ -22,6 +23,7 @@ function mapDispatchToProps (dispatch) { return { setSelectedToken: address => dispatch(actions.setSelectedToken(address)), updateTokenExchangeRate: token => dispatch(actions.updateTokenExchangeRate(token)), + hideSidebar: () => dispatch(actions.hideSidebar()), } } @@ -52,6 +54,8 @@ TokenCell.prototype.render = function () { selectedTokenAddress, tokenExchangeRates, ethToUSDRate, + hideSidebar, + sidebarOpen, // userAddress, } = props @@ -73,13 +77,16 @@ TokenCell.prototype.render = function () { }) formattedUSD = `$${currentTokenInUSD} USD`; } - + return ( h('div.token-list-item', { className: `token-list-item ${selectedTokenAddress === address ? 'token-list-item--active' : ''}`, // style: { cursor: network === '1' ? 'pointer' : 'default' }, // onClick: this.view.bind(this, address, userAddress, network), - onClick: () => setSelectedToken(address), + onClick: () => { + setSelectedToken(address) + selectedTokenAddress !== address && sidebarOpen && hideSidebar() + }, }, [ h(Identicon, { diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index b306fb7d4..00c86298d 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -40,7 +40,13 @@ function WalletView () { } WalletView.prototype.renderWalletBalance = function () { - const { selectedTokenAddress, selectedAccount, unsetSelectedToken } = this.props + const { + selectedTokenAddress, + selectedAccount, + unsetSelectedToken, + hideSidebar, + sidebarOpen + } = this.props const selectedClass = selectedTokenAddress ? '' : 'wallet-balance-wrapper--active' @@ -49,7 +55,10 @@ WalletView.prototype.renderWalletBalance = function () { return h('div', { className }, [ h('div.wallet-balance', { - onClick: unsetSelectedToken, + onClick: () => { + unsetSelectedToken() + selectedTokenAddress && sidebarOpen && hideSidebar() + }, }, [ h(BalanceComponent, { -- cgit v1.2.3 From a195427e7208096f6f873175f2cbdbbb0a802191 Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 28 Sep 2017 11:40:33 -0230 Subject: Fix send of USD and backspacing amount to 0 --- ui/app/conversion-util.js | 4 ++-- ui/app/send.js | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js index 7e02fe2bd..37877d12c 100644 --- a/ui/app/conversion-util.js +++ b/ui/app/conversion-util.js @@ -47,7 +47,7 @@ const toNormalizedDenomination = { WEI: bigNumber => bigNumber.div(BIG_NUMBER_WEI_MULTIPLIER) } const toSpecifiedDenomination = { - WEI: bigNumber => bigNumber.times(BIG_NUMBER_WEI_MULTIPLIER) + WEI: bigNumber => bigNumber.times(BIG_NUMBER_WEI_MULTIPLIER).round() } const baseChange = { hex: n => n.toString(16), @@ -83,8 +83,8 @@ const whenPropApplySetterMap = (prop, setterMap) => whenPredSetWithPropAndSetter const converter = R.pipe( whenPropApplySetterMap('fromNumericBase', toBigNumber), whenPropApplySetterMap('fromDenomination', toNormalizedDenomination), - whenPropApplySetterMap('toDenomination', toSpecifiedDenomination), whenPredSetWithPropAndSetter(fromAndToCurrencyPropsNotEqual, 'conversionRate', convert), + whenPropApplySetterMap('toDenomination', toSpecifiedDenomination), whenPredSetWithPropAndSetter(R.prop('ethToUSDRate'), 'ethToUSDRate', convert), whenPredSetWithPropAndSetter(R.prop('numberOfDecimals'), 'numberOfDecimals', round), whenPropApplySetterMap('toNumericBase', baseChange), diff --git a/ui/app/send.js b/ui/app/send.js index 4d2a5f48d..d92a6f2d5 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -230,12 +230,16 @@ SendTransactionScreen.prototype.renderAmountInput = function (activeCurrency) { placeholder: `0 ${activeCurrency}`, type: 'number', onChange: (event) => { + const amountToSend = event.target.value + ? this.getAmountToSend(event.target.value) + : '0x0' + this.setState({ newTx: Object.assign( this.state.newTx, { amount: event.target.value, - amountToSend: this.getAmountToSend(event.target.value), + amountToSend: amountToSend, } ), }) -- cgit v1.2.3 From 66ed4dfaa31be826f0d5a4a34410e34eca34d007 Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 28 Sep 2017 12:18:25 -0230 Subject: Ensure sent token value is recognized as hex. --- ui/app/components/send-token/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/send-token/index.js b/ui/app/components/send-token/index.js index 8a827e951..6e4c909be 100644 --- a/ui/app/components/send-token/index.js +++ b/ui/app/components/send-token/index.js @@ -165,7 +165,7 @@ SendTokenScreen.prototype.clearErrorsFor = function (field) { SendTokenScreen.prototype.getAmountToSend = function (amount, selectedToken) { const { decimals } = selectedToken || {} const multiplier = Math.pow(10, Number(decimals || 0)) - const sendAmount = Number(amount * multiplier).toString(16) + const sendAmount = '0x' + Number(amount * multiplier).toString(16) return sendAmount } -- cgit v1.2.3 From 06292107d756f0b25805f819cd276e4b6303ccb0 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Thu, 28 Sep 2017 16:13:53 -0700 Subject: Always set currency to USD on app mount --- ui/app/app.js | 5 +++++ ui/app/components/pending-tx/confirm-deploy-contract.js | 1 - ui/app/components/pending-tx/confirm-send-ether.js | 1 - ui/app/components/pending-tx/index.js | 2 +- ui/app/components/token-balance.js | 3 ++- ui/app/css/itcss/components/newui-sections.scss | 8 ++++++++ ui/app/css/itcss/components/send.scss | 4 ++++ ui/app/css/itcss/components/token-list.scss | 1 + 8 files changed, 21 insertions(+), 4 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index 14e6a26e2..7468551eb 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -86,9 +86,14 @@ function mapDispatchToProps (dispatch, ownProps) { hideSidebar: () => dispatch(actions.hideSidebar()), showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()), hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()), + setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')), } } +App.prototype.componentWillMount = function () { + this.props.setCurrentCurrencyToUSD() +} + App.prototype.render = function () { var props = this.props const { isLoading, loadingMessage, network } = props diff --git a/ui/app/components/pending-tx/confirm-deploy-contract.js b/ui/app/components/pending-tx/confirm-deploy-contract.js index 89a0389d7..386e14afe 100644 --- a/ui/app/components/pending-tx/confirm-deploy-contract.js +++ b/ui/app/components/pending-tx/confirm-deploy-contract.js @@ -33,7 +33,6 @@ function mapStateToProps (state) { function mapDispatchToProps (dispatch) { return { - setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('USD')), backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)), cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })), } diff --git a/ui/app/components/pending-tx/confirm-send-ether.js b/ui/app/components/pending-tx/confirm-send-ether.js index b03ec0552..330a55cce 100644 --- a/ui/app/components/pending-tx/confirm-send-ether.js +++ b/ui/app/components/pending-tx/confirm-send-ether.js @@ -32,7 +32,6 @@ function mapStateToProps (state) { function mapDispatchToProps (dispatch) { return { - setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('USD')), backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)), cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })), } diff --git a/ui/app/components/pending-tx/index.js b/ui/app/components/pending-tx/index.js index b7dd50ff1..770fb1dfd 100644 --- a/ui/app/components/pending-tx/index.js +++ b/ui/app/components/pending-tx/index.js @@ -36,7 +36,7 @@ function mapStateToProps (state) { function mapDispatchToProps (dispatch) { return { - setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('USD')), + setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')), backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)), cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })), } diff --git a/ui/app/components/token-balance.js b/ui/app/components/token-balance.js index 0342c1da9..2f71c0687 100644 --- a/ui/app/components/token-balance.js +++ b/ui/app/components/token-balance.js @@ -27,7 +27,8 @@ function TokenBalance () { TokenBalance.prototype.render = function () { const state = this.state - const { symbol, string, balanceOnly, isLoading } = state + const { symbol, string, isLoading } = state + const { balanceOnly } = this.props return isLoading ? h('span', '') diff --git a/ui/app/css/itcss/components/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss index ae6ee6311..5ce4f281c 100644 --- a/ui/app/css/itcss/components/newui-sections.scss +++ b/ui/app/css/itcss/components/newui-sections.scss @@ -177,3 +177,11 @@ $wallet-view-bg: $wild-sand; justify-content: flex-start; margin: 5% 7% 0%; } + +.fiat-amount { + text-transform: uppercase; +} + +.token-balance__amount { + padding-right: 6px; +} diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index 03e0fac1d..dee8157ef 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -370,6 +370,10 @@ font-size: 40px; line-height: 40px; margin-top: 13px; + + .token-balance__amount { + padding-right: 12px; + } } &__button-group { diff --git a/ui/app/css/itcss/components/token-list.scss b/ui/app/css/itcss/components/token-list.scss index 9a772f666..e4d6d975b 100644 --- a/ui/app/css/itcss/components/token-list.scss +++ b/ui/app/css/itcss/components/token-list.scss @@ -21,6 +21,7 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and ( &__fiat-amount { margin-top: .25%; font-size: 105%; + text-transform: uppercase; @media #{$wallet-balance-breakpoint-range} { font-size: 95%; -- cgit v1.2.3 From 4f106854ba6bbfd22b49598f9ef019aa620f5b4f Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Thu, 28 Sep 2017 16:34:42 -0700 Subject: Hide ShapeShift and Fix Modal Stylings --- ui/app/components/modals/buy-options-modal.js | 10 ++--- ui/app/components/modals/modal.js | 55 +++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/ui/app/components/modals/buy-options-modal.js b/ui/app/components/modals/buy-options-modal.js index a8033d288..f1a5aa9fd 100644 --- a/ui/app/components/modals/buy-options-modal.js +++ b/ui/app/components/modals/buy-options-modal.js @@ -57,13 +57,13 @@ BuyOptions.prototype.render = function () { h('div.buy-modal-content-option-subtitle', {}, 'Buy with Fiat'), ]), - h('div.buy-modal-content-option', {}, [ - h('div.buy-modal-content-option-title', {}, 'Shapeshift'), - h('div.buy-modal-content-option-subtitle', {}, 'Trade any digital asset for any other'), - ]), + // h('div.buy-modal-content-option', {}, [ + // h('div.buy-modal-content-option-title', {}, 'Shapeshift'), + // h('div.buy-modal-content-option-subtitle', {}, 'Trade any digital asset for any other'), + // ]), h('div.buy-modal-content-option', { - onClick: () => this.goToAccountDetailsModal() + onClick: () => this.goToAccountDetailsModal(), }, [ h('div.buy-modal-content-option-title', {}, 'Direct Deposit'), h('div.buy-modal-content-option-subtitle', {}, 'Deposit from another account'), diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index 138efc3ea..2bd56fb0a 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -17,15 +17,25 @@ const NewAccountModal = require('./new-account-modal') const accountModalStyle = { mobileModalStyle: { width: '95%', - top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh', + // top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh', boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', borderRadius: '4px', + top: '10%', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', }, laptopModalStyle: { width: '360px', - top: 'calc(33% + 45px)', + // top: 'calc(33% + 45px)', boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', borderRadius: '4px', + top: '10%', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', }, contentStyle: { borderRadius: '4px', @@ -39,14 +49,23 @@ const MODALS = { ], mobileModalStyle: { width: '95%', - top: isPopupOrNotification() === 'popup' ? '48vh' : '36.5vh', + // top: isPopupOrNotification() === 'popup' ? '48vh' : '36.5vh', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', boxShadow: '0 0 7px 0 rgba(0,0,0,0.08)', + top: '10%', }, laptopModalStyle: { width: '66%', maxWidth: '550px', - top: 'calc(30% + 10px)', + top: 'calc(10% + 10px)', + left: '0', + right: '0', + margin: '0 auto', boxShadow: '0 0 7px 0 rgba(0,0,0,0.08)', + transform: 'none', }, }, @@ -56,13 +75,23 @@ const MODALS = { ], mobileModalStyle: { width: '95%', - top: isPopupOrNotification() === 'popup' ? '48vh' : '36.5vh', + // top: isPopupOrNotification() === 'popup' ? '48vh' : '36.5vh', + top: '10%', boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', }, laptopModalStyle: { width: '375px', - top: 'calc(30% + 10px)', + // top: 'calc(30% + 10px)', + top: '10%', boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', }, }, @@ -86,11 +115,21 @@ const MODALS = { ], mobileModalStyle: { width: '95%', - top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh', + // top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh', + top: '10%', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', }, laptopModalStyle: { width: '449px', - top: 'calc(33% + 45px)', + // top: 'calc(33% + 45px)', + top: '10%', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', }, }, -- cgit v1.2.3 From 67ee5b21e6f64ac22e65f2712ae13dd8c09ed113 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Thu, 28 Sep 2017 17:39:53 -0700 Subject: Query for gas estimates --- package.json | 5 +++-- ui/app/actions.js | 4 ++-- ui/app/components/send-token/index.js | 18 ++++++++++++++++-- ui/app/send.js | 7 ++++++- yarn.lock | 9 ++++++++- 5 files changed, 35 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 264c73e06..5763f6e32 100644 --- a/package.json +++ b/package.json @@ -74,8 +74,8 @@ "end-of-stream": "^1.1.0", "ensnare": "^1.0.0", "eth-bin-to-ops": "^1.0.1", - "eth-contract-metadata": "^1.1.5", "eth-block-tracker": "^2.2.0", + "eth-contract-metadata": "^1.1.5", "eth-hd-keyring": "^1.1.1", "eth-json-rpc-filters": "^1.2.1", "eth-keyring-controller": "^2.0.0", @@ -84,6 +84,7 @@ "eth-sig-util": "^1.2.2", "eth-simple-keyring": "^1.1.1", "eth-token-tracker": "^1.1.4", + "ethereumjs-abi": "^0.6.4", "ethereumjs-tx": "^1.3.0", "ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", @@ -216,8 +217,8 @@ "react-addons-test-utils": "^15.5.1", "react-test-renderer": "^15.5.4", "react-testutils-additions": "^15.2.0", - "stylelint-config-standard": "^17.0.0", "sinon": "^4.0.0", + "stylelint-config-standard": "^17.0.0", "tape": "^4.5.1", "testem": "^1.10.3", "uglifyify": "^4.0.2", diff --git a/ui/app/actions.js b/ui/app/actions.js index 0b860ee63..ff3240abf 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -453,10 +453,10 @@ function signTx (txData) { } } -function estimateGas () { +function estimateGas (params = {}) { return (dispatch) => { return new Promise((resolve, reject) => { - global.ethQuery.estimateGas({}, (err, data) => { + global.ethQuery.estimateGas(params, (err, data) => { if (err) { dispatch(actions.displayWarning(err.message)) return reject(err) diff --git a/ui/app/components/send-token/index.js b/ui/app/components/send-token/index.js index 6e4c909be..a95a0a6d8 100644 --- a/ui/app/components/send-token/index.js +++ b/ui/app/components/send-token/index.js @@ -2,6 +2,7 @@ const Component = require('react').Component const connect = require('react-redux').connect const h = require('react-hyperscript') const classnames = require('classnames') +const abi = require('ethereumjs-abi') const inherits = require('util').inherits const actions = require('../../actions') const selectors = require('../../selectors') @@ -57,7 +58,7 @@ function mapDispatchToProps (dispatch) { dispatch(actions.signTokenTx(tokenAddress, toAddress, amount, txData)) ), updateTokenExchangeRate: token => dispatch(actions.updateTokenExchangeRate(token)), - estimateGas: () => dispatch(actions.estimateGas()), + estimateGas: params => dispatch(actions.estimateGas(params)), getGasPrice: () => dispatch(actions.getGasPrice()), } } @@ -83,15 +84,28 @@ SendTokenScreen.prototype.componentWillMount = function () { selectedToken: { symbol }, getGasPrice, estimateGas, + selectedAddress, } = this.props updateTokenExchangeRate(symbol) + const data = Array.prototype.map.call( + abi.rawEncode(['address', 'uint256'], [selectedAddress, '0x0']), + x => ('00' + x.toString(16)).slice(-2) + ).join('') + + console.log(data) Promise.all([ getGasPrice(), - estimateGas(), + estimateGas({ + from: selectedAddress, + value: '0x0', + gas: '746a528800', + data, + }), ]) .then(([blockGasPrice, estimatedGas]) => { + console.log({ blockGasPrice, estimatedGas}) this.setState({ gasPrice: blockGasPrice, gasLimit: estimatedGas, diff --git a/ui/app/send.js b/ui/app/send.js index d92a6f2d5..2e6409f32 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -100,12 +100,17 @@ function SendTransactionScreen () { SendTransactionScreen.prototype.componentWillMount = function () { const { newTx } = this.state + const { address } = this.props Promise.all([ this.props.dispatch(getGasPrice()), - this.props.dispatch(estimateGas()), + this.props.dispatch(estimateGas({ + from: address, + gas: '746a528800', + })), ]) .then(([blockGasPrice, estimatedGas]) => { + console.log({ blockGasPrice, estimatedGas}) this.setState({ newTx: { ...newTx, diff --git a/yarn.lock b/yarn.lock index 4be3a3684..acd448e6c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -397,7 +397,7 @@ async-eventemitter@^0.2.2: dependencies: async "^2.4.0" -"async-eventemitter@github:ahultgren/async-eventemitter#fa06e39e56786ba541c180061dbf2c0a5bbf951c": +async-eventemitter@ahultgren/async-eventemitter#fa06e39e56786ba541c180061dbf2c0a5bbf951c: version "0.2.3" resolved "https://codeload.github.com/ahultgren/async-eventemitter/tar.gz/fa06e39e56786ba541c180061dbf2c0a5bbf951c" dependencies: @@ -3510,6 +3510,13 @@ ethereum-ens-network-map@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/ethereum-ens-network-map/-/ethereum-ens-network-map-1.0.0.tgz#43cd7669ce950a789e151001118d4d65f210eeb7" +ethereumjs-abi@^0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/ethereumjs-abi/-/ethereumjs-abi-0.6.4.tgz#9ba1bb056492d00c27279f6eccd4d58275912c1a" + dependencies: + bn.js "^4.10.0" + ethereumjs-util "^4.3.0" + "ethereumjs-abi@git+https://github.com/ethereumjs/ethereumjs-abi.git": version "0.6.4" resolved "git+https://github.com/ethereumjs/ethereumjs-abi.git#ee6ded67235a98f3ef4ae2a338aee70a9f68fe20" -- cgit v1.2.3 From c2b8dada91c90788dcd81a0318c52a66b4b769d1 Mon Sep 17 00:00:00 2001 From: Sergey Ukustov Date: Fri, 29 Sep 2017 19:24:08 +0300 Subject: Add eth_signTypedData handler --- app/scripts/background.js | 3 +- app/scripts/lib/typed-message-manager.js | 108 +++++++++++++++++++++++++ app/scripts/metamask-controller.js | 52 ++++++++++++ ui/app/actions.js | 27 +++++++ ui/app/components/pending-typed-msg-details.js | 59 ++++++++++++++ ui/app/components/pending-typed-msg.js | 46 +++++++++++ ui/app/components/typed-message-renderer.js | 42 ++++++++++ ui/app/conf-tx.js | 25 +++++- ui/app/reducers/app.js | 4 +- ui/index.js | 2 +- ui/lib/tx-helper.js | 13 ++- 11 files changed, 372 insertions(+), 9 deletions(-) create mode 100644 app/scripts/lib/typed-message-manager.js create mode 100644 ui/app/components/pending-typed-msg-details.js create mode 100644 ui/app/components/pending-typed-msg.js create mode 100644 ui/app/components/typed-message-renderer.js diff --git a/app/scripts/background.js b/app/scripts/background.js index 195881e15..3e560d302 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -124,7 +124,8 @@ function setupController (initState) { var unapprovedTxCount = controller.txController.getUnapprovedTxCount() var unapprovedMsgCount = controller.messageManager.unapprovedMsgCount var unapprovedPersonalMsgs = controller.personalMessageManager.unapprovedPersonalMsgCount - var count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgs + var unapprovedTypedMsgs = controller.typedMessageManager.unapprovedTypedMessagesCount + var count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgs + unapprovedTypedMsgs if (count) { label = String(count) } diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js new file mode 100644 index 000000000..e3efdb45d --- /dev/null +++ b/app/scripts/lib/typed-message-manager.js @@ -0,0 +1,108 @@ +const EventEmitter = require('events') +const ObservableStore = require('obs-store') +const createId = require('./random-id') + + +module.exports = class TypedMessageManager extends EventEmitter { + constructor (opts) { + super() + this.memStore = new ObservableStore({ + unapprovedTypedMessages: {}, + unapprovedTypedMessagesCount: 0, + }) + this.messages = [] + } + + get unapprovedTypedMessagesCount () { + return Object.keys(this.getUnapprovedMsgs()).length + } + + getUnapprovedMsgs () { + return this.messages.filter(msg => msg.status === 'unapproved') + .reduce((result, msg) => { result[msg.id] = msg; return result }, {}) + } + + addUnapprovedMessage (msgParams) { + log.debug(`TypedMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`) + // create txData obj with parameters and meta data + var time = (new Date()).getTime() + var msgId = createId() + var msgData = { + id: msgId, + msgParams: msgParams, + time: time, + status: 'unapproved', + type: 'eth_signTypedData', + } + this.addMsg(msgData) + + // signal update + this.emit('update') + return msgId + } + + addMsg (msg) { + this.messages.push(msg) + this._saveMsgList() + } + + getMsg (msgId) { + return this.messages.find(msg => msg.id === msgId) + } + + approveMessage (msgParams) { + this.setMsgStatusApproved(msgParams.metamaskId) + return this.prepMsgForSigning(msgParams) + } + + setMsgStatusApproved (msgId) { + this._setMsgStatus(msgId, 'approved') + } + + setMsgStatusSigned (msgId, rawSig) { + const msg = this.getMsg(msgId) + msg.rawSig = rawSig + this._updateMsg(msg) + this._setMsgStatus(msgId, 'signed') + } + + prepMsgForSigning (msgParams) { + delete msgParams.metamaskId + return Promise.resolve(msgParams) + } + + rejectMsg (msgId) { + this._setMsgStatus(msgId, 'rejected') + } + + // + // PRIVATE METHODS + // + + _setMsgStatus (msgId, status) { + const msg = this.getMsg(msgId) + if (!msg) throw new Error('TypedMessageManager - Message not found for id: "${msgId}".') + msg.status = status + this._updateMsg(msg) + this.emit(`${msgId}:${status}`, msg) + if (status === 'rejected' || status === 'signed') { + this.emit(`${msgId}:finished`, msg) + } + } + + _updateMsg (msg) { + const index = this.messages.findIndex((message) => message.id === msg.id) + if (index !== -1) { + this.messages[index] = msg + } + this._saveMsgList() + } + + _saveMsgList () { + const unapprovedTypedMessages = this.getUnapprovedMsgs() + const unapprovedTypedMessagesCount = Object.keys(unapprovedTypedMessages).length + this.memStore.updateState({ unapprovedTypedMessages, unapprovedTypedMessagesCount }) + this.emit('updateBadge') + } + +} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 5b3161bc6..0eeb708fc 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -25,6 +25,7 @@ const InfuraController = require('./controllers/infura') const BlacklistController = require('./controllers/blacklist') const MessageManager = require('./lib/message-manager') const PersonalMessageManager = require('./lib/personal-message-manager') +const TypedMessageManager = require('./lib/typed-message-manager') const TransactionController = require('./controllers/transactions') const BalancesController = require('./controllers/computed-balances') const ConfigManager = require('./lib/config-manager') @@ -154,6 +155,7 @@ module.exports = class MetamaskController extends EventEmitter { this.networkController.lookupNetwork() this.messageManager = new MessageManager() this.personalMessageManager = new PersonalMessageManager() + this.typedMessageManager = new TypedMessageManager() this.publicConfigStore = this.initPublicConfigStore() // manual disk state subscriptions @@ -195,6 +197,7 @@ module.exports = class MetamaskController extends EventEmitter { this.balancesController.store.subscribe(this.sendUpdate.bind(this)) this.messageManager.memStore.subscribe(this.sendUpdate.bind(this)) this.personalMessageManager.memStore.subscribe(this.sendUpdate.bind(this)) + this.typedMessageManager.memStore.subscribe(this.sendUpdate.bind(this)) this.keyringController.memStore.subscribe(this.sendUpdate.bind(this)) this.preferencesController.store.subscribe(this.sendUpdate.bind(this)) this.addressBookController.store.subscribe(this.sendUpdate.bind(this)) @@ -234,6 +237,7 @@ module.exports = class MetamaskController extends EventEmitter { processMessage: this.newUnsignedMessage.bind(this), // personal_sign msg signing processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), + processTypedMessage: this.newUnsignedTypedMessage.bind(this), }) } @@ -276,6 +280,7 @@ module.exports = class MetamaskController extends EventEmitter { this.txController.memStore.getState(), this.messageManager.memStore.getState(), this.personalMessageManager.memStore.getState(), + this.typedMessageManager.memStore.getState(), this.keyringController.memStore.getState(), this.balancesController.store.getState(), this.preferencesController.store.getState(), @@ -354,6 +359,10 @@ module.exports = class MetamaskController extends EventEmitter { signPersonalMessage: nodeify(this.signPersonalMessage, this), cancelPersonalMessage: this.cancelPersonalMessage.bind(this), + // personalMessageManager + signTypedMessage: nodeify(this.signTypedMessage, this), + cancelTypedMessage: this.cancelTypedMessage.bind(this), + // notices checkNotices: noticeController.updateNoticesList.bind(noticeController), markNoticeRead: noticeController.markNoticeRead.bind(noticeController), @@ -546,6 +555,23 @@ module.exports = class MetamaskController extends EventEmitter { }) } + newUnsignedTypedMessage (msgParams, cb) { + const msgId = this.typedMessageManager.addUnapprovedMessage(msgParams) + this.sendUpdate() + this.opts.showUnconfirmedMessage() + this.typedMessageManager.once(`${msgId}:finished`, (data) => { + console.log(data) + switch (data.status) { + case 'signed': + return cb(null, data.rawSig) + case 'rejected': + return cb(new Error('MetaMask Message Signature: User denied message signature.')) + default: + return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) + } + }) + } + signMessage (msgParams, cb) { log.info('MetaMaskController - signMessage') const msgId = msgParams.metamaskId @@ -608,6 +634,24 @@ module.exports = class MetamaskController extends EventEmitter { }) } + signTypedMessage (msgParams) { + log.info('MetaMaskController - signTypedMessage') + const msgId = msgParams.metamaskId + // sets the status op the message to 'approved' + // and removes the metamaskId for signing + return this.typedMessageManager.approveMessage(msgParams) + .then((cleanMsgParams) => { + // signs the message + return this.keyringController.signTypedMessage(cleanMsgParams) + }) + .then((rawSig) => { + // tells the listener that the message has been signed + // and can be returned to the dapp + this.typedMessageManager.setMsgStatusSigned(msgId, rawSig) + return this.getState() + }) + } + cancelPersonalMessage (msgId, cb) { const messageManager = this.personalMessageManager messageManager.rejectMsg(msgId) @@ -616,6 +660,14 @@ module.exports = class MetamaskController extends EventEmitter { } } + cancelTypedMessage (msgId, cb) { + const messageManager = this.typedMessageManager + messageManager.rejectMsg(msgId) + if (cb && typeof cb === 'function') { + cb(null, this.getState()) + } + } + markAccountsFound (cb) { this.configManager.setLostAccounts([]) this.sendUpdate() diff --git a/ui/app/actions.js b/ui/app/actions.js index e793e6a21..84a1b8dcc 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -97,6 +97,8 @@ var actions = { cancelMsg: cancelMsg, signPersonalMsg, cancelPersonalMsg, + signTypedMsg, + cancelTypedMsg, signTx: signTx, updateAndApproveTx, cancelTx: cancelTx, @@ -395,6 +397,25 @@ function signPersonalMsg (msgData) { } } +function signTypedMsg (msgData) { + log.debug('action - signTypedMsg') + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + + log.debug(`actions calling background.signTypedMessage`) + background.signTypedMessage(msgData, (err, newState) => { + log.debug('signTypedMessage called back') + dispatch(actions.updateMetamaskState(newState)) + dispatch(actions.hideLoadingIndication()) + + if (err) log.error(err) + if (err) return dispatch(actions.displayWarning(err.message)) + + dispatch(actions.completedTx(msgData.metamaskId)) + }) + } +} + function signTx (txData) { return (dispatch) => { dispatch(actions.showLoadingIndication()) @@ -449,6 +470,12 @@ function cancelPersonalMsg (msgData) { return actions.completedTx(id) } +function cancelTypedMsg (msgData) { + const id = msgData.id + background.cancelTypedMessage(id) + return actions.completedTx(id) +} + function cancelTx (txData) { return (dispatch) => { log.debug(`background.cancelTransaction`) diff --git a/ui/app/components/pending-typed-msg-details.js b/ui/app/components/pending-typed-msg-details.js new file mode 100644 index 000000000..b5fd29f71 --- /dev/null +++ b/ui/app/components/pending-typed-msg-details.js @@ -0,0 +1,59 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const AccountPanel = require('./account-panel') +const TypedMessageRenderer = require('./typed-message-renderer') + +module.exports = PendingMsgDetails + +inherits(PendingMsgDetails, Component) +function PendingMsgDetails () { + Component.call(this) +} + +PendingMsgDetails.prototype.render = function () { + var state = this.props + var msgData = state.txData + + var msgParams = msgData.msgParams || {} + var address = msgParams.from || state.selectedAddress + var identity = state.identities[address] || { address: address } + var account = state.accounts[address] || { address: address } + + var { data } = msgParams + + return ( + h('div', { + key: msgData.id, + style: { + margin: '10px 20px', + }, + }, [ + + // account that will sign + h(AccountPanel, { + showFullAddress: true, + identity: identity, + account: account, + imageifyIdenticons: state.imageifyIdenticons, + }), + + // message data + h('div', { + style: { + height: '260px', + }, + }, [ + h('label.font-small', { style: { display: 'block' } }, 'YOU ARE SIGNING'), + h(TypedMessageRenderer, { + value: data, + style: { + height: '215px', + }, + }), + ]), + + ]) + ) +} diff --git a/ui/app/components/pending-typed-msg.js b/ui/app/components/pending-typed-msg.js new file mode 100644 index 000000000..f8926d0a3 --- /dev/null +++ b/ui/app/components/pending-typed-msg.js @@ -0,0 +1,46 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const PendingTxDetails = require('./pending-typed-msg-details') + +module.exports = PendingMsg + +inherits(PendingMsg, Component) +function PendingMsg () { + Component.call(this) +} + +PendingMsg.prototype.render = function () { + var state = this.props + var msgData = state.txData + + return ( + + h('div', { + key: msgData.id, + }, [ + + // header + h('h3', { + style: { + fontWeight: 'bold', + textAlign: 'center', + }, + }, 'Sign Message'), + + // message details + h(PendingTxDetails, state), + + // sign + cancel + h('.flex-row.flex-space-around', [ + h('button', { + onClick: state.cancelTypedMessage, + }, 'Cancel'), + h('button', { + onClick: state.signTypedMessage, + }, 'Sign'), + ]), + ]) + + ) +} diff --git a/ui/app/components/typed-message-renderer.js b/ui/app/components/typed-message-renderer.js new file mode 100644 index 000000000..50e8da02c --- /dev/null +++ b/ui/app/components/typed-message-renderer.js @@ -0,0 +1,42 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const extend = require('xtend') + +module.exports = TypedMessageRenderer + +inherits(TypedMessageRenderer, Component) +function TypedMessageRenderer () { + Component.call(this) +} + +TypedMessageRenderer.prototype.render = function () { + const props = this.props + const { value, style } = props + const text = renderTypedData(value) + + const defaultStyle = extend({ + width: '315px', + maxHeight: '210px', + resize: 'none', + border: 'none', + background: 'white', + padding: '3px', + }, style) + + return ( + h('div.font-small', { + style: defaultStyle, + }, text) + ) +} + +function renderTypedData(values) { + return values.map(function (value) { + return h('div', {}, [ + h('strong', {style: {display: 'block', fontWeight: 'bold', textTransform: 'capitalize'}}, String(value.name) + ':'), + h('div', {}, value.value) + ]) + }) +} \ No newline at end of file diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 15fb9a59f..f93fc2373 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -10,6 +10,7 @@ const isPopupOrNotification = require('../../app/scripts/lib/is-popup-or-notific const PendingTx = require('./components/pending-tx') const PendingMsg = require('./components/pending-msg') const PendingPersonalMsg = require('./components/pending-personal-msg') +const PendingTypedMsg = require('./components/pending-typed-msg') const Loading = require('./components/loading') module.exports = connect(mapStateToProps)(ConfirmTxScreen) @@ -22,6 +23,7 @@ function mapStateToProps (state) { unapprovedTxs: state.metamask.unapprovedTxs, unapprovedMsgs: state.metamask.unapprovedMsgs, unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs, + unapprovedTypedMessages: state.metamask.unapprovedTypedMessages, index: state.appState.currentView.context, warning: state.appState.warning, network: state.metamask.network, @@ -41,9 +43,9 @@ function ConfirmTxScreen () { ConfirmTxScreen.prototype.render = function () { const props = this.props const { network, provider, unapprovedTxs, currentCurrency, computedBalances, - unapprovedMsgs, unapprovedPersonalMsgs, conversionRate, blockGasLimit } = props + unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, conversionRate, blockGasLimit } = props - var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) + var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, network) var txData = unconfTxList[props.index] || {} var txParams = txData.params || {} @@ -112,8 +114,10 @@ ConfirmTxScreen.prototype.render = function () { cancelAllTransactions: this.cancelAllTransactions.bind(this, unconfTxList), signMessage: this.signMessage.bind(this, txData), signPersonalMessage: this.signPersonalMessage.bind(this, txData), + signTypedMessage: this.signTypedMessage.bind(this, txData), cancelMessage: this.cancelMessage.bind(this, txData), cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), + cancelTypedMessage: this.cancelTypedMessage.bind(this, txData) }), ]) ) @@ -136,6 +140,9 @@ function currentTxView (opts) { } else if (type === 'personal_sign') { log.debug('rendering personal_sign message') return h(PendingPersonalMsg, opts) + } else if (type === 'eth_signTypedData') { + log.debug('rendering eth_signTypedData message') + return h(PendingTypedMsg, opts) } } } @@ -184,6 +191,14 @@ ConfirmTxScreen.prototype.signPersonalMessage = function (msgData, event) { this.props.dispatch(actions.signPersonalMsg(params)) } +ConfirmTxScreen.prototype.signTypedMessage = function (msgData, event) { + log.info('conf-tx.js: signing typed message') + var params = msgData.msgParams + params.metamaskId = msgData.id + this.stopPropagation(event) + this.props.dispatch(actions.signTypedMsg(params)) +} + ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) { log.info('canceling message') this.stopPropagation(event) @@ -196,6 +211,12 @@ ConfirmTxScreen.prototype.cancelPersonalMessage = function (msgData, event) { this.props.dispatch(actions.cancelPersonalMsg(msgData)) } +ConfirmTxScreen.prototype.cancelTypedMessage = function (msgData, event) { + log.info('canceling typed message') + this.stopPropagation(event) + this.props.dispatch(actions.cancelTypedMsg(msgData)) +} + ConfirmTxScreen.prototype.goHome = function (event) { this.stopPropagation(event) this.props.dispatch(actions.goHome()) diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 3a98d53a9..349c25b96 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -574,9 +574,9 @@ function checkUnconfActions (state) { function getUnconfActionList (state) { const { unapprovedTxs, unapprovedMsgs, - unapprovedPersonalMsgs, network } = state.metamask + unapprovedPersonalMsgs, unapprovedTypedMessages, network } = state.metamask - const unconfActionList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) + const unconfActionList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, network) return unconfActionList } diff --git a/ui/index.js b/ui/index.js index a729138d3..ae05cbe67 100644 --- a/ui/index.js +++ b/ui/index.js @@ -37,7 +37,7 @@ function startApp (metamaskState, accountManager, opts) { }) // if unconfirmed txs, start on txConf page - const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.network) + const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.unapprovedTypedMessages, metamaskState.network) if (unapprovedTxsAll.length > 0) { store.dispatch(actions.showConfTxPage()) } diff --git a/ui/lib/tx-helper.js b/ui/lib/tx-helper.js index 5def23e51..341567e2f 100644 --- a/ui/lib/tx-helper.js +++ b/ui/lib/tx-helper.js @@ -1,20 +1,27 @@ const valuesFor = require('../app/util').valuesFor -module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, network) { +module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, typedMessages, network) { log.debug('tx-helper called with params:') - log.debug({ unapprovedTxs, unapprovedMsgs, personalMsgs, network }) + log.debug({ unapprovedTxs, unapprovedMsgs, personalMsgs, typedMessages, network }) const txValues = network ? valuesFor(unapprovedTxs).filter(txMeta => txMeta.metamaskNetworkId === network) : valuesFor(unapprovedTxs) log.debug(`tx helper found ${txValues.length} unapproved txs`) + const msgValues = valuesFor(unapprovedMsgs) log.debug(`tx helper found ${msgValues.length} unsigned messages`) let allValues = txValues.concat(msgValues) + const personalValues = valuesFor(personalMsgs) log.debug(`tx helper found ${personalValues.length} unsigned personal messages`) allValues = allValues.concat(personalValues) + + const typedValues = valuesFor(typedMessages) + log.debug(`tx helper found ${typedValues.length} unsigned typed messages`) + allValues = allValues.concat(typedValues) + allValues = allValues.sort((a, b) => { return a.time > b.time }) return allValues -} +} \ No newline at end of file -- cgit v1.2.3 From 82d1f391986ac6f7cb7d9029f50d44b5a8c9442b Mon Sep 17 00:00:00 2001 From: Sergey Ukustov Date: Fri, 29 Sep 2017 19:47:40 +0300 Subject: Respect code style --- ui/app/components/typed-message-renderer.js | 3 +-- ui/app/conf-tx.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ui/app/components/typed-message-renderer.js b/ui/app/components/typed-message-renderer.js index 50e8da02c..b7d1572c2 100644 --- a/ui/app/components/typed-message-renderer.js +++ b/ui/app/components/typed-message-renderer.js @@ -1,7 +1,6 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits -const ethUtil = require('ethereumjs-util') const extend = require('xtend') module.exports = TypedMessageRenderer @@ -36,7 +35,7 @@ function renderTypedData(values) { return values.map(function (value) { return h('div', {}, [ h('strong', {style: {display: 'block', fontWeight: 'bold', textTransform: 'capitalize'}}, String(value.name) + ':'), - h('div', {}, value.value) + h('div', {}, value.value), ]) }) } \ No newline at end of file diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index f93fc2373..cb1afedfe 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -117,7 +117,7 @@ ConfirmTxScreen.prototype.render = function () { signTypedMessage: this.signTypedMessage.bind(this, txData), cancelMessage: this.cancelMessage.bind(this, txData), cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), - cancelTypedMessage: this.cancelTypedMessage.bind(this, txData) + cancelTypedMessage: this.cancelTypedMessage.bind(this, txData), }), ]) ) -- cgit v1.2.3 From 861bd877f3bf1c0c71a00f1b90048e93dec03488 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 29 Sep 2017 11:19:54 -0700 Subject: Ensure selected account is always set if possible Fixes #2218 Subscribes to keyringController, and if only one account exists, sets it as selected. --- CHANGELOG.md | 1 + app/scripts/metamask-controller.js | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0faf6fe85..40663dcf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Current Master +- Fixed bug where sometimes the current account was not correctly set and exposed to web apps. - Added AUD, HKD, SGD, IDR, PHP to currency conversion list ## 3.10.6 2017-9-27 diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 5b3161bc6..b28f2738a 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -100,6 +100,14 @@ module.exports = class MetamaskController extends EventEmitter { encryptor: opts.encryptor || undefined, }) + // If only one account exists, make sure it is selected. + this.keyringController.store.subscribe((state) => { + const addresses = Object.keys(state.walletNicknames || {}) + if (addresses.length === 1) { + const address = addresses[0] + this.preferencesController.setSelectedAddress(address) + } + }) this.keyringController.on('newAccount', (address) => { this.preferencesController.setSelectedAddress(address) this.accountTracker.addAccount(address) @@ -222,6 +230,7 @@ module.exports = class MetamaskController extends EventEmitter { const isUnlocked = this.keyringController.memStore.getState().isUnlocked const result = [] const selectedAddress = this.preferencesController.getSelectedAddress() + // only show address if account is unlocked if (isUnlocked && selectedAddress) { result.push(selectedAddress) -- cgit v1.2.3 From 19e7adad1920ac506b3ef5c639ec110a2615bd7c Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 29 Sep 2017 11:50:24 -0700 Subject: development - fix ui dev --- development/index.html | 84 ++++++++++++++++++++++++-------------------------- ui-dev.js | 51 +++++++++++++++++------------- 2 files changed, 69 insertions(+), 66 deletions(-) diff --git a/development/index.html b/development/index.html index a0814cb55..e5a027447 100644 --- a/development/index.html +++ b/development/index.html @@ -3,62 +3,58 @@ MetaMask - - - -
+ + - + -function reload() { - window.location.reload() -} - + diff --git a/ui-dev.js b/ui-dev.js index de5dfd8ef..620d81667 100644 --- a/ui-dev.js +++ b/ui-dev.js @@ -61,30 +61,37 @@ const actions = { var css = MetaMaskUiCss() injectCss(css) -const container = document.querySelector('#test-container') - // parse opts var store = configureStore(states[selectedView]) // start app -render( - h('.super-dev-container', [ - - h(Selector, { actions, selectedKey: selectedView, states, store }), - - h('#app-content', { - style: { - height: '500px', - width: '360px', - boxShadow: 'grey 0px 2px 9px', - margin: '20px', - }, - }, [ - h(Root, { - store: store, - }), - ]), - - ] -), container) +startApp() + +function startApp(){ + const body = document.body + const container = document.createElement('div') + container.id = 'test-container' + body.appendChild(container) + + render( + h('.super-dev-container', [ + + h(Selector, { actions, selectedKey: selectedView, states, store }), + + h('#app-content', { + style: { + height: '500px', + width: '360px', + boxShadow: 'grey 0px 2px 9px', + margin: '20px', + }, + }, [ + h(Root, { + store: store, + }), + ]), + + ] + ), container) +} -- cgit v1.2.3 From ac80eaca1fc9923cd5696282ba2bc6bace22ff83 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Fri, 29 Sep 2017 12:54:05 -0700 Subject: pending-tx - dont check the balance to rebrodcast --- app/scripts/controllers/transactions.js | 11 +---------- app/scripts/lib/pending-tx-tracker.js | 15 --------------- app/scripts/metamask-controller.js | 1 - test/unit/pending-tx-test.js | 27 +-------------------------- test/unit/tx-controller-test.js | 25 ------------------------- 5 files changed, 2 insertions(+), 77 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 4f5c94675..9fdec1ead 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -32,7 +32,6 @@ module.exports = class TransactionController extends EventEmitter { this.provider = opts.provider this.blockTracker = opts.blockTracker this.signEthTx = opts.signTransaction - this.accountTracker = opts.accountTracker this.memStore = new ObservableStore({}) this.query = new EthQuery(this.provider) @@ -61,11 +60,6 @@ module.exports = class TransactionController extends EventEmitter { provider: this.provider, nonceTracker: this.nonceTracker, retryLimit: 3500, // Retry 3500 blocks, or about 1 day. - getBalance: (address) => { - const account = this.accountTracker.store.getState().accounts[address] - if (!account) return - return account.balance - }, publishTransaction: (rawTx) => this.query.sendRawTransaction(rawTx), getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager), }) @@ -84,10 +78,7 @@ module.exports = class TransactionController extends EventEmitter { this.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker)) // this is a little messy but until ethstore has been either // removed or redone this is to guard against the race condition - // where accountTracker hasent been populated by the results yet - this.blockTracker.once('latest', () => { - this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker)) - }) + this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker)) this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker)) // memstore is computed from a few different stores this._updateMemstore() diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js index b97cec9ce..3d358b00e 100644 --- a/app/scripts/lib/pending-tx-tracker.js +++ b/app/scripts/lib/pending-tx-tracker.js @@ -1,6 +1,5 @@ const EventEmitter = require('events') const EthQuery = require('ethjs-query') -const sufficientBalance = require('./util').sufficientBalance /* Utility class for tracking the transactions as they @@ -12,7 +11,6 @@ const sufficientBalance = require('./util').sufficientBalance requires a: { provider: //, nonceTracker: //see nonce tracker, - getBalnce: //(address) a function for getting balances, getPendingTransactions: //() a function for getting an array of transactions, publishTransaction: //(rawTx) a async function for publishing raw transactions, } @@ -25,7 +23,6 @@ module.exports = class PendingTransactionTracker extends EventEmitter { this.query = new EthQuery(config.provider) this.nonceTracker = config.nonceTracker this.retryLimit = config.retryLimit || Infinity - this.getBalance = config.getBalance this.getPendingTransactions = config.getPendingTransactions this.publishTransaction = config.publishTransaction } @@ -99,23 +96,11 @@ module.exports = class PendingTransactionTracker extends EventEmitter { } async _resubmitTx (txMeta) { - const address = txMeta.txParams.from - const balance = this.getBalance(address) - if (balance === undefined) return - if (txMeta.retryCount > this.retryLimit) { const err = new Error(`Gave up submitting after ${this.retryLimit} blocks un-mined.`) return this.emit('tx:failed', txMeta.id, err) } - // if the value of the transaction is greater then the balance, fail. - if (!sufficientBalance(txMeta.txParams, balance)) { - const insufficientFundsError = new Error('Insufficient balance during rebroadcast.') - this.emit('tx:failed', txMeta.id, insufficientFundsError) - log.error(insufficientFundsError) - return - } - // Only auto-submit already-signed txs: if (!('rawTx' in txMeta)) return diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b28f2738a..e152dfedb 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -132,7 +132,6 @@ module.exports = class MetamaskController extends EventEmitter { provider: this.provider, blockTracker: this.blockTracker, ethQuery: this.ethQuery, - accountTracker: this.accountTracker, }) this.txController.on('newUnaprovedTx', opts.showUnapprovedTx.bind(opts)) diff --git a/test/unit/pending-tx-test.js b/test/unit/pending-tx-test.js index 1af464656..4da0eff5d 100644 --- a/test/unit/pending-tx-test.js +++ b/test/unit/pending-tx-test.js @@ -40,14 +40,12 @@ describe('PendingTransactionTracker', function () { pendingTxTracker = new PendingTransactionTracker({ provider, - getBalance: () => {}, nonceTracker: { getGlobalLock: async () => { return { releaseLock: () => {} } } }, getPendingTransactions: () => {return []}, - sufficientBalance: () => {}, publishTransaction: () => {}, }) }) @@ -213,30 +211,7 @@ describe('PendingTransactionTracker', function () { pendingTxTracker.resubmitPendingTxs() }) }) - describe('#_resubmitTx with a too-low balance', function () { - it('should return before publishing the transaction because to low of balance', function (done) { - const lowBalance = '0x0' - pendingTxTracker.getBalance = (address) => { - assert.equal(address, txMeta.txParams.from, 'Should pass the address') - return lowBalance - } - pendingTxTracker.publishTransaction = async (rawTx) => { - done(new Error('tried to publish transaction')) - } - - // Stubbing out current account state: - // Adding the fake tx: - pendingTxTracker.once('tx:failed', (txId, err) => { - assert(err, 'Should have a error') - done() - }) - pendingTxTracker._resubmitTx(txMeta) - .catch((err) => { - assert.ifError(err, 'should not throw an error') - done(err) - }) - }) - + describe('#_resubmitTx', function () { it('should publishing the transaction', function (done) { const enoughBalance = '0x100000' pendingTxTracker.getBalance = (address) => { diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index 66772ff88..bb51ab01f 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -25,7 +25,6 @@ describe('Transaction Controller', function () { networkStore: new ObservableStore(currentNetworkId), txHistoryLimit: 10, blockTracker: { getCurrentBlock: noop, on: noop, once: noop }, - accountTracker: { store: { getState: noop } }, signTransaction: (ethTx) => new Promise((resolve) => { ethTx.sign(privKey) resolve() @@ -383,30 +382,6 @@ describe('Transaction Controller', function () { }) }) - describe('#getBalance', function () { - it('gets balance', function () { - sinon.stub(txController.accountTracker.store, 'getState').callsFake(() => { - return { - accounts: { - '0x1678a085c290ebd122dc42cba69373b5953b831d': { - address: '0x1678a085c290ebd122dc42cba69373b5953b831d', - balance: '0x00000000000000056bc75e2d63100000', - code: '0x', - nonce: '0x0', - }, - '0xc684832530fcbddae4b4230a47e991ddcec2831d': { - address: '0xc684832530fcbddae4b4230a47e991ddcec2831d', - balance: '0x0', - code: '0x', - nonce: '0x0', - }, - }, - } - }) - assert.equal(txController.pendingTxTracker.getBalance('0x1678a085c290ebd122dc42cba69373b5953b831d'), '0x00000000000000056bc75e2d63100000') - assert.equal(txController.pendingTxTracker.getBalance('0xc684832530fcbddae4b4230a47e991ddcec2831d'), '0x0') - }) - }) describe('#getPendingTransactions', function () { beforeEach(function () { -- cgit v1.2.3 From 14d58e630d6b806c9ef23adf7aff4b2fd30609c0 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 29 Sep 2017 13:17:21 -0700 Subject: Version 3.10.7 --- CHANGELOG.md | 2 ++ app/manifest.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40663dcf5..8c39bbc1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +## 3.10.7 2017-9-28 + - Fixed bug where sometimes the current account was not correctly set and exposed to web apps. - Added AUD, HKD, SGD, IDR, PHP to currency conversion list diff --git a/app/manifest.json b/app/manifest.json index 639f3fb4b..146b27f89 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.10.6", + "version": "3.10.7", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From b88d11f86ede907bdf51cd224e5ea6285e56147b Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 29 Sep 2017 16:09:38 -0700 Subject: network controller - small refactor --- app/scripts/controllers/network.js | 47 +++++++++++++++++++++++--------------- app/scripts/metamask-controller.js | 38 ++++++++++++++++-------------- 2 files changed, 50 insertions(+), 35 deletions(-) diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js index 2a17cdae8..9e05afe75 100644 --- a/app/scripts/controllers/network.js +++ b/app/scripts/controllers/network.js @@ -1,5 +1,6 @@ +const assert = require('assert') const EventEmitter = require('events') -const MetaMaskProvider = require('web3-provider-engine/zero.js') +const createMetamaskProvider = require('web3-provider-engine/zero.js') const ObservableStore = require('obs-store') const ComposedStore = require('obs-store/lib/composed') const extend = require('xtend') @@ -18,13 +19,13 @@ module.exports = class NetworkController extends EventEmitter { this._proxy = createEventEmitterProxy() this.on('networkDidChange', this.lookupNetwork) - this.providerStore.subscribe((state) => this.switchNetwork({ rpcUrl: state.rpcTarget })) + this.providerStore.subscribe((state) => this._switchNetwork({ rpcUrl: state.rpcTarget })) } - initializeProvider (opts, providerContructor = MetaMaskProvider) { - this._baseProviderParams = opts - const provider = providerContructor(opts) - this._setProvider(provider) + initializeProvider (_providerParams) { + this._baseProviderParams = _providerParams + const rpcUrl = this.getCurrentRpcAddress() + this._configureStandardProvider({ rpcUrl }) this._proxy.on('block', this._logBlock.bind(this)) this._proxy.on('error', this.verifyNetwork.bind(this)) this.ethQuery = new EthQuery(this._proxy) @@ -32,17 +33,8 @@ module.exports = class NetworkController extends EventEmitter { return this._proxy } - switchNetwork (opts) { - this.setNetworkState('loading') - const providerParams = extend(this._baseProviderParams, opts) - this._baseProviderParams = providerParams - const provider = MetaMaskProvider(providerParams) - this._setProvider(provider) - this.emit('networkDidChange') - } - verifyNetwork () { - // Check network when restoring connectivity: + // Check network when restoring connectivity: if (this.isNetworkLoading()) this.lookupNetwork() } @@ -79,10 +71,13 @@ module.exports = class NetworkController extends EventEmitter { return this.getRpcAddressForType(provider.type) } - setProviderType (type) { + async setProviderType (type) { + assert(type !== 'rpc', `NetworkController.setProviderType - cannot connect by type "rpc"`) + // skip if type already matches if (type === this.getProviderConfig().type) return const rpcTarget = this.getRpcAddressForType(type) - this.providerStore.updateState({type, rpcTarget}) + assert(rpcTarget, `NetworkController - unknown rpc address for type "${type}"`) + this.providerStore.updateState({ type, rpcTarget }) } getProviderConfig () { @@ -94,6 +89,22 @@ module.exports = class NetworkController extends EventEmitter { return provider && provider.rpcTarget ? provider.rpcTarget : DEFAULT_RPC } + // + // Private + // + + _switchNetwork (providerParams) { + this.setNetworkState('loading') + this._configureStandardProvider(providerParams) + this.emit('networkDidChange') + } + + _configureStandardProvider(_providerParams) { + const providerParams = extend(this._baseProviderParams, _providerParams) + const provider = createMetamaskProvider(providerParams) + this._setProvider(provider) + } + _setProvider (provider) { // collect old block tracker events const oldProvider = this._provider diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b28f2738a..fa6f1a245 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -217,13 +217,11 @@ module.exports = class MetamaskController extends EventEmitter { // initializeProvider () { - return this.networkController.initializeProvider({ + const providerOpts = { static: { eth_syncing: false, web3_clientVersion: `MetaMask/v${version}`, }, - // rpc data source - rpcUrl: this.networkController.getCurrentRpcAddress(), originHttpHeaderKey: 'X-Metamask-Origin', // account mgmt getAccounts: (cb) => { @@ -243,7 +241,9 @@ module.exports = class MetamaskController extends EventEmitter { processMessage: this.newUnsignedMessage.bind(this), // personal_sign msg signing processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), - }) + } + const providerProxy = this.networkController.initializeProvider(providerOpts) + return providerProxy } initPublicConfigStore () { @@ -312,13 +312,14 @@ module.exports = class MetamaskController extends EventEmitter { const txController = this.txController const noticeController = this.noticeController const addressBookController = this.addressBookController + const networkController = this.networkController return { // etc getState: (cb) => cb(null, this.getState()), - setProviderType: this.networkController.setProviderType.bind(this.networkController), setCurrentCurrency: this.setCurrentCurrency.bind(this), markAccountsFound: this.markAccountsFound.bind(this), + // coinbase buyEth: this.buyEth.bind(this), // shapeshift @@ -333,12 +334,15 @@ module.exports = class MetamaskController extends EventEmitter { // vault management submitPassword: this.submitPassword.bind(this), + // network management + setProviderType: nodeify(networkController.setProviderType, networkController), + setDefaultRpc: nodeify(this.setDefaultRpc, this), + setCustomRpc: nodeify(this.setCustomRpc, this), + // PreferencesController setSelectedAddress: nodeify(preferencesController.setSelectedAddress, preferencesController), addToken: nodeify(preferencesController.addToken, preferencesController), setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController), - setDefaultRpc: nodeify(this.setDefaultRpc, this), - setCustomRpc: nodeify(this.setCustomRpc, this), // AddressController setAddressBook: nodeify(addressBookController.setAddressBook, addressBookController), @@ -689,19 +693,19 @@ module.exports = class MetamaskController extends EventEmitter { createShapeShiftTx (depositAddress, depositType) { this.shapeshiftController.createShapeShiftTx(depositAddress, depositType) } -// network - setDefaultRpc () { - this.networkController.setRpcTarget('http://localhost:8545') - return Promise.resolve('http://localhost:8545') + // network + + async setDefaultRpc () { + const localhost = 'http://localhost:8545' + this.networkController.setRpcTarget(localhost) + return localhost } - setCustomRpc (rpcTarget, rpcList) { + async setCustomRpc (rpcTarget, rpcList) { this.networkController.setRpcTarget(rpcTarget) - - return this.preferencesController.updateFrequentRpcList(rpcTarget) - .then(() => { - return Promise.resolve(rpcTarget) - }) + await this.preferencesController.updateFrequentRpcList(rpcTarget) + return rpcTarget } + } -- cgit v1.2.3 From d6ea2fa425c90f6e05d2d59d4a79f1b573cc2e06 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 29 Sep 2017 16:35:58 -0700 Subject: network - convert localhost from custom rpc to network type --- app/scripts/config.js | 2 ++ app/scripts/controllers/network.js | 1 + app/scripts/metamask-controller.js | 7 ------- ui/app/actions.js | 23 ++++++++--------------- ui/app/app.js | 2 +- 5 files changed, 12 insertions(+), 23 deletions(-) diff --git a/app/scripts/config.js b/app/scripts/config.js index c5f260583..1d4ff7c0d 100644 --- a/app/scripts/config.js +++ b/app/scripts/config.js @@ -2,11 +2,13 @@ const MAINET_RPC_URL = 'https://mainnet.infura.io/metamask' const ROPSTEN_RPC_URL = 'https://ropsten.infura.io/metamask' const KOVAN_RPC_URL = 'https://kovan.infura.io/metamask' const RINKEBY_RPC_URL = 'https://rinkeby.infura.io/metamask' +const LOCALHOST_RPC_URL = 'http://localhost:8545' global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' module.exports = { network: { + localhost: LOCALHOST_RPC_URL, mainnet: MAINET_RPC_URL, ropsten: ROPSTEN_RPC_URL, kovan: KOVAN_RPC_URL, diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js index 9e05afe75..9079e0653 100644 --- a/app/scripts/controllers/network.js +++ b/app/scripts/controllers/network.js @@ -10,6 +10,7 @@ const RPC_ADDRESS_LIST = require('../config.js').network const DEFAULT_RPC = RPC_ADDRESS_LIST['rinkeby'] module.exports = class NetworkController extends EventEmitter { + constructor (config) { super() config.provider.rpcTarget = this.getRpcAddressForType(config.provider.type, config.provider) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index fa6f1a245..eb978115d 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -336,7 +336,6 @@ module.exports = class MetamaskController extends EventEmitter { // network management setProviderType: nodeify(networkController.setProviderType, networkController), - setDefaultRpc: nodeify(this.setDefaultRpc, this), setCustomRpc: nodeify(this.setCustomRpc, this), // PreferencesController @@ -696,12 +695,6 @@ module.exports = class MetamaskController extends EventEmitter { // network - async setDefaultRpc () { - const localhost = 'http://localhost:8545' - this.networkController.setRpcTarget(localhost) - return localhost - } - async setCustomRpc (rpcTarget, rpcList) { this.networkController.setRpcTarget(rpcTarget) await this.preferencesController.updateFrequentRpcList(rpcTarget) diff --git a/ui/app/actions.js b/ui/app/actions.js index e793e6a21..4844dd56e 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -126,7 +126,6 @@ var actions = { showAddTokenPage, addToken, setRpcTarget: setRpcTarget, - setDefaultRpcTarget: setDefaultRpcTarget, setProviderType: setProviderType, // loading overlay SHOW_LOADING: 'SHOW_LOADING_INDICATION', @@ -706,16 +705,19 @@ function markAccountsFound () { // config // -// default rpc target refers to localhost:8545 in this instance. -function setDefaultRpcTarget () { - log.debug(`background.setDefaultRpcTarget`) +function setProviderType (type) { return (dispatch) => { - background.setDefaultRpc((err, result) => { + log.debug(`background.setProviderType`) + background.setProviderType(type, (err, result) => { if (err) { log.error(err) - return dispatch(self.displayWarning('Had a problem changing networks.')) + return dispatch(self.displayWarning('Had a problem changing networks!')) } }) + return { + type: actions.SET_PROVIDER_TYPE, + value: type, + } } } @@ -744,15 +746,6 @@ function addToAddressBook (recipient, nickname) { } } -function setProviderType (type) { - log.debug(`background.setProviderType`) - background.setProviderType(type) - return { - type: actions.SET_PROVIDER_TYPE, - value: type, - } -} - function useEtherscanProvider () { log.debug(`background.useEtherscanProvider`) background.useEtherscanProvider() diff --git a/ui/app/app.js b/ui/app/app.js index ee800ea90..50121b055 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -319,7 +319,7 @@ App.prototype.renderNetworkDropdown = function () { { key: 'default', closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), - onClick: () => props.dispatch(actions.setDefaultRpcTarget()), + onClick: () => props.dispatch(actions.setProviderType('localhost')), style: { fontSize: '18px', }, -- cgit v1.2.3 From a2b6d3ffc56a3f7fdab6b5ef42426f0a65fda7c7 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 29 Sep 2017 16:37:01 -0700 Subject: network - remove long dead etherscan provider code --- ui/app/actions.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index 4844dd56e..3ea092e57 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -119,8 +119,6 @@ var actions = { SET_RPC_TARGET: 'SET_RPC_TARGET', SET_DEFAULT_RPC_TARGET: 'SET_DEFAULT_RPC_TARGET', SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE', - USE_ETHERSCAN_PROVIDER: 'USE_ETHERSCAN_PROVIDER', - useEtherscanProvider: useEtherscanProvider, showConfigPage, SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', showAddTokenPage, @@ -746,14 +744,6 @@ function addToAddressBook (recipient, nickname) { } } -function useEtherscanProvider () { - log.debug(`background.useEtherscanProvider`) - background.useEtherscanProvider() - return { - type: actions.USE_ETHERSCAN_PROVIDER, - } -} - function showLoadingIndication (message) { return { type: actions.SHOW_LOADING, -- cgit v1.2.3 From 1ad8a9a0ffb84d11baeb2ae143986d3fb10b89c8 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 29 Sep 2017 17:10:34 -0700 Subject: network - make network controller internal network switching explicit --- app/scripts/controllers/network.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js index 9079e0653..0f9db4d53 100644 --- a/app/scripts/controllers/network.js +++ b/app/scripts/controllers/network.js @@ -10,7 +10,7 @@ const RPC_ADDRESS_LIST = require('../config.js').network const DEFAULT_RPC = RPC_ADDRESS_LIST['rinkeby'] module.exports = class NetworkController extends EventEmitter { - + constructor (config) { super() config.provider.rpcTarget = this.getRpcAddressForType(config.provider.type, config.provider) @@ -20,7 +20,6 @@ module.exports = class NetworkController extends EventEmitter { this._proxy = createEventEmitterProxy() this.on('networkDidChange', this.lookupNetwork) - this.providerStore.subscribe((state) => this._switchNetwork({ rpcUrl: state.rpcTarget })) } initializeProvider (_providerParams) { @@ -64,6 +63,7 @@ module.exports = class NetworkController extends EventEmitter { type: 'rpc', rpcTarget: rpcUrl, }) + this._switchNetwork({ rpcUrl }) } getCurrentRpcAddress () { @@ -79,6 +79,7 @@ module.exports = class NetworkController extends EventEmitter { const rpcTarget = this.getRpcAddressForType(type) assert(rpcTarget, `NetworkController - unknown rpc address for type "${type}"`) this.providerStore.updateState({ type, rpcTarget }) + this._switchNetwork({ rpcUrl: rpcTarget }) } getProviderConfig () { -- cgit v1.2.3 From d5b0d8af4f4907398154449239465efba601eb4d Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 29 Sep 2017 20:57:15 -0700 Subject: Version 3.10.8 - Fix Currency Conversion In our conversion to the new Infura API, somehow we were sending upper-cased conversions to their lower-case sensitive API. Fixes the first part of #2240 --- CHANGELOG.md | 4 ++++ app/manifest.json | 2 +- app/scripts/controllers/currency.js | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c39bbc1a..e635f6158 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Current Master +## 3.10.8 2017-9-28 + +- Fixed usage of new currency fetching API. + ## 3.10.7 2017-9-28 - Fixed bug where sometimes the current account was not correctly set and exposed to web apps. diff --git a/app/manifest.json b/app/manifest.json index 146b27f89..e71018211 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.10.7", + "version": "3.10.8", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", diff --git a/app/scripts/controllers/currency.js b/app/scripts/controllers/currency.js index 9e696ce55..25a7a942e 100644 --- a/app/scripts/controllers/currency.js +++ b/app/scripts/controllers/currency.js @@ -45,7 +45,7 @@ class CurrencyController { updateConversionRate () { const currentCurrency = this.getCurrentCurrency() - return fetch(`https://api.infura.io/v1/ticker/eth${currentCurrency}`) + return fetch(`https://api.infura.io/v1/ticker/eth${currentCurrency.toLowerCase()}`) .then(response => response.json()) .then((parsedResponse) => { this.setConversionRate(Number(parsedResponse.bid)) -- cgit v1.2.3 From 8cc8fecdacb8dbc355e74bd6958ff4a4565b9346 Mon Sep 17 00:00:00 2001 From: Adam Novak Date: Sun, 1 Oct 2017 18:55:52 -0700 Subject: Don't pass origin as an HTTP header Requests with this nonstandard header are being blocked by CORS when made against Parity. Not sending it ought to fix #1779. --- app/scripts/metamask-controller.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 03e021a92..1a468b6c7 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -221,7 +221,6 @@ module.exports = class MetamaskController extends EventEmitter { eth_syncing: false, web3_clientVersion: `MetaMask/v${version}`, }, - originHttpHeaderKey: 'X-Metamask-Origin', // account mgmt getAccounts: (cb) => { const isUnlocked = this.keyringController.memStore.getState().isUnlocked -- cgit v1.2.3 From 7c4d8c45624ef840b8806589b47997e7c7c396f3 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 29 Sep 2017 12:20:09 -0230 Subject: Enables the old shapeshift UI within new ui. --- ui/app/app.js | 6 +++++- ui/app/components/shift-list-item.js | 2 +- ui/app/components/tx-list.js | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index 7468551eb..cf9850f9f 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -72,6 +72,7 @@ function mapStateToProps (state) { lastUnreadNotice: state.metamask.lastUnreadNotice, lostAccounts: state.metamask.lostAccounts, frequentRpcList: state.metamask.frequentRpcList || [], + Qr: state.appState.Qr, // state needed to get account dropdown temporarily rendering from app bar identities, @@ -395,7 +396,10 @@ App.prototype.renderPrimary = function () { width: '285px', }, }, [ - h(QrView, {key: 'qr'}), + h(QrView, { + key: 'qr', + Qr: props.Qr, + }), ]), ]) diff --git a/ui/app/components/shift-list-item.js b/ui/app/components/shift-list-item.js index 079f05e31..392b67630 100644 --- a/ui/app/components/shift-list-item.js +++ b/ui/app/components/shift-list-item.js @@ -29,7 +29,7 @@ function ShiftListItem () { ShiftListItem.prototype.render = function () { return ( - h('.transaction-list-item.flex-row', { + h('div.tx-list-item.tx-list-clickable', { style: { paddingTop: '20px', paddingBottom: '20px', diff --git a/ui/app/components/tx-list.js b/ui/app/components/tx-list.js index ef5cfa245..82541704e 100644 --- a/ui/app/components/tx-list.js +++ b/ui/app/components/tx-list.js @@ -5,6 +5,7 @@ const inherits = require('util').inherits const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') const selectors = require('../selectors') const TxListItem = require('./tx-list-item') +const ShiftListItem = require('./shift-list-item') const { formatBalance, formatDate } = require('../util') const { showConfTxPage } = require('../actions') @@ -56,8 +57,9 @@ TxList.prototype.renderTransaction = function () { TxList.prototype.renderTransactionListItem = function (transaction, conversionRate) { // console.log({transaction}) // refer to transaction-list.js:line 58 + const shapeshiftProps = {}; if (transaction.key === 'shapeshift') { - return null + return h(ShiftListItem, transaction) } const props = { -- cgit v1.2.3 From ff64fe98dde7746775396cbf94d63a1a0e91d069 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 29 Sep 2017 13:10:57 -0230 Subject: Shapeshift deposit tx modal. --- ui/app/actions.js | 5 ++- ui/app/app.js | 33 ------------------ .../components/modals/account-modal-container.js | 6 +++- ui/app/components/modals/modal.js | 8 +++++ .../modals/shapeshift-deposit-tx-modal.js | 40 ++++++++++++++++++++++ ui/app/components/tx-list.js | 2 +- ui/app/css/itcss/components/modal.scss | 14 ++++---- 7 files changed, 65 insertions(+), 43 deletions(-) create mode 100644 ui/app/components/modals/shapeshift-deposit-tx-modal.js diff --git a/ui/app/actions.js b/ui/app/actions.js index ff3240abf..1f3726f46 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -1151,7 +1151,10 @@ function reshowQrCode (data, coin) { ] dispatch(actions.hideLoadingIndication()) - return dispatch(actions.showQrView(data, message)) + return dispatch(actions.showModal({ + name: 'SHAPESHIFT_DEPOSIT_TX', + Qr: { data, message }, + })) }) } } diff --git a/ui/app/app.js b/ui/app/app.js index cf9850f9f..583497cb3 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -26,7 +26,6 @@ const InfoScreen = require('./info') const Loading = require('./components/loading') const NetworkIndicator = require('./components/network') const BuyView = require('./components/buy-button-subview') -const QrView = require('./components/qr-code') const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete') const HDRestoreVaultScreen = require('./keychains/hd/restore-vault') const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation') @@ -72,7 +71,6 @@ function mapStateToProps (state) { lastUnreadNotice: state.metamask.lastUnreadNotice, lostAccounts: state.metamask.lostAccounts, frequentRpcList: state.metamask.frequentRpcList || [], - Qr: state.appState.Qr, // state needed to get account dropdown temporarily rendering from app bar identities, @@ -372,37 +370,6 @@ App.prototype.renderPrimary = function () { log.debug('rendering buy ether screen') return h(BuyView, {key: 'buyEthView'}) - case 'qr': - log.debug('rendering show qr screen') - return h('div', { - style: { - position: 'absolute', - height: '100%', - top: '0px', - left: '0px', - }, - }, [ - h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { - onClick: () => props.dispatch(actions.backToAccountDetail(props.activeAddress)), - style: { - marginLeft: '10px', - marginTop: '50px', - }, - }), - h('div', { - style: { - position: 'absolute', - left: '44px', - width: '285px', - }, - }, [ - h(QrView, { - key: 'qr', - Qr: props.Qr, - }), - ]), - ]) - default: log.debug('rendering default, account detail screen') return h(MainContainer, {key: 'account-detail'}) diff --git a/ui/app/components/modals/account-modal-container.js b/ui/app/components/modals/account-modal-container.js index 3cad72067..c548fb7b3 100644 --- a/ui/app/components/modals/account-modal-container.js +++ b/ui/app/components/modals/account-modal-container.js @@ -30,10 +30,14 @@ module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountModalContai AccountModalContainer.prototype.render = function () { const { selectedIdentity, - children, showBackButton = false, backButtonAction, } = this.props + let { children } = this.props + + if (children.constructor !== Array) { + children = [children] + } return h('div', { style: { borderRadius: '4px' }}, [ h('div.account-modal-container', [ diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index 2bd56fb0a..765e46312 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -13,6 +13,7 @@ const AccountDetailsModal = require('./account-details-modal') const EditAccountNameModal = require('./edit-account-name-modal') const ExportPrivateKeyModal = require('./export-private-key-modal') const NewAccountModal = require('./new-account-modal') +const ShapeshiftDepositTxModal = require('./shapeshift-deposit-tx-modal.js') const accountModalStyle = { mobileModalStyle: { @@ -109,6 +110,13 @@ const MODALS = { ...accountModalStyle, }, + SHAPESHIFT_DEPOSIT_TX: { + contents: [ + h(ShapeshiftDepositTxModal), + ], + ...accountModalStyle, + }, + NEW_ACCOUNT: { contents: [ h(NewAccountModal, {}, []), diff --git a/ui/app/components/modals/shapeshift-deposit-tx-modal.js b/ui/app/components/modals/shapeshift-deposit-tx-modal.js new file mode 100644 index 000000000..1fd1ade00 --- /dev/null +++ b/ui/app/components/modals/shapeshift-deposit-tx-modal.js @@ -0,0 +1,40 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const actions = require('../../actions') +const QrView = require('../qr-code') +const AccountModalContainer = require('./account-modal-container') + +function mapStateToProps (state) { + return { + Qr: state.appState.modal.modalState.Qr, + } +} + +function mapDispatchToProps (dispatch) { + return { + hideModal: () => { + dispatch(actions.hideModal()) + }, + } +} + +inherits(ShapeshiftDepositTxModal, Component) +function ShapeshiftDepositTxModal () { + Component.call(this) + +} + +module.exports = connect(mapStateToProps, mapDispatchToProps)(ShapeshiftDepositTxModal) + +ShapeshiftDepositTxModal.prototype.render = function () { + const { Qr } = this.props + + return h(AccountModalContainer, { + }, [ + h('div', {}, [ + h(QrView, {key: 'qr', Qr}), + ]) + ]) +} diff --git a/ui/app/components/tx-list.js b/ui/app/components/tx-list.js index 82541704e..97d937aca 100644 --- a/ui/app/components/tx-list.js +++ b/ui/app/components/tx-list.js @@ -57,7 +57,7 @@ TxList.prototype.renderTransaction = function () { TxList.prototype.renderTransactionListItem = function (transaction, conversionRate) { // console.log({transaction}) // refer to transaction-list.js:line 58 - const shapeshiftProps = {}; + if (transaction.key === 'shapeshift') { return h(ShiftListItem, transaction) } diff --git a/ui/app/css/itcss/components/modal.scss b/ui/app/css/itcss/components/modal.scss index fd61ad4f4..ccfaa7db5 100644 --- a/ui/app/css/itcss/components/modal.scss +++ b/ui/app/css/itcss/components/modal.scss @@ -17,6 +17,13 @@ color: #5B5D67; } +.qr-ellip-address, .ellip-address { + width: 247px; + border: none; + font-family: 'Montserrat Light'; + font-size: 14px; +} + @media screen and (max-width: 575px) { .buy-modal-content-title-wrapper { justify-content: space-around; @@ -250,13 +257,6 @@ width: 286px; } - .qr-ellip-address, .ellip-address { - width: 247px; - border: none; - font-family: 'Montserrat Light'; - font-size: 14px; - } - .btn-clear { min-height: 28px; font-size: 14px; -- cgit v1.2.3 From 47ebcbb2ed09a4cd4b062c5fa4cb6d259369149f Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 29 Sep 2017 07:40:50 -0230 Subject: Token menu ui. --- ui/app/components/dropdowns/token-menu-dropdown.js | 38 +++++++++++++++++ ui/app/components/token-cell.js | 18 +++++++++ ui/app/css/itcss/components/token-list.scss | 47 ++++++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 ui/app/components/dropdowns/token-menu-dropdown.js diff --git a/ui/app/components/dropdowns/token-menu-dropdown.js b/ui/app/components/dropdowns/token-menu-dropdown.js new file mode 100644 index 000000000..b948534c2 --- /dev/null +++ b/ui/app/components/dropdowns/token-menu-dropdown.js @@ -0,0 +1,38 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = TokenMenuDropdown + +inherits(TokenMenuDropdown, Component) +function TokenMenuDropdown () { + Component.call(this) + + this.onClose = this.onClose.bind(this) +} + +TokenMenuDropdown.prototype.onClose = function (e) { + e.stopPropagation() + this.props.onClose() +} + +TokenMenuDropdown.prototype.render = function () { + return h('div.token-menu-dropdown', {}, [ + h('div.token-menu-dropdown__close-area', { + onClick: this.onClose, + }), + h('div.token-menu-dropdown__container', {}, [ + h('div.token-menu-dropdown__options', {}, [ + + h('div.token-menu-dropdown__option', { + onClick: (e) => { + e.stopPropagation() + console.log('div.token-menu-dropdown__option!') + }, + }, 'Hide Token') + + ]), + ]), + ]) +} + diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js index e87d2c859..df73577e9 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/token-cell.js @@ -8,6 +8,8 @@ const selectors = require('../selectors') const actions = require('../actions') const { conversionUtil } = require('../conversion-util') +const TokenMenuDropdown = require('./dropdowns/token-menu-dropdown.js') + function mapStateToProps (state) { return { network: state.metamask.network, @@ -32,6 +34,10 @@ module.exports = connect(mapStateToProps, mapDispatchToProps)(TokenCell) inherits(TokenCell, Component) function TokenCell () { Component.call(this) + + this.state = { + tokenMenuOpen: false, + } } TokenCell.prototype.componentWillMount = function () { @@ -44,6 +50,7 @@ TokenCell.prototype.componentWillMount = function () { } TokenCell.prototype.render = function () { + const { tokenMenuOpen } = this.state const props = this.props const { address, @@ -104,6 +111,17 @@ TokenCell.prototype.render = function () { }, formattedUSD), ]), + h('i.fa.fa-ellipsis-h.fa-lg.token-list-item__ellipsis.cursor-pointer', { + onClick: (e) => { + e.stopPropagation() + this.setState({ tokenMenuOpen: true }) + }, + }), + + tokenMenuOpen && h(TokenMenuDropdown, { + onClose: () => this.setState({ tokenMenuOpen: false }), + }), + /* h('button', { onClick: this.send.bind(this, address), diff --git a/ui/app/css/itcss/components/token-list.scss b/ui/app/css/itcss/components/token-list.scss index e4d6d975b..8ae0eec66 100644 --- a/ui/app/css/itcss/components/token-list.scss +++ b/ui/app/css/itcss/components/token-list.scss @@ -9,6 +9,7 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and ( cursor: pointer; transition: linear 200ms; background-color: rgba($wallet-balance-bg, 0); + position: relative; &__token-balance { font-size: 130%; @@ -44,4 +45,50 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and ( margin-right: 4%; } } + + &__ellipsis { + position: absolute; + top: 20px; + right: 24px; + } } + +.token-menu-dropdown { + height: 55px; + width: 191px; + border-radius: 4px; + background-color: rgba(0,0,0,0.82); + box-shadow: 0 2px 4px 0 rgba(0,0,0,0.5); + position: fixed; + margin-top: 20px; + margin-left: 105px; + + &__close-area { + position: fixed; + top: 0; + left: 0; + z-index: 1000; + width: 100%; + height: 100%; + } + + &__container { + padding: 16px 34px 32px; + z-index: 1050; + position: relative; + } + + &__options { + display: flex; + flex-direction: column; + justify-content: center; + } + + &__option { + color: $white; + font-family: "DIN OT"; + font-size: 16px; + line-height: 21px; + text-align: center; + } +} \ No newline at end of file -- cgit v1.2.3 From db1dd46f8dd93710d7dd88c08bbe05998fdd63c0 Mon Sep 17 00:00:00 2001 From: Branden Soropia Date: Mon, 2 Oct 2017 00:02:30 -0400 Subject: Removed MetaMasktitle. Fixed #1730. --- ui/app/app.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index 50121b055..613577913 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -167,14 +167,6 @@ App.prototype.renderAppBar = function () { }), ]), - // metamask name - props.isUnlocked && h('h1', { - style: { - position: 'relative', - left: '9px', - }, - }, 'MetaMask'), - props.isUnlocked && h('div', { style: { display: 'flex', -- cgit v1.2.3 From 36bc8f3c60ffbcc3789a683a6a74a52e355c0c2b Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Mon, 2 Oct 2017 10:59:15 -0700 Subject: Update manifest.json --- app/manifest.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/manifest.json b/app/manifest.json index 83a967a26..5f8cf6979 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -58,8 +58,7 @@ "storage", "clipboardWrite", "http://localhost:8545/", - "https://*.infura.io/", - "https://api.cryptonator.com" + "https://*.infura.io/" ], "web_accessible_resources": [ "scripts/inpage.js" -- cgit v1.2.3 From e68261130142db52cb3cf0d94633f560a87c0655 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 2 Oct 2017 11:35:26 -0700 Subject: deps - bump pe for block cache fix --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b9a38ac71..918531f15 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "valid-url": "^1.0.9", "vreme": "^3.0.2", "web3": "^0.20.1", - "web3-provider-engine": "^13.2.12", + "web3-provider-engine": "^13.3.1", "web3-stream-provider": "^3.0.1", "xtend": "^4.0.1" }, -- cgit v1.2.3 From d29b5f10ef5137ab56ecc9615e5e894082db9803 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 2 Oct 2017 13:14:42 -0700 Subject: tx state history - fix bug where initial snapshot was mutated on updateTx --- app/scripts/lib/tx-state-history-helper.js | 3 ++- app/scripts/lib/tx-state-manager.js | 2 +- test/unit/tx-state-history-helper.js | 22 ++++++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/scripts/lib/tx-state-history-helper.js b/app/scripts/lib/tx-state-history-helper.js index 304069d57..5ebd78689 100644 --- a/app/scripts/lib/tx-state-history-helper.js +++ b/app/scripts/lib/tx-state-history-helper.js @@ -24,7 +24,8 @@ function generateHistoryEntry(previousState, newState) { return jsonDiffer.compare(previousState, newState) } -function replayHistory(shortHistory) { +function replayHistory(_shortHistory) { + const shortHistory = clone(_shortHistory) return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument) } diff --git a/app/scripts/lib/tx-state-manager.js b/app/scripts/lib/tx-state-manager.js index abb9d7910..4493889bf 100644 --- a/app/scripts/lib/tx-state-manager.js +++ b/app/scripts/lib/tx-state-manager.js @@ -97,7 +97,7 @@ module.exports = class TransactionStateManger extends EventEmitter { const previousState = txStateHistoryHelper.replayHistory(txMeta.history) // generate history entry and add to history const entry = txStateHistoryHelper.generateHistoryEntry(previousState, currentState) - txMeta.history.push(entry) + txMeta.history.push(entry) // commit txMeta to state const txId = txMeta.id diff --git a/test/unit/tx-state-history-helper.js b/test/unit/tx-state-history-helper.js index 5bb6c9bee..e5075af88 100644 --- a/test/unit/tx-state-history-helper.js +++ b/test/unit/tx-state-history-helper.js @@ -20,4 +20,26 @@ describe('tx-state-history-helper', function () { }) }) }) + + it('replaying history does not mutate the original obj', function () { + const initialState = { test: true, message: 'hello', value: 1 } + const diff1 = { + "op": "replace", + "path": "/message", + "value": "haay", + } + const diff2 = { + "op": "replace", + "path": "/value", + "value": 2, + } + const history = [initialState, diff1, diff2] + + const beforeStateSnapshot = JSON.stringify(initialState) + const latestState = txStateHistoryHelper.replayHistory(history) + const afterStateSnapshot = JSON.stringify(initialState) + + assert.notEqual(initialState, latestState, 'initial state is not the same obj as the latest state') + assert.equal(beforeStateSnapshot, afterStateSnapshot, 'initial state is not modified during run') + }) }) -- cgit v1.2.3 From 833da191c37db5b5b470c69a6d4d438ff4719fec Mon Sep 17 00:00:00 2001 From: frankiebee Date: Mon, 2 Oct 2017 13:41:29 -0700 Subject: transaction - provide notes for history --- app/scripts/controllers/transactions.js | 16 +++++++++------- app/scripts/lib/tx-state-manager.js | 10 +++++----- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 9fdec1ead..94e04c429 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -66,13 +66,15 @@ module.exports = class TransactionController extends EventEmitter { this.txStateManager.store.subscribe(() => this.emit('update:badge')) - this.pendingTxTracker.on('tx:warning', this.txStateManager.updateTx.bind(this.txStateManager)) + this.pendingTxTracker.on('tx:warning', (txMeta) => { + this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning') + }) this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager)) this.pendingTxTracker.on('tx:confirmed', this.txStateManager.setTxStatusConfirmed.bind(this.txStateManager)) this.pendingTxTracker.on('tx:retry', (txMeta) => { if (!('retryCount' in txMeta)) txMeta.retryCount = 0 txMeta.retryCount++ - this.txStateManager.updateTx(txMeta) + this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:retry') }) this.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker)) @@ -168,14 +170,14 @@ module.exports = class TransactionController extends EventEmitter { const txParams = txMeta.txParams // ensure value const gasPrice = txParams.gasPrice || await this.query.gasPrice() - txParams.value = txParams.value || '0x0' txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16)) + txParams.value = txParams.value || '0x0' // set gasLimit return await this.txGasUtil.analyzeGasUsage(txMeta) } async updateAndApproveTransaction (txMeta) { - this.txStateManager.updateTx(txMeta) + this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction') await this.approveTransaction(txMeta.id) } @@ -193,7 +195,7 @@ module.exports = class TransactionController extends EventEmitter { txMeta.txParams.nonce = ethUtil.addHexPrefix(nonceLock.nextNonce.toString(16)) // add nonce debugging information to txMeta txMeta.nonceDetails = nonceLock.nonceDetails - this.txStateManager.updateTx(txMeta) + this.txStateManager.updateTx(txMeta, 'transactions#approveTransaction') // sign transaction const rawTx = await this.signTransaction(txId) await this.publishTransaction(txId, rawTx) @@ -224,7 +226,7 @@ module.exports = class TransactionController extends EventEmitter { async publishTransaction (txId, rawTx) { const txMeta = this.txStateManager.getTx(txId) txMeta.rawTx = rawTx - this.txStateManager.updateTx(txMeta) + this.txStateManager.updateTx(txMeta, 'transactions#publishTransaction') const txHash = await this.query.sendRawTransaction(rawTx) this.setTxHash(txId, txHash) this.txStateManager.setTxStatusSubmitted(txId) @@ -239,7 +241,7 @@ module.exports = class TransactionController extends EventEmitter { // Add the tx hash to the persisted meta-tx object const txMeta = this.txStateManager.getTx(txId) txMeta.hash = txHash - this.txStateManager.updateTx(txMeta) + this.txStateManager.updateTx(txMeta, 'transactions#setTxHash') } // diff --git a/app/scripts/lib/tx-state-manager.js b/app/scripts/lib/tx-state-manager.js index abb9d7910..cf8117864 100644 --- a/app/scripts/lib/tx-state-manager.js +++ b/app/scripts/lib/tx-state-manager.js @@ -82,7 +82,7 @@ module.exports = class TransactionStateManger extends EventEmitter { return txMeta } - updateTx (txMeta) { + updateTx (txMeta, note) { if (txMeta.txParams) { Object.keys(txMeta.txParams).forEach((key) => { let value = txMeta.txParams[key] @@ -96,8 +96,8 @@ module.exports = class TransactionStateManger extends EventEmitter { // recover previous tx state obj const previousState = txStateHistoryHelper.replayHistory(txMeta.history) // generate history entry and add to history - const entry = txStateHistoryHelper.generateHistoryEntry(previousState, currentState) - txMeta.history.push(entry) + const entry = txStateHistoryHelper.generateHistoryEntry(previousState, currentState, note) + txMeta.history.push(entry) // commit txMeta to state const txId = txMeta.id @@ -113,7 +113,7 @@ module.exports = class TransactionStateManger extends EventEmitter { updateTxParams (txId, txParams) { const txMeta = this.getTx(txId) txMeta.txParams = extend(txMeta.txParams, txParams) - this.updateTx(txMeta) + this.updateTx(txMeta, `txStateManager#updateTxParams`) } /* @@ -233,7 +233,7 @@ module.exports = class TransactionStateManger extends EventEmitter { if (status === 'submitted' || status === 'rejected') { this.emit(`${txMeta.id}:finished`, txMeta) } - this.updateTx(txMeta) + this.updateTx(txMeta, `txStateManager: setting status to ${status}`) this.emit('update:badge') } -- cgit v1.2.3 From c9c0e6f674c06d30794c73994c7776eb68ebedc0 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 2 Oct 2017 13:43:26 -0700 Subject: tx state history - test - fix format of history entries --- test/unit/tx-state-history-helper.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/unit/tx-state-history-helper.js b/test/unit/tx-state-history-helper.js index 5bb6c9bee..1590f83d0 100644 --- a/test/unit/tx-state-history-helper.js +++ b/test/unit/tx-state-history-helper.js @@ -20,4 +20,30 @@ describe('tx-state-history-helper', function () { }) }) }) +<<<<<<< Updated upstream +======= + + it('replaying history does not mutate the original obj', function () { + const initialState = { test: true, message: 'hello', value: 1 } + const diff1 = [{ + "op": "replace", + "path": "/message", + "value": "haay", + }] + const diff2 = [{ + "op": "replace", + "path": "/value", + "value": 2, + }] + const history = [initialState, diff1, diff2] + + const beforeStateSnapshot = JSON.stringify(initialState) + const latestState = txStateHistoryHelper.replayHistory(history) + const afterStateSnapshot = JSON.stringify(initialState) + + assert.notEqual(initialState, latestState, 'initial state is not the same obj as the latest state') + assert.equal(beforeStateSnapshot, afterStateSnapshot, 'initial state is not modified during run') + }) + +>>>>>>> Stashed changes }) -- cgit v1.2.3 From df59ef9942a950a1c5437c69b1af4111f8c07817 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 2 Oct 2017 13:44:11 -0700 Subject: tx state history - append note to first op of diff --- app/scripts/lib/tx-state-history-helper.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/scripts/lib/tx-state-history-helper.js b/app/scripts/lib/tx-state-history-helper.js index 304069d57..ecf0d076e 100644 --- a/app/scripts/lib/tx-state-history-helper.js +++ b/app/scripts/lib/tx-state-history-helper.js @@ -20,8 +20,11 @@ function migrateFromSnapshotsToDiffs(longHistory) { ) } -function generateHistoryEntry(previousState, newState) { - return jsonDiffer.compare(previousState, newState) +function generateHistoryEntry(previousState, newState, note) { + const entry = jsonDiffer.compare(previousState, newState) + // Add a note to the first op, since it breaks if we append it to the entry + if (note && entry[0]) entry[0].note = note + return entry } function replayHistory(shortHistory) { -- cgit v1.2.3 From 7af696bfbe721db74efad91ca916b18198070e76 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 2 Oct 2017 14:56:59 -0700 Subject: pending tx tracker - dont throw on load failure --- app/scripts/lib/pending-tx-tracker.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js index 3d358b00e..48d1e1d06 100644 --- a/app/scripts/lib/pending-tx-tracker.js +++ b/app/scripts/lib/pending-tx-tracker.js @@ -137,7 +137,6 @@ module.exports = class PendingTransactionTracker extends EventEmitter { message: 'There was a problem loading this transaction.', } this.emit('tx:warning', txMeta) - throw err } } -- cgit v1.2.3 From 22eaf92ec2c948ed88df30bc3a3b26f140359f09 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 2 Oct 2017 15:00:23 -0700 Subject: pending tx tracker - resubmit - warn dont error on unknown error --- app/scripts/lib/pending-tx-tracker.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js index 48d1e1d06..dcaa1d716 100644 --- a/app/scripts/lib/pending-tx-tracker.js +++ b/app/scripts/lib/pending-tx-tracker.js @@ -86,12 +86,15 @@ module.exports = class PendingTransactionTracker extends EventEmitter { // other || errorMessage.includes('gateway timeout') || errorMessage.includes('nonce too low') - || txMeta.retryCount > 1 ) // ignore resubmit warnings, return early if (isKnownTx) return // encountered real error - transition to error state - this.emit('tx:failed', txMeta.id, err) + txMeta.warning = { + error: errorMessage, + message: 'There was an error when resubmitting this transaction.', + } + this.emit('tx:warning', txMeta) })) } -- cgit v1.2.3 From a86f6d6d90bda273861079b464c290dff6ef5c0e Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 2 Oct 2017 15:14:15 -0700 Subject: pending tx tracker - test - rename tests to match event name --- test/unit/pending-tx-test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/unit/pending-tx-test.js b/test/unit/pending-tx-test.js index 4da0eff5d..097564033 100644 --- a/test/unit/pending-tx-test.js +++ b/test/unit/pending-tx-test.js @@ -57,7 +57,7 @@ describe('PendingTransactionTracker', function () { const block = Proxy.revocable({}, {}).revoke() pendingTxTracker.checkForTxInBlock(block) }) - it('should emit \'txFailed\' if the txMeta does not have a hash', function (done) { + it('should emit \'tx:failed\' if the txMeta does not have a hash', function (done) { const block = Proxy.revocable({}, {}).revoke() pendingTxTracker.getPendingTransactions = () => [txMetaNoHash] pendingTxTracker.once('tx:failed', (txId, err) => { @@ -105,7 +105,7 @@ describe('PendingTransactionTracker', function () { }) describe('#_checkPendingTx', function () { - it('should emit \'txFailed\' if the txMeta does not have a hash', function (done) { + it('should emit \'tx:failed\' if the txMeta does not have a hash', function (done) { pendingTxTracker.once('tx:failed', (txId, err) => { assert(txId, txMetaNoHash.id, 'should pass txId') done() @@ -172,7 +172,7 @@ describe('PendingTransactionTracker', function () { .catch(done) pendingTxTracker.resubmitPendingTxs() }) - it('should not emit \'txFailed\' if the txMeta throws a known txError', function (done) { + it('should not emit \'tx:failed\' if the txMeta throws a known txError', function (done) { knownErrors =[ // geth ' Replacement transaction Underpriced ', @@ -199,7 +199,7 @@ describe('PendingTransactionTracker', function () { pendingTxTracker.resubmitPendingTxs() }) - it('should emit \'txFailed\' if it encountered a real error', function (done) { + it('should emit \'tx:failed\' if it encountered a real error', function (done) { pendingTxTracker.once('tx:failed', (id, err) => err.message === 'im some real error' ? txList[id - 1].resolve() : done(err)) pendingTxTracker.getPendingTransactions = () => txList -- cgit v1.2.3 From ed77304e73e91f252e6e0a3f682569ad17a0d52d Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 2 Oct 2017 15:20:01 -0700 Subject: pending tx tracker - tx:warning event includes err obj --- app/scripts/lib/pending-tx-tracker.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js index dcaa1d716..474c4d5a2 100644 --- a/app/scripts/lib/pending-tx-tracker.js +++ b/app/scripts/lib/pending-tx-tracker.js @@ -94,7 +94,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter { error: errorMessage, message: 'There was an error when resubmitting this transaction.', } - this.emit('tx:warning', txMeta) + this.emit('tx:warning', txMeta, err) })) } @@ -139,7 +139,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter { error: err, message: 'There was a problem loading this transaction.', } - this.emit('tx:warning', txMeta) + this.emit('tx:warning', txMeta, err) } } -- cgit v1.2.3 From 25a80932a64f718ba1a39ab399b17395f0fd5d88 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 2 Oct 2017 15:20:18 -0700 Subject: pending tx tracker - test - expect warning event on resubmit failure --- test/unit/pending-tx-test.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test/unit/pending-tx-test.js b/test/unit/pending-tx-test.js index 097564033..6b62bb5b1 100644 --- a/test/unit/pending-tx-test.js +++ b/test/unit/pending-tx-test.js @@ -199,8 +199,15 @@ describe('PendingTransactionTracker', function () { pendingTxTracker.resubmitPendingTxs() }) - it('should emit \'tx:failed\' if it encountered a real error', function (done) { - pendingTxTracker.once('tx:failed', (id, err) => err.message === 'im some real error' ? txList[id - 1].resolve() : done(err)) + it('should emit \'tx:warning\' if it encountered a real error', function (done) { + pendingTxTracker.once('tx:warning', (txMeta, err) => { + if (err.message === 'im some real error') { + const matchingTx = txList.find(tx => tx.id === txMeta.id) + matchingTx.resolve() + } else { + done(err) + } + }) pendingTxTracker.getPendingTransactions = () => txList pendingTxTracker._resubmitTx = async (tx) => { throw new TypeError('im some real error') } -- cgit v1.2.3 From 062eaa6a82f6730eb180c1a1fb61f035eb123451 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 2 Oct 2017 15:39:11 -0700 Subject: pending tx tracker - on tx:warn append error message instead of error obj --- app/scripts/lib/pending-tx-tracker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js index 474c4d5a2..6f1601586 100644 --- a/app/scripts/lib/pending-tx-tracker.js +++ b/app/scripts/lib/pending-tx-tracker.js @@ -136,7 +136,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter { } } catch (err) { txMeta.warning = { - error: err, + error: err.message, message: 'There was a problem loading this transaction.', } this.emit('tx:warning', txMeta, err) -- cgit v1.2.3 From 2113d8348969f4da3c61ddf1cee4aa38f7a5958a Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 2 Oct 2017 15:39:44 -0700 Subject: ui - tx history - simplify error+warning display code --- ui/app/components/transaction-list-item.js | 34 +++++++++++++++++++----------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js index 0e5c0b5a3..a9961f47c 100644 --- a/ui/app/components/transaction-list-item.js +++ b/ui/app/components/transaction-list-item.js @@ -133,7 +133,7 @@ function recipientField (txParams, transaction, isTx, isMsg) { }, }, [ message, - failIfFailed(transaction), + renderErrorOrWarning(transaction), ]) } @@ -141,25 +141,35 @@ function formatDate (date) { return vreme.format(new Date(date), 'March 16 2014 14:30') } -function failIfFailed (transaction) { - if (transaction.status === 'rejected') { +function renderErrorOrWarning (transaction) { + const { status, err, warning } = transaction + + // show rejected + if (status === 'rejected') { return h('span.error', ' (Rejected)') } - if (transaction.err || transaction.warning) { - const { err, warning = {} } = transaction - const errFirst = !!(( err && warning ) || err) - const message = errFirst ? err.message : warning.message - - errFirst ? err.message : warning.message + // show error + if (err) { + const message = err.message || '' + return ( + h(Tooltip, { + title: message, + position: 'bottom', + }, [ + h(`span.error`, ` (Failed)`), + ]) + ) + } + // show warning + if (warning) { + const message = warning.message return h(Tooltip, { title: message, position: 'bottom', }, [ - h(`span.${errFirst ? 'error' : 'warning'}`, - ` (${errFirst ? 'Failed' : 'Warning'})` - ), + h(`span.warning`, ` (Warning)`), ]) } } -- cgit v1.2.3 From d206f183f5a07787535acd196c506145f00a199e Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 29 Sep 2017 16:33:29 -0230 Subject: Hide token confirmation modal ui --- ui/app/components/dropdowns/token-menu-dropdown.js | 18 +++++- .../modals/hide-token-confirmation-modal.js | 66 +++++++++++++++++++ ui/app/components/modals/modal.js | 15 +++++ ui/app/components/token-cell.js | 1 + ui/app/css/itcss/components/modal.scss | 73 ++++++++++++++++++++++ 5 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 ui/app/components/modals/hide-token-confirmation-modal.js diff --git a/ui/app/components/dropdowns/token-menu-dropdown.js b/ui/app/components/dropdowns/token-menu-dropdown.js index b948534c2..0f4bc2b87 100644 --- a/ui/app/components/dropdowns/token-menu-dropdown.js +++ b/ui/app/components/dropdowns/token-menu-dropdown.js @@ -1,8 +1,19 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits +const connect = require('react-redux').connect +const actions = require('../../actions') + +module.exports = connect(null, mapDispatchToProps)(TokenMenuDropdown) + +function mapDispatchToProps (dispatch) { + return { + showHideTokenConfirmationModal: (token) => { + dispatch(actions.showModal({ name: 'HIDE_TOKEN_CONFIRMATION', token })) + } + } +} -module.exports = TokenMenuDropdown inherits(TokenMenuDropdown, Component) function TokenMenuDropdown () { @@ -17,6 +28,8 @@ TokenMenuDropdown.prototype.onClose = function (e) { } TokenMenuDropdown.prototype.render = function () { + const { showHideTokenConfirmationModal } = this.props + return h('div.token-menu-dropdown', {}, [ h('div.token-menu-dropdown__close-area', { onClick: this.onClose, @@ -27,7 +40,7 @@ TokenMenuDropdown.prototype.render = function () { h('div.token-menu-dropdown__option', { onClick: (e) => { e.stopPropagation() - console.log('div.token-menu-dropdown__option!') + showHideTokenConfirmationModal(this.props.token) }, }, 'Hide Token') @@ -35,4 +48,3 @@ TokenMenuDropdown.prototype.render = function () { ]), ]) } - diff --git a/ui/app/components/modals/hide-token-confirmation-modal.js b/ui/app/components/modals/hide-token-confirmation-modal.js new file mode 100644 index 000000000..d3f06b483 --- /dev/null +++ b/ui/app/components/modals/hide-token-confirmation-modal.js @@ -0,0 +1,66 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const actions = require('../../actions') +const Identicon = require('../identicon') + +function mapStateToProps (state) { + return { + network: state.metamask.network, + token: state.appState.modal.modalState.token, + } +} + +function mapDispatchToProps (dispatch) { + return {} +} + +inherits(HideTokenConfirmationModal, Component) +function HideTokenConfirmationModal () { + Component.call(this) + + this.state = {} +} + +module.exports = connect(mapStateToProps, mapDispatchToProps)(HideTokenConfirmationModal) + +HideTokenConfirmationModal.prototype.render = function () { + const { token, network } = this.props + const { symbol, address } = token + + return h('div.hide-token-confirmation', {}, [ + h('div.hide-token-confirmation__container', { + }, [ + h('div.hide-token-confirmation__title', {}, [ + 'Hide Token?', + ]), + + h(Identicon, { + className: 'hide-token-confirmation__identicon', + diameter: 45, + address, + network, + }), + + h('div.hide-token-confirmation__symbol', {}, symbol), + + h('div.hide-token-confirmation__copy', {}, [ + 'You can add this token back in the future by going go to “Add token” in your accounts options menu.', + ]), + + h('div.hide-token-confirmation__buttons', {}, [ + h('button.btn-clear', { + onClick: () => {}, + }, [ + 'CANCEL', + ]), + h('button.btn-clear', { + onClick: () => {}, + }, [ + 'HIDE', + ]), + ]), + ]), + ]) +} diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index 765e46312..7247d840e 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -14,6 +14,7 @@ const EditAccountNameModal = require('./edit-account-name-modal') const ExportPrivateKeyModal = require('./export-private-key-modal') const NewAccountModal = require('./new-account-modal') const ShapeshiftDepositTxModal = require('./shapeshift-deposit-tx-modal.js') +const HideTokenConfirmationModal = require('./hide-token-confirmation-modal') const accountModalStyle = { mobileModalStyle: { @@ -117,6 +118,20 @@ const MODALS = { ...accountModalStyle, }, + HIDE_TOKEN_CONFIRMATION: { + contents: [ + h(HideTokenConfirmationModal, {}, []), + ], + mobileModalStyle: { + width: '95%', + top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh', + }, + laptopModalStyle: { + width: '449px', + top: 'calc(33% + 45px)', + }, + }, + NEW_ACCOUNT: { contents: [ h(NewAccountModal, {}, []), diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js index df73577e9..ad431df69 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/token-cell.js @@ -120,6 +120,7 @@ TokenCell.prototype.render = function () { tokenMenuOpen && h(TokenMenuDropdown, { onClose: () => this.setState({ tokenMenuOpen: false }), + token: { symbol, address }, }), /* diff --git a/ui/app/css/itcss/components/modal.scss b/ui/app/css/itcss/components/modal.scss index ccfaa7db5..aa18ed37d 100644 --- a/ui/app/css/itcss/components/modal.scss +++ b/ui/app/css/itcss/components/modal.scss @@ -483,3 +483,76 @@ color: $tundora; flex: 1; } + +// Hide token confirmation + +.hide-token-confirmation { + min-height: 250.72px; + width: 374.49px; + border-radius: 4px; + background-color: #FFFFFF; + box-shadow: 0 1px 7px 0 rgba(0,0,0,0.5); + + &__container { + padding: 24px 27px 21px; + display: flex; + flex-direction: column; + align-items: center; + } + + &__identicon { + margin-bottom: 10px + } + + &__symbol { + color: $tundora; + font-family: 'DIN OT'; + font-size: 16px; + line-height: 24px; + text-align: center; + margin-bottom: 7.5px; + } + + &__title { + height: 30px; + width: 271.28px; + color: $tundora; + font-family: 'DIN OT'; + font-size: 22px; + line-height: 30px; + text-align: center; + margin-bottom: 10.5px; + } + + &__copy { + height: 41px; + width: 318px; + color: $scorpion; + font-family: 'DIN OT'; + font-size: 14px; + line-height: 18px; + text-align: center; + } + + &__buttons { + display: flex; + flex-direction: row; + justify-content: center; + margin-top: 15px; + width: 100%; + + button { + height: 44px; + width: 113px; + border: 1px solid $scorpion; + border-radius: 2px; + color: $tundora; + font-family: 'DIN OT'; + font-size: 14px; + line-height: 20px; + text-align: center; + margin-left: 4px; + margin-right: 4px; + } + } +} -- cgit v1.2.3 From 45dbd017e65e5698db4580c77d723bface0e9b63 Mon Sep 17 00:00:00 2001 From: Dan Date: Mon, 2 Oct 2017 21:58:15 -0230 Subject: Add needed iterator in tx-list and path to account in selectors. --- ui/app/components/tx-list.js | 7 ++++++- ui/app/selectors.js | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/ui/app/components/tx-list.js b/ui/app/components/tx-list.js index 97d937aca..137cccf37 100644 --- a/ui/app/components/tx-list.js +++ b/ui/app/components/tx-list.js @@ -50,7 +50,11 @@ TxList.prototype.renderTransaction = function () { const { txsToRender, conversionRate } = this.props return txsToRender.length ? txsToRender.map((transaction, i) => this.renderTransactionListItem(transaction, conversionRate)) - : [h('div.tx-list-item.tx-list-item--empty', [ 'No Transactions' ])] + : [h( + 'div.tx-list-item.tx-list-item--empty', + { key: 'tx-list-none' }, + [ 'No Transactions' ], + )] } // TODO: Consider moving TxListItem into a separate component @@ -88,6 +92,7 @@ TxList.prototype.renderTransactionListItem = function (transaction, conversionRa txParams: transaction.txParams, transactionStatus, transActionId, + key: transActionId, dateString, address, transactionAmount, diff --git a/ui/app/selectors.js b/ui/app/selectors.js index 9a8bf5c7e..fdbc5fcde 100644 --- a/ui/app/selectors.js +++ b/ui/app/selectors.js @@ -13,7 +13,7 @@ module.exports = selectors function getSelectedAddress (state) { // TODO: accounts is not defined. Is it needed? - const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] + const selectedAddress = state.metamask.selectedAddress || Object.keys(state.metamask.accounts)[0] return selectedAddress } -- cgit v1.2.3 From 98641f6f6617f9c59775cb3e8ed955f154a3b8c9 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 3 Oct 2017 04:33:04 +0000 Subject: chore(package): update mocha to version 4.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 918531f15..685d98403 100644 --- a/package.json +++ b/package.json @@ -186,7 +186,7 @@ "karma-firefox-launcher": "^1.0.1", "karma-qunit": "^1.2.1", "lodash.assign": "^4.0.6", - "mocha": "^3.4.2", + "mocha": "^4.0.0", "mocha-eslint": "^4.0.0", "mocha-jsdom": "^1.1.0", "mocha-sinon": "^2.0.0", -- cgit v1.2.3 From ac4868170f4c61d13291389d01bf1002fe240ed4 Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 3 Oct 2017 14:55:52 -0230 Subject: Enables remove token and ensures add/remove update the list without need for refresh. --- app/scripts/controllers/preferences.js | 13 +++++++- app/scripts/metamask-controller.js | 1 + ui/app/actions.js | 35 ++++++++++++++++++---- ui/app/components/dropdowns/token-menu-dropdown.js | 1 + .../modals/hide-token-confirmation-modal.js | 16 +++++++--- ui/app/components/token-list.js | 14 ++++++--- ui/app/reducers/metamask.js | 6 ++++ 7 files changed, 72 insertions(+), 14 deletions(-) diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index bc4848421..ecac40481 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -42,7 +42,18 @@ class PreferencesController { } this.store.updateState({ tokens }) - return Promise.resolve() + return Promise.resolve(tokens) + } + + removeToken (rawAddress) { + const address = normalizeAddress(rawAddress) + + const tokens = this.store.getState().tokens + + const updatedTokens = tokens.filter(token => token.address !== rawAddress) + + this.store.updateState({ tokens: updatedTokens }) + return Promise.resolve(updatedTokens) } getTokens () { diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 5b3161bc6..b5c81c348 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -327,6 +327,7 @@ module.exports = class MetamaskController extends EventEmitter { // PreferencesController setSelectedAddress: nodeify(preferencesController.setSelectedAddress, preferencesController), addToken: nodeify(preferencesController.addToken, preferencesController), + removeToken: nodeify(preferencesController.removeToken, preferencesController), setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController), setDefaultRpc: nodeify(this.setDefaultRpc, this), setCustomRpc: nodeify(this.setCustomRpc, this), diff --git a/ui/app/actions.js b/ui/app/actions.js index 1f3726f46..630b6390c 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -152,6 +152,9 @@ var actions = { showAddTokenPage, addToken, addTokens, + removeToken, + updateTokens, + UPDATE_TOKENS: 'UPDATE_TOKENS', setRpcTarget: setRpcTarget, setDefaultRpcTarget: setDefaultRpcTarget, setProviderType: setProviderType, @@ -753,16 +756,31 @@ function addToken (address, symbol, decimals) { return (dispatch) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { - background.addToken(address, symbol, decimals, (err) => { + background.addToken(address, symbol, decimals, (err, tokens) => { dispatch(actions.hideLoadingIndication()) if (err) { dispatch(actions.displayWarning(err.message)) reject(err) } - resolve() - // setTimeout(() => { - // dispatch(actions.goHome()) - // }, 250) + dispatch(actions.updateTokens(tokens)) + resolve(tokens) + }) + }) + } +} + +function removeToken (address) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + return new Promise((resolve, reject) => { + background.removeToken(address, (err, tokens) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + dispatch(actions.displayWarning(err.message)) + reject(err) + } + dispatch(actions.updateTokens(tokens)) + resolve(tokens) }) }) } @@ -786,6 +804,13 @@ function addTokens (tokens) { } } +function updateTokens(newTokens) { + return { + type: actions.UPDATE_TOKENS, + newTokens + } +} + function goBackToInitView () { return { type: actions.BACK_TO_INIT_MENU, diff --git a/ui/app/components/dropdowns/token-menu-dropdown.js b/ui/app/components/dropdowns/token-menu-dropdown.js index 0f4bc2b87..7234a9b21 100644 --- a/ui/app/components/dropdowns/token-menu-dropdown.js +++ b/ui/app/components/dropdowns/token-menu-dropdown.js @@ -41,6 +41,7 @@ TokenMenuDropdown.prototype.render = function () { onClick: (e) => { e.stopPropagation() showHideTokenConfirmationModal(this.props.token) + this.props.onClose() }, }, 'Hide Token') diff --git a/ui/app/components/modals/hide-token-confirmation-modal.js b/ui/app/components/modals/hide-token-confirmation-modal.js index d3f06b483..fa3ad0b1e 100644 --- a/ui/app/components/modals/hide-token-confirmation-modal.js +++ b/ui/app/components/modals/hide-token-confirmation-modal.js @@ -13,7 +13,15 @@ function mapStateToProps (state) { } function mapDispatchToProps (dispatch) { - return {} + return { + hideModal: () => dispatch(actions.hideModal()), + hideToken: address => { + dispatch(actions.removeToken(address)) + .then(() => { + dispatch(actions.hideModal()) + }) + }, + } } inherits(HideTokenConfirmationModal, Component) @@ -26,7 +34,7 @@ function HideTokenConfirmationModal () { module.exports = connect(mapStateToProps, mapDispatchToProps)(HideTokenConfirmationModal) HideTokenConfirmationModal.prototype.render = function () { - const { token, network } = this.props + const { token, network, hideToken, hideModal } = this.props const { symbol, address } = token return h('div.hide-token-confirmation', {}, [ @@ -51,12 +59,12 @@ HideTokenConfirmationModal.prototype.render = function () { h('div.hide-token-confirmation__buttons', {}, [ h('button.btn-clear', { - onClick: () => {}, + onClick: () => hideModal(), }, [ 'CANCEL', ]), h('button.btn-clear', { - onClick: () => {}, + onClick: () => hideToken(address), }, [ 'HIDE', ]), diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index 0efa89c63..fb11be826 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -27,7 +27,6 @@ for (const address in contracts) { module.exports = connect(mapStateToProps)(TokenList) - inherits(TokenList, Component) function TokenList () { this.state = { @@ -129,15 +128,22 @@ TokenList.prototype.componentDidUpdate = function (nextProps) { const { network: oldNet, userAddress: oldAddress, + tokens, } = this.props const { network: newNet, userAddress: newAddress, + tokens: newTokens, } = nextProps - if (newNet === 'loading') return - if (!oldNet || !newNet || !oldAddress || !newAddress) return - if (oldAddress === newAddress && oldNet === newNet) return + const isLoading = newNet === 'loading' + const missingInfo = !oldNet || !newNet || !oldAddress || !newAddress + const sameUserAndNetwork = oldAddress === newAddress && oldNet === newNet + const shouldUpdateTokens = isLoading || missingInfo || sameUserAndNetwork + + const tokensLengthUnchanged = tokens.length === newTokens.length + + if (tokensLengthUnchanged && shouldUpdateTokens) return this.setState({ isLoading: true }) this.createFreshTokenTracker() diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index cdc98d05e..a0884b834 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -19,6 +19,7 @@ function reduceMetamask (state, action) { addressBook: [], selectedTokenAddress: null, tokenExchangeRates: {}, + tokens: [], }, state.metamask) switch (action.type) { @@ -146,6 +147,11 @@ function reduceMetamask (state, action) { }, }) + case actions.UPDATE_TOKENS: + return extend(metamaskState, { + tokens: action.newTokens, + }) + default: return metamaskState -- cgit v1.2.3 From 147b81068a643074a1fd7aa3e0488263a64961ad Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 4 Oct 2017 09:56:18 -0700 Subject: Include OS version --- app/scripts/platforms/extension.js | 4 ++++ ui/app/reducers.js | 2 ++ 2 files changed, 6 insertions(+) diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index 0afe04b74..83c77a77f 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -17,6 +17,10 @@ class ExtensionPlatform { return extension.runtime.getManifest().version } + getPlatformInfo () { + return extension.runtime.getPlatformInfo() + } + } module.exports = ExtensionPlatform diff --git a/ui/app/reducers.js b/ui/app/reducers.js index 6a2f44534..99cbda8aa 100644 --- a/ui/app/reducers.js +++ b/ui/app/reducers.js @@ -44,7 +44,9 @@ function rootReducer (state, action) { window.logState = function () { let state = window.METAMASK_CACHED_LOG_STATE const version = global.platform.getVersion() + const platform = global.platform.getPlatformInfo() state.version = version + state.platform = platform let stateString = JSON.stringify(state, removeSeedWords, 2) return stateString } -- cgit v1.2.3 From 2f135c2e68ae58e009212be862bf2318b55a1180 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 4 Oct 2017 09:56:40 -0700 Subject: Include browser version --- ui/app/reducers.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/app/reducers.js b/ui/app/reducers.js index 99cbda8aa..6307998c2 100644 --- a/ui/app/reducers.js +++ b/ui/app/reducers.js @@ -44,9 +44,11 @@ function rootReducer (state, action) { window.logState = function () { let state = window.METAMASK_CACHED_LOG_STATE const version = global.platform.getVersion() + const browser = global.navigator.userAgent() const platform = global.platform.getPlatformInfo() state.version = version state.platform = platform + state.browser = browser let stateString = JSON.stringify(state, removeSeedWords, 2) return stateString } -- cgit v1.2.3 From e64c64a0492aba377db6ef2c406bdb0bd1d10469 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 4 Oct 2017 10:00:07 -0700 Subject: change global to window --- ui/app/reducers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/reducers.js b/ui/app/reducers.js index 6307998c2..1224b4e92 100644 --- a/ui/app/reducers.js +++ b/ui/app/reducers.js @@ -44,7 +44,7 @@ function rootReducer (state, action) { window.logState = function () { let state = window.METAMASK_CACHED_LOG_STATE const version = global.platform.getVersion() - const browser = global.navigator.userAgent() + const browser = window.navigator.userAgent() const platform = global.platform.getPlatformInfo() state.version = version state.platform = platform -- cgit v1.2.3 From 3d80565339f02e1fa3e4bc0f8aaedbeea663a712 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 4 Oct 2017 10:55:10 -0700 Subject: Configured for callback-required function.' --- app/scripts/platforms/extension.js | 4 ++-- ui/app/config.js | 4 +++- ui/app/reducers.js | 17 +++++++++-------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index 83c77a77f..5ed2fdc4f 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -17,8 +17,8 @@ class ExtensionPlatform { return extension.runtime.getManifest().version } - getPlatformInfo () { - return extension.runtime.getPlatformInfo() + getPlatformInfo (cb) { + return extension.runtime.getPlatformInfo(cb) } } diff --git a/ui/app/config.js b/ui/app/config.js index 0fe232c07..9ba00b3dd 100644 --- a/ui/app/config.js +++ b/ui/app/config.js @@ -113,7 +113,9 @@ ConfigScreen.prototype.render = function () { alignSelf: 'center', }, onClick (event) { - exportAsFile('MetaMask State Logs', window.logState()) + window.logState((result) => { + exportAsFile('MetaMask State Logs', result) + }) }, }, 'Download State Logs'), ]), diff --git a/ui/app/reducers.js b/ui/app/reducers.js index 1224b4e92..385cafdfa 100644 --- a/ui/app/reducers.js +++ b/ui/app/reducers.js @@ -41,16 +41,17 @@ function rootReducer (state, action) { return state } -window.logState = function () { +window.logState = function (cb) { let state = window.METAMASK_CACHED_LOG_STATE const version = global.platform.getVersion() - const browser = window.navigator.userAgent() - const platform = global.platform.getPlatformInfo() - state.version = version - state.platform = platform - state.browser = browser - let stateString = JSON.stringify(state, removeSeedWords, 2) - return stateString + const browser = window.navigator.userAgent + return global.platform.getPlatformInfo((platform) => { + state.version = version + state.platform = platform + state.browser = browser + let stateString = JSON.stringify(state, removeSeedWords, 2) + return cb(stateString) + }) } function removeSeedWords (key, value) { -- cgit v1.2.3 From b158d7fea03a34cc48677d272b2a411b6bbe67ab Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 4 Oct 2017 11:00:52 -0700 Subject: Rename to maintain API --- ui/app/config.js | 2 +- ui/app/reducers.js | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ui/app/config.js b/ui/app/config.js index 9ba00b3dd..75c3bcf13 100644 --- a/ui/app/config.js +++ b/ui/app/config.js @@ -113,7 +113,7 @@ ConfigScreen.prototype.render = function () { alignSelf: 'center', }, onClick (event) { - window.logState((result) => { + window.logStateString((result) => { exportAsFile('MetaMask State Logs', result) }) }, diff --git a/ui/app/reducers.js b/ui/app/reducers.js index 385cafdfa..8ffd572c0 100644 --- a/ui/app/reducers.js +++ b/ui/app/reducers.js @@ -41,7 +41,7 @@ function rootReducer (state, action) { return state } -window.logState = function (cb) { +window.logStateString = function (cb) { let state = window.METAMASK_CACHED_LOG_STATE const version = global.platform.getVersion() const browser = window.navigator.userAgent @@ -54,6 +54,12 @@ window.logState = function (cb) { }) } +window.logState() = function () { + return window.logStateString((result) => { + return result + }) +} + function removeSeedWords (key, value) { return key === 'seedWords' ? undefined : value } -- cgit v1.2.3 From 52aee7aa9ed468186c3125d79d910df3a30dfed3 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 4 Oct 2017 11:07:16 -0700 Subject: Further adjustment to maintain API --- ui/app/reducers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/app/reducers.js b/ui/app/reducers.js index 8ffd572c0..e7669b932 100644 --- a/ui/app/reducers.js +++ b/ui/app/reducers.js @@ -54,9 +54,9 @@ window.logStateString = function (cb) { }) } -window.logState() = function () { +window.logState = function () { return window.logStateString((result) => { - return result + console.log(result) }) } -- cgit v1.2.3 From bb9c2b3563938832e4b35b6623ef724017093fcb Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 4 Oct 2017 11:14:30 -0700 Subject: lint --- ui/app/reducers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/reducers.js b/ui/app/reducers.js index e7669b932..0af7ee81c 100644 --- a/ui/app/reducers.js +++ b/ui/app/reducers.js @@ -48,7 +48,7 @@ window.logStateString = function (cb) { return global.platform.getPlatformInfo((platform) => { state.version = version state.platform = platform - state.browser = browser + state.browser = browser let stateString = JSON.stringify(state, removeSeedWords, 2) return cb(stateString) }) -- cgit v1.2.3 From 27c72ee565f02e319af2a4e2c03a885ae633ae71 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Wed, 4 Oct 2017 12:10:01 -0700 Subject: Revert to normal balances. --- ui/app/account-detail.js | 4 ++-- ui/app/components/pending-tx.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index 90724dc3f..a844daf88 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -46,7 +46,7 @@ AccountDetailScreen.prototype.render = function () { var selected = props.address || Object.keys(props.accounts)[0] var checksumAddress = selected && ethUtil.toChecksumAddress(selected) var identity = props.identities[selected] - var account = props.computedBalances[selected] + var account = props.accounts[selected] const { network, conversionRate, currentCurrency } = props return ( @@ -181,7 +181,7 @@ AccountDetailScreen.prototype.render = function () { }, [ h(EthBalance, { - value: account && account.ethBalance, + value: account && account.balance, conversionRate, currentCurrency, style: { diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 6f8c19a3c..c3350fcc1 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -33,7 +33,7 @@ function PendingTx () { PendingTx.prototype.render = function () { const props = this.props - const { currentCurrency, blockGasLimit, computedBalances } = props + const { currentCurrency, blockGasLimit } = props const conversionRate = props.conversionRate const txMeta = this.gatherTxMeta() @@ -42,8 +42,8 @@ PendingTx.prototype.render = function () { // Account Details const address = txParams.from || props.selectedAddress const identity = props.identities[address] || { address: address } - const account = computedBalances[address] - const balance = account ? account.ethBalance : '0x0' + const account = props.accounts[address] + const balance = account ? account.balance : '0x0' // recipient check const isValidAddress = !txParams.to || util.isValidAddress(txParams.to) -- cgit v1.2.3 From 23bc92a8f11bd6e3ed3f5e87b36654d178177208 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 4 Oct 2017 13:33:50 -0700 Subject: deps - bump eth-json-rpc-filters for log filter fix --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 30079e1d5..03d095228 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "eth-block-tracker": "^2.2.0", "eth-contract-metadata": "^1.1.4", "eth-hd-keyring": "^1.1.1", - "eth-json-rpc-filters": "^1.2.1", + "eth-json-rpc-filters": "^1.2.2", "eth-keyring-controller": "^2.0.0", "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", -- cgit v1.2.3 From 1cba6543a42561c6691736d58f45e97f4832912b Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 4 Oct 2017 15:35:04 -0700 Subject: Begin implementing sync injection idea --- app/scripts/contentscript.js | 3 +-- gulpfile.js | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index b4708189e..59e7f08ce 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -7,7 +7,7 @@ const ObjectMultiplex = require('obj-multiplex') const extension = require('extensionizer') const PortStream = require('./lib/port-stream.js') -const inpageText = fs.readFileSync(path.join(__dirname, 'inpage.js')).toString() +const inpageText = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'chrome', 'scripts', 'inpage.js')).toString() + '//# sourceURL=' + extension.extension.getURL('scripts/inpage.js') + '\n' // Eventually this streaming injection could be replaced with: // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction @@ -25,7 +25,6 @@ function setupInjection () { try { // inject in-page script var scriptTag = document.createElement('script') - scriptTag.src = extension.extension.getURL('scripts/inpage.js') scriptTag.textContent = inpageText scriptTag.onload = function () { this.parentNode.removeChild(this) } var container = document.head || document.documentElement diff --git a/gulpfile.js b/gulpfile.js index ac36cf983..14e26ed2e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -186,8 +186,8 @@ jsFiles.forEach((jsFile) => { gulp.task(`build:js:${jsFile}`, bundleTask({ watch: false, label: jsFile, filename: `${jsFile}.js` })) }) -gulp.task('dev:js', gulp.parallel(...jsDevStrings)) -gulp.task('build:js', gulp.parallel(...jsBuildStrings)) +gulp.task('dev:js', gulp.series(jsDevStrings.shift(), gulp.parallel(...jsDevStrings))) +gulp.task('build:js', gulp.series(jsBuildStrings.shift(), gulp.parallel(...jsBuildStrings))) // disc bundle analyzer tasks -- cgit v1.2.3 From 88686a39681ef84f2b503f25b94607d96657a190 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 4 Oct 2017 15:35:46 -0700 Subject: Enforce 0x prefix on accounts with new hd keyring --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 03d095228..298691588 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "eth-bin-to-ops": "^1.0.1", "eth-block-tracker": "^2.2.0", "eth-contract-metadata": "^1.1.4", - "eth-hd-keyring": "^1.1.1", + "eth-hd-keyring": "^1.2.1", "eth-json-rpc-filters": "^1.2.2", "eth-keyring-controller": "^2.0.0", "eth-phishing-detect": "^1.1.4", -- cgit v1.2.3 From 583342bd566131815f59941958fb35b34fa3d8bb Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 4 Oct 2017 19:47:50 -0230 Subject: Removes old/second setProviderType method from before merge (commit bd99bc2e) --- ui/app/actions.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index 7b0b0d9c5..bf9a70c77 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -902,15 +902,6 @@ function addToAddressBook (recipient, nickname) { } } -function setProviderType (type) { - log.debug(`background.setProviderType`) - background.setProviderType(type) - return { - type: actions.SET_PROVIDER_TYPE, - value: type, - } -} - function useEtherscanProvider () { log.debug(`background.useEtherscanProvider`) background.useEtherscanProvider() -- cgit v1.2.3 From e7589a099fa592b930458a16d7346ff20070a6e8 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 4 Oct 2017 22:09:37 -0700 Subject: mascara:exampl/app - add a send tx button --- mascara/example/app.js | 28 ++++++++++++++++++++-------- mascara/example/app/index.html | 2 ++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/mascara/example/app.js b/mascara/example/app.js index d0cb6ba83..598e2c84c 100644 --- a/mascara/example/app.js +++ b/mascara/example/app.js @@ -7,20 +7,32 @@ async function loadProvider() { const ethereumProvider = window.metamask.createDefaultProvider({ host: 'http://localhost:9001' }) const ethQuery = new EthQuery(ethereumProvider) const accounts = await ethQuery.accounts() - logToDom(accounts.length ? accounts[0] : 'LOCKED or undefined') - setupButton(ethQuery) + window.METAMASK_ACCOUNT = accounts[0] || 'locked' + logToDom(accounts.length ? accounts[0] : 'LOCKED or undefined', 'account') + setupButtons(ethQuery) } -function logToDom(message){ - document.getElementById('account').innerText = message +function logToDom(message, context){ + document.getElementById(context).innerText = message console.log(message) } -function setupButton (ethQuery) { - const button = document.getElementById('action-button-1') - button.addEventListener('click', async () => { +function setupButtons (ethQuery) { + const accountButton = document.getElementById('action-button-1') + accountButton.addEventListener('click', async () => { const accounts = await ethQuery.accounts() - logToDom(accounts.length ? accounts[0] : 'LOCKED or undefined') + window.METAMASK_ACCOUNT = accounts[0] || 'locked' + logToDom(accounts.length ? accounts[0] : 'LOCKED or undefined', 'account') + }) + const txButton = document.getElementById('action-button-2') + txButton.addEventListener('click', async () => { + if (!window.METAMASK_ACCOUNT || window.METAMASK_ACCOUNT === 'locked') return + const txHash = await ethQuery.sendTransaction({ + from: window.METAMASK_ACCOUNT, + to: window.METAMASK_ACCOUNT, + data: '', + }) + logToDom(txHash, 'cb-value') }) } \ No newline at end of file diff --git a/mascara/example/app/index.html b/mascara/example/app/index.html index f3e38877c..8afb6f3f2 100644 --- a/mascara/example/app/index.html +++ b/mascara/example/app/index.html @@ -10,6 +10,8 @@
+ +
\ No newline at end of file -- cgit v1.2.3 From 245ab70881d2ddabf14a6dc13d75c3f1789ab804 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 4 Oct 2017 22:10:30 -0700 Subject: add /mascara to linting --- gulpfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index ac36cf983..c18fd5949 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -151,7 +151,7 @@ gulp.task('copy:watch', function(){ gulp.task('lint', function () { // Ignoring node_modules, dist/firefox, and docs folders: - return gulp.src(['app/**/*.js', 'ui/**/*.js', '!node_modules/**', '!dist/firefox/**', '!docs/**', '!app/scripts/chromereload.js']) + return gulp.src(['app/**/*.js', 'ui/**/*.js', 'mascara/**/*.js', '!node_modules/**', '!dist/firefox/**', '!docs/**', '!app/scripts/chromereload.js']) .pipe(eslint(fs.readFileSync(path.join(__dirname, '.eslintrc')))) // eslint.format() outputs the lint results to the console. // Alternatively use eslint.formatEach() (see Docs). -- cgit v1.2.3 From 2dba03ffc530e3e2a176aea9712ffc3e189bebc2 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 4 Oct 2017 22:30:28 -0700 Subject: dont lint jquery --- gulpfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index c18fd5949..557b58a68 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -151,7 +151,7 @@ gulp.task('copy:watch', function(){ gulp.task('lint', function () { // Ignoring node_modules, dist/firefox, and docs folders: - return gulp.src(['app/**/*.js', 'ui/**/*.js', 'mascara/**/*.js', '!node_modules/**', '!dist/firefox/**', '!docs/**', '!app/scripts/chromereload.js']) + return gulp.src(['app/**/*.js', 'ui/**/*.js', 'mascara/src/*.js', 'mascara/server/*.js', '!node_modules/**', '!dist/firefox/**', '!docs/**', '!app/scripts/chromereload.js', '!mascara/test/jquery-3.1.0.min.js']) .pipe(eslint(fs.readFileSync(path.join(__dirname, '.eslintrc')))) // eslint.format() outputs the lint results to the console. // Alternatively use eslint.formatEach() (see Docs). -- cgit v1.2.3 From 7a9e2aa4f0fe8a12d1fac838f62ca4fbf6763af0 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 4 Oct 2017 23:03:47 -0700 Subject: mascara: linting and code clean up --- mascara/server/index.js | 8 +++--- mascara/server/util.js | 10 +++---- mascara/src/background.js | 67 ++++++++++++++++++----------------------------- mascara/src/proxy.js | 4 +-- mascara/src/ui.js | 10 +++---- 5 files changed, 41 insertions(+), 58 deletions(-) diff --git a/mascara/server/index.js b/mascara/server/index.js index 14e3fa18e..12b527e5d 100644 --- a/mascara/server/index.js +++ b/mascara/server/index.js @@ -5,7 +5,7 @@ const serveBundle = require('./util').serveBundle module.exports = createMetamascaraServer -function createMetamascaraServer(){ +function createMetamascaraServer () { // start bundlers const metamascaraBundle = createBundle(__dirname + '/../src/mascara.js') @@ -17,13 +17,13 @@ function createMetamascaraServer(){ const server = express() // ui window serveBundle(server, '/ui.js', uiBundle) - server.use(express.static(__dirname+'/../ui/')) - server.use(express.static(__dirname+'/../../dist/chrome')) + server.use(express.static(__dirname + '/../ui/')) + server.use(express.static(__dirname + '/../../dist/chrome')) // metamascara serveBundle(server, '/metamascara.js', metamascaraBundle) // proxy serveBundle(server, '/proxy/proxy.js', proxyBundle) - server.use('/proxy/', express.static(__dirname+'/../proxy')) + server.use('/proxy/', express.static(__dirname + '/../proxy')) // background serveBundle(server, '/background.js', backgroundBuild) diff --git a/mascara/server/util.js b/mascara/server/util.js index 6e25b35d8..6ab41b729 100644 --- a/mascara/server/util.js +++ b/mascara/server/util.js @@ -7,14 +7,14 @@ module.exports = { } -function serveBundle(server, path, bundle){ - server.get(path, function(req, res){ +function serveBundle (server, path, bundle) { + server.get(path, function (req, res) { res.setHeader('Content-Type', 'application/javascript; charset=UTF-8') res.send(bundle.latest) }) } -function createBundle(entryPoint){ +function createBundle (entryPoint) { var bundleContainer = {} @@ -30,8 +30,8 @@ function createBundle(entryPoint){ return bundleContainer - function bundle() { - bundler.bundle(function(err, result){ + function bundle () { + bundler.bundle(function (err, result) { if (err) { console.log(`Bundle failed! (${entryPoint})`) console.error(err) diff --git a/mascara/src/background.js b/mascara/src/background.js index 5ba865ad8..79bf9d378 100644 --- a/mascara/src/background.js +++ b/mascara/src/background.js @@ -1,44 +1,37 @@ global.window = global -const self = global -const pipe = require('pump') const SwGlobalListener = require('sw-stream/lib/sw-global-listener.js') -const connectionListener = new SwGlobalListener(self) +const connectionListener = new SwGlobalListener(global) const setupMultiplex = require('../../app/scripts/lib/stream-utils.js').setupMultiplex -const PortStream = require('../../app/scripts/lib/port-stream.js') const DbController = require('idb-global') const SwPlatform = require('../../app/scripts/platforms/sw') const MetamaskController = require('../../app/scripts/metamask-controller') -const extension = {} //require('../../app/scripts/lib/extension') -const storeTransform = require('obs-store/lib/transform') const Migrator = require('../../app/scripts/lib/migrator/') const migrations = require('../../app/scripts/migrations/') const firstTimeState = require('../../app/scripts/first-time-state') const STORAGE_KEY = 'metamask-config' const METAMASK_DEBUG = process.env.METAMASK_DEBUG -let popupIsOpen = false -let connectedClientCount = 0 +global.metamaskPopupIsOpen = false const log = require('loglevel') global.log = log log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn') -self.addEventListener('install', function(event) { - event.waitUntil(self.skipWaiting()) +global.addEventListener('install', function (event) { + event.waitUntil(global.skipWaiting()) }) -self.addEventListener('activate', function(event) { - event.waitUntil(self.clients.claim()) +global.addEventListener('activate', function (event) { + event.waitUntil(global.clients.claim()) }) console.log('inside:open') // // state persistence -let diskStore const dbController = new DbController({ key: STORAGE_KEY, }) @@ -47,23 +40,18 @@ loadStateFromPersistence() .then(() => console.log('MetaMask initialization complete.')) .catch((err) => console.error('WHILE SETTING UP:', err)) -// initialization flow - // // State and Persistence // -function loadStateFromPersistence() { +async function loadStateFromPersistence () { // migrations - let migrator = new Migrator({ migrations }) + const migrator = new Migrator({ migrations }) const initialState = migrator.generateInitialState(firstTimeState) dbController.initialState = initialState - return dbController.open() - .then((versionedData) => migrator.migrateData(versionedData)) - .then((versionedData) => { - dbController.put(versionedData) - return Promise.resolve(versionedData) - }) - .then((versionedData) => Promise.resolve(versionedData.data)) + const versionedData = await dbController.open() + const migratedData = await migrator.migrateData(versionedData) + await dbController.put(migratedData) + return migratedData.data } function setupController (initState, client) { @@ -89,15 +77,16 @@ function setupController (initState, client) { controller.store.subscribe((state) => { versionifyData(state) .then((versionedData) => dbController.put(versionedData)) - .catch((err) => {console.error(err)}) + .catch((err) => { console.error(err) }) }) - function versionifyData(state) { + function versionifyData (state) { return dbController.get() .then((rawData) => { return Promise.resolve({ data: state, meta: rawData.meta, - })} + }) +} ) } @@ -107,7 +96,6 @@ function setupController (initState, client) { connectionListener.on('remote', (portStream, messageEvent) => { console.log('REMOTE CONECTION FOUND***********') - connectedClientCount += 1 connectRemote(portStream, messageEvent.data.context) }) @@ -116,7 +104,7 @@ function setupController (initState, client) { if (isMetaMaskInternalProcess) { // communication with popup controller.setupTrustedCommunication(connectionStream, 'MetaMask') - popupIsOpen = true + global.metamaskPopupIsOpen = true } else { // communication with page setupUntrustedCommunication(connectionStream, context) @@ -131,24 +119,19 @@ function setupController (initState, client) { controller.setupPublicConfig(mx.createStream('publicConfig')) } - function setupTrustedCommunication (connectionStream, originDomain) { - // setup multiplexing - var mx = setupMultiplex(connectionStream) - // connect features - controller.setupProviderConnection(mx.createStream('provider'), originDomain) - } // // User Interface setup // return Promise.resolve() } +// // this will be useful later but commented out for linting for now (liiiinting) +// function sendMessageToAllClients (message) { +// global.clients.matchAll().then(function (clients) { +// clients.forEach(function (client) { +// client.postMessage(message) +// }) +// }) +// } -function sendMessageToAllClients (message) { - self.clients.matchAll().then(function(clients) { - clients.forEach(function(client) { - client.postMessage(message) - }) - }) -} function noop () {} diff --git a/mascara/src/proxy.js b/mascara/src/proxy.js index 07c5b0e3c..54c5d5cf4 100644 --- a/mascara/src/proxy.js +++ b/mascara/src/proxy.js @@ -2,7 +2,7 @@ const createParentStream = require('iframe-stream').ParentStream const SWcontroller = require('client-sw-ready-event/lib/sw-client.js') const SwStream = require('sw-stream/lib/sw-stream.js') -let intervalDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000 +const intervalDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000 const background = new SWcontroller({ fileName: '/background.js', letBeIdle: false, @@ -12,7 +12,7 @@ const background = new SWcontroller({ const pageStream = createParentStream() background.on('ready', () => { - let swStream = SwStream({ + const swStream = SwStream({ serviceWorker: background.controller, context: 'dapp', }) diff --git a/mascara/src/ui.js b/mascara/src/ui.js index 2f940ad1a..b272a2e06 100644 --- a/mascara/src/ui.js +++ b/mascara/src/ui.js @@ -17,17 +17,17 @@ var name = 'popup' window.METAMASK_UI_TYPE = name window.METAMASK_PLATFORM_TYPE = 'mascara' -let intervalDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000 +const intervalDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000 const background = new SWcontroller({ fileName: '/background.js', letBeIdle: false, intervalDelay, - wakeUpInterval: 20000 + wakeUpInterval: 20000, }) // Setup listener for when the service worker is read const connectApp = function (readSw) { - let connectionStream = SwStream({ + const connectionStream = SwStream({ serviceWorker: background.controller, context: name, }) @@ -57,7 +57,7 @@ background.on('updatefound', windowReload) background.startWorker() -function windowReload() { +function windowReload () { if (window.METAMASK_SKIP_RELOAD) return window.location.reload() } @@ -66,4 +66,4 @@ function timeout (time) { return new Promise((resolve) => { setTimeout(resolve, time || 1500) }) -} \ No newline at end of file +} -- cgit v1.2.3 From 5eca4223b2986e101f64d71916aa7ce5a66064fc Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 4 Oct 2017 23:07:40 -0700 Subject: use log.debug --- mascara/src/background.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mascara/src/background.js b/mascara/src/background.js index 79bf9d378..cf043a4f0 100644 --- a/mascara/src/background.js +++ b/mascara/src/background.js @@ -28,7 +28,7 @@ global.addEventListener('activate', function (event) { event.waitUntil(global.clients.claim()) }) -console.log('inside:open') +log.debug('inside:open') // // state persistence @@ -37,7 +37,7 @@ const dbController = new DbController({ }) loadStateFromPersistence() .then((initState) => setupController(initState)) -.then(() => console.log('MetaMask initialization complete.')) +.then(() => log.debug('MetaMask initialization complete.')) .catch((err) => console.error('WHILE SETTING UP:', err)) // @@ -95,7 +95,7 @@ function setupController (initState, client) { // connectionListener.on('remote', (portStream, messageEvent) => { - console.log('REMOTE CONECTION FOUND***********') + log.debug('REMOTE CONECTION FOUND***********') connectRemote(portStream, messageEvent.data.context) }) -- cgit v1.2.3 From 7ec068d279276a7e0fcc862d18e3fd3e1648728b Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 4 Oct 2017 23:21:30 -0700 Subject: mascara/background: use async await --- mascara/src/background.js | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/mascara/src/background.js b/mascara/src/background.js index cf043a4f0..8aa1d8fe2 100644 --- a/mascara/src/background.js +++ b/mascara/src/background.js @@ -54,7 +54,7 @@ async function loadStateFromPersistence () { return migratedData.data } -function setupController (initState, client) { +async function setupController (initState, client) { // // MetaMask Controller @@ -74,20 +74,19 @@ function setupController (initState, client) { }) global.metamaskController = controller - controller.store.subscribe((state) => { - versionifyData(state) - .then((versionedData) => dbController.put(versionedData)) - .catch((err) => { console.error(err) }) + controller.store.subscribe(async (state) => { + try { + const versionedData = await versionifyData(state) + await dbController.put(versionedData) + } catch (e) { console.error('METAMASK Error:', e) } }) - function versionifyData (state) { - return dbController.get() - .then((rawData) => { - return Promise.resolve({ - data: state, - meta: rawData.meta, - }) -} - ) + + async function versionifyData (state) { + const rawData = await dbController.get() + return { + data: state, + meta: rawData.meta, + } } // @@ -118,12 +117,6 @@ function setupController (initState, client) { controller.setupProviderConnection(mx.createStream('provider'), originDomain) controller.setupPublicConfig(mx.createStream('publicConfig')) } - - // - // User Interface setup - // - return Promise.resolve() - } // // this will be useful later but commented out for linting for now (liiiinting) // function sendMessageToAllClients (message) { -- cgit v1.2.3 From 15809894ff42bacc3babfb9aaba48389417907c0 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Thu, 5 Oct 2017 09:58:04 -0700 Subject: Add indicator for specified gas price --- app/scripts/controllers/transactions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 94e04c429..481dd62a5 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -169,6 +169,7 @@ module.exports = class TransactionController extends EventEmitter { async addTxDefaults (txMeta) { const txParams = txMeta.txParams // ensure value + txMeta.gasPriceSpecified = Boolean(txParams.gasPrice) const gasPrice = txParams.gasPrice || await this.query.gasPrice() txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16)) txParams.value = txParams.value || '0x0' -- cgit v1.2.3 From ec9c5283135d2cc3dcb633f19dd89ea79267f34a Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 5 Oct 2017 10:54:28 -0700 Subject: pending-tx - check time stamp instead of block number for resubmit --- app/scripts/controllers/transactions.js | 2 +- app/scripts/lib/pending-tx-tracker.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 94e04c429..a0f983deb 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -59,7 +59,7 @@ module.exports = class TransactionController extends EventEmitter { this.pendingTxTracker = new PendingTransactionTracker({ provider: this.provider, nonceTracker: this.nonceTracker, - retryLimit: 3500, // Retry 3500 blocks, or about 1 day. + retryTimePeriod: 86400000, // Retry 3500 blocks, or about 1 day. publishTransaction: (rawTx) => this.query.sendRawTransaction(rawTx), getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager), }) diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js index 6f1601586..3463d45bf 100644 --- a/app/scripts/lib/pending-tx-tracker.js +++ b/app/scripts/lib/pending-tx-tracker.js @@ -22,7 +22,8 @@ module.exports = class PendingTransactionTracker extends EventEmitter { super() this.query = new EthQuery(config.provider) this.nonceTracker = config.nonceTracker - this.retryLimit = config.retryLimit || Infinity + // default is one day + this.retryTimePeriod = config.retryTimePeriod || 86400000 this.getPendingTransactions = config.getPendingTransactions this.publishTransaction = config.publishTransaction } @@ -99,8 +100,8 @@ module.exports = class PendingTransactionTracker extends EventEmitter { } async _resubmitTx (txMeta) { - if (txMeta.retryCount > this.retryLimit) { - const err = new Error(`Gave up submitting after ${this.retryLimit} blocks un-mined.`) + if (Date.now() > txMeta.time + this.retryTimePeriod) { + const err = new Error(`Gave up submitting after ${this.retryTimePeriod / 3.6e+6} hours.`) return this.emit('tx:failed', txMeta.id, err) } -- cgit v1.2.3 From 3cb9da2ae56ce0b3162e64dbcf69f5f9e39ff4e8 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 5 Oct 2017 11:42:01 -0700 Subject: "fix" hours for message --- app/scripts/lib/pending-tx-tracker.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js index 3463d45bf..8a626e222 100644 --- a/app/scripts/lib/pending-tx-tracker.js +++ b/app/scripts/lib/pending-tx-tracker.js @@ -101,7 +101,8 @@ module.exports = class PendingTransactionTracker extends EventEmitter { async _resubmitTx (txMeta) { if (Date.now() > txMeta.time + this.retryTimePeriod) { - const err = new Error(`Gave up submitting after ${this.retryTimePeriod / 3.6e+6} hours.`) + const hours = (this.retryTimePeriod / 3.6e+6).toFixed(1) + const err = new Error(`Gave up submitting after ${hours} hours.`) return this.emit('tx:failed', txMeta.id, err) } -- cgit v1.2.3 From 554a3ac4e4f4493f318753410bb4332204f2efb7 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 5 Oct 2017 12:12:31 -0700 Subject: add to CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 069602915..7fc74a9d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Current Master +- Only rebrodcast transactions for a day not a days worth of blocks - Remove Slack link from info page, since it is a big phishing target. ## 3.10.8 2017-9-28 -- cgit v1.2.3 From 8d45b96db6cfe4b13cf91601e3698fda7621a76b Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 5 Oct 2017 12:44:48 -0700 Subject: Version 3.10.9 --- CHANGELOG.md | 3 +++ app/manifest.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fc74a9d7..8e9fb2530 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,11 @@ ## Current Master +## 3.10.9 2017-10-5 + - Only rebrodcast transactions for a day not a days worth of blocks - Remove Slack link from info page, since it is a big phishing target. +- Stop computing balance based on pending transactions, to avoid edge case where users are unable to send transactions. ## 3.10.8 2017-9-28 diff --git a/app/manifest.json b/app/manifest.json index 0fc43c7d4..c253a5c2b 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.10.8", + "version": "3.10.9", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From 176d03b2e8061c50108b2024f7716885097e82fd Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 5 Oct 2017 14:39:23 -0700 Subject: Require keyring-controller 2.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 298691588..1e379493d 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "eth-contract-metadata": "^1.1.4", "eth-hd-keyring": "^1.2.1", "eth-json-rpc-filters": "^1.2.2", - "eth-keyring-controller": "^2.0.0", + "eth-keyring-controller": "^2.1.0", "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", "eth-sig-util": "^1.2.2", -- cgit v1.2.3 From 9bc80d998eda937e3a8f95fa5e04fcba66e8a6f8 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 5 Oct 2017 14:39:35 -0700 Subject: Add signTypedData input validations --- app/scripts/lib/typed-message-manager.js | 11 +++++++++++ app/scripts/metamask-controller.js | 13 +++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js index e3efdb45d..e041ae9f3 100644 --- a/app/scripts/lib/typed-message-manager.js +++ b/app/scripts/lib/typed-message-manager.js @@ -1,6 +1,7 @@ const EventEmitter = require('events') const ObservableStore = require('obs-store') const createId = require('./random-id') +const assert = require('assert') module.exports = class TypedMessageManager extends EventEmitter { @@ -23,6 +24,8 @@ module.exports = class TypedMessageManager extends EventEmitter { } addUnapprovedMessage (msgParams) { + this.validateParams(msgParams) + log.debug(`TypedMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`) // create txData obj with parameters and meta data var time = (new Date()).getTime() @@ -41,6 +44,14 @@ module.exports = class TypedMessageManager extends EventEmitter { return msgId } + validateParams (params) { + assert.equal(typeof params, 'object', 'Params should ben an object.') + assert.ok('data' in params, 'Params must include a data field.') + assert.ok('from' in params, 'Params must include a from field.') + assert.ok(Array.isArray(params.data), 'Data should be an array.') + assert.equal(typeof params.from, 'string', 'From field must be a string.') + } + addMsg (msg) { this.messages.push(msg) this._saveMsgList() diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 8f773a72b..727f48f1c 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -566,11 +566,16 @@ module.exports = class MetamaskController extends EventEmitter { } newUnsignedTypedMessage (msgParams, cb) { - const msgId = this.typedMessageManager.addUnapprovedMessage(msgParams) - this.sendUpdate() - this.opts.showUnconfirmedMessage() + let msgId + try { + msgId = this.typedMessageManager.addUnapprovedMessage(msgParams) + this.sendUpdate() + this.opts.showUnconfirmedMessage() + } catch (e) { + return cb(e) + } + this.typedMessageManager.once(`${msgId}:finished`, (data) => { - console.log(data) switch (data.status) { case 'signed': return cb(null, data.rawSig) -- cgit v1.2.3 From c821a6b93a5a8e0f564b69d493c350e3763e749b Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 5 Oct 2017 14:48:40 -0700 Subject: Bump provider-engine for better sender validations --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1e379493d..c846bae41 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "valid-url": "^1.0.9", "vreme": "^3.0.2", "web3": "^0.20.1", - "web3-provider-engine": "^13.3.1", + "web3-provider-engine": "^13.3.2", "web3-stream-provider": "^3.0.1", "xtend": "^4.0.1" }, -- cgit v1.2.3 From 52bfed5d13846326fbd8940dbb7c91a4f399b190 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 5 Oct 2017 14:53:22 -0700 Subject: Bump changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e9fb2530..505c04169 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Add new support for new eth_signTypedData method per EIP 712. + ## 3.10.9 2017-10-5 - Only rebrodcast transactions for a day not a days worth of blocks -- cgit v1.2.3 From e6a618b82d5ab75920763875cd0487e4431321a2 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Thu, 5 Oct 2017 15:26:03 -0700 Subject: Fix precision to account for small wei increase. --- ui/app/components/bn-as-decimal-input.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ui/app/components/bn-as-decimal-input.js b/ui/app/components/bn-as-decimal-input.js index f3ace4720..d84834d06 100644 --- a/ui/app/components/bn-as-decimal-input.js +++ b/ui/app/components/bn-as-decimal-input.js @@ -31,7 +31,7 @@ BnAsDecimalInput.prototype.render = function () { const suffix = props.suffix const style = props.style const valueString = value.toString(10) - const newValue = this.downsize(valueString, scale, precision) + const newValue = this.downsize(valueString, scale) return ( h('.flex-column', [ @@ -145,14 +145,17 @@ BnAsDecimalInput.prototype.constructWarning = function () { } -BnAsDecimalInput.prototype.downsize = function (number, scale, precision) { +BnAsDecimalInput.prototype.downsize = function (number, scale) { // if there is no scaling, simply return the number if (scale === 0) { return Number(number) } else { // if the scale is the same as the precision, account for this edge case. - var decimals = (scale === precision) ? -1 : scale - precision - return Number(number.slice(0, -scale) + '.' + number.slice(-scale, decimals)) + var adjustedNumber = number + while (adjustedNumber.length < scale) { + adjustedNumber = '0' + adjustedNumber + } + return Number(adjustedNumber.slice(0, -scale) + '.' + adjustedNumber.slice(-scale)) } } -- cgit v1.2.3 From 106af9ec5b5671ec312117b1e2bd0ba5350d2085 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Thu, 5 Oct 2017 17:13:58 -0700 Subject: Catch an error if this is not defined. --- app/scripts/platforms/extension.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index 5ed2fdc4f..f9e1d84b7 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -18,7 +18,14 @@ class ExtensionPlatform { } getPlatformInfo (cb) { - return extension.runtime.getPlatformInfo(cb) + var info + try { + info = extension.runtime.getPlatformInfo(cb) + } catch (e) { + log.debug(e) + info = undefined + } + return info } } -- cgit v1.2.3 From f6821781d2ee3b1562008f47d9581ed21efee3ef Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Thu, 5 Oct 2017 17:17:34 -0700 Subject: Simplify try catch --- app/scripts/platforms/extension.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index f9e1d84b7..61d67e1b4 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -18,14 +18,12 @@ class ExtensionPlatform { } getPlatformInfo (cb) { - var info try { - info = extension.runtime.getPlatformInfo(cb) + return extension.runtime.getPlatformInfo(cb) } catch (e) { log.debug(e) - info = undefined + return undefined } - return info } } -- cgit v1.2.3 From 0146b55d6d7bd8717b3f3ad071c64744e21a93fd Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 6 Oct 2017 11:33:14 -0700 Subject: Check status of pending transactions on startup Fixes #1531 --- CHANGELOG.md | 2 ++ app/scripts/lib/pending-tx-tracker.js | 1 + 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e9fb2530..c037508e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Fix bug where some transactions would be shown as pending forever, even after successfully mined. + ## 3.10.9 2017-10-5 - Only rebrodcast transactions for a day not a days worth of blocks diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js index 8a626e222..5049cc4b4 100644 --- a/app/scripts/lib/pending-tx-tracker.js +++ b/app/scripts/lib/pending-tx-tracker.js @@ -26,6 +26,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter { this.retryTimePeriod = config.retryTimePeriod || 86400000 this.getPendingTransactions = config.getPendingTransactions this.publishTransaction = config.publishTransaction + this._checkPendingTxs() } // checks if a signed tx is in a block and -- cgit v1.2.3 From a32d71e8ed4c91c8ad73f4a7afc52e506ccf5247 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 6 Oct 2017 12:29:27 -0700 Subject: Add failing test for issue #2294 --- test/unit/pending-tx-test.js | 53 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/test/unit/pending-tx-test.js b/test/unit/pending-tx-test.js index 6b62bb5b1..554bd5591 100644 --- a/test/unit/pending-tx-test.js +++ b/test/unit/pending-tx-test.js @@ -5,6 +5,8 @@ const ObservableStore = require('obs-store') const clone = require('clone') const { createStubedProvider } = require('../stub/provider') const PendingTransactionTracker = require('../../app/scripts/lib/pending-tx-tracker') +const MockTxGen = require('../lib/mock-tx-gen') +const sinon = require('sinon') const noop = () => true const currentNetworkId = 42 const otherNetworkId = 36 @@ -50,6 +52,55 @@ describe('PendingTransactionTracker', function () { }) }) + describe('_checkPendingTx state management', function () { + let stub + + afterEach(function () { + if (stub) { + stub.restore() + } + }) + + it('should become failed if another tx with the same nonce succeeds', async function () { + + // SETUP + const txGen = new MockTxGen() + + txGen.generate({ + id: '456', + value: '0x01', + hash: '0xbad', + status: 'confirmed', + nonce: '0x01', + }, { count: 1 }) + + const pending = txGen.generate({ + id: '123', + value: '0x02', + hash: '0xfad', + status: 'submitted', + nonce: '0x01', + }, { count: 1 })[0] + + stub = sinon.stub(pendingTxTracker, 'getPendingTransactions') + .returns(txGen.txs) + + // THE EXPECTATION + const spy = sinon.spy() + pendingTxTracker.on('tx:failed', (txId, err) => { + assert.equal(txId, pending.id, 'should fail the pending tx') + assert.equal(err.name, 'NonceTakenErr', 'should emit a nonce taken error.') + spy(txId, err) + }) + + // THE METHOD + await pendingTxTracker._checkPendingTx(pending) + + // THE ASSERTION + return sinon.assert.calledWith(spy, pending.id, 'tx failed should be emitted') + }) + }) + describe('#checkForTxInBlock', function () { it('should return if no pending transactions', function () { // throw a type error if it trys to do anything on the block @@ -239,4 +290,4 @@ describe('PendingTransactionTracker', function () { }) }) }) -}) \ No newline at end of file +}) -- cgit v1.2.3 From be4f7b33f4f0885f2c0f5f4d537f6e9793f3fa30 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 6 Oct 2017 12:36:08 -0700 Subject: nodeify - allow callback to be optional --- app/scripts/lib/nodeify.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/scripts/lib/nodeify.js b/app/scripts/lib/nodeify.js index 832d6c6d3..19c3c8337 100644 --- a/app/scripts/lib/nodeify.js +++ b/app/scripts/lib/nodeify.js @@ -1,10 +1,18 @@ const promiseToCallback = require('promise-to-callback') +const noop = function(){} module.exports = function nodeify (fn, context) { return function(){ const args = [].slice.call(arguments) - const callback = args.pop() - if (typeof callback !== 'function') throw new Error('callback is not a function') + const lastArg = args[args.length-1] + const lastArgIsCallback = typeof lastArg === 'function' + let callback + if (lastArgIsCallback) { + callback = lastArg + args.pop() + } else { + callback = noop + } promiseToCallback(fn.apply(context, args))(callback) } } -- cgit v1.2.3 From 94513cae7bf3c8310ae6a248e12a9b7dd73e306f Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 6 Oct 2017 12:50:33 -0700 Subject: Provide method for tx tracker to refer to all txs --- app/scripts/controllers/transactions.js | 1 + app/scripts/lib/tx-state-manager.js | 8 +++++++- test/unit/pending-tx-test.js | 5 +++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index a0f983deb..ef659a300 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -62,6 +62,7 @@ module.exports = class TransactionController extends EventEmitter { retryTimePeriod: 86400000, // Retry 3500 blocks, or about 1 day. publishTransaction: (rawTx) => this.query.sendRawTransaction(rawTx), getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager), + getCompletedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager), }) this.txStateManager.store.subscribe(() => this.emit('update:badge')) diff --git a/app/scripts/lib/tx-state-manager.js b/app/scripts/lib/tx-state-manager.js index cf8117864..2250403f6 100644 --- a/app/scripts/lib/tx-state-manager.js +++ b/app/scripts/lib/tx-state-manager.js @@ -46,6 +46,12 @@ module.exports = class TransactionStateManger extends EventEmitter { return this.getFilteredTxList(opts) } + getConfirmedTransactions (address) { + const opts = { status: 'confirmed' } + if (address) opts.from = address + return this.getFilteredTxList(opts) + } + addTx (txMeta) { this.once(`${txMeta.id}:signed`, function (txId) { this.removeAllListeners(`${txMeta.id}:rejected`) @@ -242,4 +248,4 @@ module.exports = class TransactionStateManger extends EventEmitter { _saveTxList (transactions) { this.store.updateState({ transactions }) } -} \ No newline at end of file +} diff --git a/test/unit/pending-tx-test.js b/test/unit/pending-tx-test.js index 554bd5591..32421a44f 100644 --- a/test/unit/pending-tx-test.js +++ b/test/unit/pending-tx-test.js @@ -48,6 +48,7 @@ describe('PendingTransactionTracker', function () { } }, getPendingTransactions: () => {return []}, + getCompletedTransactions: () => {return []}, publishTransaction: () => {}, }) }) @@ -82,7 +83,7 @@ describe('PendingTransactionTracker', function () { nonce: '0x01', }, { count: 1 })[0] - stub = sinon.stub(pendingTxTracker, 'getPendingTransactions') + stub = sinon.stub(pendingTxTracker, 'getCompletedTransactions') .returns(txGen.txs) // THE EXPECTATION @@ -97,7 +98,7 @@ describe('PendingTransactionTracker', function () { await pendingTxTracker._checkPendingTx(pending) // THE ASSERTION - return sinon.assert.calledWith(spy, pending.id, 'tx failed should be emitted') + assert.ok(spy.calledWith(pending.id), 'tx failed should be emitted') }) }) -- cgit v1.2.3 From a417fab0ebd71d22f51a8e30590c259b32164fd2 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 6 Oct 2017 12:51:13 -0700 Subject: When checking pending txs, check for successful txs with same nonce. If a successful tx with the same nonce exists, transition tx to the failed state. Fixes #2294 --- app/scripts/lib/pending-tx-tracker.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js index 8a626e222..2d8f22ae8 100644 --- a/app/scripts/lib/pending-tx-tracker.js +++ b/app/scripts/lib/pending-tx-tracker.js @@ -25,6 +25,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter { // default is one day this.retryTimePeriod = config.retryTimePeriod || 86400000 this.getPendingTransactions = config.getPendingTransactions + this.getCompletedTransactions = config.getCompletedTransactions this.publishTransaction = config.publishTransaction } @@ -120,6 +121,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter { async _checkPendingTx (txMeta) { const txHash = txMeta.hash const txId = txMeta.id + // extra check in case there was an uncaught error during the // signature and submission process if (!txHash) { @@ -128,6 +130,15 @@ module.exports = class PendingTransactionTracker extends EventEmitter { this.emit('tx:failed', txId, noTxHashErr) return } + + // If another tx with the same nonce is mined, set as failed. + const taken = await this._checkIfNonceIsTaken(txMeta) + if (taken) { + const nonceTakenErr = new Error('Another transaction with this nonce has been mined.') + nonceTakenErr.name = 'NonceTakenErr' + return this.emit('tx:failed', txId, nonceTakenErr) + } + // get latest transaction status let txParams try { @@ -159,4 +170,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter { } nonceGlobalLock.releaseLock() } + + async _checkIfNonceIsTaken (txMeta) { + const completed = this.getCompletedTransactions() + const sameNonce = completed.filter((otherMeta) => { + return otherMeta.txParams.nonce === txMeta.txParams.nonce + }) + return sameNonce.length > 0 + } + } -- cgit v1.2.3 From bc396a7417ecfe9855ec84af0cb08fd033c42bf5 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 6 Oct 2017 13:02:34 -0700 Subject: lint fix - nodeify --- app/scripts/lib/nodeify.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/lib/nodeify.js b/app/scripts/lib/nodeify.js index 19c3c8337..d24e92206 100644 --- a/app/scripts/lib/nodeify.js +++ b/app/scripts/lib/nodeify.js @@ -4,7 +4,7 @@ const noop = function(){} module.exports = function nodeify (fn, context) { return function(){ const args = [].slice.call(arguments) - const lastArg = args[args.length-1] + const lastArg = args[args.length - 1] const lastArgIsCallback = typeof lastArg === 'function' let callback if (lastArgIsCallback) { -- cgit v1.2.3 From 3b3120c5f83d0971747abc28e9a3ddfc3da34be3 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 6 Oct 2017 13:16:44 -0700 Subject: nodeify - fix test --- test/unit/nodeify-test.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/unit/nodeify-test.js b/test/unit/nodeify-test.js index 537dae605..c7b127889 100644 --- a/test/unit/nodeify-test.js +++ b/test/unit/nodeify-test.js @@ -18,14 +18,13 @@ describe('nodeify', function () { }) }) - it('should throw if the last argument is not a function', function (done) { + it('should allow the last argument to not be a function', function (done) { const nodified = nodeify(obj.promiseFunc, obj) try { nodified('baz') - done(new Error('should have thrown if the last argument is not a function')) - } catch (err) { - assert.equal(err.message, 'callback is not a function') done() + } catch (err) { + done(new Error('should not have thrown if the last argument is not a function')) } }) }) -- cgit v1.2.3 From a1696f89a8764f17c10298a45160abf8fc7dce5e Mon Sep 17 00:00:00 2001 From: Sergey Ukustov Date: Sat, 7 Oct 2017 00:38:13 +0300 Subject: Validate data format for eth_signTypedData --- app/scripts/lib/typed-message-manager.js | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js index e041ae9f3..8b760790e 100644 --- a/app/scripts/lib/typed-message-manager.js +++ b/app/scripts/lib/typed-message-manager.js @@ -2,6 +2,7 @@ const EventEmitter = require('events') const ObservableStore = require('obs-store') const createId = require('./random-id') const assert = require('assert') +const sigUtil = require('eth-sig-util') module.exports = class TypedMessageManager extends EventEmitter { @@ -50,6 +51,9 @@ module.exports = class TypedMessageManager extends EventEmitter { assert.ok('from' in params, 'Params must include a from field.') assert.ok(Array.isArray(params.data), 'Data should be an array.') assert.equal(typeof params.from, 'string', 'From field must be a string.') + assert.doesNotThrow(() => { + sigUtil.typedSignatureHash(params.data) + }, 'Expected EIP712 typed data') } addMsg (msg) { diff --git a/package.json b/package.json index c846bae41..225742487 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "eth-keyring-controller": "^2.1.0", "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", - "eth-sig-util": "^1.2.2", + "eth-sig-util": "^1.4.0", "eth-simple-keyring": "^1.1.1", "eth-token-tracker": "^1.1.4", "ethereumjs-tx": "^1.3.0", -- cgit v1.2.3 From 53bb4bebb11b355f2655b2be0116005df573e907 Mon Sep 17 00:00:00 2001 From: Sergey Ukustov Date: Sat, 7 Oct 2017 23:25:33 +0300 Subject: More appropriate styling --- ui/app/components/typed-message-renderer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/app/components/typed-message-renderer.js b/ui/app/components/typed-message-renderer.js index b7d1572c2..a042b57be 100644 --- a/ui/app/components/typed-message-renderer.js +++ b/ui/app/components/typed-message-renderer.js @@ -22,6 +22,7 @@ TypedMessageRenderer.prototype.render = function () { border: 'none', background: 'white', padding: '3px', + overflow: 'scroll', }, style) return ( @@ -34,7 +35,7 @@ TypedMessageRenderer.prototype.render = function () { function renderTypedData(values) { return values.map(function (value) { return h('div', {}, [ - h('strong', {style: {display: 'block', fontWeight: 'bold', textTransform: 'capitalize'}}, String(value.name) + ':'), + h('strong', {style: {display: 'block', fontWeight: 'bold'}}, String(value.name) + ':'), h('div', {}, value.value), ]) }) -- cgit v1.2.3 From c9c940bfc13ddbf0dd021a1f638c050369e13017 Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 4 Oct 2017 20:42:07 -0230 Subject: Clear selected token when changing network. --- ui/app/actions.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index bf9a70c77..86ef4b4b4 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -155,6 +155,7 @@ var actions = { UPDATE_TOKENS: 'UPDATE_TOKENS', setRpcTarget: setRpcTarget, setProviderType: setProviderType, + updateProviderType, // loading overlay SHOW_LOADING: 'SHOW_LOADING_INDICATION', HIDE_LOADING: 'HIDE_LOADING_INDICATION', @@ -869,11 +870,17 @@ function setProviderType (type) { log.error(err) return dispatch(self.displayWarning('Had a problem changing networks!')) } + dispatch(actions.updateProviderType(type)) + dispatch(actions.setSelectedToken()) }) - return { - type: actions.SET_PROVIDER_TYPE, - value: type, - } + + } +} + +function updateProviderType(type) { + return { + type: actions.SET_PROVIDER_TYPE, + value: type, } } -- cgit v1.2.3 From 49aa6e73eadc5b343353c4312afc1e3b40dc18df Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 4 Oct 2017 21:01:12 -0230 Subject: Edit account modal shows and allows editing of name from props, not just placeholder. --- ui/app/components/modals/edit-account-name-modal.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/app/components/modals/edit-account-name-modal.js b/ui/app/components/modals/edit-account-name-modal.js index 5c25ac245..e2361140d 100644 --- a/ui/app/components/modals/edit-account-name-modal.js +++ b/ui/app/components/modals/edit-account-name-modal.js @@ -24,10 +24,11 @@ function mapDispatchToProps (dispatch) { } inherits(EditAccountNameModal, Component) -function EditAccountNameModal () { +function EditAccountNameModal (props) { Component.call(this) + this.state = { - inputText: '', + inputText: props.identity.name, } } -- cgit v1.2.3 From bbe893a0d8d759ba750ba78ff8aed0f0876a43ff Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 5 Oct 2017 13:14:45 -0230 Subject: UI for send screen container without form rows. --- app/scripts/lib/is-popup-or-notification.js | 5 +- ui/app/app.js | 11 ++- ui/app/css/itcss/components/send.scss | 140 ++++++++++++++++++++++++++++ ui/app/css/itcss/settings/variables.scss | 3 + ui/app/send-v2.js | 61 ++++++++++++ 5 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 ui/app/send-v2.js diff --git a/app/scripts/lib/is-popup-or-notification.js b/app/scripts/lib/is-popup-or-notification.js index 73a812d5f..e2999411f 100644 --- a/app/scripts/lib/is-popup-or-notification.js +++ b/app/scripts/lib/is-popup-or-notification.js @@ -1,6 +1,9 @@ module.exports = function isPopupOrNotification () { const url = window.location.href - if (url.match(/popup.html$/) || url.match(/home.html$/)) { + // if (url.match(/popup.html$/) || url.match(/home.html$/)) { + // Below regexes needed for feature toggles (e.g. see line ~340 in ui/app/app.js) + // Revert below regexes to above commented out regexes before merge to master + if (url.match(/popup.html(?:\?.+)*$/) || url.match(/home.html(?:\?.+)*$/)) { return 'popup' } else { return 'notification' diff --git a/ui/app/app.js b/ui/app/app.js index f1a671ab1..ac017da05 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -9,6 +9,7 @@ const NewKeyChainScreen = require('./new-keychain') // accounts const MainContainer = require('./main-container') const SendTransactionScreen = require('./send') +const SendTransactionScreen2 = require('./send-v2.js') const SendTokenScreen = require('./components/send-token') const ConfirmTxScreen = require('./conf-tx') // notice @@ -333,7 +334,15 @@ App.prototype.renderPrimary = function () { case 'sendTransaction': log.debug('rendering send tx screen') - return h(SendTransactionScreen, {key: 'send-transaction'}) + // Below param and ternary operator used for feature toggle + // Remove before merged to master + const windowParam = window.location.search.substr(1).split('=') + + const SendComponentToRender = windowParam[0] === "ft" && windowParam[1] === "send-v2" + ? SendTransactionScreen2 + : SendTransactionScreen + + return h(SendComponentToRender, {key: 'send-transaction'}) case 'sendToken': log.debug('rendering send token screen') diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index dee8157ef..72a01dc89 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -397,3 +397,143 @@ width: 100%; } } + +.send-v2 { + &__container { + height: 701px; + width: 380px; + border-radius: 8px; + background-color: $white; + box-shadow: 0 2px 4px 0 rgba(0,0,0,0.08); + display: flex; + flex-flow: column nowrap; + z-index: 25; + align-items: center; + font-family: Roboto; + position: relative; + top: -26px; + + @media screen and (max-width: $break-small) { + width: 100%; + overflow-y: auto; + top: 0; + width: 100%; + box-shadow: none; + } + } + + &__send-eth-icon { + border-radius: 50%; + width: 48px; + height: 48px; + border: 1px solid $alto; + z-index: 25; + padding: 4px; + background-color: $white; + + @media screen and (max-width: $break-small) { + position: relative; + top: 0; + } + } + + &__send-arrow-icon { + color: #f28930; + transform: rotate(-45deg); + position: absolute; + top: -2px; + left: 0; + font-size: 1.12em; + } + + &__arrow-background { + background-color: $white; + height: 14px; + width: 14px; + position: absolute; + top: 52px; + left: 199px; + border-radius: 50%; + z-index: 100; + } + + &__header { + height: 88px; + width: 380px; + background-color: $athens-grey; + position: relative; + display: flex; + justify-content: center; + align-items: center; + } + + &__header-tip { + height: 25px; + width: 25px; + background: $athens-grey; + position: absolute; + transform: rotate(45deg); + left: 178px; + top: 71px; + } + + &__title { + color: $scorpion; + font-size: 22px; + line-height: 29px; + text-align: center; + margin-top: 25px; + } + + &__copy { + color: $gray; + font-size: 14px; + font-weight: 300; + line-height: 19px; + text-align: center; + margin-top: 10px; + width: 287px; + } + + &__footer { + height: 92px; + width: 100%; + display: flex; + justify-content: space-evenly; + align-items: center; + border-top: 1px solid $alto; + position: absolute; + bottom: 0; + } + + &__next-btn, + &__cancel-btn { + width: 163px; + text-align: center; + height: 55px; + width: 163px; + border-radius: 2px; + background-color: $white; + font-family: Roboto; + font-size: 16px; + font-weight: 300; + line-height: 21px; + text-align: center; + border: 1px solid; + } + + &__next-btn__disabled { + opacity: .5; + cursor: auto; + } + + &__next-btn { + color: $curious-blue; + border-color: $curious-blue; + } + + &__cancel-btn { + color: $dusty-gray; + border-color: $dusty-gray; + } +} diff --git a/ui/app/css/itcss/settings/variables.scss b/ui/app/css/itcss/settings/variables.scss index 103a7ffe0..b0ef86075 100644 --- a/ui/app/css/itcss/settings/variables.scss +++ b/ui/app/css/itcss/settings/variables.scss @@ -7,6 +7,7 @@ $white: #fff; $black: #000; $orange: #ffa500; $red: #f00; +$gray: #808080; /* Colors @@ -39,6 +40,8 @@ $blue-lagoon: #038789; $purple: #690496; $tulip-tree: #ebb33f; $malibu-blue: #7ac9fd; +$athens-grey: #e9edf0; +$jaffa: #f28930; /* Z-Indicies diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js new file mode 100644 index 000000000..f423b90ff --- /dev/null +++ b/ui/app/send-v2.js @@ -0,0 +1,61 @@ +const { inherits } = require('util') +const PersistentForm = require('../lib/persistent-form') +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const Identicon = require('./components/identicon') + +module.exports = connect(mapStateToProps)(SendTransactionScreen) + +function mapStateToProps (state) { + return {} +} + +inherits(SendTransactionScreen, PersistentForm) +function SendTransactionScreen () { + PersistentForm.call(this) + + this.state = { + newTx: { + from: '', + to: '', + amountToSend: '0x0', + gasPrice: null, + gas: null, + amount: '0x0', + txData: null, + memo: '', + }, + } +} + +SendTransactionScreen.prototype.render = function () { + return ( + + h('div.send-v2__container', [ + h('div.send-v2__header', {}, [ + + h('img.send-v2__send-eth-icon', { src: '../images/eth_logo.svg' }), + + h('div.send-v2__arrow-background', [ + h('i.fa.fa-lg.fa-arrow-circle-right.send-v2__send-arrow-icon'), + ]), + + h('div.send-v2__header-tip'), + + ]), + + h('div.send-v2__title', 'Send Funds'), + + h('div.send-v2__copy', 'Only send ETH to an Ethereum address.'), + + h('div.send-v2__copy', 'Sending to a different crytpocurrency that is not Ethereum may result in permanent loss.'), + + // Buttons underneath card + h('div.send-v2__footer', [ + h('button.send-v2__cancel-btn', {}, 'Cancel'), + h('button.send-v2__next-btn', {}, 'Next'), + ]), + ]) + + ) +} -- cgit v1.2.3 From 49f76d27a9967cbeff0ba5b3d41277c558999472 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 6 Oct 2017 00:04:01 -0230 Subject: Adds checkFeatureToggle util. --- ui/app/app.js | 6 ++---- ui/lib/feature-toggle-utils.js | 11 +++++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 ui/lib/feature-toggle-utils.js diff --git a/ui/app/app.js b/ui/app/app.js index ac017da05..fb57775b6 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -2,6 +2,7 @@ const inherits = require('util').inherits const Component = require('react').Component const connect = require('react-redux').connect const h = require('react-hyperscript') +const { checkFeatureToggle } = require('../lib/feature-toggle-utils') const actions = require('./actions') // init const InitializeMenuScreen = require('./first-time/init-menu') @@ -334,11 +335,8 @@ App.prototype.renderPrimary = function () { case 'sendTransaction': log.debug('rendering send tx screen') - // Below param and ternary operator used for feature toggle - // Remove before merged to master - const windowParam = window.location.search.substr(1).split('=') - const SendComponentToRender = windowParam[0] === "ft" && windowParam[1] === "send-v2" + const SendComponentToRender = checkFeatureToggle('send-v2') ? SendTransactionScreen2 : SendTransactionScreen diff --git a/ui/lib/feature-toggle-utils.js b/ui/lib/feature-toggle-utils.js new file mode 100644 index 000000000..f4ff446d3 --- /dev/null +++ b/ui/lib/feature-toggle-utils.js @@ -0,0 +1,11 @@ +function checkFeatureToggle(name) { + const queryPairMap = window.location.search.substr(1).split('&') + .map(pair => pair.split('=')) + .reduce((pairs, [key, value]) => ({...pairs, [key]: value }), {}) + const featureToggles = queryPairMap['ft'] ? queryPairMap['ft'].split(',') : [] + return Boolean(featureToggles.find(ft => ft === name)) +} + +module.exports = { + checkFeatureToggle, +} \ No newline at end of file -- cgit v1.2.3 From db1258f3de88f14cd54e2b4fd1cecc62cf6361e5 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 6 Oct 2017 12:51:02 -0230 Subject: Conversion util can invert conversion rate --- ui/app/conversion-util.js | 18 ++++++++++++++++-- ui/app/send.js | 9 ++------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js index 37877d12c..20f77b35b 100644 --- a/ui/app/conversion-util.js +++ b/ui/app/conversion-util.js @@ -36,6 +36,7 @@ const BIG_NUMBER_WEI_MULTIPLIER = new BigNumber('1000000000000000000') // Individual Setters const convert = R.invoker(1, 'times') const round = R.invoker(2, 'toFormat')(R.__, BigNumber.ROUND_DOWN) +const invertConversionRate = conversionRate => () => new BigNumber(1.0).div(conversionRate) // Setter Maps const toBigNumber = { @@ -63,13 +64,23 @@ const fromAndToCurrencyPropsNotEqual = R.compose( ) // Lens -const valuePropertyLense = R.over(R.lensProp('value')) +const valuePropertyLens = R.over(R.lensProp('value')) +const conversionRateLens = R.over(R.lensProp('conversionRate')) + +// conditional conversionRate setting wrapper +const whenPredSetCRWithPropAndSetter = (pred, prop, setter) => R.when( + pred, + R.converge( + conversionRateLens, + [R.pipe(R.prop(prop), setter), R.identity] + ) +) // conditional 'value' setting wrappers const whenPredSetWithPropAndSetter = (pred, prop, setter) => R.when( pred, R.converge( - valuePropertyLense, + valuePropertyLens, [R.pipe(R.prop(prop), setter), R.identity] ) ) @@ -81,6 +92,7 @@ const whenPropApplySetterMap = (prop, setterMap) => whenPredSetWithPropAndSetter // Conversion utility function const converter = R.pipe( + whenPredSetCRWithPropAndSetter(R.prop('invertConversionRate'), 'conversionRate', invertConversionRate), whenPropApplySetterMap('fromNumericBase', toBigNumber), whenPropApplySetterMap('fromDenomination', toNormalizedDenomination), whenPredSetWithPropAndSetter(fromAndToCurrencyPropsNotEqual, 'conversionRate', convert), @@ -101,6 +113,7 @@ const conversionUtil = (value, { numberOfDecimals, conversionRate, ethToUSDRate, + invertConversionRate, }) => converter({ fromCurrency, toCurrency, @@ -111,6 +124,7 @@ const conversionUtil = (value, { numberOfDecimals, conversionRate, ethToUSDRate, + invertConversionRate, value, }); diff --git a/ui/app/send.js b/ui/app/send.js index 2e6409f32..5643d927b 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -22,7 +22,6 @@ const { const { stripHexPrefix, addHexPrefix } = require('ethereumjs-util') const { isHex, numericBalance, isValidAddress, allNull } = require('./util') const { conversionUtil, conversionGreaterThan } = require('./conversion-util') -const BigNumber = require('bignumber.js') module.exports = connect(mapStateToProps)(SendTransactionScreen) @@ -470,18 +469,14 @@ SendTransactionScreen.prototype.getAmountToSend = function (amount) { const { activeCurrency } = this.state const { conversionRate } = this.props - // TODO: need a clean way to integrate this into conversionUtil - const sendConversionRate = activeCurrency === 'ETH' - ? conversionRate - : new BigNumber(1.0).div(conversionRate) - return conversionUtil(amount, { fromNumericBase: 'dec', toNumericBase: 'hex', fromCurrency: activeCurrency, toCurrency: 'ETH', toDenomination: 'WEI', - conversionRate: sendConversionRate, + conversionRate, + invertConversionRate: activeCurrency !== 'ETH', }) } -- cgit v1.2.3 From 6f0c0e83744514c7fe70838097d96b5e3c2778ae Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Mon, 9 Oct 2017 12:12:54 -0700 Subject: Add test to look for wei precision. --- test/unit/components/bn-as-decimal-input-test.js | 36 ++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/unit/components/bn-as-decimal-input-test.js b/test/unit/components/bn-as-decimal-input-test.js index 106b3a871..d74e0fa2e 100644 --- a/test/unit/components/bn-as-decimal-input-test.js +++ b/test/unit/components/bn-as-decimal-input-test.js @@ -48,4 +48,40 @@ describe('BnInput', function () { checkValidity () { return true } }, }) }) + + it('can tolerate wei precision', function (done) { + const renderer = ReactTestUtils.createRenderer() + + let valueStr = '1000000000000000000' + + const value = new BN(valueStr, 10) + + const inputStr = '1000000000.000000001' + + let targetStr = '1000000000000000001' + + const target = new BN(targetStr, 10) + + const precision = 9 // ether precision + const scale = 9 + + const props = { + value, + scale, + precision, + onChange: (newBn) => { + assert.equal(newBn.toString(), target.toString(), 'should tolerate increase') + done() + }, + } + + const inputComponent = h(BnInput, props) + const component = additions.renderIntoDocument(inputComponent) + renderer.render(inputComponent) + const input = additions.find(component, 'input.hex-input')[0] + ReactTestUtils.Simulate.change(input, { preventDefault () {}, target: { + value: inputStr, + checkValidity () { return true } }, + }) + }) }) -- cgit v1.2.3 From c12d56063da5ed533ba63cf6e0843631659de0d3 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Mon, 9 Oct 2017 13:01:58 -0700 Subject: Fix to actually fail in earlier versions. --- test/unit/components/bn-as-decimal-input-test.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/unit/components/bn-as-decimal-input-test.js b/test/unit/components/bn-as-decimal-input-test.js index d74e0fa2e..81a8caa45 100644 --- a/test/unit/components/bn-as-decimal-input-test.js +++ b/test/unit/components/bn-as-decimal-input-test.js @@ -48,21 +48,21 @@ describe('BnInput', function () { checkValidity () { return true } }, }) }) - + it('can tolerate wei precision', function (done) { const renderer = ReactTestUtils.createRenderer() - let valueStr = '1000000000000000000' + let valueStr = '1000000000' const value = new BN(valueStr, 10) + const inputStr = '1.000000001' - const inputStr = '1000000000.000000001' - let targetStr = '1000000000000000001' + let targetStr = '1000000001' const target = new BN(targetStr, 10) - const precision = 9 // ether precision + const precision = 9 // gwei precision const scale = 9 const props = { @@ -71,6 +71,8 @@ describe('BnInput', function () { precision, onChange: (newBn) => { assert.equal(newBn.toString(), target.toString(), 'should tolerate increase') + const reInput = BnInput.prototype.downsize(newBn.toString(), 9, 9) + assert.equal(reInput.toString(), target.toString(), 'should tolerate increase') done() }, } -- cgit v1.2.3 From d82d9215fbe593293a6badc523218878cfb13dd2 Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Mon, 9 Oct 2017 13:02:52 -0700 Subject: Make modification --- test/unit/components/bn-as-decimal-input-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/components/bn-as-decimal-input-test.js b/test/unit/components/bn-as-decimal-input-test.js index 81a8caa45..58ecc9c89 100644 --- a/test/unit/components/bn-as-decimal-input-test.js +++ b/test/unit/components/bn-as-decimal-input-test.js @@ -72,7 +72,7 @@ describe('BnInput', function () { onChange: (newBn) => { assert.equal(newBn.toString(), target.toString(), 'should tolerate increase') const reInput = BnInput.prototype.downsize(newBn.toString(), 9, 9) - assert.equal(reInput.toString(), target.toString(), 'should tolerate increase') + assert.equal(reInput.toString(), inputStr, 'should tolerate increase') done() }, } -- cgit v1.2.3 From 80463072b5c0c9e826582e066fbc962b667ee355 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 6 Oct 2017 02:04:00 -0230 Subject: UI for readonly from component. From dropdown opening and closing. Mockdata. --- ui/app/components/send/from-dropdown.js | 94 ++++++++++++++++++++++++++++++++ ui/app/css/itcss/components/send.scss | 94 ++++++++++++++++++++++++++++++++ ui/app/css/itcss/settings/variables.scss | 1 + ui/app/send-v2.js | 38 ++++++++++++- 4 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 ui/app/components/send/from-dropdown.js diff --git a/ui/app/components/send/from-dropdown.js b/ui/app/components/send/from-dropdown.js new file mode 100644 index 000000000..c438cefd5 --- /dev/null +++ b/ui/app/components/send/from-dropdown.js @@ -0,0 +1,94 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Identicon = require('../identicon') + +module.exports = FromDropdown + +inherits(FromDropdown, Component) +function FromDropdown () { + Component.call(this) +} + +FromDropdown.prototype.renderSingleIdentity = function ( + account, + handleClick, + inList = false, + selectedIdentity = {} +) { + const { identity, balancesToRender } = account + const { name, address } = identity + const { primary, secondary } = balancesToRender + + const iconType = inList ? 'check' : 'caret-down' + const showIcon = !inList || address === selectedIdentity.address + + return h('div.send-v2__from-dropdown__account', { + onClick: () => handleClick(identity), + }, [ + + h('div.send-v2__from-dropdown__top-row', {}, [ + + h( + Identicon, + { + address, + diameter: 18, + className: 'send-v2__from-dropdown__identicon', + }, + ), + + h('div.send-v2__from-dropdown__account-name', {}, name), + + showIcon && h(`i.fa.fa-${iconType}.fa-lg.send-v2__from-dropdown__${iconType}`), + + ]), + + h('div.send-v2__from-dropdown__account-primary-balance', {}, primary), + + h('div.send-v2__from-dropdown__account-secondary-balance', {}, secondary), + + ]) +} + +FromDropdown.prototype.renderDropdown = function (identities, selectedIdentity, closeDropdown) { + return h('div', {}, [ + + h('div.send-v2__from-dropdown__close-area', { + onClick: closeDropdown, + }), + + h('div.send-v2__from-dropdown__list', {}, [ + + ...identities.map(identity => this.renderSingleIdentity( + identity, + () => console.log('Select identity'), + true, + selectedIdentity + )) + + ]), + + ]) +} + +FromDropdown.prototype.render = function () { + const { + identities, + selectedIdentity, + setFromField, + openDropdown, + closeDropdown, + dropdownOpen, + } = this.props + + return h('div.send-v2__from-dropdown', {}, [ + + this.renderSingleIdentity(selectedIdentity, openDropdown), + + dropdownOpen && this.renderDropdown(identities, selectedIdentity.identity, closeDropdown), + + ]) + +} + diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index 72a01dc89..ddd22f9fd 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -495,6 +495,100 @@ width: 287px; } + &__form { + display: flex; + flex-direction: column; + margin-top: 13px; + width: 100%; + } + + &__form-row { + margin: 14.5px 18px 0px; + display: flex; + position: relative; + justify-content: space-between; + } + + &__form-label { + color: $scorpion; + font-family: Roboto; + font-size: 16px; + line-height: 22px; + margin-top: 16px; + } + + &__from-dropdown { + height: 73px; + width: 240px; + border: 1px solid $alto; + border-radius: 4px; + background-color: $white; + font-family: Roboto; + line-height: 16px; + font-size: 12px; + color: $tundora; + + &__top-row { + display: flex; + margin-top: 10px; + margin-left: 8px; + position: relative; + } + + &__account-name { + font-size: 16px; + margin-left: 8px; + } + + &__caret-down, + &__check { + position: absolute; + right: 12px; + top: 1px; + } + + &__caret-down { + color: $alto; + } + + &__check { + color: $caribbean-green; + } + + &__account-primary-balance { + margin-left: 34px; + margin-top: 4px; + } + + &__account-secondary-balance { + margin-left: 34px; + color: $dusty-gray; + } + + &__close-area { + position: fixed; + top: 0; + left: 0; + z-index: 1000; + width: 100%; + height: 100%; + } + + &__list { + z-index: 1050; + position: absolute; + height: 220px; + width: 240px; + border: 1px solid $geyser; + border-radius: 4px; + background-color: $white; + box-shadow: 0 3px 6px 0 rgba(0 ,0 ,0 ,.11); + margin-top: 11px; + margin-left: -1px; + overflow-y: scroll; + } + } + &__footer { height: 92px; width: 100%; diff --git a/ui/app/css/itcss/settings/variables.scss b/ui/app/css/itcss/settings/variables.scss index b0ef86075..764d9c179 100644 --- a/ui/app/css/itcss/settings/variables.scss +++ b/ui/app/css/itcss/settings/variables.scss @@ -42,6 +42,7 @@ $tulip-tree: #ebb33f; $malibu-blue: #7ac9fd; $athens-grey: #e9edf0; $jaffa: #f28930; +$geyser: #d2d8dd; /* Z-Indicies diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index f423b90ff..53e63b784 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -3,11 +3,24 @@ const PersistentForm = require('../lib/persistent-form') const h = require('react-hyperscript') const connect = require('react-redux').connect const Identicon = require('./components/identicon') +const FromDropdown = require('./components/send/from-dropdown') module.exports = connect(mapStateToProps)(SendTransactionScreen) function mapStateToProps (state) { - return {} + const mockIdentities = Array.from(new Array(5)) + .map((v, i) => ({ + identity: { + name: `Test Account Name ${i}`, + address: `0x02f567704cc6569127e18e3d00d2c85bcbfa6f0${i}`, + }, + balancesToRender: { + primary: `100${i}.000001 ETH`, + secondary: `$30${i},000.00 USD`, + } + })) + + return { identities: mockIdentities } } inherits(SendTransactionScreen, PersistentForm) @@ -25,10 +38,14 @@ function SendTransactionScreen () { txData: null, memo: '', }, + dropdownOpen: false, } } SendTransactionScreen.prototype.render = function () { + const { identities } = this.props + const { dropdownOpen } = this.state + return ( h('div.send-v2__container', [ @@ -50,6 +67,25 @@ SendTransactionScreen.prototype.render = function () { h('div.send-v2__copy', 'Sending to a different crytpocurrency that is not Ethereum may result in permanent loss.'), + h('div.send-v2__form', {}, [ + + h('div.send-v2__form-row', [ + + h('div.send-v2__form-label', 'From:'), + + h(FromDropdown, { + dropdownOpen, + identities, + selectedIdentity: identities[0], + setFromField: () => console.log('Set From Field'), + openDropdown: () => this.setState({ dropdownOpen: true }), + closeDropdown: () => this.setState({ dropdownOpen: false }), + }), + + ]) + + ]), + // Buttons underneath card h('div.send-v2__footer', [ h('button.send-v2__cancel-btn', {}, 'Cancel'), -- cgit v1.2.3 From 71d6463984f040b2aa495a13429f6ea3505defaf Mon Sep 17 00:00:00 2001 From: Dan Date: Mon, 9 Oct 2017 20:47:52 -0230 Subject: Refactor 'rendersingleidentity' to a stand alone account-list-item component. --- ui/app/components/send/account-list-item.js | 51 +++++++++++++++++ ui/app/components/send/from-dropdown.js | 69 +++++++---------------- ui/app/css/itcss/components/account-dropdown.scss | 29 ++++++++++ ui/app/css/itcss/components/send.scss | 37 ------------ ui/app/send-v2.js | 11 ++-- 5 files changed, 105 insertions(+), 92 deletions(-) create mode 100644 ui/app/components/send/account-list-item.js diff --git a/ui/app/components/send/account-list-item.js b/ui/app/components/send/account-list-item.js new file mode 100644 index 000000000..b11527d95 --- /dev/null +++ b/ui/app/components/send/account-list-item.js @@ -0,0 +1,51 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const Identicon = require('../identicon') + +inherits(AccountListItem, Component) +function AccountListItem () { + Component.call(this) +} + +module.exports = AccountListItem + +AccountListItem.prototype.render = function () { + const { + account, + handleClick, + icon = null, + } = this.props + + const { identity, balancesToRender } = account + const { name, address } = identity + const { primary, secondary } = balancesToRender + + return h('div.account-list-item', { + onClick: () => handleClick(identity), + }, [ + + h('div.account-list-item__top-row', {}, [ + + h( + Identicon, + { + address, + diameter: 18, + className: 'account-list-item__identicon', + }, + ), + + h('div.account-list-item__account-name', {}, name), + + icon && h('div.account-list-item__icon', [icon]), + + ]), + + h('div.account-list-item__account-primary-balance', {}, primary), + + h('div.account-list-item__account-secondary-balance', {}, secondary), + + ]) +} \ No newline at end of file diff --git a/ui/app/components/send/from-dropdown.js b/ui/app/components/send/from-dropdown.js index c438cefd5..fb0a00cc2 100644 --- a/ui/app/components/send/from-dropdown.js +++ b/ui/app/components/send/from-dropdown.js @@ -2,6 +2,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits const Identicon = require('../identicon') +const AccountListItem = require('./account-list-item') module.exports = FromDropdown @@ -10,48 +11,15 @@ function FromDropdown () { Component.call(this) } -FromDropdown.prototype.renderSingleIdentity = function ( - account, - handleClick, - inList = false, - selectedIdentity = {} -) { - const { identity, balancesToRender } = account - const { name, address } = identity - const { primary, secondary } = balancesToRender +FromDropdown.prototype.getListItemIcon = function (currentAccount, selectedAccount) { + const listItemIcon = h(`i.fa.fa-check.fa-lg`, { style: { color: '#02c9b1' } }) - const iconType = inList ? 'check' : 'caret-down' - const showIcon = !inList || address === selectedIdentity.address - - return h('div.send-v2__from-dropdown__account', { - onClick: () => handleClick(identity), - }, [ - - h('div.send-v2__from-dropdown__top-row', {}, [ - - h( - Identicon, - { - address, - diameter: 18, - className: 'send-v2__from-dropdown__identicon', - }, - ), - - h('div.send-v2__from-dropdown__account-name', {}, name), - - showIcon && h(`i.fa.fa-${iconType}.fa-lg.send-v2__from-dropdown__${iconType}`), - - ]), - - h('div.send-v2__from-dropdown__account-primary-balance', {}, primary), - - h('div.send-v2__from-dropdown__account-secondary-balance', {}, secondary), - - ]) + return currentAccount.identity.address === selectedAccount.identity.address + ? listItemIcon + : null } -FromDropdown.prototype.renderDropdown = function (identities, selectedIdentity, closeDropdown) { +FromDropdown.prototype.renderDropdown = function (accounts, selectedAccount, closeDropdown) { return h('div', {}, [ h('div.send-v2__from-dropdown__close-area', { @@ -60,12 +28,11 @@ FromDropdown.prototype.renderDropdown = function (identities, selectedIdentity, h('div.send-v2__from-dropdown__list', {}, [ - ...identities.map(identity => this.renderSingleIdentity( - identity, - () => console.log('Select identity'), - true, - selectedIdentity - )) + ...accounts.map(account => h(AccountListItem, { + account, + handleClick: () => console.log('Select identity'), + icon: this.getListItemIcon(account, selectedAccount), + })) ]), @@ -74,8 +41,8 @@ FromDropdown.prototype.renderDropdown = function (identities, selectedIdentity, FromDropdown.prototype.render = function () { const { - identities, - selectedIdentity, + accounts, + selectedAccount, setFromField, openDropdown, closeDropdown, @@ -84,9 +51,13 @@ FromDropdown.prototype.render = function () { return h('div.send-v2__from-dropdown', {}, [ - this.renderSingleIdentity(selectedIdentity, openDropdown), + h(AccountListItem, { + account: selectedAccount, + handleClick: openDropdown, + icon: h(`i.fa.fa-caret-down.fa-lg`, { style: { color: '#dedede' } }) + }), - dropdownOpen && this.renderDropdown(identities, selectedIdentity.identity, closeDropdown), + dropdownOpen && this.renderDropdown(accounts, selectedAccount, closeDropdown), ]) diff --git a/ui/app/css/itcss/components/account-dropdown.scss b/ui/app/css/itcss/components/account-dropdown.scss index 1c4620e40..42f02d84d 100644 --- a/ui/app/css/itcss/components/account-dropdown.scss +++ b/ui/app/css/itcss/components/account-dropdown.scss @@ -15,3 +15,32 @@ color: $white; } } + +.account-list-item { + &__top-row { + display: flex; + margin-top: 10px; + margin-left: 8px; + position: relative; + } + + &__account-name { + font-size: 16px; + margin-left: 8px; + } + + &__icon { + position: absolute; + right: 12px; + top: 1px; + } + &__account-primary-balance { + margin-left: 34px; + margin-top: 4px; + } + + &__account-secondary-balance { + margin-left: 34px; + color: $dusty-gray; + } +} diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index ddd22f9fd..dfeb83a0a 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -528,43 +528,6 @@ font-size: 12px; color: $tundora; - &__top-row { - display: flex; - margin-top: 10px; - margin-left: 8px; - position: relative; - } - - &__account-name { - font-size: 16px; - margin-left: 8px; - } - - &__caret-down, - &__check { - position: absolute; - right: 12px; - top: 1px; - } - - &__caret-down { - color: $alto; - } - - &__check { - color: $caribbean-green; - } - - &__account-primary-balance { - margin-left: 34px; - margin-top: 4px; - } - - &__account-secondary-balance { - margin-left: 34px; - color: $dusty-gray; - } - &__close-area { position: fixed; top: 0; diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index 53e63b784..e0d7c2394 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -2,13 +2,12 @@ const { inherits } = require('util') const PersistentForm = require('../lib/persistent-form') const h = require('react-hyperscript') const connect = require('react-redux').connect -const Identicon = require('./components/identicon') const FromDropdown = require('./components/send/from-dropdown') module.exports = connect(mapStateToProps)(SendTransactionScreen) function mapStateToProps (state) { - const mockIdentities = Array.from(new Array(5)) + const mockAccounts = Array.from(new Array(5)) .map((v, i) => ({ identity: { name: `Test Account Name ${i}`, @@ -20,7 +19,7 @@ function mapStateToProps (state) { } })) - return { identities: mockIdentities } + return { accounts: mockAccounts } } inherits(SendTransactionScreen, PersistentForm) @@ -43,7 +42,7 @@ function SendTransactionScreen () { } SendTransactionScreen.prototype.render = function () { - const { identities } = this.props + const { accounts } = this.props const { dropdownOpen } = this.state return ( @@ -75,8 +74,8 @@ SendTransactionScreen.prototype.render = function () { h(FromDropdown, { dropdownOpen, - identities, - selectedIdentity: identities[0], + accounts, + selectedAccount: accounts[0], setFromField: () => console.log('Set From Field'), openDropdown: () => this.setState({ dropdownOpen: true }), closeDropdown: () => this.setState({ dropdownOpen: false }), -- cgit v1.2.3 From 24a55cf7770a6154fe723cf13cdc9998e1759f3b Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Tue, 10 Oct 2017 08:36:15 -0700 Subject: Make the function callback friendly. --- app/scripts/platforms/extension.js | 8 ++++---- ui/app/config.js | 8 ++++++-- ui/app/reducers.js | 7 +++++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index 61d67e1b4..2f47512eb 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -19,13 +19,13 @@ class ExtensionPlatform { getPlatformInfo (cb) { try { - return extension.runtime.getPlatformInfo(cb) + extension.runtime.getPlatformInfo((platform) => { + cb(null, platform) + }) } catch (e) { - log.debug(e) - return undefined + cb(e) } } - } module.exports = ExtensionPlatform diff --git a/ui/app/config.js b/ui/app/config.js index 75c3bcf13..c14fa1d28 100644 --- a/ui/app/config.js +++ b/ui/app/config.js @@ -113,8 +113,12 @@ ConfigScreen.prototype.render = function () { alignSelf: 'center', }, onClick (event) { - window.logStateString((result) => { - exportAsFile('MetaMask State Logs', result) + window.logStateString((err, result) => { + if (err) { + state.dispatch(actions.displayWarning('Error in retrieving state logs.')) + } else { + exportAsFile('MetaMask State Logs', result) + } }) }, }, 'Download State Logs'), diff --git a/ui/app/reducers.js b/ui/app/reducers.js index 0af7ee81c..3d0a58f81 100644 --- a/ui/app/reducers.js +++ b/ui/app/reducers.js @@ -45,12 +45,15 @@ window.logStateString = function (cb) { let state = window.METAMASK_CACHED_LOG_STATE const version = global.platform.getVersion() const browser = window.navigator.userAgent - return global.platform.getPlatformInfo((platform) => { + return global.platform.getPlatformInfo((err, platform) => { + if (err) { + return cb(err) + } state.version = version state.platform = platform state.browser = browser let stateString = JSON.stringify(state, removeSeedWords, 2) - return cb(stateString) + return cb(null, stateString) }) } -- cgit v1.2.3 From a387def701303e56f721fdc7c716e72641bfaf8f Mon Sep 17 00:00:00 2001 From: Kevin Serrano Date: Tue, 10 Oct 2017 08:50:59 -0700 Subject: Changelog addition. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8bb48e9f..c8f5b3115 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Add new support for new eth_signTypedData method per EIP 712. - Fix bug where some transactions would be shown as pending forever, even after successfully mined. - Fix bug where a transaction might be shown as pending forever if another tx with the same nonce was mined. +- Add OS and browser version information to state log dump (for debugging purposes only). ## 3.10.9 2017-10-5 -- cgit v1.2.3 From e79037261ec4b232299dbef14e6c30fc46c48ac7 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 10 Oct 2017 10:26:59 -0700 Subject: metamask controller - breakout getAccounts method --- app/scripts/metamask-controller.js | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 727f48f1c..840012e81 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -225,19 +225,9 @@ module.exports = class MetamaskController extends EventEmitter { web3_clientVersion: `MetaMask/v${version}`, }, // account mgmt - getAccounts: (cb) => { - const isUnlocked = this.keyringController.memStore.getState().isUnlocked - const result = [] - const selectedAddress = this.preferencesController.getSelectedAddress() - - // only show address if account is unlocked - if (isUnlocked && selectedAddress) { - result.push(selectedAddress) - } - cb(null, result) - }, + getAccounts: nodeify(this.getAccounts, this), // tx signing - processTransaction: nodeify(async (txParams) => await this.txController.newUnapprovedTransaction(txParams), this), + processTransaction: nodeify(this.txController.newUnapprovedTransaction, this.txController), // old style msg signing processMessage: this.newUnsignedMessage.bind(this), // personal_sign msg signing @@ -483,6 +473,18 @@ module.exports = class MetamaskController extends EventEmitter { // Opinionated Keyring Management // + async getAccounts () { + const isUnlocked = this.keyringController.memStore.getState().isUnlocked + const result = [] + const selectedAddress = this.preferencesController.getSelectedAddress() + + // only show address if account is unlocked + if (isUnlocked && selectedAddress) { + result.push(selectedAddress) + } + return result + }, + addNewAccount (cb) { const primaryKeyring = this.keyringController.getKeyringsByType('HD Key Tree')[0] if (!primaryKeyring) return cb(new Error('MetamaskController - No HD Key Tree found')) -- cgit v1.2.3 From f7c1bc804d12cfae4ff99b958b793a6fb68f4aa0 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 10 Oct 2017 10:39:31 -0700 Subject: metamask controller - simplify provider init --- app/scripts/metamask-controller.js | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 840012e81..df5784571 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -81,8 +81,22 @@ module.exports = class MetamaskController extends EventEmitter { }) this.blacklistController.scheduleUpdates() - // rpc provider - this.provider = this.initializeProvider() + // rpc provider and block tracker + this.provider = this.networkController.initializeProvider({ + static: { + eth_syncing: false, + web3_clientVersion: `MetaMask/v${version}`, + }, + // account mgmt + getAccounts: nodeify(this.getAccounts, this), + // tx signing + processTransaction: nodeify(this.txController.newUnapprovedTransaction, this.txController), + // old style msg signing + processMessage: this.newUnsignedMessage.bind(this), + // personal_sign msg signing + processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), + processTypedMessage: this.newUnsignedTypedMessage.bind(this), + }) this.blockTracker = this.provider._blockTracker // eth data query tools @@ -218,26 +232,6 @@ module.exports = class MetamaskController extends EventEmitter { // Constructor helpers // - initializeProvider () { - const providerOpts = { - static: { - eth_syncing: false, - web3_clientVersion: `MetaMask/v${version}`, - }, - // account mgmt - getAccounts: nodeify(this.getAccounts, this), - // tx signing - processTransaction: nodeify(this.txController.newUnapprovedTransaction, this.txController), - // old style msg signing - processMessage: this.newUnsignedMessage.bind(this), - // personal_sign msg signing - processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), - processTypedMessage: this.newUnsignedTypedMessage.bind(this), - } - const providerProxy = this.networkController.initializeProvider(providerOpts) - return providerProxy - } - initPublicConfigStore () { // get init state const publicConfigStore = new ObservableStore() -- cgit v1.2.3 From ff4e9a0d1122db83221bc956f11c9520bf0e008c Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 10 Oct 2017 10:50:45 -0700 Subject: metamask controller - define this.newTransaction to ease instantiation order --- app/scripts/metamask-controller.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index df5784571..1292d2a1e 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -90,7 +90,7 @@ module.exports = class MetamaskController extends EventEmitter { // account mgmt getAccounts: nodeify(this.getAccounts, this), // tx signing - processTransaction: nodeify(this.txController.newUnapprovedTransaction, this.txController), + processTransaction: nodeify(this.newTransaction, this), // old style msg signing processMessage: this.newUnsignedMessage.bind(this), // personal_sign msg signing @@ -525,6 +525,11 @@ module.exports = class MetamaskController extends EventEmitter { // Identity Management // + // this function wrappper lets us pass the fn reference before txController is instantiated + async newTransaction (txParams) { + return await this.txController.newUnapprovedTransaction(txParams) + } + newUnsignedMessage (msgParams, cb) { const msgId = this.messageManager.addUnapprovedMessage(msgParams) this.sendUpdate() -- cgit v1.2.3 From efa92a7fc5925533f72e876c9bf84df0a6258d4a Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 10 Oct 2017 14:13:12 -0700 Subject: network controller - refactor to use eth-rpc-client --- app/scripts/controllers/network.js | 56 ++++++++++++++++++++------------------ app/scripts/metamask-controller.js | 7 +++-- package.json | 2 ++ 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js index 0f9db4d53..412967dbe 100644 --- a/app/scripts/controllers/network.js +++ b/app/scripts/controllers/network.js @@ -5,6 +5,7 @@ const ObservableStore = require('obs-store') const ComposedStore = require('obs-store/lib/composed') const extend = require('xtend') const EthQuery = require('eth-query') +const createEthRpcClient = require('eth-rpc-client') const createEventEmitterProxy = require('../lib/events-proxy.js') const RPC_ADDRESS_LIST = require('../config.js').network const DEFAULT_RPC = RPC_ADDRESS_LIST['rinkeby'] @@ -17,7 +18,8 @@ module.exports = class NetworkController extends EventEmitter { this.networkStore = new ObservableStore('loading') this.providerStore = new ObservableStore(config.provider) this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore }) - this._proxy = createEventEmitterProxy() + this.providerProxy = createEventEmitterProxy() + this.blockTrackerProxy = createEventEmitterProxy() this.on('networkDidChange', this.lookupNetwork) } @@ -25,12 +27,11 @@ module.exports = class NetworkController extends EventEmitter { initializeProvider (_providerParams) { this._baseProviderParams = _providerParams const rpcUrl = this.getCurrentRpcAddress() - this._configureStandardProvider({ rpcUrl }) - this._proxy.on('block', this._logBlock.bind(this)) - this._proxy.on('error', this.verifyNetwork.bind(this)) - this.ethQuery = new EthQuery(this._proxy) + this._configureStandardClient({ rpcUrl }) + this.providerProxy.on('block', this._logBlock.bind(this)) + this.providerProxy.on('error', this.verifyNetwork.bind(this)) + this.ethQuery = new EthQuery(this.providerProxy) this.lookupNetwork() - return this._proxy } verifyNetwork () { @@ -76,8 +77,10 @@ module.exports = class NetworkController extends EventEmitter { assert(type !== 'rpc', `NetworkController.setProviderType - cannot connect by type "rpc"`) // skip if type already matches if (type === this.getProviderConfig().type) return + // lookup rpcTarget for type const rpcTarget = this.getRpcAddressForType(type) assert(rpcTarget, `NetworkController - unknown rpc address for type "${type}"`) + // update connection this.providerStore.updateState({ type, rpcTarget }) this._switchNetwork({ rpcUrl: rpcTarget }) } @@ -97,32 +100,33 @@ module.exports = class NetworkController extends EventEmitter { _switchNetwork (providerParams) { this.setNetworkState('loading') - this._configureStandardProvider(providerParams) + this._configureStandardClient(providerParams) this.emit('networkDidChange') } - _configureStandardProvider(_providerParams) { + _configureStandardClient(_providerParams) { const providerParams = extend(this._baseProviderParams, _providerParams) - const provider = createMetamaskProvider(providerParams) - this._setProvider(provider) - } - - _setProvider (provider) { - // collect old block tracker events - const oldProvider = this._provider - let blockTrackerHandlers - if (oldProvider) { - // capture old block handlers - blockTrackerHandlers = oldProvider._blockTracker.proxyEventHandlers - // tear down - oldProvider.removeAllListeners() - oldProvider.stop() + const client = createEthRpcClient(providerParams) + this._setClient(client) + } + + _createMetamaskProvider(providerParams) { + const { provider, blockTracker } = createEthRpcClient(providerParams) + } + + _setClient (newClient) { + // teardown old client + const oldClient = this._currentClient + if (oldClient) { + oldClient.blockTracker.stop() + // asyncEventEmitter lacks a "removeAllListeners" method + // oldClient.blockTracker.removeAllListeners + oldClient.blockTracker._events = {} } - // override block tracler - provider._blockTracker = createEventEmitterProxy(provider._blockTracker, blockTrackerHandlers) // set as new provider - this._provider = provider - this._proxy.setTarget(provider) + this._currentClient = newClient + this.providerProxy.setTarget(newClient.provider) + this.blockTrackerProxy.setTarget(newClient.blockTracker) } _logBlock (block) { diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 1292d2a1e..67bbdc15a 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -82,8 +82,8 @@ module.exports = class MetamaskController extends EventEmitter { this.blacklistController.scheduleUpdates() // rpc provider and block tracker - this.provider = this.networkController.initializeProvider({ - static: { + this.networkController.initializeProvider({ + scaffold: { eth_syncing: false, web3_clientVersion: `MetaMask/v${version}`, }, @@ -97,7 +97,8 @@ module.exports = class MetamaskController extends EventEmitter { processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), processTypedMessage: this.newUnsignedTypedMessage.bind(this), }) - this.blockTracker = this.provider._blockTracker + this.provider = this.networkController.providerProxy + this.blockTracker = this.networkController.blockTrackerProxy // eth data query tools this.ethQuery = new EthQuery(this.provider) diff --git a/package.json b/package.json index 225742487..3a6be4c61 100644 --- a/package.json +++ b/package.json @@ -71,9 +71,11 @@ "eth-contract-metadata": "^1.1.4", "eth-hd-keyring": "^1.2.1", "eth-json-rpc-filters": "^1.2.2", + "eth-json-rpc-middleware": "^1.4.1", "eth-keyring-controller": "^2.1.0", "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", + "eth-rpc-client": "^1.0.0", "eth-sig-util": "^1.4.0", "eth-simple-keyring": "^1.1.1", "eth-token-tracker": "^1.1.4", -- cgit v1.2.3 From 4d273d3ceade861d24dceed96a0f5d5f3dc229ae Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 10 Oct 2017 14:14:43 -0700 Subject: lint fixes --- app/scripts/controllers/network.js | 9 ++------- app/scripts/metamask-controller.js | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js index 412967dbe..f4665baf8 100644 --- a/app/scripts/controllers/network.js +++ b/app/scripts/controllers/network.js @@ -1,6 +1,5 @@ const assert = require('assert') const EventEmitter = require('events') -const createMetamaskProvider = require('web3-provider-engine/zero.js') const ObservableStore = require('obs-store') const ComposedStore = require('obs-store/lib/composed') const extend = require('xtend') @@ -77,10 +76,10 @@ module.exports = class NetworkController extends EventEmitter { assert(type !== 'rpc', `NetworkController.setProviderType - cannot connect by type "rpc"`) // skip if type already matches if (type === this.getProviderConfig().type) return - // lookup rpcTarget for type + // lookup rpcTarget for typecreateMetamaskProvider const rpcTarget = this.getRpcAddressForType(type) assert(rpcTarget, `NetworkController - unknown rpc address for type "${type}"`) - // update connection + // update connectioncreateMetamaskProvider this.providerStore.updateState({ type, rpcTarget }) this._switchNetwork({ rpcUrl: rpcTarget }) } @@ -110,10 +109,6 @@ module.exports = class NetworkController extends EventEmitter { this._setClient(client) } - _createMetamaskProvider(providerParams) { - const { provider, blockTracker } = createEthRpcClient(providerParams) - } - _setClient (newClient) { // teardown old client const oldClient = this._currentClient diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 67bbdc15a..a742f3cba 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -478,7 +478,7 @@ module.exports = class MetamaskController extends EventEmitter { result.push(selectedAddress) } return result - }, + } addNewAccount (cb) { const primaryKeyring = this.keyringController.getKeyringsByType('HD Key Tree')[0] -- cgit v1.2.3 From 119c2b24238b84d5a9e3beabe572da42f8e2ffcb Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Tue, 10 Oct 2017 14:16:57 -0700 Subject: Confirm eth v2 --- ui/app/components/pending-tx/confirm-send-ether.js | 38 ++--- ui/app/css/itcss/components/confirm.scss | 154 ++++++++++------- ui/app/css/itcss/settings/typography.scss | 2 +- ui/app/css/itcss/settings/variables.scss | 1 - ui/lib/feature-toggle-utils.js | 4 +- yarn.lock | 185 ++++++++------------- 6 files changed, 184 insertions(+), 200 deletions(-) diff --git a/ui/app/components/pending-tx/confirm-send-ether.js b/ui/app/components/pending-tx/confirm-send-ether.js index 330a55cce..537a9a659 100644 --- a/ui/app/components/pending-tx/confirm-send-ether.js +++ b/ui/app/components/pending-tx/confirm-send-ether.js @@ -192,7 +192,7 @@ ConfirmSendEther.prototype.render = function () { this.inputs = [] return ( - h('div.flex-column.flex-grow.confirm-screen-container', { + h('div.confirm-screen-container', { style: { minWidth: '355px' }, }, [ // Main Send token Card @@ -202,6 +202,7 @@ ConfirmSendEther.prototype.render = function () { onClick: () => backToAccountDetail(selectedAddress), }, 'BACK'), h('div.confirm-screen-title', 'Confirm Transaction'), + h('div.confirm-screen-header-tip'), ]), h('div.flex-row.flex-center.confirm-screen-identicons', [ h('div.confirm-screen-account-wrapper', [ @@ -209,11 +210,11 @@ ConfirmSendEther.prototype.render = function () { Identicon, { address: fromAddress, - diameter: 100, + diameter: 60, }, ), h('span.confirm-screen-account-name', fromName), - h('span.confirm-screen-account-number', fromAddress.slice(fromAddress.length - 4)), + // h('span.confirm-screen-account-number', fromAddress.slice(fromAddress.length - 4)), ]), h('i.fa.fa-arrow-right.fa-lg'), h('div.confirm-screen-account-wrapper', [ @@ -221,27 +222,27 @@ ConfirmSendEther.prototype.render = function () { Identicon, { address: txParams.to, - diameter: 100, + diameter: 60, }, ), h('span.confirm-screen-account-name', toName), - h('span.confirm-screen-account-number', toAddress.slice(toAddress.length - 4)), + // h('span.confirm-screen-account-number', toAddress.slice(toAddress.length - 4)), ]), ]), - h('h3.flex-center.confirm-screen-sending-to-message', { - style: { - textAlign: 'center', - fontSize: '16px', - }, - }, [ - `You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`, - ]), + // h('h3.flex-center.confirm-screen-sending-to-message', { + // style: { + // textAlign: 'center', + // fontSize: '16px', + // }, + // }, [ + // `You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`, + // ]), h('h3.flex-center.confirm-screen-send-amount', [`$${amountInUSD}`]), h('h3.flex-center.confirm-screen-send-amount-currency', [ 'USD' ]), h('div.flex-center.confirm-memo-wrapper', [ - h('h3.confirm-screen-send-memo', [ memo ]), + h('h3.confirm-screen-send-memo', [ memo ? `"${memo}"` : '' ]), ]), h('div.confirm-screen-rows', [ @@ -365,17 +366,16 @@ ConfirmSendEther.prototype.render = function () { // } ]), - h('form#pending-tx-form.flex-column.flex-center', { + h('form#pending-tx-form', { onSubmit: this.onSubmit, }, [ - - // Accept Button - h('button.confirm-screen-confirm-button', ['CONFIRM']), - // Cancel Button h('div.cancel.btn-light.confirm-screen-cancel-button', { onClick: (event) => this.cancel(event, txMeta), }, 'CANCEL'), + + // Accept Button + h('button.confirm-screen-confirm-button', ['CONFIRM']), ]), ]) ) diff --git a/ui/app/css/itcss/components/confirm.scss b/ui/app/css/itcss/components/confirm.scss index 36d1bdd9a..dc642c2bc 100644 --- a/ui/app/css/itcss/components/confirm.scss +++ b/ui/app/css/itcss/components/confirm.scss @@ -1,32 +1,43 @@ .confirm-screen-container { - position: absolute; + position: relative; align-items: center; + font-family: Roboto; + flex: 0 0 auto; + flex-flow: column nowrap; + box-shadow: 0 2px 4px 0 rgba($black, .08); + border-radius: 8px; @media screen and (max-width: 575px) { - margin-top: 35px; width: 100%; } @media screen and (min-width: 576px) { - margin-top: 6.9vh; + top: -26px; } } .confirm-screen-wrapper { + height: 100%; + width: 380px; + background-color: $white; display: flex; - flex-direction: column; + flex-flow: column nowrap; + z-index: 25; align-items: center; - z-index: 100; - top: 5%; - font-family: 'DIN NEXT'; - background: $white; - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .08); - // padding: 20px 24px 32px; - color: $scorpion; - width: 100%; + font-family: Roboto; + position: relative; + overflow-y: auto; + overflow-x: hidden; + border-top-left-radius: 8px; + border-top-right-radius: 8px; - @media screen and (min-width: $break-large) { - width: 498px; + @media screen and (max-width: $break-small) { + width: 100%; + overflow-x: hidden; + overflow-y: auto; + top: 0; + box-shadow: none; + height: calc(100vh - 41px - 100px); } } @@ -39,27 +50,34 @@ margin: 0; } -.confirm-screen-wrapper > .confirm-screen-header { - - @media screen and (max-width: $break-small) { - margin-left: 8px; - } -} - .confirm-screen-header { - font-size: 26px; + height: 88px; + background-color: $athens-grey; position: relative; display: flex; - flex-flow: row nowrap; + justify-content: center; align-items: center; + font-size: 22px; + line-height: 29px; width: 100%; - padding: 20px 24px 0; + padding: 25px 0; + flex: 0 0 auto; @media screen and (max-width: $break-small) { font-size: 22px; } } +.confirm-screen-header-tip { + height: 25px; + width: 25px; + background: $athens-grey; + position: absolute; + transform: rotate(45deg); + left: 178px; + top: 71px; +} + .confirm-screen-title { line-height: 27px; @@ -70,12 +88,12 @@ } .confirm-screen-back-button { - background: $white; - border: 1px solid $dusty-gray; + background: transparent; + border: 1px solid $curious-blue; left: 24px; position: absolute; text-align: center; - color: $black; + color: $curious-blue; padding: 6px 13px 7px 12px; border-radius: 2px; height: 30px; @@ -93,14 +111,15 @@ .confirm-screen-account-name { margin-top: 12px; + font-size: 14px; + line-height: 19px; + color: $scorpion; + text-align: center; } -.confirm-screen-account-name, .confirm-screen-row-info { font-size: 16px; - line-height: 24px; - color: $scorpion; - text-align: center; + line-height: 21px; } .confirm-screen-account-number { @@ -113,6 +132,7 @@ .confirm-screen-identicons { margin-top: 24px; + flex: 0 0 auto; i.fa-arrow-right { align-self: start; @@ -132,34 +152,34 @@ } .confirm-screen-send-amount { - font-size: 64px; color: $scorpion; margin-top: 12px; - line-height: 60px; text-align: center; - font-family: 'DIN NEXT Light'; + font-size: 40px; + font-weight: 300; + line-height: 53px; + flex: 0 0 auto; } .confirm-screen-send-amount-currency { font-size: 20px; line-height: 20px; text-align: center; + flex: 0 0 auto; } .confirm-memo-wrapper { min-height: 24px; width: 100%; border-bottom: 1px solid $alto; + flex: 0 0 auto; } .confirm-screen-send-memo { - color: $dusty-gray; + color: $scorpion; font-size: 16px; - line-height: 24px; - text-align: center; - margin-top: 21px; - margin-bottom: 18px; - font-family: 'DIN NEXT Light'; + line-height: 19px; + font-weight: 400; } .confirm-screen-label { @@ -180,7 +200,7 @@ section .confirm-screen-account-number, display: flex; flex-flow: column nowrap; width: 100%; - padding: 0 24px 32px; + flex: 0 0 auto; } .confirm-screen-section-column { @@ -191,24 +211,26 @@ section .confirm-screen-account-number, display: flex; flex-flow: row nowrap; border-bottom: 1px solid $alto; - width: calc(100% - 24px); + width: 100%; align-items: center; - padding: 12px 0; - margin: 0 12px; + padding: 12px; + padding-left: 35px; + font-size: 16px; + line-height: 22px; + font-weight: 300; } .confirm-screen-row-detail { font-size: 12px; line-height: 16px; color: $dusty-gray; - font-family: 'DIN NEXT Light'; } .confirm-screen-total-box { background-color: $wild-sand; - border-radius: 8px; - padding: 22px 14px; - margin-top: 13px; + padding: 20px; + padding-left: 35px; + border-bottom: 1px solid $alto; .confirm-screen-label { line-height: 18px; @@ -219,44 +241,62 @@ section .confirm-screen-account-number, } &__subtitle { - font-size: 14px; - line-height: 20px; - font-family: 'DIN NEXT Light'; + font-size: 12px; + line-height: 22px; + } + + .confirm-screen-row-info { + font-size: 16px; + font-weight: 500; + line-height: 21px; } } .confirm-screen-confirm-button { height: 62px; - width: 216.88px; border-radius: 2px; background-color: #02c9b1; font-size: 16px; color: $white; text-align: center; - font-family: 'DIN NEXT'; + font-family: Roboto; padding-top: 15px; padding-bottom: 15px; - margin-top: 23px; border-width: 0; box-shadow: none; + flex: 1 0 auto; + font-weight: 300; } .btn-light.confirm-screen-cancel-button { height: 62px; - width: 216.88px; background: none; border: none; opacity: 1; - width: 8em; - font-family: 'DIN NEXT'; + font-family: Roboto; border-width: 0; padding-top: 15px; padding-bottom: 15px; font-size: 16px; + line-height: 32px; box-shadow: none; cursor: pointer; + flex: 1 0 auto; + font-weight: 300; } #pending-tx-form { flex: 1 0 auto; + position: relative; + display: flex; + flex-flow: row nowrap; + background-color: $white; + padding: 19px 18px; + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; + + + @media screen and (max-width: $break-small) { + border-top: 1px solid $alto; + } } diff --git a/ui/app/css/itcss/settings/typography.scss b/ui/app/css/itcss/settings/typography.scss index 5b7817651..58e2d444e 100644 --- a/ui/app/css/itcss/settings/typography.scss +++ b/ui/app/css/itcss/settings/typography.scss @@ -1,4 +1,4 @@ -@import url('https://fonts.googleapis.com/css?family=Roboto:300,500'); +@import url('https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900'); @import url('https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css'); diff --git a/ui/app/css/itcss/settings/variables.scss b/ui/app/css/itcss/settings/variables.scss index 764d9c179..7433df81f 100644 --- a/ui/app/css/itcss/settings/variables.scss +++ b/ui/app/css/itcss/settings/variables.scss @@ -31,7 +31,6 @@ $concrete: #f3f3f3; $tundora: #4d4d4d; $nile-blue: #1b344d; $scorpion: #5d5d5d; -$caribbean-green: #02C9B1; $silver: #cdcdcd; $caribbean-green: #02c9b1; $monzo: #d0021b; diff --git a/ui/lib/feature-toggle-utils.js b/ui/lib/feature-toggle-utils.js index f4ff446d3..6d4e461ca 100644 --- a/ui/lib/feature-toggle-utils.js +++ b/ui/lib/feature-toggle-utils.js @@ -1,4 +1,4 @@ -function checkFeatureToggle(name) { +function checkFeatureToggle (name) { const queryPairMap = window.location.search.substr(1).split('&') .map(pair => pair.split('=')) .reduce((pairs, [key, value]) => ({...pairs, [key]: value }), {}) @@ -8,4 +8,4 @@ function checkFeatureToggle(name) { module.exports = { checkFeatureToggle, -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index acd448e6c..837374e1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1803,10 +1803,6 @@ caniuse-lite@^1.0.30000718, caniuse-lite@^1.0.30000726: version "1.0.30000727" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000727.tgz#20c895768398ded5f98a4beab4a76c285def41d2" -caseless@~0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" - caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -2094,16 +2090,16 @@ combined-stream@^1.0.5, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" +commander@2.11.0, commander@^2.5.0, commander@^2.6.0, commander@^2.9.0, commander@~2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" + commander@2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" dependencies: graceful-readlink ">= 1.0.0" -commander@^2.5.0, commander@^2.6.0, commander@^2.9.0, commander@~2.11.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" - commondir@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-0.0.1.tgz#89f00fdcd51b519c578733fec563e6a6da7f5be2" @@ -2287,15 +2283,15 @@ cosmiconfig@^2.1.1, cosmiconfig@^2.1.3: parse-json "^2.2.0" require-from-string "^1.1.0" -coveralls@^2.13.1: - version "2.13.1" - resolved "https://registry.yarnpkg.com/coveralls/-/coveralls-2.13.1.tgz#d70bb9acc1835ec4f063ff9dac5423c17b11f178" +coveralls@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/coveralls/-/coveralls-3.0.0.tgz#22ef730330538080d29b8c151dc9146afde88a99" dependencies: - js-yaml "3.6.1" - lcov-parse "0.0.10" - log-driver "1.2.5" - minimist "1.2.0" - request "2.79.0" + js-yaml "^3.6.1" + lcov-parse "^0.0.10" + log-driver "^1.2.5" + minimist "^1.2.0" + request "^2.79.0" create-ecdh@^4.0.0: version "4.0.0" @@ -2535,6 +2531,12 @@ debug@2.6.9: dependencies: ms "2.0.0" +debug@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + debug@^3.0.0, debug@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/debug/-/debug-3.0.1.tgz#0564c612b521dc92d9f2988f0549e34f9c98db64" @@ -2724,7 +2726,7 @@ diff@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" -diff@^3.1.0: +diff@3.3.1, diff@^3.1.0: version "3.3.1" resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" @@ -3288,7 +3290,7 @@ esprima-fb@3001.1.0-dev-harmony-fb: version "3001.1.0-dev-harmony-fb" resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-3001.0001.0000-dev-harmony-fb.tgz#b77d37abcd38ea0b77426bb8bc2922ce6b426411" -esprima@2.7.x, esprima@^2.6.0, esprima@^2.7.1: +esprima@2.7.x, esprima@^2.7.1: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" @@ -4352,16 +4354,6 @@ gaze@^1.0.0: dependencies: globule "^1.0.0" -generate-function@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74" - -generate-object-property@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" - dependencies: - is-property "^1.0.0" - get-caller-file@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" @@ -4464,24 +4456,24 @@ glob@7.1.1: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^5.0.15, glob@^5.0.3, glob@~5.0.0: - version "5.0.15" - resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" +glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.4, glob@^7.0.5, glob@^7.0.6, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1, glob@~7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: + fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "2 || 3" + minimatch "^3.0.4" once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.0.4, glob@^7.0.5, glob@^7.0.6, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1, glob@~7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" +glob@^5.0.15, glob@^5.0.3, glob@~5.0.0: + version "5.0.15" + resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" dependencies: - fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.0.4" + minimatch "2 || 3" once "^1.3.0" path-is-absolute "^1.0.0" @@ -4563,6 +4555,10 @@ graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, gr version "1.0.1" resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" +growl@1.10.3: + version "1.10.3" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f" + growl@1.9.2: version "1.9.2" resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" @@ -4790,15 +4786,6 @@ har-schema@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" -har-validator@~2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" - dependencies: - chalk "^1.1.1" - commander "^2.9.0" - is-my-json-valid "^2.12.4" - pinkie-promise "^2.0.0" - har-validator@~4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" @@ -5293,15 +5280,6 @@ is-hex-prefixed@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz#7d8d37e6ad77e5d127148913c573e082d777f554" -is-my-json-valid@^2.12.4: - version "2.16.1" - resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz#5a846777e2c2620d1e69104e5d3a03b1f6088f11" - dependencies: - generate-function "^2.0.0" - generate-object-property "^1.1.0" - jsonpointer "^4.0.0" - xtend "^4.0.0" - is-number@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-number/-/is-number-0.1.1.tgz#69a7af116963d47206ec9bd9b48a14216f1e3806" @@ -5352,10 +5330,6 @@ is-promise@^2.1, is-promise@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" -is-property@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" - is-regex@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" @@ -5568,14 +5542,7 @@ js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" -js-yaml@3.6.1: - version "3.6.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.6.1.tgz#6e5fe67d8b205ce4d22fad05b7781e8dadcc4b30" - dependencies: - argparse "^1.0.7" - esprima "^2.6.0" - -js-yaml@3.x, js-yaml@^3.2.5, js-yaml@^3.2.7, js-yaml@^3.4.3, js-yaml@^3.9.1: +js-yaml@3.x, js-yaml@^3.2.5, js-yaml@^3.2.7, js-yaml@^3.4.3, js-yaml@^3.6.1, js-yaml@^3.9.1: version "3.10.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" dependencies: @@ -5748,10 +5715,6 @@ jsonparse@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" -jsonpointer@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" - jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -5900,7 +5863,7 @@ lcid@^1.0.0: dependencies: invert-kv "^1.0.0" -lcov-parse@0.0.10: +lcov-parse@^0.0.10: version "0.0.10" resolved "https://registry.yarnpkg.com/lcov-parse/-/lcov-parse-0.0.10.tgz#1b0b8ff9ac9c7889250582b70b71315d9da6d9a3" @@ -6291,7 +6254,7 @@ lodash@^4.0.0, lodash@^4.1.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.17.4, lo version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" -log-driver@1.2.5: +log-driver@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.5.tgz#7ae4ec257302fd790d557cb10c97100d857b0056" @@ -6635,14 +6598,14 @@ minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" -minimist@1.2.0, minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0, minimist@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" - minimist@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.1.0.tgz#99df657a52574c21c9057497df742790b2b4c0de" +minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0, minimist@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + minimist@~0.0.1, minimist@~0.0.8: version "0.0.10" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" @@ -6689,7 +6652,7 @@ mocha-sinon@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mocha-sinon/-/mocha-sinon-2.0.0.tgz#723a9310e7d737d7b77c7a66821237425b032d48" -mocha@^3.2.0, mocha@^3.4.2: +mocha@^3.2.0: version "3.5.3" resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.5.3.tgz#1e0480fe36d2da5858d1eb6acc38418b26eaa20d" dependencies: @@ -6706,6 +6669,21 @@ mocha@^3.2.0, mocha@^3.4.2: mkdirp "0.5.1" supports-color "3.1.2" +mocha@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.0.1.tgz#0aee5a95cf69a4618820f5e51fa31717117daf1b" + dependencies: + browser-stdout "1.3.0" + commander "2.11.0" + debug "3.1.0" + diff "3.3.1" + escape-string-regexp "1.0.5" + glob "7.1.2" + growl "1.10.3" + he "1.1.1" + mkdirp "0.5.1" + supports-color "4.4.0" + module-deps@^4.0.8: version "4.1.1" resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-4.1.1.tgz#23215833f1da13fd606ccb8087b44852dcb821fd" @@ -7867,10 +7845,6 @@ qs@~2.2.3: version "2.2.5" resolved "https://registry.yarnpkg.com/qs/-/qs-2.2.5.tgz#1088abaf9dcc0ae5ae45b709e6c6b5888b23923c" -qs@~6.3.0: - version "6.3.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c" - qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" @@ -8382,31 +8356,6 @@ request@2, request@^2.67.0, request@^2.79.0, request@^2.81.0: tunnel-agent "^0.6.0" uuid "^3.0.0" -request@2.79.0: - version "2.79.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" - dependencies: - aws-sign2 "~0.6.0" - aws4 "^1.2.1" - caseless "~0.11.0" - combined-stream "~1.0.5" - extend "~3.0.0" - forever-agent "~0.6.1" - form-data "~2.1.1" - har-validator "~2.0.6" - hawk "~3.1.3" - http-signature "~1.1.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.7" - oauth-sign "~0.8.1" - qs "~6.3.0" - stringstream "~0.0.4" - tough-cookie "~2.3.0" - tunnel-agent "~0.4.1" - uuid "^3.0.0" - require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -9403,6 +9352,12 @@ supports-color@3.1.2: dependencies: has-flag "^1.0.0" +supports-color@4.4.0, supports-color@^4.0.0, supports-color@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" + dependencies: + has-flag "^2.0.0" + supports-color@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a" @@ -9417,12 +9372,6 @@ supports-color@^3.1.0, supports-color@^3.1.2, supports-color@^3.2.3: dependencies: has-flag "^1.0.0" -supports-color@^4.0.0, supports-color@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" - dependencies: - has-flag "^2.0.0" - sver-compat@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/sver-compat/-/sver-compat-1.5.0.tgz#3cf87dfeb4d07b4a3f14827bc186b3fd0c645cd8" @@ -9806,10 +9755,6 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" -tunnel-agent@~0.4.1: - version "0.4.3" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" - tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" @@ -10165,9 +10110,9 @@ weak@^1.0.0: bindings "^1.2.1" nan "^2.0.5" -web3-provider-engine@^13.2.12: - version "13.3.0" - resolved "https://registry.yarnpkg.com/web3-provider-engine/-/web3-provider-engine-13.3.0.tgz#77c07b9ca2c529c48ad8fdfbb9d8ef9c3637d37e" +web3-provider-engine@^13.3.1: + version "13.3.2" + resolved "https://registry.yarnpkg.com/web3-provider-engine/-/web3-provider-engine-13.3.2.tgz#a5954aa637f96f0dde5131bc20a6ce9e33e6fcd1" dependencies: async "^2.5.0" clone "^2.0.0" -- cgit v1.2.3 From c221f5ce79a1e24df4672e16bda8e85c434e11ba Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Tue, 10 Oct 2017 14:30:20 -0700 Subject: Confirm Token and Confirm Contract v2 --- .../pending-tx/confirm-deploy-contract.js | 33 ++++++++--------- ui/app/components/pending-tx/confirm-send-token.js | 42 +++++++++++----------- ui/app/css/itcss/components/confirm.scss | 6 ++-- 3 files changed, 43 insertions(+), 38 deletions(-) diff --git a/ui/app/components/pending-tx/confirm-deploy-contract.js b/ui/app/components/pending-tx/confirm-deploy-contract.js index 386e14afe..ea4aa1dde 100644 --- a/ui/app/components/pending-tx/confirm-deploy-contract.js +++ b/ui/app/components/pending-tx/confirm-deploy-contract.js @@ -270,7 +270,8 @@ ConfirmDeployContract.prototype.render = function () { h('button.confirm-screen-back-button', { onClick: () => backToAccountDetail(selectedAddress), }, 'BACK'), - h('div.confirm-screen-title', 'Confirm Transaction'), + h('div.confirm-screen-title', 'Confirm Contract'), + h('div.confirm-screen-header-tip'), ]), h('div.flex-row.flex-center.confirm-screen-identicons', [ h('div.confirm-screen-account-wrapper', [ @@ -278,11 +279,11 @@ ConfirmDeployContract.prototype.render = function () { Identicon, { address: fromAddress, - diameter: 100, + diameter: 60, }, ), h('span.confirm-screen-account-name', fromName), - h('span.confirm-screen-account-number', fromAddress.slice(fromAddress.length - 4)), + // h('span.confirm-screen-account-number', fromAddress.slice(fromAddress.length - 4)), ]), h('i.fa.fa-arrow-right.fa-lg'), h('div.confirm-screen-account-wrapper', [ @@ -292,14 +293,14 @@ ConfirmDeployContract.prototype.render = function () { ]), ]), - h('h3.flex-center.confirm-screen-sending-to-message', { - style: { - textAlign: 'center', - fontSize: '16px', - }, - }, [ - `You're deploying a new contract.`, - ]), + // h('h3.flex-center.confirm-screen-sending-to-message', { + // style: { + // textAlign: 'center', + // fontSize: '16px', + // }, + // }, [ + // `You're deploying a new contract.`, + // ]), this.renderHeroAmount(), @@ -326,17 +327,17 @@ ConfirmDeployContract.prototype.render = function () { ]), ]), - h('form#pending-tx-form.flex-column.flex-center', { + h('form#pending-tx-form', { onSubmit: this.onSubmit, }, [ - - // Accept Button - h('button.confirm-screen-confirm-button', ['CONFIRM']), - // Cancel Button h('div.cancel.btn-light.confirm-screen-cancel-button', { onClick: (event) => this.cancel(event, txMeta), }, 'CANCEL'), + + // Accept Button + h('button.confirm-screen-confirm-button', ['CONFIRM']), + ]), ]) ) diff --git a/ui/app/components/pending-tx/confirm-send-token.js b/ui/app/components/pending-tx/confirm-send-token.js index 384ac92cc..d6ff5a5af 100644 --- a/ui/app/components/pending-tx/confirm-send-token.js +++ b/ui/app/components/pending-tx/confirm-send-token.js @@ -155,7 +155,7 @@ ConfirmSendToken.prototype.renderHeroAmount = function () { h('h3.flex-center.confirm-screen-send-amount', `$${fiatAmount}`), h('h3.flex-center.confirm-screen-send-amount-currency', 'USD'), h('div.flex-center.confirm-memo-wrapper', [ - h('h3.confirm-screen-send-memo', memo), + h('h3.confirm-screen-send-memo', [ memo ? `"${memo}"` : '' ]), ]), ]) ) @@ -164,7 +164,7 @@ ConfirmSendToken.prototype.renderHeroAmount = function () { h('h3.flex-center.confirm-screen-send-amount', tokenAmount), h('h3.flex-center.confirm-screen-send-amount-currency', symbol), h('div.flex-center.confirm-memo-wrapper', [ - h('h3.confirm-screen-send-memo', memo), + h('h3.confirm-screen-send-memo', [ memo ? `"${memo}"` : '' ]), ]), ]) ) @@ -242,7 +242,7 @@ ConfirmSendToken.prototype.render = function () { this.inputs = [] return ( - h('div.flex-column.flex-grow.confirm-screen-container', { + h('div.confirm-screen-container', { style: { minWidth: '355px' }, }, [ // Main Send token Card @@ -252,6 +252,7 @@ ConfirmSendToken.prototype.render = function () { onClick: () => backToAccountDetail(selectedAddress), }, 'BACK'), h('div.confirm-screen-title', 'Confirm Transaction'), + h('div.confirm-screen-header-tip'), ]), h('div.flex-row.flex-center.confirm-screen-identicons', [ h('div.confirm-screen-account-wrapper', [ @@ -259,11 +260,11 @@ ConfirmSendToken.prototype.render = function () { Identicon, { address: fromAddress, - diameter: 100, + diameter: 60, }, ), h('span.confirm-screen-account-name', fromName), - h('span.confirm-screen-account-number', fromAddress.slice(fromAddress.length - 4)), + // h('span.confirm-screen-account-number', fromAddress.slice(fromAddress.length - 4)), ]), h('i.fa.fa-arrow-right.fa-lg'), h('div.confirm-screen-account-wrapper', [ @@ -271,22 +272,22 @@ ConfirmSendToken.prototype.render = function () { Identicon, { address: txParams.to, - diameter: 100, + diameter: 60, }, ), h('span.confirm-screen-account-name', toName), - h('span.confirm-screen-account-number', toAddress.slice(toAddress.length - 4)), + // h('span.confirm-screen-account-number', toAddress.slice(toAddress.length - 4)), ]), ]), - h('h3.flex-center.confirm-screen-sending-to-message', { - style: { - textAlign: 'center', - fontSize: '16px', - }, - }, [ - `You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`, - ]), + // h('h3.flex-center.confirm-screen-sending-to-message', { + // style: { + // textAlign: 'center', + // fontSize: '16px', + // }, + // }, [ + // `You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`, + // ]), this.renderHeroAmount(), @@ -314,18 +315,19 @@ ConfirmSendToken.prototype.render = function () { ]), ]), - h('form#pending-tx-form.flex-column.flex-center', { + h('form#pending-tx-form', { onSubmit: this.onSubmit, }, [ - - // Accept Button - h('button.confirm-screen-confirm-button', ['CONFIRM']), - // Cancel Button h('div.cancel.btn-light.confirm-screen-cancel-button', { onClick: (event) => this.cancel(event, txMeta), }, 'CANCEL'), + + // Accept Button + h('button.confirm-screen-confirm-button', ['CONFIRM']), ]), + + ]) ) } diff --git a/ui/app/css/itcss/components/confirm.scss b/ui/app/css/itcss/components/confirm.scss index dc642c2bc..643747e36 100644 --- a/ui/app/css/itcss/components/confirm.scss +++ b/ui/app/css/itcss/components/confirm.scss @@ -140,7 +140,9 @@ } i.fa-file-text-o { - font-size: 100px; + font-size: 60px; + margin: 16px 8px 0 8px; + text-align: center; } } @@ -294,7 +296,7 @@ section .confirm-screen-account-number, padding: 19px 18px; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; - + width: 100%; @media screen and (max-width: $break-small) { border-top: 1px solid $alto; -- cgit v1.2.3 From d4343fe7e57de1652d1401f70bf4c0c823d53820 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Tue, 10 Oct 2017 14:36:13 -0700 Subject: Fix recipient on send token --- ui/app/components/pending-tx/confirm-send-token.js | 6 ++++-- ui/app/css/itcss/components/confirm.scss | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ui/app/components/pending-tx/confirm-send-token.js b/ui/app/components/pending-tx/confirm-send-token.js index d6ff5a5af..92bba8f62 100644 --- a/ui/app/components/pending-tx/confirm-send-token.js +++ b/ui/app/components/pending-tx/confirm-send-token.js @@ -125,7 +125,9 @@ ConfirmSendToken.prototype.getGasFee = function () { } ConfirmSendToken.prototype.getData = function () { - const { identities } = this.props + const { identities, tokenData } = this.props + const { params = [] } = tokenData + const { value } = params[0] || {} const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} @@ -136,7 +138,7 @@ ConfirmSendToken.prototype.getData = function () { }, to: { address: txParams.to, - name: identities[txParams.to] ? identities[txParams.to].name : 'New Recipient', + name: identities[value] ? identities[value].name : 'New Recipient', }, memo: txParams.memo || '', } diff --git a/ui/app/css/itcss/components/confirm.scss b/ui/app/css/itcss/components/confirm.scss index 643747e36..5b89c3eaf 100644 --- a/ui/app/css/itcss/components/confirm.scss +++ b/ui/app/css/itcss/components/confirm.scss @@ -107,6 +107,7 @@ .confirm-screen-account-wrapper { display: flex; flex-direction: column; + align-items: center; } .confirm-screen-account-name { -- cgit v1.2.3 From b7e2bcf80616c9fb4e15cdf16041751d96b0459d Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Tue, 10 Oct 2017 14:40:00 -0700 Subject: Reduce header shadow on mobile --- ui/app/css/itcss/components/confirm.scss | 4 ++++ ui/app/css/itcss/components/header.scss | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/app/css/itcss/components/confirm.scss b/ui/app/css/itcss/components/confirm.scss index 5b89c3eaf..3576da377 100644 --- a/ui/app/css/itcss/components/confirm.scss +++ b/ui/app/css/itcss/components/confirm.scss @@ -38,6 +38,8 @@ top: 0; box-shadow: none; height: calc(100vh - 41px - 100px); + border-top-left-radius: 0; + border-top-right-radius: 0; } } @@ -301,5 +303,7 @@ section .confirm-screen-account-number, @media screen and (max-width: $break-small) { border-top: 1px solid $alto; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } } diff --git a/ui/app/css/itcss/components/header.scss b/ui/app/css/itcss/components/header.scss index ed569cb08..f750ec014 100644 --- a/ui/app/css/itcss/components/header.scss +++ b/ui/app/css/itcss/components/header.scss @@ -10,7 +10,7 @@ @media screen and (max-width: 575px) { padding: 0 12px; width: 100%; - box-shadow: 0 2px 2px 1px rgba(0, 0, 0, .08); + box-shadow: 0 0 0 1px rgba(0, 0, 0, .08); z-index: $mobile-header-z-index; } -- cgit v1.2.3 From e20ec3b3898db2a129e7af7510e5f0d7db8a27ae Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Tue, 10 Oct 2017 14:43:06 -0700 Subject: Token menu ellipsis alignment --- ui/app/css/itcss/components/token-list.scss | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ui/app/css/itcss/components/token-list.scss b/ui/app/css/itcss/components/token-list.scss index 8ae0eec66..d04f3a9b1 100644 --- a/ui/app/css/itcss/components/token-list.scss +++ b/ui/app/css/itcss/components/token-list.scss @@ -47,9 +47,14 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and ( } &__ellipsis { - position: absolute; - top: 20px; - right: 24px; + // position: absolute; + // top: 20px; + // right: 24px; + line-height: 45px; + } + + &__balance-wrapper { + flex: 1 1 auto; } } -- cgit v1.2.3 From e32d75965f848f8b26868b6476265e61b791c768 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 10 Oct 2017 17:15:14 -0700 Subject: events-proxy - clean up --- app/scripts/lib/events-proxy.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scripts/lib/events-proxy.js b/app/scripts/lib/events-proxy.js index d1199a278..840b06b1a 100644 --- a/app/scripts/lib/events-proxy.js +++ b/app/scripts/lib/events-proxy.js @@ -1,6 +1,5 @@ -module.exports = function createEventEmitterProxy(eventEmitter, listeners) { +module.exports = function createEventEmitterProxy(eventEmitter, eventHandlers = {}) { let target = eventEmitter - const eventHandlers = listeners || {} const proxy = new Proxy({}, { get: (obj, name) => { // intercept listeners @@ -14,9 +13,12 @@ module.exports = function createEventEmitterProxy(eventEmitter, listeners) { return true }, }) + proxy.setTarget(eventEmitter) + return proxy + function setTarget (eventEmitter) { target = eventEmitter - // migrate listeners + // migrate eventHandlers Object.keys(eventHandlers).forEach((name) => { eventHandlers[name].forEach((handler) => target.on(name, handler)) }) @@ -26,6 +28,4 @@ module.exports = function createEventEmitterProxy(eventEmitter, listeners) { eventHandlers[name].push(handler) target.on(name, handler) } - if (listeners) proxy.setTarget(eventEmitter) - return proxy } \ No newline at end of file -- cgit v1.2.3 From 7d50a56198f2992e908bc97b871210ec2b52123a Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 10 Oct 2017 17:15:52 -0700 Subject: util - add obj-proxy --- app/scripts/lib/obj-proxy.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 app/scripts/lib/obj-proxy.js diff --git a/app/scripts/lib/obj-proxy.js b/app/scripts/lib/obj-proxy.js new file mode 100644 index 000000000..29ca1269f --- /dev/null +++ b/app/scripts/lib/obj-proxy.js @@ -0,0 +1,19 @@ +module.exports = function createObjectProxy(obj) { + let target = obj + const proxy = new Proxy({}, { + get: (obj, name) => { + // intercept setTarget + if (name === 'setTarget') return setTarget + return target[name] + }, + set: (obj, name, value) => { + target[name] = value + return true + }, + }) + return proxy + + function setTarget (obj) { + target = obj + } +} \ No newline at end of file -- cgit v1.2.3 From 0f8d7dacb1bada269f38b3f0f73df9e8347bc492 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 10 Oct 2017 17:26:44 -0700 Subject: network-controller - use obj-proxy for providerProxy --- app/scripts/controllers/network.js | 7 ++++--- package.json | 4 ++-- test/unit/network-contoller-test.js | 25 +++++-------------------- 3 files changed, 11 insertions(+), 25 deletions(-) diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js index f4665baf8..64ed4b7c2 100644 --- a/app/scripts/controllers/network.js +++ b/app/scripts/controllers/network.js @@ -6,6 +6,7 @@ const extend = require('xtend') const EthQuery = require('eth-query') const createEthRpcClient = require('eth-rpc-client') const createEventEmitterProxy = require('../lib/events-proxy.js') +const createObjectProxy = require('../lib/obj-proxy.js') const RPC_ADDRESS_LIST = require('../config.js').network const DEFAULT_RPC = RPC_ADDRESS_LIST['rinkeby'] @@ -17,7 +18,7 @@ module.exports = class NetworkController extends EventEmitter { this.networkStore = new ObservableStore('loading') this.providerStore = new ObservableStore(config.provider) this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore }) - this.providerProxy = createEventEmitterProxy() + this.providerProxy = createObjectProxy() this.blockTrackerProxy = createEventEmitterProxy() this.on('networkDidChange', this.lookupNetwork) @@ -27,8 +28,8 @@ module.exports = class NetworkController extends EventEmitter { this._baseProviderParams = _providerParams const rpcUrl = this.getCurrentRpcAddress() this._configureStandardClient({ rpcUrl }) - this.providerProxy.on('block', this._logBlock.bind(this)) - this.providerProxy.on('error', this.verifyNetwork.bind(this)) + this.blockTrackerProxy.on('block', this._logBlock.bind(this)) + this.blockTrackerProxy.on('error', this.verifyNetwork.bind(this)) this.ethQuery = new EthQuery(this.providerProxy) this.lookupNetwork() } diff --git a/package.json b/package.json index 3a6be4c61..a5a473bed 100644 --- a/package.json +++ b/package.json @@ -71,11 +71,11 @@ "eth-contract-metadata": "^1.1.4", "eth-hd-keyring": "^1.2.1", "eth-json-rpc-filters": "^1.2.2", - "eth-json-rpc-middleware": "^1.4.1", + "eth-json-rpc-middleware": "^1.4.2", "eth-keyring-controller": "^2.1.0", "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", - "eth-rpc-client": "^1.0.0", + "eth-rpc-client": "^1.0.3", "eth-sig-util": "^1.4.0", "eth-simple-keyring": "^1.1.1", "eth-token-tracker": "^1.1.4", diff --git a/test/unit/network-contoller-test.js b/test/unit/network-contoller-test.js index 0b3b5adeb..42ca40c56 100644 --- a/test/unit/network-contoller-test.js +++ b/test/unit/network-contoller-test.js @@ -14,15 +14,15 @@ describe('# Network Controller', function () { }, }) - networkController.initializeProvider(networkControllerProviderInit, dummyProviderConstructor) + networkController.initializeProvider(networkControllerProviderInit) }) describe('network', function () { describe('#provider', function () { it('provider should be updatable without reassignment', function () { - networkController.initializeProvider(networkControllerProviderInit, dummyProviderConstructor) - const proxy = networkController._proxy - proxy.setTarget({ test: true, on: () => {} }) - assert.ok(proxy.test) + networkController.initializeProvider(networkControllerProviderInit) + const providerProxy = networkController.providerProxy + providerProxy.setTarget({ test: true }) + assert.ok(providerProxy.test) }) }) describe('#getNetworkState', function () { @@ -66,19 +66,4 @@ describe('# Network Controller', function () { }) }) -function dummyProviderConstructor() { - return { - // provider - sendAsync: noop, - // block tracker - _blockTracker: {}, - start: noop, - stop: noop, - on: noop, - addListener: noop, - once: noop, - removeAllListeners: noop, - } -} - function noop() {} \ No newline at end of file -- cgit v1.2.3 From fbab0f3a1f1bf78fdaf6b5639fb6a23d996f3645 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 6 Oct 2017 08:00:45 -0230 Subject: Send v2 to autocomplete. --- ui/app/components/send/to-autocomplete.js | 55 +++++++++++++++++++++++++++++++ ui/app/css/itcss/components/send.scss | 16 +++++++++ ui/app/send-v2.js | 25 ++++++++++++-- 3 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 ui/app/components/send/to-autocomplete.js diff --git a/ui/app/components/send/to-autocomplete.js b/ui/app/components/send/to-autocomplete.js new file mode 100644 index 000000000..3808bf496 --- /dev/null +++ b/ui/app/components/send/to-autocomplete.js @@ -0,0 +1,55 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Identicon = require('../identicon') + +module.exports = ToAutoComplete + +inherits(ToAutoComplete, Component) +function ToAutoComplete () { + Component.call(this) +} + +ToAutoComplete.prototype.render = function () { + const { to, identities, onChange } = this.props + + return h('div.send-v2__to-autocomplete', [ + + h('input.send-v2__to-autocomplete__input', { + name: 'address', + list: 'addresses', + placeholder: 'Recipient Address', + value: to, + onChange, + // onBlur: () => { + // this.setErrorsFor('to') + // }, + onFocus: event => { + // this.clearErrorsFor('to') + to && event.target.select() + }, + }), + + h('datalist#addresses', [ + // Corresponds to the addresses owned. + ...Object.entries(identities).map(([key, { address, name }]) => { + return h('option', { + value: address, + label: name, + key: address, + }) + }), + // Corresponds to previously sent-to addresses. + // ...addressBook.map(({ address, name }) => { + // return h('option', { + // value: address, + // label: name, + // key: address, + // }) + // }), + ]), + + ]) + +} + diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index dfeb83a0a..752d6ffea 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -552,6 +552,22 @@ } } + &__to-autocomplete { + &__input { + height: 54px; + width: 240px; + border: 1px solid $alto; + border-radius: 4px; + background-color: $white; + color: $dusty-gray; + padding: 10px; + font-family: Roboto; + font-size: 16px; + line-height: 21px; + font-weight: 300; + } + } + &__footer { height: 92px; width: 100%; diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index e0d7c2394..dbc8a23d0 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -3,6 +3,7 @@ const PersistentForm = require('../lib/persistent-form') const h = require('react-hyperscript') const connect = require('react-redux').connect const FromDropdown = require('./components/send/from-dropdown') +const ToAutoComplete = require('./components/send/to-autocomplete') module.exports = connect(mapStateToProps)(SendTransactionScreen) @@ -43,7 +44,8 @@ function SendTransactionScreen () { SendTransactionScreen.prototype.render = function () { const { accounts } = this.props - const { dropdownOpen } = this.state + const { dropdownOpen, newTx } = this.state + const { to } = newTx return ( @@ -81,7 +83,26 @@ SendTransactionScreen.prototype.render = function () { closeDropdown: () => this.setState({ dropdownOpen: false }), }), - ]) + ]), + + h('div.send-v2__form-row', [ + + h('div.send-v2__form-label', 'To:'), + + h(ToAutoComplete, { + to, + identities: identities.map(({ identity }) => identity), + onChange: (event) => { + this.setState({ + newTx: { + ...this.state.newTx, + to: event.target.value, + }, + }) + }, + }), + + ]), ]), -- cgit v1.2.3 From 4096ec9f693f668edfba73a98e91a4d0ef3f3e98 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 10 Oct 2017 20:20:12 -0700 Subject: deps - bump eth-json-rpc-middleware for fix --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a5a473bed..9874f035e 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "eth-contract-metadata": "^1.1.4", "eth-hd-keyring": "^1.2.1", "eth-json-rpc-filters": "^1.2.2", - "eth-json-rpc-middleware": "^1.4.2", + "eth-json-rpc-middleware": "^1.4.3", "eth-keyring-controller": "^2.1.0", "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", -- cgit v1.2.3 From d31c746210cd3130e3fee467f925987eb9578a73 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 10 Oct 2017 21:10:35 -0700 Subject: test - integration - intercept reload attempts --- test/integration/lib/first-time.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/integration/lib/first-time.js b/test/integration/lib/first-time.js index cedb14f6e..ee49d0901 100644 --- a/test/integration/lib/first-time.js +++ b/test/integration/lib/first-time.js @@ -3,6 +3,9 @@ const PASSWORD = 'password123' QUnit.module('first time usage') QUnit.test('render init screen', (assert) => { + // intercept reload attempts + window.onbeforeunload = () => true + const done = assert.async() runFirstTimeUsageTest(assert).then(done).catch((err) => { assert.notOk(err, `Error was thrown: ${err.stack}`) -- cgit v1.2.3 -- cgit v1.2.3 From f0713d4b1a28d608ddca6e251e947d1b2e14b6d3 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 11 Oct 2017 01:01:29 -0700 Subject: ui - network - fix localhost active status --- ui/app/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/app.js b/ui/app/app.js index 613577913..30d3766ab 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -319,7 +319,7 @@ App.prototype.renderNetworkDropdown = function () { [ h('i.fa.fa-question-circle.fa-lg.menu-icon'), 'Localhost 8545', - activeNetwork === 'http://localhost:8545' ? h('.check', '✓') : null, + providerType === 'localhost' ? h('.check', '✓') : null, ] ), -- cgit v1.2.3 From 9f063c320c334c997d0ce10ccad1ff323dac128a Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 11 Oct 2017 15:15:31 -0400 Subject: Fix link to token support page --- CHANGELOG.md | 1 + ui/app/add-token.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8bb48e9f..42ded93b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Add new support for new eth_signTypedData method per EIP 712. - Fix bug where some transactions would be shown as pending forever, even after successfully mined. - Fix bug where a transaction might be shown as pending forever if another tx with the same nonce was mined. +- Fix link to support article on token addresses. ## 3.10.9 2017-10-5 diff --git a/ui/app/add-token.js b/ui/app/add-token.js index 18adc7eb5..9354a4cad 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -73,7 +73,7 @@ AddTokenScreen.prototype.render = function () { }, [ h('a', { style: { fontWeight: 'bold', paddingRight: '10px'}, - href: 'https://consensyssupport.happyfox.com/staff/kb/article/24-what-is-a-token-contract-address', + href: 'https://support.metamask.io/kb/article/24-what-is-a-token-contract-address', target: '_blank', }, [ h('span', 'Token Contract Address '), -- cgit v1.2.3 From 4ed00ea2d8db6649144e7ae37672a1edefa1169e Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 11 Oct 2017 15:54:17 -0400 Subject: Version 3.11.0 --- CHANGELOG.md | 4 +++- app/manifest.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42ded93b6..ab5b3cb9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ ## Current Master -- Add new support for new eth_signTypedData method per EIP 712. +## 3.11.0 2017-10-11 + +- Add support for new eth_signTypedData method per EIP 712. - Fix bug where some transactions would be shown as pending forever, even after successfully mined. - Fix bug where a transaction might be shown as pending forever if another tx with the same nonce was mined. - Fix link to support article on token addresses. diff --git a/app/manifest.json b/app/manifest.json index c253a5c2b..a0f449c68 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.10.9", + "version": "3.11.0", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From dcf10f3d7559b428cc189fb4406580d816eae365 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 11 Oct 2017 18:33:02 -0700 Subject: nonce-tracker - use blockTracker directly --- app/scripts/controllers/transactions.js | 1 + app/scripts/lib/nonce-tracker.js | 10 +++------- test/unit/nonce-tracker-test.js | 7 ++++--- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index ef659a300..d46dee230 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -46,6 +46,7 @@ module.exports = class TransactionController extends EventEmitter { this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update')) this.nonceTracker = new NonceTracker({ provider: this.provider, + blockTracker: this.blockTracker, getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager), getConfirmedTransactions: (address) => { return this.txStateManager.getFilteredTxList({ diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js index 0029ac953..2af40a27f 100644 --- a/app/scripts/lib/nonce-tracker.js +++ b/app/scripts/lib/nonce-tracker.js @@ -4,8 +4,9 @@ const Mutex = require('await-semaphore').Mutex class NonceTracker { - constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) { + constructor ({ provider, blockTracker, getPendingTransactions, getConfirmedTransactions }) { this.provider = provider + this.blockTracker = blockTracker this.ethQuery = new EthQuery(provider) this.getPendingTransactions = getPendingTransactions this.getConfirmedTransactions = getConfirmedTransactions @@ -53,7 +54,7 @@ class NonceTracker { } async _getCurrentBlock () { - const blockTracker = this._getBlockTracker() + const blockTracker = this.blockTracker const currentBlock = blockTracker.getCurrentBlock() if (currentBlock) return currentBlock return await Promise((reject, resolve) => { @@ -139,11 +140,6 @@ class NonceTracker { return { name: 'local', nonce: highest, details: { startPoint, highest } } } - // this is a hotfix for the fact that the blockTracker will - // change when the network changes - _getBlockTracker () { - return this.provider._blockTracker - } } module.exports = NonceTracker diff --git a/test/unit/nonce-tracker-test.js b/test/unit/nonce-tracker-test.js index 8970cf84d..77af2a21c 100644 --- a/test/unit/nonce-tracker-test.js +++ b/test/unit/nonce-tracker-test.js @@ -190,12 +190,13 @@ function generateNonceTrackerWith (pending, confirmed, providerStub = '0x0') { providerResultStub.result = providerStub const provider = { sendAsync: (_, cb) => { cb(undefined, providerResultStub) }, - _blockTracker: { - getCurrentBlock: () => '0x11b568', - }, + } + const blockTracker = { + getCurrentBlock: () => '0x11b568', } return new NonceTracker({ provider, + blockTracker, getPendingTransactions, getConfirmedTransactions, }) -- cgit v1.2.3 From 5c5f9297f78c965b38087b960acfec472e16818b Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 11 Oct 2017 18:36:25 -0700 Subject: deps - bump eth-rpc-client for fix --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9874f035e..2b7b2056a 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "eth-keyring-controller": "^2.1.0", "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", - "eth-rpc-client": "^1.0.3", + "eth-rpc-client": "^1.1.3", "eth-sig-util": "^1.4.0", "eth-simple-keyring": "^1.1.1", "eth-token-tracker": "^1.1.4", -- cgit v1.2.3 From d71f14cb678b48b8666e6835c025ff14193acbb3 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 12 Oct 2017 14:03:42 -0400 Subject: Increase build readability --- gulpfile.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 6d2ff5fdd..9253949c7 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -186,8 +186,13 @@ jsFiles.forEach((jsFile) => { gulp.task(`build:js:${jsFile}`, bundleTask({ watch: false, label: jsFile, filename: `${jsFile}.js` })) }) -gulp.task('dev:js', gulp.series(jsDevStrings.shift(), gulp.parallel(...jsDevStrings))) -gulp.task('build:js', gulp.series(jsBuildStrings.shift(), gulp.parallel(...jsBuildStrings))) +// inpage must be built before all other scripts: +const firstDevString = jsDevStrings.shift() +gulp.task('dev:js', gulp.series(firstDevString, gulp.parallel(...jsDevStrings))) + +// inpage must be built before all other scripts: +const firstBuildString = jsBuildStrings.shift() +gulp.task('build:js', gulp.series(firstBuildString, gulp.parallel(...jsBuildStrings))) // disc bundle analyzer tasks -- cgit v1.2.3 From ea7926c211965e2e529e5795a4e1655e97e32144 Mon Sep 17 00:00:00 2001 From: Dan Date: Mon, 9 Oct 2017 13:55:23 -0230 Subject: Adds amount and gas field to sendV2. --- package.json | 1 + ui/app/components/send/currency-display.js | 85 +++++++++++++++++++++++ ui/app/conversion-util.js | 2 +- ui/app/css/itcss/components/currency-display.scss | 50 +++++++++++++ ui/app/css/itcss/components/index.scss | 2 + ui/app/send-v2.js | 55 +++++++++++++-- 6 files changed, 187 insertions(+), 8 deletions(-) create mode 100644 ui/app/components/send/currency-display.js create mode 100644 ui/app/css/itcss/components/currency-display.scss diff --git a/package.json b/package.json index 20a1fa8ea..7e4386cbd 100644 --- a/package.json +++ b/package.json @@ -136,6 +136,7 @@ "react-addons-css-transition-group": "^15.6.0", "react-dom": "^15.5.4", "react-hyperscript": "^3.0.0", + "react-input-autosize": "^2.0.1", "react-markdown": "^2.3.0", "react-redux": "^5.0.5", "react-select": "^1.0.0-rc.2", diff --git a/ui/app/components/send/currency-display.js b/ui/app/components/send/currency-display.js new file mode 100644 index 000000000..e0147012f --- /dev/null +++ b/ui/app/components/send/currency-display.js @@ -0,0 +1,85 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Identicon = require('../identicon') +const AutosizeInput = require('react-input-autosize').default +const { conversionUtil } = require('../../conversion-util') + +module.exports = CurrencyDisplay + +inherits(CurrencyDisplay, Component) +function CurrencyDisplay () { + Component.call(this) + + this.state = { + minWidth: null, + } +} + +function isValidNumber (text) { + const re = /^([1-9]\d*|0)(\.|\.\d*)?$/ + return re.test(text) +} + +CurrencyDisplay.prototype.componentDidMount = function () { + this.setState({ minWidth: this.refs.currencyDisplayInput.sizer.scrollWidth + 10 }) +} + +CurrencyDisplay.prototype.render = function () { + const { + className, + primaryCurrency, + convertedCurrency, + value = '', + placeholder = '0', + conversionRate, + convertedPrefix = '', + readOnly = false, + handleChange, + inputFontSize, + } = this.props + const { minWidth } = this.state + + const convertedValue = conversionUtil(value, { + fromNumericBase: 'dec', + fromCurrency: primaryCurrency, + toCurrency: convertedCurrency, + conversionRate, + }) + + return h('div.currency-display', { + className, + }, [ + + h('div.currency-display__primary-row', [ + + h(AutosizeInput, { + ref: 'currencyDisplayInput', + className: 'currency-display__input-wrapper', + inputClassName: 'currency-display__input', + value, + placeholder, + readOnly, + minWidth, + onChange: (event) => { + const newValue = event.target.value + if (newValue && !isValidNumber(newValue)) { + event.preventDefault() + } + else { + handleChange(newValue) + } + }, + style: { fontSize: inputFontSize }, + }), + + h('span.currency-display__primary-currency', {}, primaryCurrency), + + ]), + + h('div.currency-display__converted-value', {}, `${convertedPrefix}${convertedValue} ${convertedCurrency}`), + + ]) + +} + diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js index 20f77b35b..70c3c2622 100644 --- a/ui/app/conversion-util.js +++ b/ui/app/conversion-util.js @@ -125,7 +125,7 @@ const conversionUtil = (value, { conversionRate, ethToUSDRate, invertConversionRate, - value, + value: value || '0', }); const addCurrencies = (a, b, { toNumericBase, numberOfDecimals }) => { diff --git a/ui/app/css/itcss/components/currency-display.scss b/ui/app/css/itcss/components/currency-display.scss new file mode 100644 index 000000000..b2776bb47 --- /dev/null +++ b/ui/app/css/itcss/components/currency-display.scss @@ -0,0 +1,50 @@ +.currency-display { + height: 54px; + width: 240px; + border: 1px solid $alto; + border-radius: 4px; + background-color: $white; + color: $dusty-gray; + font-family: Roboto; + font-size: 16px; + font-weight: 300; + padding: 8px 10px; + position: relative; + + &__primary-row { + display: flex; + } + + &__input-wrapper { + margin-top: -1px; + } + + &__input { + color: $scorpion; + font-family: Roboto; + font-size: 16px; + line-height: 22px; + border: none; + outline: 0 !important; + } + + &__primary-currency { + color: $scorpion; + font-weight: 400; + font-family: Roboto; + font-size: 16px; + line-height: 22px; + } + + &__converted-row { + display: flex; + } + + &__converted-value, + &__converted-currency { + color: $dusty-gray; + font-family: Roboto; + font-size: 12px; + line-height: 12px; + } +} \ No newline at end of file diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss index 9b3690099..f24a9caa8 100644 --- a/ui/app/css/itcss/components/index.scss +++ b/ui/app/css/itcss/components/index.scss @@ -29,3 +29,5 @@ @import './token-list.scss'; @import './add-token.scss'; + +@import './currency-display.scss'; diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index dbc8a23d0..47f8b18bd 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -4,6 +4,7 @@ const h = require('react-hyperscript') const connect = require('react-redux').connect const FromDropdown = require('./components/send/from-dropdown') const ToAutoComplete = require('./components/send/to-autocomplete') +const CurrencyDisplay = require('./components/send/currency-display') module.exports = connect(mapStateToProps)(SendTransactionScreen) @@ -19,8 +20,12 @@ function mapStateToProps (state) { secondary: `$30${i},000.00 USD`, } })) + const conversionRate = 301.0005 - return { accounts: mockAccounts } + return { + accounts: mockAccounts, + conversionRate + } } inherits(SendTransactionScreen, PersistentForm) @@ -31,10 +36,9 @@ function SendTransactionScreen () { newTx: { from: '', to: '', - amountToSend: '0x0', gasPrice: null, - gas: null, - amount: '0x0', + gas: '0.001', + amount: '10', txData: null, memo: '', }, @@ -43,9 +47,9 @@ function SendTransactionScreen () { } SendTransactionScreen.prototype.render = function () { - const { accounts } = this.props + const { accounts, conversionRate } = this.props const { dropdownOpen, newTx } = this.state - const { to } = newTx + const { to, amount, gas } = newTx return ( @@ -91,7 +95,7 @@ SendTransactionScreen.prototype.render = function () { h(ToAutoComplete, { to, - identities: identities.map(({ identity }) => identity), + identities: accounts.map(({ identity }) => identity), onChange: (event) => { this.setState({ newTx: { @@ -104,6 +108,43 @@ SendTransactionScreen.prototype.render = function () { ]), + h('div.send-v2__form-row', [ + + h('div.send-v2__form-label', 'Amount:'), + + h(CurrencyDisplay, { + primaryCurrency: 'ETH', + convertedCurrency: 'USD', + value: amount, + conversionRate, + convertedPrefix: '$', + handleChange: (value) => { + this.setState({ + newTx: { + ...this.state.newTx, + amount: value, + }, + }) + } + }), + + ]), + + h('div.send-v2__form-row', [ + + h('div.send-v2__form-label', 'Gas fee:'), + + h(CurrencyDisplay, { + primaryCurrency: 'ETH', + convertedCurrency: 'USD', + value: gas, + conversionRate, + convertedPrefix: '$', + readOnly: true, + }), + + ]), + ]), // Buttons underneath card -- cgit v1.2.3 From 2898914a544c4f934cdbe592b7b44df4d08127c8 Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 10 Oct 2017 22:15:31 -0230 Subject: Send v2 amount unit moves correctly. --- ui/app/components/send/currency-display.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/ui/app/components/send/currency-display.js b/ui/app/components/send/currency-display.js index e0147012f..2ffddb178 100644 --- a/ui/app/components/send/currency-display.js +++ b/ui/app/components/send/currency-display.js @@ -13,6 +13,7 @@ function CurrencyDisplay () { this.state = { minWidth: null, + currentScrollWidth: null, } } @@ -22,7 +23,24 @@ function isValidNumber (text) { } CurrencyDisplay.prototype.componentDidMount = function () { - this.setState({ minWidth: this.refs.currencyDisplayInput.sizer.scrollWidth + 10 }) + this.setState({ + minWidth: this.refs.currencyDisplayInput.sizer.clientWidth + 10, + currentclientWidth: this.refs.currencyDisplayInput.sizer.clientWidth, + }) +} + +CurrencyDisplay.prototype.componentWillUpdate = function ({ value: nextValue }) { + const { value: currentValue } = this.props + const { currentclientWidth } = this.state + const newclientWidth = this.refs.currencyDisplayInput.sizer.clientWidth + + if (currentclientWidth !== newclientWidth) { + const clientWidthChange = newclientWidth - currentclientWidth + this.setState({ + minWidth: this.state.minWidth + clientWidthChange, + currentclientWidth: newclientWidth, + }) + } } CurrencyDisplay.prototype.render = function () { -- cgit v1.2.3 From 7ec77e0b4580c52bbf1723ed52d647c9d7516bd5 Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 11 Oct 2017 10:48:27 -0230 Subject: Refactor amount input: dynamic input width with vanilla js. --- package.json | 1 - ui/app/components/send/currency-display.js | 73 +++++++++++++----------------- 2 files changed, 32 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index 7e4386cbd..20a1fa8ea 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,6 @@ "react-addons-css-transition-group": "^15.6.0", "react-dom": "^15.5.4", "react-hyperscript": "^3.0.0", - "react-input-autosize": "^2.0.1", "react-markdown": "^2.3.0", "react-redux": "^5.0.5", "react-select": "^1.0.0-rc.2", diff --git a/ui/app/components/send/currency-display.js b/ui/app/components/send/currency-display.js index 2ffddb178..332d722ec 100644 --- a/ui/app/components/send/currency-display.js +++ b/ui/app/components/send/currency-display.js @@ -2,7 +2,6 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits const Identicon = require('../identicon') -const AutosizeInput = require('react-input-autosize').default const { conversionUtil } = require('../../conversion-util') module.exports = CurrencyDisplay @@ -17,29 +16,16 @@ function CurrencyDisplay () { } } -function isValidNumber (text) { +function isValidInput (text) { const re = /^([1-9]\d*|0)(\.|\.\d*)?$/ return re.test(text) } -CurrencyDisplay.prototype.componentDidMount = function () { - this.setState({ - minWidth: this.refs.currencyDisplayInput.sizer.clientWidth + 10, - currentclientWidth: this.refs.currencyDisplayInput.sizer.clientWidth, - }) -} +function resetCaretIfPastEnd (value, event) { + const caretPosition = event.target.selectionStart -CurrencyDisplay.prototype.componentWillUpdate = function ({ value: nextValue }) { - const { value: currentValue } = this.props - const { currentclientWidth } = this.state - const newclientWidth = this.refs.currencyDisplayInput.sizer.clientWidth - - if (currentclientWidth !== newclientWidth) { - const clientWidthChange = newclientWidth - currentclientWidth - this.setState({ - minWidth: this.state.minWidth + clientWidthChange, - currentclientWidth: newclientWidth, - }) + if (caretPosition > value.length) { + event.target.setSelectionRange(value.length, value.length) } } @@ -54,7 +40,6 @@ CurrencyDisplay.prototype.render = function () { convertedPrefix = '', readOnly = false, handleChange, - inputFontSize, } = this.props const { minWidth } = this.state @@ -71,27 +56,33 @@ CurrencyDisplay.prototype.render = function () { h('div.currency-display__primary-row', [ - h(AutosizeInput, { - ref: 'currencyDisplayInput', - className: 'currency-display__input-wrapper', - inputClassName: 'currency-display__input', - value, - placeholder, - readOnly, - minWidth, - onChange: (event) => { - const newValue = event.target.value - if (newValue && !isValidNumber(newValue)) { - event.preventDefault() - } - else { - handleChange(newValue) - } - }, - style: { fontSize: inputFontSize }, - }), - - h('span.currency-display__primary-currency', {}, primaryCurrency), + h('div.currency-display__input-wrapper', [ + + h('input.currency-display__input', { + value: `${value} ${primaryCurrency}`, + placeholder: `${0} ${primaryCurrency}`, + readOnly, + onChange: (event) => { + let newValue = event.target.value.split(' ')[0] + + if (newValue === '') { + handleChange('0') + } + else if (newValue.match(/^0[1-9]$/)) { + handleChange(newValue.match(/[1-9]/)[0]) + } + else if (newValue && !isValidInput(newValue)) { + event.preventDefault() + } + else { + handleChange(newValue) + } + }, + onKeyUp: event => resetCaretIfPastEnd(value, event), + onClick: event => resetCaretIfPastEnd(value, event), + }), + + ]), ]), -- cgit v1.2.3 From c9a984a237ccacf8994e6c4c2f49b7a17da92d6b Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 12 Oct 2017 14:16:40 -0400 Subject: Break up inpage file read into multiple lines --- app/scripts/contentscript.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 59e7f08ce..445608214 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -7,7 +7,9 @@ const ObjectMultiplex = require('obj-multiplex') const extension = require('extensionizer') const PortStream = require('./lib/port-stream.js') -const inpageText = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'chrome', 'scripts', 'inpage.js')).toString() + '//# sourceURL=' + extension.extension.getURL('scripts/inpage.js') + '\n' +const inpagePath = path.join(__dirname, '..', '..', 'dist', 'chrome', 'scripts', 'inpage.js') +const inpageString = fs.readFileSync(inpagePath).toString() +const inpageText = inpageString + '//# sourceURL=' + extension.extension.getURL('scripts/inpage.js') + '\n' // Eventually this streaming injection could be replaced with: // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction -- cgit v1.2.3 From 53a360b65d6b97fa4551c2953072a21cbe9f708d Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 12 Oct 2017 12:51:48 -0700 Subject: contentscript - fix inpage require and bundling --- app/scripts/contentscript.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 445608214..ffbbc73cc 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -7,9 +7,9 @@ const ObjectMultiplex = require('obj-multiplex') const extension = require('extensionizer') const PortStream = require('./lib/port-stream.js') -const inpagePath = path.join(__dirname, '..', '..', 'dist', 'chrome', 'scripts', 'inpage.js') -const inpageString = fs.readFileSync(inpagePath).toString() -const inpageText = inpageString + '//# sourceURL=' + extension.extension.getURL('scripts/inpage.js') + '\n' +const inpageContent = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'chrome', 'scripts', 'inpage.js')).toString() +const inpageSuffix = '//# sourceURL=' + extension.extension.getURL('scripts/inpage.js') + '\n' +const inpageBundle = inpageContent + inpageSuffix // Eventually this streaming injection could be replaced with: // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction @@ -27,7 +27,7 @@ function setupInjection () { try { // inject in-page script var scriptTag = document.createElement('script') - scriptTag.textContent = inpageText + scriptTag.textContent = inpageBundle scriptTag.onload = function () { this.parentNode.removeChild(this) } var container = document.head || document.documentElement // append as first child -- cgit v1.2.3 From 57179d2b05a4efae06c2375e01e9a01a5519543b Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Thu, 12 Oct 2017 18:46:09 -0400 Subject: Various styling fixes --- ui/app/app.js | 45 +++++++------ ui/app/components/dropdowns/network-dropdown.js | 2 +- ui/app/components/network.js | 11 +++- ui/app/css/itcss/components/account-dropdown.scss | 4 +- ui/app/css/itcss/components/buttons.scss | 4 +- ui/app/css/itcss/components/confirm.scss | 2 +- ui/app/css/itcss/components/header.scss | 26 ++++++-- ui/app/css/itcss/components/modal.scss | 30 ++++----- ui/app/css/itcss/components/network.scss | 77 ++++++++++++++++++----- ui/app/css/itcss/components/newui-sections.scss | 20 +++--- ui/app/css/itcss/components/sections.scss | 10 +-- ui/app/css/itcss/components/send.scss | 14 ++--- ui/app/css/itcss/components/token-list.scss | 2 +- ui/app/css/itcss/components/transaction-list.scss | 2 +- ui/app/css/itcss/generic/index.scss | 2 +- ui/app/css/itcss/settings/variables.scss | 4 ++ 16 files changed, 170 insertions(+), 85 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index fb57775b6..360ba04cf 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -27,6 +27,7 @@ const Import = require('./accounts/import') const InfoScreen = require('./info') const Loading = require('./components/loading') const NetworkIndicator = require('./components/network') +const Identicon = require('./components/identicon') const BuyView = require('./components/buy-button-subview') const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete') const HDRestoreVaultScreen = require('./keychains/hd/restore-vault') @@ -60,6 +61,7 @@ function mapStateToProps (state) { noActiveNotices: state.metamask.noActiveNotices, isInitialized: state.metamask.isInitialized, isUnlocked: state.metamask.isUnlocked, + selectedAddress: state.metamask.selectedAddress, currentView: state.appState.currentView, activeAddress: state.appState.activeAddress, transForward: state.appState.transForward, @@ -198,7 +200,7 @@ App.prototype.renderAppBar = function () { if (window.METAMASK_UI_TYPE === 'notification') { return null } - + console.log(this.props) return ( h('.full-width', { @@ -230,24 +232,31 @@ App.prototype.renderAppBar = function () { ]), - h('div.network-component-wrapper', { - style: {}, - }, [ - // Network Indicator - h(NetworkIndicator, { - network: this.props.network, - provider: this.props.provider, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - if (this.props.networkDropdownOpen === false) { - this.props.showNetworkDropdown() - } else { - this.props.hideNetworkDropdown() - } - }, + h('div.header__right-actions', [ + h('div.network-component-wrapper', { + style: {}, + }, [ + // Network Indicator + h(NetworkIndicator, { + network: this.props.network, + provider: this.props.provider, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + if (this.props.networkDropdownOpen === false) { + this.props.showNetworkDropdown() + } else { + this.props.hideNetworkDropdown() + } + }, + }), + + ]), + + h(Identicon, { + address: this.props.selectedAddress, + diameter: 32, }), - ]), ]), ]), diff --git a/ui/app/components/dropdowns/network-dropdown.js b/ui/app/components/dropdowns/network-dropdown.js index 4c578fbeb..567bf07a0 100644 --- a/ui/app/components/dropdowns/network-dropdown.js +++ b/ui/app/components/dropdowns/network-dropdown.js @@ -102,7 +102,7 @@ NetworkDropdown.prototype.render = function () { key: 'main', closeMenu: () => this.props.hideNetworkDropdown(), onClick: () => props.setProviderType('mainnet'), - style: dropdownMenuItemStyle, + style: { ...dropdownMenuItemStyle, borderColor: '#038789' }, }, [ providerType === 'mainnet' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'), diff --git a/ui/app/components/network.js b/ui/app/components/network.js index 8424a479a..b24505750 100644 --- a/ui/app/components/network.js +++ b/ui/app/components/network.js @@ -1,5 +1,6 @@ const Component = require('react').Component const h = require('react-hyperscript') +const classnames = require('classnames'); const inherits = require('util').inherits const NetworkDropdownIcon = require('./dropdowns/components/network-dropdown-icon') @@ -61,7 +62,13 @@ Network.prototype.render = function () { } return ( - h('.network-component.pointer', { + h('div.network-component.pointer', { + className: classnames('network-component pointer', { + 'ethereum-network': providerName === 'mainnet', + 'ropsten-test-network': providerName === 'ropsten' || parseInt(networkNumber) === 3, + 'kovan-test-network': providerName === 'kovan', + 'rinkeby-test-network': providerName === 'rinkeby', + }), title: hoverText, onClick: (event) => this.props.onClick(event), }, [ @@ -71,7 +78,7 @@ Network.prototype.render = function () { return h('.network-indicator', [ h(NetworkDropdownIcon, { backgroundColor: '#038789', // $blue-lagoon - nonSelectBackgroundColor: '#15afb2' + nonSelectBackgroundColor: '#15afb2', }), h('.network-name', { style: { diff --git a/ui/app/css/itcss/components/account-dropdown.scss b/ui/app/css/itcss/components/account-dropdown.scss index 42f02d84d..9966c7f3f 100644 --- a/ui/app/css/itcss/components/account-dropdown.scss +++ b/ui/app/css/itcss/components/account-dropdown.scss @@ -1,5 +1,5 @@ .account-dropdown-name { - font-family: 'DIN OT'; + font-family: Roboto; } .account-dropdown-balance { @@ -9,7 +9,7 @@ .account-dropdown-edit-button { color: $dusty-gray; - font-family: "DIN OT"; + font-family: Roboto; &:hover { color: $white; diff --git a/ui/app/css/itcss/components/buttons.scss b/ui/app/css/itcss/components/buttons.scss index 2c5e6cf57..8ba084b4a 100644 --- a/ui/app/css/itcss/components/buttons.scss +++ b/ui/app/css/itcss/components/buttons.scss @@ -52,7 +52,7 @@ button.primary { box-shadow: 0 3px 6px rgba(247, 134, 28, .36); color: $white; font-size: 1.1em; - font-family: 'Montserrat Regular'; + font-family: Roboto; text-transform: uppercase; } @@ -62,7 +62,7 @@ button.primary { box-shadow: 0 3px 6px rgba(247, 134, 28, .36); color: #585d67; // TODO: make reusable light button color font-size: 1.1em; - font-family: 'Montserrat Regular'; + font-family: Roboto; text-transform: uppercase; text-align: center; line-height: 20px; diff --git a/ui/app/css/itcss/components/confirm.scss b/ui/app/css/itcss/components/confirm.scss index 3576da377..15c752923 100644 --- a/ui/app/css/itcss/components/confirm.scss +++ b/ui/app/css/itcss/components/confirm.scss @@ -12,7 +12,7 @@ } @media screen and (min-width: 576px) { - top: -26px; + // top: -26px; } } diff --git a/ui/app/css/itcss/components/header.scss b/ui/app/css/itcss/components/header.scss index f750ec014..4fa80f047 100644 --- a/ui/app/css/itcss/components/header.scss +++ b/ui/app/css/itcss/components/header.scss @@ -8,15 +8,24 @@ flex-flow: column nowrap; @media screen and (max-width: 575px) { - padding: 0 12px; + padding: 12px; width: 100%; box-shadow: 0 0 0 1px rgba(0, 0, 0, .08); z-index: $mobile-header-z-index; } @media screen and (min-width: 576px) { - height: 14.4vh; - max-height: 97px; + height: 75px; + justify-content: center; + + &::after { + content: ''; + position: absolute; + width: 100%; + height: 32px; + background: $gallery; + bottom: -32px; + } } } @@ -45,13 +54,13 @@ } .app-header h1 { - font-family: 'Montserrat Regular'; + font-family: Roboto; text-transform: uppercase; + font-weight: 400; color: #22232c; // $shark } h2.page-subtitle { - font-family: 'Montserrat Regular'; text-transform: uppercase; color: #aeaeae; font-size: 1em; @@ -62,6 +71,7 @@ h2.page-subtitle { display: flex; flex-direction: row; align-items: center; + margin-right: 20px; } .left-menu-wrapper { @@ -69,3 +79,9 @@ h2.page-subtitle { flex-direction: row; align-items: center; } + +.header__right-actions { + display: flex; + flex-flow: row nowrap; + align-items: center; +} diff --git a/ui/app/css/itcss/components/modal.scss b/ui/app/css/itcss/components/modal.scss index aa18ed37d..556f14389 100644 --- a/ui/app/css/itcss/components/modal.scss +++ b/ui/app/css/itcss/components/modal.scss @@ -8,7 +8,7 @@ align-items: center; justify-content: center; text-align: center; - font-family: 'DIN OT'; + font-family: Roboto; padding: 0 16px; } @@ -20,7 +20,7 @@ .qr-ellip-address, .ellip-address { width: 247px; border: none; - font-family: 'Montserrat Light'; + font-family: Roboto; font-size: 14px; } @@ -192,7 +192,7 @@ padding: 5px 0 31px 0; border: 1px solid $silver; border-radius: 4px; - font-family: 'Montserrat UltraLight'; + font-family: Roboto; button { cursor: pointer; @@ -208,7 +208,7 @@ &__text { margin-top: 2px; - font-family: 'DIN OT'; + font-family: Roboto; font-size: 14px; line-height: 18px; } @@ -252,7 +252,7 @@ justify-content: center; border: 1px solid $alto; padding: 5px 10px; - font-family: 'Montserrat Light'; + font-family: Roboto; margin-top: 7px; width: 286px; } @@ -269,7 +269,7 @@ padding: 10px 22px; height: 44px; width: 235px; - font-family: 'Montserrat Light'; + font-family: Roboto; } } @@ -320,7 +320,7 @@ .private-key-password::-webkit-input-placeholder { color: $dusty-gray; - font-family: 'Montserrat UltraLight'; + font-family: Roboto; } .private-key-password-warning { @@ -333,7 +333,7 @@ width: 292px; padding: 9px 15px; margin-top: 18px; - font-family: 'Montserrat Regular'; + font-family: Roboto; } .export-private-key-buttons { @@ -362,7 +362,7 @@ .private-key-password-display-textarea { color: $crimson; - font-family: "DIN OT"; + font-family: Roboto; font-size: 16px; line-height: 21px; border: none; @@ -384,7 +384,7 @@ position: relative; border: 1px solid $alto; box-shadow: 0 0 2px 2px $alto; - font-family: 'Montserrat Light'; + font-family: Roboto; } .new-account-modal-header { @@ -439,7 +439,7 @@ width: 100%; font-size: 1em; color: $dusty-gray; - font-family: Montserrat Light; + font-family: Roboto; font-size: 17px; margin: 0 60px; } @@ -506,7 +506,7 @@ &__symbol { color: $tundora; - font-family: 'DIN OT'; + font-family: Roboto; font-size: 16px; line-height: 24px; text-align: center; @@ -517,7 +517,7 @@ height: 30px; width: 271.28px; color: $tundora; - font-family: 'DIN OT'; + font-family: Roboto; font-size: 22px; line-height: 30px; text-align: center; @@ -528,7 +528,7 @@ height: 41px; width: 318px; color: $scorpion; - font-family: 'DIN OT'; + font-family: Roboto; font-size: 14px; line-height: 18px; text-align: center; @@ -547,7 +547,7 @@ border: 1px solid $scorpion; border-radius: 2px; color: $tundora; - font-family: 'DIN OT'; + font-family: Roboto; font-size: 14px; line-height: 20px; text-align: center; diff --git a/ui/app/css/itcss/components/network.scss b/ui/app/css/itcss/components/network.scss index 012b1faf6..bf699ac57 100644 --- a/ui/app/css/itcss/components/network.scss +++ b/ui/app/css/itcss/components/network.scss @@ -1,14 +1,65 @@ +.network-component.pointer { + border: 1px solid $shark; + border-radius: 82px; + padding: 6px; + + &.ethereum-network { + border-color: rgb(3, 135, 137); + + .menu-icon-circle div { + background-color: rgba(3, 135, 137, .7) !important; + } + } + + &.ropsten-test-network { + border-color: rgb(233, 21, 80); + + .menu-icon-circle div { + background-color: rgba(233, 21, 80, .7) !important; + } + } + + &.kovan-test-network { + border-color: rgb(105, 4, 150); + + .menu-icon-circle div { + background-color: rgba(105, 4, 150, .7) !important; + } + } + + &.rinkeby-test-network { + border-color: rgb(235, 179, 63); + + .menu-icon-circle div { + background-color: rgba(235, 179, 63, .7) !important; + } + } +} + +.dropdown-menu-item { + .menu-icon-circle, + .menu-icon-circle--active { + margin: 0 16px; + } +} + .network-indicator { display: flex; align-items: center; font-size: .6em; + + .fa-caret-down { + line-height: 15px; + font-size: 12px; + padding: 0 4px; + } } .network-name { - line-height: 12px; + line-height: 15px; padding: 0 4px; - font-family: 'DIN OT'; - font-size: 10px; + font-family: Roboto; + font-size: 12px; flex: 1 0 auto; } @@ -46,29 +97,27 @@ margin: 0; } -.menu-icon-circle, .menu-icon-circle--active { - height: 23px; - width: 23px; - margin: 9px; +.menu-icon-circle, +.menu-icon-circle--active { background: none; border-radius: 22px; display: flex; justify-content: center; align-items: center; border: 1px solid transparent; - background: none; + margin: 0 4px; } .menu-icon-circle--active { - border: 1px solid white; - background: rgba(100, 100, 100, 0.4); + border: 1px solid $white; + background: rgba(100, 100, 100, .4); } -.menu-icon-circle div, .menu-icon-circle--active div { +.menu-icon-circle div, +.menu-icon-circle--active div { height: 17px; width: 17px; border-radius: 17px; - opacity: 0.7; } .menu-icon-circle--active div { @@ -93,7 +142,7 @@ height: 25px; width: 75px; color: $white; - font-family: 'DIN OT'; + font-family: Roboto; font-size: 18px; line-height: 25px; text-align: center; @@ -103,7 +152,7 @@ height: 36px; width: 265px; color: $dusty-gray; - font-family: 'DIN OT'; + font-family: Roboto; font-size: 14px; line-height: 18px; } diff --git a/ui/app/css/itcss/components/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss index 5ce4f281c..fc1dba87c 100644 --- a/ui/app/css/itcss/components/newui-sections.scss +++ b/ui/app/css/itcss/components/newui-sections.scss @@ -8,9 +8,9 @@ $wallet-view-bg: $wild-sand; // Main container .main-container { - position: absolute; + // position: absolute; z-index: $main-container-z-index; - font-family: "DIN OT Light"; + font-family: Roboto; display: flex; flex-wrap: wrap; align-items: stretch; @@ -70,7 +70,7 @@ $wallet-view-bg: $wild-sand; background: rgb(250, 250, 250); z-index: $sidebar-z-index; position: fixed; - top: 41px; + // top: 41px; left: 0; right: 0; bottom: 0; @@ -86,7 +86,9 @@ $wallet-view-bg: $wild-sand; .sidebar-overlay { z-index: $sidebar-overlay-z-index; position: fixed; - top: 41px; + // top: 41px; + height: 100%; + width: 100%; left: 0; right: 0; bottom: 0; @@ -107,7 +109,7 @@ $wallet-view-bg: $wild-sand; } .main-container { - margin-top: 6.9vh; + // margin-top: 6.9vh; width: 85%; height: 90vh; box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08); @@ -116,7 +118,7 @@ $wallet-view-bg: $wild-sand; @media screen and (min-width: 769px) { .main-container { - margin-top: 6.9vh; + // margin-top: 6.9vh; width: 80%; height: 82vh; box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08); @@ -125,7 +127,7 @@ $wallet-view-bg: $wild-sand; @media screen and (min-width: 1281px) { .main-container { - margin-top: 6.9vh; + // margin-top: 6.9vh; width: 65%; height: 82vh; box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08); @@ -142,8 +144,8 @@ $wallet-view-bg: $wild-sand; } .main-container { - margin-top: 41px; - height: calc(100% - 41px); + // margin-top: 41px; + height: 100%; width: 100%; overflow-y: auto; background-color: $white; diff --git a/ui/app/css/itcss/components/sections.scss b/ui/app/css/itcss/components/sections.scss index 5c32976a7..bc89fdccc 100644 --- a/ui/app/css/itcss/components/sections.scss +++ b/ui/app/css/itcss/components/sections.scss @@ -295,7 +295,7 @@ textarea.twelve-word-phrase { /* Info screen */ .info-gray { - font-family: 'Montserrat Regular'; + font-family: Roboto; text-transform: uppercase; color: $silver-chalice; } @@ -305,7 +305,7 @@ textarea.twelve-word-phrase { } .info { - font-family: 'Montserrat Regular', Arial; + font-family: Roboto, Arial; padding-bottom: 10px; display: inline-block; padding-left: 5px; @@ -354,7 +354,7 @@ textarea.twelve-word-phrase { } .buy-inputs { - font-family: 'Montserrat Light'; + font-family: Roboto; font-size: 13px; height: 20px; background: transparent; @@ -398,7 +398,7 @@ textarea.twelve-word-phrase { } .ex-coins { - font-family: 'Montserrat Regular'; + font-family: Roboto; text-transform: uppercase; text-align: center; font-size: 33px; @@ -409,7 +409,7 @@ textarea.twelve-word-phrase { } .marketinfo { - font-family: 'Montserrat light'; + font-family: Roboto; color: $silver-chalice; font-size: 15px; line-height: 17px; diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index 752d6ffea..80aacf1ab 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -2,7 +2,7 @@ display: flex; flex-flow: column nowrap; z-index: 25; - font-family: 'DIN OT'; + font-family: Roboto; @media screen and (max-width: $break-small) { width: 100%; @@ -19,7 +19,7 @@ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .08); padding: 46px 40.5px 26px; position: relative; - top: -26px; + // top: -26px; align-items: center; display: flex; flex-flow: column nowrap; @@ -77,7 +77,7 @@ margin: 4px 0 20px; font-size: 16px; line-height: 22.4px; - font-family: "DIN OT"; + font-family: Roboto; } .send-screen-gas-input { @@ -316,7 +316,7 @@ display: flex; flex-flow: column nowrap; z-index: 25; - font-family: "Montserrat Light"; + font-family: Roboto; &__content { width: 498px; @@ -325,7 +325,7 @@ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .08); padding: 46px 40.5px 26px; position: relative; - top: -26px; + // top: -26px; align-items: center; display: flex; flex-flow: column nowrap; @@ -404,20 +404,18 @@ width: 380px; border-radius: 8px; background-color: $white; - box-shadow: 0 2px 4px 0 rgba(0,0,0,0.08); + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .08); display: flex; flex-flow: column nowrap; z-index: 25; align-items: center; font-family: Roboto; position: relative; - top: -26px; @media screen and (max-width: $break-small) { width: 100%; overflow-y: auto; top: 0; - width: 100%; box-shadow: none; } } diff --git a/ui/app/css/itcss/components/token-list.scss b/ui/app/css/itcss/components/token-list.scss index d04f3a9b1..bbc64c324 100644 --- a/ui/app/css/itcss/components/token-list.scss +++ b/ui/app/css/itcss/components/token-list.scss @@ -91,7 +91,7 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and ( &__option { color: $white; - font-family: "DIN OT"; + font-family: Roboto; font-size: 16px; line-height: 21px; text-align: center; diff --git a/ui/app/css/itcss/components/transaction-list.scss b/ui/app/css/itcss/components/transaction-list.scss index e3fe1a8b3..76fac09e2 100644 --- a/ui/app/css/itcss/components/transaction-list.scss +++ b/ui/app/css/itcss/components/transaction-list.scss @@ -132,7 +132,7 @@ .tx-list-date { color: $dusty-gray; font-size: 12px; - font-family: "Montserrat UltraLight"; + font-family: Roboto; } .tx-list-identicon-wrapper { diff --git a/ui/app/css/itcss/generic/index.scss b/ui/app/css/itcss/generic/index.scss index 51b7cf789..9d55324e3 100644 --- a/ui/app/css/itcss/generic/index.scss +++ b/ui/app/css/itcss/generic/index.scss @@ -10,7 +10,7 @@ html, body { - font-family: 'Montserrat Regular', Arial; + font-family: Roboto, Arial; color: #4d4d4d; font-weight: 300; line-height: 1.4em; diff --git a/ui/app/css/itcss/settings/variables.scss b/ui/app/css/itcss/settings/variables.scss index 7433df81f..387d14b5f 100644 --- a/ui/app/css/itcss/settings/variables.scss +++ b/ui/app/css/itcss/settings/variables.scss @@ -71,3 +71,7 @@ $sidebar-overlay-z-index: 25; $break-small: 575px; $break-midpoint: 780px; $break-large: 576px; + + +$primary-font-type: Roboto; + -- cgit v1.2.3 From 81f62a7443d47461b5f9b20f442392562458c79a Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Fri, 13 Oct 2017 02:10:58 -0400 Subject: Adding Account Dropdown V2 --- app/images/.DS_Store | Bin 6148 -> 0 bytes app/images/import-account.svg | 18 ++++++++ app/images/plus-btn-white.svg | 17 ++++++++ app/images/settings.svg | 46 ++++++++++---------- ui/app/app.js | 5 ++- ui/app/components/account-menu/index.js | 56 +++++++++++++++++++++++++ ui/app/components/dropdowns/components/menu.js | 44 +++++++++++++++++++ ui/app/css/itcss/components/account-menu.scss | 44 +++++++++++++++++++ ui/app/css/itcss/components/header.scss | 4 ++ ui/app/css/itcss/components/index.scss | 4 ++ ui/app/css/itcss/components/menu.scss | 43 +++++++++++++++++++ 11 files changed, 256 insertions(+), 25 deletions(-) delete mode 100644 app/images/.DS_Store create mode 100644 app/images/import-account.svg create mode 100644 app/images/plus-btn-white.svg create mode 100644 ui/app/components/account-menu/index.js create mode 100644 ui/app/components/dropdowns/components/menu.js create mode 100644 ui/app/css/itcss/components/account-menu.scss create mode 100644 ui/app/css/itcss/components/menu.scss diff --git a/app/images/.DS_Store b/app/images/.DS_Store deleted file mode 100644 index d28ef2089..000000000 Binary files a/app/images/.DS_Store and /dev/null differ diff --git a/app/images/import-account.svg b/app/images/import-account.svg new file mode 100644 index 000000000..d6a81b70c --- /dev/null +++ b/app/images/import-account.svg @@ -0,0 +1,18 @@ + + + + import-account + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/images/plus-btn-white.svg b/app/images/plus-btn-white.svg new file mode 100644 index 000000000..2672d39dd --- /dev/null +++ b/app/images/plus-btn-white.svg @@ -0,0 +1,17 @@ + + + + plus-btn-white + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file diff --git a/app/images/settings.svg b/app/images/settings.svg index fe61320a5..cf9b298dd 100644 --- a/app/images/settings.svg +++ b/app/images/settings.svg @@ -1,24 +1,22 @@ - - - - - - + + + + settings + Created with Sketch. + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/app/app.js b/ui/app/app.js index 360ba04cf..92fc5e697 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -34,6 +34,7 @@ const HDRestoreVaultScreen = require('./keychains/hd/restore-vault') const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation') const ReactCSSTransitionGroup = require('react-addons-css-transition-group') const NetworkDropdown = require('./components/dropdowns/network-dropdown') +const AccountMenu = require('./components/account-menu') // Global Modals const Modal = require('./components/modals/index').Modal @@ -130,6 +131,8 @@ App.prototype.render = function () { frequentRpcList: this.props.frequentRpcList, }, []), + h(AccountMenu), + h(Loading, { isLoading: isLoading || isLoadingNetwork, loadingMessage: loadMessage, @@ -344,7 +347,7 @@ App.prototype.renderPrimary = function () { case 'sendTransaction': log.debug('rendering send tx screen') - + const SendComponentToRender = checkFeatureToggle('send-v2') ? SendTransactionScreen2 : SendTransactionScreen diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js new file mode 100644 index 000000000..7dc3d10a5 --- /dev/null +++ b/ui/app/components/account-menu/index.js @@ -0,0 +1,56 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('../../actions') +const { Menu, Item, Divider } = require('../dropdowns/components/menu') + +module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountMenu) + +inherits(AccountMenu, Component) +function AccountMenu () { Component.call(this) } + +function mapStateToProps (state) { + return { + selectedAddress: state.metamask.selectedAddress, + } +} + +function mapDispatchToProps () { + return {} +} + +AccountMenu.prototype.render = function () { + return h(Menu, { className: 'account-menu' }, [ + h(Item, { className: 'account-menu__header' }, [ + 'My Accounts', + h('button.account-menu__logout-button', 'Log out'), + ]), + h(Divider), + h(Item, { text: 'hi' }), + h(Divider), + h(Item, { + onClick: true, + icon: h('img', { src: 'images/plus-btn-white.svg' }), + text: 'Create Account', + }), + h(Item, { + onClick: true, + icon: h('img', { src: 'images/import-account.svg' }), + text: 'Import Account', + }), + h(Divider), + h(Item, { + onClick: true, + icon: h('img', { src: 'images/mm-info-icon.svg' }), + text: 'Info & Help', + }), + h(Item, { + onClick: true, + icon: h('img', { src: 'images/settings.svg' }), + text: 'Settings', + }), + ]) +} + + diff --git a/ui/app/components/dropdowns/components/menu.js b/ui/app/components/dropdowns/components/menu.js new file mode 100644 index 000000000..0cbe0f342 --- /dev/null +++ b/ui/app/components/dropdowns/components/menu.js @@ -0,0 +1,44 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') + +inherits(Menu, Component) +function Menu () { Component.call(this) } + +Menu.prototype.render = function () { + const { className = '', children, isShowing } = this.props + return isShowing + ? h('div', { className: `menu ${className}` }, children) + : h('noscript') +} + +inherits(Item, Component) +function Item () { Component.call(this) } + +Item.prototype.render = function () { + const { + icon, + children, + text, + className = '', + onClick, + } = this.props + const itemClassName = `menu__item ${className} ${onClick ? 'menu__item--clickable' : ''}` + const iconComponent = icon ? h('div.menu__item__icon', [icon]) : null + const textComponent = text ? h('div.menu__item__text', text) : null + + return children + ? h('div', { className: itemClassName }, children) + : h('div.menu__item', { className: itemClassName }, [ iconComponent, textComponent ] + .filter(d => Boolean(d)) + ) +} + +inherits(Divider, Component) +function Divider () { Component.call(this) } + +Divider.prototype.render = function () { + return h('div.menu__divider') +} + +module.exports = { Menu, Item, Divider } diff --git a/ui/app/css/itcss/components/account-menu.scss b/ui/app/css/itcss/components/account-menu.scss new file mode 100644 index 000000000..8b08c02fd --- /dev/null +++ b/ui/app/css/itcss/components/account-menu.scss @@ -0,0 +1,44 @@ +.account-menu { + position: fixed; + z-index: 100; + top: 58px; + width: 310px; + + @media screen and (max-width: 575px) { + right: calc((100vw - 100%) / 2); + } + + @media screen and (min-width: 576px) { + right: calc((100vw - 85vw) / 2); + } + + @media screen and (min-width: 769px) { + right: calc((100vw - 80vw) / 2); + } + + @media screen and (min-width: 1281px) { + right: calc((100vw - 65vw) / 2); + } + + &__header { + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + align-items: center; + } + + &__logout-button { + border: 1px solid $dusty-gray; + background-color: transparent; + color: $white; + border-radius: 4px; + font-size: 12px; + line-height: 23px; + padding: 0 24px; + } + + img { + width: 16px; + height: 16px; + } +} diff --git a/ui/app/css/itcss/components/header.scss b/ui/app/css/itcss/components/header.scss index 4fa80f047..512cbd995 100644 --- a/ui/app/css/itcss/components/header.scss +++ b/ui/app/css/itcss/components/header.scss @@ -84,4 +84,8 @@ h2.page-subtitle { display: flex; flex-flow: row nowrap; align-items: center; + + .identicon { + cursor: pointer; + } } diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss index f24a9caa8..dee0959b7 100644 --- a/ui/app/css/itcss/components/index.scss +++ b/ui/app/css/itcss/components/index.scss @@ -31,3 +31,7 @@ @import './add-token.scss'; @import './currency-display.scss'; + +@import './account-menu.scss'; + +@import './menu.scss'; diff --git a/ui/app/css/itcss/components/menu.scss b/ui/app/css/itcss/components/menu.scss new file mode 100644 index 000000000..d01c24a70 --- /dev/null +++ b/ui/app/css/itcss/components/menu.scss @@ -0,0 +1,43 @@ +.menu { + border-radius: 4px; + background: rgba($black, .8); + box-shadow: rgba($black, .15) 0 2px 2px 2px; + min-width: 150px; + color: $white; + + &__item { + padding: 18px; + display: flex; + flex-flow: row nowrap; + align-items: center; + + &--clickable { + cursor: pointer; + + &:hover { + background-color: rgba($white, .05); + } + + &:active { + background-color: rgba($white, .1); + } + } + + &__icon { + height: 16px; + width: 16px; + margin-right: 14px; + } + + &__text { + font-size: 16px; + line-height: 21px; + } + } + + &__divider { + background-color: $scorpion; + width: 100%; + height: 1px; + } +} -- cgit v1.2.3 From 803eaaf968161f16aaf72d59b979dfbb7fb9b352 Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Fri, 13 Oct 2017 16:19:22 -0400 Subject: [NewUI] SendV2-#8: Send container handles tokens; gas info dynamic from state (#2364) * Adds memo field to send-v2. * Vertical align transaction with flexbox. * Customize Gas UI * Remove internal state from InputNumber and fix use in gastooltip. * Move customize-gas-modal to its own folder and minor cleanup * Create send container, get account info from state, and make currency display more reusable * Adjusts send-v2 and container for send-token. Dynamically getting suggested gas prices. --- ui/app/app.js | 9 +- .../customize-gas-modal/gas-modal-card.js | 55 ++++++ .../components/customize-gas-modal/gas-slider.js | 50 +++++ ui/app/components/customize-gas-modal/index.js | 91 +++++++++ ui/app/components/input-number.js | 29 +-- ui/app/components/modals/modal.js | 26 +++ ui/app/components/send/account-list-item.js | 33 +++- ui/app/components/send/currency-display.js | 61 ++++-- ui/app/components/send/from-dropdown.js | 2 +- ui/app/components/send/gas-fee-display-v2.js | 47 +++++ ui/app/components/send/gas-tooltip.js | 4 +- ui/app/components/send/memo-textarea.js | 33 ++++ ui/app/components/send/send-v2-container.js | 62 +++++++ ui/app/components/send/to-autocomplete.js | 4 +- ui/app/conversion-util.js | 14 +- ui/app/css/itcss/components/account-dropdown.scss | 25 ++- ui/app/css/itcss/components/currency-display.scss | 4 - ui/app/css/itcss/components/gas-slider.scss | 51 +++++ ui/app/css/itcss/components/index.scss | 3 + ui/app/css/itcss/components/send.scss | 206 +++++++++++++++++++-- ui/app/selectors.js | 35 +++- ui/app/send-v2.js | 203 ++++++++++++++------ 22 files changed, 915 insertions(+), 132 deletions(-) create mode 100644 ui/app/components/customize-gas-modal/gas-modal-card.js create mode 100644 ui/app/components/customize-gas-modal/gas-slider.js create mode 100644 ui/app/components/customize-gas-modal/index.js create mode 100644 ui/app/components/send/gas-fee-display-v2.js create mode 100644 ui/app/components/send/memo-textarea.js create mode 100644 ui/app/components/send/send-v2-container.js create mode 100644 ui/app/css/itcss/components/gas-slider.scss diff --git a/ui/app/app.js b/ui/app/app.js index 92fc5e697..08d24d86c 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -10,7 +10,7 @@ const NewKeyChainScreen = require('./new-keychain') // accounts const MainContainer = require('./main-container') const SendTransactionScreen = require('./send') -const SendTransactionScreen2 = require('./send-v2.js') +const SendTransactionScreen2 = require('./components/send/send-v2-container') const SendTokenScreen = require('./components/send-token') const ConfirmTxScreen = require('./conf-tx') // notice @@ -356,7 +356,12 @@ App.prototype.renderPrimary = function () { case 'sendToken': log.debug('rendering send token screen') - return h(SendTokenScreen, {key: 'sendToken'}) + + const SendTokenComponentToRender = checkFeatureToggle('send-v2') + ? SendTransactionScreen2 + : SendTokenScreen + + return h(SendTokenComponentToRender, {key: 'sendToken'}) case 'newKeychain': log.debug('rendering new keychain screen') diff --git a/ui/app/components/customize-gas-modal/gas-modal-card.js b/ui/app/components/customize-gas-modal/gas-modal-card.js new file mode 100644 index 000000000..8e739ee40 --- /dev/null +++ b/ui/app/components/customize-gas-modal/gas-modal-card.js @@ -0,0 +1,55 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const InputNumber = require('../input-number.js') +const GasSlider = require('./gas-slider.js') + +module.exports = GasModalCard + +inherits(GasModalCard, Component) +function GasModalCard () { + Component.call(this) +} + +GasModalCard.prototype.render = function () { + const { + memo, + identities, + onChange, + unitLabel, + value, + min, + max, + step, + title, + copy + } = this.props + + return h('div.send-v2__gas-modal-card', [ + + h('div.send-v2__gas-modal-card__title', {}, title), + + h('div.send-v2__gas-modal-card__copy', {}, copy), + + h(InputNumber, { + unitLabel, + step, + max, + min, + placeholder: '0', + value, + onChange, + }), + + h(GasSlider, { + value, + step, + max, + min, + onChange, + }), + + ]) + +} + diff --git a/ui/app/components/customize-gas-modal/gas-slider.js b/ui/app/components/customize-gas-modal/gas-slider.js new file mode 100644 index 000000000..e76e96545 --- /dev/null +++ b/ui/app/components/customize-gas-modal/gas-slider.js @@ -0,0 +1,50 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = GasSlider + +inherits(GasSlider, Component) +function GasSlider () { + Component.call(this) +} + +GasSlider.prototype.render = function () { + const { + memo, + identities, + onChange, + unitLabel, + value, + id, + step, + max, + min, + } = this.props + + return h('div.gas-slider', [ + + h('input.gas-slider__input', { + type: 'range', + step, + max, + min, + value, + id: 'gasSlider', + onChange: event => onChange(event.target.value), + }, []), + + h('div.gas-slider__bar', [ + + h('div.gas-slider__low'), + + h('div.gas-slider__mid'), + + h('div.gas-slider__high'), + + ]), + + ]) + +} + diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/customize-gas-modal/index.js new file mode 100644 index 000000000..91e2626b4 --- /dev/null +++ b/ui/app/components/customize-gas-modal/index.js @@ -0,0 +1,91 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const actions = require('../../actions') +const GasModalCard = require('./gas-modal-card') + +function mapStateToProps (state) { + return {} +} + +function mapDispatchToProps (dispatch) { + return { + hideModal: () => dispatch(actions.hideModal()), + } +} + +inherits(CustomizeGasModal, Component) +function CustomizeGasModal () { + Component.call(this) + + this.state = { + gasPrice: '0.23', + gasLimit: '25000', + } +} + +module.exports = connect(mapStateToProps, mapDispatchToProps)(CustomizeGasModal) + +CustomizeGasModal.prototype.render = function () { + const { hideModal } = this.props + const { gasPrice, gasLimit } = this.state + + return h('div.send-v2__customize-gas', {}, [ + h('div', { + }, [ + h('div.send-v2__customize-gas__header', {}, [ + + h('div.send-v2__customize-gas__title', 'Customize Gas'), + + h('div.send-v2__customize-gas__close', { + onClick: hideModal, + }), + + ]), + + h('div.send-v2__customize-gas__body', {}, [ + + h(GasModalCard, { + value: gasPrice, + min: 0.0, + max: 5.0, + step: 0.01, + onChange: gasPrice => this.setState({ gasPrice }), + title: 'Gas Price', + copy: 'We calculate the suggested gas prices based on network success rates.', + }), + + h(GasModalCard, { + value: gasLimit, + min: 20000, + max: 100000, + step: 1, + onChange: gasLimit => this.setState({ gasLimit }), + title: 'Gas Limit', + copy: 'We calculate the suggested gas limit based on network success rates.', + }), + + ]), + + h('div.send-v2__customize-gas__footer', {}, [ + + h('div.send-v2__customize-gas__revert', { + onClick: () => console.log('Revert'), + }, ['Revert']), + + h('div.send-v2__customize-gas__buttons', [ + h('div.send-v2__customize-gas__cancel', { + onClick: this.props.hideModal, + }, ['CANCEL']), + + h('div.send-v2__customize-gas__save', { + onClick: () => console.log('Save'), + }, ['SAVE']), + ]) + + ]), + + ]), + ]) +} diff --git a/ui/app/components/input-number.js b/ui/app/components/input-number.js index 2824d77aa..16347fd5e 100644 --- a/ui/app/components/input-number.js +++ b/ui/app/components/input-number.js @@ -1,6 +1,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits +const { addCurrencies } = require('../conversion-util') module.exports = InputNumber @@ -8,49 +9,37 @@ inherits(InputNumber, Component) function InputNumber () { Component.call(this) - this.state = { - value: 0, - } - this.setValue = this.setValue.bind(this) } -InputNumber.prototype.componentWillMount = function () { - const { initValue = 0 } = this.props - - this.setState({ value: initValue }) -} - InputNumber.prototype.setValue = function (newValue) { - const { fixed, min = -1, onChange } = this.props + const { fixed, min = -1, max = Infinity, onChange } = this.props - if (fixed) newValue = Number(newValue.toFixed(4)) + newValue = Number(fixed ? newValue.toFixed(4) : newValue) - if (newValue >= min) { - this.setState({ value: newValue }) + if (newValue >= min && newValue <= max) { onChange(newValue) } } InputNumber.prototype.render = function () { - const { unitLabel, step = 1, placeholder } = this.props - const { value } = this.state + const { unitLabel, step = 1, placeholder, value = 0 } = this.props return h('div.customize-gas-input-wrapper', {}, [ h('input.customize-gas-input', { placeholder, type: 'number', - value, - onChange: (e) => this.setValue(Number(e.target.value)), + value: value, + onChange: (e) => this.setValue(e.target.value), }), h('span.gas-tooltip-input-detail', {}, [unitLabel]), h('div.gas-tooltip-input-arrows', {}, [ h('i.fa.fa-angle-up', { - onClick: () => this.setValue(value + step), + onClick: () => this.setValue(addCurrencies(value, step)), }), h('i.fa.fa-angle-down', { style: { cursor: 'pointer' }, - onClick: () => this.setValue(value - step), + onClick: () => this.setValue(addCurrencies(value, step * -1)), }), ]), ]) diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index 7247d840e..88deb2bb0 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -15,6 +15,7 @@ const ExportPrivateKeyModal = require('./export-private-key-modal') const NewAccountModal = require('./new-account-modal') const ShapeshiftDepositTxModal = require('./shapeshift-deposit-tx-modal.js') const HideTokenConfirmationModal = require('./hide-token-confirmation-modal') +const CustomizeGasModal = require('../customize-gas-modal') const accountModalStyle = { mobileModalStyle: { @@ -156,6 +157,31 @@ const MODALS = { }, }, + CUSTOMIZE_GAS: { + contents: [ + h(CustomizeGasModal, {}, []), + ], + mobileModalStyle: { + width: '355px', + height: '598px', + // top: isPopupOrNotification() === 'popup' ? '52vh' : '36.5vh', + top: '5%', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', + }, + laptopModalStyle: { + width: '720px', + height: '377px', + top: '80px', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', + }, + }, + DEFAULT: { contents: [], mobileModalStyle: {}, diff --git a/ui/app/components/send/account-list-item.js b/ui/app/components/send/account-list-item.js index b11527d95..64acde767 100644 --- a/ui/app/components/send/account-list-item.js +++ b/ui/app/components/send/account-list-item.js @@ -3,27 +3,34 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect const Identicon = require('../identicon') +const CurrencyDisplay = require('./currency-display') +const { conversionRateSelector } = require('../../selectors') inherits(AccountListItem, Component) function AccountListItem () { Component.call(this) } -module.exports = AccountListItem +function mapStateToProps(state) { + return { + conversionRate: conversionRateSelector(state) + } +} + +module.exports = connect(mapStateToProps)(AccountListItem) AccountListItem.prototype.render = function () { const { account, handleClick, icon = null, + conversionRate, } = this.props - const { identity, balancesToRender } = account - const { name, address } = identity - const { primary, secondary } = balancesToRender + const { name, address, balance } = account return h('div.account-list-item', { - onClick: () => handleClick(identity), + onClick: () => handleClick({ name, address, balance }), }, [ h('div.account-list-item__top-row', {}, [ @@ -35,7 +42,7 @@ AccountListItem.prototype.render = function () { diameter: 18, className: 'account-list-item__identicon', }, - ), + ), h('div.account-list-item__account-name', {}, name), @@ -43,9 +50,17 @@ AccountListItem.prototype.render = function () { ]), - h('div.account-list-item__account-primary-balance', {}, primary), - - h('div.account-list-item__account-secondary-balance', {}, secondary), + h(CurrencyDisplay, { + primaryCurrency: 'ETH', + convertedCurrency: 'USD', + value: balance, + conversionRate, + convertedPrefix: '$', + readOnly: true, + className: 'account-list-item__account-balances', + primaryBalanceClassName: 'account-list-item__account-primary-balance', + convertedBalanceClassName: 'account-list-item__account-secondary-balance', + }, name), ]) } \ No newline at end of file diff --git a/ui/app/components/send/currency-display.js b/ui/app/components/send/currency-display.js index 332d722ec..ed9847fdb 100644 --- a/ui/app/components/send/currency-display.js +++ b/ui/app/components/send/currency-display.js @@ -11,8 +11,7 @@ function CurrencyDisplay () { Component.call(this) this.state = { - minWidth: null, - currentScrollWidth: null, + value: null, } } @@ -29,28 +28,50 @@ function resetCaretIfPastEnd (value, event) { } } +CurrencyDisplay.prototype.handleChangeInHexWei = function (value) { + const { handleChange } = this.props + + const valueInHexWei = conversionUtil(value, { + fromNumericBase: 'dec', + toNumericBase: 'hex', + toDenomination: 'WEI', + }) + + handleChange(valueInHexWei) +} + CurrencyDisplay.prototype.render = function () { const { - className, + className = 'currency-display', + primaryBalanceClassName = 'currency-display__input', + convertedBalanceClassName = 'currency-display__converted-value', + conversionRate, primaryCurrency, convertedCurrency, - value = '', - placeholder = '0', - conversionRate, convertedPrefix = '', + placeholder = '0', readOnly = false, - handleChange, + value: initValue, } = this.props - const { minWidth } = this.state + const { value } = this.state + + const initValueToRender = conversionUtil(initValue, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromDenomination: 'WEI', + numberOfDecimals: 6, + conversionRate, + }) - const convertedValue = conversionUtil(value, { + const convertedValue = conversionUtil(value || initValueToRender, { fromNumericBase: 'dec', fromCurrency: primaryCurrency, toCurrency: convertedCurrency, + numberOfDecimals: 2, conversionRate, }) - return h('div.currency-display', { + return h('div', { className, }, [ @@ -58,35 +79,39 @@ CurrencyDisplay.prototype.render = function () { h('div.currency-display__input-wrapper', [ - h('input.currency-display__input', { - value: `${value} ${primaryCurrency}`, + h('input', { + className: primaryBalanceClassName, + value: `${value || initValueToRender} ${primaryCurrency}`, placeholder: `${0} ${primaryCurrency}`, readOnly, onChange: (event) => { let newValue = event.target.value.split(' ')[0] if (newValue === '') { - handleChange('0') + this.setState({ value: '0' }) } else if (newValue.match(/^0[1-9]$/)) { - handleChange(newValue.match(/[1-9]/)[0]) + this.setState({ value: newValue.match(/[1-9]/)[0] }) } else if (newValue && !isValidInput(newValue)) { event.preventDefault() } else { - handleChange(newValue) + this.setState({ value: newValue }) } }, - onKeyUp: event => resetCaretIfPastEnd(value, event), - onClick: event => resetCaretIfPastEnd(value, event), + onBlur: event => this.handleChangeInHexWei(event.target.value.split(' ')[0]), + onKeyUp: event => resetCaretIfPastEnd(value || initValueToRender, event), + onClick: event => resetCaretIfPastEnd(value || initValueToRender, event), }), ]), ]), - h('div.currency-display__converted-value', {}, `${convertedPrefix}${convertedValue} ${convertedCurrency}`), + h('div', { + className: convertedBalanceClassName, + }, `${convertedPrefix}${convertedValue} ${convertedCurrency}`), ]) diff --git a/ui/app/components/send/from-dropdown.js b/ui/app/components/send/from-dropdown.js index fb0a00cc2..e8e1d43f0 100644 --- a/ui/app/components/send/from-dropdown.js +++ b/ui/app/components/send/from-dropdown.js @@ -14,7 +14,7 @@ function FromDropdown () { FromDropdown.prototype.getListItemIcon = function (currentAccount, selectedAccount) { const listItemIcon = h(`i.fa.fa-check.fa-lg`, { style: { color: '#02c9b1' } }) - return currentAccount.identity.address === selectedAccount.identity.address + return currentAccount.address === selectedAccount.address ? listItemIcon : null } diff --git a/ui/app/components/send/gas-fee-display-v2.js b/ui/app/components/send/gas-fee-display-v2.js new file mode 100644 index 000000000..226ae93f8 --- /dev/null +++ b/ui/app/components/send/gas-fee-display-v2.js @@ -0,0 +1,47 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const CurrencyDisplay = require('./currency-display'); + +const { multiplyCurrencies } = require('../../conversion-util') + +module.exports = GasFeeDisplay + +inherits(GasFeeDisplay, Component) +function GasFeeDisplay () { + Component.call(this) +} + +GasFeeDisplay.prototype.render = function () { + const { + conversionRate, + gasLimit, + gasPrice, + onClick, + } = this.props + + const readyToRender = Boolean(gasLimit && gasPrice) + + return h('div', [ + + readyToRender + ? h(CurrencyDisplay, { + primaryCurrency: 'ETH', + convertedCurrency: 'USD', + value: multiplyCurrencies(gasLimit, gasPrice, { toNumericBase: 'hex' }), + conversionRate, + convertedPrefix: '$', + readOnly: true, + }) + : h('div.currency-display', 'Loading...') + , + + h('div.send-v2__sliders-icon-container', { + onClick, + }, [ + h('i.fa.fa-sliders.send-v2__sliders-icon'), + ]) + + ]) +} + diff --git a/ui/app/components/send/gas-tooltip.js b/ui/app/components/send/gas-tooltip.js index bef419e48..46aff3499 100644 --- a/ui/app/components/send/gas-tooltip.js +++ b/ui/app/components/send/gas-tooltip.js @@ -73,7 +73,7 @@ GasTooltip.prototype.render = function () { step: 1, min: 0, placeholder: '0', - initValue: gasPrice, + value: gasPrice, onChange: (newPrice) => this.updateGasPrice(newPrice), }), h('div.gas-tooltip-input-label', { @@ -89,7 +89,7 @@ GasTooltip.prototype.render = function () { step: 1, min: 0, placeholder: '0', - initValue: gasLimit, + value: gasLimit, onChange: (newLimit) => this.updateGasLimit(newLimit), }), ]), diff --git a/ui/app/components/send/memo-textarea.js b/ui/app/components/send/memo-textarea.js new file mode 100644 index 000000000..4005b9493 --- /dev/null +++ b/ui/app/components/send/memo-textarea.js @@ -0,0 +1,33 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Identicon = require('../identicon') + +module.exports = MemoTextArea + +inherits(MemoTextArea, Component) +function MemoTextArea () { + Component.call(this) +} + +MemoTextArea.prototype.render = function () { + const { memo, identities, onChange } = this.props + + return h('div.send-v2__memo-text-area', [ + + h('textarea.send-v2__memo-text-area__input', { + placeholder: 'Optional', + value: memo, + onChange, + // onBlur: () => { + // this.setErrorsFor('memo') + // }, + onFocus: event => { + // this.clearErrorsFor('memo') + }, + }), + + ]) + +} + diff --git a/ui/app/components/send/send-v2-container.js b/ui/app/components/send/send-v2-container.js new file mode 100644 index 000000000..0c8dd5335 --- /dev/null +++ b/ui/app/components/send/send-v2-container.js @@ -0,0 +1,62 @@ +const connect = require('react-redux').connect +const actions = require('../../actions') +const abi = require('ethereumjs-abi') +const SendEther = require('../../send-v2') + +const { multiplyCurrencies } = require('../../conversion-util') + +const { + accountsWithSendEtherInfoSelector, + getCurrentAccountWithSendEtherInfo, + conversionRateSelector, + getSelectedToken, + getSelectedTokenExchangeRate, + getSelectedAddress, +} = require('../../selectors') + +module.exports = connect(mapStateToProps, mapDispatchToProps)(SendEther) + +function mapStateToProps (state) { + const selectedAddress = getSelectedAddress(state); + const selectedToken = getSelectedToken(state); + const tokenExchangeRates = state.metamask.tokenExchangeRates + const selectedTokenExchangeRate = getSelectedTokenExchangeRate(state) + const conversionRate = conversionRateSelector(state) + + let data; + let primaryCurrency; + let tokenToUSDRate; + if (selectedToken) { + data = Array.prototype.map.call( + abi.rawEncode(['address', 'uint256'], [selectedAddress, '0x0']), + x => ('00' + x.toString(16)).slice(-2) + ).join('') + + primaryCurrency = selectedToken.symbol + + tokenToUSDRate = multiplyCurrencies( + conversionRate, + selectedTokenExchangeRate, + { toNumericBase: 'dec' } + ) + } + + return { + selectedAccount: getCurrentAccountWithSendEtherInfo(state), + accounts: accountsWithSendEtherInfoSelector(state), + conversionRate, + selectedToken, + primaryCurrency, + data, + tokenToUSDRate, + } +} + +function mapDispatchToProps (dispatch) { + return { + showCustomizeGasModal: () => dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' })), + estimateGas: params => dispatch(actions.estimateGas(params)), + getGasPrice: () => dispatch(actions.getGasPrice()), + updateTokenExchangeRate: token => dispatch(actions.updateTokenExchangeRate(token)), + } +} diff --git a/ui/app/components/send/to-autocomplete.js b/ui/app/components/send/to-autocomplete.js index 3808bf496..1bf1e1907 100644 --- a/ui/app/components/send/to-autocomplete.js +++ b/ui/app/components/send/to-autocomplete.js @@ -11,7 +11,7 @@ function ToAutoComplete () { } ToAutoComplete.prototype.render = function () { - const { to, identities, onChange } = this.props + const { to, accounts, onChange } = this.props return h('div.send-v2__to-autocomplete', [ @@ -32,7 +32,7 @@ ToAutoComplete.prototype.render = function () { h('datalist#addresses', [ // Corresponds to the addresses owned. - ...Object.entries(identities).map(([key, { address, name }]) => { + ...Object.entries(accounts).map(([key, { address, name }]) => { return h('option', { value: address, label: name, diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js index 70c3c2622..3a702bcdd 100644 --- a/ui/app/conversion-util.js +++ b/ui/app/conversion-util.js @@ -128,7 +128,8 @@ const conversionUtil = (value, { value: value || '0', }); -const addCurrencies = (a, b, { toNumericBase, numberOfDecimals }) => { +const addCurrencies = (a, b, options = {}) => { + const { toNumericBase, numberOfDecimals } = options const value = (new BigNumber(a)).add(b); return converter({ value, @@ -137,6 +138,16 @@ const addCurrencies = (a, b, { toNumericBase, numberOfDecimals }) => { }) } +const multiplyCurrencies = (a, b, options = {}) => { + const { toNumericBase, numberOfDecimals } = options + const value = (new BigNumber(a)).times(b); + return converter({ + value, + toNumericBase, + numberOfDecimals, + }) +} + const conversionGreaterThan = ( { value, fromNumericBase }, { value: compareToValue, fromNumericBase: compareToBase }, @@ -152,5 +163,6 @@ const conversionGreaterThan = ( module.exports = { conversionUtil, addCurrencies, + multiplyCurrencies, conversionGreaterThan, } \ No newline at end of file diff --git a/ui/app/css/itcss/components/account-dropdown.scss b/ui/app/css/itcss/components/account-dropdown.scss index 9966c7f3f..4fc7c705a 100644 --- a/ui/app/css/itcss/components/account-dropdown.scss +++ b/ui/app/css/itcss/components/account-dropdown.scss @@ -23,6 +23,16 @@ margin-left: 8px; position: relative; } + + &__account-balances { + height: auto; + border: none; + background-color: transparent; + color: #9b9b9b; + margin-left: 34px; + margin-top: 4px; + position: relative; + } &__account-name { font-size: 16px; @@ -34,13 +44,22 @@ right: 12px; top: 1px; } + + &__account-primary-balance, + &__account-secondary-balance { + font-family: Roboto; + line-height: 16px; + font-size: 12px; + font-weight: 300; + } + &__account-primary-balance { - margin-left: 34px; - margin-top: 4px; + color: $scorpion; + border: none; + outline: 0 !important; } &__account-secondary-balance { - margin-left: 34px; color: $dusty-gray; } } diff --git a/ui/app/css/itcss/components/currency-display.scss b/ui/app/css/itcss/components/currency-display.scss index b2776bb47..f2cc6e700 100644 --- a/ui/app/css/itcss/components/currency-display.scss +++ b/ui/app/css/itcss/components/currency-display.scss @@ -15,10 +15,6 @@ display: flex; } - &__input-wrapper { - margin-top: -1px; - } - &__input { color: $scorpion; font-family: Roboto; diff --git a/ui/app/css/itcss/components/gas-slider.scss b/ui/app/css/itcss/components/gas-slider.scss new file mode 100644 index 000000000..c27a560bd --- /dev/null +++ b/ui/app/css/itcss/components/gas-slider.scss @@ -0,0 +1,51 @@ +.gas-slider { + position: relative; + width: 313px; + + &__input { + width: 317px; + margin-left: -2px; + z-index: 2; + } + + input[type=range] { + -webkit-appearance: none !important; + } + + input[type=range]::-webkit-slider-thumb { + -webkit-appearance: none !important; + height: 26px; + width: 26px; + border: 2px solid #B8B8B8; + background-color: #FFFFFF; + box-shadow: 0 2px 4px 0 rgba(0,0,0,0.08); + border-radius: 50%; + position: relative; + z-index: 10; + } + + &__bar { + height: 6px; + width: 313px; + background: $alto; + display: flex; + justify-content: space-between; + position: absolute; + top: 11px; + z-index: 0; + } + + &__low, &__high { + height: 6px; + width: 49px; + z-index: 1; + } + + &__low { + background-color: $crimson; + } + + &__high { + background-color: $caribbean-green; + } +} \ No newline at end of file diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss index dee0959b7..fda002785 100644 --- a/ui/app/css/itcss/components/index.scss +++ b/ui/app/css/itcss/components/index.scss @@ -35,3 +35,6 @@ @import './account-menu.scss'; @import './menu.scss'; + +@import './gas-slider.scss'; + diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index 80aacf1ab..ddabdee2e 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -264,7 +264,7 @@ .gas-tooltip-input-arrows { position: absolute; top: 0; - left: 178px; + right: 4px; width: 17px; height: 28px; border: 1px solid #dadada; @@ -420,7 +420,16 @@ } } - &__send-eth-icon { + &__send-header-icon-container { + z-index: 25; + + @media screen and (max-width: $break-small) { + position: relative; + top: 0; + } + } + + &__send-header-icon { border-radius: 50%; width: 48px; height: 48px; @@ -428,11 +437,6 @@ z-index: 25; padding: 4px; background-color: $white; - - @media screen and (max-width: $break-small) { - position: relative; - top: 0; - } } &__send-arrow-icon { @@ -472,7 +476,7 @@ position: absolute; transform: rotate(45deg); left: 178px; - top: 71px; + top: 65px; } &__title { @@ -512,7 +516,9 @@ font-family: Roboto; font-size: 16px; line-height: 22px; - margin-top: 16px; + display: flex; + flex-flow: column; + justify-content: center; } &__from-dropdown { @@ -550,7 +556,7 @@ } } - &__to-autocomplete { + &__to-autocomplete, &__memo-text-area { &__input { height: 54px; width: 240px; @@ -566,6 +572,32 @@ } } + &__sliders-icon-container { + display: flex; + align-items: center; + justify-content: center; + height: 24px; + width: 24px; + border: 1px solid $curious-blue; + border-radius: 4px; + background-color: $white; + padding: 5px; + position: absolute; + right: 15px; + top: 14px; + cursor: pointer; + } + + &__sliders-icon { + color: $curious-blue; + } + + &__memo-text-area { + &__input { + padding: 6px 10px; + } + } + &__footer { height: 92px; width: 100%; @@ -573,8 +605,7 @@ justify-content: space-evenly; align-items: center; border-top: 1px solid $alto; - position: absolute; - bottom: 0; + margin-top: 29px; } &__next-btn, @@ -607,4 +638,155 @@ color: $dusty-gray; border-color: $dusty-gray; } + + &__customize-gas { + border: 1px solid #D8D8D8; + border-radius: 4px; + background-color: #FFFFFF; + box-shadow: 0 2px 4px 0 rgba(0,0,0,0.14); + font-family: Roboto; + display: flex; + flex-flow: column; + + @media screen and (max-width: $break-small) { + width: 355px; + height: 598px; + } + + &__header { + height: 52px; + border-bottom: 1px solid $alto; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 22px; + } + + &__title { + margin-left: 19.25px; + } + + &__close::after { + content: '\00D7'; + font-size: 1.8em; + color: $dusty-gray; + font-family: sans-serif; + cursor: pointer; + margin-right: 19.25px; + } + + &__body { + height: 248px; + display: flex; + + @media screen and (max-width: $break-small) { + width: 355px; + height: 470px; + flex-flow: column; + } + } + + &__footer { + height: 75px; + border-top: 1px solid $alto; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 22px; + } + + &__buttons { + display: flex; + justify-content: space-between; + width: 181.75px; + margin-right: 21.25px; + } + + &__revert, &__cancel, &__save { + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + } + + &__revert { + color: $silver-chalice; + font-size: 16px; + margin-left: 21.25px; + } + + &__cancel, &__save { + height: 34.64px; + width: 85.74px; + border: 1px solid $dusty-gray; + border-radius: 2px; + font-family: 'DIN OT'; + font-size: 12px; + color: $dusty-gray; + } + } + + &__gas-modal-card { + width: 360px; + display: flex; + flex-flow: column; + align-items: flex-start; + padding-left: 20px; + + &__title { + height: 26px; + width: 84px; + color: $tundora; + font-family: Roboto; + font-size: 20px; + font-weight: 300; + line-height: 26px; + margin-top: 17px; + } + + &__copy { + height: 38px; + width: 314px; + color: $tundora; + font-family: Roboto; + font-size: 14px; + line-height: 19px; + margin-top: 17px; + } + + .customize-gas-input-wrapper { + margin-top: 17px; + } + + .customize-gas-input { + height: 54px; + width: 315px; + border: 1px solid $geyser; + background-color: $white; + padding-left: 15px; + } + + .gas-tooltip-input-arrows { + width: 32px; + height: 54px; + border-left: 1px solid #dadada; + font-size: 18px; + color: $tundora; + right: 0px; + padding: 1px 4px; + display: flex; + justify-content: space-around; + align-items: center; + } + + input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + display: none; + } + + input[type="number"]:hover::-webkit-inner-spin-button { + -webkit-appearance: none; + display: none; + } + } } diff --git a/ui/app/selectors.js b/ui/app/selectors.js index fdbc5fcde..951161510 100644 --- a/ui/app/selectors.js +++ b/ui/app/selectors.js @@ -5,14 +5,16 @@ const selectors = { getSelectedIdentity, getSelectedAccount, getSelectedToken, + getSelectedTokenExchangeRate, conversionRateSelector, transactionsSelector, + accountsWithSendEtherInfoSelector, + getCurrentAccountWithSendEtherInfo, } module.exports = selectors function getSelectedAddress (state) { - // TODO: accounts is not defined. Is it needed? const selectedAddress = state.metamask.selectedAddress || Object.keys(state.metamask.accounts)[0] return selectedAddress @@ -40,10 +42,41 @@ function getSelectedToken (state) { return selectedToken || null } +function getSelectedTokenExchangeRate (state) { + const tokenExchangeRates = state.metamask.tokenExchangeRates + const selectedToken = getSelectedToken(state) || {} + const { symbol = '' } = selectedToken + + const pair = `${symbol.toLowerCase()}_eth` + const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {} + + return tokenExchangeRate +} + function conversionRateSelector (state) { return state.metamask.conversionRate } +function accountsWithSendEtherInfoSelector (state) { + const { + accounts, + identities, + } = state.metamask + + const accountsWithSendEtherInfo = Object.entries(accounts).map(([key, account]) => { + return Object.assign({}, account, identities[key]) + }) + + return accountsWithSendEtherInfo +} + +function getCurrentAccountWithSendEtherInfo (state) { + const currentAddress = getSelectedAddress(state) + const accounts = accountsWithSendEtherInfoSelector(state) + + return accounts.find(({ address }) => address === currentAddress) +} + function transactionsSelector (state) { const { network, selectedTokenAddress } = state.metamask const unapprovedMsgs = valuesFor(state.metamask.unapprovedMsgs) diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index 47f8b18bd..af7586859 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -2,61 +2,135 @@ const { inherits } = require('util') const PersistentForm = require('../lib/persistent-form') const h = require('react-hyperscript') const connect = require('react-redux').connect + +const Identicon = require('./components/identicon') const FromDropdown = require('./components/send/from-dropdown') const ToAutoComplete = require('./components/send/to-autocomplete') const CurrencyDisplay = require('./components/send/currency-display') +const MemoTextArea = require('./components/send/memo-textarea') +const GasFeeDisplay = require('./components/send/gas-fee-display-v2') -module.exports = connect(mapStateToProps)(SendTransactionScreen) - -function mapStateToProps (state) { - const mockAccounts = Array.from(new Array(5)) - .map((v, i) => ({ - identity: { - name: `Test Account Name ${i}`, - address: `0x02f567704cc6569127e18e3d00d2c85bcbfa6f0${i}`, - }, - balancesToRender: { - primary: `100${i}.000001 ETH`, - secondary: `$30${i},000.00 USD`, - } - })) - const conversionRate = 301.0005 - - return { - accounts: mockAccounts, - conversionRate - } -} +const { showModal } = require('./actions') + +module.exports = SendTransactionScreen inherits(SendTransactionScreen, PersistentForm) function SendTransactionScreen () { PersistentForm.call(this) this.state = { - newTx: { - from: '', - to: '', - gasPrice: null, - gas: '0.001', - amount: '10', - txData: null, - memo: '', - }, + from: '', + to: '', + gasPrice: null, + gasLimit: null, + amount: '0x0', + txData: null, + memo: '', dropdownOpen: false, } } +SendTransactionScreen.prototype.componentWillMount = function () { + const { + updateTokenExchangeRate, + selectedToken = {}, + getGasPrice, + estimateGas, + selectedAddress, + data, + } = this.props + const { symbol } = selectedToken || {} + + const estimateGasParams = { + from: selectedAddress, + gas: '746a528800', + } + + if (symbol) { + updateTokenExchangeRate(symbol) + Object.assign(estimateGasParams, { value: '0x0' }) + } + + if (data) { + Object.assign(estimateGasParams, { data }) + } + + Promise.all([ + getGasPrice(), + estimateGas({ + from: selectedAddress, + gas: '746a528800', + }), + ]) + .then(([blockGasPrice, estimatedGas]) => { + this.setState({ + gasPrice: blockGasPrice, + gasLimit: estimatedGas, + }) + }) +} + +SendTransactionScreen.prototype.renderHeaderIcon = function () { + const { selectedToken } = this.props + + return h('div.send-v2__send-header-icon-container', [ + selectedToken + ? h(Identicon, { + diameter: 40, + address: selectedToken.address, + }) + : h('img.send-v2__send-header-icon', { src: '../images/eth_logo.svg' }) + ]) +} + +SendTransactionScreen.prototype.renderTitle = function () { + const { selectedToken } = this.props + + return h('div.send-v2__title', [selectedToken ? 'Send Tokens' : 'Send Funds']) +} + +SendTransactionScreen.prototype.renderCopy = function () { + const { selectedToken } = this.props + + const tokenText = selectedToken ? 'tokens' : 'ETH' + + return h('div', [ + + h('div.send-v2__copy', `Only send ${tokenText} to an Ethereum address.`), + + h('div.send-v2__copy', 'Sending to a different crytpocurrency that is not Ethereum may result in permanent loss.'), + + ]) +} + SendTransactionScreen.prototype.render = function () { - const { accounts, conversionRate } = this.props - const { dropdownOpen, newTx } = this.state - const { to, amount, gas } = newTx + const { + accounts, + conversionRate, + tokenToUSDRate, + selectedToken, + showCustomizeGasModal, + selectedAccount, + primaryCurrency = 'ETH', + } = this.props + + const { + dropdownOpen, + to, + amount, + gasLimit, + gasPrice, + memo, + } = this.state + + const amountConversionRate = selectedToken ? tokenToUSDRate : conversionRate return ( h('div.send-v2__container', [ h('div.send-v2__header', {}, [ - h('img.send-v2__send-eth-icon', { src: '../images/eth_logo.svg' }), + this.renderHeaderIcon(), h('div.send-v2__arrow-background', [ h('i.fa.fa-lg.fa-arrow-circle-right.send-v2__send-arrow-icon'), @@ -66,11 +140,9 @@ SendTransactionScreen.prototype.render = function () { ]), - h('div.send-v2__title', 'Send Funds'), - - h('div.send-v2__copy', 'Only send ETH to an Ethereum address.'), + this.renderTitle(), - h('div.send-v2__copy', 'Sending to a different crytpocurrency that is not Ethereum may result in permanent loss.'), + this.renderCopy(), h('div.send-v2__form', {}, [ @@ -81,10 +153,11 @@ SendTransactionScreen.prototype.render = function () { h(FromDropdown, { dropdownOpen, accounts, - selectedAccount: accounts[0], + selectedAccount, setFromField: () => console.log('Set From Field'), openDropdown: () => this.setState({ dropdownOpen: true }), closeDropdown: () => this.setState({ dropdownOpen: false }), + conversionRate, }), ]), @@ -95,13 +168,11 @@ SendTransactionScreen.prototype.render = function () { h(ToAutoComplete, { to, - identities: accounts.map(({ identity }) => identity), + accounts, onChange: (event) => { this.setState({ - newTx: { - ...this.state.newTx, - to: event.target.value, - }, + ...this.state, + to: event.target.value, }) }, }), @@ -113,17 +184,15 @@ SendTransactionScreen.prototype.render = function () { h('div.send-v2__form-label', 'Amount:'), h(CurrencyDisplay, { - primaryCurrency: 'ETH', + primaryCurrency, convertedCurrency: 'USD', value: amount, - conversionRate, + conversionRate: amountConversionRate, convertedPrefix: '$', handleChange: (value) => { this.setState({ - newTx: { - ...this.state.newTx, - amount: value, - }, + ...this.state, + amount: value, }) } }), @@ -134,14 +203,34 @@ SendTransactionScreen.prototype.render = function () { h('div.send-v2__form-label', 'Gas fee:'), - h(CurrencyDisplay, { - primaryCurrency: 'ETH', - convertedCurrency: 'USD', - value: gas, + h(GasFeeDisplay, { + gasLimit, + gasPrice, conversionRate, - convertedPrefix: '$', - readOnly: true, - }), + onClick: showCustomizeGasModal, + }), + + h('div.send-v2__sliders-icon-container', { + onClick: showCustomizeGasModal, + }, [ + h('i.fa.fa-sliders.send-v2__sliders-icon'), + ]) + + ]), + + h('div.send-v2__form-row', [ + + h('div.send-v2__form-label', 'Transaction Memo:'), + + h(MemoTextArea, { + memo, + onChange: (event) => { + this.setState({ + ...this.state, + memo: event.target.value, + }) + }, + }), ]), -- cgit v1.2.3 From 222a203353dd977f497d44bf6581c16200b5de4f Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Fri, 13 Oct 2017 16:23:10 -0400 Subject: Fix click to copy for private key not working (#2360) --- ui/app/components/modals/export-private-key-modal.js | 8 +++++--- ui/app/components/readonly-input.js | 2 ++ ui/app/css/itcss/components/modal.scss | 8 ++++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/ui/app/components/modals/export-private-key-modal.js b/ui/app/components/modals/export-private-key-modal.js index ddc7f1352..302596eda 100644 --- a/ui/app/components/modals/export-private-key-modal.js +++ b/ui/app/components/modals/export-private-key-modal.js @@ -7,6 +7,7 @@ const actions = require('../../actions') const AccountModalContainer = require('./account-modal-container') const { getSelectedIdentity } = require('../../selectors') const ReadOnlyInput = require('../readonly-input') +const copyToClipboard = require('copy-to-clipboard') function mapStateToProps (state) { return { @@ -61,11 +62,12 @@ ExportPrivateKeyModal.prototype.renderPasswordInput = function (privateKey) { inputClass: 'private-key-password-display-textarea', textarea: true, value: plainKey, + onClick: () => copyToClipboard(plainKey), }) : h('input.private-key-password-input', { type: 'password', placeholder: 'Type password', - onChange: event => this.setState({ password: event.target.value }) + onChange: event => this.setState({ password: event.target.value }), }) } @@ -115,7 +117,7 @@ ExportPrivateKeyModal.prototype.render = function () { }), h('div.account-modal-divider'), - + h('span.modal-body-title', 'Download Private Keys'), h('div.private-key-password', {}, [ @@ -132,6 +134,6 @@ ExportPrivateKeyModal.prototype.render = function () { ), this.renderButtons(privateKey, this.state.password, address, hideModal), - + ]) } diff --git a/ui/app/components/readonly-input.js b/ui/app/components/readonly-input.js index 33b93b5a0..fcf05fb9e 100644 --- a/ui/app/components/readonly-input.js +++ b/ui/app/components/readonly-input.js @@ -15,6 +15,7 @@ ReadOnlyInput.prototype.render = function () { inputClass = '', value, textarea, + onClick, } = this.props const inputType = textarea ? 'textarea' : 'input' @@ -25,6 +26,7 @@ ReadOnlyInput.prototype.render = function () { value, readOnly: true, onFocus: event => event.target.select(), + onClick, }), ]) } diff --git a/ui/app/css/itcss/components/modal.scss b/ui/app/css/itcss/components/modal.scss index 556f14389..1ffea58a9 100644 --- a/ui/app/css/itcss/components/modal.scss +++ b/ui/app/css/itcss/components/modal.scss @@ -356,18 +356,18 @@ .private-key-password-display-wrapper { height: 80px; width: 291px; - border: 1px solid $silver; + border: 1px solid $silver; border-radius: 2px; } .private-key-password-display-textarea { color: $crimson; - font-family: Roboto; - font-size: 16px; + font-family: Roboto; + font-size: 16px; line-height: 21px; border: none; height: 75px; - width: 253px; + width: 100%; overflow: hidden; resize: none; padding: 9px 13px 8px; -- cgit v1.2.3 From 6defb880fb46a5449ade85ced70a5df43472a679 Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Fri, 13 Oct 2017 16:23:50 -0400 Subject: Fix SELECT TYPE dropdown changing sizes based on error message (#2359) --- ui/app/accounts/import/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ui/app/accounts/import/index.js b/ui/app/accounts/import/index.js index 97b387229..821bb6efe 100644 --- a/ui/app/accounts/import/index.js +++ b/ui/app/accounts/import/index.js @@ -34,8 +34,9 @@ AccountImportSubview.prototype.render = function () { const { type } = state return ( - h('div', { + h('div.flex-center', { style: { + flexDirection: 'column', }, }, [ h('.section-title.flex-row.flex-center', [ @@ -48,7 +49,8 @@ AccountImportSubview.prototype.render = function () { ]), h('div', { style: { - padding: '10px', + padding: '10px 0', + width: '260px', color: 'rgb(174, 174, 174)', }, }, [ -- cgit v1.2.3 From 54a61a40215878e4f8538b440340734fdd46fd67 Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Fri, 13 Oct 2017 16:24:17 -0400 Subject: Fix styling for Accept buttons in notices (#2358) --- ui/app/components/notice.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/notice.js b/ui/app/components/notice.js index c26505193..abfff1f5c 100644 --- a/ui/app/components/notice.js +++ b/ui/app/components/notice.js @@ -102,7 +102,7 @@ Notice.prototype.render = function () { }), ]), - h('button', { + h('button.primary', { disabled, onClick: () => { this.setState({disclaimerDisabled: true}) -- cgit v1.2.3 From a84014eff8e008fff6db5ebba7323bb52a2d2d9f Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Fri, 13 Oct 2017 16:25:33 -0400 Subject: Fix Localhost 8545 option in network dropdown (#2357) --- ui/app/components/dropdowns/network-dropdown.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/dropdowns/network-dropdown.js b/ui/app/components/dropdowns/network-dropdown.js index 567bf07a0..c64e7a1d1 100644 --- a/ui/app/components/dropdowns/network-dropdown.js +++ b/ui/app/components/dropdowns/network-dropdown.js @@ -189,7 +189,7 @@ NetworkDropdown.prototype.render = function () { { key: 'default', closeMenu: () => this.props.hideNetworkDropdown(), - onClick: () => props.setDefaultRpcTarget(), + onClick: () => props.setRpcTarget('http://localhost:8545'), style: dropdownMenuItemStyle, }, [ -- cgit v1.2.3 From b149cceda01318d04195fc2196c29c5d9f67cda0 Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Fri, 13 Oct 2017 16:34:28 -0400 Subject: Fix exception thrown when tokens is null (#2355) --- ui/app/components/token-list.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index fb11be826..4959f1cd5 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -141,7 +141,8 @@ TokenList.prototype.componentDidUpdate = function (nextProps) { const sameUserAndNetwork = oldAddress === newAddress && oldNet === newNet const shouldUpdateTokens = isLoading || missingInfo || sameUserAndNetwork - const tokensLengthUnchanged = tokens.length === newTokens.length + const oldTokensLength = tokens ? tokens.length : 0 + const tokensLengthUnchanged = oldTokensLength === newTokens.length if (tokensLengthUnchanged && shouldUpdateTokens) return -- cgit v1.2.3 From 3fd9c8b57fe46d14772086980e0e92573c1799f2 Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Fri, 13 Oct 2017 17:14:26 -0400 Subject: Fix cursor on unclickable transactions (#2356) --- ui/app/components/tx-list.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/ui/app/components/tx-list.js b/ui/app/components/tx-list.js index 137cccf37..a02849d0e 100644 --- a/ui/app/components/tx-list.js +++ b/ui/app/components/tx-list.js @@ -8,6 +8,7 @@ const TxListItem = require('./tx-list-item') const ShiftListItem = require('./shift-list-item') const { formatBalance, formatDate } = require('../util') const { showConfTxPage } = require('../actions') +const classnames = require('classnames') module.exports = connect(mapStateToProps, mapDispatchToProps)(TxList) @@ -97,18 +98,23 @@ TxList.prototype.renderTransactionListItem = function (transaction, conversionRa address, transactionAmount, transactionHash, - className: '.tx-list-item.tx-list-clickable', conversionRate, } - if (transactionStatus === 'unapproved') { + const isUnapproved = transactionStatus === 'unapproved'; + + if (isUnapproved) { opts.onClick = () => showConfTxPage({id: transActionId}) - opts.className += '.tx-list-pending-item-container' opts.transactionStatus = 'Not Started' } else if (transactionHash) { opts.onClick = () => this.view(transactionHash, transactionNetworkId) } + opts.className = classnames('.tx-list-item', { + '.tx-list-pending-item-container': isUnapproved, + '.tx-list-clickable': Boolean(transactionHash) || isUnapproved, + }) + return h(TxListItem, opts) } -- cgit v1.2.3 From a59972dcabc56c3d92f09ba1b88a2ded70ce8c34 Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Fri, 13 Oct 2017 17:14:48 -0400 Subject: Prevent adding already added tokens (#2362) --- ui/app/add-token.js | 28 +++++++++++++++++++++++++--- ui/app/css/itcss/components/add-token.scss | 15 ++++++++++++++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/ui/app/add-token.js b/ui/app/add-token.js index f723ff07c..90edc8de1 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -22,14 +22,17 @@ const ethUtil = require('ethereumjs-util') const abi = require('human-standard-token-abi') const Eth = require('ethjs-query') const EthContract = require('ethjs-contract') +const R = require('ramda') const emptyAddr = '0x0000000000000000000000000000000000000000' module.exports = connect(mapStateToProps, mapDispatchToProps)(AddTokenScreen) function mapStateToProps (state) { + const { identities, tokens } = state.metamask return { - identities: state.metamask.identities, + identities, + tokens, } } @@ -101,6 +104,15 @@ AddTokenScreen.prototype.tokenAddressDidChange = function (e) { } } +AddTokenScreen.prototype.checkExistingAddresses = function (address) { + const tokensList = this.props.tokens + const matchesAddress = existingToken => { + return existingToken.address.toLowerCase() === address.toLowerCase() + } + + return R.any(matchesAddress)(tokensList) +} + AddTokenScreen.prototype.validate = function () { const errors = {} const identitiesList = Object.keys(this.props.identities) @@ -128,6 +140,11 @@ AddTokenScreen.prototype.validate = function () { if (ownAddress) { errors.customAddress = 'Personal address detected. Input the token contract address.' } + + const tokenAlreadyAdded = this.checkExistingAddresses(customAddress) + if (tokenAlreadyAdded) { + errors.customAddress = 'Token has already been added.' + } } else if ( Object.entries(selectedTokens) .reduce((isEmpty, [ symbol, isSelected ]) => ( @@ -217,12 +234,14 @@ AddTokenScreen.prototype.renderTokenList = function () { return Array(6).fill(undefined) .map((_, i) => { const { logo, symbol, name, address } = results[i] || {} + const tokenAlreadyAdded = this.checkExistingAddresses(address) return Boolean(logo || symbol || name) && ( h('div.add-token__token-wrapper', { - className: classnames('add-token__token-wrapper', { + className: classnames({ 'add-token__token-wrapper--selected': selectedTokens[address], + 'add-token__token-wrapper--disabled': tokenAlreadyAdded, }), - onClick: () => this.toggleToken(address, results[i]), + onClick: () => !tokenAlreadyAdded && this.toggleToken(address, results[i]), }, [ h('div.add-token__token-icon', { style: { @@ -233,6 +252,9 @@ AddTokenScreen.prototype.renderTokenList = function () { h('div.add-token__token-symbol', symbol), h('div.add-token__token-name', name), ]), + tokenAlreadyAdded && ( + h('div.add-token__token-message', 'Already added') + ), ]) ) }) diff --git a/ui/app/css/itcss/components/add-token.scss b/ui/app/css/itcss/components/add-token.scss index d5d1aab71..aa8221c9a 100644 --- a/ui/app/css/itcss/components/add-token.scss +++ b/ui/app/css/itcss/components/add-token.scss @@ -4,7 +4,6 @@ flex-flow: column nowrap; align-items: center; position: relative; - top: -36px; z-index: 12; font-family: 'DIN Next Light'; @@ -189,6 +188,7 @@ border-radius: 10px; cursor: pointer; border: 2px solid transparent; + position: relative; &:hover { border: 2px solid rgba($malibu-blue, .5); @@ -197,6 +197,11 @@ &--selected { border: 2px solid $malibu-blue !important; } + + &--disabled { + opacity: .4; + pointer-events: none; + } } &__token-name { @@ -223,6 +228,14 @@ flex: 0 0 auto; } + &__token-message { + position: absolute; + color: $caribbean-green; + font-size: 11px; + bottom: 0; + left: 85px; + } + &__confirmation-token-list { display: flex; flex-flow: column nowrap; -- cgit v1.2.3 From 970fbd797a96a4e86175181ad30d0b7216d9d2c9 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Sat, 14 Oct 2017 12:05:00 +0000 Subject: fix(package): update react-simple-file-input to version 2.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2b7b2056a..fb1621e29 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "react-markdown": "^2.3.0", "react-redux": "^5.0.5", "react-select": "^1.0.0-rc.2", - "react-simple-file-input": "^1.0.0", + "react-simple-file-input": "^2.0.0", "react-tooltip-component": "^0.3.0", "readable-stream": "^2.3.3", "redux": "^3.0.5", -- cgit v1.2.3 From 06094c914b324e3debf33af374bbaa280d6dc6ef Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Sat, 14 Oct 2017 11:23:44 -0400 Subject: Move etherscan link logic into module --- package.json | 1 + test/unit/account-link-test.js | 16 ---------------- test/unit/explorer-link-test.js | 14 -------------- ui/app/components/account-dropdowns.js | 2 +- ui/app/components/shift-list-item.js | 2 +- ui/app/components/transaction-list-item.js | 2 +- ui/lib/account-link.js | 26 -------------------------- ui/lib/explorer-link.js | 6 ------ 8 files changed, 4 insertions(+), 65 deletions(-) delete mode 100644 test/unit/account-link-test.js delete mode 100644 test/unit/explorer-link-test.js delete mode 100644 ui/lib/account-link.js delete mode 100644 ui/lib/explorer-link.js diff --git a/package.json b/package.json index 2b7b2056a..6f4ac2141 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "ethereumjs-tx": "^1.3.0", "ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", + "etherscan-link": "^1.0.2", "ethjs-contract": "^0.1.9", "ethjs-ens": "^2.0.0", "ethjs-query": "^0.2.9", diff --git a/test/unit/account-link-test.js b/test/unit/account-link-test.js deleted file mode 100644 index 47a961d1f..000000000 --- a/test/unit/account-link-test.js +++ /dev/null @@ -1,16 +0,0 @@ -var assert = require('assert') -var linkGen = require('../../ui/lib/account-link') - -describe('account-link', function () { - it('adds ropsten prefix to ropsten test network', function () { - var result = linkGen('account', '3') - assert.notEqual(result.indexOf('ropsten'), -1, 'ropsten included') - assert.notEqual(result.indexOf('account'), -1, 'account included') - }) - - it('adds kovan prefix to kovan test network', function () { - var result = linkGen('account', '42') - assert.notEqual(result.indexOf('kovan'), -1, 'kovan included') - assert.notEqual(result.indexOf('account'), -1, 'account included') - }) -}) diff --git a/test/unit/explorer-link-test.js b/test/unit/explorer-link-test.js deleted file mode 100644 index a02564509..000000000 --- a/test/unit/explorer-link-test.js +++ /dev/null @@ -1,14 +0,0 @@ -var assert = require('assert') -var linkGen = require('../../ui/lib/explorer-link') - -describe('explorer-link', function () { - it('adds ropsten prefix to ropsten test network', function () { - var result = linkGen('hash', '3') - assert.notEqual(result.indexOf('ropsten'), -1, 'ropsten injected') - }) - - it('adds kovan prefix to kovan test network', function () { - var result = linkGen('hash', '42') - assert.notEqual(result.indexOf('kovan'), -1, 'kovan injected') - }) -}) diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index b087a40d4..1b46e532a 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -2,7 +2,7 @@ const Component = require('react').Component const PropTypes = require('react').PropTypes const h = require('react-hyperscript') const actions = require('../actions') -const genAccountLink = require('../../lib/account-link.js') +const genAccountLink = require('etherscan-link').createAccountLink const connect = require('react-redux').connect const Dropdown = require('./dropdown').Dropdown const DropdownMenuItem = require('./dropdown').DropdownMenuItem diff --git a/ui/app/components/shift-list-item.js b/ui/app/components/shift-list-item.js index 079f05e31..b555dee84 100644 --- a/ui/app/components/shift-list-item.js +++ b/ui/app/components/shift-list-item.js @@ -3,7 +3,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const connect = require('react-redux').connect const vreme = new (require('vreme'))() -const explorerLink = require('../../lib/explorer-link') +const explorerLink = require('etherscan-link').createExplorerLink const actions = require('../actions') const addressSummary = require('../util').addressSummary diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js index a9961f47c..891d5e227 100644 --- a/ui/app/components/transaction-list-item.js +++ b/ui/app/components/transaction-list-item.js @@ -4,7 +4,7 @@ const inherits = require('util').inherits const EthBalance = require('./eth-balance') const addressSummary = require('../util').addressSummary -const explorerLink = require('../../lib/explorer-link') +const explorerLink = require('etherscan-link').createExplorerLink const CopyButton = require('./copyButton') const vreme = new (require('vreme'))() const Tooltip = require('./tooltip') diff --git a/ui/lib/account-link.js b/ui/lib/account-link.js deleted file mode 100644 index 037d990fa..000000000 --- a/ui/lib/account-link.js +++ /dev/null @@ -1,26 +0,0 @@ -module.exports = function (address, network) { - const net = parseInt(network) - let link - switch (net) { - case 1: // main net - link = `https://etherscan.io/address/${address}` - break - case 2: // morden test net - link = `https://morden.etherscan.io/address/${address}` - break - case 3: // ropsten test net - link = `https://ropsten.etherscan.io/address/${address}` - break - case 4: // rinkeby test net - link = `https://rinkeby.etherscan.io/address/${address}` - break - case 42: // kovan test net - link = `https://kovan.etherscan.io/address/${address}` - break - default: - link = '' - break - } - - return link -} diff --git a/ui/lib/explorer-link.js b/ui/lib/explorer-link.js deleted file mode 100644 index 3b82ecd5f..000000000 --- a/ui/lib/explorer-link.js +++ /dev/null @@ -1,6 +0,0 @@ -const prefixForNetwork = require('./etherscan-prefix-for-network') - -module.exports = function (hash, network) { - const prefix = prefixForNetwork(network) - return `http://${prefix}etherscan.io/tx/${hash}` -} -- cgit v1.2.3 From a9244f5e426d6572ef135e07ab75a49c00e84942 Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 12 Oct 2017 14:12:14 -0230 Subject: Customize Gas connected to state --- ui/app/actions.js | 20 ++++++ ui/app/components/customize-gas-modal/index.js | 84 ++++++++++++++++++++++---- ui/app/components/send/gas-fee-display-v2.js | 6 +- ui/app/components/send/send-v2-container.js | 4 ++ ui/app/conversion-util.js | 16 +++-- ui/app/reducers/metamask.js | 20 ++++++ ui/app/selectors.js | 12 +++- ui/app/send-v2.js | 12 ++-- 8 files changed, 147 insertions(+), 27 deletions(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index 86ef4b4b4..b0ef7d0a3 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -133,6 +133,10 @@ var actions = { // send screen estimateGas, getGasPrice, + UPDATE_GAS_LIMIT: 'UPDATE_GAS_LIMIT', + UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE', + updateGasLimit, + updateGasPrice, // app messages confirmSeedWords: confirmSeedWords, showAccountDetail: showAccountDetail, @@ -463,12 +467,20 @@ function estimateGas (params = {}) { return reject(err) } dispatch(actions.hideWarning()) + dispatch(actions.updateGasLimit(data)) return resolve(data) }) }) } } +function updateGasLimit (gasLimit) { + return { + type: actions.UPDATE_GAS_LIMIT, + value: gasLimit, + } +} + function getGasPrice () { return (dispatch) => { return new Promise((resolve, reject) => { @@ -478,12 +490,20 @@ function getGasPrice () { return reject(err) } dispatch(actions.hideWarning()) + dispatch(actions.updateGasPrice(data)) return resolve(data) }) }) } } +function updateGasPrice (gasPrice) { + return { + type: actions.UPDATE_GAS_PRICE, + value: gasPrice, + } +} + function sendTx (txData) { log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`) return (dispatch) => { diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/customize-gas-modal/index.js index 91e2626b4..2df24b4e1 100644 --- a/ui/app/components/customize-gas-modal/index.js +++ b/ui/app/components/customize-gas-modal/index.js @@ -5,32 +5,90 @@ const connect = require('react-redux').connect const actions = require('../../actions') const GasModalCard = require('./gas-modal-card') +const { conversionUtil } = require('../../conversion-util') + +const { + getGasPrice, + getGasLimit, + conversionRateSelector, +} = require('../../selectors') + function mapStateToProps (state) { - return {} + return { + gasPrice: getGasPrice(state), + gasLimit: getGasLimit(state), + conversionRate: conversionRateSelector(state), + } } function mapDispatchToProps (dispatch) { return { hideModal: () => dispatch(actions.hideModal()), + updateGasPrice: newGasPrice => dispatch(actions.updateGasPrice(newGasPrice)), + updateGasLimit: newGasLimit => dispatch(actions.updateGasLimit(newGasLimit)), } } inherits(CustomizeGasModal, Component) -function CustomizeGasModal () { +function CustomizeGasModal (props) { Component.call(this) this.state = { - gasPrice: '0.23', - gasLimit: '25000', + gasPrice: props.gasPrice, + gasLimit: props.gasLimit, } } module.exports = connect(mapStateToProps, mapDispatchToProps)(CustomizeGasModal) +CustomizeGasModal.prototype.save = function (gasPrice, gasLimit) { + const { + updateGasPrice, + updateGasLimit, + hideModal, + } = this.props + + updateGasPrice(gasPrice) + updateGasLimit(gasLimit) + hideModal() +} + +CustomizeGasModal.prototype.convertAndSetGasLimit = function (newGasLimit) { + const convertedGasLimit = conversionUtil(newGasLimit, { + fromNumericBase: 'dec', + toNumericBase: 'hex', + }) + + this.setState({ gasLimit: convertedGasLimit }) +} + +CustomizeGasModal.prototype.convertAndSetGasPrice = function (newGasPrice) { + const convertedGasPrice = conversionUtil(newGasPrice, { + fromNumericBase: 'dec', + toNumericBase: 'hex', + fromDenomination: 'GWEI', + toDenomination: 'WEI', + }) + + this.setState({ gasPrice: convertedGasPrice }) +} + CustomizeGasModal.prototype.render = function () { - const { hideModal } = this.props + const { hideModal, conversionRate } = this.props const { gasPrice, gasLimit } = this.state + const convertedGasPrice = conversionUtil(gasPrice, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromDenomination: 'WEI', + toDenomination: 'GWEI', + }) + + const convertedGasLimit = conversionUtil(gasLimit, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + }) + return h('div.send-v2__customize-gas', {}, [ h('div', { }, [ @@ -47,21 +105,21 @@ CustomizeGasModal.prototype.render = function () { h('div.send-v2__customize-gas__body', {}, [ h(GasModalCard, { - value: gasPrice, - min: 0.0, - max: 5.0, - step: 0.01, - onChange: gasPrice => this.setState({ gasPrice }), + value: convertedGasPrice, + min: 0, + max: 1000, + step: 1, + onChange: value => this.convertAndSetGasPrice(value), title: 'Gas Price', copy: 'We calculate the suggested gas prices based on network success rates.', }), h(GasModalCard, { - value: gasLimit, + value: convertedGasLimit, min: 20000, max: 100000, step: 1, - onChange: gasLimit => this.setState({ gasLimit }), + onChange: value => this.convertAndSetGasLimit(value), title: 'Gas Limit', copy: 'We calculate the suggested gas limit based on network success rates.', }), @@ -80,7 +138,7 @@ CustomizeGasModal.prototype.render = function () { }, ['CANCEL']), h('div.send-v2__customize-gas__save', { - onClick: () => console.log('Save'), + onClick: () => this.save(gasPrice, gasLimit), }, ['SAVE']), ]) diff --git a/ui/app/components/send/gas-fee-display-v2.js b/ui/app/components/send/gas-fee-display-v2.js index 226ae93f8..961d55610 100644 --- a/ui/app/components/send/gas-fee-display-v2.js +++ b/ui/app/components/send/gas-fee-display-v2.js @@ -28,7 +28,11 @@ GasFeeDisplay.prototype.render = function () { ? h(CurrencyDisplay, { primaryCurrency: 'ETH', convertedCurrency: 'USD', - value: multiplyCurrencies(gasLimit, gasPrice, { toNumericBase: 'hex' }), + value: multiplyCurrencies(gasLimit, gasPrice, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 16, + }), conversionRate, convertedPrefix: '$', readOnly: true, diff --git a/ui/app/components/send/send-v2-container.js b/ui/app/components/send/send-v2-container.js index 0c8dd5335..c3af1c972 100644 --- a/ui/app/components/send/send-v2-container.js +++ b/ui/app/components/send/send-v2-container.js @@ -12,6 +12,8 @@ const { getSelectedToken, getSelectedTokenExchangeRate, getSelectedAddress, + getGasPrice, + getGasLimit, } = require('../../selectors') module.exports = connect(mapStateToProps, mapDispatchToProps)(SendEther) @@ -49,6 +51,8 @@ function mapStateToProps (state) { primaryCurrency, data, tokenToUSDRate, + gasPrice: getGasPrice(state), + gasLimit: getGasLimit(state), } } diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js index 3a702bcdd..3a9e9ad0f 100644 --- a/ui/app/conversion-util.js +++ b/ui/app/conversion-util.js @@ -32,6 +32,7 @@ BigNumber.config({ // Big Number Constants const BIG_NUMBER_WEI_MULTIPLIER = new BigNumber('1000000000000000000') +const BIG_NUMBER_GWEI_MULTIPLIER = new BigNumber('1000000000') // Individual Setters const convert = R.invoker(1, 'times') @@ -45,10 +46,12 @@ const toBigNumber = { BN: n => new BigNumber(n.toString(16), 16), } const toNormalizedDenomination = { - WEI: bigNumber => bigNumber.div(BIG_NUMBER_WEI_MULTIPLIER) + WEI: bigNumber => bigNumber.div(BIG_NUMBER_WEI_MULTIPLIER), + GWEI: bigNumber => bigNumber.div(BIG_NUMBER_GWEI_MULTIPLIER), } const toSpecifiedDenomination = { - WEI: bigNumber => bigNumber.times(BIG_NUMBER_WEI_MULTIPLIER).round() + WEI: bigNumber => bigNumber.times(BIG_NUMBER_WEI_MULTIPLIER).round(), + GWEI: bigNumber => bigNumber.times(BIG_NUMBER_GWEI_MULTIPLIER).round(), } const baseChange = { hex: n => n.toString(16), @@ -139,8 +142,13 @@ const addCurrencies = (a, b, options = {}) => { } const multiplyCurrencies = (a, b, options = {}) => { - const { toNumericBase, numberOfDecimals } = options - const value = (new BigNumber(a)).times(b); + const { + toNumericBase, + numberOfDecimals, + multiplicandBase, + multiplierBase, + } = options + const value = (new BigNumber(a, multiplicandBase)).times(b, multiplierBase); return converter({ value, toNumericBase, diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index a0884b834..cc24a6729 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -20,6 +20,10 @@ function reduceMetamask (state, action) { selectedTokenAddress: null, tokenExchangeRates: {}, tokens: [], + send: { + gasLimit: null, + gasPrice: null, + }, }, state.metamask) switch (action.type) { @@ -152,6 +156,22 @@ function reduceMetamask (state, action) { tokens: action.newTokens, }) + case actions.UPDATE_GAS_LIMIT: + return extend(metamaskState, { + send: { + ...metamaskState.send, + gasLimit: action.value, + }, + }) + + case actions.UPDATE_GAS_PRICE: + return extend(metamaskState, { + send: { + ...metamaskState.send, + gasPrice: action.value, + }, + }) + default: return metamaskState diff --git a/ui/app/selectors.js b/ui/app/selectors.js index 951161510..bf3d3399e 100644 --- a/ui/app/selectors.js +++ b/ui/app/selectors.js @@ -10,6 +10,8 @@ const selectors = { transactionsSelector, accountsWithSendEtherInfoSelector, getCurrentAccountWithSendEtherInfo, + getGasPrice, + getGasLimit, } module.exports = selectors @@ -46,7 +48,7 @@ function getSelectedTokenExchangeRate (state) { const tokenExchangeRates = state.metamask.tokenExchangeRates const selectedToken = getSelectedToken(state) || {} const { symbol = '' } = selectedToken - + const pair = `${symbol.toLowerCase()}_eth` const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {} @@ -92,3 +94,11 @@ function transactionsSelector (state) { : txsToRender .sort((a, b) => b.time - a.time) } + +function getGasPrice (state) { + return state.metamask.send.gasPrice +} + +function getGasLimit (state) { + return state.metamask.send.gasLimit +} diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index af7586859..314f6a666 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -62,12 +62,6 @@ SendTransactionScreen.prototype.componentWillMount = function () { gas: '746a528800', }), ]) - .then(([blockGasPrice, estimatedGas]) => { - this.setState({ - gasPrice: blockGasPrice, - gasLimit: estimatedGas, - }) - }) } SendTransactionScreen.prototype.renderHeaderIcon = function () { @@ -112,14 +106,16 @@ SendTransactionScreen.prototype.render = function () { showCustomizeGasModal, selectedAccount, primaryCurrency = 'ETH', + gasLimit, + gasPrice, } = this.props const { dropdownOpen, to, amount, - gasLimit, - gasPrice, + // gasLimit, + // gasPrice, memo, } = this.state -- cgit v1.2.3 From 7caa9142235cc0eca20d638a066d666d8cfaabee Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Mon, 16 Oct 2017 01:27:51 -0400 Subject: Fix Import Account link not hiding sidebar --- ui/app/components/dropdowns/components/account-dropdowns.js | 9 +++++++-- ui/app/components/wallet-view.js | 3 ++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ui/app/components/dropdowns/components/account-dropdowns.js b/ui/app/components/dropdowns/components/account-dropdowns.js index fc60c6005..e2eed1e4b 100644 --- a/ui/app/components/dropdowns/components/account-dropdowns.js +++ b/ui/app/components/dropdowns/components/account-dropdowns.js @@ -164,7 +164,7 @@ class AccountDropdowns extends Component { } renderAccountSelector () { - const { actions, useCssTransition, innerStyle } = this.props + const { actions, useCssTransition, innerStyle, sidebarOpen } = this.props const { accountSelectorActive, menuItemStyles } = this.state return h( @@ -223,7 +223,11 @@ class AccountDropdowns extends Component { h( DropdownMenuItem, { - closeMenu: () => {}, + closeMenu: () => { + if (sidebarOpen) { + actions.hideSidebar() + } + }, onClick: () => actions.showImportPage(), style: Object.assign( {}, @@ -457,6 +461,7 @@ const mapDispatchToProps = (dispatch) => { function mapStateToProps (state) { return { keyrings: state.metamask.keyrings, + sidebarOpen: state.appState.sidebarOpen, } } diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index 00c86298d..54d90b7ac 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -45,8 +45,9 @@ WalletView.prototype.renderWalletBalance = function () { selectedAccount, unsetSelectedToken, hideSidebar, - sidebarOpen + sidebarOpen, } = this.props + const selectedClass = selectedTokenAddress ? '' : 'wallet-balance-wrapper--active' -- cgit v1.2.3 From c77bc5d408b3717cb6de66e7458bcd888c526958 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 16 Oct 2017 04:12:51 -0700 Subject: Bump version on eth-simple-keyring Fixes bug where imported accounts could not use the new `signTypedData` method. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2b7b2056a..1dee72a1d 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "eth-query": "^2.1.2", "eth-rpc-client": "^1.1.3", "eth-sig-util": "^1.4.0", - "eth-simple-keyring": "^1.1.1", + "eth-simple-keyring": "^1.2.0", "eth-token-tracker": "^1.1.4", "ethereumjs-tx": "^1.3.0", "ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", -- cgit v1.2.3 From 10011395f6ab3f0784751dc7dc23074feac35b29 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 16 Oct 2017 04:14:09 -0700 Subject: Bump changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65f75ee18..11a093621 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Current Master - Fix bug where web3 API was sometimes injected after the page loaded. +- Fix bug where imported accounts could not use new eth_signTypedData method. ## 3.11.0 2017-10-11 -- cgit v1.2.3 From ad970180f2ca00e76302f97a532341502e990e38 Mon Sep 17 00:00:00 2001 From: Daijiro Wachi Date: Tue, 17 Oct 2017 22:03:40 +0200 Subject: Increase line-height for account nicknames --- ui/app/account-detail.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index a844daf88..d4f707e0b 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -121,6 +121,7 @@ AccountDetailScreen.prototype.render = function () { overflow: 'hidden', textOverflow: 'ellipsis', padding: '5px 0px', + lineHeight: '25px', }, }, [ identity && identity.name, -- cgit v1.2.3 From ab31eb6a17f5ab230fe47df66344cbce59223306 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 17 Oct 2017 13:09:41 -0700 Subject: Select first account on new vault creation --- app/scripts/metamask-controller.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index a742f3cba..2a45e413b 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -336,7 +336,7 @@ module.exports = class MetamaskController extends EventEmitter { // KeyringController setLocked: nodeify(keyringController.setLocked, keyringController), - createNewVaultAndKeychain: nodeify(keyringController.createNewVaultAndKeychain, keyringController), + createNewVaultAndKeychain: this.createNewVaultAndKeychain.bind(this), createNewVaultAndRestore: nodeify(keyringController.createNewVaultAndRestore, keyringController), addNewKeyring: nodeify(keyringController.addNewKeyring, keyringController), saveAccountLabel: nodeify(keyringController.saveAccountLabel, keyringController), @@ -458,6 +458,17 @@ module.exports = class MetamaskController extends EventEmitter { // Vault Management // + createNewVaultAndKeychain (password, cb) { + this.keyringController.createNewVaultAndKeychain(password) + .then((vault) => { + const { identities } = vault + const address = Object.keys(identities)[0] + this.preferencesController.setSelectedAddress(address) + cb(null, vault) + }) + .catch(reason => cb(reason)) + } + submitPassword (password, cb) { return this.keyringController.submitPassword(password) .then((newState) => { cb(null, newState) }) -- cgit v1.2.3 From d7f384485d2af15ec694208b9ef068c18c7dc91d Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 17 Oct 2017 13:19:57 -0700 Subject: Select first account when restoring seed Fixes #2348 --- app/scripts/metamask-controller.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 2a45e413b..8a51fdd8d 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -337,7 +337,7 @@ module.exports = class MetamaskController extends EventEmitter { // KeyringController setLocked: nodeify(keyringController.setLocked, keyringController), createNewVaultAndKeychain: this.createNewVaultAndKeychain.bind(this), - createNewVaultAndRestore: nodeify(keyringController.createNewVaultAndRestore, keyringController), + createNewVaultAndRestore: this.createNewVaultAndRestore.bind(this), addNewKeyring: nodeify(keyringController.addNewKeyring, keyringController), saveAccountLabel: nodeify(keyringController.saveAccountLabel, keyringController), exportAccount: nodeify(keyringController.exportAccount, keyringController), @@ -461,14 +461,28 @@ module.exports = class MetamaskController extends EventEmitter { createNewVaultAndKeychain (password, cb) { this.keyringController.createNewVaultAndKeychain(password) .then((vault) => { - const { identities } = vault - const address = Object.keys(identities)[0] + this.selectFirstIdentity(vault) this.preferencesController.setSelectedAddress(address) cb(null, vault) }) .catch(reason => cb(reason)) } + createNewVaultAndRestore (password, seed, cb) { + this.keyringController.createNewVaultAndRestore(password, seed) + .then((vault) => { + this.selectFirstIdentity(vault) + cb(null, vault) + }) + .catch(reason => cb(reason)) + } + + selectFirstIdentity (vault) { + const { identities } = vault + const address = Object.keys(identities)[0] + this.preferencesController.setSelectedAddress(address) + } + submitPassword (password, cb) { return this.keyringController.submitPassword(password) .then((newState) => { cb(null, newState) }) -- cgit v1.2.3 From 50e8599988c54bbf9ee0e9f324f79f5835fa6727 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 17 Oct 2017 13:25:27 -0700 Subject: Promisify metamask-controller vault creating methods --- app/scripts/metamask-controller.js | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 8a51fdd8d..4b11f6024 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -336,8 +336,8 @@ module.exports = class MetamaskController extends EventEmitter { // KeyringController setLocked: nodeify(keyringController.setLocked, keyringController), - createNewVaultAndKeychain: this.createNewVaultAndKeychain.bind(this), - createNewVaultAndRestore: this.createNewVaultAndRestore.bind(this), + createNewVaultAndKeychain: nodeify(this.createNewVaultAndKeychain, this), + createNewVaultAndRestore: nodeify(this.createNewVaultAndRestore, this), addNewKeyring: nodeify(keyringController.addNewKeyring, keyringController), saveAccountLabel: nodeify(keyringController.saveAccountLabel, keyringController), exportAccount: nodeify(keyringController.exportAccount, keyringController), @@ -458,23 +458,16 @@ module.exports = class MetamaskController extends EventEmitter { // Vault Management // - createNewVaultAndKeychain (password, cb) { - this.keyringController.createNewVaultAndKeychain(password) - .then((vault) => { - this.selectFirstIdentity(vault) - this.preferencesController.setSelectedAddress(address) - cb(null, vault) - }) - .catch(reason => cb(reason)) + async createNewVaultAndKeychain (password, cb) { + const vault = await this.keyringController.createNewVaultAndKeychain(password) + this.selectFirstIdentity(vault) + return vault } - createNewVaultAndRestore (password, seed, cb) { - this.keyringController.createNewVaultAndRestore(password, seed) - .then((vault) => { - this.selectFirstIdentity(vault) - cb(null, vault) - }) - .catch(reason => cb(reason)) + async createNewVaultAndRestore (password, seed, cb) { + const vault = await this.keyringController.createNewVaultAndRestore(password, seed) + this.selectFirstIdentity(vault) + return vault } selectFirstIdentity (vault) { -- cgit v1.2.3 From 9c45af3e2567e2afab9162f27fd4919cfa0957c5 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 17 Oct 2017 13:26:16 -0700 Subject: Bump changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65f75ee18..1c82c59be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Current Master - Fix bug where web3 API was sometimes injected after the page loaded. +- Fix bug where first account was sometimes not selected correctly after creating or restoring a vault. ## 3.11.0 2017-10-11 -- cgit v1.2.3 From ac43872c1a1468057974648c8ae90bf1edd708d7 Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 12 Oct 2017 14:59:03 -0230 Subject: Enable send-v2 functionality. --- ui/app/actions.js | 14 +++++++++++ ui/app/components/send/currency-display.js | 6 ++--- ui/app/components/send/from-dropdown.js | 17 ++++++++++--- ui/app/components/send/send-v2-container.js | 5 ++++ ui/app/send-v2.js | 39 +++++++++++++++++++++++++++-- 5 files changed, 72 insertions(+), 9 deletions(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index b0ef7d0a3..9744bf67f 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -137,6 +137,7 @@ var actions = { UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE', updateGasLimit, updateGasPrice, + setSelectedAddress, // app messages confirmSeedWords: confirmSeedWords, showAccountDetail: showAccountDetail, @@ -699,6 +700,19 @@ function setSelectedToken (tokenAddress) { } } +function setSelectedAddress (address) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.setSelectedAddress`) + background.setSelectedAddress(address, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + }) + } +} + function showAccountDetail (address) { return (dispatch) => { dispatch(actions.showLoadingIndication()) diff --git a/ui/app/components/send/currency-display.js b/ui/app/components/send/currency-display.js index ed9847fdb..d56c119f1 100644 --- a/ui/app/components/send/currency-display.js +++ b/ui/app/components/send/currency-display.js @@ -100,9 +100,9 @@ CurrencyDisplay.prototype.render = function () { this.setState({ value: newValue }) } }, - onBlur: event => this.handleChangeInHexWei(event.target.value.split(' ')[0]), - onKeyUp: event => resetCaretIfPastEnd(value || initValueToRender, event), - onClick: event => resetCaretIfPastEnd(value || initValueToRender, event), + onBlur: event => !readOnly && this.handleChangeInHexWei(event.target.value.split(' ')[0]), + onKeyUp: event => !readOnly && resetCaretIfPastEnd(value || initValueToRender, event), + onClick: event => !readOnly && resetCaretIfPastEnd(value || initValueToRender, event), }), ]), diff --git a/ui/app/components/send/from-dropdown.js b/ui/app/components/send/from-dropdown.js index e8e1d43f0..fd6fb7e64 100644 --- a/ui/app/components/send/from-dropdown.js +++ b/ui/app/components/send/from-dropdown.js @@ -19,7 +19,14 @@ FromDropdown.prototype.getListItemIcon = function (currentAccount, selectedAccou : null } -FromDropdown.prototype.renderDropdown = function (accounts, selectedAccount, closeDropdown) { +FromDropdown.prototype.renderDropdown = function () { + const { + accounts, + selectedAccount, + closeDropdown, + onSelect, + } = this.props + return h('div', {}, [ h('div.send-v2__from-dropdown__close-area', { @@ -30,7 +37,10 @@ FromDropdown.prototype.renderDropdown = function (accounts, selectedAccount, clo ...accounts.map(account => h(AccountListItem, { account, - handleClick: () => console.log('Select identity'), + handleClick: () => { + onSelect(account.address) + closeDropdown() + }, icon: this.getListItemIcon(account, selectedAccount), })) @@ -43,7 +53,6 @@ FromDropdown.prototype.render = function () { const { accounts, selectedAccount, - setFromField, openDropdown, closeDropdown, dropdownOpen, @@ -57,7 +66,7 @@ FromDropdown.prototype.render = function () { icon: h(`i.fa.fa-caret-down.fa-lg`, { style: { color: '#dedede' } }) }), - dropdownOpen && this.renderDropdown(accounts, selectedAccount, closeDropdown), + dropdownOpen && this.renderDropdown(), ]) diff --git a/ui/app/components/send/send-v2-container.js b/ui/app/components/send/send-v2-container.js index c3af1c972..5935a8fee 100644 --- a/ui/app/components/send/send-v2-container.js +++ b/ui/app/components/send/send-v2-container.js @@ -62,5 +62,10 @@ function mapDispatchToProps (dispatch) { estimateGas: params => dispatch(actions.estimateGas(params)), getGasPrice: () => dispatch(actions.getGasPrice()), updateTokenExchangeRate: token => dispatch(actions.updateTokenExchangeRate(token)), + signTokenTx: (tokenAddress, toAddress, amount, txData) => ( + dispatch(actions.signTokenTx(tokenAddress, toAddress, amount, txData)) + ), + signTx: txParams => dispatch(actions.signTx(txParams)), + setSelectedAddress: address => dispatch(actions.setSelectedAddress(address)) } } diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index 314f6a666..f04b95800 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -105,6 +105,7 @@ SendTransactionScreen.prototype.render = function () { selectedToken, showCustomizeGasModal, selectedAccount, + setSelectedAddress, primaryCurrency = 'ETH', gasLimit, gasPrice, @@ -150,7 +151,7 @@ SendTransactionScreen.prototype.render = function () { dropdownOpen, accounts, selectedAccount, - setFromField: () => console.log('Set From Field'), + onSelect: address => setSelectedAddress(address), openDropdown: () => this.setState({ dropdownOpen: true }), closeDropdown: () => this.setState({ dropdownOpen: false }), conversionRate, @@ -235,9 +236,43 @@ SendTransactionScreen.prototype.render = function () { // Buttons underneath card h('div.send-v2__footer', [ h('button.send-v2__cancel-btn', {}, 'Cancel'), - h('button.send-v2__next-btn', {}, 'Next'), + h('button.send-v2__next-btn', { + onClick: event => this.onSubmit(event), + }, 'Next'), ]), ]) ) } + +SendTransactionScreen.prototype.onSubmit = function (event) { + event.preventDefault() + const { + to, + amount, + } = this.state + const { + gasLimit: gas, + gasPrice, + signTokenTx, + signTx, + selectedToken, + selectedAccount: { address: from }, + } = this.props + + const txParams = { + from, + value: '0', + gas, + gasPrice, + } + + if (!selectedToken) { + txParams.value = amount + txParams.to = to + } + + selectedToken + ? signTokenTx(selectedToken.address, to, amount, txParams) + : signTx(txParams) +} -- cgit v1.2.3 From 5ee6e4d3b3d61d804340c22b73a608fb6b44a9b2 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Mon, 16 Oct 2017 01:28:25 -0400 Subject: wip --- ui/app/actions.js | 9 ++ ui/app/app.js | 11 ++- ui/app/components/account-menu/index.js | 110 +++++++++++++++++++++++-- ui/app/components/dropdowns/components/menu.js | 9 +- ui/app/components/wallet-view.js | 1 - ui/app/css/itcss/components/account-menu.scss | 13 +++ ui/app/css/itcss/components/menu.scss | 11 +++ ui/app/reducers/metamask.js | 6 ++ 8 files changed, 159 insertions(+), 11 deletions(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index 9744bf67f..7ac0acf05 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -201,6 +201,9 @@ var actions = { callBackgroundThenUpdate, forceUpdateMetamaskState, + + TOGGLE_ACCOUNT_MENU: 'TOGGLE_ACCOUNT_MENU', + toggleAccountMenu, } module.exports = actions @@ -1303,3 +1306,9 @@ function forceUpdateMetamaskState (dispatch) { dispatch(actions.updateMetamaskState(newState)) }) } + +function toggleAccountMenu () { + return { + type: actions.TOGGLE_ACCOUNT_MENU, + } +} diff --git a/ui/app/app.js b/ui/app/app.js index 08d24d86c..3f27b36c7 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -91,6 +91,7 @@ function mapDispatchToProps (dispatch, ownProps) { showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()), hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()), setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')), + toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()), } } @@ -256,10 +257,12 @@ App.prototype.renderAppBar = function () { ]), - h(Identicon, { - address: this.props.selectedAddress, - diameter: 32, - }), + h('div.account-menu__icon', { onClick: this.props.toggleAccountMenu }, [ + h(Identicon, { + address: this.props.selectedAddress, + diameter: 32, + }), + ]), ]), ]), ]), diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index 7dc3d10a5..3b1118271 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -3,7 +3,9 @@ const Component = require('react').Component const connect = require('react-redux').connect const h = require('react-hyperscript') const actions = require('../../actions') -const { Menu, Item, Divider } = require('../dropdowns/components/menu') +const { Menu, Item, Divider, CloseArea } = require('../dropdowns/components/menu') +const Identicon = require('../identicon') +const { formatBalance } = require('../../util') module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountMenu) @@ -13,21 +15,33 @@ function AccountMenu () { Component.call(this) } function mapStateToProps (state) { return { selectedAddress: state.metamask.selectedAddress, + isAccountMenuOpen: state.metamask.isAccountMenuOpen, + keyrings: state.metamask.keyrings, + identities: state.metamask.identities, + accounts: state.metamask.accounts, + } } -function mapDispatchToProps () { - return {} +// identities, accounts, selected, menuItemStyles, actions, keyrings + +function mapDispatchToProps (dispatch) { + return { + toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()), + } } AccountMenu.prototype.render = function () { - return h(Menu, { className: 'account-menu' }, [ + const { isAccountMenuOpen, toggleAccountMenu } = this.props + + return h(Menu, { className: 'account-menu', isShowing: isAccountMenuOpen }, [ + h(CloseArea, { onClick: toggleAccountMenu }), h(Item, { className: 'account-menu__header' }, [ 'My Accounts', h('button.account-menu__logout-button', 'Log out'), ]), h(Divider), - h(Item, { text: 'hi' }), + h('div.account-menu__accounts', this.renderAccounts()), h(Divider), h(Item, { onClick: true, @@ -53,4 +67,90 @@ AccountMenu.prototype.render = function () { ]) } +AccountMenu.prototype.renderAccounts = function () { + const { identities, accounts, selected, actions, keyrings } = this.props + + return Object.keys(identities).map((key, index) => { + const identity = identities[key] + const isSelected = identity.address === selected + + const balanceValue = accounts[key].balance + const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' + const simpleAddress = identity.address.substring(2).toLowerCase() + + const keyring = keyrings.find((kr) => { + return kr.accounts.includes(simpleAddress) || + kr.accounts.includes(identity.address) + }) + + return h( + 'div.account-menu__account', + { + onClick: () => { + this.props.actions.showAccountDetail(identity.address) + }, + }, + [ + h('div.account-menu__check-mark', [ + isSelected ? h('i.fa.fa-check') : null, + ]), + h( + Identicon, + { + address: identity.address, + diameter: 24, + }, + ), + + h('div.account-menu__account-info', [ + + this.indicateIfLoose(keyring), + + h('div.account-menu__name', { + style: { + fontSize: '18px', + maxWidth: '145px', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + }, identity.name || ''), + + h('div.account-menu__balance', formattedBalance), + ]), + + h('div.account-menu__action', { + onClick: () => { + actions.showEditAccountModal(identity) + }, + }, 'Edit'), + +// ======= +// }, +// ), +// this.indicateIfLoose(keyring), +// h('span', { +// style: { +// marginLeft: '20px', +// fontSize: '24px', +// maxWidth: '145px', +// whiteSpace: 'nowrap', +// overflow: 'hidden', +// textOverflow: 'ellipsis', +// }, +// }, identity.name || ''), +// h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, isSelected ? h('.check', '✓') : null), +// >>>>>>> master:ui/app/components/account-dropdowns.js + ], + ) + }) +} + +AccountMenu.prototype.indicateIfLoose = function (keyring) { + try { // Sometimes keyrings aren't loaded yet: + const type = keyring.type + const isLoose = type !== 'HD Key Tree' + return isLoose ? h('.keyring-label', 'LOOSE') : null + } catch (e) { return } +} diff --git a/ui/app/components/dropdowns/components/menu.js b/ui/app/components/dropdowns/components/menu.js index 0cbe0f342..323103f0b 100644 --- a/ui/app/components/dropdowns/components/menu.js +++ b/ui/app/components/dropdowns/components/menu.js @@ -41,4 +41,11 @@ Divider.prototype.render = function () { return h('div.menu__divider') } -module.exports = { Menu, Item, Divider } +inherits(CloseArea, Component) +function CloseArea () { Component.call(this) } + +CloseArea.prototype.render = function () { + return h('div.menu__close-area', { onClick: this.props.onClick }) +} + +module.exports = { Menu, Item, Divider, CloseArea } diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index 54d90b7ac..f06c4d512 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -136,7 +136,6 @@ WalletView.prototype.render = function () { selected: selectedAddress, network, identities, - enableAccountsSelector: true, }, []), ]), diff --git a/ui/app/css/itcss/components/account-menu.scss b/ui/app/css/itcss/components/account-menu.scss index 8b08c02fd..5ed42f627 100644 --- a/ui/app/css/itcss/components/account-menu.scss +++ b/ui/app/css/itcss/components/account-menu.scss @@ -20,6 +20,10 @@ right: calc((100vw - 65vw) / 2); } + &__icon { + cursor: pointer; + } + &__header { display: flex; flex-flow: row nowrap; @@ -41,4 +45,13 @@ width: 16px; height: 16px; } + + &__accounts { + display: flex; + flex-flow: column nowrap; + overflow-y: auto; + height: 272px; + position: relative; + z-index: 200; + } } diff --git a/ui/app/css/itcss/components/menu.scss b/ui/app/css/itcss/components/menu.scss index d01c24a70..0f83146a8 100644 --- a/ui/app/css/itcss/components/menu.scss +++ b/ui/app/css/itcss/components/menu.scss @@ -10,6 +10,8 @@ display: flex; flex-flow: row nowrap; align-items: center; + position: relative; + z-index: 200; &--clickable { cursor: pointer; @@ -40,4 +42,13 @@ width: 100%; height: 1px; } + + &__close-area { + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: 0; + z-index: 100; + } } diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index cc24a6729..a9a54e91e 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -10,6 +10,7 @@ function reduceMetamask (state, action) { var metamaskState = extend({ isInitialized: false, isUnlocked: false, + isAccountMenuOpen: false, rpcTarget: 'https://rawtestrpc.metamask.io/', identities: {}, unapprovedTxs: {}, @@ -172,6 +173,11 @@ function reduceMetamask (state, action) { }, }) + case actions.TOGGLE_ACCOUNT_MENU: + return extend(metamaskState, { + isAccountMenuOpen: !metamaskState.isAccountMenuOpen, + }) + default: return metamaskState -- cgit v1.2.3 From 085551b7e6b7dab21c21b99a40c4f79c413799d5 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Tue, 17 Oct 2017 22:36:53 -0700 Subject: New Account modal --- ui/app/accounts/import/index.js | 1 + ui/app/components/account-menu/index.js | 100 ++++++++++++------------- ui/app/components/dropdowns/components/menu.js | 4 +- ui/app/config.js | 2 +- ui/app/css/itcss/components/account-menu.scss | 63 +++++++++++++++- ui/app/css/itcss/components/confirm.scss | 2 +- ui/app/css/itcss/components/header.scss | 4 + ui/app/css/itcss/components/menu.scss | 4 + ui/app/css/itcss/components/network.scss | 1 + ui/app/css/itcss/tools/utilities.scss | 16 ++-- 10 files changed, 132 insertions(+), 65 deletions(-) diff --git a/ui/app/accounts/import/index.js b/ui/app/accounts/import/index.js index 821bb6efe..c66dcfc66 100644 --- a/ui/app/accounts/import/index.js +++ b/ui/app/accounts/import/index.js @@ -37,6 +37,7 @@ AccountImportSubview.prototype.render = function () { h('div.flex-center', { style: { flexDirection: 'column', + marginTop: '32px', }, }, [ h('.section-title.flex-row.flex-center', [ diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index 3b1118271..2ebdba24a 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -23,20 +23,50 @@ function mapStateToProps (state) { } } -// identities, accounts, selected, menuItemStyles, actions, keyrings - function mapDispatchToProps (dispatch) { return { toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()), + showAccountDetail: address => { + dispatch(actions.showAccountDetail(address)) + dispatch(actions.toggleAccountMenu()) + }, + lockMetamask: () => { + dispatch(actions.lockMetamask()) + dispatch(actions.toggleAccountMenu()) + }, + showConfigPage: () => { + console.log('hihihih') + dispatch(actions.showConfigPage()) + dispatch(actions.toggleAccountMenu()) + }, + showNewAccountModal: () => { + dispatch(actions.showModal({ name: 'NEW_ACCOUNT' })) + dispatch(actions.toggleAccountMenu()) + }, + showImportPage: () => { + dispatch(actions.showImportPage()) + dispatch(actions.toggleAccountMenu()) + }, } } AccountMenu.prototype.render = function () { - const { isAccountMenuOpen, toggleAccountMenu } = this.props - + const { + isAccountMenuOpen, + toggleAccountMenu, + showNewAccountModal, + showImportPage, + lockMetamask, + showConfigPage, + } = this.props + + console.log(showConfigPage) return h(Menu, { className: 'account-menu', isShowing: isAccountMenuOpen }, [ h(CloseArea, { onClick: toggleAccountMenu }), - h(Item, { className: 'account-menu__header' }, [ + h(Item, { + className: 'account-menu__header', + onClick: lockMetamask, + }, [ 'My Accounts', h('button.account-menu__logout-button', 'Log out'), ]), @@ -44,23 +74,22 @@ AccountMenu.prototype.render = function () { h('div.account-menu__accounts', this.renderAccounts()), h(Divider), h(Item, { - onClick: true, + onClick: showNewAccountModal, icon: h('img', { src: 'images/plus-btn-white.svg' }), text: 'Create Account', }), h(Item, { - onClick: true, + onClick: showImportPage, icon: h('img', { src: 'images/import-account.svg' }), text: 'Import Account', }), h(Divider), h(Item, { - onClick: true, icon: h('img', { src: 'images/mm-info-icon.svg' }), text: 'Info & Help', }), h(Item, { - onClick: true, + onClick: showConfigPage, icon: h('img', { src: 'images/settings.svg' }), text: 'Settings', }), @@ -68,7 +97,13 @@ AccountMenu.prototype.render = function () { } AccountMenu.prototype.renderAccounts = function () { - const { identities, accounts, selected, actions, keyrings } = this.props + const { + identities, + accounts, + selected, + keyrings, + showAccountDetail, + } = this.props return Object.keys(identities).map((key, index) => { const identity = identities[key] @@ -84,12 +119,8 @@ AccountMenu.prototype.renderAccounts = function () { }) return h( - 'div.account-menu__account', - { - onClick: () => { - this.props.actions.showAccountDetail(identity.address) - }, - }, + 'div.account-menu__account.menu__item--clickable', + { onClick: () => showAccountDetail(identity.address) }, [ h('div.account-menu__check-mark', [ isSelected ? h('i.fa.fa-check') : null, @@ -104,44 +135,11 @@ AccountMenu.prototype.renderAccounts = function () { ), h('div.account-menu__account-info', [ - - this.indicateIfLoose(keyring), - - h('div.account-menu__name', { - style: { - fontSize: '18px', - maxWidth: '145px', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - }, identity.name || ''), - + h('div.account-menu__name', identity.name || ''), h('div.account-menu__balance', formattedBalance), ]), - h('div.account-menu__action', { - onClick: () => { - actions.showEditAccountModal(identity) - }, - }, 'Edit'), - -// ======= -// }, -// ), -// this.indicateIfLoose(keyring), -// h('span', { -// style: { -// marginLeft: '20px', -// fontSize: '24px', -// maxWidth: '145px', -// whiteSpace: 'nowrap', -// overflow: 'hidden', -// textOverflow: 'ellipsis', -// }, -// }, identity.name || ''), -// h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, isSelected ? h('.check', '✓') : null), -// >>>>>>> master:ui/app/components/account-dropdowns.js + this.indicateIfLoose(keyring), ], ) }) diff --git a/ui/app/components/dropdowns/components/menu.js b/ui/app/components/dropdowns/components/menu.js index 323103f0b..f6d8a139e 100644 --- a/ui/app/components/dropdowns/components/menu.js +++ b/ui/app/components/dropdowns/components/menu.js @@ -28,8 +28,8 @@ Item.prototype.render = function () { const textComponent = text ? h('div.menu__item__text', text) : null return children - ? h('div', { className: itemClassName }, children) - : h('div.menu__item', { className: itemClassName }, [ iconComponent, textComponent ] + ? h('div', { className: itemClassName, onClick }, children) + : h('div.menu__item', { className: itemClassName, onClick }, [ iconComponent, textComponent ] .filter(d => Boolean(d)) ) } diff --git a/ui/app/config.js b/ui/app/config.js index 0fe232c07..282a28301 100644 --- a/ui/app/config.js +++ b/ui/app/config.js @@ -30,7 +30,7 @@ ConfigScreen.prototype.render = function () { var warning = state.warning return ( - h('.flex-column.flex-grow', [ + h('.flex-column.flex-grow', { style: { marginTop: '32px' } }, [ // subtitle and nav h('.section-title.flex-row.flex-center', [ diff --git a/ui/app/css/itcss/components/account-menu.scss b/ui/app/css/itcss/components/account-menu.scss index 5ed42f627..857903ce1 100644 --- a/ui/app/css/itcss/components/account-menu.scss +++ b/ui/app/css/itcss/components/account-menu.scss @@ -39,6 +39,7 @@ font-size: 12px; line-height: 23px; padding: 0 24px; + font-weight: 200; } img { @@ -50,8 +51,68 @@ display: flex; flex-flow: column nowrap; overflow-y: auto; - height: 272px; + max-height: 240px; position: relative; z-index: 200; + + &::-webkit-scrollbar { + display: none; + } + + @media screen and (max-width: 575px) { + max-height: 215px; + } + + .keyring-label { + margin-top: 5px; + } + } + + &__account { + display: flex; + flex-flow: row nowrap; + padding: 16px 14px; + flex: 0 0 auto; + + @media screen and (max-width: 575px) { + padding: 12px 14px; + } + } + + &__account-info { + flex: 1 0 auto; + display: flex; + flex-flow: column nowrap; + padding-top: 4px; + } + + &__check-mark { + width: 14px; + flex: 0 0 auto; + } + + .identicon { + margin: 0 12px 0 0; + flex: 0 0 auto; + } + + &__name { + color: $white; + font-size: 18px; + font-weight: 200; + line-height: 16px; + } + + &__balance { + color: $dusty-gray; + font-size: 14px; + line-height: 19px; + } + + &__action { + font-size: 16px; + line-height: 18px; + font-weight: 200; + cursor: pointer; } } diff --git a/ui/app/css/itcss/components/confirm.scss b/ui/app/css/itcss/components/confirm.scss index 15c752923..d4f0fe5ac 100644 --- a/ui/app/css/itcss/components/confirm.scss +++ b/ui/app/css/itcss/components/confirm.scss @@ -37,7 +37,7 @@ overflow-y: auto; top: 0; box-shadow: none; - height: calc(100vh - 41px - 100px); + height: calc(100vh - 58px - 100px); border-top-left-radius: 0; border-top-right-radius: 0; } diff --git a/ui/app/css/itcss/components/header.scss b/ui/app/css/itcss/components/header.scss index 512cbd995..ef84dc3f4 100644 --- a/ui/app/css/itcss/components/header.scss +++ b/ui/app/css/itcss/components/header.scss @@ -58,6 +58,10 @@ text-transform: uppercase; font-weight: 400; color: #22232c; // $shark + + @media screen and (max-width: 575px) { + display: none; + } } h2.page-subtitle { diff --git a/ui/app/css/itcss/components/menu.scss b/ui/app/css/itcss/components/menu.scss index 0f83146a8..c98ee70d9 100644 --- a/ui/app/css/itcss/components/menu.scss +++ b/ui/app/css/itcss/components/menu.scss @@ -13,6 +13,10 @@ position: relative; z-index: 200; + @media screen and (max-width: 575px) { + padding: 14px; + } + &--clickable { cursor: pointer; diff --git a/ui/app/css/itcss/components/network.scss b/ui/app/css/itcss/components/network.scss index bf699ac57..bb8c4eea8 100644 --- a/ui/app/css/itcss/components/network.scss +++ b/ui/app/css/itcss/components/network.scss @@ -2,6 +2,7 @@ border: 1px solid $shark; border-radius: 82px; padding: 6px; + flex: 0 0 auto; &.ethereum-network { border-color: rgb(3, 135, 137); diff --git a/ui/app/css/itcss/tools/utilities.scss b/ui/app/css/itcss/tools/utilities.scss index 4a55303b9..ca9fd0d9c 100644 --- a/ui/app/css/itcss/tools/utilities.scss +++ b/ui/app/css/itcss/tools/utilities.scss @@ -231,17 +231,15 @@ hr.horizontal-line { .keyring-label { z-index: 1; - font-size: 11px; - background: rgba(255, 0, 0, .8); - bottom: -47px; - color: $white; + font-size: 8px; + line-height: 8px; + background: rgba(255, 255, 255, 0.4); + color: #fff; border-radius: 10px; - height: 20px; - min-width: 20px; - position: absolute; - top: 0px; - right: 5px; padding: 4px; + width: 41px; + text-align: center; + height: 15px; } .ether-balance { -- cgit v1.2.3 From 03685c64b8e45d893c86478869200933de043da8 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Tue, 17 Oct 2017 22:42:56 -0700 Subject: Network dropdown update --- ui/app/components/dropdowns/network-dropdown.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/app/components/dropdowns/network-dropdown.js b/ui/app/components/dropdowns/network-dropdown.js index c64e7a1d1..736019c39 100644 --- a/ui/app/components/dropdowns/network-dropdown.js +++ b/ui/app/components/dropdowns/network-dropdown.js @@ -55,6 +55,7 @@ NetworkDropdown.prototype.render = function () { fontFamily: 'DIN OT', fontSize: '16px', lineHeight: '20px', + padding: '12px 0', } return h(Dropdown, { @@ -81,7 +82,7 @@ NetworkDropdown.prototype.render = function () { minWidth: '309px', }, innerStyle: { - padding: '10px 8px', + padding: '18px 8px', }, }, [ -- cgit v1.2.3 From 7032edf32b43e94a7f58c7bcb068da63fa6bda1b Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 18 Oct 2017 11:13:14 -0700 Subject: Stop tracking old account balances after restore vault Per @kgserrano note --- app/scripts/metamask-controller.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 4b11f6024..b6a3749e4 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -459,17 +459,30 @@ module.exports = class MetamaskController extends EventEmitter { // async createNewVaultAndKeychain (password, cb) { + this.forgetOldAccounts() const vault = await this.keyringController.createNewVaultAndKeychain(password) this.selectFirstIdentity(vault) return vault } async createNewVaultAndRestore (password, seed, cb) { + this.forgetOldAccounts() const vault = await this.keyringController.createNewVaultAndRestore(password, seed) this.selectFirstIdentity(vault) return vault } + forgetOldAccounts () { + const { accountTracker } = this + let oldAccounts = [] + try { + oldAccounts = Object.keys(accountTracker.store.getState().accounts) + } catch (e) { + log.warn('Could not load old accounts to forget', e) + } + oldAccounts.forEach(addr => accountTracker.removeAccount(addr)) + } + selectFirstIdentity (vault) { const { identities } = vault const address = Object.keys(identities)[0] -- cgit v1.2.3 From ea79eca8eb19cf7ce375e03ad8cbde010299936c Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 18 Oct 2017 12:21:22 -0700 Subject: Add validation to balance constructor --- app/scripts/controllers/balance.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/scripts/controllers/balance.js b/app/scripts/controllers/balance.js index 4fa4c78fe..f83f294cc 100644 --- a/app/scripts/controllers/balance.js +++ b/app/scripts/controllers/balance.js @@ -5,7 +5,9 @@ const BN = require('ethereumjs-util').BN class BalanceController { constructor (opts = {}) { + this._validateParams(opts) const { address, accountTracker, txController, blockTracker } = opts + this.address = address this.accountTracker = accountTracker this.txController = txController @@ -65,6 +67,14 @@ class BalanceController { return pending } + _validateParams (opts) { + const { address, accountTracker, txController, blockTracker } = opts + if (!address || !accountTracker || !txController || !blockTracker) { + const error = 'Cannot construct a balance checker without address, accountTracker, txController, and blockTracker.' + throw new Error(error) + } + } + } module.exports = BalanceController -- cgit v1.2.3 From 9cc1e8a6d867b7f0663c55b017b471132f6a719e Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 18 Oct 2017 14:22:04 -0700 Subject: Refresh computed balances controller when restoring vault --- app/scripts/controllers/computed-balances.js | 4 ++++ app/scripts/metamask-controller.js | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/scripts/controllers/computed-balances.js b/app/scripts/controllers/computed-balances.js index 2479e1b3a..3479eae2b 100644 --- a/app/scripts/controllers/computed-balances.js +++ b/app/scripts/controllers/computed-balances.js @@ -25,6 +25,10 @@ class ComputedbalancesController { } } + forgetAllBalances () { + this.balances = {} + } + _initBalanceUpdating () { const store = this.accountTracker.store.getState() this.addAnyAccountsFromStore(store) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b6a3749e4..b312106dd 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -473,7 +473,7 @@ module.exports = class MetamaskController extends EventEmitter { } forgetOldAccounts () { - const { accountTracker } = this + const { accountTracker, balancesController } = this let oldAccounts = [] try { oldAccounts = Object.keys(accountTracker.store.getState().accounts) @@ -481,6 +481,7 @@ module.exports = class MetamaskController extends EventEmitter { log.warn('Could not load old accounts to forget', e) } oldAccounts.forEach(addr => accountTracker.removeAccount(addr)) + balancesController.forgetAllBalances() } selectFirstIdentity (vault) { -- cgit v1.2.3 From 75177ce34cac589be26fb8089aac04feccdbae81 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 18 Oct 2017 15:08:34 -0700 Subject: Make account tracking more reactive We were doing a lot of conditional observation & updating. Pulled out a bunch of that for generic observer/syncers. --- app/scripts/controllers/computed-balances.js | 22 ++++++++++++++-------- app/scripts/lib/account-tracker.js | 18 ++++++++++++++++++ app/scripts/metamask-controller.js | 22 +--------------------- 3 files changed, 33 insertions(+), 29 deletions(-) diff --git a/app/scripts/controllers/computed-balances.js b/app/scripts/controllers/computed-balances.js index 3479eae2b..009405d29 100644 --- a/app/scripts/controllers/computed-balances.js +++ b/app/scripts/controllers/computed-balances.js @@ -25,22 +25,28 @@ class ComputedbalancesController { } } - forgetAllBalances () { - this.balances = {} - } - _initBalanceUpdating () { const store = this.accountTracker.store.getState() - this.addAnyAccountsFromStore(store) - this.accountTracker.store.subscribe(this.addAnyAccountsFromStore.bind(this)) + this.syncAllAccountsFromStore(store) + this.accountTracker.store.subscribe(this.syncAllAccountsFromStore.bind(this)) } - addAnyAccountsFromStore(store) { - const balances = store.accounts + syncAllAccountsFromStore(store) { + const upstream = Object.keys(store.accounts) + const balances = Object.keys(this.balances) + .map(address => this.balances[address]) + // Follow new addresses for (let address in balances) { this.trackAddressIfNotAlready(address) } + + // Unfollow old ones + balances.forEach(({ address }) => { + if (!upstream.includes(address)) { + delete this.balances[address] + } + }) } trackAddressIfNotAlready (address) { diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index cdc21282d..13dea918f 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -38,6 +38,24 @@ class AccountTracker extends EventEmitter { // public // + syncWithAddresses (addresses) { + const accounts = this.store.getState().accounts + const locals = Object.keys(accounts) + .map(account => accounts[account.address]) + + addresses.forEach((upstream) => { + if (!locals.includes(upstream)) { + this.addAccount(upstream) + } + }) + + locals.forEach((local) => { + if (!addresses.includes(local)) { + this.removeAccount(local) + } + }) + } + addAccount (address) { const accounts = this.store.getState().accounts accounts[address] = {} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b312106dd..eae4478b5 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -123,13 +123,7 @@ module.exports = class MetamaskController extends EventEmitter { const address = addresses[0] this.preferencesController.setSelectedAddress(address) } - }) - this.keyringController.on('newAccount', (address) => { - this.preferencesController.setSelectedAddress(address) - this.accountTracker.addAccount(address) - }) - this.keyringController.on('removedAccount', (address) => { - this.accountTracker.removeAccount(address) + this.accountTracker.syncWithAddresses(addresses) }) // address book controller @@ -459,31 +453,17 @@ module.exports = class MetamaskController extends EventEmitter { // async createNewVaultAndKeychain (password, cb) { - this.forgetOldAccounts() const vault = await this.keyringController.createNewVaultAndKeychain(password) this.selectFirstIdentity(vault) return vault } async createNewVaultAndRestore (password, seed, cb) { - this.forgetOldAccounts() const vault = await this.keyringController.createNewVaultAndRestore(password, seed) this.selectFirstIdentity(vault) return vault } - forgetOldAccounts () { - const { accountTracker, balancesController } = this - let oldAccounts = [] - try { - oldAccounts = Object.keys(accountTracker.store.getState().accounts) - } catch (e) { - log.warn('Could not load old accounts to forget', e) - } - oldAccounts.forEach(addr => accountTracker.removeAccount(addr)) - balancesController.forgetAllBalances() - } - selectFirstIdentity (vault) { const { identities } = vault const address = Object.keys(identities)[0] -- cgit v1.2.3 From 8da0d0b28a52d476da3623774159e8d6a595da2d Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 18 Oct 2017 15:09:32 -0700 Subject: Revert "NetworkController refactor for new EthClient interface" --- app/scripts/controllers/network.js | 54 +++++++++++++------------- app/scripts/controllers/transactions.js | 1 - app/scripts/lib/events-proxy.js | 10 ++--- app/scripts/lib/nonce-tracker.js | 10 +++-- app/scripts/lib/obj-proxy.js | 19 --------- app/scripts/metamask-controller.js | 68 ++++++++++++++++----------------- package.json | 2 - test/integration/lib/first-time.js | 3 -- test/unit/network-contoller-test.js | 25 +++++++++--- test/unit/nonce-tracker-test.js | 7 ++-- ui/app/app.js | 2 +- 11 files changed, 96 insertions(+), 105 deletions(-) delete mode 100644 app/scripts/lib/obj-proxy.js diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js index 64ed4b7c2..0f9db4d53 100644 --- a/app/scripts/controllers/network.js +++ b/app/scripts/controllers/network.js @@ -1,12 +1,11 @@ const assert = require('assert') const EventEmitter = require('events') +const createMetamaskProvider = require('web3-provider-engine/zero.js') const ObservableStore = require('obs-store') const ComposedStore = require('obs-store/lib/composed') const extend = require('xtend') const EthQuery = require('eth-query') -const createEthRpcClient = require('eth-rpc-client') const createEventEmitterProxy = require('../lib/events-proxy.js') -const createObjectProxy = require('../lib/obj-proxy.js') const RPC_ADDRESS_LIST = require('../config.js').network const DEFAULT_RPC = RPC_ADDRESS_LIST['rinkeby'] @@ -18,8 +17,7 @@ module.exports = class NetworkController extends EventEmitter { this.networkStore = new ObservableStore('loading') this.providerStore = new ObservableStore(config.provider) this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore }) - this.providerProxy = createObjectProxy() - this.blockTrackerProxy = createEventEmitterProxy() + this._proxy = createEventEmitterProxy() this.on('networkDidChange', this.lookupNetwork) } @@ -27,11 +25,12 @@ module.exports = class NetworkController extends EventEmitter { initializeProvider (_providerParams) { this._baseProviderParams = _providerParams const rpcUrl = this.getCurrentRpcAddress() - this._configureStandardClient({ rpcUrl }) - this.blockTrackerProxy.on('block', this._logBlock.bind(this)) - this.blockTrackerProxy.on('error', this.verifyNetwork.bind(this)) - this.ethQuery = new EthQuery(this.providerProxy) + this._configureStandardProvider({ rpcUrl }) + this._proxy.on('block', this._logBlock.bind(this)) + this._proxy.on('error', this.verifyNetwork.bind(this)) + this.ethQuery = new EthQuery(this._proxy) this.lookupNetwork() + return this._proxy } verifyNetwork () { @@ -77,10 +76,8 @@ module.exports = class NetworkController extends EventEmitter { assert(type !== 'rpc', `NetworkController.setProviderType - cannot connect by type "rpc"`) // skip if type already matches if (type === this.getProviderConfig().type) return - // lookup rpcTarget for typecreateMetamaskProvider const rpcTarget = this.getRpcAddressForType(type) assert(rpcTarget, `NetworkController - unknown rpc address for type "${type}"`) - // update connectioncreateMetamaskProvider this.providerStore.updateState({ type, rpcTarget }) this._switchNetwork({ rpcUrl: rpcTarget }) } @@ -100,29 +97,32 @@ module.exports = class NetworkController extends EventEmitter { _switchNetwork (providerParams) { this.setNetworkState('loading') - this._configureStandardClient(providerParams) + this._configureStandardProvider(providerParams) this.emit('networkDidChange') } - _configureStandardClient(_providerParams) { + _configureStandardProvider(_providerParams) { const providerParams = extend(this._baseProviderParams, _providerParams) - const client = createEthRpcClient(providerParams) - this._setClient(client) - } - - _setClient (newClient) { - // teardown old client - const oldClient = this._currentClient - if (oldClient) { - oldClient.blockTracker.stop() - // asyncEventEmitter lacks a "removeAllListeners" method - // oldClient.blockTracker.removeAllListeners - oldClient.blockTracker._events = {} + const provider = createMetamaskProvider(providerParams) + this._setProvider(provider) + } + + _setProvider (provider) { + // collect old block tracker events + const oldProvider = this._provider + let blockTrackerHandlers + if (oldProvider) { + // capture old block handlers + blockTrackerHandlers = oldProvider._blockTracker.proxyEventHandlers + // tear down + oldProvider.removeAllListeners() + oldProvider.stop() } + // override block tracler + provider._blockTracker = createEventEmitterProxy(provider._blockTracker, blockTrackerHandlers) // set as new provider - this._currentClient = newClient - this.providerProxy.setTarget(newClient.provider) - this.blockTrackerProxy.setTarget(newClient.blockTracker) + this._provider = provider + this._proxy.setTarget(provider) } _logBlock (block) { diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index d46dee230..ef659a300 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -46,7 +46,6 @@ module.exports = class TransactionController extends EventEmitter { this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update')) this.nonceTracker = new NonceTracker({ provider: this.provider, - blockTracker: this.blockTracker, getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager), getConfirmedTransactions: (address) => { return this.txStateManager.getFilteredTxList({ diff --git a/app/scripts/lib/events-proxy.js b/app/scripts/lib/events-proxy.js index 840b06b1a..d1199a278 100644 --- a/app/scripts/lib/events-proxy.js +++ b/app/scripts/lib/events-proxy.js @@ -1,5 +1,6 @@ -module.exports = function createEventEmitterProxy(eventEmitter, eventHandlers = {}) { +module.exports = function createEventEmitterProxy(eventEmitter, listeners) { let target = eventEmitter + const eventHandlers = listeners || {} const proxy = new Proxy({}, { get: (obj, name) => { // intercept listeners @@ -13,12 +14,9 @@ module.exports = function createEventEmitterProxy(eventEmitter, eventHandlers = return true }, }) - proxy.setTarget(eventEmitter) - return proxy - function setTarget (eventEmitter) { target = eventEmitter - // migrate eventHandlers + // migrate listeners Object.keys(eventHandlers).forEach((name) => { eventHandlers[name].forEach((handler) => target.on(name, handler)) }) @@ -28,4 +26,6 @@ module.exports = function createEventEmitterProxy(eventEmitter, eventHandlers = eventHandlers[name].push(handler) target.on(name, handler) } + if (listeners) proxy.setTarget(eventEmitter) + return proxy } \ No newline at end of file diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js index 2af40a27f..0029ac953 100644 --- a/app/scripts/lib/nonce-tracker.js +++ b/app/scripts/lib/nonce-tracker.js @@ -4,9 +4,8 @@ const Mutex = require('await-semaphore').Mutex class NonceTracker { - constructor ({ provider, blockTracker, getPendingTransactions, getConfirmedTransactions }) { + constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) { this.provider = provider - this.blockTracker = blockTracker this.ethQuery = new EthQuery(provider) this.getPendingTransactions = getPendingTransactions this.getConfirmedTransactions = getConfirmedTransactions @@ -54,7 +53,7 @@ class NonceTracker { } async _getCurrentBlock () { - const blockTracker = this.blockTracker + const blockTracker = this._getBlockTracker() const currentBlock = blockTracker.getCurrentBlock() if (currentBlock) return currentBlock return await Promise((reject, resolve) => { @@ -140,6 +139,11 @@ class NonceTracker { return { name: 'local', nonce: highest, details: { startPoint, highest } } } + // this is a hotfix for the fact that the blockTracker will + // change when the network changes + _getBlockTracker () { + return this.provider._blockTracker + } } module.exports = NonceTracker diff --git a/app/scripts/lib/obj-proxy.js b/app/scripts/lib/obj-proxy.js deleted file mode 100644 index 29ca1269f..000000000 --- a/app/scripts/lib/obj-proxy.js +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = function createObjectProxy(obj) { - let target = obj - const proxy = new Proxy({}, { - get: (obj, name) => { - // intercept setTarget - if (name === 'setTarget') return setTarget - return target[name] - }, - set: (obj, name, value) => { - target[name] = value - return true - }, - }) - return proxy - - function setTarget (obj) { - target = obj - } -} \ No newline at end of file diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index a742f3cba..727f48f1c 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -81,24 +81,9 @@ module.exports = class MetamaskController extends EventEmitter { }) this.blacklistController.scheduleUpdates() - // rpc provider and block tracker - this.networkController.initializeProvider({ - scaffold: { - eth_syncing: false, - web3_clientVersion: `MetaMask/v${version}`, - }, - // account mgmt - getAccounts: nodeify(this.getAccounts, this), - // tx signing - processTransaction: nodeify(this.newTransaction, this), - // old style msg signing - processMessage: this.newUnsignedMessage.bind(this), - // personal_sign msg signing - processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), - processTypedMessage: this.newUnsignedTypedMessage.bind(this), - }) - this.provider = this.networkController.providerProxy - this.blockTracker = this.networkController.blockTrackerProxy + // rpc provider + this.provider = this.initializeProvider() + this.blockTracker = this.provider._blockTracker // eth data query tools this.ethQuery = new EthQuery(this.provider) @@ -233,6 +218,36 @@ module.exports = class MetamaskController extends EventEmitter { // Constructor helpers // + initializeProvider () { + const providerOpts = { + static: { + eth_syncing: false, + web3_clientVersion: `MetaMask/v${version}`, + }, + // account mgmt + getAccounts: (cb) => { + const isUnlocked = this.keyringController.memStore.getState().isUnlocked + const result = [] + const selectedAddress = this.preferencesController.getSelectedAddress() + + // only show address if account is unlocked + if (isUnlocked && selectedAddress) { + result.push(selectedAddress) + } + cb(null, result) + }, + // tx signing + processTransaction: nodeify(async (txParams) => await this.txController.newUnapprovedTransaction(txParams), this), + // old style msg signing + processMessage: this.newUnsignedMessage.bind(this), + // personal_sign msg signing + processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), + processTypedMessage: this.newUnsignedTypedMessage.bind(this), + } + const providerProxy = this.networkController.initializeProvider(providerOpts) + return providerProxy + } + initPublicConfigStore () { // get init state const publicConfigStore = new ObservableStore() @@ -468,18 +483,6 @@ module.exports = class MetamaskController extends EventEmitter { // Opinionated Keyring Management // - async getAccounts () { - const isUnlocked = this.keyringController.memStore.getState().isUnlocked - const result = [] - const selectedAddress = this.preferencesController.getSelectedAddress() - - // only show address if account is unlocked - if (isUnlocked && selectedAddress) { - result.push(selectedAddress) - } - return result - } - addNewAccount (cb) { const primaryKeyring = this.keyringController.getKeyringsByType('HD Key Tree')[0] if (!primaryKeyring) return cb(new Error('MetamaskController - No HD Key Tree found')) @@ -526,11 +529,6 @@ module.exports = class MetamaskController extends EventEmitter { // Identity Management // - // this function wrappper lets us pass the fn reference before txController is instantiated - async newTransaction (txParams) { - return await this.txController.newUnapprovedTransaction(txParams) - } - newUnsignedMessage (msgParams, cb) { const msgId = this.messageManager.addUnapprovedMessage(msgParams) this.sendUpdate() diff --git a/package.json b/package.json index d82766fb5..3843a8a41 100644 --- a/package.json +++ b/package.json @@ -71,11 +71,9 @@ "eth-contract-metadata": "^1.1.4", "eth-hd-keyring": "^1.2.1", "eth-json-rpc-filters": "^1.2.2", - "eth-json-rpc-middleware": "^1.4.3", "eth-keyring-controller": "^2.1.0", "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", - "eth-rpc-client": "^1.1.3", "eth-sig-util": "^1.4.0", "eth-simple-keyring": "^1.2.0", "eth-token-tracker": "^1.1.4", diff --git a/test/integration/lib/first-time.js b/test/integration/lib/first-time.js index ee49d0901..cedb14f6e 100644 --- a/test/integration/lib/first-time.js +++ b/test/integration/lib/first-time.js @@ -3,9 +3,6 @@ const PASSWORD = 'password123' QUnit.module('first time usage') QUnit.test('render init screen', (assert) => { - // intercept reload attempts - window.onbeforeunload = () => true - const done = assert.async() runFirstTimeUsageTest(assert).then(done).catch((err) => { assert.notOk(err, `Error was thrown: ${err.stack}`) diff --git a/test/unit/network-contoller-test.js b/test/unit/network-contoller-test.js index 42ca40c56..0b3b5adeb 100644 --- a/test/unit/network-contoller-test.js +++ b/test/unit/network-contoller-test.js @@ -14,15 +14,15 @@ describe('# Network Controller', function () { }, }) - networkController.initializeProvider(networkControllerProviderInit) + networkController.initializeProvider(networkControllerProviderInit, dummyProviderConstructor) }) describe('network', function () { describe('#provider', function () { it('provider should be updatable without reassignment', function () { - networkController.initializeProvider(networkControllerProviderInit) - const providerProxy = networkController.providerProxy - providerProxy.setTarget({ test: true }) - assert.ok(providerProxy.test) + networkController.initializeProvider(networkControllerProviderInit, dummyProviderConstructor) + const proxy = networkController._proxy + proxy.setTarget({ test: true, on: () => {} }) + assert.ok(proxy.test) }) }) describe('#getNetworkState', function () { @@ -66,4 +66,19 @@ describe('# Network Controller', function () { }) }) +function dummyProviderConstructor() { + return { + // provider + sendAsync: noop, + // block tracker + _blockTracker: {}, + start: noop, + stop: noop, + on: noop, + addListener: noop, + once: noop, + removeAllListeners: noop, + } +} + function noop() {} \ No newline at end of file diff --git a/test/unit/nonce-tracker-test.js b/test/unit/nonce-tracker-test.js index 77af2a21c..8970cf84d 100644 --- a/test/unit/nonce-tracker-test.js +++ b/test/unit/nonce-tracker-test.js @@ -190,13 +190,12 @@ function generateNonceTrackerWith (pending, confirmed, providerStub = '0x0') { providerResultStub.result = providerStub const provider = { sendAsync: (_, cb) => { cb(undefined, providerResultStub) }, - } - const blockTracker = { - getCurrentBlock: () => '0x11b568', + _blockTracker: { + getCurrentBlock: () => '0x11b568', + }, } return new NonceTracker({ provider, - blockTracker, getPendingTransactions, getConfirmedTransactions, }) diff --git a/ui/app/app.js b/ui/app/app.js index 30d3766ab..613577913 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -319,7 +319,7 @@ App.prototype.renderNetworkDropdown = function () { [ h('i.fa.fa-question-circle.fa-lg.menu-icon'), 'Localhost 8545', - providerType === 'localhost' ? h('.check', '✓') : null, + activeNetwork === 'http://localhost:8545' ? h('.check', '✓') : null, ] ), -- cgit v1.2.3 From d89394a7c9a5139ed5708ce7022fbbe2809e612a Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 18 Oct 2017 17:07:22 -0700 Subject: Make account tracking much more reactive --- app/scripts/lib/account-tracker.js | 11 ++++++++--- app/scripts/metamask-controller.js | 14 +++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index 13dea918f..b9959dc25 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -41,19 +41,24 @@ class AccountTracker extends EventEmitter { syncWithAddresses (addresses) { const accounts = this.store.getState().accounts const locals = Object.keys(accounts) - .map(account => accounts[account.address]) + const toAdd = [] addresses.forEach((upstream) => { if (!locals.includes(upstream)) { - this.addAccount(upstream) + toAdd.push(upstream) } }) + const toRemove = [] locals.forEach((local) => { if (!addresses.includes(local)) { - this.removeAccount(local) + toRemove.push(local) } }) + + toAdd.forEach(upstream => this.addAccount(upstream)) + toRemove.forEach(local=> this.removeAccount(local)) + this._updateAccounts() } addAccount (address) { diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index eae4478b5..11a26df64 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -117,8 +117,10 @@ module.exports = class MetamaskController extends EventEmitter { }) // If only one account exists, make sure it is selected. - this.keyringController.store.subscribe((state) => { - const addresses = Object.keys(state.walletNicknames || {}) + this.keyringController.memStore.subscribe((state) => { + const addresses = state.keyrings.reduce((res, keyring) => { + return res.concat(keyring.accounts) + }, []) if (addresses.length === 1) { const address = addresses[0] this.preferencesController.setSelectedAddress(address) @@ -314,7 +316,7 @@ module.exports = class MetamaskController extends EventEmitter { importAccountWithStrategy: this.importAccountWithStrategy.bind(this), // vault management - submitPassword: this.submitPassword.bind(this), + submitPassword: nodeify(keyringController.submitPassword, keyringController), // network management setProviderType: nodeify(networkController.setProviderType, networkController), @@ -470,12 +472,6 @@ module.exports = class MetamaskController extends EventEmitter { this.preferencesController.setSelectedAddress(address) } - submitPassword (password, cb) { - return this.keyringController.submitPassword(password) - .then((newState) => { cb(null, newState) }) - .catch((reason) => { cb(reason) }) - } - // // Opinionated Keyring Management // -- cgit v1.2.3 From 21bde66e16c3a41a1cb8fca5e9e9e3e97875d23b Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 18 Oct 2017 17:14:26 -0700 Subject: Remove account-tracker from keyringController --- app/scripts/metamask-controller.js | 1 - package.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index dd34cec97..366bb6d98 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -96,7 +96,6 @@ module.exports = class MetamaskController extends EventEmitter { // key mgmt this.keyringController = new KeyringController({ initState: initState.KeyringController, - accountTracker: this.accountTracker, getNetwork: this.networkController.getNetworkState.bind(this.networkController), encryptor: opts.encryptor || undefined, }) diff --git a/package.json b/package.json index 3843a8a41..2e8faadee 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "eth-contract-metadata": "^1.1.4", "eth-hd-keyring": "^1.2.1", "eth-json-rpc-filters": "^1.2.2", - "eth-keyring-controller": "^2.1.0", + "eth-keyring-controller": "^2.1.1", "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", "eth-sig-util": "^1.4.0", -- cgit v1.2.3 From 4dc494fdd10603d023962fe3a3ff844c77c46d4c Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Wed, 18 Oct 2017 17:14:42 -0700 Subject: Fix bug that breaks ui dev mode --- ui/app/reducers.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui/app/reducers.js b/ui/app/reducers.js index 6a2f44534..1cded7ca7 100644 --- a/ui/app/reducers.js +++ b/ui/app/reducers.js @@ -43,7 +43,12 @@ function rootReducer (state, action) { window.logState = function () { let state = window.METAMASK_CACHED_LOG_STATE - const version = global.platform.getVersion() + let version + try { + version = global.platform.getVersion() + } catch (e) { + version = 'unable to load version.' + } state.version = version let stateString = JSON.stringify(state, removeSeedWords, 2) return stateString -- cgit v1.2.3 From 209db9ae5fef7936e7bd5e1630afdf0e2422bbb8 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 18 Oct 2017 19:53:48 -0700 Subject: deps - bump eth-json-rpc-filters --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3843a8a41..91e64c10b 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "eth-block-tracker": "^2.2.0", "eth-contract-metadata": "^1.1.4", "eth-hd-keyring": "^1.2.1", - "eth-json-rpc-filters": "^1.2.2", + "eth-json-rpc-filters": "^1.2.3", "eth-keyring-controller": "^2.1.0", "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", -- cgit v1.2.3 From 4915aff7505db7a1f042525a366ca0e7c6da1eed Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 17 Oct 2017 11:28:53 -0230 Subject: Breaks send-v2 down into renderable functions. --- ui/app/send-v2.js | 261 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 159 insertions(+), 102 deletions(-) diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index f04b95800..9f91af0e1 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -28,6 +28,9 @@ function SendTransactionScreen () { memo: '', dropdownOpen: false, } + + this.handleToChange = this.handleToChange.bind(this) + this.handleAmountChange = this.handleAmountChange.bind(this) } SendTransactionScreen.prototype.componentWillMount = function () { @@ -97,149 +100,203 @@ SendTransactionScreen.prototype.renderCopy = function () { ]) } -SendTransactionScreen.prototype.render = function () { +SendTransactionScreen.prototype.renderHeader = function () { + return h('div', [ + h('div.send-v2__header', {}, [ + + this.renderHeaderIcon(), + + h('div.send-v2__arrow-background', [ + h('i.fa.fa-lg.fa-arrow-circle-right.send-v2__send-arrow-icon'), + ]), + + h('div.send-v2__header-tip'), + + ]), + + this.renderTitle(), + + this.renderCopy(), + ]) +} + +SendTransactionScreen.prototype.renderFromRow = function () { const { accounts, conversionRate, - tokenToUSDRate, - selectedToken, - showCustomizeGasModal, selectedAccount, setSelectedAddress, - primaryCurrency = 'ETH', - gasLimit, - gasPrice, } = this.props - const { - dropdownOpen, + const { dropdownOpen } = this.state + + return h('div.send-v2__form-row', [ + + h('div.send-v2__form-label', 'From:'), + + h(FromDropdown, { + dropdownOpen, + accounts, + selectedAccount, + onSelect: address => setSelectedAddress(address), + openDropdown: () => this.setState({ dropdownOpen: true }), + closeDropdown: () => this.setState({ dropdownOpen: false }), + conversionRate, + }), + + ]) +} + +SendTransactionScreen.prototype.handleToChange = function (event) { + const to = event.target.value + + this.setState({ + ...this.state, to, - amount, - // gasLimit, - // gasPrice, - memo, - } = this.state + }) +} - const amountConversionRate = selectedToken ? tokenToUSDRate : conversionRate +SendTransactionScreen.prototype.renderToRow = function () { + const { accounts } = this.props + const { to } = this.state - return ( + return h('div.send-v2__form-row', [ - h('div.send-v2__container', [ - h('div.send-v2__header', {}, [ + h('div.send-v2__form-label', 'To:'), - this.renderHeaderIcon(), + h(ToAutoComplete, { + to, + accounts, + onChange: this.handleToChange, + }), - h('div.send-v2__arrow-background', [ - h('i.fa.fa-lg.fa-arrow-circle-right.send-v2__send-arrow-icon'), - ]), + ]) +} - h('div.send-v2__header-tip'), +SendTransactionScreen.prototype.handleAmountChange = function (value) { + const amount = value - ]), + this.setState({ + ...this.state, + amount, + }) +} + +SendTransactionScreen.prototype.renderAmountRow = function () { + const { + conversionRate, + tokenToUSDRate, + selectedToken, + primaryCurrency = 'ETH', + } = this.props - this.renderTitle(), + const { amount } = this.state - this.renderCopy(), + const amountConversionRate = selectedToken ? tokenToUSDRate : conversionRate + + return h('div.send-v2__form-row', [ + + h('div.send-v2__form-label', 'Amount:'), + + h(CurrencyDisplay, { + primaryCurrency, + convertedCurrency: 'USD', + value: amount, + conversionRate: amountConversionRate, + convertedPrefix: '$', + handleChange: this.handleAmountChange + }), + + ]) +} + +SendTransactionScreen.prototype.renderGasRow = function () { + const { + conversionRate, + showCustomizeGasModal, + gasLimit, + gasPrice, + } = this.props - h('div.send-v2__form', {}, [ + return h('div.send-v2__form-row', [ - h('div.send-v2__form-row', [ + h('div.send-v2__form-label', 'Gas fee:'), - h('div.send-v2__form-label', 'From:'), + h(GasFeeDisplay, { + gasLimit, + gasPrice, + conversionRate, + onClick: showCustomizeGasModal, + }), - h(FromDropdown, { - dropdownOpen, - accounts, - selectedAccount, - onSelect: address => setSelectedAddress(address), - openDropdown: () => this.setState({ dropdownOpen: true }), - closeDropdown: () => this.setState({ dropdownOpen: false }), - conversionRate, - }), + h('div.send-v2__sliders-icon-container', { + onClick: showCustomizeGasModal, + }, [ + h('i.fa.fa-sliders.send-v2__sliders-icon'), + ]) - ]), + ]) +} - h('div.send-v2__form-row', [ +SendTransactionScreen.prototype.renderMemoRow = function () { + const { memo } = this.state - h('div.send-v2__form-label', 'To:'), + return h('div.send-v2__form-row', [ - h(ToAutoComplete, { - to, - accounts, - onChange: (event) => { - this.setState({ - ...this.state, - to: event.target.value, - }) - }, - }), + h('div.send-v2__form-label', 'Transaction Memo:'), - ]), + h(MemoTextArea, { + memo, + onChange: (event) => { + this.setState({ + ...this.state, + memo: event.target.value, + }) + }, + }), - h('div.send-v2__form-row', [ + ]) +} - h('div.send-v2__form-label', 'Amount:'), +SendTransactionScreen.prototype.renderForm = function () { + return h('div.send-v2__form', {}, [ - h(CurrencyDisplay, { - primaryCurrency, - convertedCurrency: 'USD', - value: amount, - conversionRate: amountConversionRate, - convertedPrefix: '$', - handleChange: (value) => { - this.setState({ - ...this.state, - amount: value, - }) - } - }), + this.renderFromRow(), - ]), + this.renderToRow(), - h('div.send-v2__form-row', [ + this.renderAmountRow(), - h('div.send-v2__form-label', 'Gas fee:'), + this.renderGasRow(), - h(GasFeeDisplay, { - gasLimit, - gasPrice, - conversionRate, - onClick: showCustomizeGasModal, - }), + this.renderMemoRow(), - h('div.send-v2__sliders-icon-container', { - onClick: showCustomizeGasModal, - }, [ - h('i.fa.fa-sliders.send-v2__sliders-icon'), - ]) + ]) +} - ]), +SendTransactionScreen.prototype.renderFooter = function () { + const { goHome } = this.props - h('div.send-v2__form-row', [ + return h('div.send-v2__footer', [ + h('button.send-v2__cancel-btn', { + onClick: goHome, + }, 'Cancel'), + h('button.send-v2__next-btn', { + onClick: event => this.onSubmit(event), + }, 'Next'), + ]) +} - h('div.send-v2__form-label', 'Transaction Memo:'), +SendTransactionScreen.prototype.render = function () { + return ( - h(MemoTextArea, { - memo, - onChange: (event) => { - this.setState({ - ...this.state, - memo: event.target.value, - }) - }, - }), + h('div.send-v2__container', [ - ]), + this.renderHeader(), - ]), + this.renderForm(), - // Buttons underneath card - h('div.send-v2__footer', [ - h('button.send-v2__cancel-btn', {}, 'Cancel'), - h('button.send-v2__next-btn', { - onClick: event => this.onSubmit(event), - }, 'Next'), - ]), + this.renderFooter(), ]) ) -- cgit v1.2.3 From 4f9ac1c4fe67ec4c196ce1891ecc1743552d45ce Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 17 Oct 2017 13:16:36 -0230 Subject: Get from and update addressBook in send-v2 --- ui/app/actions.js | 2 +- ui/app/components/send/send-v2-container.js | 12 ++++++++---- ui/app/selectors.js | 5 +++++ ui/app/send-v2.js | 19 +++++++++++++++---- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index 7ac0acf05..2fbd578c5 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -934,7 +934,7 @@ function setRpcTarget (newRpc) { } // Calls the addressBookController to add a new address. -function addToAddressBook (recipient, nickname) { +function addToAddressBook (recipient, nickname = '') { log.debug(`background.addToAddressBook`) return (dispatch) => { background.setAddressBook(recipient, nickname, (err, result) => { diff --git a/ui/app/components/send/send-v2-container.js b/ui/app/components/send/send-v2-container.js index 5935a8fee..8ac5cc961 100644 --- a/ui/app/components/send/send-v2-container.js +++ b/ui/app/components/send/send-v2-container.js @@ -14,13 +14,15 @@ const { getSelectedAddress, getGasPrice, getGasLimit, + getAddressBook, } = require('../../selectors') module.exports = connect(mapStateToProps, mapDispatchToProps)(SendEther) function mapStateToProps (state) { - const selectedAddress = getSelectedAddress(state); - const selectedToken = getSelectedToken(state); + const fromAccounts = accountsWithSendEtherInfoSelector(state) + const selectedAddress = getSelectedAddress(state) + const selectedToken = getSelectedToken(state) const tokenExchangeRates = state.metamask.tokenExchangeRates const selectedTokenExchangeRate = getSelectedTokenExchangeRate(state) const conversionRate = conversionRateSelector(state) @@ -45,7 +47,8 @@ function mapStateToProps (state) { return { selectedAccount: getCurrentAccountWithSendEtherInfo(state), - accounts: accountsWithSendEtherInfoSelector(state), + fromAccounts, + toAccounts: [...fromAccounts, ...getAddressBook(state)], conversionRate, selectedToken, primaryCurrency, @@ -66,6 +69,7 @@ function mapDispatchToProps (dispatch) { dispatch(actions.signTokenTx(tokenAddress, toAddress, amount, txData)) ), signTx: txParams => dispatch(actions.signTx(txParams)), - setSelectedAddress: address => dispatch(actions.setSelectedAddress(address)) + setSelectedAddress: address => dispatch(actions.setSelectedAddress(address)), + addToAddressBook: address => dispatch(actions.addToAddressBook(address)), } } diff --git a/ui/app/selectors.js b/ui/app/selectors.js index bf3d3399e..fffe7dd61 100644 --- a/ui/app/selectors.js +++ b/ui/app/selectors.js @@ -12,6 +12,7 @@ const selectors = { getCurrentAccountWithSendEtherInfo, getGasPrice, getGasLimit, + getAddressBook, } module.exports = selectors @@ -59,6 +60,10 @@ function conversionRateSelector (state) { return state.metamask.conversionRate } +function getAddressBook (state) { + return state.metamask.addressBook +} + function accountsWithSendEtherInfoSelector (state) { const { accounts, diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index 9f91af0e1..c41ba9758 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -122,7 +122,7 @@ SendTransactionScreen.prototype.renderHeader = function () { SendTransactionScreen.prototype.renderFromRow = function () { const { - accounts, + fromAccounts, conversionRate, selectedAccount, setSelectedAddress, @@ -136,7 +136,7 @@ SendTransactionScreen.prototype.renderFromRow = function () { h(FromDropdown, { dropdownOpen, - accounts, + accounts: fromAccounts, selectedAccount, onSelect: address => setSelectedAddress(address), openDropdown: () => this.setState({ dropdownOpen: true }), @@ -157,7 +157,7 @@ SendTransactionScreen.prototype.handleToChange = function (event) { } SendTransactionScreen.prototype.renderToRow = function () { - const { accounts } = this.props + const { toAccounts } = this.props const { to } = this.state return h('div.send-v2__form-row', [ @@ -166,7 +166,7 @@ SendTransactionScreen.prototype.renderToRow = function () { h(ToAutoComplete, { to, - accounts, + accounts: toAccounts, onChange: this.handleToChange, }), @@ -302,6 +302,14 @@ SendTransactionScreen.prototype.render = function () { ) } +SendTransactionScreen.prototype.addToAddressBookIfNew = function (newAddress) { + const { toAccounts, addToAddressBook } = this.props + if (!toAccounts.find(({ address }) => newAddress === address)) { + // TODO: nickname, i.e. addToAddressBook(recipient, nickname) + addToAddressBook(newAddress) + } +} + SendTransactionScreen.prototype.onSubmit = function (event) { event.preventDefault() const { @@ -315,8 +323,11 @@ SendTransactionScreen.prototype.onSubmit = function (event) { signTx, selectedToken, selectedAccount: { address: from }, + toAccounts, } = this.props + this.addToAddressBookIfNew(to) + const txParams = { from, value: '0', -- cgit v1.2.3 From f81226fbe9f98d5a6c408e289fa0ea61a467e7dc Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 17 Oct 2017 14:52:23 -0230 Subject: Move all of send state to metamask state. --- ui/app/actions.js | 46 ++++++++++++++++ ui/app/components/customize-gas-modal/index.js | 11 +++- ui/app/components/send/from-dropdown.js | 2 +- ui/app/components/send/gas-fee-display-v2.js | 17 ++---- ui/app/components/send/send-v2-container.js | 13 +++-- ui/app/reducers/metamask.js | 46 ++++++++++++++++ ui/app/selectors.js | 5 ++ ui/app/send-v2.js | 76 ++++++++++++-------------- 8 files changed, 155 insertions(+), 61 deletions(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index 2fbd578c5..ed1ff75e5 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -135,8 +135,18 @@ var actions = { getGasPrice, UPDATE_GAS_LIMIT: 'UPDATE_GAS_LIMIT', UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE', + UPDATE_GAS_TOTAL: 'UPDATE_GAS_TOTAL', + UPDATE_SEND_FROM: 'UPDATE_SEND_FROM', + UPDATE_SEND_TO: 'UPDATE_SEND_TO', + UPDATE_SEND_AMOUNT: 'UPDATE_SEND_AMOUNT', + UPDATE_SEND_MEMO: 'UPDATE_SEND_MEMO', updateGasLimit, updateGasPrice, + updateGasTotal, + updateSendFrom, + updateSendTo, + updateSendAmount, + updateSendMemo, setSelectedAddress, // app messages confirmSeedWords: confirmSeedWords, @@ -508,6 +518,42 @@ function updateGasPrice (gasPrice) { } } +function updateGasTotal (gasTotal) { + return { + type: actions.UPDATE_GAS_TOTAL, + value: gasTotal, + } +} + +function updateSendFrom (from) { + return { + type: actions.UPDATE_SEND_FROM, + value: from, + } +} + +function updateSendTo (to) { + return { + type: actions.UPDATE_SEND_TO, + value: to, + } +} + +function updateSendAmount (amount) { + return { + type: actions.UPDATE_SEND_AMOUNT, + value: amount, + } +} + +function updateSendMemo (memo) { + return { + type: actions.UPDATE_SEND_MEMO, + value: memo, + } +} + + function sendTx (txData) { log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`) return (dispatch) => { diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/customize-gas-modal/index.js index 2df24b4e1..0ba768893 100644 --- a/ui/app/components/customize-gas-modal/index.js +++ b/ui/app/components/customize-gas-modal/index.js @@ -5,7 +5,7 @@ const connect = require('react-redux').connect const actions = require('../../actions') const GasModalCard = require('./gas-modal-card') -const { conversionUtil } = require('../../conversion-util') +const { conversionUtil, multiplyCurrencies } = require('../../conversion-util') const { getGasPrice, @@ -26,6 +26,7 @@ function mapDispatchToProps (dispatch) { hideModal: () => dispatch(actions.hideModal()), updateGasPrice: newGasPrice => dispatch(actions.updateGasPrice(newGasPrice)), updateGasLimit: newGasLimit => dispatch(actions.updateGasLimit(newGasLimit)), + updateGasTotal: newGasTotal => dispatch(actions.updateGasTotal(newGasTotal)), } } @@ -46,10 +47,18 @@ CustomizeGasModal.prototype.save = function (gasPrice, gasLimit) { updateGasPrice, updateGasLimit, hideModal, + updateGasTotal } = this.props + const newGasTotal = multiplyCurrencies(gasLimit, gasPrice, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 16, + }) + updateGasPrice(gasPrice) updateGasLimit(gasLimit) + updateGasTotal(newGasTotal) hideModal() } diff --git a/ui/app/components/send/from-dropdown.js b/ui/app/components/send/from-dropdown.js index fd6fb7e64..6f2b9da68 100644 --- a/ui/app/components/send/from-dropdown.js +++ b/ui/app/components/send/from-dropdown.js @@ -38,7 +38,7 @@ FromDropdown.prototype.renderDropdown = function () { ...accounts.map(account => h(AccountListItem, { account, handleClick: () => { - onSelect(account.address) + onSelect(account) closeDropdown() }, icon: this.getListItemIcon(account, selectedAccount), diff --git a/ui/app/components/send/gas-fee-display-v2.js b/ui/app/components/send/gas-fee-display-v2.js index 961d55610..7c3913c7f 100644 --- a/ui/app/components/send/gas-fee-display-v2.js +++ b/ui/app/components/send/gas-fee-display-v2.js @@ -1,9 +1,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits -const CurrencyDisplay = require('./currency-display'); - -const { multiplyCurrencies } = require('../../conversion-util') +const CurrencyDisplay = require('./currency-display') module.exports = GasFeeDisplay @@ -15,24 +13,17 @@ function GasFeeDisplay () { GasFeeDisplay.prototype.render = function () { const { conversionRate, - gasLimit, - gasPrice, + gasTotal, onClick, } = this.props - const readyToRender = Boolean(gasLimit && gasPrice) - return h('div', [ - readyToRender + gasTotal ? h(CurrencyDisplay, { primaryCurrency: 'ETH', convertedCurrency: 'USD', - value: multiplyCurrencies(gasLimit, gasPrice, { - toNumericBase: 'hex', - multiplicandBase: 16, - multiplierBase: 16, - }), + value: gasTotal, conversionRate, convertedPrefix: '$', readOnly: true, diff --git a/ui/app/components/send/send-v2-container.js b/ui/app/components/send/send-v2-container.js index 8ac5cc961..dcf764048 100644 --- a/ui/app/components/send/send-v2-container.js +++ b/ui/app/components/send/send-v2-container.js @@ -15,6 +15,7 @@ const { getGasPrice, getGasLimit, getAddressBook, + getSendFrom, } = require('../../selectors') module.exports = connect(mapStateToProps, mapDispatchToProps)(SendEther) @@ -46,16 +47,15 @@ function mapStateToProps (state) { } return { - selectedAccount: getCurrentAccountWithSendEtherInfo(state), + ...state.metamask.send, + from: getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state), fromAccounts, toAccounts: [...fromAccounts, ...getAddressBook(state)], conversionRate, selectedToken, primaryCurrency, data, - tokenToUSDRate, - gasPrice: getGasPrice(state), - gasLimit: getGasLimit(state), + amountConversionRate: selectedToken ? tokenToUSDRate : conversionRate, } } @@ -71,5 +71,10 @@ function mapDispatchToProps (dispatch) { signTx: txParams => dispatch(actions.signTx(txParams)), setSelectedAddress: address => dispatch(actions.setSelectedAddress(address)), addToAddressBook: address => dispatch(actions.addToAddressBook(address)), + updateGasTotal: newTotal => dispatch(actions.updateGasTotal(newTotal)), + updateSendFrom: newFrom => dispatch(actions.updateSendFrom(newFrom)), + updateSendTo: newTo => dispatch(actions.updateSendTo(newTo)), + updateSendAmount: newAmount => dispatch(actions.updateSendAmount(newAmount)), + updateSendMemo: newMemo => dispatch(actions.updateSendMemo(newMemo)), } } diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index a9a54e91e..6915dbb0f 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -24,6 +24,11 @@ function reduceMetamask (state, action) { send: { gasLimit: null, gasPrice: null, + gasTotal: null, + from: '', + to: '', + amount: '0x0', + memo: '', }, }, state.metamask) @@ -157,6 +162,7 @@ function reduceMetamask (state, action) { tokens: action.newTokens, }) + // metamask.send case actions.UPDATE_GAS_LIMIT: return extend(metamaskState, { send: { @@ -178,6 +184,46 @@ function reduceMetamask (state, action) { isAccountMenuOpen: !metamaskState.isAccountMenuOpen, }) + case actions.UPDATE_GAS_TOTAL: + return extend(metamaskState, { + send: { + ...metamaskState.send, + gasTotal: action.value, + }, + }) + + case actions.UPDATE_SEND_FROM: + return extend(metamaskState, { + send: { + ...metamaskState.send, + from: action.value, + }, + }) + + case actions.UPDATE_SEND_TO: + return extend(metamaskState, { + send: { + ...metamaskState.send, + to: action.value, + }, + }) + + case actions.UPDATE_SEND_AMOUNT: + return extend(metamaskState, { + send: { + ...metamaskState.send, + amount: action.value, + }, + }) + + case actions.UPDATE_SEND_MEMO: + return extend(metamaskState, { + send: { + ...metamaskState.send, + memo: action.value, + }, + }) + default: return metamaskState diff --git a/ui/app/selectors.js b/ui/app/selectors.js index fffe7dd61..9d4e6eb67 100644 --- a/ui/app/selectors.js +++ b/ui/app/selectors.js @@ -13,6 +13,7 @@ const selectors = { getGasPrice, getGasLimit, getAddressBook, + getSendFrom, } module.exports = selectors @@ -107,3 +108,7 @@ function getGasPrice (state) { function getGasLimit (state) { return state.metamask.send.gasLimit } + +function getSendFrom (state) { + return state.metamask.send.from +} diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index c41ba9758..e3182689d 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -12,6 +12,8 @@ const GasFeeDisplay = require('./components/send/gas-fee-display-v2') const { showModal } = require('./actions') +const { multiplyCurrencies } = require('./conversion-util') + module.exports = SendTransactionScreen inherits(SendTransactionScreen, PersistentForm) @@ -19,13 +21,6 @@ function SendTransactionScreen () { PersistentForm.call(this) this.state = { - from: '', - to: '', - gasPrice: null, - gasLimit: null, - amount: '0x0', - txData: null, - memo: '', dropdownOpen: false, } @@ -41,6 +36,7 @@ SendTransactionScreen.prototype.componentWillMount = function () { estimateGas, selectedAddress, data, + updateGasTotal, } = this.props const { symbol } = selectedToken || {} @@ -58,13 +54,23 @@ SendTransactionScreen.prototype.componentWillMount = function () { Object.assign(estimateGasParams, { data }) } - Promise.all([ - getGasPrice(), - estimateGas({ - from: selectedAddress, - gas: '746a528800', - }), - ]) + Promise + .all([ + getGasPrice(), + estimateGas({ + from: selectedAddress, + gas: '746a528800', + }), + ]) + .then(([gasPrice, gas]) => { + + const newGasTotal = multiplyCurrencies(gas, gasPrice, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 16, + }) + updateGasTotal(newGasTotal) + }) } SendTransactionScreen.prototype.renderHeaderIcon = function () { @@ -122,10 +128,11 @@ SendTransactionScreen.prototype.renderHeader = function () { SendTransactionScreen.prototype.renderFromRow = function () { const { + from, fromAccounts, conversionRate, - selectedAccount, setSelectedAddress, + updateSendFrom, } = this.props const { dropdownOpen } = this.state @@ -137,8 +144,8 @@ SendTransactionScreen.prototype.renderFromRow = function () { h(FromDropdown, { dropdownOpen, accounts: fromAccounts, - selectedAccount, - onSelect: address => setSelectedAddress(address), + selectedAccount: from, + onSelect: updateSendFrom, openDropdown: () => this.setState({ dropdownOpen: true }), closeDropdown: () => this.setState({ dropdownOpen: false }), conversionRate, @@ -148,12 +155,10 @@ SendTransactionScreen.prototype.renderFromRow = function () { } SendTransactionScreen.prototype.handleToChange = function (event) { + const { updateSendTo } = this.props const to = event.target.value - this.setState({ - ...this.state, - to, - }) + updateSendTo(to) } SendTransactionScreen.prototype.renderToRow = function () { @@ -174,26 +179,21 @@ SendTransactionScreen.prototype.renderToRow = function () { } SendTransactionScreen.prototype.handleAmountChange = function (value) { + const { updateSendAmount } = this.props const amount = value - this.setState({ - ...this.state, - amount, - }) + updateSendAmount(amount) } SendTransactionScreen.prototype.renderAmountRow = function () { const { - conversionRate, - tokenToUSDRate, selectedToken, primaryCurrency = 'ETH', + amountConversionRate, } = this.props const { amount } = this.state - const amountConversionRate = selectedToken ? tokenToUSDRate : conversionRate - return h('div.send-v2__form-row', [ h('div.send-v2__form-label', 'Amount:'), @@ -214,8 +214,7 @@ SendTransactionScreen.prototype.renderGasRow = function () { const { conversionRate, showCustomizeGasModal, - gasLimit, - gasPrice, + gasTotal, } = this.props return h('div.send-v2__form-row', [ @@ -223,8 +222,7 @@ SendTransactionScreen.prototype.renderGasRow = function () { h('div.send-v2__form-label', 'Gas fee:'), h(GasFeeDisplay, { - gasLimit, - gasPrice, + gasTotal, conversionRate, onClick: showCustomizeGasModal, }), @@ -239,6 +237,7 @@ SendTransactionScreen.prototype.renderGasRow = function () { } SendTransactionScreen.prototype.renderMemoRow = function () { + const { updateSendMemo } = this.props const { memo } = this.state return h('div.send-v2__form-row', [ @@ -247,12 +246,7 @@ SendTransactionScreen.prototype.renderMemoRow = function () { h(MemoTextArea, { memo, - onChange: (event) => { - this.setState({ - ...this.state, - memo: event.target.value, - }) - }, + onChange: (event) => updateSendMemo(event.target.value), }), ]) @@ -313,16 +307,14 @@ SendTransactionScreen.prototype.addToAddressBookIfNew = function (newAddress) { SendTransactionScreen.prototype.onSubmit = function (event) { event.preventDefault() const { + from: {address: from}, to, amount, - } = this.state - const { gasLimit: gas, gasPrice, signTokenTx, signTx, selectedToken, - selectedAccount: { address: from }, toAccounts, } = this.props -- cgit v1.2.3 From 60eda592b5979ac1fdbfb6d5b3418a4924abc14d Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 17 Oct 2017 17:43:20 -0230 Subject: Handling to and amount errors. --- ui/app/actions.js | 10 +++ ui/app/components/send/currency-display.js | 23 ++++--- ui/app/components/send/send-v2-container.js | 1 + ui/app/components/send/to-autocomplete.js | 10 +-- ui/app/conversion-util.js | 11 ++-- ui/app/css/itcss/components/send.scss | 11 ++++ ui/app/reducers/metamask.js | 16 +++++ ui/app/send-v2.js | 97 ++++++++++++++++++++++++++--- 8 files changed, 151 insertions(+), 28 deletions(-) diff --git a/ui/app/actions.js b/ui/app/actions.js index ed1ff75e5..25a4abda6 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -140,6 +140,7 @@ var actions = { UPDATE_SEND_TO: 'UPDATE_SEND_TO', UPDATE_SEND_AMOUNT: 'UPDATE_SEND_AMOUNT', UPDATE_SEND_MEMO: 'UPDATE_SEND_MEMO', + UPDATE_SEND_ERRORS: 'UPDATE_SEND_ERRORS', updateGasLimit, updateGasPrice, updateGasTotal, @@ -147,6 +148,7 @@ var actions = { updateSendTo, updateSendAmount, updateSendMemo, + updateSendErrors, setSelectedAddress, // app messages confirmSeedWords: confirmSeedWords, @@ -553,6 +555,14 @@ function updateSendMemo (memo) { } } +function updateSendErrors (error) { + console.log(`updateSendErrors error`, error); + return { + type: actions.UPDATE_SEND_ERRORS, + value: error, + } +} + function sendTx (txData) { log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`) diff --git a/ui/app/components/send/currency-display.js b/ui/app/components/send/currency-display.js index d56c119f1..f7fbb2379 100644 --- a/ui/app/components/send/currency-display.js +++ b/ui/app/components/send/currency-display.js @@ -28,16 +28,12 @@ function resetCaretIfPastEnd (value, event) { } } -CurrencyDisplay.prototype.handleChangeInHexWei = function (value) { - const { handleChange } = this.props - - const valueInHexWei = conversionUtil(value, { +function toHexWei (value) { + return conversionUtil(value, { fromNumericBase: 'dec', toNumericBase: 'hex', toDenomination: 'WEI', }) - - handleChange(valueInHexWei) } CurrencyDisplay.prototype.render = function () { @@ -51,7 +47,10 @@ CurrencyDisplay.prototype.render = function () { convertedPrefix = '', placeholder = '0', readOnly = false, + inError = false, value: initValue, + handleChange, + validate, } = this.props const { value } = this.state @@ -73,6 +72,9 @@ CurrencyDisplay.prototype.render = function () { return h('div', { className, + style: { + borderColor: inError ? 'red' : null, + }, }, [ h('div.currency-display__primary-row', [ @@ -100,8 +102,13 @@ CurrencyDisplay.prototype.render = function () { this.setState({ value: newValue }) } }, - onBlur: event => !readOnly && this.handleChangeInHexWei(event.target.value.split(' ')[0]), - onKeyUp: event => !readOnly && resetCaretIfPastEnd(value || initValueToRender, event), + onBlur: event => !readOnly && handleChange(toHexWei(event.target.value.split(' ')[0])), + onKeyUp: event => { + if (!readOnly) { + validate(toHexWei(value || initValueToRender)) + resetCaretIfPastEnd(value || initValueToRender, event) + } + }, onClick: event => !readOnly && resetCaretIfPastEnd(value || initValueToRender, event), }), diff --git a/ui/app/components/send/send-v2-container.js b/ui/app/components/send/send-v2-container.js index dcf764048..f20d80073 100644 --- a/ui/app/components/send/send-v2-container.js +++ b/ui/app/components/send/send-v2-container.js @@ -76,5 +76,6 @@ function mapDispatchToProps (dispatch) { updateSendTo: newTo => dispatch(actions.updateSendTo(newTo)), updateSendAmount: newAmount => dispatch(actions.updateSendAmount(newAmount)), updateSendMemo: newMemo => dispatch(actions.updateSendMemo(newMemo)), + updateSendErrors: newError => dispatch(actions.updateSendErrors(newError)), } } diff --git a/ui/app/components/send/to-autocomplete.js b/ui/app/components/send/to-autocomplete.js index 1bf1e1907..686a7a23e 100644 --- a/ui/app/components/send/to-autocomplete.js +++ b/ui/app/components/send/to-autocomplete.js @@ -11,7 +11,7 @@ function ToAutoComplete () { } ToAutoComplete.prototype.render = function () { - const { to, accounts, onChange } = this.props + const { to, accounts, onChange, inError } = this.props return h('div.send-v2__to-autocomplete', [ @@ -19,15 +19,15 @@ ToAutoComplete.prototype.render = function () { name: 'address', list: 'addresses', placeholder: 'Recipient Address', + className: inError ? `send-v2__error-border` : '', value: to, onChange, - // onBlur: () => { - // this.setErrorsFor('to') - // }, onFocus: event => { - // this.clearErrorsFor('to') to && event.target.select() }, + style: { + borderColor: inError ? 'red' : null, + } }), h('datalist#addresses', [ diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js index 3a9e9ad0f..1ef276a39 100644 --- a/ui/app/conversion-util.js +++ b/ui/app/conversion-util.js @@ -157,14 +157,11 @@ const multiplyCurrencies = (a, b, options = {}) => { } const conversionGreaterThan = ( - { value, fromNumericBase }, - { value: compareToValue, fromNumericBase: compareToBase }, + { ...firstProps }, + { ...secondProps }, ) => { - const firstValue = converter({ value, fromNumericBase }) - const secondValue = converter({ - value: compareToValue, - fromNumericBase: compareToBase, - }) + const firstValue = converter({ ...firstProps }) + const secondValue = converter({ ...secondProps }) return firstValue.gt(secondValue) } diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index ddabdee2e..7e72e8399 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -497,6 +497,17 @@ width: 287px; } + &__error { + font-size: 12px; + line-height: 12px; + left: 8px; + color: $red; + } + + &__error-border { + color: $red; + } + &__form { display: flex; flex-direction: column; diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index 6915dbb0f..fb2b2e674 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -29,6 +29,7 @@ function reduceMetamask (state, action) { to: '', amount: '0x0', memo: '', + errors: {}, }, }, state.metamask) @@ -224,6 +225,21 @@ function reduceMetamask (state, action) { }, }) + case actions.UPDATE_SEND_ERRORS: + console.log(123, { + ...metamaskState.send.errors, + ...action.value, + }) + return extend(metamaskState, { + send: { + ...metamaskState.send, + errors: { + ...metamaskState.send.errors, + ...action.value, + } + }, + }) + default: return metamaskState diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index e3182689d..8d368044a 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -12,7 +12,8 @@ const GasFeeDisplay = require('./components/send/gas-fee-display-v2') const { showModal } = require('./actions') -const { multiplyCurrencies } = require('./conversion-util') +const { multiplyCurrencies, conversionGreaterThan } = require('./conversion-util') +const { isValidAddress } = require('./util') module.exports = SendTransactionScreen @@ -22,10 +23,15 @@ function SendTransactionScreen () { this.state = { dropdownOpen: false, + errors: { + to: null, + amount: null, + }, } this.handleToChange = this.handleToChange.bind(this) this.handleAmountChange = this.handleAmountChange.bind(this) + this.validateAmount = this.validateAmount.bind(this) } SendTransactionScreen.prototype.componentWillMount = function () { @@ -126,6 +132,16 @@ SendTransactionScreen.prototype.renderHeader = function () { ]) } +SendTransactionScreen.prototype.renderErrorMessage = function(errorType) { + const { errors } = this.props + console.log(`! errors`, errors); + const errorMessage = errors[errorType]; + console.log(`errorMessage`, errorMessage); + return errorMessage + ? h('div.send-v2__error', [ errorMessage ] ) + : null +} + SendTransactionScreen.prototype.renderFromRow = function () { const { from, @@ -155,57 +171,122 @@ SendTransactionScreen.prototype.renderFromRow = function () { } SendTransactionScreen.prototype.handleToChange = function (event) { - const { updateSendTo } = this.props + const { updateSendTo, updateSendErrors } = this.props const to = event.target.value + let toError = null + + if (!to) { + toError = 'Required' + } else if (!isValidAddress(to)) { + toError = 'Recipient address is invalid.' + } updateSendTo(to) + updateSendErrors({ to: toError }) } SendTransactionScreen.prototype.renderToRow = function () { - const { toAccounts } = this.props + const { toAccounts, errors } = this.props const { to } = this.state return h('div.send-v2__form-row', [ - h('div.send-v2__form-label', 'To:'), + h('div.send-v2__form-label', [ + + 'To:', + + this.renderErrorMessage('to'), + + ]), h(ToAutoComplete, { to, accounts: toAccounts, onChange: this.handleToChange, + inError: Boolean(errors.to), }), ]) } SendTransactionScreen.prototype.handleAmountChange = function (value) { - const { updateSendAmount } = this.props const amount = value + const { updateSendAmount } = this.props updateSendAmount(amount) } +SendTransactionScreen.prototype.validateAmount = function (value) { + const { + from: { balance }, + updateSendErrors, + amountConversionRate, + conversionRate, + primaryCurrency, + toCurrency, + selectedToken + } = this.props + const amount = value + + let amountError = null + + const sufficientBalance = conversionGreaterThan( + { + value: balance, + fromNumericBase: 'hex', + fromCurrency: primaryCurrency, + conversionRate, + }, + { + value: amount, + fromNumericBase: 'hex', + conversionRate: amountConversionRate, + fromCurrency: selectedToken || primaryCurrency, + conversionRate: amountConversionRate, + }, + ) + console.log(`sufficientBalance`, sufficientBalance); + const amountLessThanZero = conversionGreaterThan( + { value: 0, fromNumericBase: 'dec' }, + { value: amount, fromNumericBase: 'hex' }, + ) + + if (!sufficientBalance) { + amountError = 'Insufficient funds.' + } else if (amountLessThanZero) { + amountError = 'Can not send negative amounts of ETH.' + } + + updateSendErrors({ amount: amountError }) +} + SendTransactionScreen.prototype.renderAmountRow = function () { const { selectedToken, primaryCurrency = 'ETH', amountConversionRate, + errors, } = this.props const { amount } = this.state return h('div.send-v2__form-row', [ - h('div.send-v2__form-label', 'Amount:'), + h('div.send-v2__form-label', [ + 'Amount:', + this.renderErrorMessage('amount'), + ]), h(CurrencyDisplay, { + inError: Boolean(errors.amount), primaryCurrency, convertedCurrency: 'USD', value: amount, conversionRate: amountConversionRate, convertedPrefix: '$', - handleChange: this.handleAmountChange - }), + handleChange: this.handleAmountChange, + validate: this.validateAmount, + }), ]) } -- cgit v1.2.3 From 6e73eacd5fffa70045e0a0c920795e70cc0337aa Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 18 Oct 2017 23:53:57 -0230 Subject: Turn off feature toggle. --- ui/app/app.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index 3f27b36c7..76cf1bae1 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -351,20 +351,22 @@ App.prototype.renderPrimary = function () { case 'sendTransaction': log.debug('rendering send tx screen') - const SendComponentToRender = checkFeatureToggle('send-v2') - ? SendTransactionScreen2 - : SendTransactionScreen + // Going to leave this here until we are ready to delete SendTransactionScreen v1 + // const SendComponentToRender = checkFeatureToggle('send-v2') + // ? SendTransactionScreen2 + // : SendTransactionScreen - return h(SendComponentToRender, {key: 'send-transaction'}) + return h(SendTransactionScreen2, {key: 'send-transaction'}) case 'sendToken': log.debug('rendering send token screen') - const SendTokenComponentToRender = checkFeatureToggle('send-v2') - ? SendTransactionScreen2 - : SendTokenScreen + // Going to leave this here until we are ready to delete SendTransactionScreen v1 + // const SendTokenComponentToRender = checkFeatureToggle('send-v2') + // ? SendTransactionScreen2 + // : SendTokenScreen - return h(SendTokenComponentToRender, {key: 'sendToken'}) + return h(SendTransactionScreen2, {key: 'sendToken'}) case 'newKeychain': log.debug('rendering new keychain screen') -- cgit v1.2.3 From de1da7d1b215ade650fc644adf63384a401dc240 Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 18 Oct 2017 23:58:01 -0230 Subject: Fix cancel button on send screen. --- ui/app/components/send/send-v2-container.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/app/components/send/send-v2-container.js b/ui/app/components/send/send-v2-container.js index f20d80073..ebe2b878b 100644 --- a/ui/app/components/send/send-v2-container.js +++ b/ui/app/components/send/send-v2-container.js @@ -77,5 +77,6 @@ function mapDispatchToProps (dispatch) { updateSendAmount: newAmount => dispatch(actions.updateSendAmount(newAmount)), updateSendMemo: newMemo => dispatch(actions.updateSendMemo(newMemo)), updateSendErrors: newError => dispatch(actions.updateSendErrors(newError)), + goHome: () => dispatch(actions.goHome()), } } -- cgit v1.2.3 From a8bba2f4b7ca714d46b2e1ca405aec3135aa23b2 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Wed, 18 Oct 2017 23:01:40 -0700 Subject: Merge master to NewUI-flat --- ui/lib/account-link.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 ui/lib/account-link.js diff --git a/ui/lib/account-link.js b/ui/lib/account-link.js new file mode 100644 index 000000000..037d990fa --- /dev/null +++ b/ui/lib/account-link.js @@ -0,0 +1,26 @@ +module.exports = function (address, network) { + const net = parseInt(network) + let link + switch (net) { + case 1: // main net + link = `https://etherscan.io/address/${address}` + break + case 2: // morden test net + link = `https://morden.etherscan.io/address/${address}` + break + case 3: // ropsten test net + link = `https://ropsten.etherscan.io/address/${address}` + break + case 4: // rinkeby test net + link = `https://rinkeby.etherscan.io/address/${address}` + break + case 42: // kovan test net + link = `https://kovan.etherscan.io/address/${address}` + break + default: + link = '' + break + } + + return link +} -- cgit v1.2.3 From 942de9ba0269a7a7d35bdfcb154a087e86b12b2b Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 19 Oct 2017 09:47:48 -0700 Subject: Patch sandwich-expando for security update --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2e8faadee..81515741e 100644 --- a/package.json +++ b/package.json @@ -134,7 +134,7 @@ "redux-logger": "^3.0.6", "redux-thunk": "^2.2.0", "request-promise": "^4.2.1", - "sandwich-expando": "^1.0.5", + "sandwich-expando": "^1.1.3", "semaphore": "^1.0.5", "sw-stream": "^2.0.0", "textarea-caret": "^3.0.1", -- cgit v1.2.3 From 0ae406e489a635ea094913ee5c20d1e8f2165db5 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 19 Oct 2017 09:59:57 -0700 Subject: Allow computed balances to enumerate its own view --- app/scripts/controllers/computed-balances.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/scripts/controllers/computed-balances.js b/app/scripts/controllers/computed-balances.js index 009405d29..9855f715e 100644 --- a/app/scripts/controllers/computed-balances.js +++ b/app/scripts/controllers/computed-balances.js @@ -20,9 +20,10 @@ class ComputedbalancesController { } updateAllBalances () { - for (let address in this.accountTracker.store.getState().accounts) { + Object.keys(this.balances).forEach((balance) => { + const address = balance.address this.balances[address].updateBalance() - } + }) } _initBalanceUpdating () { -- cgit v1.2.3 From f01d119cc1a6237b88e543be821d91778bcbb128 Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 19 Oct 2017 15:23:56 -0230 Subject: Fixes mobile styling. --- ui/app/components/send/gas-fee-display-v2.js | 2 +- ui/app/css/itcss/components/currency-display.scss | 2 +- ui/app/css/itcss/components/send.scss | 50 ++++++++--- ui/app/send-v2.js | 105 +++++++++++++--------- 4 files changed, 103 insertions(+), 56 deletions(-) diff --git a/ui/app/components/send/gas-fee-display-v2.js b/ui/app/components/send/gas-fee-display-v2.js index 7c3913c7f..3b39312ec 100644 --- a/ui/app/components/send/gas-fee-display-v2.js +++ b/ui/app/components/send/gas-fee-display-v2.js @@ -17,7 +17,7 @@ GasFeeDisplay.prototype.render = function () { onClick, } = this.props - return h('div', [ + return h('div.send-v2__gas-fee-display', [ gasTotal ? h(CurrencyDisplay, { diff --git a/ui/app/css/itcss/components/currency-display.scss b/ui/app/css/itcss/components/currency-display.scss index f2cc6e700..eb1776c58 100644 --- a/ui/app/css/itcss/components/currency-display.scss +++ b/ui/app/css/itcss/components/currency-display.scss @@ -1,6 +1,6 @@ .currency-display { height: 54px; - width: 240px; + width: 100%ß; border: 1px solid $alto; border-radius: 4px; background-color: $white; diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index 7e72e8399..6a5b2b869 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -414,7 +414,6 @@ @media screen and (max-width: $break-small) { width: 100%; - overflow-y: auto; top: 0; box-shadow: none; } @@ -457,6 +456,10 @@ left: 199px; border-radius: 50%; z-index: 100; + + @media screen and (max-width: $break-small) { + top: 36px; + } } &__header { @@ -467,6 +470,10 @@ display: flex; justify-content: center; align-items: center; + + @media screen and (max-width: $break-small) { + height: 59px; + } } &__header-tip { @@ -477,6 +484,10 @@ transform: rotate(45deg); left: 178px; top: 65px; + + @media screen and (max-width: $break-small) { + top: 46px; + } } &__title { @@ -509,32 +520,47 @@ } &__form { - display: flex; - flex-direction: column; margin-top: 13px; width: 100%; + + @media screen and (max-width: $break-small) { + margin-top: 0px; + height: 407px; + overflow-y: auto; + } + } + + &__form-header, &__form-header-copy { + width: 100%; + display: flex; + flex-flow: column; + align-items: center; } &__form-row { margin: 14.5px 18px 0px; - display: flex; position: relative; + display: flex; + flex-flow: row; + flex: 1 0 auto; justify-content: space-between; } + &__form-field { + flex: 1 1 auto; + } + &__form-label { color: $scorpion; font-family: Roboto; font-size: 16px; line-height: 22px; - display: flex; - flex-flow: column; - justify-content: center; + width: 88px; } &__from-dropdown { height: 73px; - width: 240px; + width: 100%; border: 1px solid $alto; border-radius: 4px; background-color: $white; @@ -570,7 +596,7 @@ &__to-autocomplete, &__memo-text-area { &__input { height: 54px; - width: 240px; + width: 100%; border: 1px solid $alto; border-radius: 4px; background-color: $white; @@ -583,6 +609,10 @@ } } + &__gas-fee-display { + width: 100%; + } + &__sliders-icon-container { display: flex; align-items: center; @@ -616,7 +646,7 @@ justify-content: space-evenly; align-items: center; border-top: 1px solid $alto; - margin-top: 29px; + background: $white; } &__next-btn, diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index 8d368044a..e8a12670b 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -103,7 +103,7 @@ SendTransactionScreen.prototype.renderCopy = function () { const tokenText = selectedToken ? 'tokens' : 'ETH' - return h('div', [ + return h('div.send-v2__form-header-copy', [ h('div.send-v2__copy', `Only send ${tokenText} to an Ethereum address.`), @@ -126,9 +126,6 @@ SendTransactionScreen.prototype.renderHeader = function () { ]), - this.renderTitle(), - - this.renderCopy(), ]) } @@ -157,15 +154,17 @@ SendTransactionScreen.prototype.renderFromRow = function () { h('div.send-v2__form-label', 'From:'), - h(FromDropdown, { - dropdownOpen, - accounts: fromAccounts, - selectedAccount: from, - onSelect: updateSendFrom, - openDropdown: () => this.setState({ dropdownOpen: true }), - closeDropdown: () => this.setState({ dropdownOpen: false }), - conversionRate, - }), + h('div.send-v2__form-field', [ + h(FromDropdown, { + dropdownOpen, + accounts: fromAccounts, + selectedAccount: from, + onSelect: updateSendFrom, + openDropdown: () => this.setState({ dropdownOpen: true }), + closeDropdown: () => this.setState({ dropdownOpen: false }), + conversionRate, + }), + ]), ]) } @@ -199,12 +198,14 @@ SendTransactionScreen.prototype.renderToRow = function () { ]), - h(ToAutoComplete, { - to, - accounts: toAccounts, - onChange: this.handleToChange, - inError: Boolean(errors.to), - }), + h('div.send-v2__form-field', [ + h(ToAutoComplete, { + to, + accounts: toAccounts, + onChange: this.handleToChange, + inError: Boolean(errors.to), + }), + ]), ]) } @@ -245,7 +246,7 @@ SendTransactionScreen.prototype.validateAmount = function (value) { conversionRate: amountConversionRate, }, ) - console.log(`sufficientBalance`, sufficientBalance); + const amountLessThanZero = conversionGreaterThan( { value: 0, fromNumericBase: 'dec' }, { value: amount, fromNumericBase: 'hex' }, @@ -277,16 +278,18 @@ SendTransactionScreen.prototype.renderAmountRow = function () { this.renderErrorMessage('amount'), ]), - h(CurrencyDisplay, { - inError: Boolean(errors.amount), - primaryCurrency, - convertedCurrency: 'USD', - value: amount, - conversionRate: amountConversionRate, - convertedPrefix: '$', - handleChange: this.handleAmountChange, - validate: this.validateAmount, - }), + h('div.send-v2__form-field', [ + h(CurrencyDisplay, { + inError: Boolean(errors.amount), + primaryCurrency, + convertedCurrency: 'USD', + value: amount, + conversionRate: amountConversionRate, + convertedPrefix: '$', + handleChange: this.handleAmountChange, + validate: this.validateAmount, + }), + ]), ]) } @@ -302,17 +305,21 @@ SendTransactionScreen.prototype.renderGasRow = function () { h('div.send-v2__form-label', 'Gas fee:'), - h(GasFeeDisplay, { - gasTotal, - conversionRate, - onClick: showCustomizeGasModal, - }), + h('div.send-v2__form-field', [ + + h(GasFeeDisplay, { + gasTotal, + conversionRate, + onClick: showCustomizeGasModal, + }), + + h('div.send-v2__sliders-icon-container', { + onClick: showCustomizeGasModal, + }, [ + h('i.fa.fa-sliders.send-v2__sliders-icon'), + ]), - h('div.send-v2__sliders-icon-container', { - onClick: showCustomizeGasModal, - }, [ - h('i.fa.fa-sliders.send-v2__sliders-icon'), - ]) + ]), ]) } @@ -325,10 +332,12 @@ SendTransactionScreen.prototype.renderMemoRow = function () { h('div.send-v2__form-label', 'Transaction Memo:'), - h(MemoTextArea, { - memo, - onChange: (event) => updateSendMemo(event.target.value), - }), + h('div.send-v2__form-field', [ + h(MemoTextArea, { + memo, + onChange: (event) => updateSendMemo(event.target.value), + }) + ]), ]) } @@ -336,6 +345,14 @@ SendTransactionScreen.prototype.renderMemoRow = function () { SendTransactionScreen.prototype.renderForm = function () { return h('div.send-v2__form', {}, [ + h('div.sendV2__form-header', [ + + this.renderTitle(), + + this.renderCopy(), + + ]), + this.renderFromRow(), this.renderToRow(), -- cgit v1.2.3 From bd11e60b8c128dd69ba1bcf58d25fa9323d91a33 Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 19 Oct 2017 15:53:02 -0230 Subject: Amount field shows insufficient funds error based on amoutn + gas total. --- ui/app/conversion-util.js | 12 ++++++++---- ui/app/send-v2.js | 17 ++++++++++++++--- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js index 1ef276a39..e008ee1cb 100644 --- a/ui/app/conversion-util.js +++ b/ui/app/conversion-util.js @@ -132,12 +132,16 @@ const conversionUtil = (value, { }); const addCurrencies = (a, b, options = {}) => { - const { toNumericBase, numberOfDecimals } = options - const value = (new BigNumber(a)).add(b); + const { + aBase, + bBase, + ...conversionOptions, + } = options + const value = (new BigNumber(a, aBase)).add(b, bBase); + return converter({ value, - toNumericBase, - numberOfDecimals, + ...conversionOptions, }) } diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index e8a12670b..c0a03690a 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -12,7 +12,11 @@ const GasFeeDisplay = require('./components/send/gas-fee-display-v2') const { showModal } = require('./actions') -const { multiplyCurrencies, conversionGreaterThan } = require('./conversion-util') +const { + multiplyCurrencies, + conversionGreaterThan, + addCurrencies, +} = require('./conversion-util') const { isValidAddress } = require('./util') module.exports = SendTransactionScreen @@ -225,12 +229,19 @@ SendTransactionScreen.prototype.validateAmount = function (value) { conversionRate, primaryCurrency, toCurrency, - selectedToken + selectedToken, + gasTotal, } = this.props const amount = value let amountError = null + const totalAmount = addCurrencies(amount, gasTotal, { + aBase: 16, + bBase: 16, + toNumericBase: 'hex', + }) + const sufficientBalance = conversionGreaterThan( { value: balance, @@ -239,7 +250,7 @@ SendTransactionScreen.prototype.validateAmount = function (value) { conversionRate, }, { - value: amount, + value: totalAmount, fromNumericBase: 'hex', conversionRate: amountConversionRate, fromCurrency: selectedToken || primaryCurrency, -- cgit v1.2.3 From 59015cccef72210f828b344aaedde9b8dd31be3b Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 18 Oct 2017 16:53:35 -0230 Subject: Min and default gas price, limit, total; comments out code for gas slider. --- .../customize-gas-modal/gas-modal-card.js | 18 ++++++++--------- ui/app/components/customize-gas-modal/index.js | 17 ++++++++++------ ui/app/components/send/send-constants.js | 23 ++++++++++++++++++++++ ui/app/send-v2.js | 7 ++++--- 4 files changed, 47 insertions(+), 18 deletions(-) create mode 100644 ui/app/components/send/send-constants.js diff --git a/ui/app/components/customize-gas-modal/gas-modal-card.js b/ui/app/components/customize-gas-modal/gas-modal-card.js index 8e739ee40..de181dc67 100644 --- a/ui/app/components/customize-gas-modal/gas-modal-card.js +++ b/ui/app/components/customize-gas-modal/gas-modal-card.js @@ -19,7 +19,7 @@ GasModalCard.prototype.render = function () { unitLabel, value, min, - max, + // max, step, title, copy @@ -34,20 +34,20 @@ GasModalCard.prototype.render = function () { h(InputNumber, { unitLabel, step, - max, + // max, min, placeholder: '0', value, onChange, }), - h(GasSlider, { - value, - step, - max, - min, - onChange, - }), + // h(GasSlider, { + // value, + // step, + // max, + // min, + // onChange, + // }), ]) diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/customize-gas-modal/index.js index 0ba768893..744891c47 100644 --- a/ui/app/components/customize-gas-modal/index.js +++ b/ui/app/components/customize-gas-modal/index.js @@ -5,6 +5,11 @@ const connect = require('react-redux').connect const actions = require('../../actions') const GasModalCard = require('./gas-modal-card') +const { + MIN_GAS_PRICE, + MIN_GAS_LIMIT, +} = require('../send/send-constants') + const { conversionUtil, multiplyCurrencies } = require('../../conversion-util') const { @@ -35,8 +40,8 @@ function CustomizeGasModal (props) { Component.call(this) this.state = { - gasPrice: props.gasPrice, - gasLimit: props.gasLimit, + gasPrice: props.gasPrice || MIN_GAS_PRICE, + gasLimit: props.gasLimit || MIN_GAS_LIMIT, } } @@ -115,8 +120,8 @@ CustomizeGasModal.prototype.render = function () { h(GasModalCard, { value: convertedGasPrice, - min: 0, - max: 1000, + min: MIN_GAS_PRICE, + // max: 1000, step: 1, onChange: value => this.convertAndSetGasPrice(value), title: 'Gas Price', @@ -125,8 +130,8 @@ CustomizeGasModal.prototype.render = function () { h(GasModalCard, { value: convertedGasLimit, - min: 20000, - max: 100000, + min: MIN_GAS_LIMIT, + // max: 100000, step: 1, onChange: value => this.convertAndSetGasLimit(value), title: 'Gas Limit', diff --git a/ui/app/components/send/send-constants.js b/ui/app/components/send/send-constants.js new file mode 100644 index 000000000..a819a8c28 --- /dev/null +++ b/ui/app/components/send/send-constants.js @@ -0,0 +1,23 @@ +const Identicon = require('../identicon') +const { multiplyCurrencies } = require('../../conversion-util') + +const MIN_GAS_PRICE_GWEI = '1' +const GWEI_FACTOR = '1e9' +const MIN_GAS_PRICE = multiplyCurrencies(GWEI_FACTOR, MIN_GAS_PRICE_GWEI, { + multiplicandBase: 16, + multiplierBase: 16, +}) +const MIN_GAS_LIMIT = (21000).toString(16) +const MIN_GAS_TOTAL = multiplyCurrencies(MIN_GAS_LIMIT, MIN_GAS_PRICE, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 16, +}) + +module.exports = { + MIN_GAS_PRICE_GWEI, + GWEI_FACTOR, + MIN_GAS_PRICE, + MIN_GAS_LIMIT, + MIN_GAS_TOTAL, +} diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index c0a03690a..8915350a3 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -10,6 +10,8 @@ const CurrencyDisplay = require('./components/send/currency-display') const MemoTextArea = require('./components/send/memo-textarea') const GasFeeDisplay = require('./components/send/gas-fee-display-v2') +const { MIN_GAS_TOTAL } = require('./components/send/send-constants') + const { showModal } = require('./actions') const { @@ -135,9 +137,8 @@ SendTransactionScreen.prototype.renderHeader = function () { SendTransactionScreen.prototype.renderErrorMessage = function(errorType) { const { errors } = this.props - console.log(`! errors`, errors); const errorMessage = errors[errorType]; - console.log(`errorMessage`, errorMessage); + return errorMessage ? h('div.send-v2__error', [ errorMessage ] ) : null @@ -309,7 +310,7 @@ SendTransactionScreen.prototype.renderGasRow = function () { const { conversionRate, showCustomizeGasModal, - gasTotal, + gasTotal = MIN_GAS_TOTAL, } = this.props return h('div.send-v2__form-row', [ -- cgit v1.2.3 From 332c7441b656ec82ebfba863e3feb4dbf365d67b Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 18 Oct 2017 23:39:26 -0230 Subject: Get currency from state in account details, send and confirm screens. --- ui/app/app.js | 5 ++- .../pending-tx/confirm-deploy-contract.js | 28 +++++++------ ui/app/components/pending-tx/confirm-send-ether.js | 48 +++++++++++----------- ui/app/components/pending-tx/confirm-send-token.js | 24 +++++------ ui/app/components/pending-tx/index.js | 3 -- ui/app/components/send/account-list-item.js | 9 ++-- ui/app/components/send/currency-display.js | 2 +- ui/app/components/send/gas-fee-display-v2.js | 4 +- ui/app/components/send/send-v2-container.js | 8 ++-- ui/app/components/tx-list-item.js | 10 +++-- ui/app/config.js | 1 + ui/app/conversion-util.js | 2 +- ui/app/selectors.js | 5 +++ ui/app/send-v2.js | 6 ++- 14 files changed, 89 insertions(+), 66 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index 76cf1bae1..de6a06a58 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -76,6 +76,7 @@ function mapStateToProps (state) { lastUnreadNotice: state.metamask.lastUnreadNotice, lostAccounts: state.metamask.lostAccounts, frequentRpcList: state.metamask.frequentRpcList || [], + currentCurrency: state.metamask.currentCurrency, // state needed to get account dropdown temporarily rendering from app bar identities, @@ -96,7 +97,9 @@ function mapDispatchToProps (dispatch, ownProps) { } App.prototype.componentWillMount = function () { - this.props.setCurrentCurrencyToUSD() + if (!this.props.currentCurrency) { + this.props.setCurrentCurrencyToUSD() + } } App.prototype.render = function () { diff --git a/ui/app/components/pending-tx/confirm-deploy-contract.js b/ui/app/components/pending-tx/confirm-deploy-contract.js index ea4aa1dde..d19cec755 100644 --- a/ui/app/components/pending-tx/confirm-deploy-contract.js +++ b/ui/app/components/pending-tx/confirm-deploy-contract.js @@ -21,10 +21,12 @@ function mapStateToProps (state) { const { conversionRate, identities, + currentCurrency, } = state.metamask const accounts = state.metamask.accounts const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] return { + currentCurrency, conversionRate, identities, selectedAddress, @@ -124,15 +126,15 @@ ConfirmDeployContract.prototype.getData = function () { } ConfirmDeployContract.prototype.getAmount = function () { - const { conversionRate } = this.props + const { conversionRate, currentCurrency } = this.props const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} - const USD = conversionUtil(txParams.value, { + const FIAT = conversionUtil(txParams.value, { fromNumericBase: 'hex', toNumericBase: 'dec', fromCurrency: 'ETH', - toCurrency: 'USD', + toCurrency: currentCurrency, numberOfDecimals: 2, fromDenomination: 'WEI', conversionRate, @@ -148,14 +150,14 @@ ConfirmDeployContract.prototype.getAmount = function () { }) return { - fiat: Number(USD), + fiat: Number(FIAT), token: Number(ETH), } } ConfirmDeployContract.prototype.getGasFee = function () { - const { conversionRate } = this.props + const { conversionRate, currentCurrency } = this.props const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} @@ -169,12 +171,12 @@ ConfirmDeployContract.prototype.getGasFee = function () { const txFeeBn = gasBn.mul(gasPriceBn) - const USD = conversionUtil(txFeeBn, { + const FIAT = conversionUtil(txFeeBn, { fromNumericBase: 'BN', toNumericBase: 'dec', fromDenomination: 'WEI', fromCurrency: 'ETH', - toCurrency: 'USD', + toCurrency: currentCurrency, numberOfDecimals: 2, conversionRate, }) @@ -189,7 +191,7 @@ ConfirmDeployContract.prototype.getGasFee = function () { }) return { - fiat: Number(USD), + fiat: Number(FIAT), eth: Number(ETH), } } @@ -200,7 +202,7 @@ ConfirmDeployContract.prototype.renderGasFee = function () { h('section.flex-row.flex-center.confirm-screen-row', [ h('span.confirm-screen-label.confirm-screen-section-column', [ 'Gas Fee' ]), h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', `$${fiatGas} USD`), + h('div.confirm-screen-row-info', `${fiatGas} FIAT`), h( 'div.confirm-screen-row-detail', @@ -212,6 +214,7 @@ ConfirmDeployContract.prototype.renderGasFee = function () { } ConfirmDeployContract.prototype.renderHeroAmount = function () { + const { currentCurrency } = this.props const { fiat: fiatAmount } = this.getAmount() const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} @@ -219,8 +222,8 @@ ConfirmDeployContract.prototype.renderHeroAmount = function () { return ( h('div.confirm-send-token__hero-amount-wrapper', [ - h('h3.flex-center.confirm-screen-send-amount', `$${fiatAmount}`), - h('h3.flex-center.confirm-screen-send-amount-currency', 'USD'), + h('h3.flex-center.confirm-screen-send-amount', `${fiatAmount}`), + h('h3.flex-center.confirm-screen-send-amount-currency', currentCurrency.toUpperCase()), h('div.flex-center.confirm-memo-wrapper', [ h('h3.confirm-screen-send-memo', memo), ]), @@ -229,6 +232,7 @@ ConfirmDeployContract.prototype.renderHeroAmount = function () { } ConfirmDeployContract.prototype.renderTotalPlusGas = function () { + const { currentCurrency } = this.props const { fiat: fiatAmount, token: tokenAmount } = this.getAmount() const { fiat: fiatGas, eth: ethGas } = this.getGasFee() @@ -240,7 +244,7 @@ ConfirmDeployContract.prototype.renderTotalPlusGas = function () { ]), h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', `$${fiatAmount + fiatGas} USD`), + h('div.confirm-screen-row-info', `${fiatAmount + fiatGas} ${currentCurrency.toUpperCase()}`), h('div.confirm-screen-row-detail', `${tokenAmount + ethGas} ETH`), ]), ]) diff --git a/ui/app/components/pending-tx/confirm-send-ether.js b/ui/app/components/pending-tx/confirm-send-ether.js index 537a9a659..51c36ba42 100644 --- a/ui/app/components/pending-tx/confirm-send-ether.js +++ b/ui/app/components/pending-tx/confirm-send-ether.js @@ -20,6 +20,7 @@ function mapStateToProps (state) { const { conversionRate, identities, + currentCurrency, } = state.metamask const accounts = state.metamask.accounts const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] @@ -27,6 +28,7 @@ function mapStateToProps (state) { conversionRate, identities, selectedAddress, + currentCurrency, } } @@ -45,15 +47,15 @@ function ConfirmSendEther () { } ConfirmSendEther.prototype.getAmount = function () { - const { conversionRate } = this.props + const { conversionRate, currentCurrency } = this.props const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} - console.log(txParams) - const USD = conversionUtil(txParams.value, { + console.log(`conversionRate, currentCurrency`, conversionRate, currentCurrency); + const FIAT = conversionUtil(txParams.value, { fromNumericBase: 'hex', toNumericBase: 'dec', fromCurrency: 'ETH', - toCurrency: 'USD', + toCurrency: currentCurrency, numberOfDecimals: 2, fromDenomination: 'WEI', conversionRate, @@ -69,14 +71,14 @@ ConfirmSendEther.prototype.getAmount = function () { }) return { - USD, + FIAT, ETH, } } ConfirmSendEther.prototype.getGasFee = function () { - const { conversionRate } = this.props + const { conversionRate, currentCurrency } = this.props const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} @@ -96,12 +98,12 @@ ConfirmSendEther.prototype.getGasFee = function () { const txFeeBn = gasBn.mul(gasPriceBn) - const USD = conversionUtil(txFeeBn, { + const FIAT = conversionUtil(txFeeBn, { fromNumericBase: 'BN', toNumericBase: 'dec', fromDenomination: 'WEI', fromCurrency: 'ETH', - toCurrency: 'USD', + toCurrency: currentCurrency, numberOfDecimals: 2, conversionRate, }) @@ -116,7 +118,7 @@ ConfirmSendEther.prototype.getGasFee = function () { }) return { - USD, + FIAT, ETH, } } @@ -125,10 +127,10 @@ ConfirmSendEther.prototype.getData = function () { const { identities } = this.props const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} - const { USD: gasFeeInUSD, ETH: gasFeeInETH } = this.getGasFee() - const { USD: amountInUSD, ETH: amountInETH } = this.getAmount() + const { FIAT: gasFeeInFIAT, ETH: gasFeeInETH } = this.getGasFee() + const { FIAT: amountInFIAT, ETH: amountInETH } = this.getAmount() - const totalInUSD = addCurrencies(gasFeeInUSD, amountInUSD, { + const totalInFIAT = addCurrencies(gasFeeInFIAT, amountInFIAT, { toNumericBase: 'dec', numberOfDecimals: 2, }) @@ -147,17 +149,17 @@ ConfirmSendEther.prototype.getData = function () { name: identities[txParams.to] ? identities[txParams.to].name : 'New Recipient', }, memo: txParams.memo || '', - gasFeeInUSD, + gasFeeInFIAT, gasFeeInETH, - amountInUSD, + amountInFIAT, amountInETH, - totalInUSD, + totalInFIAT, totalInETH, } } ConfirmSendEther.prototype.render = function () { - const { backToAccountDetail, selectedAddress } = this.props + const { backToAccountDetail, selectedAddress, currentCurrency } = this.props const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} @@ -171,10 +173,10 @@ ConfirmSendEther.prototype.render = function () { name: toName, }, memo, - gasFeeInUSD, + gasFeeInFIAT, gasFeeInETH, - amountInUSD, - totalInUSD, + amountInFIAT, + totalInFIAT, totalInETH, } = this.getData() @@ -239,8 +241,8 @@ ConfirmSendEther.prototype.render = function () { // `You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`, // ]), - h('h3.flex-center.confirm-screen-send-amount', [`$${amountInUSD}`]), - h('h3.flex-center.confirm-screen-send-amount-currency', [ 'USD' ]), + h('h3.flex-center.confirm-screen-send-amount', [`$${amountInFIAT}`]), + h('h3.flex-center.confirm-screen-send-amount-currency', [ currentCurrency.toUpperCase() ]), h('div.flex-center.confirm-memo-wrapper', [ h('h3.confirm-screen-send-memo', [ memo ? `"${memo}"` : '' ]), ]), @@ -265,7 +267,7 @@ ConfirmSendEther.prototype.render = function () { h('section.flex-row.flex-center.confirm-screen-row', [ h('span.confirm-screen-label.confirm-screen-section-column', [ 'Gas Fee' ]), h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', `$${gasFeeInUSD} USD`), + h('div.confirm-screen-row-info', `${gasFeeInFIAT} ${currentCurrency.toUpperCase()}`), h('div.confirm-screen-row-detail', `${gasFeeInETH} ETH`), ]), @@ -279,7 +281,7 @@ ConfirmSendEther.prototype.render = function () { ]), h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', `$${totalInUSD} USD`), + h('div.confirm-screen-row-info', `${totalInFIAT} ${currentCurrency.toUpperCase()}`), h('div.confirm-screen-row-detail', `${totalInETH} ETH`), ]), ]), diff --git a/ui/app/components/pending-tx/confirm-send-token.js b/ui/app/components/pending-tx/confirm-send-token.js index 92bba8f62..155242f56 100644 --- a/ui/app/components/pending-tx/confirm-send-token.js +++ b/ui/app/components/pending-tx/confirm-send-token.js @@ -81,7 +81,7 @@ ConfirmSendToken.prototype.getAmount = function () { } ConfirmSendToken.prototype.getGasFee = function () { - const { conversionRate, tokenExchangeRate, token } = this.props + const { conversionRate, tokenExchangeRate, token, currentCurrency } = this.props const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} const { decimals } = token @@ -96,12 +96,12 @@ ConfirmSendToken.prototype.getGasFee = function () { const txFeeBn = gasBn.mul(gasPriceBn) - const USD = conversionUtil(txFeeBn, { + const FIAT = conversionUtil(txFeeBn, { fromNumericBase: 'BN', toNumericBase: 'dec', fromDenomination: 'WEI', fromCurrency: 'ETH', - toCurrency: 'USD', + toCurrency: currentCurrency, numberOfDecimals: 2, conversionRate, }) @@ -116,7 +116,7 @@ ConfirmSendToken.prototype.getGasFee = function () { }) return { - fiat: +Number(USD).toFixed(2), + fiat: +Number(FIAT).toFixed(2), eth: ETH, token: tokenExchangeRate ? +(ETH * tokenExchangeRate).toFixed(decimals) @@ -145,7 +145,7 @@ ConfirmSendToken.prototype.getData = function () { } ConfirmSendToken.prototype.renderHeroAmount = function () { - const { token: { symbol } } = this.props + const { token: { symbol }, currentCurrency } = this.props const { fiat: fiatAmount, token: tokenAmount } = this.getAmount() const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} @@ -154,8 +154,8 @@ ConfirmSendToken.prototype.renderHeroAmount = function () { return fiatAmount ? ( h('div.confirm-send-token__hero-amount-wrapper', [ - h('h3.flex-center.confirm-screen-send-amount', `$${fiatAmount}`), - h('h3.flex-center.confirm-screen-send-amount-currency', 'USD'), + h('h3.flex-center.confirm-screen-send-amount', `${fiatAmount}`), + h('h3.flex-center.confirm-screen-send-amount-currency', currentCurrency), h('div.flex-center.confirm-memo-wrapper', [ h('h3.confirm-screen-send-memo', [ memo ? `"${memo}"` : '' ]), ]), @@ -173,14 +173,14 @@ ConfirmSendToken.prototype.renderHeroAmount = function () { } ConfirmSendToken.prototype.renderGasFee = function () { - const { token: { symbol } } = this.props + const { token: { symbol }, currentCurrency } = this.props const { fiat: fiatGas, token: tokenGas, eth: ethGas } = this.getGasFee() return ( h('section.flex-row.flex-center.confirm-screen-row', [ h('span.confirm-screen-label.confirm-screen-section-column', [ 'Gas Fee' ]), h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', `$${fiatGas} USD`), + h('div.confirm-screen-row-info', `${fiatGas} ${currentCurrency}`), h( 'div.confirm-screen-row-detail', @@ -192,7 +192,7 @@ ConfirmSendToken.prototype.renderGasFee = function () { } ConfirmSendToken.prototype.renderTotalPlusGas = function () { - const { token: { symbol } } = this.props + const { token: { symbol }, currentCurrency } = this.props const { fiat: fiatAmount, token: tokenAmount } = this.getAmount() const { fiat: fiatGas, token: tokenGas } = this.getGasFee() @@ -205,7 +205,7 @@ ConfirmSendToken.prototype.renderTotalPlusGas = function () { ]), h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', `$${fiatAmount + fiatGas} USD`), + h('div.confirm-screen-row-info', `${fiatAmount + fiatGas} ${currentCurrency}`), h('div.confirm-screen-row-detail', `${tokenAmount + tokenGas} ${symbol}`), ]), ]) @@ -219,7 +219,7 @@ ConfirmSendToken.prototype.renderTotalPlusGas = function () { h('div.confirm-screen-section-column', [ h('div.confirm-screen-row-info', `${tokenAmount} ${symbol}`), - h('div.confirm-screen-row-detail', `+ ${fiatGas} USD Gas`), + h('div.confirm-screen-row-detail', `+ ${fiatGas} ${currentCurrency} Gas`), ]), ]) ) diff --git a/ui/app/components/pending-tx/index.js b/ui/app/components/pending-tx/index.js index 770fb1dfd..f4f6afb8f 100644 --- a/ui/app/components/pending-tx/index.js +++ b/ui/app/components/pending-tx/index.js @@ -36,7 +36,6 @@ function mapStateToProps (state) { function mapDispatchToProps (dispatch) { return { - setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')), backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)), cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })), } @@ -58,8 +57,6 @@ PendingTx.prototype.componentWillMount = async function () { const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} - this.props.setCurrentCurrencyToUSD() - if (!txParams.to) { return this.setState({ transactionType: TX_TYPES.DEPLOY_CONTRACT, diff --git a/ui/app/components/send/account-list-item.js b/ui/app/components/send/account-list-item.js index 64acde767..0938f4cad 100644 --- a/ui/app/components/send/account-list-item.js +++ b/ui/app/components/send/account-list-item.js @@ -4,7 +4,7 @@ const inherits = require('util').inherits const connect = require('react-redux').connect const Identicon = require('../identicon') const CurrencyDisplay = require('./currency-display') -const { conversionRateSelector } = require('../../selectors') +const { conversionRateSelector, getCurrentCurrency } = require('../../selectors') inherits(AccountListItem, Component) function AccountListItem () { @@ -13,7 +13,8 @@ function AccountListItem () { function mapStateToProps(state) { return { - conversionRate: conversionRateSelector(state) + conversionRate: conversionRateSelector(state), + currentCurrency: getCurrentCurrency(state), } } @@ -25,6 +26,7 @@ AccountListItem.prototype.render = function () { handleClick, icon = null, conversionRate, + currentCurrency, } = this.props const { name, address, balance } = account @@ -52,10 +54,9 @@ AccountListItem.prototype.render = function () { h(CurrencyDisplay, { primaryCurrency: 'ETH', - convertedCurrency: 'USD', + convertedCurrency: currentCurrency, value: balance, conversionRate, - convertedPrefix: '$', readOnly: true, className: 'account-list-item__account-balances', primaryBalanceClassName: 'account-list-item__account-primary-balance', diff --git a/ui/app/components/send/currency-display.js b/ui/app/components/send/currency-display.js index f7fbb2379..2c9a2d33b 100644 --- a/ui/app/components/send/currency-display.js +++ b/ui/app/components/send/currency-display.js @@ -118,7 +118,7 @@ CurrencyDisplay.prototype.render = function () { h('div', { className: convertedBalanceClassName, - }, `${convertedPrefix}${convertedValue} ${convertedCurrency}`), + }, `${convertedValue} ${convertedCurrency.toUpperCase()}`), ]) diff --git a/ui/app/components/send/gas-fee-display-v2.js b/ui/app/components/send/gas-fee-display-v2.js index 3b39312ec..0e23b63ac 100644 --- a/ui/app/components/send/gas-fee-display-v2.js +++ b/ui/app/components/send/gas-fee-display-v2.js @@ -15,6 +15,8 @@ GasFeeDisplay.prototype.render = function () { conversionRate, gasTotal, onClick, + primaryCurrency = 'ETH', + convertedCurrency, } = this.props return h('div.send-v2__gas-fee-display', [ @@ -22,7 +24,7 @@ GasFeeDisplay.prototype.render = function () { gasTotal ? h(CurrencyDisplay, { primaryCurrency: 'ETH', - convertedCurrency: 'USD', + convertedCurrency, value: gasTotal, conversionRate, convertedPrefix: '$', diff --git a/ui/app/components/send/send-v2-container.js b/ui/app/components/send/send-v2-container.js index ebe2b878b..c14865e9f 100644 --- a/ui/app/components/send/send-v2-container.js +++ b/ui/app/components/send/send-v2-container.js @@ -16,6 +16,7 @@ const { getGasLimit, getAddressBook, getSendFrom, + getCurrentCurrency, } = require('../../selectors') module.exports = connect(mapStateToProps, mapDispatchToProps)(SendEther) @@ -30,7 +31,7 @@ function mapStateToProps (state) { let data; let primaryCurrency; - let tokenToUSDRate; + let tokenToFiatRate; if (selectedToken) { data = Array.prototype.map.call( abi.rawEncode(['address', 'uint256'], [selectedAddress, '0x0']), @@ -39,7 +40,7 @@ function mapStateToProps (state) { primaryCurrency = selectedToken.symbol - tokenToUSDRate = multiplyCurrencies( + tokenToFiatRate = multiplyCurrencies( conversionRate, selectedTokenExchangeRate, { toNumericBase: 'dec' } @@ -54,8 +55,9 @@ function mapStateToProps (state) { conversionRate, selectedToken, primaryCurrency, + convertedCurrency: getCurrentCurrency(state), data, - amountConversionRate: selectedToken ? tokenToUSDRate : conversionRate, + amountConversionRate: selectedToken ? tokenToFiatRate : conversionRate, } } diff --git a/ui/app/components/tx-list-item.js b/ui/app/components/tx-list-item.js index 8422c02b9..3bb9a2eda 100644 --- a/ui/app/components/tx-list-item.js +++ b/ui/app/components/tx-list-item.js @@ -11,11 +11,14 @@ const Identicon = require('./identicon') const { conversionUtil } = require('../conversion-util') +const { getCurrentCurrency } = require('../selectors') + module.exports = connect(mapStateToProps)(TxListItem) function mapStateToProps (state) { return { tokens: state.metamask.tokens, + currentCurrency: getCurrentCurrency(state), } } @@ -49,17 +52,18 @@ TxListItem.prototype.getSendEtherTotal = function () { transactionAmount, conversionRate, address, + currentCurrency, } = this.props if (!address) { return {} } - const totalInUSD = conversionUtil(transactionAmount, { + const totalInFiat = conversionUtil(transactionAmount, { fromNumericBase: 'hex', toNumericBase: 'dec', fromCurrency: 'ETH', - toCurrency: 'USD', + toCurrency: currentCurrency, fromDenomination: 'WEI', numberOfDecimals: 2, conversionRate, @@ -76,7 +80,7 @@ TxListItem.prototype.getSendEtherTotal = function () { return { total: `${totalInETH} ETH`, - fiatTotal: `$${totalInUSD} USD`, + fiatTotal: `${totalInFiat} ${currentCurrency.toUpperCase()}`, } } diff --git a/ui/app/config.js b/ui/app/config.js index 282a28301..8b4044882 100644 --- a/ui/app/config.js +++ b/ui/app/config.js @@ -170,6 +170,7 @@ function currentConversionInformation (metamaskState, state) { }, defaultValue: currentCurrency, }, infuraCurrencies.map((currency) => { + console.log(`currency`, currency); return h('option', {key: currency.quote.code, value: currency.quote.code}, `${currency.quote.code.toUpperCase()} - ${currency.quote.name}`) }) ), diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js index e008ee1cb..50f903d9f 100644 --- a/ui/app/conversion-util.js +++ b/ui/app/conversion-util.js @@ -36,7 +36,7 @@ const BIG_NUMBER_GWEI_MULTIPLIER = new BigNumber('1000000000') // Individual Setters const convert = R.invoker(1, 'times') -const round = R.invoker(2, 'toFormat')(R.__, BigNumber.ROUND_DOWN) +const round = R.invoker(2, 'round')(R.__, BigNumber.ROUND_DOWN) const invertConversionRate = conversionRate => () => new BigNumber(1.0).div(conversionRate) // Setter Maps diff --git a/ui/app/selectors.js b/ui/app/selectors.js index 9d4e6eb67..1cfbc3975 100644 --- a/ui/app/selectors.js +++ b/ui/app/selectors.js @@ -14,6 +14,7 @@ const selectors = { getGasLimit, getAddressBook, getSendFrom, + getCurrentCurrency, } module.exports = selectors @@ -112,3 +113,7 @@ function getGasLimit (state) { function getSendFrom (state) { return state.metamask.send.from } + +function getCurrentCurrency (state) { + return state.metamask.currentCurrency +} diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index 8915350a3..effa68b4b 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -277,6 +277,7 @@ SendTransactionScreen.prototype.renderAmountRow = function () { const { selectedToken, primaryCurrency = 'ETH', + convertedCurrency, amountConversionRate, errors, } = this.props @@ -294,10 +295,9 @@ SendTransactionScreen.prototype.renderAmountRow = function () { h(CurrencyDisplay, { inError: Boolean(errors.amount), primaryCurrency, - convertedCurrency: 'USD', + convertedCurrency, value: amount, conversionRate: amountConversionRate, - convertedPrefix: '$', handleChange: this.handleAmountChange, validate: this.validateAmount, }), @@ -309,6 +309,7 @@ SendTransactionScreen.prototype.renderAmountRow = function () { SendTransactionScreen.prototype.renderGasRow = function () { const { conversionRate, + convertedCurrency, showCustomizeGasModal, gasTotal = MIN_GAS_TOTAL, } = this.props @@ -322,6 +323,7 @@ SendTransactionScreen.prototype.renderGasRow = function () { h(GasFeeDisplay, { gasTotal, conversionRate, + convertedCurrency, onClick: showCustomizeGasModal, }), -- cgit v1.2.3 From 3b4c679ffcd76279221bb7cb6b83c53f0468ee65 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 19 Oct 2017 12:15:26 -0700 Subject: Fix bug where new account was not immediately selected --- app/scripts/metamask-controller.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 366bb6d98..457d38e26 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -324,7 +324,7 @@ module.exports = class MetamaskController extends EventEmitter { createShapeShiftTx: this.createShapeShiftTx.bind(this), // primary HD keyring management - addNewAccount: this.addNewAccount.bind(this), + addNewAccount: nodeify(this.addNewAccount, this), placeSeedWords: this.placeSeedWords.bind(this), clearSeedWordCache: this.clearSeedWordCache.bind(this), importAccountWithStrategy: this.importAccountWithStrategy.bind(this), @@ -490,10 +490,21 @@ module.exports = class MetamaskController extends EventEmitter { // Opinionated Keyring Management // - addNewAccount (cb) { + async addNewAccount (cb) { const primaryKeyring = this.keyringController.getKeyringsByType('HD Key Tree')[0] if (!primaryKeyring) return cb(new Error('MetamaskController - No HD Key Tree found')) - promiseToCallback(this.keyringController.addNewAccount(primaryKeyring))(cb) + const keyringController = this.keyringController + const oldAccounts = await keyringController.getAccounts() + const keyState = await keyringController.addNewAccount(primaryKeyring) + const newAccounts = await keyringController.getAccounts() + + newAccounts.forEach((address) => { + if (!oldAccounts.includes(address)) { + this.preferencesController.setSelectedAddress(address) + } + }) + + return keyState } // Adds the current vault's seed words to the UI's state tree. -- cgit v1.2.3 From a10a600cced6273047f224c5e19d186de091efe0 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Thu, 19 Oct 2017 12:33:43 -0700 Subject: Linted --- app/scripts/lib/account-tracker.js | 2 +- app/scripts/metamask-controller.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index b9959dc25..ce6642150 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -57,7 +57,7 @@ class AccountTracker extends EventEmitter { }) toAdd.forEach(upstream => this.addAccount(upstream)) - toRemove.forEach(local=> this.removeAccount(local)) + toRemove.forEach(local => this.removeAccount(local)) this._updateAccounts() } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 457d38e26..ad42a39fb 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1,6 +1,5 @@ const EventEmitter = require('events') const extend = require('xtend') -const promiseToCallback = require('promise-to-callback') const pump = require('pump') const Dnode = require('dnode') const ObservableStore = require('obs-store') -- cgit v1.2.3 From 89af385a352daf66ad1a6fb3bba75676fd3b9e7f Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 18 Oct 2017 15:38:53 -0230 Subject: Fix handling of arithmetic on token gas in confirm-send-token. --- ui/app/components/pending-tx/confirm-send-token.js | 49 +++++++++++++++------- ui/app/conversion-util.js | 8 ++-- ui/app/selectors.js | 11 ++++- 3 files changed, 47 insertions(+), 21 deletions(-) diff --git a/ui/app/components/pending-tx/confirm-send-token.js b/ui/app/components/pending-tx/confirm-send-token.js index 92bba8f62..de716a26a 100644 --- a/ui/app/components/pending-tx/confirm-send-token.js +++ b/ui/app/components/pending-tx/confirm-send-token.js @@ -11,12 +11,22 @@ const Identicon = require('../identicon') const ethUtil = require('ethereumjs-util') const BN = ethUtil.BN const hexToBn = require('../../../../app/scripts/lib/hex-to-bn') -const { conversionUtil } = require('../../conversion-util') +const { + conversionUtil, + multiplyCurrencies, + addCurrencies, +} = require('../../conversion-util') const MIN_GAS_PRICE_GWEI_BN = new BN(1) const GWEI_FACTOR = new BN(1e9) const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) +const { + getSelectedTokenExchangeRate, + getTokenExchangeRate, + getSelectedAddress, +} = require('../../selectors') + module.exports = connect(mapStateToProps, mapDispatchToProps)(ConfirmSendToken) function mapStateToProps (state, ownProps) { @@ -28,10 +38,8 @@ function mapStateToProps (state, ownProps) { identities, } = state.metamask const accounts = state.metamask.accounts - const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] - const tokenExchangeRates = state.metamask.tokenExchangeRates - const pair = `${symbol.toLowerCase()}_eth` - const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {} + const selectedAddress = getSelectedAddress(state) + const tokenExchangeRate = getTokenExchangeRate(state, symbol) return { conversionRate, @@ -86,17 +94,14 @@ ConfirmSendToken.prototype.getGasFee = function () { const txParams = txMeta.txParams || {} const { decimals } = token - // Gas const gas = txParams.gas - const gasBn = hexToBn(gas) - - // Gas Price const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) - const gasPriceBn = hexToBn(gasPrice) - const txFeeBn = gasBn.mul(gasPriceBn) - + const gasTotal = multiplyCurrencies(gas, gasPrice, { + multiplicandBase: 16, + multiplierBase: 16, + }) - const USD = conversionUtil(txFeeBn, { + const USD = conversionUtil(gasTotal, { fromNumericBase: 'BN', toNumericBase: 'dec', fromDenomination: 'WEI', @@ -105,7 +110,7 @@ ConfirmSendToken.prototype.getGasFee = function () { numberOfDecimals: 2, conversionRate, }) - const ETH = conversionUtil(txFeeBn, { + const ETH = conversionUtil(gasTotal, { fromNumericBase: 'BN', toNumericBase: 'dec', fromDenomination: 'WEI', @@ -114,12 +119,22 @@ ConfirmSendToken.prototype.getGasFee = function () { numberOfDecimals: 6, conversionRate, }) + const tokenGas = multiplyCurrencies(gas, gasPrice, { + toNumericBase: 'dec', + multiplicandBase: 16, + multiplierBase: 16, + toCurrency: 'BAT', + conversionRate: tokenExchangeRate, + invertConversionRate: true, + fromDenomination: 'WEI', + numberOfDecimals: decimals || 4, + }) return { fiat: +Number(USD).toFixed(2), eth: ETH, token: tokenExchangeRate - ? +(ETH * tokenExchangeRate).toFixed(decimals) + ? tokenGas : null, } } @@ -196,6 +211,8 @@ ConfirmSendToken.prototype.renderTotalPlusGas = function () { const { fiat: fiatAmount, token: tokenAmount } = this.getAmount() const { fiat: fiatGas, token: tokenGas } = this.getGasFee() + const tokenTotal = addCurrencies(tokenAmount, tokenGas || '0') + return fiatAmount && fiatGas ? ( h('section.flex-row.flex-center.confirm-screen-total-box ', [ @@ -206,7 +223,7 @@ ConfirmSendToken.prototype.renderTotalPlusGas = function () { h('div.confirm-screen-section-column', [ h('div.confirm-screen-row-info', `$${fiatAmount + fiatGas} USD`), - h('div.confirm-screen-row-detail', `${tokenAmount + tokenGas} ${symbol}`), + h('div.confirm-screen-row-detail', `${tokenTotal} ${symbol}`), ]), ]) ) diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js index e008ee1cb..51c7bd355 100644 --- a/ui/app/conversion-util.js +++ b/ui/app/conversion-util.js @@ -147,16 +147,16 @@ const addCurrencies = (a, b, options = {}) => { const multiplyCurrencies = (a, b, options = {}) => { const { - toNumericBase, - numberOfDecimals, multiplicandBase, multiplierBase, + ...conversionOptions, } = options + const value = (new BigNumber(a, multiplicandBase)).times(b, multiplierBase); + return converter({ value, - toNumericBase, - numberOfDecimals, + ...conversionOptions, }) } diff --git a/ui/app/selectors.js b/ui/app/selectors.js index 9d4e6eb67..8806a516f 100644 --- a/ui/app/selectors.js +++ b/ui/app/selectors.js @@ -6,6 +6,7 @@ const selectors = { getSelectedAccount, getSelectedToken, getSelectedTokenExchangeRate, + getTokenExchangeRate, conversionRateSelector, transactionsSelector, accountsWithSendEtherInfoSelector, @@ -57,7 +58,15 @@ function getSelectedTokenExchangeRate (state) { return tokenExchangeRate } -function conversionRateSelector (state) { +function getTokenExchangeRate (state, tokenSymbol) { + const pair = `${tokenSymbol.toLowerCase()}_eth` + const tokenExchangeRates = state.metamask.tokenExchangeRates + const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {} + + return tokenExchangeRate +} + +function conversionRateSelector (state) { return state.metamask.conversionRate } -- cgit v1.2.3 From 7362fb8dfc5b8d9f3ae9a3399f9448ea5720cd43 Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 18 Oct 2017 15:56:13 -0230 Subject: Identicon in send token show who you are sending to, not the token's identicon. --- ui/app/components/pending-tx/confirm-send-token.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/app/components/pending-tx/confirm-send-token.js b/ui/app/components/pending-tx/confirm-send-token.js index de716a26a..356da36c3 100644 --- a/ui/app/components/pending-tx/confirm-send-token.js +++ b/ui/app/components/pending-tx/confirm-send-token.js @@ -145,14 +145,14 @@ ConfirmSendToken.prototype.getData = function () { const { value } = params[0] || {} const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} - + return { from: { address: txParams.from, name: identities[txParams.from].name, }, to: { - address: txParams.to, + address: value, name: identities[value] ? identities[value].name : 'New Recipient', }, memo: txParams.memo || '', @@ -290,7 +290,7 @@ ConfirmSendToken.prototype.render = function () { h( Identicon, { - address: txParams.to, + address: toAddress, diameter: 60, }, ), -- cgit v1.2.3 From c2880c4b8fe56f3b175d75b6ae8a84271dde3e28 Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 18 Oct 2017 16:53:35 -0230 Subject: Min and default gas price, limit, total; comments out code for gas slider. --- .../customize-gas-modal/gas-modal-card.js | 18 ++++++++--------- ui/app/components/customize-gas-modal/index.js | 17 ++++++++++------ ui/app/components/send/send-constants.js | 23 ++++++++++++++++++++++ ui/app/send-v2.js | 7 ++++--- 4 files changed, 47 insertions(+), 18 deletions(-) create mode 100644 ui/app/components/send/send-constants.js diff --git a/ui/app/components/customize-gas-modal/gas-modal-card.js b/ui/app/components/customize-gas-modal/gas-modal-card.js index 8e739ee40..de181dc67 100644 --- a/ui/app/components/customize-gas-modal/gas-modal-card.js +++ b/ui/app/components/customize-gas-modal/gas-modal-card.js @@ -19,7 +19,7 @@ GasModalCard.prototype.render = function () { unitLabel, value, min, - max, + // max, step, title, copy @@ -34,20 +34,20 @@ GasModalCard.prototype.render = function () { h(InputNumber, { unitLabel, step, - max, + // max, min, placeholder: '0', value, onChange, }), - h(GasSlider, { - value, - step, - max, - min, - onChange, - }), + // h(GasSlider, { + // value, + // step, + // max, + // min, + // onChange, + // }), ]) diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/customize-gas-modal/index.js index 0ba768893..744891c47 100644 --- a/ui/app/components/customize-gas-modal/index.js +++ b/ui/app/components/customize-gas-modal/index.js @@ -5,6 +5,11 @@ const connect = require('react-redux').connect const actions = require('../../actions') const GasModalCard = require('./gas-modal-card') +const { + MIN_GAS_PRICE, + MIN_GAS_LIMIT, +} = require('../send/send-constants') + const { conversionUtil, multiplyCurrencies } = require('../../conversion-util') const { @@ -35,8 +40,8 @@ function CustomizeGasModal (props) { Component.call(this) this.state = { - gasPrice: props.gasPrice, - gasLimit: props.gasLimit, + gasPrice: props.gasPrice || MIN_GAS_PRICE, + gasLimit: props.gasLimit || MIN_GAS_LIMIT, } } @@ -115,8 +120,8 @@ CustomizeGasModal.prototype.render = function () { h(GasModalCard, { value: convertedGasPrice, - min: 0, - max: 1000, + min: MIN_GAS_PRICE, + // max: 1000, step: 1, onChange: value => this.convertAndSetGasPrice(value), title: 'Gas Price', @@ -125,8 +130,8 @@ CustomizeGasModal.prototype.render = function () { h(GasModalCard, { value: convertedGasLimit, - min: 20000, - max: 100000, + min: MIN_GAS_LIMIT, + // max: 100000, step: 1, onChange: value => this.convertAndSetGasLimit(value), title: 'Gas Limit', diff --git a/ui/app/components/send/send-constants.js b/ui/app/components/send/send-constants.js new file mode 100644 index 000000000..a819a8c28 --- /dev/null +++ b/ui/app/components/send/send-constants.js @@ -0,0 +1,23 @@ +const Identicon = require('../identicon') +const { multiplyCurrencies } = require('../../conversion-util') + +const MIN_GAS_PRICE_GWEI = '1' +const GWEI_FACTOR = '1e9' +const MIN_GAS_PRICE = multiplyCurrencies(GWEI_FACTOR, MIN_GAS_PRICE_GWEI, { + multiplicandBase: 16, + multiplierBase: 16, +}) +const MIN_GAS_LIMIT = (21000).toString(16) +const MIN_GAS_TOTAL = multiplyCurrencies(MIN_GAS_LIMIT, MIN_GAS_PRICE, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 16, +}) + +module.exports = { + MIN_GAS_PRICE_GWEI, + GWEI_FACTOR, + MIN_GAS_PRICE, + MIN_GAS_LIMIT, + MIN_GAS_TOTAL, +} diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index c0a03690a..8915350a3 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -10,6 +10,8 @@ const CurrencyDisplay = require('./components/send/currency-display') const MemoTextArea = require('./components/send/memo-textarea') const GasFeeDisplay = require('./components/send/gas-fee-display-v2') +const { MIN_GAS_TOTAL } = require('./components/send/send-constants') + const { showModal } = require('./actions') const { @@ -135,9 +137,8 @@ SendTransactionScreen.prototype.renderHeader = function () { SendTransactionScreen.prototype.renderErrorMessage = function(errorType) { const { errors } = this.props - console.log(`! errors`, errors); const errorMessage = errors[errorType]; - console.log(`errorMessage`, errorMessage); + return errorMessage ? h('div.send-v2__error', [ errorMessage ] ) : null @@ -309,7 +310,7 @@ SendTransactionScreen.prototype.renderGasRow = function () { const { conversionRate, showCustomizeGasModal, - gasTotal, + gasTotal = MIN_GAS_TOTAL, } = this.props return h('div.send-v2__form-row', [ -- cgit v1.2.3 From 376ae032fedb99d22b7c71438ec9482d2013c78e Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Thu, 19 Oct 2017 12:47:44 -0700 Subject: Fix selectors --- ui/app/app.js | 1 - ui/app/components/account-menu/index.js | 5 +- ui/app/components/wallet-view.js | 4 +- ui/app/selectors.js | 4 +- yarn.lock | 92 ++++++++++++++++++++++++++------- 5 files changed, 80 insertions(+), 26 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index de6a06a58..cf82248e4 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -207,7 +207,6 @@ App.prototype.renderAppBar = function () { if (window.METAMASK_UI_TYPE === 'notification') { return null } - console.log(this.props) return ( h('.full-width', { diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index 2ebdba24a..85bd21076 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -35,7 +35,6 @@ function mapDispatchToProps (dispatch) { dispatch(actions.toggleAccountMenu()) }, showConfigPage: () => { - console.log('hihihih') dispatch(actions.showConfigPage()) dispatch(actions.toggleAccountMenu()) }, @@ -60,7 +59,6 @@ AccountMenu.prototype.render = function () { showConfigPage, } = this.props - console.log(showConfigPage) return h(Menu, { className: 'account-menu', isShowing: isAccountMenuOpen }, [ h(CloseArea, { onClick: toggleAccountMenu }), h(Item, { @@ -105,11 +103,12 @@ AccountMenu.prototype.renderAccounts = function () { showAccountDetail, } = this.props + console.log({ accounts }) return Object.keys(identities).map((key, index) => { const identity = identities[key] const isSelected = identity.address === selected - const balanceValue = accounts[key].balance + const balanceValue = accounts[key] ? accounts[key].balance : '' const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' const simpleAddress = identity.address.substring(2).toLowerCase() diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index f06c4d512..a870a24e3 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -47,7 +47,7 @@ WalletView.prototype.renderWalletBalance = function () { hideSidebar, sidebarOpen, } = this.props - + console.log({ selectedAccount }) const selectedClass = selectedTokenAddress ? '' : 'wallet-balance-wrapper--active' @@ -63,7 +63,7 @@ WalletView.prototype.renderWalletBalance = function () { }, [ h(BalanceComponent, { - balanceValue: selectedAccount.balance, + balanceValue: selectedAccount ? selectedAccount.balance : '', style: {}, }), ] diff --git a/ui/app/selectors.js b/ui/app/selectors.js index 66b7c00e4..4c3d21d33 100644 --- a/ui/app/selectors.js +++ b/ui/app/selectors.js @@ -67,7 +67,7 @@ function getTokenExchangeRate (state, tokenSymbol) { return tokenExchangeRate } -function conversionRateSelector (state) { +function conversionRateSelector (state) { return state.metamask.conversionRate } @@ -102,7 +102,7 @@ function transactionsSelector (state) { const transactions = state.metamask.selectedAddressTxList || [] const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList) - console.log({txsToRender, selectedTokenAddress}) + // console.log({txsToRender, selectedTokenAddress}) return selectedTokenAddress ? txsToRender .filter(({ txParams: { to } }) => to === selectedTokenAddress) diff --git a/yarn.lock b/yarn.lock index 837374e1d..ef81fe3ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3406,7 +3406,7 @@ eth-ens-namehash@^1.0.2: idna-uts46 "^1.0.1" js-sha3 "^0.5.7" -eth-hd-keyring@^1.1.1, eth-hd-keyring@^1.2.0: +eth-hd-keyring@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/eth-hd-keyring/-/eth-hd-keyring-1.2.0.tgz#40bcc7ea877ef5c746f54c0c87a6b39ceb5edde3" dependencies: @@ -3416,12 +3416,23 @@ eth-hd-keyring@^1.1.1, eth-hd-keyring@^1.2.0: ethereumjs-wallet "^0.6.0" events "^1.1.1" -eth-json-rpc-filters@^1.2.1: +eth-hd-keyring@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/eth-json-rpc-filters/-/eth-json-rpc-filters-1.2.1.tgz#96e1714272a0f7d6d8efef7af8d764988f73ffc1" + resolved "https://registry.yarnpkg.com/eth-hd-keyring/-/eth-hd-keyring-1.2.1.tgz#15ab3919b4153a8497e14673e8e8039e5965131c" + dependencies: + bip39 "^2.2.0" + eth-sig-util "^1.3.0" + ethereumjs-util "^5.1.1" + ethereumjs-wallet "^0.6.0" + events "^1.1.1" + +eth-json-rpc-filters@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/eth-json-rpc-filters/-/eth-json-rpc-filters-1.2.3.tgz#6ad6db134d1fc84c4d0b60f9faf19b70d300ae1e" dependencies: await-semaphore "^0.1.1" eth-json-rpc-middleware "^1.0.0" + json-rpc-engine "^3.4.0" lodash.flatmap "^4.5.0" eth-json-rpc-middleware@^1.0.0, eth-json-rpc-middleware@^1.2.7: @@ -3439,9 +3450,9 @@ eth-json-rpc-middleware@^1.0.0, eth-json-rpc-middleware@^1.2.7: promise-to-callback "^1.0.0" tape "^4.6.3" -eth-keyring-controller@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eth-keyring-controller/-/eth-keyring-controller-2.1.0.tgz#46b2c1597d9471aab5e4f792dc109084ed196f2d" +eth-keyring-controller@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/eth-keyring-controller/-/eth-keyring-controller-2.1.1.tgz#08129c8300f0ac6de9110e0b8d51292b5c6327e3" dependencies: bip39 "^2.4.0" bluebird "^3.5.0" @@ -3480,6 +3491,13 @@ eth-sig-util@^1.3.0: ethereumjs-abi "git+https://github.com/ethereumjs/ethereumjs-abi.git" ethereumjs-util "^5.1.1" +eth-sig-util@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/eth-sig-util/-/eth-sig-util-1.4.0.tgz#ad42fd1d9c60fff19bdef7377b42fb38e92ee7e1" + dependencies: + ethereumjs-abi "git+https://github.com/ethereumjs/ethereumjs-abi.git" + ethereumjs-util "^5.1.1" + eth-simple-keyring@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/eth-simple-keyring/-/eth-simple-keyring-1.1.1.tgz#6dd75d7cc6edea7c788cf19ef9431c830cd961ae" @@ -3489,6 +3507,15 @@ eth-simple-keyring@^1.1.1: ethereumjs-wallet "^0.6.0" events "^1.1.1" +eth-simple-keyring@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/eth-simple-keyring/-/eth-simple-keyring-1.2.0.tgz#b151d2c75877e2cddf94ae5feae78214cf198846" + dependencies: + eth-sig-util "^1.3.0" + ethereumjs-util "^5.1.1" + ethereumjs-wallet "^0.6.0" + events "^1.1.1" + eth-token-tracker@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/eth-token-tracker/-/eth-token-tracker-1.1.4.tgz#29ff2457d66bfa3b8ee490e83ff40fd0cf2cec41" @@ -3512,14 +3539,7 @@ ethereum-ens-network-map@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/ethereum-ens-network-map/-/ethereum-ens-network-map-1.0.0.tgz#43cd7669ce950a789e151001118d4d65f210eeb7" -ethereumjs-abi@^0.6.4: - version "0.6.4" - resolved "https://registry.yarnpkg.com/ethereumjs-abi/-/ethereumjs-abi-0.6.4.tgz#9ba1bb056492d00c27279f6eccd4d58275912c1a" - dependencies: - bn.js "^4.10.0" - ethereumjs-util "^4.3.0" - -"ethereumjs-abi@git+https://github.com/ethereumjs/ethereumjs-abi.git": +ethereumjs-abi@^0.6.4, "ethereumjs-abi@git+https://github.com/ethereumjs/ethereumjs-abi.git": version "0.6.4" resolved "git+https://github.com/ethereumjs/ethereumjs-abi.git#ee6ded67235a98f3ef4ae2a338aee70a9f68fe20" dependencies: @@ -3609,6 +3629,10 @@ ethereumjs-wallet@^0.6.0: utf8 "^2.1.1" uuid "^2.0.1" +etherscan-link@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/etherscan-link/-/etherscan-link-1.0.2.tgz#c7b9142c4b59509b338a204b6328aea40dd3c64e" + ethjs-abi@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/ethjs-abi/-/ethjs-abi-0.2.0.tgz#d3e2c221011520fc499b71682036c14fcc2f5b25" @@ -3984,6 +4008,18 @@ faye-websocket@~0.7.2: dependencies: websocket-driver ">=0.3.6" +fbjs@^0.8.16: + version "0.8.16" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db" + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.9" + fbjs@^0.8.9: version "0.8.15" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.15.tgz#4f0695fdfcc16c37c0b07facec8cb4c4091685b9" @@ -5627,6 +5663,16 @@ json-rpc-engine@^3.2.0: babelify "^7.3.0" json-rpc-error "^2.0.0" +json-rpc-engine@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/json-rpc-engine/-/json-rpc-engine-3.4.0.tgz#8a1647a7f2cc7018f4802f41ec8208d281f78bfc" + dependencies: + async "^2.0.1" + babel-preset-env "^1.3.2" + babelify "^7.3.0" + json-rpc-error "^2.0.0" + promise-to-callback "^1.0.0" + json-rpc-error@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/json-rpc-error/-/json-rpc-error-2.0.0.tgz#a7af9c202838b5e905c7250e547f1aff77258a02" @@ -7746,6 +7792,14 @@ prop-types@^15.5.1, prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.8: fbjs "^0.8.9" loose-envify "^1.3.1" +prop-types@^15.5.7: + version "15.6.0" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856" + dependencies: + fbjs "^0.8.16" + loose-envify "^1.3.1" + object-assign "^4.1.1" + propagate@0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/propagate/-/propagate-0.4.0.tgz#f3fcca0a6fe06736a7ba572966069617c130b481" @@ -8006,9 +8060,11 @@ react-select@^1.0.0-rc.2: prop-types "^15.5.8" react-input-autosize "^1.1.5" -react-simple-file-input@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/react-simple-file-input/-/react-simple-file-input-1.0.0.tgz#0d5989b51b9bf2c25bb48a0c3fd7e73e413eaa48" +react-simple-file-input@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/react-simple-file-input/-/react-simple-file-input-2.0.0.tgz#3686982ee26f50b22a69468e22aeeb2f392826c9" + dependencies: + prop-types "^15.5.7" react-test-renderer@^15.5.4: version "15.6.1" @@ -10110,7 +10166,7 @@ weak@^1.0.0: bindings "^1.2.1" nan "^2.0.5" -web3-provider-engine@^13.3.1: +web3-provider-engine@^13.3.2: version "13.3.2" resolved "https://registry.yarnpkg.com/web3-provider-engine/-/web3-provider-engine-13.3.2.tgz#a5954aa637f96f0dde5131bc20a6ce9e33e6fcd1" dependencies: -- cgit v1.2.3 From 0458643f104d7b328e24c4403e4e3d91b4d5de92 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Thu, 19 Oct 2017 14:08:52 -0700 Subject: various styling fix on mobile --- ui/app/components/send/account-list-item.js | 8 ++++---- ui/app/css/itcss/components/account-menu.scss | 2 +- ui/app/css/itcss/components/hero-balance.scss | 16 ++++++++++------ ui/app/css/itcss/components/menu.scss | 1 + ui/app/css/itcss/components/newui-sections.scss | 15 +++++++++++++-- ui/app/css/itcss/components/send.scss | 4 +++- ui/app/css/itcss/components/transaction-list.scss | 17 +++++++++++++++-- 7 files changed, 47 insertions(+), 16 deletions(-) diff --git a/ui/app/components/send/account-list-item.js b/ui/app/components/send/account-list-item.js index 0938f4cad..ba7eec940 100644 --- a/ui/app/components/send/account-list-item.js +++ b/ui/app/components/send/account-list-item.js @@ -11,7 +11,7 @@ function AccountListItem () { Component.call(this) } -function mapStateToProps(state) { +function mapStateToProps (state) { return { conversionRate: conversionRateSelector(state), currentCurrency: getCurrentCurrency(state), @@ -22,14 +22,14 @@ module.exports = connect(mapStateToProps)(AccountListItem) AccountListItem.prototype.render = function () { const { - account, - handleClick, + account, + handleClick, icon = null, conversionRate, currentCurrency, } = this.props - const { name, address, balance } = account + const { name, address, balance } = account || {} return h('div.account-list-item', { onClick: () => handleClick({ name, address, balance }), diff --git a/ui/app/css/itcss/components/account-menu.scss b/ui/app/css/itcss/components/account-menu.scss index 857903ce1..090710f7b 100644 --- a/ui/app/css/itcss/components/account-menu.scss +++ b/ui/app/css/itcss/components/account-menu.scss @@ -5,7 +5,7 @@ width: 310px; @media screen and (max-width: 575px) { - right: calc((100vw - 100%) / 2); + right: calc(((100vw - 100%) / 2) + 8px); } @media screen and (min-width: 576px) { diff --git a/ui/app/css/itcss/components/hero-balance.scss b/ui/app/css/itcss/components/hero-balance.scss index 8f6731358..bdbdd2645 100644 --- a/ui/app/css/itcss/components/hero-balance.scss +++ b/ui/app/css/itcss/components/hero-balance.scss @@ -6,8 +6,9 @@ justify-content: flex-start; align-items: center; margin: .3em .9em 0; - height: 80vh; - max-height: 225px; + // height: 80vh; + // max-height: 225px; + flex: 0 0 auto; } @media screen and (min-width: $break-large) { @@ -26,6 +27,7 @@ @media screen and (max-width: $break-small) { flex-direction: column; + flex: 0 0 auto; } @media screen and (min-width: $break-large) { @@ -78,7 +80,9 @@ @media screen and (max-width: $break-small) { width: 100%; - height: 100px; // needed a round number to set the heights of the buttons inside + // height: 100px; // needed a round number to set the heights of the buttons inside + flex: 0 0 auto; + padding: 16px 0; } @media screen and (min-width: $break-large) { @@ -93,9 +97,9 @@ font-size: 12px; @media screen and (max-width: $break-small) { - width: 23%; - height: 55%; - border-color: $black; + border-color: $curious-blue; + color: $curious-blue; + height: 36px; } @media screen and (min-width: $break-large) { diff --git a/ui/app/css/itcss/components/menu.scss b/ui/app/css/itcss/components/menu.scss index c98ee70d9..17e24de98 100644 --- a/ui/app/css/itcss/components/menu.scss +++ b/ui/app/css/itcss/components/menu.scss @@ -12,6 +12,7 @@ align-items: center; position: relative; z-index: 200; + font-weight: 200; @media screen and (max-width: 575px) { padding: 14px; diff --git a/ui/app/css/itcss/components/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss index fc1dba87c..1ee8283ef 100644 --- a/ui/app/css/itcss/components/newui-sections.scss +++ b/ui/app/css/itcss/components/newui-sections.scss @@ -25,6 +25,17 @@ $wallet-view-bg: $wild-sand; .tx-view { flex: 63.5 0 66.5%; background: $tx-view-bg; + + // No title on mobile + @media screen and (max-width: 575px) { + .identicon-wrapper { + display: none; + } + + .account-name { + display: none; + } + } } // wallet view and sidebar @@ -70,7 +81,7 @@ $wallet-view-bg: $wild-sand; background: rgb(250, 250, 250); z-index: $sidebar-z-index; position: fixed; - // top: 41px; + top: 57px; left: 0; right: 0; bottom: 0; @@ -80,7 +91,7 @@ $wallet-view-bg: $wild-sand; overflow-y: auto; box-shadow: rgba(0, 0, 0, .15) 2px 2px 4px; width: 85%; - height: 100%; + height: calc(100% - 57px); } .sidebar-overlay { diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index 6a5b2b869..9a076551e 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -525,8 +525,9 @@ @media screen and (max-width: $break-small) { margin-top: 0px; - height: 407px; + height: 0; overflow-y: auto; + flex: 1 1 auto; } } @@ -647,6 +648,7 @@ align-items: center; border-top: 1px solid $alto; background: $white; + padding: 0 12px; } &__next-btn, diff --git a/ui/app/css/itcss/components/transaction-list.scss b/ui/app/css/itcss/components/transaction-list.scss index 76fac09e2..a5d508f11 100644 --- a/ui/app/css/itcss/components/transaction-list.scss +++ b/ui/app/css/itcss/components/transaction-list.scss @@ -19,12 +19,15 @@ // margin-top: 0.2em; // margin-bottom: 0.6em; justify-content: center; + flex: 0 0 auto; } .tx-list-header { align-self: center; font-size: 12px; color: $dusty-gray; + font-family: Roboto; + text-transform: uppercase; } } @@ -66,7 +69,7 @@ flex-flow: column nowrap; @media screen and (max-width: $break-small) { - padding: 0 1.3em; + padding: 0 1.3em .8em; } @media screen and (min-width: $break-large) { @@ -112,7 +115,7 @@ font-size: 12px; .tx-list-status { - font-size: 12px !important; + font-size: 14px !important; } .tx-list-account { @@ -121,10 +124,12 @@ .tx-list-value { font-size: 14px; + line-height: 18px; } .tx-list-fiat-value { font-size: 12px; + line-height: 16px; } } } @@ -152,6 +157,14 @@ justify-content: flex-start; align-items: flex-start; align-self: center; + + .tx-list-account-wrapper { + height: 18px; + + .tx-list-account { + line-height: 14px; + } + } } @media screen and (min-width: $break-large) { -- cgit v1.2.3 From 79be956be9f5297d6d601941e50d5ae4eca58560 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Thu, 19 Oct 2017 14:10:29 -0700 Subject: Fix network dropdown styles --- ui/app/components/dropdowns/network-dropdown.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/dropdowns/network-dropdown.js b/ui/app/components/dropdowns/network-dropdown.js index 736019c39..20dfca590 100644 --- a/ui/app/components/dropdowns/network-dropdown.js +++ b/ui/app/components/dropdowns/network-dropdown.js @@ -78,7 +78,7 @@ NetworkDropdown.prototype.render = function () { zIndex: 11, style: { position: 'absolute', - top: '38px', + top: '58px', minWidth: '309px', }, innerStyle: { -- cgit v1.2.3 From 69d8359639063fec86e115bbd1bfff277b0d749b Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Thu, 19 Oct 2017 17:48:45 -0700 Subject: Update eth-keyring-controller dependency --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7b1578f7b..167ca00ce 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "eth-contract-metadata": "^1.1.4", "eth-hd-keyring": "^1.2.1", "eth-json-rpc-filters": "^1.2.2", - "eth-keyring-controller": "^2.1.0", + "eth-keyring-controller": "^2.1.2", "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", "eth-sig-util": "^1.4.0", -- cgit v1.2.3 From 5a93ec02523e8b54222cc44cba5b80dcf8f07a7a Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Thu, 19 Oct 2017 21:06:14 -0700 Subject: Fix loading animation not showing on network change --- ui/app/app.js | 3 +- ui/app/components/buy-button-subview.js | 2 +- ui/app/components/loading.js | 75 +++++++++++++++------------------ ui/app/conf-tx.js | 4 +- 4 files changed, 38 insertions(+), 46 deletions(-) diff --git a/ui/app/app.js b/ui/app/app.js index cf82248e4..ae38fad7f 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -137,8 +137,7 @@ App.prototype.render = function () { h(AccountMenu), - h(Loading, { - isLoading: isLoading || isLoadingNetwork, + (isLoading || isLoadingNetwork) && h(Loading, { loadingMessage: loadMessage, }), diff --git a/ui/app/components/buy-button-subview.js b/ui/app/components/buy-button-subview.js index 6cf6e9eb9..a36f41df5 100644 --- a/ui/app/components/buy-button-subview.js +++ b/ui/app/components/buy-button-subview.js @@ -87,7 +87,7 @@ BuyButtonSubview.prototype.headerSubview = function () { left: '49vw', }, }, [ - h(Loading, { isLoading }), + isLoading && h(Loading), ]), // account panel diff --git a/ui/app/components/loading.js b/ui/app/components/loading.js index 163792584..e6d841aa0 100644 --- a/ui/app/components/loading.js +++ b/ui/app/components/loading.js @@ -1,45 +1,38 @@ -const inherits = require('util').inherits -const Component = require('react').Component +const { Component } = require('react') const h = require('react-hyperscript') - -inherits(LoadingIndicator, Component) -module.exports = LoadingIndicator - -function LoadingIndicator () { - Component.call(this) -} - -LoadingIndicator.prototype.render = function () { - const { isLoading, loadingMessage } = this.props - - return ( - isLoading ? h('.full-flex-height', { - style: { - left: '0px', - zIndex: 10, - position: 'absolute', - flexDirection: 'column', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - height: '100%', - width: '100%', - background: 'rgba(255, 255, 255, 0.8)', - }, - }, [ - h('img', { - src: 'images/loading.svg', - }), - - h('br'), - - showMessageIfAny(loadingMessage), - ]) : null - ) +class LoadingIndicator extends Component { + renderMessage () { + const { loadingMessage } = this.props + return loadingMessage && h('span', loadingMessage) + } + + render () { + return ( + h('.full-flex-height', { + style: { + left: '0px', + zIndex: 50, + position: 'absolute', + flexDirection: 'column', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '100%', + width: '100%', + background: 'rgba(255, 255, 255, 0.8)', + }, + }, [ + h('img', { + src: 'images/loading.svg', + }), + + h('br'), + + this.renderMessage(), + ]) + ) + } } -function showMessageIfAny (loadingMessage) { - if (!loadingMessage) return null - return h('span', loadingMessage) -} +module.exports = LoadingIndicator diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index f201010fc..dfa6f88c4 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -84,7 +84,7 @@ ConfirmTxScreen.prototype.render = function () { */ log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`) - if (unconfTxList.length === 0) return h(Loading, { isLoading: true }) + if (unconfTxList.length === 0) return h(Loading) return currentTxView({ // Properties @@ -130,7 +130,7 @@ function currentTxView (opts) { return h(PendingTypedMsg, opts) } } - return h(Loading, { isLoading: true }) + return h(Loading) } ConfirmTxScreen.prototype.buyEth = function (address, event) { -- cgit v1.2.3 From 5c902423d9f20699c636d8291b6a5f5071aeae85 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Fri, 20 Oct 2017 02:30:46 -0700 Subject: mascara - set x-frame-options header to DENY --- mascara/server/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mascara/server/index.js b/mascara/server/index.js index 12b527e5d..24739b43f 100644 --- a/mascara/server/index.js +++ b/mascara/server/index.js @@ -17,7 +17,7 @@ function createMetamascaraServer () { const server = express() // ui window serveBundle(server, '/ui.js', uiBundle) - server.use(express.static(__dirname + '/../ui/')) + server.use(express.static(__dirname + '/../ui/', { setHeaders: (res) => res.set('X-Frame-Options', 'DENY') })) server.use(express.static(__dirname + '/../../dist/chrome')) // metamascara serveBundle(server, '/metamascara.js', metamascaraBundle) -- cgit v1.2.3 From 89573533b8da75138bddffbadbaeff6f9eb68001 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 20 Oct 2017 11:48:01 -0230 Subject: Fixes add token search. --- ui/app/add-token.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/app/add-token.js b/ui/app/add-token.js index 90edc8de1..3ef9b6814 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -105,6 +105,7 @@ AddTokenScreen.prototype.tokenAddressDidChange = function (e) { } AddTokenScreen.prototype.checkExistingAddresses = function (address) { + if (!address) return false const tokensList = this.props.tokens const matchesAddress = existingToken => { return existingToken.address.toLowerCase() === address.toLowerCase() -- cgit v1.2.3 From b25c73a866140e362ea27d455101d07ccf1a56e6 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 20 Oct 2017 12:01:34 -0230 Subject: Only show erc20 tokens in add token search. --- ui/app/add-token.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/app/add-token.js b/ui/app/add-token.js index 3ef9b6814..e313babf3 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -7,7 +7,9 @@ const Fuse = require('fuse.js') const contractMap = require('eth-contract-metadata') const TokenBalance = require('./components/token-balance') const Identicon = require('./components/identicon') -const contractList = Object.entries(contractMap).map(([ _, tokenData]) => tokenData) +const contractList = Object.entries(contractMap) + .map(([ _, tokenData]) => tokenData) + .filter(tokenData => Boolean(tokenData.erc20)) const fuse = new Fuse(contractList, { shouldSort: true, threshold: 0.45, -- cgit v1.2.3 From 48f348e7296dd323c1cfd6194a88a26b3339042b Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 20 Oct 2017 11:30:56 -0700 Subject: 4.0.1 --- app/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/manifest.json b/app/manifest.json index a0f449c68..e88d8dc11 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.11.0", + "version": "4.0.1", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From 8472ca406a78468199a18c173ac8c9317dee8518 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 20 Oct 2017 12:29:12 -0700 Subject: deps - bump eth-json-rpc-filters for log filter fix --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 91e64c10b..0ecd22db5 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "eth-block-tracker": "^2.2.0", "eth-contract-metadata": "^1.1.4", "eth-hd-keyring": "^1.2.1", - "eth-json-rpc-filters": "^1.2.3", + "eth-json-rpc-filters": "^1.2.4", "eth-keyring-controller": "^2.1.0", "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", -- cgit v1.2.3 From a71c95b98744ce64f1863ea0ae4e4d450e0a606c Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 20 Oct 2017 12:32:15 -0700 Subject: changelog entry for filter fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b24cc161a..f000244ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Current Master +- Fix bug where log filters were not populated correctly - Fix bug where web3 API was sometimes injected after the page loaded. - Fix bug where first account was sometimes not selected correctly after creating or restoring a vault. - Fix bug where imported accounts could not use new eth_signTypedData method. -- cgit v1.2.3 From 80025e278b6c02f87bcfce3b8d5443722f4f9e52 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 20 Oct 2017 18:13:55 -0230 Subject: Fixes regression in confirm-send-token --- ui/app/components/pending-tx/confirm-send-token.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ui/app/components/pending-tx/confirm-send-token.js b/ui/app/components/pending-tx/confirm-send-token.js index 42b676954..6fb4ddf70 100644 --- a/ui/app/components/pending-tx/confirm-send-token.js +++ b/ui/app/components/pending-tx/confirm-send-token.js @@ -36,6 +36,7 @@ function mapStateToProps (state, ownProps) { const { conversionRate, identities, + currentCurrency, } = state.metamask const accounts = state.metamask.accounts const selectedAddress = getSelectedAddress(state) @@ -47,6 +48,7 @@ function mapStateToProps (state, ownProps) { selectedAddress, tokenExchangeRate, tokenData: tokenData || {}, + currentCurrency: currentCurrency.toUpperCase(), } } @@ -101,7 +103,7 @@ ConfirmSendToken.prototype.getGasFee = function () { multiplierBase: 16, }) - const FIAT = conversionUtil(txFeeBn, { + const FIAT = conversionUtil(gasTotal, { fromNumericBase: 'BN', toNumericBase: 'dec', fromDenomination: 'WEI', @@ -223,7 +225,7 @@ ConfirmSendToken.prototype.renderTotalPlusGas = function () { h('div.confirm-screen-section-column', [ h('div.confirm-screen-row-info', `${fiatAmount + fiatGas} ${currentCurrency}`), - h('div.confirm-screen-row-detail', `${tokenAmount + tokenGas} ${symbol}`), + h('div.confirm-screen-row-detail', `${tokenTotal} ${symbol}`), ]), ]) ) -- cgit v1.2.3 From 56dcf7c011f48c5f49e5339c9d36527d1f68cf93 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Fri, 20 Oct 2017 14:07:54 -0700 Subject: Version 3.11.1 --- CHANGELOG.md | 2 ++ app/manifest.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f000244ca..bbbc2e3be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +## 3.11.1 2017-10-20 + - Fix bug where log filters were not populated correctly - Fix bug where web3 API was sometimes injected after the page loaded. - Fix bug where first account was sometimes not selected correctly after creating or restoring a vault. diff --git a/app/manifest.json b/app/manifest.json index a0f449c68..c30d90dfa 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.11.0", + "version": "3.11.1", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From 5eb3cf43bfab3728dde151bc84439993b1a93184 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Fri, 20 Oct 2017 16:18:25 -0700 Subject: Add resiliency to confirm-send-token --- ui/app/components/pending-tx/confirm-send-token.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/app/components/pending-tx/confirm-send-token.js b/ui/app/components/pending-tx/confirm-send-token.js index 6fb4ddf70..a4c3d16e3 100644 --- a/ui/app/components/pending-tx/confirm-send-token.js +++ b/ui/app/components/pending-tx/confirm-send-token.js @@ -85,7 +85,9 @@ ConfirmSendToken.prototype.getAmount = function () { fiat: tokenExchangeRate ? +(sendTokenAmount * tokenExchangeRate * conversionRate).toFixed(2) : null, - token: +sendTokenAmount.toFixed(decimals), + token: typeof value === 'undefined' + ? 'Unknown' + : +sendTokenAmount.toFixed(decimals), } } @@ -213,8 +215,6 @@ ConfirmSendToken.prototype.renderTotalPlusGas = function () { const { fiat: fiatAmount, token: tokenAmount } = this.getAmount() const { fiat: fiatGas, token: tokenGas } = this.getGasFee() - const tokenTotal = addCurrencies(tokenAmount, tokenGas || '0') - return fiatAmount && fiatGas ? ( h('section.flex-row.flex-center.confirm-screen-total-box ', [ @@ -225,7 +225,7 @@ ConfirmSendToken.prototype.renderTotalPlusGas = function () { h('div.confirm-screen-section-column', [ h('div.confirm-screen-row-info', `${fiatAmount + fiatGas} ${currentCurrency}`), - h('div.confirm-screen-row-detail', `${tokenTotal} ${symbol}`), + h('div.confirm-screen-row-detail', `${addCurrencies(tokenAmount, tokenGas || '0')} ${symbol}`), ]), ]) ) @@ -321,7 +321,7 @@ ConfirmSendToken.prototype.render = function () { ]), ]), - h('section.flex-row.flex-center.confirm-screen-row', [ + toAddress && h('section.flex-row.flex-center.confirm-screen-row', [ h('span.confirm-screen-label.confirm-screen-section-column', [ 'To' ]), h('div.confirm-screen-section-column', [ h('div.confirm-screen-row-info', toName), -- cgit v1.2.3 From 8f3b762461ada222f82089e686a61183dd167428 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Fri, 20 Oct 2017 18:26:18 -0700 Subject: Fix Conversions bugs; Fiat value bugs --- ui/app/components/pending-tx/confirm-deploy-contract.js | 4 +++- ui/app/components/pending-tx/confirm-send-ether.js | 2 +- ui/app/components/send/currency-display.js | 12 +++++++++++- ui/app/components/token-cell.js | 8 +++++--- ui/app/send-v2.js | 7 ++++--- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/ui/app/components/pending-tx/confirm-deploy-contract.js b/ui/app/components/pending-tx/confirm-deploy-contract.js index d19cec755..a0ba94045 100644 --- a/ui/app/components/pending-tx/confirm-deploy-contract.js +++ b/ui/app/components/pending-tx/confirm-deploy-contract.js @@ -195,14 +195,16 @@ ConfirmDeployContract.prototype.getGasFee = function () { eth: Number(ETH), } } + ConfirmDeployContract.prototype.renderGasFee = function () { + const { currentCurrency } = this.props const { fiat: fiatGas, eth: ethGas } = this.getGasFee() return ( h('section.flex-row.flex-center.confirm-screen-row', [ h('span.confirm-screen-label.confirm-screen-section-column', [ 'Gas Fee' ]), h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', `${fiatGas} FIAT`), + h('div.confirm-screen-row-info', `${fiatGas} ${currentCurrency.toUpperCase()}`), h( 'div.confirm-screen-row-detail', diff --git a/ui/app/components/pending-tx/confirm-send-ether.js b/ui/app/components/pending-tx/confirm-send-ether.js index 51c36ba42..7162c7122 100644 --- a/ui/app/components/pending-tx/confirm-send-ether.js +++ b/ui/app/components/pending-tx/confirm-send-ether.js @@ -241,7 +241,7 @@ ConfirmSendEther.prototype.render = function () { // `You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`, // ]), - h('h3.flex-center.confirm-screen-send-amount', [`$${amountInFIAT}`]), + h('h3.flex-center.confirm-screen-send-amount', [`${amountInFIAT}`]), h('h3.flex-center.confirm-screen-send-amount-currency', [ currentCurrency.toUpperCase() ]), h('div.flex-center.confirm-memo-wrapper', [ h('h3.confirm-screen-send-memo', [ memo ? `"${memo}"` : '' ]), diff --git a/ui/app/components/send/currency-display.js b/ui/app/components/send/currency-display.js index 2c9a2d33b..7180b94d3 100644 --- a/ui/app/components/send/currency-display.js +++ b/ui/app/components/send/currency-display.js @@ -36,6 +36,16 @@ function toHexWei (value) { }) } +CurrencyDisplay.prototype.getAmount = function (value) { + const { selectedToken } = this.props + const { decimals } = selectedToken || {} + const multiplier = Math.pow(10, Number(decimals || 0)) + const sendAmount = '0x' + Number(value * multiplier).toString(16) + return selectedToken + ? sendAmount + : toHexWei(value) +} + CurrencyDisplay.prototype.render = function () { const { className = 'currency-display', @@ -102,7 +112,7 @@ CurrencyDisplay.prototype.render = function () { this.setState({ value: newValue }) } }, - onBlur: event => !readOnly && handleChange(toHexWei(event.target.value.split(' ')[0])), + onBlur: event => !readOnly && handleChange(this.getAmount(event.target.value.split(' ')[0])), onKeyUp: event => { if (!readOnly) { validate(toHexWei(value || initValueToRender)) diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js index ad431df69..6bb42204e 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/token-cell.js @@ -13,6 +13,7 @@ const TokenMenuDropdown = require('./dropdowns/token-menu-dropdown.js') function mapStateToProps (state) { return { network: state.metamask.network, + currentCurrency: state.metamask.currentCurrency, selectedTokenAddress: state.metamask.selectedTokenAddress, userAddress: selectors.getSelectedAddress(state), tokenExchangeRates: state.metamask.tokenExchangeRates, @@ -63,18 +64,19 @@ TokenCell.prototype.render = function () { ethToUSDRate, hideSidebar, sidebarOpen, + currentCurrency, // userAddress, } = props const pair = `${symbol.toLowerCase()}_eth`; let currentTokenToEthRate; - let currentTokenInUSD; + let currentTokenInFiat; let formattedUSD = '' if (tokenExchangeRates[pair]) { currentTokenToEthRate = tokenExchangeRates[pair].rate; - currentTokenInUSD = conversionUtil(string, { + currentTokenInFiat = conversionUtil(string, { fromNumericBase: 'dec', fromCurrency: symbol, toCurrency: 'USD', @@ -82,7 +84,7 @@ TokenCell.prototype.render = function () { conversionRate: currentTokenToEthRate, ethToUSDRate, }) - formattedUSD = `$${currentTokenInUSD} USD`; + formattedUSD = `${currentTokenInFiat} ${currentCurrency.toUpperCase()}`; } return ( diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index effa68b4b..5e64daceb 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -296,6 +296,7 @@ SendTransactionScreen.prototype.renderAmountRow = function () { inError: Boolean(errors.amount), primaryCurrency, convertedCurrency, + selectedToken, value: amount, conversionRate: amountConversionRate, handleChange: this.handleAmountChange, @@ -326,14 +327,14 @@ SendTransactionScreen.prototype.renderGasRow = function () { convertedCurrency, onClick: showCustomizeGasModal, }), - + h('div.send-v2__sliders-icon-container', { onClick: showCustomizeGasModal, }, [ h('i.fa.fa-sliders.send-v2__sliders-icon'), ]), - ]), + ]), ]) } @@ -350,7 +351,7 @@ SendTransactionScreen.prototype.renderMemoRow = function () { h(MemoTextArea, { memo, onChange: (event) => updateSendMemo(event.target.value), - }) + }), ]), ]) -- cgit v1.2.3 From a9a841ba01f6bffa29a3e0491e3a88f6aff72ac7 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 20 Oct 2017 19:33:15 -0700 Subject: 4.0.2 --- app/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/manifest.json b/app/manifest.json index e88d8dc11..33b1ac6e3 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "4.0.1", + "version": "4.0.2", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", -- cgit v1.2.3 From 0264ecaad77330b151f4bf4248b66f4659a67cce Mon Sep 17 00:00:00 2001 From: Jacky Chan Date: Fri, 18 Aug 2017 04:11:26 -0700 Subject: Adding CreatePasswordScreen --- .babelrc | 2 +- mascara/server/util.js | 2 +- .../src/app/first-time/create-password-screen.js | 13 ++++++ mascara/src/app/first-time/index.js | 54 ++++++++++++++++++++++ ui/app/app.js | 24 ++++++++++ ui/app/reducers/metamask.js | 2 + 6 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 mascara/src/app/first-time/create-password-screen.js create mode 100644 mascara/src/app/first-time/index.js diff --git a/.babelrc b/.babelrc index bca3364fc..307583ffd 100644 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,4 @@ { - "presets": ["es2015", "stage-0"], + "presets": ["es2015", "stage-0", "react"], "plugins": ["transform-runtime", "transform-async-to-generator"] } diff --git a/mascara/server/util.js b/mascara/server/util.js index 6ab41b729..af2daddb9 100644 --- a/mascara/server/util.js +++ b/mascara/server/util.js @@ -23,7 +23,7 @@ function createBundle (entryPoint) { cache: {}, packageCache: {}, plugin: [watchify], - }) + }).transform('babelify') bundler.on('update', bundle) bundle() diff --git a/mascara/src/app/first-time/create-password-screen.js b/mascara/src/app/first-time/create-password-screen.js new file mode 100644 index 000000000..afb1ad8f6 --- /dev/null +++ b/mascara/src/app/first-time/create-password-screen.js @@ -0,0 +1,13 @@ +import React, {Component, PropTypes} from 'react' + +export default class CreatePasswordScreen extends Component { + + render() { + return ( +
+ +
+ ) + } + +} \ No newline at end of file diff --git a/mascara/src/app/first-time/index.js b/mascara/src/app/first-time/index.js new file mode 100644 index 000000000..a2cb8d71c --- /dev/null +++ b/mascara/src/app/first-time/index.js @@ -0,0 +1,54 @@ +import React, {Component, PropTypes} from 'react' +import CreatePasswordScreen from './create-password-screen' + +export default class FirstTimeFlow extends Component { + + static propTypes = { + screenType: PropTypes.string + }; + + static defaultProps = { + screenType: FirstTimeFlow.CREATE_PASSWORD + }; + + static SCREEN_TYPE = { + CREATE_PASSWORD: 'create_password', + UNIQUE_IMAGE: 'unique_image', + TERM_OF_USE: 'term_of_use', + BACK_UP_PHRASE: 'back_up_phrase', + CONFIRM_BACK_UP_PHRASE: 'confirm_back_up_phrase', + BUY_ETHER: 'buy_ether' + }; + + static getScreenType = ({isInitialized, noActiveNotices, seedWords}) => { + const {SCREEN_TYPE} = FirstTimeFlow + + if (!isInitialized) { + return SCREEN_TYPE.CREATE_PASSWORD + } + + if (!noActiveNotices) { + return SCREEN_TYPE.TERM_OF_USE + } + + if (seedWords) { + return SCREEN_TYPE.BACK_UP_PHRASE + } + }; + + renderScreen() { + const {SCREEN_TYPE} = FirstTimeFlow + + switch (this.props.screenType) { + case SCREEN_TYPE.CREATE_PASSWORD: + return + default: + return